Merge pull request #14655 from snipe/feature/sc-25381/simpler_overdue_endpoints

Refactored due/overdue for audit, added due/overdue for checkin API endpoint and GUI
This commit is contained in:
snipe 2024-05-02 13:11:51 +01:00 committed by GitHub
commit 4850227c04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 561 additions and 213 deletions

View file

@ -9,7 +9,6 @@ use App\Notifications\ExpectedCheckinAdminNotification;
use App\Notifications\ExpectedCheckinNotification;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class SendExpectedCheckinAlerts extends Command
{
@ -43,25 +42,31 @@ class SendExpectedCheckinAlerts extends Command
public function handle()
{
$settings = Setting::getSettings();
$whenNotify = Carbon::now();
$assets = Asset::with('assignedTo')->whereNotNull('assigned_to')->whereNotNull('expected_checkin')->where('expected_checkin', '<=', $whenNotify)->get();
$interval = $settings->audit_warning_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval);
$assets = Asset::whereNull('deleted_at')->DueOrOverdueForCheckin($settings)->orderBy('assets.expected_checkin', 'desc')->get();
$this->info($assets->count().' assets must be checked in on or before '.$interval_date.' is deadline');
$this->info($whenNotify.' is deadline');
$this->info($assets->count().' assets');
foreach ($assets as $asset) {
if ($asset->assigned && $asset->checkedOutToUser()) {
Log::info('Sending ExpectedCheckinNotification to ' . $asset->assigned->email);
$asset->assigned->notify((new ExpectedCheckinNotification($asset)));
if ($asset->assignedTo && (isset($asset->assignedTo->email)) && ($asset->assignedTo->email!='') && $asset->checkedOutToUser()) {
$this->info('Sending User ExpectedCheckinNotification to: '.$asset->assignedTo->email);
$asset->assignedTo->notify((new ExpectedCheckinNotification($asset)));
}
}
if (($assets) && ($assets->count() > 0) && ($settings->alert_email != '')) {
// Send a rollup to the admin, if settings dictate
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item, $key) {
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item) {
return new AlertRecipient($item);
});
$this->info('Sending Admin ExpectedCheckinNotification to: '.$settings->alert_email);
\Notification::send($recipients, new ExpectedCheckinAdminNotification($assets));
}
}
}

View file

@ -3,10 +3,8 @@
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\License;
use App\Models\Recipients;
use App\Models\Recipients\AlertRecipient;
use App\Models\Setting;
use App\Notifications\ExpiringAssetsNotification;
use App\Notifications\SendUpcomingAuditNotification;
use Carbon\Carbon;
use DB;
@ -45,40 +43,26 @@ class SendUpcomingAuditReport extends Command
*/
public function handle()
{
$interval = $settings->audit_warning_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval);
$settings = Setting::getSettings();
$assets = Asset::whereNull('deleted_at')->DueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'desc')->get();
$this->info($assets->count().' assets must be audited in on or before '.$interval_date.' is deadline');
if (($settings->alert_email != '') && ($settings->audit_warning_days) && ($settings->alerts_enabled == 1)) {
if (($assets) && ($assets->count() > 0) && ($settings->alert_email != '')) {
// Send a rollup to the admin, if settings dictate
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item, $key) {
return new \App\Models\Recipients\AlertRecipient($item);
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item) {
return new AlertRecipient($item);
});
// Assets due for auditing
$this->info('Sending Admin SendUpcomingAuditNotification to: '.$settings->alert_email);
\Notification::send($recipients, new SendUpcomingAuditNotification($assets, $settings->audit_warning_days));
$assets = Asset::whereNotNull('next_audit_date')
->DueOrOverdueForAudit($settings)
->orderBy('last_audit_date', 'asc')->get();
if ($assets->count() > 0) {
$this->info(trans_choice('mail.upcoming-audits', $assets->count(),
['count' => $assets->count(), 'threshold' => $settings->audit_warning_days]));
\Notification::send($recipients, new SendUpcomingAuditNotification($assets, $settings->audit_warning_days));
$this->info('Audit report sent to '.$settings->alert_email);
} else {
$this->info('No assets to be audited. No report sent.');
}
} elseif ($settings->alert_email == '') {
$this->error('Could not send email. No alert email configured in settings');
} elseif (! $settings->audit_warning_days) {
$this->error('No audit warning days set in Admin Notifications. No mail will be sent.');
} elseif ($settings->alerts_enabled != 1) {
$this->info('Alerts are disabled in the settings. No mail will be sent');
} else {
$this->error('Something went wrong. :( ');
$this->error('Admin Notifications Email Setting: '.$settings->alert_email);
$this->error('Admin Audit Warning Setting: '.$settings->audit_warning_days);
$this->error('Admin Alerts Emnabled: '.$settings->alerts_enabled);
}
}
}

View file

