Compare commits

..

1 Commits

Author SHA1 Message Date
snipe 90c8689596 Prod assets 2026-05-12 19:43:34 +01:00
263 changed files with 5088 additions and 69527 deletions
-9
View File
@@ -4271,15 +4271,6 @@
"contributions": [
"code"
]
},
{
"login": "CybotTM",
"name": "Sebastian Mendel",
"avatar_url": "https://avatars.githubusercontent.com/u/326348?v=4",
"profile": "https://github.com/CybotTM",
"contributions": [
"code"
]
}
]
}
+1 -1
View File
@@ -113,7 +113,7 @@ ENABLE_HSTS=false
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync
QUEUE_DRIVER=sync
CACHE_PREFIX=snipeit
# --------------------------------------------
+1 -1
View File
@@ -120,7 +120,7 @@ ENABLE_HSTS=false
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync
QUEUE_DRIVER=sync
CACHE_PREFIX=snipeit
# --------------------------------------------
+1 -1
View File
@@ -72,7 +72,7 @@ CORS_ALLOWED_ORIGINS="*"
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync
QUEUE_DRIVER=sync
# --------------------------------------------
# OPTIONAL: LOGIN THROTTLING
+1 -2
View File
@@ -142,7 +142,7 @@ ENABLE_HSTS=false
# OPTIONAL: CACHE SETTINGS
# --------------------------------------------
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
QUEUE_DRIVER=sync
CACHE_PREFIX=snipeit
# --------------------------------------------
@@ -210,7 +210,6 @@ LOGIN_AUTOCOMPLETE=false
RESET_PASSWORD_LINK_EXPIRES=15
PASSWORD_CONFIRM_TIMEOUT=10800
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
TWO_FACTOR_MAX_ATTEMPTS_PER_MIN=5
INVITE_PASSWORD_LINK_EXPIRES=1500
# --------------------------------------------
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
sarif-result-file: "ethicalcheck-results.sarif"
- name: Upload sarif file to repository
uses: github/codeql-action/upload-sarif@v4
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ./ethicalcheck-results.sarif
+1 -1
View File
@@ -69,7 +69,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars.githubusercontent.com/u/10965027?v=4" width="110px;"/><br /><sub>Ellie</sub>](https://leafedfox.xyz/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeafedFox "Code") | [<img src="https://avatars.githubusercontent.com/u/20960555?v=4" width="110px;"/><br /><sub>GA Stamper</sub>](https://github.com/gastamper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gastamper "Code") | [<img src="https://avatars.githubusercontent.com/u/206553556?v=4" width="110px;"/><br /><sub>Guillaume Lefranc</sub>](https://github.com/gl-pup)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gl-pup "Code") | [<img src="https://avatars.githubusercontent.com/u/733892?v=4" width="110px;"/><br /><sub>Hajo Möller</sub>](https://github.com/dasjoe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dasjoe "Code") | [<img src="https://avatars.githubusercontent.com/u/3420063?v=4" width="110px;"/><br /><sub>Istvan Basa</sub>](https://github.com/pottom)<br />[💻](https://github.com/snipe/snipe-it/commits?author=pottom "Code") | [<img src="https://avatars.githubusercontent.com/u/810824?v=4" width="110px;"/><br /><sub>JJ Asghar</sub>](https://jjasghar.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jjasghar "Code") | [<img src="https://avatars.githubusercontent.com/u/40404495?v=4" width="110px;"/><br /><sub>James E. Msenga</sub>](https://github.com/JemCdo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JemCdo "Code") |
| [<img src="https://avatars.githubusercontent.com/u/6865786?v=4" width="110px;"/><br /><sub>Jan Felix Wiebe</sub>](https://github.com/jfwiebe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jfwiebe "Code") | [<img src="https://avatars.githubusercontent.com/u/43412008?v=4" width="110px;"/><br /><sub>Jo Drexl</sub>](https://www.nfon.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=drexljo "Code") | [<img src="https://avatars.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>Austin Sasko</sub>](https://github.com/austinsasko)<br />[💻](https://github.com/snipe/snipe-it/commits?author=austinsasko "Code") | [<img src="https://avatars.githubusercontent.com/u/4875039?v=4" width="110px;"/><br /><sub>Jasson</sub>](http://jassoncordones.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JassonCordones "Code") | [<img src="https://avatars.githubusercontent.com/u/76069640?v=4" width="110px;"/><br /><sub>Okean</sub>](https://github.com/Tinyblargon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tinyblargon "Code") | [<img src="https://avatars.githubusercontent.com/u/6515064?v=4" width="110px;"/><br /><sub>Alejandro Medrano</sub>](https://www.lst.tfo.upm.es/alejandro-medrano/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=amedranogil "Code") | [<img src="https://avatars.githubusercontent.com/u/58696401?v=4" width="110px;"/><br /><sub>Lukas Kraic</sub>](https://github.com/lukaskraic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukaskraic "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1571724?v=4" width="110px;"/><br /><sub>Герхард PICCORO Lenz McKAY </sub>](https://github-readme-stats.vercel.app/api?username=mckaygerhard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") | [<img src="https://avatars.githubusercontent.com/u/15015119?v=4" width="110px;"/><br /><sub>Johannes Pollitt</sub>](https://github.com/FlorestanII)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorestanII "Code") | [<img src="https://avatars.githubusercontent.com/u/14185442?v=4" width="110px;"/><br /><sub>Michael Strobel</sub>](https://strobelm.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=strobelm "Code") | [<img src="https://avatars.githubusercontent.com/u/634790?v=4" width="110px;"/><br /><sub>Nicky West</sub>](http://nickwest.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nickwest "Code") | [<img src="https://avatars.githubusercontent.com/u/1347327?v=4" width="110px;"/><br /><sub>akaspeh1</sub>](https://github.com/akaspeh1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akaspeh1 "Code") | [<img src="https://avatars.githubusercontent.com/u/2880129?v=4" width="110px;"/><br /><sub>Sebastian Marsching</sub>](http://sebastian.marsching.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smarsching "Code") | [<img src="https://avatars.githubusercontent.com/u/40658372?v=4" width="110px;"/><br /><sub>Mo</sub>](https://github.com/mohammad-ahmadi1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mohammad-ahmadi1 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/20994684?v=4" width="110px;"/><br /><sub>Owen V. Hayes</sub>](https://github.com/MarvelousAnything)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MarvelousAnything "Code") | [<img src="https://avatars.githubusercontent.com/u/75509373?v=4" width="110px;"/><br /><sub>Peter Gallwas</sub>](https://www.husky.nz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Husky-Devel "Code") | [<img src="https://avatars.githubusercontent.com/u/326348?v=4" width="110px;"/><br /><sub>Sebastian Mendel</sub>](https://github.com/CybotTM)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CybotTM "Code") |
| [<img src="https://avatars.githubusercontent.com/u/20994684?v=4" width="110px;"/><br /><sub>Owen V. Hayes</sub>](https://github.com/MarvelousAnything)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MarvelousAnything "Code") | [<img src="https://avatars.githubusercontent.com/u/75509373?v=4" width="110px;"/><br /><sub>Peter Gallwas</sub>](https://www.husky.nz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Husky-Devel "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
-1
View File
@@ -56,7 +56,6 @@ COPY --from=mlocati/php-extension-installer:2.1.15 /usr/bin/install-php-extensio
RUN set -eux; \
install-php-extensions \
bcmath \
exif \
gd \
ldap \
mysqli \
+1 -2
View File
@@ -7,7 +7,7 @@
This is a FOSS project for asset management in IT Operations. Knowing who has which laptop, when it was purchased in order to depreciate it correctly, handling software licenses, etc.
It is built on [Laravel 12](http://laravel.com).
It is built on [Laravel 11](http://laravel.com).
Snipe-IT is actively developed and we [release quite frequently](https://github.com/grokability/snipe-it/releases). ([Check out the live demo here](https://snipeitapp.com/demo/).)
@@ -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.
@@ -13,13 +13,8 @@ final class PreserveUnauthorizedPrivilegedPermissionsAction
* @param array<string, mixed> $originalPermissions
* @return array<string, mixed>
*/
public static function run(array $requestedPermissions, User $authenticatedUser, array $originalPermissions = [], ?User $targetUser = null): array
public static function run(array $requestedPermissions, User $authenticatedUser, array $originalPermissions = []): array
{
// Disallow non-admin/superuser users from modifying their own permissions, but allow them to modify other users' permissions (except for admin/superuser keys).
if ($targetUser && ! $authenticatedUser->isSuperUser() && $authenticatedUser->id === $targetUser->id) {
return $originalPermissions;
}
if (! $authenticatedUser->isSuperUser()) {
if (array_key_exists('superuser', $originalPermissions)) {
$requestedPermissions['superuser'] = $originalPermissions['superuser'];
@@ -1,308 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Events\CheckoutableCheckedIn;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
use App\Models\Component;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
class CheckinAndDeleteItems extends Command
{
protected $signature = 'snipeit:checkin-delete-all
{--company-id= : Only process items belonging to this company ID}
{--admin-id= : ID of the user credited for the checkins (defaults to first superadmin)}
{--no-notifications : Suppress email and webhook notifications}
{--type=all : Comma-separated types to process: assets, licenses, accessories, components, or all}
{--note= : Note recorded on each checkin action log entry}
{--dry-run : Preview what would be processed without making any changes}
{--force : Skip the confirmation prompt}';
protected $description = 'Check in all assigned items and soft-delete them, optionally scoped to a company';
public function handle(): int
{
$companyId = $this->option('company-id');
$noNotifications = $this->option('no-notifications');
$dryRun = $this->option('dry-run');
$typeOption = $this->option('type') ?? 'all';
$note = $this->option('note') ?: 'Checked in and deleted via CLI';
$allTypes = ['assets', 'licenses', 'accessories', 'components'];
$typesToProcess = $typeOption === 'all'
? $allTypes
: array_intersect(array_map('trim', explode(',', $typeOption)), $allTypes);
if (empty($typesToProcess)) {
$this->error('Invalid --type value. Use: assets, licenses, accessories, components, or all.');
return 1;
}
$admin = null;
if (! $dryRun && ! $noNotifications) {
if ($this->option('admin-id')) {
$admin = User::find($this->option('admin-id'));
if (! $admin) {
$this->error('No user found with admin-id '.$this->option('admin-id').'.');
return 1;
}
} else {
$admin = User::onlySuperAdmins()->first();
}
if (! $admin) {
$this->warn('No admin user found — notifications will be suppressed.');
$noNotifications = true;
}
}
$scopeMsg = $companyId ? "company ID {$companyId}" : 'all companies';
$typesMsg = implode(', ', $typesToProcess);
if ($dryRun) {
$this->warn('DRY RUN — no changes will be made.');
} elseif (! $this->option('force')) {
if (! $this->confirm("This will check in and soft-delete all [{$typesMsg}] for [{$scopeMsg}]. Continue?")) {
$this->info('Aborted.');
return 0;
}
}
if (in_array('assets', $typesToProcess)) {
$this->processAssets($companyId, $noNotifications, $note, $admin, $dryRun);
}
if (in_array('licenses', $typesToProcess)) {
$this->processLicenses($companyId, $noNotifications, $note, $admin, $dryRun);
}
if (in_array('accessories', $typesToProcess)) {
$this->processAccessories($companyId, $noNotifications, $note, $admin, $dryRun);
}
if (in_array('components', $typesToProcess)) {
$this->processComponents($companyId, $noNotifications, $note, $admin, $dryRun);
}
if ($dryRun) {
$this->warn('Dry run complete — no changes were made.');
}
return 0;
}
private function processAssets(?string $companyId, bool $noNotifications, string $note, ?User $admin, bool $dryRun): void
{
$query = Asset::query();
if ($companyId) {
$query->where('company_id', $companyId);
}
$assets = $query->get();
$checkedIn = 0;
$deleted = 0;
foreach ($assets as $asset) {
if ($asset->assignedTo) {
if ($dryRun) {
$this->line(' Would check in asset: '.$asset->asset_tag.' (assigned to '.$asset->assignedTo->name.')');
} else {
$target = $asset->assignedTo;
$checkin_at = now()->format('Y-m-d H:i:s');
$originalValues = $asset->getRawOriginal();
if ($noNotifications) {
DB::table('assets')->where('id', $asset->id)
->update(['assigned_to' => null, 'assigned_type' => null]);
$asset->logCheckin($target, $note, $checkin_at, $originalValues);
} else {
// Fire event before clearing so the log captures the original state
event(new CheckoutableCheckedIn($asset, $target, $admin, $note, $checkin_at, $originalValues));
DB::table('assets')->where('id', $asset->id)
->update(['assigned_to' => null, 'assigned_type' => null]);
}
$asset->licenseseats()->update(['assigned_to' => null]);
CheckoutAcceptance::pending()
->whereHasMorph('checkoutable', [Asset::class], fn (Builder $q) => $q->where('id', $asset->id))
->delete();
}
$checkedIn++;
}
if ($dryRun) {
$this->line(' Would delete asset: '.$asset->asset_tag);
} else {
$asset->delete();
}
$deleted++;
}
$action = $dryRun ? 'would be' : 'were';
$this->info("Assets: {$checkedIn} {$action} checked in, {$deleted} {$action} deleted.");
}
private function processLicenses(?string $companyId, bool $noNotifications, string $note, ?User $admin, bool $dryRun): void
{
$query = License::query();
if ($companyId) {
$query->where('company_id', $companyId);
}
$licenses = $query->get();
$seatsCheckedIn = 0;
$deleted = 0;
foreach ($licenses as $license) {
$seats = LicenseSeat::where('license_id', $license->id)
->where(fn ($q) => $q->whereNotNull('assigned_to')->orWhereNotNull('asset_id'))
->get();
foreach ($seats as $seat) {
$target = $seat->assigned_to ? $seat->user : $seat->asset;
if ($dryRun) {
$this->line(' Would check in license seat for: '.$license->name.' (assigned to '.($target?->name ?? $target?->asset_tag ?? 'unknown').')');
} else {
$seat->assigned_to = null;
$seat->asset_id = null;
$seat->save();
if ($target) {
if ($noNotifications) {
$seat->logCheckin($target, $note);
} else {
event(new CheckoutableCheckedIn($seat, $target, $admin, $note));
}
}
}
$seatsCheckedIn++;
}
if ($dryRun) {
$this->line(' Would delete license: '.$license->name);
} else {
$license->licenseseats()->delete();
$license->delete();
}
$deleted++;
}
$action = $dryRun ? 'would be' : 'were';
$this->info("Licenses: {$seatsCheckedIn} seats {$action} checked in, {$deleted} licenses {$action} deleted.");
}
private function processAccessories(?string $companyId, bool $noNotifications, string $note, ?User $admin, bool $dryRun): void
{
$query = Accessory::query();
if ($companyId) {
$query->where('company_id', $companyId);
}
$accessories = $query->get();
$checkedIn = 0;
$deleted = 0;
foreach ($accessories as $accessory) {
$checkouts = AccessoryCheckout::where('accessory_id', $accessory->id)->get();
foreach ($checkouts as $checkout) {
$target = $checkout->assignedTo;
if ($dryRun) {
$this->line(' Would check in accessory: '.$accessory->name.' (assigned to '.($target?->name ?? $target?->asset_tag ?? 'unknown').')');
} else {
$checkin_at = now()->format('Y-m-d H:i:s');
$checkout->delete();
if ($target) {
if ($noNotifications) {
$accessory->logCheckin($target, $note, $checkin_at);
} else {
event(new CheckoutableCheckedIn($accessory, $target, $admin, $note, $checkin_at));
}
}
}
$checkedIn++;
}
if ($dryRun) {
$this->line(' Would delete accessory: '.$accessory->name);
} else {
$accessory->delete();
}
$deleted++;
}
$action = $dryRun ? 'would be' : 'were';
$this->info("Accessories: {$checkedIn} {$action} checked in, {$deleted} {$action} deleted.");
}
private function processComponents(?string $companyId, bool $noNotifications, string $note, ?User $admin, bool $dryRun): void
{
$query = Component::query();
if ($companyId) {
$query->where('company_id', $companyId);
}
$components = $query->get();
$checkedIn = 0;
$deleted = 0;
foreach ($components as $component) {
$assignments = DB::table('components_assets')
->where('component_id', $component->id)
->get();
foreach ($assignments as $assignment) {
$asset = Asset::find($assignment->asset_id);
if ($dryRun) {
$this->line(' Would check in component: '.$component->name.' (assigned to '.($asset?->asset_tag ?? 'unknown').')');
} else {
$checkin_at = now()->format('Y-m-d H:i:s');
DB::table('components_assets')->where('id', $assignment->id)->delete();
if ($asset) {
if ($noNotifications) {
$component->logCheckin($asset, $note, $checkin_at);
} else {
event(new CheckoutableCheckedIn($component, $asset, $admin, $note, $checkin_at));
}
}
}
$checkedIn++;
}
if ($dryRun) {
$this->line(' Would delete component: '.$component->name);
} else {
$component->delete();
}
$deleted++;
}
$action = $dryRun ? 'would be' : 'were';
$this->info("Components: {$checkedIn} {$action} checked in, {$deleted} {$action} deleted.");
}
}
@@ -30,77 +30,41 @@ class CleanIncorrectCheckoutAcceptances extends Command
{
$deletions = 0;
$skips = 0;
$total = CheckoutAcceptance::count();
$this->info("Processing {$total} checkout acceptances...");
$bar = $this->output->createProgressBar($total);
$bar->start();
// This walks *every* checkoutacceptance. That's gnarly. But necessary
$this->withProgressBar(CheckoutAcceptance::all(), function ($checkoutAcceptance) use (&$deletions, &$skips) {
$item = $checkoutAcceptance->checkoutable;
$checkout_to_id = $checkoutAcceptance->assigned_to_id;
if (is_null($item)) {
$this->info("'Checkoutable' Item is null, going to next record");
// Chunk to avoid loading the whole table into memory; eager-load checkoutable
// to eliminate the N+1 on that relationship.
CheckoutAcceptance::with('checkoutable')
->chunkById(500, function ($chunk) use (&$deletions, &$skips, $bar) {
$idsToDelete = [];
return; // 'false' allegedly breaks execution entirely, so 'true' maybe doesn't? hrm. just straight return maybe?
}
if (get_class($item) == LicenseSeat::class) {
$item = $item->license;
}
foreach ($item->assetlog()->where('action_type', 'checkout')->get() as $assetlog) {
if ($assetlog->target_id == $checkout_to_id && $assetlog->target_type != User::class) {
// We have a checkout-to an ID for a non-User, which matches to an ID in the checkout_acceptances table
foreach ($chunk as $checkoutAcceptance) {
$item = $checkoutAcceptance->checkoutable;
$checkout_to_id = $checkoutAcceptance->assigned_to_id;
if (is_null($item)) {
$skips++;
$bar->advance();
continue;
}
if (get_class($item) === LicenseSeat::class) {
$item = $item->license;
if (is_null($item)) {
$skips++;
$bar->advance();
continue;
}
}
if (is_null($checkoutAcceptance->created_at)) {
$skips++;
$bar->advance();
continue;
}
// Push all filtering (including the ±5-second window) into the DB;
// exists() returns as soon as one matching row is found rather than
// fetching all checkout logs into PHP.
$shouldDelete = $item->assetlog()
->where('action_type', 'checkout')
->where('target_id', $checkout_to_id)
->where('target_type', '!=', User::class)
->whereBetween('created_at', [
$checkoutAcceptance->created_at->copy()->subSeconds(5),
$checkoutAcceptance->created_at->copy()->addSeconds(5),
])
->exists();
if ($shouldDelete) {
$idsToDelete[] = $checkoutAcceptance->id;
// now, let's compare the _times_ - are they close?
// I'm picking `created_at` over `action_date` because I'm more interested in when the actionlogs
// were _created_, not when they were alleged to have happened - those created_at times need to be within 'X' seconds of
// each other (currently 5)
if ($assetlog->created_at->diffInSeconds($checkoutAcceptance->created_at, true) <= 5) { // we're allowing for five _ish_ seconds of slop
$deletions++;
$checkoutAcceptance->forceDelete(); // HARD delete this record; it should have never been
return;
} else {
$skips++;
// $this->info("The two records are too far apart");
}
$bar->advance();
} else {
// $this->info("No match! checkout to id: " . $checkout_to_id." target_id: ".$assetlog->target_id." target_type: ".$assetlog->target_type);
}
// Bulk-delete the bad records in one query per chunk instead of one per row.
if (! empty($idsToDelete)) {
CheckoutAcceptance::whereIn('id', $idsToDelete)->forceDelete();
}
});
$bar->finish();
$this->newLine();
$this->info("Final deletion count: {$deletions}, and skip count: {$skips}");
}
$skips++;
});
$this->error("Final deletion count: $deletions, and skip count: $skips");
}
}
+1 -40
View File
@@ -5,7 +5,6 @@ namespace App\Console\Commands;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
class ResetDemoSettings extends Command
{
@@ -48,7 +47,7 @@ class ResetDemoSettings extends Command
$settings->auto_increment_assets = 1;
$settings->logo = 'snipe-logo.png';
$settings->alert_email = 'service@snipe-it.io';
$settings->login_note = "Use any of the following credentials to login to the demo:\n\n- `admin` / `password`\n- `assets` / `password`\n- `testuser` / `password`";
$settings->login_note = 'Use `admin` / `password` to login to the demo.';
$settings->header_color = '#3c8dbc';
$settings->link_dark_color = '#5fa4cc';
$settings->link_light_color = '#296282;';
@@ -86,44 +85,6 @@ class ResetDemoSettings extends Command
$user->save();
}
$assetsUser = User::updateOrCreate(
['username' => 'assets'],
[
'first_name' => 'Assets',
'last_name' => 'User',
'password' => Hash::make('password'),
'activated' => 1,
]
);
$assetsUser->permissions = json_encode([
'assets.view' => 1,
'assets.create' => 1,
'assets.edit' => 1,
'assets.delete' => 1,
'assets.checkout' => 1,
'assets.checkin' => 1,
'assets.audit' => 1,
'assets.files' => 1,
'assets.view.requestable' => 1,
'assets.view.encrypted_custom_fields' => 1,
]);
$assetsUser->save();
$testUser = User::updateOrCreate(
['username' => 'testuser'],
[
'first_name' => 'Test',
'last_name' => 'User',
'password' => Hash::make('password'),
'activated' => 1,
]
);
$testUser->permissions = json_encode([
'self.checkout_assets' => 1,
'assets.view.requestable' => 1,
]);
$testUser->save();
\Storage::disk('public')->put('snipe-logo.png', file_get_contents(public_path('img/demo/snipe-logo.png')));
\Storage::disk('public')->put('snipe-logo-lg.png', file_get_contents(public_path('img/demo/snipe-logo-lg.png')));
+52 -63
View File
@@ -4,9 +4,6 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use function Laravel\Prompts\info;
use function Laravel\Prompts\select;
class Version extends Command
{
/**
@@ -14,7 +11,7 @@ class Version extends Command
*
* @var string
*/
protected $signature = 'version:update';
protected $signature = 'version:update {--branch=master} {--type=patch}';
/**
* The console command description.
@@ -40,40 +37,30 @@ class Version extends Command
*/
public function handle()
{
$use_branch = select(
label: 'Which branch?',
options: ['master', 'develop'],
default: 'develop',
);
$use_type = select(
label: 'Which release type?',
options: [
'hash' => 'Hash bump',
'patch' => 'Patch release',
'minor' => 'Minor release',
'major' => 'Major release',
'pre-patch' => 'Pre-patch release',
'pre-minor' => 'Pre-minor release',
'pre-major' => 'Pre-major release',
],
default: 'hash',
scroll: 7,
);
$use_branch = $this->option('branch');
$use_type = $this->option('type');
$git_branch = trim(shell_exec('git rev-parse --abbrev-ref HEAD'));
$build_version = trim(shell_exec('git rev-list --count '.$use_branch));
$versionFile = 'config/version.php';
$full_hash_version = str_replace("\n", '', shell_exec('git describe master --tags'));
$version = explode('-', $full_hash_version);
$app_version = $version[0];
$app_version = $current_app_version = $version[0];
$hash_version = (array_key_exists('2', $version)) ? $version[2] : '';
$prerelease_version = '';
if (array_key_exists('3', $version)) {
$prerelease_version = $version[1];
$hash_version = $version[3];
$this->line('Branch is: '.$use_branch);
$this->line('Type is: '.$use_type);
$this->line('Current version is: '.$full_hash_version);
if (count($version) == 3) {
$this->line('This does not look like an alpha/beta release.');
} else {
if (array_key_exists('3', $version)) {
$this->line('The current version looks like a beta release.');
$prerelease_version = $version[1];
$hash_version = $version[3];
}
}
$app_version_raw = explode('.', $app_version);
@@ -87,52 +74,54 @@ class Version extends Command
$patch = $app_version_raw[2];
}
if ($use_type === 'major') {
if ($use_type == 'major') {
$app_version = 'v'.($maj + 1).".$min.$patch";
} elseif ($use_type === 'minor') {
} elseif ($use_type == 'minor') {
$app_version = 'v'."$maj.".($min + 1).".$patch";
} elseif ($use_type === 'pre-patch') {
$app_version = 'v'."$maj.$min.".($patch + 1).'-pre';
} elseif ($use_type === 'pre-minor') {
$app_version = 'v'."$maj.".($min + 1).'.0-pre';
} elseif ($use_type === 'pre-major') {
$app_version = 'v'.($maj + 1).'.0.0-pre';
} elseif ($use_type === 'patch') {
} elseif ($use_type == 'pre') {
$pre_raw = str_replace('beta', '', $prerelease_version);
$pre_raw = str_replace('alpha', '', $pre_raw);
$pre_raw = str_ireplace('rc', '', $pre_raw);
$pre_raw = $pre_raw++;
$this->line('Setting the pre-release to '.$prerelease_version.'-'.$pre_raw);
$app_version = 'v'."$maj.".($min + 1).".$patch";
} elseif ($use_type == 'patch') {
$app_version = 'v'."$maj.$min.".($patch + 1);
// If nothing is passed, leave the version as it is, just increment the build
} else {
$app_version = 'v'."$maj.$min.".$patch;
}
if ($use_branch === 'develop' && ! str_ends_with($app_version, '-pre')) {
// Determine if this tag already exists, or if this prior to a release
$this->line('Running: git rev-parse master '.$current_app_version);
// $pre_release = trim(shell_exec('git rev-parse '.$use_branch.' '.$current_app_version.' 2>&1 1> /dev/null'));
if ($use_branch == 'develop') {
$app_version = $app_version.'-pre';
}
$full_hash_version = str_replace($version[0], $app_version, $full_hash_version);
$full_app_version = $app_version.' - build '.$build_version.'-'.$hash_version;
$content = <<<PHP
<?php
$array = var_export(
[
'app_version' => $app_version,
'full_app_version' => $full_app_version,
'build_version' => $build_version,
'prerelease_version' => $prerelease_version,
'hash_version' => $hash_version,
'full_hash' => $full_hash_version,
'branch' => $git_branch, ],
true
);
return [
'app_version' => '$app_version',
'full_app_version' => '$full_app_version',
'build_version' => '$build_version',
'prerelease_version' => '$prerelease_version',
'hash_version' => '$hash_version',
'full_hash' => '$full_hash_version',
'branch' => '$git_branch',
];
PHP;
// Construct our file content
$content = <<<CON
<?php
return $array;
CON;
// And finally write the file and output the current version
\File::put($versionFile, $content);
info('New version: '.$full_app_version.' ('.$git_branch.')');
info('Building JS/CSS assets...');
passthru('npm run prod', $exitCode);
if ($exitCode !== 0) {
$this->error('Asset build failed with exit code '.$exitCode);
} else {
info('Assets built successfully.');
}
$this->info('Setting NEW version: '.$full_app_version.' ('.$git_branch.')');
}
}
-3
View File
@@ -31,9 +31,6 @@ enum ActionType: string
case DeleteSeats = 'delete seats';
case AddSeats = 'add seats';
// Maintenances
case MaintenanceComplete = 'completed';
// File Uploads
case Uploaded = 'uploaded';
case UploadDeleted = 'upload deleted';
-7
View File
@@ -19,7 +19,6 @@ use Illuminate\Validation\ValidationException;
use Intervention\Image\Exception\NotSupportedException;
use JsonException;
use League\OAuth2\Server\Exception\OAuthServerException;
use Livewire\Exceptions\PublicPropertyNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
@@ -42,7 +41,6 @@ class Handler extends ExceptionHandler
JsonException::class,
SCIMException::class, // these generally don't need to be reported
InvalidFormatException::class,
PublicPropertyNotFoundException::class,
];
/**
@@ -73,11 +71,6 @@ class Handler extends ExceptionHandler
public function render($request, Throwable $e)
{
// Livewire tried to set a property that doesn't exist (e.g. stale browser state sending a bare "0" as a property name)
if ($e instanceof PublicPropertyNotFoundException) {
return response()->json(['message' => $e->getMessage()], 422);
}
// CSRF token mismatch error
if ($e instanceof TokenMismatchException) {
return redirect()->back()->with('error', trans('general.token_expired'));
-3
View File
@@ -78,7 +78,6 @@ class IconHelper
case 'angle-right':
return 'fas fa-angle-right';
case 'warning':
case 'alert':
return 'fas fa-exclamation-triangle';
case 'kits':
return 'fas fa-object-group';
@@ -127,7 +126,6 @@ class IconHelper
case 'dashboard':
return 'fas fa-tachometer-alt';
case 'info-circle':
case 'info':
return 'fas fa-info-circle';
case 'caret-right':
return 'fa fa-caret-right';
@@ -158,7 +156,6 @@ class IconHelper
case 'remote':
return 'fa-solid fa-house-laptop';
case 'more-info':
case 'help':
case 'support':
return 'far fa-life-ring';
case 'plus':
@@ -4,9 +4,9 @@ namespace App\Http\Controllers\Accessories;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller;
use App\Http\Requests\AccessoryCheckoutRequest;
use App\Http\Traits\CheckInOutTrait;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\CheckoutAcceptance;
@@ -18,7 +18,7 @@ use Illuminate\Http\Request;
class AccessoryCheckoutController extends Controller
{
use CheckInOutTrait;
use CheckInOutRequest;
/**
* Return the form to checkout an Accessory to a user.
@@ -149,9 +149,6 @@ class AcceptanceController extends Controller
$item = $acceptance->checkoutable_type::find($acceptance->checkoutable_id);
$username_slug = Str::slug($assignedUser->username);
$asset_tag_slug = ($item instanceof Asset && $item->asset_tag) ? '-'.Str::slug($item->asset_tag) : '';
// If signatures are required, make sure we have one
if ($requiresSignature) {
@@ -237,7 +234,7 @@ class AcceptanceController extends Controller
if ($request->input('asset_acceptance') === 'accepted') {
$pdf_filename = 'accepted-'.$username_slug.$asset_tag_slug.'-'.date('Y-m-d-h-i-s').'.pdf';
$pdf_filename = 'accepted-'.$acceptance->checkoutable_id.'-'.$acceptance->display_checkoutable_type.'-eula-'.date('Y-m-d-h-i-s').'.pdf';
// Generate the PDF content
$pdf_content = $acceptance->generateAcceptancePdf($data, $acceptance);
+2 -3
View File
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Helpers\Helper;
use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
@@ -16,9 +17,6 @@ class ActionlogController extends Controller
{
$filename = basename((string) $filename);
$actionlog = Actionlog::where('accept_signature', $filename)->with('item')->firstOrFail();
$this->authorize('view', $actionlog->item);
// PHP doesn't let you handle file not found errors well with
// file_get_contents, so we set the error reporting for just this class
error_reporting(0);
@@ -31,6 +29,7 @@ class ActionlogController extends Controller
return redirect()->away(Storage::disk($disk)->temporaryUrl($file, now()->addMinutes(5)));
default:
$this->authorize('view', Asset::class);
$file = config('app.private_uploads').'/signatures/'.$filename;
$filetype = Helper::checkUploadIsImage($file);
@@ -4,11 +4,11 @@ namespace App\Http\Controllers\Api;
use App\Events\CheckoutableCheckedOut;
use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller;
use App\Http\Requests\AccessoryCheckoutRequest;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\StoreAccessoryRequest;
use App\Http\Traits\CheckInOutTrait;
use App\Http\Transformers\AccessoriesTransformer;
use App\Http\Transformers\ActionlogsTransformer;
use App\Http\Transformers\SelectlistTransformer;
@@ -25,7 +25,7 @@ use Illuminate\Support\Facades\DB;
class AccessoriesController extends Controller
{
use CheckInOutTrait;
use CheckInOutRequest;
/**
* Display a listing of the resource.
@@ -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')));
}
@@ -405,7 +401,6 @@ class AccessoriesController extends Controller
*/
public function selectlist(Request $request)
{
$this->authorize('view.selectlists');
$accessories = Accessory::select([
'accessories.id',
@@ -590,7 +590,6 @@ class AssetsController extends Controller
*/
public function selectlist(Request $request): array
{
$this->authorize('view.selectlists');
$assets = Asset::select([
'assets.id',
@@ -603,11 +602,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')));
}
@@ -356,8 +356,6 @@ class ConsumablesController extends Controller
*/
public function selectlist(Request $request): array
{
$this->authorize('view.selectlists');
$consumables = Consumable::select([
'consumables.id',
'consumables.name',
@@ -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
*
@@ -435,8 +268,6 @@ class LicensesController extends Controller
*/
public function selectlist(Request $request): array
{
$this->authorize('view.selectlists');
$licenses = License::select([
'licenses.id',
'licenses.name',
@@ -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');
@@ -1,87 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\FilterRequest;
use App\Http\Transformers\MaintenanceTypesTransformer;
use App\Models\MaintenanceType;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class MaintenanceTypesController extends Controller
{
public function index(FilterRequest $request): JsonResponse|array
{
$this->authorize('view', MaintenanceType::class);
$types = MaintenanceType::select(['id', 'name', 'created_at', 'updated_at', 'deleted_at']);
if ($request->input('deleted') == 'true') {
$types->onlyTrashed();
}
if ($request->filled('search')) {
$types->where('name', 'LIKE', '%'.$request->input('search').'%');
}
if ($request->filled('name')) {
$types->where('name', '=', $request->input('name'));
}
$offset = ($request->input('offset') > $types->count()) ? $types->count() : abs($request->input('offset'));
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), ['id', 'name', 'created_at', 'updated_at']) ? $request->input('sort') : 'name';
$total = $types->count();
$types = $types->orderBy($sort, $order)->skip($offset)->take($limit)->get();
return (new MaintenanceTypesTransformer)->transformMaintenanceTypes($types, $total);
}
public function show(MaintenanceType $maintenanceType): JsonResponse|array
{
$this->authorize('view', $maintenanceType);
return (new MaintenanceTypesTransformer)->transformMaintenanceType($maintenanceType);
}
public function store(Request $request): JsonResponse
{
$this->authorize('create', MaintenanceType::class);
$type = new MaintenanceType;
$type->name = $request->input('name');
$type->created_by = auth()->id();
if ($type->save()) {
return response()->json(Helper::formatStandardApiResponse('success', (new MaintenanceTypesTransformer)->transformMaintenanceType($type), trans('admin/maintenance_types/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $type->getErrors()));
}
public function update(Request $request, MaintenanceType $maintenanceType): JsonResponse
{
$this->authorize('update', $maintenanceType);
$maintenanceType->name = $request->input('name');
if ($maintenanceType->save()) {
return response()->json(Helper::formatStandardApiResponse('success', (new MaintenanceTypesTransformer)->transformMaintenanceType($maintenanceType), trans('admin/maintenance_types/message.update.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenanceType->getErrors()));
}
public function destroy(MaintenanceType $maintenanceType): JsonResponse
{
$this->authorize('delete', $maintenanceType);
$maintenanceType->delete();
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/maintenance_types/message.delete.success')));
}
}
@@ -2,19 +2,15 @@
namespace App\Http\Controllers\Api;
use App\Enums\ActionType;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\FilterRequest;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Transformers\ActionlogsTransformer;
use App\Http\Transformers\MaintenancesTransformer;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Maintenance;
use App\Models\Setting;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -42,8 +38,7 @@ class MaintenancesController extends Controller
$this->authorize('view', Asset::class);
$maintenances = Maintenance::select('maintenances.*')
->whereHas('asset')
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.status', 'adminuser', 'asset.assignedTo', 'maintenanceType', 'responsibleParty', 'completedByUser');
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.status', 'adminuser', 'asset.assignedTo');
// This invokes the Searchable model trait scopeTextSearch and will handle input by search or by advanced search filter
if ($request->filled('filter') || $request->filled('search')) {
@@ -66,39 +61,8 @@ class MaintenancesController extends Controller
$maintenances->where('maintenances.url', '=', $request->input('url'));
}
if ($request->filled('maintenance_type')) {
$maintenances->where('maintenance_type', '=', $request->input('maintenance_type'));
}
if ($request->filled('maintenance_type_id')) {
$maintenances->where('maintenance_type_id', '=', $request->input('maintenance_type_id'));
}
if ($request->filled('responsible_party_id')) {
$maintenances->where('responsible_party_id', '=', $request->input('responsible_party_id'));
}
if ($request->filled('completed')) {
if ($request->input('completed') === 'true') {
$maintenances->completed();
} else {
$maintenances->active();
}
}
if ($request->filled('upcoming_status')) {
$settings = Setting::getSettings();
switch ($request->input('upcoming_status')) {
case 'due':
$maintenances->dueForCompletion($settings);
break;
case 'overdue':
$maintenances->overdueForCompletion();
break;
case 'due-or-overdue':
$maintenances->dueOrOverdueForCompletion($settings);
break;
}
if ($request->filled('asset_maintenance_type')) {
$maintenances->where('asset_maintenance_type', '=', $request->input('asset_maintenance_type'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
@@ -109,10 +73,10 @@ class MaintenancesController extends Controller
'id',
'name',
'asset_maintenance_time',
'asset_maintenance_type',
'cost',
'start_date',
'completion_date',
'completed_at',
'notes',
'asset_tag',
'asset_name',
@@ -124,7 +88,6 @@ class MaintenancesController extends Controller
'status_label',
'model',
'model_number',
'maintenance_type',
];
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
@@ -132,37 +95,31 @@ class MaintenancesController extends Controller
switch ($sort) {
case 'created_by':
$maintenances = $maintenances->orderByCreatedBy($order);
$maintenances = $maintenances->OrderByCreatedBy($order);
break;
case 'supplier':
$maintenances = $maintenances->orderBySupplier($order);
$maintenances = $maintenances->OrderBySupplier($order);
break;
case 'asset_tag':
$maintenances = $maintenances->orderByTag($order);
$maintenances = $maintenances->OrderByTag($order);
break;
case 'asset_name':
$maintenances = $maintenances->orderByAssetName($order);
$maintenances = $maintenances->OrderByAssetName($order);
break;
case 'model':
$maintenances = $maintenances->orderByAssetModelName($order);
$maintenances = $maintenances->OrderByAssetModelName($order);
break;
case 'model_number':
$maintenances = $maintenances->orderByAssetModelNumber($order);
$maintenances = $maintenances->OrderByAssetModelNumber($order);
break;
case 'serial':
$maintenances = $maintenances->orderByAssetSerial($order);
$maintenances = $maintenances->OrderByAssetSerial($order);
break;
case 'location':
$maintenances = $maintenances->orderLocationName($order);
$maintenances = $maintenances->OrderLocationName($order);
break;
case 'status_label':
$maintenances = $maintenances->orderStatusName($order);
break;
case 'maintenance_type':
$maintenances = $maintenances->orderByMaintenanceType($order);
break;
case 'completed_at':
$maintenances = $maintenances->orderByCompletedAt($order);
$maintenances = $maintenances->OrderStatusName($order);
break;
default:
$maintenances = $maintenances->orderBy($sort, $order);
@@ -195,60 +152,19 @@ class MaintenancesController extends Controller
{
$this->authorize('update', Asset::class);
$isBulk = $request->has('asset_ids');
$assetIds = $isBulk
? array_values(array_filter((array) $request->input('asset_ids')))
: [$request->input('asset_id')];
// create a new model instance
$maintenance = new Maintenance;
$maintenance->fill($request->all());
$maintenance->created_by = auth()->id();
$maintenance = $request->handleImages($maintenance);
// Was the asset maintenance created?
if ($maintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.create.success')));
$created = new EloquentCollection;
$errors = [];
foreach ($assetIds as $assetId) {
$asset = Asset::find($assetId);
if (! $asset) {
$errors[] = trans('general.item_not_found', ['item_type' => trans('general.asset'), 'id' => $assetId]);
continue;
}
if (! Company::isCurrentUserHasAccess($asset)) {
$errors[] = trans('general.action_permission_denied', ['item_type' => trans('general.asset'), 'id' => $assetId, 'action' => trans('general.create')]);
continue;
}
$maintenance = new Maintenance;
$maintenance->fill($request->except(['asset_id', 'asset_ids']));
$maintenance->asset_id = $assetId;
$maintenance->created_by = auth()->id();
$request->handleImages($maintenance);
if ($maintenance->save()) {
$created->push($maintenance->fresh());
} else {
$errors[] = $maintenance->getErrors();
}
}
if ($isBulk) {
if ($created->isEmpty()) {
return response()->json(Helper::formatStandardApiResponse('error', null, count($errors) === 1 ? $errors[0] : $errors));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
return response()->json(Helper::formatStandardApiResponse(
'success',
(new MaintenancesTransformer)->transformMaintenances($created, $created->count()),
trans('admin/maintenances/message.create.success')
));
}
// Single asset_id path — backward compatible response shape
if ($created->isNotEmpty()) {
return response()->json(Helper::formatStandardApiResponse('success', $created->first(), trans('admin/maintenances/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, ! empty($errors) ? $errors[0] : null));
}
/**
@@ -339,35 +255,6 @@ class MaintenancesController extends Controller
}
public function complete(Request $request, Maintenance $maintenance): JsonResponse
{
$this->authorize('update', Asset::class);
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/maintenances/general.maintenance'), 'id' => $maintenance->id, 'action' => trans('admin/maintenances/form.mark_complete')])));
}
if ($maintenance->completed_at) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/maintenances/form.already_complete')));
}
$maintenance->completed_at = now();
$maintenance->completed_by = auth()->id();
$maintenance->asset_maintenance_time = (int) $maintenance->created_at->diffInDays(now(), true);
$maintenance->saveQuietly();
$logAction = new Actionlog;
$logAction->item_type = Maintenance::class;
$logAction->item_id = $maintenance->id;
$logAction->target_type = Asset::class;
$logAction->target_id = $maintenance->asset_id;
$logAction->created_by = auth()->id();
$logAction->note = $request->input('note');
$logAction->logaction(ActionType::MaintenanceComplete);
return response()->json(Helper::formatStandardApiResponse('success', (new MaintenancesTransformer)->transformMaintenance($maintenance->fresh()), trans('admin/maintenances/message.complete.success')));
}
public function history(Request $request, Maintenance $maintenance): JsonResponse|array
{
$this->authorize('history', $maintenance);
@@ -379,50 +266,4 @@ class MaintenancesController extends Controller
return response()->json((new ActionlogsTransformer)->transformActionlogs($history, $total), 200, ['Content-Type' => 'application/json;charset=utf8'], JSON_UNESCAPED_UNICODE);
}
public function notesIndex(Maintenance $maintenance): JsonResponse
{
$this->authorize('journal', $maintenance);
$notes = Actionlog::with('user:id,username')
->where('item_type', Maintenance::class)
->where('item_id', $maintenance->id)
->where('action_type', 'note added')
->orderBy('created_at', 'desc')
->get(['id', 'created_at', 'note', 'created_by', 'item_id', 'item_type', 'action_type']);
$notesArray = $notes->map(fn ($note) => [
'id' => $note->id,
'created_at' => $note->created_at,
'note' => $note->note,
'created_by' => $note->created_by,
'username' => $note->user?->username,
'item_id' => $note->item_id,
'item_type' => $note->item_type,
'action_type' => $note->action_type,
]);
return response()->json(Helper::formatStandardApiResponse('success', ['notes' => $notesArray, 'maintenance_id' => $maintenance->id]));
}
public function notesStore(Request $request, Maintenance $maintenance): JsonResponse
{
$this->authorize('update', $maintenance);
if (! $request->filled('note')) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('validation.required', ['attribute' => 'note'])), 422);
}
$logaction = new Actionlog;
$logaction->item_type = Maintenance::class;
$logaction->created_by = auth()->id();
$logaction->item_id = $maintenance->id;
$logaction->note = $request->input('note');
if ($logaction->logaction('note added')) {
return response()->json(Helper::formatStandardApiResponse('success', ['note' => $logaction->note, 'item_id' => $maintenance->id], trans('general.note_added')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, 'Something went wrong'), 500);
}
}
@@ -36,18 +36,18 @@ class ReportsController extends Controller
// then they shouldn't be able to see the activity log for that item or target,
// but if they have the general activity view permission,
// then they can see all activity logs regardless of the item or target.
if ((! Gate::allows('activity.view')) && (($request->filled('target_type') && $request->filled('target_id')) || ($request->filled('item_type') && $request->filled('item_id')))) {
if ((! Gate::allows('activity.view')) && (($request->filled('target_type')) && ($request->filled('target_id'))) || (($request->filled('item_type')) && ($request->filled('item_id')))) {
if (($request->filled('target_type')) && ($request->filled('target_id'))) {
$targetClass = Helper::normalizeFullModelName(request()->input('target_type'));
$target = $targetClass::withTrashed()->find(request()->input('target_id'));
$this->authorize('view', $target ?? $targetClass);
$target = Helper::normalizeFullModelName(request()->input('target_type'));
$target::find(request()->input('target_id'))?->withTrashed();
$this->authorize('view', $target);
}
if (($request->filled('item_type')) && ($request->filled('item_id'))) {
$itemClass = Helper::normalizeFullModelName(request()->input('item_type'));
$item = $itemClass::withTrashed()->find(request()->input('item_id'));
$this->authorize('view', $item ?? $itemClass);
$item = Helper::normalizeFullModelName(request()->input('item_type'));
$item::find(request()->input('item_id'))?->withTrashed();
$this->authorize('view', $item);
}
} else {
+8 -27
View File
@@ -22,7 +22,6 @@ use App\Models\Asset;
use App\Models\Company;
use App\Models\Consumable;
use App\Models\License;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\CurrentInventory;
use App\Notifications\WelcomeNotification;
@@ -52,6 +51,7 @@ class UsersController extends Controller
'users.address',
'users.avatar',
'users.city',
'users.company_id',
'users.country',
'users.created_by',
'users.created_at',
@@ -89,7 +89,7 @@ class UsersController extends Controller
])->with('manager')
->with('groups')
->with('userloc')
->with('companies')
->with('company')
->with('department')
->with('createdBy')
->withCount([
@@ -191,7 +191,7 @@ class UsersController extends Controller
}
if ($request->filled('company_id')) {
$users = $users->whereHas('companies', fn ($q) => $q->where('companies.id', $request->input('company_id')));
$users = $users->where('users.company_id', '=', $request->input('company_id'));
}
if ($request->filled('phone')) {
@@ -380,8 +380,6 @@ class UsersController extends Controller
*/
public function selectlist(Request $request): array
{
$this->authorize('view.selectlists');
$users = User::select(
[
'users.id',
@@ -396,13 +394,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 +441,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 +486,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')));
}
@@ -583,12 +569,15 @@ class UsersController extends Controller
requestedPermissions: NormalizePermissionsPayloadAction::run($request->input('permissions')),
authenticatedUser: $authenticatedUser,
originalPermissions: NormalizePermissionsPayloadAction::run($user->decodePermissions()),
targetUser: $user,
));
}
}
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 +606,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')));
}
@@ -135,16 +135,12 @@ class AssetCheckinController extends Controller
$asset->location_id = $asset->rtd_location_id;
if ($request->has('location_id')) {
if ($request->filled('location_id')) {
Log::debug('NEW Location ID: '.$request->input('location_id'));
$asset->location_id = $request->input('location_id');
if ($request->input('update_default_location') == 0) {
$asset->rtd_location_id = $request->input('location_id');
}
} else {
// Explicitly submitted as empty — clear the location
$asset->location_id = null;
if ($request->filled('location_id')) {
Log::debug('NEW Location ID: '.$request->input('location_id'));
$asset->location_id = $request->input('location_id');
if ($request->input('update_default_location') == 0) {
$asset->rtd_location_id = $request->input('location_id');
}
}
@@ -4,9 +4,9 @@ namespace App\Http\Controllers\Assets;
use App\Exceptions\CheckoutNotAllowed;
use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssetCheckoutRequest;
use App\Http\Traits\CheckInOutTrait;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
use App\Models\Setting;
@@ -17,7 +17,7 @@ use Illuminate\Http\RedirectResponse;
class AssetCheckoutController extends Controller
{
use CheckInOutTrait;
use CheckInOutRequest;
/**
* Returns a view that presents a form to check an asset out to a
@@ -511,7 +511,7 @@ class AssetsController extends Controller
// Validate required serial based on model setting
if ($model && $model->require_serial === 1 && empty($serial[1])) {
return Helper::getRedirectOption($request, $asset->id, 'Assets')
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
->with('warning', trans('admin/hardware/form.serial_required_post_model_update', [
'asset_model' => $model->name,
]));
@@ -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);
@@ -2,24 +2,20 @@
namespace App\Http\Controllers\Assets;
use App\Events\CheckoutableCheckedIn;
use App\Events\CheckoutablesCheckedOutInBulk;
use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssetCheckoutRequest;
use App\Http\Traits\CheckInOutTrait;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\CheckoutAcceptance;
use App\Models\Company;
use App\Models\CustomField;
use App\Models\LicenseSeat;
use App\Models\Setting;
use App\Models\Statuslabel;
use App\View\Label;
use Carbon\Carbon;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -31,7 +27,7 @@ use Illuminate\Support\Facades\Log;
class BulkAssetsController extends Controller
{
use CheckInOutTrait;
use CheckInOutRequest;
/**
* Display the bulk edit page.
@@ -77,16 +73,6 @@ class BulkAssetsController extends Controller
return redirect()->route('hardware.bulkcheckout.show');
}
if ($request->input('bulk_actions') === 'checkin') {
$referer = request()->headers->get('referer');
if ($referer && parse_url($referer, PHP_URL_HOST) === parse_url(config('app.url'), PHP_URL_HOST)) {
redirect()->setIntendedUrl($referer);
}
$request->session()->flashInput(['selected_assets' => $asset_ids]);
return redirect()->route('hardware.bulkcheckin.show');
}
if ($request->input('bulk_actions') === 'maintenance') {
$request->session()->flashInput(['selected_assets' => $asset_ids]);
@@ -773,112 +759,6 @@ class BulkAssetsController extends Controller
}
/**
* Show Bulk Checkin Page
*/
public function showCheckin(): View
{
$this->authorize('checkin', Asset::class);
$notAssigned = collect();
if (old('selected_assets') && is_array(old('selected_assets'))) {
$assets = Asset::findMany(old('selected_assets'));
[$assigned, $notAssigned] = $assets->partition(function (Asset $asset) {
return $asset->assigned_to;
});
session()->flashInput(['selected_assets' => $assigned->pluck('id')->values()->toArray()]);
}
$do_not_change = ['' => trans('general.do_not_change')];
$status_label_list = $do_not_change + Helper::statusLabelList();
return view('hardware/bulk-checkin', [
'statusLabel_list' => $status_label_list,
'removed_assets' => $notAssigned,
]);
}
/**
* Process Multiple Checkin Request
*/
public function storeCheckin(Request $request): RedirectResponse
{
$this->authorize('checkin', Asset::class);
if (! is_array($request->input('selected_assets'))) {
return redirect()->route('hardware.bulkcheckin.show')->withInput()->with('error', trans('admin/hardware/message.multi-checkin.no_assets_selected'));
}
$asset_ids = array_filter($request->input('selected_assets'));
$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')) {
$checkin_at = $request->input('checkin_at');
}
$errors = [];
$admin = auth()->user();
DB::transaction(function () use ($assets, $admin, $checkin_at, $request, &$errors) {
foreach ($assets as $asset) {
$this->authorize('checkin', $asset);
if (is_null($asset->assignedTo)) {
continue;
}
$target = $asset->assignedTo;
$originalValues = $asset->getRawOriginal();
$asset->expected_checkin = null;
$asset->assignedTo()->disassociate($asset);
$asset->accepted = null;
if ($request->filled('status_id')) {
$asset->status_id = $request->input('status_id');
}
$asset->location_id = $asset->rtd_location_id;
$asset->last_checkin = $checkin_at;
if ($request->boolean('checkin_licenses')) {
$asset->licenseseats->each(function (LicenseSeat $seat) {
$seat->update(['assigned_to' => null]);
});
}
CheckoutAcceptance::pending()->whereHasMorph('checkoutable', [Asset::class], function (Builder $query) use ($asset) {
$query->where('id', $asset->id);
})->get()->each->delete();
if ($asset->save()) {
if ($request->boolean('checkin_child_assets')) {
Asset::where('assigned_type', Asset::class)
->where('assigned_to', $asset->id)
->update(['location_id' => $asset->location_id]);
}
event(new CheckoutableCheckedIn($asset, $target, $admin, $request->input('note'), $checkin_at, $originalValues));
} else {
$errors = array_merge_recursive($errors, $asset->getErrors()->toArray());
}
}
});
if (! $errors) {
return redirect()->intended(route('hardware.index'))->with('success', trans_choice('admin/hardware/message.multi-checkin.success', count($asset_ids)));
}
return redirect()->route('hardware.bulkcheckin.show')->withInput()
->with('error', trans_choice('admin/hardware/message.multi-checkin.error', count($asset_ids)))
->withErrors($errors);
}
public function restore(Request $request): RedirectResponse
{
$this->authorize('update', Asset::class);
@@ -1,13 +1,13 @@
<?php
namespace App\Http\Traits;
namespace App\Http\Controllers;
use App\Models\Asset;
use App\Models\Location;
use App\Models\SnipeModel;
use App\Models\User;
trait CheckInOutTrait
trait CheckInOutRequest
{
/**
* Find target for checkout
@@ -4,8 +4,7 @@ namespace App\Http\Controllers\Components;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreComponentRequest;
use App\Http\Requests\UpdateComponentRequest;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Company;
use App\Models\Component;
use Illuminate\Auth\Access\AuthorizationException;
@@ -13,6 +12,7 @@ use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
/**
* This class controls all actions related to Components for
@@ -74,7 +74,7 @@ class ComponentsController extends Controller
*
* @throws AuthorizationException
*/
public function store(StoreComponentRequest $request)
public function store(ImageUploadRequest $request)
{
$this->authorize('create', Component::class);
$component = new Component;
@@ -148,10 +148,21 @@ class ComponentsController extends Controller
*
* @since [v3.0]
*/
public function update(UpdateComponentRequest $request, Component $component)
public function update(ImageUploadRequest $request, Component $component)
{
$min = $component->numCheckedOut();
$validator = Validator::make($request->all(), [
'qty' => "required|numeric|min:$min",
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput();
}
$this->authorize('update', $component);
// Update the component data
$component->name = $request->input('name');
$component->category_id = $request->input('category_id');
@@ -54,7 +54,6 @@ class GoogleAuthController extends Controller
Log::debug('Google user '.$socialUser->getEmail().' found in Snipe-IT');
$user->update([
'avatar' => $socialUser->avatar,
'last_login' => \Carbon::now(),
]);
Auth::login($user, true);
@@ -2,8 +2,8 @@
namespace App\Http\Controllers\Kits;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller;
use App\Http\Traits\CheckInOutTrait;
use App\Models\Asset;
use App\Models\PredefinedKit;
use App\Models\User;
@@ -23,7 +23,7 @@ class CheckoutKitController extends Controller
{
public $kitService;
use CheckInOutTrait;
use CheckInOutRequest;
public function __construct(PredefinedKitCheckoutService $kitService)
{
@@ -17,7 +17,7 @@ class BulkLicensesController extends Controller
$errors = [];
$success_count = 0;
foreach ($request->input('ids', []) as $id) {
foreach ($request->ids as $id) {
$license = License::find($id);
if (is_null($license)) {
@@ -13,7 +13,6 @@ use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
@@ -128,45 +127,10 @@ class LicenseCheckinController extends Controller
* @see LicenseCheckinController::create() method that provides the form view
* @since [v6.1.1]
*
* @return RedirectResponse
*
* @throws AuthorizationException
*/
public function bulkCheckinSelected(Request $request): RedirectResponse
{
$this->authorize('checkin', License::class);
$seatIds = $request->input('ids', []);
if (empty($seatIds)) {
return redirect()->back()->with('warning', trans('admin/licenses/general.bulk.checkin_selected.no_seats_selected'));
}
$seats = LicenseSeat::whereIn('id', $seatIds)
->where(function ($query) {
$query->whereNotNull('assigned_to')->orWhereNotNull('asset_id');
})
->with('license', 'user', 'asset')
->get();
$count = 0;
foreach ($seats as $seat) {
if (! $seat->license || ! Gate::allows('checkin', $seat->license)) {
continue;
}
$target = $seat->user ?? $seat->asset;
$seat->assigned_to = null;
$seat->asset_id = null;
if (! $seat->license->reassignable) {
$seat->unreassignable_seat = true;
}
if ($seat->save()) {
event(new CheckoutableCheckedIn($seat, $target, auth()->user(), null));
$count++;
}
}
return redirect()->back()->with('success', trans_choice('admin/licenses/general.bulk.checkin_selected.success', $count, ['count' => $count]));
}
public function bulkCheckin(Request $request, $licenseId)
{
@@ -15,7 +15,6 @@ use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class LicenseCheckoutController extends Controller
@@ -95,31 +94,23 @@ class LicenseCheckoutController extends Controller
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.license_is_inactive'));
}
$licenseSeat = null;
$checkoutTarget = null;
DB::transaction(function () use ($request, $license, $seatId, &$licenseSeat, &$checkoutTarget): void {
$licenseSeat = $this->findLicenseSeatToCheckout($license, $seatId, lock: true);
$licenseSeat->created_by = auth()->id();
$licenseSeat->notes = $request->input('notes');
if ($request->filled('asset_id')) {
$checkoutTarget = $this->checkoutToAsset($licenseSeat);
} elseif ($request->filled('assigned_to')) {
$checkoutTarget = $this->checkoutToUser($licenseSeat);
}
});
$licenseSeat = $this->findLicenseSeatToCheckout($license, $seatId);
$licenseSeat->created_by = auth()->id();
$licenseSeat->notes = $request->input('notes');
if ($request->filled('asset_id')) {
session()->put(['checkout_to_type' => 'asset']);
$checkoutTarget = $this->checkoutToAsset($licenseSeat);
$request->request->add(['assigned_asset' => $checkoutTarget->id]);
session()->put([
'redirect_option' => $request->input('redirect_option'),
'checkout_to_type' => 'asset',
'sign_in_place' => $request->boolean('sign_in_place'),
]);
} elseif ($request->filled('assigned_to')) {
session()->put(['checkout_to_type' => 'user']);
$checkoutTarget = $this->checkoutToUser($licenseSeat);
$request->request->add(['assigned_user' => $checkoutTarget->id]);
session()->put([
'redirect_option' => $request->input('redirect_option'),
@@ -165,11 +156,9 @@ class LicenseCheckoutController extends Controller
return redirect()->route('licenses.index')->with('error', trans('Something went wrong handling this checkout.'));
}
protected function findLicenseSeatToCheckout($license, $seatId, bool $lock = false)
protected function findLicenseSeatToCheckout($license, $seatId)
{
$licenseSeat = $seatId
? LicenseSeat::where('id', $seatId)->when($lock, fn ($q) => $q->lockForUpdate())->first()
: $license->freeSeat(lock: $lock);
$licenseSeat = LicenseSeat::find($seatId) ?? $license->freeSeat();
if (! $licenseSeat) {
if ($seatId) {
+2 -2
View File
@@ -277,7 +277,7 @@ class LocationsController extends Controller
->with('assignedAssets', $location->assignedAssets)
->with('accessories', $location->accessories)
->with('assignedAccessories', $location->assignedAccessories)
->with('users', $location->users()->with('companies')->get())
->with('users', $location->users)
->with('location', $location)
->with('consumables', $location->consumables)
->with('components', $location->components)
@@ -297,7 +297,7 @@ class LocationsController extends Controller
->with('assignedAssets', $location->assignedAssets)
->with('accessories', $location->accessories)
->with('assignedAccessories', $location->assignedAccessories)
->with('users', $location->users()->with('companies')->get())
->with('users', $location->users)
->with('location', $location)
->with('consumables', $location->consumables)
->with('components', $location->components)
@@ -1,72 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\MaintenanceType;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class MaintenanceTypesController extends Controller
{
public function index(): View
{
$this->authorize('index', MaintenanceType::class);
return view('maintenance-types.index');
}
public function create(): View
{
$this->authorize('create', MaintenanceType::class);
return view('maintenance-types.edit')->with('item', new MaintenanceType);
}
public function store(Request $request): RedirectResponse
{
$this->authorize('create', MaintenanceType::class);
$type = new MaintenanceType;
$type->name = $request->input('name');
$type->created_by = auth()->id();
if ($type->save()) {
return redirect()->route('maintenance-types.index')
->with('success', trans('admin/maintenance_types/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($type->getErrors());
}
public function edit(MaintenanceType $maintenanceType): View
{
$this->authorize('update', $maintenanceType);
return view('maintenance-types.edit')->with('item', $maintenanceType);
}
public function update(Request $request, MaintenanceType $maintenanceType): RedirectResponse
{
$this->authorize('update', $maintenanceType);
$maintenanceType->name = $request->input('name');
if ($maintenanceType->save()) {
return redirect()->route('maintenance-types.index')
->with('success', trans('admin/maintenance_types/message.update.success'));
}
return redirect()->back()->withInput()->withErrors($maintenanceType->getErrors());
}
public function destroy(MaintenanceType $maintenanceType): RedirectResponse
{
$this->authorize('delete', $maintenanceType);
$maintenanceType->delete();
return redirect()->route('maintenance-types.index')
->with('success', trans('admin/maintenance_types/message.delete.success'));
}
}
+28 -44
View File
@@ -2,14 +2,11 @@
namespace App\Http\Controllers;
use App\Enums\ActionType;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Maintenance;
use App\Models\MaintenanceType;
use Carbon\Carbon;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -60,7 +57,6 @@ class MaintenancesController extends Controller
return view('maintenances/edit')
->with('maintenanceType', Maintenance::getImprovementOptions())
->with('maintenanceTypes', MaintenanceType::orderBy('name')->get())
->with('asset', $asset)
->with('item', new Maintenance);
}
@@ -86,10 +82,6 @@ class MaintenancesController extends Controller
// Loop through the selected assets
foreach ($assets as $asset) {
if (! Company::isCurrentUserHasAccess($asset)) {
continue;
}
$maintenance = new Maintenance;
$maintenance->supplier_id = $request->input('supplier_id');
$maintenance->is_warranty = $request->input('is_warranty');
@@ -100,13 +92,20 @@ class MaintenancesController extends Controller
// Save the asset maintenance data
$maintenance->asset_id = $asset->id;
$maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$maintenance->maintenance_type_id = $request->input('maintenance_type_id');
$maintenance->name = $request->input('name');
$maintenance->start_date = $request->input('start_date');
$maintenance->completion_date = $request->input('completion_date');
$maintenance->responsible_party_id = $request->input('responsible_party_id') ?: auth()->id();
$maintenance->created_by = auth()->id();
if (($maintenance->completion_date !== null)
&& ($maintenance->start_date !== '')
&& ($maintenance->start_date !== '0000-00-00')
) {
$startDate = Carbon::parse($maintenance->start_date);
$completionDate = Carbon::parse($maintenance->completion_date);
$maintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
}
$request->handleImages($maintenance);
// Was the asset maintenance created?
@@ -142,7 +141,6 @@ class MaintenancesController extends Controller
->with('selected_assets', $maintenance->asset->pluck('id')->toArray())
->with('asset_ids', request()->input('asset_ids', []))
->with('maintenanceType', Maintenance::getImprovementOptions())
->with('maintenanceTypes', MaintenanceType::orderBy('name')->get())
->with('item', $maintenance);
}
@@ -171,12 +169,28 @@ class MaintenancesController extends Controller
$maintenance->cost = $request->input('cost');
$maintenance->notes = $request->input('notes');
$maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$maintenance->maintenance_type_id = $request->input('maintenance_type_id');
$maintenance->name = $request->input('name');
$maintenance->start_date = $request->input('start_date');
$maintenance->completion_date = $request->input('completion_date');
$maintenance->responsible_party_id = $request->input('responsible_party_id');
$maintenance->url = $request->input('url');
// Todo - put this in a getter/setter?
if (($maintenance->completion_date == null)) {
if (($maintenance->asset_maintenance_time !== 0)
|| (! is_null($maintenance->asset_maintenance_time))
) {
$maintenance->asset_maintenance_time = null;
}
}
if (($maintenance->completion_date !== null)
&& ($maintenance->start_date !== '')
&& ($maintenance->start_date !== '0000-00-00')
) {
$startDate = Carbon::parse($maintenance->start_date);
$completionDate = Carbon::parse($maintenance->completion_date);
$maintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
}
$request->handleImages($maintenance);
if ($maintenance->save()) {
@@ -239,36 +253,6 @@ class MaintenancesController extends Controller
)->validate();
}
/**
* Mark a maintenance record as complete, logging who completed it and when.
*/
public function complete(Request $request, Maintenance $maintenance): RedirectResponse
{
$this->authorize('update', $maintenance->asset);
if ($maintenance->completed_at) {
return redirect()->back()
->with('warning', trans('admin/maintenances/form.already_complete'));
}
$maintenance->completed_at = now();
$maintenance->completed_by = auth()->id();
$maintenance->asset_maintenance_time = (int) $maintenance->created_at->diffInDays(now(), true);
$maintenance->saveQuietly();
$logAction = new Actionlog;
$logAction->item_type = Maintenance::class;
$logAction->item_id = $maintenance->id;
$logAction->target_type = Asset::class;
$logAction->target_id = $maintenance->asset_id;
$logAction->created_by = auth()->id();
$logAction->note = $request->input('note');
$logAction->logaction(ActionType::MaintenanceComplete);
return redirect()->back()
->with('success', trans('admin/maintenances/message.complete.success'));
}
/**
* Delete an asset maintenance
*
-1
View File
@@ -30,7 +30,6 @@ class ModalController extends Controller
'kit-consumable',
'kit-accessory',
'location',
'maintenance-type',
'manufacturer',
'model',
'statuslabel',
+9 -14
View File
@@ -4,15 +4,13 @@ namespace App\Http\Controllers;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Maintenance;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
class NotesController extends Controller
{
public function store(Request $request): RedirectResponse
public function store(Request $request)
{
$this->authorize('update', Asset::class);
@@ -21,19 +19,13 @@ class NotesController extends Controller
'note' => 'required|string|max:50000',
'type' => [
'required',
Rule::in(['asset', 'maintenance']),
Rule::in(['asset']),
],
]);
if ($validated['type'] === 'maintenance') {
$item = Maintenance::findOrFail($validated['id']);
$this->authorize('update', $item->asset);
$redirect = redirect()->route('maintenances.show', $validated['id']);
} else {
$item = Asset::findOrFail($validated['id']);
$this->authorize('update', $item);
$redirect = redirect()->route('hardware.show', $validated['id']);
}
$item = Asset::findOrFail($validated['id']);
$this->authorize('update', $item);
$logaction = new Actionlog;
$logaction->item_id = $item->id;
@@ -42,6 +34,9 @@ class NotesController extends Controller
$logaction->created_by = Auth::id();
$logaction->logaction('note added');
return $redirect->withFragment('notes')->with('success', trans('general.note_added'));
return redirect()
->route('hardware.show', $validated['id'])
->withFragment('history')
->with('success', trans('general.note_added'));
}
}
+1 -4
View File
@@ -424,7 +424,7 @@ class ReportsController extends Controller
$row[] = $license->remaincount();
$row[] = $license->expiration_date;
$row[] = $license->purchase_date;
$row[] = ($license->depreciation != '') ? e($license->depreciation->name) : '';
$row[] = ($license->depreciation != '') ? '' : e($license->depreciation->name);
$row[] = '"'.Helper::formatCurrencyOutput($license->purchase_cost).'"';
$rows[] = implode(',', $row);
@@ -1236,9 +1236,6 @@ class ReportsController extends Controller
public function getAssetAcceptanceReport($deleted = false): View
{
$this->authorize('reports.view');
$this->disableDebugbar();
$showDeleted = $deleted == 'deleted';
$query = CheckoutAcceptance::Pending()
@@ -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,8 +168,11 @@ class BulkUsersController extends Controller
$this->conditionallyAddItem('location_id')
->conditionallyAddItem('department_id')
->conditionallyAddItem('company_id')
->conditionallyAddItem('locale')
->conditionallyAddItem('remote')
->conditionallyAddItem('ldap_import')
->conditionallyAddItem('activated')
->conditionallyAddItem('display_name')
->conditionallyAddItem('start_date')
->conditionallyAddItem('end_date')
@@ -200,7 +202,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,37 +235,11 @@ 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';
// Only sync groups if groups were selected
if ($request->filled('groups')) {
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) {
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
$authFieldUpdate = [];
if ($request->filled('activated')) {
$authFieldUpdate['activated'] = $request->input('activated');
}
if ($request->filled('ldap_import')) {
$authFieldUpdate['ldap_import'] = $request->input('ldap_import');
}
if (! empty($authFieldUpdate)) {
$user->update($authFieldUpdate);
}
if ($request->filled('groups') && auth()->user()->isSuperUser()) {
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
$user->groups()->sync($request->input('groups'));
}
}
@@ -422,7 +398,7 @@ class BulkUsersController extends Controller
*/
public function merge(Request $request)
{
$this->authorize('delete', User::class);
$this->authorize('update', User::class);
if (config('app.lock_passwords')) {
return redirect()->route('users.index')->with('error', trans('general.feature_disabled'));
@@ -443,10 +419,6 @@ class BulkUsersController extends Controller
// Walk users
foreach ($users_to_merge as $user_to_merge) {
if (! auth()->user()->can('canEditAuthFields', $user_to_merge) || ! auth()->user()->can('editableOnDemo')) {
return redirect()->route('users.index')->with('error', trans('general.insufficient_permissions'));
}
foreach ($user_to_merge->assets as $asset) {
Log::debug('Updating asset: '.$asset->asset_tag.' to '.$merge_into_user->id);
$asset->assigned_to = $request->input('merge_into_id');
@@ -489,12 +461,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));
+47 -100
View File
@@ -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')) {
@@ -165,7 +164,7 @@ class UsersController extends Controller
}
if (auth()->user()->isSuperUser() && auth()->user()->can('editableOnDemo')) {
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
$user->groups()->sync($request->input('groups'));
}
@@ -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);
@@ -312,14 +311,11 @@ class UsersController extends Controller
$user->password = bcrypt($request->input('password'));
}
if ($request->has('permission')) {
$user->permissions = json_encode(PreserveUnauthorizedPrivilegedPermissionsAction::run(
requestedPermissions: NormalizePermissionsPayloadAction::run($request->input('permission')),
authenticatedUser: $authenticatedUser,
originalPermissions: $orig_permissions_array,
targetUser: $user,
));
}
$user->permissions = json_encode(PreserveUnauthorizedPrivilegedPermissionsAction::run(
requestedPermissions: NormalizePermissionsPayloadAction::run($request->input('permission')),
authenticatedUser: $authenticatedUser,
originalPermissions: $orig_permissions_array,
));
// Only save groups if the user is a superuser
if (auth()->user()->isSuperUser()) {
@@ -337,8 +333,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 +477,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);
@@ -540,76 +534,54 @@ class UsersController extends Controller
// Open output stream
$handle = fopen('php://output', 'w');
$headers = [
// strtolower to prevent Excel from trying to open it as a SYLK file
strtolower(trans('general.id')),
trans('admin/companies/table.title'),
trans('admin/users/table.title'),
trans('general.employee_number'),
trans('admin/users/table.first_name'),
trans('admin/users/table.last_name'),
trans('admin/users/table.name'),
trans('admin/users/table.display_name'),
trans('admin/users/table.username'),
trans('admin/users/table.email'),
trans('admin/users/table.phone'),
trans('admin/users/table.mobile'),
trans('general.website'),
trans('general.address'),
trans('general.city'),
trans('general.state'),
trans('general.country'),
trans('general.zip'),
trans('admin/users/table.manager'),
trans('admin/users/table.location'),
trans('general.department'),
trans('general.assets'),
trans('general.licenses'),
trans('general.accessories'),
trans('general.consumables'),
trans('general.groups'),
trans('general.permissions'),
trans('general.notes'),
trans('admin/users/table.activated'),
trans('general.created_at'),
trans('general.importer.vip'),
trans('admin/users/general.remote'),
trans('general.language'),
trans('general.autoassign_licenses'),
trans('general.ldap_sync'),
trans('admin/users/general.two_factor_enrolled'),
trans('admin/users/general.two_factor_active'),
trans('admin/users/table.managed_users'),
trans('admin/users/table.managed_locations'),
trans('admin/users/general.department_manager'),
trans('general.created_by'),
trans('general.updated_at'),
trans('general.start_date'),
trans('general.end_date'),
trans('admin/users/table.last_login'),
trans('admin/licenses/table.deleted_at'),
];
fputcsv($handle, $headers);
$users = User::with(
'assets',
'accessories',
'consumables',
'department.manager',
'department',
'licenses',
'manager',
'groups',
'userloc',
'companies',
'createdBy'
)->withCount(['managesUsers as manages_users_count', 'managedLocations as manages_locations_count'])
->orderBy('created_at', 'DESC')
'company'
)->orderBy('created_at', 'DESC')
->chunk(500, function ($users) use ($handle) {
$headers = [
// strtolower to prevent Excel from trying to open it as a SYLK file
strtolower(trans('general.id')),
trans('admin/companies/table.title'),
trans('admin/users/table.title'),
trans('general.employee_number'),
trans('admin/users/table.first_name'),
trans('admin/users/table.last_name'),
trans('admin/users/table.name'),
trans('admin/users/table.username'),
trans('admin/users/table.email'),
trans('admin/users/table.manager'),
trans('admin/users/table.location'),
trans('general.department'),
trans('general.assets'),
trans('general.licenses'),
trans('general.accessories'),
trans('general.consumables'),
trans('general.groups'),
trans('general.permissions'),
trans('general.notes'),
trans('admin/users/table.activated'),
trans('general.created_at'),
];
fputcsv($handle, $headers);
$formatter = new EscapeFormula('`');
foreach ($users as $user) {
$user_groups = '';
foreach ($user->groups as $user_group) {
$user_groups .= $user_group->name.', ';
}
$permissionstring = '';
if ($user->isSuperUser()) {
@@ -623,23 +595,14 @@ 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,
$user->last_name,
$user->getFullNameAttribute(),
$user->getRawOriginal('display_name'),
$user->display_name,
$user->username,
$user->email,
$user->phone,
$user->mobile,
$user->website,
$user->address,
$user->city,
$user->state,
$user->country,
$user->zip,
($user->manager) ? $user->manager->display_name : '',
($user->userloc) ? $user->userloc->name : '',
($user->department) ? $user->department->name : '',
@@ -647,27 +610,11 @@ class UsersController extends Controller
$user->licenses->count(),
$user->accessories->count(),
$user->consumables->count(),
$user->groups->pluck('name')->implode(', '),
$user_groups,
$permissionstring,
$user->notes,
($user->activated == '1') ? trans('general.yes') : trans('general.no'),
$user->created_at,
($user->vip == '1') ? trans('general.yes') : trans('general.no'),
($user->remote == '1') ? trans('general.yes') : trans('general.no'),
$user->locale,
($user->autoassign_licenses == '1') ? trans('general.yes') : trans('general.no'),
($user->ldap_import == '1') ? trans('general.yes') : trans('general.no'),
($user->two_factor_active_and_enrolled()) ? trans('general.yes') : trans('general.no'),
($user->two_factor_active()) ? trans('general.yes') : trans('general.no'),
$user->manages_users_count,
$user->manages_locations_count,
($user->department && $user->department->manager) ? $user->department->manager->display_name : '',
($user->createdBy) ? $user->createdBy->display_name : '',
$user->updated_at,
$user->start_date,
$user->end_date,
$user->last_login,
$user->deleted_at,
];
// CSV_ESCAPE_FORMULAS is set to false in the .env
+4 -20
View File
@@ -19,7 +19,6 @@ use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* This controller handles all actions related to the ability for users
@@ -121,7 +120,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
@@ -201,23 +199,13 @@ class ViewAssetsController extends Controller
$settings = Setting::getSettings();
$is_admin = $user->isSuperUser() || $user->isAdmin();
if ($cancel_by_admin && ! $is_admin) {
return redirect()->back()->with('error', trans('general.insufficient_permissions'));
}
if (($item_request = $item->isRequestedBy($user)) || ($is_admin && $cancel_by_admin)) {
$item->cancelRequest($is_admin && $cancel_by_admin ? $requestingUser : null);
if (($item_request = $item->isRequestedBy($user)) || $cancel_by_admin) {
$item->cancelRequest($requestingUser);
$data['item_quantity'] = ($item_request) ? $item_request->qty : 1;
$logaction->logaction(ActionType::RequestCanceled);
if (($settings->alert_email != '') && ($settings->alerts_enabled == '1') && (! config('app.lock_passwords'))) {
try {
$settings->notify((new RequestAssetCancelation($data))->locale($settings->locale));
} catch (Exception $e) {
Log::warning('Could not send request cancellation notification: '.$e->getMessage());
}
$settings->notify((new RequestAssetCancelation($data))->locale($settings->locale));
}
return redirect()->back()->with('success')->with('success', trans('admin/hardware/message.requests.canceled'));
@@ -225,11 +213,7 @@ class ViewAssetsController extends Controller
$item->request();
if (($settings->alert_email != '') && ($settings->alerts_enabled == '1') && (! config('app.lock_passwords'))) {
$logaction->logaction('requested');
try {
$settings->notify((new RequestAssetNotification($data))->locale($settings->locale));
} catch (Exception $e) {
Log::warning('Could not send asset request notification: '.$e->getMessage());
}
$settings->notify((new RequestAssetNotification($data))->locale($settings->locale));
}
return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.success'));
-2
View File
@@ -17,7 +17,6 @@ use App\Http\Middleware\PreventBackHistory;
use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\SecurityHeaders;
use App\Http\Middleware\SetAPIResponseHeaders;
use App\Http\Middleware\SetPaginationDefaults;
use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\VerifyCsrfToken;
@@ -85,7 +84,6 @@ class Kernel extends HttpKernel
'auth:api',
CheckLocale::class,
LogAuthedUserHeader::class,
SetPaginationDefaults::class,
SubstituteBindings::class,
],
@@ -1,34 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class SetPaginationDefaults
{
public function handle(Request $request, Closure $next)
{
$limit = config('app.max_results');
$intLimit = intval($request->input('limit'));
if (abs($intLimit) > 0 && $intLimit <= config('app.max_results')) {
$limit = abs($intLimit);
}
app()->instance('api_limit_value', $limit);
if ($request->filled('page') && ! $request->filled('offset')) {
$page = max(1, intval($request->input('page')));
$offset = ($page - 1) * $limit;
} else {
$offset = intval($request->input('offset'));
$page = $limit > 0 ? (int) floor($offset / $limit) + 1 : 1;
}
app()->instance('api_offset_value', $offset);
app()->instance('api_current_page', $page);
return $next($request);
}
}
@@ -2,7 +2,6 @@
namespace App\Http\Requests;
use App\Helpers\Helper;
use App\Http\Requests\Traits\MayContainCustomFields;
use App\Models\Asset;
use App\Models\AssetModel;
@@ -27,10 +26,6 @@ class CreateMultipleAssetRequest extends ImageUploadRequest // should I extend f
{
parent::prepareForValidation();
if ($this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))) {
$this->merge(['purchase_cost' => Helper::ParseCurrency($this->input('purchase_cost'))]);
}
if (Setting::getSettings()->full_multiple_companies_support == '1' && ! $this->user()->isSuperUser()) {
$this->mergeIfMissing(['company_id' => $this->user()->company_id]);
}
+2 -2
View File
@@ -41,7 +41,7 @@ class ItemImportRequest extends FormRequest
$classString = "App\\Importer\\{$class}Importer";
$importer = new $classString($filename);
$import->field_map = request('column-mappings');
$import->created_by = $import->created_by ?? auth()->id();
$import->created_by = auth()->id();
$import->save();
$fieldMappings = [];
@@ -51,7 +51,7 @@ class ItemImportRequest extends FormRequest
if (is_null($fieldValue)) {
$errorMessage = trans('validation.import_field_empty', ['fieldname' => $field]);
$this->errorCallback($import, $field, [$field => [$errorMessage]]);
$this->errorCallback($import, $field, [$field => $errorMessage]);
return $this->errors;
}
+1 -5
View File
@@ -2,7 +2,6 @@
namespace App\Http\Requests;
use App\Helpers\Helper;
use App\Models\Accessory;
use App\Models\Category;
use Illuminate\Contracts\Validation\ValidationRule;
@@ -22,10 +21,6 @@ class StoreAccessoryRequest extends ImageUploadRequest
{
parent::prepareForValidation();
if ($this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))) {
$this->merge(['purchase_cost' => Helper::ParseCurrency($this->input('purchase_cost'))]);
}
if ($this->category_id) {
if ($category = Category::find($this->category_id)) {
$this->merge([
@@ -33,6 +28,7 @@ class StoreAccessoryRequest extends ImageUploadRequest
]);
}
}
}
/**
+26 -4
View File
@@ -2,10 +2,10 @@
namespace App\Http\Requests;
use App\Helpers\Helper;
use App\Http\Requests\Traits\MayContainCustomFields;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Setting;
use App\Rules\AssetCannotBeCheckedOutToNondeployableStatus;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
@@ -39,9 +39,6 @@ class StoreAssetRequest extends ImageUploadRequest
$this->merge([
'asset_tag' => $this->asset_tag ?? Asset::autoincrement_asset(),
'company_id' => $idForCurrentUser,
'purchase_cost' => $this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))
? Helper::ParseCurrency($this->input('purchase_cost'))
: $this->input('purchase_cost'),
]);
}
@@ -52,6 +49,15 @@ class StoreAssetRequest extends ImageUploadRequest
{
$modelRules = (new Asset)->getRules();
if (Setting::getSettings()->digit_separator === '1.234,56' && is_string($this->input('purchase_cost'))) {
// If purchase_cost was submitted as a string with a comma separator
// then we need to ignore the normal numeric rules.
// Since the original rules still live on the model they will be run
// right before saving (and after purchase_cost has been
// converted to a float via setPurchaseCostAttribute).
$modelRules = $this->removeNumericRulesFromPurchaseCost($modelRules);
}
return array_merge(
$modelRules,
['status_id' => [new AssetCannotBeCheckedOutToNondeployableStatus]],
@@ -75,4 +81,20 @@ class StoreAssetRequest extends ImageUploadRequest
}
}
}
private function removeNumericRulesFromPurchaseCost(array $rules): array
{
$purchaseCost = $rules['purchase_cost'];
// If rule is in "|" format then turn it into an array
if (is_string($purchaseCost)) {
$purchaseCost = explode('|', $purchaseCost);
}
$rules['purchase_cost'] = array_filter($purchaseCost, function ($rule) {
return $rule !== 'numeric' && $rule !== 'gte:0';
});
return $rules;
}
}
@@ -1,27 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Helpers\Helper;
use App\Models\Component;
use Illuminate\Support\Facades\Gate;
class StoreComponentRequest extends ImageUploadRequest
{
public function authorize(): bool
{
return Gate::allows('create', Component::class);
}
public function prepareForValidation(): void
{
if ($this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))) {
$this->merge(['purchase_cost' => Helper::ParseCurrency($this->input('purchase_cost'))]);
}
}
public function response(array $errors)
{
return $this->redirector->back()->withInput()->withErrors($errors, $this->errorBag);
}
}
+1 -5
View File
@@ -2,7 +2,6 @@
namespace App\Http\Requests;
use App\Helpers\Helper;
use App\Models\Category;
use App\Models\Consumable;
use Illuminate\Contracts\Validation\ValidationRule;
@@ -22,10 +21,6 @@ class StoreConsumableRequest extends ImageUploadRequest
{
parent::prepareForValidation();
if ($this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))) {
$this->merge(['purchase_cost' => Helper::ParseCurrency($this->input('purchase_cost'))]);
}
if ($this->category_id) {
if ($category = Category::find($this->category_id)) {
$this->merge([
@@ -33,6 +28,7 @@ class StoreConsumableRequest extends ImageUploadRequest
]);
}
}
}
/**
+6 -8
View File
@@ -2,7 +2,6 @@
namespace App\Http\Requests;
use App\Helpers\Helper;
use App\Http\Requests\Traits\MayContainCustomFields;
use App\Models\Asset;
use App\Models\Setting;
@@ -23,13 +22,6 @@ class UpdateAssetRequest extends ImageUploadRequest
return Gate::allows('update', $this->asset);
}
public function prepareForValidation(): void
{
if ($this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))) {
$this->merge(['purchase_cost' => Helper::ParseCurrency($this->input('purchase_cost'))]);
}
}
/**
* Get the validation rules that apply to the request.
*
@@ -59,6 +51,12 @@ class UpdateAssetRequest extends ImageUploadRequest
],
);
// if the purchase cost is passed in as a string **and** the digit_separator is ',' (as is common in the EU)
// then we tweak the purchase_cost rule to make it a string
if ($setting->digit_separator === '1.234,56' && is_string($this->input('purchase_cost'))) {
$rules['purchase_cost'] = ['nullable', 'string'];
}
return $rules;
}
}
@@ -1,35 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Helpers\Helper;
use Illuminate\Support\Facades\Gate;
class UpdateComponentRequest extends ImageUploadRequest
{
public function authorize(): bool
{
return Gate::allows('update', $this->component);
}
public function prepareForValidation(): void
{
if ($this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))) {
$this->merge(['purchase_cost' => Helper::ParseCurrency($this->input('purchase_cost'))]);
}
}
public function rules(): array
{
$min = $this->component->numCheckedOut();
return array_merge(parent::rules(), [
'qty' => "required|numeric|min:{$min}",
]);
}
public function response(array $errors)
{
return $this->redirector->back()->withInput()->withErrors($errors, $this->errorBag);
}
}
@@ -116,10 +116,10 @@ class ActionlogsTransformer
$clean_meta[$fieldname]['old'] = '************';
$clean_meta[$fieldname]['new'] = '************';
// Display the changes if the user has permission to view encrypted custom fields
if (Gate::allows('assets.view.encrypted_custom_fields')) {
$clean_meta[$fieldname]['old'] = ($enc_old) ? e(unserialize($enc_old, ['allowed_classes' => false])) : '';
$clean_meta[$fieldname]['new'] = ($enc_new) ? e(unserialize($enc_new, ['allowed_classes' => false])) : '';
// Display the changes if the user is an admin or superadmin
if (Gate::allows('admin')) {
$clean_meta[$fieldname]['old'] = ($enc_old) ? unserialize($enc_old, ['allowed_classes' => false]) : '';
$clean_meta[$fieldname]['new'] = ($enc_new) ? unserialize($enc_new, ['allowed_classes' => false]) : '';
}
}
@@ -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']);
@@ -388,9 +388,6 @@ class AssetsTransformer
$permissions_array['available_actions'] = [
'checkout' => false,
'checkin' => Gate::allows('checkin', License::class),
'bulk_selectable' => [
'checkin' => Gate::allows('checkin', License::class),
],
];
$array += $permissions_array;
@@ -75,9 +75,6 @@ class CategoriesTransformer
$permissions_array['available_actions'] = [
'update' => Gate::allows('update', Category::class),
'delete' => $category->isDeletable(),
'bulk_selectable' => [
'delete' => $category->isDeletable(),
],
];
$array += $permissions_array;
@@ -9,24 +9,8 @@ class DatatablesTransformer
**/
public function transformDatatables($objects, $total = null)
{
$objects_array = [
'total' => $total ?? count($objects),
'rows' => $objects,
];
$current_page = app('api_current_page');
$limit = (int) app('api_limit_value');
$total_pages = $limit > 0 ? (int) ceil($objects_array['total'] / $limit) : 1;
$objects_array['current_page'] = $current_page;
$objects_array['per_page'] = $limit;
$objects_array['total_pages'] = $total_pages;
$objects_array['prev_page_url'] = $current_page > 1
? request()->fullUrlWithQuery(['page' => $current_page - 1])
: null;
$objects_array['next_page_url'] = $current_page < $total_pages
? request()->fullUrlWithQuery(['page' => $current_page + 1])
: null;
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
$objects_array['rows'] = $objects;
return $objects_array;
}
@@ -36,10 +20,8 @@ class DatatablesTransformer
**/
public function transformBulkResponseWithStatusAndObjects($objects, $total)
{
$objects_array = [
'total' => $total ?? count($objects),
'rows' => $objects,
];
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
$objects_array['rows'] = $objects;
return $objects_array;
}
@@ -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) ? [
@@ -68,9 +70,6 @@ class LicenseSeatsTransformer
'clone' => Gate::allows('create', License::class),
'update' => Gate::allows('update', License::class),
'delete' => Gate::allows('delete', License::class),
'bulk_selectable' => [
'checkin' => Gate::allows('checkin', License::class) && ($seat->assigned_to || $seat->asset_id),
],
];
$array += $permissions_array;
@@ -66,6 +66,7 @@ class LicensesTransformer
'created_at' => Helper::getFormattedDateObject($license->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($license->updated_at, 'datetime'),
'deleted_at' => Helper::getFormattedDateObject($license->deleted_at, 'datetime'),
'user_can_checkout' => (bool) ($license->free_seats_count > 0),
'disabled' => $license->isInactive(),
];
@@ -75,7 +76,6 @@ class LicensesTransformer
'clone' => Gate::allows('create', License::class),
'update' => Gate::allows('update', License::class),
'delete' => $license->isDeletable(),
'user_can_checkout' => (bool) (($license->free_seats_count - License::unReassignableCount($license)) > 0),
'bulk_selectable' => [
'delete' => $license->isDeletable(),
],
@@ -1,37 +0,0 @@
<?php
namespace App\Http\Transformers;
use App\Helpers\Helper;
use App\Models\MaintenanceType;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Gate;
class MaintenanceTypesTransformer
{
public function transformMaintenanceTypes(Collection $types, int $total): array
{
$array = [];
foreach ($types as $type) {
$array[] = self::transformMaintenanceType($type);
}
return (new DatatablesTransformer)->transformDatatables($array, $total);
}
public function transformMaintenanceType(MaintenanceType $type): array
{
return [
'id' => (int) $type->id,
'name' => e($type->name),
'created_at' => Helper::getFormattedDateObject($type->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($type->updated_at, 'datetime'),
'deleted_at' => Helper::getFormattedDateObject($type->deleted_at, 'datetime'),
'available_actions' => [
'update' => Gate::allows('update', $type),
'delete' => $type->isDeletable(),
'restore' => Gate::allows('delete', $type),
],
];
}
}
@@ -82,22 +82,6 @@ class MaintenancesTransformer
'id' => (int) $assetmaintenance->adminuser->id,
'name' => e($assetmaintenance->adminuser->display_name),
] : null,
'maintenance_type' => $assetmaintenance->maintenanceType
? e($assetmaintenance->maintenanceType->name)
: null,
'responsible_party' => ($assetmaintenance->responsibleParty) ? [
'id' => (int) $assetmaintenance->responsibleParty->id,
'name' => e($assetmaintenance->responsibleParty->display_name),
] : null,
'checked_out_to_at_creation' => $assetmaintenance->checked_out_to_id ? [
'id' => (int) $assetmaintenance->checked_out_to_id,
'type' => $assetmaintenance->checked_out_to_type,
] : null,
'completed_at' => Helper::getFormattedDateObject($assetmaintenance->completed_at, 'datetime'),
'completed_by' => ($assetmaintenance->completedByUser) ? [
'id' => (int) $assetmaintenance->completedByUser->id,
'name' => e($assetmaintenance->completedByUser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($assetmaintenance->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($assetmaintenance->updated_at, 'datetime'),
'is_warranty' => (bool) $assetmaintenance->is_warranty,
@@ -107,7 +91,6 @@ class MaintenancesTransformer
$permissions_array['available_actions'] = [
'update' => (Gate::allows('update', Asset::class) && ((($assetmaintenance->asset) && $assetmaintenance->asset->deleted_at == ''))) ? true : false,
'delete' => Gate::allows('delete', Asset::class),
'complete' => Gate::allows('update', Asset::class) && ! $assetmaintenance->completed_at,
];
$array += $permissions_array;
@@ -145,23 +128,10 @@ class MaintenancesTransformer
'supplier' => ($assetmaintenance->supplier) ? e($assetmaintenance->supplier?->name) : null,
'url' => ($assetmaintenance->url) ? e($assetmaintenance->url) : null,
'cost' => Helper::formatCurrencyOutput($assetmaintenance->cost),
'maintenance_type' => $assetmaintenance->maintenanceType
? e($assetmaintenance->maintenanceType->name)
: null,
'asset_maintenance_type' => e($assetmaintenance->asset_maintenance_type),
'start_date' => Helper::getFormattedDateObject($assetmaintenance->start_date, 'date'),
'asset_maintenance_time' => $assetmaintenance->asset_maintenance_time,
'completion_date' => Helper::getFormattedDateObject($assetmaintenance->completion_date, 'date'),
'responsible_party' => ($assetmaintenance->responsibleParty) ? [
'id' => (int) $assetmaintenance->responsibleParty->id,
'name' => e($assetmaintenance->responsibleParty->display_name),
] : null,
'checked_out_to_at_creation' => ($assetmaintenance->checkedOutTo) ? e($assetmaintenance->checkedOutTo->display_name) : null,
'completed_at' => Helper::getFormattedDateObject($assetmaintenance->completed_at, 'datetime'),
'completed_by' => ($assetmaintenance->completedByUser) ? [
'id' => (int) $assetmaintenance->completedByUser->id,
'name' => e($assetmaintenance->completedByUser->display_name),
] : null,
'created_by' => ($assetmaintenance->adminuser) ? e($assetmaintenance->adminuser->display_name) : null,
'created_at' => Helper::getFormattedDateObject($assetmaintenance->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($assetmaintenance->updated_at, 'datetime'),
@@ -52,9 +52,6 @@ class ManufacturersTransformer
'update' => (($manufacturer->deleted_at == '') && (Gate::allows('update', Manufacturer::class))),
'restore' => (($manufacturer->deleted_at != '') && (Gate::allows('create', Manufacturer::class))),
'delete' => $manufacturer->isDeletable(),
'bulk_selectable' => [
'delete' => $manufacturer->isDeletable(),
],
];
$array += $permissions_array;
@@ -57,9 +57,6 @@ class SuppliersTransformer
$permissions_array['available_actions'] = [
'update' => Gate::allows('update', Supplier::class),
'delete' => (Gate::allows('delete', Supplier::class) && ($supplier->isDeletable())),
'bulk_selectable' => [
'delete' => (Gate::allows('delete', Supplier::class) && ($supplier->isDeletable())),
],
];
$array += $permissions_array;
+4 -15
View File
@@ -82,17 +82,11 @@ class UsersTransformer
'consumables_count' => (int) $user->consumables_count,
'manages_users_count' => (int) $user->manages_users_count,
'manages_locations_count' => (int) $user->manages_locations_count,
// Legacy field — kept for backward API compatibility; use `companies` for multi-company support.
'company' => $user->companies->isNotEmpty() ? [
'id' => (int) $user->companies->first()->id,
'name' => e($user->companies->first()->name),
'tag_color' => ($user->companies->first()->tag_color) ? e($user->companies->first()->tag_color) : null,
'company' => ($user->company) ? [
'id' => (int) $user->company->id,
'name' => e($user->company->name),
'tag_color' => ($user->company->tag_color) ? e($user->company->tag_color) : null,
] : null,
'companies' => $user->companies->map(fn ($c) => [
'id' => (int) $c->id,
'name' => e($c->name),
'tag_color' => $c->tag_color ? e($c->tag_color) : null,
])->values(),
'created_by' => ($user->createdBy) ? [
'id' => (int) $user->createdBy->id,
'name' => e($user->createdBy->display_name),
@@ -150,11 +144,6 @@ class UsersTransformer
'last_name' => e($user->last_name),
'username' => e($user->username),
'display_name' => e($user->display_name),
'companies' => $user->companies->map(fn ($c) => [
'id' => (int) $c->id,
'name' => e($c->name),
'tag_color' => $c->tag_color ? e($c->tag_color) : null,
])->values(),
'created_by' => $user->adminuser ? [
'id' => (int) $user->adminuser->id,
'name' => e($user->adminuser->present()->fullName),
+1 -2
View File
@@ -37,8 +37,7 @@ class AccessoryImporter extends ItemImporter
$this->log('Updating Accessory');
$this->item['model_number'] = trim($this->findCsvMatch($row, 'model_number'));
$accessory->update($this->sanitizeItemForUpdating($accessory));
// update() already saves the model, no need to call save() again while Model::unguard() is active
$accessory->setImported(true);
$accessory->save();
return;
}
+8 -28
View File
@@ -176,55 +176,35 @@ class AssetImporter extends ItemImporter
if ($editingAsset) {
$asset->update($item);
$asset->setImported(true);
} else {
$asset->fill($item);
$asset->setImported(true);
}
// If we're updating, we don't want to overwrite old fields.
// Apply custom fields to asset attributes if they exist
$customFieldsToSave = [];
if (array_key_exists('custom_fields', $this->item)) {
foreach ($this->item['custom_fields'] as $custom_field => $val) {
$asset->{$custom_field} = $val;
$customFieldsToSave[$custom_field] = $val;
}
}
// For existing assets that have custom fields, update them.
// This avoids the issue of calling save() twice with Model::unguard() active.
if ($editingAsset && ! empty($customFieldsToSave)) {
$asset->update($customFieldsToSave);
$success = true;
} elseif (! $editingAsset) {
// For new assets, save with all changes (custom fields included via direct attribute assignment above)
$success = $asset->save();
} else {
// For existing assets without custom fields, update() already saved everything
$success = true;
}
// This sets an attribute on the Loggable trait for the action log
$asset->setImported(true);
if ($success) {
if ($asset->save()) {
$this->log('Asset '.$this->item['name'].' with serial number '.$this->item['serial'].' created or updated');
$this->log('Asset '.$this->item['name'].' with serial number '.$this->item['serial'].' was created');
// If we have a target to checkout to, lets do so.
// -- created_by is a property of the abstract class Importer, which this class inherits from and it's set by
// -- the class that needs to use it (command importer or GUI importer inside the project).
if (isset($target) && ($target !== false)) {
$asset = $asset->fresh();
$targetType = get_class($target);
$alreadyCheckedOutToTarget = ($asset->assigned_to == $target->id) && ($asset->assigned_type === $targetType);
// Skip duplicate checkout noise when update mode keeps the same assignment target.
if (! $alreadyCheckedOutToTarget) {
if (! is_null($asset->assigned_to)) {
if (! is_null($asset->assigned_to)) {
if ($asset->assigned_to != $target->id) {
event(new CheckoutableCheckedIn($asset, $asset->assigned, auth()->user(), 'Checkin from CSV Importer', $checkin_date));
}
$asset->checkOut($target, $this->created_by, $checkout_date, null, 'Checkout from CSV Importer', $asset->name);
}
$asset->fresh()->checkOut($target, $this->created_by, $checkout_date, null, 'Checkout from CSV Importer', $asset->name);
}
return;
+1 -2
View File
@@ -42,8 +42,7 @@ class ComponentImporter extends ItemImporter
}
$this->log('Updating Component');
$component->update($this->sanitizeItemForUpdating($component));
// update() already saves the model, no need to call save() again while Model::unguard() is active
$component->setImported(true);
$component->save();
return;
}
+1 -2
View File
@@ -38,8 +38,7 @@ class ConsumableImporter extends ItemImporter
}
$this->log('Updating Consumable');
$consumable->update($this->sanitizeItemForUpdating($consumable));
// update() already saves the model, no need to call save() again while Model::unguard() is active
$consumable->setImported(true);
$consumable->save();
return;
}
+2 -6
View File
@@ -88,12 +88,8 @@ class LicenseImporter extends ItemImporter
// This sets an attribute on the Loggable trait for the action log
$license->setImported(true);
// For new licenses we need to save, for existing ones update() already saved
$licenseWasSaved = $editingLicense || $license->save();
if ($licenseWasSaved) {
$this->log('License '.$this->item['name'].' with serial number '.$this->item['serial'].' was created or updated');
if ($license->save()) {
$this->log('License '.$this->item['name'].' with serial number '.$this->item['serial'].' was created');
// Lets try to checkout seats if the fields exist and we have seats.
if ($license->seats > 0) {
+5 -63
View File
@@ -3,7 +3,6 @@
namespace App\Importer;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Department;
use App\Models\Setting;
use App\Models\User;
@@ -36,31 +35,6 @@ class UserImporter extends ItemImporter
$this->createUserIfNotExists($row);
}
/**
* Parse a pipe-separated company column value into an array of company IDs,
* creating companies that do not yet exist. Returns an empty array when the
* raw value is blank (so callers can treat that as "don't change").
*
* @param string $raw Raw cell value, e.g. "Acme Corp|Widget Inc"
* @return int[]
*/
private function resolveCompanyIds(string $raw): array
{
if ($raw === '') {
return [];
}
$ids = [];
foreach (array_filter(array_map('trim', explode('|', $raw))) as $name) {
$id = $this->createOrFetchCompany($name);
if ($id) {
$ids[] = (int) $id;
}
}
return Company::getIdsForCurrentUser($ids);
}
/**
* Create a user if a duplicate does not exist.
*
@@ -106,13 +80,6 @@ class UserImporter extends ItemImporter
$this->item['department_id'] = $this->createOrFetchDepartment($user_department);
}
// Resolve pipe-separated company names (e.g. "Acme Corp|Widget Inc") into IDs.
// company_id is a legacy column — company membership is managed via the pivot.
// Unset whatever the parent set so it is not written to the DB.
$companyRaw = trim($this->findCsvMatch($row, 'company'));
$companyIds = $this->resolveCompanyIds($companyRaw);
unset($this->item['company_id']);
if (is_null($this->item['username']) || $this->item['username'] == '') {
$user_full_name = $this->item['first_name'].' '.$this->item['last_name'];
$user_formatted_array = User::generateFormattedNameFromFullName($user_full_name, Setting::getSettings()->username_format);
@@ -137,13 +104,11 @@ class UserImporter extends ItemImporter
$this->log('Updating User');
// CLI imports run unauthenticated and are fully trusted; only restrict web-initiated imports.
// Note: unset must target $this->item, not the model — sanitizeItemForUpdating() reads from $this->item.
if (Auth::check() && (! Auth::user()->hasAccess('users.edit') || ! Gate::allows('canEditAuthFields', $user))) {
unset($this->item['username']);
unset($this->item['email']);
unset($this->item['password']);
unset($this->item['activated']);
if (Auth::check() && (! Gate::allows('canEditAuthFields', $user))) {
unset($user->username);
unset($user->email);
unset($user->password);
unset($user->activated);
}
$user->update($this->sanitizeItemForUpdating($user));
@@ -151,11 +116,6 @@ class UserImporter extends ItemImporter
// Why do we have to do this twice? Update should
$user->save();
// Sync company pivot when companies were specified in this row.
if (! empty($companyIds)) {
$user->companies()->sync($companyIds);
}
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)
@@ -165,17 +125,6 @@ class UserImporter extends ItemImporter
return;
}
// With FMCS enabled, the scoped lookup above only sees users in the current user's companies.
// If the username exists in another company it would appear as "not found" and fall through
// to create — but usernames are unique system-wide, so we must skip instead.
if (Auth::check() && Company::isFullMultipleCompanySupportEnabled()) {
if (User::withoutGlobalScopes()->where('username', $this->item['username'])->exists()) {
$this->log('Skipping '.$this->item['username'].': username belongs to a user outside your company scope.');
return;
}
}
// This needs to be applied after the update logic, otherwise we'll overwrite user passwords
// Issue #5408
$this->item['password'] = $this->tempPassword;
@@ -191,13 +140,6 @@ class UserImporter extends ItemImporter
if ($user->save()) {
$this->log('User '.$this->item['name'].' was created');
// Sync all resolved companies to the pivot. For single-company rows the
// User::created event already added company_id; sync() here is idempotent
// for that case and adds any additional companies for multi-company rows.
if (! empty($companyIds)) {
$user->companies()->sync($companyIds);
}
if (($user->email) && ($user->activated == '1')) {
if ($this->send_welcome) {
-6
View File
@@ -720,12 +720,6 @@ class Importer extends Component
$this->message_type = 'danger';
}
public function process(): void
{
$this->message = trans('general.token_expired');
$this->message_type = 'danger';
}
public function clearMessage()
{
$this->message = null;
+28 -25
View File
@@ -6,7 +6,6 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Laravel\Passport\Client;
use Laravel\Passport\ClientRepository;
use Laravel\Passport\Token;
use Livewire\Component;
class OauthClients extends Component
@@ -51,11 +50,11 @@ class OauthClients extends Component
->get();
if ($clients->isNotEmpty()) {
$tokenCountsByClientId = Token::query()
$tokenCountsByClientId = DB::table('oauth_access_tokens')
->whereIn('client_id', $clients->pluck('id')->all())
->get(['client_id'])
->selectRaw('client_id, COUNT(*) as token_count')
->groupBy('client_id')
->map->count();
->pluck('token_count', 'client_id');
$clients->each(function ($client) use ($tokenCountsByClientId): void {
$client->setAttribute('associated_token_count', (int) ($tokenCountsByClientId[$client->id] ?? 0));
@@ -65,28 +64,32 @@ class OauthClients extends Component
$authorizedApplications = collect();
if ($this->showAuthorizedApplications()) {
$authorizedApplications = Token::query()
->where('revoked', false)
->with([
'client',
'client.user' => fn ($q) => $q->withTrashed(),
$authorizedTokenSummary = DB::table('oauth_access_tokens as tokens')
->where('tokens.revoked', false)
->selectRaw('tokens.client_id')
->selectRaw('MAX(tokens.scopes) as scopes')
->selectRaw('MAX(tokens.created_at) as created_at')
->selectRaw('MAX(tokens.expires_at) as expires_at')
->groupBy('tokens.client_id');
$authorizedApplications = DB::table('oauth_clients as clients')
->joinSub($authorizedTokenSummary, 'token_summary', function ($join) {
$join->on('clients.id', '=', 'token_summary.client_id');
})
->leftJoin('users as creators', 'clients.user_id', '=', 'creators.id')
->select([
'clients.id as client_id',
'clients.name as client_name',
'clients.user_id as client_owner_id',
'creators.display_name as client_owner_display_name',
'creators.username as client_owner_username',
'creators.deleted_at as client_owner_deleted_at',
'token_summary.scopes',
'token_summary.created_at',
'token_summary.expires_at',
])
->orderByDesc('created_at')
->get()
->unique('client_id')
->filter(fn ($token) => $token->client !== null)
->map(fn ($token) => (object) [
'client_id' => $token->client_id,
'client_name' => $token->client->name,
'client_owner_id' => $token->client->user_id,
'client_owner_display_name' => $token->client->user?->display_name,
'client_owner_username' => $token->client->user?->username,
'client_owner_deleted_at' => $token->client->user?->deleted_at,
'scopes' => $token->scopes,
'created_at' => $token->created_at,
'expires_at' => $token->expires_at,
])
->values();
->orderByDesc('token_summary.created_at')
->get();
}
return view('livewire.oauth-clients', [
+1 -2
View File
@@ -146,8 +146,7 @@ class AccessoryCheckout extends Model
$search_str = '%'.$term.'%';
$query->where('first_name', 'like', $search_str)
->orWhere('last_name', 'like', $search_str)
->orWhere('note', 'like', $search_str)
->orWhereHas('companies', fn ($q) => $q->where('companies.name', 'like', $search_str));
->orWhere('note', 'like', $search_str);
}
}
)->select('id');
@@ -1,117 +0,0 @@
<?php
namespace App\Models\Builders;
use App\Models\Setting;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
class MaintenanceQueryBuilder extends Builder
{
public function active(): static
{
return $this->whereNull('maintenances.completed_at');
}
public function completed(): static
{
return $this->whereNotNull('maintenances.completed_at');
}
public function dueForCompletion(Setting $settings): static
{
$interval = (int) ($settings->audit_warning_days ?? 0);
$today = Carbon::now();
return $this->whereNotNull('maintenances.completion_date')
->whereNull('maintenances.completed_at')
->whereBetween('maintenances.completion_date', [
$today->format('Y-m-d'),
$today->copy()->addDays($interval)->format('Y-m-d'),
]);
}
public function overdueForCompletion(): static
{
return $this->whereNotNull('maintenances.completion_date')
->whereNull('maintenances.completed_at')
->where('maintenances.completion_date', '<', Carbon::now()->format('Y-m-d'));
}
public function dueOrOverdueForCompletion(Setting $settings): static
{
return $this->where(fn ($q) => $q->overdueForCompletion())
->orWhere(fn ($q) => $q->dueForCompletion($settings));
}
public function orderBySupplier(string $order): static
{
return $this->leftJoin('suppliers as suppliers_maintenances', 'maintenances.supplier_id', '=', 'suppliers_maintenances.id')
->orderBy('suppliers_maintenances.name', $order);
}
public function orderByTag(string $order): static
{
return $this->leftJoin('assets', 'maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.asset_tag', $order);
}
public function orderByAssetName(string $order): static
{
return $this->leftJoin('assets', 'maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.name', $order);
}
public function orderByAssetSerial(string $order): static
{
return $this->leftJoin('assets', 'maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.serial', $order);
}
public function orderStatusName(string $order): static
{
return $this->join('assets as maintained_asset', 'maintenances.asset_id', '=', 'maintained_asset.id')
->leftJoin('status_labels as maintained_asset_status', 'maintained_asset_status.id', '=', 'maintained_asset.status_id')
->orderBy('maintained_asset_status.name', $order);
}
public function orderLocationName(string $order): static
{
return $this->join('assets as maintained_asset', 'maintenances.asset_id', '=', 'maintained_asset.id')
->leftJoin('locations as maintained_asset_location', 'maintained_asset_location.id', '=', 'maintained_asset.location_id')
->orderBy('maintained_asset_location.name', $order);
}
public function orderByCreatedBy(string $order): static
{
return $this->leftJoin('users as admin_sort', 'maintenances.created_by', '=', 'admin_sort.id')
->select('maintenances.*')
->orderBy('admin_sort.first_name', $order)
->orderBy('admin_sort.last_name', $order);
}
public function orderByAssetModelName(string $order): static
{
return $this->join('assets as maintained_asset', 'maintenances.asset_id', '=', 'maintained_asset.id')
->leftJoin('models as maintained_asset_model', 'maintained_asset_model.id', '=', 'maintained_asset.model_id')
->orderBy('maintained_asset_model.name', $order);
}
public function orderByAssetModelNumber(string $order): static
{
return $this->join('assets as maintained_asset', 'maintenances.asset_id', '=', 'maintained_asset.id')
->leftJoin('models as maintained_asset_model', 'maintained_asset_model.id', '=', 'maintained_asset.model_id')
->orderBy('maintained_asset_model.model_number', $order);
}
public function orderByMaintenanceType(string $order): static
{
return $this->leftJoin('maintenance_types as maintenance_type_sort', 'maintenances.maintenance_type_id', '=', 'maintenance_type_sort.id')
->orderBy('maintenance_type_sort.name', $order);
}
public function orderByCompletedAt(string $order): static
{
return $this->orderBy('maintenances.completed_at', $order);
}
}
+24 -106
View File
@@ -11,7 +11,6 @@ use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
@@ -95,26 +94,7 @@ final class Company extends SnipeModel
'notes',
];
/**
* Return the current user's company IDs by querying the pivot table directly.
*
* We deliberately bypass the Eloquent companies() relationship here because
* loading that relationship triggers CompanyableScope on the Company model,
* which calls this method again — infinite recursion.
*/
private static function getCurrentUserCompanyIds(): array
{
if (! Auth::hasUser()) {
return [];
}
return DB::table('company_user')
->where('user_id', auth()->id())
->pluck('company_id')
->toArray();
}
public static function isFullMultipleCompanySupportEnabled()
private static function isFullMultipleCompanySupportEnabled()
{
$settings = Setting::getSettings();
@@ -199,65 +179,20 @@ final class Company extends SnipeModel
}
if (auth()->user()) {
if (auth()->user()->isSuperUser()) {
return true;
// Log::warning('Companyable is '.$companyable);
$current_user_company_id = auth()->user()->company_id;
$companyable_company_id = $companyable->company_id;
// Set this to check companyable on company
if ($companyable instanceof Company) {
$companyable_company_id = $companyable->id;
}
$userCompanyIds = self::getCurrentUserCompanyIds();
// Empty pivot = unrestricted only for true legacy "no-company" users
// (those whose scalar company_id is also null). Users who had their
// pivot cleared via the API retain their scalar company_id, so they
// do NOT qualify for this bypass.
if (empty($userCompanyIds) && is_null(auth()->user()->company_id)) {
return true;
}
// Users are scoped by pivot membership, not company_id, so check the pivot directly.
if ($companyable instanceof User) {
$companyableCompanyIds = DB::table('company_user')
->where('user_id', $companyable->id)
->pluck('company_id')
->toArray();
// A user with no pivot rows is a null-company user; no intersection is possible.
if (empty($companyableCompanyIds)) {
return false;
}
return ! empty(array_intersect($userCompanyIds, $companyableCompanyIds));
}
$companyable_company_id = ($companyable instanceof Company)
? $companyable->id
: $companyable->company_id;
return in_array($companyable_company_id, $userCompanyIds);
return ($current_user_company_id == null) || ($current_user_company_id == $companyable_company_id) || auth()->user()->isSuperUser();
}
return false;
}
/**
* Filter an array of requested company IDs to only those the current user
* belongs to. Superusers may assign any company; non-superusers are limited
* to their own pivot memberships when FMCS is enabled.
*/
public static function getIdsForCurrentUser(array $requestedIds): array
{
if (! self::isFullMultipleCompanySupportEnabled()) {
return $requestedIds;
}
$current_user = auth()->user();
if ($current_user->isSuperUser()) {
return $requestedIds;
}
$allowedIds = self::getCurrentUserCompanyIds();
return array_values(array_intersect($requestedIds, $allowedIds));
}
public static function isCurrentUserAuthorized()
@@ -267,9 +202,8 @@ final class Company extends SnipeModel
public static function canManageUsersCompanies()
{
return ! self::isFullMultipleCompanySupportEnabled()
|| auth()->user()->isSuperUser()
|| empty(self::getCurrentUserCompanyIds());
return ! self::isFullMultipleCompanySupportEnabled() || auth()->user()->isSuperUser() ||
auth()->user()->company_id == null;
}
/**
@@ -308,7 +242,7 @@ final class Company extends SnipeModel
public function users()
{
return $this->belongsToMany(User::class, 'company_user');
return $this->hasMany(User::class, 'company_id');
}
public function assets()
@@ -370,43 +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);
}
return $query->whereIn($table.$column, $companyIds);
return $query->where($table.$column, '=', $company_id);
}
}
/**
+1 -1
View File
@@ -690,7 +690,7 @@ abstract class Label
// Find one
if ($name !== null) {
return static::find()
->first(
->sole(
function ($label) use ($name) {
return $label->getName() == $name;
}
+1 -2
View File
@@ -803,7 +803,7 @@ class License extends Depreciable
*
* @return mixed
*/
public function freeSeat(bool $lock = false)
public function freeSeat()
{
return $this->licenseseats()
->whereNull('deleted_at')
@@ -813,7 +813,6 @@ class License extends Depreciable
->whereNull('asset_id');
})
->orderBy('id', 'asc')
->when($lock, fn ($q) => $q->lockForUpdate())
->first();
}
+108 -40
View File
@@ -3,7 +3,6 @@
namespace App\Models;
use App\Helpers\Helper;
use App\Models\Builders\MaintenanceQueryBuilder;
use App\Models\Traits\CompanyableChildTrait;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Loggable;
@@ -13,6 +12,7 @@ use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Gate;
use Watson\Validating\ValidatingTrait;
@@ -39,7 +39,7 @@ class Maintenance extends SnipeModel implements ICompanyableChild
protected $rules = [
'asset_id' => 'required|integer',
'supplier_id' => 'nullable|integer',
'maintenance_type_id' => 'required|integer|exists:maintenance_types,id',
'asset_maintenance_type' => 'required',
'name' => 'required|max:100',
'is_warranty' => 'boolean',
'start_date' => 'required|date_format:Y-m-d',
@@ -47,8 +47,6 @@ class Maintenance extends SnipeModel implements ICompanyableChild
'notes' => 'string|nullable',
'cost' => 'numeric|nullable|gte:0|max:99999999999999999.99',
'url' => 'nullable|url|max:255',
'responsible_party_id' => 'nullable|integer|exists:users,id',
'completed_by' => 'nullable|integer|exists:users,id',
];
/**
@@ -61,7 +59,6 @@ class Maintenance extends SnipeModel implements ICompanyableChild
'asset_id',
'supplier_id',
'asset_maintenance_type',
'maintenance_type_id',
'is_warranty',
'start_date',
'completion_date',
@@ -69,11 +66,6 @@ class Maintenance extends SnipeModel implements ICompanyableChild
'notes',
'cost',
'url',
'checked_out_to_id',
'checked_out_to_type',
'responsible_party_id',
'completed_at',
'completed_by',
];
use Searchable;
@@ -87,6 +79,7 @@ class Maintenance extends SnipeModel implements ICompanyableChild
[
'name',
'notes',
'asset_maintenance_type',
'cost',
'start_date',
'completion_date',
@@ -104,7 +97,6 @@ class Maintenance extends SnipeModel implements ICompanyableChild
'asset.status' => ['name'],
'supplier' => ['name'],
'adminuser' => ['first_name', 'last_name', 'display_name'],
'maintenanceType' => ['name'],
];
public function getCompanyableParents()
@@ -212,40 +204,116 @@ class Maintenance extends SnipeModel implements ICompanyableChild
->withTrashed();
}
public function maintenanceType()
{
return $this->belongsTo(MaintenanceType::class, 'maintenance_type_id');
}
public function responsibleParty()
{
return $this->belongsTo(User::class, 'responsible_party_id')
->withTrashed();
}
public function completedByUser()
{
return $this->belongsTo(User::class, 'completed_by')
->withTrashed();
}
public function checkedOutTo()
{
return $this->morphTo('checked_out_to');
}
public function journal()
{
return $this->assetlog()->where('action_type', '=', 'note added');
}
public function getDisplayNameAttribute()
{
return $this->name;
}
public function newEloquentBuilder($query): MaintenanceQueryBuilder
/**
* -----------------------------------------------
* BEGIN QUERY SCOPES
* -----------------------------------------------
**/
/**
* Query builder scope to order on a supplier
*
* @param Builder $query Query builder instance
* @param string $order Order
* @return Builder Modified query builder
*/
public function scopeOrderBySupplier($query, $order)
{
return new MaintenanceQueryBuilder($query);
return $query->leftJoin('suppliers as suppliers_maintenances', 'maintenances.supplier_id', '=', 'suppliers_maintenances.id')
->orderBy('suppliers_maintenances.name', $order);
}
/**
* Query builder scope to order on asset tag
*
* @param Builder $query Query builder instance
* @param string $order Order
* @return Builder Modified query builder
*/
public function scopeOrderByTag($query, $order)
{
return $query->leftJoin('assets', 'maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.asset_tag', $order);
}
/**
* Query builder scope to order on asset tag
*
* @param Builder $query Query builder instance
* @param string $order Order
* @return Builder Modified query builder
*/
public function scopeOrderByAssetName($query, $order)
{
return $query->leftJoin('assets', 'maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.name', $order);
}
/**
* Query builder scope to order on serial
*
* @param Builder $query Query builder instance
* @param string $order Order
* @return Builder Modified query builder
*/
public function scopeOrderByAssetSerial($query, $order)
{
return $query->leftJoin('assets', 'maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.serial', $order);
}
/**
* Query builder scope to order on status label name
*
* @param Builder $query Query builder instance
* @param text $order Order
* @return Builder Modified query builder
*/
public function scopeOrderStatusName($query, $order)
{
return $query->join('assets as maintained_asset', 'maintenances.asset_id', '=', 'maintained_asset.id')
->leftjoin('status_labels as maintained_asset_status', 'maintained_asset_status.id', '=', 'maintained_asset.status_id')
->orderBy('maintained_asset_status.name', $order);
}
/**
* Query builder scope to order on status label name
*
* @param Builder $query Query builder instance
* @param text $order Order
* @return Builder Modified query builder
*/
public function scopeOrderLocationName($query, $order)
{
return $query->join('assets as maintained_asset', 'maintenances.asset_id', '=', 'maintained_asset.id')
->leftjoin('locations as maintained_asset_location', 'maintained_asset_location.id', '=', 'maintained_asset.location_id')
->orderBy('maintained_asset_location.name', $order);
}
/**
* Query builder scope to order on the user that created it
*/
public function scopeOrderByCreatedBy($query, $order)
{
return $query->leftJoin('users as admin_sort', 'maintenances.created_by', '=', 'admin_sort.id')->select('maintenances.*')->orderBy('admin_sort.first_name', $order)->orderBy('admin_sort.last_name', $order);
}
public function scopeOrderByAssetModelName($query, $order)
{
return $query->join('assets as maintained_asset', 'maintenances.asset_id', '=', 'maintained_asset.id')
->leftjoin('models as maintained_asset_model', 'maintained_asset_model.id', '=', 'maintained_asset.model_id')
->orderBy('maintained_asset_model.name', $order);
}
public function scopeOrderByAssetModelNumber($query, $order)
{
return $query->join('assets as maintained_asset', 'maintenances.asset_id', '=', 'maintained_asset.id')
->leftjoin('models as maintained_asset_model', 'maintained_asset_model.id', '=', 'maintained_asset.model_id')
->orderBy('maintained_asset_model.model_number', $order);
}
}
-43
View File
@@ -1,43 +0,0 @@
<?php
namespace App\Models;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Gate;
use Watson\Validating\ValidatingTrait;
class MaintenanceType extends SnipeModel
{
use HasFactory;
use Presentable;
use SoftDeletes;
use ValidatingTrait;
protected $table = 'maintenance_types';
protected $rules = [
'name' => 'required|max:100|unique:maintenance_types,name,NULL,id,deleted_at,NULL',
];
protected $injectUniqueIdentifier = true;
protected $fillable = ['name'];
public function isDeletable(): bool
{
return Gate::allows('delete', $this)
&& ($this->deleted_at == '');
}
public function maintenances()
{
return $this->hasMany(Maintenance::class, 'maintenance_type_id');
}
public function getDisplayNameAttribute(): string
{
return $this->name;
}
}
+78 -83
View File
@@ -2,6 +2,10 @@
namespace App\Models;
use ArieTimmerman\Laravel\SCIMServer\Exceptions\SCIMException;
use ArieTimmerman\Laravel\SCIMServer\Helper;
use ArieTimmerman\Laravel\SCIMServer\Parser\Path;
use ArieTimmerman\Laravel\SCIMServer\SCIM\Schema;
use ArieTimmerman\Laravel\SCIMServer\Attribute\Attribute;
use ArieTimmerman\Laravel\SCIMServer\Attribute\Collection;
use ArieTimmerman\Laravel\SCIMServer\Attribute\Complex;
@@ -11,10 +15,9 @@ use ArieTimmerman\Laravel\SCIMServer\Attribute\JSONCollection;
use ArieTimmerman\Laravel\SCIMServer\Attribute\Meta;
use ArieTimmerman\Laravel\SCIMServer\Attribute\MutableCollection;
use ArieTimmerman\Laravel\SCIMServer\Attribute\Schema as AttributeSchema;
use ArieTimmerman\Laravel\SCIMServer\Exceptions\SCIMException;
use ArieTimmerman\Laravel\SCIMServer\Parser\Path;
use ArieTimmerman\Laravel\SCIMServer\SCIM\Schema;
use Illuminate\Database\Eloquent\Model;
use ArieTimmerman\Laravel\SCIMServer\Attribute\AttributeMapping;
use ArieTimmerman\Laravel\SCIMServer\SCIMConfig;
function a($name = null): Attribute
{
@@ -33,10 +36,11 @@ function eloquent($name, $attribute = null): Attribute
class EloquentWithRemove extends Eloquent
{
public function remove($value, Model &$object, ?Path $path = null)
public function remove($value, Model &$object, Path $path = null)
{
$object->{$this->attribute} = null;
}
}
class MappedTable extends Attribute
@@ -66,48 +70,52 @@ class MappedTable extends Attribute
$object->{$this->relationship_id_field} = $value ? $this->relationship_class::firstOrCreate([$this->relationship_field => $value])->id : null;
}
public function patch($operation, $value, Model &$object, ?Path $path = null, $removeIfNotSet = false)
public function patch($operation, $value, Model &$object, Path $path = null, $removeIfNotSet = false)
{
$object->{$this->relationship_id_field} = $value ? $this->relationship_class::firstOrCreate([$this->relationship_field => $value])->id : null;
}
}
class UpdatableComplex extends Complex
{
public function doWrite($operation, $subop, $value, Model &$object, ?Path $path = null, $removeIfNotSet = false)
public function doWrite($operation, $subop, $value, Model &$object, Path $path = null, $removeIfNotSet = false)
{
throw new \Exception("doWrite is not implemented yet for Operation: $operation ".($subop ? "($subop)" : '').'on attribute '.$this->getFullKey());
throw new \Exception("doWrite is not implemented yet for Operation: $operation " . ($subop ? "($subop)" : "") . "on attribute " . $this->getFullKey());
}
public function add($value, Model &$object)
{
$this->doWrite('add', null, $value, $object);
$this->doWrite("add", null, $value, $object);
}
public function replace($value, Model &$object, ?Path $path = null, $removeIfNotSet = false)
public function replace($value, Model &$object, Path $path = null, $removeIfNotSet = false)
{
$this->doWrite('replace', null, $value, $object, $path, $removeIfNotSet);
$this->doWrite("replace", null, $value, $object, $path, $removeIfNotSet);
}
public function patch($operation, $value, Model &$object, ?Path $path = null, $removeIfNotSet = false)
public function patch($operation, $value, Model &$object, Path $path = null, $removeIfNotSet = false)
{
$this->doWrite('patch', $operation, $value, $object, $path, $removeIfNotSet);
$this->doWrite("patch", $operation, $value, $object, $path, $removeIfNotSet);
}
public function remove($value, Model &$object, ?Path $path = null)
public function remove($value, Model &$object, Path $path = null)
{
$this->doWrite('remove', null, null, $object, $path);
$this->doWrite("remove", null, null, $object, $path);
}
}
class SnipeSCIMConfig
{
public function __construct() {}
public function __construct()
{
}
public function getConfigForResource($name)
{
$result = $this->getConfig();
return @$result[$name];
}
@@ -117,7 +125,6 @@ class SnipeSCIMConfig
}
const ENTERPRISE = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User';
const GROKABILITY = 'urn:ietf:params:scim:schemas:extension:grokability:2.0:User';
public function getUserConfig()
@@ -133,19 +140,21 @@ class SnipeSCIMConfig
'description' => 'User Account',
'map' => complex()->withSubAttributes(
new class('schemas', ['urn:ietf:params:scim:schemas:core:2.0:User', self::ENTERPRISE, self::GROKABILITY]) extends Constant
{
new class ('schemas', [
"urn:ietf:params:scim:schemas:core:2.0:User",
self::ENTERPRISE,
self::GROKABILITY
]) extends Constant {
public function replace($value, &$object, $path = null)
{
// do nothing
$this->dirty = true;
}
},
(new class('id', null) extends Constant // TODO - this 'id' is in the same namespace for objects OR groups?
{
(new class ('id', null) extends Constant { // TODO - this 'id' is in the same namespace for objects OR groups?
protected function doRead(&$object, $attributes = [])
{
return (string) $object->id;
return (string)$object->id;
}
public function remove($value, &$object, $path = null)
@@ -157,97 +166,90 @@ class SnipeSCIMConfig
new Meta('Users'),
(new AttributeSchema(Schema::SCHEMA_USER, true))->withSubAttributes(
eloquent('userName', 'username')->ensure('required'),
(new class('active', 'activated') extends Eloquent
{
(new class ('active', 'activated') extends Eloquent {
protected function doRead(&$object, $attributes = [])
{
return (bool) $object->activated; // need this extension to force boolean-ness
return (bool)$object->activated; // need this extension to force boolean-ness
}
}),
complex('name')->withSubAttributes(
eloquent('givenName', 'first_name')->ensure('required'),
eloquent('familyName', 'last_name'),
), // ->ensure('required'), It *is* a bit weird, but I would've thought 'name' is required since 'givenName' is required? But apparently not?
eloquent('displayName', 'display_name'), // yes, this is *not* under 'name' - that's the spec
// eloquent('password')->ensure('nullable')->setReturned('never'),
eloquent('displayName', 'display_name'), //yes, this is *not* under 'name' - that's the spec
//eloquent('password')->ensure('nullable')->setReturned('never'),
eloquent('externalId', 'scim_externalid'),
// Email chonk
(new class('emails') extends UpdatableComplex
{
(new class ('emails') extends UpdatableComplex {
protected function doRead(&$object, $attributes = [])
{
return collect([$object->email])->map(function ($email) {
return [
'value' => $email,
'type' => 'work', // TODO - is this how we always have done it?
'primary' => true,
'type' => 'work', //TODO - is this how we always have done it?
'primary' => true
];
})->toArray();
}
public function doWrite($operation, $subop, $value, Model &$object, ?Path $path = null, $removeIfNotSet = false)
public function doWrite($operation, $subop, $value, Model &$object, Path $path = null, $removeIfNotSet = false)
{
if ($value) {
try {
$object->email = $value[0]['value'];
} catch (\Throwable $e) {
\Log::debug($e);
throw new SCIMException("Unknown email object: '".print_r($value, true)."'", 422);
throw new SCIMException("Unknown email object: '" . print_r($value, true) . "'", 422);
}
} else {
$object->email = null;
}
}
})->withSubAttributes(
eloquent('value', 'email')->ensure('email', 'nullable'), // Weird, this 'needs' nullable to work?
eloquent('value', 'email')->ensure('email', 'nullable'), //Weird, this 'needs' nullable to work?
new Constant('type', 'work'),
(new Constant('primary', true))->ensure('boolean')
)->ensure('array')
->setMultiValued(true),
// phone chonk
(new class('phoneNumbers') extends UpdatableComplex
{
(new class ('phoneNumbers') extends UpdatableComplex {
protected function doRead(&$object, $attributes = [])
{
$phones = [];
if ($object->phone) {
$phones[] = [
'value' => $object->phone,
'type' => 'work',
'type' => 'work'
];
}
if ($object->mobile) {
$phones[] = [
'value' => $object->mobile,
'type' => 'mobile',
'type' => 'mobile'
];
}
return $phones;
}
public function doWrite($operation, $subop, $value, Model &$object, ?Path $path = null, $removeIfNotSet = false)
public function doWrite($operation, $subop, $value, Model &$object, Path $path = null, $removeIfNotSet = false)
{
\Log::debug("Phones 'value' is: ".print_r($value, true));
\Log::debug("Phones 'value' is: " . print_r($value, true));
try {
if ($operation == 'patch') {
if ($operation == "patch") {
if ($path->getValuePathFilter() != null) {
if ((string) $path == 'phoneNumbers[type eq "mobile"].value') {
$object->mobile = $value; // I don't know why the value is the raw value, but it is?
$object->mobile = $value; //I don't know why the value is the raw value, but it is?
return;
}
if ((string) $path == 'phoneNumbers[type eq "work"].value') {
$object->phone = $value; // similar, don't know why, but it is
$object->phone = $value; //similar, don't know why, but it is
return;
}
}
parent::patch($subop, $value, $object, $path, $removeIfNotSet);
return;
}
foreach ($value as $phone) {
@@ -261,14 +263,15 @@ class SnipeSCIMConfig
break;
default:
throw new SCIMException("Unknown phone type '".@$phone['type']."'", 400);
throw new SCIMException("Unknown phone type '" . @$phone['type'] . "'", 400);
}
}
} catch (\Throwable $e) {
\Log::debug($e);
throw new SCIMException("Unknown phone object(s) '".print_r($value, true)."'", 422);
throw new SCIMException("Unknown phone object(s) '" . print_r($value, true) . "'", 422);
}
}
})->withSubAttributes( // TODO: I suspect these 'sub-attributes' aren't being checked at all
(new Constant('value', 'email'))->ensure('string'), // TODO - this is WRONG, but it works somehow? Probably because it's ignored
new Constant('type', 'other'), // TODO uh, *also* wrong? but, again, seems to be ignored
@@ -276,14 +279,13 @@ class SnipeSCIMConfig
->setMultiValued(true),
// addresses chonk
(new class('addresses') extends UpdatableComplex
{
public static $addressmap = [
(new class ('addresses') extends UpdatableComplex {
static $addressmap = [
'streetAddress' => 'address',
'locality' => 'city',
'region' => 'state',
'postalCode' => 'zip',
'country' => 'country',
'country' => 'country'
];
protected function doRead(&$object, $attributes = [])
@@ -298,11 +300,10 @@ class SnipeSCIMConfig
$address['type'] = 'work';
$address['primary'] = true;
}
return $address;
}
public function doWrite($operation, $subop, $value, Model &$object, ?Path $path = null, $removeIfNotSet = false)
public function doWrite($operation, $subop, $value, Model &$object, Path $path = null, $removeIfNotSet = false)
{
// TODO - this is validated *just* for 'patch' operations, so this may not work in other write contexts
if ($path->getValuePathFilter() != null) {
@@ -310,7 +311,7 @@ class SnipeSCIMConfig
// get the part of the $path that we actually care about - something like:
// addresses[type eq "work"]
$matches = null;
if (! preg_match('/^.+\[type eq "([a-zA-Z]+)"](?:\.([a-zA-Z]+))?$/', (string) $path, $matches)) {
if (!preg_match('/^.+\[type eq "([a-zA-Z]+)"](?:\.([a-zA-Z]+))?$/', (string)$path, $matches)) {
throw new SCIMException("Unknown path type '$path'", 422);
}
$type = $matches[1];
@@ -320,13 +321,14 @@ class SnipeSCIMConfig
$attribute = array_key_exists(2, $matches) ? $matches[2] : null;
if (array_key_exists($attribute, self::$addressmap)) {
$object->{self::$addressmap[$attribute]} = $value;
return;
}
throw new SCIMException("Could not handle path for update $path", 422);
}
}
})->withSubAttributes(
eloquent('streetAddress', 'address'),
eloquent('locality', 'city'),
@@ -342,15 +344,14 @@ class SnipeSCIMConfig
eloquent('preferredLanguage', 'locale'),
(new Collection('groups'))->withSubAttributes(
eloquent('value', 'id'),
(new class('$ref') extends Eloquent
{
(new class ('$ref') extends Eloquent {
protected function doRead(&$object, $attributes = [])
{
return route(
'scim.resource',
[
'resourceType' => 'Group',
'resourceObject' => $object->id ?? 'not-saved',
'resourceObject' => $object->id ?? "not-saved"
]
);
}
@@ -367,16 +368,14 @@ class SnipeSCIMConfig
(new AttributeSchema(self::ENTERPRISE, false))->withSubAttributes(
eloquent('employeeNumber', 'employee_num')->ensure('nullable'),
new MappedTable('department', 'department', Department::class, 'department_id', 'name'),
(new class('manager') extends UpdatableComplex
{
(new class('manager') extends UpdatableComplex {
protected function doRead(&$object, $attributes = [])
{
if (! $object->manager) {
if (!$object->manager) {
return null;
}
return [
'value' => $object->manager->id, // TODO - ID's aren't unique like they're supposed to be :/
'value' => $object->manager->id, //TODO - ID's aren't unique like they're supposed to be :/
'$ref' => route('scim.resource', ['resourceType' => 'User', 'resourceObject' => $object->manager->id]),
'displayName' => $object->manager->display_name,
];
@@ -384,12 +383,10 @@ class SnipeSCIMConfig
public function doWrite($operation, $subop, $value, Model &$object, $path = null, $removeIfNotSet = false)
{
\Log::debug('What type of value is value? '.gettype($value));
\Log::debug("What type of value is value? " . gettype($value));
$manager_id = null;
if (is_null($value)) {
// nothing to do
} elseif (is_scalar($value)) {
\Log::debug('Weird Microsoft mode - set manager to the $value and move on with life?');
if (is_scalar($value)) {
\Log::debug("Weird Microsoft mode - set manager to the \$value and move on with life?");
$manager_id = $value;
} elseif (array_key_exists('$ref', $value)) {
// Here's the spec: https://datatracker.ietf.org/doc/html/rfc7643#section-4.3
@@ -399,8 +396,8 @@ class SnipeSCIMConfig
// extract ID from URL, jam it in?
$url = $value['$ref'];
$users_prefix = route('scim.resources', ['resourceType' => 'User']).'/';
if (str_starts_with($url, $users_prefix)) {
$users_prefix = route('scim.resources', ['resourceType' => 'User']) . '/';
if (string_starts_with($url, $users_prefix)) {
$manager_id = substr($url, strlen($users_prefix));
}
} elseif (array_key_exists('value', $value)) {
@@ -410,10 +407,9 @@ class SnipeSCIMConfig
// that, at least, is the spec - but *what* ID is that?! It's supposed to be a Snipe-IT one!
$manager_id = $value['value'];
}
\Log::debug("Non-Microsoft - Trying to '$operation' for manager with value: ".print_r($value, true));
\Log::debug("Non-Microsoft - Trying to '$operation' for manager with value: " . print_r($value, true));
if ($manager_id && User::find($manager_id)) {
$object->manager_id = $manager_id;
return;
}
throw new SCIMException("No manager given, or manager doesn't exist", 400);
@@ -435,24 +431,24 @@ class SnipeSCIMConfig
'class' => $this->getGroupClass(),
'singular' => 'Group',
// eager loading
//eager loading
'withRelations' => [],
'description' => 'Group',
'map' => complex()->withSubAttributes(
new class('schemas', ['urn:ietf:params:scim:schemas:core:2.0:Group']) extends Constant
{
new class ('schemas', [
"urn:ietf:params:scim:schemas:core:2.0:Group",
]) extends Constant {
public function replace($value, &$object, $path = null)
{
// do nothing
$this->dirty = true;
}
},
(new class('id', null) extends Constant
{
(new class ('id', null) extends Constant {
protected function doRead(&$object, $attributes = [])
{
return (string) $object->id;
return (string)$object->id;
}
public function remove($value, &$object, $path = null)
@@ -473,15 +469,14 @@ class SnipeSCIMConfig
}),
(new MutableCollection('members'))->withSubAttributes(
eloquent('value', 'id')->ensure('required'),
(new class('$ref') extends Eloquent
{
(new class ('$ref') extends Eloquent {
protected function doRead(&$object, $attributes = [])
{
return route(
'scim.resource',
[
'resourceType' => 'Users',
'resourceObject' => $object->id ?? 'not-saved',
'resourceObject' => $object->id ?? "not-saved"
]
);
}
+1 -1
View File
@@ -53,7 +53,7 @@ trait Searchable
*/
public function scopeTextSearch($query, $search)
{
$preparedSearch = $this->prepareSearchInput(is_array($search) ? implode(' ', $search) : (string) $search);
$preparedSearch = $this->prepareSearchInput((string) $search);
$terms = $preparedSearch['terms'];
$filters = $preparedSearch['filters'];
$filterOperator = $preparedSearch['filter_operator'];
+3 -75
View File
@@ -18,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;
@@ -60,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',
@@ -174,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'],
];
@@ -252,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();
@@ -620,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
*
@@ -787,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();
}
/**
@@ -1401,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);
}
/**
@@ -1463,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)
@@ -1505,7 +1434,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
])
->withTrashed();
}
/**
* Get all direct and indirect subordinates for this user.
*
+3 -3
View File
@@ -35,12 +35,12 @@ class AssetObserver
$same_checkin_counter = (($attributes['checkin_counter'] == $attributesOriginal['checkin_counter']));
}
// If the asset isn't being checked out, log the update.
// (Checkout/checkin/audit actions already create their own log entries; the audit
// path uses unsetEventDispatcher() so it never reaches this observer.)
// If the asset isn't being checked out or audited, log the update.
// (Those other actions already create log entries.)
if (array_key_exists('assigned_to', $attributes) && array_key_exists('assigned_to', $attributesOriginal)
&& ($attributes['assigned_to'] == $attributesOriginal['assigned_to'])
&& ($same_checkout_counter) && ($same_checkin_counter)
&& ((isset($attributes['next_audit_date']) ? $attributes['next_audit_date'] : null) == (isset($attributesOriginal['next_audit_date']) ? $attributesOriginal['next_audit_date'] : null))
&& ($attributes['last_checkout'] == $attributesOriginal['last_checkout']) && (! $restoring_or_deleting)) {
$changed = [];
-26
View File
@@ -5,23 +5,9 @@ namespace App\Observers;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Maintenance;
use App\Models\MaintenanceType;
class MaintenanceObserver
{
/**
* Capture the asset's current checkout state before the maintenance record is saved.
*/
public function creating(Maintenance $maintenance): void
{
if ($maintenance->asset_id && $asset = Asset::find($maintenance->asset_id)) {
$maintenance->checked_out_to_id = $asset->assigned_to;
$maintenance->checked_out_to_type = $asset->assigned_type;
}
$this->syncLegacyMaintenanceType($maintenance);
}
/**
* Listen to the User created event.
*
@@ -29,8 +15,6 @@ class MaintenanceObserver
*/
public function updating(Maintenance $maintenance)
{
$this->syncLegacyMaintenanceType($maintenance);
$changed = [];
foreach ($maintenance->getRawOriginal() as $key => $value) {
@@ -63,16 +47,6 @@ class MaintenanceObserver
$logAction->logaction('update');
}
private function syncLegacyMaintenanceType(Maintenance $maintenance): void
{
if ($maintenance->maintenance_type_id && ! $maintenance->asset_maintenance_type) {
$type = MaintenanceType::find($maintenance->maintenance_type_id);
if ($type) {
$maintenance->asset_maintenance_type = $type->name;
}
}
}
/**
* Listen to the Component created event when
* a new component is created.
+11 -42
View File
@@ -16,8 +16,6 @@ class UserObserver
{
// ONLY allow these fields to be stored
// NOTE: company_id is intentionally excluded — company membership changes are logged
// via User::syncCompaniesWithLogging() against the pivot table instead.
$allowed_fields = [
'email',
'activated',
@@ -33,6 +31,7 @@ class UserObserver
'employee_num',
'username',
'notes',
'company_id',
'ldap_import',
'locale',
'two_factor_enrolled',
@@ -59,44 +58,18 @@ class UserObserver
// Make sure the info is in the allow fields array
if (in_array($key, $allowed_fields)) {
$oldValue = $user->getRawOriginal()[$key];
$newValue = $user->getAttributes()[$key];
// Check and see if the value changed
if ($user->getRawOriginal()[$key] != $user->getAttributes()[$key]) {
if ($key === 'permissions') {
// Compare decoded to avoid spurious diffs from key reordering or type coercion.
$oldDecoded = json_decode($oldValue ?? '{}', true) ?: [];
$newDecoded = json_decode($newValue ?? '{}', true) ?: [];
if ($oldDecoded == $newDecoded) {
continue;
$changed[$key]['old'] = $user->getRawOriginal()[$key];
$changed[$key]['new'] = $user->getAttributes()[$key];
// Do not store the hashed password in changes
if ($key == 'password') {
$changed['password']['old'] = '*************';
$changed['password']['new'] = '*************';
}
// Only log the permission keys that actually changed.
$diffOld = [];
$diffNew = [];
foreach (array_unique(array_merge(array_keys($oldDecoded), array_keys($newDecoded))) as $permKey) {
$oldPerm = $oldDecoded[$permKey] ?? null;
$newPerm = $newDecoded[$permKey] ?? null;
if ($oldPerm != $newPerm) {
$diffOld[$permKey] = $oldPerm;
$diffNew[$permKey] = $newPerm;
}
}
$changed['permissions']['old'] = json_encode($diffOld);
$changed['permissions']['new'] = json_encode($diffNew);
continue;
}
if ($oldValue == $newValue) {
continue;
}
$changed[$key]['old'] = $oldValue;
$changed[$key]['new'] = $newValue;
// Do not store the hashed password in changes
if ($key == 'password') {
$changed['password']['old'] = '*************';
$changed['password']['new'] = '*************';
}
}
@@ -106,16 +79,12 @@ class UserObserver
$logAction = new Actionlog;
$logAction->item_type = User::class;
$logAction->item_id = $user->id;
$logAction->target_type = User::class;
$logAction->target_type = User::class; // can we instead say $logAction->item = $asset ?
$logAction->target_id = $user->id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->created_by = auth()->id();
$logAction->log_meta = json_encode($changed);
$logAction->logaction('update');
// Let syncCompaniesWithLogging() merge company changes into this entry
// rather than creating a separate log row for the same edit session.
$user->currentUpdateLogId = $logAction->id;
}
}
+1 -10
View File
@@ -37,12 +37,8 @@ final class MaintenancePolicy
* Determine whether the user can view a specific maintenance record.
* Allowed if the user can edit the associated asset.
*/
public function view(User $user, ?Maintenance $maintenance = null): bool
public function view(User $user, Maintenance $maintenance): bool
{
if (is_null($maintenance)) {
return $user->hasAccess('assets.view');
}
return Gate::allows('update', $maintenance->asset);
}
@@ -98,9 +94,4 @@ final class MaintenancePolicy
|| Gate::allows('view', $maintenance)
|| $user->hasAccess('activity.view');
}
public function journal(User $user, Maintenance $maintenance): bool
{
return Gate::allows('view', $maintenance) || $user->hasAccess('activity.view');
}
}
-11
View File
@@ -1,11 +0,0 @@
<?php
namespace App\Policies;
class MaintenanceTypePolicy extends SnipePermissionsPolicy
{
protected function columnName()
{
return 'maintenances';
}
}
-9
View File
@@ -218,15 +218,6 @@ class AccessoryPresenter extends Presenter
'visible' => true,
'formatter' => 'polymorphicItemFormatter',
],
[
'field' => 'assigned_to.companies',
'searchable' => true,
'sortable' => false,
'switchable' => true,
'title' => trans('general.companies'),
'visible' => true,
'formatter' => 'companiesArrayLinkFormatter',
],
[
'field' => 'note',
'searchable' => false,
-9
View File
@@ -461,15 +461,6 @@ class AssetPresenter extends Presenter
}
}
public function formattedNameLink()
{
if (auth()->user()->can('view', ['\App\Models\Asset', $this])) {
return '<a href="'.route('hardware.show', e($this->id)).'" class="'.(($this->deleted_at != '') ? 'deleted' : '').'">'.e($this->display_name).'</a>';
}
return '<span class="'.(($this->deleted_at != '') ? 'deleted' : '').'">'.e($this->display_name).'</span>';
}
public function modelUrl()
{
if ($this->model->model) {
-1
View File
@@ -18,7 +18,6 @@ class CategoryPresenter extends Presenter
[
'field' => 'checkbox',
'checkbox' => true,
'formatter' => 'checkboxEnabledFormatter',
'titleTooltip' => trans('general.select_all_none'),
'printIgnore' => true,
'class' => 'hidden-print',
+33 -58
View File
@@ -240,53 +240,41 @@ class LicensePresenter extends Presenter
*
* @return string
*/
public static function dataTableLayoutSeats(bool $withCheckbox = true)
public static function dataTableLayoutSeats()
{
$layout = [];
if ($withCheckbox) {
$layout[] = [
'field' => 'checkbox',
'checkbox' => true,
'formatter' => 'checkboxEnabledFormatter',
'titleTooltip' => trans('general.select_all_none'),
'printIgnore' => true,
'class' => 'hidden-print',
];
}
$layout = array_merge($layout, [[
'field' => 'id',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('general.id'),
'visible' => false,
], [
'field' => 'assigned_user',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('admin/licenses/general.user'),
'visible' => true,
'formatter' => 'usersLinkObjFormatter',
], [
'field' => 'assigned_user.email',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('admin/users/table.email'),
'visible' => true,
'formatter' => 'emailFormatter',
],
$layout = [
[
'field' => 'assigned_user.companies',
'field' => 'id',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('general.id'),
'visible' => false,
], [
'field' => 'assigned_user',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('general.companies'),
'title' => trans('admin/licenses/general.user'),
'visible' => true,
'formatter' => 'companiesArrayLinkFormatter',
'formatter' => 'usersLinkObjFormatter',
], [
'field' => 'assigned_user.email',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('admin/users/table.email'),
'visible' => true,
'formatter' => 'emailFormatter',
],
[
'field' => 'assigned_user.company',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('general.company'),
'visible' => true,
'formatter' => 'companiesLinkObjFormatter',
],
[
'field' => 'assigned_user.department',
@@ -340,27 +328,14 @@ class LicensePresenter extends Presenter
'printIgnore' => true,
'class' => 'hidden-print',
],
]);
];
return json_encode($layout);
}
public static function dataTableLayoutSeatsCheckedOutToAssets($hide_fields = [])
public static function dataTableLayoutSeatsCheckedOutToAssets()
{
$layout = [];
if (! in_array('checkbox', $hide_fields)) {
$layout[] = [
'field' => 'checkbox',
'checkbox' => true,
'formatter' => 'checkboxEnabledFormatter',
'titleTooltip' => trans('general.select_all_none'),
'printIgnore' => true,
'class' => 'hidden-print',
];
}
$layout = array_merge($layout, [
$layout = [
[
'field' => 'id',
'searchable' => false,
@@ -411,7 +386,7 @@ class LicensePresenter extends Presenter
'printIgnore' => true,
'class' => 'hidden-print',
],
]);
];
return json_encode($layout);
}
@@ -1,54 +0,0 @@
<?php
namespace App\Presenters;
class MaintenanceTypePresenter extends Presenter
{
public static function dataTableLayout(): string
{
$layout = [
[
'field' => 'id',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('general.id'),
'visible' => false,
], [
'field' => 'name',
'searchable' => true,
'sortable' => true,
'switchable' => false,
'title' => trans('general.name'),
'visible' => true,
], [
'field' => 'created_at',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('general.created_at'),
'visible' => false,
'formatter' => 'dateDisplayFormatter',
], [
'field' => 'updated_at',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('general.updated_at'),
'visible' => false,
'formatter' => 'dateDisplayFormatter',
], [
'field' => 'actions',
'searchable' => false,
'sortable' => false,
'switchable' => false,
'title' => trans('table.actions'),
'visible' => true,
'formatter' => 'maintenanceTypesActionsFormatter',
'printIgnore' => true,
],
];
return json_encode($layout);
}
}
+3 -69
View File
@@ -88,7 +88,7 @@ class MaintenancesPresenter extends Presenter
'switchable' => true,
'title' => trans('general.model_no'),
'visible' => true,
], [
],[
'field' => 'assigned_to',
'searchable' => true,
'sortable' => true,
@@ -111,43 +111,10 @@ class MaintenancesPresenter extends Presenter
'title' => trans('general.location'),
'formatter' => 'locationsLinkObjFormatter',
], [
'field' => 'maintenance_type',
'field' => 'asset_maintenance_type',
'searchable' => true,
'sortable' => true,
'switchable' => true,
'title' => trans('admin/maintenances/form.asset_maintenance_type'),
'visible' => true,
], [
'field' => 'responsible_party',
'searchable' => true,
'sortable' => false,
'switchable' => true,
'title' => trans('admin/maintenances/form.responsible_party'),
'visible' => false,
'formatter' => 'usersLinkObjFormatter',
], [
'field' => 'checked_out_to_at_creation',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('admin/maintenances/form.checked_out_to_at_creation'),
'visible' => false,
], [
'field' => 'completed_at',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('admin/maintenances/form.completed_at'),
'visible' => false,
'formatter' => 'dateDisplayFormatter',
], [
'field' => 'completed_by',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('admin/maintenances/form.completed_by'),
'visible' => false,
'formatter' => 'usersLinkObjFormatter',
], [
'field' => 'start_date',
'searchable' => true,
@@ -307,27 +274,10 @@ class MaintenancesPresenter extends Presenter
'sortable' => true,
'title' => trans('general.location'),
], [
'field' => 'maintenance_type',
'field' => 'asset_maintenance_type',
'searchable' => true,
'sortable' => true,
'switchable' => true,
'title' => trans('admin/maintenances/form.asset_maintenance_type'),
'visible' => true,
], [
'field' => 'responsible_party',
'searchable' => true,
'sortable' => false,
'switchable' => true,
'title' => trans('admin/maintenances/form.responsible_party'),
'visible' => false,
'formatter' => 'usersLinkObjFormatter',
], [
'field' => 'checked_out_to_at_creation',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('admin/maintenances/form.checked_out_to_at_creation'),
'visible' => false,
], [
'field' => 'start_date',
'searchable' => true,
@@ -345,22 +295,6 @@ class MaintenancesPresenter extends Presenter
'searchable' => true,
'sortable' => true,
'title' => trans('admin/maintenances/form.asset_maintenance_time'),
], [
'field' => 'completed_at',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('admin/maintenances/form.completed_at'),
'visible' => false,
'formatter' => 'dateDisplayFormatter',
], [
'field' => 'completed_by',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('admin/maintenances/form.completed_by'),
'visible' => false,
'formatter' => 'usersLinkObjFormatter',
], [
'field' => 'url',
'searchable' => true,

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