Compare commits
156 Commits
mcp
...
purchase-cost-fix
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b1e312292 | |||
| 9004211a59 | |||
| 053eb91457 | |||
| 3810513224 | |||
| e50e0f0e34 | |||
| cdf73f9c89 | |||
| bc808cbe46 | |||
| fdc65fb1b2 | |||
| 3db9a15dd3 | |||
| 42bf43d68d | |||
| 4548ed8a45 | |||
| 407e2d0246 | |||
| 2af7367480 | |||
| 29b9a78f54 | |||
| 382a164b9d | |||
| 9216a7550f | |||
| 4f9ba7c6cc | |||
| afb7c69ac3 | |||
| 5f232c0584 | |||
| ed931d497a | |||
| 59278c3f70 | |||
| 179d031bb2 | |||
| dc1410aa70 | |||
| 0f595a8854 | |||
| 70e1dcf1b4 | |||
| 780e3e1cd9 | |||
| 339c93ebbf | |||
| b4bb1556be | |||
| ba96aa5a61 | |||
| 2171556ec4 | |||
| 2d33368063 | |||
| 93a2f74f9e | |||
| ee61084ac8 | |||
| 8d8a1889cd | |||
| f275cb6928 | |||
| db8de1f794 | |||
| d901e821cc | |||
| 34a533b2d6 | |||
| d3d37c70ab | |||
| 475e674fc6 | |||
| 01436d0532 | |||
| 96bf7d0c2b | |||
| 529973aa77 | |||
| f4cd090ac6 | |||
| 6d5e68274d | |||
| 3e002cb940 | |||
| b7ea9a959c | |||
| dc3a16c437 | |||
| 608af84253 | |||
| 161d7e1c2b | |||
| 8627032c4f | |||
| 5bead4fbcc | |||
| ef44ba5f97 | |||
| 2dc0ec9e7e | |||
| afd435e895 | |||
| 80d1bf6a7a | |||
| 737f3ef3db | |||
| d179f47274 | |||
| 1832d95371 | |||
| a614f986f0 | |||
| f398a59d26 | |||
| c8bd104268 | |||
| 7f01bd4c56 | |||
| 6c3c7fdf49 | |||
| bdc8fc8d4a | |||
| 41be127489 | |||
| fdfae9593d | |||
| 6a21eb53c9 | |||
| efde2b4672 | |||
| daaa26cbf4 | |||
| 3e7441562c | |||
| 7b53fa5245 | |||
| d35d46f5b4 | |||
| f4772a9cad | |||
| 4c1bb7e0ac | |||
| 762ea9b4db | |||
| b16970a61e | |||
| dadb9bd81e | |||
| 13dc7de660 | |||
| 003ea36e18 | |||
| f4bd2a68c9 | |||
| be4e75d4f7 | |||
| 538c21ce1e | |||
| 626cd6cb2e | |||
| 2a56f6573d | |||
| 6ee2dc1cd6 | |||
| 3fcde8bd16 | |||
| e2ff7a7bc7 | |||
| c7efd16517 | |||
| f2907f04d9 | |||
| 7d98c267d5 | |||
| 5bc6330c13 | |||
| 1706ed597d | |||
| 6e9ba28ef7 | |||
| 554d1a44de | |||
| c0a8f4c1a4 | |||
| 08be9aac6d | |||
| a51b17fb53 | |||
| 66d5618d60 | |||
| e16c2384fd | |||
| b3323f08a0 | |||
| 7e63c2ef92 | |||
| 7f65b6d598 | |||
| 8fb8f0a4d2 | |||
| 637dbc8d2a | |||
| 978990fdff | |||
| 52a058e511 | |||
| 64bea202c5 | |||
| 37f60993ca | |||
| 32717c67c7 | |||
| 3681e3f025 | |||
| 1d0f055349 | |||
| fb3024ca9c | |||
| 005c0ea9f6 | |||
| 7c3f1f3a84 | |||
| 900e5209d9 | |||
| 4fbf416d16 | |||
| 7b7d2c87fb | |||
| 6debb3a65d | |||
| 315ba49a1d | |||
| ff57855038 | |||
| da6e837578 | |||
| a2d8f89162 | |||
| e36d65e695 | |||
| 34abf14cbe | |||
| dda7a4f22f | |||
| 283a885196 | |||
| d44aa3f16e | |||
| afb37981bf | |||
| 2b6518427a | |||
| 185e0073b3 | |||
| d0794ba71c | |||
| 1b42e2e138 | |||
| b4efabe82e | |||
| 9b37e95b58 | |||
| a92d8eeaab | |||
| e8dbb12ccc | |||
| 8a2cd19ea6 | |||
| afdf86ad0d | |||
| a5dae3f222 | |||
| 97765c08b1 | |||
| 6ad92556a1 | |||
| e2465ca2a7 | |||
| f5644928a8 | |||
| 8747ff32dd | |||
| 4ddd2f1cf8 | |||
| 11c8fd4d4c | |||
| ab04f3de93 | |||
| 4c16796256 | |||
| 516771d948 | |||
| e25ea465c5 | |||
| 30ac3d1a26 | |||
| e47c772230 | |||
| 706b623d95 | |||
| a908a76f53 | |||
| a2ec707f79 |
@@ -0,0 +1,69 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# EthicalCheck addresses the critical need to continuously security test APIs in development and in production.
|
||||
|
||||
# EthicalCheck provides the industry’s only free & automated API security testing service that uncovers security vulnerabilities using OWASP API list.
|
||||
# Developers relies on EthicalCheck to evaluate every update and release, ensuring that no APIs go to production with exploitable vulnerabilities.
|
||||
|
||||
# You develop the application and API, we bring complete and continuous security testing to you, accelerating development.
|
||||
|
||||
# Know your API and Applications are secure with EthicalCheck – our free & automated API security testing service.
|
||||
|
||||
# How EthicalCheck works?
|
||||
# EthicalCheck functions in the following simple steps.
|
||||
# 1. Security Testing.
|
||||
# Provide your OpenAPI specification or start with a public Postman collection URL.
|
||||
# EthicalCheck instantly instrospects your API and creates a map of API endpoints for security testing.
|
||||
# It then automatically creates hundreds of security tests that are non-intrusive to comprehensively and completely test for authentication, authorizations, and OWASP bugs your API. The tests addresses the OWASP API Security categories including OAuth 2.0, JWT, Rate Limit etc.
|
||||
|
||||
# 2. Reporting.
|
||||
# EthicalCheck generates security test report that includes all the tested endpoints, coverage graph, exceptions, and vulnerabilities.
|
||||
# Vulnerabilities are fully triaged, it contains CVSS score, severity, endpoint information, and OWASP tagging.
|
||||
|
||||
|
||||
# This is a starter workflow to help you get started with EthicalCheck Actions
|
||||
|
||||
name: EthicalCheck-Workflow
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events but only for the "master" branch
|
||||
# Customize trigger events based on your DevSecOps processes.
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '35 17 * * 6'
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
Trigger_EthicalCheck:
|
||||
permissions:
|
||||
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
|
||||
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: EthicalCheck Free & Automated API Security Testing Service
|
||||
uses: apisec-inc/ethicalcheck-action@005fac321dd843682b1af6b72f30caaf9952c641
|
||||
with:
|
||||
# The OpenAPI Specification URL or Swagger Path or Public Postman collection URL.
|
||||
oas-url: "http://netbanking.apisec.ai:8080/v2/api-docs"
|
||||
# The email address to which the penetration test report will be sent.
|
||||
email: "snipe@snipe.net"
|
||||
sarif-result-file: "ethicalcheck-results.sarif"
|
||||
|
||||
- name: Upload sarif file to repository
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: ./ethicalcheck-results.sarif
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"DOC1": "This file is meant to be pulled from the current HEAD of the desired branch, NOT referenced locally",
|
||||
"DOC2": "In other words, what you see locally are the requirements for your _current_ install",
|
||||
"DOC3": "Please don't rely on these versions for planning upgrades unless you've fetched the most recent version",
|
||||
"DOC4": "You should really just ignore it and run upgrade.php. Really",
|
||||
"php_min_version": "8.2.0",
|
||||
"php_max_major_minor": "8.4",
|
||||
"php_max_wontwork": "8.5.0",
|
||||
"current_snipeit_version": "8.0"
|
||||
"DOC1": "This file is meant to be pulled from the current HEAD of the desired branch, NOT referenced locally",
|
||||
"DOC2": "In other words, what you see locally are the requirements for your _current_ install",
|
||||
"DOC3": "Please don't rely on these versions for planning upgrades unless you've fetched the most recent version",
|
||||
"DOC4": "You should really just ignore it and run upgrade.php. Really",
|
||||
"php_min_version": "8.2.0",
|
||||
"php_max_major_minor": "8.5",
|
||||
"php_max_wontwork": "8.6.0",
|
||||
"current_snipeit_version": "8.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Stack
|
||||
|
||||
- **PHP 8.2+** / **Laravel 12** (framework), **Laravel Mix** (webpack) for frontend assets
|
||||
- **AdminLTE 2** / **Bootstrap 3** UI — Blade views, no Livewire/Inertia
|
||||
- **Chart.js v2.9.4** — bundled at `public/js/dist/Chart.min.js`; use `horizontalBar` type (v2 API, not v3)
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
php artisan test
|
||||
# or
|
||||
vendor/bin/phpunit
|
||||
|
||||
# Run a single test file
|
||||
php artisan test tests/Feature/Assets/AssetsTest.php
|
||||
|
||||
# Run a specific test method
|
||||
php artisan test --filter testSomeMethod
|
||||
|
||||
# Build frontend assets (dev)
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run prod
|
||||
|
||||
# Laravel Mix watch
|
||||
npm run watch
|
||||
|
||||
# Tinker / REPL
|
||||
php artisan tinker
|
||||
|
||||
# Clear caches after config/route changes
|
||||
php artisan optimize:clear
|
||||
```
|
||||
|
||||
Dev server is served via **Laravel Herd** (`herd coverage` for coverage reports).
|
||||
|
||||
## Architecture
|
||||
|
||||
### Controllers
|
||||
|
||||
Two parallel controller trees:
|
||||
- `app/Http/Controllers/` — web/UI controllers (Blade views)
|
||||
- `app/Http/Controllers/Api/` — REST API controllers (JSON, used by datatables + select2)
|
||||
|
||||
Subdirectory groupings: `Assets/`, `Licenses/`, `Users/`, `Accessories/`, `Consumables/`, `Components/`, `Kits/`, `Account/`, `Auth/`
|
||||
|
||||
### API Pattern
|
||||
|
||||
Every API controller returns data via a **Transformer** (`app/Http/Transformers/`). Never return raw model attributes from API controllers — always pass through the transformer. `DatatablesTransformer` wraps paginated results.
|
||||
|
||||
```php
|
||||
return (new AssetsTransformer)->transformAssets($assets, $assets->count());
|
||||
```
|
||||
|
||||
### Authorization
|
||||
|
||||
All authorization goes through **Policies** (`app/Policies/`). `CheckoutablePermissionsPolicy` is the base for assets/licenses/accessories/consumables — its `checkout()` / `checkin()` methods accept `$item = null` so you can use `@can('checkout', \App\Models\Asset::class)` without an instance.
|
||||
|
||||
### FMCS (Full Multiple Company Support)
|
||||
|
||||
`Setting::getSettings()->full_multiple_companies_support == '1'` gates company-scoped filtering. The select2 API endpoints (`selectlist()` methods) accept a `companyId` query param — apply it like this:
|
||||
|
||||
```php
|
||||
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($request->filled('companyId'))) {
|
||||
$query->where('table.company_id', $request->input('companyId'));
|
||||
}
|
||||
```
|
||||
|
||||
Pass `data-company-id="{{ $user->company_id }}"` in Blade to wire it to select2.
|
||||
|
||||
### Select2 AJAX Dropdowns
|
||||
|
||||
Use `class="js-data-ajax"` with `data-endpoint="hardware|licenses|consumables|..."`. `snipeit.js` auto-initializes these, forwarding `data-company-id` as `companyId` and `data-asset-status-type` as `statusType` to the API.
|
||||
|
||||
### Routes
|
||||
|
||||
All routes are in `routes/web.php` (UI) and `routes/api.php` (API). Breadcrumbs are defined inline using `->breadcrumbs(fn (Trail $trail) => ...)` from `tabuna/breadcrumbs`. Every UI route should have a breadcrumb.
|
||||
|
||||
Note: the `reports/unaccepted_assets` route is named with slashes, not dots — use `route('reports/unaccepted_assets')`.
|
||||
|
||||
### Translations
|
||||
|
||||
String keys live in `resources/lang/en-US/general.php` (and other files in that directory). Always add new UI strings as translation keys rather than hard-coding English.
|
||||
|
||||
### Checkout Redirect Flow
|
||||
|
||||
After checkout, `Helper::getRedirectOption()` reads `$request->redirect_option`. For redirecting back to the assigned user after checkout:
|
||||
- Set `redirect_option=target` in the form
|
||||
- Set `checkout_to_type=user` in the form
|
||||
- Set `assigned_user={{ $user->id }}` in the form
|
||||
|
||||
### Key Helper Methods (`app/Helpers/Helper.php`)
|
||||
|
||||
- `Helper::deployableStatusLabelList()` — status labels for checkout forms
|
||||
- `Helper::defaultChartColors()` — 10-color palette used in charts
|
||||
- `Helper::getRedirectOption($request, $id, $table)` — post-checkout redirect logic
|
||||
|
||||
### Global View Variables
|
||||
|
||||
`$snipeSettings` is injected into all views via a service provider — no need to pass `Setting::getSettings()` from every controller. Use it directly in Blade.
|
||||
|
||||
## Testing
|
||||
|
||||
Tests live in `tests/Feature/` (organized by entity) and `tests/Unit/`. Feature tests hit the database; the test environment uses `array` cache/session/mail drivers. Tests use factories for data setup.
|
||||
@@ -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 11](http://laravel.com).
|
||||
It is built on [Laravel 12](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/).)
|
||||
|
||||
|
||||
@@ -13,8 +13,13 @@ final class PreserveUnauthorizedPrivilegedPermissionsAction
|
||||
* @param array<string, mixed> $originalPermissions
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function run(array $requestedPermissions, User $authenticatedUser, array $originalPermissions = []): array
|
||||
public static function run(array $requestedPermissions, User $authenticatedUser, array $originalPermissions = [], ?User $targetUser = null): 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'];
|
||||
|
||||
@@ -78,6 +78,7 @@ 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';
|
||||
@@ -126,6 +127,7 @@ 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';
|
||||
@@ -156,6 +158,7 @@ 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 CheckInOutRequest;
|
||||
use CheckInOutTrait;
|
||||
|
||||
/**
|
||||
* Return the form to checkout an Accessory to a user.
|
||||
|
||||
@@ -149,6 +149,9 @@ 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) {
|
||||
|
||||
@@ -234,7 +237,7 @@ class AcceptanceController extends Controller
|
||||
|
||||
if ($request->input('asset_acceptance') === 'accepted') {
|
||||
|
||||
$pdf_filename = 'accepted-'.$acceptance->checkoutable_id.'-'.$acceptance->display_checkoutable_type.'-eula-'.date('Y-m-d-h-i-s').'.pdf';
|
||||
$pdf_filename = 'accepted-'.$username_slug.$asset_tag_slug.'-'.date('Y-m-d-h-i-s').'.pdf';
|
||||
|
||||
// Generate the PDF content
|
||||
$pdf_content = $acceptance->generateAcceptancePdf($data, $acceptance);
|
||||
|
||||
@@ -4,26 +4,28 @@ 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;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\AccessoryCheckout;
|
||||
use App\Models\Company;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AccessoriesController extends Controller
|
||||
{
|
||||
use CheckInOutRequest;
|
||||
use CheckInOutTrait;
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
@@ -300,40 +302,49 @@ class AccessoriesController extends Controller
|
||||
{
|
||||
$this->authorize('checkout', $accessory);
|
||||
$target = $this->determineCheckoutTarget();
|
||||
$accessory->checkout_qty = $request->input('checkout_qty', 1);
|
||||
|
||||
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
|
||||
|
||||
$accessory_checkout = new AccessoryCheckout([
|
||||
'accessory_id' => $accessory->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'assigned_to' => $target->id,
|
||||
'assigned_type' => $target::class,
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
|
||||
$accessory_checkout->created_by = auth()->id();
|
||||
$accessory_checkout->save();
|
||||
|
||||
$payload = [
|
||||
'accessory_id' => $accessory->id,
|
||||
'assigned_to' => $target->id,
|
||||
'assigned_type' => $target::class,
|
||||
'note' => $request->input('note'),
|
||||
'created_by' => auth()->id(),
|
||||
'pivot' => $accessory_checkout->id,
|
||||
];
|
||||
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')));
|
||||
}
|
||||
|
||||
// Set this value to be able to pass the qty through to the event
|
||||
event(new CheckoutableCheckedOut(
|
||||
$accessory,
|
||||
$target,
|
||||
auth()->user(),
|
||||
$request->input('note'),
|
||||
[],
|
||||
$accessory->checkout_qty,
|
||||
));
|
||||
$accessory->checkout_qty = $request->input('checkout_qty', 1);
|
||||
$payload = null;
|
||||
|
||||
// Keep checkout rows and checkout log/event atomic to avoid ghost assignments.
|
||||
DB::transaction(function () use ($accessory, $request, $target, &$payload): void {
|
||||
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
|
||||
|
||||
$accessory_checkout = new AccessoryCheckout([
|
||||
'accessory_id' => $accessory->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'assigned_to' => $target->id,
|
||||
'assigned_type' => $target::class,
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
|
||||
$accessory_checkout->created_by = auth()->id();
|
||||
$accessory_checkout->save();
|
||||
|
||||
$payload = [
|
||||
'accessory_id' => $accessory->id,
|
||||
'assigned_to' => $target->id,
|
||||
'assigned_type' => $target::class,
|
||||
'note' => $request->input('note'),
|
||||
'created_by' => auth()->id(),
|
||||
'pivot' => $accessory_checkout->id,
|
||||
];
|
||||
}
|
||||
|
||||
// Set this value to be able to pass the qty through to the event.
|
||||
event(new CheckoutableCheckedOut(
|
||||
$accessory,
|
||||
$target,
|
||||
auth()->user(),
|
||||
$request->input('note'),
|
||||
[],
|
||||
$accessory->checkout_qty,
|
||||
));
|
||||
});
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkout.success')));
|
||||
|
||||
|
||||
@@ -706,18 +706,35 @@ class AssetsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if ($asset->save()) {
|
||||
if ($request->input('assigned_user')) {
|
||||
$target = User::find(request('assigned_user'));
|
||||
} elseif ($request->input('assigned_asset')) {
|
||||
$target = Asset::find(request('assigned_asset'));
|
||||
} elseif ($request->input('assigned_location')) {
|
||||
$target = Location::find(request('assigned_location'));
|
||||
$target = $this->resolveCheckoutTargetForAssetMutation($request);
|
||||
$requestedCheckout = $request->filled('assigned_user') || $request->filled('assigned_asset') || $request->filled('assigned_location');
|
||||
|
||||
if ($requestedCheckout && (! $target)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')));
|
||||
}
|
||||
|
||||
if ($requestedCheckout) {
|
||||
$companyMismatchResponse = $this->checkoutCompanyMismatchResponse($asset, $target);
|
||||
if ($companyMismatchResponse) {
|
||||
return $companyMismatchResponse;
|
||||
}
|
||||
if (isset($target)) {
|
||||
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset creation', e($request->input('name')));
|
||||
}
|
||||
|
||||
$stored = DB::transaction(function () use ($asset, $request, $target, $requestedCheckout): bool {
|
||||
if (! $asset->save()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($requestedCheckout) {
|
||||
// Keep create + optional checkout side effects atomic.
|
||||
return $asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset creation', e($request->input('name')));
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if ($stored) {
|
||||
|
||||
if ($asset->image) {
|
||||
$asset->image = $asset->getImageUrl();
|
||||
}
|
||||
@@ -792,25 +809,54 @@ class AssetsController extends Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($asset->save()) {
|
||||
if (($request->filled('assigned_user')) && ($target = User::find($request->input('assigned_user')))) {
|
||||
$location = $target->location_id;
|
||||
} elseif (($request->filled('assigned_asset')) && ($target = Asset::find($request->input('assigned_asset')))) {
|
||||
$location = $target->location_id;
|
||||
$target = $this->resolveCheckoutTargetForAssetMutation($request, $asset->id);
|
||||
$requestedCheckout = $request->filled('assigned_user') || $request->filled('assigned_asset') || $request->filled('assigned_location');
|
||||
|
||||
Asset::where('assigned_type', Asset::class)->where('assigned_to', $asset->id)
|
||||
->update(['location_id' => $target->location_id]);
|
||||
} elseif (($request->filled('assigned_location')) && ($target = Location::find($request->input('assigned_location')))) {
|
||||
$location = $target->id;
|
||||
if ($requestedCheckout && (! $target)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')));
|
||||
}
|
||||
|
||||
if ($requestedCheckout) {
|
||||
$companyMismatchResponse = $this->checkoutCompanyMismatchResponse($asset, $target);
|
||||
if ($companyMismatchResponse) {
|
||||
return $companyMismatchResponse;
|
||||
}
|
||||
}
|
||||
|
||||
$updated = DB::transaction(function () use ($asset, $request, $target, $requestedCheckout): bool {
|
||||
if (! $asset->save()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($target)) {
|
||||
if ($requestedCheckout) {
|
||||
// Using `->has` preserves the asset name if the name parameter was not included in request.
|
||||
$asset_name = request()->has('name') ? request('name') : $asset->name;
|
||||
|
||||
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset update', $asset_name, $location);
|
||||
$location = null;
|
||||
if ($request->filled('assigned_user')) {
|
||||
$location = $target->location_id;
|
||||
} elseif ($request->filled('assigned_asset')) {
|
||||
$location = $target->location_id;
|
||||
} elseif ($request->filled('assigned_location')) {
|
||||
$location = $target->id;
|
||||
}
|
||||
|
||||
// Keep update + optional checkout side effects atomic.
|
||||
if (! $asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset update', $asset_name, $location)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($request->filled('assigned_asset')) {
|
||||
Asset::where('assigned_type', Asset::class)->where('assigned_to', $asset->id)
|
||||
->update(['location_id' => $target->location_id]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if ($updated) {
|
||||
|
||||
if ($asset->image) {
|
||||
$asset->image = $asset->getImageUrl();
|
||||
}
|
||||
@@ -829,6 +875,36 @@ class AssetsController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
|
||||
}
|
||||
|
||||
private function resolveCheckoutTargetForAssetMutation(Request $request, ?int $assetId = null): User|Asset|Location|null
|
||||
{
|
||||
if ($request->filled('assigned_user')) {
|
||||
return User::withoutGlobalScopes()->find($request->input('assigned_user'));
|
||||
}
|
||||
|
||||
if ($request->filled('assigned_asset')) {
|
||||
return Asset::withoutGlobalScopes()->where('id', '!=', $assetId)->find($request->input('assigned_asset'));
|
||||
}
|
||||
|
||||
if ($request->filled('assigned_location')) {
|
||||
return Location::withoutGlobalScopes()->find($request->input('assigned_location'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function checkoutCompanyMismatchResponse(Asset $asset, User|Asset|Location $target): ?JsonResponse
|
||||
{
|
||||
if ((Setting::getSettings()->full_multiple_companies_support == '1')
|
||||
&& (! is_null($asset->company_id))
|
||||
&& (! is_null($target->company_id))
|
||||
&& ((int) $asset->company_id !== (int) $target->company_id)
|
||||
) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a given asset (mark as deleted).
|
||||
*
|
||||
@@ -905,6 +981,7 @@ class AssetsController extends Controller
|
||||
*/
|
||||
public function checkoutByTag(AssetCheckoutRequest $request, $tag): JsonResponse
|
||||
{
|
||||
// Use the same hardened checkout path as ID-based checkout.
|
||||
if ($asset = Asset::where('asset_tag', $tag)->first()) {
|
||||
return $this->checkout($request, $asset->id);
|
||||
}
|
||||
@@ -940,19 +1017,22 @@ class AssetsController extends Controller
|
||||
|
||||
// This item is checked out to a location
|
||||
if (request('checkout_to_type') == 'location') {
|
||||
$target = Location::find(request('assigned_location'));
|
||||
// Resolve unscoped target first so FMCS mismatch can be handled explicitly.
|
||||
$target = Location::withoutGlobalScopes()->find(request('assigned_location'));
|
||||
$asset->location_id = ($target) ? $target->id : '';
|
||||
$error_payload['target_id'] = $request->input('assigned_location');
|
||||
$error_payload['target_type'] = 'location';
|
||||
} elseif (request('checkout_to_type') == 'asset') {
|
||||
$target = Asset::where('id', '!=', $asset_id)->find(request('assigned_asset'));
|
||||
// Resolve unscoped target first so FMCS mismatch can be handled explicitly.
|
||||
$target = Asset::withoutGlobalScopes()->where('id', '!=', $asset_id)->find(request('assigned_asset'));
|
||||
// Override with the asset's location_id if it has one
|
||||
$asset->location_id = (($target) && (isset($target->location_id))) ? $target->location_id : '';
|
||||
$error_payload['target_id'] = $request->input('assigned_asset');
|
||||
$error_payload['target_type'] = 'asset';
|
||||
} elseif (request('checkout_to_type') == 'user') {
|
||||
// Fetch the target and set the asset's new location_id
|
||||
$target = User::find(request('assigned_user'));
|
||||
// Resolve unscoped target first so FMCS mismatch can be handled explicitly.
|
||||
$target = User::withoutGlobalScopes()->find(request('assigned_user'));
|
||||
$asset->location_id = (($target) && (isset($target->location_id))) ? $target->location_id : '';
|
||||
$error_payload['target_id'] = $request->input('assigned_user');
|
||||
$error_payload['target_type'] = 'user';
|
||||
@@ -971,6 +1051,16 @@ class AssetsController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('error', $error_payload, 'Checkout target for asset '.e($asset->asset_tag).' is invalid - '.$error_payload['target_type'].' does not exist.'));
|
||||
}
|
||||
|
||||
// In FMCS mode, enforce explicit same-company target checks before mutating checkout state.
|
||||
$targetCompanyId = data_get($target, 'company_id');
|
||||
if ((Setting::getSettings()->full_multiple_companies_support == '1')
|
||||
&& (! is_null($asset->company_id))
|
||||
&& (! is_null($targetCompanyId))
|
||||
&& ((int) $asset->company_id !== (int) $targetCompanyId)
|
||||
) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', $error_payload, trans('general.error_user_company')));
|
||||
}
|
||||
|
||||
$checkout_at = request('checkout_at', date('Y-m-d H:i:s'));
|
||||
$expected_checkin = request('expected_checkin', null);
|
||||
$note = request('note', null);
|
||||
@@ -985,7 +1075,12 @@ class AssetsController extends Controller
|
||||
// $asset->location_id = $target->rtd_location_id;
|
||||
// }
|
||||
|
||||
if ($asset->checkOut($target, auth()->user(), $checkout_at, $expected_checkin, $note, $asset_name, $asset->location_id)) {
|
||||
// Keep checkout mutation + checkout logging/event side effects atomic.
|
||||
$wasCheckedOut = DB::transaction(function () use ($asset, $target, $checkout_at, $expected_checkin, $note, $asset_name): bool {
|
||||
return $asset->checkOut($target, auth()->user(), $checkout_at, $expected_checkin, $note, $asset_name, $asset->location_id);
|
||||
});
|
||||
|
||||
if ($wasCheckedOut) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', ['asset' => e($asset->asset_tag)], trans('admin/hardware/message.checkout.success')));
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Http\Transformers\ComponentsTransformer;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Company;
|
||||
use App\Models\Component;
|
||||
use App\Models\Setting;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -314,20 +315,33 @@ class ComponentsController extends Controller
|
||||
}
|
||||
|
||||
if ($component->numRemaining() >= $request->input('assigned_qty')) {
|
||||
// Resolve the raw target first, then enforce FMCS explicitly.
|
||||
// Scoped lookup can hide cross-company records and lead to partial writes.
|
||||
$asset = Asset::withoutGlobalScopes()->find($request->input('assigned_to'));
|
||||
|
||||
$asset = Asset::find($request->input('assigned_to'));
|
||||
$component->assigned_to = $request->input('assigned_to');
|
||||
if (! $asset) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')));
|
||||
}
|
||||
|
||||
$component->assets()->attach($component->id, [
|
||||
'component_id' => $component->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'assigned_qty' => $request->input('assigned_qty', 1),
|
||||
'created_by' => auth()->id(),
|
||||
'asset_id' => $request->input('assigned_to'),
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($component->company_id !== $asset->company_id)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||
}
|
||||
|
||||
$component->logCheckout($request->input('note'), $asset, null, [], $request->get('assigned_qty', 1));
|
||||
// Keep pivot + action log in one transaction so checkout is all-or-nothing.
|
||||
DB::transaction(function () use ($component, $request, $asset): void {
|
||||
$component->assigned_to = $request->input('assigned_to');
|
||||
|
||||
$component->assets()->attach($component->id, [
|
||||
'component_id' => $component->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'assigned_qty' => $request->input('assigned_qty', 1),
|
||||
'created_by' => auth()->id(),
|
||||
'asset_id' => $request->input('assigned_to'),
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
|
||||
$component->logCheckout($request->input('note'), $asset, null, [], $request->get('assigned_qty', 1));
|
||||
});
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.checkout.success')));
|
||||
}
|
||||
|
||||
@@ -13,9 +13,11 @@ use App\Http\Transformers\ConsumablesTransformer;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Models\Company;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ConsumablesController extends Controller
|
||||
{
|
||||
@@ -306,34 +308,42 @@ class ConsumablesController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/consumables/message.checkout.unavailable', ['requested' => $consumable->checkout_qty, 'remaining' => $consumable->numRemaining()])));
|
||||
}
|
||||
|
||||
// Check if the user exists - @TODO: this should probably be handled via validation, not here??
|
||||
if (! $user = User::find($request->input('assigned_to'))) {
|
||||
// Resolve the raw target first, then enforce FMCS explicitly.
|
||||
// Scoped lookup can hide cross-company users and make failures ambiguous.
|
||||
if (! $user = User::withoutGlobalScopes()->find($request->input('assigned_to'))) {
|
||||
// Return error message
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'No user found'));
|
||||
}
|
||||
|
||||
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')));
|
||||
}
|
||||
|
||||
// Update the consumable data
|
||||
$consumable->assigned_to = $request->input('assigned_to');
|
||||
|
||||
for ($i = 0; $i < $consumable->checkout_qty; $i++) {
|
||||
$consumable->users()->attach($consumable->id,
|
||||
[
|
||||
'consumable_id' => $consumable->id,
|
||||
'created_by' => $user->id,
|
||||
'assigned_to' => $request->input('assigned_to'),
|
||||
'note' => $request->input('note'),
|
||||
]
|
||||
);
|
||||
}
|
||||
// Keep pivot writes and checkout log/event atomic to avoid partial checkout state.
|
||||
DB::transaction(function () use ($consumable, $request, $user): void {
|
||||
for ($i = 0; $i < $consumable->checkout_qty; $i++) {
|
||||
$consumable->users()->attach($consumable->id,
|
||||
[
|
||||
'consumable_id' => $consumable->id,
|
||||
'created_by' => $user->id,
|
||||
'assigned_to' => $request->input('assigned_to'),
|
||||
'note' => $request->input('note'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
event(new CheckoutableCheckedOut(
|
||||
$consumable,
|
||||
$user,
|
||||
auth()->user(),
|
||||
$request->input('note'),
|
||||
[],
|
||||
$consumable->checkout_qty,
|
||||
));
|
||||
event(new CheckoutableCheckedOut(
|
||||
$consumable,
|
||||
$user,
|
||||
auth()->user(),
|
||||
$request->input('note'),
|
||||
[],
|
||||
$consumable->checkout_qty,
|
||||
));
|
||||
});
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/consumables/message.checkout.success')));
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@ use App\Http\Transformers\LicenseSeatsTransformer;
|
||||
use App\Models\Asset;
|
||||
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;
|
||||
|
||||
class LicenseSeatsController extends Controller
|
||||
{
|
||||
@@ -106,7 +108,8 @@ class LicenseSeatsController extends Controller
|
||||
'prohibits:asset_id',
|
||||
// must be a valid user or null to unassign
|
||||
function ($attribute, $value, $fail) {
|
||||
if (! is_null($value) && ! User::where('id', $value)->whereNull('deleted_at')->exists()) {
|
||||
// Validate existence without company scopes; FMCS checks happen explicitly below.
|
||||
if (! is_null($value) && ! User::withoutGlobalScopes()->where('id', $value)->whereNull('deleted_at')->exists()) {
|
||||
$fail('The selected assigned_to is invalid.');
|
||||
}
|
||||
},
|
||||
@@ -118,7 +121,8 @@ class LicenseSeatsController extends Controller
|
||||
'prohibits:assigned_to',
|
||||
// must be a valid asset or null to unassign
|
||||
function ($attribute, $value, $fail) {
|
||||
if (! is_null($value) && ! Asset::where('id', $value)->whereNull('deleted_at')->exists()) {
|
||||
// Validate existence without company scopes; FMCS checks happen explicitly below.
|
||||
if (! is_null($value) && ! Asset::withoutGlobalScopes()->where('id', $value)->whereNull('deleted_at')->exists()) {
|
||||
$fail('The selected asset_id is invalid.');
|
||||
}
|
||||
},
|
||||
@@ -139,6 +143,34 @@ class LicenseSeatsController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat does not belong to the specified license'));
|
||||
}
|
||||
|
||||
$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'));
|
||||
}
|
||||
|
||||
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')));
|
||||
}
|
||||
}
|
||||
|
||||
$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'));
|
||||
}
|
||||
|
||||
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')));
|
||||
}
|
||||
}
|
||||
|
||||
$oldUser = $licenseSeat->user;
|
||||
$oldAsset = $licenseSeat->asset;
|
||||
|
||||
@@ -166,11 +198,11 @@ class LicenseSeatsController extends Controller
|
||||
// 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 : User::find($licenseSeat->assigned_to);
|
||||
$target = $is_checkin ? $oldUser : $targetUser;
|
||||
}
|
||||
|
||||
if ($licenseSeat->isDirty('asset_id')) {
|
||||
$target = $is_checkin ? $oldAsset : Asset::find($licenseSeat->asset_id);
|
||||
$target = $is_checkin ? $oldAsset : $targetAsset;
|
||||
}
|
||||
|
||||
if ($assignmentTouched && is_null($target)) {
|
||||
@@ -181,13 +213,22 @@ class LicenseSeatsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if ($licenseSeat->save()) {
|
||||
// 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()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($assignmentTouched) {
|
||||
if ($is_checkin) {
|
||||
if (! $licenseSeat->license->reassignable) {
|
||||
$licenseSeat->unreassignable_seat = true;
|
||||
$licenseSeat->save();
|
||||
|
||||
if (! $licenseSeat->save()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// todo: skip if target is null?
|
||||
$licenseSeat->logCheckin($target, $licenseSeat->notes);
|
||||
} else {
|
||||
@@ -196,6 +237,10 @@ class LicenseSeatsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if ($updated) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class LicensesController extends Controller
|
||||
{
|
||||
$this->authorize('view', License::class);
|
||||
|
||||
$licenses = License::with('company', 'manufacturer', 'supplier', 'category', 'adminuser')->withCount('freeSeats as free_seats_count');
|
||||
$licenses = License::with('company', 'manufacturer', 'supplier', 'category', 'adminuser', 'licenseSeatsRelation', 'assignedCount')->withCount('freeSeats as free_seats_count');
|
||||
$settings = Setting::getSettings();
|
||||
|
||||
if ($request->input('status') == 'inactive') {
|
||||
@@ -247,7 +247,7 @@ class LicensesController extends Controller
|
||||
if ($license->assigned_seats_count == 0) {
|
||||
// Delete the license and the associated license seats
|
||||
DB::table('license_seats')
|
||||
->where('id', $license->id)
|
||||
->where('license_id', $license->id)
|
||||
->update(['assigned_to' => null, 'asset_id' => null]);
|
||||
|
||||
$licenseSeats = $license->licenseseats();
|
||||
|
||||
@@ -38,6 +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');
|
||||
|
||||
// This invokes the Searchable model trait scopeTextSearch and will handle input by search or by advanced search filter
|
||||
|
||||
@@ -6,8 +6,18 @@ use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\FilterRequest;
|
||||
use App\Http\Transformers\ActionlogsTransformer;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Component;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\Maintenance;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class ReportsController extends Controller
|
||||
@@ -125,4 +135,141 @@ class ReportsController extends Controller
|
||||
return response()->json((new ActionlogsTransformer)->transformActionlogs($actionlogs, $total), 200, ['Content-Type' => 'application/json;charset=utf8'], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns time-series data for the reports overview charts.
|
||||
*
|
||||
* Accepts ?days=N (preset, default 30) OR ?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD.
|
||||
* Also returns the immediately preceding period of equal length for comparison lines.
|
||||
*/
|
||||
public function activityChart(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('reports.view');
|
||||
|
||||
$allowedDays = [7, 14, 30, 60, 90, 180, 365];
|
||||
|
||||
if ($request->filled('start_date') && $request->filled('end_date')) {
|
||||
$curStart = Carbon::parse($request->input('start_date'))->startOfDay();
|
||||
$curEnd = Carbon::parse($request->input('end_date'))->endOfDay();
|
||||
if ($curEnd->lt($curStart)) {
|
||||
[$curStart, $curEnd] = [$curEnd, $curStart];
|
||||
}
|
||||
$days = max(1, (int) $curStart->diffInDays($curEnd) + 1);
|
||||
} else {
|
||||
$days = in_array((int) $request->input('days'), $allowedDays) ? (int) $request->input('days') : 30;
|
||||
$curEnd = Carbon::today()->endOfDay();
|
||||
$curStart = Carbon::today()->subDays($days - 1)->startOfDay();
|
||||
}
|
||||
|
||||
$prevEnd = $curStart->copy()->subSecond()->endOfDay();
|
||||
$prevStart = $prevEnd->copy()->subDays($days - 1)->startOfDay();
|
||||
|
||||
$buildDates = function (Carbon $start, Carbon $end): array {
|
||||
$dates = [];
|
||||
for ($d = $start->copy(); $d->lte($end); $d->addDay()) {
|
||||
$dates[] = $d->toDateString();
|
||||
}
|
||||
|
||||
return $dates;
|
||||
};
|
||||
|
||||
$curDates = $buildDates($curStart, $curEnd);
|
||||
$prevDates = $buildDates($prevStart, $prevEnd);
|
||||
|
||||
$pluckAction = function (string $actionType, Carbon $start, Carbon $end): array {
|
||||
return Actionlog::where('action_type', $actionType)
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
|
||||
->groupBy('date')
|
||||
->pluck('count', 'date')
|
||||
->toArray();
|
||||
};
|
||||
|
||||
// withTrashed() ensures records deleted after creation still appear in their creation-period counts.
|
||||
$pluckCreated = function (string $modelClass, Carbon $start, Carbon $end): array {
|
||||
return $modelClass::withTrashed()
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
|
||||
->groupBy('date')
|
||||
->pluck('count', 'date')
|
||||
->toArray();
|
||||
};
|
||||
|
||||
// Maintenance has no company_id column and no CompanyableTrait, so scope through
|
||||
// its asset relationship — whereHas('asset') applies Asset's FMCS global scope.
|
||||
$pluckMaintenances = function (Carbon $start, Carbon $end): array {
|
||||
return Maintenance::withTrashed()
|
||||
->whereHas('asset')
|
||||
->whereBetween('maintenances.created_at', [$start, $end])
|
||||
->selectRaw('DATE(maintenances.created_at) as date, COUNT(*) as count')
|
||||
->groupBy('date')
|
||||
->pluck('count', 'date')
|
||||
->toArray();
|
||||
};
|
||||
|
||||
// Filters by both action_type and item_type for per-category checkout/checkin counts.
|
||||
$pluckActionByType = function (string $actionType, string $modelClass, Carbon $start, Carbon $end): array {
|
||||
return Actionlog::where('action_type', $actionType)
|
||||
->where('item_type', $modelClass)
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
|
||||
->groupBy('date')
|
||||
->pluck('count', 'date')
|
||||
->toArray();
|
||||
};
|
||||
|
||||
$pluckDeletedUsers = function (Carbon $start, Carbon $end): array {
|
||||
return User::withTrashed()
|
||||
->whereNotNull('deleted_at')
|
||||
->whereBetween('deleted_at', [$start, $end])
|
||||
->selectRaw('DATE(deleted_at) as date, COUNT(*) as count')
|
||||
->groupBy('date')
|
||||
->pluck('count', 'date')
|
||||
->toArray();
|
||||
};
|
||||
|
||||
// Catches both 'checkin' and 'checkin from' action types used across different item types.
|
||||
$pluckCheckinsByType = function (string $modelClass, Carbon $start, Carbon $end): array {
|
||||
return Actionlog::whereIn('action_type', ['checkin', 'checkin from'])
|
||||
->where('item_type', $modelClass)
|
||||
->whereBetween('created_at', [$start, $end])
|
||||
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
|
||||
->groupBy('date')
|
||||
->pluck('count', 'date')
|
||||
->toArray();
|
||||
};
|
||||
|
||||
$fill = fn (array $raw, array $dates) => array_map(fn ($d) => (int) ($raw[$d] ?? 0), $dates);
|
||||
|
||||
$datasets = [];
|
||||
foreach ([
|
||||
'new_users' => fn ($s, $e) => $pluckCreated(User::class, $s, $e),
|
||||
'deleted_users' => fn ($s, $e) => $pluckDeletedUsers($s, $e),
|
||||
'asset_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', Asset::class, $s, $e),
|
||||
'asset_checkins' => fn ($s, $e) => $pluckCheckinsByType(Asset::class, $s, $e),
|
||||
'new_assets' => fn ($s, $e) => $pluckCreated(Asset::class, $s, $e),
|
||||
'new_maintenances' => fn ($s, $e) => $pluckMaintenances($s, $e),
|
||||
'new_audits' => fn ($s, $e) => $pluckAction('audit', $s, $e),
|
||||
'component_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', Component::class, $s, $e),
|
||||
'component_checkins' => fn ($s, $e) => $pluckCheckinsByType(Component::class, $s, $e),
|
||||
'new_components' => fn ($s, $e) => $pluckCreated(Component::class, $s, $e),
|
||||
'consumable_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', Consumable::class, $s, $e),
|
||||
'consumable_checkins' => fn ($s, $e) => $pluckCheckinsByType(Consumable::class, $s, $e),
|
||||
'new_consumables' => fn ($s, $e) => $pluckCreated(Consumable::class, $s, $e),
|
||||
'license_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', LicenseSeat::class, $s, $e),
|
||||
'license_checkins' => fn ($s, $e) => $pluckCheckinsByType(LicenseSeat::class, $s, $e),
|
||||
'new_licenses' => fn ($s, $e) => $pluckCreated(License::class, $s, $e),
|
||||
'accessory_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', Accessory::class, $s, $e),
|
||||
'accessory_checkins' => fn ($s, $e) => $pluckCheckinsByType(Accessory::class, $s, $e),
|
||||
'new_accessories' => fn ($s, $e) => $pluckCreated(Accessory::class, $s, $e),
|
||||
] as $key => $query) {
|
||||
$datasets[$key] = $fill($query($curStart, $curEnd), $curDates);
|
||||
$datasets['prev_'.$key] = $fill($query($prevStart, $prevEnd), $prevDates);
|
||||
}
|
||||
|
||||
return response()->json(array_merge([
|
||||
'labels' => array_map(fn ($d) => Carbon::parse($d)->format('M j'), $curDates),
|
||||
'prev_label' => $prevStart->format('M j').' – '.$prevEnd->format('M j'),
|
||||
], $datasets));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,6 +569,7 @@ class UsersController extends Controller
|
||||
requestedPermissions: NormalizePermissionsPayloadAction::run($request->input('permissions')),
|
||||
authenticatedUser: $authenticatedUser,
|
||||
originalPermissions: NormalizePermissionsPayloadAction::run($user->decodePermissions()),
|
||||
targetUser: $user,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 CheckInOutRequest;
|
||||
use CheckInOutTrait;
|
||||
|
||||
/**
|
||||
* Returns a view that presents a form to check an asset out to a
|
||||
|
||||
@@ -4,9 +4,9 @@ namespace App\Http\Controllers\Assets;
|
||||
|
||||
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\Company;
|
||||
@@ -27,7 +27,7 @@ use Illuminate\Support\Facades\Log;
|
||||
|
||||
class BulkAssetsController extends Controller
|
||||
{
|
||||
use CheckInOutRequest;
|
||||
use CheckInOutTrait;
|
||||
|
||||
/**
|
||||
* Display the bulk edit page.
|
||||
|
||||
@@ -4,7 +4,8 @@ namespace App\Http\Controllers\Components;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ImageUploadRequest;
|
||||
use App\Http\Requests\StoreComponentRequest;
|
||||
use App\Http\Requests\UpdateComponentRequest;
|
||||
use App\Models\Company;
|
||||
use App\Models\Component;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
@@ -12,7 +13,6 @@ 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(ImageUploadRequest $request)
|
||||
public function store(StoreComponentRequest $request)
|
||||
{
|
||||
$this->authorize('create', Component::class);
|
||||
$component = new Component;
|
||||
@@ -148,21 +148,10 @@ class ComponentsController extends Controller
|
||||
*
|
||||
* @since [v3.0]
|
||||
*/
|
||||
public function update(ImageUploadRequest $request, Component $component)
|
||||
public function update(UpdateComponentRequest $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');
|
||||
|
||||
@@ -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 CheckInOutRequest;
|
||||
use CheckInOutTrait;
|
||||
|
||||
public function __construct(PredefinedKitCheckoutService $kitService)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Licenses;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\License;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class BulkLicensesController extends Controller
|
||||
{
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$this->authorize('delete', License::class);
|
||||
|
||||
$errors = [];
|
||||
$success_count = 0;
|
||||
|
||||
foreach ($request->ids as $id) {
|
||||
$license = License::find($id);
|
||||
|
||||
if (is_null($license)) {
|
||||
$errors[] = trans('admin/licenses/message.does_not_exist');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $license)) {
|
||||
$errors[] = trans('general.insufficient_permissions');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($license->assigned_seats_count > 0) {
|
||||
$errors[] = trans('admin/licenses/message.delete.bulk_checkout_warning', ['license_name' => $license->name]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Since assigned_seats_count == 0, all seats already have assigned_to and asset_id as null,
|
||||
// so this update is effectively a no-op. It mirrors the single destroy() and is kept as a
|
||||
// safety net. Bypassing Eloquent events here is intentional and safe — there is nothing
|
||||
// assigned to trigger events on. Prior checkout/checkin history is preserved in action_log
|
||||
// (keyed by LicenseSeat item_type/item_id) and remains accessible even after soft-delete.
|
||||
DB::table('license_seats')
|
||||
->where('license_id', $license->id)
|
||||
->update(['assigned_to' => null, 'asset_id' => null]);
|
||||
|
||||
$license->licenseseats()->delete();
|
||||
$license->delete();
|
||||
$success_count++;
|
||||
}
|
||||
|
||||
if (count($errors) > 0) {
|
||||
if ($success_count > 0) {
|
||||
return redirect()->route('licenses.index')
|
||||
->with('success', trans_choice('admin/licenses/message.delete.partial_success', $success_count, ['count' => $success_count]))
|
||||
->with('multi_error_messages', $errors);
|
||||
}
|
||||
|
||||
return redirect()->route('licenses.index')->with('multi_error_messages', $errors);
|
||||
}
|
||||
|
||||
return redirect()->route('licenses.index')->with('success', trans('admin/licenses/message.delete.bulk_success'));
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use League\Csv\EscapeFormula;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
@@ -388,6 +389,8 @@ class LicensesController extends Controller
|
||||
|
||||
fputcsv($handle, $headers);
|
||||
|
||||
$formatter = new EscapeFormula('`');
|
||||
|
||||
foreach ($licenses as $license) {
|
||||
// Add a new row with data
|
||||
$values = [
|
||||
@@ -419,7 +422,14 @@ class LicensesController extends Controller
|
||||
$license->created_at,
|
||||
];
|
||||
|
||||
fputcsv($handle, $values);
|
||||
// CSV_ESCAPE_FORMULAS is set to false in the .env
|
||||
if (config('app.escape_formulas') === false) {
|
||||
fputcsv($handle, $values);
|
||||
|
||||
// CSV_ESCAPE_FORMULAS is set to true or is not set in the .env
|
||||
} else {
|
||||
fputcsv($handle, $formatter->escapeRecord($values));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -211,14 +211,19 @@ class ProfileController extends Controller
|
||||
*/
|
||||
public function printInventory(): View
|
||||
{
|
||||
$show_users = User::where('id', auth()->user()->id)->get();
|
||||
$userId = auth()->id();
|
||||
|
||||
return view('users/print')
|
||||
->with('assets', auth()->user()->assets())
|
||||
->with('licenses', auth()->user()->licenses()->get())
|
||||
->with('accessories', auth()->user()->accessories()->get())
|
||||
->with('consumables', auth()->user()->consumables()->get())
|
||||
->with('users', $show_users)
|
||||
$show_user = User::withInventoryRelations($userId)->first();
|
||||
|
||||
$indirectItemsCount =
|
||||
$show_user->assets->flatMap->assignedAssets->count()
|
||||
+ $show_user->assets->flatMap->components->count()
|
||||
+ $show_user->assets->flatMap->licenses->count()
|
||||
+ $show_user->assets->flatMap->assignedAccessories->count();
|
||||
|
||||
return view('users.print')
|
||||
->with('users', [$show_user])
|
||||
->with('indirectItemsCount', $indirectItemsCount)
|
||||
->with('settings', Setting::getSettings());
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,31 @@ class ReportsController extends Controller
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
$this->authorize('reports.view');
|
||||
$settings = Setting::getSettings();
|
||||
|
||||
$audit_alert_count = Asset::DueOrOverdueForAudit($settings)->count();
|
||||
$checkin_alert_count = Asset::DueOrOverdueForCheckin($settings)->count();
|
||||
// CheckoutAcceptance has no company_id column; scope through the checkoutable
|
||||
// relationship so each type's CompanyableTrait global scope is applied.
|
||||
$pending_acceptance_count = CheckoutAcceptance::pending()
|
||||
->whereHasMorph('checkoutable', [Asset::class, LicenseSeat::class, Accessory::class, Component::class, Consumable::class])
|
||||
->count();
|
||||
$licenses_low_count = License::withCount(['freeSeats as free_seats_count'])
|
||||
->get()
|
||||
->filter(fn ($l) => $l->free_seats_count <= 0)
|
||||
->count();
|
||||
|
||||
return view('reports/index', compact(
|
||||
'audit_alert_count',
|
||||
'checkin_alert_count',
|
||||
'pending_acceptance_count',
|
||||
'licenses_low_count',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a view that displays the accessories report.
|
||||
*
|
||||
@@ -252,6 +277,7 @@ class ReportsController extends Controller
|
||||
|
||||
$response = new StreamedResponse(function () {
|
||||
Log::debug('Starting streamed response');
|
||||
Log::debug('CSV escaping is set to: '.config('app.escape_formulas'));
|
||||
|
||||
// Open output stream
|
||||
$handle = fopen('php://output', 'w');
|
||||
@@ -287,6 +313,8 @@ class ReportsController extends Controller
|
||||
Log::debug('Walking results: '.$executionTime);
|
||||
$count = 0;
|
||||
|
||||
$formatter = new EscapeFormula('`');
|
||||
|
||||
foreach ($actionlogs as $actionlog) {
|
||||
$count++;
|
||||
$target_name = '';
|
||||
@@ -317,7 +345,15 @@ class ReportsController extends Controller
|
||||
$actionlog->action_source,
|
||||
$actionlog->log_meta,
|
||||
];
|
||||
fputcsv($handle, $row);
|
||||
|
||||
// CSV_ESCAPE_FORMULAS is set to false in the .env
|
||||
if (config('app.escape_formulas') === false) {
|
||||
fputcsv($handle, $row);
|
||||
|
||||
// CSV_ESCAPE_FORMULAS is set to true or is not set in the .env
|
||||
} else {
|
||||
fputcsv($handle, $formatter->escapeRecord($row));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -852,7 +888,7 @@ class ReportsController extends Controller
|
||||
}
|
||||
|
||||
if ($request->filled('purchase_date')) {
|
||||
$row[] = ($asset->purchase_date) ? $asset->purchase_date : '';
|
||||
$row[] = ($asset->purchase_date) ? Carbon::parse($asset->purchase_date)->format('Y-m-d') : '';
|
||||
}
|
||||
|
||||
if ($request->filled('purchase_cost')) {
|
||||
@@ -860,7 +896,7 @@ class ReportsController extends Controller
|
||||
}
|
||||
|
||||
if ($request->filled('eol')) {
|
||||
$row[] = ($asset->asset_eol_date != '') ? $asset->asset_eol_date : '';
|
||||
$row[] = ($asset->asset_eol_date != '') ? Carbon::parse($asset->asset_eol_date)->format('Y-m-d') : '';
|
||||
}
|
||||
|
||||
if ($request->filled('warranty')) {
|
||||
@@ -1200,6 +1236,9 @@ class ReportsController extends Controller
|
||||
public function getAssetAcceptanceReport($deleted = false): View
|
||||
{
|
||||
$this->authorize('reports.view');
|
||||
|
||||
$this->disableDebugbar();
|
||||
|
||||
$showDeleted = $deleted == 'deleted';
|
||||
|
||||
$query = CheckoutAcceptance::Pending()
|
||||
|
||||
@@ -26,6 +26,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use League\Csv\EscapeFormula;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
@@ -314,6 +315,7 @@ class UsersController extends Controller
|
||||
requestedPermissions: NormalizePermissionsPayloadAction::run($request->input('permission')),
|
||||
authenticatedUser: $authenticatedUser,
|
||||
originalPermissions: $orig_permissions_array,
|
||||
targetUser: $user,
|
||||
));
|
||||
|
||||
// Only save groups if the user is a superuser
|
||||
@@ -533,52 +535,76 @@ 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',
|
||||
'department.manager',
|
||||
'licenses',
|
||||
'manager',
|
||||
'groups',
|
||||
'userloc',
|
||||
'company'
|
||||
)->orderBy('created_at', 'DESC')
|
||||
'company',
|
||||
'createdBy'
|
||||
)->withCount(['managesUsers as manages_users_count', 'managedLocations as manages_locations_count'])
|
||||
->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()) {
|
||||
@@ -597,9 +623,18 @@ class UsersController extends Controller
|
||||
$user->employee_num,
|
||||
$user->first_name,
|
||||
$user->last_name,
|
||||
$user->display_name,
|
||||
$user->getFullNameAttribute(),
|
||||
$user->getRawOriginal('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 : '',
|
||||
@@ -607,14 +642,37 @@ class UsersController extends Controller
|
||||
$user->licenses->count(),
|
||||
$user->accessories->count(),
|
||||
$user->consumables->count(),
|
||||
$user_groups,
|
||||
$user->groups->pluck('name')->implode(', '),
|
||||
$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,
|
||||
];
|
||||
|
||||
fputcsv($handle, $values);
|
||||
// CSV_ESCAPE_FORMULAS is set to false in the .env
|
||||
if (config('app.escape_formulas') === false) {
|
||||
fputcsv($handle, $values);
|
||||
|
||||
// CSV_ESCAPE_FORMULAS is set to true or is not set in the .env
|
||||
} else {
|
||||
fputcsv($handle, $formatter->escapeRecord($values));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -639,32 +697,16 @@ class UsersController extends Controller
|
||||
{
|
||||
$this->authorize('view', User::class);
|
||||
|
||||
$user = User::where('id', $id)
|
||||
->with([
|
||||
'assets.log' => fn ($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
|
||||
'assets.assignedAssets.log' => fn ($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
|
||||
'assets.assignedAssets.defaultLoc',
|
||||
'assets.assignedAssets.location',
|
||||
'assets.assignedAssets.model.category',
|
||||
'assets.defaultLoc',
|
||||
'assets.location',
|
||||
'assets.model.category',
|
||||
'accessories.log' => fn ($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
|
||||
'accessories.category',
|
||||
'accessories.manufacturer',
|
||||
'consumables.log' => fn ($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
|
||||
'consumables.category',
|
||||
'consumables.manufacturer',
|
||||
'licenses.category',
|
||||
])
|
||||
->withTrashed()
|
||||
->first();
|
||||
$user = User::withInventoryRelations($id)->first();
|
||||
|
||||
$indirectItemsCount = $user?->assets?->flatMap->assignedAssets->count() + $user?->assets?->flatMap->components->count() + $user?->assets?->flatMap->licenses->count() + $user?->assets?->flatMap->assignedAccessories->count();
|
||||
|
||||
if ($user) {
|
||||
$this->authorize('view', $user);
|
||||
|
||||
return view('users.print')
|
||||
->with('users', [$user])
|
||||
->with('indirectItemsCount', $indirectItemsCount)
|
||||
->with('settings', Setting::getSettings());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Requests\Traits\MayContainCustomFields;
|
||||
use App\Models\Asset;
|
||||
use App\Models\AssetModel;
|
||||
@@ -26,6 +27,10 @@ class CreateMultipleAssetRequest extends ImageUploadRequest // should I extend f
|
||||
{
|
||||
parent::prepareForValidation();
|
||||
|
||||
if ($this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))) {
|
||||
$this->merge(['purchase_cost' => Helper::ParseCurrency($this->input('purchase_cost'))]);
|
||||
}
|
||||
|
||||
if (Setting::getSettings()->full_multiple_companies_support == '1' && ! $this->user()->isSuperUser()) {
|
||||
$this->mergeIfMissing(['company_id' => $this->user()->company_id]);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Category;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
@@ -21,6 +22,10 @@ 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([
|
||||
@@ -28,7 +33,6 @@ 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,6 +39,9 @@ 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'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -49,15 +52,6 @@ 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]],
|
||||
@@ -81,20 +75,4 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Models\Category;
|
||||
use App\Models\Consumable;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
@@ -21,6 +22,10 @@ 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([
|
||||
@@ -28,7 +33,6 @@ class StoreConsumableRequest extends ImageUploadRequest
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Requests\Traits\MayContainCustomFields;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Setting;
|
||||
@@ -22,6 +23,13 @@ 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.
|
||||
*
|
||||
@@ -51,12 +59,6 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
namespace App\Http\Traits;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Location;
|
||||
use App\Models\SnipeModel;
|
||||
use App\Models\User;
|
||||
|
||||
trait CheckInOutRequest
|
||||
trait CheckInOutTrait
|
||||
{
|
||||
/**
|
||||
* Find target for checkout
|
||||
@@ -9,8 +9,24 @@ class DatatablesTransformer
|
||||
**/
|
||||
public function transformDatatables($objects, $total = null)
|
||||
{
|
||||
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
|
||||
$objects_array['rows'] = $objects;
|
||||
$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;
|
||||
|
||||
return $objects_array;
|
||||
}
|
||||
@@ -20,8 +36,10 @@ class DatatablesTransformer
|
||||
**/
|
||||
public function transformBulkResponseWithStatusAndObjects($objects, $total)
|
||||
{
|
||||
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
|
||||
$objects_array['rows'] = $objects;
|
||||
$objects_array = [
|
||||
'total' => $total ?? count($objects),
|
||||
'rows' => $objects,
|
||||
];
|
||||
|
||||
return $objects_array;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,10 @@ class LicensesTransformer
|
||||
'checkin' => Gate::allows('checkin', License::class),
|
||||
'clone' => Gate::allows('create', License::class),
|
||||
'update' => Gate::allows('update', License::class),
|
||||
'delete' => (Gate::allows('delete', License::class) && ($license->free_seats_count == $license->seats)) ? true : false,
|
||||
'delete' => $license->isDeletable(),
|
||||
'bulk_selectable' => [
|
||||
'delete' => $license->isDeletable(),
|
||||
],
|
||||
];
|
||||
|
||||
$array += $permissions_array;
|
||||
|
||||
@@ -37,7 +37,8 @@ class AccessoryImporter extends ItemImporter
|
||||
$this->log('Updating Accessory');
|
||||
$this->item['model_number'] = trim($this->findCsvMatch($row, 'model_number'));
|
||||
$accessory->update($this->sanitizeItemForUpdating($accessory));
|
||||
$accessory->save();
|
||||
// update() already saves the model, no need to call save() again while Model::unguard() is active
|
||||
$accessory->setImported(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -176,35 +176,55 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
// This sets an attribute on the Loggable trait for the action log
|
||||
$asset->setImported(true);
|
||||
// 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;
|
||||
}
|
||||
|
||||
if ($asset->save()) {
|
||||
if ($success) {
|
||||
|
||||
$this->log('Asset '.$this->item['name'].' with serial number '.$this->item['serial'].' was created');
|
||||
$this->log('Asset '.$this->item['name'].' with serial number '.$this->item['serial'].' created or updated');
|
||||
|
||||
// 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)) {
|
||||
if (! is_null($asset->assigned_to)) {
|
||||
if ($asset->assigned_to != $target->id) {
|
||||
$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)) {
|
||||
event(new CheckoutableCheckedIn($asset, $asset->assigned, auth()->user(), 'Checkin from CSV Importer', $checkin_date));
|
||||
}
|
||||
}
|
||||
|
||||
$asset->fresh()->checkOut($target, $this->created_by, $checkout_date, null, 'Checkout from CSV Importer', $asset->name);
|
||||
$asset->checkOut($target, $this->created_by, $checkout_date, null, 'Checkout from CSV Importer', $asset->name);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
@@ -42,7 +42,8 @@ class ComponentImporter extends ItemImporter
|
||||
}
|
||||
$this->log('Updating Component');
|
||||
$component->update($this->sanitizeItemForUpdating($component));
|
||||
$component->save();
|
||||
// update() already saves the model, no need to call save() again while Model::unguard() is active
|
||||
$component->setImported(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ class ConsumableImporter extends ItemImporter
|
||||
}
|
||||
$this->log('Updating Consumable');
|
||||
$consumable->update($this->sanitizeItemForUpdating($consumable));
|
||||
$consumable->save();
|
||||
// update() already saves the model, no need to call save() again while Model::unguard() is active
|
||||
$consumable->setImported(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,8 +88,12 @@ class LicenseImporter extends ItemImporter
|
||||
|
||||
// This sets an attribute on the Loggable trait for the action log
|
||||
$license->setImported(true);
|
||||
if ($license->save()) {
|
||||
$this->log('License '.$this->item['name'].' with serial number '.$this->item['serial'].' was created');
|
||||
|
||||
// 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');
|
||||
|
||||
// Lets try to checkout seats if the fields exist and we have seats.
|
||||
if ($license->seats > 0) {
|
||||
|
||||
@@ -640,11 +640,11 @@ class License extends Depreciable
|
||||
/**
|
||||
* This is really dumb - needs to be refactored, since we have ~3 diff methods that do almost the same thing
|
||||
*
|
||||
* @author A. Gianotto <snipe@snipe.net>
|
||||
* @return int
|
||||
*
|
||||
* @since [v2.0]
|
||||
*
|
||||
* @return Relation
|
||||
* @author A. Gianotto <snipe@snipe.net>
|
||||
*/
|
||||
public function numRemaining()
|
||||
{
|
||||
|
||||
+114
-102
@@ -2,10 +2,6 @@
|
||||
|
||||
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;
|
||||
@@ -15,9 +11,10 @@ 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
|
||||
{
|
||||
@@ -36,11 +33,10 @@ 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
|
||||
@@ -70,52 +66,48 @@ 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];
|
||||
}
|
||||
|
||||
@@ -125,6 +117,7 @@ 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()
|
||||
@@ -140,22 +133,19 @@ 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?
|
||||
protected function doRead(&$object, $attributes = [])
|
||||
{
|
||||
return (string)$object->id;
|
||||
}
|
||||
(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;
|
||||
}
|
||||
|
||||
public function remove($value, &$object, $path = null)
|
||||
{
|
||||
@@ -166,102 +156,118 @@ 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) {
|
||||
$object->email = $value[0]['value'];
|
||||
try {
|
||||
$object->email = $value[0]['value'];
|
||||
} catch (\Throwable $e) {
|
||||
\Log::debug($e);
|
||||
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));
|
||||
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?
|
||||
return;
|
||||
\Log::debug("Phones 'value' is: ".print_r($value, true));
|
||||
try {
|
||||
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?
|
||||
|
||||
return;
|
||||
}
|
||||
if ((string) $path == 'phoneNumbers[type eq "work"].value') {
|
||||
$object->phone = $value; // similar, don't know why, but it is
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ((string)$path == 'phoneNumbers[type eq "work"].value') {
|
||||
$object->phone = $value; //similar, don't know why, but it is
|
||||
return;
|
||||
parent::patch($subop, $value, $object, $path, $removeIfNotSet);
|
||||
|
||||
return;
|
||||
}
|
||||
foreach ($value as $phone) {
|
||||
switch ($phone['type']) {
|
||||
case 'work':
|
||||
$object->phone = $phone['value'];
|
||||
break;
|
||||
|
||||
case 'mobile':
|
||||
$object->mobile = $phone['value'];
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new SCIMException("Unknown phone type '".@$phone['type']."'", 400);
|
||||
}
|
||||
}
|
||||
parent::patch($subop, $value, $object, $path, $removeIfNotSet);
|
||||
return;
|
||||
}
|
||||
foreach ($value as $phone) {
|
||||
switch ($phone['type']) {
|
||||
case 'work':
|
||||
$object->phone = $phone['value'];
|
||||
break;
|
||||
|
||||
case 'mobile':
|
||||
$object->mobile = $phone['value'];
|
||||
break;
|
||||
|
||||
default:
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
})->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
|
||||
@@ -269,13 +275,14 @@ class SnipeSCIMConfig
|
||||
->setMultiValued(true),
|
||||
|
||||
// addresses chonk
|
||||
(new class ('addresses') extends UpdatableComplex {
|
||||
static $addressmap = [
|
||||
(new class('addresses') extends UpdatableComplex
|
||||
{
|
||||
public static $addressmap = [
|
||||
'streetAddress' => 'address',
|
||||
'locality' => 'city',
|
||||
'region' => 'state',
|
||||
'postalCode' => 'zip',
|
||||
'country' => 'country'
|
||||
'country' => 'country',
|
||||
];
|
||||
|
||||
protected function doRead(&$object, $attributes = [])
|
||||
@@ -290,10 +297,11 @@ 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) {
|
||||
@@ -301,24 +309,23 @@ 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)) {
|
||||
throw new SCIMException("Unknown path type '$path'")->setCode(422);
|
||||
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];
|
||||
if ($type != 'work') {
|
||||
throw new SCIMException("Unknown object type '$type'")->setCode(422);
|
||||
throw new SCIMException("Unknown object type '$type'", 422);
|
||||
}
|
||||
$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")->setCode(422);
|
||||
throw new SCIMException("Could not handle path for update $path", 422);
|
||||
}
|
||||
}
|
||||
|
||||
})->withSubAttributes(
|
||||
eloquent('streetAddress', 'address'),
|
||||
eloquent('locality', 'city'),
|
||||
@@ -334,14 +341,15 @@ 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',
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -358,14 +366,16 @@ 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,
|
||||
];
|
||||
@@ -373,10 +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_scalar($value)) {
|
||||
\Log::debug("Weird Microsoft mode - set manager to the \$value and move on with life?");
|
||||
\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
|
||||
@@ -386,8 +396,8 @@ class SnipeSCIMConfig
|
||||
|
||||
// extract ID from URL, jam it in?
|
||||
$url = $value['$ref'];
|
||||
$users_prefix = route('scim.resources', ['resourceType' => 'User']) . '/';
|
||||
if (string_starts_with($url, $users_prefix)) {
|
||||
$users_prefix = route('scim.resources', ['resourceType' => 'User']).'/';
|
||||
if (str_starts_with($url, $users_prefix)) {
|
||||
$manager_id = substr($url, strlen($users_prefix));
|
||||
}
|
||||
} elseif (array_key_exists('value', $value)) {
|
||||
@@ -397,9 +407,10 @@ 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);
|
||||
@@ -421,24 +432,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)
|
||||
@@ -459,14 +470,15 @@ 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',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -219,6 +219,7 @@ trait Searchable
|
||||
* - "is:null" → operator = is_null, value = "" (reserved token)
|
||||
* - "is:not_null" → operator = is_not_null, value = "" (reserved token)
|
||||
* - "is:flarb" → operator = exact, value = "flarb" (exact equality)
|
||||
* - "is_not:flarb"→ operator = exact_not, value = "flarb" (exact inequality)
|
||||
*
|
||||
* `is:null` and `is:not_null` are checked before the generic `is:` prefix so they always
|
||||
* resolve to their dedicated null-check operators regardless of casing.
|
||||
@@ -249,6 +250,12 @@ trait Searchable
|
||||
return ['value' => $exactValue, 'negate' => false, 'operator' => 'exact'];
|
||||
}
|
||||
|
||||
if (str_starts_with($lower, 'is_not:')) {
|
||||
$exactNotValue = ltrim(substr($raw, 7));
|
||||
|
||||
return ['value' => $exactNotValue, 'negate' => true, 'operator' => 'exact_not'];
|
||||
}
|
||||
|
||||
if (str_starts_with($raw, '!')) {
|
||||
return ['value' => substr($raw, 1), 'negate' => true, 'operator' => 'not_like'];
|
||||
}
|
||||
@@ -296,10 +303,12 @@ trait Searchable
|
||||
$table = $this->getTable();
|
||||
$whereMethod = $boolean === 'or' ? 'orWhere' : 'where';
|
||||
$likeOperator = $negate ? 'NOT LIKE' : 'LIKE';
|
||||
$isExactOperator = in_array($operator, ['exact', 'exact_not'], true);
|
||||
$exactComparisonOperator = $operator === 'exact_not' ? '!=' : '=';
|
||||
|
||||
if (in_array($filterKey, $searchableAttributes, true)) {
|
||||
if ($operator === 'exact') {
|
||||
$query->{$whereMethod}($table.'.'.$filterKey, '=', $value);
|
||||
if ($isExactOperator) {
|
||||
$query->{$whereMethod}($table.'.'.$filterKey, $exactComparisonOperator, $value);
|
||||
} else {
|
||||
$query->{$whereMethod}($table.'.'.$filterKey, $likeOperator, '%'.$value.'%');
|
||||
}
|
||||
@@ -317,13 +326,13 @@ trait Searchable
|
||||
$virtualColumns[$filterKey]
|
||||
);
|
||||
|
||||
if ($operator === 'exact') {
|
||||
if ($isExactOperator) {
|
||||
// Exact match on the full CONCAT'd value, e.g. "John Smith" matches only
|
||||
// users whose first_name + ' ' + last_name equals exactly "John Smith".
|
||||
$concatSql = $this->buildMultipleColumnSearch($qualifiedColumns);
|
||||
// buildMultipleColumnSearch intentionally returns a fragment ending in "LIKE ?";
|
||||
// for exact matches we rewrite only the operator and keep the same SQL scaffold.
|
||||
$concatSql = str_replace(' LIKE ?', ' = ?', $concatSql);
|
||||
$concatSql = str_replace(' LIKE ?', $operator === 'exact_not' ? ' <> ?' : ' = ?', $concatSql);
|
||||
$rawMethod = $boolean === 'or' ? 'orWhereRaw' : 'whereRaw';
|
||||
$query->{$rawMethod}($concatSql, [$value]);
|
||||
} else {
|
||||
@@ -341,7 +350,7 @@ trait Searchable
|
||||
}
|
||||
|
||||
if (in_array($filterKey, $searchableCounts, true)) {
|
||||
return $this->applyCountAliasFilter($query, $filterKey, $value, $boolean, $negate);
|
||||
return $this->applyCountAliasFilter($query, $filterKey, $value, $boolean, $negate, $isExactOperator);
|
||||
}
|
||||
|
||||
// Check if this is a custom field (only for Assets - for *now*).
|
||||
@@ -351,8 +360,8 @@ trait Searchable
|
||||
$dbColumn = $this->resolveCustomFieldDbColumn($filterKey);
|
||||
|
||||
if ($dbColumn !== null) {
|
||||
if ($operator === 'exact') {
|
||||
$query->{$whereMethod}($table.'.'.$dbColumn, '=', $value);
|
||||
if ($isExactOperator) {
|
||||
$query->{$whereMethod}($table.'.'.$dbColumn, $exactComparisonOperator, $value);
|
||||
} else {
|
||||
$query->{$whereMethod}($table.'.'.$dbColumn, $likeOperator, '%'.$value.'%');
|
||||
}
|
||||
@@ -368,7 +377,7 @@ trait Searchable
|
||||
}
|
||||
|
||||
if ($this->isAssignedToRelationKey($resolvedRelationKey)) {
|
||||
return $this->applyAssignedToRelationFilter($query, $resolvedRelationKey, $value, $boolean, $negate);
|
||||
return $this->applyAssignedToRelationFilter($query, $resolvedRelationKey, $value, $boolean, $negate, $operator);
|
||||
}
|
||||
|
||||
$relationColumns = $this->getStructuredFilterRelationColumns(
|
||||
@@ -380,27 +389,29 @@ trait Searchable
|
||||
// For negated relation filters (e.g. location: !dam), include rows with
|
||||
// no related record as well as rows with related records that do not match.
|
||||
// This aligns advanced-search behavior with user expectation for "not X".
|
||||
if ($operator !== 'exact' && $likeOperator === 'NOT LIKE') {
|
||||
if ($operator === 'not_like' || $operator === 'exact_not') {
|
||||
$compoundMethod = $boolean === 'or' ? 'orWhere' : 'where';
|
||||
|
||||
$query->{$compoundMethod}(function (Builder $compoundQuery) use ($resolvedRelationKey, $relationColumns, $value): void {
|
||||
$query->{$compoundMethod}(function (Builder $compoundQuery) use ($resolvedRelationKey, $relationColumns, $value, $operator): void {
|
||||
// Critical behavior: "not X" on relations should include records with no relation.
|
||||
// Example: location=!dam should include users without a location.
|
||||
$compoundQuery->doesntHave($resolvedRelationKey)
|
||||
->orWhereHas($resolvedRelationKey, function (Builder $relationQuery) use ($resolvedRelationKey, $relationColumns, $value): void {
|
||||
->orWhereHas($resolvedRelationKey, function (Builder $relationQuery) use ($resolvedRelationKey, $relationColumns, $value, $operator): void {
|
||||
$relationTable = $this->getRelationTable($resolvedRelationKey);
|
||||
$firstConditionAdded = false;
|
||||
$relationComparisonOperator = $operator === 'exact_not' ? '!=' : 'NOT LIKE';
|
||||
$relationComparisonValue = $operator === 'exact_not' ? $value : '%'.$value.'%';
|
||||
|
||||
foreach ($relationColumns as $relationColumn) {
|
||||
if (! $firstConditionAdded) {
|
||||
$relationQuery->where($relationTable.'.'.$relationColumn, 'NOT LIKE', '%'.$value.'%');
|
||||
$relationQuery->where($relationTable.'.'.$relationColumn, $relationComparisonOperator, $relationComparisonValue);
|
||||
$firstConditionAdded = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// For negation we AND the NOT LIKE conditions so all columns must not match.
|
||||
$relationQuery->where($relationTable.'.'.$relationColumn, 'NOT LIKE', '%'.$value.'%');
|
||||
$relationQuery->where($relationTable.'.'.$relationColumn, $relationComparisonOperator, $relationComparisonValue);
|
||||
}
|
||||
|
||||
if (($resolvedRelationKey === 'adminuser') || ($resolvedRelationKey === 'user')) {
|
||||
@@ -410,7 +421,11 @@ trait Searchable
|
||||
'users.display_name',
|
||||
]);
|
||||
|
||||
$relationQuery->whereRaw(str_replace('LIKE', 'NOT LIKE', $concatSql), ["%{$value}%"]);
|
||||
if ($operator === 'exact_not') {
|
||||
$relationQuery->whereRaw(str_replace(' LIKE ?', ' <> ?', $concatSql), [$value]);
|
||||
} else {
|
||||
$relationQuery->whereRaw(str_replace('LIKE', 'NOT LIKE', $concatSql), ["%{$value}%"]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -428,6 +443,8 @@ trait Searchable
|
||||
if (! $firstConditionAdded) {
|
||||
if ($operator === 'exact') {
|
||||
$relationQuery->where($relationTable.'.'.$relationColumn, '=', $value);
|
||||
} elseif ($operator === 'exact_not') {
|
||||
$relationQuery->where($relationTable.'.'.$relationColumn, '!=', $value);
|
||||
} else {
|
||||
$relationQuery->where($relationTable.'.'.$relationColumn, $likeOperator, '%'.$value.'%');
|
||||
}
|
||||
@@ -440,6 +457,9 @@ trait Searchable
|
||||
// For exact matches across multiple columns, OR them — any column matching
|
||||
// the exact value is sufficient (e.g. name OR slug).
|
||||
$relationQuery->orWhere($relationTable.'.'.$relationColumn, '=', $value);
|
||||
} elseif ($operator === 'exact_not') {
|
||||
// For exact exclusions we AND the conditions so no column can equal the value.
|
||||
$relationQuery->where($relationTable.'.'.$relationColumn, '!=', $value);
|
||||
} elseif ($likeOperator === 'NOT LIKE') {
|
||||
// For negation we AND the NOT LIKE conditions so all columns must not match.
|
||||
$relationQuery->where($relationTable.'.'.$relationColumn, $likeOperator, '%'.$value.'%');
|
||||
@@ -459,6 +479,9 @@ trait Searchable
|
||||
if ($operator === 'exact') {
|
||||
$concatSql = str_replace(' LIKE ?', ' = ?', $concatSql);
|
||||
$relationQuery->orWhereRaw($concatSql, [$value]);
|
||||
} elseif ($operator === 'exact_not') {
|
||||
$concatSql = str_replace(' LIKE ?', ' <> ?', $concatSql);
|
||||
$relationQuery->whereRaw($concatSql, [$value]);
|
||||
} elseif ($likeOperator === 'NOT LIKE') {
|
||||
$relationQuery->whereRaw(str_replace('LIKE', 'NOT LIKE', $concatSql), ["%{$value}%"]);
|
||||
} else {
|
||||
@@ -524,7 +547,7 @@ trait Searchable
|
||||
* (Records with no assignee are excluded; they do not satisfy "has an assignee
|
||||
* where column NOT LIKE '%value%'".)
|
||||
*/
|
||||
private function applyAssignedToRelationFilter(Builder $query, string $relationKey, string $filterValue, string $boolean = 'and', bool $negate = false): Builder
|
||||
private function applyAssignedToRelationFilter(Builder $query, string $relationKey, string $filterValue, string $boolean = 'and', bool $negate = false, string $operator = 'like'): Builder
|
||||
{
|
||||
$relationName = $this->resolveAssignedToRelationName();
|
||||
|
||||
@@ -533,12 +556,14 @@ trait Searchable
|
||||
}
|
||||
|
||||
$likeOperator = $negate ? 'NOT LIKE' : 'LIKE';
|
||||
$isExactOperator = in_array($operator, ['exact', 'exact_not'], true);
|
||||
$exactComparisonOperator = $operator === 'exact_not' ? '!=' : '=';
|
||||
$relationMethod = $boolean === 'or' ? 'orWhereHasMorph' : 'whereHasMorph';
|
||||
|
||||
return $query->{$relationMethod}(
|
||||
$relationName,
|
||||
[User::class, Asset::class, Location::class],
|
||||
function (Builder $assigneeQuery, string $assigneeType) use ($filterValue, $likeOperator, $negate) {
|
||||
function (Builder $assigneeQuery, string $assigneeType) use ($filterValue, $likeOperator, $negate, $operator, $isExactOperator, $exactComparisonOperator) {
|
||||
$columns = $this->getAssigneeColumnsByType($assigneeType);
|
||||
|
||||
if (empty($columns)) {
|
||||
@@ -550,7 +575,11 @@ trait Searchable
|
||||
|
||||
foreach ($columns as $column) {
|
||||
if (! $firstConditionAdded) {
|
||||
$assigneeQuery->where($table.'.'.$column, $likeOperator, '%'.$filterValue.'%');
|
||||
if ($isExactOperator) {
|
||||
$assigneeQuery->where($table.'.'.$column, $exactComparisonOperator, $filterValue);
|
||||
} else {
|
||||
$assigneeQuery->where($table.'.'.$column, $likeOperator, '%'.$filterValue.'%');
|
||||
}
|
||||
$firstConditionAdded = true;
|
||||
|
||||
continue;
|
||||
@@ -558,17 +587,29 @@ trait Searchable
|
||||
|
||||
// For negation, AND the conditions (all columns must not match).
|
||||
// For normal LIKE, OR them (any column matching is sufficient).
|
||||
$negate
|
||||
? $assigneeQuery->where($table.'.'.$column, $likeOperator, '%'.$filterValue.'%')
|
||||
: $assigneeQuery->orWhere($table.'.'.$column, $likeOperator, '%'.$filterValue.'%');
|
||||
if ($operator === 'exact') {
|
||||
$assigneeQuery->orWhere($table.'.'.$column, '=', $filterValue);
|
||||
} elseif ($operator === 'exact_not') {
|
||||
$assigneeQuery->where($table.'.'.$column, '!=', $filterValue);
|
||||
} else {
|
||||
$negate
|
||||
? $assigneeQuery->where($table.'.'.$column, $likeOperator, '%'.$filterValue.'%')
|
||||
: $assigneeQuery->orWhere($table.'.'.$column, $likeOperator, '%'.$filterValue.'%');
|
||||
}
|
||||
}
|
||||
|
||||
if ($assigneeType === User::class) {
|
||||
$concatSql = $this->buildMultipleColumnSearch(['users.first_name', 'users.last_name']);
|
||||
|
||||
$negate
|
||||
? $assigneeQuery->whereRaw(str_replace('LIKE', 'NOT LIKE', $concatSql), ["%{$filterValue}%"])
|
||||
: $assigneeQuery->orWhereRaw($concatSql, ["%{$filterValue}%"]);
|
||||
if ($operator === 'exact') {
|
||||
$assigneeQuery->orWhereRaw(str_replace(' LIKE ?', ' = ?', $concatSql), [$filterValue]);
|
||||
} elseif ($operator === 'exact_not') {
|
||||
$assigneeQuery->whereRaw(str_replace(' LIKE ?', ' <> ?', $concatSql), [$filterValue]);
|
||||
} else {
|
||||
$negate
|
||||
? $assigneeQuery->whereRaw(str_replace('LIKE', 'NOT LIKE', $concatSql), ["%{$filterValue}%"])
|
||||
: $assigneeQuery->orWhereRaw($concatSql, ["%{$filterValue}%"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -613,7 +654,7 @@ trait Searchable
|
||||
/**
|
||||
* Apply filtering on computed count aliases (for example withCount aliases).
|
||||
*/
|
||||
private function applyCountAliasFilter(Builder $query, string $countAlias, string $filterValue, string $boolean = 'and', bool $negate = false): Builder
|
||||
private function applyCountAliasFilter(Builder $query, string $countAlias, string $filterValue, string $boolean = 'and', bool $negate = false, bool $exact = false): Builder
|
||||
{
|
||||
$havingMethod = $boolean === 'or' ? 'orHaving' : 'having';
|
||||
|
||||
@@ -623,6 +664,12 @@ trait Searchable
|
||||
return $query->{$havingMethod}($countAlias, $operator, (int) $filterValue);
|
||||
}
|
||||
|
||||
if ($exact) {
|
||||
$operator = $negate ? '!=' : '=';
|
||||
|
||||
return $query->{$havingMethod}($countAlias, $operator, $filterValue);
|
||||
}
|
||||
|
||||
$likeOperator = $negate ? 'NOT LIKE' : 'LIKE';
|
||||
|
||||
return $query->{$havingMethod}($countAlias, $likeOperator, '%'.$filterValue.'%');
|
||||
@@ -653,14 +700,21 @@ trait Searchable
|
||||
$dbColumn = $this->resolveCustomFieldDbColumn($filterKey);
|
||||
|
||||
if ($dbColumn !== null) {
|
||||
$method = match (true) {
|
||||
$isNull && $boolean === 'or' => 'orWhereNull',
|
||||
$isNull => 'whereNull',
|
||||
$boolean === 'or' => 'orWhereNotNull',
|
||||
default => 'whereNotNull',
|
||||
};
|
||||
$column = $table.'.'.$dbColumn;
|
||||
|
||||
$query->{$method}($table.'.'.$dbColumn);
|
||||
$method = $boolean === 'or' ? 'orWhere' : 'where';
|
||||
|
||||
$query->{$method}(function (Builder $subQuery) use ($column, $isNull): void {
|
||||
if ($isNull) {
|
||||
$subQuery->whereNull($column)
|
||||
->orWhere($column, '=', '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$subQuery->whereNotNull($column)
|
||||
->where($column, '!=', '');
|
||||
});
|
||||
|
||||
return $query;
|
||||
}
|
||||
@@ -668,14 +722,20 @@ trait Searchable
|
||||
|
||||
// Direct attribute column.
|
||||
if (in_array($filterKey, $searchableAttributes, true)) {
|
||||
$method = match (true) {
|
||||
$isNull && $boolean === 'or' => 'orWhereNull',
|
||||
$isNull => 'whereNull',
|
||||
$boolean === 'or' => 'orWhereNotNull',
|
||||
default => 'whereNotNull',
|
||||
};
|
||||
$column = $table.'.'.$filterKey;
|
||||
$method = $boolean === 'or' ? 'orWhere' : 'where';
|
||||
|
||||
$query->{$method}($table.'.'.$filterKey);
|
||||
$query->{$method}(function (Builder $subQuery) use ($column, $isNull): void {
|
||||
if ($isNull) {
|
||||
$subQuery->whereNull($column)
|
||||
->orWhere($column, '=', '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$subQuery->whereNotNull($column)
|
||||
->where($column, '!=', '');
|
||||
});
|
||||
|
||||
return $query;
|
||||
}
|
||||
@@ -710,7 +770,26 @@ trait Searchable
|
||||
$searchableRelations = $this->getSearchableRelations();
|
||||
$resolvedRelationKey = $this->resolveSearchableRelationKey($filterKey, $searchableRelations);
|
||||
|
||||
if ($resolvedRelationKey !== null && ! $this->isAssignedToRelationKey($resolvedRelationKey)) {
|
||||
if ($resolvedRelationKey !== null && $this->isAssignedToRelationKey($resolvedRelationKey)) {
|
||||
$method = $boolean === 'or' ? 'orWhere' : 'where';
|
||||
// Polymorphic assignment is present only when both columns are set; null matches either side missing.
|
||||
|
||||
if ($isNull) {
|
||||
$query->{$method}(function (Builder $assigneeNullQuery) use ($table): void {
|
||||
$assigneeNullQuery->whereNull($table.'.assigned_to')
|
||||
->orWhereNull($table.'.assigned_type');
|
||||
});
|
||||
} else {
|
||||
$query->{$method}(function (Builder $assigneeNotNullQuery) use ($table): void {
|
||||
$assigneeNotNullQuery->whereNotNull($table.'.assigned_to')
|
||||
->whereNotNull($table.'.assigned_type');
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
if ($resolvedRelationKey !== null) {
|
||||
if ($isNull) {
|
||||
$method = $boolean === 'or' ? 'orDoesntHave' : 'doesntHave';
|
||||
$query->{$method}($resolvedRelationKey);
|
||||
|
||||
+45
-1
@@ -725,6 +725,10 @@ 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(\App\Models\License::class, 'license_seats', 'assigned_to', 'license_id')->withPivot('id', 'created_at', 'updated_at')->wherePivotNull('asset_id')->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes the user -> reportTemplates relationship
|
||||
@@ -1389,7 +1393,47 @@ 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)
|
||||
->with([
|
||||
'assets.log' => fn ($query) => $query->withTrashed()
|
||||
->where('target_type', User::class)
|
||||
->where('target_id', $id)
|
||||
->where('action_type', 'accepted'),
|
||||
'assets.defaultLoc',
|
||||
'assets.location',
|
||||
'assets.model.category',
|
||||
'assets.assignedAssets.log' => fn ($query) => $query->withTrashed()
|
||||
->where('target_type', User::class)
|
||||
->where('target_id', $id)
|
||||
->where('action_type', 'accepted'),
|
||||
'assets.assignedAssets.assignedTo',
|
||||
'assets.assignedAssets.defaultLoc',
|
||||
'assets.assignedAssets.location',
|
||||
'assets.assignedAssets.model.category',
|
||||
'assets.components.category',
|
||||
'assets.licenses',
|
||||
'assets.licenses.category',
|
||||
'assets.assignedAccessories',
|
||||
'assets.assignedAccessories.accessory.category',
|
||||
'accessories.log' => fn ($query) => $query->withTrashed()
|
||||
->where('target_type', User::class)
|
||||
->where('target_id', $id)
|
||||
->where('action_type', 'accepted'),
|
||||
'accessories.category',
|
||||
'accessories.manufacturer',
|
||||
'consumables.log' => fn ($query) => $query->withTrashed()
|
||||
->where('target_type', User::class)
|
||||
->where('target_id', $id)
|
||||
->where('action_type', 'accepted'),
|
||||
'consumables.category',
|
||||
'consumables.manufacturer',
|
||||
'directLicenses.category',
|
||||
'licenses.category',
|
||||
])
|
||||
->withTrashed();
|
||||
}
|
||||
/**
|
||||
* Get all direct and indirect subordinates for this user.
|
||||
*
|
||||
|
||||
@@ -16,6 +16,13 @@ class LicensePresenter extends Presenter
|
||||
{
|
||||
$layout = [
|
||||
[
|
||||
'field' => 'checkbox',
|
||||
'checkbox' => true,
|
||||
'formatter' => 'checkboxEnabledFormatter',
|
||||
'titleTooltip' => trans('general.select_all_none'),
|
||||
'printIgnore' => true,
|
||||
'class' => 'hidden-print',
|
||||
], [
|
||||
'field' => 'id',
|
||||
'searchable' => false,
|
||||
'sortable' => true,
|
||||
@@ -115,7 +122,7 @@ class LicensePresenter extends Presenter
|
||||
'searchable' => false,
|
||||
'sortable' => false,
|
||||
'switchable' => true,
|
||||
'title' => '% ' . trans('general.remaining'),
|
||||
'title' => '% '.trans('general.remaining'),
|
||||
'visible' => true,
|
||||
'formatter' => 'progressBarFormatter',
|
||||
], [
|
||||
|
||||
@@ -44,11 +44,29 @@ class SettingsServiceProvider extends ServiceProvider
|
||||
return $limit;
|
||||
});
|
||||
|
||||
// Make sure the offset is actually set and is an integer
|
||||
// Make sure the offset is actually set and is an integer.
|
||||
// If 'page' is passed without 'offset', derive the offset from the page number.
|
||||
app()->singleton('api_offset_value', function () {
|
||||
$offset = intval(request('offset'));
|
||||
if (request()->filled('page') && ! request()->filled('offset')) {
|
||||
$page = max(1, intval(request('page')));
|
||||
|
||||
return $offset;
|
||||
return ($page - 1) * (int) app('api_limit_value');
|
||||
}
|
||||
|
||||
return intval(request('offset'));
|
||||
});
|
||||
|
||||
// Resolve the current page number for inclusion in API list responses.
|
||||
// Supports both page= and legacy offset= parameters.
|
||||
app()->singleton('api_current_page', function () {
|
||||
if (request()->filled('page') && ! request()->filled('offset')) {
|
||||
return max(1, intval(request('page')));
|
||||
}
|
||||
|
||||
$limit = (int) app('api_limit_value');
|
||||
$offset = (int) app('api_offset_value');
|
||||
|
||||
return $limit > 0 ? (int) floor($offset / $limit) + 1 : 1;
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"alek13/slack": "^2.0",
|
||||
"arietimmerman/laravel-scim-server": "dev-upstream_master",
|
||||
"arietimmerman/laravel-scim-server": "dev-scimv2_with_logging",
|
||||
"bacon/bacon-qr-code": "^2.0",
|
||||
"doctrine/cache": "^1.10",
|
||||
"doctrine/dbal": "^3.1",
|
||||
|
||||
Generated
+7
-7
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "6dbe361f8555681a027fca2d2c2e61fd",
|
||||
"content-hash": "3ad09b8be28e9a805ed3c1cc51c9ffaa",
|
||||
"packages": [
|
||||
{
|
||||
"name": "alek13/slack",
|
||||
@@ -74,16 +74,16 @@
|
||||
},
|
||||
{
|
||||
"name": "arietimmerman/laravel-scim-server",
|
||||
"version": "dev-upstream_master",
|
||||
"version": "dev-scimv2_with_logging",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/grokability/laravel-scim-server.git",
|
||||
"reference": "da40db79d76cf3b4c7e57cde41df6ecf8119afb7"
|
||||
"reference": "5ddb8188dd50e2bdb6f8133b9b7f0a0b54b83148"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/grokability/laravel-scim-server/zipball/da40db79d76cf3b4c7e57cde41df6ecf8119afb7",
|
||||
"reference": "da40db79d76cf3b4c7e57cde41df6ecf8119afb7",
|
||||
"url": "https://api.github.com/repos/grokability/laravel-scim-server/zipball/5ddb8188dd50e2bdb6f8133b9b7f0a0b54b83148",
|
||||
"reference": "5ddb8188dd50e2bdb6f8133b9b7f0a0b54b83148",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -131,9 +131,9 @@
|
||||
],
|
||||
"description": "Laravel Package for creating a SCIM server",
|
||||
"support": {
|
||||
"source": "https://github.com/grokability/laravel-scim-server/tree/upstream_master"
|
||||
"source": "https://github.com/grokability/laravel-scim-server/tree/scimv2_with_logging"
|
||||
},
|
||||
"time": "2025-08-28T19:24:40+00:00"
|
||||
"time": "2026-05-14T09:39:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
|
||||
+7
-8
@@ -1,11 +1,10 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'app_version' => 'v8.5.0-pre',
|
||||
'full_app_version' => 'v8.5.0-pre - build 22392-g5014b1c459',
|
||||
'build_version' => '22392',
|
||||
return array(
|
||||
'app_version' => 'v8.5.0',
|
||||
'full_app_version' => 'v8.5.0 - build 22652-g80d1bf6a7a',
|
||||
'build_version' => '22652',
|
||||
'prerelease_version' => '',
|
||||
'hash_version' => 'g5014b1c459',
|
||||
'full_hash' => 'v8.5.0-pre-207-g5014b1c459',
|
||||
'hash_version' => 'g80d1bf6a7a',
|
||||
'full_hash' => 'v8.5.0-467-g80d1bf6a7a',
|
||||
'branch' => 'develop',
|
||||
];
|
||||
);
|
||||
@@ -59,7 +59,7 @@ class AssetFactory extends Factory
|
||||
// the explicit boolean gets set in the saving() method on the observer
|
||||
$asset->asset_eol_date = $this->faker->boolean(5)
|
||||
? CarbonImmutable::parse($asset->purchase_date)->addMonths(rand(0, 20))->format('Y-m-d')
|
||||
: CarbonImmutable::parse($asset->purchase_date)->addMonths($asset->model->eol)->format('Y-m-d');
|
||||
: CarbonImmutable::parse($asset->purchase_date)->addMonths($asset->model?->eol ?? rand(12, 60))->format('Y-m-d');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ return [
|
||||
'manage' => 'crwdns6501:0crwdne6501:0',
|
||||
'field' => 'crwdns1487:0crwdne1487:0',
|
||||
'about_fieldsets_title' => 'crwdns1488:0crwdne1488:0',
|
||||
'about_fieldsets_text' => 'crwdns14566:0crwdne14566:0',
|
||||
'about_fieldsets_text' => 'crwdns14734:0crwdne14734:0',
|
||||
'custom_format' => 'crwdns6505:0crwdne6505:0',
|
||||
'encrypt_field' => 'crwdns1792:0crwdne1792:0',
|
||||
'encrypt_field_help' => 'crwdns1683:0crwdne1683:0',
|
||||
|
||||
@@ -99,6 +99,9 @@ return [
|
||||
'success' => 'crwdns753:0crwdne753:0',
|
||||
'user_does_not_exist' => 'crwdns754:0crwdne754:0',
|
||||
'already_checked_in' => 'crwdns1603:0crwdne1603:0',
|
||||
'force_checkin_orphaned_success' => 'crwdns14742:0crwdne14742:0',
|
||||
'force_checkin_not_orphaned' => 'crwdns14744:0crwdne14744:0',
|
||||
'force_checkin_error' => 'crwdns14746:0crwdne14746:0',
|
||||
|
||||
],
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ return [
|
||||
'confirm' => 'crwdns943:0crwdne943:0',
|
||||
'error' => 'crwdns944:0crwdne944:0',
|
||||
'success' => 'crwdns945:0crwdne945:0',
|
||||
'bulk_success' => 'crwdns14793:0crwdne14793:0',
|
||||
'partial_success' => 'crwdns14795:0crwdne14795:0',
|
||||
'bulk_checkout_warning' => 'crwdns14797:0crwdne14797:0',
|
||||
],
|
||||
|
||||
'checkout' => [
|
||||
|
||||
@@ -438,7 +438,7 @@ return [
|
||||
'timezone' => 'crwdns11831:0crwdne11831:0',
|
||||
'test_mail' => 'crwdns12973:0crwdne12973:0',
|
||||
'profile_edit' => 'crwdns12280:0crwdne12280:0',
|
||||
'profile_edit_help' => 'crwdns12282:0crwdne12282:0',
|
||||
'profile_edit_help' => 'crwdns14789:0crwdne14789:0',
|
||||
'default_avatar' => 'crwdns12987:0crwdne12987:0',
|
||||
'default_avatar_help' => 'crwdns12590:0crwdne12590:0',
|
||||
'restore_default_avatar' => 'crwdns12592:0crwdne12592:0',
|
||||
|
||||
@@ -122,7 +122,7 @@ return [
|
||||
'debug_warning_text' => 'crwdns1828:0crwdne1828:0',
|
||||
'delete' => 'crwdns1046:0crwdne1046:0',
|
||||
'delete_confirm' => 'crwdns2020:0crwdne2020:0',
|
||||
'delete_confirm_no_undo' => 'crwdns14568:0crwdne14568:0',
|
||||
'delete_confirm_no_undo' => 'crwdns14736:0crwdne14736:0',
|
||||
'deleted' => 'crwdns1047:0crwdne1047:0',
|
||||
'delete_seats' => 'crwdns1430:0crwdne1430:0',
|
||||
'deletion_failed' => 'crwdns6117:0crwdne6117:0',
|
||||
@@ -167,7 +167,7 @@ return [
|
||||
'image_upload' => 'crwdns1058:0crwdne1058:0',
|
||||
'filetypes_accepted_help' => 'crwdns12622:0crwdne12622:0',
|
||||
'filetypes_size_help' => 'crwdns12624:0crwdne12624:0',
|
||||
'image_filetypes_help' => 'crwdns14570:0crwdne14570:0',
|
||||
'image_filetypes_help' => 'crwdns14738:0crwdne14738:0',
|
||||
'unaccepted_image_type' => 'crwdns11365:0crwdne11365:0',
|
||||
'import' => 'crwdns1411:0crwdne1411:0',
|
||||
'documentation' => 'crwdns14462:0crwdne14462:0',
|
||||
@@ -196,6 +196,7 @@ return [
|
||||
'license' => 'crwdns1060:0crwdne1060:0',
|
||||
'license_report' => 'crwdns1141:0crwdne1141:0',
|
||||
'licenses_available' => 'crwdns12172:0crwdne12172:0',
|
||||
'licenses_with_no_seats' => 'crwdns14761:0crwdne14761:0',
|
||||
'licenses' => 'crwdns1062:0crwdne1062:0',
|
||||
'list_all' => 'crwdns1063:0crwdne1063:0',
|
||||
'loading' => 'crwdns12628:0crwdne12628:0',
|
||||
@@ -484,8 +485,22 @@ return [
|
||||
'set_to_null' => 'crwdns12738:0crwdne12738:0',
|
||||
'set_users_field_to_null' => 'crwdns11449:0crwdne11449:0',
|
||||
'na_no_purchase_date' => 'crwdns10540:0crwdne10540:0',
|
||||
'assets_by_category' => 'crwdns14763:0crwdne14763:0',
|
||||
'assets_by_status' => 'crwdns10542:0crwdne10542:0',
|
||||
'assets_by_status_type' => 'crwdns10544:0crwdne10544:0',
|
||||
'activity_overview' => 'crwdns14765:0crwdne14765:0',
|
||||
'checkouts_checkins' => 'crwdns14767:0crwdne14767:0',
|
||||
'assets_newly_added' => 'crwdns14769:0crwdne14769:0',
|
||||
'checkouts' => 'crwdns14771:0crwdne14771:0',
|
||||
'checkins' => 'crwdns14773:0crwdne14773:0',
|
||||
'assets' => 'crwdns14775:0crwdne14775:0',
|
||||
|
||||
'vs_prior_period' => 'crwdns14777:0crwdne14777:0',
|
||||
'time_range' => 'crwdns14779:0crwdne14779:0',
|
||||
'last_n_days' => 'crwdns14781:0crwdne14781:0',
|
||||
'custom_range' => 'crwdns14783:0crwdne14783:0',
|
||||
'download_chart' => 'crwdns14785:0crwdne14785:0',
|
||||
'fullscreen' => 'crwdns14787:0crwdne14787:0',
|
||||
'pie_chart_type' => 'crwdns10546:0crwdne10546:0',
|
||||
'hello_name' => 'crwdns10548:0crwdne10548:0',
|
||||
'unaccepted_profile_warning' => 'crwdns12686:0crwdne12686:0',
|
||||
@@ -599,10 +614,16 @@ return [
|
||||
'status_compatibility' => 'crwdns11910:0crwdne11910:0',
|
||||
'rtd_location_help' => 'crwdns11912:0crwdne11912:0',
|
||||
'item_not_found' => 'crwdns11914:0crwdne11914:0',
|
||||
'item_target_not_found_hard' => 'crwdns14748:0crwdne14748:0',
|
||||
'force_checkin' => 'crwdns14750:0crwdne14750:0',
|
||||
'item_not_found_short' => 'crwdns14752:0crwdne14752:0',
|
||||
'action_permission_denied' => 'crwdns11916:0crwdne11916:0',
|
||||
'action_permission_generic' => 'crwdns11918:0crwdne11918:0',
|
||||
'edit' => 'crwdns11920:0crwdne11920:0',
|
||||
'search_operator' => 'crwdns14754:0crwdne14754:0',
|
||||
'and' => 'crwdns14756:0crwdne14756:0',
|
||||
'action_source' => 'crwdns11924:0crwdne11924:0',
|
||||
'search_tip' => 'crwdns14759:0crwdne14759:0',
|
||||
'or' => 'crwdns12024:0crwdne12024:0',
|
||||
'url' => 'crwdns12054:0crwdne12054:0',
|
||||
'phone' => 'crwdns13254:0crwdne13254:0',
|
||||
|
||||
@@ -9,6 +9,7 @@ return [
|
||||
'am-ET' => 'crwdns11934:0crwdne11934:0',
|
||||
'af-ZA' => 'crwdns11936:0crwdne11936:0',
|
||||
'ar-SA' => 'crwdns11938:0crwdne11938:0',
|
||||
'hy-AM' => 'crwdns14740:0crwdne14740:0',
|
||||
'bg-BG' => 'crwdns11940:0crwdne11940:0',
|
||||
'zh-CN' => 'crwdns10572:0crwdne10572:0',
|
||||
'zh-TW' => 'crwdns10574:0crwdne10574:0',
|
||||
|
||||
@@ -48,6 +48,7 @@ return [
|
||||
'asset_tag' => 'crwdns6068:0crwdne6068:0',
|
||||
'assets_warrantee_alert' => 'crwdns13812:0crwdne13812:0',
|
||||
'assigned_to' => 'crwdns1714:0crwdne1714:0',
|
||||
'assigned_to_assets' => 'crwdns14791:0crwdne14791:0',
|
||||
'eol' => 'crwdns13814:0crwdne13814:0',
|
||||
'best_regards' => 'crwdns1715:0crwdne1715:0',
|
||||
'canceled' => 'crwdns12718:0crwdne12718:0',
|
||||
|
||||
@@ -5,7 +5,7 @@ return [
|
||||
'manage' => 'Manage',
|
||||
'field' => 'veld',
|
||||
'about_fieldsets_title' => 'Oor Fieldsets',
|
||||
'about_fieldsets_text' => 'Fieldsets allow you to create groups of custom fields that are frequently re-used for specific asset model types.',
|
||||
'about_fieldsets_text' => 'Veldstelle stel jou in staat om groepe van persoonlike velde te skep wat gereeld hergebruik word vir spesifieke tipe bates.',
|
||||
'custom_format' => 'Custom Regex format...',
|
||||
'encrypt_field' => 'Enkripteer die waarde van hierdie veld in die databasis',
|
||||
'encrypt_field_help' => 'WAARSKUWING: Om \'n veld te enkripteer, maak dit onondersoekbaar.',
|
||||
|
||||
@@ -99,6 +99,9 @@ return [
|
||||
'success' => 'Die bate is suksesvol nagegaan.',
|
||||
'user_does_not_exist' => 'Die gebruiker is ongeldig. Probeer asseblief weer.',
|
||||
'already_checked_in' => 'Daardie bate is reeds nagegaan.',
|
||||
'force_checkin_orphaned_success' => 'Invalid assignment cleared successfully.',
|
||||
'force_checkin_not_orphaned' => 'Item is not in an invalid assignment state.',
|
||||
'force_checkin_error' => 'Could not clear invalid assignment.',
|
||||
|
||||
],
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ return [
|
||||
'confirm' => 'Is jy seker jy wil hierdie lisensie uitvee?',
|
||||
'error' => 'Daar was \'n probleem met die verwydering van die lisensie. Probeer asseblief weer.',
|
||||
'success' => 'Die lisensie is suksesvol verwyder.',
|
||||
'bulk_success' => 'The selected licenses were deleted successfully.',
|
||||
'partial_success' => 'License deleted successfully. See additional information below. | :count licenses were deleted successfully. See additional information below.',
|
||||
'bulk_checkout_warning' => ':license_name has seats that are currently checked out and cannot be deleted. Please check in all seats before deleting.',
|
||||
],
|
||||
|
||||
'checkout' => [
|
||||
|
||||
@@ -438,7 +438,7 @@ return [
|
||||
'timezone' => 'Timezone',
|
||||
'test_mail' => 'Test Mail',
|
||||
'profile_edit' => 'Edit Profile',
|
||||
'profile_edit_help' => 'Allow users to edit their own profiles.',
|
||||
'profile_edit_help' => 'Allow users to edit their own profiles. (This does not include accessibility settings like light/dark mode, audio feedback, or link colors.)',
|
||||
'default_avatar' => 'Custom Default Avatar',
|
||||
'default_avatar_help' => 'This image will be displayed as a profile if a user does not have a profile photo.',
|
||||
'restore_default_avatar' => 'Restore <a href=":default_avatar" data-toggle="lightbox" data-type="image">original system default avatar</a>',
|
||||
|
||||
@@ -122,7 +122,7 @@ return [
|
||||
'debug_warning_text' => 'Hierdie program word uitgevoer in die produksiemodus met debugging aangeskakel. Dit kan sensitiewe data blootstel indien u aansoek vir die buitewêreld toeganklik is. Deaktiveer debug-modus deur die <code>APP_DEBUG</code>-waarde in jou <code>.env</code>-lêer te stel na <code>false</code>.',
|
||||
'delete' => 'verwyder',
|
||||
'delete_confirm' => 'Are you sure you wish to delete :item?',
|
||||
'delete_confirm_no_undo' => 'Are you sure, you wish to delete :item? This cannot be undone.',
|
||||
'delete_confirm_no_undo' => 'Are you sure you wish to delete :item? This cannot be undone.',
|
||||
'deleted' => 'geskrap',
|
||||
'delete_seats' => 'Plekke verwyder',
|
||||
'deletion_failed' => 'Deletion failed',
|
||||
@@ -167,7 +167,7 @@ return [
|
||||
'image_upload' => 'Laai prent op',
|
||||
'filetypes_accepted_help' => 'Accepted filetype is :types. The maximum size allowed is :size.|Accepted filetypes are :types. The maximum upload size allowed is :size.',
|
||||
'filetypes_size_help' => 'The maximum upload size allowed is :size.',
|
||||
'image_filetypes_help' => 'Accepted Filetypes are jpg, webp, png, gif, svg, and avif. The maximum upload size allowed is :size.',
|
||||
'image_filetypes_help' => 'Accepted filetypes are jpg, webp, png, gif, svg, and avif. The maximum upload size allowed is :size.',
|
||||
'unaccepted_image_type' => 'This image file was not readable. Accepted filetypes are jpg, webp, png, gif, and svg. The mimetype of this file is: :mimetype.',
|
||||
'import' => 'invoer',
|
||||
'documentation' => 'Open documentation in a new link',
|
||||
@@ -196,6 +196,7 @@ return [
|
||||
'license' => 'lisensie',
|
||||
'license_report' => 'Lisensie Verslag',
|
||||
'licenses_available' => 'Licenses available',
|
||||
'licenses_with_no_seats' => 'Licenses with No Available Seats',
|
||||
'licenses' => 'lisensies',
|
||||
'list_all' => 'Lys almal',
|
||||
'loading' => 'Loading... please wait...',
|
||||
@@ -485,8 +486,22 @@ return [
|
||||
'set_to_null' => 'Delete values for this selection|Delete values for all :selection_count selections ',
|
||||
'set_users_field_to_null' => 'Delete :field values for this user|Delete :field values for all :user_count users ',
|
||||
'na_no_purchase_date' => 'N/A - No purchase date provided',
|
||||
'assets_by_category' => 'Assets by Category',
|
||||
'assets_by_status' => 'Assets by Status',
|
||||
'assets_by_status_type' => 'Assets by Status Type',
|
||||
'activity_overview' => 'Activity Overview',
|
||||
'checkouts_checkins' => 'Checkouts & Check-ins',
|
||||
'assets_newly_added' => 'Assets Added',
|
||||
'checkouts' => 'Checkouts',
|
||||
'checkins' => 'Check-ins',
|
||||
'assets' => 'bates',
|
||||
|
||||
'vs_prior_period' => 'vs. prior period',
|
||||
'time_range' => 'Select Date Range',
|
||||
'last_n_days' => 'Last :days Days',
|
||||
'custom_range' => 'Custom Range',
|
||||
'download_chart' => 'Download Chart as PNG',
|
||||
'fullscreen' => 'Fullscreen',
|
||||
'pie_chart_type' => 'Dashboard Pie Chart Type',
|
||||
'hello_name' => 'Hello, :name!',
|
||||
'unaccepted_profile_warning' => 'You have one item requiring acceptance. Click here to accept or decline it | You have :count items requiring acceptance. Click here to accept or decline them',
|
||||
@@ -600,10 +615,16 @@ return [
|
||||
'status_compatibility' => 'If assets are already assigned, they cannot be changed to a non-deployable status type and this value change will be skipped.',
|
||||
'rtd_location_help' => 'This is the location of the asset when it is not checked out',
|
||||
'item_not_found' => ':item_type ID :id does not exist or has been deleted',
|
||||
'item_target_not_found_hard' => ':item_type ID :id does not exist or has been hard-deleted. Would you like to force a checkin?',
|
||||
'force_checkin' => 'Force Checkin',
|
||||
'item_not_found_short' => ':item_type ID :id does not exist',
|
||||
'action_permission_denied' => 'You do not have permission to :action :item_type ID :id',
|
||||
'action_permission_generic' => 'You do not have permission to :action this :item_type',
|
||||
'edit' => 'wysig',
|
||||
'search_operator' => 'Search operator',
|
||||
'and' => 'and',
|
||||
'action_source' => 'Action Source',
|
||||
'search_tip' => 'Searches return a partial match by default. For more specific results, you can use <code>not:value</code> to fuzzy-exclude, <code>is:value</code> for an exact match, <code>is_not:value</code> for an exact exclusion, <code>is:null</code> for empty values, and <code>is:not_null</code> for non-empty. (In these examples, <code>value</code> is the text you are searching for.)',
|
||||
'or' => 'or',
|
||||
'url' => 'URL',
|
||||
'phone' => 'Foon',
|
||||
|
||||
@@ -9,6 +9,7 @@ return [
|
||||
'am-ET' => 'Amharic',
|
||||
'af-ZA' => 'Afrikaans',
|
||||
'ar-SA' => 'Arabic',
|
||||
'hy-AM' => 'Armenian',
|
||||
'bg-BG' => 'Bulgarian',
|
||||
'zh-CN' => 'Chinese Simplified',
|
||||
'zh-TW' => 'Chinese Traditional',
|
||||
|
||||
@@ -48,6 +48,7 @@ return [
|
||||
'asset_tag' => 'Bate-tag',
|
||||
'assets_warrantee_alert' => 'There is :count asset with an expiring warranty or that are reaching their end of life in the next :threshold days.|There are :count assets with expiring warranties or that are reaching their end of life in the next :threshold days.',
|
||||
'assigned_to' => 'Toevertrou aan',
|
||||
'assigned_to_assets' => 'Assignments to Assets',
|
||||
'eol' => 'EOL',
|
||||
'best_regards' => 'Beste wense,',
|
||||
'canceled' => 'Gekanselleer',
|
||||
|
||||
@@ -5,7 +5,7 @@ return [
|
||||
'manage' => 'Manage',
|
||||
'field' => 'Field',
|
||||
'about_fieldsets_title' => 'About Fieldsets',
|
||||
'about_fieldsets_text' => 'Fieldsets allow you to create groups of custom fields that are frequently re-used for specific asset model types.',
|
||||
'about_fieldsets_text' => 'Fieldsets allow you to create groups of custom fields that are frequently re-used used for specific asset model types.',
|
||||
'custom_format' => 'Custom Regex format...',
|
||||
'encrypt_field' => 'Encrypt the value of this field in the database',
|
||||
'encrypt_field_help' => 'WARNING: Encrypting a field makes it unsearchable.',
|
||||
|
||||
@@ -99,6 +99,9 @@ return [
|
||||
'success' => 'Asset checked in successfully.',
|
||||
'user_does_not_exist' => 'That user is invalid. Please try again.',
|
||||
'already_checked_in' => 'That asset is already checked in.',
|
||||
'force_checkin_orphaned_success' => 'Invalid assignment cleared successfully.',
|
||||
'force_checkin_not_orphaned' => 'Item is not in an invalid assignment state.',
|
||||
'force_checkin_error' => 'Could not clear invalid assignment.',
|
||||
|
||||
],
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ return [
|
||||
'confirm' => 'Are you sure you wish to delete this license?',
|
||||
'error' => 'There was an issue deleting the license. Please try again.',
|
||||
'success' => 'The license was deleted successfully.',
|
||||
'bulk_success' => 'The selected licenses were deleted successfully.',
|
||||
'partial_success' => 'License deleted successfully. See additional information below. | :count licenses were deleted successfully. See additional information below.',
|
||||
'bulk_checkout_warning' => ':license_name has seats that are currently checked out and cannot be deleted. Please check in all seats before deleting.',
|
||||
],
|
||||
|
||||
'checkout' => [
|
||||
|
||||
@@ -438,7 +438,7 @@ return [
|
||||
'timezone' => 'Timezone',
|
||||
'test_mail' => 'Test Mail',
|
||||
'profile_edit' => 'Edit Profile',
|
||||
'profile_edit_help' => 'Allow users to edit their own profiles.',
|
||||
'profile_edit_help' => 'Allow users to edit their own profiles. (This does not include accessibility settings like light/dark mode, audio feedback, or link colors.)',
|
||||
'default_avatar' => 'Custom Default Avatar',
|
||||
'default_avatar_help' => 'This image will be displayed as a profile if a user does not have a profile photo.',
|
||||
'restore_default_avatar' => 'Restore <a href=":default_avatar" data-toggle="lightbox" data-type="image">original system default avatar</a>',
|
||||
|
||||
@@ -122,7 +122,7 @@ return [
|
||||
'debug_warning_text' => 'This application is running in production mode with debugging enabled. This can expose sensitive data if your application is accessible to the outside world. Disable debug mode by setting the <code>APP_DEBUG</code> value in your <code>.env</code> file to <code>false</code>.',
|
||||
'delete' => 'Delete',
|
||||
'delete_confirm' => 'Are you sure you wish to delete :item?',
|
||||
'delete_confirm_no_undo' => 'Are you sure, you wish to delete :item? This cannot be undone.',
|
||||
'delete_confirm_no_undo' => 'Are you sure you wish to delete :item? This cannot be undone.',
|
||||
'deleted' => 'Deleted',
|
||||
'delete_seats' => 'Deleted Seats',
|
||||
'deletion_failed' => 'Deletion failed',
|
||||
@@ -167,7 +167,7 @@ return [
|
||||
'image_upload' => 'Upload Image',
|
||||
'filetypes_accepted_help' => 'Accepted filetype is :types. The maximum size allowed is :size.|Accepted filetypes are :types. The maximum upload size allowed is :size.',
|
||||
'filetypes_size_help' => 'The maximum upload size allowed is :size.',
|
||||
'image_filetypes_help' => 'Accepted Filetypes are jpg, webp, png, gif, svg, and avif. The maximum upload size allowed is :size.',
|
||||
'image_filetypes_help' => 'Accepted filetypes are jpg, webp, png, gif, svg, and avif. The maximum upload size allowed is :size.',
|
||||
'unaccepted_image_type' => 'This image file was not readable. Accepted filetypes are jpg, webp, png, gif, and svg. The mimetype of this file is: :mimetype.',
|
||||
'import' => 'Import',
|
||||
'documentation' => 'Open documentation in a new link',
|
||||
@@ -196,6 +196,7 @@ return [
|
||||
'license' => 'License',
|
||||
'license_report' => 'License Report',
|
||||
'licenses_available' => 'Licenses available',
|
||||
'licenses_with_no_seats' => 'Licenses with No Available Seats',
|
||||
'licenses' => 'Licenses',
|
||||
'list_all' => 'List All',
|
||||
'loading' => 'Loading... please wait...',
|
||||
@@ -485,8 +486,22 @@ return [
|
||||
'set_to_null' => 'Delete values for this selection|Delete values for all :selection_count selections ',
|
||||
'set_users_field_to_null' => 'Delete :field values for this user|Delete :field values for all :user_count users ',
|
||||
'na_no_purchase_date' => 'N/A - No purchase date provided',
|
||||
'assets_by_category' => 'Assets by Category',
|
||||
'assets_by_status' => 'Assets by Status',
|
||||
'assets_by_status_type' => 'Assets by Status Type',
|
||||
'activity_overview' => 'Activity Overview',
|
||||
'checkouts_checkins' => 'Checkouts & Check-ins',
|
||||
'assets_newly_added' => 'Assets Added',
|
||||
'checkouts' => 'Checkouts',
|
||||
'checkins' => 'Check-ins',
|
||||
'assets' => 'ንብረቶች',
|
||||
|
||||
'vs_prior_period' => 'vs. prior period',
|
||||
'time_range' => 'Select Date Range',
|
||||
'last_n_days' => 'Last :days Days',
|
||||
'custom_range' => 'Custom Range',
|
||||
'download_chart' => 'Download Chart as PNG',
|
||||
'fullscreen' => 'Fullscreen',
|
||||
'pie_chart_type' => 'Dashboard Pie Chart Type',
|
||||
'hello_name' => 'Hello, :name!',
|
||||
'unaccepted_profile_warning' => 'You have one item requiring acceptance. Click here to accept or decline it | You have :count items requiring acceptance. Click here to accept or decline them',
|
||||
@@ -600,10 +615,16 @@ return [
|
||||
'status_compatibility' => 'If assets are already assigned, they cannot be changed to a non-deployable status type and this value change will be skipped.',
|
||||
'rtd_location_help' => 'This is the location of the asset when it is not checked out',
|
||||
'item_not_found' => ':item_type ID :id does not exist or has been deleted',
|
||||
'item_target_not_found_hard' => ':item_type ID :id does not exist or has been hard-deleted. Would you like to force a checkin?',
|
||||
'force_checkin' => 'Force Checkin',
|
||||
'item_not_found_short' => ':item_type ID :id does not exist',
|
||||
'action_permission_denied' => 'You do not have permission to :action :item_type ID :id',
|
||||
'action_permission_generic' => 'You do not have permission to :action this :item_type',
|
||||
'edit' => 'edit',
|
||||
'search_operator' => 'Search operator',
|
||||
'and' => 'and',
|
||||
'action_source' => 'Action Source',
|
||||
'search_tip' => 'Searches return a partial match by default. For more specific results, you can use <code>not:value</code> to fuzzy-exclude, <code>is:value</code> for an exact match, <code>is_not:value</code> for an exact exclusion, <code>is:null</code> for empty values, and <code>is:not_null</code> for non-empty. (In these examples, <code>value</code> is the text you are searching for.)',
|
||||
'or' => 'or',
|
||||
'url' => 'URL',
|
||||
'phone' => 'Phone',
|
||||
|
||||
@@ -9,6 +9,7 @@ return [
|
||||
'am-ET' => 'Amharic',
|
||||
'af-ZA' => 'Afrikaans',
|
||||
'ar-SA' => 'Arabic',
|
||||
'hy-AM' => 'Armenian',
|
||||
'bg-BG' => 'Bulgarian',
|
||||
'zh-CN' => 'Chinese Simplified',
|
||||
'zh-TW' => 'Chinese Traditional',
|
||||
|
||||
@@ -48,6 +48,7 @@ return [
|
||||
'asset_tag' => 'የንብረት መለያ',
|
||||
'assets_warrantee_alert' => 'There is :count asset with an expiring warranty or that are reaching their end of life in the next :threshold days.|There are :count assets with expiring warranties or that are reaching their end of life in the next :threshold days.',
|
||||
'assigned_to' => 'Assigned To',
|
||||
'assigned_to_assets' => 'Assignments to Assets',
|
||||
'eol' => 'EOL',
|
||||
'best_regards' => 'Best regards,',
|
||||
'canceled' => 'Canceled',
|
||||
|
||||
@@ -5,7 +5,7 @@ return [
|
||||
'manage' => 'إدارة',
|
||||
'field' => 'حقل',
|
||||
'about_fieldsets_title' => 'حول مجموعة الحقول',
|
||||
'about_fieldsets_text' => 'مجموعات الحقول تسمح لك بإنشاء مجموعات من الحقول المخصصة التي يعاد استخدامها في كثير من الأحيان لأنواع معينة من نماذج الأصول.',
|
||||
'about_fieldsets_text' => '(مجموعات الحقول) تسمح لك بإنشاء مجموعات من الحقول اللتي يمكن إعادة إستخدامها مع موديل محدد.',
|
||||
'custom_format' => 'تنسيق Regex المخصص...',
|
||||
'encrypt_field' => 'تشفير قيمة هذا الحقل في قاعدة البيانات',
|
||||
'encrypt_field_help' => 'تحذير: تشفير الحقل يجعله غير قابل للبحث.',
|
||||
|
||||
@@ -99,6 +99,9 @@ return [
|
||||
'success' => 'تم ادخال الأصل بنجاح.',
|
||||
'user_does_not_exist' => 'هذا المستخدم غير صالح. حاول مرة اخرى.',
|
||||
'already_checked_in' => 'تم ادخال هذا الأصل مسبقا.',
|
||||
'force_checkin_orphaned_success' => 'Invalid assignment cleared successfully.',
|
||||
'force_checkin_not_orphaned' => 'Item is not in an invalid assignment state.',
|
||||
'force_checkin_error' => 'Could not clear invalid assignment.',
|
||||
|
||||
],
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ return [
|
||||
'confirm' => 'هل أنت متأكد من رغبتك في حذف هذا الترخيص؟',
|
||||
'error' => 'حدثت مشكلة أثناء حذف الترخيص. يرجى إعادة المحاولة.',
|
||||
'success' => 'تم حذف الترخيص بنجاح.',
|
||||
'bulk_success' => 'The selected licenses were deleted successfully.',
|
||||
'partial_success' => 'License deleted successfully. See additional information below. | :count licenses were deleted successfully. See additional information below.',
|
||||
'bulk_checkout_warning' => ':license_name has seats that are currently checked out and cannot be deleted. Please check in all seats before deleting.',
|
||||
],
|
||||
|
||||
'checkout' => [
|
||||
|
||||
@@ -438,7 +438,7 @@ return [
|
||||
'timezone' => 'Timezone',
|
||||
'test_mail' => 'Test Mail',
|
||||
'profile_edit' => 'Edit Profile',
|
||||
'profile_edit_help' => 'Allow users to edit their own profiles.',
|
||||
'profile_edit_help' => 'Allow users to edit their own profiles. (This does not include accessibility settings like light/dark mode, audio feedback, or link colors.)',
|
||||
'default_avatar' => 'Custom Default Avatar',
|
||||
'default_avatar_help' => 'This image will be displayed as a profile if a user does not have a profile photo.',
|
||||
'restore_default_avatar' => 'Restore <a href=":default_avatar" data-toggle="lightbox" data-type="image">original system default avatar</a>',
|
||||
|
||||
@@ -122,7 +122,7 @@ return [
|
||||
'debug_warning_text' => 'هذا التطبيق يعمل في وضع الإنتاج مع تمكين التصحيح. هذا يمكن أن يعرض البيانات الحساسة إذا كان التطبيق الخاص بك هو في متناول العالم الخارجي. تعطيل وضع التصحيح عن طريق تعيين قيمة <code>APP_DEBUG</code> في ملف <code>.env</code> إلى <code>false</code>.',
|
||||
'delete' => 'حذف',
|
||||
'delete_confirm' => 'هل أنت متأكد من حذف :المنتج؟',
|
||||
'delete_confirm_no_undo' => 'هل أنت متأكد من أنك ترغب في حذف :item؟ لا يمكن التراجع بعد الحذف.',
|
||||
'delete_confirm_no_undo' => 'Are you sure you wish to delete :item? This cannot be undone.',
|
||||
'deleted' => 'تم حذفها',
|
||||
'delete_seats' => 'المقاعد المحذوفة',
|
||||
'deletion_failed' => 'فشل الحذف',
|
||||
@@ -167,7 +167,7 @@ return [
|
||||
'image_upload' => 'رفع صورة',
|
||||
'filetypes_accepted_help' => 'Accepted filetype is :types. The maximum size allowed is :size.|Accepted filetypes are :types. The maximum upload size allowed is :size.',
|
||||
'filetypes_size_help' => 'الحد الأقصى المسموح لحجم التصعيد هو :size.',
|
||||
'image_filetypes_help' => 'أنواع الملفات المسموحة هي jpg، webpp، png، gif، svg، tif. الحد الأقصى لحجم التصعيد المسموح به هو :size.',
|
||||
'image_filetypes_help' => 'Accepted filetypes are jpg, webp, png, gif, svg, and avif. The maximum upload size allowed is :size.',
|
||||
'unaccepted_image_type' => 'ملف الصورة هذا غير قابل للقراءة. أنواع الملفات المقبولة هي jpg، webpp، png، gif، svg. نوع هذا الملف هو: :mimetype.',
|
||||
'import' => 'استيراد',
|
||||
'documentation' => 'Open documentation in a new link',
|
||||
@@ -196,6 +196,7 @@ return [
|
||||
'license' => 'الترخيص',
|
||||
'license_report' => 'تقرير الترخيص',
|
||||
'licenses_available' => 'التراخيص المتاحة',
|
||||
'licenses_with_no_seats' => 'Licenses with No Available Seats',
|
||||
'licenses' => 'التراخيص',
|
||||
'list_all' => 'عرض الكل',
|
||||
'loading' => 'جار التحميل. أرجو الإنتظار...',
|
||||
@@ -485,8 +486,22 @@ return [
|
||||
'set_to_null' => 'Delete values for this selection|Delete values for all :selection_count selections ',
|
||||
'set_users_field_to_null' => 'حذف :field القيم لهذا المستخدم <unk> حذف :field القيم لجميع :user_count المستخدمين ',
|
||||
'na_no_purchase_date' => 'N/A - لا يوجد تاريخ شراء',
|
||||
'assets_by_category' => 'Assets by Category',
|
||||
'assets_by_status' => 'الأصول حسب الحالة',
|
||||
'assets_by_status_type' => 'الأصول حسب نوع الحالة',
|
||||
'activity_overview' => 'Activity Overview',
|
||||
'checkouts_checkins' => 'Checkouts & Check-ins',
|
||||
'assets_newly_added' => 'Assets Added',
|
||||
'checkouts' => 'الخارج',
|
||||
'checkins' => 'Check-ins',
|
||||
'assets' => 'الأصول',
|
||||
|
||||
'vs_prior_period' => 'vs. prior period',
|
||||
'time_range' => 'Select Date Range',
|
||||
'last_n_days' => 'Last :days Days',
|
||||
'custom_range' => 'Custom Range',
|
||||
'download_chart' => 'Download Chart as PNG',
|
||||
'fullscreen' => 'Fullscreen',
|
||||
'pie_chart_type' => 'نوع مخطط دائري لوحة التحكم',
|
||||
'hello_name' => 'مرحبا، :name!',
|
||||
'unaccepted_profile_warning' => 'You have one item requiring acceptance. Click here to accept or decline it | You have :count items requiring acceptance. Click here to accept or decline them',
|
||||
@@ -600,10 +615,16 @@ return [
|
||||
'status_compatibility' => 'إذا تم تعيين الأصول بالفعل، فإنه لا يمكن تغييرها إلى نوع حالة غير قابل للنشر وسيتم تخطي هذا التغيير في القيمة.',
|
||||
'rtd_location_help' => 'هذا هو موقع الأصل عندما لا يتم التحقق منه',
|
||||
'item_not_found' => ':item_type ID :id غير موجود أو تم حذفه',
|
||||
'item_target_not_found_hard' => ':item_type ID :id does not exist or has been hard-deleted. Would you like to force a checkin?',
|
||||
'force_checkin' => 'Force Checkin',
|
||||
'item_not_found_short' => ':item_type ID :id does not exist',
|
||||
'action_permission_denied' => 'ليس لديك الإذن لـ :action :item_type ID :id',
|
||||
'action_permission_generic' => 'ليس لديك الإذن لـ :action هذا :item_type',
|
||||
'edit' => 'تعديل',
|
||||
'search_operator' => 'Search operator',
|
||||
'and' => 'and',
|
||||
'action_source' => 'مصدر العمل',
|
||||
'search_tip' => 'Searches return a partial match by default. For more specific results, you can use <code>not:value</code> to fuzzy-exclude, <code>is:value</code> for an exact match, <code>is_not:value</code> for an exact exclusion, <code>is:null</code> for empty values, and <code>is:not_null</code> for non-empty. (In these examples, <code>value</code> is the text you are searching for.)',
|
||||
'or' => 'أو',
|
||||
'url' => 'URL',
|
||||
'phone' => 'رقم الهاتف',
|
||||
|
||||
@@ -9,6 +9,7 @@ return [
|
||||
'am-ET' => 'Amharic',
|
||||
'af-ZA' => 'الأفريكانيون',
|
||||
'ar-SA' => 'العربية',
|
||||
'hy-AM' => 'Armenian',
|
||||
'bg-BG' => 'البلغاري',
|
||||
'zh-CN' => 'الصينية المبسطة',
|
||||
'zh-TW' => 'الصينية التقليدية',
|
||||
|
||||
@@ -48,6 +48,7 @@ return [
|
||||
'asset_tag' => 'وسم الأصل',
|
||||
'assets_warrantee_alert' => 'There is :count asset with an expiring warranty or that are reaching their end of life in the next :threshold days.|There are :count assets with expiring warranties or that are reaching their end of life in the next :threshold days.',
|
||||
'assigned_to' => 'عينت الى',
|
||||
'assigned_to_assets' => 'Assignments to Assets',
|
||||
'eol' => 'نهاية العمر',
|
||||
'best_regards' => 'أفضل التحيات،',
|
||||
'canceled' => 'ملغى',
|
||||
|
||||
@@ -5,7 +5,7 @@ return [
|
||||
'manage' => 'Управление',
|
||||
'field' => 'Поле',
|
||||
'about_fieldsets_title' => 'Относно Fieldsets',
|
||||
'about_fieldsets_text' => '"Група от полета" позволяват създаването на групи от персонализирани полета, които се използват и преизползват често за специфични типове модели на активи.',
|
||||
'about_fieldsets_text' => 'Fieldsets позволяват създаването на групи от персонализирани полета, които се използват и преизползват често за специфични типове модели на активи.',
|
||||
'custom_format' => 'Персонализиран формат...',
|
||||
'encrypt_field' => 'Шифроване на стойността на това поле в базата данни',
|
||||
'encrypt_field_help' => 'ВНИМАНИЕ: Шифроване на поле го прави невалидно за търсене.',
|
||||
|
||||
@@ -99,6 +99,9 @@ return [
|
||||
'success' => 'Активът вписан успешно.',
|
||||
'user_does_not_exist' => 'Невалиден потребител. Моля опитайте отново.',
|
||||
'already_checked_in' => 'Активът е вече вписан.',
|
||||
'force_checkin_orphaned_success' => 'Invalid assignment cleared successfully.',
|
||||
'force_checkin_not_orphaned' => 'Item is not in an invalid assignment state.',
|
||||
'force_checkin_error' => 'Could not clear invalid assignment.',
|
||||
|
||||
],
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ return [
|
||||
'confirm' => 'Сигурни ли сте, че искате да изтриете този лиценз?',
|
||||
'error' => 'Възникна проблем при изтриването на този лиценз. Моля, опитайте отново.',
|
||||
'success' => 'Лицензът е изтрит.',
|
||||
'bulk_success' => 'The selected licenses were deleted successfully.',
|
||||
'partial_success' => 'License deleted successfully. See additional information below. | :count licenses were deleted successfully. See additional information below.',
|
||||
'bulk_checkout_warning' => ':license_name has seats that are currently checked out and cannot be deleted. Please check in all seats before deleting.',
|
||||
],
|
||||
|
||||
'checkout' => [
|
||||
|
||||
@@ -438,7 +438,7 @@ return [
|
||||
'timezone' => 'Часова зона',
|
||||
'test_mail' => 'Тестов е-майл',
|
||||
'profile_edit' => 'Редактиране на профил',
|
||||
'profile_edit_help' => 'Позволете на потребителите сами да могат да редактират собствените си профили.',
|
||||
'profile_edit_help' => 'Allow users to edit their own profiles. (This does not include accessibility settings like light/dark mode, audio feedback, or link colors.)',
|
||||
'default_avatar' => 'Аватар по подразбиране',
|
||||
'default_avatar_help' => 'Тази картинка ще бъде показвана като профилна снимка за потребителите, които нямат профилна снимка.',
|
||||
'restore_default_avatar' => 'Възстанови <a href=":default_avatar" data-toggle="lightbox" data-type="image">оригиналния системен аватар</a>',
|
||||
|
||||
@@ -122,7 +122,7 @@ return [
|
||||
'debug_warning_text' => 'Това приложение се изпълнява в режим на производство с разрешено отстраняване на грешки. Това може да изложи чувствителни данни, ако приложението ви е достъпно за външния свят. Забранете режим отстраняване на грешки чрез задаване на стойността <code>APP_DEBUF</code> <code>.env</code> във файла <code>false</code>.',
|
||||
'delete' => 'Изтриване',
|
||||
'delete_confirm' => 'Сигурни ли сте, че желаете изтриването на :item?',
|
||||
'delete_confirm_no_undo' => 'Сигурни ли сте, че искате да изтриете :item? Това не може да бъде върнато на обратно.',
|
||||
'delete_confirm_no_undo' => 'Сигурни ли сте че искате да изтриете :item? Това не може да бъде върнато на обратно.',
|
||||
'deleted' => 'Изтрито',
|
||||
'delete_seats' => 'Изтрити работни места за лиценз',
|
||||
'deletion_failed' => 'Неуспешно изтриване',
|
||||
@@ -196,6 +196,7 @@ return [
|
||||
'license' => 'Лиценз',
|
||||
'license_report' => 'Справка за лицензите',
|
||||
'licenses_available' => 'Налични лицензи',
|
||||
'licenses_with_no_seats' => 'Licenses with No Available Seats',
|
||||
'licenses' => 'Лицензи',
|
||||
'list_all' => 'Преглед на всички',
|
||||
'loading' => 'Зареждане... моля изчакайте...',
|
||||
@@ -485,8 +486,22 @@ return [
|
||||
'set_to_null' => 'Изтрий стойностите за маркираното|Изтрий стойностите за свички :selection_count маркирания ',
|
||||
'set_users_field_to_null' => 'Изтрий стойноста :field за този потребител|Изтрий стойността :field за всичките :user_count потребителя ',
|
||||
'na_no_purchase_date' => 'N/A - Няма дата на закупуване',
|
||||
'assets_by_category' => 'Assets by Category',
|
||||
'assets_by_status' => 'Статус на Активи',
|
||||
'assets_by_status_type' => 'Статус тип по Активи',
|
||||
'activity_overview' => 'Activity Overview',
|
||||
'checkouts_checkins' => 'Checkouts & Check-ins',
|
||||
'assets_newly_added' => 'Assets Added',
|
||||
'checkouts' => 'Изписвания',
|
||||
'checkins' => 'Check-ins',
|
||||
'assets' => 'Активи',
|
||||
|
||||
'vs_prior_period' => 'vs. prior period',
|
||||
'time_range' => 'Select Date Range',
|
||||
'last_n_days' => 'Last :days Days',
|
||||
'custom_range' => 'Custom Range',
|
||||
'download_chart' => 'Download Chart as PNG',
|
||||
'fullscreen' => 'Fullscreen',
|
||||
'pie_chart_type' => 'Кръгова диаграма на таблото',
|
||||
'hello_name' => 'Здравейте, :name!',
|
||||
'unaccepted_profile_warning' => 'Имате един артикул за приемане. Щракнете тук, за да го приемете или откажете | Имате :count артикула за приемане. Щракнете тук, за да ги приемете или откажете',
|
||||
@@ -600,10 +615,16 @@ return [
|
||||
'status_compatibility' => 'Ако артикула е вече асоцииран, не може да се му се смени статуса на забранен за изписване и тази стойност ще бъде игнорирана.',
|
||||
'rtd_location_help' => 'Това е локацията на артикула, когато не е изписан',
|
||||
'item_not_found' => ':item_type ID :id не съществува или е изтрит',
|
||||
'item_target_not_found_hard' => ':item_type ID :id does not exist or has been hard-deleted. Would you like to force a checkin?',
|
||||
'force_checkin' => 'Force Checkin',
|
||||
'item_not_found_short' => ':item_type ID :id does not exist',
|
||||
'action_permission_denied' => 'Нямате права за :action :item_type ID :id',
|
||||
'action_permission_generic' => 'Нямате права за :action на :item_type',
|
||||
'edit' => 'редакция',
|
||||
'search_operator' => 'Search operator',
|
||||
'and' => 'and',
|
||||
'action_source' => 'Източник на действие',
|
||||
'search_tip' => 'Searches return a partial match by default. For more specific results, you can use <code>not:value</code> to fuzzy-exclude, <code>is:value</code> for an exact match, <code>is_not:value</code> for an exact exclusion, <code>is:null</code> for empty values, and <code>is:not_null</code> for non-empty. (In these examples, <code>value</code> is the text you are searching for.)',
|
||||
'or' => 'или',
|
||||
'url' => 'URL адрес',
|
||||
'phone' => 'Телефон',
|
||||
|
||||
@@ -9,6 +9,7 @@ return [
|
||||
'am-ET' => 'Амхарски',
|
||||
'af-ZA' => 'Африкански',
|
||||
'ar-SA' => 'Арабски',
|
||||
'hy-AM' => 'Armenian',
|
||||
'bg-BG' => 'Български',
|
||||
'zh-CN' => 'Опростен китайски',
|
||||
'zh-TW' => 'Традиционен китайски',
|
||||
|
||||
@@ -48,6 +48,7 @@ return [
|
||||
'asset_tag' => 'Инвентарен номер',
|
||||
'assets_warrantee_alert' => 'Има :count актив с изтекла гаранция или достигнали края на живота си през следващите :threshold дни.| Има :count активи с изтекла гаранция или достигнали края на живота си през следващите :threshold дни.',
|
||||
'assigned_to' => 'Възложени на',
|
||||
'assigned_to_assets' => 'Assignments to Assets',
|
||||
'eol' => 'EOL',
|
||||
'best_regards' => 'С най-добри пожелания.',
|
||||
'canceled' => 'Отменено',
|
||||
|
||||
@@ -5,7 +5,7 @@ return [
|
||||
'manage' => 'Manage',
|
||||
'field' => 'Field',
|
||||
'about_fieldsets_title' => 'About Fieldsets',
|
||||
'about_fieldsets_text' => 'Fieldsets allow you to create groups of custom fields that are frequently re-used for specific asset model types.',
|
||||
'about_fieldsets_text' => 'Fieldsets allow you to create groups of custom fields that are frequently re-used used for specific asset model types.',
|
||||
'custom_format' => 'Custom Regex format...',
|
||||
'encrypt_field' => 'Encrypt the value of this field in the database',
|
||||
'encrypt_field_help' => 'WARNING: Encrypting a field makes it unsearchable.',
|
||||
@@ -26,12 +26,12 @@ return [
|
||||
'req' => 'Req.',
|
||||
'used_by_models' => 'Used By Models',
|
||||
'order' => 'Order',
|
||||
'create_fieldset' => 'New Fieldset',
|
||||
'create_fieldset' => 'Nou conjunt de camps',
|
||||
'update_fieldset' => 'Update Fieldset',
|
||||
'fieldset_does_not_exist' => 'Fieldset :id does not exist',
|
||||
'fieldset_updated' => 'Fieldset updated',
|
||||
'create_fieldset_title' => 'Create a new fieldset',
|
||||
'create_field' => 'New Custom Field',
|
||||
'create_field' => 'Nou camp personalitzat',
|
||||
'create_field_title' => 'Create a new custom field',
|
||||
'value_encrypted' => 'The value of this field is encrypted in the database. Only users with permission to view encrypted custom fields will be able to view the decrypted value',
|
||||
'show_in_email' => 'Include the value of this field in checkout emails sent to the user? Encrypted fields cannot be included in emails',
|
||||
|
||||
@@ -47,7 +47,7 @@ return [
|
||||
'serial_required' => 'Asset :number requires a serial number',
|
||||
'serial_required_post_model_update' => ':asset_model have been updated to require a serial number. Please add a serial number for this asset.',
|
||||
'status' => 'Estat',
|
||||
'tag' => 'Etiqueta de Recurs',
|
||||
'tag' => 'Etiqueta del recurs',
|
||||
'update' => 'Asset Update',
|
||||
'warranty' => 'Warranty',
|
||||
'warranty_expires' => 'Warranty Expires',
|
||||
|
||||
@@ -18,14 +18,14 @@ return [
|
||||
'model_deleted' => 'This Assets model has been deleted. You must restore the model before you can restore the Asset.',
|
||||
'model_invalid' => 'This model for this asset is invalid.',
|
||||
'model_invalid_fix' => 'The asset must be updated use a valid asset model before attempting to check it in or out, or to audit it.',
|
||||
'requestable' => 'Requestable',
|
||||
'requestable' => 'Sol·licitable',
|
||||
'requested' => 'Requested',
|
||||
'not_requestable' => 'Not Requestable',
|
||||
'requestable_status_warning' => 'Do not change requestable status',
|
||||
'require_serial' => 'Require Serial Number',
|
||||
'require_serial_help' => 'A serial number will be required when creating a new asset of this model.',
|
||||
'restore' => 'Restore Asset',
|
||||
'pending' => 'Pending',
|
||||
'pending' => 'Pendent',
|
||||
'undeployable' => 'Undeployable',
|
||||
'undeployable_tooltip' => 'This asset has a status label that is undeployable and cannot be checked out at this time.',
|
||||
'view' => 'View Asset',
|
||||
|
||||
@@ -99,6 +99,9 @@ return [
|
||||
'success' => 'Asset checked in successfully.',
|
||||
'user_does_not_exist' => 'That user is invalid. Please try again.',
|
||||
'already_checked_in' => 'That asset is already checked in.',
|
||||
'force_checkin_orphaned_success' => 'Invalid assignment cleared successfully.',
|
||||
'force_checkin_not_orphaned' => 'Item is not in an invalid assignment state.',
|
||||
'force_checkin_error' => 'Could not clear invalid assignment.',
|
||||
|
||||
],
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
return [
|
||||
|
||||
'asset_tag' => 'Etiqueta de Recurs',
|
||||
'asset_tag' => 'Etiqueta de recurs',
|
||||
'asset_model' => 'Model',
|
||||
'assigned_to' => 'Assigned To',
|
||||
'book_value' => 'Current Value',
|
||||
|
||||
@@ -37,6 +37,9 @@ return [
|
||||
'confirm' => 'Are you sure you wish to delete this license?',
|
||||
'error' => 'There was an issue deleting the license. Please try again.',
|
||||
'success' => 'The license was deleted successfully.',
|
||||
'bulk_success' => 'The selected licenses were deleted successfully.',
|
||||
'partial_success' => 'License deleted successfully. See additional information below. | :count licenses were deleted successfully. See additional information below.',
|
||||
'bulk_checkout_warning' => ':license_name has seats that are currently checked out and cannot be deleted. Please check in all seats before deleting.',
|
||||
],
|
||||
|
||||
'checkout' => [
|
||||
|
||||
@@ -226,7 +226,7 @@ return [
|
||||
'saml_custom_settings_help' => 'You can specify additional settings to the onelogin/php-saml library. Use at your own risk.',
|
||||
'saml_download' => 'Download Metadata',
|
||||
'setting' => 'Setting',
|
||||
'settings' => 'Settings',
|
||||
'settings' => 'Configuració',
|
||||
'show_alerts_in_menu' => 'Show alerts in top menu',
|
||||
'show_archived_in_list' => 'Archived Assets',
|
||||
'show_archived_in_list_text' => 'Show archived assets in the "all assets" listing',
|
||||
@@ -438,7 +438,7 @@ return [
|
||||
'timezone' => 'Timezone',
|
||||
'test_mail' => 'Test Mail',
|
||||
'profile_edit' => 'Edit Profile',
|
||||
'profile_edit_help' => 'Allow users to edit their own profiles.',
|
||||
'profile_edit_help' => 'Allow users to edit their own profiles. (This does not include accessibility settings like light/dark mode, audio feedback, or link colors.)',
|
||||
'default_avatar' => 'Custom Default Avatar',
|
||||
'default_avatar_help' => 'This image will be displayed as a profile if a user does not have a profile photo.',
|
||||
'restore_default_avatar' => 'Restore <a href=":default_avatar" data-toggle="lightbox" data-type="image">original system default avatar</a>',
|
||||
|
||||
@@ -10,7 +10,7 @@ return [
|
||||
'deployable' => 'Deployable',
|
||||
'info' => 'Status label types are used to describe the various states your assets could be in. They may be out for repair, lost/stolen, etc. You can create new status labels for your deployable, pending and archived assets according to your own workflow. For more information, <a href="https://snipe-it.readme.io/docs/overview#status-labels" target="_blank">see the documentation <i class="fa fa-external-link"></i></a>.',
|
||||
'name' => 'Status Name',
|
||||
'pending' => 'Pending',
|
||||
'pending' => 'Pendent',
|
||||
'status_type' => 'Status Type',
|
||||
'show_in_nav' => 'Show in side nav',
|
||||
'title' => 'Status Labels',
|
||||
|
||||
@@ -29,7 +29,7 @@ return [
|
||||
'asset' => 'Recurs',
|
||||
'asset_previous' => 'Asset (Previously Assigned)',
|
||||
'asset_report' => 'Informe de Recursos',
|
||||
'asset_tag' => 'Etiqueta de Recurs',
|
||||
'asset_tag' => 'Etiqueta de recurs',
|
||||
'asset_tags' => 'Asset Tags',
|
||||
'available' => 'Available',
|
||||
'assets_available' => 'Assets available',
|
||||
@@ -61,7 +61,7 @@ return [
|
||||
'bulk_delete' => 'Bulk Delete',
|
||||
'bulk_actions' => 'Bulk Actions',
|
||||
'bulk_checkin_delete' => 'Bulk Checkin / Delete Users',
|
||||
'byod' => 'BYOD',
|
||||
'byod' => 'Dispositiu de l\'usuari',
|
||||
'byod_help' => 'This device is owned by the user',
|
||||
'bystatus' => 'by Status',
|
||||
'cancel' => 'Cancel·la',
|
||||
@@ -94,7 +94,7 @@ return [
|
||||
'country' => 'País',
|
||||
'could_not_restore' => 'Error restoring :item_type: :error',
|
||||
'not_deleted' => 'The :item_type was not deleted and therefore cannot be restored',
|
||||
'create' => 'Crea nou',
|
||||
'create' => 'Crea',
|
||||
'created' => 'Item Created',
|
||||
'created_asset' => 'recurs creat',
|
||||
'created_at' => 'Created At',
|
||||
@@ -122,13 +122,13 @@ return [
|
||||
'debug_warning_text' => 'This application is running in production mode with debugging enabled. This can expose sensitive data if your application is accessible to the outside world. Disable debug mode by setting the <code>APP_DEBUG</code> value in your <code>.env</code> file to <code>false</code>.',
|
||||
'delete' => 'Suprimeix',
|
||||
'delete_confirm' => 'Are you sure you wish to delete :item?',
|
||||
'delete_confirm_no_undo' => 'Are you sure, you wish to delete :item? This cannot be undone.',
|
||||
'delete_confirm_no_undo' => 'Are you sure you wish to delete :item? This cannot be undone.',
|
||||
'deleted' => 'S\'ha suprimit',
|
||||
'delete_seats' => 'Deleted Seats',
|
||||
'deletion_failed' => 'Deletion failed',
|
||||
'departments' => 'Departaments',
|
||||
'department' => 'Departament',
|
||||
'deployed' => 'Deployed',
|
||||
'deployed' => 'Assignat',
|
||||
'depreciation' => 'Depreciation',
|
||||
'depreciation_type' => 'Depreciation Type',
|
||||
'depreciations' => 'Depreciations',
|
||||
@@ -167,9 +167,9 @@ return [
|
||||
'image_upload' => 'Puja una imatge',
|
||||
'filetypes_accepted_help' => 'Accepted filetype is :types. The maximum size allowed is :size.|Accepted filetypes are :types. The maximum upload size allowed is :size.',
|
||||
'filetypes_size_help' => 'The maximum upload size allowed is :size.',
|
||||
'image_filetypes_help' => 'Accepted Filetypes are jpg, webp, png, gif, svg, and avif. The maximum upload size allowed is :size.',
|
||||
'image_filetypes_help' => 'Accepted filetypes are jpg, webp, png, gif, svg, and avif. The maximum upload size allowed is :size.',
|
||||
'unaccepted_image_type' => 'This image file was not readable. Accepted filetypes are jpg, webp, png, gif, and svg. The mimetype of this file is: :mimetype.',
|
||||
'import' => 'Import',
|
||||
'import' => 'Importació',
|
||||
'documentation' => 'Open documentation in a new link',
|
||||
'import_this_file' => 'Map fields and process this file',
|
||||
'importing' => 'Importing',
|
||||
@@ -183,7 +183,7 @@ return [
|
||||
'import_file' => 'import CSV file',
|
||||
'import_type' => 'CSV import type',
|
||||
'insufficient_permissions' => 'Insufficient permissions!',
|
||||
'kits' => 'Predefined Kits',
|
||||
'kits' => 'Lots predefinits',
|
||||
'language' => 'Language',
|
||||
'last' => 'Last',
|
||||
'last_login' => 'Last Login',
|
||||
@@ -196,7 +196,8 @@ return [
|
||||
'license' => 'License',
|
||||
'license_report' => 'License Report',
|
||||
'licenses_available' => 'Licenses available',
|
||||
'licenses' => 'Licenses',
|
||||
'licenses_with_no_seats' => 'Licenses with No Available Seats',
|
||||
'licenses' => 'Llicències',
|
||||
'list_all' => 'Mostra\'ls tots',
|
||||
'loading' => 'Loading... please wait...',
|
||||
'lock_passwords' => 'This field value will not be saved in a demo installation.',
|
||||
@@ -206,7 +207,7 @@ return [
|
||||
'locations' => 'Locations',
|
||||
'logo_size' => 'Square logos look best with Logo + Text. Logo maximum display size is 50px high x 500px wide. ',
|
||||
'logout' => 'Logout',
|
||||
'lookup_by_tag' => 'Cercar per Etiqueta de Recurs',
|
||||
'lookup_by_tag' => 'Cerca per etiqueta de recurs',
|
||||
'maintenances' => 'Maintenances',
|
||||
'manage_api_keys' => 'Manage API keys',
|
||||
'manufacturer' => 'Manufacturer',
|
||||
@@ -246,7 +247,7 @@ return [
|
||||
'page_menu' => 'Showing _MENU_ items',
|
||||
'page_error' => 'Could not determine previous page. Redirected to homepage.',
|
||||
'pagination_info' => 'Showing _START_ to _END_ of _TOTAL_ items',
|
||||
'pending' => 'Pending',
|
||||
'pending' => 'Pendent',
|
||||
'people' => 'Persones',
|
||||
'per_page' => 'Results Per Page',
|
||||
'previous' => 'Previous',
|
||||
@@ -264,17 +265,17 @@ return [
|
||||
'recent_activity' => 'Activitat recent',
|
||||
'remaining' => 'Remaining',
|
||||
'remove_company' => 'Remove Company Association',
|
||||
'reports' => 'Reports',
|
||||
'reports' => 'Informes',
|
||||
'restored' => 'restored',
|
||||
'restore' => 'Restore',
|
||||
'requestable_models' => 'Requestable Models',
|
||||
'requestable_items' => 'Requestable Items',
|
||||
'requestable_items' => 'Elements sol·licitables',
|
||||
'requestable' => 'Sol·licitable',
|
||||
'requested' => 'Requested',
|
||||
'rtd' => 'Disponible',
|
||||
'requested_date' => 'Requested Date',
|
||||
'requested_assets' => 'Requested Assets',
|
||||
'requested_assets_menu' => 'Requestable Items',
|
||||
'requested_assets_menu' => 'Elements sol·licitables',
|
||||
'request_canceled' => 'Request Canceled',
|
||||
'request_item' => 'Request this item',
|
||||
'external_link_tooltip' => 'External link to',
|
||||
@@ -297,7 +298,7 @@ return [
|
||||
'select_statuslabel' => 'Select Status',
|
||||
'select_company' => 'Selecciona l\'empresa',
|
||||
'select_asset' => 'Seleccionar Recurs',
|
||||
'settings' => 'Settings',
|
||||
'settings' => 'Configuració',
|
||||
'show_deleted' => 'Show Deleted',
|
||||
'show_current' => 'Show Current',
|
||||
'sign_in' => 'Sign in',
|
||||
@@ -330,7 +331,7 @@ return [
|
||||
'total_consumables' => 'total consumables',
|
||||
'total_cost' => 'Total Cost',
|
||||
'type' => 'Type',
|
||||
'undeployable' => 'Un-deployable',
|
||||
'undeployable' => 'No assignable',
|
||||
'unknown_admin' => 'Unknown Admin',
|
||||
'unknown_user' => 'Unknown User',
|
||||
'unit_cost' => 'Unit Cost',
|
||||
@@ -367,10 +368,10 @@ return [
|
||||
'token_expired' => 'Your form session has expired. Please try again.',
|
||||
'login_enabled' => 'Login Enabled',
|
||||
'login_disabled' => 'Login Disabled',
|
||||
'audit_due' => 'Due for Audit',
|
||||
'audit_due' => 'Pendent d\'auditar',
|
||||
'audit_due_days' => '{}Assets Due or Overdue for Audit|[1]Assets Due or Overdue for Audit Within a Day|[2,*]Assets Due or Overdue for Audit Within :days Days',
|
||||
'audit_due_days_view_all' => '{}Assets Due or Overdue for Audit|[1]View All Assets Due or Overdue for Audit Within a Day|[2,*]View All Assets Due or Overdue for Audit Within :days Days',
|
||||
'checkin_due' => 'Due for Checkin',
|
||||
'checkin_due' => 'Pendent de devolució',
|
||||
'checkin_overdue' => 'Overdue for Checkin',
|
||||
'checkin_due_days' => '{}Due for Checkin|[1]Assets Due for Checkin Within :days Day|[2,*]Assets Due for Checkin Within :days Days',
|
||||
'audit_overdue' => 'Overdue for Audit',
|
||||
@@ -437,15 +438,15 @@ return [
|
||||
'license_serial' => 'Serial/Product Key',
|
||||
'invalid_category' => 'Invalid or missing category',
|
||||
'invalid_item_category_single' => 'Invalid or missing :type category. Please update the category of this :type to include a valid category before checking out.',
|
||||
'dashboard_info' => 'This is your dashboard. There are many like it, but this one is yours.',
|
||||
'dashboard_info' => 'Aquest és el teu tauler de control. N\'hi ha molts com aquest, però aquest és teu.',
|
||||
'60_percent_warning' => '60% Complete (warning)',
|
||||
'dashboard_empty' => 'It looks like you have not added anything yet, so we do not have anything awesome to display. Get started by adding some assets, accessories, consumables, or licenses now!',
|
||||
'new_asset' => 'Recurs nou',
|
||||
'new_license' => 'Llicència nova',
|
||||
'new_accessory' => 'Accessori nou',
|
||||
'new_consumable' => 'Consumible nou',
|
||||
'new_component' => 'Component nou',
|
||||
'new_user' => 'New User',
|
||||
'dashboard_empty' => 'Sembla que encara no has afegit res, per tant, no tenim res de fantàstic per mostrar. Comença afegint alguns recursos, accessoris, consumibles o llicències ara!',
|
||||
'new_asset' => 'Nou recurs',
|
||||
'new_license' => 'Nova llicència',
|
||||
'new_accessory' => 'Nou accessori',
|
||||
'new_consumable' => 'Nou consumible',
|
||||
'new_component' => 'Nou component',
|
||||
'new_user' => 'Nou usuari',
|
||||
'collapse' => 'Collapse',
|
||||
'assigned' => 'Assigned',
|
||||
'asset_count' => 'Asset Count',
|
||||
@@ -485,8 +486,22 @@ return [
|
||||
'set_to_null' => 'Delete values for this selection|Delete values for all :selection_count selections ',
|
||||
'set_users_field_to_null' => 'Delete :field values for this user|Delete :field values for all :user_count users ',
|
||||
'na_no_purchase_date' => 'N/A - No purchase date provided',
|
||||
'assets_by_category' => 'Assets by Category',
|
||||
'assets_by_status' => 'Assets by Status',
|
||||
'assets_by_status_type' => 'Assets by Status Type',
|
||||
'activity_overview' => 'Activity Overview',
|
||||
'checkouts_checkins' => 'Checkouts & Check-ins',
|
||||
'assets_newly_added' => 'Assets Added',
|
||||
'checkouts' => 'Checkouts',
|
||||
'checkins' => 'Check-ins',
|
||||
'assets' => 'Recursos',
|
||||
|
||||
'vs_prior_period' => 'vs. prior period',
|
||||
'time_range' => 'Select Date Range',
|
||||
'last_n_days' => 'Last :days Days',
|
||||
'custom_range' => 'Custom Range',
|
||||
'download_chart' => 'Download Chart as PNG',
|
||||
'fullscreen' => 'Fullscreen',
|
||||
'pie_chart_type' => 'Dashboard Pie Chart Type',
|
||||
'hello_name' => 'Hello, :name!',
|
||||
'unaccepted_profile_warning' => 'You have one item requiring acceptance. Click here to accept or decline it | You have :count items requiring acceptance. Click here to accept or decline them',
|
||||
@@ -600,10 +615,16 @@ return [
|
||||
'status_compatibility' => 'If assets are already assigned, they cannot be changed to a non-deployable status type and this value change will be skipped.',
|
||||
'rtd_location_help' => 'This is the location of the asset when it is not checked out',
|
||||
'item_not_found' => ':item_type ID :id does not exist or has been deleted',
|
||||
'item_target_not_found_hard' => ':item_type ID :id does not exist or has been hard-deleted. Would you like to force a checkin?',
|
||||
'force_checkin' => 'Force Checkin',
|
||||
'item_not_found_short' => ':item_type ID :id does not exist',
|
||||
'action_permission_denied' => 'You do not have permission to :action :item_type ID :id',
|
||||
'action_permission_generic' => 'You do not have permission to :action this :item_type',
|
||||
'edit' => 'edit',
|
||||
'search_operator' => 'Search operator',
|
||||
'and' => 'and',
|
||||
'action_source' => 'Action Source',
|
||||
'search_tip' => 'Searches return a partial match by default. For more specific results, you can use <code>not:value</code> to fuzzy-exclude, <code>is:value</code> for an exact match, <code>is_not:value</code> for an exact exclusion, <code>is:null</code> for empty values, and <code>is:not_null</code> for non-empty. (In these examples, <code>value</code> is the text you are searching for.)',
|
||||
'or' => 'or',
|
||||
'url' => 'URL',
|
||||
'phone' => 'Phone',
|
||||
|
||||
@@ -9,6 +9,7 @@ return [
|
||||
'am-ET' => 'Amharic',
|
||||
'af-ZA' => 'Afrikaans',
|
||||
'ar-SA' => 'Arabic',
|
||||
'hy-AM' => 'Armenian',
|
||||
'bg-BG' => 'Bulgarian',
|
||||
'zh-CN' => 'Chinese Simplified',
|
||||
'zh-TW' => 'Chinese Traditional',
|
||||
|
||||
@@ -45,9 +45,10 @@ return [
|
||||
'asset' => 'Recurs',
|
||||
'asset_name' => 'Nom del recurs',
|
||||
'asset_requested' => 'Asset requested',
|
||||
'asset_tag' => 'Etiqueta de Recurs',
|
||||
'asset_tag' => 'Etiqueta de recurs',
|
||||
'assets_warrantee_alert' => 'There is :count asset with an expiring warranty or that are reaching their end of life in the next :threshold days.|There are :count assets with expiring warranties or that are reaching their end of life in the next :threshold days.',
|
||||
'assigned_to' => 'Assigned To',
|
||||
'assigned_to_assets' => 'Assignments to Assets',
|
||||
'eol' => 'EOL',
|
||||
'best_regards' => 'Best regards,',
|
||||
'canceled' => 'Canceled',
|
||||
|
||||
@@ -172,7 +172,7 @@ return [
|
||||
],
|
||||
|
||||
'licenses' => [
|
||||
'name' => 'Licenses',
|
||||
'name' => 'Llicències',
|
||||
'note' => 'Grants access to the Licenses section of the application.',
|
||||
],
|
||||
'licensesview' => [
|
||||
@@ -234,7 +234,7 @@ return [
|
||||
'note' => 'Check components back into inventory that are currently checked out.',
|
||||
],
|
||||
'kits' => [
|
||||
'name' => 'Predefined Kits',
|
||||
'name' => 'Lots predefinits',
|
||||
'note' => 'Grants access to the Predefined Kits section of the application.',
|
||||
],
|
||||
'kitsview' => [
|
||||
|
||||
@@ -5,7 +5,7 @@ return [
|
||||
'manage' => 'Manage',
|
||||
'field' => 'Field',
|
||||
'about_fieldsets_title' => 'About Fieldsets',
|
||||
'about_fieldsets_text' => 'Fieldsets allow you to create groups of custom fields that are frequently re-used for specific asset model types.',
|
||||
'about_fieldsets_text' => 'Fieldsets allow you to create groups of custom fields that are frequently re-used used for specific asset model types.',
|
||||
'custom_format' => 'Custom Regex format...',
|
||||
'encrypt_field' => 'Encrypt the value of this field in the database',
|
||||
'encrypt_field_help' => 'WARNING: Encrypting a field makes it unsearchable.',
|
||||
|
||||
@@ -99,6 +99,9 @@ return [
|
||||
'success' => 'Asset checked in successfully.',
|
||||
'user_does_not_exist' => 'That user is invalid. Please try again.',
|
||||
'already_checked_in' => 'That asset is already checked in.',
|
||||
'force_checkin_orphaned_success' => 'Invalid assignment cleared successfully.',
|
||||
'force_checkin_not_orphaned' => 'Item is not in an invalid assignment state.',
|
||||
'force_checkin_error' => 'Could not clear invalid assignment.',
|
||||
|
||||
],
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user