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 2024-05-02 14:02:36 +01:00
commit 1e1782c232
30 changed files with 860 additions and 223 deletions

View file

@ -9,7 +9,6 @@ use App\Notifications\ExpectedCheckinAdminNotification;
use App\Notifications\ExpectedCheckinNotification; use App\Notifications\ExpectedCheckinNotification;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class SendExpectedCheckinAlerts extends Command class SendExpectedCheckinAlerts extends Command
{ {
@ -43,25 +42,31 @@ class SendExpectedCheckinAlerts extends Command
public function handle() public function handle()
{ {
$settings = Setting::getSettings(); $settings = Setting::getSettings();
$whenNotify = Carbon::now(); $interval = $settings->audit_warning_days ?? 0;
$assets = Asset::with('assignedTo')->whereNotNull('assigned_to')->whereNotNull('expected_checkin')->where('expected_checkin', '<=', $whenNotify)->get(); $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) { foreach ($assets as $asset) {
if ($asset->assigned && $asset->checkedOutToUser()) { if ($asset->assignedTo && (isset($asset->assignedTo->email)) && ($asset->assignedTo->email!='') && $asset->checkedOutToUser()) {
Log::info('Sending ExpectedCheckinNotification to ' . $asset->assigned->email); $this->info('Sending User ExpectedCheckinNotification to: '.$asset->assignedTo->email);
$asset->assigned->notify((new ExpectedCheckinNotification($asset))); $asset->assignedTo->notify((new ExpectedCheckinNotification($asset)));
} }
} }
if (($assets) && ($assets->count() > 0) && ($settings->alert_email != '')) { if (($assets) && ($assets->count() > 0) && ($settings->alert_email != '')) {
// Send a rollup to the admin, if settings dictate // 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); return new AlertRecipient($item);
}); });
$this->info('Sending Admin ExpectedCheckinNotification to: '.$settings->alert_email);
\Notification::send($recipients, new ExpectedCheckinAdminNotification($assets)); \Notification::send($recipients, new ExpectedCheckinAdminNotification($assets));
} }
} }
} }

View file

