Merge remote-tracking branch 'origin/develop'

Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/overrides.css
#	public/css/dist/all.css
#	public/mix-manifest.json
This commit is contained in:
snipe 2025-04-08 05:58:51 +01:00
commit f1d006c236
40 changed files with 596 additions and 150 deletions

View file

@ -34,6 +34,7 @@ use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\View\Label; use App\View\Label;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
/** /**
@ -1064,7 +1065,7 @@ class AssetsController extends Controller
* @param int $id * @param int $id
* @since [v4.0] * @since [v4.0]
*/ */
public function audit(Request $request): JsonResponse public function audit(Request $request, Asset $asset): JsonResponse
{ {
$this->authorize('audit', Asset::class); $this->authorize('audit', Asset::class);
@ -1072,36 +1073,15 @@ class AssetsController extends Controller
$settings = Setting::getSettings(); $settings = Setting::getSettings();
$dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString(); $dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
// No tag passed - return an error // Allow the asset tag to be passed in the payload (legacy method)
if (!$request->filled('asset_tag')) { 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);
}
$asset = Asset::where('asset_tag', '=', $request->input('asset_tag'))->first(); $asset = Asset::where('asset_tag', '=', $request->input('asset_tag'))->first();
}
if ($asset) { if ($asset) {
/** $originalValues = $asset->getRawOriginal();
* 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();
$asset->next_audit_date = $dt; $asset->next_audit_date = $dt;
if ($request->filled('next_audit_date')) { 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'); $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. * 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.) * We have to invoke this manually because of the unsetEventDispatcher() above.)
*/ */
if ($asset->isValid() && $asset->save()) { if ($asset->isValid() && $asset->save()) {
$asset->logAudit(request('note'), request('location_id')); $asset->logAudit(request('note'), request('location_id'), null, $originalValues);
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/hardware/message.audit.success')));
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 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. // No matching asset for the asset tag that was passed.
return response()->json(Helper::formatStandardApiResponse('error', [ return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
'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);
} }

View file

@ -6,6 +6,7 @@ use App\Events\CheckoutableCheckedIn;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\UpdateAssetRequest;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest; use App\Http\Requests\UploadFileRequest;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -390,13 +391,12 @@ class AssetsController extends Controller
$asset = $request->handleImages($asset); $asset = $request->handleImages($asset);
// Update custom fields in the database. // 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. // FIXME: No idea why this is returning a Builder error on db_column_name.
// Need to investigate and fix. Using static method for now. // Need to investigate and fix. Using static method for now.
$model = AssetModel::find($request->get('model_id')); $model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) { if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) { foreach ($model->fieldset->fields as $field) {
if ($request->has($field->db_column)) {
if ($field->field_encrypted == '1') { if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) { if (Gate::allows('assets.view.encrypted_custom_fields')) {
if (is_array($request->input($field->db_column))) { if (is_array($request->input($field->db_column))) {
@ -414,6 +414,7 @@ class AssetsController extends Controller
} }
} }
} }
}
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]); session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
@ -865,13 +866,6 @@ class AssetsController extends Controller
return view('hardware/quickscan-checkin')->with('statusLabel_list', Helper::statusLabelList()); 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() 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) public function auditStore(UploadFileRequest $request, Asset $asset)
{ {
$this->authorize('audit', Asset::class); $this->authorize('audit', Asset::class);
$rules = [ $originalValues = $asset->getRawOriginal();
'location_id' => 'exists:locations,id|nullable|numeric',
'next_audit_date' => 'date|nullable',
];
$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()) { // Check to see if they checked the box to update the physical location,
return response()->json(Helper::formatStandardApiResponse('error', null, $validator->errors()->all())); // 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. * which manually invokes Watson Validating to make sure the asset's model is valid.
* *
* @see \App\Observers\AssetObserver::updating() * @see \App\Observers\AssetObserver::updating()
* @see \App\Models\Asset::save()
*/ */
$asset->unsetEventDispatcher(); $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. * 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')); $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')); return redirect()->route('assets.audit.due')->with('success', trans('admin/hardware/message.audit.success'));
} }

View file

