Added #18767 - uploads for companies and departments

This commit is contained in:
snipe
2026-03-26 16:02:07 +00:00
parent ef22fb256b
commit 616d0f00f9
16 changed files with 137 additions and 60 deletions
@@ -9,7 +9,6 @@ use App\Models\Actionlog;
use App\Models\AssetModel; use App\Models\AssetModel;
use App\Models\CustomField; use App\Models\CustomField;
use App\Models\SnipeModel; use App\Models\SnipeModel;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
+8
View File
@@ -26,6 +26,8 @@ namespace App\Http\Controllers;
use App\Models\Accessory; use App\Models\Accessory;
use App\Models\Asset; use App\Models\Asset;
use App\Models\AssetModel; use App\Models\AssetModel;
use App\Models\Department;
use App\Models\Company;
use App\Models\Component; use App\Models\Component;
use App\Models\Consumable; use App\Models\Consumable;
use App\Models\License; use App\Models\License;
@@ -46,6 +48,8 @@ abstract class Controller extends BaseController
public static $map_object_type = [ public static $map_object_type = [
'accessories' => Accessory::class, 'accessories' => Accessory::class,
'companies' => Company::class,
'departments' => Department::class,
'maintenances' => Maintenance::class, 'maintenances' => Maintenance::class,
'assets' => Asset::class, 'assets' => Asset::class,
'audits' => Asset::class, 'audits' => Asset::class,
@@ -64,6 +68,8 @@ abstract class Controller extends BaseController
'maintenances' => 'private_uploads/maintenances/', 'maintenances' => 'private_uploads/maintenances/',
'assets' => 'private_uploads/assets/', 'assets' => 'private_uploads/assets/',
'audits' => 'private_uploads/audits/', 'audits' => 'private_uploads/audits/',
'departments' => 'private_uploads/departments/',
'companies' => 'private_uploads/companies/',
'components' => 'private_uploads/components/', 'components' => 'private_uploads/components/',
'consumables' => 'private_uploads/consumables/', 'consumables' => 'private_uploads/consumables/',
'hardware' => 'private_uploads/assets/', 'hardware' => 'private_uploads/assets/',
@@ -79,6 +85,8 @@ abstract class Controller extends BaseController
'maintenances' => 'maintenance', 'maintenances' => 'maintenance',
'assets' => 'asset', 'assets' => 'asset',
'audits' => 'audits', 'audits' => 'audits',
'companies' => 'company',
'departments' => 'department',
'components' => 'component', 'components' => 'component',
'consumables' => 'consumable', 'consumables' => 'consumable',
'hardware' => 'asset', 'hardware' => 'asset',
+9 -22
View File
@@ -423,40 +423,23 @@ class Actionlog extends SnipeModel
/** /**
* Calculate the date of the next audit * Calculate the date of the next audit
* *
* @author [A. Gianotto] [<snipe@snipe.net>] * @return Datetime | string
* *
* @since [v4.0] * @since [v4.0]
* *
* @return \Datetime * @author [A. Gianotto] [<snipe@snipe.net>]
*/ */
public function calcNextAuditDate($monthInterval = 12, $asset = null) public function calcNextAuditDate($monthInterval = 12, $asset = null)
{ {
$last_audit_date = Carbon::parse($this->created_at); $last_audit_date = Carbon::parse($this->created_at);
// If there is an asset-specific next date already given, // If there is an asset-specific next date already given,
if (($asset) && ($asset->next_audit_date)) { if (($asset) && ($asset->next_audit_date)) {
return \Carbon::parse($asset->next_audit_date); return Carbon::parse($asset->next_audit_date);
} }
return \Carbon::parse($last_audit_date)->addMonths($monthInterval)->toDateString(); return Carbon::parse($last_audit_date)->addMonths($monthInterval)->toDateString();
}
/**
* Gets action logs in chronological order, excluding uploads
*
* @author Vincent Sposato <vincent.sposato@gmail.com>
*
* @since v1.0
*
* @return Collection
*/
public function getListingOfActionLogsChronologicalOrder()
{
return $this->all()
->where('action_type', '!=', 'uploaded')
->orderBy('item_id', 'asc')
->orderBy('created_at', 'asc')
->get();
} }
/** /**
* Determines what the type of request is so we can log it to the action_log * Determines what the type of request is so we can log it to the action_log
@@ -553,8 +536,12 @@ class Actionlog extends SnipeModel
return 'private_uploads/assets/'.$this->filename; return 'private_uploads/assets/'.$this->filename;
case AssetModel::class: case AssetModel::class:
return 'private_uploads/models/'.$this->filename; return 'private_uploads/models/'.$this->filename;
case Company::class:
return 'private_uploads/companies/' . $this->filename;
case Consumable::class: case Consumable::class:
return 'private_uploads/consumables/'.$this->filename; return 'private_uploads/consumables/'.$this->filename;
case Department::class:
return 'private_uploads/departments/' . $this->filename;
case Component::class: case Component::class:
return 'private_uploads/components/'.$this->filename; return 'private_uploads/components/'.$this->filename;
case License::class: case License::class:
+6
View File
@@ -3,10 +3,13 @@
namespace App\Models; namespace App\Models;
use App\Models\Traits\CompanyableTrait; use App\Models\Traits\CompanyableTrait;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Loggable;
use App\Models\Traits\Searchable; use App\Models\Traits\Searchable;
use App\Presenters\CompanyPresenter; use App\Presenters\CompanyPresenter;
use App\Presenters\Presentable; use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@@ -22,6 +25,9 @@ final class Company extends SnipeModel
{ {
use CompanyableTrait; use CompanyableTrait;
use HasFactory; use HasFactory;
use HasUploads;
use SoftDeletes;
use Loggable;
protected $table = 'companies'; protected $table = 'companies';
+6
View File
@@ -4,11 +4,14 @@ namespace App\Models;
use App\Http\Traits\UniqueUndeletedTrait; use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\Traits\CompanyableTrait; use App\Models\Traits\CompanyableTrait;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Loggable;
use App\Models\Traits\Searchable; use App\Models\Traits\Searchable;
use App\Presenters\DepartmentPresenter; use App\Presenters\DepartmentPresenter;
use App\Presenters\Presentable; use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Watson\Validating\ValidatingTrait; use Watson\Validating\ValidatingTrait;
@@ -17,6 +20,9 @@ class Department extends SnipeModel
{ {
use CompanyableTrait; use CompanyableTrait;
use HasFactory; use HasFactory;
use HasUploads;
use Loggable;
use SoftDeletes;
/** /**
* Whether the model should inject it's identifier to the unique * Whether the model should inject it's identifier to the unique
+19
View File
@@ -112,6 +112,24 @@ class SettingsServiceProvider extends ServiceProvider
return 'locations/'; return 'locations/';
}); });
// Companies
app()->singleton('companies_upload_path', function () {
return 'companies/';
});
app()->singleton('companies_upload_url', function () {
return 'companies/';
});
// Departments
app()->singleton('departments_upload_path', function () {
return 'departments/';
});
app()->singleton('departments_upload_url', function () {
return 'departments/';
});
// Users // Users
app()->singleton('users_upload_path', function () { app()->singleton('users_upload_path', function () {
return 'avatars/'; return 'avatars/';
@@ -157,6 +175,7 @@ class SettingsServiceProvider extends ServiceProvider
return 'companies/'; return 'companies/';
}); });
// Accessories paths and URLs // Accessories paths and URLs
app()->singleton('accessories_upload_path', function () { app()->singleton('accessories_upload_path', function () {
return 'accessories/'; return 'accessories/';
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('companies', function (Blueprint $table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
}
};
+17 -5
View File
@@ -16,15 +16,22 @@
<x-page-column class="col-md-9 main-panel"> <x-page-column class="col-md-9 main-panel">
<x-tabs> <x-tabs>
<x-slot:tabnav> <x-slot:tabnav>
<x-tabs.user-tab count="{{ $company->users->count() }}"/>
<x-tabs.asset-tab count="{{ $company->assets()->AssetsForShow()->count() }}"/> <x-tabs.asset-tab count="{{ $company->assets()->AssetsForShow()->count() }}"/>
<x-tabs.license-tab count="{{ $company->licenses->count() }}"/> <x-tabs.license-tab count="{{ $company->licenses->count() }}"/>
<x-tabs.accessory-tab count="{{ $company->accessories->count() }}"/> <x-tabs.accessory-tab count="{{ $company->accessories->count() }}"/>
<x-tabs.consumable-tab count="{{ $company->consumables->count() }}"/> <x-tabs.consumable-tab count="{{ $company->consumables->count() }}"/>
<x-tabs.component-tab count="{{ $company->components->count() }}"/> <x-tabs.component-tab count="{{ $company->components->count() }}"/>
<x-tabs.user-tab count="{{ $company->users->count() }}"/> <x-tabs.files-tab count="{{ $company->uploads()->count() }}"/>
<x-tabs.upload-tab :item="$company"/>
</x-slot:tabnav> </x-slot:tabnav>
<x-slot:tabpanes> <x-slot:tabpanes>
<!-- start users tab pane -->
<x-tabs.pane name="users">
<x-table.users name="users" :route="route('api.users.index', ['company_id' => $company->id])"/>
</x-tabs.pane>
<!-- end users tab pane -->
<!-- start assets tab pane --> <!-- start assets tab pane -->
<x-tabs.pane name="assets"> <x-tabs.pane name="assets">
@@ -55,11 +62,11 @@
<x-table.components name="components" :route="route('api.components.index', ['company_id' => $company->id])"/> <x-table.components name="components" :route="route('api.components.index', ['company_id' => $company->id])"/>
</x-tabs.pane> </x-tabs.pane>
<!-- start users tab pane --> <!-- start files tab pane -->
<x-tabs.pane name="users"> <x-tabs.pane name="files">
<x-table.users name="users" :route="route('api.users.index', ['company_id' => $company->id])"/> <x-table.files object_type="companies" :object="$company"/>
</x-tabs.pane> </x-tabs.pane>
<!-- end users tab pane --> <!-- end files tab pane -->
</x-slot:tabpanes> </x-slot:tabpanes>
@@ -81,6 +88,11 @@
</x-container> </x-container>
@can('update', Company::class)
@section('moar_scripts')
@include ('modals.upload-file', ['item_type' => 'companies', 'item_id' => $company->id])
@endsection
@endcan
@stop @stop
@section('moar_scripts') @section('moar_scripts')
+30 -21
View File
@@ -16,42 +16,51 @@
@section('content') @section('content')
<x-container columns="2"> <x-container columns="2">
<x-page-column class="col-md-9 main-panel"> <x-page-column class="col-md-9 main-panel">
<x-box> <x-tabs>
<x-slot:tabnav>
<x-tabs.user-tab count="{{ $department->users->count() }}"/>
<x-tabs.files-tab count="{{ $department->uploads()->count() }}"/>
<x-tabs.upload-tab :item="$department"/>
</x-slot:tabnav>
<x-slot:bulkactions> <x-slot:tabpanes>
<x-table.bulk-users /> <!-- start users tab pane -->
</x-slot:bulkactions> <x-tabs.pane name="users">
<x-table.users name="users" :route="route('api.users.index', ['department+id' => $department->id])"/>
</x-tabs.pane>
<!-- end users tab pane -->
<x-table <!-- start files tab pane -->
show_column_search="true" <x-tabs.pane name="files">
show_advanced_search="true" <x-table.files object_type="departments" :object="$department"/>
fixed_right_number="1" </x-tabs.pane>
fixed_number="2" <!-- end files tab pane -->
buttons="licenseButtons"
api_url="{{ route('api.users.index', ['department_id' => $department->id]) }}" </x-slot:tabpanes>
:presenter="\App\Presenters\UserPresenter::dataTableLayout()"
export_filename="export-{{ str_slug($department->name) }}-users-{{ date('Y-m-d') }}" </x-tabs>
/>
</x-box>
</x-page-column> </x-page-column>
<x-page-column class="col-md-3"> <x-page-column class="col-md-3">
<x-box class="side-box expanded"> <x-box class="side-box expanded">
<x-info-panel :infoPanelObj="$department" img_path="{{ app('users_upload_url') }}"> <x-info-panel :infoPanelObj="$department" img_path="{{ app('departments_upload_url') }}">
<x-slot:buttons> <x-slot:buttons>
<x-button.edit :item="$department" :route="route('departments.edit', $department->id)" /> <x-button.edit :item="$department" :route="route('departments.edit', $department->id)"/>
<x-button.delete :item="$department" /> <x-button.delete :item="$department"/>
</x-slot:buttons> </x-slot:buttons>
</x-info-panel> </x-info-panel>
</x-box> </x-box>
</x-page-column> </x-page-column>
</x-container> </x-container>
@can('update', Department::class)
@section('moar_scripts')
@include ('modals.upload-file', ['item_type' => 'departments', 'item_id' => $department->id])
@endsection
@endcan
@stop @stop
@section('moar_scripts') @section('moar_scripts')
+1 -1
View File
@@ -109,7 +109,7 @@
<x-tabs.files-tab count="{{ $location->uploads()->count() }}"/> <x-tabs.files-tab count="{{ $location->uploads()->count() }}"/>
<x-tabs.history-tab count="{{ $location->history()->count() }}" :model="$location"/> <x-tabs.history-tab count="{{ $location->history()->count() }}" :model="$location"/>
<x-tabs.upload-tab count="{{ $location->uploads()->count() }}" :item="$location"/> <x-tabs.upload-tab :item="$location"/>
</x-slot:tabnav> </x-slot:tabnav>
+4 -4
View File
@@ -1352,7 +1352,7 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu
'index', 'index',
] ]
)->name('api.files.index') )->name('api.files.index')
->where(['object_type' => 'accessories|audits|assets|components|consumables|hardware|licenses|locations|maintenances|models|suppliers|users']); ->where(['object_type' => 'accessories|audits|assets|components|consumables|hardware|licenses|locations|maintenances|models|suppliers|users|companies|departments']);
// Get a file // Get a file
Route::get('{object_type}/{id}/files/{file_id}', Route::get('{object_type}/{id}/files/{file_id}',
@@ -1361,7 +1361,7 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu
'show', 'show',
] ]
)->name('api.files.show') )->name('api.files.show')
->where(['object_type' => 'accessories|audits|assets|components|consumables|hardware|licenses|locations|maintenances|models|suppliers|users']); ->where(['object_type' => 'accessories|audits|assets|components|consumables|hardware|licenses|locations|maintenances|models|suppliers|users|companies|departments']);
// Upload files(s) // Upload files(s)
Route::post('{object_type}/{id}/files', Route::post('{object_type}/{id}/files',
@@ -1370,7 +1370,7 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu
'store', 'store',
] ]
)->name('api.files.store') )->name('api.files.store')
->where(['object_type' => 'accessories|audits|assets|components|consumables|hardware|licenses|locations|maintenances|models|suppliers|users']); ->where(['object_type' => 'accessories|audits|assets|components|consumables|hardware|licenses|locations|maintenances|models|suppliers|users|companies|departments']);
// Delete files(s) // Delete files(s)
Route::delete('{object_type}/{id}/files/{file_id}/delete', Route::delete('{object_type}/{id}/files/{file_id}/delete',
@@ -1379,6 +1379,6 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu
'destroy', 'destroy',
] ]
)->name('api.files.destroy') )->name('api.files.destroy')
->where(['object_type' => 'accessories|assets|components|consumables|hardware|licenses|locations|maintenances|models|suppliers|users']); ->where(['object_type' => 'accessories|assets|components|consumables|hardware|licenses|locations|maintenances|models|suppliers|users|companies|departments']);
}); // end API routes }); // end API routes
+3 -3
View File
@@ -738,7 +738,7 @@ Route::group(['middleware' => 'web'], function () {
'show' 'show'
] ]
)->name('ui.files.show') )->name('ui.files.show')
->where(['object_type' => 'assets|audits|maintenances|hardware|models|users|locations|accessories|consumables|licenses|suppliers|components']); ->where(['object_type' => 'assets|audits|maintenances|hardware|models|users|locations|accessories|consumables|licenses|suppliers|components|companies|departments']);
// Upload files(s) // Upload files(s)
Route::post('{object_type}/{id}/files', Route::post('{object_type}/{id}/files',
@@ -747,7 +747,7 @@ Route::group(['middleware' => 'web'], function () {
'store' 'store'
] ]
)->name('ui.files.store') )->name('ui.files.store')
->where(['object_type' => 'assets|audits|maintenances|hardware|models|users|locations|accessories|consumables|licenses|suppliers|components']); ->where(['object_type' => 'assets|audits|maintenances|hardware|models|users|locations|accessories|consumables|licenses|suppliers|components|companies|departments']);
// Delete files(s) // Delete files(s)
Route::delete('{object_type}/{id}/files/{file_id}/delete', Route::delete('{object_type}/{id}/files/{file_id}/delete',
@@ -756,7 +756,7 @@ Route::group(['middleware' => 'web'], function () {
'destroy' 'destroy'
] ]
)->name('ui.files.destroy') )->name('ui.files.destroy')
->where(['object_type' => 'assets|maintenances|hardware|models|users|locations|accessories|consumables|licenses|suppliers|components']); ->where(['object_type' => 'assets|maintenances|hardware|models|users|locations|accessories|consumables|licenses|suppliers|components|companies|departments']);
}); });
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
@@ -51,7 +51,7 @@ class DeleteCompaniesTest extends TestCase implements TestsPermissionsRequiremen
->deleteJson(route('api.companies.destroy', $company)) ->deleteJson(route('api.companies.destroy', $company))
->assertStatusMessageIs('success'); ->assertStatusMessageIs('success');
$this->assertDatabaseMissing('companies', ['id' => $company->id]); $this->assertSoftDeleted($company);
} }
public function test_adheres_to_full_multiple_companies_support_scoping() public function test_adheres_to_full_multiple_companies_support_scoping()
@@ -50,7 +50,7 @@ class DeleteDepartmentsTest extends TestCase implements TestsFullMultipleCompani
$this->assertDatabaseHas('departments', ['id' => $departmentA->id]); $this->assertDatabaseHas('departments', ['id' => $departmentA->id]);
$this->assertDatabaseHas('departments', ['id' => $departmentB->id]); $this->assertDatabaseHas('departments', ['id' => $departmentB->id]);
$this->assertDatabaseMissing('departments', ['id' => $departmentC->id]); $this->assertSoftDeleted($departmentC);
} }
public function test_cannot_delete_department_that_still_has_users() public function test_cannot_delete_department_that_still_has_users()
@@ -72,6 +72,6 @@ class DeleteDepartmentsTest extends TestCase implements TestsFullMultipleCompani
->deleteJson(route('api.departments.destroy', $department)) ->deleteJson(route('api.departments.destroy', $department))
->assertStatusMessageIs('success'); ->assertStatusMessageIs('success');
$this->assertDatabaseMissing('departments', ['id' => $department->id]); $this->assertSoftDeleted($department);
} }
} }