@ -3,10 +3,8 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Asset; use App\Models\Asset;
use App\Models\License; use App\Models\Recipients\AlertRecipient;
use App\Models\Recipients;
use App\Models\Setting; use App\Models\Setting;
use App\Notifications\ExpiringAssetsNotification;
use App\Notifications\SendUpcomingAuditNotification; use App\Notifications\SendUpcomingAuditNotification;
use Carbon\Carbon; use Carbon\Carbon;
use DB; use DB;
@ -45,40 +43,26 @@ class SendUpcomingAuditReport extends Command
*/ */
public function handle() public function handle()
{ {
$interval = $settings->audit_warning_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval);
$settings = Setting::getSettings(); $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 // 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 \App\Models\Recipients\AlertRecipient($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] * @since [v4.0]
* @return \Illuminate\Http\JsonResponse * @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; $filter_non_deprecable_assets = false;
@ -155,17 +155,44 @@ class AssetsController extends Controller
$assets->TextSearch($request->input('search')); $assets->TextSearch($request->input('search'));
} }
// This is used by the audit reporting routes
if (Gate::allows('audit', Asset::class)) { /**
switch ($audit) { * Handle due and overdue audits and checkin dates
case 'due': */
$assets->DueOrOverdueForAudit($settings); switch ($action) {
break; case 'audits':
case 'overdue':
$assets->overdueForAudit($settings); switch ($upcoming_status) {
break; 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 // This is used by the sidenav, mostly

View file

@ -854,11 +854,11 @@ class AssetsController extends Controller
return view('hardware/audit-due'); 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

@ -14,6 +14,7 @@ use App\Models\Setting;
use App\Models\Supplier; use App\Models\Supplier;
use App\Models\User; use App\Models\User;
use App\View\Label as LabelView; use App\View\Label as LabelView;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
class LabelsController extends Controller class LabelsController extends Controller
@ -21,9 +22,9 @@ class LabelsController extends Controller
/** /**
* Returns the Label view with test data * Returns the Label view with test data
* *
* @author Grant Le Roux <grant.leroux+snipe-it@gmail.com> * @param string $labelName
* @param string $labelName
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Contracts\View\View
* @author Grant Le Roux <grant.leroux+snipe-it@gmail.com>
*/ */
public function show(string $labelName) public function show(string $labelName)
{ {
@ -66,7 +67,7 @@ class LabelsController extends Controller
$exampleAsset->model->category->id = 999999; $exampleAsset->model->category->id = 999999;
$exampleAsset->model->category->name = trans('admin/labels/table.example_category'); $exampleAsset->model->category->name = trans('admin/labels/table.example_category');
$customFieldColumns = CustomField::all()->pluck('db_column'); $customFieldColumns = CustomField::where('field_encrypted', '=', 0)->pluck('db_column');
collect(explode(';', Setting::getSettings()->label2_fields)) collect(explode(';', Setting::getSettings()->label2_fields))
->filter() ->filter()

View file

@ -11,6 +11,7 @@ use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\StreamedResponse;
/** /**
* This controller handles all actions related to Licenses for * This controller handles all actions related to Licenses for
@ -289,4 +290,106 @@ class LicensesController extends Controller
->with('item', $license) ->with('item', $license)
->with('maintained_list', $maintained_list); ->with('maintained_list', $maintained_list);
} }
/**
* Exports Licenses to CSV
*
* @author [G. Martinez]
* @since [v6.3]
* @return StreamedResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function getExportLicensesCsv()
{
$this->authorize('view', License::class);
\Debugbar::disable();
$response = new StreamedResponse(function () {
// Open output stream
$handle = fopen('php://output', 'w');
$licenses= License::with('company',
'manufacturer',
'category',
'supplier',
'adminuser',
'assignedusers')
->orderBy('created_at', 'DESC');
Company::scopeCompanyables($licenses)
->chunk(500, function ($licenses) use ($handle) {
$headers = [
// strtolower to prevent Excel from trying to open it as a SYLK file
strtolower(trans('general.id')),
trans('general.company'),
trans('general.name'),
trans('general.serial_number'),
trans('general.purchase_date'),
trans('general.purchase_cost'),
trans('general.order_number'),
trans('general.licenses_available'),
trans('admin/licenses/table.seats'),
trans('general.created_by'),
trans('general.depreciation'),
trans('general.updated_at'),
trans('admin/licenses/table.deleted_at'),
trans('general.email'),
trans('admin/hardware/form.fully_depreciated'),
trans('general.supplier'),
trans('admin/licenses/form.expiration'),
trans('admin/licenses/form.purchase_order'),
trans('admin/licenses/form.termination_date'),
trans('admin/licenses/form.maintained'),
trans('general.manufacturer'),
trans('general.category'),
trans('general.min_amt'),
trans('admin/licenses/form.reassignable'),
trans('general.notes'),
trans('general.created_at'),
];
fputcsv($handle, $headers);
foreach ($licenses as $license) {
// Add a new row with data
$values = [
$license->id,
$license->company ? $license->company->name: '',
$license->name,
$license->serial,
$license->purchase_date,
$license->purchase_cost,
$license->order_number,
$license->free_seat_count,
$license->seats,
$license->adminuser->present()->fullName(),
$license->depreciation ? $license->depreciation->name: '',
$license->updated_at,
$license->deleted_at,
$license->email,
( $license->depreciate == '1') ? trans('general.yes') : trans('general.no'),
($license->supplier) ? $license->supplier->name: '',
$license->expiration_date,
$license->purchase_order,
$license->termination_date,
( $license->maintained == '1') ? trans('general.yes') : trans('general.no'),
$license->manufacturer ? $license->manufacturer->name: '',
$license->category ? $license->category->name: '',
$license->min_amt,
( $license->reassignable == '1') ? trans('general.yes') : trans('general.no'),
$license->notes,
$license->created_at,
];
fputcsv($handle, $values);
}
});
// Close the output stream
fclose($handle);
}, 200, [
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="licenses-'.date('Y-m-d-his').'.csv"',
]);
return $response;
}
} }

View file

@ -804,10 +804,9 @@ class SettingsController extends Controller
*/ */
public function getLabels() public function getLabels()
{ {
return view('settings.labels', [ return view('settings.labels')
'setting' => Setting::getSettings(), ->with('setting', Setting::getSettings())
'customFields' => CustomField::all(), ->with('customFields', CustomField::where('field_encrypted', '=', 0)->get());
]);
} }
/** /**

View file

@ -5,6 +5,7 @@ namespace App\Http\Middleware;
use App\Models\Asset; use App\Models\Asset;
use Auth; use Auth;
use Closure; use Closure;
use App\Models\Setting;
class AssetCountForSidebar class AssetCountForSidebar
{ {
@ -24,6 +25,13 @@ class AssetCountForSidebar
\Log::debug($e); \Log::debug($e);
} }
try {
$total_assets = Asset::RTD()->count();
view()->share('total_assets', $total_assets);
} catch (\Exception $e) {
\Log::debug($e);
}
try { try {
$total_deployed_sidebar = Asset::Deployed()->count(); $total_deployed_sidebar = Asset::Deployed()->count();
view()->share('total_deployed_sidebar', $total_deployed_sidebar); view()->share('total_deployed_sidebar', $total_deployed_sidebar);
@ -59,6 +67,44 @@ class AssetCountForSidebar
\Log::debug($e); \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); return $next($request);
} }
} }

View file

@ -4,6 +4,7 @@ namespace App\Http\Requests;
use App\Models\Asset; use App\Models\Asset;
use App\Models\Company; use App\Models\Company;
use App\Models\Setting;
use Carbon\Carbon; use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException; use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
@ -45,12 +46,21 @@ class StoreAssetRequest extends ImageUploadRequest
*/ */
public function rules(): array public function rules(): array
{ {
$rules = array_merge( $modelRules = (new Asset)->getRules();
(new Asset)->getRules(),
if (Setting::getSettings()->digit_separator === '1.234,56' && is_string($this->input('purchase_cost'))) {
// If purchase_cost was submitted as a string with a comma separator
// then we need to ignore the normal numeric rules.
// Since the original rules still live on the model they will be run
// right before saving (and after purchase_cost has been
// converted to a float via setPurchaseCostAttribute).
$modelRules = $this->removeNumericRulesFromPurchaseCost($modelRules);
}
return array_merge(
$modelRules,
parent::rules(), parent::rules(),
); );
return $rules;
} }
private function parseLastAuditDate(): void private function parseLastAuditDate(): void
@ -69,4 +79,20 @@ class StoreAssetRequest extends ImageUploadRequest
} }
} }
} }
private function removeNumericRulesFromPurchaseCost(array $rules): array
{
$purchaseCost = $rules['purchase_cost'];
// If rule is in "|" format then turn it into an array
if (is_string($purchaseCost)) {
$purchaseCost = explode('|', $purchaseCost);
}
$rules['purchase_cost'] = array_filter($purchaseCost, function ($rule) {
return $rule !== 'numeric' && $rule !== 'gte:0';
});
return $rules;
}
} }

