Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90c8689596 |
@@ -4271,15 +4271,6 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "CybotTM",
|
||||
"name": "Sebastian Mendel",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/326348?v=4",
|
||||
"profile": "https://github.com/CybotTM",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ ENABLE_HSTS=false
|
||||
# --------------------------------------------
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
QUEUE_DRIVER=sync
|
||||
CACHE_PREFIX=snipeit
|
||||
|
||||
# --------------------------------------------
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ ENABLE_HSTS=false
|
||||
# --------------------------------------------
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
QUEUE_DRIVER=sync
|
||||
CACHE_PREFIX=snipeit
|
||||
|
||||
# --------------------------------------------
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ CORS_ALLOWED_ORIGINS="*"
|
||||
# --------------------------------------------
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
QUEUE_DRIVER=sync
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: LOGIN THROTTLING
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
# --------------------------------------------
|
||||
|
||||
@@ -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
@@ -69,7 +69,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
|
||||
| [<img src="https://avatars.githubusercontent.com/u/10965027?v=4" width="110px;"/><br /><sub>Ellie</sub>](https://leafedfox.xyz/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeafedFox "Code") | [<img src="https://avatars.githubusercontent.com/u/20960555?v=4" width="110px;"/><br /><sub>GA Stamper</sub>](https://github.com/gastamper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gastamper "Code") | [<img src="https://avatars.githubusercontent.com/u/206553556?v=4" width="110px;"/><br /><sub>Guillaume Lefranc</sub>](https://github.com/gl-pup)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gl-pup "Code") | [<img src="https://avatars.githubusercontent.com/u/733892?v=4" width="110px;"/><br /><sub>Hajo Möller</sub>](https://github.com/dasjoe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dasjoe "Code") | [<img src="https://avatars.githubusercontent.com/u/3420063?v=4" width="110px;"/><br /><sub>Istvan Basa</sub>](https://github.com/pottom)<br />[💻](https://github.com/snipe/snipe-it/commits?author=pottom "Code") | [<img src="https://avatars.githubusercontent.com/u/810824?v=4" width="110px;"/><br /><sub>JJ Asghar</sub>](https://jjasghar.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jjasghar "Code") | [<img src="https://avatars.githubusercontent.com/u/40404495?v=4" width="110px;"/><br /><sub>James E. Msenga</sub>](https://github.com/JemCdo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JemCdo "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/6865786?v=4" width="110px;"/><br /><sub>Jan Felix Wiebe</sub>](https://github.com/jfwiebe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jfwiebe "Code") | [<img src="https://avatars.githubusercontent.com/u/43412008?v=4" width="110px;"/><br /><sub>Jo Drexl</sub>](https://www.nfon.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=drexljo "Code") | [<img src="https://avatars.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>Austin Sasko</sub>](https://github.com/austinsasko)<br />[💻](https://github.com/snipe/snipe-it/commits?author=austinsasko "Code") | [<img src="https://avatars.githubusercontent.com/u/4875039?v=4" width="110px;"/><br /><sub>Jasson</sub>](http://jassoncordones.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JassonCordones "Code") | [<img src="https://avatars.githubusercontent.com/u/76069640?v=4" width="110px;"/><br /><sub>Okean</sub>](https://github.com/Tinyblargon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tinyblargon "Code") | [<img src="https://avatars.githubusercontent.com/u/6515064?v=4" width="110px;"/><br /><sub>Alejandro Medrano</sub>](https://www.lst.tfo.upm.es/alejandro-medrano/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=amedranogil "Code") | [<img src="https://avatars.githubusercontent.com/u/58696401?v=4" width="110px;"/><br /><sub>Lukas Kraic</sub>](https://github.com/lukaskraic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukaskraic "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/1571724?v=4" width="110px;"/><br /><sub>Герхард PICCORO Lenz McKAY </sub>](https://github-readme-stats.vercel.app/api?username=mckaygerhard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") | [<img src="https://avatars.githubusercontent.com/u/15015119?v=4" width="110px;"/><br /><sub>Johannes Pollitt</sub>](https://github.com/FlorestanII)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorestanII "Code") | [<img src="https://avatars.githubusercontent.com/u/14185442?v=4" width="110px;"/><br /><sub>Michael Strobel</sub>](https://strobelm.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=strobelm "Code") | [<img src="https://avatars.githubusercontent.com/u/634790?v=4" width="110px;"/><br /><sub>Nicky West</sub>](http://nickwest.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nickwest "Code") | [<img src="https://avatars.githubusercontent.com/u/1347327?v=4" width="110px;"/><br /><sub>akaspeh1</sub>](https://github.com/akaspeh1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akaspeh1 "Code") | [<img src="https://avatars.githubusercontent.com/u/2880129?v=4" width="110px;"/><br /><sub>Sebastian Marsching</sub>](http://sebastian.marsching.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smarsching "Code") | [<img src="https://avatars.githubusercontent.com/u/40658372?v=4" width="110px;"/><br /><sub>Mo</sub>](https://github.com/mohammad-ahmadi1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mohammad-ahmadi1 "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/20994684?v=4" width="110px;"/><br /><sub>Owen V. Hayes</sub>](https://github.com/MarvelousAnything)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MarvelousAnything "Code") | [<img src="https://avatars.githubusercontent.com/u/75509373?v=4" width="110px;"/><br /><sub>Peter Gallwas</sub>](https://www.husky.nz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Husky-Devel "Code") | [<img src="https://avatars.githubusercontent.com/u/326348?v=4" width="110px;"/><br /><sub>Sebastian Mendel</sub>](https://github.com/CybotTM)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CybotTM "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/20994684?v=4" width="110px;"/><br /><sub>Owen V. Hayes</sub>](https://github.com/MarvelousAnything)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MarvelousAnything "Code") | [<img src="https://avatars.githubusercontent.com/u/75509373?v=4" width="110px;"/><br /><sub>Peter Gallwas</sub>](https://www.husky.nz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Husky-Devel "Code") |
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
@@ -56,7 +56,6 @@ COPY --from=mlocati/php-extension-installer:2.1.15 /usr/bin/install-php-extensio
|
||||
RUN set -eux; \
|
||||
install-php-extensions \
|
||||
bcmath \
|
||||
exif \
|
||||
gd \
|
||||
ldap \
|
||||
mysqli \
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')));
|
||||
|
||||
|
||||
@@ -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.')');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -30,7 +30,6 @@ class ModalController extends Controller
|
||||
'kit-consumable',
|
||||
'kit-accessory',
|
||||
'location',
|
||||
'maintenance-type',
|
||||
'manufacturer',
|
||||
'model',
|
||||
'statuslabel',
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', [
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class MaintenanceTypePolicy extends SnipePermissionsPolicy
|
||||
{
|
||||
protected function columnName()
|
||||
{
|
||||
return 'maintenances';
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user