diff --git a/app/Console/Commands/SendExpectedCheckinAlerts.php b/app/Console/Commands/SendExpectedCheckinAlerts.php index 34e9609ce..e042e8b08 100644 --- a/app/Console/Commands/SendExpectedCheckinAlerts.php +++ b/app/Console/Commands/SendExpectedCheckinAlerts.php @@ -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)); + } } } diff --git a/app/Console/Commands/SendUpcomingAuditReport.php b/app/Console/Commands/SendUpcomingAuditReport.php index e9cba106c..fc06785cf 100644 --- a/app/Console/Commands/SendUpcomingAuditReport.php +++ b/app/Console/Commands/SendUpcomingAuditReport.php @@ -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); } + } } diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index 57307acbb..b234e9cbf 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -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 diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php index 6054718e6..ac755fd38 100755 --- a/app/Http/Controllers/Assets/AssetsController.php +++ b/app/Http/Controllers/Assets/AssetsController.php @@ -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'); } diff --git a/app/Http/Controllers/LabelsController.php b/app/Http/Controllers/LabelsController.php index 799d91038..1c5715b35 100755 --- a/app/Http/Controllers/LabelsController.php +++ b/app/Http/Controllers/LabelsController.php @@ -14,6 +14,7 @@ use App\Models\Setting; use App\Models\Supplier; use App\Models\User; use App\View\Label as LabelView; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; class LabelsController extends Controller @@ -21,9 +22,9 @@ class LabelsController extends Controller /** * Returns the Label view with test data * - * @author Grant Le Roux - * @param string $labelName + * @param string $labelName * @return \Illuminate\Contracts\View\View + * @author Grant Le Roux */ public function show(string $labelName) { @@ -66,7 +67,7 @@ class LabelsController extends Controller $exampleAsset->model->category->id = 999999; $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)) ->filter() diff --git a/app/Http/Controllers/Licenses/LicensesController.php b/app/Http/Controllers/Licenses/LicensesController.php index c55181c51..268c3f8b6 100755 --- a/app/Http/Controllers/Licenses/LicensesController.php +++ b/app/Http/Controllers/Licenses/LicensesController.php @@ -11,6 +11,7 @@ use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +use Symfony\Component\HttpFoundation\StreamedResponse; /** * This controller handles all actions related to Licenses for @@ -289,4 +290,106 @@ class LicensesController extends Controller ->with('item', $license) ->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; + } } diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index dbb6f6622..3c09c900f 100755 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -804,10 +804,9 @@ class SettingsController extends Controller */ public function getLabels() { - return view('settings.labels', [ - 'setting' => Setting::getSettings(), - 'customFields' => CustomField::all(), - ]); + return view('settings.labels') + ->with('setting', Setting::getSettings()) + ->with('customFields', CustomField::where('field_encrypted', '=', 0)->get()); } /** diff --git a/app/Http/Middleware/AssetCountForSidebar.php b/app/Http/Middleware/AssetCountForSidebar.php index 5d9656f5c..2bc3f950f 100644 --- a/app/Http/Middleware/AssetCountForSidebar.php +++ b/app/Http/Middleware/AssetCountForSidebar.php @@ -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); } } diff --git a/app/Http/Requests/StoreAssetRequest.php b/app/Http/Requests/StoreAssetRequest.php index 8e7559673..007dd2684 100644 --- a/app/Http/Requests/StoreAssetRequest.php +++ b/app/Http/Requests/StoreAssetRequest.php @@ -4,6 +4,7 @@ namespace App\Http\Requests; use App\Models\Asset; use App\Models\Company; +use App\Models\Setting; use Carbon\Carbon; use Carbon\Exceptions\InvalidFormatException; use Illuminate\Support\Facades\Gate; @@ -45,12 +46,21 @@ class StoreAssetRequest extends ImageUploadRequest */ public function rules(): array { - $rules = array_merge( - (new Asset)->getRules(), + $modelRules = (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(), ); - - return $rules; } 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; + } } diff --git a/app/Models/Asset.php b/app/Models/Asset.php index 1f4079e49..c5373a04e 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -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 + * @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 + * @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 + * @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 diff --git a/app/Models/Labels/Tapes/Brother/TZe_18mm.php b/app/Models/Labels/Tapes/Brother/TZe_18mm.php new file mode 100644 index 000000000..38c14c7aa --- /dev/null +++ b/app/Models/Labels/Tapes/Brother/TZe_18mm.php @@ -0,0 +1,19 @@ +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()); } +} \ No newline at end of file diff --git a/app/Models/Labels/Tapes/Brother/TZe_18mm_A.php b/app/Models/Labels/Tapes/Brother/TZe_18mm_A.php new file mode 100644 index 000000000..32156f5ee --- /dev/null +++ b/app/Models/Labels/Tapes/Brother/TZe_18mm_A.php @@ -0,0 +1,56 @@ +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 + ); + } + + } +} \ No newline at end of file diff --git a/app/Models/License.php b/app/Models/License.php index 7fb4f9e4c..deb1221e7 100755 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -81,6 +81,7 @@ class License extends Depreciable 'serial', 'supplier_id', 'termination_date', + 'free_seat_count', 'user_id', 'min_amt', ]; @@ -114,6 +115,7 @@ class License extends Depreciable 'category' => ['name'], 'depreciation' => ['name'], ]; + protected $appends = ['free_seat_count']; /** * Update seat counts when the license is updated @@ -280,6 +282,16 @@ class License extends Depreciable } $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 @@ -502,7 +514,13 @@ class License extends Depreciable ->whereNull('deleted_at') ->count(); } - + /** + * Returns the available seats remaining + * + * @author A. Gianotto + * @since [v2.0] + * @return int + */ /** * Returns the number of total available seats for this license @@ -579,7 +597,7 @@ class License extends Depreciable $taken = $this->assigned_seats_count; $diff = ($total - $taken); - return $diff; + return (int) $diff; } /** diff --git a/resources/assets/less/overrides.less b/resources/assets/less/overrides.less index 69bd32a3e..31e85ca26 100644 --- a/resources/assets/less/overrides.less +++ b/resources/assets/less/overrides.less @@ -900,4 +900,10 @@ input[type="radio"]:checked::before { } .datepicker.dropdown-menu { z-index: 1030 !important; -} \ No newline at end of file +} + +.sidebar-menu > li .badge { + margin-top: 0px; + filter: brightness(70%); + font-size: 70%; +} diff --git a/resources/lang/en-US/admin/licenses/table.php b/resources/lang/en-US/admin/licenses/table.php index dfce4136c..9cabf9c88 100644 --- a/resources/lang/en-US/admin/licenses/table.php +++ b/resources/lang/en-US/admin/licenses/table.php @@ -4,6 +4,7 @@ return array( 'assigned_to' => 'Assigned To', 'checkout' => 'In/Out', + 'deleted_at' => 'Deleted at', 'id' => 'ID', 'license_email' => 'License Email', 'license_name' => 'Licensed To', diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index 500ea1631..0b6f61339 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -176,7 +176,7 @@ return [ 'last_name' => 'Last Name', 'license' => 'License', 'license_report' => 'License Report', - 'licenses_available' => 'licenses available', + 'licenses_available' => 'Licenses available', 'licenses' => 'Licenses', 'list_all' => 'List All', 'loading' => 'Loading... please wait....', @@ -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', diff --git a/resources/views/hardware/audit-due.blade.php b/resources/views/hardware/audit-due.blade.php index 0232facd7..0ee17ec23 100644 --- a/resources/views/hardware/audit-due.blade.php +++ b/resources/views/hardware/audit-due.blade.php @@ -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 --}}
-
-
- @include('partials.asset-bulk-actions') + +
- {{ Form::close() }} -
-
- - + @stop @section('moar_scripts') @include('partials.bootstrap-table') - @stop diff --git a/resources/views/hardware/audit-overdue.blade.php b/resources/views/hardware/audit-overdue.blade.php deleted file mode 100644 index ddd477fb2..000000000 --- a/resources/views/hardware/audit-overdue.blade.php +++ /dev/null @@ -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') - -
-
-
-
- @include('partials.asset-bulk-actions') -
-
- - -
- -
-
- {{ Form::close() }} -
-
-
-
-@stop - -@section('moar_scripts') - @include('partials.bootstrap-table') - -@stop diff --git a/resources/views/hardware/checkin-due.blade.php b/resources/views/hardware/checkin-due.blade.php new file mode 100644 index 000000000..6ed7cf42c --- /dev/null +++ b/resources/views/hardware/checkin-due.blade.php @@ -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 --}} +
+
+ + + + +
+
+ +@stop + +@section('moar_scripts') + @include('partials.bootstrap-table') +@stop diff --git a/resources/views/hardware/view.blade.php b/resources/views/hardware/view.blade.php index a83b006c5..dfb151393 100755 --- a/resources/views/hardware/view.blade.php +++ b/resources/views/hardware/view.blade.php @@ -51,7 +51,7 @@ @@ -62,7 +62,7 @@ @@ -73,7 +73,7 @@ @@ -96,7 +96,7 @@ @@ -107,7 +107,7 @@ @@ -119,7 +119,7 @@ diff --git a/resources/views/layouts/default.blade.php b/resources/views/layouts/default.blade.php index a1a4bc30c..c22118161 100644 --- a/resources/views/layouts/default.blade.php +++ b/resources/views/layouts/default.blade.php @@ -437,6 +437,9 @@ {{ trans('general.list_all') }} + + {{ (isset($total_assets)) ? $total_assets : '' }} + @@ -447,7 +450,8 @@ - {{ $status_nav->name }} ({{ $status_nav->asset_count }}) + {{ $status_nav->name }} + {{ $status_nav->asset_count }}) @endforeach @endif @@ -455,49 +459,43 @@ - {{ trans('general.all') }} {{ trans('general.deployed') }} - ({{ (isset($total_deployed_sidebar)) ? $total_deployed_sidebar : '' }}) + {{ (isset($total_deployed_sidebar)) ? $total_deployed_sidebar : '' }} - {{ trans('general.all') }} {{ trans('general.ready_to_deploy') }} - ({{ (isset($total_rtd_sidebar)) ? $total_rtd_sidebar : '' }}) + {{ (isset($total_rtd_sidebar)) ? $total_rtd_sidebar : '' }} - {{ trans('general.all') }} {{ trans('general.pending') }} - ({{ (isset($total_pending_sidebar)) ? $total_pending_sidebar : '' }}) + {{ (isset($total_pending_sidebar)) ? $total_pending_sidebar : '' }} - {{ trans('general.all') }} {{ trans('general.undeployable') }} - ({{ (isset($total_undeployable_sidebar)) ? $total_undeployable_sidebar : '' }}) + {{ (isset($total_undeployable_sidebar)) ? $total_undeployable_sidebar : '' }} - {{ trans('general.all') }} {{ trans('general.byod') }} - ({{ (isset($total_byod_sidebar)) ? $total_byod_sidebar : '' }}) + {{ (isset($total_byod_sidebar)) ? $total_byod_sidebar : '' }} - {{ trans('general.all') }} {{ trans('admin/hardware/general.archived') }} - ({{ (isset($total_archived_sidebar)) ? $total_archived_sidebar : '' }}) + {{ (isset($total_archived_sidebar)) ? $total_archived_sidebar : '' }} {{ trans('general.audit_due') }} + {{ (isset($total_due_and_overdue_for_audit)) ? $total_due_and_overdue_for_audit : '' }} - - - {{ trans('general.audit_overdue') }} - - + @endcan + + @can('checkin', \App\Models\Asset::class) + + + {{ trans('general.checkin_due') }} + {{ (isset($total_due_and_overdue_for_checkin)) ? $total_due_and_overdue_for_checkin : '' }} + + @endcan
  •  
  • diff --git a/resources/views/licenses/index.blade.php b/resources/views/licenses/index.blade.php index 8fa52fd1f..82e34b190 100755 --- a/resources/views/licenses/index.blade.php +++ b/resources/views/licenses/index.blade.php @@ -13,6 +13,9 @@ {{ trans('general.create') }} @endcan +@can('view', \App\Models\License::class) + {{ trans('general.export') }} +@endcan @stop {{-- Page content --}} diff --git a/resources/views/notifications/markdown/report-expected-checkins.blade.php b/resources/views/notifications/markdown/report-expected-checkins.blade.php index 3715188d4..08b81350e 100644 --- a/resources/views/notifications/markdown/report-expected-checkins.blade.php +++ b/resources/views/notifications/markdown/report-expected-checkins.blade.php @@ -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 diff --git a/resources/views/partials/label2-field-definitions.blade.php b/resources/views/partials/label2-field-definitions.blade.php index ad5d1eb2e..b701ffe69 100644 --- a/resources/views/partials/label2-field-definitions.blade.php +++ b/resources/views/partials/label2-field-definitions.blade.php @@ -351,6 +351,7 @@ @foreach($customFields as $customField) + @endforeach diff --git a/routes/api.php b/routes/api.php index 842e6210d..8adb0af61 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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, diff --git a/routes/web/hardware.php b/routes/web/hardware.php index d4f289228..7833f0fda 100644 --- a/routes/web/hardware.php +++ b/routes/web/hardware.php @@ -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'); diff --git a/routes/web/licenses.php b/routes/web/licenses.php index b70347793..7212a4764 100644 --- a/routes/web/licenses.php +++ b/routes/web/licenses.php @@ -48,6 +48,13 @@ Route::group(['prefix' => 'licenses', 'middleware' => ['auth']], function () { '{licenseId}/showfile/{fileId}/{download?}', [Licenses\LicenseFilesController::class, 'show'] )->name('show.licensefile'); + Route::get( + 'export', + [ + Licenses\LicensesController::class, + 'getExportLicensesCsv' + ] + )->name('licenses.export'); }); Route::resource('licenses', Licenses\LicensesController::class, [ diff --git a/tests/Feature/Api/Assets/AssetIndexTest.php b/tests/Feature/Api/Assets/AssetIndexTest.php index 3175db695..0a21e13f2 100644 --- a/tests/Feature/Api/Assets/AssetIndexTest.php +++ b/tests/Feature/Api/Assets/AssetIndexTest.php @@ -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(); diff --git a/tests/Feature/Api/Assets/AssetStoreTest.php b/tests/Feature/Api/Assets/AssetStoreTest.php index c87474551..006a5738f 100644 --- a/tests/Feature/Api/Assets/AssetStoreTest.php +++ b/tests/Feature/Api/Assets/AssetStoreTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Api\Assets; +use App\Helpers\Helper; use App\Models\Asset; use App\Models\AssetModel; use App\Models\Company; @@ -12,6 +13,7 @@ use App\Models\Supplier; use App\Models\User; use Illuminate\Support\Facades\Crypt; use Illuminate\Testing\Fluent\AssertableJson; +use Illuminate\Testing\TestResponse; use Tests\TestCase; class AssetStoreTest extends TestCase @@ -278,6 +280,50 @@ class AssetStoreTest extends TestCase ->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() { $model = AssetModel::factory()->create(); diff --git a/tests/Unit/Helpers/HelperTest.php b/tests/Unit/Helpers/HelperTest.php index 0b5fba986..cfc8c7fac 100644 --- a/tests/Unit/Helpers/HelperTest.php +++ b/tests/Unit/Helpers/HelperTest.php @@ -16,4 +16,13 @@ class HelperTest extends TestCase { $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')); + } }