View file

@ -74,9 +74,9 @@ class Asset extends Depreciable
'eol_explicit' => 'boolean', 'eol_explicit' => 'boolean',
'last_checkout' => 'datetime', 'last_checkout' => 'datetime',
'last_checkin' => 'datetime', 'last_checkin' => 'datetime',
'expected_checkin' => 'date', 'expected_checkin' => 'datetime:m-d-Y',
'last_audit_date' => 'datetime', 'last_audit_date' => 'datetime',
'next_audit_date' => 'date', 'next_audit_date' => 'datetime:m-d-Y',
'model_id' => 'integer', 'model_id' => 'integer',
'status_id' => 'integer', 'status_id' => 'integer',
'company_id' => 'integer', 'company_id' => 'integer',
@ -1163,10 +1163,11 @@ class Asset extends Depreciable
public function scopeDueForAudit($query, $settings) public function scopeDueForAudit($query, $settings)
{ {
$interval = $settings->audit_warning_days ?? 0; $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') return $query->whereNotNull('assets.next_audit_date')
->where('assets.next_audit_date', '>=', Carbon::now()) ->whereBetween('assets.next_audit_date', [$today->format('Y-m-d'), $interval_date])
->whereRaw("DATE_SUB(assets.next_audit_date, INTERVAL $interval DAY) <= '".Carbon::now()."'")
->where('assets.archived', '=', 0) ->where('assets.archived', '=', 0)
->NotArchived(); ->NotArchived();
} }
@ -1188,7 +1189,7 @@ class Asset extends Depreciable
public function scopeOverdueForAudit($query) public function scopeOverdueForAudit($query)
{ {
return $query->whereNotNull('assets.next_audit_date') 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) ->where('assets.archived', '=', 0)
->NotArchived(); ->NotArchived();
} }
@ -1209,14 +1210,69 @@ class Asset extends Depreciable
public function scopeDueOrOverdueForAudit($query, $settings) public function scopeDueOrOverdueForAudit($query, $settings)
{ {
$interval = $settings->audit_warning_days ?? 0;
return $query->whereNotNull('assets.next_audit_date') return $query->where(function ($query) {
->whereRaw('DATE_SUB('.DB::getTablePrefix()."assets.next_audit_date, INTERVAL $interval DAY) <= '".Carbon::now()."'") $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) ->where('assets.archived', '=', 0)
->whereNotNull('assets.assigned_to')
->NotArchived(); ->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 * Query builder scope for Archived assets counting

View file

@ -0,0 +1,19 @@
<?php
namespace App\Models\Labels\Tapes\Brother;
use App\Helpers\Helper;
use App\Models\Labels\Label;
abstract class TZe_18mm extends Label
{
private const HEIGHT = 18.00;
private const MARGIN_SIDES = 3.20;
private const MARGIN_ENDS = 3.20;
public function getHeight() { return Helper::convertUnit(self::HEIGHT, 'mm', $this->getUnit()); }
public function getMarginTop() { return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit()); }
public function getMarginBottom() { return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit());}
public function getMarginLeft() { return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit()); }
public function getMarginRight() { return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit()); }
}

