diff --git a/app/Enums/ActionType.php b/app/Enums/ActionType.php new file mode 100644 index 000000000000..6798a6b78eee --- /dev/null +++ b/app/Enums/ActionType.php @@ -0,0 +1,33 @@ +restore()) { - $logaction = new Actionlog(); - $logaction->item_type = Location::class; - $logaction->item_id = $location->id; - $logaction->created_at = date('Y-m-d H:i:s'); - $logaction->created_by = auth()->id(); - $logaction->logaction('restore'); - + //Note - the LogsChanges trait on Locations will automatically do a 'restore' action in the action_log when this fires return redirect()->route('locations.index')->with('success', trans('admin/locations/message.restore.success')); } diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php index 2b767650ade3..06a9d0ce4182 100755 --- a/app/Http/Controllers/ViewAssetsController.php +++ b/app/Http/Controllers/ViewAssetsController.php @@ -4,6 +4,7 @@ use App\Actions\CheckoutRequests\CancelCheckoutRequestAction; use App\Actions\CheckoutRequests\CreateCheckoutRequestAction; +use App\Enums\ActionType; use App\Exceptions\AssetNotRequestable; use App\Models\Actionlog; use App\Models\Asset; @@ -201,7 +202,7 @@ public function getRequestItem(Request $request, $itemType, $itemId = null, $can if (($item_request = $item->isRequestedBy($user)) || $cancel_by_admin) { $item->cancelRequest($requestingUser); $data['item_quantity'] = ($item_request) ? $item_request->qty : 1; - $logaction->logaction('request_canceled'); + $logaction->logaction(ActionType::RequestCanceled); if (($settings->alert_email != '') && ($settings->alerts_enabled == '1') && (! config('app.lock_passwords'))) { $settings->notify(new RequestAssetCancelation($data)); diff --git a/app/Models/Actionlog.php b/app/Models/Actionlog.php index 786246778cd6..4a12835a9df2 100755 --- a/app/Models/Actionlog.php +++ b/app/Models/Actionlog.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Str; +use App\Enums\ActionType; /** * Model for the Actionlog (the table that keeps a historical log of @@ -335,9 +336,12 @@ public function get_src($type = 'assets', $fieldname = 'filename') * @since [v3.0] * @return bool */ - public function logaction($actiontype) + public function logaction(string|ActionType $actiontype) { - $this->action_type = $actiontype; + if (is_string($actiontype)) { + $actiontype = ActionType::from($actiontype); + } + $this->action_type = $actiontype->value; $this->remote_ip = request()->ip(); $this->user_agent = request()->header('User-Agent'); $this->action_source = $this->determineActionSource(); diff --git a/app/Models/Category.php b/app/Models/Category.php index c3b6080c1e97..aaaaeca9776f 100755 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -11,6 +11,7 @@ use Watson\Validating\ValidatingTrait; use App\Helpers\Helper; use Illuminate\Support\Str; +use App\Models\Traits\LogsChanges; /** * Model for Categories. Categories are a higher-level group @@ -27,6 +28,7 @@ class Category extends SnipeModel protected $presenter = \App\Presenters\CategoryPresenter::class; use Presentable; use SoftDeletes; + use LogsChanges; protected $table = 'categories'; protected $hidden = ['created_by', 'deleted_at']; diff --git a/app/Models/CustomField.php b/app/Models/CustomField.php index 0e8845cfb39f..be6f02f719e0 100644 --- a/app/Models/CustomField.php +++ b/app/Models/CustomField.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Http\Traits\UniqueUndeletedTrait; +use App\Models\Traits\LogsChanges; use EasySlugger\Utf8Slugger; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -14,6 +15,7 @@ class CustomField extends Model use HasFactory; use ValidatingTrait, UniqueUndeletedTrait; + use LogsChanges; /** * diff --git a/app/Models/CustomFieldset.php b/app/Models/CustomFieldset.php index f27b8386479c..3396275ff506 100644 --- a/app/Models/CustomFieldset.php +++ b/app/Models/CustomFieldset.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\Traits\LogsChanges; use App\Rules\AlphaEncrypted; use App\Rules\BooleanEncrypted; use App\Rules\DateEncrypted; @@ -22,6 +23,7 @@ class CustomFieldset extends Model { use HasFactory; use ValidatingTrait; + use LogsChanges; protected $guarded = ['id']; diff --git a/app/Models/Department.php b/app/Models/Department.php index 1569081fdd7b..e22da53a14bf 100644 --- a/app/Models/Department.php +++ b/app/Models/Department.php @@ -4,6 +4,7 @@ use App\Http\Traits\UniqueUndeletedTrait; use App\Models\Traits\CompanyableTrait; +use App\Models\Traits\LogsChanges; use App\Models\Traits\Searchable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Watson\Validating\ValidatingTrait; @@ -22,7 +23,7 @@ class Department extends SnipeModel */ protected $injectUniqueIdentifier = true; - use ValidatingTrait, UniqueUndeletedTrait; + use ValidatingTrait, UniqueUndeletedTrait, LogsChanges; protected $casts = [ 'manager_id' => 'integer', diff --git a/app/Models/Depreciation.php b/app/Models/Depreciation.php index 6e01c6d78221..7e57eb8df948 100755 --- a/app/Models/Depreciation.php +++ b/app/Models/Depreciation.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\Traits\LogsChanges; use App\Models\Traits\Searchable; use App\Presenters\Presentable; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -10,6 +11,7 @@ class Depreciation extends SnipeModel { use HasFactory; + use LogsChanges; protected $presenter = \App\Presenters\DepreciationPresenter::class; use Presentable; diff --git a/app/Models/Group.php b/app/Models/Group.php index 9f4f2e2e5677..947216a87229 100755 --- a/app/Models/Group.php +++ b/app/Models/Group.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\Traits\LogsChanges; use App\Models\Traits\Searchable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Watson\Validating\ValidatingTrait; @@ -9,6 +10,7 @@ class Group extends SnipeModel { use HasFactory; + use LogsChanges; protected $table = 'permission_groups'; diff --git a/app/Models/Location.php b/app/Models/Location.php index b8729a8e94ab..788fcbf3f0dd 100755 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -6,6 +6,7 @@ use App\Models\Traits\CompanyableTrait; use App\Models\Traits\HasUploads; use App\Models\Traits\Loggable; +use App\Models\Traits\LogsChanges; use App\Models\Traits\Searchable; use App\Presenters\Presentable; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -17,7 +18,7 @@ class Location extends SnipeModel { use HasFactory; use CompanyableTrait; - use Loggable; + use LogsChanges; protected $presenter = \App\Presenters\LocationPresenter::class; use Presentable; diff --git a/app/Models/Manufacturer.php b/app/Models/Manufacturer.php index 6d7b0106778f..0e73853f1851 100755 --- a/app/Models/Manufacturer.php +++ b/app/Models/Manufacturer.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\Traits\LogsChanges; use App\Models\Traits\Searchable; use App\Presenters\Presentable; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -16,6 +17,7 @@ class Manufacturer extends SnipeModel protected $presenter = \App\Presenters\ManufacturerPresenter::class; use Presentable; use SoftDeletes; + use LogsChanges; protected $table = 'manufacturers'; diff --git a/app/Models/PredefinedKit.php b/app/Models/PredefinedKit.php index 2d0c87066e81..854a6577d99d 100644 --- a/app/Models/PredefinedKit.php +++ b/app/Models/PredefinedKit.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\Traits\LogsChanges; use App\Models\Traits\Searchable; use App\Presenters\Presentable; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -19,6 +20,7 @@ class PredefinedKit extends SnipeModel protected $presenter = \App\Presenters\PredefinedKitPresenter::class; use HasFactory; use Presentable; + use LogsChanges; protected $table = 'kits'; /** diff --git a/app/Models/ReportTemplate.php b/app/Models/ReportTemplate.php index 452bd029d7f0..16c0d07f4625 100644 --- a/app/Models/ReportTemplate.php +++ b/app/Models/ReportTemplate.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\Traits\LogsChanges; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -14,6 +15,7 @@ class ReportTemplate extends Model use HasFactory; use SoftDeletes; use ValidatingTrait; + use LogsChanges; protected $casts = [ 'options' => 'array', diff --git a/app/Models/Setting.php b/app/Models/Setting.php index 4b96230027cd..0b69e635fe6a 100755 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\Traits\LogsChanges; use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -21,7 +22,7 @@ class Setting extends Model { use HasFactory; - use Notifiable, ValidatingTrait; + use Notifiable, ValidatingTrait, LogsChanges; /** * The cache property so that multiple invocations of this will only load the Settings record from disk only once diff --git a/app/Models/Statuslabel.php b/app/Models/Statuslabel.php index bd981d867a80..2dd7e753d341 100755 --- a/app/Models/Statuslabel.php +++ b/app/Models/Statuslabel.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Http\Traits\UniqueUndeletedTrait; +use App\Models\Traits\LogsChanges; use App\Models\Traits\Searchable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; @@ -14,6 +15,7 @@ class Statuslabel extends SnipeModel use SoftDeletes; use ValidatingTrait; use UniqueUndeletedTrait; + use LogsChanges; protected $injectUniqueIdentifier = true; diff --git a/app/Models/Supplier.php b/app/Models/Supplier.php index 2c99330604e0..6ec14334303f 100755 --- a/app/Models/Supplier.php +++ b/app/Models/Supplier.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Http\Traits\UniqueUndeletedTrait; +use App\Models\Traits\LogsChanges; use App\Models\Traits\Searchable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; @@ -12,6 +13,7 @@ class Supplier extends SnipeModel { use HasFactory; use SoftDeletes; + use LogsChanges; protected $table = 'suppliers'; diff --git a/app/Models/Traits/LogsChanges.php b/app/Models/Traits/LogsChanges.php new file mode 100644 index 000000000000..e46c8ae0e433 --- /dev/null +++ b/app/Models/Traits/LogsChanges.php @@ -0,0 +1,97 @@ +action_type = ActionType::Create; + }); + + static::updating(function ($model) { + \Log::error("UPDATING!"); + if(!$model->action_type) { + \Log::error("No action type - so definitely doing 'update'!"); + $model->action_type = ActionType::Update; + } + }); + + // The Settings object does not use soft-deletes, so it has no 'restoring' method + // so to be generic, we just look for that method existing at all, and don't call + // it if it doesn't. + if (method_exists(self::class, 'restoring')) { + static::restoring(function ($model) { + $model->action_type = ActionType::Restore; + }); + } + + /* The main functionality is here: */ + static::saving(function ($model) { + \Log::error("recording changes......."); + $model->record_changes(); + }); + + static::saved(function ($model) { + \Log::error("SAVED!!!!"); + $model->add_action_log(); + }); + + static::deleted(function ($model) { + $model->action_type = ActionType::Delete; + $model->add_action_log(); + \Log::error("deleted!!!!!!!!!!!"); + }); + } + + function record_changes() + { + $changed = []; + + // something here with custom fields is needed? or will getRawOriginal et al just do that for us? + foreach ($this->getRawOriginal() as $key => $value) { //on 'create' this doesn't write down the new attributes + if ($this->getRawOriginal()[$key] != $this->getAttributes()[$key]) { + $changed[$key]['old'] = $this->getRawOriginal()[$key]; + $changed[$key]['new'] = $this->getAttributes()[$key]; + + if (property_exists($this, 'hidden') && in_array($key, $this->hidden)) { + $changed[$key]['old'] = '*************'; //FIXME deleted_at is hidden?! + $changed[$key]['new'] = '*************'; + } + } + } + $this->meta = $changed; + } + + function add_action_log() + { + if(!$this->action_type && !$this->meta) { + \Log::warning("No action type set, and no changes to record. Not logging."); + return; + } + if($this->action_type == ActionType::Update && !$this->meta) { + \Log::warning("An update with no actual changes to record. Not logging"); + return; + } + $logAction = new Actionlog(); + $logAction->action_type = $this->action_type->value; + $logAction->item()->associate($this); + $logAction->created_at = date('Y-m-d H:i:s'); + $logAction->action_date = date('Y-m-d H:i:s'); + // target_id and target_type? + // need IP and user-agent!!!!! + $logAction->created_by = auth()->id(); + $logAction->log_meta = $this->meta ? json_encode($this->meta) : null; // this gets weird on 'create' + if($logAction->save()) { + //success! Reset for more actions later... + $this->action_type = null; + $this->meta = []; + } + } +} \ No newline at end of file diff --git a/app/Presenters/ActionlogPresenter.php b/app/Presenters/ActionlogPresenter.php index 0f82d1f66786..a2dc75113d43 100644 --- a/app/Presenters/ActionlogPresenter.php +++ b/app/Presenters/ActionlogPresenter.php @@ -102,7 +102,7 @@ public function icon() return 'fa-solid fa-rotate-right'; } - if ($this->action_type == 'note_added') { + if ($this->action_type == 'note added') { return 'fas fa-sticky-note'; } diff --git a/database/migrations/2025_10_22_144927_migrate_incorrect_action_types.php b/database/migrations/2025_10_22_144927_migrate_incorrect_action_types.php new file mode 100644 index 000000000000..b27ed56ef39d --- /dev/null +++ b/database/migrations/2025_10_22_144927_migrate_incorrect_action_types.php @@ -0,0 +1,31 @@ +where('action_type', 'request_canceled')->update(['action_type' => 'request canceled']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // no down migration for this one + } +}; diff --git a/resources/views/partials/bootstrap-table.blade.php b/resources/views/partials/bootstrap-table.blade.php index 2fb8edbb7225..1811d0f8fcf6 100644 --- a/resources/views/partials/bootstrap-table.blade.php +++ b/resources/views/partials/bootstrap-table.blade.php @@ -1081,6 +1081,8 @@ function polymorphicItemFormatter(value) { var item_destination = ''; var item_icon; + var full_url = ''; + var url_name = ''; if ((value) && (value.type)) { @@ -1111,13 +1113,29 @@ function polymorphicItemFormatter(value) { } else if (value.type == 'model') { item_destination = 'models' item_icon = ''; + } else if (value.type == 'category') { + item_destination = 'categories'; + } else if (value.type == 'setting') { + full_url = 'admin'; + url_name = '{{ trans('general.settings') }}'; + } else if (value.type == 'customField' || value.type == 'customFieldset') { + full_url = 'fields'; + url_name = '{{ trans('admin/custom_fields/general.custom_fields') }}'; //doesn't show META - FIXME!!! + } else if (value.type == 'predefinedKit') { + item_destination = 'kits'; + } else if (value.type == 'reportTemplate') { + item_destination = 'reports/templates'; + } else { + item_destination = value.type + 's'; //dumb pluralization } // display the username if it's checked out to a user, but don't do it if the username's there already if (value.username && !value.name.match('\\(') && !value.name.match('\\)')) { value.name = value.name + ' (' + value.username + ')'; } - + if (full_url && url_name) { // TODO - I don't like how this looks/works + return '' + url_name + ''; + } return ' ' + value.name + ''; } else {