@ -59,7 +59,7 @@ class AssetsController extends Controller
* @since [v4.0]
* @return \Illuminate\Http\JsonResponse
*/
public function index(Request $request, $audit = null)
public function index(Request $request, $action = null, $upcoming_status = null)
{
$filter_non_deprecable_assets = false;
@ -155,17 +155,44 @@ class AssetsController extends Controller
$assets->TextSearch($request->input('search'));
}
// This is used by the audit reporting routes
if (Gate::allows('audit', Asset::class)) {
switch ($audit) {
case 'due':
$assets->DueOrOverdueForAudit($settings);
break;
case 'overdue':
$assets->overdueForAudit($settings);
break;
/**
* Handle due and overdue audits and checkin dates
*/
switch ($action) {
case 'audits':
switch ($upcoming_status) {
case 'due':
$assets->DueForAudit($settings);
break;
case 'overdue':
$assets->OverdueForAudit();
break;
case 'due-or-overdue':
$assets->DueOrOverdueForAudit($settings);
break;
}
break;
case 'checkins':
switch ($upcoming_status) {
case 'due':
$assets->DueForCheckin($settings);
break;
case 'overdue':
$assets->OverdueForCheckin();
break;
case 'due-or-overdue':
$assets->DueOrOverdueForCheckin($settings);
break;
}
break;
}
}
/**
* End handling due and overdue audits and checkin dates
*/
// This is used by the sidenav, mostly

View file

@ -854,11 +854,11 @@ class AssetsController extends Controller
return view('hardware/audit-due');
}
public function overdueForAudit()
public function dueForCheckin()
{
$this->authorize('audit', Asset::class);
$this->authorize('checkin', Asset::class);
return view('hardware/audit-overdue');
return view('hardware/checkin-due');
}

View file

@ -5,6 +5,7 @@ namespace App\Http\Middleware;
use App\Models\Asset;
use Auth;
use Closure;
use App\Models\Setting;
class AssetCountForSidebar
{
@ -24,6 +25,13 @@ class AssetCountForSidebar
\Log::debug($e);
}
try {
$total_assets = Asset::RTD()->count();
view()->share('total_assets', $total_assets);
} catch (\Exception $e) {
\Log::debug($e);
}
try {
$total_deployed_sidebar = Asset::Deployed()->count();
view()->share('total_deployed_sidebar', $total_deployed_sidebar);
@ -59,6 +67,44 @@ class AssetCountForSidebar
\Log::debug($e);
}
try {
$settings = Setting::getSettings();
view()->share('settings', $settings);
} catch (\Exception $e) {
\Log::debug($e);
}
try {
$total_due_for_audit = Asset::DueForAudit($settings)->count();
view()->share('total_due_for_audit', $total_due_for_audit);
} catch (\Exception $e) {
\Log::debug($e);
}
try {
$total_overdue_for_audit = Asset::OverdueForAudit()->count();
view()->share('total_overdue_for_audit', $total_overdue_for_audit);
} catch (\Exception $e) {
\Log::debug($e);
}
try {
$total_due_for_checkin = Asset::DueForCheckin($settings)->count();
view()->share('total_due_for_checkin', $total_due_for_checkin);
} catch (\Exception $e) {
\Log::debug($e);
}
try {
$total_overdue_for_checkin = Asset::OverdueForCheckin()->count();
view()->share('total_overdue_for_checkin', $total_overdue_for_checkin);
} catch (\Exception $e) {
\Log::debug($e);
}
view()->share('total_due_and_overdue_for_checkin', ($total_due_for_checkin + $total_overdue_for_checkin));
view()->share('total_due_and_overdue_for_audit', ($total_due_for_audit + $total_overdue_for_audit));
return $next($request);
}
}

View file

