Skip to content
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
7afd7da
initial work, more testing/tests needed
spencerrlongg May 21, 2025
7571ff0
add fillable properties to rules, some tests, move authorization to r…
spencerrlongg May 21, 2025
120316b
wrong location import
spencerrlongg May 21, 2025
1b397cd
assertDbHas
spencerrlongg May 21, 2025
46d1c14
Added user company to checked out licenses
snipe Nov 3, 2025
9ee36df
Merge pull request #18139 from grokability/#18082-add-company-to-seat…
snipe Nov 3, 2025
92b50ca
Addresses #17994, #16925
snipe Nov 3, 2025
30e16b6
Added int
snipe Nov 3, 2025
ab555d0
Fixed #18136 - adds copy to clipbpard to asset name
snipe Nov 3, 2025
6809bbd
Merge pull request #18142 from grokability/#18136-copy-asset-name
snipe Nov 3, 2025
517f4ce
Fixed #18101: Make copy to clipboard alignment more consistent
snipe Nov 3, 2025
5d8905c
Merge pull request #18143 from grokability/#18101-make-copy-to-clipbo…
snipe Nov 3, 2025
e906d25
Merge pull request #16973 from spencerrlongg/bug/sc-29245
snipe Nov 4, 2025
ddb031f
Fixed #18148 and #17451 - return int for user_count, fixed validation
snipe Nov 4, 2025
b7a6706
Merge pull request #18150 from grokability/#18148-dept-api-request-us…
snipe Nov 4, 2025
4a39d7c
Use transformer for dept update responses
snipe Nov 4, 2025
9ddc48e
fix the headers field for Guzzle request
MarvelousAnything Nov 4, 2025
cc5ac65
Re-apply #18020, fixed #15107 (mostly) - added prefix and more option…
snipe Nov 4, 2025
c1204a5
Merge pull request #18152 from grokability/#18020-rework-pr
snipe Nov 4, 2025
c6269d6
Merge pull request #18151 from MarvelousAnything/fixes/test_webhook_c…
snipe Nov 4, 2025
a9574e8
Fixed #18133 - make the disabled toggle JS so it’s clearer
snipe Nov 4, 2025
4100f26
Override unique_undeleted in the form request
snipe Nov 4, 2025
547b3df
Added more commentary on why we’re intefering with the request
snipe Nov 4, 2025
e5c55c9
Fixed typo in the comment
snipe Nov 4, 2025
4ada47e
Use new setting variable since we already have it
snipe Nov 4, 2025
37eb638
Merge pull request #18155 from grokability/#18021-fix-patch-api-with-…
snipe Nov 4, 2025
44bfcee
Fixed #18119 - double formatting for acceptance/decline date
snipe Nov 4, 2025
c5ad451
Merge pull request #18156 from grokability/#18119-fix-double-helperin…
snipe Nov 4, 2025
88e532d
Fixed #18157 - reports permission glitch
snipe Nov 5, 2025
7434dd9
final adjustments to 24mm_E label
Godmartinz Nov 5, 2025
dea3993
Merge pull request #18161 from Godmartinz/TZE_24mm_E_adjustment
snipe Nov 5, 2025
90fc48d
Shim workaround to avoid max_input_vars and max_multipart_body_parts …
snipe Nov 6, 2025
c39c92d
Mash the ids into a string, fixed disclosure arrows
snipe Nov 6, 2025
f1b4877
A few small tweaks for new groups
snipe Nov 6, 2025
4073c9e
Updated ordering
snipe Nov 6, 2025
1382621
Added enctype back in
snipe Nov 6, 2025
4d38bd1
Renamed variable
snipe Nov 6, 2025
9f6a73b
Merge pull request #18170 from grokability/fix-for-groups
snipe Nov 6, 2025
1adb16b
Merge remote-tracking branch 'upstream/develop' into rebase-20251107
cz-lucas Nov 7, 2025
4e38fea
Rebuild static assets
cz-lucas Nov 7, 2025
0ca84fd
Remove duplicate @ endif in hardware/view.blade.php
cz-lucas Nov 7, 2025
dd36633
Add example testing environment configuration
cz-lucas Nov 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 0 additions & 19 deletions .env.testing.example

This file was deleted.

37 changes: 19 additions & 18 deletions app/Http/Controllers/Api/DepartmentsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreDepartmentRequest;
use App\Http\Transformers\DepartmentsTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Models\Department;
Expand All @@ -26,18 +27,19 @@ public function index(Request $request) : JsonResponse | array
$allowed_columns = ['id', 'name', 'image', 'users_count', 'notes'];

