ADD: Historial de acciones (#5)

This commit is contained in:
Moisés de Jesús Cortés Castellanos 2025-01-03 12:55:57 -06:00 committed by GitHub
parent ea9115064c
commit e487aa8726
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 490 additions and 43 deletions

39
app/Enums/EventTypeEk.php Normal file
View File

@ -0,0 +1,39 @@
<?php namespace App\Enums;
use Notsoweb\LaravelCore\Traits\Enums\Extended;
/**
* @copyright Copyright (c) 2001-2023 Golsystems (https://www.golsystems.mx) - All rights reserved.
*/
/**
* Tipos de configuración
*
* @author Moisés de Jesús Cortés Castellanos <ing.moisesdejesuscortesc@notsoweb.com>
*
* @version 1.0.0
*/
enum EventTypeEk : string
{
use Extended;
/**
* Texto
*/
case STRING = 'S';
/**
* JSON
*/
case JSON = 'J';
/**
* Booleano
*/
case BOOL = 'B';
/**
* Entero
*/
case INT = 'I';
}

View File

@ -0,0 +1,51 @@
<?php namespace App\Http\Controllers\Admin;
/**
* @copyright (c) 2024 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
*/
use App\Http\Controllers\Controller;
use App\Http\Requests\Users\UserActivityRequest;
use App\Models\UserEvent;
use Notsoweb\ApiResponse\Enums\ApiResponse;
/**
* Eventos del usuarios del sistema
*
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
*
* @version 1.0.0
*/
class ActivityController extends Controller
{
/**
* Actividades del usuario
*/
public function index(UserActivityRequest $request)
{
$filters = $request->all();
$model = UserEvent::with('user:id,name,paternal,maternal,profile_photo_path,deleted_at');
if(isset($filters['user']) && !empty($filters['user'])){
$model->where('user_id', $filters['user']);
}
if(isset($filters['search']) && !empty($filters['search'])){
$model->where('event', 'like', '%'.$filters['search'].'%');
}
if(isset($filters['start_date']) && !empty($filters['start_date'])){
$model->where('created_at', '>=', "{$filters['start_date']} 00:00:00");
}
if(isset($filters['end_date']) && !empty($filters['end_date'])){
$model->where('created_at', '<=', "{$filters['end_date']} 23:59:59");
}
return ApiResponse::OK->response([
'models' =>
$model->orderBy('created_at', 'desc')
->paginate(config('app.pagination'))
]);
}
}

View File

@ -5,6 +5,7 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Users\PasswordUpdateRequest; use App\Http\Requests\Users\PasswordUpdateRequest;
use App\Http\Requests\Users\UserActivityRequest;
use App\Http\Requests\Users\UserStoreRequest; use App\Http\Requests\Users\UserStoreRequest;
use App\Http\Requests\Users\UserUpdateRequest; use App\Http\Requests\Users\UserUpdateRequest;
use App\Models\User; use App\Models\User;
@ -128,4 +129,32 @@ public function updatePassword(PasswordUpdateRequest $request, User $user)
return ApiResponse::OK->response(); return ApiResponse::OK->response();
} }
/**
* Actividades del usuario
*/
public function activity(UserActivityRequest $request, User $user)
{
$filters = $request->all();
$model = $user->events()
->with('user:id,name,paternal,maternal,profile_photo_path');
if($filters['search']){
$model->where('event', 'like', '%'.$filters['search'].'%');
}
if($filters['start_date']){
$model->where('created_at', '>=', "{$filters['start_date']} 00:00:00");
}
if($filters['end_date']){
$model->where('created_at', '<=', "{$filters['end_date']} 23:59:59");
}
return ApiResponse::OK->response([
'models' =>
$model->orderBy('created_at', 'desc')
->paginate(config('app.pagination'))
]);
}
} }

View File