@ -74,9 +74,9 @@ class Asset extends Depreciable
'eol_explicit' => 'boolean',
'last_checkout' => 'datetime',
'last_checkin' => 'datetime',
'expected_checkin' => 'date',
'expected_checkin' => 'datetime:m-d-Y',
'last_audit_date' => 'datetime',
'next_audit_date' => 'date',
'next_audit_date' => 'datetime:m-d-Y',
'model_id' => 'integer',
'status_id' => 'integer',
'company_id' => 'integer',
@ -1163,10 +1163,11 @@ class Asset extends Depreciable
public function scopeDueForAudit($query, $settings)
{
$interval = $settings->audit_warning_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval)->format('Y-m-d');
return $query->whereNotNull('assets.next_audit_date')
->where('assets.next_audit_date', '>=', Carbon::now())
->whereRaw("DATE_SUB(assets.next_audit_date, INTERVAL $interval DAY) <= '".Carbon::now()."'")
->whereBetween('assets.next_audit_date', [$today->format('Y-m-d'), $interval_date])
->where('assets.archived', '=', 0)
->NotArchived();
}
@ -1188,7 +1189,7 @@ class Asset extends Depreciable
public function scopeOverdueForAudit($query)
{
return $query->whereNotNull('assets.next_audit_date')
->where('assets.next_audit_date', '<', Carbon::now())
->where('assets.next_audit_date', '<', Carbon::now()->format('Y-m-d'))
->where('assets.archived', '=', 0)
->NotArchived();
}
@ -1209,14 +1210,69 @@ class Asset extends Depreciable
public function scopeDueOrOverdueForAudit($query, $settings)
{
$interval = $settings->audit_warning_days ?? 0;
return $query->whereNotNull('assets.next_audit_date')
->whereRaw('DATE_SUB('.DB::getTablePrefix()."assets.next_audit_date, INTERVAL $interval DAY) <= '".Carbon::now()."'")
return $query->where(function ($query) {
$query->OverdueForAudit();
})->orWhere(function ($query) use ($settings) {
$query->DueForAudit($settings);
});
}
/**
* Query builder scope for Assets that are DUE for checkin, based on the assets.expected_checkin
* and settings.audit_warning_days. It checks to see if assets.expected_checkin is now
*
* @author A. Gianotto <snipe@snipe.net>
* @since v6.4.0
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeDueForCheckin($query, $settings)
{
$interval = $settings->audit_warning_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval)->format('Y-m-d');
return $query->whereNotNull('assets.expected_checkin')
->whereBetween('assets.expected_checkin', [$today->format('Y-m-d'), $interval_date])
->where('assets.archived', '=', 0)
->whereNotNull('assets.assigned_to')
->NotArchived();
}
/**
* Query builder scope for Assets that are overdue for checkin OR overdue
*
* @author A. Gianotto <snipe@snipe.net>
* @since v6.4.0
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOverdueForCheckin($query)
{
return $query->whereNotNull('assets.expected_checkin')
->where('assets.expected_checkin', '<', Carbon::now()->format('Y-m-d'))
->where('assets.archived', '=', 0)
->whereNotNull('assets.assigned_to')
->NotArchived();
}
/**
* Query builder scope for Assets that are due for checkin OR overdue
*
* @author A. Gianotto <snipe@snipe.net>
* @since v6.4.0
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeDueOrOverdueForCheckin($query, $settings)
{
return $query->where(function ($query) {
$query->OverdueForCheckin();
})->orWhere(function ($query) use ($settings) {
$query->DueForCheckin($settings);
});
}
/**
* Query builder scope for Archived assets counting

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,8 +1,8 @@
{
"/js/build/app.js": "/js/build/app.js?id=ea5f3edebafdb29b616d23fa89106080",
"/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=f677207c6cf9678eb539abecb408c374",
"/css/build/overrides.css": "/css/build/overrides.css?id=3f1125ebfd37987dd0d6e2bf18bb41ce",
"/css/build/app.css": "/css/build/app.css?id=ca42da2140f0b5be6c2d0fa84ae90108",
"/css/build/overrides.css": "/css/build/overrides.css?id=be3c0a217bc6c0e0744f75faed784887",
"/css/build/app.css": "/css/build/app.css?id=a168b0a799aa800ee926bffa1b1a434a",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=dc383f8560a8d4adb51d44fb4043e03b",
"/css/dist/skins/skin-orange.css": "/css/dist/skins/skin-orange.css?id=6f0563e726c2fe4fab4026daaa5bfdf2",
"/css/dist/skins/skin-orange-dark.css": "/css/dist/skins/skin-orange-dark.css?id=620b684d9dd9d3bb5fdda00a3a2467c3",
@ -18,7 +18,7 @@
"/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=0a82a6ae6bb4e58fe62d162c4fb50397",
"/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=da6c7997d9de2f8329142399f0ce50da",
"/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=44bf834f2110504a793dadec132a5898",
"/css/dist/all.css": "/css/dist/all.css?id=0836d3b034a9b1de17d797ff35f1983e",
"/css/dist/all.css": "/css/dist/all.css?id=628956bb260b760b56216f01d1d71fd8",
"/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/webfonts/fa-brands-400.ttf": "/css/webfonts/fa-brands-400.ttf?id=69e5d8e4e818f05fd882cceb758d1eba",

View file

@ -131,7 +131,7 @@
//Hover and active states
&:hover > a, &.active > a {
color: @sidebar-dark-hover-color;
background: @link-hover-border-color;
background: @sidebar-dark-hover-bg;
border-left-color: @link-hover-border-color;
}
//First Level Submenu

View file

@ -900,4 +900,10 @@ input[type="radio"]:checked::before {
}
.datepicker.dropdown-menu {
z-index: 1030 !important;
}
}
.sidebar-menu > li .badge {
margin-top: 0px;
filter: brightness(70%);
font-size: 70%;
}

View file

@ -313,6 +313,10 @@ return [
'token_expired' => 'Your form session has expired. Please try again.',
'login_enabled' => 'Login Enabled',
'audit_due' => 'Due for Audit',
'audit_due_days' => 'Assets Due for Audit Within :days Day|Assets Due for Audit Within :days Days',
'checkin_due' => 'Due for Checkin',
'checkin_overdue' => 'Overdue for Checkin',
'checkin_due_days' => 'Assets Due for Checkin Within :days Day|Assets Due for Checkin Within :days Days',
'audit_overdue' => 'Overdue for Audit',
'accept' => 'Accept :asset',
'i_accept' => 'I accept',

View file

@ -1,40 +1,59 @@
@extends('layouts/default')
@section('title0')
@if ((Request::get('company_id')) && ($company))
{{ $company->name }}
@endif
{{ trans('general.audit_due') }}
@stop
{{-- Page title --}}
@section('title')
@yield('title0') @parent
{{ trans_choice('general.audit_due_days', $settings->audit_warning_days, ['days' => $settings->audit_warning_days]) }}
@stop
{{-- Page content --}}
@section('content')
{{-- Page content --}}
<div class="row">
<div class="col-md-12">
<div class="box">
<div class="box-body">
@include('partials.asset-bulk-actions')
<!-- Custom Tabs -->
<div class="nav-tabs-custom">
<ul class="nav nav-tabs hidden-print">
<li class="active">
<a href="#due" data-toggle="tab">{{ trans('general.audit_due') }}
<span class="hidden-lg hidden-md">
<i class="far fa-file fa-2x" aria-hidden="true"></i>
</span>
<span class="badge">{{ (isset($total_due_for_audit)) ? $total_due_for_audit : '' }}</span>
</a>
</li>
<li>
<a href="#overdue" data-toggle="tab">{{ trans('general.audit_overdue') }}
<span class="hidden-lg hidden-md">
<i class="far fa-file fa-2x" aria-hidden="true"></i>
</span>
<span class="badge">{{ (isset($total_overdue_for_audit)) ? $total_overdue_for_audit : '' }}</span>
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="due">
@include('partials.asset-bulk-actions',
[
'id_divname' => 'dueAssetEditToolbar',
'id_formname' => 'dueAssetEditForm',
'id_button' => 'dueAssetEditButton'])
<div class="row">
<div class="table table-responsive">
<div class="col-md-12">
<table
data-click-to-select="true"
data-columns="{{ \App\Presenters\AssetAuditPresenter::dataTableLayout() }}"
data-cookie-id-table="assetsAuditListingTable"
data-cookie-id-table="dueAssetAuditListing"
data-pagination="true"
data-id-table="assetsAuditListingTable"
data-id-table="dueAssetAuditListing"
data-search="true"
data-side-pagination="server"
data-show-columns="true"
@ -44,28 +63,71 @@
data-show-refresh="true"
data-sort-order="asc"
data-sort-name="name"
data-toolbar="#assetsBulkEditToolbar"
data-bulk-button-id="#bulkAssetEditButton"
data-bulk-form-id="#assetsBulkForm"
id="assetsAuditListingTable"
data-toolbar="#dueAssetEditToolbar"
data-bulk-button-id="#dueAssetEditButton"
data-bulk-form-id="#dueAssetEditForm"
id="#dueAssetAuditListing"
class="table table-striped snipe-table"
data-url="{{ route('api.asset.to-audit', ['audit' => 'due']) }}"
data-url="{{ route('api.assets.list-upcoming', ['action' => 'audits', 'upcoming_status' => 'due']) }}"
data-export-options='{
"fileName": "export-assets-due-audit-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
</table>
</div> <!-- end col-md-12 -->
</div><!-- end table-responsive -->
</div><!-- end row -->
</div><!-- end tab-pane -->
<div class="tab-pane" id="overdue">
@include('partials.asset-bulk-actions',
[
'id_divname' => 'overdueAssetEditToolbar',
'id_formname' => 'overdueAssetEditForm',
'id_button' => 'overdueAssetEditButton'])
<div class="row">
<div class="table table-responsive">
<div class="col-md-12">
<table
data-click-to-select="true"
data-columns="{{ \App\Presenters\AssetAuditPresenter::dataTableLayout() }}"
data-cookie-id-table="overdueAssetAuditListing"
data-pagination="true"
data-id-table="overdueAssetAuditListing"
data-search="true"
data-side-pagination="server"
data-show-columns="true"
data-show-fullscreen="true"
data-show-export="true"
data-show-footer="true"
data-show-refresh="true"
data-sort-order="asc"
data-sort-name="name"
data-toolbar="#overdueAssetEditToolbar"
data-bulk-button-id="#overdueAssetEditButton"
data-bulk-form-id="#overdueAssetEditForm"
id="#overdueAssetAuditListing"
class="table table-striped snipe-table"
data-url="{{ route('api.assets.list-upcoming', ['action' => 'audits', 'upcoming_status' => 'overdue']) }}"
data-export-options='{
"fileName": "export-assets-overdue-audit-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
</table>
</div> <!-- end col-md-12 -->
</div><!-- end table-responsive -->
</div><!-- end row -->
</div><!-- end tab-pane -->
</div><!-- end tab-content -->
</div><!-- end nav-tabs-custom -->
</div><!-- /.col -->
</div><!-- /.row -->
{{ Form::close() }}
</div><!-- ./box-body -->
</div><!-- /.box -->
</div>
</div>
@stop
@section('moar_scripts')
@include('partials.bootstrap-table')
@stop

View file

@ -1,69 +0,0 @@
@extends('layouts/default')
@section('title0')
@if ((Request::get('company_id')) && ($company))
{{ $company->name }}
@endif
{{ trans('general.audit_overdue') }}
@stop
{{-- Page title --}}
@section('title')
@yield('title0') @parent
@stop
{{-- Page content --}}
@section('content')
<div class="row">
<div class="col-md-12">
<div class="box">
<div class="box-body">
@include('partials.asset-bulk-actions')
<div class="row">
<div class="col-md-12">
<table
data-click-to-select="true"
data-columns="{{ \App\Presenters\AssetAuditPresenter::dataTableLayout() }}"
data-cookie-id-table="assetsOverdueAuditListingTable"
data-pagination="true"
data-id-table="assetsOverdueAuditListingTable"
data-search="true"
data-side-pagination="server"
data-show-columns="true"
data-show-fullscreen="true"
data-show-export="true"
data-show-footer="true"
data-show-refresh="true"
data-sort-order="asc"
data-sort-name="name"
data-toolbar="#assetsBulkEditToolbar"
data-bulk-button-id="#bulkAssetEditButton"
data-bulk-form-id="#assetsBulkForm"
id="assetsAuditListingTable"
class="table table-striped snipe-table"
data-url="{{ route('api.asset.to-audit', ['audit' => 'overdue']) }}"
data-export-options='{
"fileName": "export-assets-due-audit-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
</table>
</div><!-- /.col -->
</div><!-- /.row -->
{{ Form::close() }}
</div><!-- ./box-body -->
</div><!-- /.box -->
</div>
</div>
@stop
@section('moar_scripts')
@include('partials.bootstrap-table')
@stop

View file

@ -0,0 +1,131 @@
@extends('layouts/default')
{{-- Page title --}}
@section('title')
{{ trans_choice('general.checkin_due_days', $settings->audit_warning_days, ['days' => $settings->audit_warning_days]) }}
@stop
{{-- Page content --}}
@section('content')
{{-- Page content --}}
<div class="row">
<div class="col-md-12">
<!-- Custom Tabs -->
<div class="nav-tabs-custom">
<ul class="nav nav-tabs hidden-print">
<li class="active">
<a href="#due" data-toggle="tab">{{ trans('general.checkin_due') }}
<span class="hidden-lg hidden-md">
<i class="far fa-file fa-2x" aria-hidden="true"></i>
</span>
<span class="badge">{{ (isset($total_due_for_checkin)) ? $total_due_for_checkin : '' }}</span>
</a>
</li>
<li>
<a href="#overdue" data-toggle="tab">{{ trans('general.checkin_overdue') }}
<span class="hidden-lg hidden-md">
<i class="far fa-file fa-2x" aria-hidden="true"></i>
</span>
<span class="badge">{{ (isset($total_overdue_for_checkin)) ? $total_overdue_for_checkin : '' }}</span>
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="due">
@include('partials.asset-bulk-actions',
[
'id_divname' => 'dueAssetEditToolbar',
'id_formname' => 'dueAssetEditForm',
'id_button' => 'dueAssetEditButton'])
<div class="row">
<div class="table table-responsive">
<div class="col-md-12">
<table
data-click-to-select="true"
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-cookie-id-table="dueAssetcheckinListing"
data-pagination="true"
data-id-table="dueAssetcheckinListing"
data-search="true"
data-side-pagination="server"
data-show-columns="true"
data-show-fullscreen="true"
data-show-export="true"
data-show-footer="true"
data-show-refresh="true"
data-sort-order="asc"
data-sort-name="name"
data-toolbar="#dueAssetEditToolbar"
data-bulk-button-id="#dueAssetEditButton"
data-bulk-form-id="#dueAssetEditForm"
id="#dueAssetcheckinListing"
class="table table-striped snipe-table"
data-url="{{ route('api.assets.list-upcoming', ['action' => 'checkins', 'upcoming_status' => 'due']) }}"
data-export-options='{
"fileName": "export-assets-due-checkin-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
</table>
</div> <!-- end col-md-12 -->
</div><!-- end table-responsive -->
</div><!-- end row -->
</div><!-- end tab-pane -->
<div class="tab-pane" id="overdue">
@include('partials.asset-bulk-actions',
[
'id_divname' => 'overdueAssetEditToolbar',
'id_formname' => 'overdueAssetEditForm',
'id_button' => 'overdueAssetEditButton'])
<div class="row">
<div class="table table-responsive">
<div class="col-md-12">
<table
data-click-to-select="true"
data-columns="{{ \App\Presenters\AssetPresenter::dataTableLayout() }}"
data-cookie-id-table="overdueAssetcheckinListing"
data-pagination="true"
data-id-table="overdueAssetcheckinListing"
data-search="true"
data-side-pagination="server"
data-show-columns="true"
data-show-fullscreen="true"
data-show-export="true"
data-show-footer="true"
data-show-refresh="true"
data-sort-order="asc"
data-sort-name="name"
data-toolbar="#overdueAssetEditToolbar"
data-bulk-button-id="#overdueAssetEditButton"
data-bulk-form-id="#overdueAssetEditForm"
id="#overdueAssetcheckinListing"
class="table table-striped snipe-table"
data-url="{{ route('api.assets.list-upcoming', ['action' => 'checkins', 'upcoming_status' => 'overdue']) }}"
data-export-options='{
"fileName": "export-assets-overdue-checkin-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
</table>
</div> <!-- end col-md-12 -->
</div><!-- end table-responsive -->
</div><!-- end row -->
</div><!-- end tab-pane -->
</div><!-- end tab-content -->
</div><!-- end nav-tabs-custom -->
</div><!-- /.col -->
</div><!-- /.row -->
@stop
@section('moar_scripts')
@include('partials.bootstrap-table')
@stop

View file

@ -51,7 +51,7 @@
<i class="far fa-save fa-2x" aria-hidden="true"></i>
</span>
<span class="hidden-xs hidden-sm">{{ trans('general.licenses') }}
{!! ($asset->licenses->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($asset->licenses->count()).'</badge>' : '' !!}
{!! ($asset->licenses->count() > 0 ) ? '<span class="badge badge-secondary">'.number_format($asset->licenses->count()).'</span>' : '' !!}
</span>
</a>
</li>
@ -62,7 +62,7 @@
<i class="far fa-hdd fa-2x" aria-hidden="true"></i>
</span>
<span class="hidden-xs hidden-sm">{{ trans('general.components') }}
{!! ($asset->components->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($asset->components->count()).'</badge>' : '' !!}
{!! ($asset->components->count() > 0 ) ? '<span class="badge badge-secondary">'.number_format($asset->components->count()).'</span>' : '' !!}
</span>
</a>
</li>
@ -73,7 +73,7 @@
<i class="fas fa-barcode fa-2x" aria-hidden="true"></i>
</span>
<span class="hidden-xs hidden-sm">{{ trans('general.assets') }}
{!! ($asset->assignedAssets()->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($asset->assignedAssets()->count()).'</badge>' : '' !!}
{!! ($asset->assignedAssets()->count() > 0 ) ? '<span class="badge badge-secondary">'.number_format($asset->assignedAssets()->count()).'</span>' : '' !!}
</span>
</a>
@ -96,7 +96,7 @@
<i class="fas fa-wrench fa-2x" aria-hidden="true"></i>
</span>
<span class="hidden-xs hidden-sm">{{ trans('general.maintenances') }}
{!! ($asset->assetmaintenances()->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($asset->assetmaintenances()->count()).'</badge>' : '' !!}
{!! ($asset->assetmaintenances()->count() > 0 ) ? '<span class="badge badge-secondary">'.number_format($asset->assetmaintenances()->count()).'</span>' : '' !!}
</span>
</a>
</li>
@ -107,7 +107,7 @@
<i class="far fa-file fa-2x" aria-hidden="true"></i>
</span>
<span class="hidden-xs hidden-sm">{{ trans('general.files') }}
{!! ($asset->uploads->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($asset->uploads->count()).'</badge>' : '' !!}
{!! ($asset->uploads->count() > 0 ) ? '<span class="badge badge-secondary">'.number_format($asset->uploads->count()).'</span>' : '' !!}
</span>
</a>
</li>
@ -119,7 +119,7 @@
</span>
<span class="hidden-xs hidden-sm">
{{ trans('general.additional_files') }}
{!! ($asset->model) && ($asset->model->uploads->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($asset->model->uploads->count()).'</badge>' : '' !!}
{!! ($asset->model) && ($asset->model->uploads->count() > 0 ) ? '<span class="badge badge-secondary">'.number_format($asset->model->uploads->count()).'</span>' : '' !!}
</span>
</a>
</li>

View file

@ -437,6 +437,9 @@
<a href="{{ url('hardware') }}">
<i class="far fa-circle text-grey fa-fw" aria-hidden="true"></i>
{{ trans('general.list_all') }}
<span class="badge">
{{ (isset($total_assets)) ? $total_assets : '' }}
</span>
</a>
</li>
@ -447,7 +450,8 @@
<a href="{{ route('statuslabels.show', ['statuslabel' => $status_nav->id]) }}">
<i class="fas fa-circle text-grey fa-fw"
aria-hidden="true"{!! ($status_nav->color!='' ? ' style="color: '.e($status_nav->color).'"' : '') !!}></i>
{{ $status_nav->name }} ({{ $status_nav->asset_count }})</a></li>
{{ $status_nav->name }}
<span class="badge badge-secondary">{{ $status_nav->asset_count }})</span></a></li>
@endforeach
@endif
@ -455,49 +459,43 @@
<li{!! (Request::query('status') == 'Deployed' ? ' class="active"' : '') !!}>
<a href="{{ url('hardware?status=Deployed') }}">
<i class="far fa-circle text-blue fa-fw"></i>
{{ trans('general.all') }}
{{ trans('general.deployed') }}
({{ (isset($total_deployed_sidebar)) ? $total_deployed_sidebar : '' }})
<span class="badge">{{ (isset($total_deployed_sidebar)) ? $total_deployed_sidebar : '' }}</span>
</a>
</li>
<li{!! (Request::query('status') == 'RTD' ? ' class="active"' : '') !!}>
<a href="{{ url('hardware?status=RTD') }}">
<i class="far fa-circle text-green fa-fw"></i>
{{ trans('general.all') }}
{{ trans('general.ready_to_deploy') }}
({{ (isset($total_rtd_sidebar)) ? $total_rtd_sidebar : '' }})
<span class="badge">{{ (isset($total_rtd_sidebar)) ? $total_rtd_sidebar : '' }}</span>
</a>
</li>
<li{!! (Request::query('status') == 'Pending' ? ' class="active"' : '') !!}><a
href="{{ url('hardware?status=Pending') }}"><i
class="far fa-circle text-orange fa-fw"></i>
{{ trans('general.all') }}
{{ trans('general.pending') }}
({{ (isset($total_pending_sidebar)) ? $total_pending_sidebar : '' }})
<span class="badge">{{ (isset($total_pending_sidebar)) ? $total_pending_sidebar : '' }}</span>
</a>
</li>
<li{!! (Request::query('status') == 'Undeployable' ? ' class="active"' : '') !!} ><a
href="{{ url('hardware?status=Undeployable') }}"><i
class="fas fa-times text-red fa-fw"></i>
{{ trans('general.all') }}
{{ trans('general.undeployable') }}
({{ (isset($total_undeployable_sidebar)) ? $total_undeployable_sidebar : '' }})
<span class="badge">{{ (isset($total_undeployable_sidebar)) ? $total_undeployable_sidebar : '' }}</span>
</a>
</li>
<li{!! (Request::query('status') == 'byod' ? ' class="active"' : '') !!}><a
href="{{ url('hardware?status=byod') }}"><i
class="fas fa-times text-red fa-fw"></i>
{{ trans('general.all') }}
{{ trans('general.byod') }}
({{ (isset($total_byod_sidebar)) ? $total_byod_sidebar : '' }})
<span class="badge">{{ (isset($total_byod_sidebar)) ? $total_byod_sidebar : '' }}</span>
</a>
</li>
<li{!! (Request::query('status') == 'Archived' ? ' class="active"' : '') !!}><a
href="{{ url('hardware?status=Archived') }}"><i
class="fas fa-times text-red fa-fw"></i>
{{ trans('general.all') }}
{{ trans('admin/hardware/general.archived') }}
({{ (isset($total_archived_sidebar)) ? $total_archived_sidebar : '' }})
<span class="badge">{{ (isset($total_archived_sidebar)) ? $total_archived_sidebar : '' }}</span>
</a>
</li>
<li{!! (Request::query('status') == 'Requestable' ? ' class="active"' : '') !!}><a
@ -511,13 +509,18 @@
<li{!! (Request::is('hardware/audit/due') ? ' class="active"' : '') !!}>
<a href="{{ route('assets.audit.due') }}">
<i class="fas fa-history text-yellow fa-fw"></i> {{ trans('general.audit_due') }}
<span class="badge">{{ (isset($total_due_and_overdue_for_audit)) ? $total_due_and_overdue_for_audit : '' }}</span>
</a>
</li>
<li{!! (Request::is('hardware/audit/overdue') ? ' class="active"' : '') !!}>
<a href="{{ route('assets.audit.overdue') }}">
<i class="fas fa-exclamation-triangle text-red fa-fw"></i> {{ trans('general.audit_overdue') }}
</a>
</li>
@endcan
@can('checkin', \App\Models\Asset::class)
<li{!! (Request::is('hardware/checkins/due') ? ' class="active"' : '') !!}>
<a href="{{ route('assets.checkins.due') }}">
<i class="fas fa-history text-yellow fa-fw"></i> {{ trans('general.checkin_due') }}
<span class="badge">{{ (isset($total_due_and_overdue_for_checkin)) ? $total_due_and_overdue_for_checkin : '' }}</span>
</a>
</li>
@endcan
<li class="divider">&nbsp;</li>

View file

@ -10,7 +10,7 @@
@php
$checkin = Helper::getFormattedDateObject($asset->expected_checkin, 'date');
@endphp
| [{{ $asset->present()->name }}]({{ route('hardware.show', ['hardware' => $asset->id]) }}) | [{{ $asset->assigned->present()->fullName }}]({{ route($asset->targetShowRoute().'.show', [$asset->assigned->id]) }}) | {{ $checkin['formatted'] }}
| [{{ $asset->present()->name }}]({{ route('hardware.show', ['hardware' => $asset->id]) }}) | [{{ $asset->assignedTo->present()->fullName }}]({{ route($asset->targetShowRoute().'.show', [$asset->assignedTo->id]) }}) | {{ $checkin['formatted'] }}
@endforeach
@endcomponent

View file

@ -496,13 +496,27 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
)->name('api.assets.show.byserial')
->where('any', '.*');
Route::get('audit/{audit}',
// LEGACY URL - Get assets that are due or overdue for audit
Route::get('audit/{status}',
[
Api\AssetsController::class,
'index'
]
)->name('api.asset.to-audit');
// This gets the "due or overdue" API endpoints for audits and checkins
Route::get('{action}/{upcoming_status}',
[
Api\AssetsController::class,
'index'
]
)->name('api.assets.list-upcoming')
->where(['action' => 'audits|checkins', 'upcoming_status' => 'due|overdue|due-or-overdue']);
Route::post('audit',
[
Api\AssetsController::class,

View file

@ -50,26 +50,10 @@ Route::group(
[AssetsController::class, 'dueForAudit']
)->name('assets.audit.due');
Route::get('audit/overdue',
[AssetsController::class, 'overdueForAudit']
)->name('assets.audit.overdue');
Route::get('audit/due',
[AssetsController::class, 'dueForAudit']
)->name('assets.audit.due');
Route::get('audit/overdue',
[AssetsController::class, 'overdueForAudit']
)->name('assets.audit.overdue');
Route::get('audit/due',
[AssetsController::class, 'dueForAudit']
)->name('assets.audit.due');
Route::get('audit/overdue',
[AssetsController::class, 'overdueForAudit']
)->name('assets.audit.overdue');
Route::get('checkins/due',
[AssetsController::class, 'dueForCheckin']
)->name('assets.checkins.due');
Route::get('audit/{id}',
[AssetsController::class, 'audit']
)->name('asset.audit.create');

View file

@ -7,10 +7,10 @@ use App\Models\Company;
use App\Models\User;
use Illuminate\Testing\Fluent\AssertableJson;
use Tests\TestCase;
use Carbon\Carbon;
class AssetIndexTest extends TestCase
{
public function testAssetIndexReturnsExpectedAssets()
public function testAssetApiIndexReturnsExpectedAssets()
{
Asset::factory()->count(3)->create();
@ -30,7 +30,102 @@ class AssetIndexTest extends TestCase
->assertJson(fn(AssertableJson $json) => $json->has('rows', 3)->etc());
}
public function testAssetIndexAdheresToCompanyScoping()
public function testAssetApiIndexReturnsDisplayUpcomingAuditsDue()
{
Asset::factory()->count(3)->create(['next_audit_date' => Carbon::now()->format('Y-m-d')]);
$this->actingAsForApi(User::factory()->superuser()->create())
->getJson(
route('api.assets.list-upcoming', ['action' => 'audits', 'upcoming_status' => 'due']))
->assertOk()
->assertJsonStructure([
'total',
'rows',
])
->assertJson(fn(AssertableJson $json) => $json->has('rows', 3)->etc());
}
public function testAssetApiIndexReturnsOverdueForAudit()
{
Asset::factory()->count(3)->create(['next_audit_date' => Carbon::now()->subDays(1)->format('Y-m-d')]);
$this->actingAsForApi(User::factory()->superuser()->create())
->getJson(
route('api.assets.list-upcoming', ['action' => 'audits', 'upcoming_status' => 'overdue']))
->assertOk()
->assertJsonStructure([
'total',
'rows',
])
->assertJson(fn(AssertableJson $json) => $json->has('rows', 3)->etc());
}
public function testAssetApiIndexReturnsDueOrOverdueForAudit()
{
Asset::factory()->count(3)->create(['next_audit_date' => Carbon::now()->format('Y-m-d')]);
Asset::factory()->count(2)->create(['next_audit_date' => Carbon::now()->subDays(1)->format('Y-m-d')]);
$this->actingAsForApi(User::factory()->superuser()->create())
->getJson(
route('api.assets.list-upcoming', ['action' => 'audits', 'upcoming_status' => 'due-or-overdue']))
->assertOk()
->assertJsonStructure([
'total',
'rows',
])
->assertJson(fn(AssertableJson $json) => $json->has('rows', 5)->etc());
}
public function testAssetApiIndexReturnsDueForExpectedCheckin()
{
Asset::factory()->count(3)->create(['assigned_to' => '1', 'expected_checkin' => Carbon::now()->format('Y-m-d')]);
$this->actingAsForApi(User::factory()->superuser()->create())
->getJson(
route('api.assets.list-upcoming', ['action' => 'checkins', 'upcoming_status' => 'due'])
)
->assertOk()
->assertJsonStructure([
'total',
'rows',
])
->assertJson(fn(AssertableJson $json) => $json->has('rows', 3)->etc());
}
public function testAssetApiIndexReturnsOverdueForExpectedCheckin()
{
Asset::factory()->count(3)->create(['assigned_to' => '1', 'expected_checkin' => Carbon::now()->subDays(1)->format('Y-m-d')]);
$this->actingAsForApi(User::factory()->superuser()->create())
->getJson(route('api.assets.list-upcoming', ['action' => 'checkins', 'upcoming_status' => 'overdue']))
->assertOk()
->assertJsonStructure([
'total',
'rows',
])
->assertJson(fn(AssertableJson $json) => $json->has('rows', 3)->etc());
}
public function testAssetApiIndexReturnsDueOrOverdueForExpectedCheckin()
{
Asset::factory()->count(3)->create(['assigned_to' => '1', 'expected_checkin' => Carbon::now()->subDays(1)->format('Y-m-d')]);
Asset::factory()->count(2)->create(['assigned_to' => '1', 'expected_checkin' => Carbon::now()->format('Y-m-d')]);
$this->actingAsForApi(User::factory()->superuser()->create())
->getJson(route('api.assets.list-upcoming', ['action' => 'checkins', 'upcoming_status' => 'due-or-overdue']))
->assertOk()
->assertJsonStructure([
'total',
'rows',
])
->assertJson(fn(AssertableJson $json) => $json->has('rows', 5)->etc());
}
public function testAssetApiIndexAdheresToCompanyScoping()
{
[$companyA, $companyB] = Company::factory()->count(2)->create();