Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c02a6c105a | |||
| cfa8069953 | |||
| b3be2baf40 | |||
| 069912d051 | |||
| 86245ad4ae | |||
| c8bafdad79 | |||
| c94fce2367 | |||
| 653b1327cb | |||
| 849b217300 | |||
| 371f096e54 | |||
| 72a11113e7 | |||
| b0635f24db | |||
| 96088c416e | |||
| c8f3e833e5 | |||
| 5307a44fab | |||
| 2d6eb5d80a | |||
| 90e2c105cd | |||
| 875b0bbdec | |||
| be1f1bd1c5 | |||
| c9be696c84 | |||
| 187f160b21 | |||
| 8908b67b3d | |||
| 4373f761c7 | |||
| 8e9bd5dbb1 | |||
| 751541a54d | |||
| 3972799e56 | |||
| db2afd0dc7 | |||
| 460daf71b6 | |||
| 3074bae47c | |||
| 0f80950a91 | |||
| 2620b60048 | |||
| 81b1cdc6e9 | |||
| 0304933c53 | |||
| f0d84f5350 | |||
| 1ad562f8b9 | |||
| a5cea247f1 | |||
| 571bc39495 | |||
| 8ea78fae21 | |||
| ed6b3c04ab | |||
| a4ca0a592f | |||
| 90c8689596 |
@@ -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
@@ -113,7 +113,7 @@ ENABLE_HSTS=false
|
||||
# --------------------------------------------
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
QUEUE_DRIVER=sync
|
||||
CACHE_PREFIX=snipeit
|
||||
|
||||
# --------------------------------------------
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ ENABLE_HSTS=false
|
||||
# --------------------------------------------
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
QUEUE_DRIVER=sync
|
||||
CACHE_PREFIX=snipeit
|
||||
|
||||
# --------------------------------------------
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ CORS_ALLOWED_ORIGINS="*"
|
||||
# --------------------------------------------
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
QUEUE_DRIVER=sync
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: LOGIN THROTTLING
|
||||
|
||||
+2
-3
@@ -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
@@ -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!
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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')));
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) ? [
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"ext-curl": "*",
|
||||
"ext-exif": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-json": "*",
|
||||
|
||||
Generated
+1
-2
@@ -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": "*",
|
||||
|
||||
@@ -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
@@ -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',
|
||||
];
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -51,4 +51,4 @@ SECURE_COOKIES=false
|
||||
# --------------------------------------------
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
QUEUE_DRIVER=sync
|
||||
|
||||
+1
-1
@@ -60,4 +60,4 @@ SECURE_COOKIES=false
|
||||
# --------------------------------------------
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
QUEUE_DRIVER=sync
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-5413
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1645
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1273
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+6
-24718
File diff suppressed because one or more lines are too long
Vendored
+1
-414
File diff suppressed because one or more lines are too long
+1
-135
@@ -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}}
|
||||
|
||||
Vendored
+2
-53227
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-38849
File diff suppressed because one or more lines are too long
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user