diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index d780bd8bd..d60ded391 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -34,6 +34,7 @@ use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Route; use App\View\Label; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Facades\Validator; /** @@ -1064,7 +1065,7 @@ class AssetsController extends Controller * @param int $id * @since [v4.0] */ - public function audit(Request $request): JsonResponse + public function audit(Request $request, Asset $asset): JsonResponse { $this->authorize('audit', Asset::class); @@ -1072,36 +1073,15 @@ class AssetsController extends Controller $settings = Setting::getSettings(); $dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString(); - // No tag passed - return an error - if (!$request->filled('asset_tag')) { - return response()->json(Helper::formatStandardApiResponse('error', [ - 'asset_tag' => '', - 'error' => trans('admin/hardware/message.no_tag'), - ], trans('admin/hardware/message.no_tag')), 200); + // Allow the asset tag to be passed in the payload (legacy method) + if ($request->filled('asset_tag')) { + $asset = Asset::where('asset_tag', '=', $request->input('asset_tag'))->first(); } - - $asset = Asset::where('asset_tag', '=', $request->input('asset_tag'))->first(); - - if ($asset) { - /** - * Even though we do a save() further down, we don't want to log this as a "normal" asset update, - * which would trigger the Asset Observer and would log an asset *update* log entry (because the - * de-normed fields like next_audit_date on the asset itself will change on save()) *in addition* to - * the audit log entry we're creating through this controller. - * - * To prevent this double-logging (one for update and one for audit), we skip the observer and bypass - * that de-normed update log entry by using unsetEventDispatcher(), BUT invoking unsetEventDispatcher() - * will bypass normal model-level validation that's usually handled at the observer ) - * - * We handle validation on the save() by checking if the asset is valid via the ->isValid() method, - * which manually invokes Watson Validating to make sure the asset's model is valid. - * - * @see \App\Observers\AssetObserver::updating() - */ - $asset->unsetEventDispatcher(); + $originalValues = $asset->getRawOriginal(); + $asset->next_audit_date = $dt; if ($request->filled('next_audit_date')) { @@ -1116,33 +1096,89 @@ class AssetsController extends Controller $asset->last_audit_date = date('Y-m-d H:i:s'); + // Set up the payload for re-display in the API response + $payload = [ + 'id' => $asset->id, + 'asset_tag' => $asset->asset_tag, + 'note' => $request->input('note'), + 'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date), + ]; + + + /** + * Update custom fields in the database. + * Validation for these fields is handled through the AssetRequest form request + * $model = AssetModel::find($request->get('model_id')); + */ + if (($asset->model) && ($asset->model->fieldset)) { + $payload['custom_fields'] = []; + foreach ($asset->model->fieldset->fields as $field) { + if (($field->display_audit=='1') && ($request->has($field->db_column))) { + if ($field->field_encrypted == '1') { + if (Gate::allows('assets.view.encrypted_custom_fields')) { + if (is_array($request->input($field->db_column))) { + $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column))); + } else { + $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column)); + } + } + } else { + if (is_array($request->input($field->db_column))) { + $asset->{$field->db_column} = implode(', ', $request->input($field->db_column)); + } else { + $asset->{$field->db_column} = $request->input($field->db_column); + } + } + $payload['custom_fields'][$field->db_column] = $request->input($field->db_column); + } + + } + } + + // Validate custom fields + Validator::make($asset->toArray(), $asset->customFieldValidationRules())->validate(); + + // Validate the rest of the data before we turn off the event dispatcher + if ($asset->isInvalid()) { + return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors())); + } + + + /** + * Even though we do a save() further down, we don't want to log this as a "normal" asset update, + * which would trigger the Asset Observer and would log an asset *update* log entry (because the + * de-normed fields like next_audit_date on the asset itself will change on save()) *in addition* to + * the audit log entry we're creating through this controller. + * + * To prevent this double-logging (one for update and one for audit), we skip the observer and bypass + * that de-normed update log entry by using unsetEventDispatcher(), BUT invoking unsetEventDispatcher() + * will bypass normal model-level validation that's usually handled at the observer) + * + * We handle validation on the save() by checking if the asset is valid via the ->isValid() method, + * which manually invokes Watson Validating to make sure the asset's model is valid. + * + * @see \App\Observers\AssetObserver::updating() + * @see \App\Models\Asset::save() + */ + + $asset->unsetEventDispatcher(); + + /** * Invoke Watson Validating to check the asset itself and check to make sure it saved correctly. * We have to invoke this manually because of the unsetEventDispatcher() above.) */ if ($asset->isValid() && $asset->save()) { - $asset->logAudit(request('note'), request('location_id')); - - return response()->json(Helper::formatStandardApiResponse('success', [ - 'asset_tag' => e($asset->asset_tag), - 'note' => e($request->input('note')), - 'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date), - ], trans('admin/hardware/message.audit.success'))); + $asset->logAudit(request('note'), request('location_id'), null, $originalValues); + return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/hardware/message.audit.success'))); } - // Asset failed validation or was not able to be saved - return response()->json(Helper::formatStandardApiResponse('error', [ - 'asset_tag' => e($asset->asset_tag), - 'error' => $asset->getErrors()->first(), - ], trans('admin/hardware/message.audit.error', ['error' => $asset->getErrors()->first()])), 200); } // No matching asset for the asset tag that was passed. - return response()->json(Helper::formatStandardApiResponse('error', [ - 'asset_tag' => e($request->input('asset_tag')), - 'error' => trans('admin/hardware/message.audit.error'), - ], trans('admin/hardware/message.audit.error', ['error' => trans('admin/hardware/message.does_not_exist')])), 200); + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200); + } diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php index 9578acea6..835689977 100755 --- a/app/Http/Controllers/Assets/AssetsController.php +++ b/app/Http/Controllers/Assets/AssetsController.php @@ -6,6 +6,7 @@ use App\Events\CheckoutableCheckedIn; use App\Helpers\Helper; use App\Http\Controllers\Controller; use App\Http\Requests\ImageUploadRequest; +use App\Http\Requests\UpdateAssetRequest; use App\Models\Actionlog; use App\Http\Requests\UploadFileRequest; use Illuminate\Support\Facades\Log; @@ -390,26 +391,26 @@ class AssetsController extends Controller $asset = $request->handleImages($asset); // Update custom fields in the database. - // Validation for these fields is handlded through the AssetRequest form request // FIXME: No idea why this is returning a Builder error on db_column_name. // Need to investigate and fix. Using static method for now. $model = AssetModel::find($request->get('model_id')); if (($model) && ($model->fieldset)) { foreach ($model->fieldset->fields as $field) { - - if ($field->field_encrypted == '1') { - if (Gate::allows('assets.view.encrypted_custom_fields')) { - if (is_array($request->input($field->db_column))) { - $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column))); - } else { - $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column)); + if ($request->has($field->db_column)) { + if ($field->field_encrypted == '1') { + if (Gate::allows('assets.view.encrypted_custom_fields')) { + if (is_array($request->input($field->db_column))) { + $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column))); + } else { + $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column)); + } } - } - } else { - if (is_array($request->input($field->db_column))) { - $asset->{$field->db_column} = implode(', ', $request->input($field->db_column)); } else { - $asset->{$field->db_column} = $request->input($field->db_column); + if (is_array($request->input($field->db_column))) { + $asset->{$field->db_column} = implode(', ', $request->input($field->db_column)); + } else { + $asset->{$field->db_column} = $request->input($field->db_column); + } } } } @@ -865,13 +866,6 @@ class AssetsController extends Controller return view('hardware/quickscan-checkin')->with('statusLabel_list', Helper::statusLabelList()); } - public function audit(Asset $asset) - { - $settings = Setting::getSettings(); - $this->authorize('audit', Asset::class); - $dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString(); - return view('hardware/audit')->with('asset', $asset)->with('next_audit_date', $dt)->with('locations_list'); - } public function dueForAudit() { @@ -888,19 +882,59 @@ class AssetsController extends Controller } + public function audit(Asset $asset) + { + $settings = Setting::getSettings(); + $this->authorize('audit', Asset::class); + $dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString(); + return view('hardware/audit')->with('asset', $asset)->with('item', $asset)->with('next_audit_date', $dt)->with('locations_list'); + } + public function auditStore(UploadFileRequest $request, Asset $asset) { + $this->authorize('audit', Asset::class); - $rules = [ - 'location_id' => 'exists:locations,id|nullable|numeric', - 'next_audit_date' => 'date|nullable', - ]; + $originalValues = $asset->getRawOriginal(); - $validator = Validator::make($request->all(), $rules); + $asset->next_audit_date = $request->input('next_audit_date'); + $asset->last_audit_date = date('Y-m-d H:i:s'); - if ($validator->fails()) { - return response()->json(Helper::formatStandardApiResponse('error', null, $validator->errors()->all())); + // Check to see if they checked the box to update the physical location, + // not just note it in the audit notes + if ($request->input('update_location') == '1') { + $asset->location_id = $request->input('location_id'); + } + + // Update custom fields in the database + if (($asset->model) && ($asset->model->fieldset)) { + foreach ($asset->model->fieldset->fields as $field) { + if (($field->display_audit=='1') && ($request->has($field->db_column))) { + if ($field->field_encrypted == '1') { + if (Gate::allows('assets.view.encrypted_custom_fields')) { + if (is_array($request->input($field->db_column))) { + $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column))); + } else { + $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column)); + } + } + } else { + if (is_array($request->input($field->db_column))) { + $asset->{$field->db_column} = implode(', ', $request->input($field->db_column)); + } else { + $asset->{$field->db_column} = $request->input($field->db_column); + } + } + } + } + } + + // Validate custom fields + Validator::make($asset->toArray(), $asset->customFieldValidationRules())->validate(); + + // Validate the rest of the data before we turn off the event dispatcher + if ($asset->isInvalid()) { + return redirect()->back()->withInput()->withErrors($asset->getErrors()); } /** @@ -917,18 +951,11 @@ class AssetsController extends Controller * which manually invokes Watson Validating to make sure the asset's model is valid. * * @see \App\Observers\AssetObserver::updating() + * @see \App\Models\Asset::save() */ + $asset->unsetEventDispatcher(); - $asset->next_audit_date = $request->input('next_audit_date'); - $asset->last_audit_date = date('Y-m-d H:i:s'); - - // Check to see if they checked the box to update the physical location, - // not just note it in the audit notes - if ($request->input('update_location') == '1') { - $asset->location_id = $request->input('location_id'); - } - /** * Invoke Watson Validating to check the asset itself and check to make sure it saved correctly. @@ -942,7 +969,7 @@ class AssetsController extends Controller $file_name = $request->handleFile('private_uploads/audits/', 'audit-'.$asset->id, $request->file('image')); } - $asset->logAudit($request->input('note'), $request->input('location_id'), $file_name); + $asset->logAudit($request->input('note'), $request->input('location_id'), $file_name, $originalValues); return redirect()->route('assets.audit.due')->with('success', trans('admin/hardware/message.audit.success')); } diff --git a/app/Http/Controllers/CustomFieldsController.php b/app/Http/Controllers/CustomFieldsController.php index 7c0bdefa2..4c63179d5 100644 --- a/app/Http/Controllers/CustomFieldsController.php +++ b/app/Http/Controllers/CustomFieldsController.php @@ -106,6 +106,7 @@ class CustomFieldsController extends Controller "show_in_requestable_list" => $request->get("show_in_requestable_list", 0), "display_checkin" => $request->get("display_checkin", 0), "display_checkout" => $request->get("display_checkout", 0), + "display_audit" => $request->get("display_audit", 0), "created_by" => auth()->id() ]); @@ -250,6 +251,7 @@ class CustomFieldsController extends Controller $field->show_in_requestable_list = $request->get("show_in_requestable_list", 0); $field->display_checkin = $request->get("display_checkin", 0); $field->display_checkout = $request->get("display_checkout", 0); + $field->display_audit = $request->get("display_audit", 0); if ($request->get('format') == 'CUSTOM REGEX') { $field->format = e($request->get('custom_format')); diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index bd22562a6..33afac531 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -243,7 +243,7 @@ class ReportsController extends Controller $header = [ trans('general.date'), - trans('general.admin'), + trans('general.created_by'), trans('general.action'), trans('general.type'), trans('general.item'), diff --git a/app/Http/Requests/Traits/MayContainCustomFields.php b/app/Http/Requests/Traits/MayContainCustomFields.php index bbdf62893..2b12ccff6 100644 --- a/app/Http/Requests/Traits/MayContainCustomFields.php +++ b/app/Http/Requests/Traits/MayContainCustomFields.php @@ -10,19 +10,36 @@ trait MayContainCustomFields // this gets called automatically on a form request public function withValidator($validator) { - // find the model - if ($this->method() == 'POST') { - $asset_model = AssetModel::find($this->model_id); - } - if ($this->method() == 'PATCH' || $this->method() == 'PUT') { - $asset_model = $this->asset->model; + + // In case the model is being changed via form + if (request()->has('model_id')!='') { + + $asset_model = AssetModel::find(request()->input('model_id')); + + // or if we have it available to route-model-binding + } elseif ((request()->route('asset') && (request()->route('asset')->model_id))) { + + $asset_model = AssetModel::find(request()->route('asset')->model_id); + + } else { + + if ($this->method() == 'POST') { + $asset_model = AssetModel::find($this->model_id); + } + + if ($this->method() == 'PATCH' || $this->method() == 'PUT') { + $asset_model = $this->asset->model; + } } + + // collect the custom fields in the request $validator->after(function ($validator) use ($asset_model) { $request_fields = $this->collect()->keys()->filter(function ($attributes) { return str_starts_with($attributes, '_snipeit_'); }); - // if there are custom fields, find the one's that don't exist on the model's fieldset and add an error to the validator's error bag + + // if there are custom fields, find the ones that don't exist on the model's fieldset and add an error to the validator's error bag if (count($request_fields) > 0 && $validator->errors()->isEmpty()) { $request_fields->diff($asset_model?->fieldset?->fields?->pluck('db_column')) ->each(function ($request_field_name) use ($request_fields, $validator) { diff --git a/app/Http/Transformers/CustomFieldsTransformer.php b/app/Http/Transformers/CustomFieldsTransformer.php index d6401a3e5..501d264b5 100644 --- a/app/Http/Transformers/CustomFieldsTransformer.php +++ b/app/Http/Transformers/CustomFieldsTransformer.php @@ -50,6 +50,9 @@ class CustomFieldsTransformer 'display_in_user_view' => ($field->display_in_user_view =='1') ? true : false, 'auto_add_to_fieldsets' => ($field->auto_add_to_fieldsets == '1') ? true : false, 'show_in_listview' => ($field->show_in_listview == '1') ? true : false, + 'display_checkin' => ($field->display_checkin == '1') ? true : false, + 'display_checkout' => ($field->display_checkout == '1') ? true : false, + 'display_audit' => ($field->display_audit == '1') ? true : false, 'created_at' => Helper::getFormattedDateObject($field->created_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($field->updated_at, 'datetime'), ]; diff --git a/app/Models/Asset.php b/app/Models/Asset.php index 673012cf6..7fd80eaf0 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -213,6 +213,31 @@ class Asset extends Depreciable $this->attributes['expected_checkin'] = $value; } + + + public function customFieldValidationRules() + { + + $customFieldValidationRules = []; + + if (($this->model) && ($this->model->fieldset)) { + + foreach ($this->model->fieldset->fields as $field) { + + if ($field->format == 'BOOLEAN'){ + $this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN); + } + } + + $customFieldValidationRules += $this->model->fieldset->validation_rules(); + } + + return $customFieldValidationRules; + + } + + + /** * This handles the custom field validation for assets * @@ -220,29 +245,7 @@ class Asset extends Depreciable */ public function save(array $params = []) { - if ($this->model_id != '') { - $model = AssetModel::find($this->model_id); - - if (($model) && ($model->fieldset)) { - - foreach ($model->fieldset->fields as $field){ - if($field->format == 'BOOLEAN'){ - $this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN); - } - } - - $this->rules += $model->fieldset->validation_rules(); - - if ($this->model->fieldset){ - foreach ($this->model->fieldset->fields as $field){ - if($field->format == 'BOOLEAN'){ - $this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN); - } - } - } - } - } - + $this->rules += $this->customFieldValidationRules(); return parent::save($params); } @@ -254,7 +257,7 @@ class Asset extends Depreciable /** * Returns the warranty expiration date as Carbon object - * @return \Carbon|null + * @return \Carbon\Carbon|null */ public function getWarrantyExpiresAttribute() { @@ -687,6 +690,21 @@ class Asset extends Depreciable ->withTrashed(); } + + /** + * Get the list of audits for this asset + * + * @author [A. Gianotto] [] + * @since [v2.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function audits() + { + return $this->assetlog()->where('action_type', '=', 'audit') + ->orderBy('created_at', 'desc') + ->withTrashed(); + } + /** * Get the list of checkins for this asset * diff --git a/app/Models/Loggable.php b/app/Models/Loggable.php index 1e4a91202..93b82f840 100644 --- a/app/Models/Loggable.php +++ b/app/Models/Loggable.php @@ -220,9 +220,41 @@ trait Loggable * @since [v4.0] * @return \App\Models\Actionlog */ - public function logAudit($note, $location_id, $filename = null) + public function logAudit($note, $location_id, $filename = null, $originalValues = []) { + $log = new Actionlog; + + if (static::class == Asset::class) { + if ($asset = Asset::find($log->item_id)) { + // add the custom fields that were changed + if ($asset->model->fieldset) { + $fields_array = []; + foreach ($asset->model->fieldset->fields as $field) { + if ($field->display_audit == 1) { + $fields_array[$field->db_column] = $asset->{$field->db_column}; + } + } + } + } + } + + $changed = []; + + unset($originalValues['updated_at'], $originalValues['last_audit_date']); + foreach ($originalValues as $key => $value) { + + if ($value != $this->getAttributes()[$key]) { + $changed[$key]['old'] = $value; + $changed[$key]['new'] = $this->getAttributes()[$key]; + } + } + + if (!empty($changed)){ + $log->log_meta = json_encode($changed); + } + + $location = Location::find($location_id); if (static::class == LicenseSeat::class) { $log->item_type = License::class; @@ -235,6 +267,7 @@ trait Loggable $log->note = $note; $log->created_by = auth()->id(); $log->filename = $filename; + $log->action_date = date('Y-m-d H:i:s'); $log->logaction('audit'); $params = [ @@ -276,6 +309,7 @@ trait Loggable $log->item_id = $this->id; } $log->location_id = null; + $log->action_date = date('Y-m-d H:i:s'); $log->note = $note; $log->created_by = $created_by; $log->logaction('create'); @@ -303,6 +337,7 @@ trait Loggable $log->note = $note; $log->target_id = null; $log->created_at = date('Y-m-d H:i:s'); + $log->action_date = date('Y-m-d H:i:s'); $log->filename = $filename; $log->logaction('uploaded'); diff --git a/app/Presenters/AccessoryPresenter.php b/app/Presenters/AccessoryPresenter.php index 6d423bed7..86b4d1bc0 100644 --- a/app/Presenters/AccessoryPresenter.php +++ b/app/Presenters/AccessoryPresenter.php @@ -229,7 +229,7 @@ class AccessoryPresenter extends Presenter 'field' => 'created_by', 'searchable' => false, 'sortable' => false, - 'title' => trans('general.admin'), + 'title' => trans('general.created_by'), 'visible' => false, 'formatter' => 'usersLinkObjFormatter', ], diff --git a/app/Presenters/ActionlogPresenter.php b/app/Presenters/ActionlogPresenter.php index 834b88e75..4b7aefc87 100644 --- a/app/Presenters/ActionlogPresenter.php +++ b/app/Presenters/ActionlogPresenter.php @@ -102,6 +102,10 @@ class ActionlogPresenter extends Presenter return 'fas fa-sticky-note'; } + if ($this->action_type == 'audit') { + return 'fas fa-clipboard-check'; + } + return 'fa-solid fa-rotate-right'; } diff --git a/app/Presenters/LocationPresenter.php b/app/Presenters/LocationPresenter.php index 42f56a309..e36a38682 100644 --- a/app/Presenters/LocationPresenter.php +++ b/app/Presenters/LocationPresenter.php @@ -262,7 +262,7 @@ class LocationPresenter extends Presenter 'field' => 'created_by', 'searchable' => false, 'sortable' => false, - 'title' => trans('general.admin'), + 'title' => trans('general.created_by'), 'visible' => false, 'formatter' => 'usersLinkObjFormatter', ], diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index bb89d3524..d8b1ac141 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -385,6 +385,12 @@ class UserFactory extends Factory return $this->appendPermission(['suppliers.delete' => '1']); } + public function auditAssets() + { + return $this->appendPermission(['assets.audit' => '1']); + } + + private function appendPermission(array $permission) { return $this->state(function ($currentState) use ($permission) { diff --git a/database/migrations/2025_04_07_145418_add_custom_fields_to_audit.php b/database/migrations/2025_04_07_145418_add_custom_fields_to_audit.php new file mode 100644 index 000000000..d5594fc03 --- /dev/null +++ b/database/migrations/2025_04_07_145418_add_custom_fields_to_audit.php @@ -0,0 +1,28 @@ +boolean('display_audit')->default(0); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('custom_fields', function (Blueprint $table) { + $table->dropColumn('display_audit'); + }); + } +}; diff --git a/public/css/build/app.css b/public/css/build/app.css index 66da69f4e..060421ac6 100644 --- a/public/css/build/app.css +++ b/public/css/build/app.css @@ -1175,7 +1175,8 @@ th.css-history > .th-inner::before { padding: 6px 12px; height: 34px; } -.form-group.has-error label { +.form-group.has-error label, +.form-group.has-error .help-block { color: #a94442; } .select2-container--default .select2-selection--multiple { diff --git a/public/css/build/overrides.css b/public/css/build/overrides.css index a4c6ddb9e..fb5c19221 100644 --- a/public/css/build/overrides.css +++ b/public/css/build/overrides.css @@ -806,7 +806,8 @@ th.css-history > .th-inner::before { padding: 6px 12px; height: 34px; } -.form-group.has-error label { +.form-group.has-error label, +.form-group.has-error .help-block { color: #a94442; } .select2-container--default .select2-selection--multiple { diff --git a/public/css/dist/all.css b/public/css/dist/all.css index d508c1d25..2708d3e64 100644 --- a/public/css/dist/all.css +++ b/public/css/dist/all.css @@ -22510,7 +22510,8 @@ th.css-history > .th-inner::before { padding: 6px 12px; height: 34px; } -.form-group.has-error label { +.form-group.has-error label, +.form-group.has-error .help-block { color: #a94442; } .select2-container--default .select2-selection--multiple { @@ -24061,7 +24062,8 @@ th.css-history > .th-inner::before { padding: 6px 12px; height: 34px; } -.form-group.has-error label { +.form-group.has-error label, +.form-group.has-error .help-block { color: #a94442; } .select2-container--default .select2-selection--multiple { diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 4da14272a..295126469 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -2,8 +2,8 @@ "/js/build/app.js": "/js/build/app.js?id=607de09b70b83ef82a427e4b36341682", "/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=06c13e817cc022028b3f4a33c0ca303a", "/css/dist/skins/_all-skins.css": "/css/dist/skins/_all-skins.css?id=79aa889a1a6691013be6c342ca7391cd", - "/css/build/overrides.css": "/css/build/overrides.css?id=712a9b236379148e57e34b70c878bff9", - "/css/build/app.css": "/css/build/app.css?id=9e4ea63fed30049dc054a87a0acfaa61", + "/css/build/overrides.css": "/css/build/overrides.css?id=4d62149a0ee9dc139bdf03ff2f83930d", + "/css/build/app.css": "/css/build/app.css?id=d47ce0dc14671bb4e462e111001488e5", "/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=4ea0068716c1bb2434d87a16d51b98c9", "/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=7b315b9612b8fde8f9c5b0ddb6bba690", "/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=ea22079836a432d7f46a5d390c445e13", @@ -19,7 +19,7 @@ "/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=a82b065847bf3cd5d713c04ee8dc86c6", "/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=6ea836d8126de101081c49abbdb89417", "/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=76482123f6c70e866d6b971ba91de7bb", - "/css/dist/all.css": "/css/dist/all.css?id=0d5289d806efaf15e9d47833843cc986", + "/css/dist/all.css": "/css/dist/all.css?id=7c861c2086473c513fe26c21e3c4d433", "/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/js/select2/i18n/af.js": "/js/select2/i18n/af.js?id=4f6fcd73488ce79fae1b7a90aceaecde", diff --git a/resources/assets/less/overrides.less b/resources/assets/less/overrides.less index 4640017c0..8c8b80e46 100644 --- a/resources/assets/less/overrides.less +++ b/resources/assets/less/overrides.less @@ -886,7 +886,7 @@ th.css-history > .th-inner::before { height: 34px; } -.form-group.has-error label { +.form-group.has-error label, .form-group.has-error .help-block { color: #a94442; } diff --git a/resources/lang/en-US/admin/custom_fields/general.php b/resources/lang/en-US/admin/custom_fields/general.php index f17eedda1..c9ae61c96 100644 --- a/resources/lang/en-US/admin/custom_fields/general.php +++ b/resources/lang/en-US/admin/custom_fields/general.php @@ -59,5 +59,6 @@ return [ 'encrypted_options' => 'This field is encrypted, so some display options will not be available.', 'display_checkin' => 'Display in checkin forms', 'display_checkout' => 'Display in checkout forms', + 'display_audit' => 'Display in audit forms', ]; diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index e20c7dfd5..72d577ff6 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -31,6 +31,7 @@ return [ 'accept_assets_menu' => 'Accept Assets', 'accept_item' => 'Accept Item', 'audit' => 'Audit', + 'audits' => 'Audits', 'audit_report' => 'Audit Log', 'assets' => 'Assets', 'assets_audited' => 'assets audited', diff --git a/resources/views/accessories/view.blade.php b/resources/views/accessories/view.blade.php index a4f4656bf..41ffbb95f 100644 --- a/resources/views/accessories/view.blade.php +++ b/resources/views/accessories/view.blade.php @@ -125,7 +125,7 @@ {{ trans('general.record_created') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} {{ trans('general.action') }} {{ trans('general.file_name') }} {{ trans('general.item') }} diff --git a/resources/views/consumables/view.blade.php b/resources/views/consumables/view.blade.php index 93031b63c..4ef82efc8 100644 --- a/resources/views/consumables/view.blade.php +++ b/resources/views/consumables/view.blade.php @@ -421,7 +421,7 @@ {{ trans('general.date') }} {{ trans('general.notes') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} @@ -472,7 +472,7 @@ {{ trans('admin/hardware/table.icon') }} {{ trans('general.date') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} {{ trans('general.action') }} {{ trans('general.file_name') }} {{ trans('general.item') }} diff --git a/resources/views/custom_fields/fields/edit.blade.php b/resources/views/custom_fields/fields/edit.blade.php index 8ff41b7a4..4969899a9 100644 --- a/resources/views/custom_fields/fields/edit.blade.php +++ b/resources/views/custom_fields/fields/edit.blade.php @@ -224,6 +224,14 @@ + +
+ +
+
diff --git a/resources/views/custom_fields/index.blade.php b/resources/views/custom_fields/index.blade.php index 5b9338baf..ee2778d77 100644 --- a/resources/views/custom_fields/index.blade.php +++ b/resources/views/custom_fields/index.blade.php @@ -196,6 +196,14 @@ + + + + {{ trans('admin/custom_fields/general.display_audit') }} + + + {{ trans('admin/custom_fields/general.field_element_short') }} @@ -227,6 +235,7 @@ {!! ($field->is_unique=='1') ? '' : '' !!} {!! ($field->display_checkin=='1') ? '' : '' !!} {!! ($field->display_checkout=='1') ? '' : '' !!} + {!! ($field->display_audit=='1') ? '' : '' !!} {{ $field->element }} @foreach($field->fieldset as $fieldset) diff --git a/resources/views/hardware/audit.blade.php b/resources/views/hardware/audit.blade.php index baeb56d59..72be0e833 100644 --- a/resources/views/hardware/audit.blade.php +++ b/resources/views/hardware/audit.blade.php @@ -115,6 +115,12 @@
+ + @include("models/custom_fields_form", [ + 'model' => $asset->model, + 'show_display_checkout_fields' => 'true' + ]) +
diff --git a/resources/views/hardware/quickscan.blade.php b/resources/views/hardware/quickscan.blade.php index e81981ed6..0dd3fb570 100644 --- a/resources/views/hardware/quickscan.blade.php +++ b/resources/views/hardware/quickscan.blade.php @@ -142,7 +142,7 @@ var formData = $('#audit-form').serializeArray(); $.ajax({ - url: "{{ route('api.asset.audit') }}", + url: "{{ route('api.asset.audit.legacy') }}", type : 'POST', headers: { "X-Requested-With": 'XMLHttpRequest', diff --git a/resources/views/hardware/view.blade.php b/resources/views/hardware/view.blade.php index e7bd3460e..ba6556c41 100755 --- a/resources/views/hardware/view.blade.php +++ b/resources/views/hardware/view.blade.php @@ -111,6 +111,22 @@ @endif + @if ($asset->audits->count() > 0) +
  • + + + + + +
  • + @endif +
  • -
    + +
    + +
    +
    + + + + + + + + + + + + + + + +
    {{ trans('general.date') }}{{ trans('general.created_by') }}{{ trans('general.file_name') }}{{ trans('general.notes') }}{{ trans('general.download') }}{{ trans('admin/hardware/table.changed')}}{{ trans('admin/settings/general.login_ip') }}{{ trans('admin/settings/general.login_user_agent') }}{{ trans('general.action_source') }}
    +
    +
    +
    + + +
    @@ -1419,7 +1481,7 @@ {{ trans('admin/hardware/table.icon') }} {{ trans('general.date') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} {{ trans('general.action') }} {{ trans('general.file_name') }} {{ trans('general.item') }} diff --git a/resources/views/layouts/default.blade.php b/resources/views/layouts/default.blade.php index 82f7eca50..10340a6c1 100644 --- a/resources/views/layouts/default.blade.php +++ b/resources/views/layouts/default.blade.php @@ -526,7 +526,7 @@ dir="{{ Helper::determineLanguageDirection() }}"> @can('audit', \App\Models\Asset::class) - + {{ trans('general.audit_due') }} {{ (isset($total_due_and_overdue_for_audit)) ? $total_due_and_overdue_for_audit : '' }} diff --git a/resources/views/licenses/view.blade.php b/resources/views/licenses/view.blade.php index ffcc94621..6d7b406df 100755 --- a/resources/views/licenses/view.blade.php +++ b/resources/views/licenses/view.blade.php @@ -498,7 +498,7 @@ {{ trans('general.record_created') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} {{ trans('general.action') }} {{ trans('general.file_name') }} {{ trans('general.item') }} diff --git a/resources/views/locations/view.blade.php b/resources/views/locations/view.blade.php index 6ee75d6bf..345a40a3d 100644 --- a/resources/views/locations/view.blade.php +++ b/resources/views/locations/view.blade.php @@ -434,7 +434,7 @@ {{ trans('admin/hardware/table.icon') }} {{ trans('general.date') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} {{ trans('general.action') }} {{ trans('general.item') }} {{ trans('general.target') }} diff --git a/resources/views/models/custom_fields_form.blade.php b/resources/views/models/custom_fields_form.blade.php index 3297c15c9..2346218a3 100644 --- a/resources/views/models/custom_fields_form.blade.php +++ b/resources/views/models/custom_fields_form.blade.php @@ -101,10 +101,4 @@ @endif - + diff --git a/resources/views/partials/bootstrap-table.blade.php b/resources/views/partials/bootstrap-table.blade.php index 76656a0a2..761acf481 100644 --- a/resources/views/partials/bootstrap-table.blade.php +++ b/resources/views/partials/bootstrap-table.blade.php @@ -341,7 +341,7 @@ function hardwareAuditFormatter(value, row) { - return '{{ trans('general.audit') }}'; + return '{{ trans('general.audit') }}'; } diff --git a/resources/views/reports/asset_maintenances.blade.php b/resources/views/reports/asset_maintenances.blade.php index 3da2e798f..406375ccd 100644 --- a/resources/views/reports/asset_maintenances.blade.php +++ b/resources/views/reports/asset_maintenances.blade.php @@ -46,7 +46,7 @@ {{ trans('general.location') }} {{ trans('admin/hardware/form.default_location') }} {{ trans('admin/asset_maintenances/table.is_warranty') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} {{ trans('admin/asset_maintenances/form.notes') }} diff --git a/resources/views/users/view.blade.php b/resources/views/users/view.blade.php index 7ee3c4926..e77ab3bd2 100755 --- a/resources/views/users/view.blade.php +++ b/resources/views/users/view.blade.php @@ -1003,7 +1003,7 @@ {{ trans('general.signature') }} @endif {{ trans('admin/hardware/table.serial') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} {{ trans('admin/settings/general.login_ip') }} {{ trans('admin/settings/general.login_user_agent') }} {{ trans('general.action_source') }} diff --git a/routes/api.php b/routes/api.php index 9adc83af2..5724990c7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -511,8 +511,17 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi ->where(['action' => 'audit|audits|checkins', 'upcoming_status' => 'due|overdue|due-or-overdue']); + // Legacy URL for audit + Route::post('audit', + [ + Api\AssetsController::class, + 'audit' + ] + )->name('api.asset.audit.legacy'); - Route::post('audit', + + // Newer url for audit + Route::post('{asset}/audit', [ Api\AssetsController::class, 'audit' diff --git a/routes/web/hardware.php b/routes/web/hardware.php index 3b65f730e..21f4f40d4 100644 --- a/routes/web/hardware.php +++ b/routes/web/hardware.php @@ -63,14 +63,14 @@ Route::group( ->push(trans_choice('general.checkin_due_days', Setting::getSettings()->due_checkin_days, ['days' => Setting::getSettings()->due_checkin_days]), route('assets.audit.due')) ); - Route::get('audit/{asset}', [AssetsController::class, 'audit']) + Route::get('{asset}/audit', [AssetsController::class, 'audit']) ->name('asset.audit.create') ->breadcrumbs(fn (Trail $trail, Asset $asset) => $trail->parent('hardware.show', $asset) ->push(trans('general.audit')) ); - Route::post('audit/{asset}', + Route::post('{asset}/audit', [AssetsController::class, 'auditStore'] )->name('asset.audit.store'); diff --git a/tests/Feature/Assets/Api/AuditAssetTest.php b/tests/Feature/Assets/Api/AuditAssetTest.php new file mode 100644 index 000000000..f93a65ae6 --- /dev/null +++ b/tests/Feature/Assets/Api/AuditAssetTest.php @@ -0,0 +1,78 @@ +actingAsForApi(User::factory()->auditAssets()->create()) + ->postJson(route('api.asset.audit', 123456789)) + ->assertStatusMessageIs('error'); + } + + public function testRequiresPermissionToAuditAsset() + { + $asset = Asset::factory()->create(); + $this->actingAsForApi(User::factory()->create()) + ->postJson(route('api.asset.audit', $asset)) + ->assertForbidden(); + } + + public function testLegacyAssetAuditIsSaved() + { + $asset = Asset::factory()->create(); + $this->actingAsForApi(User::factory()->auditAssets()->create()) + ->postJson(route('api.asset.audit.legacy'), [ + 'asset_tag' => $asset->asset_tag, + 'note' => 'test', + ]) + ->assertStatusMessageIs('success') + ->assertJson( + [ + 'messages' =>trans('admin/hardware/message.audit.success'), + 'payload' => [ + 'id' => $asset->id, + 'asset_tag' => $asset->asset_tag, + 'note' => 'test' + ], + ]) + ->assertStatus(200); + + } + + + public function testAssetAuditIsSaved() + { + $asset = Asset::factory()->create(); + $this->actingAsForApi(User::factory()->auditAssets()->create()) + ->postJson(route('api.asset.audit', $asset), [ + 'note' => 'test' + ]) + ->assertStatusMessageIs('success') + ->assertJson( + [ + 'messages' =>trans('admin/hardware/message.audit.success'), + 'payload' => [ + 'id' => $asset->id, + 'asset_tag' => $asset->asset_tag, + 'note' => 'test' + ], + ]) + ->assertStatus(200); + + } + + +} diff --git a/tests/Feature/Assets/Ui/AuditAssetTest.php b/tests/Feature/Assets/Ui/AuditAssetTest.php new file mode 100644 index 000000000..a966e6f45 --- /dev/null +++ b/tests/Feature/Assets/Ui/AuditAssetTest.php @@ -0,0 +1,34 @@ +actingAs(User::factory()->create()) + ->get(route('clone/hardware', Asset::factory()->create())) + ->assertForbidden(); + } + + public function testPageCanBeAccessed(): void + { + $this->actingAs(User::factory()->auditAssets()->create()) + ->get(route('asset.audit.create', Asset::factory()->create())) + ->assertStatus(200); + } + + public function testAssetCanBeAudited() + { + $response = $this->actingAs(User::factory()->auditAssets()->create()) + ->post(route('asset.audit.store', Asset::factory()->create())) + ->assertStatus(302) + ->assertRedirect(route('assets.audit.due')); + + $this->followRedirects($response)->assertSee('success'); + } +}