Compare commits

..

41 Commits

Author SHA1 Message Date
snipe c02a6c105a Bumped version 2026-05-26 23:24:04 +01:00
snipe cfa8069953 Merge remote-tracking branch 'origin/develop' 2026-05-25 13:35:12 +01:00
snipe b3be2baf40 Merge remote-tracking branch 'origin/develop' 2026-05-23 01:54:06 +01:00
snipe 069912d051 Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	config/version.php
#	public/js/dist/all.js
#	public/js/dist/all.js.map
#	public/mix-manifest.json
2026-05-22 13:06:41 +01:00
snipe 86245ad4ae Merge remote-tracking branch 'origin/develop' 2026-05-22 12:44:37 +01:00
snipe c8bafdad79 Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	config/version.php
2026-05-22 12:28:56 +01:00
snipe c94fce2367 Merge remote-tracking branch 'origin/develop' 2026-05-22 12:00:57 +01:00
snipe 653b1327cb Merge remote-tracking branch 'origin/develop' 2026-05-22 11:56:32 +01:00
snipe 849b217300 Merge remote-tracking branch 'origin/develop' 2026-05-22 10:44:13 +01:00
snipe 371f096e54 Merge remote-tracking branch 'origin/develop' 2026-05-22 09:31:44 +01:00
snipe 72a11113e7 Merge remote-tracking branch 'origin/develop' 2026-05-21 20:20:17 +01:00
snipe b0635f24db Merge remote-tracking branch 'origin/develop' 2026-05-21 16:12:50 +01:00
snipe 96088c416e Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	public/js/dist/all.js
#	public/js/dist/all.js.map
#	public/mix-manifest.json
2026-05-21 15:26:47 +01:00
snipe c8f3e833e5 Prod assets 2026-05-21 15:26:15 +01:00
snipe 5307a44fab Merge remote-tracking branch 'origin/develop' 2026-05-21 15:06:36 +01:00
snipe 2d6eb5d80a Merge remote-tracking branch 'origin/develop' 2026-05-20 18:08:38 +01:00
snipe 90e2c105cd Merge remote-tracking branch 'origin/develop' 2026-05-20 18:05:03 +01:00
snipe 875b0bbdec Merge remote-tracking branch 'origin/develop' 2026-05-19 08:38:14 +01:00
snipe be1f1bd1c5 Merge remote-tracking branch 'origin/develop' 2026-05-19 08:09:13 +01:00
snipe c9be696c84 Merge remote-tracking branch 'origin/develop' 2026-05-18 20:14:54 +01:00
snipe 187f160b21 Merge remote-tracking branch 'origin/develop' 2026-05-18 16:31:34 +01:00
snipe 8908b67b3d Merge remote-tracking branch 'origin/develop' 2026-05-18 16:26:27 +01:00
snipe 4373f761c7 Merge remote-tracking branch 'origin/develop' 2026-05-18 16:18:04 +01:00
snipe 8e9bd5dbb1 Merge remote-tracking branch 'origin/develop' 2026-05-18 13:54:48 +01:00
snipe 751541a54d Merge remote-tracking branch 'origin/develop' 2026-05-18 13:12:22 +01:00
snipe 3972799e56 Merge remote-tracking branch 'origin/develop' 2026-05-18 12:47:05 +01:00
snipe db2afd0dc7 Merge remote-tracking branch 'origin/develop' 2026-05-18 12:33:42 +01:00
snipe 460daf71b6 Merge remote-tracking branch 'origin/develop' 2026-05-18 12:04:12 +01:00
snipe 3074bae47c Merge remote-tracking branch 'origin/develop' 2026-05-18 11:56:26 +01:00
snipe 0f80950a91 Merge remote-tracking branch 'origin/develop' 2026-05-15 01:56:34 +01:00
snipe 2620b60048 Merge remote-tracking branch 'origin/develop' 2026-05-15 00:43:51 +01:00
snipe 81b1cdc6e9 Merge remote-tracking branch 'origin/develop' 2026-05-14 16:58:04 +01:00
snipe 0304933c53 Merge remote-tracking branch 'origin/develop' 2026-05-14 16:46:42 +01:00
snipe f0d84f5350 Merge remote-tracking branch 'origin/develop' 2026-05-14 16:34:42 +01:00
snipe 1ad562f8b9 Merge remote-tracking branch 'origin/develop' 2026-05-14 12:38:37 +01:00
snipe a5cea247f1 Merge remote-tracking branch 'origin/develop' 2026-05-14 11:35:54 +01:00
snipe 571bc39495 Merge remote-tracking branch 'origin/develop' 2026-05-14 10:40:52 +01:00
snipe 8ea78fae21 Merge remote-tracking branch 'origin/develop' 2026-05-13 21:59:26 +01:00
snipe ed6b3c04ab Merge remote-tracking branch 'origin/develop' 2026-05-13 12:18:25 +01:00
snipe a4ca0a592f Merge remote-tracking branch 'origin/develop' 2026-05-13 10:20:00 +01:00
snipe 90c8689596 Prod assets 2026-05-12 19:43:34 +01:00
1124 changed files with 1116 additions and 134879 deletions
-9
View File
@@ -4271,15 +4271,6 @@
"contributions": [
"code"
]
},
{
"login": "CybotTM",
"name": "Sebastian Mendel",
"avatar_url": "https://avatars.githubusercontent.com/u/326348?v=4",
"profile": "https://github.com/CybotTM",
"contributions": [
"code"
]
}
]
}
+1 -1
View File
@@ -113,7 +113,7 @@ ENABLE_HSTS=false
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync
QUEUE_DRIVER=sync
CACHE_PREFIX=snipeit
# --------------------------------------------
+1 -1
View File
@@ -120,7 +120,7 @@ ENABLE_HSTS=false
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync
QUEUE_DRIVER=sync
CACHE_PREFIX=snipeit
# --------------------------------------------
+1 -1
View File
@@ -72,7 +72,7 @@ CORS_ALLOWED_ORIGINS="*"
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync
QUEUE_DRIVER=sync
# --------------------------------------------
# OPTIONAL: LOGIN THROTTLING
+2 -3
View File
@@ -133,7 +133,7 @@ BS_TABLE_DEEPLINK=true
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
ALLOW_IFRAMING=false
REFERRER_POLICY=same-origin
ENABLE_CSP=true
ENABLE_CSP=false
ADDITIONAL_CSP_URLS=null
CORS_ALLOWED_ORIGINS=null
ENABLE_HSTS=false
@@ -142,7 +142,7 @@ ENABLE_HSTS=false
# OPTIONAL: CACHE SETTINGS
# --------------------------------------------
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
QUEUE_DRIVER=sync
CACHE_PREFIX=snipeit
# --------------------------------------------
@@ -210,7 +210,6 @@ LOGIN_AUTOCOMPLETE=false
RESET_PASSWORD_LINK_EXPIRES=15
PASSWORD_CONFIRM_TIMEOUT=10800
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
TWO_FACTOR_MAX_ATTEMPTS_PER_MIN=5
INVITE_PASSWORD_LINK_EXPIRES=1500
# --------------------------------------------
+1 -1
View File
@@ -69,7 +69,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars.githubusercontent.com/u/10965027?v=4" width="110px;"/><br /><sub>Ellie</sub>](https://leafedfox.xyz/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeafedFox "Code") | [<img src="https://avatars.githubusercontent.com/u/20960555?v=4" width="110px;"/><br /><sub>GA Stamper</sub>](https://github.com/gastamper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gastamper "Code") | [<img src="https://avatars.githubusercontent.com/u/206553556?v=4" width="110px;"/><br /><sub>Guillaume Lefranc</sub>](https://github.com/gl-pup)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gl-pup "Code") | [<img src="https://avatars.githubusercontent.com/u/733892?v=4" width="110px;"/><br /><sub>Hajo Möller</sub>](https://github.com/dasjoe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dasjoe "Code") | [<img src="https://avatars.githubusercontent.com/u/3420063?v=4" width="110px;"/><br /><sub>Istvan Basa</sub>](https://github.com/pottom)<br />[💻](https://github.com/snipe/snipe-it/commits?author=pottom "Code") | [<img src="https://avatars.githubusercontent.com/u/810824?v=4" width="110px;"/><br /><sub>JJ Asghar</sub>](https://jjasghar.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jjasghar "Code") | [<img src="https://avatars.githubusercontent.com/u/40404495?v=4" width="110px;"/><br /><sub>James E. Msenga</sub>](https://github.com/JemCdo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JemCdo "Code") |
| [<img src="https://avatars.githubusercontent.com/u/6865786?v=4" width="110px;"/><br /><sub>Jan Felix Wiebe</sub>](https://github.com/jfwiebe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jfwiebe "Code") | [<img src="https://avatars.githubusercontent.com/u/43412008?v=4" width="110px;"/><br /><sub>Jo Drexl</sub>](https://www.nfon.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=drexljo "Code") | [<img src="https://avatars.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>Austin Sasko</sub>](https://github.com/austinsasko)<br />[💻](https://github.com/snipe/snipe-it/commits?author=austinsasko "Code") | [<img src="https://avatars.githubusercontent.com/u/4875039?v=4" width="110px;"/><br /><sub>Jasson</sub>](http://jassoncordones.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JassonCordones "Code") | [<img src="https://avatars.githubusercontent.com/u/76069640?v=4" width="110px;"/><br /><sub>Okean</sub>](https://github.com/Tinyblargon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tinyblargon "Code") | [<img src="https://avatars.githubusercontent.com/u/6515064?v=4" width="110px;"/><br /><sub>Alejandro Medrano</sub>](https://www.lst.tfo.upm.es/alejandro-medrano/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=amedranogil "Code") | [<img src="https://avatars.githubusercontent.com/u/58696401?v=4" width="110px;"/><br /><sub>Lukas Kraic</sub>](https://github.com/lukaskraic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukaskraic "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1571724?v=4" width="110px;"/><br /><sub>Герхард PICCORO Lenz McKAY </sub>](https://github-readme-stats.vercel.app/api?username=mckaygerhard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") | [<img src="https://avatars.githubusercontent.com/u/15015119?v=4" width="110px;"/><br /><sub>Johannes Pollitt</sub>](https://github.com/FlorestanII)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorestanII "Code") | [<img src="https://avatars.githubusercontent.com/u/14185442?v=4" width="110px;"/><br /><sub>Michael Strobel</sub>](https://strobelm.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=strobelm "Code") | [<img src="https://avatars.githubusercontent.com/u/634790?v=4" width="110px;"/><br /><sub>Nicky West</sub>](http://nickwest.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nickwest "Code") | [<img src="https://avatars.githubusercontent.com/u/1347327?v=4" width="110px;"/><br /><sub>akaspeh1</sub>](https://github.com/akaspeh1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akaspeh1 "Code") | [<img src="https://avatars.githubusercontent.com/u/2880129?v=4" width="110px;"/><br /><sub>Sebastian Marsching</sub>](http://sebastian.marsching.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smarsching "Code") | [<img src="https://avatars.githubusercontent.com/u/40658372?v=4" width="110px;"/><br /><sub>Mo</sub>](https://github.com/mohammad-ahmadi1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mohammad-ahmadi1 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/20994684?v=4" width="110px;"/><br /><sub>Owen V. Hayes</sub>](https://github.com/MarvelousAnything)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MarvelousAnything "Code") | [<img src="https://avatars.githubusercontent.com/u/75509373?v=4" width="110px;"/><br /><sub>Peter Gallwas</sub>](https://www.husky.nz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Husky-Devel "Code") | [<img src="https://avatars.githubusercontent.com/u/326348?v=4" width="110px;"/><br /><sub>Sebastian Mendel</sub>](https://github.com/CybotTM)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CybotTM "Code") |
| [<img src="https://avatars.githubusercontent.com/u/20994684?v=4" width="110px;"/><br /><sub>Owen V. Hayes</sub>](https://github.com/MarvelousAnything)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MarvelousAnything "Code") | [<img src="https://avatars.githubusercontent.com/u/75509373?v=4" width="110px;"/><br /><sub>Peter Gallwas</sub>](https://www.husky.nz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Husky-Devel "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
-1
View File
@@ -56,7 +56,6 @@ COPY --from=mlocati/php-extension-installer:2.1.15 /usr/bin/install-php-extensio
RUN set -eux; \
install-php-extensions \
bcmath \
exif \
gd \
ldap \
mysqli \
-1
View File
@@ -98,7 +98,6 @@ Since the release of the JSON REST API, several third-party developers have been
- [InQRy (archived)](https://github.com/Microsoft/InQRy) by [@Microsoft](https://github.com/Microsoft)
- [Marksman (archived)](https://github.com/Scope-IT/marksman) - A Windows agent for Snipe-IT
- [Python Module (archived)](https://github.com/jbloomer/SnipeIT-PythonAPI) by [@jbloomer](https://github.com/jbloomer)
[IT-Tools](https://github.com/chrisnox/Snipeit-it-tools) by @chrisnox - Browser bookmarklets for PDF handover/return protocols, digital signatures, label printing (Zebra ZD410), AirWatch MDM sync and Lansweeper CSV import.
We also have a handful of [Google Apps scripts](https://github.com/grokability/google-apps-scripts-for-snipe-it) to help with various tasks.
@@ -234,10 +234,6 @@ class AccessoriesController extends Controller
$total = $accessory_checkouts->count();
$accessory_checkouts = $accessory_checkouts->skip($offset)->take($limit)->get();
$accessory_checkouts->loadMorph('assignedTo', [
User::class => ['companies'],
]);
return (new AccessoriesTransformer)->transformCheckedoutAccessory($accessory_checkouts, $total);
}
@@ -307,7 +303,7 @@ class AccessoriesController extends Controller
$this->authorize('checkout', $accessory);
$target = $this->determineCheckoutTarget();
if ((Setting::getSettings()->full_multiple_companies_support == '1') && (! $target->companies()->where('companies.id', $accessory->company_id)->exists())) {
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($accessory->company_id !== $target->company_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
}
+2 -11
View File
@@ -371,12 +371,6 @@ class AssetsController extends Controller
$assets->where('assets.order_number', '=', strval($request->input('order_number')));
}
foreach ($all_custom_fields as $field) {
if ($request->filled($field->db_column_name())) {
$assets->where($field->db_column_name(), '=', $request->input($field->db_column_name()));
}
}
// This is kinda gross, but we need to do this because the Bootstrap Tables
// API passes custom field ordering as custom_fields.fieldname, and we have to strip
// that out to let the default sorter below order them correctly on the assets table.
@@ -609,11 +603,8 @@ class AssetsController extends Controller
])->with('model', 'status', 'assignedTo')
->NotArchived();
if ((Setting::getSettings()->full_multiple_companies_support == '1') && $request->filled('companyId')) {
$companyIds = array_values(array_filter(array_map('intval', explode(',', $request->input('companyId')))));
if (! empty($companyIds)) {
$assets->whereIn('assets.company_id', $companyIds);
}
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($request->filled('companyId'))) {
$assets->where('assets.company_id', $request->input('companyId'));
}
if ($request->filled('statusType') && $request->input('statusType') === 'RTD') {
@@ -315,7 +315,7 @@ class ConsumablesController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, 'No user found'));
}
if ((Setting::getSettings()->full_multiple_companies_support == '1') && (! $user->companies()->where('companies.id', $consumable->company_id)->exists())) {
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($consumable->company_id !== $user->company_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
}
@@ -27,7 +27,7 @@ class LicenseSeatsController extends Controller
if ($license = License::find($licenseId)) {
$this->authorize('view', $license);
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department', 'user.companies', 'asset.company')
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department', 'user.company', 'asset.company')
->where('license_seats.license_id', $licenseId);
if ($request->input('status') == 'available') {
@@ -132,110 +132,91 @@ class LicenseSeatsController extends Controller
$this->authorize('checkout', License::class);
$errorResponse = null;
$updatedSeat = null;
$licenseSeat = LicenseSeat::with(['license', 'asset', 'user'])->find($seatId);
// Fetch the seat with a pessimistic lock inside a transaction so concurrent requests
// on the same seat serialise rather than racing to overwrite each other's assignment.
DB::transaction(function () use ($request, $licenseId, $seatId, $validated, &$errorResponse, &$updatedSeat): void {
$licenseSeat = LicenseSeat::with(['license', 'asset', 'user'])
->lockForUpdate()
->find($seatId);
if (! $licenseSeat) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat not found'));
}
if (! $licenseSeat) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, 'Seat not found'));
$license = $licenseSeat->license;
if (! $license || $license->id != intval($licenseId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat does not belong to the specified license'));
}
return;
$targetUser = null;
if (! is_null($request->input('assigned_to'))) {
// Resolve unscoped target so we can return a clean cross-company error instead of a hidden-not-found.
$targetUser = User::withoutGlobalScopes()->find($request->input('assigned_to'));
if (! $targetUser) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Target not found'));
}
$license = $licenseSeat->license;
if (! $license || $license->id != intval($licenseId)) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, 'Seat does not belong to the specified license'));
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($license->company_id !== $targetUser->company_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
}
}
return;
$targetAsset = null;
if (! is_null($request->input('asset_id'))) {
// Resolve unscoped target so FMCS company mismatch can be enforced explicitly.
$targetAsset = Asset::withoutGlobalScopes()->find($request->input('asset_id'));
if (! $targetAsset) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Target not found'));
}
$targetUser = null;
if (! is_null($request->input('assigned_to'))) {
// Resolve unscoped target so we can return a clean cross-company error instead of a hidden-not-found.
$targetUser = User::withoutGlobalScopes()->find($request->input('assigned_to'));
if (! $targetUser) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, 'Target not found'));
return;
}
if ((Setting::getSettings()->full_multiple_companies_support == '1') && (! $targetUser->companies()->where('companies.id', $license->company_id)->exists())) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
return;
}
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($license->company_id !== $targetAsset->company_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
}
}
$targetAsset = null;
if (! is_null($request->input('asset_id'))) {
// Resolve unscoped target so FMCS company mismatch can be enforced explicitly.
$targetAsset = Asset::withoutGlobalScopes()->find($request->input('asset_id'));
$oldUser = $licenseSeat->user;
$oldAsset = $licenseSeat->asset;
if (! $targetAsset) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, 'Target not found'));
// attempt to update the license seat
$licenseSeat->fill($validated);
return;
}
// check if this update is a checkin operation
// 1. are relevant fields touched at all?
$assignmentTouched = $licenseSeat->isDirty('assigned_to') || $licenseSeat->isDirty('asset_id');
$anythingTouched = $licenseSeat->isDirty();
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($license->company_id !== $targetAsset->company_id)) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
if (! $anythingTouched) {
return response()->json(
Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success'))
);
}
if ($assignmentTouched && $licenseSeat->unreassignable_seat) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.unavailable')));
}
return;
}
}
$oldUser = $licenseSeat->user;
$oldAsset = $licenseSeat->asset;
$licenseSeat->fill($validated);
$assignmentTouched = $licenseSeat->isDirty('assigned_to') || $licenseSeat->isDirty('asset_id');
$anythingTouched = $licenseSeat->isDirty();
if (! $anythingTouched) {
$updatedSeat = $licenseSeat;
return;
}
if ($assignmentTouched && $licenseSeat->unreassignable_seat) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.unavailable')));
return;
}
// Are the assignment fields cleared? If yes, this is a checkin operation.
$is_checkin = ($assignmentTouched && $licenseSeat->assigned_to === null && $licenseSeat->asset_id === null);
// The logging functions expect only one "target"; assets take precedence over users.
$target = null;
if ($licenseSeat->isDirty('assigned_to')) {
$target = $is_checkin ? $oldUser : $targetUser;
}
if ($licenseSeat->isDirty('asset_id')) {
$target = $is_checkin ? $oldAsset : $targetAsset;
}
if ($assignmentTouched && is_null($target)) {
// Both fields are null but one was provided — the related model is purged or bad data.
if (! is_null($request->input('asset_id')) || ! is_null($request->input('assigned_to'))) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, 'Target not found'));
return;
}
// 2. are they cleared? if yes then this is a checkin operation
$is_checkin = ($assignmentTouched && $licenseSeat->assigned_to === null && $licenseSeat->asset_id === null);
$target = null;
// the logging functions expect only one "target". if both asset and user are present in the request,
// we simply let assets take precedence over users...
if ($licenseSeat->isDirty('assigned_to')) {
$target = $is_checkin ? $oldUser : $targetUser;
}
if ($licenseSeat->isDirty('asset_id')) {
$target = $is_checkin ? $oldAsset : $targetAsset;
}
if ($assignmentTouched && is_null($target)) {
// if both asset_id and assigned_to are null then we are "checking-in"
// a related model that does not exist (possible purged or bad data).
if (! is_null($request->input('asset_id')) || ! is_null($request->input('assigned_to'))) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Target not found'));
}
}
// Keep seat updates and checkout/checkin logging atomic to prevent partial state changes.
$updated = DB::transaction(function () use ($licenseSeat, $assignmentTouched, $is_checkin, $target, $request): bool {
if (! $licenseSeat->save()) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, $licenseSeat->getErrors()));
return;
return false;
}
if ($assignmentTouched) {
@@ -244,29 +225,25 @@ class LicenseSeatsController extends Controller
$licenseSeat->unreassignable_seat = true;
if (! $licenseSeat->save()) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, $licenseSeat->getErrors()));
return;
return false;
}
}
// todo: skip if target is null?
$licenseSeat->logCheckin($target, $licenseSeat->notes);
} else {
// in this case, relevant fields are touched but it's not a checkin operation. so it must be a checkout operation.
$licenseSeat->logCheckout($request->input('notes'), $target);
}
}
$updatedSeat = $licenseSeat;
return true;
});
if ($errorResponse) {
return $errorResponse;
if ($updated) {
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
}
if ($updatedSeat) {
return response()->json(Helper::formatStandardApiResponse('success', $updatedSeat, trans('admin/licenses/message.update.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, 'An unexpected error occurred'), 500);
return Helper::formatStandardApiResponse('error', null, $licenseSeat->getErrors());
}
}
@@ -2,21 +2,15 @@
namespace App\Http\Controllers\Api;
use App\Events\CheckoutableCheckedIn;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\FilterRequest;
use App\Http\Transformers\ActionlogsTransformer;
use App\Http\Transformers\LicenseSeatsTransformer;
use App\Http\Transformers\LicensesTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Models\Asset;
use App\Models\Company;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -267,167 +261,6 @@ class LicensesController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.assoc_users')));
}
/**
* Checkout a license seat to a user or asset.
*
* Accepts an optional `seat_id`; if omitted the next available free seat is used.
* `target_type` must be "user" or "asset". Supply `assigned_to` for users or
* `asset_id` for assets.
*
* This will eventually use the same form request the UI uses, but we need to update the field names first.
*
* @param int $licenseId
*/
public function checkout(Request $request, $licenseId): JsonResponse
{
$license = License::findOrFail($licenseId);
$this->authorize('checkout', $license);
$validated = $this->validate($request, [
'seat_id' => 'sometimes|integer|nullable',
'target_type' => 'required|in:user,asset',
'assigned_to' => 'required_if:target_type,user|integer|nullable',
'asset_id' => 'required_if:target_type,asset|integer|nullable',
'notes' => 'sometimes|string|nullable',
]);
if ($license->isInactive()) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.license_is_inactive')));
}
$errorResponse = null;
$updatedSeat = null;
$target = null;
DB::transaction(function () use ($license, $validated, &$errorResponse, &$updatedSeat, &$target): void {
$seatId = $validated['seat_id'] ?? null;
$licenseSeat = $seatId
? LicenseSeat::where('id', $seatId)->where('license_id', $license->id)->lockForUpdate()->first()
: $license->freeSeat(lock: true);
if (! $licenseSeat) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.not_enough_seats')));
return;
}
if ($licenseSeat->unreassignable_seat) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.unavailable')));
return;
}
if ($validated['target_type'] === 'user') {
$target = User::withoutGlobalScopes()->whereNull('deleted_at')->find($validated['assigned_to'] ?? null);
if (! $target) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.user_does_not_exist')));
return;
}
if (Company::isFullMultipleCompanySupportEnabled() && ! $target->companies()->where('companies.id', $license->company_id)->exists()) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
return;
}
$licenseSeat->assigned_to = $target->id;
$licenseSeat->asset_id = null;
} else {
$target = Asset::withoutGlobalScopes()->whereNull('deleted_at')->find($validated['asset_id'] ?? null);
if (! $target) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.asset_does_not_exist')));
return;
}
if (Company::isFullMultipleCompanySupportEnabled() && $license->company_id && $license->company_id !== $target->company_id) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
return;
}
$licenseSeat->asset_id = $target->id;
$licenseSeat->assigned_to = null;
if ($target->checkedOutToUser()) {
$licenseSeat->assigned_to = $target->assigned_to;
}
}
$licenseSeat->notes = $validated['notes'] ?? null;
$licenseSeat->created_by = auth()->id();
if (! $licenseSeat->save()) {
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, $licenseSeat->getErrors()));
return;
}
event(new CheckoutableCheckedOut($licenseSeat, $target, auth()->user(), $validated['notes'] ?? null));
$updatedSeat = $licenseSeat->load('license', 'user', 'asset');
});
if ($errorResponse) {
return $errorResponse;
}
if ($updatedSeat) {
return response()->json(Helper::formatStandardApiResponse('success', (new LicenseSeatsTransformer)->transformLicenseSeat($updatedSeat), trans('admin/licenses/message.checkout.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, 'An unexpected error occurred'), 500);
}
/**
* Checkin a license seat.
*
* `seat_id` is required to identify which seat to check back in.
*
* @param int $licenseId
*/
public function checkin(Request $request, $licenseId): JsonResponse
{
$license = License::findOrFail($licenseId);
$this->authorize('checkin', $license);
$validated = $this->validate($request, [
'seat_id' => 'required|integer',
'notes' => 'sometimes|string|nullable',
]);
$licenseSeat = LicenseSeat::where('id', $validated['seat_id'])
->where('license_id', $license->id)
->first();
if (! $licenseSeat) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.not_found')));
}
if (is_null($licenseSeat->assigned_to) && is_null($licenseSeat->asset_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkin.error')));
}
$target = $licenseSeat->user ?? $licenseSeat->asset;
$licenseSeat->assigned_to = null;
$licenseSeat->asset_id = null;
$licenseSeat->notes = $validated['notes'] ?? null;
if (! $license->reassignable) {
$licenseSeat->unreassignable_seat = true;
}
if (! $licenseSeat->save()) {
return response()->json(Helper::formatStandardApiResponse('error', null, $licenseSeat->getErrors()));
}
event(new CheckoutableCheckedIn($licenseSeat, $target, auth()->user(), $licenseSeat->notes));
return response()->json(Helper::formatStandardApiResponse('success', (new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat->load('license', 'user', 'asset')), trans('admin/licenses/message.checkin.success')));
}
/**
* Gets a paginated collection for the select2 menus
*
@@ -427,10 +427,6 @@ class LocationsController extends Controller
$locations = Company::scopeCompanyables($locations);
}
if ((Setting::getSettings()->full_multiple_companies_support == '1') && $request->filled('companyId')) {
$locations->where('locations.company_id', $request->input('companyId'));
}
$page = 1;
if ($request->filled('page')) {
$page = $request->input('page');
+8 -24
View File
@@ -22,7 +22,6 @@ use App\Models\Asset;
use App\Models\Company;
use App\Models\Consumable;
use App\Models\License;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\CurrentInventory;
use App\Notifications\WelcomeNotification;
@@ -52,6 +51,7 @@ class UsersController extends Controller
'users.address',
'users.avatar',
'users.city',
'users.company_id',
'users.country',
'users.created_by',
'users.created_at',
@@ -89,7 +89,7 @@ class UsersController extends Controller
])->with('manager')
->with('groups')
->with('userloc')
->with('companies')
->with('company')
->with('department')
->with('createdBy')
->withCount([
@@ -191,7 +191,7 @@ class UsersController extends Controller
}
if ($request->filled('company_id')) {
$users = $users->whereHas('companies', fn ($q) => $q->where('companies.id', $request->input('company_id')));
$users = $users->where('users.company_id', '=', $request->input('company_id'));
}
if ($request->filled('phone')) {
@@ -396,13 +396,6 @@ class UsersController extends Controller
]
)->where('show_in_list', '=', '1');
if ((Setting::getSettings()->full_multiple_companies_support == '1') && $request->filled('companyId')) {
$companyIds = array_values(array_filter(array_map('intval', explode(',', $request->input('companyId')))));
if (! empty($companyIds)) {
$users->whereHas('companies', fn ($q) => $q->whereIn('companies.id', $companyIds));
}
}
if ($request->filled('search')) {
$users = $users->where(function ($query) use ($request) {
$query->SimpleNameSearch($request->input('search'))
@@ -450,6 +443,7 @@ class UsersController extends Controller
$authenticatedUser = auth()->user();
$user = new User;
$user->fill($request->all());
$user->company_id = Company::getIdForCurrentUser($request->input('company_id'));
$user->created_by = auth()->id();
if ($request->has('permissions')) {
@@ -494,12 +488,6 @@ class UsersController extends Controller
$user->groups()->sync($request->input('groups'));
}
// Sync company memberships from company_ids[] or fall back to scalar company_id
$companyIds = array_filter(
(array) ($request->input('company_ids') ?? ($request->filled('company_id') ? [$request->input('company_id')] : []))
);
$user->syncCompaniesWithLogging(Company::getIdsForCurrentUser(array_map('intval', $companyIds)));
return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.create')));
}
@@ -589,6 +577,10 @@ class UsersController extends Controller
}
if ($request->filled('company_id')) {
$user->company_id = Company::getIdForCurrentUser($request->input('company_id'));
}
if ($user->id == $request->input('manager_id')) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
}
@@ -617,14 +609,6 @@ class UsersController extends Controller
$user->groups()->sync($request->input('groups'));
}
// Sync company memberships when company_ids[] or company_id is provided
if ($request->has('company_ids') || $request->filled('company_id')) {
$companyIds = array_filter(
(array) ($request->input('company_ids') ?? ($request->filled('company_id') ? [$request->input('company_id')] : []))
);
$user->syncCompaniesWithLogging(Company::getIdsForCurrentUser(array_map('intval', $companyIds)));
}
return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.update')));
}
@@ -84,7 +84,7 @@ class AssetCheckinController extends Controller
public function store(AssetCheckinRequest $request, $assetId = null, $backto = null): RedirectResponse
{
// Check if the asset exists
if (is_null($asset = Asset::withTrashed()->find($assetId))) {
if (is_null($asset = Asset::find($assetId))) {
// Redirect to the asset management page with error
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
@@ -567,12 +567,11 @@ class AssetsController extends Controller
*
* @since [v3.0]
*/
public function getAssetBySerial(Request $request, $serial = null): RedirectResponse
public function getAssetBySerial(Request $request): RedirectResponse
{
$serial = $serial ?: $request->input('serial');
$topsearch = ($request->input('topsearch') == 'true');
if (! $asset = Asset::where('serial', '=', $serial)->first()) {
if (! $asset = Asset::where('serial', '=', $request->input('serial'))->first()) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('view', $asset);
@@ -783,7 +783,7 @@ class BulkAssetsController extends Controller
$notAssigned = collect();
if (old('selected_assets') && is_array(old('selected_assets'))) {
$assets = Asset::withTrashed()->findMany(old('selected_assets'));
$assets = Asset::findMany(old('selected_assets'));
[$assigned, $notAssigned] = $assets->partition(function (Asset $asset) {
return $asset->assigned_to;
@@ -814,7 +814,7 @@ class BulkAssetsController extends Controller
$asset_ids = array_filter($request->input('selected_assets'));
$assets = Asset::withTrashed()->findOrFail($asset_ids);
$assets = Asset::findOrFail($asset_ids);
$checkin_at = date('Y-m-d H:i:s');
if ($request->filled('checkin_at') && $request->input('checkin_at') != date('Y-m-d')) {
@@ -15,7 +15,6 @@ use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class LicenseCheckoutController extends Controller
@@ -95,31 +94,23 @@ class LicenseCheckoutController extends Controller
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.license_is_inactive'));
}
$licenseSeat = null;
$checkoutTarget = null;
DB::transaction(function () use ($request, $license, $seatId, &$licenseSeat, &$checkoutTarget): void {
$licenseSeat = $this->findLicenseSeatToCheckout($license, $seatId, lock: true);
$licenseSeat->created_by = auth()->id();
$licenseSeat->notes = $request->input('notes');
if ($request->filled('asset_id')) {
$checkoutTarget = $this->checkoutToAsset($licenseSeat);
} elseif ($request->filled('assigned_to')) {
$checkoutTarget = $this->checkoutToUser($licenseSeat);
}
});
$licenseSeat = $this->findLicenseSeatToCheckout($license, $seatId);
$licenseSeat->created_by = auth()->id();
$licenseSeat->notes = $request->input('notes');
if ($request->filled('asset_id')) {
session()->put(['checkout_to_type' => 'asset']);
$checkoutTarget = $this->checkoutToAsset($licenseSeat);
$request->request->add(['assigned_asset' => $checkoutTarget->id]);
session()->put([
'redirect_option' => $request->input('redirect_option'),
'checkout_to_type' => 'asset',
'sign_in_place' => $request->boolean('sign_in_place'),
]);
} elseif ($request->filled('assigned_to')) {
session()->put(['checkout_to_type' => 'user']);
$checkoutTarget = $this->checkoutToUser($licenseSeat);
$request->request->add(['assigned_user' => $checkoutTarget->id]);
session()->put([
'redirect_option' => $request->input('redirect_option'),
@@ -165,11 +156,9 @@ class LicenseCheckoutController extends Controller
return redirect()->route('licenses.index')->with('error', trans('Something went wrong handling this checkout.'));
}
protected function findLicenseSeatToCheckout($license, $seatId, bool $lock = false)
protected function findLicenseSeatToCheckout($license, $seatId)
{
$licenseSeat = $seatId
? LicenseSeat::where('id', $seatId)->when($lock, fn ($q) => $q->lockForUpdate())->first()
: $license->freeSeat(lock: $lock);
$licenseSeat = LicenseSeat::find($seatId) ?? $license->freeSeat();
if (! $licenseSeat) {
if ($seatId) {
+2 -2
View File
@@ -277,7 +277,7 @@ class LocationsController extends Controller
->with('assignedAssets', $location->assignedAssets)
->with('accessories', $location->accessories)
->with('assignedAccessories', $location->assignedAccessories)
->with('users', $location->users()->with('companies')->get())
->with('users', $location->users)
->with('location', $location)
->with('consumables', $location->consumables)
->with('components', $location->components)
@@ -297,7 +297,7 @@ class LocationsController extends Controller
->with('assignedAssets', $location->assignedAssets)
->with('accessories', $location->accessories)
->with('assignedAccessories', $location->assignedAccessories)
->with('users', $location->users()->with('companies')->get())
->with('users', $location->users)
->with('location', $location)
->with('consumables', $location->consumables)
->with('components', $location->components)
@@ -8,7 +8,6 @@ use App\Models\Asset;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\CurrentInventory;
use App\Rules\CssColor;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -64,12 +63,6 @@ class ProfileController extends Controller
$user->enable_sounds = $request->input('enable_sounds', false);
$user->enable_confetti = $request->input('enable_confetti', false);
$request->validate([
'link_light_color' => ['nullable', new CssColor],
'link_dark_color' => ['nullable', new CssColor],
'nav_link_color' => ['nullable', new CssColor],
]);
$user->link_light_color = $request->input('link_light_color', '#296282');
$user->link_dark_color = $request->input('link_dark_color', '#296282');
$user->nav_link_color = $request->input('nav_link_color', '#FFFFFF');
@@ -19,7 +19,6 @@ use App\Models\Group;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\MailTest;
use App\Rules\CssColor;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
@@ -190,13 +189,6 @@ class SettingsController extends Controller
$request->validate(['site_name' => 'required']);
}
$request->validate([
'header_color' => ['nullable', new CssColor],
'link_light_color' => ['nullable', new CssColor],
'link_dark_color' => ['nullable', new CssColor],
'nav_link_color' => ['nullable', new CssColor],
]);
$setting->header_color = $request->input('header_color', '#3c8dbc');
$setting->link_light_color = $request->input('link_light_color', '#296282');
$setting->link_dark_color = $request->input('link_dark_color', '#5fa4cc');
-7
View File
@@ -6,7 +6,6 @@ use App\Http\Requests\SetupUserRequest;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\FirstAdminNotification;
use App\Rules\CssColor;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;
@@ -167,12 +166,6 @@ class SetupController extends Controller
$settings->alerts_enabled = 1;
$settings->pwd_secure_min = 10;
$settings->brand = 1;
$request->validate([
'link_light_color' => ['nullable', new CssColor],
'link_dark_color' => ['nullable', new CssColor],
'nav_link_color' => ['nullable', new CssColor],
]);
$settings->link_light_color = $request->input('link_light_color', '#296282');
$settings->link_dark_color = $request->input('link_dark_color', '#296282');
$settings->nav_link_color = $request->input('nav_link_color', '#FFFFFF');
@@ -8,7 +8,6 @@ use App\Http\Controllers\Controller;
use App\Models\Accessory;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Company;
use App\Models\ConsumableAssignment;
use App\Models\Group;
use App\Models\License;
@@ -169,6 +168,7 @@ class BulkUsersController extends Controller
$this->conditionallyAddItem('location_id')
->conditionallyAddItem('department_id')
->conditionallyAddItem('company_id')
->conditionallyAddItem('locale')
->conditionallyAddItem('remote')
->conditionallyAddItem('display_name')
@@ -200,7 +200,7 @@ class BulkUsersController extends Controller
$this->update_array['manager_id'] = null;
}
if ($request->input('null_company_ids') == '1') {
if ($request->input('null_company_id') == '1') {
$this->update_array['company_id'] = null;
}
@@ -233,22 +233,6 @@ class BulkUsersController extends Controller
->update(['location_id' => $this->update_array['location_id']]);
}
// Handle company pivot sync separately from the mass update.
// company_ids[] comes from the multi-select; null_company_ids clears all memberships.
$bulkCompanyIds = array_filter(array_map('intval', (array) $request->input('company_ids', [])));
$clearCompanies = $request->input('null_company_ids') == '1';
if ($bulkCompanyIds || $clearCompanies) {
$allowedIds = Company::getIdsForCurrentUser($bulkCompanyIds);
// Also update the scalar company_id column for display/backward compat.
$scalarCompanyId = $allowedIds[0] ?? null;
User::whereIn('id', $user_raw_array)->where('id', '!=', auth()->id())
->update(['company_id' => $scalarCompanyId]);
foreach ($users as $user) {
$user->companies()->sync($allowedIds);
}
}
// Fields that require canEditAuthFields (non-admins cannot touch admins/superusers,
// admins cannot touch superusers) must be applied per-user, not via mass update.
foreach ($users as $user) {
@@ -489,12 +473,6 @@ class BulkUsersController extends Controller
$managedLocation->save();
}
// Carry over company pivot memberships from the merged user into the target.
$mergedCompanyIds = $user_to_merge->companies()->pluck('companies.id')->toArray();
if (! empty($mergedCompanyIds)) {
$merge_into_user->companies()->syncWithoutDetaching($mergedCompanyIds);
}
$user_to_merge->delete();
event(new UserMerged($user_to_merge, $merge_into_user, $admin));
@@ -123,7 +123,7 @@ class UsersController extends Controller
$user->mobile = $request->input('mobile');
$user->location_id = $request->input('location_id', null);
$user->department_id = $request->input('department_id', null);
$companyIds = array_filter(array_map('intval', (array) ($request->input('company_ids') ?? ($request->filled('company_id') ? [$request->input('company_id')] : []))));
$user->company_id = Company::getIdForUser($request->input('company_id', null));
$user->manager_id = $request->input('manager_id', null);
$user->notes = $request->input('notes');
$user->address = $request->input('address', null);
@@ -153,7 +153,6 @@ class UsersController extends Controller
}
if ($user->save()) {
$user->syncCompaniesWithLogging(Company::getIdsForCurrentUser($companyIds));
if (($user->activated == '1') && ($user->email != '') && ($request->input('send_welcome') == '1')) {
@@ -276,7 +275,7 @@ class UsersController extends Controller
$user->phone = $request->input('phone');
$user->mobile = $request->input('mobile');
$user->location_id = $request->input('location_id', null);
$companyIds = array_filter(array_map('intval', (array) ($request->input('company_ids') ?? ($request->filled('company_id') ? [$request->input('company_id')] : []))));
$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);
@@ -337,8 +336,6 @@ class UsersController extends Controller
session()->put(['redirect_option' => $request->input('redirect_option')]);
if ($user->save()) {
$user->syncCompaniesWithLogging(Company::getIdsForCurrentUser($companyIds));
// Redirect to the user page
return Helper::getRedirectOption($request, $user->id, 'Users')
->with('success', trans('admin/users/message.success.update'));
@@ -483,7 +480,7 @@ class UsersController extends Controller
$permissions = $request->input('permissions', []);
app('request')->request->set('permissions', $permissions);
$user_to_clone = User::with('userloc', 'companies')->withTrashed()->find($user->id);
$user_to_clone = User::with('userloc')->withTrashed()->find($user->id);
// Make sure they can view this particular user
$this->authorize('view', $user_to_clone);
@@ -601,7 +598,7 @@ class UsersController extends Controller
'manager',
'groups',
'userloc',
'companies',
'company',
'createdBy'
)->withCount(['managesUsers as manages_users_count', 'managedLocations as manages_locations_count'])
->orderBy('created_at', 'DESC')
@@ -623,7 +620,7 @@ class UsersController extends Controller
// Add a new row with data
$values = [
$user->id,
$user->companies->pluck('name')->implode('|'),
($user->company) ? $user->company->name : '',
$user->jobtitle,
$user->employee_num,
$user->first_name,
@@ -121,7 +121,6 @@ class ViewAssetsController extends Controller
'consumables',
'accessories',
'licenses',
'companies',
])->find($selectedUserId);
// If the user to view couldn't be found (shouldn't happen with proper logic), redirect with error
-2
View File
@@ -17,7 +17,6 @@ use App\Http\Middleware\PreventBackHistory;
use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\SecurityHeaders;
use App\Http\Middleware\SetAPIResponseHeaders;
use App\Http\Middleware\SetPaginationDefaults;
use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\VerifyCsrfToken;
@@ -85,7 +84,6 @@ class Kernel extends HttpKernel
'auth:api',
CheckLocale::class,
LogAuthedUserHeader::class,
SetPaginationDefaults::class,
SubstituteBindings::class,
],
@@ -1,34 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class SetPaginationDefaults
{
public function handle(Request $request, Closure $next)
{
$limit = config('app.max_results');
$intLimit = intval($request->input('limit'));
if (abs($intLimit) > 0 && $intLimit <= config('app.max_results')) {
$limit = abs($intLimit);
}
app()->instance('api_limit_value', $limit);
if ($request->filled('page') && ! $request->filled('offset')) {
$page = max(1, intval($request->input('page')));
$offset = ($page - 1) * $limit;
} else {
$offset = intval($request->input('offset'));
$page = $limit > 0 ? (int) floor($offset / $limit) + 1 : 1;
}
app()->instance('api_offset_value', $offset);
app()->instance('api_current_page', $page);
return $next($request);
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ class ItemImportRequest extends FormRequest
$classString = "App\\Importer\\{$class}Importer";
$importer = new $classString($filename);
$import->field_map = request('column-mappings');
$import->created_by = $import->created_by ?? auth()->id();
$import->created_by = auth()->id();
$import->save();
$fieldMappings = [];
@@ -293,28 +293,6 @@ class ActionlogsTransformer
$clean_meta[trans('general.company')] = $clean_meta['company_id'];
unset($clean_meta['company_id']);
}
if (array_key_exists('companies', $clean_meta)) {
// clean_field() JSON-encodes array values into a string (e.g. "[14,15]").
// Decode them back to integer arrays before resolving names.
// Use withoutGlobalScopes so FMCS does not hide companies from the log viewer.
$resolveCompanyNames = function ($rawValue): string {
$ids = json_decode($rawValue, true);
if (empty($ids) || ! is_array($ids)) {
return trans('general.unassigned');
}
return collect($ids)
->map(fn ($id) => Company::withoutGlobalScopes()->withTrashed()->find($id))
->map(fn ($c) => $c ? e($c->name) : trans('general.deleted'))
->join(', ');
};
$clean_meta['companies']['old'] = $resolveCompanyNames($clean_meta['companies']['old']);
$clean_meta['companies']['new'] = $resolveCompanyNames($clean_meta['companies']['new']);
$clean_meta[trans('general.companies')] = $clean_meta['companies'];
unset($clean_meta['companies']);
}
if (array_key_exists('supplier_id', $clean_meta)) {
$oldSupplier = $supplier->find($clean_meta['supplier_id']['old']);
@@ -38,11 +38,13 @@ class LicenseSeatsTransformer
'tag_color' => $seat->user->department->tag_color ? e($seat->user->department->tag_color) : null,
] : null,
'companies' => $seat->user->companies->map(fn ($c) => [
'id' => (int) $c->id,
'name' => e($c->name),
'tag_color' => $c->tag_color ? e($c->tag_color) : null,
])->values(),
'company' => ($seat->user->company) ?
[
'id' => (int) $seat->user->company->id,
'name' => e($seat->user->company->name),
'tag_color' => $seat->user->company->tag_color ? e($seat->user->company->tag_color) : null,
] : null,
'created_at' => Helper::getFormattedDateObject($seat->created_at, 'datetime'),
] : null,
'assigned_asset' => ($seat->asset) ? [
+4 -15
View File
@@ -82,17 +82,11 @@ class UsersTransformer
'consumables_count' => (int) $user->consumables_count,
'manages_users_count' => (int) $user->manages_users_count,
'manages_locations_count' => (int) $user->manages_locations_count,
// Legacy field — kept for backward API compatibility; use `companies` for multi-company support.
'company' => $user->companies->isNotEmpty() ? [
'id' => (int) $user->companies->first()->id,
'name' => e($user->companies->first()->name),
'tag_color' => ($user->companies->first()->tag_color) ? e($user->companies->first()->tag_color) : null,
'company' => ($user->company) ? [
'id' => (int) $user->company->id,
'name' => e($user->company->name),
'tag_color' => ($user->company->tag_color) ? e($user->company->tag_color) : null,
] : null,
'companies' => $user->companies->map(fn ($c) => [
'id' => (int) $c->id,
'name' => e($c->name),
'tag_color' => $c->tag_color ? e($c->tag_color) : null,
])->values(),
'created_by' => ($user->createdBy) ? [
'id' => (int) $user->createdBy->id,
'name' => e($user->createdBy->display_name),
@@ -150,11 +144,6 @@ class UsersTransformer
'last_name' => e($user->last_name),
'username' => e($user->username),
'display_name' => e($user->display_name),
'companies' => $user->companies->map(fn ($c) => [
'id' => (int) $c->id,
'name' => e($c->name),
'tag_color' => $c->tag_color ? e($c->tag_color) : null,
])->values(),
'created_by' => $user->adminuser ? [
'id' => (int) $user->adminuser->id,
'name' => e($user->adminuser->present()->fullName),
+5 -63
View File
@@ -3,7 +3,6 @@
namespace App\Importer;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Department;
use App\Models\Setting;
use App\Models\User;
@@ -36,31 +35,6 @@ class UserImporter extends ItemImporter
$this->createUserIfNotExists($row);
}
/**
* Parse a pipe-separated company column value into an array of company IDs,
* creating companies that do not yet exist. Returns an empty array when the
* raw value is blank (so callers can treat that as "don't change").
*
* @param string $raw Raw cell value, e.g. "Acme Corp|Widget Inc"
* @return int[]
*/
private function resolveCompanyIds(string $raw): array
{
if ($raw === '') {
return [];
}
$ids = [];
foreach (array_filter(array_map('trim', explode('|', $raw))) as $name) {
$id = $this->createOrFetchCompany($name);
if ($id) {
$ids[] = (int) $id;
}
}
return Company::getIdsForCurrentUser($ids);
}
/**
* Create a user if a duplicate does not exist.
*
@@ -106,13 +80,6 @@ class UserImporter extends ItemImporter
$this->item['department_id'] = $this->createOrFetchDepartment($user_department);
}
// Resolve pipe-separated company names (e.g. "Acme Corp|Widget Inc") into IDs.
// company_id is a legacy column — company membership is managed via the pivot.
// Unset whatever the parent set so it is not written to the DB.
$companyRaw = trim($this->findCsvMatch($row, 'company'));
$companyIds = $this->resolveCompanyIds($companyRaw);
unset($this->item['company_id']);
if (is_null($this->item['username']) || $this->item['username'] == '') {
$user_full_name = $this->item['first_name'].' '.$this->item['last_name'];
$user_formatted_array = User::generateFormattedNameFromFullName($user_full_name, Setting::getSettings()->username_format);
@@ -137,13 +104,11 @@ class UserImporter extends ItemImporter
$this->log('Updating User');
// CLI imports run unauthenticated and are fully trusted; only restrict web-initiated imports.
// Note: unset must target $this->item, not the model — sanitizeItemForUpdating() reads from $this->item.
if (Auth::check() && (! Auth::user()->hasAccess('users.edit') || ! Gate::allows('canEditAuthFields', $user))) {
unset($this->item['username']);
unset($this->item['email']);
unset($this->item['password']);
unset($this->item['activated']);
if (Auth::check() && (! Gate::allows('canEditAuthFields', $user))) {
unset($user->username);
unset($user->email);
unset($user->password);
unset($user->activated);
}
$user->update($this->sanitizeItemForUpdating($user));
@@ -151,11 +116,6 @@ class UserImporter extends ItemImporter
// Why do we have to do this twice? Update should
$user->save();
// Sync company pivot when companies were specified in this row.
if (! empty($companyIds)) {
$user->companies()->sync($companyIds);
}
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)
@@ -165,17 +125,6 @@ class UserImporter extends ItemImporter
return;
}
// With FMCS enabled, the scoped lookup above only sees users in the current user's companies.
// If the username exists in another company it would appear as "not found" and fall through
// to create — but usernames are unique system-wide, so we must skip instead.
if (Auth::check() && Company::isFullMultipleCompanySupportEnabled()) {
if (User::withoutGlobalScopes()->where('username', $this->item['username'])->exists()) {
$this->log('Skipping '.$this->item['username'].': username belongs to a user outside your company scope.');
return;
}
}
// This needs to be applied after the update logic, otherwise we'll overwrite user passwords
// Issue #5408
$this->item['password'] = $this->tempPassword;
@@ -191,13 +140,6 @@ class UserImporter extends ItemImporter
if ($user->save()) {
$this->log('User '.$this->item['name'].' was created');
// Sync all resolved companies to the pivot. For single-company rows the
// User::created event already added company_id; sync() here is idempotent
// for that case and adds any additional companies for multi-company rows.
if (! empty($companyIds)) {
$user->companies()->sync($companyIds);
}
if (($user->email) && ($user->activated == '1')) {
if ($this->send_welcome) {
+1 -2
View File
@@ -146,8 +146,7 @@ class AccessoryCheckout extends Model
$search_str = '%'.$term.'%';
$query->where('first_name', 'like', $search_str)
->orWhere('last_name', 'like', $search_str)
->orWhere('note', 'like', $search_str)
->orWhereHas('companies', fn ($q) => $q->where('companies.name', 'like', $search_str));
->orWhere('note', 'like', $search_str);
}
}
)->select('id');
+75 -12
View File
@@ -8,7 +8,6 @@ use App\Helpers\Helper;
use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\Traits\Acceptable;
use App\Models\Traits\CompanyableTrait;
use App\Models\Traits\HasCustomFields;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Loggable;
use App\Models\Traits\Requestable;
@@ -21,6 +20,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
@@ -37,7 +37,6 @@ class Asset extends Depreciable
protected $with = ['model', 'adminuser', 'location', 'company'];
use CompanyableTrait;
use HasCustomFields;
use HasFactory;
use HasUploads;
use Loggable;
@@ -253,9 +252,41 @@ class Asset extends Depreciable
$this->attributes['expected_checkin'] = $value;
}
protected function getCustomFieldset(): ?CustomFieldset
public function customFieldValidationRules()
{
return $this->model?->fieldset ?? null;
$customFieldValidationRules = [];
if (($this->model) && ($this->model->fieldset)) {
foreach ($this->model->fieldset->fields as $field) {
// this just casts booleans that may come through as strings to an actual boolean type
// adding !$field->field_encrypted because when the encrypted value comes through it
// screws things up for the encrypted validation rules (and the encrypted string
// is not a valid boolean type)
if ($field->format == 'BOOLEAN' && ! $field->field_encrypted) {
$this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN);
}
}
$customFieldValidationRules += $this->model->fieldset->validation_rules();
}
return $customFieldValidationRules;
}
/**
* This handles the custom field validation for assets
*
* @var array
*/
public function save(array $params = [])
{
$this->rules += $this->customFieldValidationRules();
return parent::save($params);
}
public function getDisplayNameAttribute()
@@ -456,18 +487,16 @@ class Asset extends Depreciable
public function availableForCheckIn()
{
if ($this->assigned_to == '') {
return false;
}
// Deleted assets that are still checked out should always allow checkin
if ($this->deleted_at != '') {
// This asset is currently assigned to anyone and is not deleted...
if (($this->assigned_to != '') && ($this->status) && ($this->status->archived == '0')
&& ($this->status->deployable == '1')
) {
return true;
}
return $this->status
&& ($this->status->archived == '0')
&& ($this->status->deployable == '1');
return false;
}
/**
@@ -577,6 +606,40 @@ class Asset extends Depreciable
return $this->rules;
}
public function customFieldsForCheckinCheckout($checkin_checkout)
{
// Check to see if any of the custom fields were included on the form and if they have any values
if (($this->model) && ($this->model->fieldset) && ($this->model->fieldset->fields)) {
foreach ($this->model->fieldset->fields as $field) {
if (($field->{$checkin_checkout} == 1) && (request()->has($field->db_column))) {
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
if (is_array(request()->input($field->db_column))) {
$this->{$field->db_column} = Crypt::encrypt(implode(', ', request()->input($field->db_column)));
} else {
$this->{$field->db_column} = Crypt::encrypt(request()->input($field->db_column));
}
}
} else {
if (is_array(request()->input($field->db_column))) {
$this->{$field->db_column} = implode(', ', request()->input($field->db_column));
} else {
$this->{$field->db_column} = request()->input($field->db_column);
}
}
}
}
}
}
public function manufacturer()
{
return $this->hasOneThrough(Manufacturer::class, AssetModel::class, 'id', 'id', 'model_id', 'manufacturer_id');
+24 -116
View File
@@ -11,7 +11,6 @@ use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
@@ -95,26 +94,7 @@ final class Company extends SnipeModel
'notes',
];
/**
* Return the current user's company IDs by querying the pivot table directly.
*
* We deliberately bypass the Eloquent companies() relationship here because
* loading that relationship triggers CompanyableScope on the Company model,
* which calls this method again — infinite recursion.
*/
private static function getCurrentUserCompanyIds(): array
{
if (! Auth::hasUser()) {
return [];
}
return DB::table('company_user')
->where('user_id', auth()->id())
->pluck('company_id')
->toArray();
}
public static function isFullMultipleCompanySupportEnabled()
private static function isFullMultipleCompanySupportEnabled()
{
$settings = Setting::getSettings();
@@ -199,65 +179,20 @@ final class Company extends SnipeModel
}
if (auth()->user()) {
if (auth()->user()->isSuperUser()) {
return true;
// Log::warning('Companyable is '.$companyable);
$current_user_company_id = auth()->user()->company_id;
$companyable_company_id = $companyable->company_id;
// Set this to check companyable on company
if ($companyable instanceof Company) {
$companyable_company_id = $companyable->id;
}
$userCompanyIds = self::getCurrentUserCompanyIds();
// Empty pivot = unrestricted only for true legacy "no-company" users
// (those whose scalar company_id is also null). Users who had their
// pivot cleared via the API retain their scalar company_id, so they
// do NOT qualify for this bypass.
if (empty($userCompanyIds) && is_null(auth()->user()->company_id)) {
return true;
}
// Users are scoped by pivot membership, not company_id, so check the pivot directly.
if ($companyable instanceof User) {
$companyableCompanyIds = DB::table('company_user')
->where('user_id', $companyable->id)
->pluck('company_id')
->toArray();
// A user with no pivot rows is a null-company user; no intersection is possible.
if (empty($companyableCompanyIds)) {
return false;
}
return ! empty(array_intersect($userCompanyIds, $companyableCompanyIds));
}
$companyable_company_id = ($companyable instanceof Company)
? $companyable->id
: $companyable->company_id;
return in_array($companyable_company_id, $userCompanyIds);
return ($current_user_company_id == null) || ($current_user_company_id == $companyable_company_id) || auth()->user()->isSuperUser();
}
return false;
}
/**
* Filter an array of requested company IDs to only those the current user
* belongs to. Superusers may assign any company; non-superusers are limited
* to their own pivot memberships when FMCS is enabled.
*/
public static function getIdsForCurrentUser(array $requestedIds): array
{
if (! self::isFullMultipleCompanySupportEnabled()) {
return $requestedIds;
}
$current_user = auth()->user();
if ($current_user->isSuperUser()) {
return $requestedIds;
}
$allowedIds = self::getCurrentUserCompanyIds();
return array_values(array_intersect($requestedIds, $allowedIds));
}
public static function isCurrentUserAuthorized()
@@ -267,9 +202,8 @@ final class Company extends SnipeModel
public static function canManageUsersCompanies()
{
return ! self::isFullMultipleCompanySupportEnabled()
|| auth()->user()->isSuperUser()
|| empty(self::getCurrentUserCompanyIds());
return ! self::isFullMultipleCompanySupportEnabled() || auth()->user()->isSuperUser() ||
auth()->user()->company_id == null;
}
/**
@@ -308,7 +242,7 @@ final class Company extends SnipeModel
public function users()
{
return $this->belongsToMany(User::class, 'company_user');
return $this->hasMany(User::class, 'company_id');
}
public function assets()
@@ -370,53 +304,27 @@ final class Company extends SnipeModel
*/
private static function scopeCompanyablesDirectly($query, $column = 'company_id', $table_name = null)
{
$companyIds = self::getCurrentUserCompanyIds();
$company_id = null;
// Get the company ID of the logged-in user, or set it to null if there is no company associated with the user
if (Auth::hasUser()) {
$company_id = auth()->user()->company_id;
}
// If we are scoping the companies table itself, look for the company.id
if ($query->getModel()->getTable() == 'companies') {
if (empty($companyIds)) {
return $query->whereNull('companies.id');
}
return $query->whereIn('companies.id', $companyIds);
}
// Users are scoped by pivot membership (company_user), not by company_id column,
// since a user may belong to multiple companies and company_id alone is insufficient.
if ($query->getModel()->getTable() == 'users') {
if (empty($companyIds)) {
// No pivot memberships: mirror old null-company behavior — show only users
// who are also not in any company via the pivot.
return $query->whereNotIn('users.id', function ($sub) {
$sub->select('user_id')->from('company_user');
});
}
return $query->whereIn('users.id', function ($sub) use ($companyIds) {
$sub->select('user_id')->from('company_user')->whereIn('company_id', $companyIds);
});
return $query->where('companies.id', '=', $company_id);
}
// If the column exists in the table, use it to scope the query
if ($query && $query->getModel() && Schema::hasColumn($query->getModel()->getTable(), $column)) {
if ((($query) && ($query->getModel()) && (Schema::hasColumn($query->getModel()->getTable(), $column)))) {
// Dynamically get the table name if it's not passed in, based on the model we're querying against
$table = ($table_name) ? $table_name.'.' : $query->getModel()->getTable().'.';
if (empty($companyIds)) {
return $query->whereNull($table.$column);
}
// action_logs: a NULL company_id means the logged object (AssetModel, Company, etc.)
// has no company_id column of its own. Those are global objects, visible to all users,
// so their log entries should not be hidden by the company filter.
if ($query->getModel()->getTable() === 'action_logs') {
return $query->where(function ($q) use ($table, $column, $companyIds) {
$q->whereIn($table.$column, $companyIds)
->orWhereNull($table.$column);
});
}
return $query->whereIn($table.$column, $companyIds);
return $query->where($table.$column, '=', $company_id);
}
}
/**
+1 -2
View File
@@ -803,7 +803,7 @@ class License extends Depreciable
*
* @return mixed
*/
public function freeSeat(bool $lock = false)
public function freeSeat()
{
return $this->licenseseats()
->whereNull('deleted_at')
@@ -813,7 +813,6 @@ class License extends Depreciable
->whereNull('asset_id');
})
->orderBy('id', 'asc')
->when($lock, fn ($q) => $q->lockForUpdate())
->first();
}
-30
View File
@@ -3,9 +3,7 @@
namespace App\Models;
use App\Helpers\Helper;
use App\Rules\CssColor;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
@@ -175,34 +173,6 @@ class Setting extends Model
*
* @author A. Gianotto <snipe@snipe.net>
*/
protected function headerColor(): Attribute
{
return Attribute::make(
get: fn (?string $value) => CssColor::sanitize($value, '#3c8dbc'),
);
}
protected function linkLightColor(): Attribute
{
return Attribute::make(
get: fn (?string $value) => CssColor::sanitize($value, '#296282'),
);
}
protected function linkDarkColor(): Attribute
{
return Attribute::make(
get: fn (?string $value) => CssColor::sanitize($value, '#5fa4cc'),
);
}
protected function navLinkColor(): Attribute
{
return Attribute::make(
get: fn (?string $value) => CssColor::sanitize($value, '#ffffff'),
);
}
public function show_custom_css(): string
{
$custom_css = self::getSettings()->custom_css;
-66
View File
@@ -1,66 +0,0 @@
<?php
namespace App\Models\Traits;
use App\Models\CustomFieldset;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
trait HasCustomFields
{
/**
* Return the CustomFieldset for this model instance.
* Override in each model to supply the correct fieldset.
*/
protected function getCustomFieldset(): ?CustomFieldset
{
return null;
}
public function customFieldValidationRules(): array
{
$fieldset = $this->getCustomFieldset();
if (! $fieldset) {
return [];
}
foreach ($fieldset->fields as $field) {
if ($field->format === 'BOOLEAN' && ! $field->field_encrypted) {
$this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN);
}
}
return $fieldset->validation_rules();
}
public function save(array $params = [])
{
$this->rules += $this->customFieldValidationRules();
return parent::save($params);
}
public function customFieldsForCheckinCheckout(string $checkin_checkout): void
{
$fieldset = $this->getCustomFieldset();
if (! $fieldset?->fields) {
return;
}
foreach ($fieldset->fields as $field) {
if (($field->{$checkin_checkout} == 1) && request()->has($field->db_column)) {
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
$value = request()->input($field->db_column);
$this->{$field->db_column} = Crypt::encrypt(is_array($value) ? implode(', ', $value) : $value);
}
} else {
$value = request()->input($field->db_column);
$this->{$field->db_column} = is_array($value) ? implode(', ', $value) : $value;
}
}
}
}
}
-5
View File
@@ -3,17 +3,12 @@
namespace App\Models\Traits;
use App\Models\Actionlog;
use App\Models\CompanyableScope;
trait HasUploads
{
public function uploads()
{
// Bypass FMCS company scoping: access is already gated by the policy on the
// parent object. Objects like AssetModel and Company have no company_id, so
// their upload logs always have company_id = null, which the scope would hide.
return $this->hasMany(Actionlog::class, 'item_id')
->withoutGlobalScope(CompanyableScope::class)
->where('item_type', self::class)
->where('action_type', '=', 'uploaded')
->whereNotNull('filename')
+2 -5
View File
@@ -4,7 +4,6 @@ namespace App\Models\Traits;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\CompanyableScope;
use App\Models\ICompanyableChild;
use App\Models\License;
use App\Models\LicenseSeat;
@@ -42,15 +41,13 @@ trait Loggable
public function history()
{
// Bypass FMCS company scoping: access is already gated by the policy on the
// parent object. Objects like AssetModel and Company have no company_id, so
// their history logs always have company_id = null, which the scope would hide.
return $this->morphMany(Actionlog::class, 'item')
->withoutGlobalScope(CompanyableScope::class)
->orWhere(function ($query) {
$query->where('target_type', '=', static::class)
->where('target_id', '=', $this->getKey());
});
}
public function getHistory(Request $request)
+3 -97
View File
@@ -9,7 +9,6 @@ use App\Models\Traits\Loggable;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use App\Presenters\UserPresenter;
use App\Rules\CssColor;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
@@ -19,7 +18,6 @@ use Illuminate\Contracts\Translation\HasLocalePreference;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -61,13 +59,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
protected $injectUniqueIdentifier = true;
/**
* Transient (non-persisted) ID of the Actionlog entry written by UserObserver::updating()
* during the current request. syncCompaniesWithLogging() merges company changes into this
* entry instead of creating a separate one, so a single edit session produces one log row.
*/
public ?int $currentUpdateLogId = null;
protected $fillable = [
'activated',
'address',
@@ -175,7 +166,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
'userloc' => ['name', 'address', 'address2', 'city', 'state', 'zip'],
'department' => ['name'],
'groups' => ['name'],
'companies' => ['name'],
'company' => ['name'],
'manager' => ['first_name', 'last_name', 'username', 'display_name'],
'adminuser' => ['first_name', 'last_name', 'display_name'],
];
@@ -253,15 +244,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
protected static function booted(): void
{
// Bridge for factories/seeders that still set company_id directly: ensure
// that company appears in the pivot so FMCS scoping works correctly.
// Application code (controllers, importers) writes only to the pivot.
static::created(function (User $user) {
if ($user->company_id) {
$user->companies()->syncWithoutDetaching([$user->company_id]);
}
});
static::forceDeleted(function (User $user) {
CheckoutRequest::where(['user_id' => $user->id])->forceDelete();
$user->purgeAssociatedPassportTokens();
@@ -621,51 +603,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
return $this->belongsTo(Company::class, 'company_id');
}
public function companies(): BelongsToMany
{
return $this->belongsToMany(Company::class, 'company_user');
}
/**
* Sync company pivot membership and log the change if the set of companies changed.
*
* When called after $user->save() in the same request, UserObserver::updating() will
* have already written an Actionlog row and stored its ID in $this->currentUpdateLogId.
* In that case we merge the company change into that existing entry so that a single
* edit session (field changes + company changes) produces one log row, not two.
*/
public function syncCompaniesWithLogging(array $companyIds): void
{
$oldIds = $this->companies()->orderBy('companies.id')->pluck('companies.id')->toArray();
$this->companies()->sync($companyIds);
$newIds = $this->companies()->orderBy('companies.id')->pluck('companies.id')->toArray();
if ($oldIds === $newIds) {
return;
}
$companyChange = ['companies' => ['old' => $oldIds, 'new' => $newIds]];
if ($this->currentUpdateLogId && ($existing = Actionlog::find($this->currentUpdateLogId))) {
$meta = json_decode($existing->log_meta ?? '{}', true) ?: [];
$existing->log_meta = json_encode(array_merge($meta, $companyChange));
$existing->save();
$this->currentUpdateLogId = null;
return;
}
$logAction = new Actionlog;
$logAction->item_type = static::class;
$logAction->item_id = $this->id;
$logAction->target_type = static::class;
$logAction->target_id = $this->id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->created_by = auth()->id();
$logAction->log_meta = json_encode($companyChange);
$logAction->logaction('update');
}
/**
* Establishes the user -> department relationship
*
@@ -714,27 +651,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
return $this->last_name ? $this->first_name.' '.$this->last_name : $this->first_name;
}
protected function linkLightColor(): Attribute
{
return Attribute::make(
get: fn (?string $value) => CssColor::sanitize($value, '#296282'),
);
}
protected function linkDarkColor(): Attribute
{
return Attribute::make(
get: fn (?string $value) => CssColor::sanitize($value, '#5fa4cc'),
);
}
protected function navLinkColor(): Attribute
{
return Attribute::make(
get: fn (?string $value) => CssColor::sanitize($value, '#ffffff'),
);
}
/**
* Establishes the user -> assets relationship
*
@@ -809,10 +725,9 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
{
return $this->belongsToMany(License::class, 'license_seats', 'assigned_to', 'license_id')->withPivot('id', 'created_at', 'updated_at');
}
public function directLicenses()
{
return $this->belongsToMany(License::class, 'license_seats', 'assigned_to', 'license_id')->withPivot('id', 'created_at', 'updated_at')->wherePivotNull('asset_id')->withTrashed();
return $this->belongsToMany(\App\Models\License::class, 'license_seats', 'assigned_to', 'license_id')->withPivot('id', 'created_at', 'updated_at')->wherePivotNull('asset_id')->withTrashed();
}
/**
@@ -1423,14 +1338,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
*/
public function scopeOrderCompany($query, $order)
{
$sub = DB::table('company_user')
->join('companies', 'companies.id', '=', 'company_user.company_id')
->select('company_user.user_id', DB::raw('MIN(companies.name) as min_company_name'))
->groupBy('company_user.user_id');
return $query
->leftJoinSub($sub, 'companies_sort', 'companies_sort.user_id', '=', 'users.id')
->orderBy('companies_sort.min_company_name', $order);
return $query->leftJoin('companies as companies_user', 'users.company_id', '=', 'companies_user.id')->orderBy('companies_user.name', $order);
}
/**
@@ -1485,7 +1393,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
->orwhereRaw('CONCAT(users.first_name," ",users.last_name) LIKE \''.$search.'%\'');
}
public function scopeWithInventoryRelations($query, int $id)
{
return $query->where('id', $id)
@@ -1527,7 +1434,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
])
->withTrashed();
}
/**
* Get all direct and indirect subordinates for this user.
*
+11 -42
View File
@@ -16,8 +16,6 @@ class UserObserver
{
// ONLY allow these fields to be stored
// NOTE: company_id is intentionally excluded — company membership changes are logged
// via User::syncCompaniesWithLogging() against the pivot table instead.
$allowed_fields = [
'email',
'activated',
@@ -33,6 +31,7 @@ class UserObserver
'employee_num',
'username',
'notes',
'company_id',
'ldap_import',
'locale',
'two_factor_enrolled',
@@ -59,44 +58,18 @@ class UserObserver
// Make sure the info is in the allow fields array
if (in_array($key, $allowed_fields)) {
$oldValue = $user->getRawOriginal()[$key];
$newValue = $user->getAttributes()[$key];
// Check and see if the value changed
if ($user->getRawOriginal()[$key] != $user->getAttributes()[$key]) {
if ($key === 'permissions') {
// Compare decoded to avoid spurious diffs from key reordering or type coercion.
$oldDecoded = json_decode($oldValue ?? '{}', true) ?: [];
$newDecoded = json_decode($newValue ?? '{}', true) ?: [];
if ($oldDecoded == $newDecoded) {
continue;
$changed[$key]['old'] = $user->getRawOriginal()[$key];
$changed[$key]['new'] = $user->getAttributes()[$key];
// Do not store the hashed password in changes
if ($key == 'password') {
$changed['password']['old'] = '*************';
$changed['password']['new'] = '*************';
}
// Only log the permission keys that actually changed.
$diffOld = [];
$diffNew = [];
foreach (array_unique(array_merge(array_keys($oldDecoded), array_keys($newDecoded))) as $permKey) {
$oldPerm = $oldDecoded[$permKey] ?? null;
$newPerm = $newDecoded[$permKey] ?? null;
if ($oldPerm != $newPerm) {
$diffOld[$permKey] = $oldPerm;
$diffNew[$permKey] = $newPerm;
}
}
$changed['permissions']['old'] = json_encode($diffOld);
$changed['permissions']['new'] = json_encode($diffNew);
continue;
}
if ($oldValue == $newValue) {
continue;
}
$changed[$key]['old'] = $oldValue;
$changed[$key]['new'] = $newValue;
// Do not store the hashed password in changes
if ($key == 'password') {
$changed['password']['old'] = '*************';
$changed['password']['new'] = '*************';
}
}
@@ -106,16 +79,12 @@ class UserObserver
$logAction = new Actionlog;
$logAction->item_type = User::class;
$logAction->item_id = $user->id;
$logAction->target_type = User::class;
$logAction->target_type = User::class; // can we instead say $logAction->item = $asset ?
$logAction->target_id = $user->id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->created_by = auth()->id();
$logAction->log_meta = json_encode($changed);
$logAction->logaction('update');
// Let syncCompaniesWithLogging() merge company changes into this entry
// rather than creating a separate log row for the same edit session.
$user->currentUpdateLogId = $logAction->id;
}
}
-9
View File
@@ -218,15 +218,6 @@ class AccessoryPresenter extends Presenter
'visible' => true,
'formatter' => 'polymorphicItemFormatter',
],
[
'field' => 'assigned_to.companies',
'searchable' => true,
'sortable' => false,
'switchable' => true,
'title' => trans('general.companies'),
'visible' => true,
'formatter' => 'companiesArrayLinkFormatter',
],
[
'field' => 'note',
'searchable' => false,
+3 -19
View File
@@ -17,7 +17,7 @@ class AssetPresenter extends Presenter
*
* @return string
*/
public static function dataTableLayout($hide_fields = [])
public static function dataTableLayout()
{
$layout = [
[
@@ -278,23 +278,7 @@ class AssetPresenter extends Presenter
'title' => trans('general.updated_at'),
'visible' => false,
'formatter' => 'dateDisplayFormatter',
],
];
if (! in_array('deleted_at', $hide_fields)) {
$layout[] = [
'field' => 'deleted_at',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('general.deleted_at'),
'visible' => true,
'formatter' => 'dateDisplayFormatter',
];
}
$layout = array_merge($layout, [
[
], [
'field' => 'last_checkout',
'searchable' => false,
'sortable' => true,
@@ -339,7 +323,7 @@ class AssetPresenter extends Presenter
'formatter' => 'trueFalseFormatter',
],
]);
];
// This looks complicated, but we have to confirm that the custom fields exist in custom fieldsets
// *and* those fieldsets are associated with models, otherwise we'll trigger
+3 -3
View File
@@ -280,13 +280,13 @@ class LicensePresenter extends Presenter
'formatter' => 'emailFormatter',
],
[
'field' => 'assigned_user.companies',
'field' => 'assigned_user.company',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('general.companies'),
'title' => trans('general.company'),
'visible' => true,
'formatter' => 'companiesArrayLinkFormatter',
'formatter' => 'companiesLinkObjFormatter',
],
[
'field' => 'assigned_user.department',
+4 -4
View File
@@ -83,13 +83,13 @@ class UserPresenter extends Presenter
'formatter' => 'usersLinkFormatter',
],
[
'field' => 'companies',
'field' => 'company',
'searchable' => true,
'sortable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('general.companies'),
'title' => trans('admin/companies/table.title'),
'visible' => false,
'formatter' => 'companiesArrayLinkFormatter',
'formatter' => 'companiesLinkObjFormatter',
],
[
'field' => 'employee_num',
-6
View File
@@ -103,11 +103,5 @@ class RouteServiceProvider extends ServiceProvider
return Limit::perMinute(config('auth.password_reset.max_attempts_per_min'))->by(optional($request->user())->id ?: $request->ip());
});
// Rate limiter for two-factor authentication — keyed on user ID since the user is already
// password-authenticated at this stage, preventing distributed brute force across IPs.
RateLimiter::for('two_factor', function (Request $request) {
return Limit::perMinute(config('auth.two_factor.max_attempts_per_min'))->by(optional($request->user())->id ?: $request->ip());
});
}
}
+37
View File
@@ -32,6 +32,43 @@ class SettingsServiceProvider extends ServiceProvider
$view->with('snipeSettings', Setting::getSettings());
});
// Make sure the limit is actually set, is an integer and does not exceed system limits
app()->singleton('api_limit_value', function () {
$limit = config('app.max_results');
$int_limit = intval(request('limit'));
if ((abs($int_limit) > 0) && ($int_limit <= config('app.max_results'))) {
$limit = abs($int_limit);
}
return $limit;
});
// Make sure the offset is actually set and is an integer.
// If 'page' is passed without 'offset', derive the offset from the page number.
app()->singleton('api_offset_value', function () {
if (request()->filled('page') && ! request()->filled('offset')) {
$page = max(1, intval(request('page')));
return ($page - 1) * (int) app('api_limit_value');
}
return intval(request('offset'));
});
// Resolve the current page number for inclusion in API list responses.
// Supports both page= and legacy offset= parameters.
app()->singleton('api_current_page', function () {
if (request()->filled('page') && ! request()->filled('offset')) {
return max(1, intval(request('page')));
}
$limit = (int) app('api_limit_value');
$offset = (int) app('api_offset_value');
return $limit > 0 ? (int) floor($offset / $limit) + 1 : 1;
});
/**
* Set some common variables so that they're globally available.
* The paths should always be public (versus private uploads)
+2 -7
View File
@@ -353,15 +353,10 @@ class ValidationServiceProvider extends ServiceProvider
Validator::extend('fmcs_location', function ($attribute, $value, $parameters, $validator) {
$settings = Setting::getSettings();
if ($settings->full_multiple_companies_support == '1' && $settings->scope_locations_fmcs == '1') {
$data = $validator->getData();
// Support both multi-company (company_ids[]) and single-company (company_id) requests
$companyIds = array_filter(array_unique(array_merge(
(array) ($data['company_ids'] ?? []),
[$data['company_id'] ?? null]
)));
$company_id = array_get($validator->getData(), 'company_id');
$location = Location::find($value);
if ($location && ! in_array($location->company_id, $companyIds)) {
if (($location) && ($company_id != $location->company_id)) {
return false;
}
}
-47
View File
@@ -1,47 +0,0 @@
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Translation\PotentiallyTranslatedString;
class CssColor implements ValidationRule
{
private static function pattern(): string
{
$num = '\s*[\d.]+\s*';
$pct = '\s*[\d.]+%\s*';
$alpha = '(?:,\s*[\d.]+\s*)?';
$hex = '#[0-9a-fA-F]{3,8}';
$rgb = "rgba?\({$num},{$num},{$num}{$alpha}\)";
$hsl = "hsla?\({$num},{$pct},{$pct}{$alpha}\)";
return "/^(?:{$hex}|{$rgb}|{$hsl})$/i";
}
/**
* Return $value if it is a safe CSS color, otherwise return $default.
* Use this for defense-in-depth when rendering color values already in the database.
*/
public static function sanitize(?string $value, string $default): string
{
if ($value && preg_match(self::pattern(), trim($value))) {
return $value;
}
return $default;
}
/**
* Run the validation rule.
*
* @param Closure(string, ?string=): PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! preg_match(self::pattern(), $value)) {
$fail(trans('validation.valid_css_color'));
}
}
}
-1
View File
@@ -19,7 +19,6 @@
"require": {
"php": "^8.2",
"ext-curl": "*",
"ext-exif": "*",
"ext-fileinfo": "*",
"ext-iconv": "*",
"ext-json": "*",
Generated
+1 -2
View File
@@ -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": "09f6cf88befc67d5f5ead4d38c37e857",
"content-hash": "ed0655f6c3c75cda1939dfc27b492029",
"packages": [
{
"name": "alek13/slack",
@@ -16835,7 +16835,6 @@
"platform": {
"php": "^8.2",
"ext-curl": "*",
"ext-exif": "*",
"ext-fileinfo": "*",
"ext-iconv": "*",
"ext-json": "*",
-4
View File
@@ -122,10 +122,6 @@ return [
'max_attempts_per_min' => env('PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN', 50),
],
'two_factor' => [
'max_attempts_per_min' => env('TWO_FACTOR_MAX_ATTEMPTS_PER_MIN', 5),
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
+6 -6
View File
@@ -1,11 +1,11 @@
<?php
return [
'app_version' => 'v8.6.1',
'full_app_version' => 'v8.6.1 - build 22962-g4edf40acaf',
'build_version' => '22962',
'app_version' => 'v8.6.0',
'full_app_version' => 'v8.6.0 - build 22854-gcfa8069953',
'build_version' => '22854',
'prerelease_version' => '',
'hash_version' => 'g4edf40acaf',
'full_hash' => 'v8.6.1-149-g4edf40acaf',
'branch' => 'develop',
'hash_version' => 'gcfa8069953',
'full_hash' => 'v8.6.0-195-gcfa8069953',
'branch' => 'master',
];
-20
View File
@@ -450,26 +450,6 @@ class UserFactory extends Factory
return $this->appendPermission(['assets.audit' => '1']);
}
public function manageModelFiles()
{
return $this->appendPermission(['models.files' => '1']);
}
public function manageLocationFiles()
{
return $this->appendPermission(['locations.files' => '1']);
}
public function manageCompanyFiles()
{
return $this->appendPermission(['companies.files' => '1']);
}
public function manageSupplierFiles()
{
return $this->appendPermission(['suppliers.files' => '1']);
}
private function appendPermission(array $permission)
{
return $this->state(function ($currentState) use ($permission) {
@@ -1,38 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('company_user', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('company_id')->index();
$table->unsignedInteger('user_id')->index();
$table->timestamps();
$table->unique(['company_id', 'user_id']);
});
// Seed pivot from existing users.company_id values
DB::table('users')
->whereNotNull('company_id')
->orderBy('id')
->each(function ($user) {
DB::table('company_user')->insertOrIgnore([
'company_id' => $user->company_id,
'user_id' => $user->id,
'created_at' => now(),
'updated_at' => now(),
]);
});
}
public function down(): void
{
Schema::dropIfExists('company_user');
}
};
+20 -37
View File
@@ -33,18 +33,27 @@ class UserSeeder extends Seeder
$departmentIds = Department::all()->pluck('id');
// Named admins get multiple companies — they manage assets across several organisations.
foreach (['firstAdmin', 'snipeAdmin', 'testAdmin'] as $state) {
$user = User::factory()->{$state}()->create([
'company_id' => null,
User::factory()->count(1)->firstAdmin()
->state(new Sequence(fn ($sequence) => [
'company_id' => $companyIds->random(),
'department_id' => $departmentIds->random(),
]);
$ids = $companyIds->random(min(rand(2, 3), $companyIds->count()))->toArray();
User::where('id', $user->id)->update(['company_id' => $ids[0]]);
$user->companies()->sync($ids);
}
]))
->create();
User::factory()->count(1)->snipeAdmin()
->state(new Sequence(fn ($sequence) => [
'company_id' => $companyIds->random(),
'department_id' => $departmentIds->random(),
]))
->create();
User::factory()->count(1)->testAdmin()
->state(new Sequence(fn ($sequence) => [
'company_id' => $companyIds->random(),
'department_id' => $departmentIds->random(),
]))
->create();
// Superusers — one company each.
User::factory()->count(3)->superuser()
->state(new Sequence(fn ($sequence) => [
'company_id' => $companyIds->random(),
@@ -52,7 +61,6 @@ class UserSeeder extends Seeder
]))
->create();
// Admins — one company each.
User::factory()->count(3)->admin()
->state(new Sequence(fn ($sequence) => [
'company_id' => $companyIds->random(),
@@ -60,38 +68,13 @@ class UserSeeder extends Seeder
]))
->create();
// Regular users — three groups:
// ~30 % (600) no company
// ~50 % (1 000) one company
// ~20 % (400) two or three companies
User::factory()->count(600)->viewAssets()
->state(new Sequence(fn ($sequence) => [
'company_id' => null,
'department_id' => $departmentIds->random(),
]))
->create();
User::factory()->count(1000)->viewAssets()
User::factory()->count(2000)->viewAssets()
->state(new Sequence(fn ($sequence) => [
'company_id' => $companyIds->random(),
'department_id' => $departmentIds->random(),
]))
->create();
$multiCompanyUsers = User::factory()->count(400)->viewAssets()
->state(new Sequence(fn ($sequence) => [
'company_id' => null,
'department_id' => $departmentIds->random(),
]))
->create();
foreach ($multiCompanyUsers as $user) {
$ids = $companyIds->random(min(rand(2, 3), $companyIds->count()))->toArray();
User::where('id', $user->id)->update(['company_id' => $ids[0]]);
$user->companies()->sync($ids);
}
$src = public_path('/img/demo/avatars/');
$dst = 'avatars'.'/';
$del_files = Storage::files($dst);
+1 -1
View File
@@ -51,4 +51,4 @@ SECURE_COOKIES=false
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync
QUEUE_DRIVER=sync
+1 -1
View File
@@ -60,4 +60,4 @@ SECURE_COOKIES=false
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync
QUEUE_DRIVER=sync
+1 -1
View File
@@ -23,7 +23,7 @@
<env name="CACHE_DRIVER" value="array"/>
<env name="MAIL_FROM_ADDR" value="app@example.com"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<ini name="display_errors" value="true"/>
</php>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1645
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6 -24718
View File
File diff suppressed because one or more lines are too long
+1 -414
View File
File diff suppressed because one or more lines are too long
+1 -135
View File
@@ -1,135 +1 @@
#signature-pad {
padding-top: 250px;
margin: auto;
}
.m-signature-pad {
position: relative;
font-size: 10px;
width: 100%;
height: 300px;
border: 1px solid #e8e8e8;
background-color: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.27), 0 0 40px rgba(0, 0, 0, 0.08) inset;
border-radius: 4px;
}
.m-signature-pad:before, .m-signature-pad:after {
position: absolute;
z-index: -1;
content: "";
width: 40%;
height: 10px;
left: 20px;
bottom: 10px;
background: transparent;
-webkit-transform: skew(-3deg) rotate(-3deg);
-moz-transform: skew(-3deg) rotate(-3deg);
-ms-transform: skew(-3deg) rotate(-3deg);
-o-transform: skew(-3deg) rotate(-3deg);
transform: skew(-3deg) rotate(-3deg);
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.4);
}
.m-signature-pad:after {
left: auto;
right: 20px;
-webkit-transform: skew(3deg) rotate(3deg);
-moz-transform: skew(3deg) rotate(3deg);
-ms-transform: skew(3deg) rotate(3deg);
-o-transform: skew(3deg) rotate(3deg);
transform: skew(3deg) rotate(3deg);
}
.m-signature-pad--body {
position: absolute;
top: 20px;
bottom: 60px;
border: 1px solid #f4f4f4;
background-color: white;
}
.m-signature-pad--body
canvas {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border-radius: 4px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.02) inset;
}
.m-signature-pad--footer {
position: absolute;
left: 20px;
right: 20px;
bottom: 20px;
height: 40px;
}
.m-signature-pad--footer
.description {
color: #C3C3C3;
text-align: center;
font-size: 1.2em;
margin-top: 1.8em;
}
.m-signature-pad--footer
.button {
position: absolute;
bottom: 0;
}
.m-signature-pad--footer
.button.clear {
left: 0;
}
.m-signature-pad--footer
.button.save {
right: 0;
}
@media screen and (max-width: 1024px) {
.m-signature-pad {
top: 0;
left: 0;
right: 0;
bottom: 0;
width: auto;
height: auto;
min-width: 250px;
min-height: 140px;
margin: 5%;
}
}
@media screen and (min-device-width: 768px) and (max-device-width: 1024px) {
.m-signature-pad {
margin: 10%;
}
}
@media screen and (max-height: 320px) {
.m-signature-pad--body {
left: 0;
right: 0;
top: 0;
bottom: 32px;
}
.m-signature-pad--footer {
left: 20px;
right: 20px;
bottom: 4px;
height: 28px;
}
.m-signature-pad--footer
.description {
font-size: 1em;
margin-top: 1em;
}
}
#signature-pad{padding-top:250px;margin:auto}.m-signature-pad{position:relative;font-size:10px;width:100%;height:300px;border:1px solid #e8e8e8;background-color:#fff;box-shadow:0 1px 4px rgba(0,0,0,.27),0 0 40px rgba(0,0,0,.08) inset;border-radius:4px}.m-signature-pad:after,.m-signature-pad:before{position:absolute;z-index:-1;content:"";width:40%;height:10px;left:20px;bottom:10px;background:0 0;-webkit-transform:skew(-3deg) rotate(-3deg);-moz-transform:skew(-3deg) rotate(-3deg);-ms-transform:skew(-3deg) rotate(-3deg);-o-transform:skew(-3deg) rotate(-3deg);transform:skew(-3deg) rotate(-3deg);box-shadow:0 8px 12px rgba(0,0,0,.4)}.m-signature-pad:after{left:auto;right:20px;-webkit-transform:skew(3deg) rotate(3deg);-moz-transform:skew(3deg) rotate(3deg);-ms-transform:skew(3deg) rotate(3deg);-o-transform:skew(3deg) rotate(3deg);transform:skew(3deg) rotate(3deg)}.m-signature-pad--body{position:absolute;top:20px;bottom:60px;border:1px solid #f4f4f4;background-color:#fff}.m-signature-pad--body canvas{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:4px;box-shadow:0 0 5px rgba(0,0,0,.02) inset}.m-signature-pad--footer{position:absolute;left:20px;right:20px;bottom:20px;height:40px}.m-signature-pad--footer .description{color:#c3c3c3;text-align:center;font-size:1.2em;margin-top:1.8em}.m-signature-pad--footer .button{position:absolute;bottom:0}.m-signature-pad--footer .button.clear{left:0}.m-signature-pad--footer .button.save{right:0}@media screen and (max-width:1024px){.m-signature-pad{top:0;left:0;right:0;bottom:0;width:auto;height:auto;min-width:250px;min-height:140px;margin:5%}}@media screen and (min-device-width:768px) and (max-device-width:1024px){.m-signature-pad{margin:10%}}@media screen and (max-height:320px){.m-signature-pad--body{left:0;right:0;top:0;bottom:32px}.m-signature-pad--footer{left:20px;right:20px;bottom:4px;height:28px}.m-signature-pad--footer .description{font-size:1em;margin-top:1em}}
+2 -53227
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -38849
View File
File diff suppressed because one or more lines are too long
+5 -5
View File
@@ -1,9 +1,9 @@
{
"/js/dist/all.js": "/js/dist/all.js?id=4619b48bfce17ad41fc5a2e9ee578988",
"/css/build/overrides.css": "/css/build/overrides.css?id=c173dd71d56c1089bf560a849586d93e",
"/css/build/app.css": "/css/build/app.css?id=63ef76491d01db361ad53cf1c8c7114f",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=ee0ed88465dd878588ed044eefb67723",
"/css/dist/all.css": "/css/dist/all.css?id=57e6bf27bcfad47e58a82b9842a7d5bd",
"/js/dist/all.js": "/js/dist/all.js?id=a7ea6cdd7a7105bc604ce52bf82f5920",
"/css/build/overrides.css": "/css/build/overrides.css?id=9bfab28a94932d45568ad50f3c6c5e2c",
"/css/build/app.css": "/css/build/app.css?id=4b2abd7fa3560ada549e9d08bd836aa8",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=bdf169bc2141f453390614c138cdce95",
"/css/dist/all.css": "/css/dist/all.css?id=f5f404325dedd1abd00dc781664c0034",
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/js/select2/i18n/af.js": "/js/select2/i18n/af.js?id=4f6fcd73488ce79fae1b7a90aceaecde",
+1 -1
View File
@@ -210,7 +210,7 @@ $(function () {
search: params.term,
page: params.page || 1,
statusType: link.data("asset-status-type"),
companyId: link.data("company-ids") || link.data("company-id"),
companyId: link.data("company-id"),
};
return data;
},
@@ -19,7 +19,6 @@ return [
'required_acceptance' => 'crwdns1244:0crwdne1244:0',
'global_signature_required_notice' => 'crwdns14708:0crwdne14708:0',
'required_eula' => 'crwdns1245:0crwdne1245:0',
'required_signature' => 'crwdns14815:0crwdne14815:0',
'no_default_eula' => 'crwdns1246:0crwdne1246:0',
'update' => 'crwdns639:0crwdne639:0',
'use_default_eula' => 'crwdns1247:0crwdne1247:0',
@@ -5,7 +5,7 @@ return [
'manage' => 'crwdns6501:0crwdne6501:0',
'field' => 'crwdns1487:0crwdne1487:0',
'about_fieldsets_title' => 'crwdns1488:0crwdne1488:0',
'about_fieldsets_text' => 'crwdns14799:0crwdne14799:0',
'about_fieldsets_text' => 'crwdns14734:0crwdne14734:0',
'custom_format' => 'crwdns6505:0crwdne6505:0',
'encrypt_field' => 'crwdns1792:0crwdne1792:0',
'encrypt_field_help' => 'crwdns1683:0crwdne1683:0',
@@ -64,6 +64,4 @@ return [
'optional_infos' => 'crwdns10490:0crwdne10490:0',
'order_details' => 'crwdns10492:0crwdne10492:0',
'calc_eol' => 'crwdns12782:0crwdne12782:0',
'checkin_licenses' => 'crwdns14891:0crwdne14891:0',
'checkin_child_assets' => 'crwdns14893:0crwdne14893:0',
];
@@ -8,7 +8,6 @@ return [
'bulk_checkout' => 'crwdns12902:0crwdne12902:0',
'bulk_checkin' => 'crwdns12904:0crwdne12904:0',
'checkin' => 'crwdns756:0crwdne756:0',
'checkin_assets' => 'crwdns14883:0crwdne14883:0',
'checkout' => 'crwdns1905:0crwdne1905:0',
'clear' => 'crwdns13286:0crwdne13286:0',
'clone' => 'crwdns758:0crwdne758:0',
@@ -94,12 +94,6 @@ return [
'success' => 'crwdns12770:0crwdne12770:0',
],
'multi-checkin' => [
'error' => 'crwdns14885:0crwdne14885:0',
'success' => 'crwdns14887:0crwdne14887:0',
'no_assets_selected' => 'crwdns14889:0crwdne14889:0',
],
'checkin' => [
'error' => 'crwdns752:0crwdne752:0',
'success' => 'crwdns753:0crwdne753:0',
@@ -31,11 +31,6 @@ return [
'log_msg' => 'crwdns12570:0crwdne12570:0',
],
'checkin_selected' => [
'success' => 'crwdns14899:0crwdne14899:0',
'no_seats_selected' => 'crwdns14901:0crwdne14901:0',
],
'checkout_all' => [
'button' => 'crwdns11561:0crwdne11561:0',
'modal' => 'crwdns11563:0crwdne11563:0',
@@ -1,7 +0,0 @@
<?php
return [
'maintenance_types' => 'crwdns14875:0crwdne14875:0',
'create' => 'crwdns14877:0crwdne14877:0',
'update' => 'crwdns14879:0crwdne14879:0',
];
@@ -1,22 +0,0 @@
<?php
return [
'not_found' => 'crwdns14835:0crwdne14835:0',
'create' => [
'error' => 'crwdns14837:0crwdne14837:0',
'success' => 'crwdns14839:0crwdne14839:0',
],
'update' => [
'error' => 'crwdns14841:0crwdne14841:0',
'success' => 'crwdns14843:0crwdne14843:0',
],
'delete' => [
'confirm' => 'crwdns14845:0crwdne14845:0',
'error' => 'crwdns14847:0crwdne14847:0',
'success' => 'crwdns14849:0crwdne14849:0',
],
'complete' => [
'success' => 'crwdns14851:0crwdne14851:0',
'error' => 'crwdns14853:0crwdne14853:0',
],
];
@@ -5,18 +5,11 @@ return [
'asset_maintenance_type' => 'crwdns14588:0crwdne14588:0',
'title' => 'crwdns13588:0crwdne13588:0',
'start_date' => 'crwdns13590:0crwdne13590:0',
'completion_date' => 'crwdns14859:0crwdne14859:0',
'completion_date' => 'crwdns13592:0crwdne13592:0',
'cost' => 'crwdns13594:0crwdne13594:0',
'is_warranty' => 'crwdns13596:0crwdne13596:0',
'asset_maintenance_time' => 'crwdns14586:0crwdne14586:0',
'notes' => 'crwdns13600:0crwdne13600:0',
'update' => 'crwdns13602:0crwdne13602:0',
'create' => 'crwdns13604:0crwdne13604:0',
'responsible_party' => 'crwdns14861:0crwdne14861:0',
'checked_out_to_at_creation' => 'crwdns14863:0crwdne14863:0',
'completed_at' => 'crwdns14865:0crwdne14865:0',
'completed_by' => 'crwdns14867:0crwdne14867:0',
'mark_complete' => 'crwdns14869:0crwdne14869:0',
'already_complete' => 'crwdns14871:0crwdne14871:0',
'completion_notes' => 'crwdns14873:0crwdne14873:0',
];
@@ -14,10 +14,4 @@ return [
'hardware_support' => 'crwdns13576:0crwdne13576:0',
'configuration_change' => 'crwdns13578:0crwdne13578:0',
'pat_test' => 'crwdns13580:0crwdne13580:0',
'checked_out_to_help' => 'crwdns14823:0crwdne14823:0',
'show_completed' => 'crwdns14825:0crwdne14825:0',
'show_active' => 'crwdns14827:0crwdne14827:0',
'due' => 'crwdns14829:0crwdne14829:0',
'overdue' => 'crwdns14831:0crwdne14831:0',
'completed' => 'crwdns14833:0crwdne14833:0',
];
@@ -18,9 +18,4 @@ return [
'asset_maintenance_incomplete' => 'crwdns13552:0crwdne13552:0',
'warranty' => 'crwdns13554:0crwdne13554:0',
'not_warranty' => 'crwdns13556:0crwdne13556:0',
'complete' => [
'confirm' => 'crwdns14817:0crwdne14817:0',
'success' => 'crwdns14819:0crwdne14819:0',
'error' => 'crwdns14821:0crwdne14821:0',
],
];
+2 -3
View File
@@ -37,9 +37,8 @@ return [
'user_activated' => 'crwdns6826:0crwdne6826:0',
'activation_status_warning' => 'crwdns6747:0crwdne6747:0',
'group_memberships_helpblock' => 'crwdns6749:0crwdne6749:0',
'superadmin_permission_warning' => 'crwdns14805:0crwdne14805:0',
'self_permission_warning' => 'crwdns14807:0crwdne14807:0',
'admin_permission_warning' => 'crwdns14809:0crwdne14809:0',
'superadmin_permission_warning' => 'crwdns6751:0crwdne6751:0',
'admin_permission_warning' => 'crwdns6753:0crwdne6753:0',
'remove_group_memberships' => 'crwdns6755:0crwdne6755:0',
'warning_deletion_information' => 'crwdns14444:0crwdne14444:0',
'update_user_assets_status' => 'crwdns10488:0crwdne10488:0',
+4 -12
View File
@@ -85,7 +85,6 @@ return [
'click_here' => 'crwdns1854:0crwdne1854:0',
'clear_selection' => 'crwdns1962:0crwdne1962:0',
'companies' => 'crwdns1444:0crwdne1444:0',
'companies_var' => 'crwdns14905:0crwdne14905:0',
'company' => 'crwdns1445:0crwdne1445:0',
'component' => 'crwdns1571:0crwdne1571:0',
'components' => 'crwdns1572:0crwdne1572:0',
@@ -123,7 +122,7 @@ return [
'debug_warning_text' => 'crwdns1828:0crwdne1828:0',
'delete' => 'crwdns1046:0crwdne1046:0',
'delete_confirm' => 'crwdns2020:0crwdne2020:0',
'delete_confirm_no_undo' => 'crwdns14801:0crwdne14801:0',
'delete_confirm_no_undo' => 'crwdns14736:0crwdne14736:0',
'deleted' => 'crwdns1047:0crwdne1047:0',
'delete_seats' => 'crwdns1430:0crwdne1430:0',
'deletion_failed' => 'crwdns6117:0crwdne6117:0',
@@ -168,7 +167,7 @@ return [
'image_upload' => 'crwdns1058:0crwdne1058:0',
'filetypes_accepted_help' => 'crwdns12622:0crwdne12622:0',
'filetypes_size_help' => 'crwdns12624:0crwdne12624:0',
'image_filetypes_help' => 'crwdns14803:0crwdne14803:0',
'image_filetypes_help' => 'crwdns14738:0crwdne14738:0',
'unaccepted_image_type' => 'crwdns11365:0crwdne11365:0',
'import' => 'crwdns1411:0crwdne1411:0',
'documentation' => 'crwdns14462:0crwdne14462:0',
@@ -210,7 +209,6 @@ return [
'logout' => 'crwdns1066:0crwdne1066:0',
'lookup_by_tag' => 'crwdns1648:0crwdne1648:0',
'maintenances' => 'crwdns1998:0crwdne1998:0',
'maintenance_complete' => 'crwdns14855:0crwdne14855:0',
'manage_api_keys' => 'crwdns12630:0crwdne12630:0',
'manufacturer' => 'crwdns1067:0crwdne1067:0',
'manufacturers' => 'crwdns1068:0crwdne1068:0',
@@ -239,7 +237,7 @@ return [
'note_added' => 'crwdns12858:0crwdne12858:0',
'options' => 'crwdns12888:0crwdne12888:0',
'preview' => 'crwdns12890:0crwdne12890:0',
'add_note' => 'crwdns14857:0crwdne14857:0',
'add_note' => 'crwdns12860:0crwdne12860:0',
'note_edited' => 'crwdns12862:0crwdne12862:0',
'edit_note' => 'crwdns12864:0crwdne12864:0',
'note_deleted' => 'crwdns12866:0crwdne12866:0',
@@ -257,8 +255,6 @@ return [
'processing' => 'crwdns1279:0crwdne1279:0',
'profile' => 'crwdns14476:0crwdne14476:0',
'purchase_cost' => 'crwdns1830:0crwdne1830:0',
'purchase_cost_format_help' => 'crwdns14811:0crwdne14811:0',
'purchase_cost_invalid' => 'crwdns14813:0crwdne14813:0',
'purchase_date' => 'crwdns1831:0crwdne1831:0',
'qty' => 'crwdns1328:0crwdne1328:0',
'quantity' => 'crwdns1473:0crwdne1473:0',
@@ -288,7 +284,6 @@ return [
'select_var' => 'crwdns11455:0crwdne11455:0', // this will eventually replace all of our other selects
'select' => 'crwdns1281:0crwdne1281:0',
'select_all' => 'crwdns6155:0crwdne6155:0',
'selected' => 'crwdns14897:0crwdne14897:0',
'search' => 'crwdns1290:0crwdne1290:0',
'select_category' => 'crwdns1663:0crwdne1663:0',
'select_datasource' => 'crwdns12632:0crwdne12632:0',
@@ -508,7 +503,7 @@ return [
'fullscreen' => 'crwdns14787:0crwdne14787:0',
'pie_chart_type' => 'crwdns10546:0crwdne10546:0',
'hello_name' => 'crwdns10548:0crwdne10548:0',
'unaccepted_profile_warning' => 'crwdns14907:0{1}crwdne14907:0',
'unaccepted_profile_warning' => 'crwdns12686:0crwdne12686:0',
'start_date' => 'crwdns11168:0crwdne11168:0',
'end_date' => 'crwdns11170:0crwdne11170:0',
'alt_uploaded_image_thumbnail' => 'crwdns11172:0crwdne11172:0',
@@ -520,7 +515,6 @@ return [
'pre_flight' => 'crwdns11319:0crwdne11319:0',
'skip_to_main_content' => 'crwdns11321:0crwdne11321:0',
'toggle_navigation' => 'crwdns11323:0crwdne11323:0',
'toggle_password_visibility' => 'crwdns14903:0crwdne14903:0',
'alerts' => 'crwdns11325:0crwdne11325:0',
'tasks_view_all' => 'crwdns11327:0crwdne11327:0',
'true' => 'crwdns11329:0crwdne11329:0',
@@ -582,7 +576,6 @@ return [
'error_user_company_accept_view' => 'crwdns11787:0crwdne11787:0',
'error_assets_already_checked_out' => 'crwdns13826:0crwdne13826:0',
'assigned_assets_removed' => 'crwdns13830:0crwdne13830:0',
'unassigned_assets_removed' => 'crwdns14895:0crwdne14895:0',
'upload_files' => 'crwdns14580:0crwdne14580:0',
'uploaded_files' => 'crwdns14582:0crwdne14582:0',
'sign_in_place' => 'crwdns14700:0crwdne14700:0',
@@ -682,7 +675,6 @@ return [
'user_managed_passwords' => 'crwdns12870:0crwdne12870:0',
'user_managed_passwords_disallow' => 'crwdns12872:0crwdne12872:0',
'user_managed_passwords_allow' => 'crwdns12874:0crwdne12874:0',
'user_managed_passwords_bulk_help' => 'crwdns14881:0crwdne14881:0',
'from' => 'crwdns13170:0crwdne13170:0',
'by' => 'crwdns13172:0crwdne13172:0',
'by_user' => 'crwdns14246:0crwdne14246:0',
@@ -19,7 +19,6 @@ return [
'required_acceptance' => 'Hierdie gebruiker sal per e-pos met \'n skakel gestuur word om die aanvaarding van hierdie item te bevestig.',
'global_signature_required_notice' => 'User signatures are currently required globally via the admin settings, so signatures will still be required regardless of this category setting if the item is checked out to a user (versus a location, etc).',
'required_eula' => 'Hierdie gebruiker sal \'n afskrif van die EULA ontvang',
'required_signature' => 'This user will be required to sign to confirm acceptance of this item.',
'no_default_eula' => 'Geen primêre standaard EULA gevind nie. Voeg een by Instellings.',
'update' => 'Opdateer kategorie',
'use_default_eula' => 'Gebruik eerder die <a href="#" data-toggle="modal" data-target="#eulaModal">primary standaard EULA</a>.',
@@ -5,7 +5,7 @@ return [
'manage' => 'Manage',
'field' => 'veld',
'about_fieldsets_title' => 'Oor Fieldsets',
'about_fieldsets_text' => 'Fieldsets allow you to create groups of custom fields that are frequently re-used for specific asset model types.',
'about_fieldsets_text' => 'Veldstelle stel jou in staat om groepe van persoonlike velde te skep wat gereeld hergebruik word vir spesifieke tipe bates.',
'custom_format' => 'Custom Regex format...',
'encrypt_field' => 'Enkripteer die waarde van hierdie veld in die databasis',
'encrypt_field_help' => 'WAARSKUWING: Om \'n veld te enkripteer, maak dit onondersoekbaar.',
@@ -64,6 +64,4 @@ return [
'optional_infos' => 'Optional Information',
'order_details' => 'Order Related Information',
'calc_eol' => 'If nulling the EOL date, use automatic EOL calculation based on the purchase date and EOL rate.',
'checkin_licenses' => 'Checkin associated license seats',
'checkin_child_assets' => 'Checkin associated assets',
];
@@ -8,7 +8,6 @@ return [
'bulk_checkout' => 'Grootmaat Checkout',
'bulk_checkin' => 'Bulk Checkin',
'checkin' => 'Kontrole bate',
'checkin_assets' => 'Checkin Assets',
'checkout' => 'Checkout Asset',
'clear' => 'Clear',
'clone' => 'Klone Bate',
@@ -94,12 +94,6 @@ return [
'success' => 'Asset checked out successfully.|Assets checked out successfully.',
],
'multi-checkin' => [
'error' => 'Asset was not checked in, please try again|Assets were not checked in, please try again',
'success' => 'Asset checked in successfully.|Assets checked in successfully.',
'no_assets_selected' => 'You must select at least one asset from the list',
],
'checkin' => [
'error' => 'Bate is nie nagegaan nie, probeer asseblief weer',
'success' => 'Die bate is suksesvol nagegaan.',
@@ -31,11 +31,6 @@ return [
'log_msg' => 'Checked in via bulk license checkin in license GUI',
],
'checkin_selected' => [
'success' => ':count seat checked in successfully. | :count seats checked in successfully.',
'no_seats_selected' => 'No seats were selected.',
],
'checkout_all' => [
'button' => 'Checkout All Seats',
'modal' => 'This action will checkout one seat to the first available user. | This action will checkout all :available_seats_count seats to the first available users. A user is considered available for this seat if they do not already have this license checked out to them, and the Auto-Assign License property is enabled on their user account.',
@@ -1,7 +0,0 @@
<?php
return [
'maintenance_types' => 'Maintenance Types',
'create' => 'Create Maintenance Type',
'update' => 'Update Maintenance Type',
];
@@ -1,22 +0,0 @@
<?php
return [
'not_found' => 'Maintenance type not found.',
'create' => [
'error' => 'Maintenance type was not created, please try again.',
'success' => 'Maintenance type created successfully.',
],
'update' => [
'error' => 'Maintenance type was not updated, please try again.',
'success' => 'Maintenance type updated successfully.',
],
'delete' => [
'confirm' => 'Are you sure you wish to delete this maintenance type?',
'error' => 'There was an issue deleting this maintenance type. Please try again.',
'success' => 'The maintenance type was deleted successfully.',
],
'complete' => [
'success' => 'Maintenance marked as complete.',
'error' => 'There was an issue marking this maintenance as complete. Please try again.',
],
];
@@ -5,18 +5,11 @@ return [
'asset_maintenance_type' => 'tipe',
'title' => 'Titel',
'start_date' => 'Start Date',
'completion_date' => 'Expected Completion',
'completion_date' => 'Completion Date',
'cost' => 'koste',
'is_warranty' => 'Garantieverbetering',
'asset_maintenance_time' => 'Duration',
'notes' => 'notas',
'update' => 'Update Asset Maintenance',
'create' => 'Create Asset Maintenance',
'responsible_party' => 'Responsible Party',
'checked_out_to_at_creation' => 'Gekontroleer na',
'completed_at' => 'Completed At',
'completed_by' => 'Completed By',
'mark_complete' => 'Mark Complete',
'already_complete' => 'Already Completed',
'completion_notes' => 'Completion Notes',
];
@@ -14,10 +14,4 @@ return [
'hardware_support' => 'Hardware Support',
'configuration_change' => 'Configuration Change',
'pat_test' => 'PAT Test',
'checked_out_to_help' => 'The user, etc that the asset was checked out to at the time of maintenance creation. This is for historical reference and does not affect the current checkout status of the asset.',
'show_completed' => 'Show Completed',
'show_active' => 'Show Active',
'due' => 'Due',
'overdue' => 'Overdue',
'completed' => 'voltooi',
];
@@ -18,9 +18,4 @@ return [
'asset_maintenance_incomplete' => 'Nog nie voltooi nie',
'warranty' => 'waarborg',
'not_warranty' => 'Nie waarborg nie',
'complete' => [
'confirm' => 'Are you sure you want to mark this maintenance as complete? This cannot be undone.',
'success' => 'Maintenance marked as complete.',
'error' => 'There was an issue marking this maintenance as complete. Please try again.',
],
];
+2 -3
View File
@@ -37,9 +37,8 @@ return [
'user_activated' => 'User can login',
'activation_status_warning' => 'Do not change activation status',
'group_memberships_helpblock' => 'Only superadmins may edit group memberships.',
'superadmin_permission_warning' => 'Only superadmins may grant or revoke superadmin access.',
'self_permission_warning' => 'Only superadmins may edit their own permissions.',
'admin_permission_warning' => 'Only users with admins rights or greater may grant or revoke admin access.',
'superadmin_permission_warning' => 'Only superadmins may grant a user superadmin access.',
'admin_permission_warning' => 'Only users with admins rights or greater may grant a user admin access.',
'remove_group_memberships' => 'Remove Group Memberships',
'warning_deletion_information' => 'You are about to checkin ALL items from the :count user(s) listed below.',
'update_user_assets_status' => 'Update all assets for these users to this status',

Some files were not shown because too many files have changed in this diff Show More