Compare commits
156 Commits
| 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 |
@@ -1,7 +1,6 @@
|
||||
# GitHub Copilot Custom Instructions for Snipe-IT
|
||||
|
||||
These instructions guide Copilot to generate code that aligns with modern Laravel 12 standards, PHP 8.2/8.4 features,
|
||||
software engineering principles, and industry best practices to improve software quality, maintainability, and security.
|
||||
These instructions guide Copilot to generate code that aligns with modern Laravel 11 standards, PHP 8.2/8.4 features, software engineering principles, and industry best practices to improve software quality, maintainability, and security.
|
||||
|
||||
## ✅ General Coding Standards
|
||||
|
||||
@@ -23,7 +22,7 @@ software engineering principles, and industry best practices to improve software
|
||||
- Adopt **final classes** where extension is not intended.
|
||||
- Use **Named Arguments** for improved clarity when calling functions with multiple parameters.
|
||||
|
||||
## ✅ Laravel 12 Project Structure & Conventions
|
||||
## ✅ Laravel 11 Project Structure & Conventions
|
||||
|
||||
- Follow the official Laravel project structure:
|
||||
- `app/Http/Controllers` - Controllers
|
||||
@@ -33,7 +32,6 @@ software engineering principles, and industry best practices to improve software
|
||||
- `app/Enums` - Enums
|
||||
- `app/Actions` - Single-responsibility action classes
|
||||
- `app/Policies` - Authorization logic
|
||||
- `app/Models/Builders` - Query scoping logic
|
||||
|
||||
- Controllers must:
|
||||
- Use dependency injection.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('audit_location')]
|
||||
#[Title('Audit Location')]
|
||||
#[Description('Review all assets at a location, flag overdue audits and status anomalies')]
|
||||
class AuditLocationPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$location = $request->get('location');
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are conducting an asset audit for location: {$location}.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. Find the location record for "{$location}" (search by name if needed).
|
||||
2. List all assets currently assigned to or located at that location.
|
||||
3. Identify any assets with overdue audit dates (next_audit_date is in the past).
|
||||
4. Flag any assets with unexpected status labels (e.g. archived, pending, or out-for-repair assets that appear to still be at this location).
|
||||
5. Note any assets that have been at this location longer than expected without a check-in or audit event.
|
||||
6. Produce a summary report with: total asset count, assets requiring audit, assets with status anomalies, and any recommended actions.
|
||||
|
||||
Present the findings clearly so they can be acted on or exported.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('location', 'Name or ID of the location to audit', required: true),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('end_of_life_review')]
|
||||
#[Title('End of Life Review')]
|
||||
#[Description('Identify assets that have passed their EOL date or are fully depreciated, and recommend disposition actions')]
|
||||
class EndOfLifeReviewPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$department = $request->get('department');
|
||||
$category = $request->get('category');
|
||||
|
||||
$scope = collect([
|
||||
$department ? "department: {$department}" : null,
|
||||
$category ? "category: {$category}" : null,
|
||||
])->filter()->implode(' and ');
|
||||
|
||||
$scopeLine = $scope
|
||||
? "Limit the review to assets in {$scope}."
|
||||
: 'Review assets across the entire organisation.';
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are conducting an end-of-life and depreciation review. {$scopeLine}
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. List assets that have passed their asset_eol_date (end-of-life date is in the past).
|
||||
2. List assets that are fully depreciated based on their depreciation schedule and purchase date.
|
||||
3. For each identified asset, show: asset tag, name, model, assigned user or location, EOL date, purchase date, and current status.
|
||||
4. Group findings by category for easier review.
|
||||
5. Recommend disposition for each group: retire and replace, redeploy to a lower-demand role, send for repair, or archive.
|
||||
6. Provide a cost summary if purchase cost data is available — total value of end-of-life assets.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('department', 'Limit review to a specific department', required: false),
|
||||
new Argument('category', 'Limit review to a specific asset category', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('expiring_licenses')]
|
||||
#[Title('Expiring Licenses')]
|
||||
#[Description('Review license seat usage and flag licenses expiring within a given number of days')]
|
||||
class ExpiringLicensesPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$days = (int) ($request->get('days', 30));
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are reviewing software license health across the organisation. Focus on licenses expiring within {$days} days.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. List all licenses in the system.
|
||||
2. Identify licenses whose expiration date falls within the next {$days} days.
|
||||
3. For each expiring license, show: license name, total seats, seats in use, seats free, and the expiration date.
|
||||
4. Flag any licenses that are over-deployed (more seats checked out than purchased).
|
||||
5. Flag any licenses that are under-used (many free seats that may indicate unused subscriptions worth cancelling).
|
||||
6. Produce a prioritised action list: renewals needed urgently, over-deployments to resolve, and possible cancellations.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('days', 'Number of days ahead to check for expiring licenses (default: 30)', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('find_available_asset')]
|
||||
#[Title('Find Available Asset')]
|
||||
#[Description('Find an undeployed asset by category or model and optionally check it out to a user')]
|
||||
class FindAvailableAssetPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$category = $request->get('category');
|
||||
$model = $request->get('model');
|
||||
$assignTo = $request->get('assign_to');
|
||||
|
||||
$assetDescription = collect([
|
||||
$category ? "category: {$category}" : null,
|
||||
$model ? "model: {$model}" : null,
|
||||
])->filter()->implode(' / ');
|
||||
|
||||
$assignLine = $assignTo
|
||||
? "If a suitable asset is found, check it out to the user: {$assignTo}."
|
||||
: 'Ask whether the found asset should be checked out to a specific user before proceeding.';
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You need to find an available (undeployed) asset matching {$assetDescription}.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. Search for assets with a Ready-to-Deploy status that match the requested {$assetDescription}.
|
||||
2. If multiple options are available, list them with their asset tags, serial numbers, and any relevant details so the best one can be selected.
|
||||
3. {$assignLine}
|
||||
4. Confirm the final asset tag, serial number, and checkout status once complete.
|
||||
|
||||
If no available assets match, report what was found and suggest alternatives (different models in the same category, or assets currently out for repair that may return soon).
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('category', 'Asset category to search (e.g. Laptop, Monitor)', required: false),
|
||||
new Argument('model', 'Specific model name to search for', required: false),
|
||||
new Argument('assign_to', 'Username to check the asset out to once found', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('inventory_summary')]
|
||||
#[Title('Inventory Summary')]
|
||||
#[Description('Produce a high-level inventory count by category, broken down by deployment status')]
|
||||
class InventorySummaryPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$location = $request->get('location');
|
||||
$department = $request->get('department');
|
||||
|
||||
$scope = collect([
|
||||
$location ? "location: {$location}" : null,
|
||||
$department ? "department: {$department}" : null,
|
||||
])->filter()->implode(' and ');
|
||||
|
||||
$scopeLine = $scope
|
||||
? "Scope the report to {$scope}."
|
||||
: 'Report across the entire organisation.';
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are generating an inventory summary report. {$scopeLine}
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. List assets (filtered by the scope above if provided) and tally counts by status: Deployed, Ready to Deploy, Archived, Pending, Out for Repair.
|
||||
2. Break the deployed count down by asset category (laptops, monitors, phones, etc.).
|
||||
3. List the top 5 models by total quantity.
|
||||
4. Show total purchase value of the inventory if cost data is available.
|
||||
5. Highlight any categories with zero available (Ready to Deploy) assets — potential stock-out risk.
|
||||
6. Present the results as a concise executive summary with a supporting breakdown table.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('location', 'Limit report to a specific location', required: false),
|
||||
new Argument('department', 'Limit report to a specific department', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('offboard_employee')]
|
||||
#[Title('Offboard Employee')]
|
||||
#[Description('Guide through checking in all equipment and licenses from a departing employee and deactivating their account')]
|
||||
class OffboardEmployeePrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$username = $request->get('username');
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are helping offboard a departing employee with username: {$username}.
|
||||
|
||||
Please complete the following offboarding steps using the available tools:
|
||||
|
||||
1. Look up the user account for {$username} and display a summary of everything currently assigned to them (assets, licenses, accessories, consumables).
|
||||
2. Check in all assigned assets from this user.
|
||||
3. Check in all assigned accessories from this user.
|
||||
4. Revoke or check in any license seats assigned to this user.
|
||||
5. Deactivate the user account.
|
||||
6. Provide a final summary of all items that were checked in and confirm the account has been deactivated.
|
||||
|
||||
If any items cannot be checked in automatically, flag them for manual follow-up.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('username', 'Username of the departing employee', required: true),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('onboard_employee')]
|
||||
#[Title('Onboard Employee')]
|
||||
#[Description('Guide through creating a new employee account and assigning appropriate equipment and licenses')]
|
||||
class OnboardEmployeePrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$firstName = $request->get('first_name');
|
||||
$lastName = $request->get('last_name');
|
||||
$department = $request->get('department');
|
||||
$location = $request->get('location');
|
||||
$title = $request->get('title');
|
||||
|
||||
$fullName = trim("{$firstName} {$lastName}");
|
||||
|
||||
$context = collect([
|
||||
$department ? "Department: {$department}" : null,
|
||||
$location ? "Location: {$location}" : null,
|
||||
$title ? "Job title: {$title}" : null,
|
||||
])->filter()->implode("\n");
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are helping onboard a new employee.
|
||||
|
||||
Employee details:
|
||||
- First name: {$firstName}
|
||||
- Last name: {$lastName}
|
||||
{$context}
|
||||
|
||||
Please complete the following onboarding steps using the available tools:
|
||||
|
||||
1. Create a new user account using first_name "{$firstName}" and last_name "{$lastName}" along with the details provided above. Ask for any missing required fields (username and, optionally, email address) before proceeding. Do not ask for a password — one will be set automatically.
|
||||
2. If the new account has an email address, ask whether you should send them a password reset link so they can set their own password. Use send_password_reset if the answer is yes.
|
||||
3. Search for available (undeployed) assets suitable for their role — typically a laptop and any other standard equipment for their department or location.
|
||||
4. Check out the selected assets to the new user.
|
||||
5. Check whether any software license seats are available that should be assigned (e.g. productivity suites, VPN, etc.) and assign them.
|
||||
6. Summarise what was set up: the user account created, whether a password reset email was sent, assets checked out, and licenses assigned.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('first_name', 'First name of the new employee', required: true),
|
||||
new Argument('last_name', 'Last name of the new employee', required: false),
|
||||
new Argument('department', 'Department the employee will join', required: false),
|
||||
new Argument('location', 'Primary office location', required: false),
|
||||
new Argument('title', 'Job title', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Server\Prompt;
|
||||
|
||||
abstract class SnipePrompt extends Prompt
|
||||
{
|
||||
/**
|
||||
* Returns a trailing instruction telling the model which language to respond in,
|
||||
* derived from the authenticated user's locale setting. Returns an empty string
|
||||
* for English locales so the prompt text is unchanged for the majority of users.
|
||||
*/
|
||||
protected function localeInstruction(): string
|
||||
{
|
||||
$locale = auth()->user()?->locale ?? app()->getLocale();
|
||||
|
||||
if (str_starts_with($locale, 'en')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return "\n\nPlease respond in the language that corresponds to locale: {$locale}.";
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('user_inventory')]
|
||||
#[Title('User Inventory')]
|
||||
#[Description('List everything currently assigned to a specific user across all asset types')]
|
||||
class UserInventoryPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$username = $request->get('username');
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are pulling a complete inventory of everything assigned to the user: {$username}.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. Look up the user account for {$username} and display their basic info (name, department, location, job title).
|
||||
2. List all assets currently checked out to this user (asset tag, name, model, serial, status).
|
||||
3. List all accessories checked out to this user.
|
||||
4. List all license seats assigned to this user.
|
||||
5. List any consumables that have been checked out to this user.
|
||||
6. Calculate the total purchase value of all assigned assets if cost data is available.
|
||||
7. Present a clean summary grouped by item type, suitable for sharing with a manager or for an audit.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('username', 'Username of the user to review', required: true),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Prompts;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Name('warranty_expiring')]
|
||||
#[Title('Warranty Expiring')]
|
||||
#[Description('List assets whose warranty expires within a given number of days')]
|
||||
class WarrantyExpiringPrompt extends SnipePrompt
|
||||
{
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$days = (int) ($request->get('days', 90));
|
||||
|
||||
$prompt = <<<TEXT
|
||||
You are reviewing assets whose warranty is expiring soon. Focus on assets expiring within {$days} days.
|
||||
|
||||
Please complete the following steps using the available tools:
|
||||
|
||||
1. List assets and filter for those whose warranty expiration date (calculated from purchase_date + warranty_months) falls within the next {$days} days.
|
||||
2. For each asset, show: asset tag, name, model, assigned user or location, purchase date, warranty months, and calculated warranty end date.
|
||||
3. Group by urgency: expiring within 30 days, 31–60 days, and 61–{$days} days.
|
||||
4. Flag any assets that are deployed to critical roles or users where warranty coverage is especially important.
|
||||
5. Recommend actions: extend warranty, schedule replacement, or note as acceptable risk.
|
||||
TEXT;
|
||||
|
||||
return Response::text(trim($prompt).$this->localeInstruction());
|
||||
}
|
||||
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
new Argument('days', 'Number of days ahead to check for warranty expiry (default: 90)', required: false),
|
||||
];
|
||||
}
|
||||
}
|
||||
-1066
File diff suppressed because it is too large
Load Diff
@@ -1,275 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Servers;
|
||||
|
||||
use App\Mcp\Prompts\AuditLocationPrompt;
|
||||
use App\Mcp\Prompts\EndOfLifeReviewPrompt;
|
||||
use App\Mcp\Prompts\ExpiringLicensesPrompt;
|
||||
use App\Mcp\Prompts\FindAvailableAssetPrompt;
|
||||
use App\Mcp\Prompts\InventorySummaryPrompt;
|
||||
use App\Mcp\Prompts\OffboardEmployeePrompt;
|
||||
use App\Mcp\Prompts\OnboardEmployeePrompt;
|
||||
use App\Mcp\Prompts\UserInventoryPrompt;
|
||||
use App\Mcp\Prompts\WarrantyExpiringPrompt;
|
||||
use App\Mcp\Tools\AddAssetNoteTool;
|
||||
use App\Mcp\Tools\AuditAssetTool;
|
||||
use App\Mcp\Tools\CheckinAccessoryTool;
|
||||
use App\Mcp\Tools\CheckinAssetTool;
|
||||
use App\Mcp\Tools\CheckinComponentTool;
|
||||
use App\Mcp\Tools\CheckinLicenseTool;
|
||||
use App\Mcp\Tools\CheckoutAccessoryTool;
|
||||
use App\Mcp\Tools\CheckoutAssetTool;
|
||||
use App\Mcp\Tools\CheckoutComponentTool;
|
||||
use App\Mcp\Tools\CheckoutConsumableTool;
|
||||
use App\Mcp\Tools\CheckoutLicenseTool;
|
||||
use App\Mcp\Tools\CreateAccessoryTool;
|
||||
use App\Mcp\Tools\CreateAssetModelTool;
|
||||
use App\Mcp\Tools\CreateAssetTool;
|
||||
use App\Mcp\Tools\CreateCategoryTool;
|
||||
use App\Mcp\Tools\CreateCompanyTool;
|
||||
use App\Mcp\Tools\CreateComponentTool;
|
||||
use App\Mcp\Tools\CreateConsumableTool;
|
||||
use App\Mcp\Tools\CreateDepartmentTool;
|
||||
use App\Mcp\Tools\CreateDepreciationTool;
|
||||
use App\Mcp\Tools\CreateGroupTool;
|
||||
use App\Mcp\Tools\CreateLicenseTool;
|
||||
use App\Mcp\Tools\CreateLocationTool;
|
||||
use App\Mcp\Tools\CreateMaintenanceTool;
|
||||
use App\Mcp\Tools\CreateManufacturerTool;
|
||||
use App\Mcp\Tools\CreateStatusLabelTool;
|
||||
use App\Mcp\Tools\CreateSupplierTool;
|
||||
use App\Mcp\Tools\CreateUserTool;
|
||||
use App\Mcp\Tools\DeleteAccessoryTool;
|
||||
use App\Mcp\Tools\DeleteAssetModelTool;
|
||||
use App\Mcp\Tools\DeleteAssetTool;
|
||||
use App\Mcp\Tools\DeleteCategoryTool;
|
||||
use App\Mcp\Tools\DeleteCompanyTool;
|
||||
use App\Mcp\Tools\DeleteComponentTool;
|
||||
use App\Mcp\Tools\DeleteConsumableTool;
|
||||
use App\Mcp\Tools\DeleteDepartmentTool;
|
||||
use App\Mcp\Tools\DeleteDepreciationTool;
|
||||
use App\Mcp\Tools\DeleteGroupTool;
|
||||
use App\Mcp\Tools\DeleteLicenseTool;
|
||||
use App\Mcp\Tools\DeleteLocationTool;
|
||||
use App\Mcp\Tools\DeleteManufacturerTool;
|
||||
use App\Mcp\Tools\DeleteStatusLabelTool;
|
||||
use App\Mcp\Tools\DeleteSupplierTool;
|
||||
use App\Mcp\Tools\DeleteUserTool;
|
||||
use App\Mcp\Tools\GetActivityLogTool;
|
||||
use App\Mcp\Tools\GetCurrentUserTool;
|
||||
use App\Mcp\Tools\GetUserAssetsTool;
|
||||
use App\Mcp\Tools\ListAssetModelsTool;
|
||||
use App\Mcp\Tools\ListAssetNotesTool;
|
||||
use App\Mcp\Tools\ListAssetsTool;
|
||||
use App\Mcp\Tools\ListCategoriesTool;
|
||||
use App\Mcp\Tools\ListCompaniesTool;
|
||||
use App\Mcp\Tools\ListConsumablesTool;
|
||||
use App\Mcp\Tools\ListDepreciationsTool;
|
||||
use App\Mcp\Tools\ListGroupsTool;
|
||||
use App\Mcp\Tools\ListHistoryTool;
|
||||
use App\Mcp\Tools\ListLicensesTool;
|
||||
use App\Mcp\Tools\ListLocationsTool;
|
||||
use App\Mcp\Tools\ListMaintenancesTool;
|
||||
use App\Mcp\Tools\ListManufacturersTool;
|
||||
use App\Mcp\Tools\ListStatusLabelsTool;
|
||||
use App\Mcp\Tools\ListSuppliersTool;
|
||||
use App\Mcp\Tools\ListUploadsTool;
|
||||
use App\Mcp\Tools\ListUsersTool;
|
||||
use App\Mcp\Tools\Reset2FATool;
|
||||
use App\Mcp\Tools\RestoreAssetTool;
|
||||
use App\Mcp\Tools\RestoreUserTool;
|
||||
use App\Mcp\Tools\SendPasswordResetTool;
|
||||
use App\Mcp\Tools\ShowAssetModelTool;
|
||||
use App\Mcp\Tools\ShowAssetTool;
|
||||
use App\Mcp\Tools\ShowCategoryTool;
|
||||
use App\Mcp\Tools\ShowCompanyTool;
|
||||
use App\Mcp\Tools\ShowConsumableTool;
|
||||
use App\Mcp\Tools\ShowDepreciationTool;
|
||||
use App\Mcp\Tools\ShowGroupTool;
|
||||
use App\Mcp\Tools\ShowLicenseTool;
|
||||
use App\Mcp\Tools\ShowLocationTool;
|
||||
use App\Mcp\Tools\ShowManufacturerTool;
|
||||
use App\Mcp\Tools\ShowStatusLabelTool;
|
||||
use App\Mcp\Tools\ShowSupplierTool;
|
||||
use App\Mcp\Tools\ShowUserTool;
|
||||
use App\Mcp\Tools\UpdateAccessoryTool;
|
||||
use App\Mcp\Tools\UpdateAssetModelTool;
|
||||
use App\Mcp\Tools\UpdateAssetTool;
|
||||
use App\Mcp\Tools\UpdateCategoryTool;
|
||||
use App\Mcp\Tools\UpdateCompanyTool;
|
||||
use App\Mcp\Tools\UpdateComponentTool;
|
||||
use App\Mcp\Tools\UpdateConsumableTool;
|
||||
use App\Mcp\Tools\UpdateDepartmentTool;
|
||||
use App\Mcp\Tools\UpdateDepreciationTool;
|
||||
use App\Mcp\Tools\UpdateGroupTool;
|
||||
use App\Mcp\Tools\UpdateLicenseTool;
|
||||
use App\Mcp\Tools\UpdateLocationTool;
|
||||
use App\Mcp\Tools\UpdateManufacturerTool;
|
||||
use App\Mcp\Tools\UpdateProfileTool;
|
||||
use App\Mcp\Tools\UpdateStatusLabelTool;
|
||||
use App\Mcp\Tools\UpdateSupplierTool;
|
||||
use App\Mcp\Tools\UpdateUserTool;
|
||||
use Laravel\Mcp\Server;
|
||||
use Laravel\Mcp\Server\Attributes\Instructions;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Version;
|
||||
|
||||
#[Name('Snipe-IT MCP Server')]
|
||||
#[Version('0.0.1')]
|
||||
#[Instructions('This server allows you to interact with the Snipe-IT asset management database. You can list, view, check out, and check in assets.')]
|
||||
class SnipeMCPServer extends Server
|
||||
{
|
||||
protected array $tools = [
|
||||
// Assets
|
||||
ShowAssetTool::class,
|
||||
ListAssetsTool::class,
|
||||
CreateAssetTool::class,
|
||||
UpdateAssetTool::class,
|
||||
DeleteAssetTool::class,
|
||||
RestoreAssetTool::class,
|
||||
CheckoutAssetTool::class,
|
||||
CheckinAssetTool::class,
|
||||
AuditAssetTool::class,
|
||||
AddAssetNoteTool::class,
|
||||
ListAssetNotesTool::class,
|
||||
|
||||
// Cross-type tools
|
||||
ListUploadsTool::class,
|
||||
ListHistoryTool::class,
|
||||
|
||||
// Users
|
||||
ListUsersTool::class,
|
||||
ShowUserTool::class,
|
||||
CreateUserTool::class,
|
||||
UpdateUserTool::class,
|
||||
DeleteUserTool::class,
|
||||
RestoreUserTool::class,
|
||||
GetCurrentUserTool::class,
|
||||
UpdateProfileTool::class,
|
||||
GetUserAssetsTool::class,
|
||||
Reset2FATool::class,
|
||||
SendPasswordResetTool::class,
|
||||
|
||||
// Accessories
|
||||
CreateAccessoryTool::class,
|
||||
UpdateAccessoryTool::class,
|
||||
DeleteAccessoryTool::class,
|
||||
CheckoutAccessoryTool::class,
|
||||
CheckinAccessoryTool::class,
|
||||
|
||||
// Components
|
||||
CreateComponentTool::class,
|
||||
UpdateComponentTool::class,
|
||||
DeleteComponentTool::class,
|
||||
CheckoutComponentTool::class,
|
||||
CheckinComponentTool::class,
|
||||
|
||||
// Consumables
|
||||
ListConsumablesTool::class,
|
||||
ShowConsumableTool::class,
|
||||
CreateConsumableTool::class,
|
||||
UpdateConsumableTool::class,
|
||||
DeleteConsumableTool::class,
|
||||
CheckoutConsumableTool::class,
|
||||
|
||||
// Licenses
|
||||
ListLicensesTool::class,
|
||||
ShowLicenseTool::class,
|
||||
CreateLicenseTool::class,
|
||||
UpdateLicenseTool::class,
|
||||
DeleteLicenseTool::class,
|
||||
CheckoutLicenseTool::class,
|
||||
CheckinLicenseTool::class,
|
||||
|
||||
// Departments
|
||||
CreateDepartmentTool::class,
|
||||
UpdateDepartmentTool::class,
|
||||
DeleteDepartmentTool::class,
|
||||
|
||||
// Companies
|
||||
ListCompaniesTool::class,
|
||||
ShowCompanyTool::class,
|
||||
CreateCompanyTool::class,
|
||||
UpdateCompanyTool::class,
|
||||
DeleteCompanyTool::class,
|
||||
|
||||
// Categories
|
||||
ListCategoriesTool::class,
|
||||
ShowCategoryTool::class,
|
||||
CreateCategoryTool::class,
|
||||
UpdateCategoryTool::class,
|
||||
DeleteCategoryTool::class,
|
||||
|
||||
// Manufacturers
|
||||
ListManufacturersTool::class,
|
||||
ShowManufacturerTool::class,
|
||||
CreateManufacturerTool::class,
|
||||
UpdateManufacturerTool::class,
|
||||
DeleteManufacturerTool::class,
|
||||
|
||||
// Suppliers
|
||||
ListSuppliersTool::class,
|
||||
ShowSupplierTool::class,
|
||||
CreateSupplierTool::class,
|
||||
UpdateSupplierTool::class,
|
||||
DeleteSupplierTool::class,
|
||||
|
||||
// Status Labels
|
||||
ListStatusLabelsTool::class,
|
||||
ShowStatusLabelTool::class,
|
||||
CreateStatusLabelTool::class,
|
||||
UpdateStatusLabelTool::class,
|
||||
DeleteStatusLabelTool::class,
|
||||
|
||||
// Locations
|
||||
ListLocationsTool::class,
|
||||
ShowLocationTool::class,
|
||||
CreateLocationTool::class,
|
||||
UpdateLocationTool::class,
|
||||
DeleteLocationTool::class,
|
||||
|
||||
// Asset Models
|
||||
ListAssetModelsTool::class,
|
||||
ShowAssetModelTool::class,
|
||||
CreateAssetModelTool::class,
|
||||
UpdateAssetModelTool::class,
|
||||
DeleteAssetModelTool::class,
|
||||
|
||||
// Depreciations
|
||||
ListDepreciationsTool::class,
|
||||
ShowDepreciationTool::class,
|
||||
CreateDepreciationTool::class,
|
||||
UpdateDepreciationTool::class,
|
||||
DeleteDepreciationTool::class,
|
||||
|
||||
// Groups
|
||||
ListGroupsTool::class,
|
||||
ShowGroupTool::class,
|
||||
CreateGroupTool::class,
|
||||
UpdateGroupTool::class,
|
||||
DeleteGroupTool::class,
|
||||
|
||||
// Maintenance
|
||||
ListMaintenancesTool::class,
|
||||
CreateMaintenanceTool::class,
|
||||
|
||||
// Activity Log
|
||||
GetActivityLogTool::class,
|
||||
];
|
||||
|
||||
protected array $resources = [
|
||||
//
|
||||
];
|
||||
|
||||
protected array $prompts = [
|
||||
OnboardEmployeePrompt::class,
|
||||
OffboardEmployeePrompt::class,
|
||||
AuditLocationPrompt::class,
|
||||
FindAvailableAssetPrompt::class,
|
||||
ExpiringLicensesPrompt::class,
|
||||
EndOfLifeReviewPrompt::class,
|
||||
WarrantyExpiringPrompt::class,
|
||||
InventorySummaryPrompt::class,
|
||||
UserInventoryPrompt::class,
|
||||
];
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('add_asset_note')]
|
||||
#[Title('Add Asset Note')]
|
||||
#[Description('Add a manual note to a Snipe-IT asset identified by asset tag, serial number, or numeric ID')]
|
||||
class AddAssetNoteTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|string|max:100',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'id' => 'nullable|integer',
|
||||
'note' => 'required|string|max:50000',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('update', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$logaction = new Actionlog;
|
||||
$logaction->item_type = Asset::class;
|
||||
$logaction->item_id = $asset->id;
|
||||
$logaction->note = $request->get('note');
|
||||
$logaction->created_by = auth()->id();
|
||||
|
||||
if ($logaction->logaction('note added')) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.note_added_to_asset', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.note_added_successfully'),
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'asset_id' => $asset->id,
|
||||
'note' => $logaction->note,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.note_save_failed')));
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))->first();
|
||||
}
|
||||
if ($request->filled('serial')) {
|
||||
return Asset::where('serial', $request->get('serial'))->first();
|
||||
}
|
||||
if ($request->filled('id')) {
|
||||
return Asset::find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset'),
|
||||
'serial' => $schema->string()->description('Serial number of the asset'),
|
||||
'id' => $schema->number()->description('Numeric ID of the asset'),
|
||||
'note' => $schema->string()->description('Note text to add to the asset'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the note was saved'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset'),
|
||||
'asset_id' => $schema->number()->description('Numeric ID of the asset'),
|
||||
'note' => $schema->string()->description('The note that was saved'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Setting;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('audit_asset')]
|
||||
#[Title('Audit Asset')]
|
||||
#[Description('Record an audit for a Snipe-IT asset, updating the last audit date and optionally the location')]
|
||||
class AuditAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|max:100',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'id' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:1000',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'next_audit_date' => 'nullable|date',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('audit', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$originalValues = $asset->getRawOriginal();
|
||||
$settings = Setting::getSettings();
|
||||
|
||||
$asset->last_audit_date = date('Y-m-d H:i:s');
|
||||
|
||||
if ($request->filled('next_audit_date')) {
|
||||
$asset->next_audit_date = $request->get('next_audit_date');
|
||||
} elseif (! is_null($settings->audit_interval)) {
|
||||
$asset->next_audit_date = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$asset->location_id = $request->get('location_id');
|
||||
}
|
||||
|
||||
// Bypass the observer to avoid logging a spurious asset-update entry
|
||||
// alongside the audit log entry created by logAudit() below
|
||||
$asset->unsetEventDispatcher();
|
||||
|
||||
if ($asset->isValid() && $asset->save()) {
|
||||
$asset->logAudit($request->get('note'), $request->get('location_id'), null, $originalValues);
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_audited', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_audited', ['asset_tag' => $asset->asset_tag]),
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'last_audit_date' => $asset->last_audit_date,
|
||||
'next_audit_date' => $asset->next_audit_date,
|
||||
'location' => $asset->location?->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.audit_failed', ['error' => $asset->getErrors()->first()])));
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))->with('location')->first();
|
||||
}
|
||||
if ($request->filled('serial')) {
|
||||
return Asset::where('serial', $request->get('serial'))->with('location')->first();
|
||||
}
|
||||
if ($request->filled('id')) {
|
||||
return Asset::with('location')->find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset to audit'),
|
||||
'serial' => $schema->string()->description('Serial number of the asset to audit'),
|
||||
'id' => $schema->number()->description('Numeric ID of the asset to audit'),
|
||||
'note' => $schema->string()->description('Optional audit note'),
|
||||
'location_id' => $schema->number()->description('Location ID where the asset was found (also updates the asset location)'),
|
||||
'next_audit_date' => $schema->string()->description('Override the next audit date (YYYY-MM-DD); defaults to now plus the audit_interval from settings'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the audit succeeded'),
|
||||
'error' => $schema->boolean()->description('True if the audit failed'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the audited asset'),
|
||||
'last_audit_date' => $schema->string()->description('Timestamp of the audit just recorded'),
|
||||
'next_audit_date' => $schema->string()->description('Date of the next scheduled audit'),
|
||||
'location' => $schema->string()->description('Location name where the asset was found'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\AccessoryCheckout;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkin_accessory')]
|
||||
#[Title('Checkin Accessory')]
|
||||
#[Description('Check in a Snipe-IT accessory checkout record by its checkout ID')]
|
||||
class CheckinAccessoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'checkout_id' => 'required|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$checkout = AccessoryCheckout::find($request->get('checkout_id'));
|
||||
|
||||
if (! $checkout) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_checkout_not_found')));
|
||||
}
|
||||
|
||||
$accessory = Accessory::find($checkout->accessory_id);
|
||||
|
||||
if (! $accessory) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkin', $accessory)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$target = $checkout->assigned_type && $checkout->assigned_to
|
||||
? $checkout->assigned_type::find($checkout->assigned_to)
|
||||
: null;
|
||||
|
||||
$accessory->logCheckin($target, $request->get('note'));
|
||||
|
||||
if ($checkout->delete()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.accessory_checked_in', ['name' => $accessory->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.accessory_checked_in', ['name' => $accessory->name]),
|
||||
'accessory_id' => $accessory->id,
|
||||
'accessory_name' => $accessory->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkin_failed')));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'checkout_id' => $schema->number()->description('ID of the checkout record to check in (returned by checkout_accessory)'),
|
||||
'note' => $schema->string()->description('Optional checkin note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkin succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'accessory_id' => $schema->number()->description('Numeric ID of the accessory'),
|
||||
'accessory_name' => $schema->string()->description('Name of the accessory'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkin_asset')]
|
||||
#[Title('Check In Asset')]
|
||||
#[Description('Check a currently checked-out Snipe-IT asset back in')]
|
||||
class CheckinAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|max:100',
|
||||
'id' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:1000',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkin', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$target = $asset->assignedTo;
|
||||
|
||||
if (is_null($target)) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_checked_out', ['asset_tag' => $asset->asset_tag])));
|
||||
}
|
||||
|
||||
$originalValues = $asset->getRawOriginal();
|
||||
$checkinAt = date('Y-m-d H:i:s');
|
||||
|
||||
$asset->expected_checkin = null;
|
||||
$asset->last_checkin = now();
|
||||
$asset->assignedTo()->disassociate($asset);
|
||||
$asset->accepted = null;
|
||||
$asset->location_id = $asset->rtd_location_id;
|
||||
|
||||
if ($asset->save()) {
|
||||
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->get('note'), $checkinAt, $originalValues));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_checked_in', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_checked_in', ['asset_tag' => $asset->asset_tag]),
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'model' => $asset->model?->name,
|
||||
'location' => $asset->location?->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkin_failed_error', ['error' => $asset->getErrors()->first()])));
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))
|
||||
->with('model', 'location')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($request->filled('id')) {
|
||||
return Asset::with('model', 'location')->find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()
|
||||
->description('Asset tag of the asset to check in'),
|
||||
'id' => $schema->number()
|
||||
->description('Numeric ID of the asset to check in'),
|
||||
'note' => $schema->string()
|
||||
->description('Optional note to attach to this checkin'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->string()->description('True if the checkin succeeded'),
|
||||
'error' => $schema->string()->description('True if the checkin failed'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the checked-in asset'),
|
||||
'model' => $schema->string()->description('Model name of the checked-in asset'),
|
||||
'location' => $schema->string()->description('Location the asset returned to'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Component;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkin_component')]
|
||||
#[Title('Checkin Component')]
|
||||
#[Description('Check in one or more units of a Snipe-IT component from an asset using the checkout record ID')]
|
||||
class CheckinComponentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'component_asset_id' => 'required|integer',
|
||||
'checkin_qty' => 'nullable|integer|min:1',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$componentAsset = DB::table('components_assets')->find($request->get('component_asset_id'));
|
||||
|
||||
if (! $componentAsset) {
|
||||
return Response::make(Response::error(trans('mcp.component_checkout_not_found')));
|
||||
}
|
||||
|
||||
$component = Component::find($componentAsset->component_id);
|
||||
|
||||
if (! $component) {
|
||||
return Response::make(Response::error(trans('mcp.component_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkin', $component)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$maxCheckin = $componentAsset->assigned_qty ?? 1;
|
||||
$checkinQty = (int) $request->get('checkin_qty', $maxCheckin);
|
||||
|
||||
if ($checkinQty > $maxCheckin) {
|
||||
return Response::make(Response::error(
|
||||
'Checkin quantity ('.$checkinQty.') exceeds assigned quantity ('.$maxCheckin.')'
|
||||
));
|
||||
}
|
||||
|
||||
$remaining = $maxCheckin - $checkinQty;
|
||||
|
||||
if ($remaining === 0) {
|
||||
DB::table('components_assets')->where('id', $componentAsset->id)->delete();
|
||||
} else {
|
||||
DB::table('components_assets')->where('id', $componentAsset->id)->update(['assigned_qty' => $remaining]);
|
||||
}
|
||||
|
||||
$asset = Asset::find($componentAsset->asset_id);
|
||||
|
||||
event(new CheckoutableCheckedIn($component, $asset, auth()->user(), $request->get('note'), Carbon::now()));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.component_checked_in', ['name' => $component->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.component_checked_in', ['name' => $component->name]),
|
||||
'component_id' => $component->id,
|
||||
'component_name' => $component->name,
|
||||
'checkin_qty' => $checkinQty,
|
||||
'qty_still_checked_out' => $remaining,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'component_asset_id' => $schema->number()->description('ID of the checkout record to check in (returned by checkout_component)'),
|
||||
'checkin_qty' => $schema->number()->description('Number of units to check in (default: all assigned units)'),
|
||||
'note' => $schema->string()->description('Optional checkin note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkin succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'component_id' => $schema->number()->description('Numeric ID of the component'),
|
||||
'component_name' => $schema->string()->description('Name of the component'),
|
||||
'checkin_qty' => $schema->number()->description('Number of units checked in'),
|
||||
'qty_still_checked_out' => $schema->number()->description('Units remaining checked out on this record (0 means fully returned)'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Models\Asset;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkin_license')]
|
||||
#[Title('Checkin License')]
|
||||
#[Description('Check in a Snipe-IT license seat by its seat ID, returning it to the available pool')]
|
||||
class CheckinLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'seat_id' => 'required|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$seat = LicenseSeat::with('license')->find($request->get('seat_id'));
|
||||
|
||||
if (! $seat) {
|
||||
return Response::make(Response::error(trans('mcp.license_seat_not_found')));
|
||||
}
|
||||
|
||||
if (is_null($seat->assigned_to) && is_null($seat->asset_id)) {
|
||||
return Response::make(Response::error(trans('mcp.seat_not_checked_out')));
|
||||
}
|
||||
|
||||
$license = $seat->license;
|
||||
|
||||
if (! $license) {
|
||||
return Response::make(Response::error(trans('mcp.license_not_found')));
|
||||
}
|
||||
|
||||
// License checkin uses the checkout gate (matching application behavior)
|
||||
if (! Gate::allows('checkout', $license)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$returnTo = null;
|
||||
if ($seat->assigned_to) {
|
||||
$returnTo = User::withTrashed()->find($seat->assigned_to);
|
||||
} elseif ($seat->asset_id) {
|
||||
$returnTo = Asset::find($seat->asset_id);
|
||||
}
|
||||
|
||||
$note = $request->get('note');
|
||||
|
||||
$seat->assigned_to = null;
|
||||
$seat->asset_id = null;
|
||||
$seat->notes = $note;
|
||||
|
||||
if (! $license->reassignable) {
|
||||
$seat->unreassignable_seat = true;
|
||||
}
|
||||
|
||||
if ($seat->save()) {
|
||||
event(new CheckoutableCheckedIn($seat, $returnTo, auth()->user(), $note));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_seat_checked_in', ['id' => $seat->id]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_seat_checked_in', ['id' => $seat->id]),
|
||||
'seat_id' => $seat->id,
|
||||
'license_id' => $license->id,
|
||||
'license_name' => $license->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkin_failed')));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'seat_id' => $schema->number()->description('ID of the license seat to check in (returned by checkout_license)'),
|
||||
'note' => $schema->string()->description('Optional checkin note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkin succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'seat_id' => $schema->number()->description('ID of the seat that was checked in'),
|
||||
'license_id' => $schema->number()->description('Numeric ID of the license'),
|
||||
'license_name' => $schema->string()->description('Name of the license'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\AccessoryCheckout;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Location;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_accessory')]
|
||||
#[Title('Checkout Accessory')]
|
||||
#[Description('Check out a Snipe-IT accessory to a user, location, or asset')]
|
||||
class CheckoutAccessoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'checkout_to_type' => 'required|in:user,location,asset',
|
||||
'assigned_user' => 'nullable|integer',
|
||||
'assigned_location' => 'nullable|integer',
|
||||
'assigned_asset' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$accessory = $this->resolveAccessory($request);
|
||||
|
||||
if (! $accessory) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $accessory)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($accessory->numRemaining() < 1) {
|
||||
return Response::make(Response::error(trans('mcp.no_units_available')));
|
||||
}
|
||||
|
||||
$checkoutType = $request->get('checkout_to_type');
|
||||
|
||||
$target = match ($checkoutType) {
|
||||
'user' => User::find($request->get('assigned_user')),
|
||||
'location' => Location::find($request->get('assigned_location')),
|
||||
'asset' => Asset::find($request->get('assigned_asset')),
|
||||
};
|
||||
|
||||
if (! $target) {
|
||||
return Response::make(Response::error(trans('mcp.checkout_target_not_found', ['type' => $checkoutType])));
|
||||
}
|
||||
|
||||
$checkout = new AccessoryCheckout([
|
||||
'accessory_id' => $accessory->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'assigned_to' => $target->id,
|
||||
'assigned_type' => $target::class,
|
||||
'note' => $request->get('note'),
|
||||
]);
|
||||
$checkout->created_by = auth()->id();
|
||||
$checkout->save();
|
||||
|
||||
event(new CheckoutableCheckedOut(
|
||||
$accessory,
|
||||
$target,
|
||||
auth()->user(),
|
||||
$request->get('note'),
|
||||
[],
|
||||
1,
|
||||
));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.accessory_checked_out', ['name' => $accessory->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.accessory_checked_out', ['name' => $accessory->name]),
|
||||
'accessory_id' => $accessory->id,
|
||||
'accessory_name' => $accessory->name,
|
||||
'checkout_id' => $checkout->id,
|
||||
'checked_out_to_type' => $checkoutType,
|
||||
'checked_out_to_id' => $target->id,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveAccessory(Request $request): ?Accessory
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Accessory::withCount('checkouts as checkouts_count')->find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Accessory::withCount('checkouts as checkouts_count')->where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the accessory to check out'),
|
||||
'name' => $schema->string()->description('Name of the accessory to check out'),
|
||||
'checkout_to_type' => $schema->string()->description('Target type: user, location, or asset (required)'),
|
||||
'assigned_user' => $schema->number()->description('User ID to check out to'),
|
||||
'assigned_location' => $schema->number()->description('Location ID to check out to'),
|
||||
'assigned_asset' => $schema->number()->description('Asset ID to check out to'),
|
||||
'note' => $schema->string()->description('Optional checkout note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkout succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'accessory_id' => $schema->number()->description('Numeric ID of the accessory'),
|
||||
'accessory_name' => $schema->string()->description('Name of the accessory'),
|
||||
'checkout_id' => $schema->number()->description('ID of the checkout record (use this for checkin)'),
|
||||
'checked_out_to_type' => $schema->string()->description('Type of target: user, location, or asset'),
|
||||
'checked_out_to_id' => $schema->number()->description('ID of the target'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Location;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_asset')]
|
||||
#[Title('Checkout Asset')]
|
||||
#[Description('Check out a Snipe-IT asset to a user, location, or another asset')]
|
||||
class CheckoutAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|max:100',
|
||||
'id' => 'nullable|integer',
|
||||
'checkout_to_type' => 'required|string|in:user,location,asset',
|
||||
'assigned_user' => 'nullable|integer',
|
||||
'assigned_location' => 'nullable|integer',
|
||||
'assigned_asset' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:1000',
|
||||
'checkout_at' => 'nullable|date',
|
||||
'expected_checkin' => 'nullable|date',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if (! $asset->availableForCheckout()) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_available', ['asset_tag' => $asset->asset_tag])));
|
||||
}
|
||||
|
||||
$checkoutType = $request->get('checkout_to_type');
|
||||
$target = null;
|
||||
|
||||
if ($checkoutType === 'user') {
|
||||
$target = User::find($request->get('assigned_user'));
|
||||
if ($target) {
|
||||
$asset->location_id = $target->location_id ?? $asset->location_id;
|
||||
}
|
||||
} elseif ($checkoutType === 'location') {
|
||||
$target = Location::find($request->get('assigned_location'));
|
||||
if ($target) {
|
||||
$asset->location_id = $target->id;
|
||||
}
|
||||
} elseif ($checkoutType === 'asset') {
|
||||
$target = Asset::where('id', '!=', $asset->id)->find($request->get('assigned_asset'));
|
||||
if ($target) {
|
||||
$asset->location_id = $target->location_id ?? $asset->location_id;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $target) {
|
||||
return Response::make(Response::error(trans('mcp.checkout_target_not_found', ['type' => $checkoutType])));
|
||||
}
|
||||
|
||||
$checkoutAt = $request->filled('checkout_at') ? $request->get('checkout_at') : date('Y-m-d H:i:s');
|
||||
$expectedCheckin = $request->filled('expected_checkin') ? $request->get('expected_checkin') : null;
|
||||
$note = $request->filled('note') ? $request->get('note') : null;
|
||||
|
||||
if ($asset->checkOut($target, auth()->user(), $checkoutAt, $expectedCheckin, $note, $asset->name, $asset->location_id)) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_checked_out', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_checked_out', ['asset_tag' => $asset->asset_tag]),
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'checked_out_to_type' => $checkoutType,
|
||||
'checked_out_to_id' => $target->id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkout_failed')));
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))
|
||||
->with('status')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($request->filled('id')) {
|
||||
return Asset::with('status')->find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()
|
||||
->description('Asset tag of the asset to check out'),
|
||||
'id' => $schema->number()
|
||||
->description('Numeric ID of the asset to check out'),
|
||||
'checkout_to_type' => $schema->string()
|
||||
->description('What to check the asset out to: user, location, or asset')
|
||||
->required(),
|
||||
'assigned_user' => $schema->number()
|
||||
->description('ID of the user to check the asset out to (when checkout_to_type is user)'),
|
||||
'assigned_location' => $schema->number()
|
||||
->description('ID of the location to check the asset out to (when checkout_to_type is location)'),
|
||||
'assigned_asset' => $schema->number()
|
||||
->description('ID of the asset to check the asset out to (when checkout_to_type is asset)'),
|
||||
'note' => $schema->string()
|
||||
->description('Optional note to attach to this checkout'),
|
||||
'checkout_at' => $schema->string()
|
||||
->description('Checkout date/time (defaults to now, format: YYYY-MM-DD)'),
|
||||
'expected_checkin' => $schema->string()
|
||||
->description('Expected checkin date (format: YYYY-MM-DD)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->string()->description('True if the checkout succeeded'),
|
||||
'error' => $schema->string()->description('True if the checkout failed'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the checked-out asset'),
|
||||
'checked_out_to_type' => $schema->string()->description('Type of entity the asset was checked out to'),
|
||||
'checked_out_to_id' => $schema->number()->description('ID of the entity the asset was checked out to'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Component;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_component')]
|
||||
#[Title('Checkout Component')]
|
||||
#[Description('Check out one or more units of a Snipe-IT component to an asset')]
|
||||
class CheckoutComponentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:191',
|
||||
'asset_id' => 'required|integer|exists:assets,id',
|
||||
'assigned_qty' => 'nullable|integer|min:1',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$component = $this->resolveComponent($request);
|
||||
|
||||
if (! $component) {
|
||||
return Response::make(Response::error(trans('mcp.component_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $component)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$qty = (int) $request->get('assigned_qty', 1);
|
||||
|
||||
if ($component->numRemaining() < $qty) {
|
||||
return Response::make(Response::error(
|
||||
'Not enough units available. Requested: '.$qty.', remaining: '.$component->numRemaining()
|
||||
));
|
||||
}
|
||||
|
||||
$asset = Asset::find($request->get('asset_id'));
|
||||
|
||||
$component->assets()->attach($component->id, [
|
||||
'component_id' => $component->id,
|
||||
'created_at' => Carbon::now(),
|
||||
'assigned_qty' => $qty,
|
||||
'created_by' => auth()->id(),
|
||||
'asset_id' => $asset->id,
|
||||
'note' => $request->get('note'),
|
||||
]);
|
||||
|
||||
$pivotId = $component->assets()->wherePivot('asset_id', $asset->id)->latest('components_assets.created_at')->first()?->pivot->id;
|
||||
|
||||
$component->logCheckout($request->get('note'), $asset, null, [], $qty);
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.component_checked_out', ['name' => $component->name, 'asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.component_checked_out', ['name' => $component->name, 'asset_tag' => $asset->asset_tag]),
|
||||
'component_id' => $component->id,
|
||||
'component_name' => $component->name,
|
||||
'asset_id' => $asset->id,
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'assigned_qty' => $qty,
|
||||
'component_asset_id' => $pivotId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveComponent(Request $request): ?Component
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Component::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Component::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the component to check out'),
|
||||
'name' => $schema->string()->description('Name of the component to check out'),
|
||||
'asset_id' => $schema->number()->description('Asset ID to check the component out to (required)'),
|
||||
'assigned_qty' => $schema->number()->description('Number of units to check out (default: 1)'),
|
||||
'note' => $schema->string()->description('Optional checkout note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkout succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'component_id' => $schema->number()->description('Numeric ID of the component'),
|
||||
'component_name' => $schema->string()->description('Name of the component'),
|
||||
'asset_id' => $schema->number()->description('ID of the asset checked out to'),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset checked out to'),
|
||||
'assigned_qty' => $schema->number()->description('Number of units checked out'),
|
||||
'component_asset_id' => $schema->number()->description('ID of the checkout record (use this for checkin)'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_consumable')]
|
||||
#[Title('Checkout Consumable')]
|
||||
#[Description('Check out a Snipe-IT consumable to a user')]
|
||||
class CheckoutConsumableTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'assigned_to' => 'required|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$consumable = $this->resolveConsumable($request);
|
||||
|
||||
if (! $consumable) {
|
||||
return Response::make(Response::error(trans('mcp.consumable_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $consumable)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($consumable->numRemaining() <= 0) {
|
||||
return Response::make(Response::error(trans('mcp.no_units_remaining')));
|
||||
}
|
||||
|
||||
$user = User::find($request->get('assigned_to'));
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_found')));
|
||||
}
|
||||
|
||||
$consumable->users()->attach($consumable->id, [
|
||||
'consumable_id' => $consumable->id,
|
||||
'created_by' => auth()->id(),
|
||||
'assigned_to' => $user->id,
|
||||
'note' => $request->get('note'),
|
||||
]);
|
||||
|
||||
event(new CheckoutableCheckedOut(
|
||||
$consumable,
|
||||
$user,
|
||||
auth()->user(),
|
||||
$request->get('note'),
|
||||
[],
|
||||
1,
|
||||
));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.consumable_checked_out', ['name' => $consumable->name, 'username' => $user->username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.consumable_checked_out', ['name' => $consumable->name, 'username' => $user->username]),
|
||||
'consumable_id' => $consumable->id,
|
||||
'consumable_name' => $consumable->name,
|
||||
'assigned_to_id' => $user->id,
|
||||
'assigned_to_username' => $user->username,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveConsumable(Request $request): ?Consumable
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Consumable::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Consumable::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the consumable to check out'),
|
||||
'name' => $schema->string()->description('Name of the consumable to check out'),
|
||||
'assigned_to' => $schema->number()->description('User ID to check out to (required)'),
|
||||
'note' => $schema->string()->description('Optional checkout note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkout succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'consumable_id' => $schema->number()->description('Numeric ID of the consumable'),
|
||||
'consumable_name' => $schema->string()->description('Name of the consumable'),
|
||||
'assigned_to_id' => $schema->number()->description('ID of the user the consumable was checked out to'),
|
||||
'assigned_to_username' => $schema->string()->description('Username of the user the consumable was checked out to'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Models\Asset;
|
||||
use App\Models\License;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('checkout_license')]
|
||||
#[Title('Checkout License')]
|
||||
#[Description('Check out an available license seat to a user or asset')]
|
||||
class CheckoutLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'assigned_to' => 'nullable|integer',
|
||||
'asset_id' => 'nullable|integer',
|
||||
'note' => 'nullable|string|max:65535',
|
||||
]);
|
||||
|
||||
$license = $this->resolveLicense($request);
|
||||
|
||||
if (! $license) {
|
||||
return Response::make(Response::error(trans('mcp.license_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('checkout', $license)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($license->numRemaining() < 1) {
|
||||
return Response::make(Response::error(trans('mcp.no_available_seats')));
|
||||
}
|
||||
|
||||
if (! $request->filled('assigned_to') && ! $request->filled('asset_id')) {
|
||||
return Response::make(Response::error(trans('mcp.provide_user_or_asset')));
|
||||
}
|
||||
|
||||
$seat = $license->freeSeat();
|
||||
|
||||
if (! $seat) {
|
||||
return Response::make(Response::error(trans('mcp.no_free_seat')));
|
||||
}
|
||||
|
||||
$note = $request->get('note');
|
||||
|
||||
if ($request->filled('assigned_to')) {
|
||||
$target = User::find($request->get('assigned_to'));
|
||||
if (! $target) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_found')));
|
||||
}
|
||||
$seat->assigned_to = $target->id;
|
||||
$seat->notes = $note;
|
||||
|
||||
if ($seat->save()) {
|
||||
event(new CheckoutableCheckedOut($seat, $target, auth()->user(), $note, [], 1));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_seat_checked_out_user', ['username' => $target->username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_seat_checked_out_user', ['username' => $target->username]),
|
||||
'license_id' => $license->id,
|
||||
'license_name' => $license->name,
|
||||
'seat_id' => $seat->id,
|
||||
'assigned_to_type' => 'user',
|
||||
'assigned_to_id' => $target->id,
|
||||
]);
|
||||
}
|
||||
} elseif ($request->filled('asset_id')) {
|
||||
$target = Asset::find($request->get('asset_id'));
|
||||
if (! $target) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
$seat->asset_id = $target->id;
|
||||
if ($target->checkedOutToUser()) {
|
||||
$seat->assigned_to = $target->assigned_to;
|
||||
}
|
||||
$seat->notes = $note;
|
||||
|
||||
if ($seat->save()) {
|
||||
event(new CheckoutableCheckedOut($seat, $target, auth()->user(), $note, [], 1));
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_seat_checked_out_asset', ['asset_tag' => $target->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_seat_checked_out_asset', ['asset_tag' => $target->asset_tag]),
|
||||
'license_id' => $license->id,
|
||||
'license_name' => $license->name,
|
||||
'seat_id' => $seat->id,
|
||||
'assigned_to_type' => 'asset',
|
||||
'assigned_to_id' => $target->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.checkout_failed')));
|
||||
}
|
||||
|
||||
private function resolveLicense(Request $request): ?License
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return License::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return License::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the license to check out'),
|
||||
'name' => $schema->string()->description('Name of the license to check out'),
|
||||
'assigned_to' => $schema->number()->description('User ID to assign the seat to'),
|
||||
'asset_id' => $schema->number()->description('Asset ID to assign the seat to'),
|
||||
'note' => $schema->string()->description('Optional checkout note'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the checkout succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'license_id' => $schema->number()->description('Numeric ID of the license'),
|
||||
'license_name' => $schema->string()->description('Name of the license'),
|
||||
'seat_id' => $schema->number()->description('ID of the seat record (use this for checkin)'),
|
||||
'assigned_to_type' => $schema->string()->description('Type of entity checked out to: user or asset'),
|
||||
'assigned_to_id' => $schema->number()->description('ID of the entity checked out to'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Company;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_accessory')]
|
||||
#[Title('Create Accessory')]
|
||||
#[Description('Create a new Snipe-IT accessory')]
|
||||
class CreateAccessoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Accessory::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'qty' => 'nullable|integer|min:0',
|
||||
'model_number' => 'nullable|string|max:255',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'order_number' => 'nullable|string|max:255',
|
||||
'purchase_cost' => 'nullable|numeric|min:0',
|
||||
'purchase_date' => 'nullable|date_format:Y-m-d',
|
||||
'min_amt' => 'nullable|integer|min:0',
|
||||
'requestable' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$accessory = new Accessory;
|
||||
$accessory->fill($request->only([
|
||||
'name', 'category_id', 'qty', 'model_number', 'manufacturer_id',
|
||||
'supplier_id', 'location_id', 'order_number', 'purchase_cost',
|
||||
'purchase_date', 'min_amt', 'requestable', 'notes',
|
||||
]));
|
||||
|
||||
$accessory->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$accessory->created_by = auth()->id();
|
||||
|
||||
if ($accessory->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.accessory_created', ['name' => $accessory->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.accessory_created', ['name' => $accessory->name]),
|
||||
'id' => $accessory->id,
|
||||
'name' => $accessory->name,
|
||||
'qty' => $accessory->qty,
|
||||
'category_id' => $accessory->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $accessory->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Accessory name (required)'),
|
||||
'category_id' => $schema->number()->description('Category ID — must be an accessory category (required)'),
|
||||
'qty' => $schema->number()->description('Total quantity in stock'),
|
||||
'model_number' => $schema->string()->description('Model number'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'location_id' => $schema->number()->description('Location ID'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'order_number' => $schema->string()->description('Order number'),
|
||||
'purchase_cost' => $schema->number()->description('Purchase cost per unit'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'min_amt' => $schema->number()->description('Minimum quantity threshold for alerts'),
|
||||
'requestable' => $schema->boolean()->description('Whether users can request this accessory'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the accessory was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new accessory'),
|
||||
'name' => $schema->string()->description('Name of the new accessory'),
|
||||
'qty' => $schema->number()->description('Total quantity'),
|
||||
'category_id' => $schema->number()->description('Category ID'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\AssetModel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_asset_model')]
|
||||
#[Title('Create Asset Model')]
|
||||
#[Description('Create a new Snipe-IT asset model')]
|
||||
class CreateAssetModelTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', AssetModel::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'model_number' => 'nullable|string|max:255',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'depreciation_id' => 'nullable|integer|exists:depreciations,id',
|
||||
'eol' => 'nullable|integer|min:0|max:240',
|
||||
'min_amt' => 'nullable|integer|min:0',
|
||||
'notes' => 'nullable|string',
|
||||
'requestable' => 'nullable|boolean',
|
||||
'require_serial' => 'nullable|boolean',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$assetModel = new AssetModel;
|
||||
$assetModel->name = $request->get('name');
|
||||
$assetModel->category_id = $request->get('category_id');
|
||||
$assetModel->created_by = auth()->id();
|
||||
|
||||
foreach (['model_number', 'manufacturer_id', 'depreciation_id', 'eol', 'min_amt', 'notes', 'requestable', 'require_serial'] as $f) {
|
||||
if ($request->filled($f)) {
|
||||
$assetModel->{$f} = $request->get($f);
|
||||
}
|
||||
}
|
||||
|
||||
if ($assetModel->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_model_created', ['name' => $assetModel->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_model_created', ['name' => $assetModel->name]),
|
||||
'id' => $assetModel->id,
|
||||
'name' => $assetModel->name,
|
||||
'category_id' => $assetModel->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $assetModel->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Asset model name (required)'),
|
||||
'category_id' => $schema->number()->description('Category ID (required)'),
|
||||
'model_number' => $schema->string()->description('Model number'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'depreciation_id' => $schema->number()->description('Depreciation schedule ID'),
|
||||
'eol' => $schema->number()->description('End of life in months (0-240)'),
|
||||
'min_amt' => $schema->number()->description('Minimum quantity alert threshold'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'requestable' => $schema->boolean()->description('Whether the model can be requested'),
|
||||
'require_serial' => $schema->boolean()->description('Whether serial numbers are required'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the asset model was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new asset model'),
|
||||
'name' => $schema->string()->description('Name of the new asset model'),
|
||||
'category_id' => $schema->number()->description('Category ID of the new asset model'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_asset')]
|
||||
#[Title('Create Asset')]
|
||||
#[Description('Create a new Snipe-IT asset')]
|
||||
class CreateAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Asset::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'model_id' => 'required|integer|exists:models,id',
|
||||
'status_id' => 'required|integer|exists:status_labels,id',
|
||||
'asset_tag' => 'required|string|max:255',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'serial' => 'nullable|string',
|
||||
'company_id' => 'nullable|integer',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'rtd_location_id' => 'nullable|integer|exists:locations,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'purchase_date' => 'nullable|date_format:Y-m-d',
|
||||
'purchase_cost' => 'nullable|numeric',
|
||||
'order_number' => 'nullable|string|max:191',
|
||||
'warranty_months' => 'nullable|integer|min:0|max:240',
|
||||
'requestable' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string|max:65535',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$asset = new Asset;
|
||||
$asset->model_id = $request->get('model_id');
|
||||
$asset->status_id = $request->get('status_id');
|
||||
$asset->asset_tag = $request->get('asset_tag');
|
||||
$asset->created_by = auth()->id();
|
||||
|
||||
foreach (['name', 'serial', 'company_id', 'location_id', 'rtd_location_id', 'supplier_id', 'purchase_date', 'purchase_cost', 'order_number', 'warranty_months', 'requestable', 'notes'] as $field) {
|
||||
if ($request->filled($field)) {
|
||||
$asset->{$field} = $request->get($field);
|
||||
}
|
||||
}
|
||||
|
||||
if ($asset->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_created', ['asset_tag' => $asset->asset_tag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_created', ['asset_tag' => $asset->asset_tag]),
|
||||
'id' => $asset->id,
|
||||
'asset_tag' => $asset->asset_tag,
|
||||
'name' => $asset->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $asset->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'model_id' => $schema->number()->description('Asset model ID (required)'),
|
||||
'status_id' => $schema->number()->description('Status label ID (required)'),
|
||||
'asset_tag' => $schema->string()->description('Asset tag (required)'),
|
||||
'name' => $schema->string()->description('Display name for the asset'),
|
||||
'serial' => $schema->string()->description('Serial number'),
|
||||
'company_id' => $schema->number()->description('Company ID'),
|
||||
'location_id' => $schema->number()->description('Current location ID'),
|
||||
'rtd_location_id' => $schema->number()->description('Default RTD location ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'purchase_cost' => $schema->number()->description('Purchase cost'),
|
||||
'order_number' => $schema->string()->description('Order number'),
|
||||
'warranty_months' => $schema->number()->description('Warranty length in months (0-240)'),
|
||||
'requestable' => $schema->boolean()->description('Whether the asset is user-requestable'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the asset was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new asset'),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the new asset'),
|
||||
'name' => $schema->string()->description('Display name of the new asset'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_category')]
|
||||
#[Title('Create Category')]
|
||||
#[Description('Create a new Snipe-IT category')]
|
||||
class CreateCategoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Category::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'category_type' => 'required|string|in:asset,accessory,consumable,component,license',
|
||||
'checkin_email' => 'nullable|boolean',
|
||||
'require_acceptance' => 'nullable|boolean',
|
||||
'use_default_eula' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$category = new Category;
|
||||
$category->name = $request->get('name');
|
||||
$category->category_type = $request->get('category_type');
|
||||
$category->created_by = auth()->id();
|
||||
|
||||
foreach (['checkin_email', 'require_acceptance', 'use_default_eula', 'notes'] as $field) {
|
||||
if ($request->filled($field)) {
|
||||
$category->{$field} = $request->get($field);
|
||||
}
|
||||
}
|
||||
|
||||
if ($category->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.category_created', ['name' => $category->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.category_created', ['name' => $category->name]),
|
||||
'id' => $category->id,
|
||||
'name' => $category->name,
|
||||
'category_type' => $category->category_type,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $category->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Category name (required)'),
|
||||
'category_type' => $schema->string()->description('Category type (required): asset, accessory, consumable, component, or license'),
|
||||
'checkin_email' => $schema->boolean()->description('Send checkin email when items are checked in'),
|
||||
'require_acceptance' => $schema->boolean()->description('Require user acceptance when checking out'),
|
||||
'use_default_eula' => $schema->boolean()->description('Use the default EULA'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the category was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new category'),
|
||||
'name' => $schema->string()->description('Name of the new category'),
|
||||
'category_type' => $schema->string()->description('Type of the new category'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_company')]
|
||||
#[Title('Create Company')]
|
||||
#[Description('Create a new Snipe-IT company')]
|
||||
class CreateCompanyTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Company::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'phone' => 'nullable|string',
|
||||
'fax' => 'nullable|string',
|
||||
'email' => 'nullable|string',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$company = new Company;
|
||||
$company->name = $request->get('name');
|
||||
if ($request->filled('phone')) {
|
||||
$company->phone = $request->get('phone');
|
||||
}
|
||||
if ($request->filled('fax')) {
|
||||
$company->fax = $request->get('fax');
|
||||
}
|
||||
if ($request->filled('email')) {
|
||||
$company->email = $request->get('email');
|
||||
}
|
||||
if ($request->filled('notes')) {
|
||||
$company->notes = $request->get('notes');
|
||||
}
|
||||
$company->created_by = auth()->id();
|
||||
|
||||
if ($company->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.company_created', ['name' => $company->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.company_created', ['name' => $company->name]),
|
||||
'id' => $company->id,
|
||||
'name' => $company->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $company->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Company name (required)'),
|
||||
'phone' => $schema->string()->description('Company phone number'),
|
||||
'fax' => $schema->string()->description('Company fax number'),
|
||||
'email' => $schema->string()->description('Company email address'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the company was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new company'),
|
||||
'name' => $schema->string()->description('Name of the new company'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Component;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_component')]
|
||||
#[Title('Create Component')]
|
||||
#[Description('Create a new Snipe-IT component')]
|
||||
class CreateComponentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Component::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:191',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'qty' => 'required|integer|min:1',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'model_number' => 'nullable|string|max:255',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'order_number' => 'nullable|string|max:255',
|
||||
'purchase_cost' => 'nullable|numeric|min:0',
|
||||
'purchase_date' => 'nullable|date_format:Y-m-d',
|
||||
'min_amt' => 'nullable|integer|min:0',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$component = new Component;
|
||||
$component->fill($request->only([
|
||||
'name', 'category_id', 'qty', 'serial', 'model_number',
|
||||
'manufacturer_id', 'supplier_id', 'location_id',
|
||||
'order_number', 'purchase_cost', 'purchase_date', 'min_amt', 'notes',
|
||||
]));
|
||||
|
||||
$component->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$component->created_by = auth()->id();
|
||||
|
||||
if ($component->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.component_created', ['name' => $component->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.component_created', ['name' => $component->name]),
|
||||
'id' => $component->id,
|
||||
'name' => $component->name,
|
||||
'qty' => $component->qty,
|
||||
'category_id' => $component->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $component->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Component name (required)'),
|
||||
'category_id' => $schema->number()->description('Category ID — must be a component category (required)'),
|
||||
'qty' => $schema->number()->description('Total quantity in stock (required, min 1)'),
|
||||
'serial' => $schema->string()->description('Serial number'),
|
||||
'model_number' => $schema->string()->description('Model number'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'location_id' => $schema->number()->description('Location ID'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'order_number' => $schema->string()->description('Order number'),
|
||||
'purchase_cost' => $schema->number()->description('Purchase cost per unit'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'min_amt' => $schema->number()->description('Minimum quantity threshold for alerts'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the component was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new component'),
|
||||
'name' => $schema->string()->description('Name of the new component'),
|
||||
'qty' => $schema->number()->description('Total quantity'),
|
||||
'category_id' => $schema->number()->description('Category ID'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Consumable;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_consumable')]
|
||||
#[Title('Create Consumable')]
|
||||
#[Description('Create a new Snipe-IT consumable')]
|
||||
class CreateConsumableTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Consumable::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'qty' => 'required|integer|min:0',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'company_id' => 'nullable|integer',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'item_no' => 'nullable|string|max:255',
|
||||
'order_number' => 'nullable|string|max:255',
|
||||
'model_number' => 'nullable|string|max:255',
|
||||
'purchase_cost' => 'nullable|numeric|min:0',
|
||||
'purchase_date' => 'nullable|date_format:Y-m-d',
|
||||
'min_amt' => 'nullable|integer|min:0',
|
||||
'requestable' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$consumable = new Consumable;
|
||||
$consumable->fill($request->only([
|
||||
'name', 'qty', 'category_id', 'company_id', 'location_id', 'manufacturer_id',
|
||||
'supplier_id', 'item_no', 'order_number', 'model_number', 'purchase_cost',
|
||||
'purchase_date', 'min_amt', 'requestable', 'notes',
|
||||
]));
|
||||
$consumable->created_by = auth()->id();
|
||||
|
||||
if ($consumable->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.consumable_created', ['name' => $consumable->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.consumable_created', ['name' => $consumable->name]),
|
||||
'id' => $consumable->id,
|
||||
'name' => $consumable->name,
|
||||
'qty' => $consumable->qty,
|
||||
'category_id' => $consumable->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $consumable->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Consumable name (required)'),
|
||||
'qty' => $schema->number()->description('Total quantity in stock (required)'),
|
||||
'category_id' => $schema->number()->description('Category ID — must be a consumable category (required)'),
|
||||
'company_id' => $schema->number()->description('Company ID'),
|
||||
'location_id' => $schema->number()->description('Location ID'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'item_no' => $schema->string()->description('Item number'),
|
||||
'order_number' => $schema->string()->description('Order number'),
|
||||
'model_number' => $schema->string()->description('Model number'),
|
||||
'purchase_cost' => $schema->number()->description('Purchase cost per unit'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'min_amt' => $schema->number()->description('Minimum quantity threshold for alerts'),
|
||||
'requestable' => $schema->boolean()->description('Whether users can request this consumable'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the consumable was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new consumable'),
|
||||
'name' => $schema->string()->description('Name of the new consumable'),
|
||||
'qty' => $schema->number()->description('Total quantity'),
|
||||
'category_id' => $schema->number()->description('Category ID'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Department;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_department')]
|
||||
#[Title('Create Department')]
|
||||
#[Description('Create a new Snipe-IT department')]
|
||||
class CreateDepartmentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Department::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'manager_id' => 'nullable|integer|exists:users,id',
|
||||
'phone' => 'nullable|string|max:255',
|
||||
'fax' => 'nullable|string|max:255',
|
||||
'notes' => 'nullable|string|max:255',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$department = new Department;
|
||||
$department->fill($request->only([
|
||||
'name', 'location_id', 'manager_id', 'phone', 'fax', 'notes',
|
||||
]));
|
||||
|
||||
$department->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$department->created_by = auth()->id();
|
||||
|
||||
if ($department->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.department_created', ['name' => $department->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.department_created', ['name' => $department->name]),
|
||||
'id' => $department->id,
|
||||
'name' => $department->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $department->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Department name (required)'),
|
||||
'location_id' => $schema->number()->description('Location ID'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'manager_id' => $schema->number()->description('User ID of the department manager'),
|
||||
'phone' => $schema->string()->description('Department phone number'),
|
||||
'fax' => $schema->string()->description('Department fax number'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the department was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new department'),
|
||||
'name' => $schema->string()->description('Name of the new department'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Depreciation;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_depreciation')]
|
||||
#[Title('Create Depreciation')]
|
||||
#[Description('Create a new Snipe-IT depreciation schedule')]
|
||||
class CreateDepreciationTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Depreciation::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'months' => 'required|integer|min:1|max:3600',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$depreciation = new Depreciation;
|
||||
$depreciation->name = $request->get('name');
|
||||
$depreciation->months = $request->get('months');
|
||||
|
||||
if ($depreciation->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.depreciation_created', ['name' => $depreciation->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.depreciation_created', ['name' => $depreciation->name]),
|
||||
'id' => $depreciation->id,
|
||||
'name' => $depreciation->name,
|
||||
'months' => $depreciation->months,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $depreciation->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Depreciation name (required)'),
|
||||
'months' => $schema->number()->description('Depreciation period in months (required, 1-3600)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the depreciation was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new depreciation'),
|
||||
'name' => $schema->string()->description('Name of the new depreciation'),
|
||||
'months' => $schema->number()->description('Depreciation period in months'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Group;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_group')]
|
||||
#[Title('Create Group')]
|
||||
#[Description('Create a new Snipe-IT permission group. Requires superadmin. Permissions are a JSON object mapping permission keys to 1 (grant) or -1 (deny).')]
|
||||
class CreateGroupTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('superadmin')) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'permissions' => 'nullable|string',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$permissions = null;
|
||||
if ($request->filled('permissions')) {
|
||||
$result = $this->parseAndValidatePermissions($request->get('permissions'));
|
||||
if (is_string($result)) {
|
||||
return Response::make(Response::error($result));
|
||||
}
|
||||
$permissions = $result;
|
||||
}
|
||||
|
||||
$group = new Group;
|
||||
$group->name = $request->get('name');
|
||||
if ($permissions !== null) {
|
||||
$group->permissions = json_encode($permissions);
|
||||
}
|
||||
if ($request->filled('notes')) {
|
||||
$group->notes = $request->get('notes');
|
||||
}
|
||||
$group->created_by = auth()->id();
|
||||
|
||||
if ($group->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.group_created', ['name' => $group->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.group_created', ['name' => $group->name]),
|
||||
'id' => $group->id,
|
||||
'name' => $group->name,
|
||||
'permissions' => $group->decodePermissions(),
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $group->getErrors()->first()])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a JSON permissions string and validate all keys against config('permissions').
|
||||
* Returns the decoded array on success, or an error string on failure.
|
||||
*/
|
||||
private function parseAndValidatePermissions(string $raw): array|string
|
||||
{
|
||||
$decoded = json_decode($raw, true);
|
||||
if (! is_array($decoded)) {
|
||||
return trans('mcp.invalid_permissions_format');
|
||||
}
|
||||
|
||||
$validKeys = collect(config('permissions'))
|
||||
->flatMap(fn ($perms) => collect($perms)->pluck('permission'))
|
||||
->unique()
|
||||
->flip()
|
||||
->all();
|
||||
|
||||
foreach (array_keys($decoded) as $key) {
|
||||
if (! isset($validKeys[$key])) {
|
||||
return trans('mcp.invalid_permission_key', ['key' => $key]);
|
||||
}
|
||||
if (! in_array((int) $decoded[$key], [1, -1], true)) {
|
||||
return trans('mcp.invalid_permission_value', ['key' => $key]);
|
||||
}
|
||||
}
|
||||
|
||||
return array_map('intval', $decoded);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Group name (required, must be unique)'),
|
||||
'permissions' => $schema->string()->description(
|
||||
'JSON object mapping permission keys to 1 (grant) or -1 (deny). '.
|
||||
'Valid keys include: superuser, admin, import, reports.view, '.
|
||||
'assets.view, assets.create, assets.edit, assets.delete, assets.checkout, assets.checkin, assets.audit, '.
|
||||
'users.view, users.create, users.edit, users.delete, '.
|
||||
'licenses.view, licenses.create, licenses.edit, licenses.delete, licenses.checkout, licenses.checkin, '.
|
||||
'accessories.view, accessories.create, accessories.edit, accessories.delete, accessories.checkout, accessories.checkin, '.
|
||||
'components.view, components.create, components.edit, components.delete, components.checkout, components.checkin, '.
|
||||
'consumables.view, consumables.create, consumables.edit, consumables.delete, consumables.checkout, '.
|
||||
'and many more. Example: {"assets.view":1,"assets.create":1,"assets.edit":-1}'
|
||||
),
|
||||
'notes' => $schema->string()->description('Notes about the group'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the group was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new group'),
|
||||
'name' => $schema->string()->description('Name of the new group'),
|
||||
'permissions' => $schema->object()->description('Permissions set on the group'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\License;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_license')]
|
||||
#[Title('Create License')]
|
||||
#[Description('Create a new Snipe-IT software license')]
|
||||
class CreateLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', License::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'seats' => 'required|integer|min:1',
|
||||
'category_id' => 'required|integer|exists:categories,id',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'purchase_date' => 'nullable|date_format:Y-m-d',
|
||||
'purchase_cost' => 'nullable|numeric|min:0',
|
||||
'purchase_order' => 'nullable|string|max:255',
|
||||
'order_number' => 'nullable|string|max:255',
|
||||
'expiration_date' => 'nullable|date_format:Y-m-d',
|
||||
'termination_date' => 'nullable|date_format:Y-m-d',
|
||||
'license_name' => 'nullable|string|max:255',
|
||||
'license_email' => 'nullable|email|max:255',
|
||||
'maintained' => 'nullable|boolean',
|
||||
'reassignable' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string',
|
||||
'min_amt' => 'nullable|integer|min:0',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$license = new License;
|
||||
$license->fill($request->only([
|
||||
'name', 'seats', 'category_id', 'serial', 'manufacturer_id',
|
||||
'supplier_id', 'purchase_date', 'purchase_cost', 'purchase_order',
|
||||
'order_number', 'expiration_date', 'termination_date',
|
||||
'license_name', 'license_email', 'maintained', 'reassignable',
|
||||
'notes', 'min_amt',
|
||||
]));
|
||||
|
||||
$license->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$license->created_by = auth()->id();
|
||||
|
||||
if ($license->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_created', ['name' => $license->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_created', ['name' => $license->name]),
|
||||
'id' => $license->id,
|
||||
'name' => $license->name,
|
||||
'seats' => $license->seats,
|
||||
'category_id' => $license->category_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $license->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('License name (required)'),
|
||||
'seats' => $schema->number()->description('Number of seats (required, min 1)'),
|
||||
'category_id' => $schema->number()->description('Category ID — must be a license category (required)'),
|
||||
'serial' => $schema->string()->description('Product key / serial number'),
|
||||
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
|
||||
'purchase_cost' => $schema->number()->description('Purchase cost'),
|
||||
'purchase_order' => $schema->string()->description('Purchase order number'),
|
||||
'order_number' => $schema->string()->description('Order number'),
|
||||
'expiration_date' => $schema->string()->description('License expiration date (YYYY-MM-DD)'),
|
||||
'termination_date' => $schema->string()->description('License termination date (YYYY-MM-DD)'),
|
||||
'license_name' => $schema->string()->description('Name of the licensed user/organization'),
|
||||
'license_email' => $schema->string()->description('Email of the licensed user/organization'),
|
||||
'maintained' => $schema->boolean()->description('Whether the license is under maintenance'),
|
||||
'reassignable' => $schema->boolean()->description('Whether seats can be reassigned after checkin'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'min_amt' => $schema->number()->description('Minimum seat threshold for alerts'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the license was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new license'),
|
||||
'name' => $schema->string()->description('Name of the new license'),
|
||||
'seats' => $schema->number()->description('Total seat count'),
|
||||
'category_id' => $schema->number()->description('Category ID'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Location;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_location')]
|
||||
#[Title('Create Location')]
|
||||
#[Description('Create a new Snipe-IT location')]
|
||||
class CreateLocationTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Location::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'address' => 'nullable|string',
|
||||
'address2' => 'nullable|string',
|
||||
'city' => 'nullable|string',
|
||||
'state' => 'nullable|string',
|
||||
'country' => 'nullable|string',
|
||||
'zip' => 'nullable|string',
|
||||
'phone' => 'nullable|string|max:255',
|
||||
'fax' => 'nullable|string|max:255',
|
||||
'currency' => 'nullable|string',
|
||||
'parent_id' => 'nullable|integer|exists:locations,id',
|
||||
'manager_id' => 'nullable|integer|exists:users,id',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$location = new Location;
|
||||
$location->name = $request->get('name');
|
||||
|
||||
foreach (['address', 'address2', 'city', 'state', 'country', 'zip', 'phone', 'fax', 'currency', 'parent_id', 'manager_id'] as $field) {
|
||||
if ($request->filled($field)) {
|
||||
$location->{$field} = $request->get($field);
|
||||
}
|
||||
}
|
||||
|
||||
if ($location->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.location_created', ['name' => $location->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.location_created', ['name' => $location->name]),
|
||||
'id' => $location->id,
|
||||
'name' => $location->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $location->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Location name (required)'),
|
||||
'address' => $schema->string()->description('Street address'),
|
||||
'address2' => $schema->string()->description('Address line 2'),
|
||||
'city' => $schema->string()->description('City'),
|
||||
'state' => $schema->string()->description('State'),
|
||||
'country' => $schema->string()->description('Country'),
|
||||
'zip' => $schema->string()->description('Zip code'),
|
||||
'phone' => $schema->string()->description('Phone number'),
|
||||
'fax' => $schema->string()->description('Fax number'),
|
||||
'currency' => $schema->string()->description('Currency code'),
|
||||
'parent_id' => $schema->number()->description('Parent location ID'),
|
||||
'manager_id' => $schema->number()->description('Manager user ID'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the location was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new location'),
|
||||
'name' => $schema->string()->description('Name of the new location'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Maintenance;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_maintenance')]
|
||||
#[Title('Create Maintenance')]
|
||||
#[Description('Create a new asset maintenance record')]
|
||||
class CreateMaintenanceTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('update', Asset::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'asset_id' => 'required|integer|exists:assets,id',
|
||||
'title' => 'required|string|max:255',
|
||||
'asset_maintenance_type' => 'nullable|string|max:255',
|
||||
'supplier_id' => 'nullable|integer|exists:suppliers,id',
|
||||
'is_warranty' => 'nullable|boolean',
|
||||
'cost' => 'nullable|numeric|min:0',
|
||||
'start_date' => 'nullable|date_format:Y-m-d',
|
||||
'completion_date' => 'nullable|date_format:Y-m-d',
|
||||
'notes' => 'nullable|string',
|
||||
'user_id' => 'nullable|integer|exists:users,id',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$maintenance = new Maintenance;
|
||||
$maintenance->asset_id = $request->get('asset_id');
|
||||
$maintenance->name = $request->get('title');
|
||||
$maintenance->asset_maintenance_type = $request->get('asset_maintenance_type', 'Maintenance');
|
||||
$maintenance->start_date = $request->filled('start_date') ? $request->get('start_date') : now()->format('Y-m-d');
|
||||
$maintenance->created_by = auth()->id();
|
||||
$maintenance->is_warranty = 0;
|
||||
|
||||
foreach (['supplier_id', 'is_warranty', 'cost', 'completion_date', 'notes', 'user_id'] as $field) {
|
||||
if ($request->filled($field)) {
|
||||
$maintenance->{$field} = $request->get($field);
|
||||
}
|
||||
}
|
||||
|
||||
if ($maintenance->save()) {
|
||||
$maintenance->load('asset');
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.maintenance_created', ['name' => $maintenance->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.maintenance_created', ['name' => $maintenance->name]),
|
||||
'id' => $maintenance->id,
|
||||
'title' => $maintenance->name,
|
||||
'asset_id' => $maintenance->asset_id,
|
||||
'asset_tag' => $maintenance->asset?->asset_tag,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $maintenance->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_id' => $schema->number()->description('Asset ID the maintenance is for (required)'),
|
||||
'title' => $schema->string()->description('Maintenance title/name (required)'),
|
||||
'asset_maintenance_type' => $schema->string()->description('Type of maintenance (e.g. maintenance, repair, upgrade)'),
|
||||
'supplier_id' => $schema->number()->description('Supplier ID'),
|
||||
'is_warranty' => $schema->boolean()->description('Whether this is a warranty maintenance'),
|
||||
'cost' => $schema->number()->description('Cost of the maintenance'),
|
||||
'start_date' => $schema->string()->description('Start date (YYYY-MM-DD, defaults to today)'),
|
||||
'completion_date' => $schema->string()->description('Completion date (YYYY-MM-DD)'),
|
||||
'notes' => $schema->string()->description('Notes about the maintenance'),
|
||||
'user_id' => $schema->number()->description('Technician user ID'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the maintenance was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new maintenance record'),
|
||||
'title' => $schema->string()->description('Title of the maintenance'),
|
||||
'asset_id' => $schema->number()->description('Asset ID'),
|
||||
'asset_tag' => $schema->string()->description('Asset tag'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Manufacturer;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_manufacturer')]
|
||||
#[Title('Create Manufacturer')]
|
||||
#[Description('Create a new Snipe-IT manufacturer')]
|
||||
class CreateManufacturerTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Manufacturer::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'url' => 'nullable|string|max:255',
|
||||
'support_url' => 'nullable|string|max:255',
|
||||
'support_email' => 'nullable|email|max:191',
|
||||
'support_phone' => 'nullable|string|max:191',
|
||||
'warranty_lookup_url' => 'nullable|string|max:255',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$manufacturer = new Manufacturer;
|
||||
$manufacturer->fill($request->only([
|
||||
'name', 'url', 'support_url', 'support_email', 'support_phone', 'warranty_lookup_url', 'notes',
|
||||
]));
|
||||
|
||||
if ($manufacturer->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.manufacturer_created', ['name' => $manufacturer->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.manufacturer_created', ['name' => $manufacturer->name]),
|
||||
'id' => $manufacturer->id,
|
||||
'name' => $manufacturer->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $manufacturer->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Manufacturer name (required)'),
|
||||
'url' => $schema->string()->description('Manufacturer website URL'),
|
||||
'support_url' => $schema->string()->description('Support website URL'),
|
||||
'support_email' => $schema->string()->description('Support email address'),
|
||||
'support_phone' => $schema->string()->description('Support phone number'),
|
||||
'warranty_lookup_url' => $schema->string()->description('Warranty lookup URL'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the manufacturer was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new manufacturer'),
|
||||
'name' => $schema->string()->description('Name of the new manufacturer'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Statuslabel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_status_label')]
|
||||
#[Title('Create Status Label')]
|
||||
#[Description('Create a new Snipe-IT status label')]
|
||||
class CreateStatusLabelTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Statuslabel::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|string|in:deployable,pending,archived,undeployable',
|
||||
'color' => 'nullable|string',
|
||||
'notes' => 'nullable|string',
|
||||
'default_label' => 'nullable|boolean',
|
||||
'show_in_nav' => 'nullable|boolean',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$statuslabel = new Statuslabel;
|
||||
$statuslabel->name = $request->get('name');
|
||||
|
||||
$statusType = Statuslabel::getStatuslabelTypesForDB($request->get('type'));
|
||||
$statuslabel->deployable = $statusType['deployable'];
|
||||
$statuslabel->pending = $statusType['pending'];
|
||||
$statuslabel->archived = $statusType['archived'];
|
||||
|
||||
if ($request->filled('color')) {
|
||||
$statuslabel->color = $request->get('color');
|
||||
}
|
||||
if ($request->filled('notes')) {
|
||||
$statuslabel->notes = $request->get('notes');
|
||||
}
|
||||
$statuslabel->default_label = $request->get('default_label', 0);
|
||||
$statuslabel->show_in_nav = $request->get('show_in_nav', 0);
|
||||
|
||||
if ($statuslabel->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.status_label_created', ['name' => $statuslabel->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.status_label_created', ['name' => $statuslabel->name]),
|
||||
'id' => $statuslabel->id,
|
||||
'name' => $statuslabel->name,
|
||||
'type' => $statuslabel->getStatuslabelType(),
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $statuslabel->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Status label name (required)'),
|
||||
'type' => $schema->string()->description('Status label type: deployable, pending, archived, or undeployable (required)'),
|
||||
'color' => $schema->string()->description('Display color in #RRGGBB format'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'default_label' => $schema->boolean()->description('Whether this is the default label'),
|
||||
'show_in_nav' => $schema->boolean()->description('Whether to show in navigation'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the status label was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new status label'),
|
||||
'name' => $schema->string()->description('Name of the new status label'),
|
||||
'type' => $schema->string()->description('Type of the new status label'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Supplier;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_supplier')]
|
||||
#[Title('Create Supplier')]
|
||||
#[Description('Create a new Snipe-IT supplier')]
|
||||
class CreateSupplierTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', Supplier::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'address' => 'nullable|string',
|
||||
'address2' => 'nullable|string',
|
||||
'city' => 'nullable|string',
|
||||
'state' => 'nullable|string',
|
||||
'country' => 'nullable|string',
|
||||
'zip' => 'nullable|string',
|
||||
'phone' => 'nullable|string',
|
||||
'fax' => 'nullable|string',
|
||||
'email' => 'nullable|email',
|
||||
'url' => 'nullable|string',
|
||||
'contact' => 'nullable|string',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
$supplier = new Supplier;
|
||||
$supplier->fill($request->only([
|
||||
'name', 'address', 'address2', 'city', 'state', 'country', 'zip',
|
||||
'phone', 'fax', 'email', 'url', 'contact', 'notes',
|
||||
]));
|
||||
|
||||
if ($supplier->save()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.supplier_created', ['name' => $supplier->name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.supplier_created', ['name' => $supplier->name]),
|
||||
'id' => $supplier->id,
|
||||
'name' => $supplier->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $supplier->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'name' => $schema->string()->description('Supplier name (required)'),
|
||||
'address' => $schema->string()->description('Address line 1'),
|
||||
'address2' => $schema->string()->description('Address line 2'),
|
||||
'city' => $schema->string()->description('City'),
|
||||
'state' => $schema->string()->description('State'),
|
||||
'country' => $schema->string()->description('Country'),
|
||||
'zip' => $schema->string()->description('Postal code'),
|
||||
'phone' => $schema->string()->description('Phone number'),
|
||||
'fax' => $schema->string()->description('Fax number'),
|
||||
'email' => $schema->string()->description('Email address'),
|
||||
'url' => $schema->string()->description('Website URL'),
|
||||
'contact' => $schema->string()->description('Contact name'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the supplier was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new supplier'),
|
||||
'name' => $schema->string()->description('Name of the new supplier'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Group;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('create_user')]
|
||||
#[Title('Create User')]
|
||||
#[Description('Create a new Snipe-IT user account')]
|
||||
class CreateUserTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('create', User::class)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'first_name' => 'required|string|max:191',
|
||||
'last_name' => 'nullable|string|max:191',
|
||||
'username' => 'required|string|max:191',
|
||||
'email' => 'nullable|email|max:191',
|
||||
'password' => 'nullable|string|min:8',
|
||||
'employee_num' => 'nullable|string|max:191',
|
||||
'jobtitle' => 'nullable|string|max:191',
|
||||
'phone' => 'nullable|string|max:35',
|
||||
'mobile' => 'nullable|string|max:35',
|
||||
'company_id' => 'nullable|integer|exists:companies,id',
|
||||
'department_id' => 'nullable|integer|exists:departments,id',
|
||||
'location_id' => 'nullable|integer|exists:locations,id',
|
||||
'manager_id' => 'nullable|integer|exists:users,id',
|
||||
'activated' => 'nullable|boolean',
|
||||
'notes' => 'nullable|string',
|
||||
'start_date' => 'nullable|date_format:Y-m-d',
|
||||
'end_date' => 'nullable|date_format:Y-m-d',
|
||||
'vip' => 'nullable|boolean',
|
||||
'remote' => 'nullable|boolean',
|
||||
'website' => 'nullable|url|max:191',
|
||||
'address' => 'nullable|string|max:191',
|
||||
'city' => 'nullable|string|max:191',
|
||||
'state' => 'nullable|string|max:191',
|
||||
'country' => 'nullable|string|max:191',
|
||||
'zip' => 'nullable|string|max:10',
|
||||
'group_ids' => 'nullable|array',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return Response::make(Response::error($e->validator->errors()->first()));
|
||||
}
|
||||
|
||||
if (User::where('username', $request->get('username'))->exists()) {
|
||||
return Response::make(Response::error(trans('mcp.username_taken', ['username' => $request->get('username')])));
|
||||
}
|
||||
|
||||
$user = new User;
|
||||
$user->fill($request->only([
|
||||
'first_name', 'last_name', 'username', 'email', 'employee_num',
|
||||
'jobtitle', 'phone', 'mobile', 'department_id', 'location_id',
|
||||
'manager_id', 'notes', 'start_date', 'end_date', 'vip', 'remote',
|
||||
'website', 'address', 'city', 'state', 'country', 'zip',
|
||||
]));
|
||||
|
||||
$user->activated = $request->filled('activated') ? (bool) $request->get('activated') : true;
|
||||
$user->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$user->created_by = auth()->id();
|
||||
|
||||
if ($request->filled('password')) {
|
||||
$user->password = bcrypt($request->get('password'));
|
||||
} else {
|
||||
$user->password = $user->noPassword();
|
||||
}
|
||||
|
||||
if ($user->save()) {
|
||||
$groupIds = [];
|
||||
if ($request->filled('group_ids') && auth()->user()->isSuperUser()) {
|
||||
$groupIds = Group::whereIn('id', $request->get('group_ids'))->pluck('id')->all();
|
||||
$user->groups()->sync($groupIds);
|
||||
} elseif ($request->filled('group_ids')) {
|
||||
return Response::make(Response::error(trans('mcp.superadmin_required_for_groups')));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.user_created', ['username' => $user->username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.user_created', ['username' => $user->username]),
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'email' => $user->email,
|
||||
'first_name' => $user->first_name,
|
||||
'last_name' => $user->last_name,
|
||||
'group_ids' => $groupIds,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.create_failed', ['error' => $user->getErrors()->first()])));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'first_name' => $schema->string()->description('First name (required)'),
|
||||
'last_name' => $schema->string()->description('Last name'),
|
||||
'username' => $schema->string()->description('Username (required, must be unique)'),
|
||||
'email' => $schema->string()->description('Email address'),
|
||||
'password' => $schema->string()->description('Password (min 8 characters; if omitted, account will have no password set)'),
|
||||
'employee_num' => $schema->string()->description('Employee number'),
|
||||
'jobtitle' => $schema->string()->description('Job title'),
|
||||
'phone' => $schema->string()->description('Phone number'),
|
||||
'mobile' => $schema->string()->description('Mobile number'),
|
||||
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
|
||||
'department_id' => $schema->number()->description('Department ID'),
|
||||
'location_id' => $schema->number()->description('Location ID'),
|
||||
'manager_id' => $schema->number()->description('Manager user ID'),
|
||||
'activated' => $schema->boolean()->description('Whether the account is active (default: true)'),
|
||||
'notes' => $schema->string()->description('Notes'),
|
||||
'start_date' => $schema->string()->description('Employment start date (YYYY-MM-DD)'),
|
||||
'end_date' => $schema->string()->description('Employment end date (YYYY-MM-DD)'),
|
||||
'vip' => $schema->boolean()->description('Mark user as VIP'),
|
||||
'remote' => $schema->boolean()->description('Mark user as remote'),
|
||||
'website' => $schema->string()->description('Website URL'),
|
||||
'address' => $schema->string()->description('Street address'),
|
||||
'city' => $schema->string()->description('City'),
|
||||
'state' => $schema->string()->description('State/province'),
|
||||
'country' => $schema->string()->description('Country'),
|
||||
'zip' => $schema->string()->description('Postal/ZIP code'),
|
||||
'group_ids' => $schema->array()->description('Array of permission group IDs to assign (requires superadmin). Example: [1, 3]'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the user was created'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'id' => $schema->number()->description('Numeric ID of the new user'),
|
||||
'username' => $schema->string()->description('Username of the new user'),
|
||||
'email' => $schema->string()->description('Email of the new user'),
|
||||
'first_name' => $schema->string()->description('First name'),
|
||||
'last_name' => $schema->string()->description('Last name'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_accessory')]
|
||||
#[Title('Delete Accessory')]
|
||||
#[Description('Soft-delete a Snipe-IT accessory. The accessory must have no units currently checked out.')]
|
||||
class DeleteAccessoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$accessory = $this->resolveAccessory($request);
|
||||
|
||||
if (! $accessory) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $accessory)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($accessory->numCheckedOut() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.accessory_has_checkouts')));
|
||||
}
|
||||
|
||||
$name = $accessory->name;
|
||||
|
||||
$accessory->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.accessory_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.accessory_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveAccessory(Request $request): ?Accessory
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Accessory::withCount('checkouts as checkouts_count')->find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Accessory::withCount('checkouts as checkouts_count')->where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the accessory to delete'),
|
||||
'name' => $schema->string()->description('Name of the accessory to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted accessory'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\AssetModel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_asset_model')]
|
||||
#[Title('Delete Asset Model')]
|
||||
#[Description('Soft-delete a Snipe-IT asset model by numeric ID or name')]
|
||||
class DeleteAssetModelTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$model = $this->resolveModel($request);
|
||||
|
||||
if (! $model) {
|
||||
return Response::make(Response::error(trans('mcp.asset_model_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $model)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($model->assets()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.model_has_assets')));
|
||||
}
|
||||
|
||||
$name = $model->name;
|
||||
|
||||
$model->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_model_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_model_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveModel(Request $request): ?AssetModel
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return AssetModel::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return AssetModel::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the asset model to delete'),
|
||||
'name' => $schema->string()->description('Name of the asset model to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted asset model'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_asset')]
|
||||
#[Title('Delete Asset')]
|
||||
#[Description('Soft-delete a Snipe-IT asset. If the asset is currently checked out it will be checked in first.')]
|
||||
class DeleteAssetTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'asset_tag' => 'nullable|max:100',
|
||||
'serial' => 'nullable|string|max:255',
|
||||
'id' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
$asset = $this->resolveAsset($request);
|
||||
|
||||
if (! $asset) {
|
||||
return Response::make(Response::error(trans('mcp.asset_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $asset)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$assetTag = $asset->asset_tag;
|
||||
|
||||
if ($asset->assignedTo) {
|
||||
$target = $asset->assignedTo;
|
||||
$originalValues = $asset->getRawOriginal();
|
||||
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checked in on delete', date('Y-m-d H:i:s'), $originalValues));
|
||||
DB::table('assets')->where('id', $asset->id)->update(['assigned_to' => null]);
|
||||
}
|
||||
|
||||
$asset->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.asset_deleted', ['asset_tag' => $assetTag]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.asset_deleted', ['asset_tag' => $assetTag]),
|
||||
'asset_tag' => $assetTag,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveAsset(Request $request): ?Asset
|
||||
{
|
||||
if ($request->filled('asset_tag')) {
|
||||
return Asset::where('asset_tag', $request->get('asset_tag'))->first();
|
||||
}
|
||||
if ($request->filled('serial')) {
|
||||
return Asset::where('serial', $request->get('serial'))->first();
|
||||
}
|
||||
if ($request->filled('id')) {
|
||||
return Asset::find($request->get('id'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the asset to delete'),
|
||||
'serial' => $schema->string()->description('Serial number of the asset to delete'),
|
||||
'id' => $schema->number()->description('Numeric ID of the asset to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'error' => $schema->boolean()->description('True if the deletion failed'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'asset_tag' => $schema->string()->description('Asset tag of the deleted asset'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_category')]
|
||||
#[Title('Delete Category')]
|
||||
#[Description('Soft-delete a Snipe-IT category. The category must have no items assigned to it.')]
|
||||
class DeleteCategoryTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$category = $this->resolveCategory($request);
|
||||
|
||||
if (! $category) {
|
||||
return Response::make(Response::error(trans('mcp.category_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $category)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $category->name;
|
||||
|
||||
try {
|
||||
$category->delete();
|
||||
} catch (\Exception $e) {
|
||||
return Response::make(Response::error(trans('mcp.category_delete_failed', ['error' => $e->getMessage()])));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.category_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.category_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveCategory(Request $request): ?Category
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Category::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Category::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the category to delete'),
|
||||
'name' => $schema->string()->description('Name of the category to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted category'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Company;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_company')]
|
||||
#[Title('Delete Company')]
|
||||
#[Description('Soft-delete a Snipe-IT company by numeric ID or name')]
|
||||
class DeleteCompanyTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$company = $this->resolveCompany($request);
|
||||
|
||||
if (! $company) {
|
||||
return Response::make(Response::error(trans('mcp.company_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $company)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $company->name;
|
||||
|
||||
$company->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.company_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.company_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveCompany(Request $request): ?Company
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Company::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Company::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the company to delete'),
|
||||
'name' => $schema->string()->description('Name of the company to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted company'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Component;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_component')]
|
||||
#[Title('Delete Component')]
|
||||
#[Description('Soft-delete a Snipe-IT component. The component must have no units currently checked out to assets.')]
|
||||
class DeleteComponentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:191',
|
||||
]);
|
||||
|
||||
$component = $this->resolveComponent($request);
|
||||
|
||||
if (! $component) {
|
||||
return Response::make(Response::error(trans('mcp.component_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $component)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($component->numCheckedOut() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.component_has_checkouts')));
|
||||
}
|
||||
|
||||
$name = $component->name;
|
||||
|
||||
$component->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.component_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.component_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveComponent(Request $request): ?Component
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Component::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Component::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the component to delete'),
|
||||
'name' => $schema->string()->description('Name of the component to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted component'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Consumable;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_consumable')]
|
||||
#[Title('Delete Consumable')]
|
||||
#[Description('Soft-delete a Snipe-IT consumable. The consumable must have no units currently checked out.')]
|
||||
class DeleteConsumableTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$consumable = $this->resolveConsumable($request);
|
||||
|
||||
if (! $consumable) {
|
||||
return Response::make(Response::error(trans('mcp.consumable_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $consumable)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($consumable->users()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.consumable_has_checkouts')));
|
||||
}
|
||||
|
||||
$name = $consumable->name;
|
||||
|
||||
$consumable->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.consumable_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.consumable_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveConsumable(Request $request): ?Consumable
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Consumable::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Consumable::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the consumable to delete'),
|
||||
'name' => $schema->string()->description('Name of the consumable to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted consumable'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Department;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_department')]
|
||||
#[Title('Delete Department')]
|
||||
#[Description('Soft-delete a Snipe-IT department. The department must have no users assigned to it.')]
|
||||
class DeleteDepartmentTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$department = $this->resolveDepartment($request);
|
||||
|
||||
if (! $department) {
|
||||
return Response::make(Response::error(trans('mcp.department_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $department)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($department->users->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.department_has_users')));
|
||||
}
|
||||
|
||||
$name = $department->name;
|
||||
|
||||
$department->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.department_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.department_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveDepartment(Request $request): ?Department
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Department::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Department::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the department to delete'),
|
||||
'name' => $schema->string()->description('Name of the department to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted department'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Depreciation;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_depreciation')]
|
||||
#[Title('Delete Depreciation')]
|
||||
#[Description('Soft-delete a Snipe-IT depreciation schedule by numeric ID or name')]
|
||||
class DeleteDepreciationTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$dep = $this->resolveDepreciation($request);
|
||||
|
||||
if (! $dep) {
|
||||
return Response::make(Response::error(trans('mcp.depreciation_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $dep)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $dep->name;
|
||||
|
||||
$dep->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.depreciation_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.depreciation_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveDepreciation(Request $request): ?Depreciation
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Depreciation::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Depreciation::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the depreciation to delete'),
|
||||
'name' => $schema->string()->description('Name of the depreciation to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted depreciation'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Group;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_group')]
|
||||
#[Title('Delete Group')]
|
||||
#[Description('Delete a Snipe-IT permission group by ID or name. The group must have no users assigned.')]
|
||||
class DeleteGroupTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('superadmin')) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
if ($request->filled('id')) {
|
||||
$group = Group::find($request->get('id'));
|
||||
} elseif ($request->filled('name')) {
|
||||
$group = Group::where('name', $request->get('name'))->first();
|
||||
} else {
|
||||
return Response::make(Response::error(trans('mcp.id_or_name_required')));
|
||||
}
|
||||
|
||||
if (! $group) {
|
||||
return Response::make(Response::error(trans('mcp.group_not_found')));
|
||||
}
|
||||
|
||||
$groupName = $group->name;
|
||||
|
||||
if ($group->delete()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.group_deleted', ['name' => $groupName]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.group_deleted', ['name' => $groupName]),
|
||||
'name' => $groupName,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.delete_failed')));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric group ID to delete'),
|
||||
'name' => $schema->string()->description('Group name to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted group'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\License;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_license')]
|
||||
#[Title('Delete License')]
|
||||
#[Description('Soft-delete a Snipe-IT license. The license must have no seats currently assigned to users or assets.')]
|
||||
class DeleteLicenseTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$license = $this->resolveLicense($request);
|
||||
|
||||
if (! $license) {
|
||||
return Response::make(Response::error(trans('mcp.license_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $license)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($license->assignedCount()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.license_has_seats_assigned')));
|
||||
}
|
||||
|
||||
$name = $license->name;
|
||||
|
||||
DB::table('license_seats')
|
||||
->where('license_id', $license->id)
|
||||
->update(['assigned_to' => null, 'asset_id' => null]);
|
||||
|
||||
$license->licenseseats()->delete();
|
||||
$license->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.license_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.license_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveLicense(Request $request): ?License
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return License::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return License::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the license to delete'),
|
||||
'name' => $schema->string()->description('Name of the license to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted license'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Location;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_location')]
|
||||
#[Title('Delete Location')]
|
||||
#[Description('Soft-delete a Snipe-IT location by numeric ID or name')]
|
||||
class DeleteLocationTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$location = $this->resolveLocation($request);
|
||||
|
||||
if (! $location) {
|
||||
return Response::make(Response::error(trans('mcp.location_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $location)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($location->users()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.location_has_users')));
|
||||
}
|
||||
|
||||
if ($location->children()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.location_has_child_locations')));
|
||||
}
|
||||
|
||||
$name = $location->name;
|
||||
|
||||
$location->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.location_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.location_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveLocation(Request $request): ?Location
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Location::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Location::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the location to delete'),
|
||||
'name' => $schema->string()->description('Name of the location to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted location'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Manufacturer;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_manufacturer')]
|
||||
#[Title('Delete Manufacturer')]
|
||||
#[Description('Soft-delete a Snipe-IT manufacturer identified by numeric ID or name')]
|
||||
class DeleteManufacturerTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$manufacturer = $this->resolveManufacturer($request);
|
||||
|
||||
if (! $manufacturer) {
|
||||
return Response::make(Response::error(trans('mcp.manufacturer_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $manufacturer)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $manufacturer->name;
|
||||
|
||||
$manufacturer->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.manufacturer_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.manufacturer_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveManufacturer(Request $request): ?Manufacturer
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Manufacturer::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Manufacturer::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the manufacturer to delete'),
|
||||
'name' => $schema->string()->description('Name of the manufacturer to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted manufacturer'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Statuslabel;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_status_label')]
|
||||
#[Title('Delete Status Label')]
|
||||
#[Description('Soft-delete a Snipe-IT status label identified by numeric ID or name')]
|
||||
class DeleteStatusLabelTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$label = $this->resolveStatusLabel($request);
|
||||
|
||||
if (! $label) {
|
||||
return Response::make(Response::error(trans('mcp.status_label_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $label)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($label->assets()->count() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.status_label_has_assets')));
|
||||
}
|
||||
|
||||
$name = $label->name;
|
||||
|
||||
$label->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.status_label_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.status_label_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveStatusLabel(Request $request): ?Statuslabel
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Statuslabel::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Statuslabel::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the status label to delete'),
|
||||
'name' => $schema->string()->description('Name of the status label to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted status label'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Supplier;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_supplier')]
|
||||
#[Title('Delete Supplier')]
|
||||
#[Description('Soft-delete a Snipe-IT supplier identified by numeric ID or name')]
|
||||
class DeleteSupplierTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'name' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$supplier = $this->resolveSupplier($request);
|
||||
|
||||
if (! $supplier) {
|
||||
return Response::make(Response::error(trans('mcp.supplier_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $supplier)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$name = $supplier->name;
|
||||
|
||||
$supplier->delete();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.supplier_deleted', ['name' => $name]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.supplier_deleted', ['name' => $name]),
|
||||
'name' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveSupplier(Request $request): ?Supplier
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return Supplier::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('name')) {
|
||||
return Supplier::where('name', $request->get('name'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric ID of the supplier to delete'),
|
||||
'name' => $schema->string()->description('Name of the supplier to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'name' => $schema->string()->description('Name of the deleted supplier'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('delete_user')]
|
||||
#[Title('Delete User')]
|
||||
#[Description('Soft-delete a Snipe-IT user. The user must have no assets, licenses, accessories, or consumables assigned.')]
|
||||
class DeleteUserTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|integer',
|
||||
'username' => 'nullable|string|max:191',
|
||||
'email' => 'nullable|string|max:191',
|
||||
]);
|
||||
|
||||
$user = $this->resolveUser($request);
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.user_not_found')));
|
||||
}
|
||||
|
||||
if (! Gate::allows('delete', $user)) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
if ($user->id === auth()->id()) {
|
||||
return Response::make(Response::error(trans('mcp.user_cannot_delete_self')));
|
||||
}
|
||||
|
||||
if ($user->allAssignedCount() > 0) {
|
||||
return Response::make(Response::error(trans('mcp.user_has_items')));
|
||||
}
|
||||
|
||||
$username = $user->username;
|
||||
|
||||
if ($user->delete()) {
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.user_deleted', ['username' => $username]))
|
||||
)->withStructuredContent([
|
||||
'success' => true,
|
||||
'message' => trans('mcp.user_deleted', ['username' => $username]),
|
||||
'username' => $username,
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::make(Response::error(trans('mcp.delete_failed_error', ['error' => $user->getErrors()->first()])));
|
||||
}
|
||||
|
||||
private function resolveUser(Request $request): ?User
|
||||
{
|
||||
if ($request->filled('id')) {
|
||||
return User::find($request->get('id'));
|
||||
}
|
||||
if ($request->filled('username')) {
|
||||
return User::where('username', $request->get('username'))->first();
|
||||
}
|
||||
if ($request->filled('email')) {
|
||||
return User::where('email', $request->get('email'))->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric user ID to delete'),
|
||||
'username' => $schema->string()->description('Username of the user to delete'),
|
||||
'email' => $schema->string()->description('Email address of the user to delete'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'success' => $schema->boolean()->description('True if the deletion succeeded'),
|
||||
'message' => $schema->string()->description('Human-readable result message')->required(),
|
||||
'username' => $schema->string()->description('Username of the deleted user'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\Actionlog;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('get_activity_log')]
|
||||
#[Title('Get Activity Log')]
|
||||
#[Description('Retrieve the Snipe-IT activity log with optional filtering by item type, item ID, user, and action type')]
|
||||
class GetActivityLogTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! Gate::allows('activity.view')) {
|
||||
return Response::make(Response::error(trans('mcp.unauthorized')));
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'item_type' => 'nullable|string|max:255',
|
||||
'item_id' => 'nullable|integer',
|
||||
'user_id' => 'nullable|integer',
|
||||
'action_type' => 'nullable|string|max:255',
|
||||
'limit' => 'nullable|integer|min:1|max:500',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
]);
|
||||
|
||||
$logs = Actionlog::with('user', 'item')->orderBy('created_at', 'desc');
|
||||
|
||||
if ($request->filled('item_type')) {
|
||||
$logs->where('item_type', $request->get('item_type'));
|
||||
}
|
||||
|
||||
if ($request->filled('item_id')) {
|
||||
$logs->where('item_id', $request->get('item_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('user_id')) {
|
||||
$logs->where('user_id', $request->get('user_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('action_type')) {
|
||||
$logs->where('action_type', $request->get('action_type'));
|
||||
}
|
||||
|
||||
$total = $logs->count();
|
||||
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
|
||||
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
|
||||
|
||||
$results = $logs->skip($offset)->take($limit)->get();
|
||||
|
||||
$activityData = $results->map(fn (Actionlog $log) => [
|
||||
'id' => $log->id,
|
||||
'action_type' => $log->action_type,
|
||||
'item_type' => $log->item_type,
|
||||
'item_id' => $log->item_id,
|
||||
'user_id' => $log->user_id,
|
||||
'user' => $log->user?->username,
|
||||
'note' => $log->note,
|
||||
'created_at' => $log->created_at?->toDateTimeString(),
|
||||
])->values()->all();
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.list_activity', ['total' => $total, 'count' => count($activityData)]))
|
||||
)->withStructuredContent([
|
||||
'total' => $total,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'activity' => $activityData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'item_type' => $schema->string()->description('Filter by item type (e.g. App\\Models\\Asset)'),
|
||||
'item_id' => $schema->number()->description('Filter by item ID'),
|
||||
'user_id' => $schema->number()->description('Filter by user ID'),
|
||||
'action_type' => $schema->string()->description('Filter by action type (e.g. checkout, checkin, update)'),
|
||||
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
|
||||
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
|
||||
];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'total' => $schema->number()->description('Total number of matching log entries')->required(),
|
||||
'offset' => $schema->number()->description('Current pagination offset')->required(),
|
||||
'limit' => $schema->number()->description('Results per page')->required(),
|
||||
'activity' => $schema->array()->description('List of activity log entries')->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mcp\Tools;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('get_current_user')]
|
||||
#[Title('Get Current User')]
|
||||
#[Description('Return information about the currently authenticated Snipe-IT user')]
|
||||
class GetCurrentUserTool extends Tool
|
||||
{
|
||||
public function handle(Request $request): ResponseFactory
|
||||
{
|
||||
if (! auth()->check()) {
|
||||
return Response::make(Response::error(trans('mcp.not_authenticated')));
|
||||
}
|
||||
|
||||
$user = User::with('company', 'department', 'userloc')->find(auth()->id());
|
||||
|
||||
if (! $user) {
|
||||
return Response::make(Response::error(trans('mcp.not_authenticated')));
|
||||
}
|
||||
|
||||
return Response::make(
|
||||
Response::text(trans('mcp.current_user', ['username' => $user->username]))
|
||||
)->withStructuredContent([
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'first_name' => $user->first_name,
|
||||
'last_name' => $user->last_name,
|
||||
'email' => $user->email,
|
||||
'company' => $user->company?->name,
|
||||
'department' => $user->department?->name,
|
||||
'location' => $user->userloc?->name,
|
||||
'employee_num' => $user->employee_num,
|
||||
'title' => $user->jobtitle,
|
||||
'phone' => $user->phone,
|
||||
'activated' => (bool) $user->activated,
|
||||
]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'id' => $schema->number()->description('Numeric user ID')->required(),
|
||||
'username' => $schema->string()->description('Username')->required(),
|
||||
'first_name' => $schema->string()->description('First name'),
|
||||
'last_name' => $schema->string()->description('Last name'),
|
||||
'email' => $schema->string()->description('Email address'),
|
||||
'company' => $schema->string()->description('Company name'),
|
||||
'department' => $schema->string()->description('Department name'),
|
||||
'location' => $schema->string()->description('Default location name'),
|
||||
'employee_num' => $schema->string()->description('Employee number'),
|
||||
'title' => $schema->string()->description('Job title'),
|
||||
'phone' => $schema->string()->description('Phone number'),
|
||||
'activated' => $schema->boolean()->description('Whether the account is activated'),
|
||||
];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user