View file

@ -0,0 +1,56 @@
<?php
namespace App\Models\Labels\Tapes\Brother;
class TZe_18mm_A extends TZe_18mm
{
private const BARCODE_SIZE = 3.20;
private const BARCODE_MARGIN = 0.30;
private const TEXT_SIZE_MOD = 1.00;
public function getUnit() { return 'mm'; }
public function getWidth() { return 50.0; }
public function getSupportAssetTag() { return true; }
public function getSupport1DBarcode() { return true; }
public function getSupport2DBarcode() { return false; }
public function getSupportFields() { return 1; }
public function getSupportLogo() { return false; }
public function getSupportTitle() { return false; }
public function preparePDF($pdf) {}
public function write($pdf, $record) {
$pa = $this->getPrintableArea();
if ($record->has('barcode1d')) {
static::write1DBarcode(
$pdf, $record->get('barcode1d')->content, $record->get('barcode1d')->type,
$pa->x1, $pa->y1, $pa->w, self::BARCODE_SIZE
);
}
$currentY = $pa->y1 + self::BARCODE_SIZE + self::BARCODE_MARGIN;
$usableHeight = $pa->h - self::BARCODE_SIZE - self::BARCODE_MARGIN;
$fontSize = $usableHeight + self::TEXT_SIZE_MOD;
$tagWidth = $pa->w / 3;
$fieldWidth = $pa->w / 3 * 2;
static::writeText(
$pdf, $record->get('tag'),
$pa->x1, $currentY,
'freemono', 'b', $fontSize, 'L',
$tagWidth, $usableHeight, true, 0, 0
);
if ($record->get('fields')->count() >= 1) {
static::writeText(
$pdf, $record->get('fields')->values()->get(0)['value'],
$pa->x1 + ($tagWidth), $currentY,
'freemono', 'b', $fontSize, 'R',
$fieldWidth, $usableHeight, true, 0, 0
);
}
}
}

View file