$departments = Department::select(
'departments.id',
'departments.name',
'departments.phone',
'departments.fax',
'departments.location_id',
'departments.company_id',
'departments.manager_id',
'departments.created_at',
'departments.updated_at',
'departments.image',
'departments.notes',
)->with('users')->with('location')->with('manager')->with('company')->withCount('users as users_count');
[
'departments.id',
'departments.name',
'departments.phone',
'departments.fax',
'departments.location_id',
'departments.company_id',
'departments.manager_id',
'departments.created_at',
'departments.updated_at',
'departments.image',
'departments.notes'
])->with('users')->with('location')->with('manager')->with('company')->withCount('users as users_count');

if ($request->filled('search')) {
$departments = $departments->TextSearch($request->input('search'));
Expand Down Expand Up @@ -94,18 +96,17 @@ public function index(Request $request) : JsonResponse | array
* @since [v4.0]
* @param \App\Http\Requests\ImageUploadRequest $request
*/
public function store(ImageUploadRequest $request) : JsonResponse
public function store(StoreDepartmentRequest $request): JsonResponse
{
$this->authorize('create', Department::class);
$department = new Department;
$department->fill($request->all());
$department->fill($request->validated());
$department = $request->handleImages($department);

$department->created_by = auth()->id();
$department->manager_id = ($request->filled('manager_id') ? $request->input('manager_id') : null);

if ($department->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $department, trans('admin/departments/message.create.success')));
return response()->json(Helper::formatStandardApiResponse('success', (new DepartmentsTransformer)->transformDepartment($department), trans('admin/departments/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $department->getErrors()));

Expand All @@ -121,7 +122,7 @@ public function store(ImageUploadRequest $request) : JsonResponse
public function show($id) : array
{
$this->authorize('view', Department::class);
$department = Department::findOrFail($id);
$department = Department::withCount('users as users_count')->findOrFail($id);
return (new DepartmentsTransformer)->transformDepartment($department);
}

Expand All @@ -141,7 +142,7 @@ public function update(ImageUploadRequest $request, $id) : JsonResponse
$department = $request->handleImages($department);

if ($department->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $department, trans('admin/departments/message.update.success')));
return response()->json(Helper::formatStandardApiResponse('success', (new DepartmentsTransformer)->transformDepartment($department), trans('admin/departments/message.update.success')));
}

return response()->json(Helper::formatStandardApiResponse('error', null, $department->getErrors()));
Expand Down
3 changes: 2 additions & 1 deletion app/Http/Controllers/Api/GroupsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function index(Request $request) : JsonResponse | array

$this->authorize('view', Group::class);

$groups = Group::select('id', 'name', 'permissions', 'notes', 'created_at', 'updated_at', 'created_by')->with('adminuser')->withCount('users as users_count');
$groups = Group::select(['id', 'name', 'permissions', 'notes', 'created_at', 'updated_at', 'created_by'])->with('adminuser')->withCount('users as users_count');

if ($request->filled('search')) {
$groups = $groups->TextSearch($request->input('search'));
Expand All @@ -51,6 +51,7 @@ public function index(Request $request) : JsonResponse | array
'id',
'name',
'created_at',
'updated_at',
'users_count',
];

Expand Down
10 changes: 6 additions & 4 deletions app/Http/Controllers/Api/LicenseSeatsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ public function index(Request $request, $licenseId) : JsonResponse | array
if ($license = License::find($licenseId)) {
$this->authorize('view', $license);

$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department')
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department', 'user.company', 'asset.company')
->where('license_seats.license_id', $licenseId);

if ($request->input('status') == 'available') {
$seats->whereNull('license_seats.assigned_to');
$seats->whereNull('license_seats.assigned_to')->whereNull('license_seats.asset_id');
}

if ($request->input('status') == 'assigned') {
Expand All @@ -40,8 +40,10 @@ public function index(Request $request, $licenseId) : JsonResponse | array

$order = $request->input('order') === 'asc' ? 'asc' : 'desc';

if ($request->input('sort') == 'department') {
if ($request->input('sort') == 'assigned_user.department') {
$seats->OrderDepartments($order);
} elseif ($request->input('sort') == 'assigned_user.company') {
$seats->OrderCompany($order);
} else {
$seats->orderBy('updated_at', $order);
}
Expand Down Expand Up @@ -83,7 +85,7 @@ public function show($licenseId, $seatId) : JsonResponse | array
return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat not found'));
}
// 2. does the seat belong to the specified license?
if (! $license = $licenseSeat->license()->first() || $license->id != intval($licenseId)) {
if (! $licenseSeat = $licenseSeat->license()->first() || $licenseSeat->id != intval($licenseId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat does not belong to the specified license'));
}

Expand Down
43 changes: 35 additions & 8 deletions app/Http/Controllers/GroupsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ public function create(Request $request) : View
$permissions = config('permissions');
$groupPermissions = Helper::selectedPermissionsArray($permissions, $permissions);
$selectedPermissions = $request->old('permissions', $groupPermissions);

$users = \App\Models\User::orderBy('first_name', 'asc')->orderBy('last_name', 'asc')->get();
// Show the page
return view('groups/edit', compact('permissions', 'selectedPermissions', 'groupPermissions'))->with('group', $group);
return view('groups/edit', compact('permissions', 'selectedPermissions', 'groupPermissions'))
->with('group', $group)
->with('associated_users', [])
->with('unselected_users', $users);
}

/**
Expand All @@ -60,12 +63,23 @@ public function store(Request $request) : RedirectResponse
// create a new group instance
$group = new Group();
$group->name = $request->input('name');

if ($request->filled('permission')) {
$group->permissions = json_encode($request->array('permission'));
} else {
$group->permissions = null;
}

$group->permissions = json_encode($request->input('permission'));
$group->created_by = auth()->id();
$group->notes = $request->input('notes');

if ($group->save()) {
$group->users()->sync($request->input('associated_users'));

if ($request->filled('users_to_sync')) {
$associated_users = explode(',',$request->input('users_to_sync'));
$group->users()->sync($associated_users);
}
return redirect()->route('groups.index')->with('success', trans('admin/groups/message.success.create'));
}

Expand All @@ -89,10 +103,12 @@ public function edit(Group $group) : View | RedirectResponse
$groupPermissions = [];
}
$selected_array = Helper::selectedPermissionsArray($permissions, $groupPermissions);
$associated_users = $group->users()->get();
$associated_users = $group->users()->orderBy('first_name', 'asc')->orderBy('last_name', 'asc')->get();

// Get the unselected users
$unselected_users = \App\Models\User::whereNotIn('id', $associated_users->pluck('id')->toArray())->orderBy('first_name', 'asc')->orderBy('last_name', 'asc')->get();

//dd($associated_users->toArray());
return view('groups.edit', compact('group', 'permissions', 'selected_array', 'groupPermissions'))->with('associated_users', $associated_users);
return view('groups.edit', compact('group', 'permissions', 'selected_array', 'groupPermissions'))->with('associated_users', $associated_users)->with('unselected_users', $unselected_users);
}

/**
Expand All @@ -106,13 +122,24 @@ public function edit(Group $group) : View | RedirectResponse
public function update(Request $request, Group $group) : RedirectResponse
{
$group->name = $request->input('name');
$group->permissions = json_encode($request->input('permission'));

if ($request->filled('permission')) {
$group->permissions = json_encode($request->array('permission'));
} else {
$group->permissions = null;
}

$group->notes = $request->input('notes');


if (! config('app.lock_passwords')) {
if ($group->save()) {
$group->users()->sync($request->input('associated_users'));

if ($request->filled('users_to_sync')) {
$associated_users = explode(',',$request->input('users_to_sync'));
$group->users()->sync($associated_users);
}

return redirect()->route('groups.index')->with('success', trans('admin/groups/message.success.update'));
}

Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,7 @@ public function postLabels(StoreLabelSettings $request) : RedirectResponse
$setting->label2_asset_logo = $request->input('label2_asset_logo');
$setting->label2_1d_type = $request->input('label2_1d_type');
$setting->label2_2d_type = $request->input('label2_2d_type');
$setting->label2_2d_prefix = $request->input('label2_2d_prefix');
$setting->label2_2d_target = $request->input('label2_2d_target');
$setting->label2_fields = $request->input('label2_fields');
$setting->label2_empty_row_count = $request->input('label2_empty_row_count');
Expand Down
32 changes: 32 additions & 0 deletions app/Http/Requests/StoreDepartmentRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace App\Http\Requests;

use App\Models\Department;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;

class StoreDepartmentRequest extends ImageUploadRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Gate::allows('create', new Department);
}

/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$modelRules = (new Department)->getRules();

return array_merge(
$modelRules,
);
}
}
1 change: 1 addition & 0 deletions app/Http/Requests/StoreLabelSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function rules(): array
'labels_pagewidth' => 'numeric|nullable',
'labels_pageheight' => 'numeric|nullable',
'qr_text' => 'max:31|nullable',
'label2_2d_prefix' => 'nullable|max:191',
'label2_template' => [
'required',
Rule::in($names),
Expand Down
14 changes: 11 additions & 3 deletions app/Http/Requests/UpdateAssetRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,31 @@ public function authorize()
*/
public function rules()
{
$setting = Setting::getSettings();

$rules = array_merge(
parent::rules(),
(new Asset)->getRules(),
// this is to overwrite rulesets that include required, and rewrite unique_undeleted
// This overwrites the rulesets that are set at the model level (via Watson) but are not necessarily required at the request level when doing a PATCH update.
// Confusingly, this skips the unique_undeleted validator at the model level (and therefore the UniqueUndeletedTrait), so we have to re-add those
// rules here without the requiredness, since those values will already exist if you're updating an existing asset.
[
'model_id' => ['integer', 'exists:models,id,deleted_at,NULL', 'not_array'],
'status_id' => ['integer', 'exists:status_labels,id'],
'asset_tag' => [
'min:1', 'max:255', 'not_array',
Rule::unique('assets', 'asset_tag')->ignore($this->asset)->withoutTrashed()
Rule::unique('assets', 'asset_tag')->ignore($this->asset)->withoutTrashed(),
],
'serial' => [
'string', 'max:255', 'not_array',
$setting->unique_serial=='1' ? Rule::unique('assets', 'serial')->ignore($this->asset)->withoutTrashed() : 'nullable',
],
],
);

// 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::getSettings()->digit_separator === '1.234,56' && is_string($this->input('purchase_cost'))) {
if ($setting->digit_separator === '1.234,56' && is_string($this->input('purchase_cost'))) {
$rules['purchase_cost'] = ['nullable', 'string'];
}

Expand Down
2 changes: 1 addition & 1 deletion app/Http/Transformers/DepartmentsTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function transformDepartment(Department $department = null)
'id' => (int) $department->location->id,
'name' => e($department->location->name),
] : null,
'users_count' => e($department->users_count),
'users_count' => (int) ($department->users_count),
'notes' => Helper::parseEscapedMarkedownInline($department->notes),
'created_at' => Helper::getFormattedDateObject($department->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($department->updated_at, 'datetime'),
Expand Down
6 changes: 6 additions & 0 deletions app/Http/Transformers/LicenseSeatsTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ public function transformLicenseSeat(LicenseSeat $seat)
'name' => e($seat->user->department->name),

] : null,
'company'=> ($seat->user->company) ?
[
'id' => (int) $seat->user->company->id,
'name' => e($seat->user->company->name),

] : null,
'created_at' => Helper::getFormattedDateObject($seat->created_at, 'datetime'),
] : null,
'assigned_asset' => ($seat->asset) ? [
Expand Down
2 changes: 1 addition & 1 deletion app/Livewire/SlackSettingsForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public function testWebhook(){
]);

try {
$test = $webhook->post($this->webhook_endpoint, ['body' => $payload, ['headers' => ['Content-Type' => 'application/json']]]);
$test = $webhook->post($this->webhook_endpoint, ['body' => $payload, 'headers' => ['Content-Type' => 'application/json']]);

if(($test->getStatusCode() == 302)||($test->getStatusCode() == 301)){
return session()->flash('error' , trans('admin/settings/message.webhook.error_redirect', ['endpoint' => $this->webhook_endpoint]));
Expand Down
2 changes: 1 addition & 1 deletion app/Models/Actionlog.php
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ public function daysUntilNextAudit($monthInterval = 12, $asset = null)
{
$now = Carbon::now();
$last_audit_date = $this->created_at; // this is the action log's created at, not the asset itself
$next_audit = $last_audit_date->addMonth($monthInterval); // this actually *modifies* the $last_audit_date
$next_audit = $last_audit_date->addMonth((int) $monthInterval); // this actually *modifies* the $last_audit_date
$next_audit_days = (int) round($now->diffInDays($next_audit, true));
$override_default_next = $next_audit;

Expand Down
4 changes: 2 additions & 2 deletions app/Models/Asset.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,8 @@ public function getDisplayNameAttribute()

protected function warrantyExpires(): Attribute
{
return Attribute::make(
get: fn(mixed $value, array $attributes) => ($attributes['warranty_months'] && $attributes['purchase_date']) ? Carbon::parse($attributes['purchase_date'])->addMonths($attributes['warranty_months']) : null,
return Attribute:: make(
get: fn(mixed $value, array $attributes) => ($attributes['warranty_months'] && $attributes['purchase_date']) ? Carbon::parse($attributes['purchase_date'])->addMonths((int)$attributes['warranty_months']) : null,
);
}

Expand Down
11 changes: 7 additions & 4 deletions app/Models/Department.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@ class Department extends SnipeModel
];

protected $rules = [
'name' => 'required|max:255|is_unique_department',
'location_id' => 'numeric|nullable',
'company_id' => 'numeric|nullable',
'manager_id' => 'numeric|nullable',
'name' => 'required|max:255|is_unique_across_company_and_location:departments,name',
'location_id' => 'numeric|nullable|exists:locations,id',
'company_id' => 'numeric|nullable|exists:companies,id',
'manager_id' => 'numeric|nullable|exists:users,id',
'phone' => 'string|max:255|nullable',
'fax' => 'string|max:255|nullable',
'notes' => 'string|max:255|nullable',
];

/**
Expand Down
Loading
Loading