@ -106,6 +106,7 @@ class CustomFieldsController extends Controller
"show_in_requestable_list" => $request->get("show_in_requestable_list", 0), "show_in_requestable_list" => $request->get("show_in_requestable_list", 0),
"display_checkin" => $request->get("display_checkin", 0), "display_checkin" => $request->get("display_checkin", 0),
"display_checkout" => $request->get("display_checkout", 0), "display_checkout" => $request->get("display_checkout", 0),
"display_audit" => $request->get("display_audit", 0),
"created_by" => auth()->id() "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->show_in_requestable_list = $request->get("show_in_requestable_list", 0);
$field->display_checkin = $request->get("display_checkin", 0); $field->display_checkin = $request->get("display_checkin", 0);
$field->display_checkout = $request->get("display_checkout", 0); $field->display_checkout = $request->get("display_checkout", 0);
$field->display_audit = $request->get("display_audit", 0);
if ($request->get('format') == 'CUSTOM REGEX') { if ($request->get('format') == 'CUSTOM REGEX') {
$field->format = e($request->get('custom_format')); $field->format = e($request->get('custom_format'));

View file

@ -243,7 +243,7 @@ class ReportsController extends Controller
$header = [ $header = [
trans('general.date'), trans('general.date'),
trans('general.admin'), trans('general.created_by'),
trans('general.action'), trans('general.action'),
trans('general.type'), trans('general.type'),
trans('general.item'), trans('general.item'),

View file

@ -10,19 +10,36 @@ trait MayContainCustomFields
// this gets called automatically on a form request // this gets called automatically on a form request
public function withValidator($validator) public function withValidator($validator)
{ {
// find the 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') { if ($this->method() == 'POST') {
$asset_model = AssetModel::find($this->model_id); $asset_model = AssetModel::find($this->model_id);
} }
if ($this->method() == 'PATCH' || $this->method() == 'PUT') { if ($this->method() == 'PATCH' || $this->method() == 'PUT') {
$asset_model = $this->asset->model; $asset_model = $this->asset->model;
} }
}
// collect the custom fields in the request // collect the custom fields in the request
$validator->after(function ($validator) use ($asset_model) { $validator->after(function ($validator) use ($asset_model) {
$request_fields = $this->collect()->keys()->filter(function ($attributes) { $request_fields = $this->collect()->keys()->filter(function ($attributes) {
return str_starts_with($attributes, '_snipeit_'); 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()) { if (count($request_fields) > 0 && $validator->errors()->isEmpty()) {
$request_fields->diff($asset_model?->fieldset?->fields?->pluck('db_column')) $request_fields->diff($asset_model?->fieldset?->fields?->pluck('db_column'))
->each(function ($request_field_name) use ($request_fields, $validator) { ->each(function ($request_field_name) use ($request_fields, $validator) {

View file

@ -50,6 +50,9 @@ class CustomFieldsTransformer
'display_in_user_view' => ($field->display_in_user_view =='1') ? true : false, 'display_in_user_view' => ($field->display_in_user_view =='1') ? true : false,
'auto_add_to_fieldsets' => ($field->auto_add_to_fieldsets == '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, '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'), 'created_at' => Helper::getFormattedDateObject($field->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($field->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($field->updated_at, 'datetime'),
]; ];

View file

@ -57,7 +57,9 @@ class Actionlog extends SnipeModel
'user_agent', 'user_agent',
'item_type', 'item_type',
'target_type', 'target_type',
'action_source' 'action_source',
'created_at',
'action_date',
]; ];
/** /**
@ -69,7 +71,25 @@ class Actionlog extends SnipeModel
'company' => ['name'], 'company' => ['name'],
'adminuser' => ['first_name','last_name','username', 'email'], 'adminuser' => ['first_name','last_name','username', 'email'],
'user' => ['first_name','last_name','username', 'email'], 'user' => ['first_name','last_name','username', 'email'],
'assets' => ['asset_tag','name', 'serial'], 'assets' => ['asset_tag','name', 'serial', 'order_number', 'notes', 'purchase_date'],
'assets.model' => ['name', 'model_number', 'eol', 'notes'],
'assets.model.category' => ['name', 'notes'],
'assets.model.manufacturer' => ['name', 'notes'],
'licenses' => ['name', 'serial', 'notes', 'order_number', 'license_email', 'license_name', 'purchase_order', 'purchase_date'],
'licenses.category' => ['name', 'notes'],
'licenses.supplier' => ['name'],
'consumables' => ['name', 'notes', 'order_number', 'model_number', 'item_no', 'purchase_date'],
'consumables.category' => ['name', 'notes'],
'consumables.location' => ['name', 'notes'],
'consumables.supplier' => ['name', 'notes'],
'components' => ['name', 'notes', 'purchase_date'],
'components.category' => ['name', 'notes'],
'components.location' => ['name', 'notes'],
'components.supplier' => ['name', 'notes'],
'accessories' => ['name', 'purchase_date'],
'accessories.category' => ['name'],
'accessories.location' => ['name', 'notes'],
'accessories.supplier' => ['name', 'notes'],
]; ];
/** /**
@ -134,6 +154,54 @@ class Actionlog extends SnipeModel
return $this->hasMany(\App\Models\Asset::class, 'id', 'item_id'); return $this->hasMany(\App\Models\Asset::class, 'id', 'item_id');
} }
/**
* Establishes the actionlog -> license relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function licenses()
{
return $this->hasMany(\App\Models\License::class, 'id', 'item_id');
}
/**
* Establishes the actionlog -> consumable relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function consumables()
{
return $this->hasMany(\App\Models\Consumable::class, 'id', 'item_id');
}
/**
* Establishes the actionlog -> consumable relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function accessories()
{
return $this->hasMany(\App\Models\Accessory::class, 'id', 'item_id');
}
/**
* Establishes the actionlog -> components relationship
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function components()
{
return $this->hasMany(\App\Models\Component::class, 'id', 'item_id');
}
/** /**
* Establishes the actionlog -> item type relationship * Establishes the actionlog -> item type relationship
* *

View file

@ -213,6 +213,31 @@ class Asset extends Depreciable
$this->attributes['expected_checkin'] = $value; $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 * This handles the custom field validation for assets
* *
@ -220,29 +245,7 @@ class Asset extends Depreciable
*/ */
public function save(array $params = []) public function save(array $params = [])
{ {
if ($this->model_id != '') { $this->rules += $this->customFieldValidationRules();
$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);
}
}
}
}
}
return parent::save($params); return parent::save($params);
} }
@ -254,7 +257,7 @@ class Asset extends Depreciable
/** /**
* Returns the warranty expiration date as Carbon object * Returns the warranty expiration date as Carbon object
* @return \Carbon|null * @return \Carbon\Carbon|null
*/ */
public function getWarrantyExpiresAttribute() public function getWarrantyExpiresAttribute()
{ {
@ -687,6 +690,21 @@ class Asset extends Depreciable
->withTrashed(); ->withTrashed();
} }
/**
* Get the list of audits for this asset
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @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 * Get the list of checkins for this asset
* *

View file

@ -220,9 +220,41 @@ trait Loggable
* @since [v4.0] * @since [v4.0]
* @return \App\Models\Actionlog * @return \App\Models\Actionlog
*/ */
public function logAudit($note, $location_id, $filename = null) public function logAudit($note, $location_id, $filename = null, $originalValues = [])
{ {
$log = new Actionlog; $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); $location = Location::find($location_id);
if (static::class == LicenseSeat::class) { if (static::class == LicenseSeat::class) {
$log->item_type = License::class; $log->item_type = License::class;
@ -235,6 +267,7 @@ trait Loggable
$log->note = $note; $log->note = $note;
$log->created_by = auth()->id(); $log->created_by = auth()->id();
$log->filename = $filename; $log->filename = $filename;
$log->action_date = date('Y-m-d H:i:s');
$log->logaction('audit'); $log->logaction('audit');
$params = [ $params = [
@ -276,6 +309,7 @@ trait Loggable
$log->item_id = $this->id; $log->item_id = $this->id;
} }
$log->location_id = null; $log->location_id = null;
$log->action_date = date('Y-m-d H:i:s');
$log->note = $note; $log->note = $note;
$log->created_by = $created_by; $log->created_by = $created_by;
$log->logaction('create'); $log->logaction('create');
@ -303,6 +337,7 @@ trait Loggable
$log->note = $note; $log->note = $note;
$log->target_id = null; $log->target_id = null;
$log->created_at = date('Y-m-d H:i:s'); $log->created_at = date('Y-m-d H:i:s');
$log->action_date = date('Y-m-d H:i:s');
$log->filename = $filename; $log->filename = $filename;
$log->logaction('uploaded'); $log->logaction('uploaded');

View file

@ -229,7 +229,7 @@ class AccessoryPresenter extends Presenter
'field' => 'created_by', 'field' => 'created_by',
'searchable' => false, 'searchable' => false,
'sortable' => false, 'sortable' => false,
'title' => trans('general.admin'), 'title' => trans('general.created_by'),
'visible' => false, 'visible' => false,
'formatter' => 'usersLinkObjFormatter', 'formatter' => 'usersLinkObjFormatter',
], ],

View file

@ -102,6 +102,10 @@ class ActionlogPresenter extends Presenter
return 'fas fa-sticky-note'; return 'fas fa-sticky-note';
} }
if ($this->action_type == 'audit') {
return 'fas fa-clipboard-check';
}
return 'fa-solid fa-rotate-right'; return 'fa-solid fa-rotate-right';
} }

View file

@ -262,7 +262,7 @@ class LocationPresenter extends Presenter
'field' => 'created_by', 'field' => 'created_by',
'searchable' => false, 'searchable' => false,
'sortable' => false, 'sortable' => false,
'title' => trans('general.admin'), 'title' => trans('general.created_by'),
'visible' => false, 'visible' => false,
'formatter' => 'usersLinkObjFormatter', 'formatter' => 'usersLinkObjFormatter',
], ],

View file

@ -1,10 +1,10 @@
<?php <?php
return array ( return array (
'app_version' => 'v8.0.4', 'app_version' => 'v8.0.4',
'full_app_version' => 'v8.0.4 - build 17333-gaf408bb45', 'full_app_version' => 'v8.0.4 - build 17400-gb0b5a9669',
'build_version' => '17333', 'build_version' => '17400',
'prerelease_version' => '', 'prerelease_version' => '',
'hash_version' => 'gaf408bb45', 'hash_version' => 'gb0b5a9669',
'full_hash' => 'v8.0.4-135-gaf408bb45', 'full_hash' => 'v8.0.4-202-gb0b5a9669',
'branch' => 'master', 'branch' => 'master',
); );

View file

@ -385,6 +385,12 @@ class UserFactory extends Factory
return $this->appendPermission(['suppliers.delete' => '1']); return $this->appendPermission(['suppliers.delete' => '1']);
} }
public function auditAssets()
{
return $this->appendPermission(['assets.audit' => '1']);
}
private function appendPermission(array $permission) private function appendPermission(array $permission)
{ {
return $this->state(function ($currentState) use ($permission) { return $this->state(function ($currentState) use ($permission) {

View file

@ -0,0 +1,28 @@
<?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::table('custom_fields', function (Blueprint $table) {
$table->boolean('display_audit')->default(0);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('custom_fields', function (Blueprint $table) {
$table->dropColumn('display_audit');
});
}
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -2,8 +2,8 @@
"/js/build/app.js": "/js/build/app.js?id=970945c192cb3217d5f371a1931d7d77", "/js/build/app.js": "/js/build/app.js?id=970945c192cb3217d5f371a1931d7d77",
"/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=d34ae2483cbe2c77478c45f4006eba55", "/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=d34ae2483cbe2c77478c45f4006eba55",
"/css/dist/skins/_all-skins.css": "/css/dist/skins/_all-skins.css?id=6bf62cdec2477f3176df196fd0c99662", "/css/dist/skins/_all-skins.css": "/css/dist/skins/_all-skins.css?id=6bf62cdec2477f3176df196fd0c99662",
"/css/build/overrides.css": "/css/build/overrides.css?id=3ca26335f461f9d198f3c9869dc0ce52", "/css/build/overrides.css": "/css/build/overrides.css?id=12c469a76750af9fee1c92722c563e7e",
"/css/build/app.css": "/css/build/app.css?id=35c2aaddd23e600b56f77c95b04523ac", "/css/build/app.css": "/css/build/app.css?id=1aed878e9211529befa4fdeed11ac52e",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=a67bd93bed52e6a29967fe472de66d6c", "/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=a67bd93bed52e6a29967fe472de66d6c",
"/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=fc7adb943668ac69fe4b646625a7571f", "/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=fc7adb943668ac69fe4b646625a7571f",
"/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=53edc92eb2d272744bc7404ec259930e", "/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=53edc92eb2d272744bc7404ec259930e",
@ -19,7 +19,7 @@
"/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=091d9625203be910caca3e229afe438f", "/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=091d9625203be910caca3e229afe438f",
"/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=18787b3f00a3be7be38ee4e26cbd2a07", "/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=18787b3f00a3be7be38ee4e26cbd2a07",
"/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=1f33ca3d860461c1127ec465ab3ebb6b", "/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=1f33ca3d860461c1127ec465ab3ebb6b",
"/css/dist/all.css": "/css/dist/all.css?id=119a35ca4c300a14baefbdcbb203d08e", "/css/dist/all.css": "/css/dist/all.css?id=d092c5407b7e50f6a01e5e918ec737ee",
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/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", "/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", "/js/select2/i18n/af.js": "/js/select2/i18n/af.js?id=4f6fcd73488ce79fae1b7a90aceaecde",

View file

@ -886,7 +886,7 @@ th.css-history > .th-inner::before {
height: 34px; height: 34px;
} }
.form-group.has-error label { .form-group.has-error label, .form-group.has-error .help-block {
color: #a94442; color: #a94442;
} }

View file

@ -59,5 +59,6 @@ return [
'encrypted_options' => 'This field is encrypted, so some display options will not be available.', 'encrypted_options' => 'This field is encrypted, so some display options will not be available.',
'display_checkin' => 'Display in checkin forms', 'display_checkin' => 'Display in checkin forms',
'display_checkout' => 'Display in checkout forms', 'display_checkout' => 'Display in checkout forms',
'display_audit' => 'Display in audit forms',
]; ];

View file

@ -31,6 +31,7 @@ return [
'accept_assets_menu' => 'Accept Assets', 'accept_assets_menu' => 'Accept Assets',
'accept_item' => 'Accept Item', 'accept_item' => 'Accept Item',
'audit' => 'Audit', 'audit' => 'Audit',
'audits' => 'Audits',
'audit_report' => 'Audit Log', 'audit_report' => 'Audit Log',
'assets' => 'Assets', 'assets' => 'Assets',
'assets_audited' => 'assets audited', 'assets_audited' => 'assets audited',

View file

@ -125,7 +125,7 @@
<thead> <thead>
<tr> <tr>
<th class="col-sm-2" data-visible="false" data-sortable="true" data-field="created_at" data-formatter="dateDisplayFormatter">{{ trans('general.record_created') }}</th> <th class="col-sm-2" data-visible="false" data-sortable="true" data-field="created_at" data-formatter="dateDisplayFormatter">{{ trans('general.record_created') }}</th>
<th class="col-sm-2"data-visible="true" data-sortable="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.admin') }}</th> <th class="col-sm-2"data-visible="true" data-sortable="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.created_by') }}</th>
<th class="col-sm-2" data-sortable="true" data-visible="true" data-field="action_type">{{ trans('general.action') }}</th> <th class="col-sm-2" data-sortable="true" data-visible="true" data-field="action_type">{{ trans('general.action') }}</th>
<th class="col-sm-2" data-field="file" data-visible="false" data-formatter="fileUploadNameFormatter">{{ trans('general.file_name') }}</th> <th class="col-sm-2" data-field="file" data-visible="false" data-formatter="fileUploadNameFormatter">{{ trans('general.file_name') }}</th>
<th class="col-sm-2" data-sortable="true" data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th> <th class="col-sm-2" data-sortable="true" data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th>

View file

@ -421,7 +421,7 @@
{{ trans('general.date') }} {{ trans('general.date') }}
</th> </th>
<th data-searchable="false" data-sortable="false" data-field="note">{{ trans('general.notes') }}</th> <th data-searchable="false" data-sortable="false" data-field="note">{{ trans('general.notes') }}</th>
<th data-searchable="false" data-sortable="false" data-field="admin">{{ trans('general.admin') }}</th> <th data-searchable="false" data-sortable="false" data-field="admin">{{ trans('general.created_by') }}</th>
</tr> </tr>
</thead> </thead>
</table> </table>
@ -472,7 +472,7 @@
<tr> <tr>
<th data-visible="true" data-field="icon" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter">{{ trans('admin/hardware/table.icon') }}</th> <th data-visible="true" data-field="icon" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter">{{ trans('admin/hardware/table.icon') }}</th>
<th data-visible="true" data-field="action_date" data-sortable="true" data-formatter="dateDisplayFormatter">{{ trans('general.date') }}</th> <th data-visible="true" data-field="action_date" data-sortable="true" data-formatter="dateDisplayFormatter">{{ trans('general.date') }}</th>
<th data-visible="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.admin') }}</th> <th data-visible="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.created_by') }}</th>
<th data-visible="true" data-field="action_type">{{ trans('general.action') }}</th> <th data-visible="true" data-field="action_type">{{ trans('general.action') }}</th>
<th class="col-sm-2" data-field="file" data-visible="false" data-formatter="fileUploadNameFormatter">{{ trans('general.file_name') }}</th> <th class="col-sm-2" data-field="file" data-visible="false" data-formatter="fileUploadNameFormatter">{{ trans('general.file_name') }}</th>
<th data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th> <th data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th>

View file

@ -224,6 +224,14 @@
</label> </label>
</div> </div>
<!-- Show in Audit Form -->
<div class="col-md-9 col-md-offset-3" id="display_audit" style="padding-bottom: 10px;">
<label class="form-control">
<input type="checkbox" name="display_audit" aria-label="display_audit" value="1" {{ (old('display_audit') || $field->display_audit) ? ' checked="checked"' : '' }}>
{{ trans('admin/custom_fields/general.display_audit') }}
</label>
</div>
<!-- Show in View All Assets profile view --> <!-- Show in View All Assets profile view -->
<div class="col-md-9 col-md-offset-3" id="display_in_user_view"> <div class="col-md-9 col-md-offset-3" id="display_in_user_view">

View file

@ -196,6 +196,14 @@
</span> </span>
</th> </th>
<th data-sortable="true" data-visible="false" data-searchable="false" class="text-center"
data-tooltip="{{ trans('admin/custom_fields/general.display_audit') }}">
<x-icon type="due" />
<span class="sr-only">
{{ trans('admin/custom_fields/general.display_audit') }}
</span>
</th>
<th data-sortable="true" data-searchable="true" class="text-center">{{ trans('admin/custom_fields/general.field_element_short') }}</th> <th data-sortable="true" data-searchable="true" class="text-center">{{ trans('admin/custom_fields/general.field_element_short') }}</th>
@ -227,6 +235,7 @@
<td class="text-center">{!! ($field->is_unique=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"><span class="sr-only">'.trans('general.yes').'</span></i>' : '<i class="fas fa-times text-danger" aria-hidden="true"><span class="sr-only">'.trans('general.no').'</span></i>' !!}</td> <td class="text-center">{!! ($field->is_unique=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"><span class="sr-only">'.trans('general.yes').'</span></i>' : '<i class="fas fa-times text-danger" aria-hidden="true"><span class="sr-only">'.trans('general.no').'</span></i>' !!}</td>
<td class="text-center">{!! ($field->display_checkin=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"><span class="sr-only">'.trans('general.yes').'</span></i>' : '<i class="fas fa-times text-danger" aria-hidden="true"><span class="sr-only">'.trans('general.no').'</span></i>' !!}</td> <td class="text-center">{!! ($field->display_checkin=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"><span class="sr-only">'.trans('general.yes').'</span></i>' : '<i class="fas fa-times text-danger" aria-hidden="true"><span class="sr-only">'.trans('general.no').'</span></i>' !!}</td>
<td class="text-center">{!! ($field->display_checkout=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"><span class="sr-only">'.trans('general.yes').'</span></i>' : '<i class="fas fa-times text-danger" aria-hidden="true"><span class="sr-only">'.trans('general.no').'</span></i>' !!}</td> <td class="text-center">{!! ($field->display_checkout=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"><span class="sr-only">'.trans('general.yes').'</span></i>' : '<i class="fas fa-times text-danger" aria-hidden="true"><span class="sr-only">'.trans('general.no').'</span></i>' !!}</td>
<td class="text-center">{!! ($field->display_audit=='1') ? '<i class="fas fa-check text-success" aria-hidden="true"><span class="sr-only">'.trans('general.yes').'</span></i>' : '<i class="fas fa-times text-danger" aria-hidden="true"><span class="sr-only">'.trans('general.no').'</span></i>' !!}</td>
<td>{{ $field->element }}</td> <td>{{ $field->element }}</td>
<td> <td>
@foreach($field->fieldset as $fieldset) @foreach($field->fieldset as $fieldset)

View file

@ -115,6 +115,12 @@
</div> </div>
</div> </div>
<!-- Custom fields -->
@include("models/custom_fields_form", [
'model' => $asset->model,
'show_display_checkout_fields' => 'true'
])
<!-- Note --> <!-- Note -->
<div class="form-group{{ $errors->has('note') ? ' has-error' : '' }}"> <div class="form-group{{ $errors->has('note') ? ' has-error' : '' }}">

View file

@ -142,7 +142,7 @@
var formData = $('#audit-form').serializeArray(); var formData = $('#audit-form').serializeArray();
$.ajax({ $.ajax({
url: "{{ route('api.asset.audit') }}", url: "{{ route('api.asset.audit.legacy') }}",
type : 'POST', type : 'POST',
headers: { headers: {
"X-Requested-With": 'XMLHttpRequest', "X-Requested-With": 'XMLHttpRequest',

View file

@ -111,6 +111,22 @@
@endif @endif
@if ($asset->audits->count() > 0)
<li>
<a href="#audits" data-toggle="tab" data-tooltip="true">
<span class="hidden-lg hidden-md">
<i class="fas fa-clipboard-check fa-2x"></i>
</span>
<span class="hidden-xs hidden-sm">
{{ trans('general.audits') }}
{!! ($asset->audits()->count() > 0 ) ? '<span class="badge badge-secondary">'.number_format($asset->audits()->count()).'</span>' : '' !!}
</span>
</a>
</li>
@endif
<li> <li>
<a href="#history" data-toggle="tab"> <a href="#history" data-toggle="tab">
<span class="hidden-lg hidden-md"> <span class="hidden-lg hidden-md">
@ -1390,6 +1406,52 @@
</div> <!-- /.row --> </div> <!-- /.row -->
</div> <!-- /.tab-pane maintenances --> </div> <!-- /.tab-pane maintenances -->
<div class="tab-pane fade" id="audits">
<!-- checked out assets table -->
<div class="row">
<div class="col-md-12">
<table
class="table table-striped snipe-table"
id="asseAuditHistory"
data-pagination="true"
data-id-table="asseAuditHistory"
data-search="true"
data-side-pagination="server"
data-show-columns="true"
data-show-fullscreen="true"
data-show-refresh="true"
data-sort-order="desc"
data-sort-name="created_at"
data-show-export="true"
data-export-options='{
"fileName": "export-asset-{{ $asset->id }}-audits",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'
data-url="{{ route('api.activity.index', ['item_id' => $asset->id, 'item_type' => 'asset', 'action_type' => 'audit']) }}"
data-cookie-id-table="assetHistory"
data-cookie="true">
<thead>
<tr>
<th data-visible="true" data-field="icon" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter">{{ trans('admin/hardware/table.icon') }}</th>
<th data-visible="true" data-field="created_at" data-sortable="true" data-formatter="dateDisplayFormatter">{{ trans('general.date') }}</th>
<th data-visible="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.created_by') }}</th>
<th class="col-sm-2" data-field="file" data-sortable="true" data-visible="false" data-formatter="fileUploadNameFormatter">{{ trans('general.file_name') }}</th>
<th data-field="note">{{ trans('general.notes') }}</th>
<th data-visible="false" data-field="file" data-visible="false" data-formatter="fileUploadFormatter">{{ trans('general.download') }}</th>
<th data-field="log_meta" data-visible="true" data-formatter="changeLogFormatter">{{ trans('admin/hardware/table.changed')}}</th>
<th data-field="remote_ip" data-visible="false" data-sortable="true">{{ trans('admin/settings/general.login_ip') }}</th>
<th data-field="user_agent" data-visible="false" data-sortable="true">{{ trans('admin/settings/general.login_user_agent') }}</th>
<th data-field="action_source" data-visible="false" data-sortable="true">{{ trans('general.action_source') }}</th>
</tr>
</thead>
</table>
</div>
</div> <!-- /.row -->
</div> <!-- /.tab-pane history -->
<div class="tab-pane fade" id="history"> <div class="tab-pane fade" id="history">
<!-- checked out assets table --> <!-- checked out assets table -->
<div class="row"> <div class="row">
@ -1419,7 +1481,7 @@
<tr> <tr>
<th data-visible="true" data-field="icon" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter">{{ trans('admin/hardware/table.icon') }}</th> <th data-visible="true" data-field="icon" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter">{{ trans('admin/hardware/table.icon') }}</th>
<th data-visible="true" data-field="action_date" data-sortable="true" data-formatter="dateDisplayFormatter">{{ trans('general.date') }}</th> <th data-visible="true" data-field="action_date" data-sortable="true" data-formatter="dateDisplayFormatter">{{ trans('general.date') }}</th>
<th data-visible="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.admin') }}</th> <th data-visible="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.created_by') }}</th>
<th data-visible="true" data-field="action_type">{{ trans('general.action') }}</th> <th data-visible="true" data-field="action_type">{{ trans('general.action') }}</th>
<th class="col-sm-2" data-field="file" data-visible="false" data-formatter="fileUploadNameFormatter">{{ trans('general.file_name') }}</th> <th class="col-sm-2" data-field="file" data-visible="false" data-formatter="fileUploadNameFormatter">{{ trans('general.file_name') }}</th>
<th data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th> <th data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th>

View file

@ -526,7 +526,7 @@ dir="{{ Helper::determineLanguageDirection() }}">
@can('audit', \App\Models\Asset::class) @can('audit', \App\Models\Asset::class)
<li{!! (Request::is('hardware/audit/due') ? ' class="active"' : '') !!}> <li{!! (Request::is('hardware/audit/due') ? ' class="active"' : '') !!}>
<a href="{{ route('assets.audit.due') }}"> <a href="{{ route('assets.audit.due') }}">
<x-icon type="due" class="text-yellow fa-fw"/> <x-icon type="audit" class="text-yellow fa-fw"/>
{{ trans('general.audit_due') }} {{ trans('general.audit_due') }}
<span class="badge">{{ (isset($total_due_and_overdue_for_audit)) ? $total_due_and_overdue_for_audit : '' }}</span> <span class="badge">{{ (isset($total_due_and_overdue_for_audit)) ? $total_due_and_overdue_for_audit : '' }}</span>
</a> </a>

View file

@ -498,7 +498,7 @@
<thead> <thead>
<tr> <tr>
<th class="col-sm-2" data-visible="false" data-sortable="true" data-field="created_at" data-formatter="dateDisplayFormatter">{{ trans('general.record_created') }}</th> <th class="col-sm-2" data-visible="false" data-sortable="true" data-field="created_at" data-formatter="dateDisplayFormatter">{{ trans('general.record_created') }}</th>
<th class="col-sm-2"data-visible="true" data-sortable="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.admin') }}</th> <th class="col-sm-2"data-visible="true" data-sortable="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.created_by') }}</th>
<th class="col-sm-2" data-sortable="true" data-visible="true" data-field="action_type">{{ trans('general.action') }}</th> <th class="col-sm-2" data-sortable="true" data-visible="true" data-field="action_type">{{ trans('general.action') }}</th>
<th class="col-sm-2" data-field="file" data-visible="false" data-formatter="fileUploadNameFormatter">{{ trans('general.file_name') }}</th> <th class="col-sm-2" data-field="file" data-visible="false" data-formatter="fileUploadNameFormatter">{{ trans('general.file_name') }}</th>
<th class="col-sm-2" data-sortable="true" data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th> <th class="col-sm-2" data-sortable="true" data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th>

View file

@ -434,7 +434,7 @@
<tr> <tr>
<th data-visible="true" data-field="icon" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter">{{ trans('admin/hardware/table.icon') }}</th> <th data-visible="true" data-field="icon" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter">{{ trans('admin/hardware/table.icon') }}</th>
<th class="col-sm-2" data-visible="true" data-field="action_date" data-formatter="dateDisplayFormatter">{{ trans('general.date') }}</th> <th class="col-sm-2" data-visible="true" data-field="action_date" data-formatter="dateDisplayFormatter">{{ trans('general.date') }}</th>
<th class="col-sm-1" data-visible="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.admin') }}</th> <th class="col-sm-1" data-visible="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.created_by') }}</th>
<th class="col-sm-1" data-visible="true" data-field="action_type">{{ trans('general.action') }}</th> <th class="col-sm-1" data-visible="true" data-field="action_type">{{ trans('general.action') }}</th>
<th class="col-sm-2" data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th> <th class="col-sm-2" data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th>
<th class="col-sm-2" data-visible="true" data-field="target" data-formatter="polymorphicItemFormatter">{{ trans('general.target') }}</th> <th class="col-sm-2" data-visible="true" data-field="target" data-formatter="polymorphicItemFormatter">{{ trans('general.target') }}</th>

View file

@ -101,10 +101,4 @@
@endif @endif
<script nonce="{{ csrf_token() }}">
// We have to re-call the tooltip since this is pulled in after the DOM has loaded
$('[data-tooltip="true"]').tooltip({
container: 'body',
animation: true,
});
</script>

View file

@ -341,7 +341,7 @@
function hardwareAuditFormatter(value, row) { function hardwareAuditFormatter(value, row) {
return '<a href="{{ config('app.url') }}/hardware/audit/' + row.id + '/" class="btn btn-sm bg-yellow" data-tooltip="true" title="Audit this item">{{ trans('general.audit') }}</a>'; return '<a href="{{ config('app.url') }}/hardware/' + row.id + '/audit" class="btn btn-sm bg-yellow" data-tooltip="true" title="Audit this item">{{ trans('general.audit') }}</a>';
} }

View file

@ -46,7 +46,7 @@
<th data-sortable="true" data-field="location" data-formatter="deployedLocationFormatter" data-visible="false">{{ trans('general.location') }}</th> <th data-sortable="true" data-field="location" data-formatter="deployedLocationFormatter" data-visible="false">{{ trans('general.location') }}</th>
<th data-sortable="true" data-field="rtd_location" data-formatter="deployedLocationFormatter" data-visible="false">{{ trans('admin/hardware/form.default_location') }}</th> <th data-sortable="true" data-field="rtd_location" data-formatter="deployedLocationFormatter" data-visible="false">{{ trans('admin/hardware/form.default_location') }}</th>
<th data-searchable="true" data-sortable="true" data-field="is_warranty" data-formatter="trueFalseFormatter">{{ trans('admin/asset_maintenances/table.is_warranty') }}</th> <th data-searchable="true" data-sortable="true" data-field="is_warranty" data-formatter="trueFalseFormatter">{{ trans('admin/asset_maintenances/table.is_warranty') }}</th>
<th data-searchable="true" data-sortable="true" data-field="user_id" data-formatter="usersLinkObjFormatter">{{ trans('general.admin') }}</th> <th data-searchable="true" data-sortable="true" data-field="user_id" data-formatter="usersLinkObjFormatter">{{ trans('general.created_by') }}</th>
<th data-searchable="true" data-sortable="true" data-field="notes" data-visible="false">{{ trans('admin/asset_maintenances/form.notes') }}</th> <th data-searchable="true" data-sortable="true" data-field="notes" data-visible="false">{{ trans('admin/asset_maintenances/form.notes') }}</th>
</tr> </tr>
</thead> </thead>

View file

@ -1003,7 +1003,7 @@
<th data-field="signature_file" data-visible="false" data-formatter="imageFormatter">{{ trans('general.signature') }}</th> <th data-field="signature_file" data-visible="false" data-formatter="imageFormatter">{{ trans('general.signature') }}</th>
@endif @endif
<th data-field="item.serial" data-visible="false">{{ trans('admin/hardware/table.serial') }}</th> <th data-field="item.serial" data-visible="false">{{ trans('admin/hardware/table.serial') }}</th>
<th data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.admin') }}</th> <th data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.created_by') }}</th>
<th data-field="remote_ip" data-visible="false" data-sortable="true">{{ trans('admin/settings/general.login_ip') }}</th> <th data-field="remote_ip" data-visible="false" data-sortable="true">{{ trans('admin/settings/general.login_ip') }}</th>
<th data-field="user_agent" data-visible="false" data-sortable="true">{{ trans('admin/settings/general.login_user_agent') }}</th> <th data-field="user_agent" data-visible="false" data-sortable="true">{{ trans('admin/settings/general.login_user_agent') }}</th>
<th data-field="action_source" data-visible="false" data-sortable="true">{{ trans('general.action_source') }}</th> <th data-field="action_source" data-visible="false" data-sortable="true">{{ trans('general.action_source') }}</th>

View file

@ -511,12 +511,21 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
->where(['action' => 'audit|audits|checkins', 'upcoming_status' => 'due|overdue|due-or-overdue']); ->where(['action' => 'audit|audits|checkins', 'upcoming_status' => 'due|overdue|due-or-overdue']);
// Legacy URL for audit
Route::post('audit', Route::post('audit',
[ [
Api\AssetsController::class, Api\AssetsController::class,
'audit' 'audit'
] ]
)->name('api.asset.audit.legacy');
// Newer url for audit
Route::post('{asset}/audit',
[
Api\AssetsController::class,
'audit'
]
)->name('api.asset.audit'); )->name('api.asset.audit');
Route::post('{id}/checkin', Route::post('{id}/checkin',

View file

@ -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')) ->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') ->name('asset.audit.create')
->breadcrumbs(fn (Trail $trail, Asset $asset) => ->breadcrumbs(fn (Trail $trail, Asset $asset) =>
$trail->parent('hardware.show', $asset) $trail->parent('hardware.show', $asset)
->push(trans('general.audit')) ->push(trans('general.audit'))
); );
Route::post('audit/{asset}', Route::post('{asset}/audit',
[AssetsController::class, 'auditStore'] [AssetsController::class, 'auditStore']
)->name('asset.audit.store'); )->name('asset.audit.store');

View file

@ -0,0 +1,78 @@
<?php
namespace Tests\Feature\Assets\Api;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Company;
use App\Models\Location;
use App\Models\Statuslabel;
use App\Models\Supplier;
use App\Models\User;
use App\Models\CustomField;
use Illuminate\Support\Facades\Crypt;
use Tests\TestCase;
class AuditAssetTest extends TestCase
{
public function testThatANonExistentAssetIdReturnsError()
{
$this->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);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Tests\Feature\Assets\Ui;
use App\Models\Asset;
use App\Models\User;
use Tests\TestCase;
class AuditAssetTest extends TestCase
{
public function testPermissionRequiredToCreateAssetModel()
{
$this->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');
}
}