@ -19,7 +19,7 @@ class PasswordUpdateRequest extends FormRequest
*/ */
public function authorize(): bool public function authorize(): bool
{ {
return true; return auth()->user()->hasPermissionTo('users.edit');
} }
/** /**

View File

@ -0,0 +1,37 @@
<?php namespace App\Http\Requests\Users;
/**
* @copyright (c) 2024 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
*/
use Illuminate\Foundation\Http\FormRequest;
/**
* Descripción
*
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
*
* @version 1.0.0
*/
class UserActivityRequest extends FormRequest
{
/**
* Determinar si el usuario está autorizado para realizar esta solicitud
*/
public function authorize(): bool
{
return true;
}
/**
* Obtener las reglas de validación que se aplican a la solicitud
*/
public function rules(): array
{
return [
'search' => ['nullable', 'string', 'max:255'],
'start_date' => ['nullable', 'date'],
'end_date' => ['nullable', 'date'],
'user' => ['nullable', 'exists:users,id']
];
}
}

View File

@ -8,9 +8,11 @@
use App\Http\Traits\IsNotifiable; use App\Http\Traits\IsNotifiable;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Laravel\Passport\HasApiTokens; use Laravel\Passport\HasApiTokens;
use Notsoweb\LaravelCore\Traits\Models\Extended;
use Spatie\Permission\Traits\HasRoles; use Spatie\Permission\Traits\HasRoles;
/** /**
@ -22,11 +24,13 @@
*/ */
class User extends Authenticatable class User extends Authenticatable
{ {
use HasApiTokens, use Extended,
HasApiTokens,
HasFactory, HasFactory,
HasRoles, HasRoles,
HasProfilePhoto, HasProfilePhoto,
IsNotifiable; IsNotifiable,
SoftDeletes;
/** /**
* Atributos permitidos * Atributos permitidos
@ -69,6 +73,22 @@ protected function casts(): array
'profile_photo_url', 'profile_photo_url',
]; ];
/**
* Un usuario puede generar muchos eventos
*/
public function events()
{
return $this->hasMany(UserEvent::class);
}
/**
* Evento
*/
public function reports()
{
return $this->morphMany(UserEvent::class, 'reportable');
}
/** /**
* Nombre completo del usuario * Nombre completo del usuario
*/ */

93
app/Models/UserEvent.php Normal file
View File

@ -0,0 +1,93 @@
<?php namespace App\Models;
/**
* @copyright (c) 2024 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
*/
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
/**
* Eventos del usuario
*
* Acciones que los usuarios realizan en el sistema.
*
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
*
* @version 1.0.0
*/
class UserEvent extends Model
{
/**
* Atributos que se pueden asignar masivamente
*/
protected $fillable = [
'event',
'name',
'data',
'reportable_id',
'reportable_type',
'user_id'
];
/**
* Transformaciones
*/
protected $casts = [
'data' => 'json'
];
/**
* Atributos virtuales
*/
protected $appends = [
'description'
];
/**
* Desactivar fecha actualización
*/
const UPDATED_AT = null;
/**
* Relación con el usuario
*/
public function user()
{
return $this->belongsTo(User::class)->withTrashed();
}
/**
* Descripción del evento
*/
public function description() : Attribute
{
return Attribute::make(
get: fn($value) => __($this->event, ['model' => $this->name]),
);
}
/**
* Relación con el modelo reportable
*/
public function reportable()
{
return $this->morphTo();
}
/**
* Reportar un evento
*/
public static function report(Model $model, string $event, string $key = 'name', bool $reportChanges = false)
{
$event = strtolower(explode('\\', get_class($model))[2]) . '.' . $event;
self::create([
'event' => $event,
'name' => $model->{$key},
'data' => $reportChanges ? $model->getContrastChanges() : $model->fillableToArray(),
'reportable_id' => $model->id,
'reportable_type' => get_class($model),
'user_id' => auth()->user()?->id
]);
}
}

View File

@ -0,0 +1,78 @@
<?php namespace App\Observers;
/**
* @copyright (c) 2024 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
*/
use App\Models\User;
use App\Models\UserEvent;
/**
* Observador del modelo User
*
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
*
* @version 1.0.0
*/
class UserObserver
{
/**
* Manipulador del evento "created" del modelo User
*/
public function created(User $user): void
{
UserEvent::report(
model: $user,
event: __FUNCTION__,
key: 'email'
);
}
/**
* Manipulador del evento "updated" del modelo User
*/
public function updated(User $user): void
{
UserEvent::report(
model: $user,
event: __FUNCTION__,
key: 'email',
reportChanges: true
);
}
/**
* Manipulador del evento "deleted" del modelo User
*/
public function deleted(User $user): void
{
UserEvent::report(
model: $user,
event: __FUNCTION__,
key: 'email'
);
}
/**
* Manipulador del evento "restored" del modelo User
*/
public function restored(User $user): void
{
UserEvent::report(
model: $user,
event: __FUNCTION__,
key: 'email'
);
}
/**
* Manipulador del evento "force deleted" del modelo User
*/
public function forceDeleted(User $user): void
{
UserEvent::report(
model: $user,
event: __FUNCTION__,
key: 'email'
);
}
}

View File

@ -0,0 +1,34 @@
<?php namespace App\Providers;
/**
* @copyright (c) 2024 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
*/
use App\Models\User;
use App\Observers\UserObserver;
use Illuminate\Support\ServiceProvider;
/**
* Observadores de la aplicación
*
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
*
* @version 1.0.0
*/
class ObserverProvider extends ServiceProvider
{
/**
* Registrar servicios
*/
public function register(): void
{
//
}
/**
* Inicializar servicios
*/
public function boot(): void
{
User::observe([UserObserver::class]);
}
}

View File

@ -2,6 +2,7 @@
return [ return [
App\Providers\AppServiceProvider::class, App\Providers\AppServiceProvider::class,
App\Providers\ObserverProvider::class,
Notsoweb\LaravelCore\ServiceProvider::class, Notsoweb\LaravelCore\ServiceProvider::class,
Spatie\Permission\PermissionServiceProvider::class, Spatie\Permission\PermissionServiceProvider::class,
]; ];

48
composer.lock generated
View File

@ -2202,16 +2202,16 @@
}, },
{ {
"name": "league/commonmark", "name": "league/commonmark",
"version": "2.6.0", "version": "2.6.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/commonmark.git", "url": "https://github.com/thephpleague/commonmark.git",
"reference": "d150f911e0079e90ae3c106734c93137c184f932" "reference": "d990688c91cedfb69753ffc2512727ec646df2ad"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d150f911e0079e90ae3c106734c93137c184f932", "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d990688c91cedfb69753ffc2512727ec646df2ad",
"reference": "d150f911e0079e90ae3c106734c93137c184f932", "reference": "d990688c91cedfb69753ffc2512727ec646df2ad",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2305,7 +2305,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-12-07T15:34:16+00:00" "time": "2024-12-29T14:10:59+00:00"
}, },
{ {
"name": "league/config", "name": "league/config",
@ -3328,16 +3328,16 @@
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
"version": "v5.3.1", "version": "v5.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nikic/PHP-Parser.git", "url": "https://github.com/nikic/PHP-Parser.git",
"reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" "reference": "447a020a1f875a434d62f2a401f53b82a396e494"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494",
"reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", "reference": "447a020a1f875a434d62f2a401f53b82a396e494",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3380,9 +3380,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/nikic/PHP-Parser/issues", "issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0"
}, },
"time": "2024-10-08T18:51:32+00:00" "time": "2024-12-30T11:07:19+00:00"
}, },
{ {
"name": "notsoweb/api-response", "name": "notsoweb/api-response",
@ -3430,12 +3430,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/notsoweb/laravel-core.git", "url": "https://github.com/notsoweb/laravel-core.git",
"reference": "97eac719b383e9e995827b8eeb17d325ded66b7e" "reference": "12da28db3a1b4634616fefa4bfb9fc7c277b7393"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/notsoweb/laravel-core/zipball/97eac719b383e9e995827b8eeb17d325ded66b7e", "url": "https://api.github.com/repos/notsoweb/laravel-core/zipball/12da28db3a1b4634616fefa4bfb9fc7c277b7393",
"reference": "97eac719b383e9e995827b8eeb17d325ded66b7e", "reference": "12da28db3a1b4634616fefa4bfb9fc7c277b7393",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3467,7 +3467,7 @@
"issues": "https://github.com/notsoweb/laravel-core/issues", "issues": "https://github.com/notsoweb/laravel-core/issues",
"source": "https://github.com/notsoweb/laravel-core/tree/main" "source": "https://github.com/notsoweb/laravel-core/tree/main"
}, },
"time": "2024-12-20T18:15:03+00:00" "time": "2024-12-31T01:43:06+00:00"
}, },
{ {
"name": "nunomaduro/termwind", "name": "nunomaduro/termwind",
@ -8393,16 +8393,16 @@
}, },
{ {
"name": "laravel/pint", "name": "laravel/pint",
"version": "v1.18.3", "version": "v1.19.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/pint.git", "url": "https://github.com/laravel/pint.git",
"reference": "cef51821608239040ab841ad6e1c6ae502ae3026" "reference": "8169513746e1bac70c85d6ea1524d9225d4886f0"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/cef51821608239040ab841ad6e1c6ae502ae3026", "url": "https://api.github.com/repos/laravel/pint/zipball/8169513746e1bac70c85d6ea1524d9225d4886f0",
"reference": "cef51821608239040ab841ad6e1c6ae502ae3026", "reference": "8169513746e1bac70c85d6ea1524d9225d4886f0",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8413,10 +8413,10 @@
"php": "^8.1.0" "php": "^8.1.0"
}, },
"require-dev": { "require-dev": {
"friendsofphp/php-cs-fixer": "^3.65.0", "friendsofphp/php-cs-fixer": "^3.66.0",
"illuminate/view": "^10.48.24", "illuminate/view": "^10.48.25",
"larastan/larastan": "^2.9.11", "larastan/larastan": "^2.9.12",
"laravel-zero/framework": "^10.4.0", "laravel-zero/framework": "^10.48.25",
"mockery/mockery": "^1.6.12", "mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^1.17.0", "nunomaduro/termwind": "^1.17.0",
"pestphp/pest": "^2.36.0" "pestphp/pest": "^2.36.0"
@ -8455,7 +8455,7 @@
"issues": "https://github.com/laravel/pint/issues", "issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint" "source": "https://github.com/laravel/pint"
}, },
"time": "2024-11-26T15:34:00+00:00" "time": "2024-12-30T16:20:10+00:00"
}, },
{ {
"name": "laravel/sail", "name": "laravel/sail",

View File

@ -12,7 +12,7 @@
| other UI elements where an application name needs to be displayed. | other UI elements where an application name needs to be displayed.
| |
*/ */
'version' => '0.9.4', 'version' => '0.9.5',
'name' => env('APP_NAME', 'Laravel'), 'name' => env('APP_NAME', 'Laravel'),

View File

@ -24,6 +24,7 @@ public function up(): void
$table->foreignId('current_team_id')->nullable(); $table->foreignId('current_team_id')->nullable();
$table->string('profile_photo_path', 2048)->nullable(); $table->string('profile_photo_path', 2048)->nullable();
$table->timestamps(); $table->timestamps();
$table->softDeletes();
}); });
Schema::create('password_reset_tokens', function (Blueprint $table) { Schema::create('password_reset_tokens', function (Blueprint $table) {

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_events', function (Blueprint $table) {
$table->id();
$table->string('event');
$table->string('name');
$table->json('data')->nullable();
$table->morphs('reportable');
$table->foreignId('user_id')
->nullable()
->constrained()
->nullOnDelete();
$table->timestamp('created_at')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_events');
}
};

View File

@ -56,6 +56,16 @@ public function run(): void
$systemPulse = $this->onPermission('pulse', 'Monitoreo de Pulse', $pulse, 'api'); $systemPulse = $this->onPermission('pulse', 'Monitoreo de Pulse', $pulse, 'api');
$pulse = PermissionType::create([
'name' => 'Historial de actividades'
]);
$activityIndex = $this->onIndex(
code: 'activities',
type: $pulse,
guardName: 'api'
);
// Desarrollador // Desarrollador
Role::create([ Role::create([
'name' => 'developer', 'name' => 'developer',
@ -79,7 +89,8 @@ public function run(): void
$roleCreate, $roleCreate,
$roleEdit, $roleEdit,
$roleDestroy, $roleDestroy,
$systemPulse $systemPulse,
$activityIndex
); );
} }
} }

