Compare commits

...

1176 Commits

Author SHA1 Message Date
snipe
6f233bc218 Get more of the select2 ajax lists working 2025-11-03 15:38:43 +00:00
snipe
448f4b94af Remove classes 2025-11-03 14:03:31 +00:00
snipe
7c58fb93a9 Fixed test 2025-11-01 02:53:47 +00:00
snipe
80e4e4a40e Updated more form fields 2025-11-01 02:45:59 +00:00
snipe
7161d56ae2 Prod assets for some reason 2025-10-31 19:07:17 +00:00
snipe
1e85c7b575 Put the form-control-static back since it didn’t cut down the number of files :( 2025-10-31 19:05:36 +00:00
snipe
03130f0947 Try to undo the asset stuff for the PR 2025-10-31 19:03:50 +00:00
snipe
4812e25e5c Added name awareness in child elements 2025-10-31 19:03:19 +00:00
snipe
e6c49da11c Use new blade for textarea 2025-10-31 19:02:54 +00:00
snipe
6f3ee1914a Updated maintenances screen 2025-10-31 19:02:39 +00:00
snipe
9232ee781e Updated validation JS for select2 2025-10-31 19:02:25 +00:00
snipe
3d675d375c Re-add the form static so it’s less noisy for this PR 2025-10-31 19:02:02 +00:00
snipe
d2edc77197 Debugging - remove later 2025-10-31 19:01:42 +00:00
snipe
53716cbe90 Still not working (pre-selecting the ID of the asset) 2025-10-31 19:00:55 +00:00
snipe
8ce3fe0df0 Pass name to input 2025-10-31 16:21:41 +00:00
snipe
affee0a990 Handle unfilled data 2025-10-31 16:21:22 +00:00
snipe
6d5f515f48 Make select2 ajax a little more flexible 2025-10-31 12:23:08 +00:00
snipe
d96844498f This doesn’t actually work yet 2025-10-31 12:22:56 +00:00
snipe
08609d4f56 Moar refactorings 2025-10-30 22:26:21 +00:00
snipe
be344c440f Different approach - rely on nested slots 2025-10-30 21:30:10 +00:00
snipe
96cc2c9ee9 Move form into its own component 2025-10-30 16:35:15 +00:00
snipe
fb1a89442c Removed p-static class 2025-10-30 14:10:06 +00:00
snipe
359e0197bf Start building out the box+form stuff 2025-10-30 14:02:33 +00:00
snipe
6dbb836a01 Merge pull request #18125 from grokability/form-row-again
Added form row component
2025-10-29 16:33:15 +00:00
snipe
3426afe5a8 Account for input styles 2025-10-29 16:15:22 +00:00
snipe
4bbf923eb6 Smaller default row for textarea 2025-10-29 16:11:56 +00:00
snipe
e2c3480194 Apply form rows to manufacturers 2025-10-29 16:11:47 +00:00
snipe
73159076f6 Better handle input groups in js validator 2025-10-29 16:11:37 +00:00
snipe
90d040573d Added regular link icon 2025-10-29 16:07:16 +00:00
snipe
155481a442 Merge pull request #18120 from grokability/small-form-fixes-settings
Fixed form label alignments in settings section
2025-10-29 11:16:59 +00:00
snipe
2f31bfc5fe Fixed some HTML labels in settings 2025-10-29 11:13:07 +00:00
snipe
8d0c88dc74 Merge pull request #18116 from akemidx/auditwarningthreshold
Fixed #17329 Audit Warning Threshold could be negative
2025-10-28 20:44:58 +00:00
snipe
07256fd833 Merge pull request #18044 from mohammad-ahmadi1/ISSUE-17004-fix-db-dump-ssl-issue
fix: update mysqldump options to use --ssl-mode=DISABLED for modern versions
2025-10-28 20:37:08 +00:00
snipe
776cd43a58 Change text string for item (versus asset) 2025-10-28 20:36:05 +00:00
akemidx
acb5309aab min = 0 2025-10-28 16:32:21 -04:00
snipe
c68f81db3c Merge pull request #18113 from Godmartinz/customfieldvalue-in-importer-fix
Adds #17433  `is_null` check to import handler for custom fields
2025-10-28 20:27:22 +00:00
snipe
ac8e341b37 Merge pull request #18115 from marcusmoore/fixes/rb-20449-remove-admin-from-acceptance-email
Fixed #18112 - fix consumable and license acceptances
2025-10-28 20:26:59 +00:00
snipe
36122b3966 Added BACKUP_TIME_LIMIT to env example 2025-10-28 20:26:23 +00:00
akemidx
79bcf472f0 greater than or equal to zero 2025-10-28 16:13:33 -04:00
Marcus Moore
55d86da846 Remove references to administrator in acceptance notification 2025-10-28 13:07:00 -07:00
Godfrey M
e4f8c1ba3f fix custom field value conditional check 2025-10-28 12:24:35 -07:00
Godfrey M
c36236b7dc add is_null acheck to import hanlder for custom fields 2025-10-28 11:37:38 -07:00
snipe
63994333d0 Merge pull request #18111 from grokability/#16914-better-ldap-sync-phrasing
Fixed #16914: better ldap sync phrasing
2025-10-28 15:51:10 +00:00
snipe
da4c7d8934 One more tweak 2025-10-28 15:47:56 +00:00
snipe
186721eca0 Added breadcrumbs 2025-10-28 15:42:59 +00:00
snipe
f53d939c86 Better explanation for location sync, nicer look 2025-10-28 15:42:53 +00:00
snipe
23e6909708 Merge pull request #18110 from grokability/#18107-normalize-to-strings
Fixed #18107: normalize "to" strings
2025-10-28 15:03:16 +00:00
snipe
cf421fe1c1 Added user-specific “to” 2025-10-28 15:02:15 +00:00
snipe
4d80e806e4 Use “-“ instead of “to” string, added placeholders 2025-10-28 15:02:03 +00:00
snipe
60a7b7f7ff Merge pull request #18109 from grokability/audit-improvements
Limit the upcoming audit email to 30 records, added optional --with-output
2025-10-28 14:31:55 +00:00
snipe
90263eab06 Added note for —with-option flag 2025-10-28 14:20:40 +00:00
snipe
9d8f251fc4 Limit the email to 30 records, added optional --with-output 2025-10-28 14:15:15 +00:00
snipe
2b4986571c Merge pull request #18108 from uberbrady/fix_ldap_tests
Fixed - LDAP test needs to be fixed to match new behavior
2025-10-28 12:49:11 +00:00
Brady Wetherington
890d13bd52 LDAP test needs to be fixed to match new behavior 2025-10-28 12:30:30 +00:00
snipe
e698e71137 Merge pull request #18100 from marcusmoore/fixes/20318-license-seat-display-name
Added null safe operator in case of missing license
2025-10-28 09:29:25 +00:00
snipe
d064a5530a Merge remote-tracking branch 'origin/master' into develop 2025-10-28 02:10:07 +00:00
snipe
ab4fbf6c19 Merge pull request #18105 from grokability/ldap-fast-find-and-bind
Possible fix for 504 gateway timeout on unreachable LDAP server
2025-10-28 02:09:41 +00:00
snipe
728afa8361 Possible fix for 504 gateway timeout on unreachable LDAP server 2025-10-27 23:45:12 +00:00
snipe
b77019c16e Merge remote-tracking branch 'origin/develop' 2025-10-27 19:32:28 +00:00
snipe
6703448b80 Merge pull request #18102 from marcusmoore/fixes/rb-20434-undefined-permissions-variable
Fixed issue when viewing user that does not have permissions set
2025-10-27 19:31:54 +00:00
Marcus Moore
776ba19a1f Define default permissions array 2025-10-27 12:28:55 -07:00
Marcus Moore
1f499e0d44 Add null safe operator in case of missing license 2025-10-27 10:47:55 -07:00
snipe
0a6eb61103 Merge remote-tracking branch 'origin/develop' 2025-10-27 13:20:18 +00:00
snipe
32a2eed5ec Fixed #18075 - make require_serial boolean in API transformer 2025-10-27 13:19:37 +00:00
snipe
40a70d39d0 Merge remote-tracking branch 'origin/develop' 2025-10-27 12:42:40 +00:00
snipe
5697054e98 Merge pull request #18099 from grokability/fix-cjk-for-acceptance-translated-strings
Fixed #18097 - check for CJK in field labels as well as content
2025-10-27 12:41:59 +00:00
snipe
def5969e1c Fixed #18097 - check for CJK in field labels as well as content 2025-10-27 12:34:30 +00:00
snipe
78a418630d Merge remote-tracking branch 'origin/develop' 2025-10-27 12:21:25 +00:00
snipe
6d76e7b2d4 Added dumb pverride for reports :( 2025-10-27 12:21:16 +00:00
snipe
f052c8b44c Merge pull request #18076 from uberbrady/move_traits_into_directories
Moved Traits into its directory and modify the FCO's to point to them
2025-10-27 11:43:31 +00:00
snipe
0e6991d56d Merge remote-tracking branch 'origin/develop' 2025-10-27 11:42:05 +00:00
snipe
06eab5f8a4 Merge pull request #18093 from Godmartinz/fix-warranty-part-of-expiring-asset-query
Fixed expiring warranties not being included in the expiring alerts notification
2025-10-27 11:41:51 +00:00
snipe
4a481e79c4 Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	public/css/build/app.css
#	public/css/build/app.css.map
#	public/css/build/overrides.css
#	public/css/build/overrides.css.map
#	public/css/dist/all.css
#	public/js/dist/all.js
#	public/js/dist/all.js.map
#	public/mix-manifest.json
2025-10-27 11:14:05 +00:00
snipe
ee4abbcbaa Updated dev assets 2025-10-27 11:12:13 +00:00
snipe
dcc82d742f Fixed RB-20430 - 500 on LDAP if baseDN is not set 2025-10-27 11:09:24 +00:00
snipe
19cb2089d7 Merge pull request #18098 from grokability/dependabot/github_actions/develop/actions/upload-artifact-5
Bump actions/upload-artifact from 4 to 5
2025-10-27 10:47:59 +00:00
snipe
04923b06b0 Merge pull request #18078 from grokability/groups-ui-improvements
Groups UI improvements, ability to add users from the group edit screen
2025-10-27 10:47:11 +00:00
dependabot[bot]
e16755d491 Bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 08:19:26 +00:00
snipe
742b0769a4 Use translation string 2025-10-26 12:10:08 +00:00
snipe
df68dca9dc Warn if user has individual permission overrides 2025-10-25 18:57:17 +01:00
snipe
4a5bf78d58 Merge branch 'develop' into groups-ui-improvements 2025-10-25 18:31:22 +01:00
snipe
7947237489 Double check the admin status when toggling the superadmin 2025-10-25 18:29:52 +01:00
snipe
1115205164 Normalize the JS 2025-10-25 18:20:22 +01:00
snipe
d5d01136c4 Fixed js errors 2025-10-25 17:57:59 +01:00
snipe
3d47277614 Added cursor style 2025-10-25 17:57:44 +01:00
snipe
b937bea04f Working, but there’s a bit of a jitter I need to fix 2025-10-25 15:59:47 +01:00
snipe
fff14632bc Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	public/css/build/app.css
#	public/css/build/app.css.map
#	public/css/build/overrides.css
#	public/css/build/overrides.css.map
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-10-25 14:59:31 +01:00
snipe
9bdf1a620f Built assets 2025-10-25 14:58:27 +01:00
snipe
60099aa989 FAFOing on disclosure arrows being remembered 2025-10-25 14:56:01 +01:00
snipe
f3976e5dd8 Merge pull request #18094 from Godmartinz/custom_field_color_fixx
Fixed fieldset colors on dark themes
2025-10-23 20:32:05 +01:00
Godfrey M
d7d6893304 fix Custom Fields section header font color 2025-10-23 12:05:13 -07:00
Godfrey M
99549ce805 rewrite query for expired warranties on assets, concat queries" 2025-10-23 11:01:51 -07:00
snipe
7eb15fe04d Merge pull request #18091 from uberbrady/fix_backup_durations
Moved import time limit inside class, added new backup time limit
2025-10-23 18:51:35 +01:00
Godfrey M
e2019a13ab rewrite expired assets collection query 2025-10-23 10:49:58 -07:00
Brady Wetherington
4b7a06761a Moved import time limit inside class, added new backup time limit 2025-10-23 16:05:09 +01:00
snipe
8f4a1f5801 Moved JS and styles into js and css files 2025-10-23 13:24:26 +01:00
snipe
891bec9cdb Styling fixes 2025-10-23 13:06:45 +01:00
Godfrey M
c5252ea583 skip in one more spot 2025-10-22 12:23:14 -07:00
Godfrey M
82d553c180 skip test if sqllite 2025-10-22 12:19:13 -07:00
Godfrey M
71e34355b9 remove unwanted changes 2025-10-22 11:51:00 -07:00
Godfrey M
2bad8c72e4 fixes warranty part of expiring alerts query 2025-10-22 11:43:54 -07:00
Godfrey M
6134ca01ac Merge remote-tracking branch 'origin/develop' into develop 2025-10-22 10:48:05 -07:00
snipe
be8193ebff Fixed inherit permissions 2025-10-22 15:00:04 +01:00
snipe
430ee46645 Small tweaks to inherit js 2025-10-22 13:41:59 +01:00
snipe
76fbbf29e8 Replace generate text with icon button 2025-10-22 13:37:45 +01:00
snipe
3108159d95 Jitter CSS tweaks 2025-10-22 12:37:39 +01:00
snipe
f282a1ead7 Removed duplicated permissions js code 2025-10-22 12:03:55 +01:00
snipe
324bc4957d Merge pull request #18080 from Godmartinz/add-null-safe-operator-to-manager
Fixes admin alerts when a location doesnt have a manager
2025-10-22 11:56:59 +01:00
snipe
c3b5c4dfae Moved the logic into the permissions partial, added translations 2025-10-21 22:10:09 +01:00
Godfrey M
4ab5d97e86 add nullsafe operator to location manager 2025-10-21 14:06:08 -07:00
Godfrey M
b4696ef11e added alert_email to send conditional in listener 2025-10-21 13:11:21 -07:00
snipe
294ffb72a4 Translations!!! 2025-10-21 20:36:09 +01:00
snipe
8c534d29d3 WTF tower?? 2025-10-21 19:36:28 +01:00
snipe
d6cb262f9d Bumped version 2025-10-21 19:22:36 +01:00
snipe
5211e2ae20 Some UI refinement 2025-10-21 19:22:36 +01:00
snipe
90832fd1ad Checked check 2025-10-21 19:22:36 +01:00
snipe
889cbc69e1 Starter improvements - needs testing 2025-10-21 19:22:36 +01:00
snipe
15948370d4 Updated assets 2025-10-21 19:22:24 +01:00
snipe
63c5177b37 Merge pull request #18077 from Godmartinz/acceptance-notif-fixes-pt2
Adds admin to decline notification, fix asset and model name translations on asset notification
2025-10-21 18:31:28 +01:00
Godfrey M
4fbfaf6b9f add admin to decline, fix asset and model name translations 2025-10-21 10:22:34 -07:00
Brady Wetherington
9e68497b63 Moved Traits into directory and modify the users to point to them 2025-10-21 16:45:58 +01:00
snipe
0b9e13bf1e Merge remote-tracking branch 'origin/develop' 2025-10-21 12:58:06 +01:00
snipe
485d343e0f Merge pull request #18058 from uberbrady/ldap_fetch_dn_before_login_FIXED
FIXED - perform Ldap fetch for dn (Distinguished Name) before logging-in (fixed)
2025-10-21 12:56:31 +01:00
snipe
07f0e8a3be Merge pull request #18070 from Godmartinz/fix-admin-in-user-acceptance-notif
Fixes `admin` parameter acceptance notifications
2025-10-21 12:54:03 +01:00
snipe
cc5afb1cd8 Merge pull request #18074 from grokability/smaller-audit-image-fix
Fixed audit images not displaying inline
2025-10-21 12:51:20 +01:00
snipe
c69f1c0890 Smaller fix for missing audit images 2025-10-21 12:43:59 +01:00
Godfrey M
a8733bdedf remove nullsafe operator 2025-10-20 11:01:38 -07:00
Godfrey M
4222e4eb51 add nullsafe operator after admin 2025-10-20 11:00:59 -07:00
Godfrey M
5db4441f5c fix admin param in user accepted notification 2025-10-20 10:51:24 -07:00
snipe
2bcfe97211 Merge remote-tracking branch 'origin/develop' 2025-10-20 16:03:55 +01:00
snipe
4e74c97c84 Merge pull request #18066 from smarsching/issue-18065
Fixed #18065: Ensure that private_key_bits is always an int
2025-10-20 13:51:42 +01:00
Sebastian Marsching
71a46c9bd6 Fixed #18065: ensure that private_key_bits is always an int. 2025-10-18 18:39:43 +02:00
snipe
1a7c7fdebf Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	config/version.php
#	public/css/build/app.css
#	public/css/build/app.css.map
#	public/css/build/overrides.css
#	public/css/build/overrides.css.map
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-10-17 18:37:18 +01:00
snipe
65ff3d414a Bumped version 2025-10-17 18:34:54 +01:00
snipe
9fa38b70c8 Bumped version 2025-10-17 18:26:44 +01:00
snipe
470172f53f Re-run assets 2025-10-17 18:26:05 +01:00
snipe
81261d9e36 Put the session check back in - possible fix for #18004 2025-10-17 18:25:50 +01:00
snipe
3ab2e20119 Merge remote-tracking branch 'origin/develop' 2025-10-17 17:12:47 +01:00
snipe
b773d576ea Updated cipher-base library 2025-10-17 17:12:31 +01:00
snipe
23feb64b5a Fixed potential XSS on locations 2025-10-17 17:07:57 +01:00
snipe
87fe9d9d3d Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	public/css/build/app.css
#	public/css/build/app.css.map
#	public/css/build/overrides.css
#	public/css/build/overrides.css.map
#	public/css/dist/all.css
#	public/css/dist/skins/_all-skins.css
#	public/css/dist/skins/_all-skins.css.map
#	public/css/dist/skins/_all-skins.min.css
#	public/css/dist/skins/skin-contrast.css
#	public/css/dist/skins/skin-contrast.css.map
#	public/css/dist/skins/skin-contrast.min.css
#	public/mix-manifest.json
2025-10-17 17:00:10 +01:00
snipe
b1ef3f51cb Merge pull request #18064 from grokability/new-fieldsets
Use new fieldset component
2025-10-17 16:58:17 +01:00
snipe
e1b6488f8e Use new fieldset component 2025-10-17 16:55:36 +01:00
snipe
a472dede2b Merge pull request #18063 from grokability/improved-fieldset-UI-and-better-help-test-for-ldap-mapping
New legend styles and additional help hints for LDAP
2025-10-17 15:45:05 +01:00
snipe
263cc3f7a1 New styles and additional help hints for LDAP 2025-10-17 15:29:24 +01:00
Brady Wetherington
598612d4bf Got tests to pass? Not very happy about it, tbh. 2025-10-16 19:11:49 +01:00
Brady Wetherington
a07d83e583 Improved find-and-bind for complex LDAP directory structures 2025-10-16 19:11:49 +01:00
snipe
d355812433 Updated assets 2025-10-16 19:06:22 +01:00
snipe
1afc14f5ab Merge remote-tracking branch 'origin/develop' 2025-10-16 19:04:30 +01:00
snipe
8947b667ae Fixed status labels skin for visited link buttons 2025-10-16 19:04:15 +01:00
snipe
6b41796d44 Fixed restore button on asset view page 2025-10-16 19:04:01 +01:00
snipe
6efe8eb55b Small fix for high contrast skin 2025-10-16 18:33:33 +01:00
snipe
4c8f8918e8 Merge remote-tracking branch 'origin/develop' 2025-10-16 15:49:59 +01:00
snipe
4cb5bb1855 Merge pull request #18057 from grokability/user-selector-fix
Fixed #18053 - user selector missing on page load without cookies
2025-10-16 15:49:39 +01:00
snipe
eb007e025a Fixed #18053 - user selector missing on page load without cookies 2025-10-16 15:47:25 +01:00
snipe
18aefa9dee Merge remote-tracking branch 'origin/develop' 2025-10-16 15:10:29 +01:00
snipe
2ec540bd36 Updated language translations 2025-10-16 15:10:17 +01:00
snipe
1374ec408a Merge pull request #18019 from akemidx/emdashcharfix
Fixed #17363 - Emdash Character Encoding error
2025-10-16 15:07:31 +01:00
snipe
8f8fed2b79 Merge remote-tracking branch 'origin/develop' 2025-10-16 14:33:30 +01:00
snipe
a1e6f01fe9 Fixed updating notes in bulk edit 2025-10-16 14:33:21 +01:00
snipe
d7496f22e5 Merge remote-tracking branch 'origin/develop' 2025-10-16 14:04:44 +01:00
snipe
7de25a1c37 Merge pull request #18056 from grokability/add-expected-checkin-to-view-assets
Added expected_checkin to user’s View Assigned page
2025-10-16 14:04:31 +01:00
snipe
78da89340c Added expected_checkin to user’s View Assigned page 2025-10-16 13:59:59 +01:00
snipe
60606115fe Merge remote-tracking branch 'origin/develop' 2025-10-16 12:46:47 +01:00
snipe
57b49fc31c Check for array key for license report/formatters 2025-10-16 12:46:17 +01:00
snipe
6e90c8f6e6 Bumped hash for master 2025-10-16 11:28:37 +01:00
snipe
a7ff2595a5 Bumped hash 2025-10-16 11:28:01 +01:00
snipe
1edea6abef Fixed RB-20347 - ambigious field in custom fields 2025-10-16 11:27:25 +01:00
snipe
a3bd58bda6 Foundation 2025-10-16 11:17:48 +01:00
snipe
3339d1999f Merge remote-tracking branch 'origin/develop' 2025-10-16 11:12:56 +01:00
akemidx
df3e8ec0f3 cleaning up notes 2025-10-15 17:35:25 -04:00
snipe
2dbec867d9 Merge pull request #18038 from boteroTradebe/develop
Fixed LDAP sync not syncing all user fields
2025-10-15 17:25:33 +01:00
Mohammad Ahmadi
3fc651d659 fix: update mysqldump options to use --ssl-mode=DISABLED for modern versions 2025-10-15 00:30:53 +02:00
snipe
b91d23023d Fix blade template syntax for accessory fields 2025-10-14 13:45:58 +01:00
boteroTradebe
cc234c60b8 Update LdapSync.php to populate/update user fields
Added missing IF statements for address, city, state, and ZIP code. Previously, this information would be pulled via LDAP sync but would not update the user data due to the missing IF statements for these attributes.
2025-10-13 13:42:11 -05:00
snipe
f1883c8004 Merge remote-tracking branch 'origin/develop' 2025-10-13 14:48:46 +01:00
snipe
79f6ddc8ee Merge pull request #18036 from grokability/better-messaging-for-bulk-deletes
Fixed #18034 - Shows success message on partial bulk delete success
2025-10-13 14:30:26 +01:00
snipe
89f439a18d Shows success message on partial bulk delete success 2025-10-13 14:25:05 +01:00
snipe
79eb5bfad9 Merge pull request #18035 from grokability/fixes-bulk-error-notification
Fixed bulk error display to be more consistent and correct HTML
2025-10-13 14:02:11 +01:00
snipe
3a36b7dafd Fixed display to be more consistent and correct HTML 2025-10-13 13:57:04 +01:00
snipe
1fe2fd9891 Merge pull request #17573 from spencerrlongg/feature/8709-bulk-deletion-of-asset-categories-suppliers-manufacturers
Fixed #8709 - Bulk Deletion of Categories, Suppliers, Manufa...
2025-10-13 13:20:13 +01:00
snipe
5fdb999ece Merge branch 'develop' into feature/8709-bulk-deletion-of-asset-categories-suppliers-manufacturers 2025-10-13 13:07:53 +01:00
snipe
5a38d9c2b6 Merge pull request #18029 from grokability/dependabot/github_actions/develop/github/codeql-action-4
Bump github/codeql-action from 3 to 4
2025-10-13 12:00:49 +01:00
snipe
b1c7dc6cbb Merge remote-tracking branch 'origin/develop' 2025-10-13 11:32:19 +01:00
snipe
b21b2ac41c Merge pull request #18032 from grokability/move-asset-private-files
Added migration to move asset model private uploads
2025-10-13 11:31:55 +01:00
snipe
1396c597ef Move files 2025-10-13 11:19:25 +01:00
dependabot[bot]
46af72ee4e Bump github/codeql-action from 3 to 4
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 08:19:22 +00:00
snipe
e3e8f553c8 Merge remote-tracking branch 'origin/develop' 2025-10-09 18:39:13 +01:00
snipe
549928d3d1 Updated assets 2025-10-09 18:38:59 +01:00
snipe
abefec9628 Upgraded jdpdf to ^3.0.3 2025-10-09 18:37:59 +01:00
snipe
b483eeded4 Merge remote-tracking branch 'origin/develop' 2025-10-09 11:24:47 +01:00
snipe
fab85dafa8 Added employee number and email to acceptance PDF 2025-10-09 11:24:16 +01:00
snipe
955faed919 Merge pull request #18018 from grokability/advanced-user-search
Normalize advanced search
2025-10-09 11:04:28 +01:00
snipe
3ff516180d Merge remote-tracking branch 'origin/develop' 2025-10-09 10:56:08 +01:00
snipe
0c8ca6d6b0 Search on status label name 2025-10-09 10:54:11 +01:00
snipe
a5de077e04 Merge pull request #17975 from marcusmoore/replace-date-and-time-display-macros
Removed date and time display format form macros
2025-10-09 04:54:05 +01:00
akemidx
c0a99d6b52 notes 2025-10-08 19:20:32 -04:00
Marcus Moore
a1e65cd897 Merge branch 'develop' into replace-date-and-time-display-macros
# Conflicts:
#	resources/macros/macros.php
2025-10-08 13:10:56 -07:00
snipe
5d65f1ffc5 Merge pull request #17976 from marcusmoore/17204-replace-form-digit-separator
Fixed #17204 - replace Form::digit_separator macro
2025-10-08 21:01:11 +01:00
snipe
b7193a06fd Normalize advanced search 2025-10-08 20:51:35 +01:00
akemidx
cba090f8eb notes 2025-10-08 15:50:00 -04:00
snipe
0d6baa1081 Merge remote-tracking branch 'origin/develop' 2025-10-08 15:29:02 +01:00
snipe
bc60d796a3 Fixed RB-20329 - ambiguous clause on deleted_at 2025-10-08 15:28:48 +01:00
snipe
85a208526b Merge remote-tracking branch 'origin/develop' 2025-10-08 10:38:08 +01:00
snipe
c3ac0a750d Added licenses checkin permission 2025-10-08 10:37:58 +01:00
snipe
0f111127a5 Merge pull request #18005 from marcusmoore/fixes/drop-index-migration
Fixed exception when rolling back migrations
2025-10-08 05:26:22 +01:00
snipe
cd266a6bef Merge remote-tracking branch 'origin/develop' 2025-10-08 05:13:14 +01:00
snipe
0f4945621c Merge pull request #18009 from grokability/#18002-audit-image-location
Fixed #18002 - use correct path for audit images to determine validity
2025-10-08 05:12:54 +01:00
snipe
9a477f227b Fixed #18002 - use correct path for audit images to determine validity 2025-10-08 05:09:04 +01:00
akemidx
3c81257325 fix? 2025-10-07 20:23:46 -04:00
Marcus Moore
218fe9ebdc Allow Laravel to calculate index name 2025-10-07 16:04:12 -07:00
spencerrlongg
24bb45ab97 change translation 2025-10-07 13:26:08 -05:00
snipe
85f39de540 Merge pull request #18001 from grokability/#17670-fixes-advanced-search-on-assets
Fixed #17670 - advanced search on relationships not working
2025-10-07 17:35:05 +01:00
snipe
5cc3277e2d Merge pull request #18003 from uberbrady/fix_file_upload_base64
Fixed [FD-50921] - base64-encoded image files for asset creation was broken
2025-10-07 17:34:53 +01:00
snipe
d5ef7f3204 Merge remote-tracking branch 'origin/develop' 2025-10-07 17:29:24 +01:00
snipe
cb47d01f51 Fixed variable name in location print 2025-10-07 17:29:12 +01:00
Brady Wetherington
ee499c1385 Fix base64-encoded image files for asset creation; add test 2025-10-07 16:00:16 +01:00
snipe
4ae4af5c10 Fixed #17670 - advanced search on relationships not working 2025-10-07 15:39:09 +01:00
snipe
b0f5fe7e25 Merge remote-tracking branch 'origin/develop' 2025-10-07 14:32:33 +01:00
snipe
fd32585efc Added notes back to audit email 2025-10-07 14:32:24 +01:00
snipe
c675bb7252 Merge remote-tracking branch 'origin/develop' 2025-10-07 14:29:21 +01:00
snipe
e65d11d71e Pulled incorrect button label 2025-10-07 14:29:10 +01:00
snipe
c2680334f1 Clearer title on maintenances within asset context 2025-10-07 14:28:38 +01:00
snipe
1217a02ec1 Merge pull request #17992 from marcusmoore/17963-undelete-files-command
Added command to remove invalid "upload deleted" entries from the action log
2025-10-07 14:17:56 +01:00
snipe
143a091f45 Merge remote-tracking branch 'origin/develop' 2025-10-07 14:15:59 +01:00
snipe
49ecc93dbe Merge pull request #17999 from grokability/updated-audit-report
Improved upcoming audit email layout and cli feedback
2025-10-07 14:15:44 +01:00
snipe
1735fb6bed Improved audit email 2025-10-07 14:08:33 +01:00
snipe
8fefc11b4d Merge remote-tracking branch 'origin/develop' 2025-10-07 12:09:32 +01:00
snipe
1a3b22171c Merge pull request #17997 from grokability/#17924-add-url-to-maintenances
Fixed #17924 - added url to maintenances
2025-10-07 12:09:06 +01:00
snipe
5e773be260 Enhanced tests 2025-10-07 12:05:37 +01:00
snipe
c317a1dc8b Added url to factory 2025-10-07 12:03:35 +01:00
snipe
9401ffc83c Added link to view 2025-10-07 11:55:32 +01:00
snipe
479a852446 Fixed #17924 - added url to maintenances 2025-10-07 11:49:10 +01:00
snipe
9188fb03f0 Merge remote-tracking branch 'origin/develop' 2025-10-07 10:49:59 +01:00
snipe
36bb91cbc3 Merge pull request #17995 from grokability/#17910-show-counts-on-mobile
Fixed #17910 - added counts to mobile view for assets
2025-10-07 10:47:41 +01:00
snipe
56e5ab8bc6 Fixed #17910 - added counts to mobile view for assets 2025-10-07 10:41:03 +01:00
snipe
607eb6ca03 Merge remote-tracking branch 'origin/develop' 2025-10-07 09:42:59 +01:00
snipe
ad745cc84b Fixed #17911 - updated language string 2025-10-07 09:42:30 +01:00
Marcus Moore
5a13d5ea6f Update command description 2025-10-06 15:23:55 -07:00
Marcus Moore
0b065eb7fe Improve command 2025-10-06 15:20:43 -07:00
Marcus Moore
e2c0e4bc66 WIP build out command 2025-10-06 15:14:15 -07:00
Marcus Moore
655b0e6778 Scaffold command 2025-10-06 14:59:25 -07:00
Marcus Moore
cd785c9fc3 Remove name_display_format macro that was accidentally re-added 2025-10-06 13:06:52 -07:00
Marcus Moore
23885f5166 Merge branch 'develop' into 17204-replace-form-digit-separator
# Conflicts:
#	resources/macros/macros.php
2025-10-06 13:04:34 -07:00
snipe
44ef39e419 Merge remote-tracking branch 'origin/develop' 2025-10-06 20:53:14 +01:00
snipe
e05a0ef565 Merge pull request #17967 from marcusmoore/fixes/17963-delete-file-via-api-fix
Fixed #17963 - over eager deletion of asset files via api
2025-10-06 20:52:57 +01:00
snipe
0ea9c0647f Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	config/version.php
2025-10-06 15:14:56 +01:00
snipe
00b9ba2b75 Merge pull request #17989 from grokability/fixes-tcpdf-png-handling
Fixed #17940 - pngs not showing in acceptance PDFs
2025-10-06 15:13:58 +01:00
snipe
f1e3bc9531 Fixed #17940 - use base64encoding on images in acceptance PDF 2025-10-06 15:02:53 +01:00
snipe
7360e093b2 Bumped version 2025-10-06 10:41:15 +01:00
snipe
5f639bc24f Updated translations 2025-10-06 10:39:04 +01:00
snipe
884d2a9552 Merge remote-tracking branch 'origin/develop' 2025-10-04 14:09:54 +01:00
snipe
fd8a8b29b1 Added inactive and expired to licenses table listing 2025-10-04 13:18:51 +01:00
snipe
4e1ef40c05 Updated active button colors 2025-10-04 13:16:46 +01:00
snipe
00af0ddff5 Merge pull request #17982 from grokability/expiring-alerts-improvements
Small expiring alerts improvements
2025-10-04 01:11:03 +01:00
snipe
2467e823a5 Reordered buttons 2025-10-04 01:06:56 +01:00
snipe
224642fcb5 Removed funky search cookie 2025-10-04 01:04:25 +01:00
snipe
59518ca2c5 Small style updates 2025-10-04 00:31:17 +01:00
snipe
dfb7c73069 Added column search to users 2025-10-04 00:31:06 +01:00
snipe
70eccceab3 Updated button class for add new 2025-10-04 00:18:09 +01:00
snipe
3f69b70367 Moved btn methods closer to the BS table 2025-10-03 23:54:38 +01:00
snipe
94a0a2f8be Fixed dropdown toggle 2025-10-03 23:50:40 +01:00
snipe
7e23596ab8 Translate export to CSV 2025-10-03 23:09:33 +01:00
snipe
3c58a5dd3d Fixed column selector 2025-10-03 20:36:52 +01:00
snipe
51789ccbf3 Got tooltips to work on built-in buttons! 2025-10-03 18:54:05 +01:00
snipe
6ae4a9aa1a Added tooltips to custom btn methods 2025-10-03 17:14:47 +01:00
snipe
0aebd669b2 Fixed accessor for console view 2025-10-03 16:25:31 +01:00
snipe
2a92c4899d Don’t use parenthases unless a manufacturer is given 2025-10-03 16:19:00 +01:00
snipe
530089895a Added category and manufacturer to expiring license report 2025-10-03 16:13:00 +01:00
snipe
ae8289fc8c Eager load cagory and manufacturer 2025-10-03 16:12:40 +01:00
snipe
90f4dfb48b Use regular blade syntax 2025-10-03 16:03:03 +01:00
snipe
ec2eddf538 Add order scope to query 2025-10-03 16:02:07 +01:00
snipe
44d31d4b39 Moved date fields closer together 2025-10-03 16:01:49 +01:00
snipe
4934e7666c Added text string 2025-10-03 16:01:38 +01:00
snipe
35bf0d020e Added breadcrumb to expiring 2025-10-03 16:01:27 +01:00
snipe
4934dc85ac Reverse expires diff direction 2025-10-03 16:00:58 +01:00
snipe
5ceb50d7e5 Added expiring licenses to API and UI 2025-10-03 16:00:23 +01:00
snipe
ae7ccbb7bd Fixed icons 2025-10-03 15:59:55 +01:00
snipe
1cd9fc47aa Use diff_in_days instead 2025-10-03 15:03:14 +01:00
snipe
013c50607a Put old setter back because reasons? 2025-10-03 14:46:32 +01:00
snipe
d7bf9b7f2e Added more accessors and mutators 2025-10-03 14:38:37 +01:00
snipe
4702fdddc6 Nicer output in console command 2025-10-03 14:38:08 +01:00
snipe
dd06a530c0 Added Terminates string 2025-10-03 14:37:43 +01:00
snipe
c36125dc95 Added CSS to the message blade 2025-10-03 14:37:25 +01:00
snipe
ae43f93d0a Improved expiring assets and licenses email 2025-10-03 14:37:12 +01:00
snipe
8918b17f77 Updated test 2025-10-03 14:36:16 +01:00
snipe
dfd05e8b5b Refactored scope 2025-10-03 14:13:58 +01:00
snipe
3daa6dd051 Added accessors for termination date 2025-10-03 14:13:28 +01:00
snipe
14e43192e6 Merge remote-tracking branch 'origin/develop' 2025-10-03 09:17:35 +01:00
snipe
6cf88b1792 Merge pull request #17978 from marcusmoore/17205-replace-form-email-format
Fixed #17205 - replace Form:: email_format
2025-10-03 09:15:49 +01:00
snipe
6b9839367f Merge pull request #17973 from marcusmoore/fixes/17972-update-last-checkin-upon-edit
Fixed #17972 - set last_checkin if asset is checked in during an update
2025-10-03 09:15:35 +01:00
snipe
34fcf5d616 Merge pull request #17974 from marcusmoore/replace-form-checkbox
Replaced Form::checkbox with raw html
2025-10-03 09:15:12 +01:00
snipe
1cf3c74e67 Merge pull request #17979 from akemidx/term-date-on-license-report
Fixed #17977: Term date on license report
2025-10-03 09:14:07 +01:00
snipe
16b57b931e Merge pull request #17980 from marcusmoore/17206-replace-name-display-format-macro
Fixed #17206 - replace Form::name_display_format macro
2025-10-03 09:13:23 +01:00
Marcus Moore
3457e7d617 Remove Form::name_display_format macro 2025-10-02 16:08:37 -07:00
Marcus Moore
edbe8001e6 Replace Form::name_display_format 2025-10-02 16:08:16 -07:00
akemidx
71644c1cbe added term date 2025-10-02 19:00:18 -04:00
Marcus Moore
03fd8df8bd Remove Form::email_format 2025-10-02 15:52:09 -07:00
Marcus Moore
71d622b6dd Replace Form:: email_format 2025-10-02 15:51:41 -07:00
Marcus Moore
689d5a2d58 Remove digit_separator form macro 2025-10-02 15:39:28 -07:00
Marcus Moore
b2e9eb866c Replace digit_separator form macro
Fixes #17204
2025-10-02 15:38:59 -07:00
Marcus Moore
c8b7782d1d Remove date_display_format and time_display_format macros 2025-10-02 15:27:20 -07:00
Marcus Moore
673f936689 Replace Form:: time_display_format on localization screen 2025-10-02 15:26:31 -07:00
Marcus Moore
2ca0d39e51 Replace Form:: date_display_format on localization screen 2025-10-02 15:21:51 -07:00
Marcus Moore
908c8bc397 Remove Form::checkbox on user create screen 2025-10-02 14:33:48 -07:00
Marcus Moore
93082e1e87 Set last_checkin if asset checked in during update 2025-10-02 13:56:42 -07:00
Marcus Moore
ef0a6aa25e Add failing condition 2025-10-02 13:53:19 -07:00
spencerrlongg
b9f4dc1e9d translation 2025-10-02 12:00:52 -05:00
snipe
90afec864e Fixed info text help block class 2025-10-02 08:07:54 +01:00
akemidx
b082fb6692 investigation and notes 2025-10-01 18:21:25 -04:00
Marcus Moore
4bbbd786cd Constrain to "uploaded" action_type 2025-10-01 14:37:36 -07:00
snipe
0c1b2a54e7 Merge remote-tracking branch 'origin/develop' 2025-10-01 22:27:06 +01:00
snipe
a6ded20ede Merge pull request #17966 from uberbrady/fix_filetype_validation
Cleanups and improvements to output on snipeit:restore command
2025-10-01 22:26:49 +01:00
Brady Wetherington
9b96314371 Cleanups and improvements to output on snipeit:restore command 2025-10-01 22:07:45 +01:00
snipe
85e16ecd51 Merge remote-tracking branch 'origin/develop' 2025-10-01 21:34:50 +01:00
snipe
2ac36cdfd6 Fixed alignment on create-new dropdown in header 2025-10-01 21:32:28 +01:00
snipe
e40c532354 Merge remote-tracking branch 'origin/develop' 2025-10-01 12:13:08 +01:00
snipe
9404dff79c Added note about markdown 2025-10-01 12:12:56 +01:00
snipe
55b324d8d6 Merge remote-tracking branch 'origin/develop' 2025-10-01 11:49:15 +01:00
snipe
3ed2e2d79e Added link to docs for common issues in upgrading script 2025-10-01 11:49:05 +01:00
snipe
3152d9eadd Merge remote-tracking branch 'origin/develop' 2025-10-01 11:24:33 +01:00
snipe
f1266ab5d6 Check if telescope tables already exist 2025-10-01 11:24:20 +01:00
snipe
e70f1408aa Merge remote-tracking branch 'origin/develop' 2025-10-01 11:19:35 +01:00
snipe
664e3984e3 Merge pull request #17877 from grokability/label-cjk-fix
Fixed CJK on labels
2025-10-01 11:16:55 +01:00
snipe
665c13e238 Merge pull request #17957 from marcusmoore/fixes/17956-handle-force-deleted-model-in-asset-edit
Fixed #17956 - handle accessing deleted model during asset update
2025-10-01 11:10:11 +01:00
snipe
8a667b20c2 Merge branch 'develop' into fixes/17956-handle-force-deleted-model-in-asset-edit 2025-10-01 11:09:40 +01:00
snipe
3693241292 Merge pull request #17959 from marcusmoore/fixes/17958-handle-force-deleted-model-in-bulk-edit
Fixes #17958 - handle accessing deleted model during bulk asset update
2025-10-01 11:06:32 +01:00
akemidx
a384245368 investigation and notes 2025-09-30 16:33:33 -04:00
Marcus Moore
3c3acff79b Fix more attempted access of deleted model 2025-09-30 12:22:26 -07:00
Marcus Moore
e15de83a95 Fix attempted access of deleted model 2025-09-30 12:19:12 -07:00
Marcus Moore
636fccbf97 Add failing test 2025-09-30 12:18:51 -07:00
Marcus Moore
7d8ed399a8 Fix accessing force deleted model 2025-09-30 11:27:56 -07:00
Marcus Moore
272385db6c Add failing test 2025-09-30 11:27:38 -07:00
snipe
9334b8df47 Improve Bug Report template details
Updated version placeholders and descriptions for clarity.
2025-09-30 11:57:06 +01:00
snipe
e0bc2ae86f Add PHP and Composer version fields to bug report 2025-09-30 11:45:55 +01:00
snipe
2f019bb033 Merge remote-tracking branch 'origin/develop' 2025-09-30 11:40:34 +01:00
snipe
291be64aa0 Refined remnaining asset count for archived 2025-09-30 11:40:21 +01:00
snipe
75c83236ff Merge remote-tracking branch 'origin/develop' 2025-09-30 11:20:44 +01:00
snipe
72be171917 Added archived to model view 2025-09-30 11:20:33 +01:00
snipe
4ddee4ac40 Merge remote-tracking branch 'origin/develop' 2025-09-30 10:55:00 +01:00
snipe
43cd0d7eb3 Use min_amt formatter 2025-09-30 10:54:43 +01:00
snipe
6b693e2644 Merge remote-tracking branch 'origin/develop' 2025-09-30 10:51:35 +01:00
snipe
eeea69d8f2 Make available_assets_count sortable 2025-09-30 10:51:25 +01:00
snipe
72cf921a4b Merge remote-tracking branch 'origin/develop' 2025-09-30 10:46:12 +01:00
spencerrlongg
32882f81e7 replace box-default 2025-09-29 15:52:43 -05:00
spencerrlongg
36f5099932 added new counts and throw new exceptions and catch them 2025-09-29 15:16:41 -05:00
snipe
bec88a0441 Merge pull request #17950 from grokability/#17932-fix-remaining-counts-in-model-listing
Fixed #17932 - incorrect number for remaining assets in asset models
2025-09-29 20:55:52 +01:00
snipe
6e67e3a8a0 Fixed #17932 - incorrect number for remaining assets in asset models 2025-09-29 20:55:06 +01:00
snipe
4dc3c30354 Merge remote-tracking branch 'origin/develop' 2025-09-29 19:04:42 +01:00
snipe
947ccf911d Merge pull request #17868 from Godmartinz/adds-Tze_24mm-variant
Adds Brother Label TZe_24mm_E variant
2025-09-29 15:48:10 +01:00
snipe
06f313febe Merge pull request #17869 from marcusmoore/api-components-assigned-to-asset
Added api endpoint for retrieving components checked out to asset
2025-09-29 15:47:53 +01:00
snipe
b387136b8f Merge pull request #17883 from akemidx/purchasepricereportfilter
FEATURE: Purchase Cost Report Filter
2025-09-29 15:37:39 +01:00
snipe
31614c5da1 Merge pull request #17888 from marcusmoore/fixes/bulk-checkout-extra-requests
Fixed excessive api requests on bulk checkout page
2025-09-29 14:46:19 +01:00
snipe
146b5a3085 Merge pull request #17933 from marcusmoore/17914-bulk-checkout-error-ux
Fixed #17914 - Improve UX around attempted bulk checkout of assigned assets
2025-09-29 14:44:20 +01:00
snipe
397cc1754a Merge remote-tracking branch 'origin/develop' 2025-09-29 11:05:45 +01:00
snipe
ff1297cac5 Merge pull request #17945 from kingspride/develop
with --no-interactive, make composer non-interactive aswell
2025-09-29 11:04:30 +01:00
William Kirstaedter
8af3cf4056 with --no-interactive, make composer non-interactive aswell 2025-09-29 11:39:23 +02:00
Marcus Moore
9edec9e212 Extract translation 2025-09-25 11:09:27 -07:00
snipe
be4362c59a Merge pull request #17925 from Godmartinz/fix-factory-auto-gen-action-logs
Adds option to disable auto generating action log from acceptance factory
2025-09-25 11:10:34 +01:00
Marcus Moore
8461b147de Link to removed assets 2025-09-24 16:38:48 -07:00
Godfrey M
82bdd43168 renamed variable 2025-09-24 15:38:30 -07:00
Godfrey M
533d82d4d8 remove unnecessary changes 2025-09-24 15:34:02 -07:00
Godfrey M
6f990dd1de adds an option to disable Auto assigned an actionlogs in factories 2025-09-24 15:27:16 -07:00
Marcus Moore
be848598e3 Keep removed asset out of scope of partial 2025-09-24 14:32:25 -07:00
snipe
62a58fa23b Merge remote-tracking branch 'origin/develop' 2025-09-24 14:52:55 +01:00
snipe
7d0742054f Merge pull request #17923 from uberbrady/fix_checkout_type_selector2
Fixed #17919 - correct the behavior of the checkout type selector
2025-09-24 14:52:05 +01:00
Brady Wetherington
dcf7e83507 Remove extra pointless class="active" 2025-09-24 14:47:13 +01:00
Brady Wetherington
407c2bf0c8 Switch to ?: from ?? to better handle empty strings 2025-09-24 14:45:43 +01:00
Brady Wetherington
c46227ee94 Fix to the checkout-selector issue 2025-09-24 14:28:49 +01:00
snipe
d8171eb056 Remove duplicate PUT route for hardware assets
Removed duplicate route definition for updating hardware assets.
2025-09-23 13:24:56 +01:00
snipe
c67ca500db Clarify descriptions for multiple company support 2025-09-23 13:06:33 +01:00
snipe
0081e7b731 Revise demo reproduction question in Bug Report template 2025-09-23 13:05:26 +01:00
snipe
4e6483d3ed Enhance Bug Report template with required validations
Make fields required
2025-09-23 13:04:19 +01:00
snipe
0ddf0002c4 Change dropdown options to single quotes in Bug Report
Fixed quotes
2025-09-23 13:02:36 +01:00
snipe
ad0daf33b9 Add dropdown options to Bug Report template
Added additional fields to bug report template related to FMCS
2025-09-23 13:01:37 +01:00
Marcus Moore
c614c44d4c Remove assigned assets from bulk checkout 2025-09-22 16:04:29 -07:00
snipe
8a46579588 Merge pull request #17887 from marcusmoore/fixes/17404-prevent-bulk-checkout-across-companies
Fixed #17404 - Disallow bulk checkout of assets across companies
2025-09-22 18:57:43 +01:00
Marcus Moore
fb9fb9c097 Merge branch 'develop' into fixes/17404-prevent-bulk-checkout-across-companies
# Conflicts:
#	app/Http/Controllers/Assets/BulkAssetsController.php
#	tests/Feature/Checkouts/Ui/BulkAssetCheckoutTest.php
2025-09-22 10:52:04 -07:00
snipe
d9399534ce Merge pull request #17909 from spacjalex/17908-fix-typo
Fix #17908: typo in storage location of backups
2025-09-22 14:28:05 +01:00
spacjalex
17a749bbed fix typo 2025-09-22 15:23:14 +02:00
snipe
0b60c6a939 Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	public/css/build/app.css
#	public/css/build/app.css.map
#	public/css/build/overrides.css
#	public/css/build/overrides.css.map
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-09-19 12:44:49 +01:00
snipe
25ce63f00b Merge pull request #17904 from grokability/#17804-searchable-columns
Fixed #17804 - make columns searchable in column picker
2025-09-19 12:43:46 +01:00
snipe
2462bc05b3 Added column search to additional views 2025-09-19 12:41:30 +01:00
snipe
c3748da0b1 Fixed #17804 - make columns searchable in column picker 2025-09-19 12:16:56 +01:00
snipe
90c242a441 Merge pull request #17897 from marcusmoore/fixes/17896-prevent-bulk-checkout-of-checked-out-assets
Fixed #17896 - Prevent assigned assets from being bulk checked out
2025-09-19 07:03:43 +01:00
Marcus Moore
52239a88b5 Improve test name 2025-09-18 17:27:17 -07:00
Marcus Moore
7a3596c86d Test against other types 2025-09-18 17:21:27 -07:00
Marcus Moore
ac8a9e38f0 Implement fix 2025-09-18 17:18:27 -07:00
Marcus Moore
5c08f3a27e Add failing test 2025-09-18 17:14:33 -07:00
Marcus Moore
2dc11a84bf Fix test name 2025-09-18 17:05:08 -07:00
Marcus Moore
2960ea15f5 Consolidate to data provider 2025-09-18 14:29:12 -07:00
Marcus Moore
17aab4c490 Implement test 2025-09-18 14:20:05 -07:00
Marcus Moore
59d0f0d292 Re-order assertions 2025-09-18 14:05:13 -07:00
Marcus Moore
27d13a113a Implement test 2025-09-18 14:01:44 -07:00
Marcus Moore
c58e999fbb Scaffold tests 2025-09-18 13:11:06 -07:00
Marcus Moore
a02a96d5c4 Extract translation string 2025-09-18 12:57:56 -07:00
Marcus Moore
47e9e4704d Improve error message 2025-09-18 12:56:36 -07:00
Marcus Moore
b2ad9d404e Fix re-population of assets 2025-09-18 12:38:11 -07:00
snipe
925d48640d Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	config/version.php
2025-09-18 14:49:56 +01:00
snipe
5216dd75bf Bumped version 2025-09-18 14:49:15 +01:00
snipe
028b4e7b79 Merge remote-tracking branch 'origin/develop' 2025-09-18 13:59:25 +01:00
snipe
b8b45d2d81 Merge pull request #17892 from grokability/#17891-fixes-maintenance-file-route
Fixed #17891 - missing maintenance file deletion route
2025-09-18 13:59:10 +01:00
snipe
4b2b2cb68e Fixed #17891 - missing maintenance file deletion route 2025-09-18 13:58:30 +01:00
snipe
625a46a2c2 Merge remote-tracking branch 'origin/develop' 2025-09-18 13:52:08 +01:00
snipe
be4ace293e Use trans_choice for user acceptance 2025-09-18 13:51:57 +01:00
snipe
ebc1e27c22 Merge remote-tracking branch 'origin/develop' 2025-09-18 13:40:07 +01:00
snipe
764b363bbc A few small tweaks to acceptance screen design 2025-09-18 13:38:37 +01:00
Marcus Moore
705474dc14 Avoid pre-loading all assets on page load 2025-09-17 16:56:37 -07:00
Marcus Moore
e639d7726b Disallow bulk checkout across companies 2025-09-17 14:32:27 -07:00
snipe
357e85d358 Merge remote-tracking branch 'origin/develop' 2025-09-17 22:02:11 +01:00
snipe
9da9166442 Merge pull request #17886 from grokability/small-tweaks-to-acceptance-pdf
Small adjustments for acceptance PDF layout
2025-09-17 22:01:41 +01:00
snipe
8ea339f0ef More small tweaks 2025-09-17 22:00:49 +01:00
Marcus Moore
e29b0aa6a4 Add todo 2025-09-17 13:55:54 -07:00
Marcus Moore
d2157868f2 Populate failing test 2025-09-17 13:49:32 -07:00
snipe
89b36ba63f Derp. Uncomment the acceptance. 2025-09-17 21:43:40 +01:00
snipe
1d3dfa1fa4 Pull the acceptance stuff into the model 2025-09-17 21:43:17 +01:00
Marcus Moore
89cfafd933 Scaffold test 2025-09-17 13:40:34 -07:00
snipe
ca567eec8a Small adjustments for layout 2025-09-17 21:08:13 +01:00
snipe
75cfcb83aa Merge remote-tracking branch 'origin/develop' 2025-09-17 14:05:06 +01:00
snipe
41da31c379 Merge pull request #17885 from grokability/#8859-show-cost-footer-on-models
Fixed #8859 - adds purchase sums on model view
2025-09-17 14:04:38 +01:00
snipe
e81f63f46b Fixed #8859 - adds purchase sums on model view 2025-09-17 14:03:48 +01:00
snipe
ade03e4827 Merge pull request #17882 from Godmartinz/add-total-cost-columns
Adds total cost to Accessories, Consumables, Components
2025-09-17 13:56:08 +01:00
snipe
63a4d1ad33 Merge remote-tracking branch 'origin/develop' 2025-09-17 11:44:41 +01:00
snipe
33a4c88c3a Added table to deleted_at clauses to resolve ambiguity 2025-09-17 11:44:28 +01:00
akemidx
69c5dbfc23 formatting 2025-09-17 05:39:45 -04:00
akemidx
cf1bccfd65 prep for val 2025-09-16 15:24:44 -04:00
akemidx
99acf018f1 validation rule & query 2025-09-16 15:17:59 -04:00
snipe
f04d6f37e5 Merge remote-tracking branch 'origin/develop' 2025-09-16 19:22:06 +01:00
snipe
1f79776b8f Pull HTML tags out before converting markdown 2025-09-16 19:21:39 +01:00
Godfrey M
11e5f851f0 typo 2025-09-16 10:49:33 -07:00
Godfrey M
4ca1db8a1b remove footer formatter from consumable purchase cost 2025-09-16 10:43:02 -07:00
Godfrey M
14b829aa30 add total cost to components and consumables 2025-09-16 10:37:32 -07:00
Godfrey M
384652b3df add total cost to accessories 2025-09-16 10:10:49 -07:00
snipe
469069b471 Merge remote-tracking branch 'origin/develop' 2025-09-16 14:33:57 +01:00
snipe
9db65c6ae9 Merge pull request #17881 from grokability/#17873-eula-tab-on-users
Fixed #17873 - Added EULA tab to user view
2025-09-16 14:28:50 +01:00
snipe
1346e33e99 Check that the person trying to download can see both the user and the target 2025-09-16 14:21:03 +01:00
snipe
ab9cc447aa Use more specific filename 2025-09-16 14:20:20 +01:00
akemidx
cb63c12d2f i think this is gonna need livewire to validate lol 2025-09-16 08:24:22 -04:00
snipe
fe9e0444b4 Added EULA tab to user view 2025-09-16 13:20:50 +01:00
akemidx
6ce0fd20ce works, needs error handling 2025-09-16 08:11:42 -04:00
snipe
a18957dbe9 Include output even if there is nothing to send 2025-09-16 12:33:06 +01:00
snipe
13d5b724ee Fixed tests 2025-09-16 12:16:18 +01:00
snipe
b383cd9493 Fixed CJK on labels 2025-09-16 12:10:01 +01:00
snipe
c7d8203da9 Merge pull request #17866 from grokability/_reworked_tcpdf
Fixed #14744 and #17808 - Added CJK and Arabic font support for asset acceptance
2025-09-16 12:04:15 +01:00
snipe
96b5c1d8e1 Merge pull request #17876 from uberbrady/add_users_location_index
Add new index to users over deleted_at and location_id
2025-09-16 12:03:45 +01:00
Brady Wetherington
882ee80424 Add new index to users over deleted_at and location_id 2025-09-16 11:54:24 +01:00
snipe
e977771fe4 One more query tweak 2025-09-16 11:50:50 +01:00
snipe
4339e4552e Fixed nesting in orWhere 2025-09-16 11:50:50 +01:00
snipe
9bca5912d9 Merge remote-tracking branch 'origin/develop' 2025-09-16 11:39:12 +01:00
snipe
b54d222943 One more query tweak 2025-09-16 11:39:02 +01:00
snipe
23756ba1c7 Merge remote-tracking branch 'origin/develop' 2025-09-16 11:27:27 +01:00
snipe
e4e613550a Merge pull request #17875 from grokability/fixed-eol-query
Fixed nesting in orWhere
2025-09-16 11:27:07 +01:00
snipe
d1207444db Fixed nesting in orWhere 2025-09-16 11:20:49 +01:00
Marcus Moore
06f060161d Apply offset and limit 2025-09-15 15:43:54 -07:00
Marcus Moore
73e0628124 Populate test 2025-09-15 15:26:30 -07:00
Marcus Moore
7393c4170b Apply first pass and scaffold additional test 2025-09-15 15:22:35 -07:00
Marcus Moore
73e185bf9d Scaffold route and tests 2025-09-15 15:12:05 -07:00
snipe
0bad75b263 Fixed declined asset name 2025-09-15 20:56:57 +01:00
snipe
74b98083e2 Override the getEula() method at the SnipeModel level 2025-09-15 20:05:35 +01:00
snipe
9034b5ec11 Slightly nicer display 2025-09-15 20:04:17 +01:00
Godfrey M
77153c3e78 adds Tze_24mm variant 2025-09-15 11:20:35 -07:00
snipe
927f557672 Fixed sorting on updated/created at 2025-09-15 18:13:46 +01:00
snipe
86fb089901 Remove unused blades 2025-09-15 18:13:32 +01:00
snipe
630ea05e17 A little more cleanup 2025-09-15 17:16:05 +01:00
snipe
7df5196083 A little cleanup 2025-09-15 16:09:49 +01:00
snipe
01f7b5d709 Added CJK and Arabic font support 2025-09-15 14:46:11 +01:00
snipe
07227887f6 Merge remote-tracking branch 'origin/develop' 2025-09-15 14:42:08 +01:00
snipe
ec47ee3573 Merge pull request #17850 from Godmartinz/change-purchase-cost-to-unit
Rewords Purchase cost to Unit Cost for Accessories, Components, Consumables
2025-09-15 14:20:04 +01:00
snipe
13d3b103f1 Merge remote-tracking branch 'origin/develop' 2025-09-15 13:43:23 +01:00
snipe
7062962cc8 Show warnings on the dates if expired or terminated 2025-09-15 13:43:08 +01:00
snipe
fde447846a Merge pull request #17865 from grokability/show-inactive-licenses
Show inactive licenses
2025-09-15 13:42:22 +01:00
snipe
319cb1bd1e Removed logging 2025-09-15 13:23:44 +01:00
snipe
58cda5ae6d Added scopes 2025-09-15 13:22:25 +01:00
snipe
251a3db880 Fixed factory 2025-09-15 13:20:34 +01:00
snipe
30b6dcd767 Added status to breadcrumbs 2025-09-15 13:13:50 +01:00
snipe
05f6622912 Added status to URL 2025-09-15 13:13:32 +01:00
snipe
36183ac19d Fixed url and tooltip text 2025-09-15 13:13:18 +01:00
snipe
f6a823e0a8 Escape text 2025-09-15 13:13:04 +01:00
snipe
312353551d Changed button color 2025-09-15 13:11:58 +01:00
snipe
4bdfd0e115 Merge remote-tracking branch 'origin/develop' 2025-09-15 10:32:08 +01:00
snipe
fd5c9cee38 Merge pull request #17863 from grokability/added-status-label-to-asset-view
Added status label to asset view
2025-09-15 10:31:54 +01:00
snipe
84bf71802c Added status label to asset view 2025-09-15 10:31:06 +01:00
snipe
786b20708e Merge remote-tracking branch 'origin/develop' 2025-09-15 08:38:56 +01:00
snipe
35739c2eef Merge pull request #17835 from marcusmoore/feature/10052-assigned-assets-via-api
Fixed #10052 - Added api endpoint for retrieving assets checked out to asset
2025-09-15 08:38:35 +01:00
snipe
1914a71623 Merge pull request #17851 from marcusmoore/fixes/qty-in-component-email
Fixed potentially incorrect qty in component checkout email
2025-09-15 08:37:48 +01:00
snipe
dcc53886d9 Merge pull request #17857 from uberbrady/fix_client_tls_ldap
Fixed #17414 - client-side TLS certificate didn't work in Google LDAP
2025-09-15 08:37:14 +01:00
Brady Wetherington
21ef87ef09 Merge branch 'develop' into fix_client_tls_ldap 2025-09-12 18:52:43 +01:00
snipe
0e957cad84 Merge remote-tracking branch 'origin/develop' 2025-09-12 18:08:15 +01:00
snipe
b67f808da9 Fixed fieldname 2025-09-12 18:07:40 +01:00
snipe
ad69447b53 Merge pull request #17858 from grokability/ignore-expiring-licenses-with-past-termination-date
Ignore expiring licenses with past termination date
2025-09-12 17:55:14 +01:00
snipe
b4614df88c Removed awkward period 2025-09-12 17:39:52 +01:00
snipe
7171247cdc Updated tests 2025-09-12 17:37:36 +01:00
snipe
6d0084f108 Nicer alert layout for expiring asset report 2025-09-12 17:37:27 +01:00
snipe
29359f42ae Added table printout 2025-09-12 17:37:12 +01:00
snipe
cf875bf872 Changed verbiage 2025-09-12 17:37:03 +01:00
snipe
13c0d335d3 Refactor alert query 2025-09-12 17:36:51 +01:00
snipe
ceb33409b5 Removed duplicate array key 2025-09-12 17:36:33 +01:00
Brady Wetherington
83597d4a8b Possible fix to client-side TLS certificate issue for google LDAP 2025-09-12 17:12:35 +01:00
Marcus Moore
81eefc5448 Merge branch 'develop' into fixes/qty-in-component-email 2025-09-11 14:14:42 -07:00
Marcus Moore
082bc3ece4 Use correct qty in component checkout email 2025-09-11 14:10:31 -07:00
snipe
b2406b61fb Merge pull request #17826 from marcusmoore/fixes/17585-accessory-checkout-amount-in-subject
Fixed #17585 - Display accessory checkout qty in subject line and intro text
2025-09-11 21:55:20 +01:00
Marcus Moore
15698d7694 Update introduction lines with qty 2025-09-11 13:50:48 -07:00
Godfrey M
990cd82f97 change purchase cost to unit cost in several places 2025-09-11 10:46:20 -07:00
snipe
c87829b3e8 Merge pull request #17849 from Godmartinz/rollbar-unreassignablecount-fix
Fixed typo in UnreassignableCount
2025-09-11 18:43:07 +01:00
Godfrey M
6799c41d65 fixed typo 2025-09-11 10:23:53 -07:00
snipe
80c059be58 Skip terminated licenses in alert 2025-09-11 13:14:53 +01:00
snipe
aa3f896538 Merge pull request #17842 from Godmartinz/expiration-and-termination-license-enhancement
Adds #8799 Prevention of checkout of expired or terminated licenses
2025-09-11 11:46:15 +01:00
snipe
85c728f313 Merge remote-tracking branch 'origin/develop' 2025-09-11 11:40:26 +01:00
snipe
d8c17a8a5e One more small tweak to language 2025-09-11 11:40:15 +01:00
akemidx
50e210b2db fixing naming convention to match 2025-09-10 17:35:09 -04:00
Godfrey M
850939367c adds translation warning message 2025-09-10 12:59:35 -07:00
Marcus Moore
bf4fef9bf7 Merge branch 'develop' into fixes/17585-accessory-checkout-amount-in-subject 2025-09-10 12:49:41 -07:00
Godfrey M
d5175961a4 adds a seperate formatter for checking out on license index, fix interactions 2025-09-10 12:42:10 -07:00
snipe
ba3fb8cd66 Merge remote-tracking branch 'origin/develop' 2025-09-10 20:19:55 +01:00
snipe
e7e1d6a232 Small tweaks to language 2025-09-10 20:17:33 +01:00
snipe
712345f3a0 Added link to discussions and discord 2025-09-10 20:14:19 +01:00
snipe
54c9bc3dcb Merge pull request #17840 from marcusmoore/issue-template
Made install method required in issue template
2025-09-10 20:14:10 +01:00
Godfrey M
e796c0da4a disallows checkout of expired or terminated licenses 2025-09-10 12:08:14 -07:00
Marcus Moore
cf8ff0f43e Add note on English 2025-09-10 10:41:20 -07:00
Marcus Moore
e67ce23a7c Require installation method 2025-09-10 10:23:22 -07:00
snipe
d9fb7dc754 Merge remote-tracking branch 'origin/develop' 2025-09-10 15:48:31 +01:00
snipe
a66bb95a81 A few more tweaks 2025-09-10 15:48:21 +01:00
snipe
2249dad9d7 Merge remote-tracking branch 'origin/develop' 2025-09-10 15:43:21 +01:00
snipe
c66fa33b2e Have I mentioned how much I hate YAML? 2025-09-10 15:43:09 +01:00
snipe
5c4fa630ae Merge remote-tracking branch 'origin/develop' 2025-09-10 15:40:24 +01:00
snipe
56eebb9db4 Small tweaks to templates 2025-09-10 15:40:12 +01:00
snipe
ede74ad24e Merge remote-tracking branch 'origin/develop' 2025-09-10 15:38:30 +01:00
snipe
d9773f107e Added feature request form
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:38:19 +01:00
snipe
27542a8f91 Okay, I guess we can’t require that field :(
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:34:30 +01:00
snipe
5dc07b94aa Merge remote-tracking branch 'origin/develop' 2025-09-10 15:33:20 +01:00
snipe
e09112f46a Require installation type
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:33:06 +01:00
snipe
d7acf721ae Merge remote-tracking branch 'origin/develop' 2025-09-10 15:30:54 +01:00
snipe
50a17a82b6 Sigh
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:30:42 +01:00
snipe
eff5232828 Merge remote-tracking branch 'origin/develop' 2025-09-10 15:26:14 +01:00
snipe
7eb032d646 Rename issue template (I guess?)
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:25:51 +01:00
snipe
3eb29b1cdb Merge remote-tracking branch 'origin/develop' 2025-09-10 15:21:05 +01:00
snipe
e065f22f8e Merge pull request #17838 from grokability/issue-form
New GH issue form
2025-09-10 15:20:51 +01:00
snipe
c1b4ba1f85 Last one for now
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:20:27 +01:00
snipe
eeaec471f0 Dunno if this will work
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:19:29 +01:00
snipe
0d3c8678d8 Moved text
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:17:16 +01:00
snipe
bbddf5f95b Still a few more tweaks
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:15:09 +01:00
snipe
3b8c8b3af9 Nicer markdown
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:12:38 +01:00
snipe
84753aa13f Added browser console
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:08:37 +01:00
snipe
90b84451d8 Fixed indenting
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:07:04 +01:00
snipe
54c8ae41cc Still hate YAML
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:06:03 +01:00
snipe
7d32b1a724 God I hate YAML
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:05:27 +01:00
snipe
69ffd63ca6 Removed backticks
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:02:50 +01:00
snipe
4857c19eb6 More tweaks
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 15:02:07 +01:00
snipe
d535e23da0 More template tweaks
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 14:51:33 +01:00
snipe
30e02544ab Try renaming so I can preview
via https://github.com/orgs/community/discussions/7039#discussioncomment-5327083

Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 14:43:16 +01:00
snipe
ee53925bd2 Starter for preview
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 14:40:38 +01:00
snipe
c9961f63b4 Merge remote-tracking branch 'origin/develop' 2025-09-10 13:53:21 +01:00
snipe
40495b8a17 Small demo tweaks
Signed-off-by: snipe <snipe@snipe.net>
2025-09-10 12:33:33 +01:00
akemidx
b1de98f05d first front end 2025-09-09 19:18:29 -04:00
Marcus Moore
6bc9a82a7a Formatting 2025-09-09 14:51:22 -07:00
snipe
09e843a800 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/app.css.map
#	public/css/build/overrides.css
#	public/css/build/overrides.css.map
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-09-09 22:34:33 +01:00
Marcus Moore
6504ee37bd Remove dump 2025-09-09 13:09:52 -07:00
Marcus Moore
082bff2fa8 Add and use query scope helper 2025-09-09 13:04:14 -07:00
Marcus Moore
ab7bd86336 Improve assertions 2025-09-09 13:01:59 -07:00
Marcus Moore
eada0b0bb5 Implement test 2025-09-09 11:58:43 -07:00
Marcus Moore
f221f9f22a Implement test 2025-09-09 11:50:41 -07:00
snipe
6731e44a0d Merge pull request #17828 from marcusmoore/fixes/17586-acceptance-banner-qty
Fixed #17586 - Display accurate quantity in banner
2025-09-09 19:38:34 +01:00
Marcus Moore
acc37045e4 Implement test 2025-09-09 11:34:50 -07:00
Marcus Moore
a7c5899c16 Implement test 2025-09-09 11:33:54 -07:00
Marcus Moore
80b02635a9 Add test stub 2025-09-09 10:10:53 -07:00
snipe
f90de5ec67 Merge pull request #17833 from grokability/tighter-controls-on-licenses
Tighter controls on license deletion, also fixes #16227 adding more tables to location print
2025-09-09 15:35:12 +01:00
snipe
9a3e046530 Fixed HTML
Signed-off-by: snipe <snipe@snipe.net>
2025-09-09 15:30:34 +01:00
snipe
7f56e461fe Added child location icon
Signed-off-by: snipe <snipe@snipe.net>
2025-09-09 15:19:00 +01:00
snipe
1da37e0d38 Added child location count
Signed-off-by: snipe <snipe@snipe.net>
2025-09-09 15:18:47 +01:00
snipe
0004d4936c Cleaned up print view
Signed-off-by: snipe <snipe@snipe.net>
2025-09-09 15:18:28 +01:00
snipe
7a6fdc4e0a Added parent ID to location API
Signed-off-by: snipe <snipe@snipe.net>
2025-09-09 15:18:09 +01:00
snipe
2eb727bd0c Added tests
Signed-off-by: snipe <snipe@snipe.net>
2025-09-09 13:51:56 +01:00
snipe
57af507170 Added deleted button to locations, check for additional relations
Signed-off-by: snipe <snipe@snipe.net>
2025-09-09 12:20:34 +01:00
snipe
e37f87465c Merge pull request #17827 from Godmartinz/duplicate-emails-fix
Fixed #17756 - Duplicate checkout emails
2025-09-09 09:33:03 +01:00
Marcus Moore
324070f345 Begin to build out test 2025-09-08 17:20:22 -07:00
Marcus Moore
e1aa843b6d Scaffold test 2025-09-08 17:05:14 -07:00
Godfrey M
e652a7fd61 fix tests 2025-09-08 14:56:15 -07:00
Marcus Moore
2397bfbad0 Improve variable name 2025-09-08 14:51:19 -07:00
Marcus Moore
7e2bc8e452 Display accurate quantity in banner 2025-09-08 14:48:11 -07:00
Godfrey M
00e8fd0483 cloned mailable 2025-09-08 14:44:58 -07:00
Marcus Moore
6d8bf2c665 Display accessory checkout qty in subject line 2025-09-08 14:25:56 -07:00
snipe
77b79dbd95 Merge remote-tracking branch 'origin/develop' 2025-09-08 15:04:42 +01:00
snipe
72466f1aab Revert "Merge pull request #17823 from grokability/#17822-fix-n+1-in-topmenu"
This reverts commit 6901deccbf, reversing
changes made to 6b87c90e02.

Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 15:04:28 +01:00
snipe
dafc6c5136 Merge remote-tracking branch 'origin/develop' 2025-09-08 14:34:16 +01:00
snipe
6901deccbf Merge pull request #17823 from grokability/#17822-fix-n+1-in-topmenu
Fixed #17822 - n+1 in top menu check
2025-09-08 14:33:47 +01:00
snipe
5a9c906eb9 Added filter to assets loading
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 14:32:58 +01:00
snipe
b95b60b49e Use eager-loaded model assets
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 14:07:49 +01:00
snipe
14408ef18f Fixed n+1 in top menu check
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 13:55:22 +01:00
snipe
c790147a5c Merge remote-tracking branch 'origin/develop' 2025-09-08 13:33:29 +01:00
snipe
6b87c90e02 Use scope for assets for show in sidebar
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 13:33:19 +01:00
snipe
80c39c5ef3 Merge remote-tracking branch 'origin/develop' 2025-09-08 12:17:41 +01:00
snipe
2b4d5222eb Made “add new” buttons clearer on table headers
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 12:17:29 +01:00
snipe
9604ecebad Fixed jobtitle in advanced search
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 12:01:59 +01:00
snipe
9a3e84d84c Merge remote-tracking branch 'origin/develop' 2025-09-08 09:27:51 +01:00
snipe
0d67970a45 Update @swift2512 as a contributor 2025-09-08 09:27:34 +01:00
snipe
913b9f0c40 Reworks PR #15490 - adds location to inventory email
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 09:26:36 +01:00
snipe
610a5745f0 Merge pull request #17818 from grokability/dependabot/github_actions/develop/actions/stale-10
Bump actions/stale from 9 to 10
2025-09-08 09:17:19 +01:00
dependabot[bot]
dff12324c6 Bump actions/stale from 9 to 10
Bumps [actions/stale](https://github.com/actions/stale) from 9 to 10.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v9...v10)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '10'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 08:16:24 +00:00
snipe
f340390fc8 Merge pull request #17703 from marcusmoore/17369-accessory-checkout-qty-scaffold
Fixed issues around accessory acceptance
2025-09-08 09:09:26 +01:00
snipe
643960c829 Merge remote-tracking branch 'origin/develop' 2025-09-08 08:25:08 +01:00
snipe
be81e74921 Shifted log meta position
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 08:24:50 +01:00
snipe
1737018325 Merge remote-tracking branch 'origin/develop' 2025-09-08 08:14:45 +01:00
snipe
2bee8729e4 Better escaping for display_name in API controller
Signed-off-by: snipe <snipe@snipe.net>
2025-09-08 08:14:33 +01:00
snipe
5d03038734 Use auto-direction for <p> in preview
Signed-off-by: snipe <snipe@snipe.net>
2025-09-05 15:12:38 +01:00
snipe
75b11de0f4 Bumped composer packages
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 19:55:30 +01:00
snipe
484d5ba76e Merge remote-tracking branch 'origin/develop' 2025-09-04 17:15:53 +01:00
snipe
c5bede8594 More small display_name fixes
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 17:15:06 +01:00
snipe
798685d0b8 Merge remote-tracking branch 'origin/develop' 2025-09-04 16:56:55 +01:00
snipe
cd9ea6ae3b Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:56:43 +01:00
snipe
cb7654ae90 Merge remote-tracking branch 'origin/develop' 2025-09-04 16:55:57 +01:00
snipe
113b762ec7 Fix for null location in locations print
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:55:44 +01:00
snipe
78704d8b85 Fixed #17803 - checked out to name in custom report
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:51:10 +01:00
snipe
1109db76fe Merge pull request #17797 from grokability/#17796-search-on-model-name-and-number
Fixed #17796 - search on model name and number on importer
2025-09-04 16:35:43 +01:00
snipe
b1b390febf Removed flaky test
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:30:02 +01:00
snipe
be451fa0c0 Removed asset model from original factory
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:14:27 +01:00
snipe
1fa553c785 Use nths
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:04:16 +01:00
snipe
905f61371d Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 16:02:40 +01:00
snipe
7da5210a01 Switched to nth from fetchOne in CSV reader
Signed-off-by: snipe <snipe@snipe.net>
2025-09-04 12:50:12 +01:00
snipe
18172d3896 Merge pull request #17752 from marcusmoore/fixes/existing-assets-report
Improve expiring alerts notification layout
2025-09-04 12:33:32 +01:00
snipe
c28e78b9e2 Merge pull request #16063 from Godmartinz/checkin_non_reassignable_license
Allows check-ins of unreassignable licenses
2025-09-04 11:22:17 +01:00
snipe
e7827a3847 Merge pull request #17800 from marcusmoore/chore/test-action-logs
Upload log file in GitHub Action tests
2025-09-04 10:20:12 +01:00
Marcus Moore
039564e74c Wrap migration in check 2025-09-03 12:52:46 -07:00
Marcus Moore
e164595a0f Fall back to displaying 1 2025-09-03 12:35:18 -07:00
Marcus Moore
d29e09a3ff Merge branch 'develop' into 17369-accessory-checkout-qty-scaffold
# Conflicts:
#	resources/views/account/accept/create.blade.php
#	resources/views/account/accept/index.blade.php
2025-09-03 12:26:43 -07:00
Godfrey M
db9f85e9da Merge branch 'develop' into checkin_non_reassignable_license
# Conflicts:
#	app/Models/License.php
#	resources/views/licenses/view.blade.php
#	tests/Feature/Checkins/Api/LicenseCheckInTest.php
2025-09-03 11:09:51 -07:00
Marcus Moore
27022954b1 Inline icon 2025-09-03 10:44:19 -07:00
Marcus Moore
30362c924f Upload log as artifact 2025-09-03 10:13:01 -07:00
snipe
bf63b15b46 Merge pull request #17799 from grokability/#17798-adds-require-serial-to-importer
Fixed #17798 - added `require_serial` to model importer
2025-09-03 15:32:42 +01:00
snipe
19aea4bd6c Fixed #17798 - added require_serial to model importer
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 15:25:56 +01:00
snipe
090890e9c6 Fixed #17796 - search on model name and number on importer
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 15:15:29 +01:00
snipe
00c394345a Merge remote-tracking branch 'origin/develop' 2025-09-03 15:09:42 +01:00
snipe
605022a9e3 Merge pull request #17795 from grokability/#17791-larger-currency-field
Fixed #17791 - increase size of purchase cost field
2025-09-03 15:09:28 +01:00
snipe
b06c58fe7b Switch to older style rules for consistency
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 15:06:27 +01:00
snipe
f5c8b3eb04 Fixed #17791 - increase size of purchase cost field
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 15:03:49 +01:00
snipe
739980aa09 Merge pull request #16947 from Godmartinz/add-require-serial-to-models
Adds require serial as Asset Model option
2025-09-03 14:53:15 +01:00
snipe
afde5943e3 Fixed typo - #17784
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 14:39:59 +01:00
snipe
32300cb42c Merge pull request #17788 from grokability/add-delete-log-instead-of-soft-deleting-the-log-itself
Fixed #17777 - Log upload deletion
2025-09-03 14:37:32 +01:00
snipe
dffcb62fa1 Merge remote-tracking branch 'origin/develop' 2025-09-03 14:35:31 +01:00
snipe
de3b1697c8 Merge pull request #17760 from Godmartinz/fix-translation-string-in-notifications
Fixes #17759 translation used in asset check in/out notifications
2025-09-03 14:33:57 +01:00
snipe
8c668b72b7 Merge remote-tracking branch 'origin/develop' 2025-09-03 08:45:20 +01:00
snipe
a18fb10b5a Merge pull request #17783 from marcusmoore/fixes/company-in-location-print
Fixed company name reference in location print
2025-09-03 08:44:40 +01:00
snipe
52140dbe06 Log upload deletion
Signed-off-by: snipe <snipe@snipe.net>
2025-09-03 08:37:42 +01:00
Marcus Moore
db5bb1928e Merge branch 'develop' into fixes/existing-assets-report
# Conflicts:
#	resources/views/notifications/markdown/report-expiring-assets.blade.php
2025-09-02 15:07:23 -07:00
Marcus Moore
65b66beb07 Make icon more prominent 2025-09-02 14:55:46 -07:00
Marcus Moore
c83504b4e7 Use display_name in place of presenter 2025-09-02 12:57:40 -07:00
Godfrey M
cd2e7ee31d fix google and slack notifications 2025-09-02 10:53:42 -07:00
Godfrey M
c3a0a0415a fix MS Teams Notifications 2025-09-02 10:48:02 -07:00
snipe
709f4672b7 Merge pull request #17771 from grokability/#10107-remember-checkout-to-type
Fixed #10107 - remember checkout to type
2025-09-02 13:46:13 +01:00
snipe
b54ecd4da0 Merge remote-tracking branch 'origin/develop' 2025-09-02 13:45:41 +01:00
snipe
e6c030b050 Merge pull request #17781 from grokability/#17780-add-withtrashed-to-files
Fixed #17780 - Added `withTrashed()` to allow viewing files on deleted objects
2025-09-02 13:45:18 +01:00
snipe
7bd3a791a1 Added withTrashed() to allow viewing files on deleted objects
Signed-off-by: snipe <snipe@snipe.net>
2025-09-02 13:39:35 +01:00
snipe
87a7e3501b Merge remote-tracking branch 'origin/develop' 2025-09-02 11:30:44 +01:00
snipe
b9cfc03b4f display name fix
Signed-off-by: snipe <snipe@snipe.net>
2025-09-02 11:30:34 +01:00
snipe
daefec3013 Merge remote-tracking branch 'origin/develop' 2025-09-01 20:32:57 +01:00
snipe
131327a64d Fixed “undefined” error on status labels in BS tables
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 20:32:42 +01:00
snipe
183a9742c4 Merge remote-tracking branch 'origin/develop' 2025-09-01 17:08:43 +01:00
snipe
77d002a158 Use null for role as well
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 16:43:05 +01:00
snipe
94699893ac Updated fieldname in user API request
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 16:17:39 +01:00
snipe
9f81989bdd Return null instead of blank for display_name in API
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 16:06:28 +01:00
snipe
15abe36c53 More tweaks to display
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 13:58:02 +01:00
snipe
3094e007ee Set session to remember checkout type
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 13:28:07 +01:00
snipe
eb259aee22 Set asset selector to true for components since it will always be required
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 13:18:58 +01:00
snipe
04b83f8176 Merge remote-tracking branch 'origin/develop' 2025-09-01 12:28:40 +01:00
snipe
c05c8defb9 Merge pull request #17769 from grokability/#9978-add-pivot-to-accessories-api
Fixes #9978 - Added payload to accessories API
2025-09-01 12:28:08 +01:00
snipe
bf5668a42e Added payload to accessories API
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 12:23:39 +01:00
snipe
335ab3f064 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
#	public/css/build/app.css
#	public/css/build/app.css.map
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-09-01 11:57:07 +01:00
snipe
ec310bc8fb Updated dev assets
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 11:56:01 +01:00
snipe
db477421b2 Bumped to 8.3.1
Signed-off-by: snipe <snipe@snipe.net>
2025-09-01 11:54:53 +01:00
snipe
30a9496cf5 Merge pull request #17748 from Godmartinz/parent-location-in-asset-view
Adds #10969 Parent location to Asset Checked out to info
2025-09-01 11:49:42 +01:00
snipe
6cefa0d0b3 Merge pull request #17745 from Godmartinz/dropdown-link-color-fix
Fixes #17488 (Part 2) Nav dropdown link color and skin names
2025-09-01 11:48:29 +01:00
snipe
9284984265 Merge pull request #17768 from Speedyduck300000/develop
Fixed #17742: don't delete random actionlog when trying to delete a file
2025-09-01 11:40:06 +01:00
Speedyduck300000
53b96168a9 fixed uploadfilescontroller to use the file_id to delete the correct
entry.
2025-09-01 07:34:18 +02:00
Godfrey M
eadce51f10 fixes check in check out translations for assets in notiications 2025-08-29 11:12:05 -07:00
snipe
7dd493da35 Merge remote-tracking branch 'origin/develop' 2025-08-29 10:17:26 +01:00
snipe
b3c583b6dc Few more ->display_name
Signed-off-by: snipe <snipe@snipe.net>
2025-08-29 10:17:16 +01:00
snipe
560bd6da92 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	resources/views/notifications/markdown/report-expiring-assets.blade.php
2025-08-29 10:08:56 +01:00
snipe
28abeab31d Fixed a few more display_name instances
Signed-off-by: snipe <snipe@snipe.net>
2025-08-29 10:06:58 +01:00
snipe
a5824ccc5f Merge pull request #17754 from marcusmoore/fixes/name-in-expiring-assets-master
Patch #17751 to master take 2
2025-08-29 01:25:46 +01:00
Marcus Moore
830a7964a4 Use display_name in place of name() 2025-08-28 17:23:56 -07:00
snipe
12a649ec4b Merge pull request #17751 from marcusmoore/fixes/name-in-expiring-assets
Fixed user display name in expiring asset notification
2025-08-29 01:11:05 +01:00
Marcus Moore
35b79e4d14 Remove old comments 2025-08-28 16:56:40 -07:00
Marcus Moore
751dad7f2e Inline days 2025-08-28 16:56:12 -07:00
Marcus Moore
b08d86220a First pass at moving to table structure 2025-08-28 16:53:13 -07:00
Marcus Moore
3a27ecc475 Use display_name in place of name() 2025-08-28 16:22:24 -07:00
Godfrey M
da6fab5d43 adds parent location to checked out to location 2025-08-28 13:37:40 -07:00
snipe
ca95b29cd6 Add telescope tables to the excepted tables
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 19:52:56 +01:00
Godfrey M
c5c68e9dd5 fix dropdown link color and skin names 2025-08-28 11:49:37 -07:00
snipe
44fbde26fa Merge pull request #17743 from grokability/normalize-trait-locations
Moved model traits into proper directory
2025-08-28 18:52:43 +01:00
snipe
6e2bcd6aa9 Merge pull request #17680 from grokability/added-telescope
Added laravel telescope for dev environment
2025-08-28 18:52:18 +01:00
snipe
b1359c3277 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-08-28 18:34:14 +01:00
snipe
9c0202e5ce Bumped to 8.3.0
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 18:33:46 +01:00
snipe
39ef353073 Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 18:32:07 +01:00
snipe
7b5d90dd81 Moved model traits into proper directory
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 18:23:26 +01:00
snipe
0ba8f5cc5a Merge remote-tracking branch 'origin/develop' 2025-08-28 18:06:29 +01:00
snipe
d1129081df Asset nothing is sent if send_welcome is not checked/passed
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 18:05:40 +01:00
snipe
315a812df5 Fixed typos
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 16:41:56 +01:00
snipe
6fb9e2c38e Merge remote-tracking branch 'origin/develop' 2025-08-28 16:12:18 +01:00
snipe
cfc979acf0 Merge pull request #17432 from oolivero45/patch-1
Fixed #17431: EULA not displaying on asset acceptance page
2025-08-28 16:09:36 +01:00
snipe
d7407d70a3 Merge pull request #17741 from grokability/improved-user-create-tests
Improved user create tests
2025-08-28 13:32:46 +01:00
snipe
8ccd2e97a8 Improved user create tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 13:27:08 +01:00
snipe
988204619f Merge pull request #17740 from grokability/renamed-user-test
Renamed the test for consistency
2025-08-28 13:05:05 +01:00
snipe
cad6cc3007 Renamed the test for consistency
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 13:02:01 +01:00
snipe
eebc2ab8be Merge remote-tracking branch 'origin/develop' 2025-08-28 07:30:15 +01:00
snipe
b303875f1d Merge pull request #17734 from grokability/#17726-add-welcome-email-to-new-user-form
Fixed #17726: add welcome email to new user form
2025-08-28 07:29:56 +01:00
snipe
d5cc61f378 Added send to API call for creating users
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 07:28:51 +01:00
snipe
0d7ec43262 Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:19:41 +01:00
snipe
d3747f4daa Added welcome email to controller
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:12:01 +01:00
snipe
af695e7dc8 Added help to user importer
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:11:52 +01:00
snipe
1edbfd87df Added welcome email checkbox to user create form
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:11:40 +01:00
snipe
454be01a6c Updated translations
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:11:23 +01:00
snipe
b65b3151ee Merge remote-tracking branch 'origin/develop' 2025-08-28 05:29:48 +01:00
snipe
745fc515f1 Merge pull request #17713 from Godmartinz/fix-localization-for-email-notifications
Adds #5554 locale for acceptance notifications and checkin/out emails
2025-08-28 05:29:28 +01:00
snipe
715b9c1182 Merge pull request #17730 from Godmartinz/update-asset-accepance-with-category
Adds #9000 Item type to Account Asset Acceptance index
2025-08-28 05:22:54 +01:00
spencerrlongg
1d24b7985b another translation change 2025-08-27 17:25:17 -05:00
spencerrlongg
526bb2c650 more translation changes 2025-08-27 17:23:46 -05:00
spencerrlongg
c450c0ddb8 lots of translation changes 2025-08-27 17:17:18 -05:00
Godfrey M
95be847d87 renamed attribute 2025-08-27 14:10:51 -07:00
Godfrey M
c1a6546eba change column header 2025-08-27 14:09:13 -07:00
Godfrey M
648c25a0a7 adds item type to Accept asset index 2025-08-27 14:06:10 -07:00
Godfrey M
f2ec7f2975 fix tests 2025-08-27 13:22:35 -07:00
Godfrey M
f518af6d61 fix class name 2025-08-27 13:09:05 -07:00
snipe
13a0f49f5f Merge remote-tracking branch 'origin/develop' 2025-08-27 16:36:05 +01:00
snipe
b11c6a5c06 Updated depreciation translation with more information.
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 16:35:49 +01:00
snipe
5822e4e692 Merge pull request #17729 from grokability/exit-early-if-ldap-troubleshooter-cannot-decrypt-ldap-pw
Put LDAP troubleshooter's decrypt in a try/catch to avoid crashing if it cannot decrypt the password
2025-08-27 15:47:22 +01:00
snipe
199eefafa1 Merge remote-tracking branch 'origin/develop' 2025-08-27 14:38:47 +01:00
snipe
c5b58f9ecc Merge remote-tracking branch 'origin/develop' 2025-08-27 13:32:25 +01:00
snipe
6b68fe4de6 Merge remote-tracking branch 'origin/develop' 2025-08-27 13:30:19 +01:00
snipe
3461bbfdb3 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/app.css.map
#	public/css/build/overrides.css
#	public/css/build/overrides.css.map
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-08-27 12:28:22 +01:00
spencerrlongg
51f6927076 add details block for more than 3 errors to notification 2025-08-26 21:50:08 -05:00
Marcus Moore
4298aad008 Merge branch 'develop' into 17369-accessory-checkout-qty-scaffold
# Conflicts:
#	app/Http/Controllers/Account/AcceptanceController.php
#	resources/views/notifications/markdown/asset-acceptance.blade.php
2025-08-26 11:39:52 -07:00
Marcus Moore
823c67400d Fix indent 2025-08-26 11:34:10 -07:00
Marcus Moore
3160d1064d Remove unused import 2025-08-26 11:32:38 -07:00
spencerrlongg
1d88cf443f revert SuppliersController as well 2025-08-25 18:45:31 -05:00
spencerrlongg
7b6c0c3a40 Revert "tests passing, needs some manual testing"
This reverts commit fdb0651bf4.
2025-08-25 18:42:11 -05:00
spencerrlongg
fdb0651bf4 tests passing, needs some manual testing 2025-08-25 18:25:36 -05:00
spencerrlongg
c39d484611 rm unused import, new prop 2025-08-25 16:30:53 -05:00
spencerrlongg
c42996429f rm unused import 2025-08-25 14:10:12 -05:00
spencerrlongg
a091baf5a6 component finally working 2025-08-25 14:06:56 -05:00
Godfrey M
128bdf500a sends an email for to locale and cc locale 2025-08-25 12:02:23 -07:00
Marcus Moore
918426a2fa Add qty to accept assets table 2025-08-21 15:49:36 -07:00
Marcus Moore
c076b37c9b Add qty to accessory eula pdf 2025-08-21 15:05:24 -07:00
Marcus Moore
7c58bfa282 Add qty to AcceptanceAssetDeclinedNotification 2025-08-21 14:56:24 -07:00
Marcus Moore
0caaba156d Add qty to AcceptanceAssetAcceptedNotification 2025-08-21 14:54:43 -07:00
Marcus Moore
c22575812d Add qty to AcceptanceAssetAcceptedToUserNotification 2025-08-21 14:53:00 -07:00
Marcus Moore
0ede4da816 Remove CleanDeclinedAccessoryCheckouts command 2025-08-21 14:34:31 -07:00
Marcus Moore
98e23ff92e Remove legacy testing from test 2025-08-21 14:34:16 -07:00
snipe
00a17cd55e Merge remote-tracking branch 'origin/develop' 2025-08-21 11:51:50 +01:00
spencerrlongg
643d44af22 change \Throwable to \Exceptionm, add missing report()s 2025-08-20 23:09:59 -05:00
spencerrlongg
b934f43db0 rename all exceptions 2025-08-20 22:58:39 -05:00
Marcus Moore
c7bdad649a Build out command 2025-08-20 16:06:11 -07:00
Marcus Moore
18c2508d2f Scaffold command for cleaning accessory_checkout 2025-08-20 13:49:19 -07:00
Marcus Moore
4dcfd8b353 Improve method name 2025-08-20 13:19:00 -07:00
Marcus Moore
726116574d Improve readability? 2025-08-20 12:40:03 -07:00
Marcus Moore
27f02014ca Add failing test 2025-08-20 12:26:55 -07:00
Marcus Moore
f80f1acaa7 Improve variable name 2025-08-20 12:15:58 -07:00
Marcus Moore
39d5ffeceb Implement fix 2025-08-20 12:05:58 -07:00
Marcus Moore
48ba7eed3e Remove legacy checks from test 2025-08-20 11:45:36 -07:00
snipe
f39afe5a65 Merge remote-tracking branch 'origin/develop' 2025-08-20 15:56:19 +01:00
snipe
35b358d336 Check for $user to handle tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 14:47:58 +01:00
snipe
ae109be631 Small tweaks
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 14:43:52 +01:00
snipe
3f7ed73395 Added laravel telescope for dev environment
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 14:26:28 +01:00
snipe
7612ee6b08 Merge remote-tracking branch 'origin/develop' 2025-08-20 14:17:38 +01:00
snipe
2ed2b0101a Merge remote-tracking branch 'origin/develop' 2025-08-20 12:43:57 +01:00
snipe
5ca9d31964 Merge remote-tracking branch 'origin/develop' 2025-08-20 11:32:27 +01:00
snipe
2fcd8cd261 Merge remote-tracking branch 'origin/develop' 2025-08-20 11:00:26 +01:00
snipe
0ffa47a2c6 Merge remote-tracking branch 'origin/develop' 2025-08-20 09:58:54 +01:00
Marcus Moore
9caa240fdb Revert "Remove total qty of accessory checkouts"
This reverts commit 3ffb73a516.
2025-08-19 17:17:46 -07:00
Marcus Moore
3ffb73a516 Remove total qty of accessory checkouts 2025-08-19 17:04:13 -07:00
Marcus Moore
cc1132be87 Remove flakiness 2025-08-19 16:50:36 -07:00
Marcus Moore
1c31f126ef Clear some flakiness 2025-08-19 16:46:41 -07:00
Marcus Moore
d8eaf2676f Add a couple of notes 2025-08-19 15:54:46 -07:00
snipe
e203d4dee3 Merge remote-tracking branch 'origin/develop' 2025-08-19 14:49:00 +01:00
snipe
b47d773e13 Merge remote-tracking branch 'origin/develop' 2025-08-19 14:34:32 +01:00
snipe
a8d0a4a95d Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/js/dist/all.js
#	public/js/dist/all.js.map
#	public/mix-manifest.json
2025-08-19 14:12:58 +01:00
snipe
3fb0804cef Merge remote-tracking branch 'origin/develop' 2025-08-19 13:57:36 +01:00
snipe
6811ebcd52 Merge remote-tracking branch 'origin/develop' 2025-08-19 10:12:56 +01:00
snipe
4fe7bfb851 Merge remote-tracking branch 'origin/develop' 2025-08-18 15:24:25 +01:00
snipe
fb60985d03 Merge remote-tracking branch 'origin/develop' 2025-08-18 12:47:19 +01:00
snipe
8f575923cf Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/app.css.map
#	public/css/build/overrides.css
#	public/css/build/overrides.css.map
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-08-18 11:26:43 +01:00
snipe
0ecfd02649 Merge remote-tracking branch 'origin/develop' 2025-08-18 11:00:30 +01:00
snipe
420aaf4f61 Merge remote-tracking branch 'origin/develop' 2025-08-18 09:45:19 +01:00
snipe
0c35f213e1 Merge remote-tracking branch 'origin/develop' 2025-08-17 14:54:43 +01:00
snipe
f68813af13 Merge remote-tracking branch 'origin/develop' 2025-08-17 14:11:41 +01:00
snipe
37a90d0ce9 Merge remote-tracking branch 'origin/develop' 2025-08-15 15:07:29 +02:00
snipe
02f1291e8f Merge remote-tracking branch 'origin/develop' 2025-08-15 14:41:24 +02:00
snipe
92e4f6b5d9 Merge remote-tracking branch 'origin/develop' 2025-08-15 14:31:53 +02:00
snipe
7b7738fbcc Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/app.css.map
#	public/css/build/overrides.css
#	public/css/build/overrides.css.map
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-08-15 14:24:30 +02:00
Marcus Moore
3101212c49 Continue to scaffold test 2025-08-14 17:24:36 -07:00
Marcus Moore
3f0ac103a1 Scaffold test 2025-08-14 13:29:18 -07:00
Marcus Moore
59bd6ca360 Update button text 2025-08-14 13:21:11 -07:00
Marcus Moore
22fe9a786e Display number of items being accepted 2025-08-14 12:36:46 -07:00
snipe
31197604a3 Merge pull request #17602 from grokability/develop
Merge develop into master
2025-08-13 21:19:39 +02:00
Marcus Moore
d2ee8de9ac Attach qty to CheckoutAcceptance 2025-08-13 12:14:10 -07:00
Marcus Moore
03d3fb6a5f Add qty to checkout_acceptances table 2025-08-13 12:09:56 -07:00
spencerrlongg
e33b1b6c90 fixed maintenances 2025-08-12 13:34:40 -05:00
Spencer Long
30520297e8 Merge branch 'develop' into feature/8709-bulk-deletion-of-asset-categories-suppliers-manufacturers 2025-08-12 11:58:11 -06:00
spencerrlongg
78ca1d1335 some cleanup 2025-08-11 21:07:08 -05:00
spencerrlongg
6159ee8c2c category done! 2025-08-11 20:16:43 -05:00
spencerrlongg
5cd5392958 manufacturer completed, just categories left 2025-08-11 19:42:09 -05:00
spencerrlongg
0dcdfc5d14 fix tests after routing change 2025-08-11 19:11:40 -05:00
spencerrlongg
d0e068f1c0 suppliers completely done, rinse and repeat for the other two 2025-08-11 19:04:36 -05:00
snipe
f42a2d7457 Merge remote-tracking branch 'origin/develop' 2025-08-11 20:45:38 +01:00
snipe
d29619b67c Merge remote-tracking branch 'origin/develop' 2025-08-11 18:50:17 +01:00
snipe
f5235cb835 Merge remote-tracking branch 'origin/develop' 2025-08-11 18:12:51 +01:00
snipe
ee830e0cb4 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/app.css.map
#	public/css/build/overrides.css
#	public/css/build/overrides.css.map
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-08-11 14:58:57 +01:00
snipe
0cd3be003d Merge remote-tracking branch 'origin/develop' 2025-08-11 13:07:00 +01:00
snipe
c93e35ec77 Merge remote-tracking branch 'origin/develop' 2025-08-11 11:18:31 +01:00
snipe
9538a76232 Merge remote-tracking branch 'origin/develop' 2025-08-11 06:26:29 +01:00
snipe
05876bb124 Merge remote-tracking branch 'origin/develop' 2025-08-11 05:05:13 +01:00
snipe
8bcd5a6d2a Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-08-10 21:04:55 +01:00
snipe
a36afbcb25 Merge remote-tracking branch 'origin/develop' 2025-08-10 21:02:48 +01:00
snipe
ebd8d085cf Merge remote-tracking branch 'origin/develop' 2025-08-10 21:01:34 +01:00
snipe
505148b024 Merge remote-tracking branch 'origin/develop' 2025-08-10 20:51:27 +01:00
spencerrlongg
3eefeec4ce partials made, need to figure out all this jquery, button disabled 2025-08-09 22:40:00 -05:00
spencerrlongg
b61419c1ce oop, revert delete 2025-08-09 22:19:28 -05:00
spencerrlongg
f590fcffbc some tests, a component i probably won't use, beginning of front end 2025-08-09 22:16:23 -05:00
snipe
e87e924ac2 Merge remote-tracking branch 'origin/develop' 2025-08-08 11:37:46 +01:00
snipe
90f261bab6 Merge remote-tracking branch 'origin/develop' 2025-08-08 11:32:14 +01:00
snipe
f7dfb09a4d Merge remote-tracking branch 'origin/develop' 2025-08-08 09:56:23 +01:00
snipe
3135917127 Merge remote-tracking branch 'origin/develop' 2025-08-07 17:03:01 +01:00
snipe
52afa3d36d Merge remote-tracking branch 'origin/develop' 2025-08-06 16:35:59 +01:00
snipe
242aa60e04 Merge remote-tracking branch 'origin/develop' 2025-08-06 16:26:14 +01:00
snipe
7a3c2c27ff Merge remote-tracking branch 'origin/develop' 2025-08-05 23:19:38 +01:00
snipe
5d124360c2 Merge remote-tracking branch 'origin/develop' 2025-08-05 19:35:12 +01:00
snipe
365d7448d5 Merge remote-tracking branch 'origin/develop' 2025-08-05 19:06:34 +01:00
snipe
9a0102c723 Merge remote-tracking branch 'origin/develop' 2025-08-05 19:03:00 +01:00
snipe
2f77f2cb2b Merge remote-tracking branch 'origin/develop' 2025-08-05 18:13:27 +01:00
snipe
528e3a2106 Merge remote-tracking branch 'origin/develop' 2025-08-05 17:48:12 +01:00
snipe
032a664d4c Merge remote-tracking branch 'origin/develop' 2025-08-04 22:27:59 +01:00
snipe
aac1864c9b Merge remote-tracking branch 'origin/develop' 2025-08-04 21:23:41 +01:00
snipe
e3477f3306 Merge remote-tracking branch 'origin/develop' 2025-08-02 18:41:39 +01:00
snipe
6620a4f87b Merge remote-tracking branch 'origin/develop' 2025-08-02 14:47:09 +01:00
snipe
c0e9dff5bf Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/js/dist/all.js
#	public/js/dist/all.js.map
#	public/js/dist/bootstrap-table.js
#	public/mix-manifest.json
2025-08-01 23:13:22 +01:00
snipe
2d961c435a Merge remote-tracking branch 'origin/develop' 2025-07-31 04:11:31 +01:00
snipe
7c95f03166 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/AdminLTE.css
#	public/css/build/app.css
#	public/css/build/overrides.css
#	public/css/dist/all.css
#	public/css/dist/bootstrap-table.css
#	public/css/dist/skins/_all-skins.css
#	public/css/dist/skins/_all-skins.min.css
#	public/css/dist/skins/skin-black-dark.css
#	public/css/dist/skins/skin-black-dark.min.css
#	public/css/dist/skins/skin-black.css
#	public/css/dist/skins/skin-black.min.css
#	public/css/dist/skins/skin-blue-dark.css
#	public/css/dist/skins/skin-blue-dark.min.css
#	public/css/dist/skins/skin-blue.css
#	public/css/dist/skins/skin-blue.min.css
#	public/css/dist/skins/skin-contrast.css
#	public/css/dist/skins/skin-contrast.min.css
#	public/css/dist/skins/skin-green-dark.css
#	public/css/dist/skins/skin-green-dark.min.css
#	public/css/dist/skins/skin-green.css
#	public/css/dist/skins/skin-green.min.css
#	public/css/dist/skins/skin-orange-dark.css
#	public/css/dist/skins/skin-orange-dark.min.css
#	public/css/dist/skins/skin-orange.css
#	public/css/dist/skins/skin-orange.min.css
#	public/css/dist/skins/skin-purple-dark.css
#	public/css/dist/skins/skin-purple-dark.min.css
#	public/css/dist/skins/skin-purple.css
#	public/css/dist/skins/skin-purple.min.css
#	public/css/dist/skins/skin-red-dark.css
#	public/css/dist/skins/skin-red-dark.min.css
#	public/css/dist/skins/skin-red.css
#	public/css/dist/skins/skin-red.min.css
#	public/css/dist/skins/skin-yellow-dark.css
#	public/css/dist/skins/skin-yellow-dark.min.css
#	public/css/dist/skins/skin-yellow.css
#	public/css/dist/skins/skin-yellow.min.css
#	public/js/build/app.js
#	public/js/build/vendor.js
#	public/js/dist/all.js
#	public/js/dist/bootstrap-table.js
#	public/mix-manifest.json
2025-07-28 17:41:13 +01:00
snipe
31e5c13b50 Merge remote-tracking branch 'origin/develop' 2025-07-28 03:30:22 +01:00
snipe
4a9fe4f981 Merge remote-tracking branch 'origin/develop' 2025-07-24 15:54:54 +01:00
snipe
4fcc5587ee Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-07-24 15:36:06 +01:00
snipe
6ca49a20ce Merge remote-tracking branch 'origin/develop' 2025-07-24 15:29:54 +01:00
snipe
28f293fdc1 Merge remote-tracking branch 'origin/develop' 2025-07-24 13:07:09 +01:00
snipe
b3e7619adc Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-07-23 16:11:20 +01:00
snipe
6e56d56137 Merge remote-tracking branch 'origin/develop' 2025-07-23 15:06:25 +01:00
snipe
2528f6a07b Merge remote-tracking branch 'origin/develop' 2025-07-23 14:56:49 +01:00
snipe
423d07c919 Merge remote-tracking branch 'origin/develop' 2025-07-23 12:41:20 +01:00
snipe
cc608de4bf Merge remote-tracking branch 'origin/develop' 2025-07-23 12:28:35 +01:00
snipe
f999a68608 Merge remote-tracking branch 'origin/develop' 2025-07-22 20:28:53 +01:00
snipe
db78a9f18f Merge remote-tracking branch 'origin/develop' 2025-07-22 15:25:51 +01:00
snipe
816039f48e Merge remote-tracking branch 'origin/develop' 2025-07-22 15:18:50 +01:00
snipe
ae240bae6d Updated prod CSS
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 15:14:42 +01:00
snipe
9e30c69e6d Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/overrides.css
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-07-22 15:14:32 +01:00
snipe
43c7de9049 Updated prod assets
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 14:38:04 +01:00
snipe
7e51c5db81 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/overrides.css
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-07-22 14:34:10 +01:00
snipe
0ee3c45e7b Merge remote-tracking branch 'origin/develop' 2025-07-22 13:39:54 +01:00
snipe
981e69929c Merge remote-tracking branch 'origin/develop' 2025-07-21 12:36:44 +01:00
Oliver Cox
553ab8851a Fix #17431: EULA not displaying on asset acceptance page
Changed two instances of === to ==, as this was causing a comparison to fail and preventing EULAs from being displayed on asset acceptance pages as expected.
2025-07-20 23:00:18 +01:00
snipe
1eae5d12fc Merge remote-tracking branch 'origin/develop' 2025-07-18 17:56:20 +01:00
snipe
8863208333 Merge remote-tracking branch 'origin/develop' 2025-07-16 17:35:55 +01:00
snipe
5f38a74a72 Merge remote-tracking branch 'origin/develop' 2025-07-16 17:02:29 +01:00
snipe
fe15dacb1f Merge remote-tracking branch 'origin/develop' 2025-07-16 16:54:30 +01:00
snipe
c2d44cf2f2 Merge remote-tracking branch 'origin/develop' 2025-07-16 12:25:14 +01:00
snipe
7f1bdb6f34 Merge remote-tracking branch 'origin/develop' 2025-07-15 10:58:58 +01:00
spencerrlongg
7cdfaa93ec a couple more tests and cleanup 2025-07-10 16:45:15 -05:00
spencerrlongg
59ccc70303 bulk changes that should make this work 2025-07-10 16:02:39 -05:00
spencerrlongg
f1584b722d work on bulk tests, switching branches to check something 2025-07-10 15:29:42 -05:00
snipe
b0305e12d2 Merge remote-tracking branch 'origin/develop' 2025-07-10 13:11:10 +01:00
spencerrlongg
4d8c5a86a4 routes added, tests scratched but need writing 2025-07-09 23:01:58 -05:00
snipe
58f76b5c99 Merge remote-tracking branch 'origin/develop' 2025-07-09 21:01:24 +01:00
snipe
7c4ee632cf Merge remote-tracking branch 'origin/develop' 2025-07-09 20:58:17 +01:00
snipe
b6b0f716eb Merge remote-tracking branch 'origin/develop' 2025-07-09 20:22:25 +01:00
snipe
bd0e04ed15 Merge remote-tracking branch 'origin/develop' 2025-07-09 15:48:17 +01:00
snipe
8599981d44 Merge remote-tracking branch 'origin/develop' 2025-07-09 15:46:53 +01:00
snipe
6fc6e95c67 Merge remote-tracking branch 'origin/develop' 2025-07-08 22:02:34 +01:00
snipe
43b585bde8 Merge remote-tracking branch 'origin/develop' 2025-07-08 21:59:58 +01:00
snipe
710f89291f Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-07-08 21:27:06 +01:00
spencerrlongg
5f835aa009 delete unused imports 2025-07-07 18:07:26 -05:00
spencerrlongg
d5ca543719 start introducing parent exception 2025-07-07 18:05:23 -05:00
snipe
4c6249eb9e Merge remote-tracking branch 'origin/develop' 2025-07-07 22:09:45 +01:00
snipe
016900bad8 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-07-07 21:54:04 +01:00
snipe
2e8ae33761 Merge remote-tracking branch 'origin/develop' 2025-07-07 20:58:36 +01:00
Godfrey M
2b8ea9a233 add required to input validation 2025-07-07 10:46:54 -07:00
snipe
0d325060da Merge remote-tracking branch 'origin/develop' 2025-07-07 17:06:24 +01:00
snipe
1a6e98e18f Merge remote-tracking branch 'origin/develop' 2025-07-07 16:24:04 +01:00
snipe
97e34595f6 Merge remote-tracking branch 'origin/develop' 2025-07-07 16:00:10 +01:00
snipe
a179d5234b Merge remote-tracking branch 'origin/develop' 2025-07-07 14:43:28 +01:00
snipe
64c6121fdb Merge remote-tracking branch 'origin/develop' 2025-07-07 14:00:55 +01:00
snipe
08d8954a85 Merge remote-tracking branch 'origin/develop' 2025-07-07 14:00:03 +01:00
snipe
4f20955d0d Merge remote-tracking branch 'origin/develop' 2025-07-07 13:46:03 +01:00
snipe
3a703c8bcf Merge remote-tracking branch 'origin/develop' 2025-07-07 13:35:07 +01:00
snipe
ccbffa086b Merge remote-tracking branch 'origin/develop' 2025-07-07 11:36:33 +01:00
snipe
07ee4be840 Merge remote-tracking branch 'origin/develop' 2025-07-02 18:38:12 +01:00
snipe
4cc9b2d312 Merge remote-tracking branch 'origin/develop' 2025-07-02 17:21:39 +01:00
snipe
24dddae1d1 Merge remote-tracking branch 'origin/develop' 2025-07-02 11:57:57 +01:00
snipe
ad0165d085 Merge remote-tracking branch 'origin/develop' 2025-07-02 11:44:30 +01:00
snipe
39dc38c5d1 Merge remote-tracking branch 'origin/develop' 2025-07-01 23:32:43 +01:00
snipe
046ce19dbb Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-06-30 11:46:24 +01:00
snipe
33263f5a93 Merge remote-tracking branch 'origin/develop' 2025-06-27 13:52:09 +01:00
snipe
8ef8e76300 Merge remote-tracking branch 'origin/develop' 2025-06-27 12:48:54 +01:00
snipe
73cfdae9e7 Merge remote-tracking branch 'origin/develop' 2025-06-25 15:00:46 +01:00
snipe
54a3e41281 Merge remote-tracking branch 'origin/develop' 2025-06-25 14:58:46 +01:00
snipe
d59ba6da84 Merge remote-tracking branch 'origin/develop' 2025-06-25 13:11:39 +01:00
snipe
1b28b06934 Merge remote-tracking branch 'origin/develop' 2025-06-25 11:04:15 +01:00
snipe
ff3e69a56c Merge remote-tracking branch 'origin/develop' 2025-06-23 20:51:24 +01:00
snipe
6b975a5fb4 Merge remote-tracking branch 'origin/develop' 2025-06-23 16:41:42 +01:00
snipe
3a02b15124 Merge remote-tracking branch 'origin/develop' 2025-06-23 12:38:40 +01:00
snipe
5f73d81935 Merge remote-tracking branch 'origin/develop' 2025-06-22 20:14:59 +01:00
snipe
002b5c0f6f Merge remote-tracking branch 'origin/develop' 2025-06-22 20:07:52 +01:00
snipe
8086842570 Merge remote-tracking branch 'origin/develop' 2025-06-22 19:42:03 +01:00
snipe
51f2d5a664 Merge remote-tracking branch 'origin/develop' 2025-06-22 19:30:42 +01:00
spencerrlongg
db63ad1cf4 add image check to supplier action 2025-06-17 18:13:37 -05:00
snipe
b74b76de75 Merge remote-tracking branch 'origin/develop' 2025-06-17 18:17:10 +01:00
snipe
a1b1498106 Merge remote-tracking branch 'origin/develop' 2025-06-17 17:51:37 +01:00
snipe
1158851ea7 Merge remote-tracking branch 'origin/develop' 2025-06-17 15:57:12 +01:00
snipe
e1ab9e959e Merge remote-tracking branch 'origin/develop' 2025-06-17 13:03:27 +01:00
snipe
2bbac3ae9d Merge remote-tracking branch 'origin/develop' 2025-06-17 12:38:52 +01:00
snipe
e889b1d5e5 Merge remote-tracking branch 'origin/develop' 2025-06-17 11:42:00 +01:00
snipe
233bf856f4 Merge remote-tracking branch 'origin/develop' 2025-06-17 11:39:12 +01:00
snipe
bca843e06c Merge remote-tracking branch 'origin/develop' 2025-06-16 20:32:40 +01:00
snipe
30a79a1278 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-06-16 14:45:53 +01:00
snipe
0f92dee2c4 Merge remote-tracking branch 'origin/develop' 2025-06-15 03:41:23 +01:00
snipe
f04efede15 Merge remote-tracking branch 'origin/develop' 2025-06-15 02:59:47 +01:00
snipe
f0dfdf6720 Merge remote-tracking branch 'origin/develop' 2025-06-14 23:56:06 +01:00
snipe
e26d731382 Merge remote-tracking branch 'origin/develop' 2025-06-14 23:08:48 +01:00
snipe
d684d3e559 Merge remote-tracking branch 'origin/develop' 2025-06-14 22:22:54 +01:00
snipe
47c54cb998 Merge remote-tracking branch 'origin/develop' 2025-06-14 17:45:44 +01:00
snipe
592cb2b3ec Merge remote-tracking branch 'origin/develop' 2025-06-14 17:08:11 +01:00
snipe
f5a7871a2e Merge remote-tracking branch 'origin/develop' 2025-06-14 16:44:48 +01:00
snipe
ec411fa0db Merge remote-tracking branch 'origin/develop' 2025-06-14 12:17:43 +01:00
spencerrlongg
5da79cd5ca destroy manufacturer action, bulk manufacturer controller 2025-06-12 17:11:45 -05:00
snipe
a850a9bb83 Merge remote-tracking branch 'origin/develop' 2025-06-12 21:34:59 +01:00
spencerrlongg
13c971b171 some refinements, bulk categories controller 2025-06-12 15:19:49 -05:00
spencerrlongg
1cee7e43ed more (reusable) exceptions, a couple notes 2025-06-12 00:47:57 -05:00
spencerrlongg
5e81c63d6e some notes and things moved 2025-06-11 11:39:14 -05:00
snipe
479b7a3f94 Merge remote-tracking branch 'origin/develop' 2025-06-11 10:32:49 +01:00
snipe
f7cfee77c9 Merge remote-tracking branch 'origin/develop' 2025-06-11 10:20:22 +01:00
snipe
65a8126a13 Merge remote-tracking branch 'origin/develop' 2025-06-09 13:40:44 +01:00
snipe
a39bc102d5 Merge remote-tracking branch 'origin/develop' 2025-06-08 15:30:32 +01:00
snipe
81d930c4d2 Merge remote-tracking branch 'origin/develop' 2025-06-08 15:19:19 +01:00
snipe
6839623061 Merge remote-tracking branch 'origin/develop' 2025-06-06 17:03:37 +01:00
snipe
7de2809d42 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/js/dist/bootstrap-table.js
#	public/mix-manifest.json
2025-06-06 14:05:47 +01:00
snipe
98ec6b6886 Merge remote-tracking branch 'origin/develop' 2025-06-06 12:48:54 +01:00
snipe
04827f00cc Merge remote-tracking branch 'origin/develop' 2025-06-06 11:54:10 +01:00
snipe
660bfc6578 Merge remote-tracking branch 'origin/develop' 2025-06-06 08:31:09 +01:00
snipe
1152cd5537 Merge remote-tracking branch 'origin/develop' 2025-06-06 08:26:52 +01:00
spencerrlongg
17456482d6 category destroy action 2025-06-04 21:42:58 -05:00
spencerrlongg
a0431e1912 supplier actions working 2025-06-04 20:44:18 -05:00
snipe
f30e8497b2 Merge remote-tracking branch 'origin/develop' 2025-06-04 16:17:26 +01:00
snipe
06495bc45d Merge remote-tracking branch 'origin/develop' 2025-06-04 12:13:30 +01:00
snipe
26067916b3 Merge remote-tracking branch 'origin/develop' 2025-06-04 11:49:03 +01:00
snipe
c36ee4852b Merge remote-tracking branch 'origin/develop' 2025-06-04 10:40:56 +01:00
snipe
2cb992ad44 Merge remote-tracking branch 'origin/develop' 2025-06-04 08:43:52 +01:00
spencerrlongg
ee5aac8008 supplier destroy action creaated and a lot scratched out 2025-06-03 20:25:07 -05:00
snipe
083b7be6c0 Make version number match tagged version :(
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 11:03:33 +01:00
snipe
e24854558f Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-06-03 11:03:05 +01:00
snipe
e4314cf426 Merge remote-tracking branch 'origin/develop' 2025-06-03 06:07:20 +01:00
snipe
4106e4e45c Merge remote-tracking branch 'origin/develop' 2025-06-02 22:29:09 +01:00
snipe
05f143db2b Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-06-02 18:15:54 +01:00
snipe
64aeaeeeea Merge remote-tracking branch 'origin/develop' 2025-06-02 17:29:52 +01:00
snipe
61db37ab0d Merge remote-tracking branch 'origin/develop' 2025-06-02 15:28:08 +01:00
snipe
f9c4d921e7 Merge remote-tracking branch 'origin/develop' 2025-06-02 15:07:15 +01:00
snipe
ca099df573 Merge remote-tracking branch 'origin/develop' 2025-06-02 15:04:00 +01:00
snipe
28b584b8bc Merge remote-tracking branch 'origin/develop' 2025-06-02 02:08:19 +01:00
snipe
70449e694d Merge remote-tracking branch 'origin/develop' 2025-05-29 22:18:50 +01:00
snipe
8395ea552d Merge remote-tracking branch 'origin/develop' 2025-05-29 16:17:58 +01:00
snipe
dc66452633 Merge remote-tracking branch 'origin/develop' 2025-05-29 13:32:41 +01:00
snipe
53ce44ac91 Merge remote-tracking branch 'origin/develop' 2025-05-29 12:38:40 +01:00
snipe
c7c3243bbc Merge remote-tracking branch 'origin/develop' 2025-05-29 12:35:21 +01:00
snipe
8bdd77d33d Merge remote-tracking branch 'origin/develop' 2025-05-29 12:24:51 +01:00
snipe
acd7d0db3a Merge remote-tracking branch 'origin/develop' 2025-05-29 12:09:08 +01:00
snipe
2bfadb8a3c Merge remote-tracking branch 'origin/develop' 2025-05-28 15:21:34 +01:00
snipe
0912e4af7b Merge remote-tracking branch 'origin/develop' 2025-05-26 13:17:38 +01:00
snipe
5aa5c48018 Merge remote-tracking branch 'origin/develop' 2025-05-23 19:37:42 +01:00
snipe
8cdd998f79 Merge remote-tracking branch 'origin/develop' 2025-05-23 17:35:08 +01:00
snipe
050d4d6b25 Merge remote-tracking branch 'origin/develop' 2025-05-23 14:51:48 +01:00
snipe
366cd11238 Merge remote-tracking branch 'origin/develop' 2025-05-23 14:31:10 +01:00
snipe
58d6443331 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-05-23 13:27:44 +01:00
snipe
101b8afb56 Merge remote-tracking branch 'origin/develop' 2025-05-23 13:07:47 +01:00
snipe
5df5c47945 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-05-23 10:43:58 +01:00
snipe
a04740ba86 Merge remote-tracking branch 'origin/develop' 2025-05-23 10:42:44 +01:00
snipe
425ad93ac5 Merge remote-tracking branch 'origin/develop' 2025-05-23 10:17:10 +01:00
Godfrey M
b0067fee51 remove some N+1s, collect an array of missing serial errors 2025-05-20 12:31:34 -07:00
Godfrey M
732c3dae89 added require_serial to model factory 2025-05-20 09:53:51 -07:00
Godfrey M
d45bd67cae added corrections 2025-05-20 09:51:02 -07:00
Godfrey M
9200de5032 made require_serial column nullable 2025-05-19 09:58:37 -07:00
snipe
8a44144c20 Merge pull request #16954 from grokability/develop
Merge dev into master
2025-05-16 11:16:14 +02:00
snipe
ee82c70582 Merge remote-tracking branch 'origin/develop' 2025-05-15 18:32:05 +02:00
snipe
c87e8e606b Merge remote-tracking branch 'origin/develop' 2025-05-15 18:02:47 +02:00
snipe
37a50dd953 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-05-15 17:51:54 +02:00
snipe
a2669a3084 Removed temp code
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:31:16 +02:00
snipe
77da22f4dd Print errors if they exist (temp)
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:26:04 +02:00
snipe
7830ffe202 Temp echo errors
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:24:08 +02:00
snipe
1c9e20d59f Merge remote-tracking branch 'origin/develop' 2025-05-15 17:10:57 +02:00
Godfrey M
3fbbff5a47 revert unnecessary change to laels 2025-05-14 12:57:01 -07:00
Godfrey M
c22efc2c3d add to present and transformer and api 2025-05-14 12:55:41 -07:00
Godfrey M
8c0281bf70 adds tests for requiring serial to asset model 2025-05-14 12:31:27 -07:00
snipe
320edac286 Merge remote-tracking branch 'origin/develop' 2025-05-14 17:39:30 +02:00
snipe
d49878371d Merge remote-tracking branch 'origin/develop' 2025-05-14 16:23:38 +02:00
snipe
d2575a5d9b Add @JassonCordones as a contributor 2025-05-14 15:03:04 +02:00
Marcus Moore
ea6cf72580 Formatting 2025-05-14 15:03:04 +02:00
Marcus Moore
2118155b37 Fix bug in getImageUrl method 2025-05-14 15:03:04 +02:00
Marcus Moore
ba4f5bb71f Add test for existing functionality 2025-05-14 15:03:04 +02:00
Marcus Moore
d5a74a5a8b Remove unneeded div 2025-05-14 15:03:04 +02:00
Marcus Moore
23be1df360 Remove the replaced locales form macro 2025-05-14 15:03:04 +02:00
Marcus Moore
b5c1a1da4c Replace Form::locales on user setup 2025-05-14 15:03:04 +02:00
Marcus Moore
c11e784f51 Replace Form::locales on bulk edit users page 2025-05-14 15:03:04 +02:00
Marcus Moore
06f51c8f9c Replace Form::locales on user edit page 2025-05-14 15:03:04 +02:00
Marcus Moore
181bcbbda6 Replace Form::locales on localization page 2025-05-14 15:03:04 +02:00
Marcus Moore
d008ead6a4 Fix input name 2025-05-14 15:03:04 +02:00
Marcus Moore
75924be958 Introduce locale select component and make replacement on profile page 2025-05-14 15:03:04 +02:00
snipe
b1a6e3f8a2 Merge pull request #16930 from JassonCordones/master
fix typo in snipeit.sh
2025-05-14 15:01:41 +02:00
snipe
06712a6041 Merge remote-tracking branch 'origin/develop' 2025-05-14 13:42:51 +02:00
Godfrey M
720a4bc4a2 add warning to update method for missing a serial 2025-05-13 12:54:28 -07:00
Godfrey M
7fd93645b3 valdiation fires for asset creation 2025-05-13 12:17:58 -07:00
snipe
62b16339a9 Merge remote-tracking branch 'origin/develop' 2025-05-13 20:44:34 +02:00
Jasson
9a2f1a36ba fix typo in snipeit.sh
fix redirect output to stderr
2025-05-13 14:31:37 -04:00
Godfrey M
fcbfbca6d0 add checkbox to model edit and create 2025-05-13 11:10:46 -07:00
Godfrey M
f2bca9491c changed name of field in model fillable 2025-05-13 10:59:02 -07:00
Godfrey M
b48f309ab6 add require_serial to bulk asset model blades and lang 2025-05-13 10:58:05 -07:00
Godfrey M
0b1be3e63b add migration, model and controller update 2025-05-12 11:44:34 -07:00
snipe
95cc4d3a73 Merge remote-tracking branch 'origin/develop' 2025-05-09 21:16:58 +01:00
snipe
497eeeb2e0 Merge remote-tracking branch 'origin/develop' 2025-05-09 19:29:01 +01:00
snipe
4be21ca249 Merge remote-tracking branch 'origin/develop' 2025-05-09 18:03:38 +01:00
snipe
e8598e214e Merge remote-tracking branch 'origin/develop' 2025-05-09 17:23:22 +01:00
snipe
54b1d65e3c Merge remote-tracking branch 'origin/develop' 2025-05-09 14:46:37 +01:00
snipe
f7648496d3 Merge remote-tracking branch 'origin/develop' 2025-05-08 16:26:02 +01:00
snipe
59a57c7197 Merge remote-tracking branch 'origin/develop' 2025-05-08 15:43:53 +01:00
snipe
5659b26827 Merge remote-tracking branch 'origin/develop' 2025-05-08 15:22:43 +01:00
snipe
ee4443aaf0 Merge remote-tracking branch 'origin/develop' 2025-05-08 15:09:26 +01:00
snipe
839dcad358 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
#	public/css/dist/skins/_all-skins.css
#	public/css/dist/skins/_all-skins.min.css
#	public/css/dist/skins/skin-black-dark.css
#	public/css/dist/skins/skin-black-dark.min.css
#	public/css/dist/skins/skin-blue-dark.css
#	public/css/dist/skins/skin-blue-dark.min.css
#	public/css/dist/skins/skin-green-dark.css
#	public/css/dist/skins/skin-green-dark.min.css
#	public/css/dist/skins/skin-orange-dark.css
#	public/css/dist/skins/skin-orange-dark.min.css
#	public/css/dist/skins/skin-purple-dark.css
#	public/css/dist/skins/skin-purple-dark.min.css
#	public/css/dist/skins/skin-red-dark.css
#	public/css/dist/skins/skin-red-dark.min.css
#	public/css/dist/skins/skin-yellow-dark.css
#	public/css/dist/skins/skin-yellow-dark.min.css
#	public/mix-manifest.json
2025-05-07 11:41:31 +01:00
snipe
d67933ab49 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	.all-contributorsrc
#	CONTRIBUTORS.md
2025-05-06 16:46:41 +01:00
Godfrey M
0eb3f6b952 set max to 5 2025-05-05 14:42:06 +01:00
Godfrey M
68b0f80fce fix input max, and help block position 2025-05-05 14:42:06 +01:00
Godfrey M
93489529a3 adds Field offset option to labels 2025-05-05 14:42:06 +01:00
snipe
511be74e74 Add @chfsx as a contributor 2025-05-05 14:42:06 +01:00
Fabian Schmid
bdee067803 [FIX] set upload-limit 2025-05-05 14:42:06 +01:00
snipe
32156cace3 Merge pull request #16847 from realchrisolin/issue16214
add barcode support for Avery 3490
2025-05-05 14:40:54 +01:00
snipe
30688114be Merge remote-tracking branch 'origin/develop' 2025-05-05 14:04:52 +01:00
snipe
34088bcc17 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-05-05 11:17:01 +01:00
Calvin Schwartz
07835766cc adds support for Avery 3490 2025-05-01 21:11:42 -04:00
Godfrey M
8c129c10af removed payload from api error message 2025-04-30 11:11:53 -07:00
Brady Wetherington
251851ec6a Try to get Docker images to build for both architectures 2025-04-30 18:00:39 +01:00
snipe
049a669186 Merge remote-tracking branch 'origin/develop' 2025-04-30 16:46:16 +01:00
snipe
d29f13bae9 Merge remote-tracking branch 'origin/develop' 2025-04-30 16:26:38 +01:00
snipe
c758355df9 Merge remote-tracking branch 'origin/develop' 2025-04-30 16:14:34 +01:00
snipe
79d97a83af Merge remote-tracking branch 'origin/develop' 2025-04-30 15:48:38 +01:00
snipe
85bd47c240 Merge remote-tracking branch 'origin/develop' 2025-04-30 15:35:10 +01:00
snipe
473ead9616 Merge remote-tracking branch 'origin/develop' 2025-04-30 13:57:27 +01:00
snipe
cf2850933c Merge remote-tracking branch 'origin/develop' 2025-04-30 10:19:43 +01:00
snipe
ff2564c57a Merge remote-tracking branch 'origin/develop' 2025-04-30 10:12:05 +01:00
snipe
91d3848246 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-04-29 23:17:51 +01:00
snipe
c031f0b45e Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/dist/skins/_all-skins.css
#	public/css/dist/skins/_all-skins.min.css
#	public/css/dist/skins/skin-blue-dark.css
#	public/css/dist/skins/skin-blue-dark.min.css
#	public/css/dist/skins/skin-green-dark.css
#	public/css/dist/skins/skin-green-dark.min.css
#	public/css/dist/skins/skin-orange-dark.css
#	public/css/dist/skins/skin-orange-dark.min.css
#	public/css/dist/skins/skin-purple-dark.css
#	public/css/dist/skins/skin-purple-dark.min.css
#	public/css/dist/skins/skin-red-dark.css
#	public/css/dist/skins/skin-red-dark.min.css
#	public/css/dist/skins/skin-yellow-dark.css
#	public/css/dist/skins/skin-yellow-dark.min.css
#	public/mix-manifest.json
2025-04-29 20:38:41 +01:00
snipe
fdbb9568ae Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
#	public/css/build/app.css
#	public/css/build/overrides.css
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-04-29 12:39:31 +01:00
snipe
d817883459 Merge remote-tracking branch 'origin/develop' 2025-04-29 10:25:42 +01:00
snipe
12255979ac Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-04-29 10:22:21 +01:00
snipe
366b61850b Merge remote-tracking branch 'origin/develop' 2025-04-26 17:54:02 +01:00
snipe
89be6bd183 Merge remote-tracking branch 'origin/develop' 2025-04-24 14:06:00 +01:00
snipe
e120331a2c Merge remote-tracking branch 'origin/develop' 2025-04-24 12:16:53 +01:00
snipe
cb8a212d96 Merge remote-tracking branch 'origin/develop' 2025-04-23 21:57:20 +01:00
snipe
7aec431ac5 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>
2025-04-23 21:30:16 +01:00
snipe
d19681dea1 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>
2025-04-23 21:05:38 +01:00
Godfrey M
63ce2a14fe fix the ghosts pt2 2025-04-23 10:17:11 -07:00
Godfrey M
f435ebb110 fix the ghosts 2025-04-23 10:15:26 -07:00
Godfrey M
843f001bf6 rename test class 2025-04-23 09:33:56 -07:00
snipe
bf2299daf8 Merge remote-tracking branch 'origin/develop' 2025-04-22 17:51:48 +01:00
snipe
164930d0dd Merge remote-tracking branch 'origin/develop' 2025-04-22 16:35:23 +01:00
snipe
387dbac809 Merge remote-tracking branch 'origin/develop' 2025-04-22 14:36:14 +01:00
snipe
3b661e5a99 Merge remote-tracking branch 'origin/develop' 2025-04-22 12:39:35 +01:00
snipe
90c1c0e655 Merge remote-tracking branch 'origin/develop' 2025-04-22 11:42:49 +01:00
snipe
21d8e7695b Merge remote-tracking branch 'origin/develop' 2025-04-21 20:19:54 +01:00
Godfrey M
0d28165c04 merged develop, fix conflicts 2025-04-21 10:54:01 -07:00
Godfrey M
ee31bfbcd4 Merge branch 'develop' into checkin_non_reassignable_license
# Conflicts:
#	app/Helpers/Helper.php
#	app/Http/Controllers/Api/LicenseSeatsController.php
#	app/Http/Transformers/LicensesTransformer.php
2025-04-21 10:49:35 -07:00
snipe
1acc452cfe Merge remote-tracking branch 'origin/develop' 2025-04-21 14:59:26 +01:00
snipe
1375e1feee Merge remote-tracking branch 'origin/develop' 2025-04-21 12:21:08 +01:00
snipe
2187adf59a Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>
2025-04-19 15:34:58 +01:00
snipe
0dcb315d9d Merge remote-tracking branch 'origin/develop' 2025-04-18 00:56:26 +01:00
snipe
327ccbd0c9 Merge remote-tracking branch 'origin/develop' 2025-04-18 00:37:11 +01:00
snipe
f571d400e6 Merge remote-tracking branch 'origin/develop' 2025-04-17 23:29:12 +01:00
snipe
293aa52335 Merge remote-tracking branch 'origin/develop' 2025-04-17 23:13:04 +01:00
snipe
ca178ae9a7 Merge remote-tracking branch 'origin/develop' 2025-04-17 22:48:35 +01:00
snipe
d0c810e418 Merge remote-tracking branch 'origin/develop' 2025-04-17 16:42:37 +01:00
snipe
d496d2caeb Merge remote-tracking branch 'origin/develop' 2025-04-17 11:59:24 +01:00
snipe
e70b75c350 Merge remote-tracking branch 'origin/develop' 2025-04-17 01:00:34 +01:00
snipe
a50befeda5 Merge remote-tracking branch 'origin/develop' 2025-04-16 20:16:18 +01:00
snipe
e2616e8039 Merge remote-tracking branch 'origin/develop' 2025-04-16 15:52:18 +01:00
snipe
904266debe Merge remote-tracking branch 'origin/develop' 2025-04-16 09:47:18 +01:00
snipe
72d5783795 Merge remote-tracking branch 'origin/develop' 2025-04-16 09:19:05 +01:00
snipe
d699fb1473 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>
2025-04-15 20:46:43 +01:00
snipe
fab1a6c33a Merge remote-tracking branch 'origin/develop' 2025-04-15 20:30:02 +01:00
snipe
be73c30194 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/overrides.css
#	public/css/dist/all.css
#	public/js/dist/bootstrap-table.js
#	public/mix-manifest.json
2025-04-15 20:01:20 +01:00
snipe
451646fe4f Prod assets
Signed-off-by: snipe <snipe@snipe.net>
2025-04-15 19:42:26 +01:00
snipe
decc919991 Merge remote-tracking branch 'origin/develop' 2025-04-15 19:33:23 +01:00
snipe
e0b4005921 Merge remote-tracking branch 'origin/develop' 2025-04-15 16:48:17 +01:00
snipe
3ef36e7534 Merge remote-tracking branch 'origin/develop' 2025-04-14 11:02:06 +01:00
snipe
1949e1e1e9 Merge remote-tracking branch 'origin/develop' 2025-04-14 09:26:25 +01:00
snipe
3358382358 Comment out location scoping option for now
Signed-off-by: snipe <snipe@snipe.net>
2025-04-09 21:44:38 +01:00
snipe
3eca3ecd75 Merge remote-tracking branch 'origin/develop' 2025-04-09 21:31:41 +01:00
snipe
140c6b91b0 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/overrides.css
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-04-09 02:40:28 +01:00
snipe
a5315ec240 Merge remote-tracking branch 'origin/develop' 2025-04-08 08:32:25 +01:00
snipe
93f1656e0b Merge remote-tracking branch 'origin/develop' 2025-04-08 06:51:21 +01:00
snipe
f1d006c236 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/overrides.css
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-04-08 05:58:51 +01:00
snipe
b0b5a96694 Merge remote-tracking branch 'origin/develop' 2025-04-07 13:54:23 +01:00
snipe
7dbe9a85f4 Merge remote-tracking branch 'origin/develop' 2025-04-05 21:02:33 +01:00
snipe
d2c39528d5 Merge remote-tracking branch 'origin/develop' 2025-04-05 15:44:22 +01:00
snipe
0420543c94 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/overrides.css
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-04-05 14:11:07 +01:00
snipe
aae0db902b Merge remote-tracking branch 'origin/develop' 2025-04-03 15:41:08 +01:00
snipe
88dae7cef7 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/overrides.css
#	public/css/dist/all.css
#	public/mix-manifest.json
2025-04-03 15:28:10 +01:00
snipe
e5cb17e934 Merge remote-tracking branch 'origin/develop' 2025-04-03 10:16:18 +01:00
snipe
9d609805f2 Merge remote-tracking branch 'origin/develop' 2025-04-02 18:33:48 +01:00
snipe
e2b9ca8254 Merge remote-tracking branch 'origin/develop' 2025-04-02 14:03:22 +01:00
snipe
e16a2fe8af Merge remote-tracking branch 'origin/develop' 2025-04-02 13:51:09 +01:00
snipe
22d61a533d Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-04-02 12:50:35 +01:00
snipe
af408bb45f Merge remote-tracking branch 'origin/develop' 2025-04-01 21:33:07 +01:00
snipe
24bfbc06f0 Merge remote-tracking branch 'origin/develop' 2025-04-01 19:40:03 +01:00
snipe
5eb9f353b5 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/dist/all.css
#	public/css/dist/bootstrap-table.css
#	public/js/dist/bootstrap-table.js
#	public/mix-manifest.json
2025-04-01 11:25:33 +01:00
snipe
473ce15f47 Merge pull request #16526 from snipe/develop
Merge #16486  and #16519 into master
2025-03-19 15:31:50 -01:00
snipe
eb9cfbaed6 Merge pull request #16498 from snipe/develop
Merge develop into master
2025-03-12 23:23:44 +00:00
snipe
faeb037ff9 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/overrides.css
#	public/css/dist/all.css
#	public/js/build/app.js
#	public/js/dist/all.js
#	public/mix-manifest.json
2025-03-12 21:26:34 +00:00
snipe
07602f697d Merge remote-tracking branch 'origin/develop' 2025-03-12 18:13:22 +00:00
snipe
11abb0fdb1 Merge remote-tracking branch 'origin/develop' 2025-03-11 22:13:34 +00:00
snipe
deeb2fa543 Merge remote-tracking branch 'origin/develop' 2025-03-11 21:14:12 +00:00
snipe
ef8d5ff11e Merge remote-tracking branch 'origin/develop' 2025-03-06 12:06:10 +00:00
snipe
91f3e07b83 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-03-05 17:05:28 +00:00
snipe
c29bdbdacb Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/dist/skins/_all-skins.css
#	public/css/dist/skins/_all-skins.min.css
#	public/css/dist/skins/skin-blue.css
#	public/css/dist/skins/skin-blue.min.css
#	public/mix-manifest.json
2025-03-05 13:46:29 +00:00
snipe
a20d104d2f Merge remote-tracking branch 'origin/develop' 2025-03-05 11:59:47 +00:00
snipe
a61dd8ac17 Merge remote-tracking branch 'origin/develop' 2025-03-05 10:52:42 +00:00
snipe
7ee9a690ea Merge remote-tracking branch 'origin/develop' 2025-03-05 01:12:22 +00:00
snipe
5ba94c6c41 Merge remote-tracking branch 'origin/develop' 2025-03-05 00:12:09 +00:00
snipe
9fa855c837 Prod assets
Signed-off-by: snipe <snipe@snipe.net>
2025-03-04 23:30:45 +00:00
snipe
9251007574 Merge remote-tracking branch 'origin/develop' 2025-03-04 23:29:31 +00:00
snipe
cc73b984cb Merge remote-tracking branch 'origin/develop' 2025-03-04 21:13:43 +00:00
snipe
548ef97c32 Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-03-04 19:57:33 +00:00
snipe
ed8a486726 Merge remote-tracking branch 'origin/develop' 2025-03-04 19:54:08 +00:00
snipe
1ab0911fc8 Merge remote-tracking branch 'origin/develop' 2025-03-04 19:52:16 +00:00
snipe
bdbaea7294 Merge remote-tracking branch 'origin/develop' 2025-03-04 19:43:28 +00:00
snipe
5cfd1f6fb2 Merge remote-tracking branch 'origin/develop' 2025-03-04 17:16:26 +00:00
snipe
5eda67381f Merge remote-tracking branch 'origin/develop' 2025-03-04 17:07:13 +00:00
snipe
2c8b8bfaf2 Merge remote-tracking branch 'origin/develop' 2025-03-04 17:05:55 +00:00
snipe
8f3159751a Merge remote-tracking branch 'origin/develop' 2025-03-04 17:01:07 +00:00
snipe
4b05e55b29 Merge remote-tracking branch 'origin/develop' 2025-03-04 15:56:05 +00:00
snipe
3d3c13fcd0 Merge remote-tracking branch 'origin/develop' 2025-03-04 15:38:58 +00:00
snipe
88e1d8a8cf Merge remote-tracking branch 'origin/develop' 2025-03-04 15:28:53 +00:00
snipe
e007db34e2 Merge remote-tracking branch 'origin/develop' 2025-03-03 22:12:26 +00:00
snipe
f9f06d2c02 Merge remote-tracking branch 'origin/develop' 2025-02-27 19:08:45 +00:00
snipe
234f7d00c8 Merge remote-tracking branch 'origin/develop' 2025-02-27 16:18:18 +00:00
snipe
9924553da5 Merge remote-tracking branch 'origin/develop' 2025-02-27 15:45:57 +00:00
snipe
df38d7e3ed Merge remote-tracking branch 'origin/develop'
Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
2025-02-27 12:22:30 +00:00
snipe
44dd061619 Merge remote-tracking branch 'origin/develop' 2025-02-26 20:55:57 +00:00
snipe
7603a932b1 Merge remote-tracking branch 'origin/develop' 2025-02-26 20:29:42 +00:00
snipe
138e7acc13 Merge remote-tracking branch 'origin/develop' 2025-02-26 12:47:54 +00:00
snipe
e863d3e7e5 Merge remote-tracking branch 'origin/develop' 2025-02-26 12:02:00 +00:00
snipe
c8e401f5ed Merge remote-tracking branch 'origin/develop' 2025-02-26 11:59:53 +00:00
snipe
3ba20a8e28 Merge remote-tracking branch 'origin/develop' 2025-02-26 11:39:10 +00:00
snipe
ebae63752f Merge remote-tracking branch 'origin/develop' 2025-02-26 10:25:18 +00:00
snipe
8bc73901cf Merge remote-tracking branch 'origin/develop' 2025-02-26 08:25:35 +00:00
snipe
b4f70d9244 Merge remote-tracking branch 'origin/develop' 2025-02-26 07:16:59 +00:00
snipe
21e9f2bba3 Merge remote-tracking branch 'origin/develop' 2025-02-26 07:11:22 +00:00
snipe
881f4e3d6a Merge remote-tracking branch 'origin/develop' 2025-02-25 14:43:57 +00:00
snipe
b141945add Updated branch
Signed-off-by: snipe <snipe@snipe.net>
2025-02-25 12:18:52 +00:00
Godfrey M
1c67d6802d added testing to Api check in, renamed other test method 2025-01-27 12:18:24 -08:00
Godfrey M
5da8c86ec7 fix license query for bulk licenses checkin for assets 2025-01-22 10:55:41 -08:00
Godfrey Martinez
10a2d59ec1 Merge pull request #27 from Godmartinz/checkin_non_reassignable_license_cleanup
adds bulk check in of unreassinable licenses, cleans up methods used for counting.
2025-01-22 10:49:47 -08:00
Godfrey M
34e8360b10 moves counts to licenses, allows bulk check in of unreassignable licenses 2025-01-22 10:46:28 -08:00
Godfrey Martinez
ca259ee4c3 Merge pull request #26 from Godmartinz/checkin_non_reassignable_license_cleanup
moved methods to licenseSeat model, clean up code, removed unused namespaces etc
2025-01-21 11:47:42 -08:00
Godfrey M
2143952a1e removed unused models, castged unreassignable_seat, clean up in general 2025-01-21 11:43:47 -08:00
Godfrey M
6bab6e7151 remove unintended change 2025-01-16 12:27:01 -08:00
Godfrey M
d217c2e295 last rename 2025-01-16 12:24:32 -08:00
Godfrey M
52bf0faaa5 renamed unassignable to reassignable_seat 2025-01-16 12:19:59 -08:00
Godfrey M
3f3f2bfc61 fixed factory and test 2025-01-16 12:14:03 -08:00
Godfrey M
f050864fb4 renamed dead to unassigned, replaced 0 and 1 with true and flase 2025-01-16 12:12:14 -08:00
Godfrey M
db11fc35f4 fixed typo, fixed variable name 2025-01-16 12:00:49 -08:00
Godfrey M
f47a2b10c0 updated column name, updated Api license checkin and out 2025-01-16 11:53:15 -08:00
Godfrey M
344b4e7d60 fixes test to check if checked in licenses is unavailable 2025-01-16 09:46:03 -08:00
Godfrey M
7a23372489 adds migration for column unavailable, changes logic to utlize value 2025-01-15 15:36:12 -08:00
Godfrey M
9da15a8e58 get button color correct 2025-01-13 14:44:26 -08:00
Godfrey M
50e0e4a07b remove unrelated change 2025-01-13 14:37:03 -08:00
Godfrey M
5e1562ae4c adds count on tabs and reports and index 2025-01-13 14:31:54 -08:00
Godfrey M
8a0ed49623 allows checkin of unreassignable license seats 2025-01-13 13:35:48 -08:00
1519 changed files with 18103 additions and 155600 deletions

View File

@@ -3206,7 +3206,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/3755203?v=4", "avatar_url": "https://avatars.githubusercontent.com/u/3755203?v=4",
"profile": "https://github.com/swift2512", "profile": "https://github.com/swift2512",
"contributions": [ "contributions": [
"bug" "bug",
"code"
] ]
}, },
{ {

View File

@@ -190,6 +190,7 @@ APP_ALLOW_INSECURE_HOSTS=false
GOOGLE_MAPS_API= GOOGLE_MAPS_API=
LDAP_MEM_LIM=500M LDAP_MEM_LIM=500M
LDAP_TIME_LIM=600 LDAP_TIME_LIM=600
BACKUP_TIME_LIMIT=600
IMPORT_TIME_LIMIT=600 IMPORT_TIME_LIMIT=600
IMPORT_MEMORY_LIMIT=500M IMPORT_MEMORY_LIMIT=500M
REPORT_TIME_LIMIT=12000 REPORT_TIME_LIMIT=12000

163
.github/ISSUE_TEMPLATE/Bug-Report.yml vendored Normal file
View File

@@ -0,0 +1,163 @@
name: Bug Report
description: File a bug report.
title: "[Bug]: "
projects: ["grokability/snipe-it"]
type: bug
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Most issues are documented in the [Snipe-IT repository's issues](https://github.com/grokability/snipe-it/issues) or in the official [Common Issues section of the Documentation](https://snipe-it.readme.io/docs/common-issues#/) and are due to the following:
- `.env` misconfiguration
- [Server Permissions](https://snipe-it.readme.io/docs/debugging-permissions#/)
- [Database Migrations](https://snipe-it.readme.io/docs/database-issues#run-migrations)
Please make sure you've checked these resources before submitting a new issue. If you find an existing issue, please add your context to it instead of opening a new issue. If your issue is more of a question, consider [opening a new discussion](https://github.com/grokability/snipe-it/discussions) or [pop by our Discord](https://discord.gg/yZFtShAcKk) instead of creating an issue.
**Please write your bug report in English.** You can use tools like [DeepL](https://www.deepl.com) or [Google Translate](https://translate.google.com/) to translate if necessary.
**If you choose to upload screenshots or videos (which we always encourage), please make sure they do not contain any sensitive information.**
- type: input
id: version
attributes:
label: Snipe-IT Version
description: What version of Snipe-IT are you seeing this issue on? You can find the version number in the footer of any page in Snipe-IT.
placeholder: ex. v8.3.2 - build 19577 (master)
validations:
required: true
- type: input
id: php-version
attributes:
label: PHP Version
description: What version of PHP are you running? You can find the version of PHP your webserver is running in the `Admin Settings` section in the footer, and the cli version by running `php -v` via command line .
placeholder: ex. v8.3.1 (web), PHP 8.4.12 (cli)
validations:
required: true
- type: input
id: composer-version
attributes:
label: Composer Version
description: What version of composer are you running? You can find the version number by running `composer --version`.
placeholder: ex. 2.8.10
validations:
required: true
- type: input
id: db-version
attributes:
label: MySQL/MariaDB version
description: What database are you using, and what version?
placeholder: ex. MySQL 5.7
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: How did you install Snipe-IT?
options:
- Git install
- Manual install (downloading zip/tar.gz)
- Docker
- install.sh
- Hosted by Grokability
- Other
- Not sure
validations:
required: true
- type: dropdown
id: upgrade-or-fresh
attributes:
label: Is this a fresh install or an upgrade?
options:
- Fresh install
- Upgrade
- NA
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see! (Be nice!)
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- Other
- type: dropdown
id: on-demo
attributes:
label: Can you reproduce this on the public demo?
description: You can check this at https://demo.snipeitapp.com.
options:
- 'Yes'
- 'No'
- N/A
validations:
required: true
- type: dropdown
id: fmcs
attributes:
label: Do you have full multiple company support enabled?
description: You can check this in your Snipe-IT installation at `Admin Settings > General Settings > Scoping`.
options:
- 'Yes'
- 'No'
validations:
required: true
- type: dropdown
id: fmcs-location
attributes:
label: If you have full multiple company support enabled, do you have location scoping to company enabled?
description: You can check this in your Snipe-IT installation at `Admin Settings > General Settings > Scoping`.
options:
- 'Yes'
- 'No'
- I do not have full multiple company support enabled
validations:
required: true
- type: textarea
id: server-logs
attributes:
label: Application log output
description: Please copy and paste any relevant log output from `storage/logs/laravel.log`. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: textarea
id: browser-logs
attributes:
label: Browser console output
description: Please copy and paste any relevant log output from your browser console. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: checkboxes
id: common-issues
attributes:
label: Common Issues
description: Please make sure you have done the following before submitting your issue.
options:
- label: I have searched this repo for existing issues related to my issue (including closed issues)
required: true
- label: My APP_URL is set correctly in my .env file (including http or https and no trailing slash)
required: true
- label: I have searched the official Snipe-IT documentation and have checked the Common Issues documentation (where applicable)
required: true
- label: I have run database migrations (where applicable).
required: true
- label: I have attached screenshots and/or videos of the issue (where applicable)
required: true
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/grokability/snipe-it/blob/master/CODE_OF_CONDUCT.md).
options:
- label: I agree to follow this project's Code of Conduct
required: true

View File

@@ -0,0 +1,38 @@
name: Feature Request
description: Request a new feature.
title: "[Feature]: "
projects: ["grokability/snipe-it"]
type: feature
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request! Please make sure to search the existing issues in this repository to see if your feature has already been requested, and feel free to add your context to any existing requests.
**Please write your issue in English.** You can use tools like [DeepL](https://www.deepl.com) or [Google Translate](https://translate.google.com/) to translate if necessary.
**If you choose to upload screenshots or videos (which we always encourage), please make sure they do not contain any sensitive information.**
- type: input
id: version
attributes:
label: Snipe-IT Version
description: What version of Snipe-IT are you currently running? You can find the version number in the footer of any page in Snipe-IT.
placeholder: ex. v8.3.1 - build 19577 (master)
validations:
required: true
- type: textarea
id: feature-description
attributes:
label: How can we help?
description: Let us know in detail what feature you'd like to see added. While we can't promise to implement every feature request, we do read every one and take them into consideration when planning future releases.
placeholder: Tell us what you'd like to see in Snipe-IT! (Be nice!)
validations:
required: true
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/grokability/snipe-it/blob/master/CODE_OF_CONDUCT.md).
options:
- label: I agree to follow this project's Code of Conduct
required: true

View File

@@ -30,10 +30,10 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v4
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v3 uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v4

View File

@@ -52,6 +52,6 @@ jobs:
# Upload the SARIF file generated in the previous step # Upload the SARIF file generated in the previous step
- name: Upload SARIF results file - name: Upload SARIF results file
uses: github/codeql-action/upload-sarif@v3 uses: github/codeql-action/upload-sarif@v4
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@@ -11,7 +11,7 @@ jobs:
issues: write issues: write
# pull-requests: write # pull-requests: write
steps: steps:
- uses: actions/stale@v9 - uses: actions/stale@v10
with: with:
debug-only: true debug-only: true
ascending: true ascending: true

View File

@@ -76,4 +76,16 @@ jobs:
DB_DATABASE: snipeit DB_DATABASE: snipeit
DB_PORT: ${{ job.services.mysql.ports[3306] }} DB_PORT: ${{ job.services.mysql.ports[3306] }}
DB_USERNAME: root DB_USERNAME: root
LOG_CHANNEL: single
LOG_LEVEL: debug
run: php artisan test run: php artisan test
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v5
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |
storage/logs/*.log
if-no-files-found: ignore
retention-days: 7

View File

@@ -75,4 +75,16 @@ jobs:
DB_PORT: ${{ job.services.postgresql.ports[5432] }} DB_PORT: ${{ job.services.postgresql.ports[5432] }}
DB_USERNAME: snipeit DB_USERNAME: snipeit
DB_PASSWORD: password DB_PASSWORD: password
LOG_CHANNEL: single
LOG_LEVEL: debug
run: php artisan test run: php artisan test
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v5
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |
storage/logs/*.log
if-no-files-found: ignore
retention-days: 7

View File

@@ -61,4 +61,16 @@ jobs:
- name: Execute tests (Unit and Feature tests) via PHPUnit - name: Execute tests (Unit and Feature tests) via PHPUnit
env: env:
DB_CONNECTION: sqlite DB_CONNECTION: sqlite
LOG_CHANNEL: single
LOG_LEVEL: debug
run: php artisan test run: php artisan test
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v5
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |
storage/logs/*.log
if-no-files-found: ignore
retention-days: 7

View File

@@ -52,7 +52,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars.githubusercontent.com/u/2565989?v=4" width="110px;"/><br /><sub>coach1988</sub>](https://github.com/coach1988)<br />[💻](https://github.com/snipe/snipe-it/commits?author=coach1988 "Code") | [<img src="https://avatars.githubusercontent.com/u/11910225?v=4" width="110px;"/><br /><sub>MrM</sub>](https://github.com/mauro-miatello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mauro-miatello "Code") | [<img src="https://avatars.githubusercontent.com/u/60405354?v=4" width="110px;"/><br /><sub>koiakoia</sub>](https://github.com/koiakoia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koiakoia "Code") | [<img src="https://avatars.githubusercontent.com/u/5323832?v=4" width="110px;"/><br /><sub>Mustafa Online</sub>](https://github.com/mustafa-online)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mustafa-online "Code") | [<img src="https://avatars.githubusercontent.com/u/104601439?v=4" width="110px;"/><br /><sub>franceslui</sub>](https://github.com/franceslui)<br />[💻](https://github.com/snipe/snipe-it/commits?author=franceslui "Code") | [<img src="https://avatars.githubusercontent.com/u/125313163?v=4" width="110px;"/><br /><sub>Q4kK</sub>](https://github.com/Q4kK)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Q4kK "Code") | [<img src="https://avatars.githubusercontent.com/u/55590532?v=4" width="110px;"/><br /><sub>squintfox</sub>](https://github.com/squintfox)<br />[💻](https://github.com/snipe/snipe-it/commits?author=squintfox "Code") | | [<img src="https://avatars.githubusercontent.com/u/2565989?v=4" width="110px;"/><br /><sub>coach1988</sub>](https://github.com/coach1988)<br />[💻](https://github.com/snipe/snipe-it/commits?author=coach1988 "Code") | [<img src="https://avatars.githubusercontent.com/u/11910225?v=4" width="110px;"/><br /><sub>MrM</sub>](https://github.com/mauro-miatello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mauro-miatello "Code") | [<img src="https://avatars.githubusercontent.com/u/60405354?v=4" width="110px;"/><br /><sub>koiakoia</sub>](https://github.com/koiakoia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koiakoia "Code") | [<img src="https://avatars.githubusercontent.com/u/5323832?v=4" width="110px;"/><br /><sub>Mustafa Online</sub>](https://github.com/mustafa-online)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mustafa-online "Code") | [<img src="https://avatars.githubusercontent.com/u/104601439?v=4" width="110px;"/><br /><sub>franceslui</sub>](https://github.com/franceslui)<br />[💻](https://github.com/snipe/snipe-it/commits?author=franceslui "Code") | [<img src="https://avatars.githubusercontent.com/u/125313163?v=4" width="110px;"/><br /><sub>Q4kK</sub>](https://github.com/Q4kK)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Q4kK "Code") | [<img src="https://avatars.githubusercontent.com/u/55590532?v=4" width="110px;"/><br /><sub>squintfox</sub>](https://github.com/squintfox)<br />[💻](https://github.com/snipe/snipe-it/commits?author=squintfox "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1380084?v=4" width="110px;"/><br /><sub>Jeff Clay</sub>](https://github.com/jeffclay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jeffclay "Code") | [<img src="https://avatars.githubusercontent.com/u/52716446?v=4" width="110px;"/><br /><sub>Phil J R</sub>](https://github.com/PP-JN-RL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PP-JN-RL "Code") | [<img src="https://avatars.githubusercontent.com/u/1496725?v=4" width="110px;"/><br /><sub>i_virus</sub>](https://www.corelight.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chandanchowdhury "Code") | [<img src="https://avatars.githubusercontent.com/u/1020541?v=4" width="110px;"/><br /><sub>Paul Grime</sub>](https://github.com/gitgrimbo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gitgrimbo "Code") | [<img src="https://avatars.githubusercontent.com/u/922815?v=4" width="110px;"/><br /><sub>Lee Porte</sub>](https://leeporte.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeePorte "Code") | [<img src="https://avatars.githubusercontent.com/u/23613427?v=4" width="110px;"/><br /><sub>BRYAN </sub>](https://github.com/bryanlopezinc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Tests") | [<img src="https://avatars.githubusercontent.com/u/64061710?v=4" width="110px;"/><br /><sub>U-H-T</sub>](https://github.com/U-H-T)<br />[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") | | [<img src="https://avatars.githubusercontent.com/u/1380084?v=4" width="110px;"/><br /><sub>Jeff Clay</sub>](https://github.com/jeffclay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jeffclay "Code") | [<img src="https://avatars.githubusercontent.com/u/52716446?v=4" width="110px;"/><br /><sub>Phil J R</sub>](https://github.com/PP-JN-RL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PP-JN-RL "Code") | [<img src="https://avatars.githubusercontent.com/u/1496725?v=4" width="110px;"/><br /><sub>i_virus</sub>](https://www.corelight.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chandanchowdhury "Code") | [<img src="https://avatars.githubusercontent.com/u/1020541?v=4" width="110px;"/><br /><sub>Paul Grime</sub>](https://github.com/gitgrimbo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gitgrimbo "Code") | [<img src="https://avatars.githubusercontent.com/u/922815?v=4" width="110px;"/><br /><sub>Lee Porte</sub>](https://leeporte.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeePorte "Code") | [<img src="https://avatars.githubusercontent.com/u/23613427?v=4" width="110px;"/><br /><sub>BRYAN </sub>](https://github.com/bryanlopezinc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Tests") | [<img src="https://avatars.githubusercontent.com/u/64061710?v=4" width="110px;"/><br /><sub>U-H-T</sub>](https://github.com/U-H-T)<br />[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") |
| [<img src="https://avatars.githubusercontent.com/u/5395363?v=4" width="110px;"/><br /><sub>Matt Tyree</sub>](https://github.com/Tyree)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [<img src="https://avatars.githubusercontent.com/u/292081?v=4" width="110px;"/><br /><sub>Florent Bervas</sub>](http://spoontux.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorentDotMe "Code") | [<img src="https://avatars.githubusercontent.com/u/4498077?v=4" width="110px;"/><br /><sub>Daniel Albertsen</sub>](https://ditscheri.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dbakan "Code") | [<img src="https://avatars.githubusercontent.com/u/100710244?v=4" width="110px;"/><br /><sub>r-xyz</sub>](https://github.com/r-xyz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=r-xyz "Code") | [<img src="https://avatars.githubusercontent.com/u/47491036?v=4" width="110px;"/><br /><sub>Steven Mainor</sub>](https://github.com/DrekiDegga)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DrekiDegga "Code") | [<img src="https://avatars.githubusercontent.com/u/65785975?v=4" width="110px;"/><br /><sub>arne-kroeger</sub>](https://github.com/arne-kroeger)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arne-kroeger "Code") | [<img src="https://avatars.githubusercontent.com/u/167117705?v=4" width="110px;"/><br /><sub>Glukose1</sub>](https://github.com/Glukose1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Glukose1 "Code") | | [<img src="https://avatars.githubusercontent.com/u/5395363?v=4" width="110px;"/><br /><sub>Matt Tyree</sub>](https://github.com/Tyree)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [<img src="https://avatars.githubusercontent.com/u/292081?v=4" width="110px;"/><br /><sub>Florent Bervas</sub>](http://spoontux.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorentDotMe "Code") | [<img src="https://avatars.githubusercontent.com/u/4498077?v=4" width="110px;"/><br /><sub>Daniel Albertsen</sub>](https://ditscheri.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dbakan "Code") | [<img src="https://avatars.githubusercontent.com/u/100710244?v=4" width="110px;"/><br /><sub>r-xyz</sub>](https://github.com/r-xyz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=r-xyz "Code") | [<img src="https://avatars.githubusercontent.com/u/47491036?v=4" width="110px;"/><br /><sub>Steven Mainor</sub>](https://github.com/DrekiDegga)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DrekiDegga "Code") | [<img src="https://avatars.githubusercontent.com/u/65785975?v=4" width="110px;"/><br /><sub>arne-kroeger</sub>](https://github.com/arne-kroeger)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arne-kroeger "Code") | [<img src="https://avatars.githubusercontent.com/u/167117705?v=4" width="110px;"/><br /><sub>Glukose1</sub>](https://github.com/Glukose1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Glukose1 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1197791?v=4" width="110px;"/><br /><sub>Scarzy</sub>](https://github.com/Scarzy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scarzy "Code") | [<img src="https://avatars.githubusercontent.com/u/37372069?v=4" width="110px;"/><br /><sub>setpill</sub>](https://github.com/setpill)<br />[💻](https://github.com/snipe/snipe-it/commits?author=setpill "Code") | [<img src="https://avatars.githubusercontent.com/u/3755203?v=4" width="110px;"/><br /><sub>swift2512</sub>](https://github.com/swift2512)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aswift2512 "Bug reports") | [<img src="https://avatars.githubusercontent.com/u/6136439?v=4" width="110px;"/><br /><sub>Darren Rainey</sub>](https://darrenraineys.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DarrenRainey "Code") | [<img src="https://avatars.githubusercontent.com/u/133033121?v=4" width="110px;"/><br /><sub>maciej-poleszczyk</sub>](https://github.com/maciej-poleszczyk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=maciej-poleszczyk "Code") | [<img src="https://avatars.githubusercontent.com/u/143394709?v=4" width="110px;"/><br /><sub>Sebastian Groß</sub>](https://github.com/sgross-emlix)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sgross-emlix "Code") | [<img src="https://avatars.githubusercontent.com/u/41107778?v=4" width="110px;"/><br /><sub>Anouar Touati</sub>](https://github.com/AnouarTouati)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AnouarTouati "Code") | | [<img src="https://avatars.githubusercontent.com/u/1197791?v=4" width="110px;"/><br /><sub>Scarzy</sub>](https://github.com/Scarzy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scarzy "Code") | [<img src="https://avatars.githubusercontent.com/u/37372069?v=4" width="110px;"/><br /><sub>setpill</sub>](https://github.com/setpill)<br />[💻](https://github.com/snipe/snipe-it/commits?author=setpill "Code") | [<img src="https://avatars.githubusercontent.com/u/3755203?v=4" width="110px;"/><br /><sub>swift2512</sub>](https://github.com/swift2512)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aswift2512 "Bug reports") [💻](https://github.com/snipe/snipe-it/commits?author=swift2512 "Code") | [<img src="https://avatars.githubusercontent.com/u/6136439?v=4" width="110px;"/><br /><sub>Darren Rainey</sub>](https://darrenraineys.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DarrenRainey "Code") | [<img src="https://avatars.githubusercontent.com/u/133033121?v=4" width="110px;"/><br /><sub>maciej-poleszczyk</sub>](https://github.com/maciej-poleszczyk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=maciej-poleszczyk "Code") | [<img src="https://avatars.githubusercontent.com/u/143394709?v=4" width="110px;"/><br /><sub>Sebastian Groß</sub>](https://github.com/sgross-emlix)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sgross-emlix "Code") | [<img src="https://avatars.githubusercontent.com/u/41107778?v=4" width="110px;"/><br /><sub>Anouar Touati</sub>](https://github.com/AnouarTouati)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AnouarTouati "Code") |
| [<img src="https://avatars.githubusercontent.com/u/25596663?v=4" width="110px;"/><br /><sub>aHVzY2g</sub>](https://github.com/aHVzY2g)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aHVzY2g "Code") | [<img src="https://avatars.githubusercontent.com/u/13408130?v=4" width="110px;"/><br /><sub>林博仁 Buo-ren Lin</sub>](https://brlin.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=brlin-tw "Code") | [<img src="https://avatars.githubusercontent.com/u/18550946?v=4" width="110px;"/><br /><sub>Adugna Gizaw</sub>](https://orbalia.pythonanywhere.com/)<br />[🌍](#translation-addex12 "Translation") | [<img src="https://avatars.githubusercontent.com/u/760989?v=4" width="110px;"/><br /><sub>Jesse Ostrander</sub>](https://github.com/jostrander)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jostrander "Code") | [<img src="https://avatars.githubusercontent.com/u/31522486?v=4" width="110px;"/><br /><sub>James M</sub>](https://github.com/azmcnutt)<br />[💻](https://github.com/snipe/snipe-it/commits?author=azmcnutt "Code") | [<img src="https://avatars.githubusercontent.com/u/5183146?v=4" width="110px;"/><br /><sub>Fiala06</sub>](https://github.com/Fiala06)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Fiala06 "Code") | [<img src="https://avatars.githubusercontent.com/u/28693782?v=4" width="110px;"/><br /><sub>Nathan Taylor</sub>](https://github.com/ntaylor-86)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ntaylor-86 "Code") | | [<img src="https://avatars.githubusercontent.com/u/25596663?v=4" width="110px;"/><br /><sub>aHVzY2g</sub>](https://github.com/aHVzY2g)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aHVzY2g "Code") | [<img src="https://avatars.githubusercontent.com/u/13408130?v=4" width="110px;"/><br /><sub>林博仁 Buo-ren Lin</sub>](https://brlin.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=brlin-tw "Code") | [<img src="https://avatars.githubusercontent.com/u/18550946?v=4" width="110px;"/><br /><sub>Adugna Gizaw</sub>](https://orbalia.pythonanywhere.com/)<br />[🌍](#translation-addex12 "Translation") | [<img src="https://avatars.githubusercontent.com/u/760989?v=4" width="110px;"/><br /><sub>Jesse Ostrander</sub>](https://github.com/jostrander)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jostrander "Code") | [<img src="https://avatars.githubusercontent.com/u/31522486?v=4" width="110px;"/><br /><sub>James M</sub>](https://github.com/azmcnutt)<br />[💻](https://github.com/snipe/snipe-it/commits?author=azmcnutt "Code") | [<img src="https://avatars.githubusercontent.com/u/5183146?v=4" width="110px;"/><br /><sub>Fiala06</sub>](https://github.com/Fiala06)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Fiala06 "Code") | [<img src="https://avatars.githubusercontent.com/u/28693782?v=4" width="110px;"/><br /><sub>Nathan Taylor</sub>](https://github.com/ntaylor-86)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ntaylor-86 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/16699443?v=4" width="110px;"/><br /><sub>fvollmer</sub>](https://github.com/fvollmer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fvollmer "Code") | [<img src="https://avatars.githubusercontent.com/u/109086466?v=4" width="110px;"/><br /><sub>36864</sub>](https://github.com/36864)<br />[💻](https://github.com/snipe/snipe-it/commits?author=36864 "Code") | [<img src="https://avatars.githubusercontent.com/u/365751?v=4" width="110px;"/><br /><sub>Daniel O'Connor</sub>](http://clockwerx.blogspot.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CloCkWeRX "Code") | [<img src="https://avatars.githubusercontent.com/u/102852568?v=4" width="110px;"/><br /><sub>BeatSpark</sub>](https://github.com/BeatSpark)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BeatSpark "Code") | [<img src="https://avatars.githubusercontent.com/u/59203607?v=4" width="110px;"/><br /><sub>mrdahbi</sub>](https://github.com/mrdahbi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mrdahbi "Code") | [<img src="https://avatars.githubusercontent.com/u/6661332?v=4" width="110px;"/><br /><sub>Fabian Schmid</sub>](http://sr.solutions)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chfsx "Code") | [<img src="https://avatars.githubusercontent.com/u/1288116?v=4" width="110px;"/><br /><sub>Chris Olin</sub>](https://www.chrisolin.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=realchrisolin "Code") | | [<img src="https://avatars.githubusercontent.com/u/16699443?v=4" width="110px;"/><br /><sub>fvollmer</sub>](https://github.com/fvollmer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fvollmer "Code") | [<img src="https://avatars.githubusercontent.com/u/109086466?v=4" width="110px;"/><br /><sub>36864</sub>](https://github.com/36864)<br />[💻](https://github.com/snipe/snipe-it/commits?author=36864 "Code") | [<img src="https://avatars.githubusercontent.com/u/365751?v=4" width="110px;"/><br /><sub>Daniel O'Connor</sub>](http://clockwerx.blogspot.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CloCkWeRX "Code") | [<img src="https://avatars.githubusercontent.com/u/102852568?v=4" width="110px;"/><br /><sub>BeatSpark</sub>](https://github.com/BeatSpark)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BeatSpark "Code") | [<img src="https://avatars.githubusercontent.com/u/59203607?v=4" width="110px;"/><br /><sub>mrdahbi</sub>](https://github.com/mrdahbi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mrdahbi "Code") | [<img src="https://avatars.githubusercontent.com/u/6661332?v=4" width="110px;"/><br /><sub>Fabian Schmid</sub>](http://sr.solutions)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chfsx "Code") | [<img src="https://avatars.githubusercontent.com/u/1288116?v=4" width="110px;"/><br /><sub>Chris Olin</sub>](https://www.chrisolin.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=realchrisolin "Code") |
| [<img src="https://avatars.githubusercontent.com/u/3803132?v=4" width="110px;"/><br /><sub>Dan</sub>](https://github.com/mnemonicly)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mnemonicly "Code") | [<img src="https://avatars.githubusercontent.com/u/43917728?v=4" width="110px;"/><br /><sub>Nebel</sub>](https://github.com/NebelKreis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NebelKreis "Code") | [<img src="https://avatars.githubusercontent.com/u/132433803?v=4" width="110px;"/><br /><sub>test1337ahp</sub>](https://github.com/test1337ahp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=test1337ahp "Code") | [<img src="https://avatars.githubusercontent.com/u/1916566?v=4" width="110px;"/><br /><sub>Jonathon Reinhart</sub>](https://github.com/JonathonReinhart)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JonathonReinhart "Code") | [<img src="https://avatars.githubusercontent.com/u/484742?v=4" width="110px;"/><br /><sub>aranar-pro</sub>](https://github.com/aranar-pro)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aranar-pro "Code") | [<img src="https://avatars.githubusercontent.com/u/27019397?v=4" width="110px;"/><br /><sub>Phil</sub>](https://github.com/phil-flip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=phil-flip "Code") | [<img src="https://avatars.githubusercontent.com/u/6473460?v=4" width="110px;"/><br /><sub>Steffy Fort</sub>](https://fe80.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fe80 "Code") | | [<img src="https://avatars.githubusercontent.com/u/3803132?v=4" width="110px;"/><br /><sub>Dan</sub>](https://github.com/mnemonicly)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mnemonicly "Code") | [<img src="https://avatars.githubusercontent.com/u/43917728?v=4" width="110px;"/><br /><sub>Nebel</sub>](https://github.com/NebelKreis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NebelKreis "Code") | [<img src="https://avatars.githubusercontent.com/u/132433803?v=4" width="110px;"/><br /><sub>test1337ahp</sub>](https://github.com/test1337ahp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=test1337ahp "Code") | [<img src="https://avatars.githubusercontent.com/u/1916566?v=4" width="110px;"/><br /><sub>Jonathon Reinhart</sub>](https://github.com/JonathonReinhart)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JonathonReinhart "Code") | [<img src="https://avatars.githubusercontent.com/u/484742?v=4" width="110px;"/><br /><sub>aranar-pro</sub>](https://github.com/aranar-pro)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aranar-pro "Code") | [<img src="https://avatars.githubusercontent.com/u/27019397?v=4" width="110px;"/><br /><sub>Phil</sub>](https://github.com/phil-flip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=phil-flip "Code") | [<img src="https://avatars.githubusercontent.com/u/6473460?v=4" width="110px;"/><br /><sub>Steffy Fort</sub>](https://fe80.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fe80 "Code") |

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Actions\Categories;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssetModels;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Models\Category;
use Illuminate\Support\Facades\Storage;
class DestroyCategoryAction
{
/**
* @throws ItemStillHasAssets
* @throws ItemStillHasAssetModels
* @throws ItemStillHasComponents
* @throws ItemStillHasAccessories
* @throws ItemStillHasLicenses
* @throws ItemStillHasConsumables
*/
static function run(Category $category): bool
{
$category->loadCount([
'assets as assets_count',
'accessories as accessories_count',
'consumables as consumables_count',
'components as components_count',
'licenses as licenses_count',
'models as models_count'
]);
if ($category->assets_count > 0) {
throw new ItemStillHasAssets($category);
}
if ($category->accessories_count > 0) {
throw new ItemStillHasAccessories($category);
}
if ($category->consumables_count > 0) {
throw new ItemStillHasConsumables($category);
}
if ($category->components_count > 0) {
throw new ItemStillHasComponents($category);
}
if ($category->licenses_count > 0) {
throw new ItemStillHasLicenses($category);
}
if ($category->models_count > 0) {
throw new ItemStillHasAssetModels($category);
}
Storage::disk('public')->delete('categories'.'/'.$category->image);
$category->delete();
return true;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Actions\Manufacturers;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Models\Manufacturer;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class DeleteManufacturerAction
{
/**
* @throws ItemStillHasAssets
* @throws ItemStillHasComponents
* @throws ItemStillHasAccessories
* @throws ItemStillHasLicenses
* @throws ItemStillHasConsumables
*/
static function run(Manufacturer $manufacturer): bool
{
$manufacturer->loadCount([
'assets as assets_count',
'accessories as accessories_count',
'consumables as consumables_count',
'components as components_count',
'licenses as licenses_count',
]);
if ($manufacturer->assets_count > 0) {
throw new ItemStillHasAssets($manufacturer);
}
if ($manufacturer->accessories_count > 0) {
throw new ItemStillHasAccessories($manufacturer);
}
if ($manufacturer->consumables_count > 0) {
throw new ItemStillHasConsumables($manufacturer);
}
if ($manufacturer->components_count > 0) {
throw new ItemStillHasComponents($manufacturer);
}
if ($manufacturer->licenses_count > 0) {
throw new ItemStillHasLicenses($manufacturer);
}
if ($manufacturer->image) {
try {
Storage::disk('public')->delete('manufacturers/'.$manufacturer->image);
} catch (\Exception $e) {
Log::info($e);
}
}
$manufacturer->delete();
//dd($manufacturer);
return true;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Actions\Suppliers;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Models\Supplier;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasMaintenances;
use App\Exceptions\ItemStillHasLicenses;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class DestroySupplierAction
{
/**
*
* @throws ItemStillHasLicenses
* @throws ItemStillHasAssets
* @throws ItemStillHasMaintenances
* @throws ItemStillHasAccessories
* @throws ItemStillHasConsumables
* @throws ItemStillHasComponents
*/
static function run(Supplier $supplier): bool
{
$supplier->loadCount([
'maintenances as maintenances_count',
'assets as assets_count',
'licenses as licenses_count',
'accessories as accessories_count',
'consumables as consumables_count',
'components as components_count',
]);
if ($supplier->assets_count > 0) {
throw new ItemStillHasAssets($supplier);
}
if ($supplier->maintenances_count > 0) {
throw new ItemStillHasMaintenances($supplier);
}
if ($supplier->licenses_count > 0) {
throw new ItemStillHasLicenses($supplier);
}
if ($supplier->accessories_count > 0) {
throw new ItemStillHasAccessories($supplier);
}
if ($supplier->consumables_count > 0) {
throw new ItemStillHasConsumables($supplier);
}
if ($supplier->components_count > 0) {
throw new ItemStillHasComponents($supplier);
}
if ($supplier->image) {
try {
Storage::disk('public')->delete('suppliers/'.$supplier->image);
} catch (\Exception $e) {
Log::info($e->getMessage());
}
}
$supplier->delete();
return true;
}
}

View File

@@ -317,9 +317,21 @@ class LdapSync extends Command
if($ldap_map["jobtitle"] != null){ if($ldap_map["jobtitle"] != null){
$user->jobtitle = $item['jobtitle']; $user->jobtitle = $item['jobtitle'];
} }
if($ldap_map["address"] != null){
$user->address = $item['address'];
}
if($ldap_map["city"] != null){
$user->city = $item['city'];
}
if($ldap_map["state"] != null){
$user->state = $item['state'];
}
if($ldap_map["country"] != null){ if($ldap_map["country"] != null){
$user->country = $item['country']; $user->country = $item['country'];
} }
if($ldap_map["zip"] != null){
$user->zip = $item['zip'];
}
if($ldap_map["dept"] != null){ if($ldap_map["dept"] != null){
$user->department_id = $department->id; $user->department_id = $department->id;
} }

View File

@@ -8,8 +8,6 @@ use Symfony\Component\Console\Input\InputOption;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Symfony\Component\Console\Helper\ProgressIndicator; use Symfony\Component\Console\Helper\ProgressIndicator;
ini_set('max_execution_time', env('IMPORT_TIME_LIMIT', 600)); //600 seconds = 10 minutes
ini_set('memory_limit', env('IMPORT_MEMORY_LIMIT', '500M'));
/** /**
* Class ObjectImportCommand * Class ObjectImportCommand
@@ -52,6 +50,9 @@ class ObjectImportCommand extends Command
*/ */
public function handle() public function handle()
{ {
ini_set('max_execution_time', env('IMPORT_TIME_LIMIT', 600)); //600 seconds = 10 minutes
ini_set('memory_limit', env('IMPORT_MEMORY_LIMIT', '500M'));
$this->progressIndicator = new ProgressIndicator($this->output); $this->progressIndicator = new ProgressIndicator($this->output);
$filename = $this->argument('filename'); $filename = $this->argument('filename');

View File

@@ -59,6 +59,9 @@ class PaveIt extends Command
'migrations', 'migrations',
'settings', 'settings',
'users', 'users',
'telescope_entries',
'telescope_entries_tags',
'telescope_monitoring',
]; ];
// We only need to find out what these are so we can nuke these columns on the assets table. // We only need to find out what these are so we can nuke these columns on the assets table.

View File

@@ -65,7 +65,7 @@ class Purge extends Command
$maintenances = 0; $maintenances = 0;
foreach ($assets as $asset) { foreach ($assets as $asset) {
$this->info('- Asset "'.$asset->present()->name().'" deleted.'); $this->info('- Asset "'.$asset->display_name.'" deleted.');
$asset_assoc += $asset->assetlog()->count(); $asset_assoc += $asset->assetlog()->count();
$asset->assetlog()->forceDelete(); $asset->assetlog()->forceDelete();
$maintenances += $asset->maintenances()->count(); $maintenances += $asset->maintenances()->count();

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Console\Commands;
use App\Models\Actionlog;
use Illuminate\Console\Command;
class RemoveInvalidUploadDeleteActionLogItems extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:remove-invalid-upload-delete-action-log-items';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Permanently remove invalid "upload deleted" action log items that have a null filename. This command can potentially result in deleted files being "resurrected" in the UI.';
/**
* Execute the console command.
*/
public function handle()
{
$invalidLogs = Actionlog::query()
->where('action_type', 'upload deleted')
->whereNull('filename')
->withTrashed()
->get();
$this->info("{$invalidLogs->count()} invalid log items found.");
if ($invalidLogs->count() === 0) {
return 0;
}
$this->table(['ID', 'Action Type', 'Item Type', 'Item ID', 'Created At', 'Deleted At'], $invalidLogs->map(fn($log) => [
$log->id,
$log->action_type,
$log->item_type,
$log->item_id,
$log->created_at,
$log->deleted_at,
])->toArray());
if ($this->confirm("Do you wish to remove {$invalidLogs->count()} log items?")) {
$invalidLogs->each(fn($log) => $log->forceDelete());
}
return 0;
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use ZipArchive; use ZipArchive;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use enshrined\svgSanitize\Sanitizer;
class SQLStreamer { class SQLStreamer {
private $input; private $input;
@@ -242,9 +243,10 @@ class RestoreFromBackup extends Command
$private_dirs = [ $private_dirs = [
'storage/private_uploads/accessories', 'storage/private_uploads/accessories',
'storage/private_uploads/assetmodels', 'storage/private_uploads/assetmodels' => 'storage/private_uploads/models', //this was changed from assetmodels => models Aug 10 2025
'storage/private_uploads/maintenances', 'storage/private_uploads/asset_maintenances' => 'storage/private_uploads/maintenances', //this was changed from asset_maintenances => maintenances Aug 10 2025
'storage/private_uploads/models', 'storage/private_uploads/maintenances', //but let 'maintenances' take precedence
'storage/private_uploads/models', //and let 'models' take precedence
'storage/private_uploads/assets', // these are asset _files_, not the pictures. 'storage/private_uploads/assets', // these are asset _files_, not the pictures.
'storage/private_uploads/audits', 'storage/private_uploads/audits',
'storage/private_uploads/components', 'storage/private_uploads/components',
@@ -262,7 +264,7 @@ class RestoreFromBackup extends Command
]; ];
$public_dirs = [ $public_dirs = [
'public/uploads/accessories', 'public/uploads/accessories',
'public/uploads/assetmodels', // 'public/uploads/assetmodels' => 'public/uploads/models', //according to git, this was _never_ a thing... (see below)
'public/uploads/maintenances', 'public/uploads/maintenances',
'public/uploads/assets', // these are asset _pictures_, not asset files 'public/uploads/assets', // these are asset _pictures_, not asset files
'public/uploads/avatars', 'public/uploads/avatars',
@@ -273,7 +275,7 @@ class RestoreFromBackup extends Command
'public/uploads/departments', 'public/uploads/departments',
'public/uploads/locations', 'public/uploads/locations',
'public/uploads/manufacturers', 'public/uploads/manufacturers',
'public/uploads/models', 'public/uploads/models', // ...it's been this way for 9 years (as of late 2025)
'public/uploads/suppliers', 'public/uploads/suppliers',
]; ];
@@ -286,8 +288,6 @@ class RestoreFromBackup extends Command
'public/uploads/favicon-uploaded.*', 'public/uploads/favicon-uploaded.*',
]; ];
$all_files = $private_dirs + $public_dirs;
$sqlfiles = []; $sqlfiles = [];
$sqlfile_indices = []; $sqlfile_indices = [];
@@ -295,6 +295,20 @@ class RestoreFromBackup extends Command
$boring_files = []; $boring_files = [];
$unsafe_files = []; $unsafe_files = [];
$good_extensions = config('filesystems.allowed_upload_extensions_array');
$private_extensions = array_merge($good_extensions, ["csv", "key"]); //add csv, and 'key'
$public_extensions = array_diff($good_extensions, ["xml"]); //remove xml
$sanitizer = new Sanitizer();
/**
* TODO: I _hate_ the "continue 3" thing we keep doing here
* I think a better approach might be to have the "each file" stuff be in a method on this class, and the
* boring_files and interesting_files be properties on it that we fill out. Then, in that method, we could
* just do a 'return' once the file is actually handled (yay or nay). We could also start to break out some of
* the _other_ things that we do into their own methods too? But I don't care about that as much.
*/
for ($i = 0; $i < $za->numFiles; $i++) { for ($i = 0; $i < $za->numFiles; $i++) {
$stat_results = $za->statIndex($i); $stat_results = $za->statIndex($i);
// echo "index: $i\n"; // echo "index: $i\n";
@@ -309,7 +323,7 @@ class RestoreFromBackup extends Command
// skip macOS resource fork files (?!?!?!) // skip macOS resource fork files (?!?!?!)
if (strpos($raw_path, '__MACOSX') !== false && strpos($raw_path, '._') !== false) { if (strpos($raw_path, '__MACOSX') !== false && strpos($raw_path, '._') !== false) {
//print "SKIPPING macOS Resource fork file: $raw_path\n"; //print "SKIPPING macOS Resource fork file: $raw_path\n";
$boring_files[] = $raw_path; // $boring_files[] = $raw_path; //stop adding this to the boring files list; it's just confusing
continue; continue;
} }
if (@pathinfo($raw_path, PATHINFO_EXTENSION) == 'sql') { if (@pathinfo($raw_path, PATHINFO_EXTENSION) == 'sql') {
@@ -318,44 +332,70 @@ class RestoreFromBackup extends Command
$sqlfile_indices[] = $i; $sqlfile_indices[] = $i;
continue; continue;
} }
if ($raw_path[-1] == '/') {
//last character is '/' - this is a directory, and we don't need it, and we don't need to warn about it
continue;
}
if (in_array(basename($raw_path), [".gitkeep", ".gitignore", ".DS_Store"])) {
//skip these boring files silently without reporting on them; they're stupid
continue;
}
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
foreach (array_merge($private_dirs, $public_dirs) as $dir) { foreach (['public' => $public_dirs, 'private' => $private_dirs] as $purpose => $dirs) {
$last_pos = strrpos($raw_path, $dir . '/'); $allowed_extensions = match ($purpose) {
if ($last_pos !== false) { 'public' => $public_extensions,
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $dir - last_pos+strlen(\$dir) is: ".($last_pos+strlen($dir))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n"); 'private' => $private_extensions,
//print("We would copy $raw_path to $dir.\n"); //FIXME append to a path? };
$interesting_files[$raw_path] = ['dest' => $dir, 'index' => $i]; foreach ($dirs as $dir => $destdir) {
continue 2; if (is_int($dir)) {
if ($last_pos + strlen($dir) + 1 == strlen($raw_path)) { $dir = $destdir;
// we don't care about that; we just want files with the appropriate prefix }
//print("FOUND THE EXACT DIRECTORY: $dir AT: $raw_path!!!\n"); $last_pos = strrpos($raw_path, $dir . '/');
if ($last_pos !== false) {
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $dir - last_pos+strlen(\$dir) is: ".($last_pos+strlen($dir))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
//print("We would copy $raw_path to $dir.\n"); //FIXME append to a path?
//the CSV bit, below, is because we store CSV files as "blahcsv" - without an extension
if (!in_array($extension, $allowed_extensions) && !($dir == "storage/private_uploads/imports" && substr($raw_path, -3) == "csv" && $extension == "")) {
$unsafe_files[] = $raw_path;
Log::debug($raw_path . ' from directory ' . $dir . ' is being skipped');
} else {
if ($dir != $destdir) {
Log::debug("Getting ready to save file $raw_path to new directory $destdir");
}
$interesting_files[$raw_path] = ['dest' => $destdir, 'index' => $i];
}
continue 3;
} }
} }
} }
$good_extensions = config('filesystems.allowed_upload_extensions_array'); foreach (['public' => $public_files, 'private' => $private_files] as $purpose => $files) {
$allowed_extensions = match ($purpose) {
foreach (array_merge($private_files, $public_files) as $file) { 'public' => $public_extensions,
$has_wildcard = (strpos($file, '*') !== false); 'private' => $private_extensions,
if ($has_wildcard) { };
$file = substr($file, 0, -1); //trim last character (which should be the wildcard) foreach ($files as $file) {
} $has_wildcard = (strpos($file, '*') !== false);
$last_pos = strrpos($raw_path, $file); // no trailing slash! if ($has_wildcard) {
if ($last_pos !== false) { $file = substr($file, 0, -1); //trim last character (which should be the wildcard)
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
if (!in_array($extension, $good_extensions)) {
// gathering potentially unsafe files here to return at exit
$unsafe_files[] = $raw_path;
Log::debug('Potentially unsafe file '.$raw_path.' is being skipped');
$boring_files[] = $raw_path;
continue 2;
} }
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $file - last_pos+strlen(\$file) is: ".($last_pos+strlen($file))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n"); $last_pos = strrpos($raw_path, $file); // no trailing slash!
//no wildcards found in $file, process 'normally' if ($last_pos !== false) {
if ($last_pos + strlen($file) == strlen($raw_path) || $has_wildcard) { //again, no trailing slash. or this is a wildcard and we just take it. if (!in_array($extension, $allowed_extensions)) {
// print("FOUND THE EXACT FILE: $file AT: $raw_path!!!\n"); //we *do* care about this, though. // gathering potentially unsafe files here to return at exit
$interesting_files[$raw_path] = ['dest' => dirname($file), 'index' => $i]; $unsafe_files[] = $raw_path;
continue 2; Log::debug('Potentially unsafe file ' . $raw_path . ' is being skipped');
$boring_files[] = $raw_path;
continue 3;
}
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $file - last_pos+strlen(\$file) is: ".($last_pos+strlen($file))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
//no wildcards found in $file, process 'normally'
if ($last_pos + strlen($file) == strlen($raw_path) || $has_wildcard) { //again, no trailing slash. or this is a wildcard and we just take it.
// print("FOUND THE EXACT FILE: $file AT: $raw_path!!!\n"); //we *do* care about this, though.
$interesting_files[$raw_path] = ['dest' => dirname($file), 'index' => $i];
continue 3;
}
} }
} }
} }
@@ -492,18 +532,25 @@ class RestoreFromBackup extends Command
} }
foreach ($interesting_files as $pretty_file_name => $file_details) { foreach ($interesting_files as $pretty_file_name => $file_details) {
$ugly_file_name = $za->statIndex($file_details['index'])['name']; $ugly_file_name = $za->statIndex($file_details['index'])['name'];
$fp = $za->getStream($ugly_file_name); $migrated_file_name = $file_details['dest'] . '/' . basename($pretty_file_name);
//$this->info("Weird problem, here are file details? ".print_r($file_details,true)); if (strcasecmp(substr($pretty_file_name, -4), ".svg") === 0) {
if (!is_dir($file_details['dest'])) { $svg_contents = $za->getFromIndex($file_details['index']);
mkdir($file_details['dest'], 0755, true); //0755 is what Laravel uses, so we do that $cleaned_svg = $sanitizer->sanitize($svg_contents);
file_put_contents($migrated_file_name, $cleaned_svg);
} else {
$fp = $za->getStream($ugly_file_name);
//$this->info("Weird problem, here are file details? ".print_r($file_details,true));
if (!is_dir($file_details['dest'])) {
mkdir($file_details['dest'], 0755, true); //0755 is what Laravel uses, so we do that
}
$migrated_file = fopen($migrated_file_name, 'w');
while (($buffer = fgets($fp, SQLStreamer::$buffer_size)) !== false) {
fwrite($migrated_file, $buffer);
}
fclose($migrated_file);
fclose($fp);
//$this->info("Wrote $ugly_file_name to $pretty_file_name");
} }
$migrated_file = fopen($file_details['dest'].'/'.basename($pretty_file_name), 'w');
while (($buffer = fgets($fp, SQLStreamer::$buffer_size)) !== false) {
fwrite($migrated_file, $buffer);
}
fclose($migrated_file);
fclose($fp);
//$this->info("Wrote $ugly_file_name to $pretty_file_name");
if ($bar) { if ($bar) {
$bar->advance(); $bar->advance();
} }

View File

@@ -2,6 +2,7 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Helpers\Helper;
use App\Mail\ExpiringAssetsMail; use App\Mail\ExpiringAssetsMail;
use App\Mail\ExpiringLicenseMail; use App\Mail\ExpiringLicenseMail;
use App\Models\Asset; use App\Models\Asset;
@@ -52,19 +53,73 @@ class SendExpirationAlerts extends Command
->filter(fn($item) => !empty($item)) ->filter(fn($item) => !empty($item))
->all(); ->all();
// Expiring Assets // Expiring Assets
$assets = Asset::getExpiringWarrantee($alert_interval); $assets = Asset::getExpiringWarrantyOrEol($alert_interval);
if ($assets->count() > 0) { if ($assets->count() > 0) {
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $alert_interval]));
Mail::to($recipients)->send(new ExpiringAssetsMail($assets, $alert_interval)); Mail::to($recipients)->send(new ExpiringAssetsMail($assets, $alert_interval));
$this->table(
[
trans('general.id'),
trans('admin/hardware/form.tag'),
trans('admin/hardware/form.model'),
trans('general.model_no'),
trans('general.purchase_date'),
trans('admin/hardware/form.eol_rate'),
trans('admin/hardware/form.eol_date'),
trans('admin/hardware/form.warranty_expires'),
],
$assets->map(fn($item) =>
[
trans('general.id') => $item->id,
trans('admin/hardware/form.tag') => $item->asset_tag,
trans('admin/hardware/form.model') => $item->model->name,
trans('general.model_no') => $item->model->model_number,
trans('general.purchase_date') => $item->purchase_date_formatted,
trans('admin/hardware/form.eol_rate') => $item->model->eol,
trans('admin/hardware/form.eol_date') => $item->eol_date ? $item->eol_formatted_date .' ('.$item->eol_diff_for_humans.')' : '',
trans('admin/hardware/form.warranty_expires') => $item->warranty_expires ? $item->warranty_expires_formatted_date .' ('.$item->warranty_expires_diff_for_humans.')' : '',
])
);
} }
// Expiring licenses // Expiring licenses
$licenses = License::getExpiringLicenses($alert_interval); $licenses = License::query()->ExpiringLicenses($alert_interval)
->with('manufacturer','category')
->orderBy('expiration_date', 'ASC')
->orderBy('termination_date', 'ASC')
->get();
if ($licenses->count() > 0) { if ($licenses->count() > 0) {
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $alert_interval]));
Mail::to($recipients)->send(new ExpiringLicenseMail($licenses, $alert_interval)); Mail::to($recipients)->send(new ExpiringLicenseMail($licenses, $alert_interval));
$this->table(
[
trans('general.id'),
trans('general.name'),
trans('general.purchase_date'),
trans('admin/licenses/form.expiration'),
trans('mail.expires'),
trans('admin/licenses/form.termination_date'),
trans('mail.terminates')],
$licenses->map(fn($item) => [
trans('general.id') => $item->id,
trans('general.name') => $item->name,
trans('general.purchase_date') => $item->purchase_date_formatted,
trans('admin/licenses/form.expiration') => $item->expires_formatted_date,
trans('mail.expires') => $item->expires_formatted_date ? $item->expires_diff_for_humans : '',
trans('admin/licenses/form.termination_date') => $item->terminates_formatted_date,
trans('mail.terminates') => $item->terminates_diff_for_humans
])
);
} }
// Send a message even if the count is 0
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $alert_interval]));
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $alert_interval]));
} else { } else {
if ($settings->alert_email == '') { if ($settings->alert_email == '') {
$this->error('Could not send email. No alert email configured in settings'); $this->error('Could not send email. No alert email configured in settings');

View File

@@ -16,7 +16,7 @@ class SendUpcomingAuditReport extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'snipeit:upcoming-audits'; protected $signature = 'snipeit:upcoming-audits {--with-output : Display the results in a table in your console in addition to sending the email}';
/** /**
* The console command description. * The console command description.
@@ -47,21 +47,69 @@ class SendUpcomingAuditReport extends Command
$today = Carbon::now(); $today = Carbon::now();
$interval_date = $today->copy()->addDays($interval); $interval_date = $today->copy()->addDays($interval);
$assets = Asset::whereNull('deleted_at')->dueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'desc')->get(); $assets_query = Asset::whereNull('deleted_at')->dueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'asc')->with('supplier');
$this->info($assets->count() . ' assets must be audited in on or before ' . $interval_date . ' is deadline'); $asset_count = $assets_query->count();
$this->info(number_format($asset_count) . ' assets must be audited on or before ' . $interval_date);
if (!$this->option('with-output')) {
if ((count($assets) !== 0) && ($assets->count() > 0) && ($settings->alert_email != '')) { $this->info('Run this command with the --with-output option to see the full list in the console.');
// Send a rollup to the admin, if settings dictate
$recipients = collect(explode(',', $settings->alert_email))
->map(fn($item) => trim($item))
->filter(fn($item) => !empty($item))
->all();
$this->info('Sending Admin SendUpcomingAuditNotification to: ' . $settings->alert_email);
Mail::to($recipients)->send(new SendUpcomingAuditMail($assets, $settings->audit_warning_days));
} }
if ($asset_count > 0) {
$assets_for_email = $assets_query->limit(30)->get();
// Send a rollup to the admin, if settings dictate
if ($settings->alert_email != '') {
$recipients = collect(explode(',', $settings->alert_email))
->map(fn($item) => trim($item))
->filter(fn($item) => !empty($item))
->all();
Mail::to($recipients)->send(new SendUpcomingAuditMail($assets_for_email, $settings->audit_warning_days, $asset_count));
$this->info('Audit notification sent to: ' . $settings->alert_email);
} else {
$this->info('There is no admin alert email set so no email will be sent.');
}
if ($this->option('with-output')) {
// Get the full list if the user wants output in the console
$assets_for_output = $assets_query->limit(null)->get();
$this->table(
[
trans('general.id'),
trans('general.name'),
trans('general.last_audit'),
trans('general.next_audit_date'),
trans('mail.Days'),
trans('mail.supplier'),
trans('mail.assigned_to'),
],
$assets_for_output->map(fn($item) => [
trans('general.id') => $item->id,
trans('general.name') => $item->display_name,
trans('general.last_audit') => $item->last_audit_formatted_date,
trans('general.next_audit_date') => $item->next_audit_formatted_date,
trans('mail.Days') => round($item->next_audit_diff_in_days),
trans('mail.supplier') => $item->supplier ? $item->supplier->name : '',
trans('mail.assigned_to') => $item->assignedTo ? $item->assignedTo->display_name : '',
])
);
}
} else {
$this->info('There are no assets due for audit in the next ' . $interval . ' days.');
}
} }
} }

View File

@@ -37,6 +37,8 @@ class SystemBackup extends Command
*/ */
public function handle() public function handle()
{ {
ini_set('max_execution_time', env('BACKUP_TIME_LIMIT', 600)); //600 seconds = 10 minutes
if ($this->option('filename')) { if ($this->option('filename')) {
$filename = $this->option('filename'); $filename = $this->option('filename');

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasAccessories extends ItemStillHasChildren
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasAssetModels extends ItemStillHasChildren
{
//
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasAssets extends ItemStillHasChildren
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasChildren extends Exception
{
//public function __construct($message, $code = 0, Exception $previous = null, $parent, $children)
//{
// trans()
//
//}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasComponents extends ItemStillHasChildren
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasConsumables extends ItemStillHasChildren
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasLicenses extends ItemStillHasChildren
{
//
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ItemStillHasMaintenances extends ItemStillHasChildren
{
//
}

View File

@@ -95,7 +95,7 @@ class Helper
$Parsedown->setSafeMode(true); $Parsedown->setSafeMode(true);
if ($str) { if ($str) {
return $Parsedown->text($str); return $Parsedown->text(strip_tags($str));
} }
} }
@@ -105,7 +105,7 @@ class Helper
$Parsedown->setSafeMode(true); $Parsedown->setSafeMode(true);
if ($str) { if ($str) {
return $Parsedown->line($str); return $Parsedown->line(strip_tags($str));
} }
} }
@@ -435,6 +435,34 @@ class Helper
return $colors[$index]; return $colors[$index];
} }
/**
* Check if a string has any RTL characters
* @param $value
* @return bool
*/
public static function hasRtl($string) {
$rtlChar = '/[\x{0590}-\x{083F}]|[\x{08A0}-\x{08FF}]|[\x{FB1D}-\x{FDFF}]|[\x{FE70}-\x{FEFF}]/u';
return preg_match($rtlChar, $string) != 0;
}
// is chinese, japanese or korean language
public static function isCjk($string) {
return Helper::isChinese($string) || Helper::isJapanese($string) || Helper::isKorean($string);
}
public static function isChinese($string) {
return preg_match("/\p{Han}+/u", $string);
}
public static function isJapanese($string) {
return preg_match('/[\x{4E00}-\x{9FBF}\x{3040}-\x{309F}\x{30A0}-\x{30FF}]/u', $string);
}
public static function isKorean($string) {
return preg_match('/[\x{3130}-\x{318F}\x{AC00}-\x{D7AF}]/u', $string);
}
/** /**
* Increases or decreases the brightness of a color by a percentage of the current brightness. * Increases or decreases the brightness of a color by a percentage of the current brightness.
* *
@@ -1706,5 +1734,5 @@ class Helper
} }
} }
return $mismatched; return $mismatched;
} }
} }

View File

@@ -16,6 +16,7 @@ class IconHelper
case 'clone': case 'clone':
return 'far fa-clone'; return 'far fa-clone';
case 'delete': case 'delete':
case 'upload deleted':
return 'fas fa-trash'; return 'fas fa-trash';
case 'create': case 'create':
return 'fa-solid fa-plus'; return 'fa-solid fa-plus';
@@ -39,6 +40,8 @@ class IconHelper
return 'fa-solid fa-trash-arrow-up'; return 'fa-solid fa-trash-arrow-up';
case 'external-link': case 'external-link':
return 'fa fa-external-link'; return 'fa fa-external-link';
case 'link':
return 'fa fa-link';
case 'email': case 'email':
return 'fa-regular fa-envelope'; return 'fa-regular fa-envelope';
case 'phone': case 'phone':
@@ -139,7 +142,7 @@ class IconHelper
case 'more-files': case 'more-files':
return 'fa-solid fa-laptop-file'; return 'fa-solid fa-laptop-file';
case 'maintenances': case 'maintenances':
return 'fas fa-wrench'; return 'fa-solid fa-screwdriver-wrench';
case 'seats': case 'seats':
return 'far fa-list-alt'; return 'far fa-list-alt';
case 'globe-us': case 'globe-us':
@@ -194,6 +197,10 @@ class IconHelper
case 'note': case 'note':
case 'notes': case 'notes':
return 'fas fa-sticky-note'; return 'fas fa-sticky-note';
case 'tip':
return 'fa-solid fa-lightbulb';
case 'highlight':
return 'fa-solid fa-highlighter';
} }
} }
} }

View File

@@ -71,6 +71,7 @@ class AccessoryCheckoutController extends Controller
$this->authorize('checkout', $accessory); $this->authorize('checkout', $accessory);
$target = $this->determineCheckoutTarget(); $target = $this->determineCheckoutTarget();
session()->put(['checkout_to_type' => $target]);
$accessory->checkout_qty = $request->input('checkout_qty', 1); $accessory->checkout_qty = $request->input('checkout_qty', 1);

View File

@@ -8,33 +8,23 @@ use App\Events\ItemAccepted;
use App\Events\ItemDeclined; use App\Events\ItemDeclined;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Mail\CheckoutAcceptanceResponseMail; use App\Mail\CheckoutAcceptanceResponseMail;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\CheckoutAcceptance; use App\Models\CheckoutAcceptance;
use App\Models\Company; use App\Models\Company;
use App\Models\Contracts\Acceptable; use App\Models\Contracts\Acceptable;
use App\Models\Setting; use App\Models\Setting;
use App\Models\User; use App\Models\User;
use App\Models\AssetModel;
use App\Models\Accessory;
use App\Models\License;
use App\Models\Component;
use App\Models\Consumable;
use App\Notifications\AcceptanceAssetAcceptedNotification; use App\Notifications\AcceptanceAssetAcceptedNotification;
use App\Notifications\AcceptanceAssetAcceptedToUserNotification; use App\Notifications\AcceptanceAssetAcceptedToUserNotification;
use App\Notifications\AcceptanceAssetDeclinedNotification; use App\Notifications\AcceptanceAssetDeclinedNotification;
use Exception; use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\Http\Controllers\SettingsController;
use Barryvdh\DomPDF\Facade\Pdf;
use Carbon\Carbon;
use \Illuminate\Contracts\View\View; use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse; use \Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use App\Helpers\Helper;
class AcceptanceController extends Controller class AcceptanceController extends Controller
{ {
@@ -85,6 +75,10 @@ class AcceptanceController extends Controller
public function store(Request $request, $id) : RedirectResponse public function store(Request $request, $id) : RedirectResponse
{ {
$acceptance = CheckoutAcceptance::find($id); $acceptance = CheckoutAcceptance::find($id);
$assigned_user = User::find($acceptance->assigned_to_id);
$settings = Setting::getSettings();
$sig_filename='';
if (is_null($acceptance)) { if (is_null($acceptance)) {
return redirect()->route('account.accept')->with('error', trans('admin/hardware/message.does_not_exist')); return redirect()->route('account.accept')->with('error', trans('admin/hardware/message.does_not_exist'));
@@ -107,140 +101,80 @@ class AcceptanceController extends Controller
} }
/** /**
* Get the signature and save it * Check for the signature directory
*/ */
if (! Storage::exists('private_uploads/signatures')) { if (! Storage::exists('private_uploads/signatures')) {
Storage::makeDirectory('private_uploads/signatures', 775); Storage::makeDirectory('private_uploads/signatures', 775);
} }
/**
* Check for the eula-pdfs directory
*/
if (! Storage::exists('private_uploads/eula-pdfs')) {
Storage::makeDirectory('private_uploads/eula-pdfs', 775);
}
$item = $acceptance->checkoutable_type::find($acceptance->checkoutable_id); $item = $acceptance->checkoutable_type::find($acceptance->checkoutable_id);
$display_model = '';
$pdf_view_route = ''; // If signatures are required, make sure we have one
$pdf_filename = 'accepted-eula-'.date('Y-m-d-h-i-s').'.pdf'; if (Setting::getSettings()->require_accept_signature == '1') {
$sig_filename='';
// The item was accepted, check for a signature
if ($request->filled('signature_output')) {
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png';
$data_uri = $request->input('signature_output');
$encoded_image = explode(',', $data_uri);
$decoded_image = base64_decode($encoded_image[1]);
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
// No image data is present, kick them back.
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
} else {
return redirect()->back()->with('error', trans('general.shitty_browser'));
}
}
// Convert PDF logo to base64 for TCPDF
// This is needed for TCPDF to properly embed the image if it's a png and the cache isn't writable
$encoded_logo = null;
if ($settings->acceptance_pdf_logo) {
$encoded_logo = base64_encode(file_get_contents(public_path() . '/uploads/' . $settings->acceptance_pdf_logo));
}
// Get the data array ready for the notifications and PDF generation
$data = [
'item_tag' => $item->asset_tag,
'item_name' => $item->name, // this handles licenses seats, which don't have a 'name' field
'item_model' => $item->model?->name,
'item_serial' => $item->serial,
'item_status' => $item->assetstatus?->name,
'eula' => $item->getEula(),
'note' => $request->input('note'),
'check_out_date' => Helper::getFormattedDateObject($acceptance->created_at, 'datetime', false),
'accepted_date' => Helper::getFormattedDateObject(now()->format('Y-m-d H:i:s'), 'datetime', false),
'declined_date' => Helper::getFormattedDateObject(now()->format('Y-m-d H:i:s'), 'datetime', false),
'assigned_to' => $assigned_user->display_name,
'email' => $assigned_user->email,
'employee_num' => $assigned_user->employee_num,
'site_name' => $settings->site_name,
'company_name' => $item->company?->name?? $settings->site_name,
'signature' => (($sig_filename && array_key_exists('1', $encoded_image))) ? $encoded_image[1] : null,
'logo' => ($encoded_logo) ?? null,
'date_settings' => $settings->date_display_format,
'qty' => $acceptance->qty ?? 1,
];
if ($request->input('asset_acceptance') == 'accepted') { if ($request->input('asset_acceptance') == 'accepted') {
/**
* Check for the eula-pdfs directory
*/
if (! Storage::exists('private_uploads/eula-pdfs')) {
Storage::makeDirectory('private_uploads/eula-pdfs', 775);
}
if (Setting::getSettings()->require_accept_signature == '1') { $pdf_filename = 'accepted-'.$acceptance->checkoutable_id.'-'.$acceptance->display_checkoutable_type.'-eula-'.date('Y-m-d-h-i-s').'.pdf';
// Check if the signature directory exists, if not create it
if (!Storage::exists('private_uploads/signatures')) {
Storage::makeDirectory('private_uploads/signatures', 775);
}
// The item was accepted, check for a signature // Generate the PDF content
if ($request->filled('signature_output')) { $pdf_content = $acceptance->generateAcceptancePdf($data, $acceptance);
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png'; Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf_content);
$data_uri = $request->input('signature_output');
$encoded_image = explode(',', $data_uri);
$decoded_image = base64_decode($encoded_image[1]);
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
// No image data is present, kick them back.
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
} else {
return redirect()->back()->with('error', trans('general.shitty_browser'));
}
}
$assigned_user = User::find($acceptance->assigned_to_id);
// this is horrible
switch($acceptance->checkoutable_type){
case 'App\Models\Asset':
$pdf_view_route ='account.accept.accept-asset-eula';
$asset_model = AssetModel::find($item->model_id);
if (!$asset_model) {
return redirect()->back()->with('error', trans('admin/models/message.does_not_exist'));
}
$display_model = $asset_model->name;
break;
case 'App\Models\Accessory':
$pdf_view_route ='account.accept.accept-accessory-eula';
$accessory = Accessory::find($item->id);
$display_model = $accessory->name;
break;
case 'App\Models\LicenseSeat':
$pdf_view_route ='account.accept.accept-license-eula';
$license = License::find($item->license_id);
$display_model = $license->name;
break;
case 'App\Models\Component':
$pdf_view_route ='account.accept.accept-component-eula';
$component = Component::find($item->id);
$display_model = $component->name;
break;
case 'App\Models\Consumable':
$pdf_view_route ='account.accept.accept-consumable-eula';
$consumable = Consumable::find($item->id);
$display_model = $consumable->name;
break;
}
// if ($acceptance->checkoutable_type == 'App\Models\Asset') {
// $pdf_view_route ='account.accept.accept-asset-eula';
// $asset_model = AssetModel::find($item->model_id);
// $display_model = $asset_model->name;
// $assigned_to = User::find($item->assigned_to)->present()->fullName;
//
// } elseif ($acceptance->checkoutable_type== 'App\Models\Accessory') {
// $pdf_view_route ='account.accept.accept-accessory-eula';
// $accessory = Accessory::find($item->id);
// $display_model = $accessory->name;
// $assigned_to = User::find($item->assignedTo);
//
// }
/**
* Gather the data for the PDF. We fire this whether there is a signature required or not,
* since we want the moment-in-time proof of what the EULA was when they accepted it.
*/
$branding_settings = SettingsController::getPDFBranding();
$path_logo = "";
// Check for the PDF logo path and use that, otherwise use the regular logo path
if (!is_null($branding_settings->acceptance_pdf_logo)) {
$path_logo = public_path() . '/uploads/' . $branding_settings->acceptance_pdf_logo;
} elseif (!is_null($branding_settings->logo)) {
$path_logo = public_path() . '/uploads/' . $branding_settings->logo;
}
$data = [
'item_tag' => $item->asset_tag,
'item_model' => $display_model,
'item_serial' => $item->serial,
'item_status' => $item->assetstatus?->name,
'eula' => $item->getEula(),
'note' => $request->input('note'),
'check_out_date' => Carbon::parse($acceptance->created_at)->format('Y-m-d'),
'accepted_date' => Carbon::parse($acceptance->accepted_at)->format('Y-m-d'),
'assigned_to' => $assigned_user->present()->fullName,
'company_name' => $branding_settings->site_name,
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'logo' => $path_logo,
'date_settings' => $branding_settings->date_display_format,
'admin' => auth()->user()->present()?->fullName,
];
if ($pdf_view_route!='') {
Log::debug($pdf_filename.' is the filename, and the route was specified.');
$pdf = Pdf::loadView($pdf_view_route, $data);
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf->output());
}
// Log the acceptance
$acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note')); $acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note'));
// Send the PDF to the signing user // Send the PDF to the signing user
@@ -248,15 +182,14 @@ class AcceptanceController extends Controller
// Add the attachment for the signing user into the $data array // Add the attachment for the signing user into the $data array
$data['file'] = $pdf_filename; $data['file'] = $pdf_filename;
try { try {
$assigned_user->notify(new AcceptanceAssetAcceptedToUserNotification($data)); $assigned_user->notify((new AcceptanceAssetAcceptedToUserNotification($data))->locale($assigned_user->locale));
} catch (\Exception $e) { } catch (\Exception $e) {
Log::warning($e); Log::warning($e);
} }
} }
try { try {
$acceptance->notify(new AcceptanceAssetAcceptedNotification($data)); $acceptance->notify((new AcceptanceAssetAcceptedNotification($data))->locale(Setting::getSettings()->locale));
} catch (\Exception $e) { } catch (\Exception $e) {
Log::warning($e); Log::warning($e);
} }
@@ -264,95 +197,21 @@ class AcceptanceController extends Controller
$return_msg = trans('admin/users/message.accepted'); $return_msg = trans('admin/users/message.accepted');
// Item was declined
} else { } else {
/** for ($i = 0; $i < ($acceptance->qty ?? 1); $i++) {
* Check for the eula-pdfs directory $acceptance->decline($sig_filename, $request->input('note'));
*/
if (! Storage::exists('private_uploads/eula-pdfs')) {
Storage::makeDirectory('private_uploads/eula-pdfs', 775);
} }
if (Setting::getSettings()->require_accept_signature == '1') {
// Check if the signature directory exists, if not create it
if (!Storage::exists('private_uploads/signatures')) {
Storage::makeDirectory('private_uploads/signatures', 775);
}
// The item was accepted, check for a signature
if ($request->filled('signature_output')) {
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png';
$data_uri = $request->input('signature_output');
$encoded_image = explode(',', $data_uri);
$decoded_image = base64_decode($encoded_image[1]);
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
// No image data is present, kick them back.
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
} else {
return redirect()->back()->with('error', trans('general.shitty_browser'));
}
}
// Format the data to send the declined notification
$branding_settings = SettingsController::getPDFBranding();
// This is the most horriblest
switch($acceptance->checkoutable_type){
case 'App\Models\Asset':
$asset_model = AssetModel::find($item->model_id);
$display_model = $asset_model->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Accessory':
$accessory = Accessory::find($item->id);
$display_model = $accessory->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\LicenseSeat':
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Component':
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Consumable':
$consumable = Consumable::find($item->id);
$display_model = $consumable->name;
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
}
$data = [
'item_tag' => $item->asset_tag,
'item_model' => $display_model,
'item_serial' => $item->serial,
'item_status' => $item->assetstatus?->name,
'note' => $request->input('note'),
'declined_date' => Carbon::parse($acceptance->declined_at)->format('Y-m-d'),
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'assigned_to' => $assigned_to,
'company_name' => $branding_settings->site_name,
'date_settings' => $branding_settings->date_display_format,
];
if ($pdf_view_route!='') {
Log::debug($pdf_filename.' is the filename, and the route was specified.');
$pdf = Pdf::loadView($pdf_view_route, $data);
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf->output());
}
$acceptance->decline($sig_filename, $request->input('note'));
$acceptance->notify(new AcceptanceAssetDeclinedNotification($data)); $acceptance->notify(new AcceptanceAssetDeclinedNotification($data));
Log::debug('New event acceptance.'); Log::debug('New event acceptance.');
event(new CheckoutDeclined($acceptance)); event(new CheckoutDeclined($acceptance));
$return_msg = trans('admin/users/message.declined'); $return_msg = trans('admin/users/message.declined');
} }
// Send an email notification if one is requested
if ($acceptance->alert_on_response_id) { if ($acceptance->alert_on_response_id) {
try { try {
$recipient = User::find($acceptance->alert_on_response_id); $recipient = User::find($acceptance->alert_on_response_id);
@@ -371,9 +230,10 @@ class AcceptanceController extends Controller
Log::warning($e); Log::warning($e);
} }
} }
return redirect()->to('account/accept')->with('success', $return_msg); return redirect()->to('account/accept')->with('success', $return_msg);
} }
} }

View File

@@ -3,11 +3,13 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Models\Actionlog;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use \Illuminate\Http\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\BinaryFileResponse;
use \Illuminate\Http\Response;
class ActionlogController extends Controller class ActionlogController extends Controller
{ {
public function displaySig($filename) : RedirectResponse | Response | bool public function displaySig($filename) : RedirectResponse | Response | bool
@@ -39,17 +41,29 @@ class ActionlogController extends Controller
public function getStoredEula($filename) : Response | BinaryFileResponse | RedirectResponse public function getStoredEula($filename) : Response | BinaryFileResponse | RedirectResponse
{ {
$this->authorize('view', \App\Models\Asset::class);
if (config('filesystems.default') == 's3_private') { if ($actionlog = Actionlog::where('filename', $filename)->with('user')->with('target')->firstOrFail()) {
return redirect()->away(Storage::disk('s3_private')->temporaryUrl('private_uploads/eula-pdfs/'.$filename, now()->addMinutes(5)));
$this->authorize('view', $actionlog->target);
$this->authorize('view', $actionlog->user);
if (config('filesystems.default') == 's3_private') {
return redirect()->away(Storage::disk('s3_private')->temporaryUrl('private_uploads/eula-pdfs/' . $filename, now()->addMinutes(5)));
}
if (Storage::exists('private_uploads/eula-pdfs/' . $filename)) {
if (request()->input('inline') == 'true') {
return response()->file(config('app.private_uploads') . '/eula-pdfs/' . $filename);
}
return response()->download(config('app.private_uploads') . '/eula-pdfs/' . $filename);
}
return redirect()->back()->with('error', trans('general.file_does_not_exist'));
} }
if (Storage::exists('private_uploads/eula-pdfs/'.$filename)) { return redirect()->back()->with('error', trans('general.record_not_found'));
return response()->download(config('app.private_uploads').'/eula-pdfs/'.$filename);
}
return redirect()->back()->with('error', trans('general.file_does_not_exist'));
} }
} }

View File

@@ -54,6 +54,15 @@ class AccessoriesController extends Controller
'notes', 'notes',
'checkouts_count', 'checkouts_count',
'qty', 'qty',
// These are *relationships* so we wouldn't normally include them in this array,
// since they would normally create a `column not found` error,
// BUT we account for them in the ordering switch down at the end of this method
// DO NOT ADD ANYTHING TO THIS LIST WITHOUT CHECKING THE ORDERING SWITCH BELOW!
'company',
'location',
'category',
'supplier',
'manufacturer',
]; ];
@@ -61,10 +70,23 @@ class AccessoriesController extends Controller
->with('category', 'company', 'manufacturer', 'checkouts', 'location', 'supplier', 'adminuser') ->with('category', 'company', 'manufacturer', 'checkouts', 'location', 'supplier', 'adminuser')
->withCount('checkouts as checkouts_count'); ->withCount('checkouts as checkouts_count');
if ($request->filled('search')) { $filter = [];
$accessories = $accessories->TextSearch($request->input('search'));
if ($request->filled('filter')) {
$filter = json_decode($request->input('filter'), true);
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
return in_array($key, $allowed_columns);
}, ARRAY_FILTER_USE_KEY);
} }
if ((! is_null($filter)) && (count($filter)) > 0) {
$accessories->ByFilter($filter);
} elseif ($request->filled('search')) {
$accessories->TextSearch($request->input('search'));
}
if ($request->filled('company_id')) { if ($request->filled('company_id')) {
$accessories->where('accessories.company_id', '=', $request->input('company_id')); $accessories->where('accessories.company_id', '=', $request->input('company_id'));
} }
@@ -288,32 +310,42 @@ class AccessoriesController extends Controller
'note' => $request->input('note'), 'note' => $request->input('note'),
]); ]);
$accessory_checkout->created_by = auth()->id(); $accessory_checkout->created_by = auth()->id();
$accessory_checkout->save(); $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 // Set this value to be able to pass the qty through to the event
event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note'))); event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success'))); return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkout.success')));
} }
/** /**
* Check in the item so that it can be checked out again to someone else * Check in the item so that it can be checked out again to someone else
* *
* @uses Accessory::checkin_email() to determine if an email can and should be sent
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param Request $request * @param Request $request
* @param int $accessoryUserId * @param int $accessoryUserId
* @param string $backto * @param string $backto
* @return \Illuminate\Http\RedirectResponse * @return JsonResponse
* @uses Accessory::checkin_email() to determine if an email can and should be sent
* @author [A. Gianotto] [<snipe@snipe.net>]
* @internal param int $accessoryId * @internal param int $accessoryId
*/ */
public function checkin(Request $request, $accessoryUserId = null) public function checkin(Request $request, $accessoryUserId = null)
{ {
if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryUserId))) { if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryUserId))) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist'))); return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist', ['id' => $accessoryUserId])));
} }
$accessory = Accessory::find($accessory_checkout->accessory_id); $accessory = Accessory::find($accessory_checkout->accessory_id);
@@ -327,7 +359,14 @@ class AccessoriesController extends Controller
$user = User::find($accessory_checkout->assigned_to); $user = User::find($accessory_checkout->assigned_to);
} }
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkin.success'))); $payload = [
'accessory_id' => $accessory->id,
'note' => $request->input('note'),
'created_by' => auth()->id(),
'pivot' => $accessory_checkout->id,
];
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkin.success')));
} }
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.checkin.error'))); return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.checkin.error')));

View File

@@ -46,10 +46,20 @@ class AssetModelsController extends Controller
'manufacturer', 'manufacturer',
'requestable', 'requestable',
'assets_count', 'assets_count',
'assets_assigned_count',
'assets_archived_count',
'remaining',
'category', 'category',
'fieldset', 'fieldset',
'deleted_at', 'deleted_at',
'updated_at', 'updated_at',
'require_serial',
// These are *relationships* so we wouldn't normally include them in this array,
// since they would normally create a `column not found` error,
// BUT we account for them in the ordering switch down at the end of this method
// DO NOT ADD ANYTHING TO THIS LIST WITHOUT CHECKING THE ORDERING SWITCH BELOW!
'manufacturer',
'category',
]; ];
$assetmodels = AssetModel::select([ $assetmodels = AssetModel::select([
@@ -69,9 +79,31 @@ class AssetModelsController extends Controller
'models.fieldset_id', 'models.fieldset_id',
'models.deleted_at', 'models.deleted_at',
'models.updated_at', 'models.updated_at',
'models.require_serial'
]) ])
->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues', 'adminuser') ->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues', 'adminuser')
->withCount('assets as assets_count'); ->withCount('assets as assets_count')
->withCount('availableAssets as remaining')
->withCount('assignedAssets as assets_assigned_count')
->withCount('archivedAssets as assets_archived_count');
$filter = [];
if ($request->filled('filter')) {
$filter = json_decode($request->input('filter'), true);
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
return in_array($key, $allowed_columns);
}, ARRAY_FILTER_USE_KEY);
}
if ((! is_null($filter)) && (count($filter)) > 0) {
$assetmodels->ByFilter($filter);
} elseif ($request->filled('search')) {
$assetmodels->TextSearch($request->input('search'));
}
if ($request->input('status')=='deleted') { if ($request->input('status')=='deleted') {
$assetmodels->onlyTrashed(); $assetmodels->onlyTrashed();

View File

@@ -6,6 +6,7 @@ use App\Events\CheckoutableCheckedIn;
use App\Http\Requests\StoreAssetRequest; use App\Http\Requests\StoreAssetRequest;
use App\Http\Requests\UpdateAssetRequest; use App\Http\Requests\UpdateAssetRequest;
use App\Http\Traits\MigratesLegacyAssetLocations; use App\Http\Traits\MigratesLegacyAssetLocations;
use App\Http\Transformers\ComponentsTransformer;
use App\Models\AccessoryCheckout; use App\Models\AccessoryCheckout;
use App\Models\CheckoutAcceptance; use App\Models\CheckoutAcceptance;
use App\Models\LicenseSeat; use App\Models\LicenseSeat;
@@ -115,6 +116,22 @@ class AssetsController extends Controller
'asset_eol_date', 'asset_eol_date',
'requestable', 'requestable',
'jobtitle', 'jobtitle',
// These are *relationships* so we wouldn't normally include them in this array,
// since they would normally create a `column not found` error,
// BUT we account for them in the ordering switch down at the end of this method
// DO NOT ADD ANYTHING TO THIS LIST WITHOUT CHECKING THE ORDERING SWITCH BELOW!
'company',
'model',
'location',
'rtd_location',
'category',
'status_label',
'manufacturer',
'supplier',
'jobtitle',
'assigned_to',
'created_by',
]; ];
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load $all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
@@ -131,6 +148,7 @@ class AssetsController extends Controller
$filter = array_filter($filter, function ($key) use ($allowed_columns) { $filter = array_filter($filter, function ($key) use ($allowed_columns) {
return in_array($key, $allowed_columns); return in_array($key, $allowed_columns);
}, ARRAY_FILTER_USE_KEY); }, ARRAY_FILTER_USE_KEY);
} }
$assets = Asset::select('assets.*') $assets = Asset::select('assets.*')
@@ -165,7 +183,7 @@ class AssetsController extends Controller
// Search custom fields by column name // Search custom fields by column name
foreach ($all_custom_fields as $field) { foreach ($all_custom_fields as $field) {
if ($request->filled($field->db_column_name()) && $field->db_column_name()) { if ($request->filled($field->db_column_name()) && $field->db_column_name()) {
$assets->where($field->db_column_name(), '=', $request->input($field->db_column_name())); $assets->where('assets.'.$field->db_column_name(), '=', $request->input($field->db_column_name()));
} }
} }
@@ -1290,9 +1308,19 @@ class AssetsController extends Controller
public function assignedAssets(Request $request, Asset $asset) : JsonResponse | array public function assignedAssets(Request $request, Asset $asset) : JsonResponse | array
{ {
$this->authorize('view', Asset::class);
$this->authorize('view', $asset);
return []; $query = Asset::where([
// to do 'assigned_to' => $asset->id,
'assigned_type' => Asset::class,
]);
$total = $query->count();
$assets = $query->applyOffsetAndLimit($total)->get();
return (new AssetsTransformer)->transformAssets($assets, $total);
} }
public function assignedAccessories(Request $request, Asset $asset) : JsonResponse | array public function assignedAccessories(Request $request, Asset $asset) : JsonResponse | array
@@ -1312,6 +1340,18 @@ class AssetsController extends Controller
return (new AssetsTransformer)->transformCheckedoutAccessories($accessory_checkouts, $total); return (new AssetsTransformer)->transformCheckedoutAccessories($accessory_checkouts, $total);
} }
public function assignedComponents(Request $request, Asset $asset): JsonResponse|array
{
$this->authorize('view', Asset::class);
$this->authorize('view', $asset);
$asset->loadCount('components');
$total = $asset->components_count;
$components = $asset->load(['components' => fn($query) => $query->applyOffsetAndLimit($total)])->components;
return (new ComponentsTransformer)->transformComponents($components, $total);
}
/** /**
* Generate asset labels by tag * Generate asset labels by tag

View File

@@ -2,6 +2,8 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Actions\Categories\DestroyCategoryAction;
use App\Exceptions\ItemStillHasChildren;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Transformers\CategoriesTransformer; use App\Http\Transformers\CategoriesTransformer;
@@ -38,6 +40,8 @@ class CategoriesController extends Controller
'consumables_count', 'consumables_count',
'components_count', 'components_count',
'licenses_count', 'licenses_count',
'created_at',
'updated_at',
'image', 'image',
'notes', 'notes',
]; ];
@@ -59,6 +63,23 @@ class CategoriesController extends Controller
->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count'); ->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count');
$filter = [];
if ($request->filled('filter')) {
$filter = json_decode($request->input('filter'), true);
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
return in_array($key, $allowed_columns);
}, ARRAY_FILTER_USE_KEY);
}
if ((! is_null($filter)) && (count($filter)) > 0) {
$categories->ByFilter($filter);
} elseif ($request->filled('search')) {
$categories->TextSearch($request->input('search'));
}
/* /*
* This checks to see if we should override the Admin Setting to show archived assets in list. * This checks to see if we should override the Admin Setting to show archived assets in list.
* We don't currently use it within the Snipe-IT GUI, but will be useful for API integrations where they * We don't currently use it within the Snipe-IT GUI, but will be useful for API integrations where they
@@ -72,10 +93,6 @@ class CategoriesController extends Controller
$categories = $categories->withCount('showableAssets as assets_count'); $categories = $categories->withCount('showableAssets as assets_count');
} }
if ($request->filled('search')) {
$categories = $categories->TextSearch($request->input('search'));
}
if ($request->filled('name')) { if ($request->filled('name')) {
$categories->where('name', '=', $request->input('name')); $categories->where('name', '=', $request->input('name'));
} }
@@ -209,17 +226,21 @@ class CategoriesController extends Controller
* @param int $id * @param int $id
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function destroy($id) : JsonResponse public function destroy(Category $category): JsonResponse
{ {
$this->authorize('delete', Category::class); $this->authorize('delete', Category::class);
$category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($id); try {
DestroyCategoryAction::run(category: $category);
if (! $category->isDeletable()) { } catch (ItemStillHasChildren $e) {
return response()->json( return response()->json(
Helper::formatStandardApiResponse('error', null, trans('admin/categories/message.assoc_items', ['asset_type'=>$category->category_type])) Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.general_assoc_warning', ['asset_type' => $category->category_type]))
);
} catch (\Exception $e) {
report($e);
return response()->json(
Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong'))
); );
} }
$category->delete();
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/categories/message.delete.success'))); return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/categories/message.delete.success')));
} }

View File

@@ -45,16 +45,40 @@ class ComponentsController extends Controller
'qty', 'qty',
'image', 'image',
'notes', 'notes',
// These are *relationships* so we wouldn't normally include them in this array,
// since they would normally create a `column not found` error,
// BUT we account for them in the ordering switch down at the end of this method
// DO NOT ADD ANYTHING TO THIS LIST WITHOUT CHECKING THE ORDERING SWITCH BELOW!
'company',
'location',
'category',
'manufacturer',
'supplier',
]; ];
$components = Component::select('components.*') $components = Component::select('components.*')
->with('company', 'location', 'category', 'assets', 'supplier', 'adminuser', 'manufacturer', 'uncontrainedAssets') ->with('company', 'location', 'category', 'assets', 'supplier', 'adminuser', 'manufacturer', 'uncontrainedAssets')
->withSum('uncontrainedAssets', 'components_assets.assigned_qty'); ->withSum('uncontrainedAssets', 'components_assets.assigned_qty');
if ($request->filled('search')) { $filter = [];
$components = $components->TextSearch($request->input('search'));
if ($request->filled('filter')) {
$filter = json_decode($request->input('filter'), true);
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
return in_array($key, $allowed_columns);
}, ARRAY_FILTER_USE_KEY);
} }
if ((! is_null($filter)) && (count($filter)) > 0) {
$components->ByFilter($filter);
} elseif ($request->filled('search')) {
$components->TextSearch($request->input('search'));
}
if ($request->filled('name')) { if ($request->filled('name')) {
$components->where('name', '=', $request->input('name')); $components->where('name', '=', $request->input('name'));
} }

View File

@@ -31,10 +31,53 @@ class ConsumablesController extends Controller
$consumables = Consumable::with('company', 'location', 'category', 'supplier', 'manufacturer') $consumables = Consumable::with('company', 'location', 'category', 'supplier', 'manufacturer')
->withCount('users as consumables_users_count'); ->withCount('users as consumables_users_count');
if ($request->filled('search')) { // This array is what determines which fields should be allowed to be sorted on ON the table itself.
$consumables = $consumables->TextSearch(e($request->input('search'))); // These must match a column on the consumables table directly.
$allowed_columns = [
'id',
'name',
'order_number',
'min_amt',
'purchase_date',
'purchase_cost',
'company',
'category',
'model_number',
'item_no',
'manufacturer',
'location',
'qty',
'image',
// These are *relationships* so we wouldn't normally include them in this array,
// since they would normally create a `column not found` error,
// BUT we account for them in the ordering switch down at the end of this method
// DO NOT ADD ANYTHING TO THIS LIST WITHOUT CHECKING THE ORDERING SWITCH BELOW!
'company',
'location',
'category',
'supplier',
'manufacturer',
];
$filter = [];
if ($request->filled('filter')) {
$filter = json_decode($request->input('filter'), true);
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
return in_array($key, $allowed_columns);
}, ARRAY_FILTER_USE_KEY);
} }
if ((! is_null($filter)) && (count($filter)) > 0) {
$consumables->ByFilter($filter);
} elseif ($request->filled('search')) {
$consumables->TextSearch($request->input('search'));
}
if ($request->filled('name')) { if ($request->filled('name')) {
$consumables->where('name', '=', $request->input('name')); $consumables->where('name', '=', $request->input('name'));
} }
@@ -96,25 +139,6 @@ class ConsumablesController extends Controller
$consumables = $consumables->OrderByCreatedBy($order); $consumables = $consumables->OrderByCreatedBy($order);
break; break;
default: default:
// This array is what determines which fields should be allowed to be sorted on ON the table itself.
// These must match a column on the consumables table directly.
$allowed_columns = [
'id',
'name',
'order_number',
'min_amt',
'purchase_date',
'purchase_cost',
'company',
'category',
'model_number',
'item_no',
'manufacturer',
'location',
'qty',
'image'
];
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at'; $sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
$consumables = $consumables->orderBy($sort, $order); $consumables = $consumables->orderBy($sort, $order);
break; break;

View File

@@ -69,7 +69,7 @@ class ImportController extends Controller
if (function_exists('iconv')) { if (function_exists('iconv')) {
$file_contents = $file->getContent(); //TODO - this *does* load the whole file in RAM, but we need that to be able to 'iconv' it? $file_contents = $file->getContent(); //TODO - this *does* load the whole file in RAM, but we need that to be able to 'iconv' it?
$encoding = $detector->getEncoding($file_contents); $encoding = $detector->getEncoding($file_contents);
\Log::warning("Discovered encoding: $encoding in uploaded CSV"); \Log::debug("Discovered encoding: $encoding in uploaded CSV");
$reader = null; $reader = null;
if (strcasecmp($encoding, 'UTF-8') != 0) { if (strcasecmp($encoding, 'UTF-8') != 0) {
$transliterated = false; $transliterated = false;
@@ -103,7 +103,7 @@ class ImportController extends Controller
$reader = Reader::createFromFileObject($file->openFile('r')); //file pointer leak? $reader = Reader::createFromFileObject($file->openFile('r')); //file pointer leak?
try { try {
$import->header_row = $reader->fetchOne(0); $import->header_row = $reader->nth(0);
} catch (JsonEncodingException $e) { } catch (JsonEncodingException $e) {
return response()->json( return response()->json(
Helper::formatStandardApiResponse( Helper::formatStandardApiResponse(
@@ -136,7 +136,7 @@ class ImportController extends Controller
try { try {
// Grab the first row to display via ajax as the user picks fields // Grab the first row to display via ajax as the user picks fields
$import->first_row = $reader->fetchOne(1); $import->first_row = $reader->nth(1);
} catch (JsonEncodingException $e) { } catch (JsonEncodingException $e) {
return response()->json( return response()->json(
Helper::formatStandardApiResponse( Helper::formatStandardApiResponse(

View File

@@ -128,7 +128,9 @@ class LicenseSeatsController extends Controller
// nothing to update // nothing to update
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success'))); return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
} }
if( $touched && $licenseSeat->unreassignable_seat) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.unavailable')));
}
// the logging functions expect only one "target". if both asset and user are present in the request, // 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... // we simply let assets take precedence over users...
if ($licenseSeat->isDirty('assigned_to')) { if ($licenseSeat->isDirty('assigned_to')) {
@@ -145,7 +147,11 @@ class LicenseSeatsController extends Controller
if ($licenseSeat->save()) { if ($licenseSeat->save()) {
if ($is_checkin) { if ($is_checkin) {
$licenseSeat->logCheckin($target, $request->input('notes')); if(!$licenseSeat->license->reassignable){
$licenseSeat->unreassignable_seat = true;
$licenseSeat->save();
}
$licenseSeat->logCheckin($target, $licenseSeat->notes);
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success'))); return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
} }

View File

@@ -7,6 +7,7 @@ use App\Http\Controllers\Controller;
use App\Http\Transformers\LicensesTransformer; use App\Http\Transformers\LicensesTransformer;
use App\Http\Transformers\SelectlistTransformer; use App\Http\Transformers\SelectlistTransformer;
use App\Models\License; use App\Models\License;
use App\Models\Setting;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@@ -25,6 +26,15 @@ class LicensesController extends Controller
$this->authorize('view', License::class); $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')->withCount('freeSeats as free_seats_count');
$settings = Setting::getSettings();
if ($request->input('status')=='inactive') {
$licenses->ExpiredLicenses();
} elseif ($request->input('status')=='expiring') {
$licenses->ExpiringLicenses($settings->alert_interval);
} else {
$licenses->ActiveLicenses();
}
if ($request->filled('company_id')) { if ($request->filled('company_id')) {
$licenses->where('licenses.company_id', '=', $request->input('company_id')); $licenses->where('licenses.company_id', '=', $request->input('company_id'));
@@ -94,6 +104,8 @@ class LicensesController extends Controller
$licenses->onlyTrashed(); $licenses->onlyTrashed();
} }
// Make sure the offset and limit are actually integers and do not exceed system limits // Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $licenses->count()) ? $licenses->count() : app('api_offset_value'); $offset = ($request->input('offset') > $licenses->count()) ? $licenses->count() : app('api_offset_value');
$limit = app('api_limit_value'); $limit = app('api_limit_value');

View File

@@ -37,10 +37,14 @@ class LocationsController extends Controller
'address', 'address',
'address2', 'address2',
'assets_count', 'assets_count',
'assets_count', 'assigned_assets_count',
'rtd_assets_count',
'accessories_count',
'assigned_accessories_count', 'assigned_accessories_count',
'assigned_assets_count', 'components_count',
'assigned_assets_count', 'consumables_count',
'users_count',
'children_count',
'city', 'city',
'country', 'country',
'created_at', 'created_at',
@@ -54,7 +58,6 @@ class LocationsController extends Controller
'rtd_assets_count', 'rtd_assets_count',
'state', 'state',
'updated_at', 'updated_at',
'users_count',
'zip', 'zip',
'notes', 'notes',
]; ];
@@ -79,8 +82,9 @@ class LocationsController extends Controller
'locations.currency', 'locations.currency',
'locations.company_id', 'locations.company_id',
'locations.notes', 'locations.notes',
'locations.created_by',
'locations.deleted_at',
]) ])
->withCount('assignedAssets as assigned_assets_count')
->withCount('assignedAssets as assigned_assets_count') ->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count') ->withCount('assets as assets_count')
->withCount('assignedAccessories as assigned_accessories_count') ->withCount('assignedAccessories as assigned_accessories_count')
@@ -88,6 +92,8 @@ class LocationsController extends Controller
->withCount('rtd_assets as rtd_assets_count') ->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count') ->withCount('children as children_count')
->withCount('users as users_count') ->withCount('users as users_count')
->withCount('consumables as consumables_count')
->withCount('components as components_count')
->with('adminuser'); ->with('adminuser');
// Only scope locations if the setting is enabled // Only scope locations if the setting is enabled
@@ -131,6 +137,14 @@ class LocationsController extends Controller
$locations->where('locations.company_id', '=', $request->input('company_id')); $locations->where('locations.company_id', '=', $request->input('company_id'));
} }
if ($request->filled('parent_id')) {
$locations->where('locations.parent_id', '=', $request->input('parent_id'));
}
if ($request->input('status') == 'deleted') {
$locations->onlyTrashed();
}
// Make sure the offset and limit are actually integers and do not exceed system limits // Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $locations->count()) ? $locations->count() : app('api_offset_value'); $offset = ($request->input('offset') > $locations->count()) ? $locations->count() : app('api_offset_value');
$limit = app('api_limit_value'); $limit = app('api_limit_value');
@@ -224,8 +238,13 @@ class LocationsController extends Controller
]) ])
->withCount('assignedAssets as assigned_assets_count') ->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count') ->withCount('assets as assets_count')
->withCount('assignedAccessories as assigned_accessories_count')
->withCount('accessories as accessories_count')
->withCount('rtd_assets as rtd_assets_count') ->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count') ->withCount('users as users_count')
->withCount('consumables as consumables_count')
->withCount('components as components_count')
->findOrFail($id); ->findOrFail($id);
return (new LocationsTransformer)->transformLocation($location); return (new LocationsTransformer)->transformLocation($location);
@@ -320,11 +339,15 @@ class LocationsController extends Controller
{ {
$this->authorize('delete', Location::class); $this->authorize('delete', Location::class);
$location = Location::withCount('assignedAssets as assigned_assets_count') $location = Location::withCount('assignedAssets as assigned_assets_count')
->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count') ->withCount('assets as assets_count')
->withCount('assignedAccessories as assigned_accessories_count')
->withCount('accessories as accessories_count')
->withCount('rtd_assets as rtd_assets_count') ->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count') ->withCount('children as children_count')
->withCount('users as users_count') ->withCount('users as users_count')
->withCount('accessories as accessories_count') ->withCount('consumables as consumables_count')
->withCount('components as components_count')
->findOrFail($id); ->findOrFail($id);
if (! $location->isDeletable()) { if (! $location->isDeletable()) {

View File

@@ -52,6 +52,10 @@ class MaintenancesController extends Controller
$maintenances->where('maintenances.created_by', '=', $request->input('created_by')); $maintenances->where('maintenances.created_by', '=', $request->input('created_by'));
} }
if ($request->filled('url')) {
$maintenances->where('maintenances.url', '=', $request->input('url'));
}
if ($request->filled('asset_maintenance_type')) { if ($request->filled('asset_maintenance_type')) {
$maintenances->where('asset_maintenance_type', '=', $request->input('asset_maintenance_type')); $maintenances->where('asset_maintenance_type', '=', $request->input('asset_maintenance_type'));
} }

View File

@@ -2,6 +2,13 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Actions\Manufacturers\DeleteManufacturerAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasChildren;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Transformers\ManufacturersTransformer; use App\Http\Transformers\ManufacturersTransformer;
@@ -184,19 +191,19 @@ class ManufacturersController extends Controller
* @since [v4.0] * @since [v4.0]
* @param int $id * @param int $id
*/ */
public function destroy($id) : JsonResponse public function destroy(Manufacturer $manufacturer): JsonResponse
{ {
$this->authorize('delete', Manufacturer::class);
$manufacturer = Manufacturer::findOrFail($id);
$this->authorize('delete', $manufacturer); $this->authorize('delete', $manufacturer);
try {
if ($manufacturer->isDeletable()) { DeleteManufacturerAction::run($manufacturer);
$manufacturer->delete(); } catch (ItemStillHasChildren $e) {
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/manufacturers/message.delete.success'))); return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.general_assoc_warning', ['item' => trans('general.manufacturer')])));
} catch (\Exception $e) {
report($e);
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
} }
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/manufacturers/message.assoc_users'))); return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/manufacturers/message.delete.success')));
} }
/** /**

View File

@@ -69,7 +69,7 @@ class ProfileController extends Controller
if ($checkoutRequest && $checkoutRequest->itemRequested()) { if ($checkoutRequest && $checkoutRequest->itemRequested()) {
$assets = [ $assets = [
'image' => e($checkoutRequest->itemRequested()->present()->getImageUrl()), 'image' => e($checkoutRequest->itemRequested()->present()->getImageUrl()),
'name' => e($checkoutRequest->itemRequested()->present()->name()), 'name' => e($checkoutRequest->itemRequested()->display_name),
'type' => e($checkoutRequest->itemType()), 'type' => e($checkoutRequest->itemType()),
'qty' => (int) $checkoutRequest->quantity, 'qty' => (int) $checkoutRequest->quantity,
'location' => ($checkoutRequest->location()) ? e($checkoutRequest->location()->name) : null, 'location' => ($checkoutRequest->location()) ? e($checkoutRequest->location()->name) : null,

View File

@@ -2,6 +2,13 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Actions\Suppliers\DestroySupplierAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasMaintenances;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasLicenses;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Transformers\SelectlistTransformer; use App\Http\Transformers\SelectlistTransformer;
@@ -191,27 +198,40 @@ class SuppliersController extends Controller
* @since [v4.0] * @since [v4.0]
* @param int $id * @param int $id
*/ */
public function destroy($id) : JsonResponse public function destroy(Supplier $supplier): JsonResponse
{ {
$this->authorize('delete', Supplier::class);
$supplier = Supplier::with('maintenances', 'assets', 'licenses')->withCount('maintenances as maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->findOrFail($id);
$this->authorize('delete', $supplier); $this->authorize('delete', $supplier);
try {
DestroySupplierAction::run(supplier: $supplier);
if ($supplier->assets_count > 0) { } catch (ItemStillHasAssets $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count]))); return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_assets', [
'asset_count' => (int) $supplier->assets_count, 'item' => trans('general.supplier')
])));
} catch (ItemStillHasMaintenances $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_maintenances', [
'asset_maintenances_count' => $supplier->asset_maintenances_count, 'item' => trans('general.supplier')
])));
} catch (ItemStillHasLicenses $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_licenses', [
'licenses_count' => (int) $supplier->licenses_count, 'item' => trans('general.supplier')
])));
} catch (ItemStillHasAccessories $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_accessories', [
'accessories_count' => (int) $supplier->accessories_count, 'item' => trans('general.supplier')
])));
} catch (ItemStillHasConsumables $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_consumables', [
'consumables_count' => (int) $supplier->consumables_count, 'item' => trans('general.supplier')
])));
} catch (ItemStillHasComponents $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_components', [
'components_count' => (int) $supplier->components_count, 'item' => trans('general.supplier')
])));
} catch (\Exception $e) {
report($e);
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
} }
if ($supplier->maintenances_count > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_maintenances', ['maintenances_count' => $supplier->maintenances_count])));
}
if ($supplier->licenses_count > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_licenses', ['licenses_count' => (int) $supplier->licenses_count])));
}
$supplier->delete();
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/suppliers/message.delete.success'))); return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/suppliers/message.delete.success')));
} }

View File

@@ -32,7 +32,7 @@ class UploadedFilesController extends Controller
{ {
// Check the permissions to make sure the user can view the object // Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id); $object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('view', $object); $this->authorize('view', $object);
if (!$object) { if (!$object) {
@@ -51,11 +51,7 @@ class UploadedFilesController extends Controller
]; ];
$uploads = Actionlog::select('action_logs.*') $uploads = self::$map_object_type[$object_type]::withTrashed()->find($id)->uploads()
->whereNotNull('filename')
->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)
->where('action_type', '=', 'uploaded')
->with('adminuser'); ->with('adminuser');
$offset = ($request->input('offset') > $uploads->count()) ? $uploads->count() : abs($request->input('offset')); $offset = ($request->input('offset') > $uploads->count()) ? $uploads->count() : abs($request->input('offset'));
@@ -96,7 +92,7 @@ class UploadedFilesController extends Controller
{ {
// Check the permissions to make sure the user can view the object // Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id); $object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('view', $object); $this->authorize('view', $object);
if (!$object) { if (!$object) {
@@ -144,7 +140,7 @@ class UploadedFilesController extends Controller
public function show($object_type, $id, $file_id) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse public function show($object_type, $id, $file_id) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{ {
// Check the permissions to make sure the user can view the object // Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id); $object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('view', $object); $this->authorize('view', $object);
if (!$object) { if (!$object) {
@@ -188,7 +184,7 @@ class UploadedFilesController extends Controller
{ {
// Check the permissions to make sure the user can view the object // Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id); $object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('update', self::$map_object_type[$object_type]); $this->authorize('update', self::$map_object_type[$object_type]);
if (!$object) { if (!$object) {
@@ -197,8 +193,12 @@ class UploadedFilesController extends Controller
// Check for the file // Check for the file
$log = Actionlog::find($file_id)->where('item_type', self::$map_object_type[$object_type]) $log = Actionlog::query()
->where('item_id', $object->id)->first(); ->where('id', $file_id)
->where('action_type', 'uploaded')
->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)
->first();
if ($log) { if ($log) {
// Check the file actually exists, and delete it // Check the file actually exists, and delete it
@@ -206,7 +206,7 @@ class UploadedFilesController extends Controller
Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename); Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
} }
// Delete the record of the file // Delete the record of the file
if ($log->delete()) { if ($log->logUploadDelete($object, $log->filename)) {
return response()->json(Helper::formatStandardApiResponse('success', null, trans_choice('general.file_upload_status.delete.success', 1)), 200); return response()->json(Helper::formatStandardApiResponse('success', null, trans_choice('general.file_upload_status.delete.success', 1)), 200);
} }
@@ -217,4 +217,4 @@ class UploadedFilesController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('general.file_upload_status.delete.error', 1)), 500); return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('general.file_upload_status.delete.error', 1)), 500);
} }
} }

View File

@@ -20,6 +20,7 @@ use App\Models\Consumable;
use App\Models\License; use App\Models\License;
use App\Models\User; use App\Models\User;
use App\Notifications\CurrentInventory; use App\Notifications\CurrentInventory;
use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@@ -102,9 +103,75 @@ class UsersController extends Controller
'managedLocations as manages_locations_count' 'managedLocations as manages_locations_count'
]); ]);
$allowed_columns =
[
'last_name',
'first_name',
'display_name',
'email',
'jobtitle',
'username',
'employee_num',
'groups',
'activated',
'created_at',
'updated_at',
'two_factor_enrolled',
'two_factor_optin',
'last_login',
'assets_count',
'licenses_count',
'consumables_count',
'accessories_count',
'manages_users_count',
'manages_locations_count',
'phone',
'mobile',
'address',
'city',
'state',
'country',
'zip',
'id',
'ldap_import',
'two_factor_optin',
'two_factor_enrolled',
'remote',
'vip',
'start_date',
'end_date',
'autoassign_licenses',
'website',
'locale',
'notes',
'employee_num',
if ($request->filled('search') != '') { // These are *relationships* so we wouldn't normally include them in this array,
$users = $users->TextSearch($request->input('search')); // since they would normally create a `column not found` error,
// BUT we account for them in the ordering switch down at the end of this method
// DO NOT ADD ANYTHING TO THIS LIST WITHOUT CHECKING THE ORDERING SWITCH BELOW!
'company',
'location',
'department',
'manager',
'created_by',
];
$filter = [];
if ($request->filled('filter')) {
$filter = json_decode($request->input('filter'), true);
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
return in_array($key, $allowed_columns);
}, ARRAY_FILTER_USE_KEY);
}
if ((! is_null($filter)) && (count($filter)) > 0) {
$users->ByFilter($filter);
} elseif ($request->filled('search')) {
$users->TextSearch($request->input('search'));
} }
if ($request->filled('activated')) { if ($request->filled('activated')) {
@@ -285,49 +352,6 @@ class UsersController extends Controller
$users->orderBy('first_name', $order); $users->orderBy('first_name', $order);
break; break;
default: default:
$allowed_columns =
[
'last_name',
'first_name',
'display_name',
'email',
'jobtitle',
'username',
'employee_num',
'groups',
'activated',
'created_at',
'updated_at',
'two_factor_enrolled',
'two_factor_optin',
'last_login',
'assets_count',
'licenses_count',
'consumables_count',
'accessories_count',
'manages_users_count',
'manages_locations_count',
'phone',
'mobile',
'address',
'city',
'state',
'country',
'zip',
'id',
'ldap_import',
'two_factor_optin',
'two_factor_enrolled',
'remote',
'vip',
'start_date',
'end_date',
'autoassign_licenses',
'website',
'locale',
'notes',
];
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'first_name'; $sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'first_name';
$users = $users->orderBy($sort, $order); $users = $users->orderBy($sort, $order);
break; break;
@@ -434,9 +458,20 @@ class UsersController extends Controller
$user->password = $user->noPassword(); $user->password = $user->noPassword();
} }
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar'); app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
if ($user->save()) { if ($user->save()) {
if (($user->activated == '1') && ($user->email != '') && ($request->input('send_welcome') == '1')) {
try {
$user->notify(new WelcomeNotification($user));
} catch (\Exception $e) {
Log::warning('Could not send welcome notification for user: ' . $e->getMessage());
}
}
if ($request->filled('groups')) { if ($request->filled('groups')) {
$user->groups()->sync($request->input('groups')); $user->groups()->sync($request->input('groups'));
} else { } else {
@@ -548,7 +583,7 @@ class UsersController extends Controller
Asset::where('assigned_type', User::class) Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]); ->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
} }
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar'); app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
if ($user->save()) { if ($user->save()) {
// Check if the request has groups passed and has a value, AND that the user us a superuser // Check if the request has groups passed and has a value, AND that the user us a superuser

View File

@@ -82,6 +82,7 @@ class AssetModelsController extends Controller
$model->notes = $request->input('notes'); $model->notes = $request->input('notes');
$model->created_by = auth()->id(); $model->created_by = auth()->id();
$model->requestable = $request->has('requestable'); $model->requestable = $request->has('requestable');
$model->require_serial = $request->input('require_serial', 0);
if ($request->input('fieldset_id') != '') { if ($request->input('fieldset_id') != '') {
$model->fieldset_id = $request->input('fieldset_id'); $model->fieldset_id = $request->input('fieldset_id');
@@ -155,7 +156,7 @@ class AssetModelsController extends Controller
$model->category_id = $request->input('category_id'); $model->category_id = $request->input('category_id');
$model->notes = $request->input('notes'); $model->notes = $request->input('notes');
$model->requestable = $request->input('requestable', '0'); $model->requestable = $request->input('requestable', '0');
$model->require_serial = $request->input('require_serial', 0);
$model->fieldset_id = $request->input('fieldset_id'); $model->fieldset_id = $request->input('fieldset_id');
if ($model->save()) { if ($model->save()) {

View File

@@ -65,6 +65,8 @@ class AssetCheckoutController extends Controller
*/ */
public function store(AssetCheckoutRequest $request, $assetId) : RedirectResponse public function store(AssetCheckoutRequest $request, $assetId) : RedirectResponse
{ {
try { try {
// Check if the asset exists // Check if the asset exists
if (! $asset = Asset::find($assetId)) { if (! $asset = Asset::find($assetId)) {
@@ -81,6 +83,7 @@ class AssetCheckoutController extends Controller
$admin = auth()->user(); $admin = auth()->user();
$target = $this->determineCheckoutTarget(); $target = $this->determineCheckoutTarget();
session()->put(['checkout_to_type' => $target]);
$asset = $this->updateAssetLocation($asset, $target); $asset = $this->updateAssetLocation($asset, $target);

View File

@@ -110,17 +110,35 @@ class AssetsController extends Controller
// This is only necessary on create, not update, since bulk editing is handled // This is only necessary on create, not update, since bulk editing is handled
// differently // differently
$asset_tags = $request->input('asset_tags'); $asset_tags = $request->input('asset_tags');
$model = AssetModel::find($request->input('model_id'));
$serial_errors = [];
$serials = $request->input('serials');
$settings = Setting::getSettings(); $settings = Setting::getSettings();
//Validate required serial based on model setting
for ($a = 1, $aMax = count($asset_tags); $a <= $aMax; $a++) {
if ($model && $model->require_serial === 1 && empty($serials[$a])) {
$serial_errors["serials.$a"] = trans('admin/hardware/form.serial_required', ['number' => $a]);
}
}
if (!empty($serial_errors)) {
return redirect()->back()
->withInput()
->withErrors($serial_errors);
}
$asset = null;
$companyId = Company::getIdForCurrentUser($request->input('company_id'));
$successes = []; $successes = [];
$failures = []; $failures = [];
$serials = $request->input('serials');
$asset = null;
for ($a = 1; $a <= count($asset_tags); $a++) { for ($a = 1, $aMax = count($asset_tags); $a <= $aMax; $a++) {
$asset = new Asset(); $asset = new Asset();
$asset->model()->associate(AssetModel::find($request->input('model_id')));
$asset->model()->associate($model);
$asset->name = $request->input('name'); $asset->name = $request->input('name');
// Check for a corresponding serial // Check for a corresponding serial
@@ -132,7 +150,7 @@ class AssetsController extends Controller
$asset->asset_tag = $asset_tags[$a]; $asset->asset_tag = $asset_tags[$a];
} }
$asset->company_id = Company::getIdForCurrentUser($request->input('company_id')); $asset->company_id = $companyId;
$asset->model_id = $request->input('model_id'); $asset->model_id = $request->input('model_id');
$asset->order_number = $request->input('order_number'); $asset->order_number = $request->input('order_number');
$asset->notes = $request->input('notes'); $asset->notes = $request->input('notes');
@@ -172,7 +190,6 @@ class AssetsController extends Controller
// Update custom fields in the database. // Update custom fields in the database.
// Validation for these fields is handled through the AssetRequest form request // Validation for these fields is handled through the AssetRequest form request
$model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) { if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) { foreach ($model->fieldset->fields as $field) {
@@ -346,7 +363,7 @@ class AssetsController extends Controller
$asset->purchase_cost = $request->input('purchase_cost', null); $asset->purchase_cost = $request->input('purchase_cost', null);
$asset->purchase_date = $request->input('purchase_date', null); $asset->purchase_date = $request->input('purchase_date', null);
$asset->next_audit_date = $request->input('next_audit_date', null); $asset->next_audit_date = $request->input('next_audit_date', null);
if ($request->filled('purchase_date') && !$request->filled('asset_eol_date') && ($asset->model->eol > 0)) { if ($request->filled('purchase_date') && !$request->filled('asset_eol_date') && ($asset->model?->eol > 0)) {
$asset->purchase_date = $request->input('purchase_date', null); $asset->purchase_date = $request->input('purchase_date', null);
$asset->asset_eol_date = Carbon::parse($request->input('purchase_date'))->addMonths($asset->model->eol)->format('Y-m-d'); $asset->asset_eol_date = Carbon::parse($request->input('purchase_date'))->addMonths($asset->model->eol)->format('Y-m-d');
$asset->eol_explicit = false; $asset->eol_explicit = false;
@@ -362,7 +379,7 @@ class AssetsController extends Controller
} else { } else {
$asset->eol_explicit = true; $asset->eol_explicit = true;
} }
} elseif (!$request->filled('asset_eol_date') && (($asset->model->eol) == 0)) { } elseif (!$request->filled('asset_eol_date') && (($asset->model?->eol) == 0)) {
$asset->asset_eol_date = null; $asset->asset_eol_date = null;
$asset->eol_explicit = false; $asset->eol_explicit = false;
} }
@@ -381,6 +398,7 @@ class AssetsController extends Controller
$asset->assigned_to = null; $asset->assigned_to = null;
$asset->assigned_type = null; $asset->assigned_type = null;
$asset->accepted = null; $asset->accepted = null;
$asset->last_checkin = now();
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on asset update with '.$status->getStatuslabelType().' status', date('Y-m-d H:i:s'), $originalValues)); event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on asset update with '.$status->getStatuslabelType().' status', date('Y-m-d H:i:s'), $originalValues));
} }
@@ -453,6 +471,13 @@ class AssetsController extends Controller
]); ]);
//Validate required serial based on model setting
if ($model && $model->require_serial === 1 && empty($serial[1])) {
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
->with('warning', trans('admin/hardware/form.serial_required_post_model_update', [
'asset_model' => $model->name
]));
}
if ($asset->save()) { if ($asset->save()) {
return Helper::getRedirectOption($request, $asset->id, 'Assets') return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success', trans('admin/hardware/message.update.success')); ->with('success', trans('admin/hardware/message.update.success'));

View File

@@ -163,7 +163,7 @@ class BulkAssetsController extends Controller
$modelNames = []; $modelNames = [];
foreach($models as $model) { foreach($models as $model) {
$modelNames[] = $model->model->name; $modelNames[] = $model->model?->name;
} }
if ($request->filled('bulk_actions')) { if ($request->filled('bulk_actions')) {
@@ -240,10 +240,6 @@ class BulkAssetsController extends Controller
$custom_fields_to_null[str_replace('null', '', $key)] = $value; $custom_fields_to_null[str_replace('null', '', $key)] = $value;
} }
if (! $request->filled('ids') || count($request->input('ids')) == 0) { if (! $request->filled('ids') || count($request->input('ids')) == 0) {
return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected')); return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected'));
@@ -274,6 +270,7 @@ class BulkAssetsController extends Controller
|| ($request->filled('company_id')) || ($request->filled('company_id'))
|| ($request->filled('status_id')) || ($request->filled('status_id'))
|| ($request->filled('model_id')) || ($request->filled('model_id'))
|| ($request->filled('notes'))
|| ($request->filled('next_audit_date')) || ($request->filled('next_audit_date'))
|| ($request->filled('asset_eol_date')) || ($request->filled('asset_eol_date'))
|| ($request->filled('null_name')) || ($request->filled('null_name'))
@@ -470,7 +467,7 @@ class BulkAssetsController extends Controller
*/ */
// Does the model have a fieldset? // Does the model have a fieldset?
if ($asset->model->fieldset) { if ($asset->model?->fieldset) {
foreach ($asset->model->fieldset->fields as $field) { foreach ($asset->model->fieldset->fields as $field) {
// null custom fields // null custom fields
@@ -621,9 +618,25 @@ class BulkAssetsController extends Controller
{ {
$this->authorize('checkout', Asset::class); $this->authorize('checkout', Asset::class);
$alreadyAssigned = collect();
if (old('selected_assets') && is_array(old('selected_assets'))) {
$assets = Asset::findMany(old('selected_assets'));
[$assignable, $alreadyAssigned] = $assets->partition(function (Asset $asset) {
return !$asset->assigned_to;
});
session()->flashInput(['selected_assets' => $assignable->pluck('id')->values()->toArray()]);
}
$do_not_change = ['' => trans('general.do_not_change')]; $do_not_change = ['' => trans('general.do_not_change')];
$status_label_list = $do_not_change + Helper::deployableStatusLabelList(); $status_label_list = $do_not_change + Helper::deployableStatusLabelList();
return view('hardware/bulk-checkout')->with('statusLabel_list', $status_label_list);
return view('hardware/bulk-checkout', [
'statusLabel_list' => $status_label_list,
'removed_assets' => $alreadyAssigned,
]);
} }
/** /**
@@ -637,6 +650,7 @@ class BulkAssetsController extends Controller
$admin = auth()->user(); $admin = auth()->user();
$target = $this->determineCheckoutTarget(); $target = $this->determineCheckoutTarget();
session()->put(['checkout_to_type' => $target]);
if (! is_array($request->get('selected_assets'))) { if (! is_array($request->get('selected_assets'))) {
return redirect()->route('hardware.bulkcheckout.show')->withInput()->with('error', trans('admin/hardware/message.checkout.no_assets_selected')); return redirect()->route('hardware.bulkcheckout.show')->withInput()->with('error', trans('admin/hardware/message.checkout.no_assets_selected'));
@@ -646,6 +660,30 @@ class BulkAssetsController extends Controller
$assets = Asset::findOrFail($asset_ids); $assets = Asset::findOrFail($asset_ids);
// Prevent checking out assets that are already checked out
if ($assets->pluck('assigned_to')->unique()->filter()->isNotEmpty()) {
// re-add the asset ids so the assets select is re-populated
$request->session()->flashInput(['selected_assets' => $asset_ids]);
return redirect(route('hardware.bulkcheckout.show'))
->with('error', trans('general.error_assets_already_checked_out'));
}
// Prevent checking out assets across companies if FMCS enabled
if (Setting::getSettings()->full_multiple_companies_support && $target->company_id) {
$company_ids = $assets->pluck('company_id')->unique();
// if there is more than one unique company id or the singular company id does not match
// then the checkout is invalid
if ($company_ids->count() > 1 || $company_ids->first() != $target->company_id) {
// re-add the asset ids so the assets select is re-populated
$request->session()->flashInput(['selected_assets' => $asset_ids]);
return redirect(route('hardware.bulkcheckout.show'))
->with('error', trans('general.error_user_company_multiple'));
}
}
if (request('checkout_to_type') == 'asset') { if (request('checkout_to_type') == 'asset') {
foreach ($asset_ids as $asset_id) { foreach ($asset_ids as $asset_id) {
if ($target->id == $asset_id) { if ($target->id == $asset_id) {

View File

@@ -92,7 +92,9 @@ class BulkAssetModelsController extends Controller
$update_array['min_amt'] = $request->input('min_amt'); $update_array['min_amt'] = $request->input('min_amt');
} }
if ($request->filled('require_serial')) {
$update_array['require_serial'] = $request->input('require_serial');
}
if (count($update_array) > 0) { if (count($update_array) > 0) {
AssetModel::whereIn('id', $models_raw_array)->update($update_array); AssetModel::whereIn('id', $models_raw_array)->update($update_array);

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Categories\DestroyCategoryAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssetModels;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Models\Category;
use Illuminate\Http\Request;
class BulkCategoriesController extends Controller
{
public function destroy(Request $request)
{
$this->authorize('delete', Category::class);
$errors = [];
$success_count = 0;
foreach ($request->ids as $id) {
$category = Category::find($id);
if (is_null($category)) {
$errors[] = trans('admin/categories/message.does_not_exist');
continue;
}
try {
DestroyCategoryAction::run(category: $category);
$success_count++;
} catch (ItemStillHasAccessories $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_assets_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
} catch (ItemStillHasAssetModels) {
$errors[] = trans('general.bulk_delete_associations.assoc_asset_models_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
} catch (ItemStillHasAssets) {
$errors[] = trans('general.bulk_delete_associations.assoc_assets_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
} catch (ItemStillHasComponents) {
$errors[] = trans('general.bulk_delete_associations.assoc_components_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
} catch (ItemStillHasConsumables) {
$errors[] = trans('general.bulk_delete_associations.assoc_consumables_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
} catch (ItemStillHasLicenses) {
$errors[] = trans('general.bulk_delete_associations.assoc_licenses_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);;
} catch (\Exception $e) {
report($e);
$errors[] = trans('general.something_went_wrong');
}
}
if (count($errors) > 0) {
if ($success_count > 0) {
return redirect()->route('categories.index')->with('success', trans_choice('admin/categories/message.delete.partial_success', $success_count, ['count' => $success_count]))->with('multi_error_messages', $errors);
}
return redirect()->route('categories.index')->with('multi_error_messages', $errors);
} else {
return redirect()->route('categories.index')->with('success', trans('admin/categories/message.delete.bulk_success'));
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Manufacturers\DeleteManufacturerAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssetModels;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasChildren;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Models\Manufacturer;
use Illuminate\Http\Request;
class BulkManufacturersController extends Controller
{
public function destroy(Request $request)
{
$this->authorize('delete', Manufacturer::class);
$errors = [];
$success_count = 0;
foreach ($request->ids as $id) {
$manufacturer = Manufacturer::find($id);
if (is_null($manufacturer)) {
$errors[] = trans('admin/manufacturers/message.does_not_exist');
continue;
}
try {
DeleteManufacturerAction::run(manufacturer: $manufacturer);
$success_count++;
} catch (ItemStillHasAssets $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_assets_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);
} catch (ItemStillHasAccessories $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_accessories_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);
} catch (ItemStillHasConsumables $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_consumables_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);
} catch (ItemStillHasComponents $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_components_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);
} catch (ItemStillHasLicenses $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_licenses_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);;
} catch (\Exception $e) {
report($e);
$errors[] = trans('general.something_went_wrong');
}
}
if (count($errors) > 0) {
if ($success_count > 0) {
return redirect()->route('manufacturers.index')->with('success', trans_choice('admin/manufacturers/message.delete.partial_success', $success_count, ['count' => $success_count]))->with('multi_error_messages', $errors);
}
return redirect()->route('manufacturers.index')->with('multi_error_messages', $errors);
} else {
return redirect()->route('manufacturers.index')->with('success', trans('admin/manufacturers/message.delete.bulk_success'));
}
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Suppliers\DestroySupplierAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasMaintenances;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasLicenses;
use App\Models\Supplier;
use Illuminate\Http\Request;
class BulkSuppliersController extends Controller
{
public function destroy(Request $request)
{
$this->authorize('delete', Supplier::class);
$errors = [];
$success_count = 0;
foreach ($request->ids as $id) {
$supplier = Supplier::find($id);
if (is_null($supplier)) {
$errors[] = trans('admin/suppliers/message.delete.not_found');
continue;
}
try {
DestroySupplierAction::run(supplier: $supplier);
} catch (ItemStillHasAssets $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_assets', ['asset_count' => (int) $supplier->assets_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
} catch (ItemStillHasMaintenances $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_maintenances', ['asset_maintenances_count' => $supplier->asset_maintenances_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
} catch (ItemStillHasLicenses $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_licenses', ['licenses_count' => (int) $supplier->licenses_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
} catch (ItemStillHasAccessories $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_accessories', ['accessories_count' => (int) $supplier->accessories_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
} catch (ItemStillHasConsumables $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_consumables', ['consumables_count' => (int) $supplier->consumables_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
} catch (ItemStillHasComponents $e) {
$errors[] = trans('general.bulk_delete_associations.assoc_components', ['components_count' => (int) $supplier->components_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
} catch (\Exception $e) {
report($e);
$errors[] = trans('general.something_went_wrong');
}
}
if (count($errors) > 0) {
if ($success_count > 0) {
return redirect()->route('suppliers.index')->with('success', trans_choice('admin/suppliers/message.delete.partial_success', $success_count, ['count' => $success_count]))->with('multi_error_messages', $errors);
}
return redirect()->route('suppliers.index')->with('multi_error_messages', $errors);
} else {
return redirect()->route('suppliers.index')->with('success', trans('admin/suppliers/message.delete.bulk_success'));
}
}
}

View File

@@ -2,6 +2,14 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Categories\DestroyCategoryAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssetModels;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasChildren;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\ImageUploadRequest;
use App\Models\Category; use App\Models\Category;
@@ -143,20 +151,18 @@ class CategoriesController extends Controller
* @since [v1.0] * @since [v1.0]
* @param int $categoryId * @param int $categoryId
*/ */
public function destroy($categoryId) : RedirectResponse public function destroy(Category $category): RedirectResponse
{ {
$this->authorize('delete', Category::class); $this->authorize('delete', Category::class);
// Check if the category exists try {
if (is_null($category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($categoryId))) { DestroyCategoryAction::run($category);
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.not_found')); } catch (ItemStillHasChildren $e) {
return redirect()->route('categories.index')->with('error', trans('general.bulk_delete_associations.general_assoc_warning', ['item' => trans('general.category')]));
} catch (\Exception $e) {
report($e);
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.delete.error'));
} }
if (! $category->isDeletable()) {
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.assoc_items', ['asset_type'=> $category->category_type]));
}
Storage::disk('public')->delete('categories'.'/'.$category->image);
$category->delete();
return redirect()->route('categories.index')->with('success', trans('admin/categories/message.delete.success')); return redirect()->route('categories.index')->with('success', trans('admin/categories/message.delete.success'));
} }

View File

@@ -102,13 +102,15 @@ class ComponentCheckoutController extends Controller
return redirect()->route('components.checkout.show', $componentId)->with('error', trans('general.error_user_company')); return redirect()->route('components.checkout.show', $componentId)->with('error', trans('general.error_user_company'));
} }
$component->checkout_qty = $request->input('assigned_qty');
// Update the component data // Update the component data
$component->asset_id = $request->input('asset_id'); $component->asset_id = $request->input('asset_id');
$component->assets()->attach($component->id, [ $component->assets()->attach($component->id, [
'component_id' => $component->id, 'component_id' => $component->id,
'created_by' => auth()->user()->id, 'created_by' => auth()->user()->id,
'created_at' => date('Y-m-d H:i:s'), 'created_at' => date('Y-m-d H:i:s'),
'assigned_qty' => $request->input('assigned_qty'), 'assigned_qty' => $component->checkout_qty,
'asset_id' => $request->input('asset_id'), 'asset_id' => $request->input('asset_id'),
'note' => $request->input('note'), 'note' => $request->input('note'),
]); ]);

View File

@@ -45,6 +45,7 @@ abstract class Controller extends BaseController
'accessories' => Accessory::class, 'accessories' => Accessory::class,
'maintenances' => Maintenance::class, 'maintenances' => Maintenance::class,
'assets' => Asset::class, 'assets' => Asset::class,
'audits' => Asset::class,
'components' => Component::class, 'components' => Component::class,
'consumables' => Consumable::class, 'consumables' => Consumable::class,
'hardware' => Asset::class, 'hardware' => Asset::class,
@@ -58,6 +59,7 @@ abstract class Controller extends BaseController
'accessories' => 'private_uploads/accessories/', 'accessories' => 'private_uploads/accessories/',
'maintenances' => 'private_uploads/maintenances/', 'maintenances' => 'private_uploads/maintenances/',
'assets' => 'private_uploads/assets/', 'assets' => 'private_uploads/assets/',
'audits' => 'private_uploads/audits/',
'components' => 'private_uploads/components/', 'components' => 'private_uploads/components/',
'consumables' => 'private_uploads/consumables/', 'consumables' => 'private_uploads/consumables/',
'hardware' => 'private_uploads/assets/', 'hardware' => 'private_uploads/assets/',
@@ -71,6 +73,7 @@ abstract class Controller extends BaseController
'accessories' => 'accessory', 'accessories' => 'accessory',
'maintenances' => 'maintenance', 'maintenances' => 'maintenance',
'assets' => 'asset', 'assets' => 'asset',
'audits' => 'audits',
'components' => 'component', 'components' => 'component',
'consumables' => 'consumable', 'consumables' => 'consumable',
'hardware' => 'asset', 'hardware' => 'asset',

View File

@@ -65,6 +65,7 @@ class GroupsController extends Controller
$group->notes = $request->input('notes'); $group->notes = $request->input('notes');
if ($group->save()) { if ($group->save()) {
$group->users()->sync($request->input('associated_users'));
return redirect()->route('groups.index')->with('success', trans('admin/groups/message.success.create')); return redirect()->route('groups.index')->with('success', trans('admin/groups/message.success.create'));
} }
@@ -88,7 +89,10 @@ class GroupsController extends Controller
$groupPermissions = []; $groupPermissions = [];
} }
$selected_array = Helper::selectedPermissionsArray($permissions, $groupPermissions); $selected_array = Helper::selectedPermissionsArray($permissions, $groupPermissions);
return view('groups.edit', compact('group', 'permissions', 'selected_array', 'groupPermissions')); $associated_users = $group->users()->get();
//dd($associated_users->toArray());
return view('groups.edit', compact('group', 'permissions', 'selected_array', 'groupPermissions'))->with('associated_users', $associated_users);
} }
/** /**
@@ -105,8 +109,10 @@ class GroupsController extends Controller
$group->permissions = json_encode($request->input('permission')); $group->permissions = json_encode($request->input('permission'));
$group->notes = $request->input('notes'); $group->notes = $request->input('notes');
if (! config('app.lock_passwords')) { if (! config('app.lock_passwords')) {
if ($group->save()) { if ($group->save()) {
$group->users()->sync($request->input('associated_users'));
return redirect()->route('groups.index')->with('success', trans('admin/groups/message.success.update')); return redirect()->route('groups.index')->with('success', trans('admin/groups/message.success.update'));
} }

View File

@@ -64,12 +64,7 @@ class LicenseCheckinController extends Controller
$this->authorize('checkout', $license); $this->authorize('checkout', $license);
if (! $license->reassignable) {
// Not allowed to checkin
Session::flash('error', trans('admin/licenses/message.checkin.not_reassignable') . '.');
return redirect()->back()->withInput();
}
// Declare the rules for the form validation // Declare the rules for the form validation
$rules = [ $rules = [
@@ -98,6 +93,9 @@ class LicenseCheckinController extends Controller
$licenseSeat->assigned_to = null; $licenseSeat->assigned_to = null;
$licenseSeat->asset_id = null; $licenseSeat->asset_id = null;
$licenseSeat->notes = $request->input('notes'); $licenseSeat->notes = $request->input('notes');
if (! $licenseSeat->license->reassignable) {
$licenseSeat->unreassignable_seat = true;
}
session()->put(['redirect_option' => $request->get('redirect_option')]); session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($request->get('redirect_option') === 'target'){ if ($request->get('redirect_option') === 'target'){
@@ -106,7 +104,7 @@ class LicenseCheckinController extends Controller
// Was the asset updated? // Was the asset updated?
if ($licenseSeat->save()) { if ($licenseSeat->save()) {
event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $request->input('notes'))); event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $licenseSeat->notes));
return Helper::getRedirectOption($request, $license->id, 'Licenses') return Helper::getRedirectOption($request, $license->id, 'Licenses')
@@ -132,21 +130,17 @@ class LicenseCheckinController extends Controller
$license = License::findOrFail($licenseId); $license = License::findOrFail($licenseId);
$this->authorize('checkin', $license); $this->authorize('checkin', $license);
if (! $license->reassignable) {
// Not allowed to checkin
Session::flash('error', 'License not reassignable.');
return redirect()->back()->withInput();
}
$licenseSeatsByUser = LicenseSeat::where('license_id', '=', $licenseId) $licenseSeatsByUser = LicenseSeat::where('license_id', '=', $licenseId)
->whereNotNull('assigned_to') ->whereNotNull('assigned_to')
->with('user') ->with('user', 'license')
->get(); ->get();
$license = $licenseSeatsByUser->first()?->license;
foreach ($licenseSeatsByUser as $user_seat) { foreach ($licenseSeatsByUser as $user_seat) {
$user_seat->assigned_to = null; $user_seat->assigned_to = null;
if ($license && ! $license->reassignable) {
$user_seat->unreassignable_seat = true;
}
if ($user_seat->save()) { if ($user_seat->save()) {
Log::debug('Checking in '.$license->name.' from user '.$user_seat->username); Log::debug('Checking in '.$license->name.' from user '.$user_seat->username);
$user_seat->logCheckin($user_seat->user, trans('admin/licenses/general.bulk.checkin_all.log_msg')); $user_seat->logCheckin($user_seat->user, trans('admin/licenses/general.bulk.checkin_all.log_msg'));
@@ -159,9 +153,12 @@ class LicenseCheckinController extends Controller
->get(); ->get();
$count = 0; $count = 0;
$license = $licenseSeatsByAsset->first()?->license;
foreach ($licenseSeatsByAsset as $asset_seat) { foreach ($licenseSeatsByAsset as $asset_seat) {
$asset_seat->asset_id = null; $asset_seat->asset_id = null;
if ($license && ! $license->reassignable) {
$asset_seat->unreassignable_seat = true;
}
if ($asset_seat->save()) { if ($asset_seat->save()) {
Log::debug('Checking in '.$license->name.' from asset '.$asset_seat->asset_tag); Log::debug('Checking in '.$license->name.' from asset '.$asset_seat->asset_tag);
$asset_seat->logCheckin($asset_seat->asset, trans('admin/licenses/general.bulk.checkin_all.log_msg')); $asset_seat->logCheckin($asset_seat->asset, trans('admin/licenses/general.bulk.checkin_all.log_msg'));

View File

@@ -25,7 +25,7 @@ class LicenseCheckoutController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>] * @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0] * @since [v1.0]
* @param $id * @param $id
* @return \Illuminate\Contracts\View\View * @return \Illuminate\Contracts\View\View |\Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Illuminate\Auth\Access\AuthorizationException
*/ */
public function create(License $license) public function create(License $license)
@@ -39,6 +39,16 @@ class LicenseCheckoutController extends Controller
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats')); return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats'));
} }
// Make sure the license is expired or terminated
if ($license->isInactive()) {
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.license_is_inactive'));
}
// We don't currently allow checking out licenses to locations, so we'll reset that to user if needed
if (session()->get('checkout_to_type') == 'location') {
session()->put(['checkout_to_type' => 'user']);
}
// Return the checkout view // Return the checkout view
return view('licenses/checkout', compact('license')); return view('licenses/checkout', compact('license'));
} }
@@ -65,22 +75,31 @@ class LicenseCheckoutController extends Controller
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.not_found')); return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.not_found'));
} }
$this->authorize('checkout', $license); $this->authorize('checkout', $license);
// Make sure there is at least one available to checkout
if ($license->availCount()->count() < 1) {
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats'));
}
// Make sure the license is expired or terminated
if ($license->isInactive()) {
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.license_is_inactive'));
}
$licenseSeat = $this->findLicenseSeatToCheckout($license, $seatId); $licenseSeat = $this->findLicenseSeatToCheckout($license, $seatId);
$licenseSeat->created_by = auth()->id(); $licenseSeat->created_by = auth()->id();
$licenseSeat->notes = $request->input('notes'); $licenseSeat->notes = $request->input('notes');
$checkoutMethod = 'checkoutTo'.ucwords(request('checkout_to_type'));
if ($request->filled('asset_id')) { if ($request->filled('asset_id')) {
session()->put(['checkout_to_type' => 'asset']);
$checkoutTarget = $this->checkoutToAsset($licenseSeat); $checkoutTarget = $this->checkoutToAsset($licenseSeat);
$request->request->add(['assigned_asset' => $checkoutTarget->id]); $request->request->add(['assigned_asset' => $checkoutTarget->id]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => 'asset']); session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => 'asset']);
} elseif ($request->filled('assigned_to')) { } elseif ($request->filled('assigned_to')) {
session()->put(['checkout_to_type' => 'user']);
$checkoutTarget = $this->checkoutToUser($licenseSeat); $checkoutTarget = $this->checkoutToUser($licenseSeat);
$request->request->add(['assigned_user' => $checkoutTarget->id]); $request->request->add(['assigned_user' => $checkoutTarget->id]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => 'user']); session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => 'user']);
@@ -89,6 +108,7 @@ class LicenseCheckoutController extends Controller
if ($checkoutTarget) { if ($checkoutTarget) {
return Helper::getRedirectOption($request, $license->id, 'Licenses') return Helper::getRedirectOption($request, $license->id, 'Licenses')
->with('success', trans('admin/licenses/message.checkout.success')); ->with('success', trans('admin/licenses/message.checkout.success'));
} }
@@ -110,6 +130,7 @@ class LicenseCheckoutController extends Controller
throw new \Illuminate\Http\Exceptions\HttpResponseException(redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats'))); throw new \Illuminate\Http\Exceptions\HttpResponseException(redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats')));
} }
if (! $licenseSeat->license->is($license)) { if (! $licenseSeat->license->is($license)) {
throw new \Illuminate\Http\Exceptions\HttpResponseException(redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.mismatch'))); throw new \Illuminate\Http\Exceptions\HttpResponseException(redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.mismatch')));
} }

View File

@@ -245,16 +245,28 @@ class LicensesController extends Controller
$license = License::with('assignedusers')->find($license->id); $license = License::with('assignedusers')->find($license->id);
$users_count = User::where('autoassign_licenses', '1')->count(); $users_count = User::where('autoassign_licenses', '1')->count();
$total_seats_count = $license->totalSeatsByLicenseID();
$total_seats_count = (int) $license->totalSeatsByLicenseID();
$available_seats_count = $license->availCount()->count(); $available_seats_count = $license->availCount()->count();
$checkedout_seats_count = ($total_seats_count - $available_seats_count); $unreassignable_seats_count = License::unReassignableCount($license);
if(!$license->reassignable){
$checkedout_seats_count = ($total_seats_count - $available_seats_count - $unreassignable_seats_count );
}
else {
$checkedout_seats_count = ($total_seats_count - $available_seats_count);
}
if($license->isInactive()){
session()->flash('warning', (trans('admin/licenses/message.checkout.license_is_inactive')));
}
$this->authorize('view', $license); $this->authorize('view', $license);
return view('licenses.view', compact('license')) return view('licenses.view', compact('license'))
->with('users_count', $users_count) ->with('users_count', $users_count)
->with('total_seats_count', $total_seats_count) ->with('total_seats_count', $total_seats_count)
->with('available_seats_count', $available_seats_count) ->with('available_seats_count', $available_seats_count)
->with('checkedout_seats_count', $checkedout_seats_count); ->with('checkedout_seats_count', $checkedout_seats_count)
->with('unreassignable_seats_count', $unreassignable_seats_count);
} }

View File

@@ -189,30 +189,36 @@ class LocationsController extends Controller
{ {
$this->authorize('delete', Location::class); $this->authorize('delete', Location::class);
if (is_null($location = Location::find($locationId))) { $location = Location::withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('assignedAccessories as assigned_accessories_count')
->withCount('accessories as accessories_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count')
->withCount('consumables as consumables_count')
->withCount('components as components_count')
->find($locationId);
if (!$location) {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.does_not_exist')); return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.does_not_exist'));
} }
if ($location->users()->count() > 0) { if ($location->isDeletable()) {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_users'));
} elseif ($location->children()->count() > 0) {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_child_loc'));
} elseif ($location->assets()->count() > 0) {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_assets'));
} elseif ($location->assignedassets()->count() > 0) {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_assets'));
}
if ($location->image) { if ($location->image) {
try { try {
Storage::disk('public')->delete('locations/'.$location->image); Storage::disk('public')->delete('locations/'.$location->image);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error($e); Log::error($e);
}
} }
$location->delete();
return redirect()->to(route('locations.index'))->with('success', trans('admin/locations/message.delete.success'));
} else {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_users'));
} }
$location->delete();
return redirect()->to(route('locations.index'))->with('success', trans('admin/locations/message.delete.success'));
} }
/** /**
@@ -247,23 +253,41 @@ class LocationsController extends Controller
$this->authorize('view', Location::class); $this->authorize('view', Location::class);
if ($location = Location::where('id', $id)->first()) { if ($location = Location::where('id', $id)->first()) {
$parent = Location::where('id', $location->parent_id)->first();
$manager = User::where('id', $location->manager_id)->first();
$company = Company::where('id', $location->company_id)->first();
$users = User::where('location_id', $id)->with('company', 'department', 'location')->get();
$assets = Asset::where('assigned_to', $id)->where('assigned_type', Location::class)->with('model', 'model.category')->get();
return view('locations/print') return view('locations/print')
->with('assets', $assets) ->with('assigned', false)
->with('users',$users) ->with('assets', $location->assets)
->with('assignedAssets', $location->assignedAssets)
->with('accessories', $location->accessories)
->with('assignedAccessories', $location->assignedAccessories)
->with('users',$location->users)
->with('location', $location) ->with('location', $location)
->with('parent', $parent) ->with('consumables', $location->consumables)
->with('manager', $manager) ->with('components', $location->components)
->with('company', $company); ->with('children', $location->children);
} }
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist')); return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
} }
public function print_all_assigned($id) : View | RedirectResponse
{
$this->authorize('view', Location::class);
if ($location = Location::where('id', $id)->first()) {
return view('locations/print')
->with('assigned', true)
->with('assets', $location->assets)
->with('assignedAssets', $location->assignedAssets)
->with('accessories', $location->accessories)
->with('assignedAccessories', $location->assignedAccessories)
->with('users',$location->users)
->with('location', $location)
->with('consumables', $location->consumables)
->with('components', $location->components)
->with('children', $location->children);
}
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
}
/** /**
* Returns a view that presents a form to clone a location. * Returns a view that presents a form to clone a location.
@@ -321,33 +345,12 @@ class LocationsController extends Controller
return redirect()->route('locations.index')->with('success', trans('admin/locations/message.restore.success')); return redirect()->route('locations.index')->with('success', trans('admin/locations/message.restore.success'));
} }
// Check validation
return redirect()->back()->with('error', trans('general.could_not_restore', ['item_type' => trans('general.location'), 'error' => $location->getErrors()->first()])); return redirect()->back()->with('error', trans('general.could_not_restore', ['item_type' => trans('general.location'), 'error' => $location->getErrors()->first()]));
} }
return redirect()->back()->with('error', trans('admin/models/message.does_not_exist')); return redirect()->back()->with('error', trans('admin/models/message.does_not_exist'));
} }
public function print_all_assigned($id) : View | RedirectResponse
{
$this->authorize('view', Location::class);
if ($location = Location::where('id', $id)->first()) {
$parent = Location::where('id', $location->parent_id)->first();
$manager = User::where('id', $location->manager_id)->first();
$company = Company::where('id', $location->company_id)->first();
$users = User::where('location_id', $id)->with('company', 'department', 'location')->get();
$assets = Asset::where('location_id', $id)->with('model', 'model.category')->get();
return view('locations/print')
->with('assets', $assets)
->with('users',$users)
->with('location', $location)
->with('parent', $parent)
->with('manager', $manager)
->with('company', $company);
}
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
}
/** /**
* Returns a view that allows the user to bulk delete locations * Returns a view that allows the user to bulk delete locations
@@ -366,8 +369,12 @@ class LocationsController extends Controller
$locations = Location::whereIn('id', $locations_raw_array) $locations = Location::whereIn('id', $locations_raw_array)
->withCount('assignedAssets as assigned_assets_count') ->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count') ->withCount('assets as assets_count')
->withCount('assignedAccessories as assigned_accessories_count')
->withCount('accessories as accessories_count')
->withCount('rtd_assets as rtd_assets_count') ->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count') ->withCount('children as children_count')
->withCount('consumables as consumables_count')
->withCount('components as components_count')
->withCount('users as users_count')->get(); ->withCount('users as users_count')->get();
$valid_count = 0; $valid_count = 0;
@@ -400,9 +407,13 @@ class LocationsController extends Controller
$locations = Location::whereIn('id', $locations_raw_array) $locations = Location::whereIn('id', $locations_raw_array)
->withCount('assignedAssets as assigned_assets_count') ->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count') ->withCount('assets as assets_count')
->withCount('assignedAccessories as assigned_accessories_count')
->withCount('accessories as accessories_count')
->withCount('rtd_assets as rtd_assets_count') ->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count') ->withCount('children as children_count')
->withCount('users as users_count')->get(); ->withCount('users as users_count')
->withCount('consumables as consumables_count')
->withCount('components as components_count')->get();
$success_count = 0; $success_count = 0;
$error_count = 0; $error_count = 0;

View File

@@ -68,6 +68,12 @@ class MaintenancesController extends Controller
{ {
$this->authorize('update', Asset::class); $this->authorize('update', Asset::class);
\Log::error(print_r($request->input('selected_assets[]'), true));
if (!$request->filled('selected_assets')) {
return redirect()->back()->withInput()->with('error', 'No assets were selected.');
}
$assets = Asset::whereIn('id', $request->input('selected_assets'))->get(); $assets = Asset::whereIn('id', $request->input('selected_assets'))->get();
// Loop through the selected assets // Loop through the selected assets
@@ -78,6 +84,7 @@ class MaintenancesController extends Controller
$maintenance->is_warranty = $request->input('is_warranty'); $maintenance->is_warranty = $request->input('is_warranty');
$maintenance->cost = $request->input('cost'); $maintenance->cost = $request->input('cost');
$maintenance->notes = $request->input('notes'); $maintenance->notes = $request->input('notes');
$maintenance->url = $request->input('url');
// Save the asset maintenance data // Save the asset maintenance data
$maintenance->asset_id = $asset->id; $maintenance->asset_id = $asset->id;
@@ -152,6 +159,7 @@ class MaintenancesController extends Controller
$maintenance->name = $request->input('name'); $maintenance->name = $request->input('name');
$maintenance->start_date = $request->input('start_date'); $maintenance->start_date = $request->input('start_date');
$maintenance->completion_date = $request->input('completion_date'); $maintenance->completion_date = $request->input('completion_date');
$maintenance->url = $request->input('url');
// Todo - put this in a getter/setter? // Todo - put this in a getter/setter?

View File

@@ -2,6 +2,14 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Manufacturers\DeleteManufacturerAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasChildren;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasLicenses;
use App\Helpers\Helper;
use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\ImageUploadRequest;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\Manufacturer; use App\Models\Manufacturer;
@@ -157,32 +165,18 @@ class ManufacturersController extends Controller
* @param int $manufacturerId * @param int $manufacturerId
* @since [v1.0] * @since [v1.0]
*/ */
public function destroy($manufacturerId) : RedirectResponse public function destroy(Manufacturer $manufacturer): RedirectResponse
{ {
$this->authorize('delete', Manufacturer::class); $this->authorize('delete', $manufacturer);
if (is_null($manufacturer = Manufacturer::withTrashed()->withCount('models as models_count')->find($manufacturerId))) { try {
return redirect()->route('manufacturers.index')->with('error', trans('admin/manufacturers/message.not_found')); DeleteManufacturerAction::run($manufacturer);
} catch (ItemStillHasChildren $e) {
return redirect()->route('manufacturers.index')->with('error', trans('general.bulk_delete_associations.general_assoc_warning', ['item' => trans('general.manufacturer')]));
} catch (\Exception $e) {
report($e);
return redirect()->route('manufacturers.index')->with('error', trans('general.something_went_wrong'));
} }
if (! $manufacturer->isDeletable()) {
return redirect()->route('manufacturers.index')->with('error', trans('admin/manufacturers/message.assoc_users'));
}
if ($manufacturer->image) {
try {
Storage::disk('public')->delete('manufacturers/'.$manufacturer->image);
} catch (\Exception $e) {
Log::info($e);
}
}
// Soft delete the manufacturer if active, permanent delete if is already deleted
if ($manufacturer->deleted_at === null) {
$manufacturer->delete();
} else {
$manufacturer->forceDelete();
}
// Redirect to the manufacturers management page
return redirect()->route('manufacturers.index')->with('success', trans('admin/manufacturers/message.delete.success')); return redirect()->route('manufacturers.index')->with('success', trans('admin/manufacturers/message.delete.success'));
} }

View File

@@ -159,7 +159,7 @@ class ReportsController extends Controller
$row[] = e($asset->serial); $row[] = e($asset->serial);
if ($target = $asset->assignedTo) { if ($target = $asset->assignedTo) {
$row[] = e($target->present()->name()); $row[] = e($target->display_name);
} else { } else {
$row[] = ''; // Empty string if unassigned $row[] = ''; // Empty string if unassigned
} }
@@ -274,22 +274,18 @@ class ReportsController extends Controller
$target_name = ''; $target_name = '';
if ($actionlog->target) { if ($actionlog->target) {
if ($actionlog->targetType() == 'user') { $target_name = $actionlog->target->display_name;
$target_name = $actionlog->target->display_name;
} else {
$target_name = $actionlog->target->getDisplayNameAttribute();
}
} }
if($actionlog->item){ if ($actionlog->item){
$item_name = e($actionlog->item->getDisplayNameAttribute()); $item_name = e($actionlog->item->display_name);
} else { } else {
$item_name = ''; $item_name = '';
} }
$row = [ $row = [
$actionlog->created_at, $actionlog->created_at,
($actionlog->adminuser) ? e($actionlog->adminuser->display_name) : '', ($actionlog->adminuser) ? $actionlog->adminuser->display_name : '',
$actionlog->present()->actionType(), $actionlog->present()->actionType(),
e($actionlog->itemType()), e($actionlog->itemType()),
($actionlog->itemType() == 'user') ? $actionlog->filename : $item_name, ($actionlog->itemType() == 'user') ? $actionlog->filename : $item_name,
@@ -298,10 +294,10 @@ class ReportsController extends Controller
(($actionlog->item) && ($actionlog->item->model)) ? $actionlog->item->model->model_number : null, (($actionlog->item) && ($actionlog->item->model)) ? $actionlog->item->model->model_number : null,
$target_name, $target_name,
($actionlog->note) ? e($actionlog->note) : '', ($actionlog->note) ? e($actionlog->note) : '',
$actionlog->log_meta,
$actionlog->remote_ip, $actionlog->remote_ip,
$actionlog->user_agent, $actionlog->user_agent,
$actionlog->action_source, $actionlog->action_source,
$actionlog->log_meta,
]; ];
fputcsv($handle, $row); fputcsv($handle, $row);
} }
@@ -440,10 +436,8 @@ class ReportsController extends Controller
// Open output stream // Open output stream
$handle = fopen('php://output', 'w'); $handle = fopen('php://output', 'w');
stream_set_timeout($handle, 2000); stream_set_timeout($handle, 2000);
if ($request->filled('use_bom')) { fprintf($handle, chr(0xEF).chr(0xBB).chr(0xBF));
fprintf($handle, chr(0xEF).chr(0xBB).chr(0xBF));
}
$header = []; $header = [];
@@ -689,6 +683,14 @@ class ReportsController extends Controller
$assets->whereBetween('assets.purchase_date', [$request->input('purchase_start'), $request->input('purchase_end')]); $assets->whereBetween('assets.purchase_date', [$request->input('purchase_start'), $request->input('purchase_end')]);
} }
if ($request->filled('purchase_cost_start')) {
if ($request->filled('purchase_cost_end')) {
$assets->whereBetween('assets.purchase_cost', [$request->input('purchase_cost_start'), $request->input('purchase_cost_end')]);
} else {
$assets->where('assets.purchase_cost', ">", $request->input('purchase_cost_start'));
}
}
if (($request->filled('created_start')) && ($request->filled('created_end'))) { if (($request->filled('created_start')) && ($request->filled('created_end'))) {
$created_start = Carbon::parse($request->input('created_start'))->startOfDay(); $created_start = Carbon::parse($request->input('created_start'))->startOfDay();
$created_end = Carbon::parse($request->input('created_end'))->endOfDay(); $created_end = Carbon::parse($request->input('created_end'))->endOfDay();
@@ -830,7 +832,7 @@ class ReportsController extends Controller
} }
if ($request->filled('location')) { if ($request->filled('location')) {
$row[] = ($asset->location) ? $asset->location->present()->name() : ''; $row[] = ($asset->location) ? $asset->location->display_name : '';
} }
if ($request->filled('location_address')) { if ($request->filled('location_address')) {
@@ -843,7 +845,7 @@ class ReportsController extends Controller
} }
if ($request->filled('rtd_location')) { if ($request->filled('rtd_location')) {
$row[] = ($asset->defaultLoc) ? $asset->defaultLoc->present()->name() : ''; $row[] = ($asset->defaultLoc) ? $asset->defaultLoc->display_name : '';
} }
if ($request->filled('rtd_location_address')) { if ($request->filled('rtd_location_address')) {
@@ -856,8 +858,8 @@ class ReportsController extends Controller
} }
if ($request->filled('assigned_to')) { if ($request->filled('assigned_to')) {
$row[] = ($asset->checkedOutToUser() && $asset->assigned) ?? $asset->assigned->display_name; $row[] = ($asset->assigned) ? $asset->assigned->display_name : '';
$row[] = ($asset->checkedOutToUser() && $asset->assigned) ? 'user' : $asset->assignedType(); $row[] = ($asset->assigned) ? $asset->assignedType() : '';
} }
if ($request->filled('username')) { if ($request->filled('username')) {
@@ -1260,7 +1262,7 @@ class ReportsController extends Controller
$row[] = str_replace(',', '', e($item['assetItem']->model->name)); $row[] = str_replace(',', '', e($item['assetItem']->model->name));
$row[] = str_replace(',', '', e($item['assetItem']->name)); $row[] = str_replace(',', '', e($item['assetItem']->name));
$row[] = str_replace(',', '', e($item['assetItem']->asset_tag)); $row[] = str_replace(',', '', e($item['assetItem']->asset_tag));
$row[] = str_replace(',', '', e(($item['acceptance']->assignedTo) ? $item['acceptance']->assignedTo->present()->name() : trans('admin/reports/general.deleted_user'))); $row[] = str_replace(',', '', e(($item['acceptance']->assignedTo) ? $item['acceptance']->assignedTo->display_name : trans('admin/reports/general.deleted_user')));
$rows[] = implode(',', $row); $rows[] = implode(',', $row);
} }
} }

View File

@@ -2,10 +2,18 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Suppliers\DestroySupplierAction;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Exceptions\ItemStillHasMaintenances;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasLicenses;
use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\ImageUploadRequest;
use App\Models\Supplier; use App\Models\Supplier;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View; use \Illuminate\Contracts\View\View;
use Illuminate\Support\MessageBag;
/** /**
* This controller handles all actions related to Suppliers for * This controller handles all actions related to Suppliers for
@@ -118,30 +126,41 @@ class SuppliersController extends Controller
* *
* @param int $supplierId * @param int $supplierId
*/ */
public function destroy($supplierId) : RedirectResponse public function destroy(Supplier $supplier): RedirectResponse
{ {
$this->authorize('delete', Supplier::class); $this->authorize('delete', Supplier::class);
if (is_null($supplier = Supplier::with('maintenances', 'assets', 'licenses')->withCount('maintenances as maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->find($supplierId))) { try {
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.not_found')); DestroySupplierAction::run(supplier: $supplier);
} catch (ItemStillHasAssets $e) {
return redirect()->route('suppliers.index')->with('error', trans('general.bulk_delete_associations.assoc_assets', [
'asset_count' => (int) $supplier->assets_count, 'item' => trans('general.supplier')
]));
} catch (ItemStillHasMaintenances $e) {
return redirect()->route('suppliers.index')->with('error', trans('general.bulk_delete_associations.assoc_maintenances', [
'asset_maintenances_count' => $supplier->asset_maintenances_count, 'item' => trans('general.supplier')
]));
} catch (ItemStillHasLicenses $e) {
return redirect()->route('suppliers.index')->with('error', trans('general.bulk_delete_associations.assoc_licenses', [
'licenses_count' => (int) $supplier->licenses_count, 'item' => trans('general.supplier')
]));
} catch (ItemStillHasAccessories $e) {
return redirect()->route('suppliers.index')->with('error', trans('general.bulk_delete_associations.assoc_accessories', [
'accessories_count' => (int) $supplier->accessories_count, 'item' => trans('general.supplier')
]));
} catch (ItemStillHasConsumables $e) {
return redirect()->route('suppliers.index')->with('error', trans('general.bulk_delete_associations.assoc_consumables', [
'consumables_count' => (int) $supplier->consumables_count, 'item' => trans('general.supplier')
]));
} catch (ItemStillHasComponents $e) {
return redirect()->route('suppliers.index')->with('error', trans('general.bulk_delete_associations.assoc_components', [
'components_count' => (int) $supplier->components_count, 'item' => trans('general.supplier')
]));
} catch (\Exception $e) {
report($e);
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.error'));
} }
if ($supplier->assets_count > 0) { return redirect()->route('suppliers.index')->with('success', trans('admin/suppliers/message.delete.success'));
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count]));
}
if ($supplier->maintenances_count > 0) {
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_maintenances', ['maintenances_count' => $supplier->maintenances_count]));
}
if ($supplier->licenses_count > 0) {
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_licenses', ['licenses_count' => (int) $supplier->licenses_count]));
}
$supplier->delete();
return redirect()->route('suppliers.index')->with('success',
trans('admin/suppliers/message.delete.success')
);
} }
/** /**
@@ -154,6 +173,5 @@ class SuppliersController extends Controller
{ {
$this->authorize('view', Supplier::class); $this->authorize('view', Supplier::class);
return view('suppliers/view', compact('supplier')); return view('suppliers/view', compact('supplier'));
} }
} }

View File

@@ -36,7 +36,7 @@ class UploadedFilesController extends Controller
{ {
// Check the permissions to make sure the user can view the object // Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id); $object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('update', $object); $this->authorize('update', $object);
if (!$object) { if (!$object) {
@@ -85,7 +85,7 @@ class UploadedFilesController extends Controller
public function show($object_type, $id, $file_id) : RedirectResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse public function show($object_type, $id, $file_id) : RedirectResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{ {
// Check the permissions to make sure the user can view the object // Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id); $object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('view', $object); $this->authorize('view', $object);
if (!$object) { if (!$object) {
@@ -130,7 +130,7 @@ class UploadedFilesController extends Controller
{ {
// Check the permissions to make sure the user can view the object // Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id); $object = self::$map_object_type[$object_type]::withTrashed()->find($id);
$this->authorize('update', self::$map_object_type[$object_type]); $this->authorize('update', self::$map_object_type[$object_type]);
if (!$object) { if (!$object) {
@@ -139,7 +139,7 @@ class UploadedFilesController extends Controller
// Check for the file // Check for the file
$log = Actionlog::find($file_id)->where('item_type', self::$map_object_type[$object_type]) $log = Actionlog::where('id',$file_id)->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)->first(); ->where('item_id', $object->id)->first();
if ($log) { if ($log) {
@@ -148,7 +148,7 @@ class UploadedFilesController extends Controller
Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename); Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
} }
// Delete the record of the file // Delete the record of the file
if ($log->delete()) { if ($log->logUploadDelete($object, $log->filename)) {
return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.delete.success', 1)); return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.delete.success', 1));
} }

View File

@@ -13,7 +13,9 @@ use App\Models\Company;
use App\Models\Group; use App\Models\Group;
use App\Models\Setting; use App\Models\Setting;
use App\Models\User; use App\Models\User;
use App\Notifications\WelcomeNotification;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Password;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
use App\Notifications\CurrentInventory; use App\Notifications\CurrentInventory;
@@ -128,7 +130,7 @@ class UsersController extends Controller
// we have to invoke the form request here to handle image uploads // we have to invoke the form request here to handle image uploads
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar'); app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
if($request->get('redirect_option') === 'back'){ if ($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']); session()->put(['redirect_option' => 'index']);
} else { } else {
session()->put(['redirect_option' => $request->get('redirect_option')]); session()->put(['redirect_option' => $request->get('redirect_option')]);
@@ -136,6 +138,18 @@ class UsersController extends Controller
if ($user->save()) { if ($user->save()) {
if (($user->activated == '1') && ($user->email != '') && ($request->input('send_welcome') == '1')) {
try {
$user->notify(new WelcomeNotification($user));
} catch (\Exception $e) {
Log::warning('Could not send welcome notification for user: ' . $e->getMessage());
}
}
if ($request->filled('groups')) { if ($request->filled('groups')) {
$user->groups()->sync($request->input('groups')); $user->groups()->sync($request->input('groups'));
} else { } else {

View File

@@ -34,10 +34,7 @@ class AssetCountForSidebar
} }
try { try {
$total_assets = Asset::count(); $total_assets = Asset::AssetsForShow()->count();
if ($settings->show_archived_in_list != '1') {
$total_assets -= Asset::Archived()->count();
}
view()->share('total_assets', $total_assets); view()->share('total_assets', $total_assets);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::debug($e); Log::debug($e);

View File

@@ -14,6 +14,15 @@ class CustomAssetReportRequest extends Request
return true; return true;
} }
public function prepareForValidation()
{
if($this->filled('purchase_cost_end') && !$this->filled('purchase_cost_start')){
$this->merge(['purchase_cost_start' => 0 ]);
}
}
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *
@@ -24,6 +33,7 @@ class CustomAssetReportRequest extends Request
return [ return [
'purchase_start' => 'date|date_format:Y-m-d|nullable', 'purchase_start' => 'date|date_format:Y-m-d|nullable',
'purchase_end' => 'date|date_format:Y-m-d|nullable', 'purchase_end' => 'date|date_format:Y-m-d|nullable',
'purchase_cost_end' => 'numeric|nullable|gte:purchase_cost_start',
'created_start' => 'date|date_format:Y-m-d|nullable', 'created_start' => 'date|date_format:Y-m-d|nullable',
'created_end' => 'date|date_format:Y-m-d|nullable', 'created_end' => 'date|date_format:Y-m-d|nullable',
'checkout_date_start' => 'date|date_format:Y-m-d|nullable', 'checkout_date_start' => 'date|date_format:Y-m-d|nullable',

View File

@@ -109,7 +109,7 @@ class SettingsSamlRequest extends FormRequest
]; ];
$pkey = openssl_pkey_new([ $pkey = openssl_pkey_new([
'private_key_bits' => config('app.saml_key_size'), 'private_key_bits' => (int) config('app.saml_key_size'),
'private_key_type' => OPENSSL_KEYTYPE_RSA, 'private_key_type' => OPENSSL_KEYTYPE_RSA,
]); ]);

View File

@@ -26,6 +26,7 @@ class StoreAssetRequest extends ImageUploadRequest
public function prepareForValidation(): void public function prepareForValidation(): void
{ {
parent::prepareForValidation(); // call ImageUploadRequest thing
// Guard against users passing in an array for company_id instead of an integer. // Guard against users passing in an array for company_id instead of an integer.
// If the company_id is not an integer then we simply use what was // If the company_id is not an integer then we simply use what was
// provided to be caught by model level validation later. // provided to be caught by model level validation later.

View File

@@ -32,7 +32,7 @@ class StoreNotificationSettings extends FormRequest
], ],
'alert_threshold' => 'numeric|nullable', 'alert_threshold' => 'numeric|nullable',
'alert_interval' => 'numeric|nullable|gt:0', 'alert_interval' => 'numeric|nullable|gt:0',
'audit_warning_days' => 'numeric|nullable', 'audit_warning_days' => 'numeric|nullable|gte:0',
'due_checkin_days' => 'numeric|nullable|gt:0', 'due_checkin_days' => 'numeric|nullable|gt:0',
'audit_interval' => 'numeric|nullable|gt:0', 'audit_interval' => 'numeric|nullable|gt:0',
]; ];

View File

@@ -2,6 +2,7 @@
namespace App\Http\Traits; namespace App\Http\Traits;
use Illuminate\Validation\ValidationException;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -38,20 +39,13 @@ trait ConvertsBase64ToFiles
if (!$base64Contents) { if (!$base64Contents) {
return; return;
} }
// autogenerate filenames
if ($filename == 'auto'){
$header = explode(';', $base64Contents, 2)[0];
// Grab the image type from the header while we're at it.
$filename = $key . '.' . substr($header, strpos($header, '/')+1);
}
// Generate a temporary path to store the Base64 contents // Generate a temporary path to store the Base64 contents
$tempFilePath = tempnam(sys_get_temp_dir(), $filename); $tempFilePath = tempnam(sys_get_temp_dir(), $filename);
// Store the contents using a stream, or by decoding manually // Store the contents using a stream, or throw an Error (which doesn't do anything?)
if (Str::startsWith($base64Contents, 'data:') && count(explode(',', $base64Contents)) > 1) { if (Str::startsWith($base64Contents, 'data:') && count(explode(',', $base64Contents)) > 1) {
$source = fopen($base64Contents, 'r'); $source = fopen($base64Contents, 'r'); // PHP has special processing for "data:" URL's
$destination = fopen($tempFilePath, 'w'); $destination = fopen($tempFilePath, 'w');
stream_copy_to_stream($source, $destination); stream_copy_to_stream($source, $destination);
@@ -59,7 +53,8 @@ trait ConvertsBase64ToFiles
fclose($source); fclose($source);
fclose($destination); fclose($destination);
} else { } else {
file_put_contents($tempFilePath, base64_decode($base64Contents, true)); // TODO - to get a better error message here, can we maybe do something with modifying the errorBag?
throw new ValidationException("Need Base64 URL starting with 'data:'"); // This doesn't actually throw?
} }
$uploadedFile = new UploadedFile($tempFilePath, $filename, null, null, true); $uploadedFile = new UploadedFile($tempFilePath, $filename, null, null, true);

View File

@@ -36,6 +36,7 @@ class AccessoriesTransformer
'qty' => ($accessory->qty) ? (int) $accessory->qty : null, 'qty' => ($accessory->qty) ? (int) $accessory->qty : null,
'purchase_date' => ($accessory->purchase_date) ? Helper::getFormattedDateObject($accessory->purchase_date, 'date') : null, 'purchase_date' => ($accessory->purchase_date) ? Helper::getFormattedDateObject($accessory->purchase_date, 'date') : null,
'purchase_cost' => Helper::formatCurrencyOutput($accessory->purchase_cost), 'purchase_cost' => Helper::formatCurrencyOutput($accessory->purchase_cost),
'total_cost' => Helper::formatCurrencyOutput($accessory->totalCostSum()),
'order_number' => ($accessory->order_number) ? e($accessory->order_number) : null, 'order_number' => ($accessory->order_number) ? e($accessory->order_number) : null,
'min_qty' => ($accessory->min_amt) ? (int) $accessory->min_amt : null, // Legacy - should phase out - replaced by below, for the bootstrap table formatter 'min_qty' => ($accessory->min_amt) ? (int) $accessory->min_amt : null, // Legacy - should phase out - replaced by below, for the bootstrap table formatter
'min_amt' => ($accessory->min_amt) ? (int) $accessory->min_amt : null, 'min_amt' => ($accessory->min_amt) ? (int) $accessory->min_amt : null,

View File

@@ -50,17 +50,20 @@ class ActionlogsTransformer
public function transformActionlog (Actionlog $actionlog, $settings = null) public function transformActionlog (Actionlog $actionlog, $settings = null)
{ {
$icon = $actionlog->present()->icon(); $icon = $actionlog->present()->icon();
if (($actionlog->filename!='') && ($actionlog->action_type!='upload deleted')) {
$icon = Helper::filetype_icon($actionlog->filename);
}
static $custom_fields = false; static $custom_fields = false;
if ($custom_fields === false) { if ($custom_fields === false) {
$custom_fields = CustomField::all(); $custom_fields = CustomField::all();
} }
if ($actionlog->filename!='') {
$icon = Helper::filetype_icon($actionlog->filename);
}
// This is necessary since we can't escape special characters within a JSON object // This is necessary since we can't escape special characters within a JSON object
if (($actionlog->log_meta) && ($actionlog->log_meta!='')) { if (($actionlog->log_meta) && ($actionlog->log_meta!='')) {
@@ -144,15 +147,16 @@ class ActionlogsTransformer
[ [
'url' => $actionlog->uploads_file_url(), 'url' => $actionlog->uploads_file_url(),
'filename' => $actionlog->filename, 'filename' => $actionlog->filename,
'inlineable' => StorageHelper::allowSafeInline($actionlog->uploads_file_url()), 'inlineable' => StorageHelper::allowSafeInline($actionlog->uploads_file_path()),
'exists_on_disk' => Storage::exists($actionlog->uploads_file_path()) ? true : false, 'exists_on_disk' => Storage::exists($actionlog->uploads_file_path()) ? true : false,
'mediatype' => StorageHelper::getMediaType($actionlog->uploads_file_path()),
] : null, ] : null,
'item' => ($actionlog->item) ? [ 'item' => ($actionlog->item) ? [
'id' => (int) $actionlog->item->id, 'id' => (int) $actionlog->item->id,
'name' => e($actionlog->item->display_name) ?? null, 'name' => e($actionlog->item->display_name) ?? null,
'type' => e($actionlog->itemType()), 'type' => e($actionlog->itemType()),
'serial' =>e($actionlog->item->serial) ? e($actionlog->item->serial) : null 'serial' => e($actionlog->item->serial) ? e($actionlog->item->serial) : null
] : null, ] : null,
'location' => ($actionlog->location) ? [ 'location' => ($actionlog->location) ? [
'id' => (int) $actionlog->location->id, 'id' => (int) $actionlog->location->id,
@@ -165,7 +169,7 @@ class ActionlogsTransformer
'action_type' => $actionlog->present()->actionType(), 'action_type' => $actionlog->present()->actionType(),
'admin' => ($actionlog->adminuser) ? [ 'admin' => ($actionlog->adminuser) ? [
'id' => (int) $actionlog->adminuser->id, 'id' => (int) $actionlog->adminuser->id,
'name' => e($actionlog->adminuser->display_name), 'name' => e($actionlog->adminuser->display_name) ?? null,
'first_name'=> e($actionlog->adminuser->first_name), 'first_name'=> e($actionlog->adminuser->first_name),
'last_name'=> e($actionlog->adminuser->last_name) 'last_name'=> e($actionlog->adminuser->last_name)
] : null, ] : null,
@@ -177,7 +181,7 @@ class ActionlogsTransformer
] : null, ] : null,
'target' => ($actionlog->target) ? [ 'target' => ($actionlog->target) ? [
'id' => (int) $actionlog->target->id, 'id' => (int) $actionlog->target->id,
'name' => ($actionlog->target->display_name) ?? null, 'name' => e($actionlog->target->display_name) ?? null,
'type' => e($actionlog->targetType()), 'type' => e($actionlog->targetType()),
] : null, ] : null,

View File

@@ -48,12 +48,15 @@ class AssetModelsTransformer
'image' => ($assetmodel->image != '') ? Storage::disk('public')->url('models/'.e($assetmodel->image)) : null, 'image' => ($assetmodel->image != '') ? Storage::disk('public')->url('models/'.e($assetmodel->image)) : null,
'model_number' => ($assetmodel->model_number ? e($assetmodel->model_number): null), 'model_number' => ($assetmodel->model_number ? e($assetmodel->model_number): null),
'min_amt' => ($assetmodel->min_amt) ? (int) $assetmodel->min_amt : null, 'min_amt' => ($assetmodel->min_amt) ? (int) $assetmodel->min_amt : null,
'remaining' => (int) ($assetmodel->assets_count - $assetmodel->min_amt),
'depreciation' => ($assetmodel->depreciation) ? [ 'depreciation' => ($assetmodel->depreciation) ? [
'id' => (int) $assetmodel->depreciation->id, 'id' => (int) $assetmodel->depreciation->id,
'name'=> e($assetmodel->depreciation->name), 'name'=> e($assetmodel->depreciation->name),
] : null, ] : null,
'assets_count' => (int) $assetmodel->assets_count, 'assets_count' => (int) $assetmodel->assets_count,
'assets_assigned_count' => (int) $assetmodel->assets_assigned_count,
'assets_archived_count' => (int) $assetmodel->assets_archived_count,
'remaining' => (int) ($assetmodel->assets_count - (int) $assetmodel->assets_assigned_count) - (int) $assetmodel->assets_archived_count,
'category' => ($assetmodel->category) ? [ 'category' => ($assetmodel->category) ? [
'id' => (int) $assetmodel->category->id, 'id' => (int) $assetmodel->category->id,
'name'=> e($assetmodel->category->name), 'name'=> e($assetmodel->category->name),
@@ -65,6 +68,7 @@ class AssetModelsTransformer
'default_fieldset_values' => $default_field_values, 'default_fieldset_values' => $default_field_values,
'eol' => ($assetmodel->eol > 0) ? $assetmodel->eol.' months' : 'None', 'eol' => ($assetmodel->eol > 0) ? $assetmodel->eol.' months' : 'None',
'requestable' => ($assetmodel->requestable == '1') ? true : false, 'requestable' => ($assetmodel->requestable == '1') ? true : false,
'require_serial' => ($assetmodel->require_serial == '1') ? true : false,
'notes' => Helper::parseEscapedMarkedownInline($assetmodel->notes), 'notes' => Helper::parseEscapedMarkedownInline($assetmodel->notes),
'created_by' => ($assetmodel->adminuser) ? [ 'created_by' => ($assetmodel->adminuser) ? [
'id' => (int) $assetmodel->adminuser->id, 'id' => (int) $assetmodel->adminuser->id,

View File

@@ -43,6 +43,7 @@ class ComponentsTransformer
'order_number' => e($component->order_number), 'order_number' => e($component->order_number),
'purchase_date' => Helper::getFormattedDateObject($component->purchase_date, 'date'), 'purchase_date' => Helper::getFormattedDateObject($component->purchase_date, 'date'),
'purchase_cost' => Helper::formatCurrencyOutput($component->purchase_cost), 'purchase_cost' => Helper::formatCurrencyOutput($component->purchase_cost),
'total_cost' => Helper::formatCurrencyOutput($component->totalCostSum()),
'remaining' => (int) $component->numRemaining(), 'remaining' => (int) $component->numRemaining(),
'company' => ($component->company) ? [ 'company' => ($component->company) ? [
'id' => (int) $component->company->id, 'id' => (int) $component->company->id,
@@ -76,7 +77,7 @@ class ComponentsTransformer
$array[] = [ $array[] = [
'assigned_pivot_id' => $asset->pivot->id, 'assigned_pivot_id' => $asset->pivot->id,
'id' => (int) $asset->id, 'id' => (int) $asset->id,
'name' => e($asset->model->present()->name).' '.e($asset->present()->name), 'name' => e($asset->model->display_name).' '.e($asset->display_name),
'qty' => $asset->pivot->assigned_qty, 'qty' => $asset->pivot->assigned_qty,
'note' => $asset->pivot->note, 'note' => $asset->pivot->note,
'type' => 'asset', 'type' => 'asset',

View File

@@ -37,6 +37,7 @@ class ConsumablesTransformer
'remaining' => $consumable->numRemaining(), 'remaining' => $consumable->numRemaining(),
'order_number' => e($consumable->order_number), 'order_number' => e($consumable->order_number),
'purchase_cost' => Helper::formatCurrencyOutput($consumable->purchase_cost), 'purchase_cost' => Helper::formatCurrencyOutput($consumable->purchase_cost),
'total_cost' => Helper::formatCurrencyOutput($consumable->totalCostSum()),
'purchase_date' => Helper::getFormattedDateObject($consumable->purchase_date, 'date'), 'purchase_date' => Helper::getFormattedDateObject($consumable->purchase_date, 'date'),
'qty' => (int) $consumable->qty, 'qty' => (int) $consumable->qty,
'notes' => ($consumable->notes) ? Helper::parseEscapedMarkedownInline($consumable->notes) : null, 'notes' => ($consumable->notes) ? Helper::parseEscapedMarkedownInline($consumable->notes) : null,

View File

@@ -7,7 +7,6 @@ use App\Models\License;
use App\Models\LicenseSeat; use App\Models\LicenseSeat;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
class LicenseSeatsTransformer class LicenseSeatsTransformer
{ {
public function transformLicenseSeats(Collection $seats, $total) public function transformLicenseSeats(Collection $seats, $total)
@@ -52,6 +51,7 @@ class LicenseSeatsTransformer
'reassignable' => (bool) $seat->license->reassignable, 'reassignable' => (bool) $seat->license->reassignable,
'notes' => e($seat->notes), 'notes' => e($seat->notes),
'user_can_checkout' => (($seat->assigned_to == '') && ($seat->asset_id == '')), 'user_can_checkout' => (($seat->assigned_to == '') && ($seat->asset_id == '')),
'disabled' => $seat->unreassignable_seat || $seat->license->isInactive(),
]; ];
$permissions_array['available_actions'] = [ $permissions_array['available_actions'] = [

View File

@@ -31,13 +31,13 @@ class LicensesTransformer
'purchase_order' => ($license->purchase_order) ? e($license->purchase_order) : null, 'purchase_order' => ($license->purchase_order) ? e($license->purchase_order) : null,
'purchase_date' => Helper::getFormattedDateObject($license->purchase_date, 'date'), 'purchase_date' => Helper::getFormattedDateObject($license->purchase_date, 'date'),
'termination_date' => Helper::getFormattedDateObject($license->termination_date, 'date'), 'termination_date' => Helper::getFormattedDateObject($license->termination_date, 'date'),
'expiration_date' => Helper::getFormattedDateObject($license->expiration_date, 'date'),
'depreciation' => ($license->depreciation) ? ['id' => (int) $license->depreciation->id,'name'=> e($license->depreciation->name)] : null, 'depreciation' => ($license->depreciation) ? ['id' => (int) $license->depreciation->id,'name'=> e($license->depreciation->name)] : null,
'purchase_cost' => Helper::formatCurrencyOutput($license->purchase_cost), 'purchase_cost' => Helper::formatCurrencyOutput($license->purchase_cost),
'purchase_cost_numeric' => $license->purchase_cost, 'purchase_cost_numeric' => $license->purchase_cost,
'notes' => Helper::parseEscapedMarkedownInline($license->notes), 'notes' => Helper::parseEscapedMarkedownInline($license->notes),
'expiration_date' => Helper::getFormattedDateObject($license->expiration_date, 'date'),
'seats' => (int) $license->seats, 'seats' => (int) $license->seats,
'free_seats_count' => (int) $license->free_seats_count, 'free_seats_count' => (int) $license->free_seats_count - License::unReassignableCount($license),
'remaining' => (int) $license->free_seats_count, 'remaining' => (int) $license->free_seats_count,
'min_amt' => ($license->min_amt) ? (int) ($license->min_amt) : null, 'min_amt' => ($license->min_amt) ? (int) ($license->min_amt) : null,
'license_name' => ($license->license_name) ? e($license->license_name) : null, 'license_name' => ($license->license_name) ? e($license->license_name) : null,
@@ -54,7 +54,7 @@ class LicensesTransformer
'updated_at' => Helper::getFormattedDateObject($license->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($license->updated_at, 'datetime'),
'deleted_at' => Helper::getFormattedDateObject($license->deleted_at, 'datetime'), 'deleted_at' => Helper::getFormattedDateObject($license->deleted_at, 'datetime'),
'user_can_checkout' => (bool) ($license->free_seats_count > 0), 'user_can_checkout' => (bool) ($license->free_seats_count > 0),
'disabled' => $license->isInactive(),
]; ];
$permissions_array['available_actions'] = [ $permissions_array['available_actions'] = [

View File

@@ -53,6 +53,9 @@ class LocationsTransformer
'assets_count' => (int) $location->assets_count, 'assets_count' => (int) $location->assets_count,
'rtd_assets_count' => (int) $location->rtd_assets_count, 'rtd_assets_count' => (int) $location->rtd_assets_count,
'users_count' => (int) $location->users_count, 'users_count' => (int) $location->users_count,
'consumables_count' => (int) $location->consumables_count,
'components_count' => (int) $location->components_count,
'children_count' => (int) $location->children_count,
'currency' => ($location->currency) ? e($location->currency) : null, 'currency' => ($location->currency) ? e($location->currency) : null,
'ldap_ou' => ($location->ldap_ou) ? e($location->ldap_ou) : null, 'ldap_ou' => ($location->ldap_ou) ? e($location->ldap_ou) : null,
'notes' => Helper::parseEscapedMarkedownInline($location->notes), 'notes' => Helper::parseEscapedMarkedownInline($location->notes),
@@ -76,12 +79,13 @@ class LocationsTransformer
]; ];
$permissions_array['available_actions'] = [ $permissions_array['available_actions'] = [
'update' => Gate::allows('update', Location::class) ? true : false, 'update' => (Gate::allows('update', Location::class) && ($location->deleted_at == '')),
'delete' => $location->isDeletable(), 'delete' => $location->isDeletable(),
'bulk_selectable' => [ 'bulk_selectable' => [
'delete' => $location->isDeletable() 'delete' => $location->isDeletable()
], ],
'clone' => (Gate::allows('create', Location::class) && ($location->deleted_at == '')), 'clone' => (Gate::allows('create', Location::class) && ($location->deleted_at == '')),
'restore' => (Gate::allows('create', Location::class) && ($location->deleted_at != '')),
]; ];
$array += $permissions_array; $array += $permissions_array;

View File

@@ -66,6 +66,7 @@ class MaintenancesTransformer
'id' => $assetmaintenance->supplier->id, 'id' => $assetmaintenance->supplier->id,
'name'=> e($assetmaintenance->supplier->name) 'name'=> e($assetmaintenance->supplier->name)
] : null, ] : null,
'url' => ($assetmaintenance->url) ? e($assetmaintenance->url) : null,
'cost' => Helper::formatCurrencyOutput($assetmaintenance->cost), 'cost' => Helper::formatCurrencyOutput($assetmaintenance->cost),
'asset_maintenance_type' => e($assetmaintenance->asset_maintenance_type), 'asset_maintenance_type' => e($assetmaintenance->asset_maintenance_type),
'start_date' => Helper::getFormattedDateObject($assetmaintenance->start_date, 'date'), 'start_date' => Helper::getFormattedDateObject($assetmaintenance->start_date, 'date'),

View File

@@ -4,7 +4,6 @@ namespace App\Http\Transformers;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
class ProfileTransformer class ProfileTransformer
@@ -26,7 +25,7 @@ class ProfileTransformer
'id' => (int) $file->id, 'id' => (int) $file->id,
'icon' => Helper::filetype_icon($file->filename), 'icon' => Helper::filetype_icon($file->filename),
'item' => ($file->item) ? [ 'item' => ($file->item) ? [
'name' => ($file->itemType()=='user') ? e($file->item->display_name) : e($file->item->getDisplayNameAttribute()), 'name' => $file->item->display_name ? e($file->item->display_name) : null,
'type' => e($file->itemType()), 'type' => e($file->itemType()),
] : null, ] : null,
'filename' => e($file->filename), 'filename' => e($file->filename),

View File

@@ -22,7 +22,7 @@ class UsersTransformer
public function transformUser(User $user) public function transformUser(User $user)
{ {
$role = ''; $role = null;
if ($user->isSuperUser()) { if ($user->isSuperUser()) {
$role = 'superadmin'; $role = 'superadmin';
} elseif ($user->isAdmin()) { } elseif ($user->isAdmin()) {
@@ -34,7 +34,7 @@ class UsersTransformer
'name' => e($user->getFullNameAttribute()) ?? null, 'name' => e($user->getFullNameAttribute()) ?? null,
'first_name' => e($user->first_name) ?? null, 'first_name' => e($user->first_name) ?? null,
'last_name' => e($user->last_name) ?? null, 'last_name' => e($user->last_name) ?? null,
'display_name' => e($user->getRawOriginal('display_name')) ?? null, 'display_name' => ($user->getRawOriginal('display_name')) ? e($user->getRawOriginal('display_name')) : null,
'username' => e($user->username) ?? null, 'username' => e($user->username) ?? null,
'remote' => ($user->remote == '1') ? true : false, 'remote' => ($user->remote == '1') ? true : false,
'locale' => ($user->locale) ? e($user->locale) : null, 'locale' => ($user->locale) ? e($user->locale) : null,

View File

@@ -44,7 +44,7 @@ class AssetImporter extends ItemImporter
foreach ($this->customFields as $customField) { foreach ($this->customFields as $customField) {
$customFieldValue = $this->array_smart_custom_field_fetch($row, $customField); $customFieldValue = $this->array_smart_custom_field_fetch($row, $customField);
if ($customFieldValue) { if (!is_null($customFieldValue)) {
if ($customField->field_encrypted == 1) { if ($customField->field_encrypted == 1) {
$this->item['custom_fields'][$customField->db_column_name()] = Crypt::encrypt($customFieldValue); $this->item['custom_fields'][$customField->db_column_name()] = Crypt::encrypt($customFieldValue);
$this->log('Custom Field '.$customField->name.': '.Crypt::encrypt($customFieldValue)); $this->log('Custom Field '.$customField->name.': '.Crypt::encrypt($customFieldValue));

View File

@@ -40,11 +40,32 @@ class AssetModelImporter extends ItemImporter
{ {
$editingAssetModel = false; $editingAssetModel = false;
$assetModel = AssetModel::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
/**
* This part gets a little confusing, since folks might be importing multiple models with the same name and different model numbers for the first time
* or they might be wanting to update existing models with new model numbers.
*/
// They are not trying to update existing models, so we'll check for duplicates with model name *and* number
if (! $this->updating) {
$this->log('Finding model by name and model number: '.$this->findCsvMatch($row, 'name').' / '.$this->findCsvMatch($row, 'model_number'));
$assetModel = AssetModel::where('name', '=', $this->findCsvMatch($row, 'name'))->where('model_number', '=', $this->findCsvMatch($row, 'model_number'))->first();
} else {
if ($this->findCsvMatch($row, 'id')!='') {
// Override model if an ID was given
$this->log('Finding model by ID: '.$this->findCsvMatch($row, 'id'));
$assetModel = AssetModel::find($this->findCsvMatch($row, 'id'));
} else {
$this->log('Finding model by name: '.$this->findCsvMatch($row, 'name'));
$assetModel = AssetModel::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
}
}
if ($assetModel) { if ($assetModel) {
if (! $this->updating) { if (! $this->updating) {
$this->log('A matching Model '.$this->item['name'].' already exists'); $this->log('A matching Model '.$this->item['name'].' already exists and we are not updating. Skipping.');
return; return;
} }
@@ -66,6 +87,7 @@ class AssetModelImporter extends ItemImporter
$this->item['fieldset'] = trim($this->findCsvMatch($row, 'fieldset')); $this->item['fieldset'] = trim($this->findCsvMatch($row, 'fieldset'));
$this->item['depreciation'] = trim($this->findCsvMatch($row, 'depreciation')); $this->item['depreciation'] = trim($this->findCsvMatch($row, 'depreciation'));
$this->item['requestable'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'requestable'))) == 1) ? 1 : 0; $this->item['requestable'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'requestable'))) == 1) ? 1 : 0;
$this->item['require_serial'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'require_serial'))) == 1) ? 1 : 0;
if (!empty($this->item['category'])) { if (!empty($this->item['category'])) {
if ($category = $this->createOrFetchCategory($this->item['category'])) { if ($category = $this->createOrFetchCategory($this->item['category'])) {

View File

@@ -27,7 +27,7 @@ class ManufacturerImporter extends ItemImporter
} }
/** /**
* Create a supplier if a duplicate does not exist. * Create a manufacturer if a duplicate does not exist.
* @todo Investigate how this should interact with Importer::createManufacturerIfNotExists * @todo Investigate how this should interact with Importer::createManufacturerIfNotExists
* *
* @author A. Gianotto * @author A. Gianotto
@@ -39,16 +39,16 @@ class ManufacturerImporter extends ItemImporter
$editingManufacturer = false; $editingManufacturer = false;
$supplier = Manufacturer::where('name', '=', $this->findCsvMatch($row, 'name'))->first(); $manufacturer = Manufacturer::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
if ($this->findCsvMatch($row, 'id')!='') { if ($this->findCsvMatch($row, 'id')!='') {
// Override supplier if an ID was given // Override manufacturer if an ID was given
\Log::debug('Finding supplier by ID: '.$this->findCsvMatch($row, 'id')); \Log::debug('Finding manufacturer by ID: '.$this->findCsvMatch($row, 'id'));
$supplier = Manufacturer::find($this->findCsvMatch($row, 'id')); $manufacturer = Manufacturer::find($this->findCsvMatch($row, 'id'));
} }
if ($supplier) { if ($manufacturer) {
if (! $this->updating) { if (! $this->updating) {
$this->log('A matching Manufacturer '.$this->item['name'].' already exists'); $this->log('A matching Manufacturer '.$this->item['name'].' already exists');
return; return;
@@ -58,8 +58,8 @@ class ManufacturerImporter extends ItemImporter
$editingManufacturer = true; $editingManufacturer = true;
} else { } else {
$this->log('No Matching Manufacturer, Create a new one'); $this->log('No Matching Manufacturer, Create a new one');
$supplier = new Manufacturer; $manufacturer = new Manufacturer;
$supplier->created_by = auth()->id(); $manufacturer->created_by = auth()->id();
} }
// Pull the records from the CSV to determine their values // Pull the records from the CSV to determine their values
@@ -79,21 +79,21 @@ class ManufacturerImporter extends ItemImporter
if ($editingManufacturer) { if ($editingManufacturer) {
Log::debug('Updating existing supplier'); Log::debug('Updating existing manufacturer');
$supplier->update($this->sanitizeItemForUpdating($supplier)); $manufacturer->update($this->sanitizeItemForUpdating($manufacturer));
} else { } else {
Log::debug('Creating supplier'); Log::debug('Creating manufacturer');
$supplier->fill($this->sanitizeItemForStoring($supplier)); $manufacturer->fill($this->sanitizeItemForStoring($manufacturer));
} }
if ($supplier->save()) { if ($manufacturer->save()) {
$this->log('Manufacturer '.$supplier->name.' created or updated from CSV import'); $this->log('Manufacturer '.$manufacturer->name.' created or updated from CSV import');
return $supplier; return $manufacturer;
} else { } else {
Log::debug($supplier->getErrors()); Log::debug($manufacturer->getErrors());
$this->logError($supplier, 'Manufacturer "'.$this->item['name'].'"'); $this->logError($manufacturer, 'Manufacturer "'.$this->item['name'].'"');
return $supplier->errors; return $manufacturer->errors;
} }

View File

@@ -96,7 +96,8 @@ class CheckoutableListener
if (!empty($to)) { if (!empty($to)) {
try { try {
Mail::to(array_flatten($to))->cc(array_flatten($cc))->send($mailable); $toMail = (clone $mailable)->locale($notifiable->locale);
Mail::to(array_flatten($to))->send($toMail);
Log::info('Checkout Mail sent to checkout target'); Log::info('Checkout Mail sent to checkout target');
} catch (ClientException $e) { } catch (ClientException $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage()); Log::debug("Exception caught during checkout email: " . $e->getMessage());
@@ -104,6 +105,16 @@ class CheckoutableListener
Log::debug("Exception caught during checkout email: " . $e->getMessage()); Log::debug("Exception caught during checkout email: " . $e->getMessage());
} }
} }
if (!empty($cc)) {
try {
$ccMail = (clone $mailable)->locale(Setting::getSettings()->locale);
Mail::to(array_flatten($cc))->send($ccMail);
} catch (ClientException $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage());
}
}
} }
if ($shouldSendWebhookNotification) { if ($shouldSendWebhookNotification) {
@@ -178,15 +189,26 @@ class CheckoutableListener
[$to, $cc] = $this->generateEmailRecipients($shouldSendEmailToUser, $shouldSendEmailToAlertAddress, $notifiable); [$to, $cc] = $this->generateEmailRecipients($shouldSendEmailToUser, $shouldSendEmailToAlertAddress, $notifiable);
try { if (!empty($to)) {
if (!empty($to)) { try {
Mail::to(array_flatten($to))->cc(array_flatten($cc))->send($mailable); $toMail = (clone $mailable)->locale($notifiable->locale);
Log::info('Checkin Mail sent to CC addresses'); Mail::to(array_flatten($to))->send($toMail);
Log::info('Checkin Mail sent to checkin target');
} catch (ClientException $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
}
}
if (!empty($cc)) {
try {
$ccMail = (clone $mailable)->locale(Setting::getSettings()->locale);
Mail::to(array_flatten($cc))->send($ccMail);
} catch (ClientException $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
} }
} catch (ClientException $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
} catch (Exception $e) {
Log::debug("Exception caught during checkin email: " . $e->getMessage());
} }
} }
@@ -240,6 +262,12 @@ class CheckoutableListener
$acceptance->checkoutable()->associate($event->checkoutable); $acceptance->checkoutable()->associate($event->checkoutable);
$acceptance->assignedTo()->associate($event->checkedOutTo); $acceptance->assignedTo()->associate($event->checkedOutTo);
$acceptance->qty = 1;
if (isset($event->checkoutable->checkout_qty)) {
$acceptance->qty = $event->checkoutable->checkout_qty;
}
$category = $this->getCategoryFromCheckoutable($event->checkoutable); $category = $this->getCategoryFromCheckoutable($event->checkoutable);
if ($category?->alert_on_response) { if ($category?->alert_on_response) {

View File

@@ -403,6 +403,7 @@ class Importer extends Component
$this->assetmodels_fields = [ $this->assetmodels_fields = [
'id' => trans('general.id'),
'category' => trans('general.category'), 'category' => trans('general.category'),
'eol' => trans('general.eol'), 'eol' => trans('general.eol'),
'fieldset' => trans('admin/models/general.fieldset'), 'fieldset' => trans('admin/models/general.fieldset'),
@@ -412,6 +413,7 @@ class Importer extends Component
'model_number' => trans('general.model_no'), 'model_number' => trans('general.model_no'),
'notes' => trans('general.item_notes', ['item' => trans('admin/hardware/form.model')]), 'notes' => trans('general.item_notes', ['item' => trans('admin/hardware/form.model')]),
'requestable' => trans('admin/models/general.requestable'), 'requestable' => trans('admin/models/general.requestable'),
'require_serial' => trans('admin/hardware/general.require_serial'),
]; ];
@@ -535,6 +537,10 @@ class Importer extends Component
'product key', 'product key',
'key', 'key',
], ],
'require_serial' =>
[
'serial required',
],
'model_number' => 'model_number' =>
[ [
'model', 'model',

View File

@@ -43,7 +43,7 @@ class CheckoutAccessoryMail extends Mailable
return new Envelope( return new Envelope(
from: $from, from: $from,
subject: trans('mail.Accessory_Checkout_Notification'), subject: trans_choice('mail.Accessory_Checkout_Notification', $this->checkout_qty),
); );
} }
@@ -83,17 +83,19 @@ class CheckoutAccessoryMail extends Mailable
], ],
); );
} }
private function introductionLine(): string private function introductionLine(): string
{ {
if ($this->target instanceof Location) { if ($this->target instanceof Location) {
return trans('mail.new_item_checked_location', ['location' => $this->target->name ]); return trans_choice('mail.new_item_checked_location', $this->checkout_qty, ['location' => $this->target->name]);
} }
if ($this->requiresAcceptance()) { if ($this->requiresAcceptance()) {
return trans('mail.new_item_checked_with_acceptance'); return trans_choice('mail.new_item_checked_with_acceptance', $this->checkout_qty);
} }
if (!$this->requiresAcceptance()) { if (!$this->requiresAcceptance()) {
return trans('mail.new_item_checked'); return trans_choice('mail.new_item_checked', $this->checkout_qty);
} }
// we shouldn't get here but let's send a default message just in case // we shouldn't get here but let's send a default message just in case

View File

@@ -87,7 +87,7 @@ class CheckoutAssetMail extends Mailable
$name = $this->target->assignedto?->display_name; $name = $this->target->assignedto?->display_name;
} }
else if($this->target instanceof Location){ else if($this->target instanceof Location){
$name = $this->target->manager->name; $name = $this->target->manager?->name;
} }
// Check if the item has custom fields associated with it // Check if the item has custom fields associated with it
@@ -138,14 +138,15 @@ class CheckoutAssetMail extends Mailable
private function introductionLine(): string private function introductionLine(): string
{ {
if ($this->firstTimeSending && $this->target instanceof Location) { if ($this->firstTimeSending && $this->target instanceof Location) {
return trans('mail.new_item_checked_location', ['location' => $this->target->name ]); return trans_choice('mail.new_item_checked_location', 1, ['location' => $this->target->name]);
} }
if ($this->firstTimeSending && $this->requiresAcceptance()) { if ($this->firstTimeSending && $this->requiresAcceptance()) {
return trans('mail.new_item_checked_with_acceptance'); return trans_choice('mail.new_item_checked_with_acceptance', 1);
} }
if ($this->firstTimeSending && !$this->requiresAcceptance()) { if ($this->firstTimeSending && !$this->requiresAcceptance()) {
return trans('mail.new_item_checked'); return trans_choice('mail.new_item_checked', 1);
} }
if (!$this->firstTimeSending && $this->requiresAcceptance()) { if (!$this->firstTimeSending && $this->requiresAcceptance()) {

View File

@@ -26,7 +26,7 @@ class CheckoutComponentMail extends Mailable
$this->note = $note; $this->note = $note;
$this->target = $checkedOutTo; $this->target = $checkedOutTo;
$this->acceptance = $acceptance; $this->acceptance = $acceptance;
$this->qty = $component->assets->first()?->pivot?->assigned_qty; $this->qty = $component->checkout_qty;
$this->settings = Setting::getSettings(); $this->settings = Setting::getSettings();
} }

View File

@@ -17,10 +17,11 @@ class SendUpcomingAuditMail extends Mailable
/** /**
* Create a new message instance. * Create a new message instance.
*/ */
public function __construct($params, $threshold) public function __construct($params, $threshold, $total)
{ {
$this->assets = $params; $this->assets = $params;
$this->threshold = $threshold; $this->threshold = $threshold;
$this->total = $total;
} }
/** /**
@@ -32,7 +33,7 @@ class SendUpcomingAuditMail extends Mailable
return new Envelope( return new Envelope(
from: $from, from: $from,
subject: trans_choice('mail.upcoming-audits', $this->assets->count(), ['count' => $this->assets->count(), 'threshold' => $this->threshold]), subject: trans_choice('mail.upcoming-audits', $this->total, ['count' => $this->total, 'threshold' => $this->threshold]),
); );
} }
@@ -49,6 +50,7 @@ class SendUpcomingAuditMail extends Mailable
with: [ with: [
'assets' => $this->assets, 'assets' => $this->assets,
'threshold' => $this->threshold, 'threshold' => $this->threshold,
'total' => $this->total,
], ],
); );
} }

Some files were not shown because too many files have changed in this diff Show More