diff --git a/.all-contributorsrc b/.all-contributorsrc index ec5db5021..8ea582b9b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3181,6 +3181,42 @@ "contributions": [ "code" ] + }, + { + "login": "Glukose1", + "name": "Glukose1", + "avatar_url": "https://avatars.githubusercontent.com/u/167117705?v=4", + "profile": "https://github.com/Glukose1", + "contributions": [ + "code" + ] + }, + { + "login": "Scarzy", + "name": "Scarzy", + "avatar_url": "https://avatars.githubusercontent.com/u/1197791?v=4", + "profile": "https://github.com/Scarzy", + "contributions": [ + "code" + ] + }, + { + "login": "setpill", + "name": "setpill", + "avatar_url": "https://avatars.githubusercontent.com/u/37372069?v=4", + "profile": "https://github.com/setpill", + "contributions": [ + "code" + ] + }, + { + "login": "swift2512", + "name": "swift2512", + "avatar_url": "https://avatars.githubusercontent.com/u/3755203?v=4", + "profile": "https://github.com/swift2512", + "contributions": [ + "bug" + ] } ] } diff --git a/.env.dev.docker b/.env.dev.docker index 7b9e2000c..983063bbd 100644 --- a/.env.dev.docker +++ b/.env.dev.docker @@ -1,6 +1,8 @@ # -------------------------------------------- # REQUIRED: DB SETUP # -------------------------------------------- +# https://mariadb.com/kb/en/mariadb-server-docker-official-image-environment-variables/ + MYSQL_DATABASE=snipeit MYSQL_USER=snipeit MYSQL_PASSWORD=changeme1234 diff --git a/.env.example b/.env.example index 426af4ff8..3bfee7bf2 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,8 @@ DB_PREFIX=null DB_DUMP_PATH='/usr/bin' DB_CHARSET=utf8mb4 DB_COLLATION=utf8mb4_unicode_ci +DB_SANITIZE_BY_DEFAULT=false + # -------------------------------------------- # OPTIONAL: SSL DATABASE SETTINGS diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 53acc282e..000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,43 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - security - - :woman_technologist: ready for dev - - :moneybag: bounty - - :hand: bug - - "🔐 security" - - "👩‍💻 ready for dev" - - "💰 bounty" - - "✋ bug" - -exemptMilestones: true - -# Label to use when marking an issue as stale -staleLabel: stale - -only: issues - -# Comment to post when removing the stale label. -unmarkComment: > - Okay, it looks like this issue or feature request might still be important. We'll re-open - it for now. Thank you for letting us know! - -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - Is this still relevant? We haven't heard from anyone in a bit. If so, - please comment with any updates or additional detail. - - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Don't - take it personally, we just need to keep a handle on things. Thank you - for your contributions! -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: > - This issue has been automatically closed because it has not had - recent activity. If you believe this is still an issue, please confirm that - this issue is still happening in the most recent version of Snipe-IT and reply - to this thread to re-open it. diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..14ddea225 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,39 @@ +name: 'Close stale issues' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + # contents: write # only for delete-branch option + issues: write + # pull-requests: write + steps: + - uses: actions/stale@v9 + with: + debug-only: true + operations-per-run: 100 # just while we're debugging + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-stale: 60 + days-before-close: 7 + exempt-all-milestones: true + stale-issue-message: > + Is this still relevant? We haven't heard from anyone in a bit. If so, + please comment with any updates or additional detail. + + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Don't + take it personally, we just need to keep a handle on things. Thank you + for your contributions! + close-issue-message: > + This issue has been automatically closed because it has not had + recent activity. If you believe this is still an issue, please confirm that + this issue is still happening in the most recent version of Snipe-IT and reply + to this thread to re-open it. + # There doesn't seem to be a 'reopen issue message'? + # Since there is no 'stale-pr-message' - PR's should not be stale'd + stale-issue-label: stale + exempt-issue-labels: > + pinned,security,:woman_technologist: ready for dev,:moneybag: bounty,:hand: bug,🔐 security,👩‍💻 ready for dev,💰 bounty,✋ bug \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0715ac049..17a7d28da 100755 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ storage/private_uploads/users/* tests/_data/scenarios tests/_output/* tests/_support/_generated/* +tests/coverage/* /npm-debug.log /storage/oauth-private.key /storage/oauth-public.key diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0e4efc974..f821c1f17 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -52,6 +52,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken | [
bilias](https://github.com/bilias)
[💻](https://github.com/snipe/snipe-it/commits?author=bilias "Code") | [
coach1988](https://github.com/coach1988)
[💻](https://github.com/snipe/snipe-it/commits?author=coach1988 "Code") | [
MrM](https://github.com/mauro-miatello)
[💻](https://github.com/snipe/snipe-it/commits?author=mauro-miatello "Code") | [
koiakoia](https://github.com/koiakoia)
[💻](https://github.com/snipe/snipe-it/commits?author=koiakoia "Code") | [
Mustafa Online](https://github.com/mustafa-online)
[💻](https://github.com/snipe/snipe-it/commits?author=mustafa-online "Code") | [
franceslui](https://github.com/franceslui)
[💻](https://github.com/snipe/snipe-it/commits?author=franceslui "Code") | [
Q4kK](https://github.com/Q4kK)
[💻](https://github.com/snipe/snipe-it/commits?author=Q4kK "Code") | | [
squintfox](https://github.com/squintfox)
[💻](https://github.com/snipe/snipe-it/commits?author=squintfox "Code") | [
Jeff Clay](https://github.com/jeffclay)
[💻](https://github.com/snipe/snipe-it/commits?author=jeffclay "Code") | [
Phil J R](https://github.com/PP-JN-RL)
[💻](https://github.com/snipe/snipe-it/commits?author=PP-JN-RL "Code") | [
i_virus](https://www.corelight.com/)
[💻](https://github.com/snipe/snipe-it/commits?author=chandanchowdhury "Code") | [
Paul Grime](https://github.com/gitgrimbo)
[💻](https://github.com/snipe/snipe-it/commits?author=gitgrimbo "Code") | [
Lee Porte](https://leeporte.co.uk)
[💻](https://github.com/snipe/snipe-it/commits?author=LeePorte "Code") | [
BRYAN ](https://github.com/bryanlopezinc)
[💻](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Tests") | | [
U-H-T](https://github.com/U-H-T)
[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") | [
Matt Tyree](https://github.com/Tyree)
[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [
Florent Bervas](http://spoontux.net)
[💻](https://github.com/snipe/snipe-it/commits?author=FlorentDotMe "Code") | [
Daniel Albertsen](https://ditscheri.com)
[💻](https://github.com/snipe/snipe-it/commits?author=dbakan "Code") | [
r-xyz](https://github.com/r-xyz)
[💻](https://github.com/snipe/snipe-it/commits?author=r-xyz "Code") | [
Steven Mainor](https://github.com/DrekiDegga)
[💻](https://github.com/snipe/snipe-it/commits?author=DrekiDegga "Code") | [
arne-kroeger](https://github.com/arne-kroeger)
[💻](https://github.com/snipe/snipe-it/commits?author=arne-kroeger "Code") | +| [
Glukose1](https://github.com/Glukose1)
[💻](https://github.com/snipe/snipe-it/commits?author=Glukose1 "Code") | [
Scarzy](https://github.com/Scarzy)
[💻](https://github.com/snipe/snipe-it/commits?author=Scarzy "Code") | [
setpill](https://github.com/setpill)
[💻](https://github.com/snipe/snipe-it/commits?author=setpill "Code") | [
swift2512](https://github.com/swift2512)
[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aswift2512 "Bug reports") | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! diff --git a/Dockerfile.alpine b/Dockerfile.alpine index c08cbbd95..2c83a1a11 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -79,12 +79,12 @@ USER root VOLUME ["/var/lib/snipeit"] -# Entrypoints -COPY docker/entrypoint_alpine.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh +# Startup script +COPY docker/startup_alpine.sh /startup.sh +RUN chmod +x /startup.sh ENTRYPOINT ["/sbin/tini", "--"] -CMD ["/entrypoint.sh"] +CMD ["/startup.sh"] EXPOSE 80 diff --git a/Dockerfile.fpm-alpine b/Dockerfile.fpm-alpine index 77302524d..b7fb27298 100644 --- a/Dockerfile.fpm-alpine +++ b/Dockerfile.fpm-alpine @@ -97,7 +97,7 @@ RUN set -eux; \ VOLUME [ "/var/lib/snipeit" ] COPY --chown=www-data:www-data docker/docker-secrets.env /var/www/html/.env -COPY --chmod=655 docker/docker-entrypoint.sh /usr/local/bin/docker-snipeit-entrypoint +COPY --chmod=655 docker/startup_alpine_fpm.sh /startup.sh COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf -ENTRYPOINT [ "/usr/local/bin/docker-snipeit-entrypoint" ] -CMD [ "/usr/local/bin/docker-php-entrypoint", "php-fpm" ] +ENTRYPOINT [ "/startup.sh" ] +CMD [ "/startup.sh", "php-fpm" ] diff --git a/README.md b/README.md index 6d2c45ac7..0086c7b32 100644 --- a/README.md +++ b/README.md @@ -72,12 +72,13 @@ Since the release of the JSON REST API, several third-party developers have been - [Snipe-IT plugin for Jira Service Desk](https://marketplace.atlassian.com/apps/1220964/snipe-it-for-jira) - [Python 3 CSV importer](https://github.com/gastamper/snipeit-csvimporter) - allows importing assets into Snipe-IT based on Item Name rather than Asset Tag. - [Snipe-IT Kubernetes Helm Chart](https://github.com/t3n/helm-charts/tree/master/snipeit) - For more information, [click here](https://hub.helm.sh/charts/t3n/snipeit). -- [Snipe-IT Bulk Edit](https://github.com/bricelabelle/snipe-it-bulkedit) - Google Script files to use Google Sheets as a bulk checkout/checkin/edit tool for Snipe-it. -- [MosyleSnipeSync](https://github.com/RodneyLeeBrands/MosyleSnipeSync) by [@Karpadiem](https://github.com/Karpadiem) - Python script to synchronize information between Mosyle and Snipe-IT +- [Snipe-IT Bulk Edit](https://github.com/bricelabelle/snipe-it-bulkedit) - Google Script files to use Google Sheets as a bulk checkout/checkin/edit tool for Snipe-IT. +- [MosyleSnipeSync](https://github.com/RodneyLeeBrands/MosyleSnipeSync) by [@Karpadiem](https://github.com/Karpadiem) - Python script to synchronize information between Mosyle and Snipe-IT. - [WWW::SnipeIT](https://github.com/SEDC/perl-www-snipeit) by [@SEDC](https://github.com/SEDC) - perl module for accessing the API - [UniFi to Snipe-IT](https://github.com/RodneyLeeBrands/UnifiSnipeSync) by [@karpadiem](https://github.com/karpadiem) - Python script that synchronizes UniFi devices with Snipe-IT. - [Kandji2Snipe](https://github.com/grokability/kandji2snipe) by [@briangoldstein](https://github.com/briangoldstein) - Python script that synchronizes Kandji with Snipe-IT. -- [SnipeAgent](https://github.com/ReticentRobot/SnipeAgent) by @ReticentRobot - Windows agent for Snipe-IT +- [SnipeAgent](https://github.com/ReticentRobot/SnipeAgent) by [@ReticentRobot](https://github.com/ReticentRobot) - Windows agent for Snipe-IT. +- [Gate Pass Generator](https://github.com/cha7uraAE/snipe-it-gate-pass-system) by [@cha7uraAE](https://github.com/cha7uraAE) - A Streamlit application for generating gate passes based on hardware data from a Snipe-IT API. ----- diff --git a/app/Console/Commands/RemoveExplicitEols.php b/app/Console/Commands/RemoveExplicitEols.php new file mode 100644 index 000000000..f5164c2ee --- /dev/null +++ b/app/Console/Commands/RemoveExplicitEols.php @@ -0,0 +1,60 @@ +option('model_name') == 'all') { + $assets = Asset::all(); + $this->updateAssets($assets); + } else { + $assetModel = AssetModel::where('name', '=', $this->option('model_name'))->first(); + + if ($assetModel) { + $assets = Asset::where('model_id', '=', $assetModel->id)->get(); + $this->updateAssets($assets); + } else { + $this->error('Asset model not found'); + } + } + $endTime = microtime(true); + $executionTime = ($endTime - $startTime); + $this->info('Command executed in ' . round($executionTime, 2) . ' seconds.'); + } + + private function updateAssets($assets) + { + foreach ($assets as $asset) { + $asset->eol_explicit = 0; + $asset->asset_eol_date = null; + $asset->save(); + } + + $this->info($assets->count() . ' Assets updated successfully'); + } +} diff --git a/app/Console/Commands/SendAcceptanceReminder.php b/app/Console/Commands/SendAcceptanceReminder.php index dd9e59f61..a11ea8e27 100644 --- a/app/Console/Commands/SendAcceptanceReminder.php +++ b/app/Console/Commands/SendAcceptanceReminder.php @@ -47,9 +47,10 @@ class SendAcceptanceReminder extends Command { $pending = CheckoutAcceptance::pending()->where('checkoutable_type', 'App\Models\Asset') ->whereHas('checkoutable', function($query) { - $query->where('archived', 0); + $query->where('accepted_at', null) + ->where('declined_at', null); }) - ->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model', 'checkoutable.adminuser']) + ->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model', 'checkoutable.admin']) ->get(); $count = 0; diff --git a/app/Helpers/IconHelper.php b/app/Helpers/IconHelper.php new file mode 100644 index 000000000..b56871f83 --- /dev/null +++ b/app/Helpers/IconHelper.php @@ -0,0 +1,190 @@ + + * + * @version v1.0 + * @author [T. Scarsbrook] [] + */ +class AssetModelFilesController extends Controller +{ + /** + * Accepts a POST to upload a file to the server. + * + * @param \App\Http\Requests\UploadFileRequest $request + * @param int $assetModelId + * @since [v7.0.12] + * @author [r-xyz] + */ + public function store(UploadFileRequest $request, $assetModelId = null) : JsonResponse + { + // Start by checking if the asset being acted upon exists + if (! $assetModel = AssetModel::find($assetModelId)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404); + } + + // Make sure we are allowed to update this asset + $this->authorize('update', $assetModel); + + if ($request->hasFile('file')) { + // If the file storage directory doesn't exist; create it + if (! Storage::exists('private_uploads/assetmodels')) { + Storage::makeDirectory('private_uploads/assetmodels', 775); + } + + // Loop over the attached files and add them to the asset + foreach ($request->file('file') as $file) { + $file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$assetModel->id, $file); + + $assetModel->logUpload($file_name, e($request->get('notes'))); + } + + // All done - report success + return response()->json(Helper::formatStandardApiResponse('success', $assetModel, trans('admin/models/message.upload.success'))); + } + + // We only reach here if no files were included in the POST, so tell the user this + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.upload.nofiles')), 500); + } + + /** + * List the files for an asset. + * + * @param int $assetModelId + * @since [v7.0.12] + * @author [r-xyz] + */ + public function list($assetModelId = null) : JsonResponse + { + // Start by checking if the asset being acted upon exists + if (! $assetModel = AssetModel::find($assetModelId)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404); + } + + // the asset is valid + if (isset($assetModel->id)) { + $this->authorize('view', $assetModel); + + // Check that there are some uploads on this asset that can be listed + if ($assetModel->uploads->count() > 0) { + $files = array(); + foreach ($assetModel->uploads as $upload) { + array_push($files, $upload); + } + // Give the list of files back to the user + return response()->json(Helper::formatStandardApiResponse('success', $files, trans('admin/models/message.upload.success'))); + } + + // There are no files. + return response()->json(Helper::formatStandardApiResponse('success', array(), trans('admin/models/message.upload.success'))); + } + + // Send back an error message + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.error')), 500); + } + + /** + * Check for permissions and display the file. + * + * @param int $assetModelId + * @param int $fileId + * @return \Illuminate\Http\JsonResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + * @since [v7.0.12] + * @author [r-xyz] + */ + public function show($assetModelId = null, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse + { + // Start by checking if the asset being acted upon exists + if (! $assetModel = AssetModel::find($assetModelId)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404); + } + + // the asset is valid + if (isset($assetModel->id)) { + $this->authorize('view', $assetModel); + + // Check that the file being requested exists for the asset + if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $assetModel->id)->find($fileId)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.no_match', ['id' => $fileId])), 404); + } + + // Form the full filename with path + $file = 'private_uploads/assetmodels/'.$log->filename; + Log::debug('Checking for '.$file); + + if ($log->action_type == 'audit') { + $file = 'private_uploads/audits/'.$log->filename; + } + + // Check the file actually exists on the filesystem + if (! Storage::exists($file)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.does_not_exist', ['id' => $fileId])), 404); + } + + if (request('inline') == 'true') { + + $headers = [ + 'Content-Disposition' => 'inline', + ]; + + return Storage::download($file, $log->filename, $headers); + } + + return StorageHelper::downloader($file); + } + + // Send back an error message + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.error', ['id' => $fileId])), 500); + } + + /** + * Delete the associated file + * + * @param int $assetModelId + * @param int $fileId + * @since [v7.0.12] + * @author [r-xyz] + */ + public function destroy($assetModelId = null, $fileId = null) : JsonResponse + { + // Start by checking if the asset being acted upon exists + if (! $assetModel = AssetModel::find($assetModelId)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404); + } + + $rel_path = 'private_uploads/assetmodels'; + + // the asset is valid + if (isset($assetModel->id)) { + $this->authorize('update', $assetModel); + + // Check for the file + $log = Actionlog::find($fileId); + if ($log) { + // Check the file actually exists, and delete it + if (Storage::exists($rel_path.'/'.$log->filename)) { + Storage::delete($rel_path.'/'.$log->filename); + } + // Delete the record of the file + $log->delete(); + + // All deleting done - notify the user of success + return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/models/message.deletefile.success')), 200); + } + + // The file doesn't seem to really exist, so report an error + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500); + } + + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500); + } +} diff --git a/app/Http/Controllers/Api/AssetModelsController.php b/app/Http/Controllers/Api/AssetModelsController.php index 835f4d22e..9f7819342 100644 --- a/app/Http/Controllers/Api/AssetModelsController.php +++ b/app/Http/Controllers/Api/AssetModelsController.php @@ -78,6 +78,10 @@ class AssetModelsController extends Controller $assetmodels = $assetmodels->where('models.category_id', '=', $request->input('category_id')); } + if ($request->filled('depreciation_id')) { + $assetmodels = $assetmodels->where('models.depreciation_id', '=', $request->input('depreciation_id')); + } + if ($request->filled('search')) { $assetmodels->TextSearch($request->input('search')); } diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index 8361971d5..1243f1212 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -602,7 +602,7 @@ class AssetsController extends Controller if ($field->field_encrypted == '1') { Log::debug('This model field is encrypted in this fieldset.'); - if (Gate::allows('admin')) { + if (Gate::allows('assets.view.encrypted_custom_fields')) { // If input value is null, use custom field's default value if (($field_val == null) && ($request->has('model_id') != '')) { @@ -695,7 +695,7 @@ class AssetsController extends Controller } } if ($field->field_encrypted == '1') { - if (Gate::allows('admin')) { + if (Gate::allows('assets.view.encrypted_custom_fields')) { $field_val = Crypt::encrypt($field_val); } else { $problems_updating_encrypted_custom_fields = true; @@ -928,7 +928,7 @@ class AssetsController extends Controller } } - if ($request->has('status_id')) { + if ($request->filled('status_id')) { $asset->status_id = $request->input('status_id'); } @@ -978,7 +978,7 @@ class AssetsController extends Controller public function checkinByTag(Request $request, $tag = null) : JsonResponse { $this->authorize('checkin', Asset::class); - if(null == $tag && null !== ($request->input('asset_tag'))) { + if (null == $tag && null !== ($request->input('asset_tag'))) { $tag = $request->input('asset_tag'); } $asset = Asset::where('asset_tag', $tag)->first(); diff --git a/app/Http/Controllers/Api/ConsumablesController.php b/app/Http/Controllers/Api/ConsumablesController.php index 1665b7f4f..7be4c3d2d 100644 --- a/app/Http/Controllers/Api/ConsumablesController.php +++ b/app/Http/Controllers/Api/ConsumablesController.php @@ -86,6 +86,9 @@ class ConsumablesController extends Controller case 'company': $consumables = $consumables->OrderCompany($order); break; + case 'remaining': + $consumables = $consumables->OrderRemaining($order); + break; case 'supplier': $consumables = $consumables->OrderSupplier($order); break; diff --git a/app/Http/Controllers/Api/DepreciationsController.php b/app/Http/Controllers/Api/DepreciationsController.php index 0209eae39..72e0f3a14 100644 --- a/app/Http/Controllers/Api/DepreciationsController.php +++ b/app/Http/Controllers/Api/DepreciationsController.php @@ -20,9 +20,22 @@ class DepreciationsController extends Controller public function index(Request $request) : JsonResponse | array { $this->authorize('view', Depreciation::class); - $allowed_columns = ['id','name','months','depreciation_min', 'depreciation_type','created_at']; + $allowed_columns = [ + 'id', + 'name', + 'months', + 'depreciation_min', + 'depreciation_type', + 'created_at', + 'assets_count', + 'models_count', + 'licenses_count', + ]; - $depreciations = Depreciation::select('id','name','months','depreciation_min','depreciation_type','user_id','created_at','updated_at'); + $depreciations = Depreciation::select('id','name','months','depreciation_min','depreciation_type','user_id','created_at','updated_at') + ->withCount('assets as assets_count') + ->withCount('models as models_count') + ->withCount('licenses as licenses_count'); if ($request->filled('search')) { $depreciations = $depreciations->TextSearch($request->input('search')); diff --git a/app/Http/Controllers/Api/LicensesController.php b/app/Http/Controllers/Api/LicensesController.php index 71ad01b59..0dae68dbb 100644 --- a/app/Http/Controllers/Api/LicensesController.php +++ b/app/Http/Controllers/Api/LicensesController.php @@ -27,7 +27,7 @@ class LicensesController extends Controller $licenses = License::with('company', 'manufacturer', 'supplier','category', 'adminuser')->withCount('freeSeats as free_seats_count'); if ($request->filled('company_id')) { - $licenses->where('company_id', '=', $request->input('company_id')); + $licenses->where('licenses.company_id', '=', $request->input('company_id')); } if ($request->filled('name')) { diff --git a/app/Http/Controllers/Api/PredefinedKitsController.php b/app/Http/Controllers/Api/PredefinedKitsController.php index 2bc118db3..26ccb5035 100644 --- a/app/Http/Controllers/Api/PredefinedKitsController.php +++ b/app/Http/Controllers/Api/PredefinedKitsController.php @@ -246,7 +246,7 @@ class PredefinedKitsController extends Controller $relation = $kit->models(); if ($relation->find($model_id)) { - return response()->json(Helper::formatStandardApiResponse('error', null, ['model' => 'Model already attached to kit'])); + return response()->json(Helper::formatStandardApiResponse('error', null, ['model' => trans('admin/kits/general.model_already_attached')])); } $relation->attach($model_id, ['quantity' => $quantity]); diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 9200f80b1..856b3b6a6 100644 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -427,13 +427,10 @@ class UsersController extends Controller * @param \Illuminate\Http\Request $request * @param int $id */ - public function update(SaveUserRequest $request, $id) : JsonResponse + public function update(SaveUserRequest $request, User $user): JsonResponse { $this->authorize('update', User::class); - if ($user = User::find($id)) { - - $this->authorize('update', $user); /** @@ -443,12 +440,10 @@ class UsersController extends Controller * */ - - if ((($id == 1) || ($id == 2)) && (config('app.lock_passwords'))) { + if ((($user->id == 1) || ($user->id == 2)) && (config('app.lock_passwords'))) { return response()->json(Helper::formatStandardApiResponse('error', null, 'Permission denied. You cannot update user information via API on the demo.')); } - $user->fill($request->all()); if ($user->id == $request->input('manager_id')) { @@ -473,16 +468,13 @@ class UsersController extends Controller $user->permissions = $permissions_array; } - // Update the location of any assets checked out to this user Asset::where('assigned_type', User::class) ->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]); - app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar'); if ($user->save()) { - // Check if the request has groups passed and has a value, AND that the user us a superuser if (($request->has('groups')) && (auth()->user()->isSuperUser())) { @@ -496,18 +488,10 @@ class UsersController extends Controller // Sync the groups since the user is a superuser and the groups pass validation $user->groups()->sync($request->input('groups')); - - } - return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.update'))); } - return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors())); - } - - return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id')))); - } /** diff --git a/app/Http/Controllers/AssetModelsController.php b/app/Http/Controllers/AssetModelsController.php index 4b114312e..94c630c09 100755 --- a/app/Http/Controllers/AssetModelsController.php +++ b/app/Http/Controllers/AssetModelsController.php @@ -202,6 +202,7 @@ class AssetModelsController extends Controller if ($model->image) { try { Storage::disk('public')->delete('models/'.$model->image); + $model->update(['image' => null]); } catch (\Exception $e) { Log::info($e); } @@ -233,7 +234,7 @@ class AssetModelsController extends Controller if ($model->restore()) { $logaction = new Actionlog(); - $logaction->item_type = User::class; + $logaction->item_type = AssetModel::class; $logaction->item_id = $model->id; $logaction->created_at = date('Y-m-d H:i:s'); $logaction->user_id = auth()->id(); diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php index 2430cd00f..59b22b386 100755 --- a/app/Http/Controllers/Assets/AssetsController.php +++ b/app/Http/Controllers/Assets/AssetsController.php @@ -165,7 +165,7 @@ class AssetsController extends Controller if (($model) && ($model->fieldset)) { foreach ($model->fieldset->fields as $field) { if ($field->field_encrypted == '1') { - if (Gate::allows('admin')) { + if (Gate::allows('assets.view.encrypted_custom_fields')) { if (is_array($request->input($field->db_column))) { $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column))); } else { @@ -388,7 +388,7 @@ class AssetsController extends Controller foreach ($model->fieldset->fields as $field) { if ($field->field_encrypted == '1') { - if (Gate::allows('admin')) { + if (Gate::allows('assets.view.encrypted_custom_fields')) { if (is_array($request->input($field->db_column))) { $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column))); } else { @@ -844,7 +844,7 @@ class AssetsController extends Controller { $this->authorize('checkin', Asset::class); - return view('hardware/quickscan-checkin'); + return view('hardware/quickscan-checkin')->with('statusLabel_list', Helper::statusLabelList()); } public function audit($id) diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index 287bc0611..d58edbaca 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -227,7 +227,8 @@ class BulkAssetsController extends Controller * its checkout status. */ - if (($request->filled('purchase_date')) + if (($request->filled('name')) + || ($request->filled('purchase_date')) || ($request->filled('expected_checkin')) || ($request->filled('purchase_cost')) || ($request->filled('supplier_id')) @@ -239,6 +240,7 @@ class BulkAssetsController extends Controller || ($request->filled('status_id')) || ($request->filled('model_id')) || ($request->filled('next_audit_date')) + || ($request->filled('null_name')) || ($request->filled('null_purchase_date')) || ($request->filled('null_expected_checkin_date')) || ($request->filled('null_next_audit_date')) @@ -251,13 +253,14 @@ class BulkAssetsController extends Controller $this->update_array = []; /** - * Leave out model_id and status here because we do math on that later. We have to do some extra - * validation and checks on those two. + * Leave out model_id and status here because we do math on that later. We have to do some + * extra validation and checks on those two. * * It's tempting to make these match the request check above, but some of these values require * extra work to make sure the data makes sense. */ - $this->conditionallyAddItem('purchase_date') + $this->conditionallyAddItem('name') + ->conditionallyAddItem('purchase_date') ->conditionallyAddItem('expected_checkin') ->conditionallyAddItem('order_number') ->conditionallyAddItem('requestable') @@ -271,6 +274,11 @@ class BulkAssetsController extends Controller /** * Blank out fields that were requested to be blanked out via checkbox */ + if ($request->input('null_name')=='1') { + + $this->update_array['name'] = null; + } + if ($request->input('null_purchase_date')=='1') { $this->update_array['purchase_date'] = null; } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 9ac976b43..e7b10877c 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -508,8 +508,8 @@ class LoginController extends Controller protected function validator(array $data) { return Validator::make($data, [ - 'username' => 'required', - 'password' => 'required', + 'username' => 'required|not_array', + 'password' => 'required|not_array', ]); } diff --git a/app/Http/Controllers/DepreciationsController.php b/app/Http/Controllers/DepreciationsController.php index c564cc98f..888f7a7e7 100755 --- a/app/Http/Controllers/DepreciationsController.php +++ b/app/Http/Controllers/DepreciationsController.php @@ -193,13 +193,20 @@ class DepreciationsController extends Controller */ public function show($id) : View | RedirectResponse { - if (is_null($depreciation = Depreciation::find($id))) { - // Redirect to the blogs management page - return redirect()->route('depreciations.index')->with('error', trans('admin/depreciations/message.does_not_exist')); - } + $depreciation = Depreciation::withCount('assets as assets_count') + ->withCount('models as models_count') + ->withCount('licenses as licenses_count') + ->find($id); $this->authorize('view', $depreciation); - return view('depreciations/view', compact('depreciation')); + if ($depreciation) { + return view('depreciations/view', compact('depreciation')); + + } + + return redirect()->route('depreciations.index')->with('error', trans('admin/depreciations/message.does_not_exist')); + + } } diff --git a/app/Http/Controllers/Kits/CheckoutKitController.php b/app/Http/Controllers/Kits/CheckoutKitController.php index c75e4ea8f..bf4f64a8d 100644 --- a/app/Http/Controllers/Kits/CheckoutKitController.php +++ b/app/Http/Controllers/Kits/CheckoutKitController.php @@ -62,10 +62,10 @@ class CheckoutKitController extends Controller $checkout_result = $this->kitService->checkout($request, $kit, $user); if (Arr::has($checkout_result, 'errors') && count($checkout_result['errors']) > 0) { - return redirect()->back()->with('error', trans('general.checkout_error'))->with('error_messages', $checkout_result['errors']); + return redirect()->back()->with('error', trans('admin/kits/general.checkout_error'))->with('error_messages', $checkout_result['errors']); } - return redirect()->back()->with('success', trans('general.checkout_success')) + return redirect()->back()->with('success', trans('admin/kits/general.checkout_success')) ->with('assets', Arr::get($checkout_result, 'assets', null)) ->with('accessories', Arr::get($checkout_result, 'accessories', null)) ->with('consumables', Arr::get($checkout_result, 'consumables', null)); diff --git a/app/Http/Controllers/LocationsController.php b/app/Http/Controllers/LocationsController.php index c498f0992..f32e6b848 100755 --- a/app/Http/Controllers/LocationsController.php +++ b/app/Http/Controllers/LocationsController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Http\Requests\ImageUploadRequest; +use App\Models\Actionlog; use App\Models\Asset; use App\Models\Location; use App\Models\User; @@ -193,7 +194,13 @@ class LocationsController extends Controller */ public function show($id = null) : View | RedirectResponse { - $location = Location::find($id); + $location = Location::withCount('assignedAssets as assigned_assets_count') + ->withCount('assets as assets_count') + ->withCount('rtd_assets as rtd_assets_count') + ->withCount('children as children_count') + ->withCount('users as users_count') + ->withTrashed() + ->find($id); if (isset($location->id)) { return view('locations/view', compact('location')); @@ -249,6 +256,41 @@ class LocationsController extends Controller } + /** + * Restore a given Asset Model (mark as un-deleted) + * + * @author [A. Gianotto] [] + * @since [v1.0] + * @param int $id + */ + public function postRestore($id) : RedirectResponse + { + $this->authorize('create', Location::class); + + if ($location = Location::withTrashed()->find($id)) { + + if ($location->deleted_at == '') { + return redirect()->back()->with('error', trans('general.not_deleted', ['item_type' => trans('general.location')])); + } + + if ($location->restore()) { + $logaction = new Actionlog(); + $logaction->item_type = Location::class; + $logaction->item_id = $location->id; + $logaction->created_at = date('Y-m-d H:i:s'); + $logaction->user_id = auth()->id(); + $logaction->logaction('restore'); + + return redirect()->route('locations.index')->with('success', trans('admin/locations/message.restore.success')); + } + + // Check validation + return redirect()->back()->with('error', trans('general.could_not_restore', ['item_type' => trans('general.location'), 'error' => $location->getErrors()->first()])); + } + + return redirect()->back()->with('error', trans('admin/models/message.does_not_exist')); + + } public function print_all_assigned($id) : View | RedirectResponse { if ($location = Location::where('id', $id)->first()) { diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index bf0b1c3ec..abe09e8af 100755 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -50,6 +50,7 @@ class ProfileController extends Controller $user->skin = $request->input('skin'); $user->phone = $request->input('phone'); $user->enable_sounds = $request->input('enable_sounds', false); + $user->enable_confetti = $request->input('enable_confetti', false); if (! config('app.lock_passwords')) { $user->locale = $request->input('locale', 'en-US'); diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index e58647d72..31b4179b4 100755 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -637,6 +637,7 @@ class SettingsController extends Controller $setting->alert_threshold = $request->input('alert_threshold'); $setting->audit_interval = $request->input('audit_interval'); $setting->audit_warning_days = $request->input('audit_warning_days'); + $setting->due_checkin_days = $request->input('due_checkin_days'); $setting->show_alerts_in_menu = $request->input('show_alerts_in_menu', '0'); if ($setting->save()) { @@ -1203,7 +1204,7 @@ class SettingsController extends Controller * @author [A. Gianotto] [] * @since [v6.0] */ - public function postRestore($filename = null) : RedirectResponse + public function postRestore(Request $request, $filename = null): RedirectResponse { if (! config('app.lock_passwords')) { @@ -1223,13 +1224,29 @@ class SettingsController extends Controller Log::debug('Attempting to restore from: '. storage_path($path).'/'.$filename); - // run the restore command - Artisan::call('snipeit:restore', - [ + $restore_params = [ '--force' => true, '--no-progress' => true, - 'filename' => storage_path($path).'/'.$filename - ]); + 'filename' => storage_path($path) . '/' . $filename + ]; + + if ($request->input('clean')) { + Log::debug("Attempting 'clean' - first, guessing prefix..."); + Artisan::call('snipeit:restore', [ + '--sanitize-guess-prefix' => true, + 'filename' => storage_path($path) . '/' . $filename + ]); + $guess_prefix_output = Artisan::output(); + Log::debug("Sanitize output is: $guess_prefix_output"); + list($prefix, $_output) = explode("\n", $guess_prefix_output); + Log::debug("prefix is: '$prefix'"); + $restore_params['--sanitize-with-prefix'] = $prefix; + } + + // run the restore command + Artisan::call('snipeit:restore', + $restore_params + ); // If it's greater than 300, it probably worked $output = Artisan::output(); diff --git a/app/Http/Controllers/Users/BulkUsersController.php b/app/Http/Controllers/Users/BulkUsersController.php index 1a8f84b7a..5d1007e79 100644 --- a/app/Http/Controllers/Users/BulkUsersController.php +++ b/app/Http/Controllers/Users/BulkUsersController.php @@ -30,7 +30,7 @@ class BulkUsersController extends Controller * @author [A. Gianotto] [] * @since [v1.7] * @param Request $request - * @return \Illuminate\Contracts\View\View + * @return \Illuminate\Contracts\View\View | \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function edit(Request $request) @@ -116,6 +116,9 @@ class BulkUsersController extends Controller ->conditionallyAddItem('remote') ->conditionallyAddItem('ldap_import') ->conditionallyAddItem('activated') + ->conditionallyAddItem('start_date') + ->conditionallyAddItem('end_date') + ->conditionallyAddItem('city') ->conditionallyAddItem('autoassign_licenses'); @@ -146,7 +149,14 @@ class BulkUsersController extends Controller $this->update_array['company_id'] = null; } - + if ($request->input('null_start_date')=='1') { + $this->update_array['start_date'] = null; + } + + if ($request->input('null_end_date')=='1') { + $this->update_array['end_date'] = null; + } + if (! $manager_conflict) { $this->conditionallyAddItem('manager_id'); } diff --git a/app/Http/Controllers/Users/UsersController.php b/app/Http/Controllers/Users/UsersController.php index 1e203e71d..1d7fc91eb 100755 --- a/app/Http/Controllers/Users/UsersController.php +++ b/app/Http/Controllers/Users/UsersController.php @@ -186,7 +186,7 @@ class UsersController extends Controller { $this->authorize('update', User::class); - $user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($id); + $user = User::with(['assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc'])->withTrashed()->find($id); if ($user) { @@ -214,83 +214,79 @@ class UsersController extends Controller * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ - public function update(SaveUserRequest $request, $id = null) + public function update(SaveUserRequest $request, User $user) { $this->authorize('update', User::class); // This is a janky hack to prevent people from changing admin demo user data on the public demo. // The $ids 1 and 2 are special since they are seeded as superadmins in the demo seeder. // Thanks, jerks. You are why we can't have nice things. - snipe - - if ((($id == 1) || ($id == 2)) && (config('app.lock_passwords'))) { + if ((($user->id == 1) || ($user->id == 2)) && (config('app.lock_passwords'))) { return redirect()->route('users.index')->with('error', trans('general.permission_denied_superuser_demo')); } - // We need to reverse the UI specific logic for our // permissions here before we update the user. $permissions = $request->input('permissions', []); app('request')->request->set('permissions', $permissions); - $user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($id); + $user->load(['assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc'])->withTrashed(); - // User is valid - continue... - if ($user) { - $this->authorize('update', $user); + $this->authorize('update', $user); - // Figure out of this user was an admin before this edit - $orig_permissions_array = $user->decodePermissions(); - $orig_superuser = '0'; - if (is_array($orig_permissions_array)) { - if (array_key_exists('superuser', $orig_permissions_array)) { - $orig_superuser = $orig_permissions_array['superuser']; - } + // Figure out of this user was an admin before this edit + $orig_permissions_array = $user->decodePermissions(); + $orig_superuser = '0'; + if (is_array($orig_permissions_array)) { + if (array_key_exists('superuser', $orig_permissions_array)) { + $orig_superuser = $orig_permissions_array['superuser']; } + } - // Only save groups if the user is a superuser - if (auth()->user()->isSuperUser()) { - $user->groups()->sync($request->input('groups')); - } + // Only save groups if the user is a superuser + if (auth()->user()->isSuperUser()) { + $user->groups()->sync($request->input('groups')); + } - // Update the user fields - $user->username = trim($request->input('username')); - $user->email = trim($request->input('email')); - $user->first_name = $request->input('first_name'); - $user->last_name = $request->input('last_name'); - $user->two_factor_optin = $request->input('two_factor_optin') ?: 0; - $user->locale = $request->input('locale'); - $user->employee_num = $request->input('employee_num'); - $user->activated = $request->input('activated', 0); - $user->jobtitle = $request->input('jobtitle', null); - $user->phone = $request->input('phone'); - $user->location_id = $request->input('location_id', null); - $user->company_id = Company::getIdForUser($request->input('company_id', null)); - $user->manager_id = $request->input('manager_id', null); - $user->notes = $request->input('notes'); - $user->department_id = $request->input('department_id', null); - $user->address = $request->input('address', null); - $user->city = $request->input('city', null); - $user->state = $request->input('state', null); - $user->country = $request->input('country', null); - // if a user is editing themselves we should always keep activated true - $user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0); - $user->zip = $request->input('zip', null); - $user->remote = $request->input('remote', 0); - $user->vip = $request->input('vip', 0); - $user->website = $request->input('website', null); - $user->start_date = $request->input('start_date', null); - $user->end_date = $request->input('end_date', null); - $user->autoassign_licenses = $request->input('autoassign_licenses', 0); + // Update the user fields + $user->username = trim($request->input('username')); + $user->email = trim($request->input('email')); + $user->first_name = $request->input('first_name'); + $user->last_name = $request->input('last_name'); + $user->two_factor_optin = $request->input('two_factor_optin') ?: 0; + $user->locale = $request->input('locale'); + $user->employee_num = $request->input('employee_num'); + $user->activated = $request->input('activated', 0); + $user->jobtitle = $request->input('jobtitle', null); + $user->phone = $request->input('phone'); + $user->location_id = $request->input('location_id', null); + $user->company_id = Company::getIdForUser($request->input('company_id', null)); + $user->manager_id = $request->input('manager_id', null); + $user->notes = $request->input('notes'); + $user->department_id = $request->input('department_id', null); + $user->address = $request->input('address', null); + $user->city = $request->input('city', null); + $user->state = $request->input('state', null); + $user->country = $request->input('country', null); + // if a user is editing themselves we should always keep activated true + $user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0); + $user->zip = $request->input('zip', null); + $user->remote = $request->input('remote', 0); + $user->vip = $request->input('vip', 0); + $user->website = $request->input('website', null); + $user->start_date = $request->input('start_date', null); + $user->end_date = $request->input('end_date', null); + $user->autoassign_licenses = $request->input('autoassign_licenses', 0); - // Update the location of any assets checked out to this user - Asset::where('assigned_type', User::class) - ->where('assigned_to', $user->id) - ->update(['location_id' => $request->input('location_id', null)]); + // Update the location of any assets checked out to this user + Asset::where('assigned_type', User::class) + ->where('assigned_to', $user->id) + ->update(['location_id' => $request->input('location_id', null)]); - // Do we want to update the user password? - if ($request->filled('password')) { - $user->password = bcrypt($request->input('password')); - } + // Do we want to update the user password? + if ($request->filled('password')) { + $user->password = bcrypt($request->input('password')); + } // Update the location of any assets checked out to this user @@ -318,13 +314,7 @@ class UsersController extends Controller return redirect()->to(Helper::getRedirectOption($request, $user->id, 'Users')) ->with('success', trans('admin/users/message.success.update')); } - return redirect()->back()->withInput()->withErrors($user->getErrors()); - - - } - - return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', compact('id'))); } /** @@ -601,29 +591,29 @@ class UsersController extends Controller /** * Print inventory * - * @author Aladin Alaily * @since [v1.8] - * @return \Illuminate\Http\RedirectResponse + * @author Aladin Alaily */ public function printInventory($id) { $this->authorize('view', User::class); - $user = User::where('id', $id)->withTrashed()->first(); - + if ($user = User::where('id', $id)->withTrashed()->first()) { - // Make sure they can view this particular user - $this->authorize('view', $user); + $this->authorize('view', $user); + $assets = Asset::where('assigned_to', $id)->where('assigned_type', User::class)->with('model', 'model.category')->get(); + $accessories = $user->accessories()->get(); + $consumables = $user->consumables()->get(); - $assets = Asset::where('assigned_to', $id)->where('assigned_type', User::class)->with('model', 'model.category')->get(); - $accessories = $user->accessories()->get(); - $consumables = $user->consumables()->get(); + return view('users/print')->with('assets', $assets) + ->with('licenses', $user->licenses()->get()) + ->with('accessories', $accessories) + ->with('consumables', $consumables) + ->with('show_user', $user) + ->with('settings', Setting::getSettings()); + } + + return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', compact('id'))); - return view('users/print')->with('assets', $assets) - ->with('licenses', $user->licenses()->get()) - ->with('accessories', $accessories) - ->with('consumables', $consumables) - ->with('show_user', $user) - ->with('settings', Setting::getSettings()); } /** diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 73358454d..8c9289a79 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -14,6 +14,7 @@ class Kernel extends HttpKernel * @var array */ protected $middleware = [ + \App\Http\Middleware\TrustProxies::class, \App\Http\Middleware\NoSessionStore::class, \Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class, \Illuminate\Session\Middleware\StartSession::class, @@ -21,6 +22,7 @@ class Kernel extends HttpKernel \App\Http\Middleware\CheckForSetup::class, \App\Http\Middleware\CheckForDebug::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, + \App\Http\Middleware\TrimStrings::class, \App\Http\Middleware\SecurityHeaders::class, \App\Http\Middleware\PreventBackHistory::class, \Illuminate\Http\Middleware\HandleCors::class, @@ -48,6 +50,7 @@ class Kernel extends HttpKernel 'api' => [ 'auth:api', + \App\Http\Middleware\CheckLocale::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ]; diff --git a/app/Http/Requests/SaveUserRequest.php b/app/Http/Requests/SaveUserRequest.php index b38193c15..5a47362cf 100644 --- a/app/Http/Requests/SaveUserRequest.php +++ b/app/Http/Requests/SaveUserRequest.php @@ -6,6 +6,7 @@ use App\Models\Setting; use Illuminate\Contracts\Validation\Validator; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Http\Exceptions\HttpResponseException; +use App\Rules\UserCannotSwitchCompaniesIfItemsAssigned; class SaveUserRequest extends FormRequest { @@ -34,6 +35,7 @@ class SaveUserRequest extends FormRequest $rules = [ 'department_id' => 'nullable|exists:departments,id', 'manager_id' => 'nullable|exists:users,id', + 'company_id' => ['nullable','exists:companies,id'] ]; switch ($this->method()) { @@ -52,11 +54,13 @@ class SaveUserRequest extends FormRequest $rules['first_name'] = 'required|string|min:1'; $rules['username'] = 'required_unless:ldap_import,1|string|min:1'; $rules['password'] = Setting::passwordComplexityRulesSaving('update').'|confirmed'; + $rules['company_id'] = [new UserCannotSwitchCompaniesIfItemsAssigned()]; break; // Save only what's passed case 'PATCH': $rules['password'] = Setting::passwordComplexityRulesSaving('update'); + $rules['company_id'] = [new UserCannotSwitchCompaniesIfItemsAssigned()]; break; default: diff --git a/app/Http/Requests/UpdateAssetRequest.php b/app/Http/Requests/UpdateAssetRequest.php index a749e5816..1b379358f 100644 --- a/app/Http/Requests/UpdateAssetRequest.php +++ b/app/Http/Requests/UpdateAssetRequest.php @@ -4,6 +4,7 @@ namespace App\Http\Requests; use App\Http\Requests\Traits\MayContainCustomFields; use App\Models\Asset; +use App\Models\Setting; use Illuminate\Support\Facades\Gate; use Illuminate\Validation\Rule; @@ -41,6 +42,12 @@ class UpdateAssetRequest extends ImageUploadRequest ], ); + // if the purchase cost is passed in as a string **and** the digit_separator is ',' (as is common in the EU) + // then we tweak the purchase_cost rule to make it a string + if (Setting::getSettings()->digit_separator === '1.234,56' && is_string($this->input('purchase_cost'))) { + $rules['purchase_cost'] = ['nullable', 'string']; + } + return $rules; } } diff --git a/app/Http/Traits/TwoColumnUniqueUndeletedTrait.php b/app/Http/Traits/TwoColumnUniqueUndeletedTrait.php index 4aae02bfb..0cf0edb45 100644 --- a/app/Http/Traits/TwoColumnUniqueUndeletedTrait.php +++ b/app/Http/Traits/TwoColumnUniqueUndeletedTrait.php @@ -11,15 +11,17 @@ trait TwoColumnUniqueUndeletedTrait * @param string $field * @return string */ - protected function prepareTwoColumnUniqueUndeletedRule($parameters, $field) + protected function prepareTwoColumnUniqueUndeletedRule($parameters) { $column = $parameters[0]; $value = $this->{$parameters[0]}; + // This is an existing model we're updating so ignore the current ID ($this->getKey()) if ($this->exists) { return 'two_column_unique_undeleted:'.$this->table.','.$this->getKey().','.$column.','.$value; } + // This is a new record, so we can ignore the current ID return 'two_column_unique_undeleted:'.$this->table.',0,'.$column.','.$value; } } diff --git a/app/Http/Transformers/AssetsTransformer.php b/app/Http/Transformers/AssetsTransformer.php index d3a19929e..17693fccf 100644 --- a/app/Http/Transformers/AssetsTransformer.php +++ b/app/Http/Transformers/AssetsTransformer.php @@ -86,7 +86,7 @@ class AssetsTransformer 'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date, 'date'), 'deleted_at' => Helper::getFormattedDateObject($asset->deleted_at, 'datetime'), 'purchase_date' => Helper::getFormattedDateObject($asset->purchase_date, 'date'), - 'age' => $asset->purchase_date ? $asset->purchase_date->diffForHumans() : '', + 'age' => $asset->purchase_date ? $asset->purchase_date->locale(app()->getLocale())->diffForHumans() : '', 'last_checkout' => Helper::getFormattedDateObject($asset->last_checkout, 'datetime'), 'last_checkin' => Helper::getFormattedDateObject($asset->last_checkin, 'datetime'), 'expected_checkin' => Helper::getFormattedDateObject($asset->expected_checkin, 'date'), diff --git a/app/Http/Transformers/DepreciationsTransformer.php b/app/Http/Transformers/DepreciationsTransformer.php index 87e2ddaca..b3dc8c5aa 100644 --- a/app/Http/Transformers/DepreciationsTransformer.php +++ b/app/Http/Transformers/DepreciationsTransformer.php @@ -28,6 +28,9 @@ class DepreciationsTransformer 'name' => e($depreciation->name), 'months' => $depreciation->months.' '.trans('general.months'), 'depreciation_min' => $depreciation->depreciation_type === 'percent' ? $depreciation->depreciation_min.'%' : $depreciation->depreciation_min, + 'assets_count' => $depreciation->assets_count, + 'models_count' => $depreciation->models_count, + 'licenses_count' => $depreciation->licenses_count, 'created_at' => Helper::getFormattedDateObject($depreciation->created_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($depreciation->updated_at, 'datetime') ]; diff --git a/app/Importer/AssetImporter.php b/app/Importer/AssetImporter.php index 4bb887bcd..781a6311f 100644 --- a/app/Importer/AssetImporter.php +++ b/app/Importer/AssetImporter.php @@ -71,8 +71,10 @@ class AssetImporter extends ItemImporter $asset = Asset::where(['asset_tag'=> (string) $asset_tag])->first(); if ($asset) { if (! $this->updating) { - $this->log('A matching Asset '.$asset_tag.' already exists'); - return; + $exists_error = trans('general.import_asset_tag_exists', ['asset_tag' => $asset_tag]); + $this->log($exists_error); + $this->addErrorToBag($asset, 'asset_tag', $exists_error); + return $exists_error; } $this->log('Updating Asset'); diff --git a/app/Importer/Importer.php b/app/Importer/Importer.php index 973872350..c2214ef37 100644 --- a/app/Importer/Importer.php +++ b/app/Importer/Importer.php @@ -281,6 +281,13 @@ abstract class Importer } } + protected function addErrorToBag($item, $field, $error_message) + { + if ($this->errorCallback) { + call_user_func($this->errorCallback, $item, $field, [$field => [$error_message]]); + } + } + /** * Finds the user matching given data, or creates a new one if there is no match. * This is NOT used by the User Import, only for Asset/Accessory/etc where diff --git a/app/Importer/ItemImporter.php b/app/Importer/ItemImporter.php index ee680413d..29197ca5d 100644 --- a/app/Importer/ItemImporter.php +++ b/app/Importer/ItemImporter.php @@ -196,64 +196,77 @@ class ItemImporter extends Importer { $condition = array(); $asset_model_name = $this->findCsvMatch($row, 'asset_model'); + $asset_model_category = $this->findCsvMatch($row, 'category'); $asset_modelNumber = $this->findCsvMatch($row, 'model_number'); + // TODO: At the moment, this means we can't update the model number if the model name stays the same. if (! $this->shouldUpdateField($asset_model_name)) { return; } + if ((empty($asset_model_name)) && (! empty($asset_modelNumber))) { $asset_model_name = $asset_modelNumber; } elseif ((empty($asset_model_name)) && (empty($asset_modelNumber))) { $asset_model_name = 'Unknown'; } - if ((!empty($asset_model_name)) && (empty($asset_modelNumber))) { - $condition[] = ['name', '=', $asset_model_name]; - } elseif ((!empty($asset_model_name)) && (!empty($asset_modelNumber))) { - $condition[] = ['name', '=', $asset_model_name]; - $condition[] = ['model_number', '=', $asset_modelNumber]; + $asset_model = AssetModel::select('id'); + + if (!empty($asset_model_name)) { + $asset_model = $asset_model->where('name', '=', $asset_model_name); + + if (!empty($asset_modelNumber)) { + $asset_model = $asset_model->where('model_number', '=', $asset_modelNumber); + } } $editingModel = $this->updating; - $asset_model = AssetModel::where($condition)->first(); + $asset_model = $asset_model->first(); if ($asset_model) { + if (! $this->updating) { $this->log('A matching model already exists, returning it.'); - return $asset_model->id; } + $this->log('Matching Model found, updating it.'); $item = $this->sanitizeItemForStoring($asset_model, $editingModel); $item['name'] = $asset_model_name; $item['notes'] = $this->findCsvMatch($row, 'model_notes'); - if(!empty($asset_modelNumber)){ + if (!empty($asset_modelNumber)){ $item['model_number'] = $asset_modelNumber; } $asset_model->update($item); $asset_model->save(); $this->log('Asset Model Updated'); - + return $asset_model->id; - } - $this->log('No Matching Model, Creating a new one'); + } + + $this->log('No Matching Model, Creating a new one'); $asset_model = new AssetModel(); $item = $this->sanitizeItemForStoring($asset_model, $editingModel); $item['name'] = $asset_model_name; $item['model_number'] = $asset_modelNumber; $item['notes'] = $this->findCsvMatch($row, 'model_notes'); + $item['category_id'] = $this->createOrFetchCategory($asset_model_category); $asset_model->fill($item); + //$asset_model = AssetModel::firstOrNew($item); $item = null; + + if ($asset_model->save()) { $this->log('Asset Model '.$asset_model_name.' with model number '.$asset_modelNumber.' was created'); return $asset_model->id; } + $this->log('Asset Model Errors: '.$asset_model->getErrors()); $this->logError($asset_model, 'Asset Model "'.$asset_model_name.'"'); return null; diff --git a/app/Livewire/Importer.php b/app/Livewire/Importer.php index e164af36d..3c6f7990e 100644 --- a/app/Livewire/Importer.php +++ b/app/Livewire/Importer.php @@ -3,30 +3,25 @@ namespace App\Livewire; use App\Models\CustomField; -use Livewire\Component; - use App\Models\Import; use Illuminate\Support\Facades\Storage; - -use Illuminate\Foundation\Auth\Access\AuthorizesRequests; - +use Livewire\Attributes\Computed; +use Livewire\Component; class Importer extends Component { - use AuthorizesRequests; - - public $files; - - public $progress; //upload progress - '-1' means don't show + public $progress = -1; //upload progress - '-1' means don't show public $progress_message; - public $progress_bar_class; + public $progress_bar_class = 'progress-bar-warning'; public $message; //status/error message? public $message_type; //success/error? //originally from ImporterFile public $import_errors; // - public ?Import $activeFile = null; + public $activeFileId; + public $headerRow = []; + public $typeOfImport; public $importTypes; public $columnOptions; public $statusType; @@ -35,7 +30,6 @@ class Importer extends Component public $send_welcome; public $run_backup; public $field_map; // we need a separate variable for the field-mapping, because the keys in the normal array are too complicated for Livewire to understand - public $file_id; // TODO: I can't figure out *why* we need this, but it really seems like we do. I can't seem to pull the id from the activeFile for some reason? // Make these variables public - we set the properties in the constructor so we can localize them (versus the old static arrays) public $accessories_fields; @@ -51,10 +45,8 @@ class Importer extends Component 'files.*.file_path' => 'required|string', 'files.*.created_at' => 'required|string', 'files.*.filesize' => 'required|integer', - 'activeFile' => 'Import', - 'activeFile.import_type' => 'string', - 'activeFile.field_map' => 'array', - 'activeFile.header_row' => 'array', + 'headerRow' => 'array', + 'typeOfImport' => 'string', 'field_map' => 'array' ]; @@ -68,15 +60,13 @@ class Importer extends Component { $tmp = array(); if ($this->activeFile) { - $tmp = array_combine($this->activeFile->header_row, $this->field_map); + $tmp = array_combine($this->headerRow, $this->field_map); $tmp = array_filter($tmp); } return json_encode($tmp); } - - private function getColumns($type) { switch ($type) { @@ -115,76 +105,66 @@ class Importer extends Component return $results; } - public function updating($name, $new_import_type) + public function updatingTypeOfImport($type) { - if ($name == "activeFile.import_type") { - - // go through each header, find a matching field to try and map it to. - foreach ($this->activeFile->header_row as $i => $header) { - // do we have something mapped already? - if (array_key_exists($i, $this->field_map)) { - // yes, we do. Is it valid for this type of import? - // (e.g. the import type might have been changed...?) - if (array_key_exists($this->field_map[$i], $this->columnOptions[$new_import_type])) { - //yes, this key *is* valid. Continue on to the next field. - continue; - } else { - //no, this key is *INVALID* for this import type. Better set it to null - // and we'll hope that the $aliases_fields or something else picks it up. - $this->field_map[$i] = null; // fingers crossed! But it's not likely, tbh. - } // TODO - strictly speaking, this isn't necessary here I don't think. + // go through each header, find a matching field to try and map it to. + foreach ($this->headerRow as $i => $header) { + // do we have something mapped already? + if (array_key_exists($i, $this->field_map)) { + // yes, we do. Is it valid for this type of import? + // (e.g. the import type might have been changed...?) + if (array_key_exists($this->field_map[$i], $this->columnOptions[$type])) { + //yes, this key *is* valid. Continue on to the next field. + continue; + } else { + //no, this key is *INVALID* for this import type. Better set it to null + // and we'll hope that the $aliases_fields or something else picks it up. + $this->field_map[$i] = null; // fingers crossed! But it's not likely, tbh. + } // TODO - strictly speaking, this isn't necessary here I don't think. + } + // first, check for exact matches + foreach ($this->columnOptions[$type] as $v => $text) { + if (strcasecmp($text, $header) === 0) { // case-INSENSITIVe on purpose! + $this->field_map[$i] = $v; + continue 2; //don't bother with the alias check, go to the next header } - // first, check for exact matches - foreach ($this->columnOptions[$new_import_type] as $value => $text) { - if (strcasecmp($text, $header) === 0) { // case-INSENSITIVe on purpose! - $this->field_map[$i] = $value; - continue 2; //don't bother with the alias check, go to the next header - } - } - // if you got here, we didn't find a match. Try the $aliases_fields - foreach ($this->aliases_fields as $key => $alias_values) { - foreach ($alias_values as $alias_value) { - if (strcasecmp($alias_value, $header) === 0) { // aLsO CaSe-INSENSitiVE! - // Make *absolutely* sure that this key actually _exists_ in this import type - - // you can trigger this by importing accessories with a 'Warranty' column (which don't exist - // in "Accessories"!) - if (array_key_exists($key, $this->columnOptions[$new_import_type])) { - $this->field_map[$i] = $key; - continue 3; // bust out of both of these loops; as well as the surrounding one - e.g. move on to the next header - } + } + // if you got here, we didn't find a match. Try the $aliases_fields + foreach ($this->aliases_fields as $key => $alias_values) { + foreach ($alias_values as $alias_value) { + if (strcasecmp($alias_value, $header) === 0) { // aLsO CaSe-INSENSitiVE! + // Make *absolutely* sure that this key actually _exists_ in this import type - + // you can trigger this by importing accessories with a 'Warranty' column (which don't exist + // in "Accessories"!) + if (array_key_exists($key, $this->columnOptions[$type])) { + $this->field_map[$i] = $key; + continue 3; // bust out of both of these loops; as well as the surrounding one - e.g. move on to the next header } } } - // and if you got here, we got nothing. Let's recommend 'null' - $this->field_map[$i] = null; // Booooo :( } + // and if you got here, we got nothing. Let's recommend 'null' + $this->field_map[$i] = null; // Booooo :( } } - public function boot() { // FIXME - delete or undelete. - ///////$this->activeFile = null; // I do *not* understand why I have to do this, but, well, whatever. - } - - public function mount() { $this->authorize('import'); - $this->progress = -1; // '-1' means 'don't show the progressbar' - $this->progress_bar_class = 'progress-bar-warning'; $this->importTypes = [ - 'asset' => trans('general.assets'), - 'accessory' => trans('general.accessories'), + 'asset' => trans('general.assets'), + 'accessory' => trans('general.accessories'), 'consumable' => trans('general.consumables'), - 'component' => trans('general.components'), - 'license' => trans('general.licenses'), - 'user' => trans('general.users'), - 'location' => trans('general.locations'), + 'component' => trans('general.components'), + 'license' => trans('general.licenses'), + 'user' => trans('general.users'), + 'location' => trans('general.locations'), ]; /** * These are the item-type specific columns */ - $this->accessories_fields = [ + $this->accessories_fields = [ 'company' => trans('general.company'), 'location' => trans('general.location'), 'quantity' => trans('general.qty'), @@ -307,7 +287,7 @@ class Importer extends Component 'manufacturer' => trans('general.manufacturer'), ]; - $this->users_fields = [ + $this->users_fields = [ 'id' => trans('general.id'), 'company' => trans('general.company'), 'location' => trans('general.location'), @@ -332,12 +312,12 @@ class Importer extends Component 'website' => trans('general.website'), 'avatar' => trans('general.image'), 'gravatar' => trans('general.importer.gravatar'), - 'start_date' => trans('general.start_date'), - 'end_date' => trans('general.end_date'), - 'employee_num' => trans('general.employee_number'), + 'start_date' => trans('general.start_date'), + 'end_date' => trans('general.end_date'), + 'employee_num' => trans('general.employee_number'), ]; - $this->locations_fields = [ + $this->locations_fields = [ 'name' => trans('general.item_name_var', ['item' => trans('general.location')]), 'address' => trans('general.address'), 'address2' => trans('general.importer.address2'), @@ -374,6 +354,12 @@ class Importer extends Component 'model name', 'model', ], + 'eol_date' => + [ + 'eol', + 'eol date', + 'asset eol date', + ], 'gravatar' => [ 'gravatar', @@ -504,19 +490,16 @@ class Importer extends Component ]; $this->columnOptions[''] = $this->getColumns(''); //blank mode? I don't know what this is supposed to mean - foreach($this->importTypes AS $type => $name) { + foreach ($this->importTypes as $type => $name) { $this->columnOptions[$type] = $this->getColumns($type); } - if ($this->activeFile) { - $this->field_map = $this->activeFile->field_map ? array_values($this->activeFile->field_map) : []; - } } public function selectFile($id) { $this->clearMessage(); - $this->activeFile = Import::find($id); + $this->activeFileId = $id; if (!$this->activeFile) { $this->message = trans('admin/hardware/message.import.file_missing'); @@ -525,15 +508,17 @@ class Importer extends Component return; } + $this->headerRow = $this->activeFile->header_row; + $this->typeOfImport = $this->activeFile->import_type; + $this->field_map = null; - foreach($this->activeFile->header_row as $element) { - if(isset($this->activeFile->field_map[$element])) { + foreach ($this->headerRow as $element) { + if (isset($this->activeFile->field_map[$element])) { $this->field_map[] = $this->activeFile->field_map[$element]; } else { $this->field_map[] = null; // re-inject the 'nulls' if a file was imported with some 'Do Not Import' settings } } - $this->file_id = $id; $this->import_errors = null; $this->statusText = null; @@ -541,21 +526,33 @@ class Importer extends Component public function destroy($id) { - // TODO: why don't we just do File::find($id)? This seems dumb. - foreach($this->files as $file) { - if ($id == $file->id) { - if (Storage::delete('private_uploads/imports/'.$file->file_path)) { - $file->delete(); + $this->authorize('import'); - $this->message = trans('admin/hardware/message.import.file_delete_success'); - $this->message_type = 'success'; - return; - } else { - $this->message = trans('admin/hardware/message.import.file_delete_error'); - $this->message_type = 'danger'; - } - } + $import = Import::find($id); + + // Check that the import wasn't deleted after while page was already loaded... + // @todo: next up...handle the file being missing for other interactions... + // for example having an import open in two tabs, deleting it, and then changing + // the import type in the other tab. The error message below wouldn't display in that case. + if (!$import) { + $this->message = trans('admin/hardware/message.import.file_already_deleted'); + $this->message_type = 'danger'; + + return; } + + if (Storage::delete('private_uploads/imports/' . $import->file_path)) { + $import->delete(); + $this->message = trans('admin/hardware/message.import.file_delete_success'); + $this->message_type = 'success'; + + unset($this->files); + + return; + } + + $this->message = trans('admin/hardware/message.import.file_delete_error'); + $this->message_type = 'danger'; } public function clearMessage() @@ -564,11 +561,22 @@ class Importer extends Component $this->message_type = null; } + #[Computed] + public function files() + { + return Import::orderBy('id', 'desc')->get(); + } + + #[Computed] + public function activeFile() + { + return Import::find($this->activeFileId); + } + public function render() { - $this->files = Import::orderBy('id','desc')->get(); //HACK - slows down renders. return view('livewire.importer') - ->extends('layouts.default') - ->section('content'); + ->extends('layouts.default') + ->section('content'); } } diff --git a/app/Models/Asset.php b/app/Models/Asset.php index b3bf126bd..dd2f1c8e2 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -1315,7 +1315,7 @@ class Asset extends Depreciable public function scopeDueForCheckin($query, $settings) { - $interval = $settings->audit_warning_days ?? 0; + $interval = $settings->due_checkin_days ?? 0; $today = Carbon::now(); $interval_date = $today->copy()->addDays($interval)->format('Y-m-d'); @@ -1561,7 +1561,7 @@ class Asset extends Depreciable $leftJoin->on('assets_dept_users.id', '=', 'assets.assigned_to') ->where('assets.assigned_type', '=', User::class); })->where(function ($query) use ($search) { - $query->where('assets_dept_users.department_id', '=', $search); + $query->whereIn('assets_dept_users.department_id', $search); })->withTrashed()->whereNull('assets.deleted_at'); //workaround for laravel bug } @@ -1811,7 +1811,9 @@ class Asset extends Depreciable public function scopeInCategory($query, $category_id) { return $query->join('models as category_models', 'assets.model_id', '=', 'category_models.id') - ->join('categories', 'category_models.category_id', '=', 'categories.id')->where('category_models.category_id', '=', $category_id); + ->join('categories', 'category_models.category_id', '=', 'categories.id') + ->whereIn('category_models.category_id', (!is_array($category_id) ? explode(',',$category_id): $category_id)); + //->whereIn('category_models.category_id', $category_id); } /** @@ -1825,7 +1827,7 @@ class Asset extends Depreciable public function scopeByManufacturer($query, $manufacturer_id) { return $query->join('models', 'assets.model_id', '=', 'models.id') - ->join('manufacturers', 'models.manufacturer_id', '=', 'manufacturers.id')->where('models.manufacturer_id', '=', $manufacturer_id); + ->join('manufacturers', 'models.manufacturer_id', '=', 'manufacturers.id')->whereIn('models.manufacturer_id', (!is_array($manufacturer_id) ? explode(',',$manufacturer_id): $manufacturer_id)); } diff --git a/app/Models/AssetModel.php b/app/Models/AssetModel.php index 07e7a5e24..e9b859e12 100755 --- a/app/Models/AssetModel.php +++ b/app/Models/AssetModel.php @@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Storage; use Watson\Validating\ValidatingTrait; use \App\Presenters\AssetModelPresenter; +use App\Http\Traits\TwoColumnUniqueUndeletedTrait; /** * Model for Asset Models. Asset Models contain higher level @@ -21,21 +22,8 @@ class AssetModel extends SnipeModel { use HasFactory; use SoftDeletes; - protected $presenter = AssetModelPresenter::class; use Loggable, Requestable, Presentable; - - protected $table = 'models'; - protected $hidden = ['user_id', 'deleted_at']; - - // Declare the rules for the model validation - protected $rules = [ - 'name' => 'required|min:1|max:255', - 'model_number' => 'max:255|nullable', - 'min_amt' => 'integer|min:0|nullable', - 'category_id' => 'required|integer|exists:categories,id', - 'manufacturer_id' => 'integer|exists:manufacturers,id|nullable', - 'eol' => 'integer:min:0|max:240|nullable', - ]; + use TwoColumnUniqueUndeletedTrait; /** * Whether the model should inject its identifier to the unique @@ -44,8 +32,26 @@ class AssetModel extends SnipeModel * * @var bool */ + protected $injectUniqueIdentifier = true; use ValidatingTrait; + protected $table = 'models'; + protected $hidden = ['user_id', 'deleted_at']; + protected $presenter = AssetModelPresenter::class; + + // Declare the rules for the model validation + + + protected $rules = [ + 'name' => 'string|required|min:1|max:255|two_column_unique_undeleted:model_number', + 'model_number' => 'string|max:255|nullable|two_column_unique_undeleted:name', + 'min_amt' => 'integer|min:0|nullable', + 'category_id' => 'required|integer|exists:categories,id', + 'manufacturer_id' => 'integer|exists:manufacturers,id|nullable', + 'eol' => 'integer:min:0|max:240|nullable', + ]; + + /** * The attributes that are mass assignable. @@ -73,7 +79,12 @@ class AssetModel extends SnipeModel * * @var array */ - protected $searchableAttributes = ['name', 'model_number', 'notes', 'eol']; + protected $searchableAttributes = [ + 'name', + 'model_number', + 'notes', + 'eol' + ]; /** * The relations and their attributes that should be included when searching the model. @@ -86,6 +97,9 @@ class AssetModel extends SnipeModel 'manufacturer' => ['name'], ]; + + + /** * Establishes the model -> assets relationship * diff --git a/app/Models/Consumable.php b/app/Models/Consumable.php index 944ac5bfd..3b33035b1 100644 --- a/app/Models/Consumable.php +++ b/app/Models/Consumable.php @@ -425,6 +425,20 @@ class Consumable extends SnipeModel return $query->leftJoin('companies', 'consumables.company_id', '=', 'companies.id')->orderBy('companies.name', $order); } + /** + * Query builder scope to order on remaining + * + * @param \Illuminate\Database\Query\Builder $query Query builder instance + * @param string $order Order + * + * @return \Illuminate\Database\Query\Builder Modified query builder + */ + public function scopeOrderRemaining($query, $order) + { + $order_by = 'consumables.qty - consumables_users_count ' . $order; + return $query->orderByRaw($order_by); + } + /** * Query builder scope to order on supplier * diff --git a/app/Models/Depreciation.php b/app/Models/Depreciation.php index 9faa1b86e..7aceddf7c 100755 --- a/app/Models/Depreciation.php +++ b/app/Models/Depreciation.php @@ -75,4 +75,17 @@ class Depreciation extends SnipeModel { return $this->hasMany(\App\Models\License::class, 'depreciation_id'); } + + /** + * Establishes the depreciation -> assets relationship + * + * @author A. Gianotto + * @since [v5.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function assets() + { + return $this->hasManyThrough(\App\Models\Asset::class, \App\Models\AssetModel::class, 'depreciation_id', 'model_id'); + } + } diff --git a/app/Models/Location.php b/app/Models/Location.php index f08a51a98..e6c310979 100755 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -33,7 +33,7 @@ class Location extends SnipeModel 'country' => 'min:2|max:191|nullable', 'zip' => 'max:10|nullable', 'manager_id' => 'exists:users,id|nullable', - 'parent_id' => 'non_circular:locations,id', + 'parent_id' => 'nullable|exists:locations,id|non_circular:locations,id', ]; protected $casts = [ diff --git a/app/Models/Setting.php b/app/Models/Setting.php index 1a25735e0..d775be81c 100755 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -74,7 +74,10 @@ class Setting extends Model 'login_remote_user_header_name' => 'string|nullable', 'thumbnail_max_h' => 'numeric|max:500|min:25', 'pwd_secure_min' => 'numeric|required|min:8', + 'alert_threshold' => 'numeric|nullable', + 'alert_interval' => 'numeric|nullable', 'audit_warning_days' => 'numeric|nullable', + 'due_checkin_days' => 'numeric|nullable', 'audit_interval' => 'numeric|nullable', 'custom_forgot_pass_url' => 'url|nullable', 'privacy_policy_link' => 'nullable|url', diff --git a/app/Models/SnipeModel.php b/app/Models/SnipeModel.php index af12c3d29..f26946d22 100644 --- a/app/Models/SnipeModel.php +++ b/app/Models/SnipeModel.php @@ -21,6 +21,11 @@ class SnipeModel extends Model */ public function setPurchaseCostAttribute($value) { + if (is_float($value)) { + //value is *already* a floating-point number. Just assign it directly + $this->attributes['purchase_cost'] = $value; + return; + } $value = Helper::ParseCurrency($value); if ($value == 0) { diff --git a/app/Presenters/AssetPresenter.php b/app/Presenters/AssetPresenter.php index b86135f33..e55cb00c2 100644 --- a/app/Presenters/AssetPresenter.php +++ b/app/Presenters/AssetPresenter.php @@ -579,6 +579,6 @@ class AssetPresenter extends Presenter public function glyph() { - return ''; + return ''; } } diff --git a/app/Presenters/CompanyPresenter.php b/app/Presenters/CompanyPresenter.php index 7603191fc..bcb77c7eb 100644 --- a/app/Presenters/CompanyPresenter.php +++ b/app/Presenters/CompanyPresenter.php @@ -65,40 +65,46 @@ class CompanyPresenter extends Presenter 'field' => 'users_count', 'searchable' => false, 'sortable' => true, - 'title' => '', + 'title' => trans('general.users'), 'visible' => true, + 'class' => 'css-users', ], [ 'field' => 'assets_count', 'searchable' => false, 'sortable' => true, - 'title' => '', + 'title' => trans('general.assets'), 'visible' => true, + 'class' => 'css-barcode', ], [ 'field' => 'licenses_count', 'searchable' => false, 'sortable' => true, + 'title' => trans('general.licenses'), 'visible' => true, - 'title' => ' ', + 'class' => 'css-license', ], [ 'field' => 'accessories_count', 'searchable' => false, 'sortable' => true, + 'title' => trans('general.accessories'), 'visible' => true, - 'title' => ' ', + 'class' => 'css-accessory', ], [ 'field' => 'consumables_count', 'searchable' => false, 'sortable' => true, + 'title' => trans('general.consumables'), 'visible' => true, - 'title' => ' ', + 'class' => 'css-consumable', ], [ 'field' => 'components_count', 'searchable' => false, 'sortable' => true, + 'title' => trans('general.components'), 'visible' => true, - 'title' => ' ', + 'class' => 'css-component', ], [ 'field' => 'updated_at', 'searchable' => false, diff --git a/app/Presenters/ConsumablePresenter.php b/app/Presenters/ConsumablePresenter.php index d3e73de1c..dc22c69e2 100644 --- a/app/Presenters/ConsumablePresenter.php +++ b/app/Presenters/ConsumablePresenter.php @@ -75,13 +75,13 @@ class ConsumablePresenter extends Presenter ], [ 'field' => 'qty', 'searchable' => false, - 'sortable' => false, + 'sortable' => true, 'title' => trans('admin/components/general.total'), 'visible' => true, ], [ 'field' => 'remaining', 'searchable' => false, - 'sortable' => false, + 'sortable' => true, 'title' => trans('admin/components/general.remaining'), 'visible' => true, ], [ diff --git a/app/Presenters/DepreciationPresenter.php b/app/Presenters/DepreciationPresenter.php index 9df1fe132..cfba53162 100644 --- a/app/Presenters/DepreciationPresenter.php +++ b/app/Presenters/DepreciationPresenter.php @@ -46,6 +46,26 @@ class DepreciationPresenter extends Presenter "title" => trans('admin/depreciations/table.depreciation_min'), "visible" => true, ], + [ + 'field' => 'assets_count', + 'searchable' => false, + 'sortable' => true, + 'title' => trans('general.assets'), + 'visible' => true, + ], + [ + 'field' => 'models_count', + 'searchable' => false, + 'sortable' => true, + 'title' => trans('general.asset_models'), + 'visible' => true, + ], [ + 'field' => 'licenses_count', + 'searchable' => false, + 'sortable' => true, + 'title' => trans('general.licenses'), + 'visible' => true, + ], [ 'field' => 'actions', 'searchable' => false, diff --git a/app/Presenters/DepreciationReportPresenter.php b/app/Presenters/DepreciationReportPresenter.php index 50a8b73b5..690538561 100644 --- a/app/Presenters/DepreciationReportPresenter.php +++ b/app/Presenters/DepreciationReportPresenter.php @@ -394,6 +394,6 @@ class DepreciationReportPresenter extends Presenter public function glyph() { - return ''; + return ''; } } diff --git a/app/Presenters/LocationPresenter.php b/app/Presenters/LocationPresenter.php index 56d710ac9..d6bbe0db1 100644 --- a/app/Presenters/LocationPresenter.php +++ b/app/Presenters/LocationPresenter.php @@ -235,7 +235,7 @@ class LocationPresenter extends Presenter public function glyph() { - return ''; + return ''; } public function fullName() diff --git a/app/Presenters/ManufacturerPresenter.php b/app/Presenters/ManufacturerPresenter.php index 3e36cbcde..07a22c9ea 100644 --- a/app/Presenters/ManufacturerPresenter.php +++ b/app/Presenters/ManufacturerPresenter.php @@ -94,36 +94,36 @@ class ManufacturerPresenter extends Presenter 'searchable' => false, 'sortable' => true, 'switchable' => true, - 'title' => ' ' - .'', + 'title' => trans('general.assets'), 'visible' => true, + 'class' => 'css-barcode', ], [ 'field' => 'licenses_count', 'searchable' => false, 'sortable' => true, 'switchable' => true, - 'title' => ' ' - .'', + 'title' => trans('general.licenses'), 'visible' => true, + 'class' => 'css-license', ], [ 'field' => 'consumables_count', 'searchable' => false, 'sortable' => true, 'switchable' => true, - 'title' => ' ' - .'', + 'title' => trans('general.consumables'), 'visible' => true, + 'class' => 'css-consumable', ], [ 'field' => 'accessories_count', 'searchable' => false, 'sortable' => true, 'switchable' => true, - 'title' => ' ' - .'', + 'title' => trans('general.accessories'), 'visible' => true, + 'class' => 'css-accessory', ], [ 'field' => 'created_at', diff --git a/app/Presenters/UserPresenter.php b/app/Presenters/UserPresenter.php index 7054676a9..635eaa86a 100644 --- a/app/Presenters/UserPresenter.php +++ b/app/Presenters/UserPresenter.php @@ -492,6 +492,6 @@ class UserPresenter extends Presenter public function glyph() { - return ''; + return ''; } } diff --git a/app/Providers/ValidationServiceProvider.php b/app/Providers/ValidationServiceProvider.php index 041aaad98..1f3abca8a 100644 --- a/app/Providers/ValidationServiceProvider.php +++ b/app/Providers/ValidationServiceProvider.php @@ -6,10 +6,7 @@ use App\Models\CustomField; use App\Models\Department; use App\Models\Setting; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Crypt; -use Illuminate\Support\Facades\Log; use Illuminate\Support\ServiceProvider; -use Illuminate\Validation\Rule; use Illuminate\Support\Facades\Validator; /** @@ -91,18 +88,26 @@ class ValidationServiceProvider extends ServiceProvider * * $parameters[0] - the name of the first table we're looking at * $parameters[1] - the ID (this will be 0 on new creations) - * $parameters[2] - the name of the second table we're looking at + * $parameters[2] - the name of the second field we're looking at * $parameters[3] - the value that the request is passing for the second table we're * checking for uniqueness across * */ Validator::extend('two_column_unique_undeleted', function ($attribute, $value, $parameters, $validator) { + if (count($parameters)) { + $count = DB::table($parameters[0]) - ->select('id')->where($attribute, '=', $value) - ->whereNull('deleted_at') - ->where('id', '!=', $parameters[1]) - ->where($parameters[2], $parameters[3])->count(); + ->select('id') + ->where($attribute, '=', $value) + ->where('id', '!=', $parameters[1]); + + if ($parameters[3]!='') { + $count = $count->where($parameters[2], $parameters[3]); + } + + $count = $count->whereNull('deleted_at') + ->count(); return $count < 1; } diff --git a/app/Rules/UserCannotSwitchCompaniesIfItemsAssigned.php b/app/Rules/UserCannotSwitchCompaniesIfItemsAssigned.php new file mode 100644 index 000000000..c3dd58f42 --- /dev/null +++ b/app/Rules/UserCannotSwitchCompaniesIfItemsAssigned.php @@ -0,0 +1,23 @@ +route('user')->id); + if (($value) && ($user->allAssignedCount() > 0) && (Setting::getSettings()->full_multiple_companies_support)) { + $fail(trans('admin/users/message.error.multi_company_items_assigned')); + } + } +} diff --git a/composer.json b/composer.json index 5467e94e3..6d8931257 100644 --- a/composer.json +++ b/composer.json @@ -78,7 +78,7 @@ "fakerphp/faker": "^1.16", "larastan/larastan": "^2.9", "mockery/mockery": "^1.4", - "nunomaduro/phpinsights": "^2.7", + "nunomaduro/phpinsights": "^2.11", "php-mock/php-mock-phpunit": "^2.10", "phpunit/phpunit": "^10.0", "squizlabs/php_codesniffer": "^3.5", @@ -120,7 +120,9 @@ ], "post-create-project-cmd": [ "php artisan key:generate" - ] + ], + "coverage:herd:clover": "herd coverage vendor/bin/phpunit --coverage-clover tests/coverage/clover.xml", + "coverage:herd:html": "herd coverage vendor/bin/phpunit --coverage-html tests/coverage/html" }, "config": { "preferred-install": "dist", diff --git a/composer.lock b/composer.lock index f7c46672d..715070df0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "35c741a2d3300848d758b187554b5b17", + "content-hash": "3819ab4ef72eb77fabe494c0e746b83b", "packages": [ { "name": "alek13/slack", diff --git a/config/app.php b/config/app.php index 060d82d52..bc74b4dd0 100755 --- a/config/app.php +++ b/config/app.php @@ -372,7 +372,9 @@ return [ 'Google2FA' => PragmaRX\Google2FALaravel\Facade::class, 'Image' => Intervention\Image\ImageServiceProvider::class, 'Carbon' => Carbon\Carbon::class, - 'Helper' => App\Helpers\Helper::class, // makes it much easier to use 'Helper::blah' in blades (which is where we usually use this) + 'Helper' => App\Helpers\Helper::class, + // makes it much easier to use 'Helper::blah' in blades (which is where we usually use this) + 'Icon' => App\Helpers\IconHelper::class, 'Socialite' => Laravel\Socialite\Facades\Socialite::class, diff --git a/config/backup.php b/config/backup.php index 3571329b2..089087733 100644 --- a/config/backup.php +++ b/config/backup.php @@ -237,4 +237,6 @@ return [ ], ], + 'sanitize_by_default' => env('DB_SANITIZE_BY_DEFAULT', false), + ]; diff --git a/config/version.php b/config/version.php index 4079451d3..8ba20219a 100644 --- a/config/version.php +++ b/config/version.php @@ -1,10 +1,10 @@ 'v7.0.10', - 'full_app_version' => 'v7.0.10 - build 14684-gc2bcc2e2d', - 'build_version' => '14684', + 'app_version' => 'v7.0.11', + 'full_app_version' => 'v7.0.11 - build 15044-g46ed07642', + 'build_version' => '15044', 'prerelease_version' => '', - 'hash_version' => 'gc2bcc2e2d', - 'full_hash' => 'v7.0.10-311-gc2bcc2e2d', - 'branch' => 'master', + 'hash_version' => 'g46ed07642', + 'full_hash' => 'v7.0.11-133-g46ed07642', + 'branch' => 'develop', ); \ No newline at end of file diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 656fc8672..151c11431 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -296,6 +296,11 @@ class UserFactory extends Factory return $this->appendPermission(['reports.view' => '1']); } + public function canImport() + { + return $this->appendPermission(['import' => '1']); + } + private function appendPermission(array $permission) { return $this->state(function ($currentState) use ($permission) { diff --git a/database/migrations/2024_08_15_111816_add_confetti_to_users.php b/database/migrations/2024_08_15_111816_add_confetti_to_users.php new file mode 100644 index 000000000..a3aa85d4f --- /dev/null +++ b/database/migrations/2024_08_15_111816_add_confetti_to_users.php @@ -0,0 +1,28 @@ +boolean('enable_confetti')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('enable_confetti'); + }); + } +}; diff --git a/database/migrations/2024_08_16_104137_add_due_checkin_days_to_settings.php b/database/migrations/2024_08_16_104137_add_due_checkin_days_to_settings.php new file mode 100644 index 000000000..09314b88f --- /dev/null +++ b/database/migrations/2024_08_16_104137_add_due_checkin_days_to_settings.php @@ -0,0 +1,28 @@ +integer('due_checkin_days')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('settings', function (Blueprint $table) { + $table->dropColumn('due_checkin_days'); + }); + } +}; diff --git a/dev.docker-compose.yml b/dev.docker-compose.yml index 15272ce5c..6cf4a1e2f 100644 --- a/dev.docker-compose.yml +++ b/dev.docker-compose.yml @@ -1,3 +1,5 @@ +# Compose file to spin up a local Snipe-IT for development. + version: '3' services: @@ -7,44 +9,40 @@ services: dockerfile: Dockerfile.alpine container_name: snipeit ports: - - "8000:80" - volumes: - - ./storage/logs:/var/www/html/storage/logs + - "8000:80" depends_on: - - mariadb - - redis + redis: + # The default needs to be stated. + condition: service_started + mariadb: + condition: service_healthy + restart: true env_file: - - .env.docker - networks: - - snipeit-backend + - .env.dev.docker mariadb: - image: mariadb:10.6.4-focal + image: mariadb:11.5.2 volumes: - - db:/var/lib/mysql + - db:/var/lib/mysql env_file: - - .env.docker - networks: - - snipeit-backend + - .env.dev.docker ports: - "3306:3306" + healthcheck: + # https://mariadb.com/kb/en/using-healthcheck-sh/#compose-file-example + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 5s + timeout: 2s + retries: 5 redis: - image: redis:6.2.5-buster - networks: - - snipeit-backend + image: redis:7.4.0 mailhog: image: mailhog/mailhog:v1.0.1 ports: - # - 1025:1025 - - "8025:8025" - networks: - - snipeit-backend - + # - 1025:1025 + - "8025:8025" volumes: db: {} - -networks: - snipeit-backend: {} diff --git a/docker-compose.yml b/docker-compose.yml index c7c1983a4..d830a9436 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,13 @@ +# Compose file for production. + volumes: db_data: storage: services: app: - image: snipe/snipe-it:${APP_VERSION:-v6.4.1} - restart: always + image: snipe/snipe-it:${APP_VERSION:-v7.0.11} + restart: unless-stopped volumes: - storage:/var/lib/snipeit ports: @@ -18,8 +20,8 @@ services: - .env db: - image: mariadb:10.6.4-focal - restart: always + image: mariadb:11.5.2 + restart: unless-stopped volumes: - db_data:/var/lib/mysql environment: @@ -28,7 +30,8 @@ services: MYSQL_PASSWORD: ${DB_PASSWORD} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} healthcheck: - test: mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD + # https://mariadb.com/kb/en/using-healthcheck-sh/#compose-file-example + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 5s timeout: 1s retries: 5 diff --git a/docker/startup.sh b/docker/startup.sh index 62002a2ba..2f6be7b0f 100644 --- a/docker/startup.sh +++ b/docker/startup.sh @@ -1,7 +1,48 @@ #!/bin/bash +# Cribbed from nextcloud docker official repo +# https://github.com/nextcloud/docker/blob/master/docker-entrypoint.sh +# usage: file_env VAR [DEFAULT] +# ie: file_env 'XYZ_DB_PASSWORD' 'example' +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + local varValue=$(env | grep -E "^${var}=" | sed -E -e "s/^${var}=//") + local fileVarValue=$(env | grep -E "^${fileVar}=" | sed -E -e "s/^${fileVar}=//") + if [ -n "${varValue}" ] && [ -n "${fileVarValue}" ]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + if [ -n "${varValue}" ]; then + export "$var"="${varValue}" + elif [ -n "${fileVarValue}" ]; then + export "$var"="$(cat "${fileVarValue}")" + elif [ -n "${def}" ]; then + export "$var"="$def" + fi + unset "$fileVar" +} + +# Add docker secrets support for the variables below: +file_env APP_KEY +file_env DB_HOST +file_env DB_PORT +file_env DB_DATABASE +file_env DB_USERNAME +file_env DB_PASSWORD +file_env REDIS_HOST +file_env REDIS_PASSWORD +file_env REDIS_PORT +file_env MAIL_HOST +file_env MAIL_PORT +file_env MAIL_USERNAME +file_env MAIL_PASSWORD + # fix key if needed -if [ -z "$APP_KEY" ] +if [ -z "$APP_KEY" -a -z "$APP_KEY_FILE" ] then echo "Please re-run this container with an environment variable \$APP_KEY" echo "An example APP_KEY you could use is: " diff --git a/docker/entrypoint_alpine.sh b/docker/startup_alpine.sh similarity index 60% rename from docker/entrypoint_alpine.sh rename to docker/startup_alpine.sh index c1a75b0cb..d9d6c8a9a 100644 --- a/docker/entrypoint_alpine.sh +++ b/docker/startup_alpine.sh @@ -1,7 +1,48 @@ #!/bin/sh +# Cribbed from nextcloud docker official repo +# https://github.com/nextcloud/docker/blob/master/docker-entrypoint.sh +# usage: file_env VAR [DEFAULT] +# ie: file_env 'XYZ_DB_PASSWORD' 'example' +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + local varValue=$(env | grep -E "^${var}=" | sed -E -e "s/^${var}=//") + local fileVarValue=$(env | grep -E "^${fileVar}=" | sed -E -e "s/^${fileVar}=//") + if [ -n "${varValue}" ] && [ -n "${fileVarValue}" ]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + if [ -n "${varValue}" ]; then + export "$var"="${varValue}" + elif [ -n "${fileVarValue}" ]; then + export "$var"="$(cat "${fileVarValue}")" + elif [ -n "${def}" ]; then + export "$var"="$def" + fi + unset "$fileVar" +} + +# Add docker secrets support for the variables below: +file_env APP_KEY +file_env DB_HOST +file_env DB_PORT +file_env DB_DATABASE +file_env DB_USERNAME +file_env DB_PASSWORD +file_env REDIS_HOST +file_env REDIS_PASSWORD +file_env REDIS_PORT +file_env MAIL_HOST +file_env MAIL_PORT +file_env MAIL_USERNAME +file_env MAIL_PASSWORD + # fix key if needed -if [ -z "$APP_KEY" ] +if [ -z "$APP_KEY" -a -z "$APP_KEY_FILE" ] then echo "Please re-run this container with an environment variable \$APP_KEY" echo "An example APP_KEY you could use is: " diff --git a/docker/docker-entrypoint.sh b/docker/startup_alpine_fpm.sh similarity index 100% rename from docker/docker-entrypoint.sh rename to docker/startup_alpine_fpm.sh diff --git a/package-lock.json b/package-lock.json index afe7b5f7c..a5296ae5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,14 +15,15 @@ "bootstrap-colorpicker": "^2.5.3", "bootstrap-datepicker": "^1.10.0", "bootstrap-less": "^3.3.8", - "bootstrap-table": "1.23.0", + "bootstrap-table": "1.23.2", + "canvas-confetti": "^1.9.3", "chart.js": "^2.9.4", "clipboard": "^2.0.11", "css-loader": "^5.0.0", "ekko-lightbox": "^5.1.1", "imagemin": "^8.0.1", "jquery-slimscroll": "^1.3.8", - "jquery-ui": "^1.13.3", + "jquery-ui": "^1.14.0", "jquery-validation": "^1.21.0", "jquery.iframe-transport": "^1.0.0", "jspdf-autotable": "^3.8.2", @@ -36,7 +37,7 @@ "signature_pad": "^4.2.0", "tableexport.jquery.plugin": "1.30.0", "tether": "^1.4.0", - "webpack": "^5.92.0" + "webpack": "^5.94.0" }, "devDependencies": { "all-contributors-cli": "^6.26.1", @@ -2104,25 +2105,10 @@ "@types/node": "*" } }, - "node_modules/@types/eslint": { - "version": "8.56.10", - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, "node_modules/@types/express": { "version": "4.17.21", @@ -3692,9 +3678,9 @@ "license": "MIT" }, "node_modules/bootstrap-table": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/bootstrap-table/-/bootstrap-table-1.23.0.tgz", - "integrity": "sha512-fAIhu2CAqMsZWkzeFxXyh0yQA2DMBdB0tCdr1iF6bKr3c/Hf79cw5PykNt7NdtqLz/a0p192S8EKyT5lG4yrpw==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/bootstrap-table/-/bootstrap-table-1.23.2.tgz", + "integrity": "sha512-1IFiWFZzbKlleXgYEHdwHkX6rxlQMEx2N1tA8rJK/j08pI+NjIGnxFeXUL26yQLQ0U135eis/BX3OV1+anY25g==", "peerDependencies": { "jquery": "3" } @@ -4098,6 +4084,15 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-confetti": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.3.tgz", + "integrity": "sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/canvg": { "version": "3.0.10", "license": "MIT", @@ -5300,9 +5295,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", - "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -7057,10 +7052,11 @@ "license": "BSD-2-Clause" }, "node_modules/jquery-ui": { - "version": "1.13.3", - "license": "MIT", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.14.0.tgz", + "integrity": "sha512-mPfYKBoRCf0MzaT2cyW5i3IuZ7PfTITaasO5OFLAQxrHuI+ZxruPa+4/K1OMNT8oElLWGtIxc9aRbyw20BKr8g==", "dependencies": { - "jquery": ">=1.8.0 <4.0.0" + "jquery": ">=1.12.0 <5.0.0" } }, "node_modules/jquery-validation": { @@ -10869,11 +10865,10 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.92.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", - "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", @@ -10882,7 +10877,7 @@ "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", diff --git a/package.json b/package.json index 95ccf94f8..3d8e3eda2 100644 --- a/package.json +++ b/package.json @@ -35,14 +35,15 @@ "bootstrap-colorpicker": "^2.5.3", "bootstrap-datepicker": "^1.10.0", "bootstrap-less": "^3.3.8", - "bootstrap-table": "1.23.0", + "bootstrap-table": "1.23.2", + "canvas-confetti": "^1.9.3", "chart.js": "^2.9.4", "clipboard": "^2.0.11", "css-loader": "^5.0.0", "ekko-lightbox": "^5.1.1", "imagemin": "^8.0.1", "jquery-slimscroll": "^1.3.8", - "jquery-ui": "^1.13.3", + "jquery-ui": "^1.14.0", "jquery-validation": "^1.21.0", "jquery.iframe-transport": "^1.0.0", "jspdf-autotable": "^3.8.2", @@ -56,6 +57,6 @@ "signature_pad": "^4.2.0", "tableexport.jquery.plugin": "1.30.0", "tether": "^1.4.0", - "webpack": "^5.92.0" + "webpack": "^5.94.0" } } diff --git a/public/css/build/app.css b/public/css/build/app.css index bcc317f06..d9f77a0b5 100644 --- a/public/css/build/app.css +++ b/public/css/build/app.css @@ -697,6 +697,9 @@ body { font-size: 14px; white-space: normal; } +.modal-warning .modal-help { + color: #fff8af; +} .bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading { z-index: 0 !important; } @@ -828,19 +831,36 @@ body { } .select2-selection--multiple { border-color: #d2d6de !important; - height: 34px; + overflow-y: auto; } .select2-selection__choice { border-radius: 0px !important; } +.select2-search select2-search--inline { + height: 35px !important; + float: left; + margin: 0; +} +.select2-selection__rendered needsclick { + color: red; +} +.select2-results__option { + padding: 5px; + -moz-user-select: none; + user-select: none; + -webkit-user-select: none; + margin: 0px; +} img.navbar-brand-img, .navbar-brand > img { float: left; padding: 5px 5px 5px 0; max-height: 50px; } -.input-daterange { - border-radius: 0px; +.input-daterange, +.input-daterange input:first-child, +.input-daterange input:last-child { + border-radius: 0px !important; } .btn.bg-maroon, .btn.bg-purple { @@ -1006,6 +1026,7 @@ th.css-consumable > .th-inner, th.css-envelope > .th-inner, th.css-users > .th-inner, th.css-location > .th-inner, +th.css-component > .th-inner, th.css-accessory > .th-inner { font-size: 0px; line-height: 0.75 !important; @@ -1021,6 +1042,7 @@ th.css-consumable > .th-inner::before, th.css-envelope > .th-inner::before, th.css-users > .th-inner::before, th.css-location > .th-inner::before, +th.css-component > .th-inner::before, th.css-accessory > .th-inner::before { display: inline-block; font-size: 20px; @@ -1074,6 +1096,11 @@ th.css-location > .th-inner::before { font-size: 19px; margin-bottom: 0px; } +th.css-component > .th-inner::before { + content: "\f0a0"; + font-family: "Font Awesome 5 Free"; + font-weight: 500; +} .small-box .inner { padding-left: 15px; padding-right: 15px; @@ -1122,15 +1149,37 @@ th.css-location > .th-inner::before { margin-top: 50px; } } +@media screen and (max-width: 992px) { + .info-stack-container { + display: flex; + flex-direction: column; + } + .col-md-3.col-xs-12.col-sm-push-9.info-stack { + left: auto; + order: 1; + } + .col-md-9.col-xs-12.col-sm-pull-3.info-stack { + right: auto; + order: 2; + } + .info-stack-container > .col-md-9.col-xs-12.col-sm-pull-3.info-stack > .row-new-striped > .row > .col-sm-2 { + width: auto; + float: none; + } +} @media screen and (max-width: 1318px) and (min-width: 1200px) { - .box { + .admin.box { height: 170px; } } -.ellipsis { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; +@media screen and (max-width: 1494px) and (min-width: 1200px) { + .dashboard.small-box { + white-space: nowrap; + text-overflow: ellipsis; + max-width: 188px; + display: block; + overflow: hidden; + } } /** Form-stuff overrides for checkboxes and stuff **/ label.form-control { diff --git a/public/css/build/overrides.css b/public/css/build/overrides.css index 7b9e7d1b9..c927401a4 100644 --- a/public/css/build/overrides.css +++ b/public/css/build/overrides.css @@ -330,6 +330,9 @@ body { font-size: 14px; white-space: normal; } +.modal-warning .modal-help { + color: #fff8af; +} .bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading { z-index: 0 !important; } @@ -461,19 +464,36 @@ body { } .select2-selection--multiple { border-color: #d2d6de !important; - height: 34px; + overflow-y: auto; } .select2-selection__choice { border-radius: 0px !important; } +.select2-search select2-search--inline { + height: 35px !important; + float: left; + margin: 0; +} +.select2-selection__rendered needsclick { + color: red; +} +.select2-results__option { + padding: 5px; + -moz-user-select: none; + user-select: none; + -webkit-user-select: none; + margin: 0px; +} img.navbar-brand-img, .navbar-brand > img { float: left; padding: 5px 5px 5px 0; max-height: 50px; } -.input-daterange { - border-radius: 0px; +.input-daterange, +.input-daterange input:first-child, +.input-daterange input:last-child { + border-radius: 0px !important; } .btn.bg-maroon, .btn.bg-purple { @@ -639,6 +659,7 @@ th.css-consumable > .th-inner, th.css-envelope > .th-inner, th.css-users > .th-inner, th.css-location > .th-inner, +th.css-component > .th-inner, th.css-accessory > .th-inner { font-size: 0px; line-height: 0.75 !important; @@ -654,6 +675,7 @@ th.css-consumable > .th-inner::before, th.css-envelope > .th-inner::before, th.css-users > .th-inner::before, th.css-location > .th-inner::before, +th.css-component > .th-inner::before, th.css-accessory > .th-inner::before { display: inline-block; font-size: 20px; @@ -707,6 +729,11 @@ th.css-location > .th-inner::before { font-size: 19px; margin-bottom: 0px; } +th.css-component > .th-inner::before { + content: "\f0a0"; + font-family: "Font Awesome 5 Free"; + font-weight: 500; +} .small-box .inner { padding-left: 15px; padding-right: 15px; @@ -755,15 +782,37 @@ th.css-location > .th-inner::before { margin-top: 50px; } } +@media screen and (max-width: 992px) { + .info-stack-container { + display: flex; + flex-direction: column; + } + .col-md-3.col-xs-12.col-sm-push-9.info-stack { + left: auto; + order: 1; + } + .col-md-9.col-xs-12.col-sm-pull-3.info-stack { + right: auto; + order: 2; + } + .info-stack-container > .col-md-9.col-xs-12.col-sm-pull-3.info-stack > .row-new-striped > .row > .col-sm-2 { + width: auto; + float: none; + } +} @media screen and (max-width: 1318px) and (min-width: 1200px) { - .box { + .admin.box { height: 170px; } } -.ellipsis { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; +@media screen and (max-width: 1494px) and (min-width: 1200px) { + .dashboard.small-box { + white-space: nowrap; + text-overflow: ellipsis; + max-width: 188px; + display: block; + overflow: hidden; + } } /** Form-stuff overrides for checkboxes and stuff **/ label.form-control { diff --git a/public/css/dist/all.css b/public/css/dist/all.css index 47beba874..134919d74 100644 --- a/public/css/dist/all.css +++ b/public/css/dist/all.css @@ -20968,7 +20968,7 @@ hr { .ekko-lightbox{display:-ms-flexbox!important;display:flex!important;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;padding-right:0!important}.ekko-lightbox-container{position:relative}.ekko-lightbox-container>div.ekko-lightbox-item{position:absolute;top:0;left:0;bottom:0;right:0;width:100%}.ekko-lightbox iframe{width:100%;height:100%}.ekko-lightbox-nav-overlay{z-index:1;position:absolute;top:0;left:0;width:100%;height:100%;display:-ms-flexbox;display:flex}.ekko-lightbox-nav-overlay a{-ms-flex:1;flex:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;opacity:0;transition:opacity .5s;color:#fff;font-size:30px;z-index:1}.ekko-lightbox-nav-overlay a>*{-ms-flex-positive:1;flex-grow:1}.ekko-lightbox-nav-overlay a>:focus{outline:none}.ekko-lightbox-nav-overlay a span{padding:0 30px}.ekko-lightbox-nav-overlay a:last-child span{text-align:right}.ekko-lightbox-nav-overlay a:hover{text-decoration:none}.ekko-lightbox-nav-overlay a:focus{outline:none}.ekko-lightbox-nav-overlay a.disabled{cursor:default;visibility:hidden}.ekko-lightbox a:hover{opacity:1;text-decoration:none}.ekko-lightbox .modal-dialog{display:none}.ekko-lightbox .modal-footer{text-align:left}.ekko-lightbox-loader{position:absolute;top:0;left:0;bottom:0;right:0;width:100%;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.ekko-lightbox-loader>div{width:40px;height:40px;position:relative;text-align:center}.ekko-lightbox-loader>div>div{width:100%;height:100%;border-radius:50%;background-color:#fff;opacity:.6;position:absolute;top:0;left:0;animation:a 2s infinite ease-in-out}.ekko-lightbox-loader>div>div:last-child{animation-delay:-1s}.modal-dialog .ekko-lightbox-loader>div>div{background-color:#333}@keyframes a{0%,to{transform:scale(0);-webkit-transform:scale(0)}50%{transform:scale(1);-webkit-transform:scale(1)}} /*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImVra28tbGlnaHRib3guY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGVBQ0UsOEJBQXlCLEFBQXpCLHVCQUF5QixBQUN6QixzQkFBb0IsQUFBcEIsbUJBQW9CLEFBQ3BCLHFCQUF3QixBQUF4Qix1QkFBd0IsQUFDeEIseUJBQTZCLENBQzlCLEFBQ0QseUJBQ0UsaUJBQW1CLENBQ3BCLEFBQ0QsZ0RBQ0Usa0JBQW1CLEFBQ25CLE1BQU8sQUFDUCxPQUFRLEFBQ1IsU0FBVSxBQUNWLFFBQVMsQUFDVCxVQUFZLENBQ2IsQUFDRCxzQkFDRSxXQUFZLEFBQ1osV0FBYSxDQUNkLEFBQ0QsMkJBQ0UsVUFBYSxBQUNiLGtCQUFtQixBQUNuQixNQUFPLEFBQ1AsT0FBUSxBQUNSLFdBQVksQUFDWixZQUFhLEFBQ2Isb0JBQWMsQUFBZCxZQUFjLENBQ2YsQUFDRCw2QkFDRSxXQUFRLEFBQVIsT0FBUSxBQUNSLG9CQUFjLEFBQWQsYUFBYyxBQUNkLHNCQUFvQixBQUFwQixtQkFBb0IsQUFDcEIsVUFBVyxBQUNYLHVCQUF5QixBQUN6QixXQUFZLEFBQ1osZUFBZ0IsQUFDaEIsU0FBYSxDQUNkLEFBQ0QsK0JBQ0Usb0JBQWEsQUFBYixXQUFhLENBQ2QsQUFDRCxvQ0FDRSxZQUFjLENBQ2YsQUFDRCxrQ0FDRSxjQUFnQixDQUNqQixBQUNELDZDQUNFLGdCQUFrQixDQUNuQixBQUNELG1DQUNFLG9CQUFzQixDQUN2QixBQUNELG1DQUNFLFlBQWMsQ0FDZixBQUNELHNDQUNFLGVBQWdCLEFBQ2hCLGlCQUFtQixDQUNwQixBQUNELHVCQUNFLFVBQVcsQUFDWCxvQkFBc0IsQ0FDdkIsQUFDRCw2QkFDRSxZQUFjLENBQ2YsQUFDRCw2QkFDRSxlQUFpQixDQUNsQixBQUNELHNCQUNFLGtCQUFtQixBQUNuQixNQUFPLEFBQ1AsT0FBUSxBQUNSLFNBQVUsQUFDVixRQUFTLEFBQ1QsV0FBWSxBQUNaLG9CQUFjLEFBQWQsYUFBYyxBQUVkLDBCQUF1QixBQUF2QixzQkFBdUIsQUFFdkIscUJBQXdCLEFBQXhCLHVCQUF3QixBQUV4QixzQkFBb0IsQUFBcEIsa0JBQW9CLENBQ3JCLEFBQ0QsMEJBQ0UsV0FBWSxBQUNaLFlBQWEsQUFDYixrQkFBbUIsQUFDbkIsaUJBQW1CLENBQ3BCLEFBQ0QsOEJBQ0UsV0FBWSxBQUNaLFlBQWEsQUFDYixrQkFBbUIsQUFDbkIsc0JBQXVCLEFBQ3ZCLFdBQWEsQUFDYixrQkFBbUIsQUFDbkIsTUFBTyxBQUNQLE9BQVEsQUFDUixtQ0FBNkMsQ0FDOUMsQUFDRCx5Q0FDRSxtQkFBcUIsQ0FDdEIsQUFDRCw0Q0FDRSxxQkFBdUIsQ0FDeEIsQUFVRCxhQUNFLE1BRUUsbUJBQW9CLEFBQ3BCLDBCQUE0QixDQUM3QixBQUNELElBQ0UsbUJBQW9CLEFBQ3BCLDBCQUE0QixDQUM3QixDQUNGIiwiZmlsZSI6ImVra28tbGlnaHRib3guY3NzIiwic291cmNlc0NvbnRlbnQiOlsiLmVra28tbGlnaHRib3gge1xuICBkaXNwbGF5OiBmbGV4ICFpbXBvcnRhbnQ7XG4gIGFsaWduLWl0ZW1zOiBjZW50ZXI7XG4gIGp1c3RpZnktY29udGVudDogY2VudGVyO1xuICBwYWRkaW5nLXJpZ2h0OiAwcHghaW1wb3J0YW50O1xufVxuLmVra28tbGlnaHRib3gtY29udGFpbmVyIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xufVxuLmVra28tbGlnaHRib3gtY29udGFpbmVyID4gZGl2LmVra28tbGlnaHRib3gtaXRlbSB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgdG9wOiAwO1xuICBsZWZ0OiAwO1xuICBib3R0b206IDA7XG4gIHJpZ2h0OiAwO1xuICB3aWR0aDogMTAwJTtcbn1cbi5la2tvLWxpZ2h0Ym94IGlmcmFtZSB7XG4gIHdpZHRoOiAxMDAlO1xuICBoZWlnaHQ6IDEwMCU7XG59XG4uZWtrby1saWdodGJveC1uYXYtb3ZlcmxheSB7XG4gIHotaW5kZXg6IDEwMDtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB0b3A6IDA7XG4gIGxlZnQ6IDA7XG4gIHdpZHRoOiAxMDAlO1xuICBoZWlnaHQ6IDEwMCU7XG4gIGRpc3BsYXk6IGZsZXg7XG59XG4uZWtrby1saWdodGJveC1uYXYtb3ZlcmxheSBhIHtcbiAgZmxleDogMTtcbiAgZGlzcGxheTogZmxleDtcbiAgYWxpZ24taXRlbXM6IGNlbnRlcjtcbiAgb3BhY2l0eTogMDtcbiAgdHJhbnNpdGlvbjogb3BhY2l0eSAwLjVzO1xuICBjb2xvcjogI2ZmZjtcbiAgZm9udC1zaXplOiAzMHB4O1xuICB6LWluZGV4OiAxMDA7XG59XG4uZWtrby1saWdodGJveC1uYXYtb3ZlcmxheSBhID4gKiB7XG4gIGZsZXgtZ3JvdzogMTtcbn1cbi5la2tvLWxpZ2h0Ym94LW5hdi1vdmVybGF5IGEgPiAqOmZvY3VzIHtcbiAgb3V0bGluZTogbm9uZTtcbn1cbi5la2tvLWxpZ2h0Ym94LW5hdi1vdmVybGF5IGEgc3BhbiB7XG4gIHBhZGRpbmc6IDAgMzBweDtcbn1cbi5la2tvLWxpZ2h0Ym94LW5hdi1vdmVybGF5IGE6bGFzdC1jaGlsZCBzcGFuIHtcbiAgdGV4dC1hbGlnbjogcmlnaHQ7XG59XG4uZWtrby1saWdodGJveC1uYXYtb3ZlcmxheSBhOmhvdmVyIHtcbiAgdGV4dC1kZWNvcmF0aW9uOiBub25lO1xufVxuLmVra28tbGlnaHRib3gtbmF2LW92ZXJsYXkgYTpmb2N1cyB7XG4gIG91dGxpbmU6IG5vbmU7XG59XG4uZWtrby1saWdodGJveC1uYXYtb3ZlcmxheSBhLmRpc2FibGVkIHtcbiAgY3Vyc29yOiBkZWZhdWx0O1xuICB2aXNpYmlsaXR5OiBoaWRkZW47XG59XG4uZWtrby1saWdodGJveCBhOmhvdmVyIHtcbiAgb3BhY2l0eTogMTtcbiAgdGV4dC1kZWNvcmF0aW9uOiBub25lO1xufVxuLmVra28tbGlnaHRib3ggLm1vZGFsLWRpYWxvZyB7XG4gIGRpc3BsYXk6IG5vbmU7XG59XG4uZWtrby1saWdodGJveCAubW9kYWwtZm9vdGVyIHtcbiAgdGV4dC1hbGlnbjogbGVmdDtcbn1cbi5la2tvLWxpZ2h0Ym94LWxvYWRlciB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgdG9wOiAwO1xuICBsZWZ0OiAwO1xuICBib3R0b206IDA7XG4gIHJpZ2h0OiAwO1xuICB3aWR0aDogMTAwJTtcbiAgZGlzcGxheTogZmxleDtcbiAgLyogZXN0YWJsaXNoIGZsZXggY29udGFpbmVyICovXG4gIGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47XG4gIC8qIG1ha2UgbWFpbiBheGlzIHZlcnRpY2FsICovXG4gIGp1c3RpZnktY29udGVudDogY2VudGVyO1xuICAvKiBjZW50ZXIgaXRlbXMgdmVydGljYWxseSwgaW4gdGhpcyBjYXNlICovXG4gIGFsaWduLWl0ZW1zOiBjZW50ZXI7XG59XG4uZWtrby1saWdodGJveC1sb2FkZXIgPiBkaXYge1xuICB3aWR0aDogNDBweDtcbiAgaGVpZ2h0OiA0MHB4O1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gIHRleHQtYWxpZ246IGNlbnRlcjtcbn1cbi5la2tvLWxpZ2h0Ym94LWxvYWRlciA+IGRpdiA+IGRpdiB7XG4gIHdpZHRoOiAxMDAlO1xuICBoZWlnaHQ6IDEwMCU7XG4gIGJvcmRlci1yYWRpdXM6IDUwJTtcbiAgYmFja2dyb3VuZC1jb2xvcjogI2ZmZjtcbiAgb3BhY2l0eTogMC42O1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogMDtcbiAgbGVmdDogMDtcbiAgYW5pbWF0aW9uOiBzay1ib3VuY2UgMnMgaW5maW5pdGUgZWFzZS1pbi1vdXQ7XG59XG4uZWtrby1saWdodGJveC1sb2FkZXIgPiBkaXYgPiBkaXY6bGFzdC1jaGlsZCB7XG4gIGFuaW1hdGlvbi1kZWxheTogLTFzO1xufVxuLm1vZGFsLWRpYWxvZyAuZWtrby1saWdodGJveC1sb2FkZXIgPiBkaXYgPiBkaXYge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjMzMzO1xufVxuQC13ZWJraXQta2V5ZnJhbWVzIHNrLWJvdW5jZSB7XG4gIDAlLFxuICAxMDAlIHtcbiAgICAtd2Via2l0LXRyYW5zZm9ybTogc2NhbGUoMCk7XG4gIH1cbiAgNTAlIHtcbiAgICAtd2Via2l0LXRyYW5zZm9ybTogc2NhbGUoMSk7XG4gIH1cbn1cbkBrZXlmcmFtZXMgc2stYm91bmNlIHtcbiAgMCUsXG4gIDEwMCUge1xuICAgIHRyYW5zZm9ybTogc2NhbGUoMCk7XG4gICAgLXdlYmtpdC10cmFuc2Zvcm06IHNjYWxlKDApO1xuICB9XG4gIDUwJSB7XG4gICAgdHJhbnNmb3JtOiBzY2FsZSgxKTtcbiAgICAtd2Via2l0LXRyYW5zZm9ybTogc2NhbGUoMSk7XG4gIH1cbn1cbiJdfQ== */ -.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .search,.bootstrap-table .fixed-table-toolbar .columns{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px !important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.4286}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0 !important}.bootstrap-table .fixed-table-container .table th,.bootstrap-table .fixed-table-container .table td{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table thead th,.bootstrap-table .fixed-table-container .table tfoot th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table thead th:focus,.bootstrap-table .fixed-table-container .table tfoot th:focus{outline:0 solid rgba(0,0,0,0)}.bootstrap-table .fixed-table-container .table thead th.detail,.bootstrap-table .fixed-table-container .table tfoot th.detail{width:30px}.bootstrap-table .fixed-table-container .table thead th .th-inner,.bootstrap-table .fixed-table-container .table tfoot th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table thead th .sortable,.bootstrap-table .fixed-table-container .table tfoot th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px !important}.bootstrap-table .fixed-table-container .table thead th .sortable.sortable-center,.bootstrap-table .fixed-table-container .table tfoot th .sortable.sortable-center{padding-left:20px !important;padding-right:20px !important}.bootstrap-table .fixed-table-container .table thead th .both,.bootstrap-table .fixed-table-container .table tfoot th .both{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC")}.bootstrap-table .fixed-table-container .table thead th .asc,.bootstrap-table .fixed-table-container .table tfoot th .asc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==")}.bootstrap-table .fixed-table-container .table thead th .desc,.bootstrap-table .fixed-table-container .table tfoot th .desc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ")}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:rgba(0,0,0,.075)}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:bold;display:inline-block;min-width:30%;width:auto !important;text-align:left !important}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value{width:100% !important;text-align:left !important}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=radio],.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=checkbox]{margin:0 auto !important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.25rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dee2e6;border-right:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow:auto auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:flex;justify-content:center;position:absolute;bottom:0;width:100%;max-width:100%;z-index:1000;transition:visibility 0s,opacity .15s ease-in-out;opacity:0;visibility:hidden}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open{visibility:visible;opacity:1}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:loading;background:#212529;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#212529}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination-detail,.bootstrap-table .fixed-table-pagination>.pagination{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:"⬅"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:"➡"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100% !important;background:#fff;height:100vh;overflow-y:scroll}.bootstrap-table.bootstrap4 .pagination-lg .page-link,.bootstrap-table.bootstrap5 .pagination-lg .page-link{padding:.5rem 1rem}.bootstrap-table.bootstrap5 .float-left{float:left}.bootstrap-table.bootstrap5 .float-right{float:right}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes loading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} +.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .search,.bootstrap-table .fixed-table-toolbar .columns{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px !important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.4286}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0 !important}.bootstrap-table .fixed-table-container .table th,.bootstrap-table .fixed-table-container .table td{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table thead th,.bootstrap-table .fixed-table-container .table tfoot th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table thead th:focus,.bootstrap-table .fixed-table-container .table tfoot th:focus{outline:0 solid rgba(0,0,0,0)}.bootstrap-table .fixed-table-container .table thead th.detail,.bootstrap-table .fixed-table-container .table tfoot th.detail{width:30px}.bootstrap-table .fixed-table-container .table thead th .th-inner,.bootstrap-table .fixed-table-container .table tfoot th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table thead th .sortable,.bootstrap-table .fixed-table-container .table tfoot th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px !important}.bootstrap-table .fixed-table-container .table thead th .sortable.sortable-center,.bootstrap-table .fixed-table-container .table tfoot th .sortable.sortable-center{padding-left:20px !important;padding-right:20px !important}.bootstrap-table .fixed-table-container .table thead th .both,.bootstrap-table .fixed-table-container .table tfoot th .both{background-image:url('data:image/svg+xml;utf8,');background-size:16px 16px;background-position:center right 2px}.bootstrap-table .fixed-table-container .table thead th .asc,.bootstrap-table .fixed-table-container .table tfoot th .asc{background-image:url('data:image/svg+xml;utf8,')}.bootstrap-table .fixed-table-container .table thead th .desc,.bootstrap-table .fixed-table-container .table tfoot th .desc{background-image:url('data:image/svg+xml;utf8,')}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:rgba(0,0,0,.075)}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:bold;display:inline-block;min-width:30%;width:auto !important;text-align:left !important}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value{width:100% !important;text-align:left !important}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=radio],.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=checkbox]{margin:0 auto !important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.25rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dee2e6;border-right:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow:auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:flex;justify-content:center;position:absolute;bottom:0;width:100%;max-width:100%;z-index:1000;transition:visibility 0s,opacity .15s ease-in-out;opacity:0;visibility:hidden}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open{visibility:visible;opacity:1}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:loading;background:#212529;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#212529}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination-detail,.bootstrap-table .fixed-table-pagination>.pagination{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:"⬅"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:"➡"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100% !important;background:#fff;height:100vh;overflow-y:scroll}.bootstrap-table.bootstrap4 .pagination-lg .page-link,.bootstrap-table.bootstrap5 .pagination-lg .page-link{padding:.5rem 1rem}.bootstrap-table.bootstrap5 .float-left{float:left}.bootstrap-table.bootstrap5 .float-right{float:right}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes loading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; @@ -21669,6 +21669,9 @@ body { font-size: 14px; white-space: normal; } +.modal-warning .modal-help { + color: #fff8af; +} .bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading { z-index: 0 !important; } @@ -21800,19 +21803,36 @@ body { } .select2-selection--multiple { border-color: #d2d6de !important; - height: 34px; + overflow-y: auto; } .select2-selection__choice { border-radius: 0px !important; } +.select2-search select2-search--inline { + height: 35px !important; + float: left; + margin: 0; +} +.select2-selection__rendered needsclick { + color: red; +} +.select2-results__option { + padding: 5px; + -moz-user-select: none; + user-select: none; + -webkit-user-select: none; + margin: 0px; +} img.navbar-brand-img, .navbar-brand > img { float: left; padding: 5px 5px 5px 0; max-height: 50px; } -.input-daterange { - border-radius: 0px; +.input-daterange, +.input-daterange input:first-child, +.input-daterange input:last-child { + border-radius: 0px !important; } .btn.bg-maroon, .btn.bg-purple { @@ -21978,6 +21998,7 @@ th.css-consumable > .th-inner, th.css-envelope > .th-inner, th.css-users > .th-inner, th.css-location > .th-inner, +th.css-component > .th-inner, th.css-accessory > .th-inner { font-size: 0px; line-height: 0.75 !important; @@ -21993,6 +22014,7 @@ th.css-consumable > .th-inner::before, th.css-envelope > .th-inner::before, th.css-users > .th-inner::before, th.css-location > .th-inner::before, +th.css-component > .th-inner::before, th.css-accessory > .th-inner::before { display: inline-block; font-size: 20px; @@ -22046,6 +22068,11 @@ th.css-location > .th-inner::before { font-size: 19px; margin-bottom: 0px; } +th.css-component > .th-inner::before { + content: "\f0a0"; + font-family: "Font Awesome 5 Free"; + font-weight: 500; +} .small-box .inner { padding-left: 15px; padding-right: 15px; @@ -22094,15 +22121,37 @@ th.css-location > .th-inner::before { margin-top: 50px; } } +@media screen and (max-width: 992px) { + .info-stack-container { + display: flex; + flex-direction: column; + } + .col-md-3.col-xs-12.col-sm-push-9.info-stack { + left: auto; + order: 1; + } + .col-md-9.col-xs-12.col-sm-pull-3.info-stack { + right: auto; + order: 2; + } + .info-stack-container > .col-md-9.col-xs-12.col-sm-pull-3.info-stack > .row-new-striped > .row > .col-sm-2 { + width: auto; + float: none; + } +} @media screen and (max-width: 1318px) and (min-width: 1200px) { - .box { + .admin.box { height: 170px; } } -.ellipsis { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; +@media screen and (max-width: 1494px) and (min-width: 1200px) { + .dashboard.small-box { + white-space: nowrap; + text-overflow: ellipsis; + max-width: 188px; + display: block; + overflow: hidden; + } } /** Form-stuff overrides for checkboxes and stuff **/ label.form-control { @@ -23093,6 +23142,9 @@ body { font-size: 14px; white-space: normal; } +.modal-warning .modal-help { + color: #fff8af; +} .bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading { z-index: 0 !important; } @@ -23224,19 +23276,36 @@ body { } .select2-selection--multiple { border-color: #d2d6de !important; - height: 34px; + overflow-y: auto; } .select2-selection__choice { border-radius: 0px !important; } +.select2-search select2-search--inline { + height: 35px !important; + float: left; + margin: 0; +} +.select2-selection__rendered needsclick { + color: red; +} +.select2-results__option { + padding: 5px; + -moz-user-select: none; + user-select: none; + -webkit-user-select: none; + margin: 0px; +} img.navbar-brand-img, .navbar-brand > img { float: left; padding: 5px 5px 5px 0; max-height: 50px; } -.input-daterange { - border-radius: 0px; +.input-daterange, +.input-daterange input:first-child, +.input-daterange input:last-child { + border-radius: 0px !important; } .btn.bg-maroon, .btn.bg-purple { @@ -23402,6 +23471,7 @@ th.css-consumable > .th-inner, th.css-envelope > .th-inner, th.css-users > .th-inner, th.css-location > .th-inner, +th.css-component > .th-inner, th.css-accessory > .th-inner { font-size: 0px; line-height: 0.75 !important; @@ -23417,6 +23487,7 @@ th.css-consumable > .th-inner::before, th.css-envelope > .th-inner::before, th.css-users > .th-inner::before, th.css-location > .th-inner::before, +th.css-component > .th-inner::before, th.css-accessory > .th-inner::before { display: inline-block; font-size: 20px; @@ -23470,6 +23541,11 @@ th.css-location > .th-inner::before { font-size: 19px; margin-bottom: 0px; } +th.css-component > .th-inner::before { + content: "\f0a0"; + font-family: "Font Awesome 5 Free"; + font-weight: 500; +} .small-box .inner { padding-left: 15px; padding-right: 15px; @@ -23518,15 +23594,37 @@ th.css-location > .th-inner::before { margin-top: 50px; } } +@media screen and (max-width: 992px) { + .info-stack-container { + display: flex; + flex-direction: column; + } + .col-md-3.col-xs-12.col-sm-push-9.info-stack { + left: auto; + order: 1; + } + .col-md-9.col-xs-12.col-sm-pull-3.info-stack { + right: auto; + order: 2; + } + .info-stack-container > .col-md-9.col-xs-12.col-sm-pull-3.info-stack > .row-new-striped > .row > .col-sm-2 { + width: auto; + float: none; + } +} @media screen and (max-width: 1318px) and (min-width: 1200px) { - .box { + .admin.box { height: 170px; } } -.ellipsis { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; +@media screen and (max-width: 1494px) and (min-width: 1200px) { + .dashboard.small-box { + white-space: nowrap; + text-overflow: ellipsis; + max-width: 188px; + display: block; + overflow: hidden; + } } /** Form-stuff overrides for checkboxes and stuff **/ label.form-control { diff --git a/public/css/dist/bootstrap-table.css b/public/css/dist/bootstrap-table.css index 29f63a096..0b8274e97 100644 --- a/public/css/dist/bootstrap-table.css +++ b/public/css/dist/bootstrap-table.css @@ -1,4 +1,4 @@ -.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .search,.bootstrap-table .fixed-table-toolbar .columns{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px !important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.4286}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0 !important}.bootstrap-table .fixed-table-container .table th,.bootstrap-table .fixed-table-container .table td{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table thead th,.bootstrap-table .fixed-table-container .table tfoot th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table thead th:focus,.bootstrap-table .fixed-table-container .table tfoot th:focus{outline:0 solid rgba(0,0,0,0)}.bootstrap-table .fixed-table-container .table thead th.detail,.bootstrap-table .fixed-table-container .table tfoot th.detail{width:30px}.bootstrap-table .fixed-table-container .table thead th .th-inner,.bootstrap-table .fixed-table-container .table tfoot th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table thead th .sortable,.bootstrap-table .fixed-table-container .table tfoot th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px !important}.bootstrap-table .fixed-table-container .table thead th .sortable.sortable-center,.bootstrap-table .fixed-table-container .table tfoot th .sortable.sortable-center{padding-left:20px !important;padding-right:20px !important}.bootstrap-table .fixed-table-container .table thead th .both,.bootstrap-table .fixed-table-container .table tfoot th .both{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC")}.bootstrap-table .fixed-table-container .table thead th .asc,.bootstrap-table .fixed-table-container .table tfoot th .asc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==")}.bootstrap-table .fixed-table-container .table thead th .desc,.bootstrap-table .fixed-table-container .table tfoot th .desc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ")}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:rgba(0,0,0,.075)}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:bold;display:inline-block;min-width:30%;width:auto !important;text-align:left !important}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value{width:100% !important;text-align:left !important}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=radio],.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=checkbox]{margin:0 auto !important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.25rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dee2e6;border-right:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow:auto auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:flex;justify-content:center;position:absolute;bottom:0;width:100%;max-width:100%;z-index:1000;transition:visibility 0s,opacity .15s ease-in-out;opacity:0;visibility:hidden}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open{visibility:visible;opacity:1}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:loading;background:#212529;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#212529}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination-detail,.bootstrap-table .fixed-table-pagination>.pagination{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:"⬅"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:"➡"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100% !important;background:#fff;height:100vh;overflow-y:scroll}.bootstrap-table.bootstrap4 .pagination-lg .page-link,.bootstrap-table.bootstrap5 .pagination-lg .page-link{padding:.5rem 1rem}.bootstrap-table.bootstrap5 .float-left{float:left}.bootstrap-table.bootstrap5 .float-right{float:right}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes loading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} +.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .search,.bootstrap-table .fixed-table-toolbar .columns{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px !important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.4286}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0 !important}.bootstrap-table .fixed-table-container .table th,.bootstrap-table .fixed-table-container .table td{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table thead th,.bootstrap-table .fixed-table-container .table tfoot th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table thead th:focus,.bootstrap-table .fixed-table-container .table tfoot th:focus{outline:0 solid rgba(0,0,0,0)}.bootstrap-table .fixed-table-container .table thead th.detail,.bootstrap-table .fixed-table-container .table tfoot th.detail{width:30px}.bootstrap-table .fixed-table-container .table thead th .th-inner,.bootstrap-table .fixed-table-container .table tfoot th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table thead th .sortable,.bootstrap-table .fixed-table-container .table tfoot th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px !important}.bootstrap-table .fixed-table-container .table thead th .sortable.sortable-center,.bootstrap-table .fixed-table-container .table tfoot th .sortable.sortable-center{padding-left:20px !important;padding-right:20px !important}.bootstrap-table .fixed-table-container .table thead th .both,.bootstrap-table .fixed-table-container .table tfoot th .both{background-image:url('data:image/svg+xml;utf8,');background-size:16px 16px;background-position:center right 2px}.bootstrap-table .fixed-table-container .table thead th .asc,.bootstrap-table .fixed-table-container .table tfoot th .asc{background-image:url('data:image/svg+xml;utf8,')}.bootstrap-table .fixed-table-container .table thead th .desc,.bootstrap-table .fixed-table-container .table tfoot th .desc{background-image:url('data:image/svg+xml;utf8,')}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:rgba(0,0,0,.075)}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:bold;display:inline-block;min-width:30%;width:auto !important;text-align:left !important}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value{width:100% !important;text-align:left !important}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=radio],.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=checkbox]{margin:0 auto !important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.25rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dee2e6;border-right:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow:auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:flex;justify-content:center;position:absolute;bottom:0;width:100%;max-width:100%;z-index:1000;transition:visibility 0s,opacity .15s ease-in-out;opacity:0;visibility:hidden}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open{visibility:visible;opacity:1}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:loading;background:#212529;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#212529}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination-detail,.bootstrap-table .fixed-table-pagination>.pagination{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:"⬅"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:"➡"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100% !important;background:#fff;height:100vh;overflow-y:scroll}.bootstrap-table.bootstrap4 .pagination-lg .page-link,.bootstrap-table.bootstrap5 .pagination-lg .page-link{padding:.5rem 1rem}.bootstrap-table.bootstrap5 .float-left{float:left}.bootstrap-table.bootstrap5 .float-right{float:right}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes loading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} .fix-sticky{position:fixed !important;overflow:hidden;z-index:100}.fix-sticky table thead{background:#fff}.fix-sticky table thead.thead-light{background:#e9ecef}.fix-sticky table thead.thead-dark{background:#212529} diff --git a/public/js/build/app.js b/public/js/build/app.js index 66bbc638c..3c8051012 100644 --- a/public/js/build/app.js +++ b/public/js/build/app.js @@ -130,74 +130,43 @@ pieOptions = { //----------------- var baseUrl = $('meta[name="baseUrl"]').attr('content'); -(function ($, settings) { - var Components = {}; - Components.modals = {}; +$(function () { + var $el = $('table'); // confirm restore modal - Components.modals.confirmRestore = function () { - var $el = $('table'); - var events = { - 'click': function click(evnt) { - var $context = $(this); - var $restoreConfirmModal = $('#restoreConfirmModal'); - var href = $context.attr('href'); - var message = $context.attr('data-content'); - var title = $context.attr('data-title'); - $('#restoreConfirmModalLabel').text(title); - $restoreConfirmModal.find('.modal-body').text(message); - $('#restoreForm').attr('action', href); - $restoreConfirmModal.modal({ - show: true - }); - return false; - } - }; - var render = function render() { - $el.on('click', '.restore-asset', events['click']); - }; - return { - render: render - }; - }; + + $el.on('click', '.restore-asset', function (evnt) { + var $context = $(this); + var $restoreConfirmModal = $('#restoreConfirmModal'); + var href = $context.attr('href'); + var message = $context.attr('data-content'); + var title = $context.attr('data-title'); + $('#confirmModalLabel').text(title); + $restoreConfirmModal.find('.modal-body').text(message); + $('#restoreForm').attr('action', href); + $restoreConfirmModal.modal({ + show: true + }); + return false; + }); // confirm delete modal - Components.modals.confirmDelete = function () { - var $el = $('table'); - var events = { - 'click': function click(evnt) { - var $context = $(this); - var $dataConfirmModal = $('#dataConfirmModal'); - var href = $context.attr('href'); - var message = $context.attr('data-content'); - var title = $context.attr('data-title'); - $('#myModalLabel').text(title); - $dataConfirmModal.find('.modal-body').text(message); - $('#deleteForm').attr('action', href); - $dataConfirmModal.modal({ - show: true - }); - return false; - } - }; - var render = function render() { - $el.on('click', '.delete-asset', events['click']); - }; - return { - render: render - }; - }; - /** - * Application start point - * Component definition stays out of load event, execution only happens. - */ - $(function () { - new Components.modals.confirmRestore().render(); - new Components.modals.confirmDelete().render(); + $el.on('click', '.delete-asset', function (evnt) { + var $context = $(this); + var $dataConfirmModal = $('#dataConfirmModal'); + var href = $context.attr('href'); + var message = $context.attr('data-content'); + var title = $context.attr('data-title'); + $('#myModalLabel').text(title); + $dataConfirmModal.find('.modal-body').text(message); + $('#deleteForm').attr('action', href); + $dataConfirmModal.modal({ + show: true + }); + return false; }); -})(jQuery, window.snipeit.settings); -$(document).ready(function () { + /* * Slideout help menu */ @@ -3157,6 +3126,903 @@ if (typeof jQuery === 'undefined') { }(jQuery); +/***/ }), + +/***/ "./node_modules/canvas-confetti/dist/confetti.browser.js": +/*!***************************************************************!*\ + !*** ./node_modules/canvas-confetti/dist/confetti.browser.js ***! + \***************************************************************/ +/***/ (() => { + +// canvas-confetti v1.9.3 built on 2024-04-30T22:19:17.794Z +!(function (window, module) { +// source content +/* globals Map */ + +(function main(global, module, isWorker, workerSize) { + var canUseWorker = !!( + global.Worker && + global.Blob && + global.Promise && + global.OffscreenCanvas && + global.OffscreenCanvasRenderingContext2D && + global.HTMLCanvasElement && + global.HTMLCanvasElement.prototype.transferControlToOffscreen && + global.URL && + global.URL.createObjectURL); + + var canUsePaths = typeof Path2D === 'function' && typeof DOMMatrix === 'function'; + var canDrawBitmap = (function () { + // this mostly supports ssr + if (!global.OffscreenCanvas) { + return false; + } + + var canvas = new OffscreenCanvas(1, 1); + var ctx = canvas.getContext('2d'); + ctx.fillRect(0, 0, 1, 1); + var bitmap = canvas.transferToImageBitmap(); + + try { + ctx.createPattern(bitmap, 'no-repeat'); + } catch (e) { + return false; + } + + return true; + })(); + + function noop() {} + + // create a promise if it exists, otherwise, just + // call the function directly + function promise(func) { + var ModulePromise = module.exports.Promise; + var Prom = ModulePromise !== void 0 ? ModulePromise : global.Promise; + + if (typeof Prom === 'function') { + return new Prom(func); + } + + func(noop, noop); + + return null; + } + + var bitmapMapper = (function (skipTransform, map) { + // see https://github.com/catdad/canvas-confetti/issues/209 + // creating canvases is actually pretty expensive, so we should create a + // 1:1 map for bitmap:canvas, so that we can animate the confetti in + // a performant manner, but also not store them forever so that we don't + // have a memory leak + return { + transform: function(bitmap) { + if (skipTransform) { + return bitmap; + } + + if (map.has(bitmap)) { + return map.get(bitmap); + } + + var canvas = new OffscreenCanvas(bitmap.width, bitmap.height); + var ctx = canvas.getContext('2d'); + ctx.drawImage(bitmap, 0, 0); + + map.set(bitmap, canvas); + + return canvas; + }, + clear: function () { + map.clear(); + } + }; + })(canDrawBitmap, new Map()); + + var raf = (function () { + var TIME = Math.floor(1000 / 60); + var frame, cancel; + var frames = {}; + var lastFrameTime = 0; + + if (typeof requestAnimationFrame === 'function' && typeof cancelAnimationFrame === 'function') { + frame = function (cb) { + var id = Math.random(); + + frames[id] = requestAnimationFrame(function onFrame(time) { + if (lastFrameTime === time || lastFrameTime + TIME - 1 < time) { + lastFrameTime = time; + delete frames[id]; + + cb(); + } else { + frames[id] = requestAnimationFrame(onFrame); + } + }); + + return id; + }; + cancel = function (id) { + if (frames[id]) { + cancelAnimationFrame(frames[id]); + } + }; + } else { + frame = function (cb) { + return setTimeout(cb, TIME); + }; + cancel = function (timer) { + return clearTimeout(timer); + }; + } + + return { frame: frame, cancel: cancel }; + }()); + + var getWorker = (function () { + var worker; + var prom; + var resolves = {}; + + function decorate(worker) { + function execute(options, callback) { + worker.postMessage({ options: options || {}, callback: callback }); + } + worker.init = function initWorker(canvas) { + var offscreen = canvas.transferControlToOffscreen(); + worker.postMessage({ canvas: offscreen }, [offscreen]); + }; + + worker.fire = function fireWorker(options, size, done) { + if (prom) { + execute(options, null); + return prom; + } + + var id = Math.random().toString(36).slice(2); + + prom = promise(function (resolve) { + function workerDone(msg) { + if (msg.data.callback !== id) { + return; + } + + delete resolves[id]; + worker.removeEventListener('message', workerDone); + + prom = null; + + bitmapMapper.clear(); + + done(); + resolve(); + } + + worker.addEventListener('message', workerDone); + execute(options, id); + + resolves[id] = workerDone.bind(null, { data: { callback: id }}); + }); + + return prom; + }; + + worker.reset = function resetWorker() { + worker.postMessage({ reset: true }); + + for (var id in resolves) { + resolves[id](); + delete resolves[id]; + } + }; + } + + return function () { + if (worker) { + return worker; + } + + if (!isWorker && canUseWorker) { + var code = [ + 'var CONFETTI, SIZE = {}, module = {};', + '(' + main.toString() + ')(this, module, true, SIZE);', + 'onmessage = function(msg) {', + ' if (msg.data.options) {', + ' CONFETTI(msg.data.options).then(function () {', + ' if (msg.data.callback) {', + ' postMessage({ callback: msg.data.callback });', + ' }', + ' });', + ' } else if (msg.data.reset) {', + ' CONFETTI && CONFETTI.reset();', + ' } else if (msg.data.resize) {', + ' SIZE.width = msg.data.resize.width;', + ' SIZE.height = msg.data.resize.height;', + ' } else if (msg.data.canvas) {', + ' SIZE.width = msg.data.canvas.width;', + ' SIZE.height = msg.data.canvas.height;', + ' CONFETTI = module.exports.create(msg.data.canvas);', + ' }', + '}', + ].join('\n'); + try { + worker = new Worker(URL.createObjectURL(new Blob([code]))); + } catch (e) { + // eslint-disable-next-line no-console + typeof console !== undefined && typeof console.warn === 'function' ? console.warn('🎊 Could not load worker', e) : null; + + return null; + } + + decorate(worker); + } + + return worker; + }; + })(); + + var defaults = { + particleCount: 50, + angle: 90, + spread: 45, + startVelocity: 45, + decay: 0.9, + gravity: 1, + drift: 0, + ticks: 200, + x: 0.5, + y: 0.5, + shapes: ['square', 'circle'], + zIndex: 100, + colors: [ + '#26ccff', + '#a25afd', + '#ff5e7e', + '#88ff5a', + '#fcff42', + '#ffa62d', + '#ff36ff' + ], + // probably should be true, but back-compat + disableForReducedMotion: false, + scalar: 1 + }; + + function convert(val, transform) { + return transform ? transform(val) : val; + } + + function isOk(val) { + return !(val === null || val === undefined); + } + + function prop(options, name, transform) { + return convert( + options && isOk(options[name]) ? options[name] : defaults[name], + transform + ); + } + + function onlyPositiveInt(number){ + return number < 0 ? 0 : Math.floor(number); + } + + function randomInt(min, max) { + // [min, max) + return Math.floor(Math.random() * (max - min)) + min; + } + + function toDecimal(str) { + return parseInt(str, 16); + } + + function colorsToRgb(colors) { + return colors.map(hexToRgb); + } + + function hexToRgb(str) { + var val = String(str).replace(/[^0-9a-f]/gi, ''); + + if (val.length < 6) { + val = val[0]+val[0]+val[1]+val[1]+val[2]+val[2]; + } + + return { + r: toDecimal(val.substring(0,2)), + g: toDecimal(val.substring(2,4)), + b: toDecimal(val.substring(4,6)) + }; + } + + function getOrigin(options) { + var origin = prop(options, 'origin', Object); + origin.x = prop(origin, 'x', Number); + origin.y = prop(origin, 'y', Number); + + return origin; + } + + function setCanvasWindowSize(canvas) { + canvas.width = document.documentElement.clientWidth; + canvas.height = document.documentElement.clientHeight; + } + + function setCanvasRectSize(canvas) { + var rect = canvas.getBoundingClientRect(); + canvas.width = rect.width; + canvas.height = rect.height; + } + + function getCanvas(zIndex) { + var canvas = document.createElement('canvas'); + + canvas.style.position = 'fixed'; + canvas.style.top = '0px'; + canvas.style.left = '0px'; + canvas.style.pointerEvents = 'none'; + canvas.style.zIndex = zIndex; + + return canvas; + } + + function ellipse(context, x, y, radiusX, radiusY, rotation, startAngle, endAngle, antiClockwise) { + context.save(); + context.translate(x, y); + context.rotate(rotation); + context.scale(radiusX, radiusY); + context.arc(0, 0, 1, startAngle, endAngle, antiClockwise); + context.restore(); + } + + function randomPhysics(opts) { + var radAngle = opts.angle * (Math.PI / 180); + var radSpread = opts.spread * (Math.PI / 180); + + return { + x: opts.x, + y: opts.y, + wobble: Math.random() * 10, + wobbleSpeed: Math.min(0.11, Math.random() * 0.1 + 0.05), + velocity: (opts.startVelocity * 0.5) + (Math.random() * opts.startVelocity), + angle2D: -radAngle + ((0.5 * radSpread) - (Math.random() * radSpread)), + tiltAngle: (Math.random() * (0.75 - 0.25) + 0.25) * Math.PI, + color: opts.color, + shape: opts.shape, + tick: 0, + totalTicks: opts.ticks, + decay: opts.decay, + drift: opts.drift, + random: Math.random() + 2, + tiltSin: 0, + tiltCos: 0, + wobbleX: 0, + wobbleY: 0, + gravity: opts.gravity * 3, + ovalScalar: 0.6, + scalar: opts.scalar, + flat: opts.flat + }; + } + + function updateFetti(context, fetti) { + fetti.x += Math.cos(fetti.angle2D) * fetti.velocity + fetti.drift; + fetti.y += Math.sin(fetti.angle2D) * fetti.velocity + fetti.gravity; + fetti.velocity *= fetti.decay; + + if (fetti.flat) { + fetti.wobble = 0; + fetti.wobbleX = fetti.x + (10 * fetti.scalar); + fetti.wobbleY = fetti.y + (10 * fetti.scalar); + + fetti.tiltSin = 0; + fetti.tiltCos = 0; + fetti.random = 1; + } else { + fetti.wobble += fetti.wobbleSpeed; + fetti.wobbleX = fetti.x + ((10 * fetti.scalar) * Math.cos(fetti.wobble)); + fetti.wobbleY = fetti.y + ((10 * fetti.scalar) * Math.sin(fetti.wobble)); + + fetti.tiltAngle += 0.1; + fetti.tiltSin = Math.sin(fetti.tiltAngle); + fetti.tiltCos = Math.cos(fetti.tiltAngle); + fetti.random = Math.random() + 2; + } + + var progress = (fetti.tick++) / fetti.totalTicks; + + var x1 = fetti.x + (fetti.random * fetti.tiltCos); + var y1 = fetti.y + (fetti.random * fetti.tiltSin); + var x2 = fetti.wobbleX + (fetti.random * fetti.tiltCos); + var y2 = fetti.wobbleY + (fetti.random * fetti.tiltSin); + + context.fillStyle = 'rgba(' + fetti.color.r + ', ' + fetti.color.g + ', ' + fetti.color.b + ', ' + (1 - progress) + ')'; + + context.beginPath(); + + if (canUsePaths && fetti.shape.type === 'path' && typeof fetti.shape.path === 'string' && Array.isArray(fetti.shape.matrix)) { + context.fill(transformPath2D( + fetti.shape.path, + fetti.shape.matrix, + fetti.x, + fetti.y, + Math.abs(x2 - x1) * 0.1, + Math.abs(y2 - y1) * 0.1, + Math.PI / 10 * fetti.wobble + )); + } else if (fetti.shape.type === 'bitmap') { + var rotation = Math.PI / 10 * fetti.wobble; + var scaleX = Math.abs(x2 - x1) * 0.1; + var scaleY = Math.abs(y2 - y1) * 0.1; + var width = fetti.shape.bitmap.width * fetti.scalar; + var height = fetti.shape.bitmap.height * fetti.scalar; + + var matrix = new DOMMatrix([ + Math.cos(rotation) * scaleX, + Math.sin(rotation) * scaleX, + -Math.sin(rotation) * scaleY, + Math.cos(rotation) * scaleY, + fetti.x, + fetti.y + ]); + + // apply the transform matrix from the confetti shape + matrix.multiplySelf(new DOMMatrix(fetti.shape.matrix)); + + var pattern = context.createPattern(bitmapMapper.transform(fetti.shape.bitmap), 'no-repeat'); + pattern.setTransform(matrix); + + context.globalAlpha = (1 - progress); + context.fillStyle = pattern; + context.fillRect( + fetti.x - (width / 2), + fetti.y - (height / 2), + width, + height + ); + context.globalAlpha = 1; + } else if (fetti.shape === 'circle') { + context.ellipse ? + context.ellipse(fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI) : + ellipse(context, fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI); + } else if (fetti.shape === 'star') { + var rot = Math.PI / 2 * 3; + var innerRadius = 4 * fetti.scalar; + var outerRadius = 8 * fetti.scalar; + var x = fetti.x; + var y = fetti.y; + var spikes = 5; + var step = Math.PI / spikes; + + while (spikes--) { + x = fetti.x + Math.cos(rot) * outerRadius; + y = fetti.y + Math.sin(rot) * outerRadius; + context.lineTo(x, y); + rot += step; + + x = fetti.x + Math.cos(rot) * innerRadius; + y = fetti.y + Math.sin(rot) * innerRadius; + context.lineTo(x, y); + rot += step; + } + } else { + context.moveTo(Math.floor(fetti.x), Math.floor(fetti.y)); + context.lineTo(Math.floor(fetti.wobbleX), Math.floor(y1)); + context.lineTo(Math.floor(x2), Math.floor(y2)); + context.lineTo(Math.floor(x1), Math.floor(fetti.wobbleY)); + } + + context.closePath(); + context.fill(); + + return fetti.tick < fetti.totalTicks; + } + + function animate(canvas, fettis, resizer, size, done) { + var animatingFettis = fettis.slice(); + var context = canvas.getContext('2d'); + var animationFrame; + var destroy; + + var prom = promise(function (resolve) { + function onDone() { + animationFrame = destroy = null; + + context.clearRect(0, 0, size.width, size.height); + bitmapMapper.clear(); + + done(); + resolve(); + } + + function update() { + if (isWorker && !(size.width === workerSize.width && size.height === workerSize.height)) { + size.width = canvas.width = workerSize.width; + size.height = canvas.height = workerSize.height; + } + + if (!size.width && !size.height) { + resizer(canvas); + size.width = canvas.width; + size.height = canvas.height; + } + + context.clearRect(0, 0, size.width, size.height); + + animatingFettis = animatingFettis.filter(function (fetti) { + return updateFetti(context, fetti); + }); + + if (animatingFettis.length) { + animationFrame = raf.frame(update); + } else { + onDone(); + } + } + + animationFrame = raf.frame(update); + destroy = onDone; + }); + + return { + addFettis: function (fettis) { + animatingFettis = animatingFettis.concat(fettis); + + return prom; + }, + canvas: canvas, + promise: prom, + reset: function () { + if (animationFrame) { + raf.cancel(animationFrame); + } + + if (destroy) { + destroy(); + } + } + }; + } + + function confettiCannon(canvas, globalOpts) { + var isLibCanvas = !canvas; + var allowResize = !!prop(globalOpts || {}, 'resize'); + var hasResizeEventRegistered = false; + var globalDisableForReducedMotion = prop(globalOpts, 'disableForReducedMotion', Boolean); + var shouldUseWorker = canUseWorker && !!prop(globalOpts || {}, 'useWorker'); + var worker = shouldUseWorker ? getWorker() : null; + var resizer = isLibCanvas ? setCanvasWindowSize : setCanvasRectSize; + var initialized = (canvas && worker) ? !!canvas.__confetti_initialized : false; + var preferLessMotion = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion)').matches; + var animationObj; + + function fireLocal(options, size, done) { + var particleCount = prop(options, 'particleCount', onlyPositiveInt); + var angle = prop(options, 'angle', Number); + var spread = prop(options, 'spread', Number); + var startVelocity = prop(options, 'startVelocity', Number); + var decay = prop(options, 'decay', Number); + var gravity = prop(options, 'gravity', Number); + var drift = prop(options, 'drift', Number); + var colors = prop(options, 'colors', colorsToRgb); + var ticks = prop(options, 'ticks', Number); + var shapes = prop(options, 'shapes'); + var scalar = prop(options, 'scalar'); + var flat = !!prop(options, 'flat'); + var origin = getOrigin(options); + + var temp = particleCount; + var fettis = []; + + var startX = canvas.width * origin.x; + var startY = canvas.height * origin.y; + + while (temp--) { + fettis.push( + randomPhysics({ + x: startX, + y: startY, + angle: angle, + spread: spread, + startVelocity: startVelocity, + color: colors[temp % colors.length], + shape: shapes[randomInt(0, shapes.length)], + ticks: ticks, + decay: decay, + gravity: gravity, + drift: drift, + scalar: scalar, + flat: flat + }) + ); + } + + // if we have a previous canvas already animating, + // add to it + if (animationObj) { + return animationObj.addFettis(fettis); + } + + animationObj = animate(canvas, fettis, resizer, size , done); + + return animationObj.promise; + } + + function fire(options) { + var disableForReducedMotion = globalDisableForReducedMotion || prop(options, 'disableForReducedMotion', Boolean); + var zIndex = prop(options, 'zIndex', Number); + + if (disableForReducedMotion && preferLessMotion) { + return promise(function (resolve) { + resolve(); + }); + } + + if (isLibCanvas && animationObj) { + // use existing canvas from in-progress animation + canvas = animationObj.canvas; + } else if (isLibCanvas && !canvas) { + // create and initialize a new canvas + canvas = getCanvas(zIndex); + document.body.appendChild(canvas); + } + + if (allowResize && !initialized) { + // initialize the size of a user-supplied canvas + resizer(canvas); + } + + var size = { + width: canvas.width, + height: canvas.height + }; + + if (worker && !initialized) { + worker.init(canvas); + } + + initialized = true; + + if (worker) { + canvas.__confetti_initialized = true; + } + + function onResize() { + if (worker) { + // TODO this really shouldn't be immediate, because it is expensive + var obj = { + getBoundingClientRect: function () { + if (!isLibCanvas) { + return canvas.getBoundingClientRect(); + } + } + }; + + resizer(obj); + + worker.postMessage({ + resize: { + width: obj.width, + height: obj.height + } + }); + return; + } + + // don't actually query the size here, since this + // can execute frequently and rapidly + size.width = size.height = null; + } + + function done() { + animationObj = null; + + if (allowResize) { + hasResizeEventRegistered = false; + global.removeEventListener('resize', onResize); + } + + if (isLibCanvas && canvas) { + if (document.body.contains(canvas)) { + document.body.removeChild(canvas); + } + canvas = null; + initialized = false; + } + } + + if (allowResize && !hasResizeEventRegistered) { + hasResizeEventRegistered = true; + global.addEventListener('resize', onResize, false); + } + + if (worker) { + return worker.fire(options, size, done); + } + + return fireLocal(options, size, done); + } + + fire.reset = function () { + if (worker) { + worker.reset(); + } + + if (animationObj) { + animationObj.reset(); + } + }; + + return fire; + } + + // Make default export lazy to defer worker creation until called. + var defaultFire; + function getDefaultFire() { + if (!defaultFire) { + defaultFire = confettiCannon(null, { useWorker: true, resize: true }); + } + return defaultFire; + } + + function transformPath2D(pathString, pathMatrix, x, y, scaleX, scaleY, rotation) { + var path2d = new Path2D(pathString); + + var t1 = new Path2D(); + t1.addPath(path2d, new DOMMatrix(pathMatrix)); + + var t2 = new Path2D(); + // see https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrix/DOMMatrix + t2.addPath(t1, new DOMMatrix([ + Math.cos(rotation) * scaleX, + Math.sin(rotation) * scaleX, + -Math.sin(rotation) * scaleY, + Math.cos(rotation) * scaleY, + x, + y + ])); + + return t2; + } + + function shapeFromPath(pathData) { + if (!canUsePaths) { + throw new Error('path confetti are not supported in this browser'); + } + + var path, matrix; + + if (typeof pathData === 'string') { + path = pathData; + } else { + path = pathData.path; + matrix = pathData.matrix; + } + + var path2d = new Path2D(path); + var tempCanvas = document.createElement('canvas'); + var tempCtx = tempCanvas.getContext('2d'); + + if (!matrix) { + // attempt to figure out the width of the path, up to 1000x1000 + var maxSize = 1000; + var minX = maxSize; + var minY = maxSize; + var maxX = 0; + var maxY = 0; + var width, height; + + // do some line skipping... this is faster than checking + // every pixel and will be mostly still correct + for (var x = 0; x < maxSize; x += 2) { + for (var y = 0; y < maxSize; y += 2) { + if (tempCtx.isPointInPath(path2d, x, y, 'nonzero')) { + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } + } + } + + width = maxX - minX; + height = maxY - minY; + + var maxDesiredSize = 10; + var scale = Math.min(maxDesiredSize/width, maxDesiredSize/height); + + matrix = [ + scale, 0, 0, scale, + -Math.round((width/2) + minX) * scale, + -Math.round((height/2) + minY) * scale + ]; + } + + return { + type: 'path', + path: path, + matrix: matrix + }; + } + + function shapeFromText(textData) { + var text, + scalar = 1, + color = '#000000', + // see https://nolanlawson.com/2022/04/08/the-struggle-of-using-native-emoji-on-the-web/ + fontFamily = '"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", "EmojiOne Color", "Android Emoji", "Twemoji Mozilla", "system emoji", sans-serif'; + + if (typeof textData === 'string') { + text = textData; + } else { + text = textData.text; + scalar = 'scalar' in textData ? textData.scalar : scalar; + fontFamily = 'fontFamily' in textData ? textData.fontFamily : fontFamily; + color = 'color' in textData ? textData.color : color; + } + + // all other confetti are 10 pixels, + // so this pixel size is the de-facto 100% scale confetti + var fontSize = 10 * scalar; + var font = '' + fontSize + 'px ' + fontFamily; + + var canvas = new OffscreenCanvas(fontSize, fontSize); + var ctx = canvas.getContext('2d'); + + ctx.font = font; + var size = ctx.measureText(text); + var width = Math.ceil(size.actualBoundingBoxRight + size.actualBoundingBoxLeft); + var height = Math.ceil(size.actualBoundingBoxAscent + size.actualBoundingBoxDescent); + + var padding = 2; + var x = size.actualBoundingBoxLeft + padding; + var y = size.actualBoundingBoxAscent + padding; + width += padding + padding; + height += padding + padding; + + canvas = new OffscreenCanvas(width, height); + ctx = canvas.getContext('2d'); + ctx.font = font; + ctx.fillStyle = color; + + ctx.fillText(text, x, y); + + var scale = 1 / scalar; + + return { + type: 'bitmap', + // TODO these probably need to be transfered for workers + bitmap: canvas.transferToImageBitmap(), + matrix: [scale, 0, 0, scale, -width * scale / 2, -height * scale / 2] + }; + } + + module.exports = function() { + return getDefaultFire().apply(this, arguments); + }; + module.exports.reset = function() { + getDefaultFire().reset(); + }; + module.exports.create = confettiCannon; + module.exports.shapeFromPath = shapeFromPath; + module.exports.shapeFromText = shapeFromText; +}((function () { + if (typeof window !== 'undefined') { + return window; + } + + if (typeof self !== 'undefined') { + return self; + } + + return this || {}; +})(), module, false)); + +// end source content + + window.confetti = module.exports; +}(window, {})); + + /***/ }), /***/ "./node_modules/jquery-ui/ui/version.js": @@ -3181,7 +4047,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ $.ui = $.ui || {}; -return $.ui.version = "1.13.3"; +return $.ui.version = "1.14.0"; } ); @@ -3195,7 +4061,7 @@ return $.ui.version = "1.13.3"; /***/ ((module, exports, __webpack_require__) => { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! - * jQuery UI Widget 1.13.3 + * jQuery UI Widget 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -32476,6 +33342,7 @@ __webpack_require__.r(__webpack_exports__); /******/ // This entry module depends on other loaded chunks and execution need to be delayed /******/ __webpack_require__.O(undefined, ["css/dist/skins/skin-black-dark","css/dist/skins/_all-skins","css/build/overrides","css/build/app","css/build/AdminLTE","css/dist/skins/skin-yellow","css/dist/skins/skin-yellow-dark","css/dist/skins/skin-red","css/dist/skins/skin-red-dark","css/dist/skins/skin-purple","css/dist/skins/skin-purple-dark","css/dist/skins/skin-orange","css/dist/skins/skin-orange-dark","css/dist/skins/skin-green","css/dist/skins/skin-green-dark","css/dist/skins/skin-contrast","css/dist/skins/skin-blue","css/dist/skins/skin-blue-dark","css/dist/skins/skin-black"], () => (__webpack_require__("./resources/assets/js/snipeit.js"))) /******/ __webpack_require__.O(undefined, ["css/dist/skins/skin-black-dark","css/dist/skins/_all-skins","css/build/overrides","css/build/app","css/build/AdminLTE","css/dist/skins/skin-yellow","css/dist/skins/skin-yellow-dark","css/dist/skins/skin-red","css/dist/skins/skin-red-dark","css/dist/skins/skin-purple","css/dist/skins/skin-purple-dark","css/dist/skins/skin-orange","css/dist/skins/skin-orange-dark","css/dist/skins/skin-green","css/dist/skins/skin-green-dark","css/dist/skins/skin-contrast","css/dist/skins/skin-blue","css/dist/skins/skin-blue-dark","css/dist/skins/skin-black"], () => (__webpack_require__("./resources/assets/js/snipeit_modals.js"))) +/******/ __webpack_require__.O(undefined, ["css/dist/skins/skin-black-dark","css/dist/skins/_all-skins","css/build/overrides","css/build/app","css/build/AdminLTE","css/dist/skins/skin-yellow","css/dist/skins/skin-yellow-dark","css/dist/skins/skin-red","css/dist/skins/skin-red-dark","css/dist/skins/skin-purple","css/dist/skins/skin-purple-dark","css/dist/skins/skin-orange","css/dist/skins/skin-orange-dark","css/dist/skins/skin-green","css/dist/skins/skin-green-dark","css/dist/skins/skin-contrast","css/dist/skins/skin-blue","css/dist/skins/skin-blue-dark","css/dist/skins/skin-black"], () => (__webpack_require__("./node_modules/canvas-confetti/dist/confetti.browser.js"))) /******/ __webpack_require__.O(undefined, ["css/dist/skins/skin-black-dark","css/dist/skins/_all-skins","css/build/overrides","css/build/app","css/build/AdminLTE","css/dist/skins/skin-yellow","css/dist/skins/skin-yellow-dark","css/dist/skins/skin-red","css/dist/skins/skin-red-dark","css/dist/skins/skin-purple","css/dist/skins/skin-purple-dark","css/dist/skins/skin-orange","css/dist/skins/skin-orange-dark","css/dist/skins/skin-green","css/dist/skins/skin-green-dark","css/dist/skins/skin-contrast","css/dist/skins/skin-blue","css/dist/skins/skin-blue-dark","css/dist/skins/skin-black"], () => (__webpack_require__("./node_modules/admin-lte/build/less/AdminLTE.less"))) /******/ __webpack_require__.O(undefined, ["css/dist/skins/skin-black-dark","css/dist/skins/_all-skins","css/build/overrides","css/build/app","css/build/AdminLTE","css/dist/skins/skin-yellow","css/dist/skins/skin-yellow-dark","css/dist/skins/skin-red","css/dist/skins/skin-red-dark","css/dist/skins/skin-purple","css/dist/skins/skin-purple-dark","css/dist/skins/skin-orange","css/dist/skins/skin-orange-dark","css/dist/skins/skin-green","css/dist/skins/skin-green-dark","css/dist/skins/skin-contrast","css/dist/skins/skin-blue","css/dist/skins/skin-blue-dark","css/dist/skins/skin-black"], () => (__webpack_require__("./resources/assets/less/app.less"))) /******/ __webpack_require__.O(undefined, ["css/dist/skins/skin-black-dark","css/dist/skins/_all-skins","css/build/overrides","css/build/app","css/build/AdminLTE","css/dist/skins/skin-yellow","css/dist/skins/skin-yellow-dark","css/dist/skins/skin-red","css/dist/skins/skin-red-dark","css/dist/skins/skin-purple","css/dist/skins/skin-purple-dark","css/dist/skins/skin-orange","css/dist/skins/skin-orange-dark","css/dist/skins/skin-green","css/dist/skins/skin-green-dark","css/dist/skins/skin-contrast","css/dist/skins/skin-blue","css/dist/skins/skin-blue-dark","css/dist/skins/skin-black"], () => (__webpack_require__("./resources/assets/less/overrides.less"))) diff --git a/public/js/build/vendor.js b/public/js/build/vendor.js index 102713f1f..6f934b73d 100644 --- a/public/js/build/vendor.js +++ b/public/js/build/vendor.js @@ -12707,14 +12707,14 @@ return Tether; })); -/*! jQuery UI - v1.13.3 - 2024-04-26 +/*! jQuery UI - v1.14.0 - 2024-08-05 * https://jqueryui.com * Includes: widget.js, position.js, data.js, disable-selection.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js, focusable.js, form-reset-mixin.js, jquery-patch.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/draggable.js, widgets/droppable.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/resizable.js, widgets/selectable.js, widgets/selectmenu.js, widgets/slider.js, widgets/sortable.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js * Copyright OpenJS Foundation and other contributors; Licensed MIT */ ( function( factory ) { "use strict"; - + if ( typeof define === "function" && define.amd ) { // AMD. Register as an anonymous module. @@ -12729,11 +12729,11 @@ return Tether; $.ui = $.ui || {}; -var version = $.ui.version = "1.13.3"; +var version = $.ui.version = "1.14.0"; /*! - * jQuery UI Widget 1.13.3 + * jQuery UI Widget 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -13475,7 +13475,7 @@ var widget = $.widget; /*! - * jQuery UI Position 1.13.3 + * jQuery UI Position 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -13972,7 +13972,7 @@ var position = $.ui.position; /*! - * jQuery UI :data 1.13.3 + * jQuery UI :data 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -13987,21 +13987,15 @@ var position = $.ui.position; var data = $.extend( $.expr.pseudos, { - data: $.expr.createPseudo ? - $.expr.createPseudo( function( dataName ) { - return function( elem ) { - return !!$.data( elem, dataName ); - }; - } ) : - - // Support: jQuery <1.8 - function( elem, i, match ) { - return !!$.data( elem, match[ 3 ] ); - } + data: $.expr.createPseudo( function( dataName ) { + return function( elem ) { + return !!$.data( elem, dataName ); + }; + } ) } ); /*! - * jQuery UI Disable Selection 1.13.3 + * jQuery UI Disable Selection 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -14044,18 +14038,17 @@ var jQuery = $; /*! - * jQuery Color Animations v2.2.0 + * jQuery Color Animations v3.0.0 * https://github.com/jquery/jquery-color * * Copyright OpenJS Foundation and other contributors * Released under the MIT license. * https://jquery.org/license * - * Date: Sun May 10 09:02:36 2020 +0200 + * Date: Wed May 15 16:49:44 2024 +0200 */ - var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor " + "borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor", @@ -14180,10 +14173,6 @@ var jQuery = $; floor: true } }, - support = color.support = {}, - - // element for support tests - supportElem = jQuery( "

" )[ 0 ], // colors = jQuery.Color.names colors, @@ -14191,10 +14180,6 @@ var jQuery = $; // local aliases of functions called often each = jQuery.each; -// determine rgba support immediately -supportElem.style.cssText = "background-color:rgba(1,1,1,.5)"; -support.rgba = supportElem.style.backgroundColor.indexOf( "rgba" ) > -1; - // define cache name and alpha properties // for rgba and hsla spaces each( spaces, function( spaceName, space ) { @@ -14232,12 +14217,6 @@ function clamp( value, prop, allowEmpty ) { // ~~ is an short way of doing floor for positive numbers value = type.floor ? ~~value : parseFloat( value ); - // IE will pass in empty strings as value for alpha, - // which will hit this case - if ( isNaN( value ) ) { - return prop.def; - } - if ( type.mod ) { // we add mod before modding to make sure that negatives values @@ -14350,7 +14329,10 @@ color.fn = jQuery.extend( color.prototype, { } ); // everything defined but alpha? - if ( inst[ cache ] && jQuery.inArray( null, inst[ cache ].slice( 0, 3 ) ) < 0 ) { + if ( inst[ cache ] && jQuery.inArray( + null, + inst[ cache ].slice( 0, 3 ) + ) < 0 ) { // use the default of 1 if ( inst[ cache ][ 3 ] == null ) { @@ -14462,7 +14444,7 @@ color.fn = jQuery.extend( color.prototype, { prefix = "rgb("; } - return prefix + rgba.join() + ")"; + return prefix + rgba.join( ", " ) + ")"; }, toHslaString: function() { var prefix = "hsla(", @@ -14482,7 +14464,7 @@ color.fn = jQuery.extend( color.prototype, { hsla.pop(); prefix = "hsl("; } - return prefix + hsla.join() + ")"; + return prefix + hsla.join( ", " ) + ")"; }, toHexString: function( includeAlpha ) { var rgba = this._rgba.slice(), @@ -14495,12 +14477,11 @@ color.fn = jQuery.extend( color.prototype, { return "#" + jQuery.map( rgba, function( v ) { // default to 0 when nulls exist - v = ( v || 0 ).toString( 16 ); - return v.length === 1 ? "0" + v : v; + return ( "0" + ( v || 0 ).toString( 16 ) ).substr( -2 ); } ).join( "" ); }, toString: function() { - return this._rgba[ 3 ] === 0 ? "transparent" : this.toRgbaString(); + return this.toRgbaString(); } } ); color.fn.parse.prototype = color.fn; @@ -14667,37 +14648,15 @@ color.hook = function( hook ) { each( hooks, function( _i, hook ) { jQuery.cssHooks[ hook ] = { set: function( elem, value ) { - var parsed, curElem, - backgroundColor = ""; + var parsed; - if ( value !== "transparent" && ( getType( value ) !== "string" || ( parsed = stringParse( value ) ) ) ) { + if ( value !== "transparent" && + ( getType( value ) !== "string" || + ( parsed = stringParse( value ) ) ) ) { value = color( parsed || value ); - if ( !support.rgba && value._rgba[ 3 ] !== 1 ) { - curElem = hook === "backgroundColor" ? elem.parentNode : elem; - while ( - ( backgroundColor === "" || backgroundColor === "transparent" ) && - curElem && curElem.style - ) { - try { - backgroundColor = jQuery.css( curElem, "backgroundColor" ); - curElem = curElem.parentNode; - } catch ( e ) { - } - } - - value = value.blend( backgroundColor && backgroundColor !== "transparent" ? - backgroundColor : - "_default" ); - } - value = value.toRgbaString(); } - try { - elem.style[ hook ] = value; - } catch ( e ) { - - // wrapped to prevent IE from throwing errors on "invalid" values like 'auto' or 'inherit' - } + elem.style[ hook ] = value; } }; jQuery.fx.step[ hook ] = function( fx ) { @@ -14756,7 +14715,7 @@ colors = jQuery.Color.names = { /*! - * jQuery UI Effects 1.13.3 + * jQuery UI Effects 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -14819,26 +14778,14 @@ function camelCase( string ) { function getElementStyles( elem ) { var key, len, - style = elem.ownerDocument.defaultView ? - elem.ownerDocument.defaultView.getComputedStyle( elem, null ) : - elem.currentStyle, + style = elem.ownerDocument.defaultView.getComputedStyle( elem ), styles = {}; - if ( style && style.length && style[ 0 ] && style[ style[ 0 ] ] ) { - len = style.length; - while ( len-- ) { - key = style[ len ]; - if ( typeof style[ key ] === "string" ) { - styles[ camelCase( key ) ] = style[ key ]; - } - } - - // Support: Opera, IE <9 - } else { - for ( key in style ) { - if ( typeof style[ key ] === "string" ) { - styles[ key ] = style[ key ]; - } + len = style.length; + while ( len-- ) { + key = style[ len ]; + if ( typeof style[ key ] === "string" ) { + styles[ camelCase( key ) ] = style[ key ]; } } @@ -14863,15 +14810,6 @@ function styleDifference( oldStyle, newStyle ) { return diff; } -// Support: jQuery <1.8 -if ( !$.fn.addBack ) { - $.fn.addBack = function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); - }; -} - $.effects.animateClass = function( value, duration, easing, callback ) { var o = $.speed( duration, easing, callback ); @@ -15011,7 +14949,7 @@ if ( $.expr && $.expr.pseudos && $.expr.pseudos.animated ) { } )( $.expr.pseudos.animated ); } -if ( $.uiBackCompat !== false ) { +if ( $.uiBackCompat === true ) { $.extend( $.effects, { // Saves a set of properties in a data storage @@ -15140,7 +15078,7 @@ if ( $.uiBackCompat !== false ) { } $.extend( $.effects, { - version: "1.13.3", + version: "1.14.0", define: function( name, mode, effect ) { if ( !effect ) { @@ -15497,7 +15435,7 @@ $.fn.extend( { // as toggle can be either show or hide depending on element state args.mode = modes.shift(); - if ( $.uiBackCompat !== false && !defaultMode ) { + if ( $.uiBackCompat === true && !defaultMode ) { if ( elem.is( ":hidden" ) ? mode === "hide" : mode === "show" ) { // Call the core method to track "olddisplay" properly @@ -15708,7 +15646,7 @@ var effect = $.effects; /*! - * jQuery UI Effects Blind 1.13.3 + * jQuery UI Effects Blind 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -15763,7 +15701,7 @@ var effectsEffectBlind = $.effects.define( "blind", "hide", function( options, d /*! - * jQuery UI Effects Bounce 1.13.3 + * jQuery UI Effects Bounce 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -15858,7 +15796,7 @@ var effectsEffectBounce = $.effects.define( "bounce", function( options, done ) /*! - * jQuery UI Effects Clip 1.13.3 + * jQuery UI Effects Clip 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -15908,7 +15846,7 @@ var effectsEffectClip = $.effects.define( "clip", "hide", function( options, don /*! - * jQuery UI Effects Drop 1.13.3 + * jQuery UI Effects Drop 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -15962,7 +15900,7 @@ var effectsEffectDrop = $.effects.define( "drop", "hide", function( options, don /*! - * jQuery UI Effects Explode 1.13.3 + * jQuery UI Effects Explode 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16058,7 +15996,7 @@ var effectsEffectExplode = $.effects.define( "explode", "hide", function( option /*! - * jQuery UI Effects Fade 1.13.3 + * jQuery UI Effects Fade 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16090,7 +16028,7 @@ var effectsEffectFade = $.effects.define( "fade", "toggle", function( options, d /*! - * jQuery UI Effects Fold 1.13.3 + * jQuery UI Effects Fold 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16164,7 +16102,7 @@ var effectsEffectFold = $.effects.define( "fold", "hide", function( options, don /*! - * jQuery UI Effects Highlight 1.13.3 + * jQuery UI Effects Highlight 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16206,7 +16144,7 @@ var effectsEffectHighlight = $.effects.define( "highlight", "show", function( op /*! - * jQuery UI Effects Size 1.13.3 + * jQuery UI Effects Size 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16384,7 +16322,7 @@ var effectsEffectSize = $.effects.define( "size", function( options, done ) { /*! - * jQuery UI Effects Scale 1.13.3 + * jQuery UI Effects Scale 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16424,7 +16362,7 @@ var effectsEffectScale = $.effects.define( "scale", function( options, done ) { /*! - * jQuery UI Effects Puff 1.13.3 + * jQuery UI Effects Puff 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16450,7 +16388,7 @@ var effectsEffectPuff = $.effects.define( "puff", "hide", function( options, don /*! - * jQuery UI Effects Pulsate 1.13.3 + * jQuery UI Effects Pulsate 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16499,7 +16437,7 @@ var effectsEffectPulsate = $.effects.define( "pulsate", "show", function( option /*! - * jQuery UI Effects Shake 1.13.3 + * jQuery UI Effects Shake 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16558,7 +16496,7 @@ var effectsEffectShake = $.effects.define( "shake", function( options, done ) { /*! - * jQuery UI Effects Slide 1.13.3 + * jQuery UI Effects Slide 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16619,7 +16557,7 @@ var effectsEffectSlide = $.effects.define( "slide", "show", function( options, d /*! - * jQuery UI Effects Transfer 1.13.3 + * jQuery UI Effects Transfer 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16635,7 +16573,7 @@ var effectsEffectSlide = $.effects.define( "slide", "show", function( options, d var effect; -if ( $.uiBackCompat !== false ) { +if ( $.uiBackCompat === true ) { effect = $.effects.define( "transfer", function( options, done ) { $( this ).transfer( options, done ); } ); @@ -16644,7 +16582,7 @@ var effectsEffectTransfer = effect; /*! - * jQuery UI Focusable 1.13.3 + * jQuery UI Focusable 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16693,20 +16631,10 @@ $.ui.focusable = function( element, hasTabindex ) { focusableIfVisible = hasTabindex; } - return focusableIfVisible && $( element ).is( ":visible" ) && visible( $( element ) ); + return focusableIfVisible && $( element ).is( ":visible" ) && + $( element ).css( "visibility" ) === "visible"; }; -// Support: IE 8 only -// IE 8 doesn't resolve inherit to visible/hidden for computed values -function visible( element ) { - var visibility = element.css( "visibility" ); - while ( visibility === "inherit" ) { - element = element.parent(); - visibility = element.css( "visibility" ); - } - return visibility === "visible"; -} - $.extend( $.expr.pseudos, { focusable: function( element ) { return $.ui.focusable( element, $.attr( element, "tabindex" ) != null ); @@ -16716,17 +16644,8 @@ $.extend( $.expr.pseudos, { var focusable = $.ui.focusable; - -// Support: IE8 Only -// IE8 does not support the form attribute and when it is supplied. It overwrites the form prop -// with a string, so we need to find the proper form. -var form = $.fn._form = function() { - return typeof this[ 0 ].form === "string" ? this.closest( "form" ) : $( this[ 0 ].form ); -}; - - /*! - * jQuery UI Form Reset Mixin 1.13.3 + * jQuery UI Form Reset Mixin 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16754,7 +16673,7 @@ var formResetMixin = $.ui.formResetMixin = { }, _bindFormResetHandler: function() { - this.form = this.element._form(); + this.form = $( this.element.prop( "form" ) ); if ( !this.form.length ) { return; } @@ -16788,7 +16707,7 @@ var formResetMixin = $.ui.formResetMixin = { /*! - * jQuery UI Support for jQuery core 1.8.x and newer 1.13.3 + * jQuery UI Legacy jQuery Core patches 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16797,50 +16716,17 @@ var formResetMixin = $.ui.formResetMixin = { * */ -//>>label: jQuery 1.8+ Support +//>>label: Legacy jQuery Core patches //>>group: Core -//>>description: Support version 1.8.x and newer of jQuery core +//>>description: Backport `.even()`, `.odd()` and `$.escapeSelector` to older jQuery Core versions (deprecated) -// Support: jQuery 1.9.x or older -// $.expr[ ":" ] is deprecated. -if ( !$.expr.pseudos ) { - $.expr.pseudos = $.expr[ ":" ]; -} - -// Support: jQuery 1.11.x or older -// $.unique has been renamed to $.uniqueSort -if ( !$.uniqueSort ) { - $.uniqueSort = $.unique; -} - // Support: jQuery 2.2.x or older. // This method has been defined in jQuery 3.0.0. // Code from https://github.com/jquery/jquery/blob/e539bac79e666bba95bba86d690b4e609dca2286/src/selector/escapeSelector.js if ( !$.escapeSelector ) { - - // CSS string/identifier serialization - // https://drafts.csswg.org/cssom/#common-serializing-idioms - var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; - - var fcssescape = function( ch, asCodePoint ) { - if ( asCodePoint ) { - - // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER - if ( ch === "\0" ) { - return "\uFFFD"; - } - - // Control characters and (dependent upon position) numbers get escaped as code points - return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; - } - - // Other potentially-special ASCII characters get backslash-escaped - return "\\" + ch; - }; - - $.escapeSelector = function( sel ) { - return ( sel + "" ).replace( rcssescape, fcssescape ); + $.escapeSelector = function( id ) { + return CSS.escape( id + "" ); }; } @@ -16863,7 +16749,7 @@ if ( !$.fn.even || !$.fn.odd ) { ; /*! - * jQuery UI Keycode 1.13.3 + * jQuery UI Keycode 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16898,7 +16784,7 @@ var keycode = $.ui.keyCode = { /*! - * jQuery UI Labels 1.13.3 + * jQuery UI Labels 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16924,9 +16810,8 @@ var labels = $.fn.labels = function() { return this.pushStack( this[ 0 ].labels ); } - // Support: IE <= 11, FF <= 37, Android <= 2.3 only - // Above browsers do not support control.labels. Everything below is to support them - // as well as document fragments. control.labels does not work on document fragments + // If `control.labels` is empty - e.g. inside of document fragments - find + // the labels manually labels = this.eq( 0 ).parents( "label" ); // Look for the label based on the id @@ -16941,7 +16826,7 @@ var labels = $.fn.labels = function() { ancestors = ancestor.add( ancestor.length ? ancestor.siblings() : this.siblings() ); // Create a selector for the label based on the id - selector = "label[for='" + $.escapeSelector( id ) + "']"; + selector = "label[for='" + CSS.escape( id ) + "']"; labels = labels.add( ancestors.find( selector ).addBack( selector ) ); @@ -16953,7 +16838,7 @@ var labels = $.fn.labels = function() { /*! - * jQuery UI Scroll Parent 1.13.3 + * jQuery UI Scroll Parent 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -16987,7 +16872,7 @@ var scrollParent = $.fn.scrollParent = function( includeHidden ) { /*! - * jQuery UI Tabbable 1.13.3 + * jQuery UI Tabbable 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -17011,7 +16896,7 @@ var tabbable = $.extend( $.expr.pseudos, { /*! - * jQuery UI Unique ID 1.13.3 + * jQuery UI Unique ID 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -17049,7 +16934,7 @@ var uniqueId = $.fn.extend( { /*! - * jQuery UI Accordion 1.13.3 + * jQuery UI Accordion 1.14.0 * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -17070,7 +16955,7 @@ var uniqueId = $.fn.extend( { var widgetsAccordion = $.widget( "ui.accordion", { - version: "1.13.3", + version: "1.14.0", options: { active: 0, animate: {}, @@ -17082,7 +16967,17 @@ var widgetsAccordion = $.widget( "ui.accordion", { collapsible: false, event: "click", header: function( elem ) { - return elem.find( "> li > :first-child" ).add( elem.find( "> :not(li)" ).even() ); + return elem + .find( "> li > :first-child" ) + .add( + elem.find( "> :not(li)" ) + + // Support: jQuery <3.5 only + // We could use `.even()` but that's unavailable in older jQuery. + .filter( function( i ) { + return i % 2 === 0; + } ) + ); }, heightStyle: "auto", icons: { @@ -17217,13 +17112,7 @@ var widgetsAccordion = $.widget( "ui.accordion", { this._super( value ); this.element.attr( "aria-disabled", value ); - - // Support: IE8 Only - // #5332 / #6059 - opacity doesn't cascade to positioned elements in IE - // so we need to add the disabled class to the headers and panels this._toggleClass( null, "ui-state-disabled", !!value ); - this._toggleClass( this.headers.add( this.headers.next() ), null, "ui-state-disabled", - !!value ); }, _keydown: function( event ) { @@ -17641,47 +17530,13 @@ var widgetsAccordion = $.widget( "ui.accordion", { this._removeClass( prev, "ui-accordion-header-active" ) ._addClass( prev, "ui-accordion-header-collapsed" ); - // Work around for rendering bug in IE (#5421) - if ( toHide.length ) { - toHide.parent()[ 0 ].className = toHide.parent()[ 0 ].className; - } this._trigger( "activate", null, data ); } } ); - -var safeActiveElement = $.ui.safeActiveElement = function( document ) { - var activeElement; - - // Support: IE 9 only - // IE9 throws an "Unspecified error" accessing document.activeElement from an