View File

@ -26,6 +26,7 @@ public function run(): void
User::create([ User::create([
'name' => 'Developer', 'name' => 'Developer',
'paternal' => 'Notsoweb', 'paternal' => 'Notsoweb',
'maternal' => 'Software',
'email' => $developer->email, 'email' => $developer->email,
'password' => $developer->hash, 'password' => $developer->hash,
])->assignRole(__('developer')); ])->assignRole(__('developer'));
@ -33,8 +34,9 @@ public function run(): void
$admin = UserSecureSupport::create('admin@notsoweb.com'); $admin = UserSecureSupport::create('admin@notsoweb.com');
User::create([ User::create([
'name' => 'Developer', 'name' => 'Admin',
'paternal' => 'Notsoweb', 'paternal' => 'Notsoweb',
'maternal' => 'Software',
'email' => $admin->email, 'email' => $admin->email,
'password' => $admin->hash, 'password' => $admin->hash,
])->assignRole(__('admin')); ])->assignRole(__('admin'));
@ -44,6 +46,7 @@ public function run(): void
User::create([ User::create([
'name' => 'Demo', 'name' => 'Demo',
'paternal' => 'Notsoweb', 'paternal' => 'Notsoweb',
'maternal' => 'Software',
'email' => $demo->email, 'email' => $demo->email,
'password' => $demo->hash, 'password' => $demo->hash,
]); ]);

