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 {