@ -81,6 +81,7 @@ class License extends Depreciable
'serial', 'serial',
'supplier_id', 'supplier_id',
'termination_date', 'termination_date',
'free_seat_count',
'user_id', 'user_id',
'min_amt', 'min_amt',
]; ];
@ -114,6 +115,7 @@ class License extends Depreciable
'category' => ['name'], 'category' => ['name'],
'depreciation' => ['name'], 'depreciation' => ['name'],
]; ];
protected $appends = ['free_seat_count'];
/** /**
* Update seat counts when the license is updated * Update seat counts when the license is updated
@ -280,6 +282,16 @@ class License extends Depreciable
} }
$this->attributes['termination_date'] = $value; $this->attributes['termination_date'] = $value;
} }
/**
* Sets free_seat_count attribute
*
* @author G. Martinez
* @since [v6.3]
* @return mixed
*/
public function getFreeSeatCountAttribute(){
return $this->attributes['free_seat_count'] = $this->remaincount();
}
/** /**
* Establishes the license -> company relationship * Establishes the license -> company relationship
@ -502,7 +514,13 @@ class License extends Depreciable
->whereNull('deleted_at') ->whereNull('deleted_at')
->count(); ->count();
} }
/**
* Returns the available seats remaining
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v2.0]
* @return int
*/
/** /**
* Returns the number of total available seats for this license * Returns the number of total available seats for this license
@ -579,7 +597,7 @@ class License extends Depreciable
$taken = $this->assigned_seats_count; $taken = $this->assigned_seats_count;
$diff = ($total - $taken); $diff = ($total - $taken);
return $diff; return (int) $diff;
} }
/** /**

View file

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

View file

@ -4,6 +4,7 @@ return array(
'assigned_to' => 'Assigned To', 'assigned_to' => 'Assigned To',
'checkout' => 'In/Out', 'checkout' => 'In/Out',
'deleted_at' => 'Deleted at',
'id' => 'ID', 'id' => 'ID',
'license_email' => 'License Email', 'license_email' => 'License Email',
'license_name' => 'Licensed To', 'license_name' => 'Licensed To',

View file

@ -176,7 +176,7 @@ return [
'last_name' => 'Last Name', 'last_name' => 'Last Name',
'license' => 'License', 'license' => 'License',
'license_report' => 'License Report', 'license_report' => 'License Report',
'licenses_available' => 'licenses available', 'licenses_available' => 'Licenses available',
'licenses' => 'Licenses', 'licenses' => 'Licenses',
'list_all' => 'List All', 'list_all' => 'List All',
'loading' => 'Loading... please wait....', 'loading' => 'Loading... please wait....',
@ -313,6 +313,10 @@ return [
'token_expired' => 'Your form session has expired. Please try again.', 'token_expired' => 'Your form session has expired. Please try again.',
'login_enabled' => 'Login Enabled', 'login_enabled' => 'Login Enabled',
'audit_due' => 'Due for Audit', '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', 'audit_overdue' => 'Overdue for Audit',
'accept' => 'Accept :asset', 'accept' => 'Accept :asset',
'i_accept' => 'I accept', 'i_accept' => 'I accept',

View file

@ -1,40 +1,59 @@
@extends('layouts/default') @extends('layouts/default')
@section('title0')
@if ((Request::get('company_id')) && ($company))
{{ $company->name }}
@endif
{{ trans('general.audit_due') }}
@stop
{{-- Page title --}} {{-- Page title --}}
@section('title') @section('title')
@yield('title0') @parent {{ trans_choice('general.audit_due_days', $settings->audit_warning_days, ['days' => $settings->audit_warning_days]) }}
@stop @stop
{{-- Page content --}} {{-- Page content --}}
@section('content') @section('content')
{{-- Page content --}}
<div class="row"> <div class="row">
<div class="col-md-12"> <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="row">
<div class="table table-responsive">
<div class="col-md-12"> <div class="col-md-12">
<table <table
data-click-to-select="true" data-click-to-select="true"
data-columns="{{ \App\Presenters\AssetAuditPresenter::dataTableLayout() }}" data-columns="{{ \App\Presenters\AssetAuditPresenter::dataTableLayout() }}"
data-cookie-id-table="assetsAuditListingTable" data-cookie-id-table="dueAssetAuditListing"
data-pagination="true" data-pagination="true"
data-id-table="assetsAuditListingTable" data-id-table="dueAssetAuditListing"
data-search="true" data-search="true"
data-side-pagination="server" data-side-pagination="server"
data-show-columns="true" data-show-columns="true"
@ -44,28 +63,71 @@
data-show-refresh="true" data-show-refresh="true"
data-sort-order="asc" data-sort-order="asc"
data-sort-name="name" data-sort-name="name"
data-toolbar="#assetsBulkEditToolbar" data-toolbar="#dueAssetEditToolbar"
data-bulk-button-id="#bulkAssetEditButton" data-bulk-button-id="#dueAssetEditButton"
data-bulk-form-id="#assetsBulkForm" data-bulk-form-id="#dueAssetEditForm"
id="assetsAuditListingTable" id="#dueAssetAuditListing"
class="table table-striped snipe-table" 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='{ data-export-options='{
"fileName": "export-assets-due-audit-{{ date('Y-m-d') }}", "fileName": "export-assets-due-audit-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"] "ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'> }'>
</table> </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><!-- /.col -->
</div><!-- /.row --> </div><!-- /.row -->
{{ Form::close() }}
</div><!-- ./box-body -->
</div><!-- /.box -->
</div>
</div>
@stop @stop
@section('moar_scripts') @section('moar_scripts')
@include('partials.bootstrap-table') @include('partials.bootstrap-table')
@stop @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> <i class="far fa-save fa-2x" aria-hidden="true"></i>
</span> </span>
<span class="hidden-xs hidden-sm">{{ trans('general.licenses') }} <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> </span>
</a> </a>
</li> </li>
@ -62,7 +62,7 @@
<i class="far fa-hdd fa-2x" aria-hidden="true"></i> <i class="far fa-hdd fa-2x" aria-hidden="true"></i>
</span> </span>
<span class="hidden-xs hidden-sm">{{ trans('general.components') }} <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> </span>
</a> </a>
</li> </li>
@ -73,7 +73,7 @@
<i class="fas fa-barcode fa-2x" aria-hidden="true"></i> <i class="fas fa-barcode fa-2x" aria-hidden="true"></i>
</span> </span>
<span class="hidden-xs hidden-sm">{{ trans('general.assets') }} <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> </span>
</a> </a>
@ -96,7 +96,7 @@
<i class="fas fa-wrench fa-2x" aria-hidden="true"></i> <i class="fas fa-wrench fa-2x" aria-hidden="true"></i>
</span> </span>
<span class="hidden-xs hidden-sm">{{ trans('general.maintenances') }} <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> </span>
</a> </a>
</li> </li>
@ -107,7 +107,7 @@
<i class="far fa-file fa-2x" aria-hidden="true"></i> <i class="far fa-file fa-2x" aria-hidden="true"></i>
</span> </span>
<span class="hidden-xs hidden-sm">{{ trans('general.files') }} <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> </span>
</a> </a>
</li> </li>
@ -119,7 +119,7 @@
</span> </span>
<span class="hidden-xs hidden-sm"> <span class="hidden-xs hidden-sm">
{{ trans('general.additional_files') }} {{ 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> </span>
</a> </a>
</li> </li>

View file

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

View file

@ -13,6 +13,9 @@
{{ trans('general.create') }} {{ trans('general.create') }}
</a> </a>
@endcan @endcan
@can('view', \App\Models\License::class)
<a class="btn btn-default pull-right" href="{{ route('licenses.export') }}" style="margin-right: 5px;">{{ trans('general.export') }}</a>
@endcan
@stop @stop
{{-- Page content --}} {{-- Page content --}}

View file

@ -10,7 +10,7 @@
@php @php
$checkin = Helper::getFormattedDateObject($asset->expected_checkin, 'date'); $checkin = Helper::getFormattedDateObject($asset->expected_checkin, 'date');
@endphp @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 @endforeach
@endcomponent @endcomponent

View file

@ -351,6 +351,7 @@
</optgroup> </optgroup>
<optgroup label="Custom Fields"> <optgroup label="Custom Fields">
@foreach($customFields as $customField) @foreach($customFields as $customField)
<option value="{{ $customField->db_column }}">{{ $customField->name }}</option> <option value="{{ $customField->db_column }}">{{ $customField->name }}</option>
@endforeach @endforeach
</optgroup> </optgroup>

View file

@ -496,13 +496,27 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
)->name('api.assets.show.byserial') )->name('api.assets.show.byserial')
->where('any', '.*'); ->where('any', '.*');
Route::get('audit/{audit}', // LEGACY URL - Get assets that are due or overdue for audit
Route::get('audit/{status}',
[ [
Api\AssetsController::class, Api\AssetsController::class,
'index' 'index'
] ]
)->name('api.asset.to-audit'); )->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', Route::post('audit',
[ [
Api\AssetsController::class, Api\AssetsController::class,

View file

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

View file

@ -48,6 +48,13 @@ Route::group(['prefix' => 'licenses', 'middleware' => ['auth']], function () {
'{licenseId}/showfile/{fileId}/{download?}', '{licenseId}/showfile/{fileId}/{download?}',
[Licenses\LicenseFilesController::class, 'show'] [Licenses\LicenseFilesController::class, 'show']
)->name('show.licensefile'); )->name('show.licensefile');
Route::get(
'export',
[
Licenses\LicensesController::class,
'getExportLicensesCsv'
]
)->name('licenses.export');
}); });
Route::resource('licenses', Licenses\LicensesController::class, [ Route::resource('licenses', Licenses\LicensesController::class, [

View file

@ -7,10 +7,10 @@ use App\Models\Company;
use App\Models\User; use App\Models\User;
use Illuminate\Testing\Fluent\AssertableJson; use Illuminate\Testing\Fluent\AssertableJson;
use Tests\TestCase; use Tests\TestCase;
use Carbon\Carbon;
class AssetIndexTest extends TestCase class AssetIndexTest extends TestCase
{ {
public function testAssetIndexReturnsExpectedAssets() public function testAssetApiIndexReturnsExpectedAssets()
{ {
Asset::factory()->count(3)->create(); Asset::factory()->count(3)->create();
@ -30,7 +30,102 @@ class AssetIndexTest extends TestCase
->assertJson(fn(AssertableJson $json) => $json->has('rows', 3)->etc()); ->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(); [$companyA, $companyB] = Company::factory()->count(2)->create();

View file

@ -2,6 +2,7 @@
namespace Tests\Feature\Api\Assets; namespace Tests\Feature\Api\Assets;
use App\Helpers\Helper;
use App\Models\Asset; use App\Models\Asset;
use App\Models\AssetModel; use App\Models\AssetModel;
use App\Models\Company; use App\Models\Company;
@ -12,6 +13,7 @@ use App\Models\Supplier;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use Illuminate\Testing\Fluent\AssertableJson; use Illuminate\Testing\Fluent\AssertableJson;
use Illuminate\Testing\TestResponse;
use Tests\TestCase; use Tests\TestCase;
class AssetStoreTest extends TestCase class AssetStoreTest extends TestCase
@ -278,6 +280,50 @@ class AssetStoreTest extends TestCase
->assertStatusMessageIs('error'); ->assertStatusMessageIs('error');
} }
public function testStoresPeriodAsDecimalSeparatorForPurchaseCost()
{
$this->settings->set([
'default_currency' => 'USD',
'digit_separator' => '1,234.56',
]);
$response = $this->actingAsForApi(User::factory()->superuser()->create())
->postJson(route('api.assets.store'), [
'asset_tag' => 'random-string',
'model_id' => AssetModel::factory()->create()->id,
'status_id' => Statuslabel::factory()->create()->id,
// API accepts float
'purchase_cost' => 12.34,
])
->assertStatusMessageIs('success');
$asset = Asset::find($response['payload']['id']);
$this->assertEquals(12.34, $asset->purchase_cost);
}
public function testStoresPeriodAsCommaSeparatorForPurchaseCost()
{
$this->settings->set([
'default_currency' => 'EUR',
'digit_separator' => '1.234,56',
]);
$response = $this->actingAsForApi(User::factory()->superuser()->create())
->postJson(route('api.assets.store'), [
'asset_tag' => 'random-string',
'model_id' => AssetModel::factory()->create()->id,
'status_id' => Statuslabel::factory()->create()->id,
// API also accepts string for comma separated values
'purchase_cost' => '12,34',
])
->assertStatusMessageIs('success');
$asset = Asset::find($response['payload']['id']);
$this->assertEquals(12.34, $asset->purchase_cost);
}
public function testUniqueSerialNumbersIsEnforcedWhenEnabled() public function testUniqueSerialNumbersIsEnforcedWhenEnabled()
{ {
$model = AssetModel::factory()->create(); $model = AssetModel::factory()->create();

View file

@ -16,4 +16,13 @@ class HelperTest extends TestCase
{ {
$this->assertIsString(Helper::defaultChartColors(-1)); $this->assertIsString(Helper::defaultChartColors(-1));
} }
public function testParseCurrencyMethod()
{
$this->settings->set(['default_currency' => 'USD']);
$this->assertSame(12.34, Helper::ParseCurrency('USD 12.34'));
$this->settings->set(['digit_separator' => '1.234,56']);
$this->assertSame(12.34, Helper::ParseCurrency('12,34'));
}
} }