9
lang/es/user.php Normal file
View File

@ -0,0 +1,9 @@
<?php
return [
'created' => 'El usuario :model ha sido creado',
'updated' => 'El usuario :model ha sido actualizado',
'deleted' => 'El usuario :model ha sido eliminado',
'restored' => 'El usuario :model ha sido restaurado',
];

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Http\Controllers\Admin\ActivityController;
use App\Http\Controllers\Admin\PermissionTypeController; use App\Http\Controllers\Admin\PermissionTypeController;
use App\Http\Controllers\Admin\RoleController; use App\Http\Controllers\Admin\RoleController;
use App\Http\Controllers\MyUserController; use App\Http\Controllers\MyUserController;
@ -26,6 +27,9 @@
Route::get('roles', [MyUserController::class, 'roles'])->name('roles'); Route::get('roles', [MyUserController::class, 'roles'])->name('roles');
}); });
Route::prefix('admin')->name('admin.')->group(function() {
Route::apiResource('activities', ActivityController::class)->only(['index']);
Route::prefix('users')->name('users.')->group(function() { Route::prefix('users')->name('users.')->group(function() {
Route::prefix('{user}')->group(function() { Route::prefix('{user}')->group(function() {
Route::get('roles', [UserController::class, 'roles'])->name('roles'); Route::get('roles', [UserController::class, 'roles'])->name('roles');
@ -40,6 +44,8 @@
Route::apiResource('roles', RoleController::class); Route::apiResource('roles', RoleController::class);
Route::get('roles/{role}/permissions', [RoleController::class, 'permissions'])->name('roles.permissions'); Route::get('roles/{role}/permissions', [RoleController::class, 'permissions'])->name('roles.permissions');
Route::put('roles/{role}/permissions', [RoleController::class, 'updatePermissions'])->name('roles.permissions.update'); Route::put('roles/{role}/permissions', [RoleController::class, 'updatePermissions'])->name('roles.permissions.update');
});
Route::prefix('permission-types')->name('permission-types.')->group(function() { Route::prefix('permission-types')->name('permission-types.')->group(function() {
Route::get('all', [PermissionTypeController::class, 'all'])->name('all'); Route::get('all', [PermissionTypeController::class, 'all'])->name('all');