Compare commits
960 Commits
label-cjk-
...
#18172-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc0c95acb0 | ||
|
|
5d0807110e | ||
|
|
33e72f6183 | ||
|
|
3aa046bfa7 | ||
|
|
9454ff677b | ||
|
|
a5c7b8f609 | ||
|
|
9365d1adc6 | ||
|
|
ddd2a96ac5 | ||
|
|
bf8ff51234 | ||
|
|
7891f52bd4 | ||
|
|
baa4a8a461 | ||
|
|
34daffcdf4 | ||
|
|
21baea27a8 | ||
|
|
e1d3714445 | ||
|
|
8b7e0a0d78 | ||
|
|
a1cc427c9c | ||
|
|
5dc675040d | ||
|
|
1258eb6533 | ||
|
|
83abfc9ca6 | ||
|
|
b42d6677cc | ||
|
|
e8c644a600 | ||
|
|
9f6a73b290 | ||
|
|
4d38bd1c62 | ||
|
|
138262114d | ||
|
|
4073c9e638 | ||
|
|
f1b4877a98 | ||
|
|
c39c92d0d7 | ||
|
|
90fc48d959 | ||
|
|
7f10a53105 | ||
|
|
7ae1b7a765 | ||
|
|
22a43a0463 | ||
|
|
dea399398a | ||
|
|
7434dd9458 | ||
|
|
88e532dbc4 | ||
|
|
c5ad451c39 | ||
|
|
44bfceeb0f | ||
|
|
37eb63837b | ||
|
|
4ada47e3b0 | ||
|
|
e5c55c9ab3 | ||
|
|
547b3df7b4 | ||
|
|
4100f2600c | ||
|
|
a9574e8fd6 | ||
|
|
c6269d6bbc | ||
|
|
c1204a5301 | ||
|
|
cc5ac65909 | ||
|
|
9ddc48e3d7 | ||
|
|
4a39d7c67a | ||
|
|
b7a6706591 | ||
|
|
ddb031f091 | ||
|
|
e906d25776 | ||
|
|
27d7449459 | ||
|
|
5d8905c997 | ||
|
|
517f4ce121 | ||
|
|
6809bbd3d5 | ||
|
|
ab555d05e1 | ||
|
|
f7bc538fdf | ||
|
|
2de66ad5db | ||
|
|
30e16b6213 | ||
|
|
92b50ca7ae | ||
|
|
9ee36df979 | ||
|
|
46d1c14e1a | ||
|
|
61895011fb | ||
|
|
32d43034bd | ||
|
|
dfc6cdc127 | ||
|
|
16e93f9e18 | ||
|
|
7395b1a4eb | ||
|
|
fa98557225 | ||
|
|
894606b62e | ||
|
|
f0a6a0026a | ||
|
|
070e0c93be | ||
|
|
2b27b733e5 | ||
|
|
0355c2b642 | ||
|
|
3bad19fb56 | ||
|
|
0f84d51a48 | ||
|
|
2e8572d9c5 | ||
|
|
df53d5d966 | ||
|
|
23838959ca | ||
|
|
6dbb836a01 | ||
|
|
3426afe5a8 | ||
|
|
4bbf923eb6 | ||
|
|
e2c3480194 | ||
|
|
73159076f6 | ||
|
|
90d040573d | ||
|
|
155481a442 | ||
|
|
2f31bfc5fe | ||
|
|
8d0c88dc74 | ||
|
|
07256fd833 | ||
|
|
776cd43a58 | ||
|
|
acb5309aab | ||
|
|
c68f81db3c | ||
|
|
ac8e341b37 | ||
|
|
36122b3966 | ||
|
|
79bcf472f0 | ||
|
|
55d86da846 | ||
|
|
e4f8c1ba3f | ||
|
|
c36236b7dc | ||
|
|
63994333d0 | ||
|
|
da4c7d8934 | ||
|
|
186721eca0 | ||
|
|
f53d939c86 | ||
|
|
23e6909708 | ||
|
|
cf421fe1c1 | ||
|
|
4d80e806e4 | ||
|
|
60a7b7f7ff | ||
|
|
90263eab06 | ||
|
|
9d8f251fc4 | ||
|
|
2b4986571c | ||
|
|
890d13bd52 | ||
|
|
e698e71137 | ||
|
|
d064a5530a | ||
|
|
ab4fbf6c19 | ||
|
|
728afa8361 | ||
|
|
b77019c16e | ||
|
|
6703448b80 | ||
|
|
776ba19a1f | ||
|
|
1f499e0d44 | ||
|
|
0a6eb61103 | ||
|
|
32a2eed5ec | ||
|
|
40a70d39d0 | ||
|
|
5697054e98 | ||
|
|
def5969e1c | ||
|
|
78a418630d | ||
|
|
6d76e7b2d4 | ||
|
|
f052c8b44c | ||
|
|
0e6991d56d | ||
|
|
06eab5f8a4 | ||
|
|
4a481e79c4 | ||
|
|
ee4abbcbaa | ||
|
|
dcc82d742f | ||
|
|
19cb2089d7 | ||
|
|
04923b06b0 | ||
|
|
e16755d491 | ||
|
|
742b0769a4 | ||
|
|
df68dca9dc | ||
|
|
4a5bf78d58 | ||
|
|
7947237489 | ||
|
|
1115205164 | ||
|
|
d5d01136c4 | ||
|
|
3d47277614 | ||
|
|
b937bea04f | ||
|
|
fff14632bc | ||
|
|
9bdf1a620f | ||
|
|
60099aa989 | ||
|
|
f3976e5dd8 | ||
|
|
d7d6893304 | ||
|
|
99549ce805 | ||
|
|
7eb15fe04d | ||
|
|
e2019a13ab | ||
|
|
4b7a06761a | ||
|
|
b96e2fb52c | ||
|
|
8f4a1f5801 | ||
|
|
891bec9cdb | ||
|
|
c5252ea583 | ||
|
|
82d553c180 | ||
|
|
71e34355b9 | ||
|
|
2bad8c72e4 | ||
|
|
6134ca01ac | ||
|
|
be8193ebff | ||
|
|
430ee46645 | ||
|
|
76fbbf29e8 | ||
|
|
3108159d95 | ||
|
|
f282a1ead7 | ||
|
|
324bc4957d | ||
|
|
c3b5c4dfae | ||
|
|
4ab5d97e86 | ||
|
|
b4696ef11e | ||
|
|
294ffb72a4 | ||
|
|
8c534d29d3 | ||
|
|
d6cb262f9d | ||
|
|
5211e2ae20 | ||
|
|
90832fd1ad | ||
|
|
889cbc69e1 | ||
|
|
15948370d4 | ||
|
|
63c5177b37 | ||
|
|
4fbfaf6b9f | ||
|
|
9e68497b63 | ||
|
|
0b9e13bf1e | ||
|
|
485d343e0f | ||
|
|
07f0e8a3be | ||
|
|
cc5afb1cd8 | ||
|
|
c69f1c0890 | ||
|
|
a8733bdedf | ||
|
|
4222e4eb51 | ||
|
|
5db4441f5c | ||
|
|
2bcfe97211 | ||
|
|
4e74c97c84 | ||
|
|
71a46c9bd6 | ||
|
|
1a7c7fdebf | ||
|
|
65ff3d414a | ||
|
|
9fa38b70c8 | ||
|
|
470172f53f | ||
|
|
81261d9e36 | ||
|
|
3ab2e20119 | ||
|
|
b773d576ea | ||
|
|
23feb64b5a | ||
|
|
87fe9d9d3d | ||
|
|
b1ef3f51cb | ||
|
|
e1b6488f8e | ||
|
|
a472dede2b | ||
|
|
263cc3f7a1 | ||
|
|
598612d4bf | ||
|
|
a07d83e583 | ||
|
|
d355812433 | ||
|
|
1afc14f5ab | ||
|
|
8947b667ae | ||
|
|
6b41796d44 | ||
|
|
6efe8eb55b | ||
|
|
4c8f8918e8 | ||
|
|
4cb5bb1855 | ||
|
|
eb007e025a | ||
|
|
18aefa9dee | ||
|
|
2ec540bd36 | ||
|
|
1374ec408a | ||
|
|
8f8fed2b79 | ||
|
|
a1e6f01fe9 | ||
|
|
d7496f22e5 | ||
|
|
7de25a1c37 | ||
|
|
78da89340c | ||
|
|
60606115fe | ||
|
|
57b49fc31c | ||
|
|
6e90c8f6e6 | ||
|
|
a7ff2595a5 | ||
|
|
1edea6abef | ||
|
|
a3bd58bda6 | ||
|
|
3339d1999f | ||
|
|
df3e8ec0f3 | ||
|
|
2dbec867d9 | ||
|
|
3fc651d659 | ||
|
|
b91d23023d | ||
|
|
cc234c60b8 | ||
|
|
f1883c8004 | ||
|
|
79f6ddc8ee | ||
|
|
89f439a18d | ||
|
|
79eb5bfad9 | ||
|
|
3a36b7dafd | ||
|
|
1fe2fd9891 | ||
|
|
5fdb999ece | ||
|
|
5a38d9c2b6 | ||
|
|
b1c7dc6cbb | ||
|
|
b21b2ac41c | ||
|
|
1396c597ef | ||
|
|
46af72ee4e | ||
|
|
e3e8f553c8 | ||
|
|
549928d3d1 | ||
|
|
abefec9628 | ||
|
|
b483eeded4 | ||
|
|
fab85dafa8 | ||
|
|
955faed919 | ||
|
|
3ff516180d | ||
|
|
0c8ca6d6b0 | ||
|
|
a5de077e04 | ||
|
|
c0a99d6b52 | ||
|
|
a1e65cd897 | ||
|
|
5d65f1ffc5 | ||
|
|
b7193a06fd | ||
|
|
cba090f8eb | ||
|
|
0d6baa1081 | ||
|
|
bc60d796a3 | ||
|
|
85a208526b | ||
|
|
c3ac0a750d | ||
|
|
0f111127a5 | ||
|
|
cd266a6bef | ||
|
|
0f4945621c | ||
|
|
9a477f227b | ||
|
|
3c81257325 | ||
|
|
218fe9ebdc | ||
|
|
24bb45ab97 | ||
|
|
85f39de540 | ||
|
|
5cc3277e2d | ||
|
|
d5ef7f3204 | ||
|
|
cb47d01f51 | ||
|
|
ee499c1385 | ||
|
|
4ae4af5c10 | ||
|
|
b0f5fe7e25 | ||
|
|
fd32585efc | ||
|
|
c675bb7252 | ||
|
|
e65d11d71e | ||
|
|
c2680334f1 | ||
|
|
1217a02ec1 | ||
|
|
143a091f45 | ||
|
|
49ecc93dbe | ||
|
|
1735fb6bed | ||
|
|
8fefc11b4d | ||
|
|
1a3b22171c | ||
|
|
5e773be260 | ||
|
|
c317a1dc8b | ||
|
|
9401ffc83c | ||
|
|
479a852446 | ||
|
|
9188fb03f0 | ||
|
|
36bb91cbc3 | ||
|
|
56e5ab8bc6 | ||
|
|
607eb6ca03 | ||
|
|
ad745cc84b | ||
|
|
5a13d5ea6f | ||
|
|
0b065eb7fe | ||
|
|
e2c0e4bc66 | ||
|
|
655b0e6778 | ||
|
|
cd785c9fc3 | ||
|
|
23885f5166 | ||
|
|
44ef39e419 | ||
|
|
e05a0ef565 | ||
|
|
0ea9c0647f | ||
|
|
00b9ba2b75 | ||
|
|
f1e3bc9531 | ||
|
|
7360e093b2 | ||
|
|
5f639bc24f | ||
|
|
884d2a9552 | ||
|
|
fd8a8b29b1 | ||
|
|
4e1ef40c05 | ||
|
|
00af0ddff5 | ||
|
|
2467e823a5 | ||
|
|
224642fcb5 | ||
|
|
59518ca2c5 | ||
|
|
dfb7c73069 | ||
|
|
70eccceab3 | ||
|
|
3f69b70367 | ||
|
|
94a0a2f8be | ||
|
|
7e23596ab8 | ||
|
|
3c58a5dd3d | ||
|
|
51789ccbf3 | ||
|
|
6ae4a9aa1a | ||
|
|
0aebd669b2 | ||
|
|
2a92c4899d | ||
|
|
530089895a | ||
|
|
ae8289fc8c | ||
|
|
90f4dfb48b | ||
|
|
ec2eddf538 | ||
|
|
44d31d4b39 | ||
|
|
4934e7666c | ||
|
|
35bf0d020e | ||
|
|
4934dc85ac | ||
|
|
5ceb50d7e5 | ||
|
|
ae7ccbb7bd | ||
|
|
1cd9fc47aa | ||
|
|
013c50607a | ||
|
|
d7bf9b7f2e | ||
|
|
4702fdddc6 | ||
|
|
dd06a530c0 | ||
|
|
c36125dc95 | ||
|
|
ae43f93d0a | ||
|
|
8918b17f77 | ||
|
|
dfd05e8b5b | ||
|
|
3daa6dd051 | ||
|
|
14e43192e6 | ||
|
|
6cf88b1792 | ||
|
|
6b9839367f | ||
|
|
34fcf5d616 | ||
|
|
1cf3c74e67 | ||
|
|
16b57b931e | ||
|
|
3457e7d617 | ||
|
|
edbe8001e6 | ||
|
|
71644c1cbe | ||
|
|
03fd8df8bd | ||
|
|
71d622b6dd | ||
|
|
689d5a2d58 | ||
|
|
b2e9eb866c | ||
|
|
c8b7782d1d | ||
|
|
673f936689 | ||
|
|
2ca0d39e51 | ||
|
|
908c8bc397 | ||
|
|
93082e1e87 | ||
|
|
ef0a6aa25e | ||
|
|
b9f4dc1e9d | ||
|
|
90afec864e | ||
|
|
b082fb6692 | ||
|
|
4bbbd786cd | ||
|
|
0c1b2a54e7 | ||
|
|
a6ded20ede | ||
|
|
9b96314371 | ||
|
|
85e16ecd51 | ||
|
|
2ac36cdfd6 | ||
|
|
e40c532354 | ||
|
|
9404dff79c | ||
|
|
55b324d8d6 | ||
|
|
3ed2e2d79e | ||
|
|
3152d9eadd | ||
|
|
f1266ab5d6 | ||
|
|
e70f1408aa | ||
|
|
664e3984e3 | ||
|
|
665c13e238 | ||
|
|
8a667b20c2 | ||
|
|
3693241292 | ||
|
|
a384245368 | ||
|
|
3c3acff79b | ||
|
|
e15de83a95 | ||
|
|
636fccbf97 | ||
|
|
7d8ed399a8 | ||
|
|
272385db6c | ||
|
|
9334b8df47 | ||
|
|
e0bc2ae86f | ||
|
|
2f019bb033 | ||
|
|
291be64aa0 | ||
|
|
75c83236ff | ||
|
|
72be171917 | ||
|
|
4ddee4ac40 | ||
|
|
43cd0d7eb3 | ||
|
|
6b693e2644 | ||
|
|
eeea69d8f2 | ||
|
|
72cf921a4b | ||
|
|
32882f81e7 | ||
|
|
36f5099932 | ||
|
|
bec88a0441 | ||
|
|
6e67e3a8a0 | ||
|
|
4dc3c30354 | ||
|
|
947ccf911d | ||
|
|
06f313febe | ||
|
|
b387136b8f | ||
|
|
31614c5da1 | ||
|
|
146b5a3085 | ||
|
|
397cc1754a | ||
|
|
ff1297cac5 | ||
|
|
8af3cf4056 | ||
|
|
9edec9e212 | ||
|
|
ca44ee94a4 | ||
|
|
3ae7a77032 | ||
|
|
be4362c59a | ||
|
|
8461b147de | ||
|
|
881c789a75 | ||
|
|
fea0189479 | ||
|
|
82bdd43168 | ||
|
|
533d82d4d8 | ||
|
|
6f990dd1de | ||
|
|
be848598e3 | ||
|
|
6ca0e19819 | ||
|
|
bf6964ee62 | ||
|
|
9ac2ea2a52 | ||
|
|
62a58fa23b | ||
|
|
7d0742054f | ||
|
|
dcf7e83507 | ||
|
|
407c2bf0c8 | ||
|
|
c46227ee94 | ||
|
|
0ce20c1edd | ||
|
|
d31d99b40f | ||
|
|
3527c357cc | ||
|
|
2b5254e68f | ||
|
|
6f3323c195 | ||
|
|
5af85bfe7d | ||
|
|
20adad3c6b | ||
|
|
ca8eae4064 | ||
|
|
7077faaf4a | ||
|
|
d8171eb056 | ||
|
|
c67ca500db | ||
|
|
0081e7b731 | ||
|
|
4e6483d3ed | ||
|
|
0ddf0002c4 | ||
|
|
ad0daf33b9 | ||
|
|
c614c44d4c | ||
|
|
ab30df10ff | ||
|
|
a6cb75c481 | ||
|
|
8a46579588 | ||
|
|
fb9fb9c097 | ||
|
|
d9399534ce | ||
|
|
17a749bbed | ||
|
|
0b60c6a939 | ||
|
|
25ce63f00b | ||
|
|
2462bc05b3 | ||
|
|
c3748da0b1 | ||
|
|
90c242a441 | ||
|
|
52239a88b5 | ||
|
|
7a3596c86d | ||
|
|
ac8a9e38f0 | ||
|
|
5c08f3a27e | ||
|
|
2dc11a84bf | ||
|
|
2960ea15f5 | ||
|
|
17aab4c490 | ||
|
|
59d0f0d292 | ||
|
|
27d13a113a | ||
|
|
c58e999fbb | ||
|
|
a02a96d5c4 | ||
|
|
47e9e4704d | ||
|
|
b2ad9d404e | ||
|
|
51ce570eb3 | ||
|
|
925d48640d | ||
|
|
5216dd75bf | ||
|
|
028b4e7b79 | ||
|
|
b8b45d2d81 | ||
|
|
4b2b2cb68e | ||
|
|
625a46a2c2 | ||
|
|
be4ace293e | ||
|
|
ebc1e27c22 | ||
|
|
764b363bbc | ||
|
|
705474dc14 | ||
|
|
e639d7726b | ||
|
|
357e85d358 | ||
|
|
9da9166442 | ||
|
|
8ea339f0ef | ||
|
|
e29b0aa6a4 | ||
|
|
d2157868f2 | ||
|
|
89b36ba63f | ||
|
|
1d3dfa1fa4 | ||
|
|
89cfafd933 | ||
|
|
ca567eec8a | ||
|
|
75cfcb83aa | ||
|
|
41da31c379 | ||
|
|
e81f63f46b | ||
|
|
ade03e4827 | ||
|
|
63a4d1ad33 | ||
|
|
33a4c88c3a | ||
|
|
69c5dbfc23 | ||
|
|
dcbb09bbd7 | ||
|
|
58eac619ea | ||
|
|
cf1bccfd65 | ||
|
|
99acf018f1 | ||
|
|
f04d6f37e5 | ||
|
|
1f79776b8f | ||
|
|
11e5f851f0 | ||
|
|
4ca1db8a1b | ||
|
|
14b829aa30 | ||
|
|
384652b3df | ||
|
|
469069b471 | ||
|
|
9db65c6ae9 | ||
|
|
1346e33e99 | ||
|
|
ab9cc447aa | ||
|
|
cb63c12d2f | ||
|
|
fe9e0444b4 | ||
|
|
6ce0fd20ce | ||
|
|
a18957dbe9 | ||
|
|
13d5b724ee | ||
|
|
9bca5912d9 | ||
|
|
23756ba1c7 | ||
|
|
06f060161d | ||
|
|
73e0628124 | ||
|
|
7393c4170b | ||
|
|
73e185bf9d | ||
|
|
77153c3e78 | ||
|
|
07227887f6 | ||
|
|
13d3b103f1 | ||
|
|
4bdfd0e115 | ||
|
|
786b20708e | ||
|
|
0e957cad84 | ||
|
|
85c728f313 | ||
|
|
50e210b2db | ||
|
|
ba3fb8cd66 | ||
|
|
d9fb7dc754 | ||
|
|
2249dad9d7 | ||
|
|
5c4fa630ae | ||
|
|
ede74ad24e | ||
|
|
27542a8f91 | ||
|
|
5dc07b94aa | ||
|
|
d7acf721ae | ||
|
|
eff5232828 | ||
|
|
3eb29b1cdb | ||
|
|
c9961f63b4 | ||
|
|
b1de98f05d | ||
|
|
09e843a800 | ||
|
|
77b79dbd95 | ||
|
|
dafc6c5136 | ||
|
|
c790147a5c | ||
|
|
80c39c5ef3 | ||
|
|
9a3e84d84c | ||
|
|
643960c829 | ||
|
|
1737018325 | ||
|
|
484d5ba76e | ||
|
|
798685d0b8 | ||
|
|
cb7654ae90 | ||
|
|
00c394345a | ||
|
|
dffcb62fa1 | ||
|
|
8c668b72b7 | ||
|
|
b54ecd4da0 | ||
|
|
87a7e3501b | ||
|
|
daefec3013 | ||
|
|
183a9742c4 | ||
|
|
04b83f8176 | ||
|
|
335ab3f064 | ||
|
|
7dd493da35 | ||
|
|
560bd6da92 | ||
|
|
a5824ccc5f | ||
|
|
830a7964a4 | ||
|
|
b1359c3277 | ||
|
|
0ba8f5cc5a | ||
|
|
6fb9e2c38e | ||
|
|
eebc2ab8be | ||
|
|
b65b3151ee | ||
|
|
1d24b7985b | ||
|
|
526bb2c650 | ||
|
|
c450c0ddb8 | ||
|
|
13a0f49f5f | ||
|
|
199eefafa1 | ||
|
|
c5b58f9ecc | ||
|
|
6b68fe4de6 | ||
|
|
3461bbfdb3 | ||
|
|
51f6927076 | ||
|
|
1d88cf443f | ||
|
|
7b6c0c3a40 | ||
|
|
fdb0651bf4 | ||
|
|
c39d484611 | ||
|
|
c42996429f | ||
|
|
a091baf5a6 | ||
|
|
00a17cd55e | ||
|
|
643d44af22 | ||
|
|
b934f43db0 | ||
|
|
f39afe5a65 | ||
|
|
7612ee6b08 | ||
|
|
2ed2b0101a | ||
|
|
5ca9d31964 | ||
|
|
2fcd8cd261 | ||
|
|
0ffa47a2c6 | ||
|
|
e203d4dee3 | ||
|
|
b47d773e13 | ||
|
|
a8d0a4a95d | ||
|
|
3fb0804cef | ||
|
|
6811ebcd52 | ||
|
|
4fe7bfb851 | ||
|
|
fb60985d03 | ||
|
|
8f575923cf | ||
|
|
0ecfd02649 | ||
|
|
420aaf4f61 | ||
|
|
0c35f213e1 | ||
|
|
f68813af13 | ||
|
|
37a90d0ce9 | ||
|
|
02f1291e8f | ||
|
|
92e4f6b5d9 | ||
|
|
7b7738fbcc | ||
|
|
31197604a3 | ||
|
|
e33b1b6c90 | ||
|
|
30520297e8 | ||
|
|
78ca1d1335 | ||
|
|
6159ee8c2c | ||
|
|
5cd5392958 | ||
|
|
0dcdfc5d14 | ||
|
|
d0e068f1c0 | ||
|
|
f42a2d7457 | ||
|
|
d29619b67c | ||
|
|
f5235cb835 | ||
|
|
ee830e0cb4 | ||
|
|
0cd3be003d | ||
|
|
c93e35ec77 | ||
|
|
9538a76232 | ||
|
|
05876bb124 | ||
|
|
8bcd5a6d2a | ||
|
|
a36afbcb25 | ||
|
|
ebd8d085cf | ||
|
|
505148b024 | ||
|
|
3eefeec4ce | ||
|
|
b61419c1ce | ||
|
|
f590fcffbc | ||
|
|
e87e924ac2 | ||
|
|
90f261bab6 | ||
|
|
f7dfb09a4d | ||
|
|
3135917127 | ||
|
|
52afa3d36d | ||
|
|
242aa60e04 | ||
|
|
7a3c2c27ff | ||
|
|
5d124360c2 | ||
|
|
365d7448d5 | ||
|
|
9a0102c723 | ||
|
|
2f77f2cb2b | ||
|
|
528e3a2106 | ||
|
|
032a664d4c | ||
|
|
aac1864c9b | ||
|
|
e3477f3306 | ||
|
|
6620a4f87b | ||
|
|
c0e9dff5bf | ||
|
|
2d961c435a | ||
|
|
7c95f03166 | ||
|
|
31e5c13b50 | ||
|
|
4a9fe4f981 | ||
|
|
4fcc5587ee | ||
|
|
6ca49a20ce | ||
|
|
28f293fdc1 | ||
|
|
b3e7619adc | ||
|
|
6e56d56137 | ||
|
|
2528f6a07b | ||
|
|
423d07c919 | ||
|
|
cc608de4bf | ||
|
|
f999a68608 | ||
|
|
db78a9f18f | ||
|
|
816039f48e | ||
|
|
ae240bae6d | ||
|
|
9e30c69e6d | ||
|
|
43c7de9049 | ||
|
|
7e51c5db81 | ||
|
|
0ee3c45e7b | ||
|
|
981e69929c | ||
|
|
1eae5d12fc | ||
|
|
8863208333 | ||
|
|
5f38a74a72 | ||
|
|
fe15dacb1f | ||
|
|
c2d44cf2f2 | ||
|
|
7f1bdb6f34 | ||
|
|
7cdfaa93ec | ||
|
|
59ccc70303 | ||
|
|
f1584b722d | ||
|
|
b0305e12d2 | ||
|
|
4d8c5a86a4 | ||
|
|
58f76b5c99 | ||
|
|
7c4ee632cf | ||
|
|
b6b0f716eb | ||
|
|
bd0e04ed15 | ||
|
|
8599981d44 | ||
|
|
6fc6e95c67 | ||
|
|
43b585bde8 | ||
|
|
710f89291f | ||
|
|
5f835aa009 | ||
|
|
d5ca543719 | ||
|
|
4c6249eb9e | ||
|
|
016900bad8 | ||
|
|
2e8ae33761 | ||
|
|
0d325060da | ||
|
|
1a6e98e18f | ||
|
|
97e34595f6 | ||
|
|
a179d5234b | ||
|
|
64c6121fdb | ||
|
|
08d8954a85 | ||
|
|
4f20955d0d | ||
|
|
3a703c8bcf | ||
|
|
ccbffa086b | ||
|
|
07ee4be840 | ||
|
|
4cc9b2d312 | ||
|
|
24dddae1d1 | ||
|
|
ad0165d085 | ||
|
|
39dc38c5d1 | ||
|
|
046ce19dbb | ||
|
|
33263f5a93 | ||
|
|
8ef8e76300 | ||
|
|
73cfdae9e7 | ||
|
|
54a3e41281 | ||
|
|
d59ba6da84 | ||
|
|
1b28b06934 | ||
|
|
ff3e69a56c | ||
|
|
6b975a5fb4 | ||
|
|
3a02b15124 | ||
|
|
5f73d81935 | ||
|
|
002b5c0f6f | ||
|
|
8086842570 | ||
|
|
51f2d5a664 | ||
|
|
db63ad1cf4 | ||
|
|
b74b76de75 | ||
|
|
a1b1498106 | ||
|
|
1158851ea7 | ||
|
|
e1ab9e959e | ||
|
|
2bbac3ae9d | ||
|
|
e889b1d5e5 | ||
|
|
233bf856f4 | ||
|
|
bca843e06c | ||
|
|
30a79a1278 | ||
|
|
0f92dee2c4 | ||
|
|
f04efede15 | ||
|
|
f0dfdf6720 | ||
|
|
e26d731382 | ||
|
|
d684d3e559 | ||
|
|
47c54cb998 | ||
|
|
592cb2b3ec | ||
|
|
f5a7871a2e | ||
|
|
ec411fa0db | ||
|
|
5da79cd5ca | ||
|
|
a850a9bb83 | ||
|
|
13c971b171 | ||
|
|
1cee7e43ed | ||
|
|
5e81c63d6e | ||
|
|
479b7a3f94 | ||
|
|
f7cfee77c9 | ||
|
|
65a8126a13 | ||
|
|
a39bc102d5 | ||
|
|
81d930c4d2 | ||
|
|
6839623061 | ||
|
|
7de2809d42 | ||
|
|
98ec6b6886 | ||
|
|
04827f00cc | ||
|
|
660bfc6578 | ||
|
|
1152cd5537 | ||
|
|
17456482d6 | ||
|
|
a0431e1912 | ||
|
|
f30e8497b2 | ||
|
|
06495bc45d | ||
|
|
26067916b3 | ||
|
|
c36ee4852b | ||
|
|
2cb992ad44 | ||
|
|
ee5aac8008 | ||
|
|
083b7be6c0 | ||
|
|
e24854558f | ||
|
|
e4314cf426 | ||
|
|
4106e4e45c | ||
|
|
05f143db2b | ||
|
|
64aeaeeeea | ||
|
|
61db37ab0d | ||
|
|
f9c4d921e7 | ||
|
|
ca099df573 | ||
|
|
28b584b8bc | ||
|
|
70449e694d | ||
|
|
8395ea552d | ||
|
|
dc66452633 | ||
|
|
53ce44ac91 | ||
|
|
c7c3243bbc | ||
|
|
8bdd77d33d | ||
|
|
acd7d0db3a | ||
|
|
2bfadb8a3c | ||
|
|
0912e4af7b | ||
|
|
5aa5c48018 | ||
|
|
8cdd998f79 | ||
|
|
050d4d6b25 | ||
|
|
366cd11238 | ||
|
|
58d6443331 | ||
|
|
101b8afb56 | ||
|
|
5df5c47945 | ||
|
|
a04740ba86 | ||
|
|
425ad93ac5 | ||
|
|
1b397cd780 | ||
|
|
120316bae0 | ||
|
|
7571ff007f | ||
|
|
7afd7da2b4 | ||
|
|
8a44144c20 | ||
|
|
ee82c70582 | ||
|
|
c87e8e606b | ||
|
|
37a50dd953 | ||
|
|
a2669a3084 | ||
|
|
77da22f4dd | ||
|
|
7830ffe202 | ||
|
|
1c9e20d59f | ||
|
|
320edac286 | ||
|
|
d49878371d | ||
|
|
d2575a5d9b | ||
|
|
ea6cf72580 | ||
|
|
2118155b37 | ||
|
|
ba4f5bb71f | ||
|
|
d5a74a5a8b | ||
|
|
23be1df360 | ||
|
|
b5c1a1da4c | ||
|
|
c11e784f51 | ||
|
|
06f51c8f9c | ||
|
|
181bcbbda6 | ||
|
|
d008ead6a4 | ||
|
|
75924be958 | ||
|
|
b1a6e3f8a2 | ||
|
|
06712a6041 | ||
|
|
62b16339a9 | ||
|
|
9a2f1a36ba | ||
|
|
95cc4d3a73 | ||
|
|
497eeeb2e0 | ||
|
|
4be21ca249 | ||
|
|
e8598e214e | ||
|
|
54b1d65e3c | ||
|
|
f7648496d3 | ||
|
|
59a57c7197 | ||
|
|
5659b26827 | ||
|
|
ee4443aaf0 | ||
|
|
839dcad358 | ||
|
|
d67933ab49 | ||
|
|
0eb3f6b952 | ||
|
|
68b0f80fce | ||
|
|
93489529a3 | ||
|
|
511be74e74 | ||
|
|
bdee067803 | ||
|
|
32156cace3 | ||
|
|
30688114be | ||
|
|
34088bcc17 | ||
|
|
07835766cc | ||
|
|
251851ec6a | ||
|
|
049a669186 | ||
|
|
d29f13bae9 | ||
|
|
c758355df9 | ||
|
|
79d97a83af | ||
|
|
85bd47c240 | ||
|
|
473ead9616 | ||
|
|
cf2850933c | ||
|
|
ff2564c57a | ||
|
|
91d3848246 | ||
|
|
c031f0b45e | ||
|
|
fdbb9568ae | ||
|
|
d817883459 | ||
|
|
12255979ac | ||
|
|
366b61850b | ||
|
|
89be6bd183 | ||
|
|
e120331a2c | ||
|
|
cb8a212d96 | ||
|
|
7aec431ac5 | ||
|
|
d19681dea1 | ||
|
|
bf2299daf8 | ||
|
|
164930d0dd | ||
|
|
387dbac809 | ||
|
|
3b661e5a99 | ||
|
|
90c1c0e655 | ||
|
|
21d8e7695b | ||
|
|
1acc452cfe | ||
|
|
1375e1feee | ||
|
|
2187adf59a | ||
|
|
0dcb315d9d | ||
|
|
327ccbd0c9 | ||
|
|
f571d400e6 | ||
|
|
293aa52335 | ||
|
|
ca178ae9a7 | ||
|
|
d0c810e418 | ||
|
|
d496d2caeb | ||
|
|
e70b75c350 | ||
|
|
a50befeda5 | ||
|
|
e2616e8039 | ||
|
|
904266debe | ||
|
|
72d5783795 | ||
|
|
d699fb1473 | ||
|
|
fab1a6c33a | ||
|
|
be73c30194 | ||
|
|
451646fe4f | ||
|
|
decc919991 | ||
|
|
e0b4005921 | ||
|
|
3ef36e7534 | ||
|
|
1949e1e1e9 | ||
|
|
3358382358 | ||
|
|
3eca3ecd75 | ||
|
|
140c6b91b0 | ||
|
|
a5315ec240 | ||
|
|
93f1656e0b | ||
|
|
f1d006c236 | ||
|
|
b0b5a96694 | ||
|
|
7dbe9a85f4 | ||
|
|
d2c39528d5 | ||
|
|
0420543c94 | ||
|
|
aae0db902b | ||
|
|
88dae7cef7 | ||
|
|
e5cb17e934 | ||
|
|
9d609805f2 | ||
|
|
e2b9ca8254 | ||
|
|
e16a2fe8af | ||
|
|
22d61a533d | ||
|
|
af408bb45f | ||
|
|
24bfbc06f0 | ||
|
|
5eb9f353b5 | ||
|
|
473ce15f47 | ||
|
|
eb9cfbaed6 | ||
|
|
faeb037ff9 | ||
|
|
07602f697d | ||
|
|
11abb0fdb1 | ||
|
|
deeb2fa543 | ||
|
|
ef8d5ff11e | ||
|
|
91f3e07b83 | ||
|
|
c29bdbdacb | ||
|
|
a20d104d2f | ||
|
|
a61dd8ac17 | ||
|
|
7ee9a690ea | ||
|
|
5ba94c6c41 | ||
|
|
9fa855c837 | ||
|
|
9251007574 | ||
|
|
cc73b984cb | ||
|
|
548ef97c32 | ||
|
|
ed8a486726 | ||
|
|
1ab0911fc8 | ||
|
|
bdbaea7294 | ||
|
|
5cfd1f6fb2 | ||
|
|
5eda67381f | ||
|
|
2c8b8bfaf2 | ||
|
|
8f3159751a | ||
|
|
4b05e55b29 | ||
|
|
3d3c13fcd0 | ||
|
|
88e1d8a8cf | ||
|
|
e007db34e2 | ||
|
|
f9f06d2c02 | ||
|
|
234f7d00c8 | ||
|
|
9924553da5 | ||
|
|
df38d7e3ed | ||
|
|
44dd061619 | ||
|
|
7603a932b1 | ||
|
|
138e7acc13 | ||
|
|
e863d3e7e5 | ||
|
|
c8e401f5ed | ||
|
|
3ba20a8e28 | ||
|
|
ebae63752f | ||
|
|
8bc73901cf | ||
|
|
b4f70d9244 | ||
|
|
21e9f2bba3 | ||
|
|
881f4e3d6a | ||
|
|
b141945add |
@@ -4235,6 +4235,33 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "smarsching",
|
||||
"name": "Sebastian Marsching",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2880129?v=4",
|
||||
"profile": "http://sebastian.marsching.com/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mohammad-ahmadi1",
|
||||
"name": "Mo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/40658372?v=4",
|
||||
"profile": "https://github.com/mohammad-ahmadi1",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "MarvelousAnything",
|
||||
"name": "Owen V. Hayes",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20994684?v=4",
|
||||
"profile": "https://github.com/MarvelousAnything",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -190,6 +190,7 @@ APP_ALLOW_INSECURE_HOSTS=false
|
||||
GOOGLE_MAPS_API=
|
||||
LDAP_MEM_LIM=500M
|
||||
LDAP_TIME_LIM=600
|
||||
BACKUP_TIME_LIMIT=600
|
||||
IMPORT_TIME_LIMIT=600
|
||||
IMPORT_MEMORY_LIMIT=500M
|
||||
REPORT_TIME_LIMIT=12000
|
||||
|
||||
60
.github/ISSUE_TEMPLATE/Bug-Report.yml
vendored
60
.github/ISSUE_TEMPLATE/Bug-Report.yml
vendored
@@ -23,7 +23,23 @@ body:
|
||||
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.1 - build 19577 (master)
|
||||
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
|
||||
@@ -48,6 +64,16 @@ body:
|
||||
- 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:
|
||||
@@ -67,6 +93,38 @@ body:
|
||||
- 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:
|
||||
|
||||
6
.github/workflows/SA-codeql.yml
vendored
6
.github/workflows/SA-codeql.yml
vendored
@@ -30,10 +30,10 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
2
.github/workflows/codacy-analysis.yml
vendored
2
.github/workflows/codacy-analysis.yml
vendored
@@ -52,6 +52,6 @@ jobs:
|
||||
|
||||
# Upload the SARIF file generated in the previous step
|
||||
- name: Upload SARIF results file
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
2
.github/workflows/tests-mysql.yml
vendored
2
.github/workflows/tests-mysql.yml
vendored
@@ -82,7 +82,7 @@ jobs:
|
||||
|
||||
- name: Upload Laravel logs as artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
|
||||
path: |
|
||||
|
||||
2
.github/workflows/tests-postgres.yml
vendored
2
.github/workflows/tests-postgres.yml
vendored
@@ -81,7 +81,7 @@ jobs:
|
||||
|
||||
- name: Upload Laravel logs as artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
|
||||
path: |
|
||||
|
||||
2
.github/workflows/tests-sqlite.yml
vendored
2
.github/workflows/tests-sqlite.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
- name: Upload Laravel logs as artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
|
||||
path: |
|
||||
|
||||
@@ -68,7 +68,8 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
|
||||
| [<img src="https://avatars.githubusercontent.com/u/181059?v=4" width="110px;"/><br /><sub>Juan Font</sub>](https://github.com/juanfont)<br />[💻](https://github.com/snipe/snipe-it/commits?author=juanfont "Code") | [<img src="https://avatars.githubusercontent.com/u/13137708?v=4" width="110px;"/><br /><sub>Juho Taipale</sub>](https://github.com/juhotaipale)<br />[💻](https://github.com/snipe/snipe-it/commits?author=juhotaipale "Code") | [<img src="https://avatars.githubusercontent.com/u/1007419?v=4" width="110px;"/><br /><sub>Korvin Szanto</sub>](https://github.com/KorvinSzanto)<br />[💻](https://github.com/snipe/snipe-it/commits?author=KorvinSzanto "Code") | [<img src="https://avatars.githubusercontent.com/u/8513053?v=4" width="110px;"/><br /><sub>Lewis Foster</sub>](https://lewisfoster.foo/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sniff122 "Code") | [<img src="https://avatars.githubusercontent.com/u/33877541?v=4" width="110px;"/><br /><sub>Logan Swartzendruber</sub>](https://github.com/loganswartz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=loganswartz "Code") | [<img src="https://avatars.githubusercontent.com/u/1156208?v=4" width="110px;"/><br /><sub>Lorenzo P.</sub>](https://github.com/lopezio)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lopezio "Code") | [<img src="https://avatars.githubusercontent.com/u/33946590?v=4" width="110px;"/><br /><sub>Lukas Jung</sub>](https://github.com/m4us1ne)<br />[💻](https://github.com/snipe/snipe-it/commits?author=m4us1ne "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/10965027?v=4" width="110px;"/><br /><sub>Ellie</sub>](https://leafedfox.xyz/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeafedFox "Code") | [<img src="https://avatars.githubusercontent.com/u/20960555?v=4" width="110px;"/><br /><sub>GA Stamper</sub>](https://github.com/gastamper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gastamper "Code") | [<img src="https://avatars.githubusercontent.com/u/206553556?v=4" width="110px;"/><br /><sub>Guillaume Lefranc</sub>](https://github.com/gl-pup)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gl-pup "Code") | [<img src="https://avatars.githubusercontent.com/u/733892?v=4" width="110px;"/><br /><sub>Hajo Möller</sub>](https://github.com/dasjoe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dasjoe "Code") | [<img src="https://avatars.githubusercontent.com/u/3420063?v=4" width="110px;"/><br /><sub>Istvan Basa</sub>](https://github.com/pottom)<br />[💻](https://github.com/snipe/snipe-it/commits?author=pottom "Code") | [<img src="https://avatars.githubusercontent.com/u/810824?v=4" width="110px;"/><br /><sub>JJ Asghar</sub>](https://jjasghar.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jjasghar "Code") | [<img src="https://avatars.githubusercontent.com/u/40404495?v=4" width="110px;"/><br /><sub>James E. Msenga</sub>](https://github.com/JemCdo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JemCdo "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/6865786?v=4" width="110px;"/><br /><sub>Jan Felix Wiebe</sub>](https://github.com/jfwiebe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jfwiebe "Code") | [<img src="https://avatars.githubusercontent.com/u/43412008?v=4" width="110px;"/><br /><sub>Jo Drexl</sub>](https://www.nfon.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=drexljo "Code") | [<img src="https://avatars.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>Austin Sasko</sub>](https://github.com/austinsasko)<br />[💻](https://github.com/snipe/snipe-it/commits?author=austinsasko "Code") | [<img src="https://avatars.githubusercontent.com/u/4875039?v=4" width="110px;"/><br /><sub>Jasson</sub>](http://jassoncordones.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JassonCordones "Code") | [<img src="https://avatars.githubusercontent.com/u/76069640?v=4" width="110px;"/><br /><sub>Okean</sub>](https://github.com/Tinyblargon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tinyblargon "Code") | [<img src="https://avatars.githubusercontent.com/u/6515064?v=4" width="110px;"/><br /><sub>Alejandro Medrano</sub>](https://www.lst.tfo.upm.es/alejandro-medrano/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=amedranogil "Code") | [<img src="https://avatars.githubusercontent.com/u/58696401?v=4" width="110px;"/><br /><sub>Lukas Kraic</sub>](https://github.com/lukaskraic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukaskraic "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/1571724?v=4" width="110px;"/><br /><sub>Герхард PICCORO Lenz McKAY </sub>](https://github-readme-stats.vercel.app/api?username=mckaygerhard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") | [<img src="https://avatars.githubusercontent.com/u/15015119?v=4" width="110px;"/><br /><sub>Johannes Pollitt</sub>](https://github.com/FlorestanII)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorestanII "Code") | [<img src="https://avatars.githubusercontent.com/u/14185442?v=4" width="110px;"/><br /><sub>Michael Strobel</sub>](https://strobelm.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=strobelm "Code") | [<img src="https://avatars.githubusercontent.com/u/634790?v=4" width="110px;"/><br /><sub>Nicky West</sub>](http://nickwest.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nickwest "Code") | [<img src="https://avatars.githubusercontent.com/u/1347327?v=4" width="110px;"/><br /><sub>akaspeh1</sub>](https://github.com/akaspeh1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akaspeh1 "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/1571724?v=4" width="110px;"/><br /><sub>Герхард PICCORO Lenz McKAY </sub>](https://github-readme-stats.vercel.app/api?username=mckaygerhard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") | [<img src="https://avatars.githubusercontent.com/u/15015119?v=4" width="110px;"/><br /><sub>Johannes Pollitt</sub>](https://github.com/FlorestanII)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorestanII "Code") | [<img src="https://avatars.githubusercontent.com/u/14185442?v=4" width="110px;"/><br /><sub>Michael Strobel</sub>](https://strobelm.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=strobelm "Code") | [<img src="https://avatars.githubusercontent.com/u/634790?v=4" width="110px;"/><br /><sub>Nicky West</sub>](http://nickwest.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nickwest "Code") | [<img src="https://avatars.githubusercontent.com/u/1347327?v=4" width="110px;"/><br /><sub>akaspeh1</sub>](https://github.com/akaspeh1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akaspeh1 "Code") | [<img src="https://avatars.githubusercontent.com/u/2880129?v=4" width="110px;"/><br /><sub>Sebastian Marsching</sub>](http://sebastian.marsching.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smarsching "Code") | [<img src="https://avatars.githubusercontent.com/u/40658372?v=4" width="110px;"/><br /><sub>Mo</sub>](https://github.com/mohammad-ahmadi1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mohammad-ahmadi1 "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/20994684?v=4" width="110px;"/><br /><sub>Owen V. Hayes</sub>](https://github.com/MarvelousAnything)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MarvelousAnything "Code") |
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
59
app/Actions/Categories/DestroyCategoryAction.php
Normal file
59
app/Actions/Categories/DestroyCategoryAction.php
Normal 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;
|
||||
}
|
||||
}
|
||||
63
app/Actions/Manufacturers/DeleteManufacturerAction.php
Normal file
63
app/Actions/Manufacturers/DeleteManufacturerAction.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
72
app/Actions/Suppliers/DestroySupplierAction.php
Normal file
72
app/Actions/Suppliers/DestroySupplierAction.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -317,9 +317,21 @@ class LdapSync extends Command
|
||||
if($ldap_map["jobtitle"] != null){
|
||||
$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){
|
||||
$user->country = $item['country'];
|
||||
}
|
||||
if($ldap_map["zip"] != null){
|
||||
$user->zip = $item['zip'];
|
||||
}
|
||||
if($ldap_map["dept"] != null){
|
||||
$user->department_id = $department->id;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
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
|
||||
@@ -52,6 +50,9 @@ class ObjectImportCommand extends Command
|
||||
*/
|
||||
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);
|
||||
|
||||
$filename = $this->argument('filename');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Console\Commands;
|
||||
use Illuminate\Console\Command;
|
||||
use ZipArchive;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use enshrined\svgSanitize\Sanitizer;
|
||||
|
||||
class SQLStreamer {
|
||||
private $input;
|
||||
@@ -242,9 +243,10 @@ class RestoreFromBackup extends Command
|
||||
|
||||
$private_dirs = [
|
||||
'storage/private_uploads/accessories',
|
||||
'storage/private_uploads/assetmodels',
|
||||
'storage/private_uploads/maintenances',
|
||||
'storage/private_uploads/models',
|
||||
'storage/private_uploads/assetmodels' => 'storage/private_uploads/models', //this was changed from assetmodels => models Aug 10 2025
|
||||
'storage/private_uploads/asset_maintenances' => 'storage/private_uploads/maintenances', //this was changed from asset_maintenances => maintenances Aug 10 2025
|
||||
'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/audits',
|
||||
'storage/private_uploads/components',
|
||||
@@ -262,7 +264,7 @@ class RestoreFromBackup extends Command
|
||||
];
|
||||
$public_dirs = [
|
||||
'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/assets', // these are asset _pictures_, not asset files
|
||||
'public/uploads/avatars',
|
||||
@@ -273,7 +275,7 @@ class RestoreFromBackup extends Command
|
||||
'public/uploads/departments',
|
||||
'public/uploads/locations',
|
||||
'public/uploads/manufacturers',
|
||||
'public/uploads/models',
|
||||
'public/uploads/models', // ...it's been this way for 9 years (as of late 2025)
|
||||
'public/uploads/suppliers',
|
||||
];
|
||||
|
||||
@@ -286,8 +288,6 @@ class RestoreFromBackup extends Command
|
||||
'public/uploads/favicon-uploaded.*',
|
||||
];
|
||||
|
||||
$all_files = $private_dirs + $public_dirs;
|
||||
|
||||
$sqlfiles = [];
|
||||
$sqlfile_indices = [];
|
||||
|
||||
@@ -295,6 +295,20 @@ class RestoreFromBackup extends Command
|
||||
$boring_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++) {
|
||||
$stat_results = $za->statIndex($i);
|
||||
// echo "index: $i\n";
|
||||
@@ -309,7 +323,7 @@ class RestoreFromBackup extends Command
|
||||
// skip macOS resource fork files (?!?!?!)
|
||||
if (strpos($raw_path, '__MACOSX') !== false && strpos($raw_path, '._') !== false) {
|
||||
//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;
|
||||
}
|
||||
if (@pathinfo($raw_path, PATHINFO_EXTENSION) == 'sql') {
|
||||
@@ -318,44 +332,70 @@ class RestoreFromBackup extends Command
|
||||
$sqlfile_indices[] = $i;
|
||||
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) {
|
||||
$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?
|
||||
$interesting_files[$raw_path] = ['dest' => $dir, 'index' => $i];
|
||||
continue 2;
|
||||
if ($last_pos + strlen($dir) + 1 == strlen($raw_path)) {
|
||||
// we don't care about that; we just want files with the appropriate prefix
|
||||
//print("FOUND THE EXACT DIRECTORY: $dir AT: $raw_path!!!\n");
|
||||
foreach (['public' => $public_dirs, 'private' => $private_dirs] as $purpose => $dirs) {
|
||||
$allowed_extensions = match ($purpose) {
|
||||
'public' => $public_extensions,
|
||||
'private' => $private_extensions,
|
||||
};
|
||||
foreach ($dirs as $dir => $destdir) {
|
||||
if (is_int($dir)) {
|
||||
$dir = $destdir;
|
||||
}
|
||||
$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 (array_merge($private_files, $public_files) as $file) {
|
||||
$has_wildcard = (strpos($file, '*') !== false);
|
||||
if ($has_wildcard) {
|
||||
$file = substr($file, 0, -1); //trim last character (which should be the wildcard)
|
||||
}
|
||||
$last_pos = strrpos($raw_path, $file); // no trailing slash!
|
||||
if ($last_pos !== false) {
|
||||
$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;
|
||||
foreach (['public' => $public_files, 'private' => $private_files] as $purpose => $files) {
|
||||
$allowed_extensions = match ($purpose) {
|
||||
'public' => $public_extensions,
|
||||
'private' => $private_extensions,
|
||||
};
|
||||
foreach ($files as $file) {
|
||||
$has_wildcard = (strpos($file, '*') !== false);
|
||||
if ($has_wildcard) {
|
||||
$file = substr($file, 0, -1); //trim last character (which should be the wildcard)
|
||||
}
|
||||
//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 2;
|
||||
$last_pos = strrpos($raw_path, $file); // no trailing slash!
|
||||
if ($last_pos !== false) {
|
||||
if (!in_array($extension, $allowed_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 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) {
|
||||
$ugly_file_name = $za->statIndex($file_details['index'])['name'];
|
||||
$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_name = $file_details['dest'] . '/' . basename($pretty_file_name);
|
||||
if (strcasecmp(substr($pretty_file_name, -4), ".svg") === 0) {
|
||||
$svg_contents = $za->getFromIndex($file_details['index']);
|
||||
$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) {
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
@@ -3,13 +3,18 @@
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Mail\UnacceptedAssetReminderMail;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Asset;
|
||||
use App\Models\CheckoutAcceptance;
|
||||
use App\Models\Component;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Notifications\CheckoutAssetNotification;
|
||||
use App\Notifications\CurrentInventory;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class SendAcceptanceReminder extends Command
|
||||
@@ -26,7 +31,7 @@ class SendAcceptanceReminder extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'This will resend users with unaccepted assets a reminder to accept or decline them.';
|
||||
protected $description = 'This will resend users with unaccepted items a reminder to accept or decline them.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
@@ -45,19 +50,30 @@ class SendAcceptanceReminder extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$pending = CheckoutAcceptance::pending()->where('checkoutable_type', 'App\Models\Asset')
|
||||
->whereHas('checkoutable', function($query) {
|
||||
$query->where('accepted_at', null)
|
||||
->where('declined_at', null);
|
||||
})
|
||||
->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model', 'checkoutable.adminuser'])
|
||||
->get();
|
||||
$pending = CheckoutAcceptance::query()
|
||||
->with([
|
||||
'checkoutable' => function (MorphTo $morph) {
|
||||
$morph->morphWith([
|
||||
Asset::class => ['model.category', 'assignedTo', 'adminuser', 'company', 'checkouts'],
|
||||
Accessory::class => ['category', 'company', 'checkouts'],
|
||||
LicenseSeat::class => ['user', 'license', 'checkouts'],
|
||||
Component::class => ['assignedTo', 'company', 'checkouts'],
|
||||
Consumable::class => ['company', 'checkouts'],
|
||||
]);
|
||||
},
|
||||
'assignedTo',
|
||||
])
|
||||
->whereHasMorph(
|
||||
'checkoutable',
|
||||
[Asset::class, Accessory::class, LicenseSeat::class, Component::class, Consumable::class],
|
||||
fn ($q) => $q->whereNull('accepted_at')
|
||||
->whereNull('declined_at')
|
||||
)
|
||||
->pending()
|
||||
->get();
|
||||
|
||||
$count = 0;
|
||||
$unacceptedAssetGroups = $pending
|
||||
->filter(function($acceptance) {
|
||||
return $acceptance->checkoutable_type == 'App\Models\Asset';
|
||||
})
|
||||
->map(function($acceptance) {
|
||||
return ['assetItem' => $acceptance->checkoutable, 'acceptance' => $acceptance];
|
||||
})
|
||||
|
||||
@@ -56,27 +56,68 @@ class SendExpirationAlerts extends Command
|
||||
$assets = Asset::getExpiringWarrantyOrEol($alert_interval);
|
||||
|
||||
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));
|
||||
|
||||
$this->table(
|
||||
['ID', 'Tag', 'Model', 'Model Number', 'EOL', 'EOL Months', 'Warranty Expires', 'Warranty Months'],
|
||||
$assets->map(fn($item) => ['ID' => $item->id, 'Tag' => $item->asset_tag, 'Model' => $item->model->name, 'Model Number' => $item->model->model_number, 'EOL' => $item->asset_eol_date, 'EOL Months' => $item->model->eol, 'Warranty Expires' => $item->warranty_expires, 'Warranty Months' => $item->warranty_months])
|
||||
);
|
||||
[
|
||||
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
|
||||
$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) {
|
||||
$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));
|
||||
|
||||
$this->table(
|
||||
['ID', 'Name', 'Expires', 'Termination Date'],
|
||||
$licenses->map(fn($item) => ['ID' => $item->id, 'Name' => $item->name, 'Expires' => $item->expiration_date, 'Termination Date' => $item->termination_date])
|
||||
[
|
||||
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 {
|
||||
|
||||
@@ -52,7 +52,9 @@ class SendInventoryAlerts extends Command
|
||||
return new AlertRecipient($item);
|
||||
});
|
||||
|
||||
\Notification::send($recipients, new InventoryAlert($items, $settings->alert_threshold));
|
||||
Notification::send($recipients, new InventoryAlert($items, $settings->alert_threshold));
|
||||
} else {
|
||||
$this->info('No low inventory items found. No mail sent.');
|
||||
}
|
||||
} else {
|
||||
if ($settings->alert_email == '') {
|
||||
|
||||
@@ -16,7 +16,7 @@ class SendUpcomingAuditReport extends Command
|
||||
*
|
||||
* @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.
|
||||
@@ -47,21 +47,69 @@ class SendUpcomingAuditReport extends Command
|
||||
$today = Carbon::now();
|
||||
$interval_date = $today->copy()->addDays($interval);
|
||||
|
||||
$assets = Asset::whereNull('deleted_at')->dueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'desc')->get();
|
||||
$this->info($assets->count() . ' assets must be audited in on or before ' . $interval_date . ' is deadline');
|
||||
|
||||
|
||||
if ((count($assets) !== 0) && ($assets->count() > 0) && ($settings->alert_email != '')) {
|
||||
// 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));
|
||||
$assets_query = Asset::whereNull('deleted_at')->dueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'asc')->with('supplier');
|
||||
$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')) {
|
||||
$this->info('Run this command with the --with-output option to see the full list in the console.');
|
||||
}
|
||||
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ class SystemBackup extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
ini_set('max_execution_time', env('BACKUP_TIME_LIMIT', 600)); //600 seconds = 10 minutes
|
||||
|
||||
if ($this->option('filename')) {
|
||||
$filename = $this->option('filename');
|
||||
|
||||
|
||||
33
app/Enums/ActionType.php
Normal file
33
app/Enums/ActionType.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
enum ActionType: string
|
||||
{
|
||||
// General
|
||||
case Create = 'create';
|
||||
case Update = 'update';
|
||||
case Delete = 'delete';
|
||||
case Restore = 'restore';
|
||||
|
||||
// Assets/Accessories/Components/Licenses/Consumables
|
||||
case Checkout = 'checkout';
|
||||
case CheckinFrom = 'checkin from';
|
||||
case Requested = 'requested';
|
||||
case RequestCanceled = 'request canceled';
|
||||
case Accepted = 'accepted';
|
||||
case Declined = 'declined';
|
||||
case Audit = 'audit';
|
||||
case NoteAdded = 'note added';
|
||||
|
||||
// Users
|
||||
case TwoFactorReset = '2FA reset';
|
||||
case Merged = 'merged';
|
||||
|
||||
// Licenses
|
||||
case DeleteSeats = 'delete seats';
|
||||
case AddSeats = 'add seats';
|
||||
|
||||
// File Uploads
|
||||
case Uploaded = 'uploaded';
|
||||
case UploadDeleted = 'upload deleted';
|
||||
}
|
||||
10
app/Exceptions/ItemStillHasAccessories.php
Normal file
10
app/Exceptions/ItemStillHasAccessories.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasAccessories extends ItemStillHasChildren
|
||||
{
|
||||
//
|
||||
}
|
||||
10
app/Exceptions/ItemStillHasAssetModels.php
Normal file
10
app/Exceptions/ItemStillHasAssetModels.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasAssetModels extends ItemStillHasChildren
|
||||
{
|
||||
//
|
||||
}
|
||||
9
app/Exceptions/ItemStillHasAssets.php
Normal file
9
app/Exceptions/ItemStillHasAssets.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasAssets extends ItemStillHasChildren
|
||||
{
|
||||
}
|
||||
14
app/Exceptions/ItemStillHasChildren.php
Normal file
14
app/Exceptions/ItemStillHasChildren.php
Normal 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()
|
||||
//
|
||||
//}
|
||||
}
|
||||
10
app/Exceptions/ItemStillHasComponents.php
Normal file
10
app/Exceptions/ItemStillHasComponents.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasComponents extends ItemStillHasChildren
|
||||
{
|
||||
//
|
||||
}
|
||||
10
app/Exceptions/ItemStillHasConsumables.php
Normal file
10
app/Exceptions/ItemStillHasConsumables.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasConsumables extends ItemStillHasChildren
|
||||
{
|
||||
//
|
||||
}
|
||||
10
app/Exceptions/ItemStillHasLicenses.php
Normal file
10
app/Exceptions/ItemStillHasLicenses.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasLicenses extends ItemStillHasChildren
|
||||
{
|
||||
//
|
||||
}
|
||||
10
app/Exceptions/ItemStillHasMaintenances.php
Normal file
10
app/Exceptions/ItemStillHasMaintenances.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasMaintenances extends ItemStillHasChildren
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -95,7 +95,7 @@ class Helper
|
||||
$Parsedown->setSafeMode(true);
|
||||
|
||||
if ($str) {
|
||||
return $Parsedown->text($str);
|
||||
return $Parsedown->text(strip_tags($str));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ class Helper
|
||||
$Parsedown->setSafeMode(true);
|
||||
|
||||
if ($str) {
|
||||
return $Parsedown->line($str);
|
||||
return $Parsedown->line(strip_tags($str));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ class IconHelper
|
||||
return 'fa-solid fa-trash-arrow-up';
|
||||
case 'external-link':
|
||||
return 'fa fa-external-link';
|
||||
case 'link':
|
||||
return 'fa fa-link';
|
||||
case 'email':
|
||||
return 'fa-regular fa-envelope';
|
||||
case 'phone':
|
||||
@@ -195,6 +197,12 @@ class IconHelper
|
||||
case 'note':
|
||||
case 'notes':
|
||||
return 'fas fa-sticky-note';
|
||||
case 'tip':
|
||||
return 'fa-solid fa-lightbulb';
|
||||
case 'highlight':
|
||||
return 'fa-solid fa-highlighter';
|
||||
case 'inherit':
|
||||
return 'fa-solid fa-layer-group';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +182,11 @@ class AccessoriesController extends Controller
|
||||
|
||||
$accessory = $request->handleImages($accessory);
|
||||
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
if($request->get('redirect_option') === 'back'){
|
||||
session()->put(['redirect_option' => 'index']);
|
||||
} else {
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
}
|
||||
|
||||
if ($accessory->save()) {
|
||||
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
|
||||
|
||||
@@ -13,11 +13,6 @@ use App\Models\Company;
|
||||
use App\Models\Contracts\Acceptable;
|
||||
use App\Models\Setting;
|
||||
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\AcceptanceAssetAcceptedToUserNotification;
|
||||
use App\Notifications\AcceptanceAssetDeclinedNotification;
|
||||
@@ -26,12 +21,9 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Http\Controllers\SettingsController;
|
||||
use Carbon\Carbon;
|
||||
use \Illuminate\Contracts\View\View;
|
||||
use \Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use TCPDF;
|
||||
use App\Helpers\Helper;
|
||||
|
||||
class AcceptanceController extends Controller
|
||||
@@ -83,6 +75,10 @@ class AcceptanceController extends Controller
|
||||
public function store(Request $request, $id) : RedirectResponse
|
||||
{
|
||||
$acceptance = CheckoutAcceptance::find($id);
|
||||
$assigned_user = User::find($acceptance->assigned_to_id);
|
||||
$settings = Setting::getSettings();
|
||||
$sig_filename='';
|
||||
|
||||
|
||||
if (is_null($acceptance)) {
|
||||
return redirect()->route('account.accept')->with('error', trans('admin/hardware/message.does_not_exist'));
|
||||
@@ -118,136 +114,67 @@ class AcceptanceController extends Controller
|
||||
Storage::makeDirectory('private_uploads/eula-pdfs', 775);
|
||||
}
|
||||
|
||||
|
||||
$item = $acceptance->checkoutable_type::find($acceptance->checkoutable_id);
|
||||
$display_model = '';
|
||||
$pdf_view_route = '';
|
||||
$pdf_filename = 'accepted-eula-'.date('Y-m-d-h-i-s').'.pdf';
|
||||
$sig_filename='';
|
||||
|
||||
// If signatures are required, make sure we have one
|
||||
if (Setting::getSettings()->require_accept_signature == '1') {
|
||||
|
||||
// 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 (Setting::getSettings()->require_accept_signature == '1') {
|
||||
|
||||
// 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'));
|
||||
}
|
||||
}
|
||||
|
||||
$assigned_user = User::find($acceptance->assigned_to_id);
|
||||
|
||||
|
||||
/**
|
||||
* 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 H:i:s'),
|
||||
'accepted_date' => Carbon::parse($acceptance->accepted_at)->format('Y-m-d H:i:s'),
|
||||
'assigned_to' => $assigned_user->display_name,
|
||||
'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,
|
||||
'qty' => $acceptance->qty ?? 1,
|
||||
];
|
||||
|
||||
// set some language dependent data:
|
||||
$lg = Array();
|
||||
$lg['a_meta_charset'] = 'UTF-8';
|
||||
$lg['w_page'] = 'page';
|
||||
|
||||
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
|
||||
$pdf->setRTL(false);
|
||||
$pdf->setLanguageArray($lg);
|
||||
$pdf->SetFontSubsetting(true);
|
||||
$pdf->SetCreator('Snipe-IT');
|
||||
$pdf->SetAuthor($data['assigned_to']);
|
||||
$pdf->SetTitle('Asset Acceptance: '.$data['item_tag']);
|
||||
$pdf->SetSubject('Asset Acceptance: '.$data['item_tag']);
|
||||
$pdf->SetKeywords('Snipe-IT, assets, acceptance, eula', 'tos');
|
||||
$pdf->SetFont('dejavusans', '', 8, '', true);
|
||||
$pdf->SetPrintHeader(false);
|
||||
$pdf->SetPrintFooter(false);
|
||||
$pdf->setHeaderFont(Array(PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN));
|
||||
$pdf->setFooterFont(Array(PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA));
|
||||
|
||||
$pdf->AddPage();
|
||||
$pdf->writeHTML('<img src="'.$path_logo.'" height="30">', true, 0, true, 0, '');
|
||||
|
||||
if ($data['item_serial']) {
|
||||
$pdf->writeHTML("<strong>" . trans('general.asset_tag') . '</strong>: ' . $data['item_tag'], true, 0, true, 0, '');
|
||||
}
|
||||
$pdf->writeHTML("<strong>".trans('general.asset_model').'</strong>: '.$data['item_model'], true, 0, true, 0, '');
|
||||
if ($data['item_serial']) {
|
||||
$pdf->writeHTML("<strong>".trans('admin/hardware/form.serial').'</strong>: '.$data['item_serial'], true, 0, true, 0, '');
|
||||
}
|
||||
$pdf->writeHTML("<strong>".trans('general.assigned_date').'</strong>: '.$data['check_out_date'], true, 0, true, 0, '');
|
||||
$pdf->writeHTML("<strong>".trans('general.assignee').'</strong>: '.$data['assigned_to'], true, 0, true, 0, '');
|
||||
$pdf->Ln();
|
||||
|
||||
// Break the EULA into lines based on newlines, and check each line for RTL or CJK characters
|
||||
$eula_lines = preg_split("/\r\n|\n|\r/", $item->getEula());
|
||||
|
||||
foreach ($eula_lines as $eula_line) {
|
||||
Helper::hasRtl($eula_line) ? $pdf->setRTL(true) : $pdf->setRTL(false);
|
||||
Helper::isCjk($eula_line) ? $pdf->SetFont('cid0cs', '', 9) : $pdf->SetFont('dejavusans', '', 8, '', true);
|
||||
|
||||
$pdf->writeHTML(Helper::parseEscapedMarkedown($eula_line), true, 0, true, 0, '');
|
||||
}
|
||||
$pdf->Ln();
|
||||
$pdf->Ln();
|
||||
$pdf->setRTL(false);
|
||||
$pdf->writeHTML('<br><br>', true, 0, true, 0, '');
|
||||
|
||||
if ($data['note'] != null) {
|
||||
Helper::isCjk($data['note']) ? $pdf->SetFont('cid0cs', '', 9) : $pdf->SetFont('dejavusans', '', 8, '', true);
|
||||
$pdf->writeHTML("<strong>".trans('general.notes') . '</strong>: ' . $data['note'], true, 0, true, 0, '');
|
||||
$pdf->Ln();
|
||||
}
|
||||
|
||||
if ($data['signature'] != null) {
|
||||
|
||||
$pdf->writeHTML('<img src="'.$data['signature'].'" style="max-width: 600px;">', true, 0, true, 0, '');
|
||||
$pdf->writeHTML('<hr>', true, 0, true, 0, '');
|
||||
}
|
||||
|
||||
$pdf->writeHTML("<strong>".trans('general.accepted_date').'</strong>: '.$data['accepted_date'], true, 0, true, 0, '');
|
||||
|
||||
|
||||
$pdf_content = $pdf->Output($pdf_filename, 'S');
|
||||
$pdf_filename = 'accepted-'.$acceptance->checkoutable_id.'-'.$acceptance->display_checkoutable_type.'-eula-'.date('Y-m-d-h-i-s').'.pdf';
|
||||
|
||||
// Generate the PDF content
|
||||
$pdf_content = $acceptance->generateAcceptancePdf($data, $acceptance);
|
||||
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf_content);
|
||||
|
||||
|
||||
// Log the acceptance
|
||||
$acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note'));
|
||||
|
||||
// Send the PDF to the signing user
|
||||
@@ -270,45 +197,9 @@ class AcceptanceController extends Controller
|
||||
|
||||
$return_msg = trans('admin/users/message.accepted');
|
||||
|
||||
// Item was not accepted
|
||||
// Item was declined
|
||||
} else {
|
||||
|
||||
if (Setting::getSettings()->require_accept_signature == '1') {
|
||||
|
||||
// The item was declined, 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();
|
||||
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
|
||||
|
||||
$data = [
|
||||
'item_tag' => $item->asset_tag,
|
||||
'item_model' => $item->model ? $item->model->name : $item->display_name,
|
||||
'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,
|
||||
'qty' => $acceptance->qty ?? 1,
|
||||
];
|
||||
|
||||
|
||||
for ($i = 0; $i < ($acceptance->qty ?? 1); $i++) {
|
||||
$acceptance->decline($sig_filename, $request->input('note'));
|
||||
}
|
||||
@@ -319,6 +210,8 @@ class AcceptanceController extends Controller
|
||||
$return_msg = trans('admin/users/message.declined');
|
||||
}
|
||||
|
||||
|
||||
// Send an email notification if one is requested
|
||||
if ($acceptance->alert_on_response_id) {
|
||||
try {
|
||||
$recipient = User::find($acceptance->alert_on_response_id);
|
||||
@@ -337,9 +230,10 @@ class AcceptanceController extends Controller
|
||||
Log::warning($e);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->to('account/accept')->with('success', $return_msg);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Models\Actionlog;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use \Illuminate\Http\Response;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use \Illuminate\Http\Response;
|
||||
|
||||
class ActionlogController extends Controller
|
||||
{
|
||||
public function displaySig($filename) : RedirectResponse | Response | bool
|
||||
@@ -39,17 +41,29 @@ class ActionlogController extends Controller
|
||||
|
||||
public function getStoredEula($filename) : Response | BinaryFileResponse | RedirectResponse
|
||||
{
|
||||
$this->authorize('view', \App\Models\Asset::class);
|
||||
|
||||
if (config('filesystems.default') == 's3_private') {
|
||||
return redirect()->away(Storage::disk('s3_private')->temporaryUrl('private_uploads/eula-pdfs/'.$filename, now()->addMinutes(5)));
|
||||
if ($actionlog = Actionlog::where('filename', $filename)->with('user')->with('target')->firstOrFail()) {
|
||||
|
||||
$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 response()->download(config('app.private_uploads').'/eula-pdfs/'.$filename);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', trans('general.file_does_not_exist'));
|
||||
|
||||
return redirect()->back()->with('error', trans('general.record_not_found'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,15 @@ class AccessoriesController extends Controller
|
||||
'notes',
|
||||
'checkouts_count',
|
||||
'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')
|
||||
->withCount('checkouts as checkouts_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$accessories = $accessories->TextSearch($request->input('search'));
|
||||
$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) {
|
||||
$accessories->ByFilter($filter);
|
||||
} elseif ($request->filled('search')) {
|
||||
$accessories->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$accessories->where('accessories.company_id', '=', $request->input('company_id'));
|
||||
}
|
||||
|
||||
@@ -46,11 +46,20 @@ class AssetModelsController extends Controller
|
||||
'manufacturer',
|
||||
'requestable',
|
||||
'assets_count',
|
||||
'assets_assigned_count',
|
||||
'assets_archived_count',
|
||||
'remaining',
|
||||
'category',
|
||||
'fieldset',
|
||||
'deleted_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([
|
||||
@@ -73,7 +82,28 @@ class AssetModelsController extends Controller
|
||||
'models.require_serial'
|
||||
])
|
||||
->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') {
|
||||
$assetmodels->onlyTrashed();
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Events\CheckoutableCheckedIn;
|
||||
use App\Http\Requests\StoreAssetRequest;
|
||||
use App\Http\Requests\UpdateAssetRequest;
|
||||
use App\Http\Traits\MigratesLegacyAssetLocations;
|
||||
use App\Http\Transformers\ComponentsTransformer;
|
||||
use App\Models\AccessoryCheckout;
|
||||
use App\Models\CheckoutAcceptance;
|
||||
use App\Models\LicenseSeat;
|
||||
@@ -115,6 +116,22 @@ class AssetsController extends Controller
|
||||
'asset_eol_date',
|
||||
'requestable',
|
||||
'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
|
||||
@@ -131,6 +148,7 @@ class AssetsController extends Controller
|
||||
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
|
||||
return in_array($key, $allowed_columns);
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
}
|
||||
|
||||
$assets = Asset::select('assets.*')
|
||||
@@ -165,7 +183,7 @@ class AssetsController extends Controller
|
||||
// Search custom fields by column name
|
||||
foreach ($all_custom_fields as $field) {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1322,6 +1340,18 @@ class AssetsController extends Controller
|
||||
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
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Actions\Categories\DestroyCategoryAction;
|
||||
use App\Exceptions\ItemStillHasChildren;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\CategoriesTransformer;
|
||||
@@ -61,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');
|
||||
|
||||
|
||||
$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.
|
||||
* We don't currently use it within the Snipe-IT GUI, but will be useful for API integrations where they
|
||||
@@ -74,10 +93,6 @@ class CategoriesController extends Controller
|
||||
$categories = $categories->withCount('showableAssets as assets_count');
|
||||
}
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$categories = $categories->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$categories->where('name', '=', $request->input('name'));
|
||||
}
|
||||
@@ -211,17 +226,21 @@ class CategoriesController extends Controller
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function destroy($id) : JsonResponse
|
||||
public function destroy(Category $category): JsonResponse
|
||||
{
|
||||
$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);
|
||||
|
||||
if (! $category->isDeletable()) {
|
||||
try {
|
||||
DestroyCategoryAction::run(category: $category);
|
||||
} catch (ItemStillHasChildren $e) {
|
||||
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')));
|
||||
}
|
||||
|
||||
@@ -45,16 +45,40 @@ class ComponentsController extends Controller
|
||||
'qty',
|
||||
'image',
|
||||
'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.*')
|
||||
->with('company', 'location', 'category', 'assets', 'supplier', 'adminuser', 'manufacturer', 'uncontrainedAssets')
|
||||
->withSum('uncontrainedAssets', 'components_assets.assigned_qty');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$components = $components->TextSearch($request->input('search'));
|
||||
$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) {
|
||||
$components->ByFilter($filter);
|
||||
} elseif ($request->filled('search')) {
|
||||
$components->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$components->where('name', '=', $request->input('name'));
|
||||
}
|
||||
|
||||
@@ -31,10 +31,53 @@ class ConsumablesController extends Controller
|
||||
$consumables = Consumable::with('company', 'location', 'category', 'supplier', 'manufacturer')
|
||||
->withCount('users as consumables_users_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$consumables = $consumables->TextSearch(e($request->input('search')));
|
||||
// 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',
|
||||
// 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')) {
|
||||
$consumables->where('name', '=', $request->input('name'));
|
||||
}
|
||||
@@ -96,25 +139,6 @@ class ConsumablesController extends Controller
|
||||
$consumables = $consumables->OrderByCreatedBy($order);
|
||||
break;
|
||||
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';
|
||||
$consumables = $consumables->orderBy($sort, $order);
|
||||
break;
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreDepartmentRequest;
|
||||
use App\Http\Transformers\DepartmentsTransformer;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Models\Department;
|
||||
@@ -26,18 +27,19 @@ class DepartmentsController extends Controller
|
||||
$allowed_columns = ['id', 'name', 'image', 'users_count', 'notes'];
|
||||
|
||||
$departments = Department::select(
|
||||
'departments.id',
|
||||
'departments.name',
|
||||
'departments.phone',
|
||||
'departments.fax',
|
||||
'departments.location_id',
|
||||
'departments.company_id',
|
||||
'departments.manager_id',
|
||||
'departments.created_at',
|
||||
'departments.updated_at',
|
||||
'departments.image',
|
||||
'departments.notes',
|
||||
)->with('users')->with('location')->with('manager')->with('company')->withCount('users as users_count');
|
||||
[
|
||||
'departments.id',
|
||||
'departments.name',
|
||||
'departments.phone',
|
||||
'departments.fax',
|
||||
'departments.location_id',
|
||||
'departments.company_id',
|
||||
'departments.manager_id',
|
||||
'departments.created_at',
|
||||
'departments.updated_at',
|
||||
'departments.image',
|
||||
'departments.notes'
|
||||
])->with('users')->with('location')->with('manager')->with('company')->withCount('users as users_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$departments = $departments->TextSearch($request->input('search'));
|
||||
@@ -94,18 +96,17 @@ class DepartmentsController extends Controller
|
||||
* @since [v4.0]
|
||||
* @param \App\Http\Requests\ImageUploadRequest $request
|
||||
*/
|
||||
public function store(ImageUploadRequest $request) : JsonResponse
|
||||
public function store(StoreDepartmentRequest $request): JsonResponse
|
||||
{
|
||||
$this->authorize('create', Department::class);
|
||||
$department = new Department;
|
||||
$department->fill($request->all());
|
||||
$department->fill($request->validated());
|
||||
$department = $request->handleImages($department);
|
||||
|
||||
$department->created_by = auth()->id();
|
||||
$department->manager_id = ($request->filled('manager_id') ? $request->input('manager_id') : null);
|
||||
|
||||
if ($department->save()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $department, trans('admin/departments/message.create.success')));
|
||||
return response()->json(Helper::formatStandardApiResponse('success', (new DepartmentsTransformer)->transformDepartment($department), trans('admin/departments/message.create.success')));
|
||||
}
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $department->getErrors()));
|
||||
|
||||
@@ -121,7 +122,7 @@ class DepartmentsController extends Controller
|
||||
public function show($id) : array
|
||||
{
|
||||
$this->authorize('view', Department::class);
|
||||
$department = Department::findOrFail($id);
|
||||
$department = Department::withCount('users as users_count')->findOrFail($id);
|
||||
return (new DepartmentsTransformer)->transformDepartment($department);
|
||||
}
|
||||
|
||||
@@ -141,7 +142,7 @@ class DepartmentsController extends Controller
|
||||
$department = $request->handleImages($department);
|
||||
|
||||
if ($department->save()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $department, trans('admin/departments/message.update.success')));
|
||||
return response()->json(Helper::formatStandardApiResponse('success', (new DepartmentsTransformer)->transformDepartment($department), trans('admin/departments/message.update.success')));
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $department->getErrors()));
|
||||
|
||||
@@ -24,7 +24,7 @@ class GroupsController extends Controller
|
||||
|
||||
$this->authorize('view', Group::class);
|
||||
|
||||
$groups = Group::select('id', 'name', 'permissions', 'notes', 'created_at', 'updated_at', 'created_by')->with('adminuser')->withCount('users as users_count');
|
||||
$groups = Group::select(['id', 'name', 'permissions', 'notes', 'created_at', 'updated_at', 'created_by'])->with('adminuser')->withCount('users as users_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$groups = $groups->TextSearch($request->input('search'));
|
||||
@@ -50,6 +50,7 @@ class GroupsController extends Controller
|
||||
'id',
|
||||
'name',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'users_count',
|
||||
];
|
||||
|
||||
|
||||
@@ -26,11 +26,11 @@ class LicenseSeatsController extends Controller
|
||||
if ($license = License::find($licenseId)) {
|
||||
$this->authorize('view', $license);
|
||||
|
||||
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department')
|
||||
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department', 'user.company', 'asset.company')
|
||||
->where('license_seats.license_id', $licenseId);
|
||||
|
||||
if ($request->input('status') == 'available') {
|
||||
$seats->whereNull('license_seats.assigned_to');
|
||||
$seats->whereNull('license_seats.assigned_to')->whereNull('license_seats.asset_id');
|
||||
}
|
||||
|
||||
if ($request->input('status') == 'assigned') {
|
||||
@@ -40,8 +40,10 @@ class LicenseSeatsController extends Controller
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
if ($request->input('sort') == 'department') {
|
||||
if ($request->input('sort') == 'assigned_user.department') {
|
||||
$seats->OrderDepartments($order);
|
||||
} elseif ($request->input('sort') == 'assigned_user.company') {
|
||||
$seats->OrderCompany($order);
|
||||
} else {
|
||||
$seats->orderBy('updated_at', $order);
|
||||
}
|
||||
@@ -83,7 +85,7 @@ class LicenseSeatsController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat not found'));
|
||||
}
|
||||
// 2. does the seat belong to the specified license?
|
||||
if (! $license = $licenseSeat->license()->first() || $license->id != intval($licenseId)) {
|
||||
if (! $licenseSeat = $licenseSeat->license()->first() || $licenseSeat->id != intval($licenseId)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat does not belong to the specified license'));
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\LicensesTransformer;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Models\License;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -25,9 +26,12 @@ class LicensesController extends Controller
|
||||
$this->authorize('view', License::class);
|
||||
|
||||
$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();
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@ class MaintenancesController extends Controller
|
||||
$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')) {
|
||||
$maintenances->where('asset_maintenance_type', '=', $request->input('asset_maintenance_type'));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
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\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\ManufacturersTransformer;
|
||||
@@ -184,19 +191,19 @@ class ManufacturersController extends Controller
|
||||
* @since [v4.0]
|
||||
* @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);
|
||||
|
||||
if ($manufacturer->isDeletable()) {
|
||||
$manufacturer->delete();
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/manufacturers/message.delete.success')));
|
||||
try {
|
||||
DeleteManufacturerAction::run($manufacturer);
|
||||
} catch (ItemStillHasChildren $e) {
|
||||
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')));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
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\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
@@ -191,27 +198,40 @@ class SuppliersController extends Controller
|
||||
* @since [v4.0]
|
||||
* @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);
|
||||
|
||||
|
||||
if ($supplier->assets_count > 0) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count])));
|
||||
try {
|
||||
DestroySupplierAction::run(supplier: $supplier);
|
||||
} catch (ItemStillHasAssets $e) {
|
||||
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')));
|
||||
}
|
||||
|
||||
|
||||
@@ -193,8 +193,12 @@ class UploadedFilesController extends Controller
|
||||
|
||||
|
||||
// Check for the file
|
||||
$log = Actionlog::find($file_id)->where('item_type', self::$map_object_type[$object_type])
|
||||
->where('item_id', $object->id)->first();
|
||||
$log = Actionlog::query()
|
||||
->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) {
|
||||
// Check the file actually exists, and delete it
|
||||
@@ -213,4 +217,4 @@ class UploadedFilesController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('general.file_upload_status.delete.error', 1)), 500);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,9 +103,75 @@ class UsersController extends Controller
|
||||
'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') != '') {
|
||||
$users = $users->TextSearch($request->input('search'));
|
||||
// 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',
|
||||
'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')) {
|
||||
@@ -286,49 +352,6 @@ class UsersController extends Controller
|
||||
$users->orderBy('first_name', $order);
|
||||
break;
|
||||
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';
|
||||
$users = $users->orderBy($sort, $order);
|
||||
break;
|
||||
|
||||
@@ -363,7 +363,7 @@ class AssetsController extends Controller
|
||||
$asset->purchase_cost = $request->input('purchase_cost', null);
|
||||
$asset->purchase_date = $request->input('purchase_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->asset_eol_date = Carbon::parse($request->input('purchase_date'))->addMonths($asset->model->eol)->format('Y-m-d');
|
||||
$asset->eol_explicit = false;
|
||||
@@ -379,7 +379,7 @@ class AssetsController extends Controller
|
||||
} else {
|
||||
$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->eol_explicit = false;
|
||||
}
|
||||
@@ -398,6 +398,7 @@ class AssetsController extends Controller
|
||||
$asset->assigned_to = null;
|
||||
$asset->assigned_type = 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));
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ class BulkAssetsController extends Controller
|
||||
$modelNames = [];
|
||||
|
||||
foreach($models as $model) {
|
||||
$modelNames[] = $model->model->name;
|
||||
$modelNames[] = $model->model?->name;
|
||||
}
|
||||
|
||||
if ($request->filled('bulk_actions')) {
|
||||
@@ -240,10 +240,6 @@ class BulkAssetsController extends Controller
|
||||
$custom_fields_to_null[str_replace('null', '', $key)] = $value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (! $request->filled('ids') || count($request->input('ids')) == 0) {
|
||||
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('status_id'))
|
||||
|| ($request->filled('model_id'))
|
||||
|| ($request->filled('notes'))
|
||||
|| ($request->filled('next_audit_date'))
|
||||
|| ($request->filled('asset_eol_date'))
|
||||
|| ($request->filled('null_name'))
|
||||
@@ -470,7 +467,7 @@ class BulkAssetsController extends Controller
|
||||
*/
|
||||
|
||||
// Does the model have a fieldset?
|
||||
if ($asset->model->fieldset) {
|
||||
if ($asset->model?->fieldset) {
|
||||
foreach ($asset->model->fieldset->fields as $field) {
|
||||
|
||||
// null custom fields
|
||||
@@ -621,9 +618,25 @@ class BulkAssetsController extends Controller
|
||||
{
|
||||
$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')];
|
||||
$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,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -647,6 +660,30 @@ class BulkAssetsController extends Controller
|
||||
|
||||
$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') {
|
||||
foreach ($asset_ids as $asset_id) {
|
||||
if ($target->id == $asset_id) {
|
||||
|
||||
59
app/Http/Controllers/BulkCategoriesController.php
Normal file
59
app/Http/Controllers/BulkCategoriesController.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
57
app/Http/Controllers/BulkManufacturersController.php
Normal file
57
app/Http/Controllers/BulkManufacturersController.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
58
app/Http/Controllers/BulkSuppliersController.php
Normal file
58
app/Http/Controllers/BulkSuppliersController.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
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\Http\Requests\ImageUploadRequest;
|
||||
use App\Models\Category;
|
||||
@@ -143,20 +151,18 @@ class CategoriesController extends Controller
|
||||
* @since [v1.0]
|
||||
* @param int $categoryId
|
||||
*/
|
||||
public function destroy($categoryId) : RedirectResponse
|
||||
public function destroy(Category $category): RedirectResponse
|
||||
{
|
||||
$this->authorize('delete', Category::class);
|
||||
// Check if the category exists
|
||||
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))) {
|
||||
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.not_found'));
|
||||
try {
|
||||
DestroyCategoryAction::run($category);
|
||||
} 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'));
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ use App\Models\Consumable;
|
||||
use App\Models\License;
|
||||
use App\Models\Location;
|
||||
use App\Models\Maintenance;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
@@ -45,12 +46,14 @@ abstract class Controller extends BaseController
|
||||
'accessories' => Accessory::class,
|
||||
'maintenances' => Maintenance::class,
|
||||
'assets' => Asset::class,
|
||||
'audits' => Asset::class,
|
||||
'components' => Component::class,
|
||||
'consumables' => Consumable::class,
|
||||
'hardware' => Asset::class,
|
||||
'licenses' => License::class,
|
||||
'locations' => Location::class,
|
||||
'models' => AssetModel::class,
|
||||
'suppliers' => Supplier::class,
|
||||
'users' => User::class,
|
||||
];
|
||||
|
||||
@@ -58,12 +61,14 @@ abstract class Controller extends BaseController
|
||||
'accessories' => 'private_uploads/accessories/',
|
||||
'maintenances' => 'private_uploads/maintenances/',
|
||||
'assets' => 'private_uploads/assets/',
|
||||
'audits' => 'private_uploads/audits/',
|
||||
'components' => 'private_uploads/components/',
|
||||
'consumables' => 'private_uploads/consumables/',
|
||||
'hardware' => 'private_uploads/assets/',
|
||||
'licenses' => 'private_uploads/licenses/',
|
||||
'locations' => 'private_uploads/locations/',
|
||||
'models' => 'private_uploads/models/',
|
||||
'suppliers' => 'private_uploads/suppliers/',
|
||||
'users' => 'private_uploads/users/',
|
||||
];
|
||||
|
||||
@@ -71,12 +76,14 @@ abstract class Controller extends BaseController
|
||||
'accessories' => 'accessory',
|
||||
'maintenances' => 'maintenance',
|
||||
'assets' => 'asset',
|
||||
'audits' => 'audits',
|
||||
'components' => 'component',
|
||||
'consumables' => 'consumable',
|
||||
'hardware' => 'asset',
|
||||
'licenses' => 'license',
|
||||
'locations' => 'location',
|
||||
'models' => 'model',
|
||||
'suppliers' => 'supplier',
|
||||
'users' => 'user',
|
||||
];
|
||||
|
||||
|
||||
@@ -43,9 +43,12 @@ class GroupsController extends Controller
|
||||
$permissions = config('permissions');
|
||||
$groupPermissions = Helper::selectedPermissionsArray($permissions, $permissions);
|
||||
$selectedPermissions = $request->old('permissions', $groupPermissions);
|
||||
|
||||
$users = \App\Models\User::orderBy('first_name', 'asc')->orderBy('last_name', 'asc')->get();
|
||||
// Show the page
|
||||
return view('groups/edit', compact('permissions', 'selectedPermissions', 'groupPermissions'))->with('group', $group);
|
||||
return view('groups/edit', compact('permissions', 'selectedPermissions', 'groupPermissions'))
|
||||
->with('group', $group)
|
||||
->with('associated_users', [])
|
||||
->with('unselected_users', $users);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,11 +63,23 @@ class GroupsController extends Controller
|
||||
// create a new group instance
|
||||
$group = new Group();
|
||||
$group->name = $request->input('name');
|
||||
|
||||
if ($request->filled('permission')) {
|
||||
$group->permissions = json_encode($request->array('permission'));
|
||||
} else {
|
||||
$group->permissions = null;
|
||||
}
|
||||
|
||||
$group->permissions = json_encode($request->input('permission'));
|
||||
$group->created_by = auth()->id();
|
||||
$group->notes = $request->input('notes');
|
||||
|
||||
if ($group->save()) {
|
||||
|
||||
if ($request->filled('users_to_sync')) {
|
||||
$associated_users = explode(',',$request->input('users_to_sync'));
|
||||
$group->users()->sync($associated_users);
|
||||
}
|
||||
return redirect()->route('groups.index')->with('success', trans('admin/groups/message.success.create'));
|
||||
}
|
||||
|
||||
@@ -88,7 +103,12 @@ class GroupsController extends Controller
|
||||
$groupPermissions = [];
|
||||
}
|
||||
$selected_array = Helper::selectedPermissionsArray($permissions, $groupPermissions);
|
||||
return view('groups.edit', compact('group', 'permissions', 'selected_array', 'groupPermissions'));
|
||||
$associated_users = $group->users()->orderBy('first_name', 'asc')->orderBy('last_name', 'asc')->get();
|
||||
|
||||
// Get the unselected users
|
||||
$unselected_users = \App\Models\User::whereNotIn('id', $associated_users->pluck('id')->toArray())->orderBy('first_name', 'asc')->orderBy('last_name', 'asc')->get();
|
||||
|
||||
return view('groups.edit', compact('group', 'permissions', 'selected_array', 'groupPermissions'))->with('associated_users', $associated_users)->with('unselected_users', $unselected_users);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,11 +122,24 @@ class GroupsController extends Controller
|
||||
public function update(Request $request, Group $group) : RedirectResponse
|
||||
{
|
||||
$group->name = $request->input('name');
|
||||
$group->permissions = json_encode($request->input('permission'));
|
||||
|
||||
if ($request->filled('permission')) {
|
||||
$group->permissions = json_encode($request->array('permission'));
|
||||
} else {
|
||||
$group->permissions = null;
|
||||
}
|
||||
|
||||
$group->notes = $request->input('notes');
|
||||
|
||||
|
||||
if (! config('app.lock_passwords')) {
|
||||
if ($group->save()) {
|
||||
|
||||
if ($request->filled('users_to_sync')) {
|
||||
$associated_users = explode(',',$request->input('users_to_sync'));
|
||||
$group->users()->sync($associated_users);
|
||||
}
|
||||
|
||||
return redirect()->route('groups.index')->with('success', trans('admin/groups/message.success.update'));
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ class MaintenancesController extends Controller
|
||||
$maintenance->is_warranty = $request->input('is_warranty');
|
||||
$maintenance->cost = $request->input('cost');
|
||||
$maintenance->notes = $request->input('notes');
|
||||
$maintenance->url = $request->input('url');
|
||||
|
||||
// Save the asset maintenance data
|
||||
$maintenance->asset_id = $asset->id;
|
||||
@@ -152,6 +153,7 @@ class MaintenancesController extends Controller
|
||||
$maintenance->name = $request->input('name');
|
||||
$maintenance->start_date = $request->input('start_date');
|
||||
$maintenance->completion_date = $request->input('completion_date');
|
||||
$maintenance->url = $request->input('url');
|
||||
|
||||
|
||||
// Todo - put this in a getter/setter?
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
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\Models\Actionlog;
|
||||
use App\Models\Manufacturer;
|
||||
@@ -157,32 +165,18 @@ class ManufacturersController extends Controller
|
||||
* @param int $manufacturerId
|
||||
* @since [v1.0]
|
||||
*/
|
||||
public function destroy($manufacturerId) : RedirectResponse
|
||||
public function destroy(Manufacturer $manufacturer): RedirectResponse
|
||||
{
|
||||
$this->authorize('delete', Manufacturer::class);
|
||||
if (is_null($manufacturer = Manufacturer::withTrashed()->withCount('models as models_count')->find($manufacturerId))) {
|
||||
return redirect()->route('manufacturers.index')->with('error', trans('admin/manufacturers/message.not_found'));
|
||||
$this->authorize('delete', $manufacturer);
|
||||
try {
|
||||
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'));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,21 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Mail\CheckoutAccessoryMail;
|
||||
use App\Mail\CheckoutAssetMail;
|
||||
use App\Mail\CheckoutComponentMail;
|
||||
use App\Mail\CheckoutConsumableMail;
|
||||
use App\Mail\CheckoutLicenseMail;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\AccessoryCheckout;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Category;
|
||||
use App\Models\Checkoutable;
|
||||
use App\Models\Component;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\Maintenance;
|
||||
use App\Models\CheckoutAcceptance;
|
||||
use App\Models\Company;
|
||||
@@ -18,9 +27,11 @@ use App\Models\License;
|
||||
use App\Models\ReportTemplate;
|
||||
use App\Models\Setting;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use \Illuminate\Contracts\View\View;
|
||||
use League\Csv\Reader;
|
||||
@@ -436,10 +447,8 @@ class ReportsController extends Controller
|
||||
// Open output stream
|
||||
$handle = fopen('php://output', 'w');
|
||||
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 = [];
|
||||
|
||||
@@ -685,6 +694,14 @@ class ReportsController extends Controller
|
||||
$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'))) {
|
||||
$created_start = Carbon::parse($request->input('created_start'))->startOfDay();
|
||||
$created_end = Carbon::parse($request->input('created_end'))->endOfDay();
|
||||
@@ -1110,34 +1127,30 @@ class ReportsController extends Controller
|
||||
$this->authorize('reports.view');
|
||||
$showDeleted = $deleted == 'deleted';
|
||||
|
||||
$query = CheckoutAcceptance::pending()
|
||||
->where('checkoutable_type', 'App\Models\Asset')
|
||||
$query = CheckoutAcceptance::Pending()
|
||||
->with([
|
||||
'checkoutable' => function (MorphTo $query) {
|
||||
$query->morphWith([
|
||||
AssetModel::class => ['model'],
|
||||
Company::class => ['company'],
|
||||
Asset::class => ['assignedTo'],
|
||||
])->with('model.category');
|
||||
$query->withTrashed()->morphWith([
|
||||
Asset::class => ['model.category', 'assignedTo', 'company'],
|
||||
Accessory::class => ['category','checkouts', 'company'],
|
||||
LicenseSeat::class => ['user', 'license'],
|
||||
Component::class => ['assignedTo', 'company'],
|
||||
Consumable::class => ['company'],
|
||||
]);
|
||||
},
|
||||
'assignedTo' => function($query){
|
||||
$query->withTrashed();
|
||||
}
|
||||
]);
|
||||
])->orderByDesc('checkout_acceptances.created_at');
|
||||
|
||||
|
||||
if ($showDeleted) {
|
||||
$query->withTrashed();
|
||||
}
|
||||
|
||||
$assetsForReport = $query->get()
|
||||
->map(function ($acceptance) {
|
||||
return [
|
||||
'assetItem' => $acceptance->checkoutable,
|
||||
'acceptance' => $acceptance,
|
||||
];
|
||||
});
|
||||
$itemsForReport = $query->get()->map(fn ($unaccepted) => Checkoutable::fromAcceptance($unaccepted));
|
||||
|
||||
return view('reports/unaccepted_assets', compact('assetsForReport','showDeleted' ));
|
||||
return view('reports/unaccepted_assets', compact('itemsForReport','showDeleted' ));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1149,41 +1162,77 @@ class ReportsController extends Controller
|
||||
public function sentAssetAcceptanceReminder(Request $request) : RedirectResponse
|
||||
{
|
||||
$this->authorize('reports.view');
|
||||
|
||||
if (!$acceptance = CheckoutAcceptance::pending()->find($request->input('acceptance_id'))) {
|
||||
$id = $request->input('acceptance_id');
|
||||
$query = CheckoutAcceptance::query()
|
||||
->with([
|
||||
'checkoutable' => function (MorphTo $query) {
|
||||
$query->withTrashed()->morphWith([
|
||||
Asset::class => ['model.category', 'assignedTo', 'company', 'checkouts'],
|
||||
Accessory::class => ['category', 'company', 'checkouts'],
|
||||
LicenseSeat::class => ['user', 'license', 'checkouts'],
|
||||
Component::class => ['assignedTo', 'company', 'checkouts'],
|
||||
Consumable::class => ['company', 'checkouts'],
|
||||
]);
|
||||
},
|
||||
'assignedTo' => fn ($q) => $q->withTrashed(),
|
||||
])
|
||||
->pending();
|
||||
$acceptance = $query->find($id);
|
||||
if (!$acceptance) {
|
||||
Log::debug('No pending acceptances');
|
||||
// Redirect to the unaccepted assets report page with error
|
||||
// Redirect to the unaccepted items report page with error
|
||||
return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.bad_data'));
|
||||
}
|
||||
$item = $acceptance->checkoutable;
|
||||
$assignee = $acceptance->assignedTo ?? $item->assignedTo ?? null;
|
||||
$email = $assignee?->email;
|
||||
$locale = $assignee?->locale;
|
||||
|
||||
$assetItem = $acceptance->checkoutable;
|
||||
|
||||
Log::debug(print_r($assetItem, true));
|
||||
Log::debug(print_r($acceptance, true));
|
||||
|
||||
if (is_null($acceptance->created_at)){
|
||||
Log::debug('No acceptance created_at');
|
||||
return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.bad_data'));
|
||||
} else {
|
||||
$logItem_res = $assetItem->checkouts()->where('created_at', '=', $acceptance->created_at)->get();
|
||||
|
||||
if($item instanceof LicenseSeat){
|
||||
$logItem_res = $item->license->checkouts()->with('adminuser')->where('created_at', '=', $acceptance->created_at)->get();
|
||||
}
|
||||
else{
|
||||
$logItem_res = $item->checkouts()->with('adminuser')->where('created_at', '=', $acceptance->created_at)->get();
|
||||
}
|
||||
if ($logItem_res->isEmpty()){
|
||||
Log::debug('Acceptance date mismatch');
|
||||
return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.bad_data'));
|
||||
}
|
||||
$logItem = $logItem_res[0];
|
||||
}
|
||||
$email = $assetItem->assignedTo?->email;
|
||||
$locale = $assetItem->assignedTo?->locale;
|
||||
|
||||
if (is_null($email) || $email === '') {
|
||||
return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.no_email'));
|
||||
}
|
||||
|
||||
Mail::to($email)->send((new CheckoutAssetMail($assetItem, $assetItem->assignedTo, $logItem->user, $acceptance, $logItem->note, firstTimeSending: false))->locale($locale));
|
||||
$mailable = $this->getCheckoutMailType($acceptance, $logItem);
|
||||
Mail::to($email)->send($mailable->locale($locale));
|
||||
|
||||
return redirect()->route('reports/unaccepted_assets')->with('success', trans('admin/reports/general.reminder_sent'));
|
||||
}
|
||||
private function getCheckoutMailType(CheckoutAcceptance $acceptance, $logItem) : Mailable
|
||||
{
|
||||
$lookup = [
|
||||
Accessory::class => CheckoutAccessoryMail::class,
|
||||
Asset::class => CheckoutAssetMail::class,
|
||||
LicenseSeat::class => CheckoutLicenseMail::class,
|
||||
Consumable::class => CheckoutConsumableMail::class,
|
||||
Component::class => CheckoutComponentMail::class,
|
||||
];
|
||||
$mailable= $lookup[get_class($acceptance->checkoutable)];
|
||||
|
||||
return new $mailable($acceptance->checkoutable,
|
||||
$acceptance->checkedOutTo ?? $acceptance->assignedTo,
|
||||
$logItem->adminuser,
|
||||
$acceptance,
|
||||
$acceptance->note);
|
||||
|
||||
}
|
||||
/**
|
||||
* sentAssetAcceptanceReminder
|
||||
*
|
||||
@@ -1215,31 +1264,41 @@ class ReportsController extends Controller
|
||||
public function postAssetAcceptanceReport($deleted = false) : Response
|
||||
{
|
||||
$this->authorize('reports.view');
|
||||
$showDeleted = $deleted == 'deleted';
|
||||
$showDeleted = request('deleted') === 'deleted';;
|
||||
|
||||
/**
|
||||
* Get all assets with pending checkout acceptances
|
||||
*/
|
||||
if($showDeleted) {
|
||||
$acceptances = CheckoutAcceptance::pending()->where('checkoutable_type', 'App\Models\Asset')->withTrashed()->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model'])->get();
|
||||
} else {
|
||||
$acceptances = CheckoutAcceptance::pending()->where('checkoutable_type', 'App\Models\Asset')->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model'])->get();
|
||||
}
|
||||
|
||||
$assetsForReport = $acceptances
|
||||
->filter(function($acceptance) {
|
||||
return $acceptance->checkoutable_type == 'App\Models\Asset';
|
||||
})
|
||||
->map(function($acceptance) {
|
||||
return ['assetItem' => $acceptance->checkoutable, 'acceptance' => $acceptance];
|
||||
});
|
||||
$acceptances = CheckoutAcceptance::pending()
|
||||
->with([
|
||||
'checkoutable' => function (MorphTo $acceptance) {
|
||||
$acceptance->withTrashed()->morphWith([
|
||||
Asset::class => ['model.category', 'assignedTo', 'company'],
|
||||
Accessory::class => ['category','checkouts', 'company'],
|
||||
LicenseSeat::class => ['user', 'license'],
|
||||
Component::class => ['assignedTo', 'company'],
|
||||
Consumable::class => ['company'],
|
||||
]);
|
||||
},
|
||||
'assignedTo',
|
||||
])->orderByDesc('checkout_acceptances.created_at');
|
||||
|
||||
if ($showDeleted) {
|
||||
$acceptances->withTrashed();
|
||||
}
|
||||
|
||||
$itemsForReport = $acceptances->get()->map(fn ($unaccepted) => Checkoutable::fromAcceptance($unaccepted));
|
||||
|
||||
$rows = [];
|
||||
|
||||
$header = [
|
||||
trans('general.date'),
|
||||
trans('general.type'),
|
||||
trans('admin/companies/table.title'),
|
||||
trans('general.category'),
|
||||
trans('admin/hardware/form.model'),
|
||||
trans('admin/hardware/form.name'),
|
||||
trans('general.name'),
|
||||
trans('admin/hardware/table.asset_tag'),
|
||||
trans('admin/hardware/table.checkoutto'),
|
||||
];
|
||||
@@ -1247,16 +1306,19 @@ class ReportsController extends Controller
|
||||
$header = array_map('trim', $header);
|
||||
$rows[] = implode(',', $header);
|
||||
|
||||
foreach ($assetsForReport as $item) {
|
||||
foreach ($itemsForReport as $item) {
|
||||
|
||||
if ($item['assetItem'] != null){
|
||||
if ($item != null){
|
||||
|
||||
$row = [ ];
|
||||
$row[] = str_replace(',', '', e($item['assetItem']->model->category->name));
|
||||
$row[] = str_replace(',', '', e($item['assetItem']->model->name));
|
||||
$row[] = str_replace(',', '', e($item['assetItem']->name));
|
||||
$row[] = str_replace(',', '', e($item['assetItem']->asset_tag));
|
||||
$row[] = str_replace(',', '', e(($item['acceptance']->assignedTo) ? $item['acceptance']->assignedTo->display_name : trans('admin/reports/general.deleted_user')));
|
||||
$row[] = str_replace(',', '', $item->acceptance->created_at);
|
||||
$row[] = str_replace(',', '', $item->type);
|
||||
$row[] = str_replace(',', '', $item->plain_text_company);
|
||||
$row[] = str_replace(',', '', $item->plain_text_category);
|
||||
$row[] = str_replace(',', '', $item->plain_text_model);
|
||||
$row[] = str_replace(',', '', $item->plain_text_name);
|
||||
$row[] = str_replace(',', '', $item->asset_tag);
|
||||
$row[] = str_replace(',', '', ($item->acceptance->assignedto) ? $item->acceptance->assignedto->display_name : trans('admin/reports/general.deleted_user'));
|
||||
$rows[] = implode(',', $row);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -772,6 +772,7 @@ class SettingsController extends Controller
|
||||
$setting->label2_asset_logo = $request->input('label2_asset_logo');
|
||||
$setting->label2_1d_type = $request->input('label2_1d_type');
|
||||
$setting->label2_2d_type = $request->input('label2_2d_type');
|
||||
$setting->label2_2d_prefix = $request->input('label2_2d_prefix');
|
||||
$setting->label2_2d_target = $request->input('label2_2d_target');
|
||||
$setting->label2_fields = $request->input('label2_fields');
|
||||
$setting->label2_empty_row_count = $request->input('label2_empty_row_count');
|
||||
|
||||
@@ -2,10 +2,18 @@
|
||||
|
||||
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\Models\Supplier;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use \Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\MessageBag;
|
||||
|
||||
/**
|
||||
* This controller handles all actions related to Suppliers for
|
||||
@@ -118,30 +126,41 @@ class SuppliersController extends Controller
|
||||
*
|
||||
* @param int $supplierId
|
||||
*/
|
||||
public function destroy($supplierId) : RedirectResponse
|
||||
public function destroy(Supplier $supplier): RedirectResponse
|
||||
{
|
||||
$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))) {
|
||||
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.not_found'));
|
||||
try {
|
||||
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('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')
|
||||
);
|
||||
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);
|
||||
return view('suppliers/view', compact('supplier'));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +308,13 @@ class UsersController extends Controller
|
||||
$permissions_array['superuser'] = $orig_superuser;
|
||||
}
|
||||
|
||||
// Unset any of the inherited user permissions (0), since that behavior is the default anyway
|
||||
foreach ($permissions_array as $permission => $value) {
|
||||
if ($value == '0') {
|
||||
unset($permissions_array[$permission]);
|
||||
}
|
||||
|
||||
}
|
||||
$user->permissions = json_encode($permissions_array);
|
||||
|
||||
// Only save groups if the user is a superuser
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\CheckoutRequests\CancelCheckoutRequestAction;
|
||||
use App\Actions\CheckoutRequests\CreateCheckoutRequestAction;
|
||||
use App\Enums\ActionType;
|
||||
use App\Exceptions\AssetNotRequestable;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
@@ -201,7 +202,7 @@ class ViewAssetsController extends Controller
|
||||
if (($item_request = $item->isRequestedBy($user)) || $cancel_by_admin) {
|
||||
$item->cancelRequest($requestingUser);
|
||||
$data['item_quantity'] = ($item_request) ? $item_request->qty : 1;
|
||||
$logaction->logaction('request_canceled');
|
||||
$logaction->logaction(ActionType::RequestCanceled);
|
||||
|
||||
if (($settings->alert_email != '') && ($settings->alerts_enabled == '1') && (! config('app.lock_passwords'))) {
|
||||
$settings->notify(new RequestAssetCancelation($data));
|
||||
|
||||
@@ -14,6 +14,15 @@ class CustomAssetReportRequest extends Request
|
||||
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.
|
||||
*
|
||||
@@ -24,6 +33,7 @@ class CustomAssetReportRequest extends Request
|
||||
return [
|
||||
'purchase_start' => '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_end' => 'date|date_format:Y-m-d|nullable',
|
||||
'checkout_date_start' => 'date|date_format:Y-m-d|nullable',
|
||||
|
||||
@@ -109,7 +109,7 @@ class SettingsSamlRequest extends FormRequest
|
||||
];
|
||||
|
||||
$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,
|
||||
]);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class StoreAssetRequest extends ImageUploadRequest
|
||||
|
||||
public function prepareForValidation(): void
|
||||
{
|
||||
parent::prepareForValidation(); // call ImageUploadRequest thing
|
||||
// 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
|
||||
// provided to be caught by model level validation later.
|
||||
|
||||
32
app/Http/Requests/StoreDepartmentRequest.php
Normal file
32
app/Http/Requests/StoreDepartmentRequest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Department;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class StoreDepartmentRequest extends ImageUploadRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return Gate::allows('create', new Department);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$modelRules = (new Department)->getRules();
|
||||
|
||||
return array_merge(
|
||||
$modelRules,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ class StoreLabelSettings extends FormRequest
|
||||
'labels_pagewidth' => 'numeric|nullable',
|
||||
'labels_pageheight' => 'numeric|nullable',
|
||||
'qr_text' => 'max:31|nullable',
|
||||
'label2_2d_prefix' => 'nullable|max:191',
|
||||
'label2_template' => [
|
||||
'required',
|
||||
Rule::in($names),
|
||||
|
||||
@@ -32,7 +32,7 @@ class StoreNotificationSettings extends FormRequest
|
||||
],
|
||||
'alert_threshold' => 'numeric|nullable',
|
||||
'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',
|
||||
'audit_interval' => 'numeric|nullable|gt:0',
|
||||
];
|
||||
|
||||
@@ -28,23 +28,31 @@ class UpdateAssetRequest extends ImageUploadRequest
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
$setting = Setting::getSettings();
|
||||
|
||||
$rules = array_merge(
|
||||
parent::rules(),
|
||||
(new Asset)->getRules(),
|
||||
// this is to overwrite rulesets that include required, and rewrite unique_undeleted
|
||||
// This overwrites the rulesets that are set at the model level (via Watson) but are not necessarily required at the request level when doing a PATCH update.
|
||||
// Confusingly, this skips the unique_undeleted validator at the model level (and therefore the UniqueUndeletedTrait), so we have to re-add those
|
||||
// rules here without the requiredness, since those values will already exist if you're updating an existing asset.
|
||||
[
|
||||
'model_id' => ['integer', 'exists:models,id,deleted_at,NULL', 'not_array'],
|
||||
'status_id' => ['integer', 'exists:status_labels,id'],
|
||||
'asset_tag' => [
|
||||
'min:1', 'max:255', 'not_array',
|
||||
Rule::unique('assets', 'asset_tag')->ignore($this->asset)->withoutTrashed()
|
||||
Rule::unique('assets', 'asset_tag')->ignore($this->asset)->withoutTrashed(),
|
||||
],
|
||||
'serial' => [
|
||||
'string', 'max:255', 'not_array',
|
||||
$setting->unique_serial=='1' ? Rule::unique('assets', 'serial')->ignore($this->asset)->withoutTrashed() : 'nullable',
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
// if the purchase cost is passed in as a string **and** the digit_separator is ',' (as is common in the EU)
|
||||
// then we tweak the purchase_cost rule to make it a string
|
||||
if (Setting::getSettings()->digit_separator === '1.234,56' && is_string($this->input('purchase_cost'))) {
|
||||
if ($setting->digit_separator === '1.234,56' && is_string($this->input('purchase_cost'))) {
|
||||
$rules['purchase_cost'] = ['nullable', 'string'];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Traits;
|
||||
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -38,20 +39,13 @@ trait ConvertsBase64ToFiles
|
||||
if (!$base64Contents) {
|
||||
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
|
||||
$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) {
|
||||
$source = fopen($base64Contents, 'r');
|
||||
$source = fopen($base64Contents, 'r'); // PHP has special processing for "data:" URL's
|
||||
$destination = fopen($tempFilePath, 'w');
|
||||
|
||||
stream_copy_to_stream($source, $destination);
|
||||
@@ -59,7 +53,8 @@ trait ConvertsBase64ToFiles
|
||||
fclose($source);
|
||||
fclose($destination);
|
||||
} 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);
|
||||
|
||||
@@ -36,6 +36,7 @@ class AccessoriesTransformer
|
||||
'qty' => ($accessory->qty) ? (int) $accessory->qty : null,
|
||||
'purchase_date' => ($accessory->purchase_date) ? Helper::getFormattedDateObject($accessory->purchase_date, 'date') : null,
|
||||
'purchase_cost' => Helper::formatCurrencyOutput($accessory->purchase_cost),
|
||||
'total_cost' => Helper::formatCurrencyOutput($accessory->totalCostSum()),
|
||||
'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_amt' => ($accessory->min_amt) ? (int) $accessory->min_amt : null,
|
||||
|
||||
@@ -147,8 +147,9 @@ class ActionlogsTransformer
|
||||
[
|
||||
'url' => $actionlog->uploads_file_url(),
|
||||
'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,
|
||||
'mediatype' => StorageHelper::getMediaType($actionlog->uploads_file_path()),
|
||||
] : null,
|
||||
|
||||
'item' => ($actionlog->item) ? [
|
||||
|
||||
@@ -48,12 +48,15 @@ class AssetModelsTransformer
|
||||
'image' => ($assetmodel->image != '') ? Storage::disk('public')->url('models/'.e($assetmodel->image)) : null,
|
||||
'model_number' => ($assetmodel->model_number ? e($assetmodel->model_number): null),
|
||||
'min_amt' => ($assetmodel->min_amt) ? (int) $assetmodel->min_amt : null,
|
||||
'remaining' => (int) ($assetmodel->assets_count - $assetmodel->min_amt),
|
||||
|
||||
'depreciation' => ($assetmodel->depreciation) ? [
|
||||
'id' => (int) $assetmodel->depreciation->id,
|
||||
'name'=> e($assetmodel->depreciation->name),
|
||||
] : null,
|
||||
'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) ? [
|
||||
'id' => (int) $assetmodel->category->id,
|
||||
'name'=> e($assetmodel->category->name),
|
||||
@@ -65,7 +68,7 @@ class AssetModelsTransformer
|
||||
'default_fieldset_values' => $default_field_values,
|
||||
'eol' => ($assetmodel->eol > 0) ? $assetmodel->eol.' months' : 'None',
|
||||
'requestable' => ($assetmodel->requestable == '1') ? true : false,
|
||||
'require_serial' => $assetmodel->require_serial,
|
||||
'require_serial' => ($assetmodel->require_serial == '1') ? true : false,
|
||||
'notes' => Helper::parseEscapedMarkedownInline($assetmodel->notes),
|
||||
'created_by' => ($assetmodel->adminuser) ? [
|
||||
'id' => (int) $assetmodel->adminuser->id,
|
||||
|
||||
@@ -43,6 +43,7 @@ class ComponentsTransformer
|
||||
'order_number' => e($component->order_number),
|
||||
'purchase_date' => Helper::getFormattedDateObject($component->purchase_date, 'date'),
|
||||
'purchase_cost' => Helper::formatCurrencyOutput($component->purchase_cost),
|
||||
'total_cost' => Helper::formatCurrencyOutput($component->totalCostSum()),
|
||||
'remaining' => (int) $component->numRemaining(),
|
||||
'company' => ($component->company) ? [
|
||||
'id' => (int) $component->company->id,
|
||||
|
||||
@@ -37,6 +37,7 @@ class ConsumablesTransformer
|
||||
'remaining' => $consumable->numRemaining(),
|
||||
'order_number' => e($consumable->order_number),
|
||||
'purchase_cost' => Helper::formatCurrencyOutput($consumable->purchase_cost),
|
||||
'total_cost' => Helper::formatCurrencyOutput($consumable->totalCostSum()),
|
||||
'purchase_date' => Helper::getFormattedDateObject($consumable->purchase_date, 'date'),
|
||||
'qty' => (int) $consumable->qty,
|
||||
'notes' => ($consumable->notes) ? Helper::parseEscapedMarkedownInline($consumable->notes) : null,
|
||||
|
||||
@@ -43,7 +43,7 @@ class DepartmentsTransformer
|
||||
'id' => (int) $department->location->id,
|
||||
'name' => e($department->location->name),
|
||||
] : null,
|
||||
'users_count' => e($department->users_count),
|
||||
'users_count' => (int) ($department->users_count),
|
||||
'notes' => Helper::parseEscapedMarkedownInline($department->notes),
|
||||
'created_at' => Helper::getFormattedDateObject($department->created_at, 'datetime'),
|
||||
'updated_at' => Helper::getFormattedDateObject($department->updated_at, 'datetime'),
|
||||
|
||||
@@ -36,6 +36,12 @@ class LicenseSeatsTransformer
|
||||
'name' => e($seat->user->department->name),
|
||||
|
||||
] : null,
|
||||
'company'=> ($seat->user->company) ?
|
||||
[
|
||||
'id' => (int) $seat->user->company->id,
|
||||
'name' => e($seat->user->company->name),
|
||||
|
||||
] : null,
|
||||
'created_at' => Helper::getFormattedDateObject($seat->created_at, 'datetime'),
|
||||
] : null,
|
||||
'assigned_asset' => ($seat->asset) ? [
|
||||
|
||||
@@ -66,6 +66,7 @@ class MaintenancesTransformer
|
||||
'id' => $assetmaintenance->supplier->id,
|
||||
'name'=> e($assetmaintenance->supplier->name)
|
||||
] : null,
|
||||
'url' => ($assetmaintenance->url) ? e($assetmaintenance->url) : null,
|
||||
'cost' => Helper::formatCurrencyOutput($assetmaintenance->cost),
|
||||
'asset_maintenance_type' => e($assetmaintenance->asset_maintenance_type),
|
||||
'start_date' => Helper::getFormattedDateObject($assetmaintenance->start_date, 'date'),
|
||||
|
||||
@@ -68,7 +68,8 @@ class UsersTransformer
|
||||
] : null,
|
||||
'notes'=> Helper::parseEscapedMarkedownInline($user->notes),
|
||||
'role' => $role,
|
||||
'permissions' => $user->decodePermissions(),
|
||||
'user_permissions' => $user->decodePermissions(),
|
||||
'effective_permissions' => $user->getEffectivePermissions('true'),
|
||||
'activated' => ($user->activated == '1') ? true : false,
|
||||
'autoassign_licenses' => ($user->autoassign_licenses == '1') ? true : false,
|
||||
'ldap_import' => ($user->ldap_import == '1') ? true : false,
|
||||
|
||||
@@ -44,7 +44,7 @@ class AssetImporter extends ItemImporter
|
||||
foreach ($this->customFields as $customField) {
|
||||
$customFieldValue = $this->array_smart_custom_field_fetch($row, $customField);
|
||||
|
||||
if ($customFieldValue) {
|
||||
if (!is_null($customFieldValue)) {
|
||||
if ($customField->field_encrypted == 1) {
|
||||
$this->item['custom_fields'][$customField->db_column_name()] = Crypt::encrypt($customFieldValue);
|
||||
$this->log('Custom Field '.$customField->name.': '.Crypt::encrypt($customFieldValue));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Component;
|
||||
|
||||
class CategoryEditForm extends Component
|
||||
@@ -12,43 +13,25 @@ class CategoryEditForm extends Component
|
||||
|
||||
public $eulaText;
|
||||
|
||||
public $originalSendCheckInEmailValue;
|
||||
|
||||
public bool $requireAcceptance;
|
||||
|
||||
public bool $sendCheckInEmail;
|
||||
|
||||
public bool $useDefaultEula;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->originalSendCheckInEmailValue = $this->sendCheckInEmail;
|
||||
|
||||
if ($this->eulaText || $this->useDefaultEula) {
|
||||
$this->sendCheckInEmail = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.category-edit-form');
|
||||
}
|
||||
|
||||
public function updated($property, $value)
|
||||
{
|
||||
if (! in_array($property, ['eulaText', 'useDefaultEula'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sendCheckInEmail = $this->eulaText || $this->useDefaultEula ? 1 : $this->originalSendCheckInEmailValue;
|
||||
}
|
||||
|
||||
public function getShouldDisplayEmailMessageProperty(): bool
|
||||
#[Computed]
|
||||
public function emailWillBeSendDueToEula(): bool
|
||||
{
|
||||
return $this->eulaText || $this->useDefaultEula;
|
||||
}
|
||||
|
||||
public function getEmailMessageProperty(): string
|
||||
#[Computed]
|
||||
public function emailMessage(): string
|
||||
{
|
||||
if ($this->useDefaultEula) {
|
||||
return trans('admin/categories/general.email_will_be_sent_due_to_global_eula');
|
||||
@@ -57,13 +40,9 @@ class CategoryEditForm extends Component
|
||||
return trans('admin/categories/general.email_will_be_sent_due_to_category_eula');
|
||||
}
|
||||
|
||||
public function getEulaTextDisabledProperty()
|
||||
#[Computed]
|
||||
public function eulaTextDisabled()
|
||||
{
|
||||
return (bool)$this->useDefaultEula;
|
||||
}
|
||||
|
||||
public function getSendCheckInEmailDisabledProperty()
|
||||
{
|
||||
return $this->eulaText || $this->useDefaultEula;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ class SlackSettingsForm extends Component
|
||||
]);
|
||||
|
||||
try {
|
||||
$test = $webhook->post($this->webhook_endpoint, ['body' => $payload, ['headers' => ['Content-Type' => 'application/json']]]);
|
||||
$test = $webhook->post($this->webhook_endpoint, ['body' => $payload, 'headers' => ['Content-Type' => 'application/json']]);
|
||||
|
||||
if(($test->getStatusCode() == 302)||($test->getStatusCode() == 301)){
|
||||
return session()->flash('error' , trans('admin/settings/message.webhook.error_redirect', ['endpoint' => $this->webhook_endpoint]));
|
||||
|
||||
@@ -87,7 +87,7 @@ class CheckoutAssetMail extends Mailable
|
||||
$name = $this->target->assignedto?->display_name;
|
||||
}
|
||||
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
|
||||
|
||||
@@ -17,10 +17,11 @@ class SendUpcomingAuditMail extends Mailable
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($params, $threshold)
|
||||
public function __construct($params, $threshold, $total)
|
||||
{
|
||||
$this->assets = $params;
|
||||
$this->threshold = $threshold;
|
||||
$this->total = $total;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +33,7 @@ class SendUpcomingAuditMail extends Mailable
|
||||
|
||||
return new Envelope(
|
||||
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: [
|
||||
'assets' => $this->assets,
|
||||
'threshold' => $this->threshold,
|
||||
'total' => $this->total,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Helpers\Helper;
|
||||
use App\Models\Traits\Acceptable;
|
||||
use App\Models\Traits\CompanyableTrait;
|
||||
use App\Models\Traits\HasUploads;
|
||||
use App\Models\Traits\Loggable;
|
||||
use App\Models\Traits\Searchable;
|
||||
use App\Presenters\Presentable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -357,6 +358,10 @@ class Accessory extends SnipeModel
|
||||
|
||||
$accessory_checkout->limit(1)->delete();
|
||||
}
|
||||
public function totalCostSum() {
|
||||
|
||||
return $this->purchase_cost !== null ? $this->qty * $this->purchase_cost : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------
|
||||
@@ -386,7 +391,91 @@ class Accessory extends SnipeModel
|
||||
* BEGIN QUERY SCOPES
|
||||
* -----------------------------------------------
|
||||
**/
|
||||
/**
|
||||
* Query builder scope to search on text filters for complex Bootstrap Tables API
|
||||
*
|
||||
* @param \Illuminate\Database\Query\Builder $query Query builder instance
|
||||
* @param text $filter JSON array of search keys and terms
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder Modified query builder
|
||||
*/
|
||||
|
||||
public function scopeByFilter($query, $filter)
|
||||
{
|
||||
return $query->where(
|
||||
function ($query) use ($filter) {
|
||||
foreach ($filter as $fieldname => $search_val) {
|
||||
|
||||
if ($fieldname == 'name') {
|
||||
$query->where('accessories.name', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'notes') {
|
||||
$query->where('accessories.notes', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'model_number') {
|
||||
$query->where('accessories.model_number', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'order_number') {
|
||||
$query->where('accessories.order_number', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'purchase_cost') {
|
||||
$query->where('accessories.purchase_cost', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'location') {
|
||||
$query->whereHas(
|
||||
'location', function ($query) use ($search_val) {
|
||||
$query->where('locations.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ($fieldname == 'manufacturer') {
|
||||
$query->whereHas(
|
||||
'manufacturer', function ($query) use ($search_val) {
|
||||
$query->where('manufacturers.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if ($fieldname == 'supplier') {
|
||||
$query->whereHas(
|
||||
'supplier', function ($query) use ($search_val) {
|
||||
$query->where('suppliers.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if ($fieldname == 'category') {
|
||||
$query->whereHas(
|
||||
'category', function ($query) use ($search_val) {
|
||||
$query->where('categories.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ($fieldname == 'company') {
|
||||
$query->whereHas(
|
||||
'company', function ($query) use ($search_val) {
|
||||
$query->where('companies.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query builder scope to order on created_by name
|
||||
|
||||
@@ -9,6 +9,7 @@ use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Enums\ActionType;
|
||||
|
||||
/**
|
||||
* Model for the Actionlog (the table that keeps a historical log of
|
||||
@@ -335,9 +336,12 @@ class Actionlog extends SnipeModel
|
||||
* @since [v3.0]
|
||||
* @return bool
|
||||
*/
|
||||
public function logaction($actiontype)
|
||||
public function logaction(string|ActionType $actiontype)
|
||||
{
|
||||
$this->action_type = $actiontype;
|
||||
if (is_string($actiontype)) {
|
||||
$actiontype = ActionType::from($actiontype);
|
||||
}
|
||||
$this->action_type = $actiontype->value;
|
||||
$this->remote_ip = request()->ip();
|
||||
$this->user_agent = request()->header('User-Agent');
|
||||
$this->action_source = $this->determineActionSource();
|
||||
@@ -360,7 +364,7 @@ class Actionlog extends SnipeModel
|
||||
{
|
||||
$now = Carbon::now();
|
||||
$last_audit_date = $this->created_at; // this is the action log's created at, not the asset itself
|
||||
$next_audit = $last_audit_date->addMonth($monthInterval); // this actually *modifies* the $last_audit_date
|
||||
$next_audit = $last_audit_date->addMonth((int) $monthInterval); // this actually *modifies* the $last_audit_date
|
||||
$next_audit_days = (int) round($now->diffInDays($next_audit, true));
|
||||
$override_default_next = $next_audit;
|
||||
|
||||
@@ -478,6 +482,10 @@ class Actionlog extends SnipeModel
|
||||
$object = 'models';
|
||||
}
|
||||
|
||||
if ($this->action_type == 'audit') {
|
||||
$object = 'audits';
|
||||
}
|
||||
|
||||
return route('ui.files.show', [
|
||||
'object_type' => $object,
|
||||
'id' => $this->item_id,
|
||||
@@ -493,6 +501,10 @@ class Actionlog extends SnipeModel
|
||||
return 'private_uploads/eula-pdfs/'.$this->filename;
|
||||
}
|
||||
|
||||
if ($this->action_type == 'audit') {
|
||||
return 'private_uploads/audits/'.$this->filename;
|
||||
}
|
||||
|
||||
switch ($this->item_type) {
|
||||
case Accessory::class:
|
||||
return 'private_uploads/accessories/'.$this->filename;
|
||||
@@ -510,6 +522,8 @@ class Actionlog extends SnipeModel
|
||||
return 'private_uploads/locations/'.$this->filename;
|
||||
case Maintenance::class:
|
||||
return 'private_uploads/maintenances/'.$this->filename;
|
||||
case Supplier::class:
|
||||
return 'private_uploads/suppliers/'.$this->filename;
|
||||
case User::class:
|
||||
return 'private_uploads/users/'.$this->filename;
|
||||
default:
|
||||
|
||||
@@ -9,6 +9,8 @@ use App\Http\Traits\UniqueUndeletedTrait;
|
||||
use App\Models\Traits\Acceptable;
|
||||
use App\Models\Traits\CompanyableTrait;
|
||||
use App\Models\Traits\HasUploads;
|
||||
use App\Models\Traits\Loggable;
|
||||
use App\Models\Traits\Requestable;
|
||||
use App\Models\Traits\Searchable;
|
||||
use App\Presenters\AssetPresenter;
|
||||
use App\Presenters\Presentable;
|
||||
@@ -227,7 +229,6 @@ class Asset extends Depreciable
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function customFieldValidationRules()
|
||||
{
|
||||
|
||||
@@ -266,7 +267,6 @@ class Asset extends Depreciable
|
||||
return parent::save($params);
|
||||
}
|
||||
|
||||
|
||||
public function getDisplayNameAttribute()
|
||||
{
|
||||
return $this->present()->name();
|
||||
@@ -277,22 +277,136 @@ class Asset extends Depreciable
|
||||
*
|
||||
* @return \Carbon\Carbon|null
|
||||
*/
|
||||
public function getWarrantyExpiresAttribute()
|
||||
|
||||
|
||||
protected function warrantyExpires(): Attribute
|
||||
{
|
||||
if (isset($this->attributes['warranty_months']) && isset($this->attributes['purchase_date'])) {
|
||||
if (is_string($this->attributes['purchase_date']) || is_string($this->attributes['purchase_date'])) {
|
||||
$purchase_date = \Carbon\Carbon::parse($this->attributes['purchase_date']);
|
||||
} else {
|
||||
$purchase_date = \Carbon\Carbon::instance($this->attributes['purchase_date']);
|
||||
}
|
||||
$purchase_date->setTime(0, 0, 0);
|
||||
|
||||
return $purchase_date->addMonths((int) $this->attributes['warranty_months']);
|
||||
}
|
||||
|
||||
return null;
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value, array $attributes) => ($attributes['warranty_months'] && $attributes['purchase_date']) ? Carbon::parse($attributes['purchase_date'])->addMonths((int)$attributes['warranty_months']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
protected function warrantyExpiresFormattedDate(): Attribute
|
||||
{
|
||||
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value, array $attributes) => Helper::getFormattedDateObject($this->warrantyExpires, 'date', false)
|
||||
);
|
||||
}
|
||||
|
||||
protected function warrantyExpiresDiff(): Attribute
|
||||
{
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value, array $attributes) => $this->warrantyExpires ? round((Carbon::now()->diffInDays($this->warrantyExpires))) : null,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
protected function warrantyExpiresDiffForHumans(): Attribute
|
||||
{
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value, array $attributes) => $this->warrantyExpires ? Carbon::parse($this->warrantyExpires)->diffForHumans() : null,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected function lastAuditFormattedDate(): Attribute
|
||||
{
|
||||
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value, array $attributes) => Helper::getFormattedDateObject($this->last_audit_date, 'datetime', false)
|
||||
);
|
||||
}
|
||||
|
||||
protected function lastAuditDiff(): Attribute
|
||||
{
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value, array $attributes) => $this->warrantyExpires ? round((Carbon::now()->diffInDays($this->warrantyExpires))) : null,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
protected function lastAuditDiffForHumans(): Attribute
|
||||
{
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value, array $attributes) => $attributes['last_audit_date'] ? Carbon::parse($attributes['last_audit_date'])->diffForHumans() : null,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
protected function nextAuditFormattedDate(): Attribute
|
||||
{
|
||||
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value, array $attributes) => Helper::getFormattedDateObject($this->next_audit_date, 'date', false)
|
||||
);
|
||||
}
|
||||
|
||||
protected function nextAuditDiffInDays(): Attribute
|
||||
{
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value, array $attributes) => $attributes['next_audit_date'] ? Carbon::now()->diffInDays($attributes['next_audit_date']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
protected function nextAuditDiffForHumans(): Attribute
|
||||
{
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value, array $attributes) => $attributes['next_audit_date'] ? Carbon::parse($attributes['next_audit_date'])->diffForHumans() : null,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
protected function eolDate(): Attribute
|
||||
{
|
||||
|
||||
return Attribute:: make(
|
||||
get: function(mixed $value, array $attributes) {
|
||||
if ($attributes['asset_eol_date'] && $attributes['eol_explicit'] == '1') {
|
||||
return Carbon::parse($attributes['asset_eol_date']);
|
||||
} elseif ($attributes['purchase_date'] && $this->model && ((int) $this->model->eol > 0)) {
|
||||
return Carbon::parse($attributes['purchase_date'])->addMonths((int) $this->model->eol);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected function eolFormattedDate(): Attribute
|
||||
{
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value, array $attributes) => $this->eolDate ? Helper::getFormattedDateObject($this->eolDate, 'date', false) : null,
|
||||
);
|
||||
}
|
||||
|
||||
protected function eolDiffInDays(): Attribute
|
||||
{
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value, array $attributes) => $this->eolDate ? round((Carbon::now()->diffInDays(Carbon::parse($this->eolDate), false, 1))) : null,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
protected function eolDiffForHumans(): Attribute
|
||||
{
|
||||
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value, array $attributes) => $this->eolDate ? Carbon::parse($this->eolDate)->diffForHumans() : null,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
protected function expectedCheckinFormattedDate(): Attribute
|
||||
{
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value, array $attributes) => array_key_exists('expected_checkin', $attributes) ? Helper::getFormattedDateObject($attributes['expected_checkin'], 'date', false) : null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes the asset -> company relationship
|
||||
@@ -825,25 +939,37 @@ class Asset extends Depreciable
|
||||
*/
|
||||
public static function getExpiringWarrantyOrEol($days = 30)
|
||||
{
|
||||
|
||||
return self::where('archived', '=', '0')
|
||||
$now = now();
|
||||
$end = now()->addDays($days);
|
||||
|
||||
$expired_assets = self::query()
|
||||
->where('archived', '=', '0')
|
||||
->NotArchived()
|
||||
->whereNull('deleted_at')
|
||||
->where(function ($query) use ($days) {
|
||||
// Check for manual asset EOL first
|
||||
$query->where(function ($query) use ($days) {
|
||||
$query->whereNotNull('asset_eol_date')
|
||||
->whereBetween('asset_eol_date', [Carbon::now(), Carbon::now()->addDays($days)]);
|
||||
// Otherwise use the warranty months + purchase date + threshold
|
||||
})->orWhere(function ($query) use ($days) {
|
||||
$query->whereNotNull('purchase_date')
|
||||
->whereNotNull('warranty_months')
|
||||
->whereBetween('purchase_date', [Carbon::now(), Carbon::now()->addMonths('assets.warranty_months')->addDays($days)]);
|
||||
});
|
||||
})
|
||||
->orderBy('asset_eol_date', 'ASC')
|
||||
->orderBy('purchase_date', 'ASC')
|
||||
->whereNotNull('asset_eol_date')
|
||||
->whereBetween('asset_eol_date', [$now, $end])
|
||||
->get();
|
||||
|
||||
$assets_with_warranties = self::query()
|
||||
->where('archived', '=', '0')
|
||||
->NotArchived()
|
||||
->whereNull('deleted_at')
|
||||
->whereNotNull('purchase_date')
|
||||
->whereNotNull('warranty_months')
|
||||
->get();
|
||||
|
||||
$expired_warranties = $assets_with_warranties->filter(function ($asset) use ($now, $end) {
|
||||
$expiration_window = Carbon::parse($asset->purchase_date)->addMonths((int) $asset->warranty_months);
|
||||
|
||||
return $expiration_window->betweenIncluded($now, $end);
|
||||
});
|
||||
return $expired_assets->concat($expired_warranties)
|
||||
->unique('id')
|
||||
->sortBy([
|
||||
['asset_eol_date', 'ASC'],
|
||||
['purchase_date', 'ASC']
|
||||
])
|
||||
->values();
|
||||
}
|
||||
|
||||
|
||||
@@ -1775,16 +1901,32 @@ class Asset extends Depreciable
|
||||
);
|
||||
}
|
||||
|
||||
if ($fieldname =='assigned_to') {
|
||||
if ($fieldname == 'assigned_to') {
|
||||
$query->whereHasMorph(
|
||||
'assignedTo', [User::class], function ($query) use ($search_val) {
|
||||
$query->where(
|
||||
function ($query) use ($search_val) {
|
||||
$query->where('users.first_name', 'LIKE', '%'.$search_val.'%')
|
||||
->orWhere('users.last_name', 'LIKE', '%'.$search_val.'%');
|
||||
->orWhere('users.last_name', 'LIKE', '%'.$search_val.'%')
|
||||
->orWhere('users.username', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
)->orWhereHasMorph(
|
||||
'assignedTo', [Location::class], function ($query) use ($search_val) {
|
||||
$query->where('locations.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
)->orWhereHasMorph(
|
||||
'assignedTo', [Asset::class], function ($query) use ($search_val) {
|
||||
$query->where(
|
||||
function ($query) use ($search_val) {
|
||||
// Don't use the asset table prefix here because it will pull from the original asset,
|
||||
// not the subselect we're doing here to get the assigned asset
|
||||
$query->where('name', 'LIKE', '%'.$search_val.'%')
|
||||
->orWhere('asset_tag', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1824,51 +1966,44 @@ class Asset extends Depreciable
|
||||
}
|
||||
|
||||
if ($fieldname == 'model') {
|
||||
$query->where(
|
||||
function ($query) use ($search_val) {
|
||||
$query->whereHas(
|
||||
'model', function ($query) use ($search_val) {
|
||||
$query->where('models.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
$query->whereHas(
|
||||
'model', function ($query) use ($search_val) {
|
||||
$query->where('models.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if ($fieldname == 'model_number') {
|
||||
$query->where(
|
||||
function ($query) use ($search_val) {
|
||||
$query->whereHas(
|
||||
'model', function ($query) use ($search_val) {
|
||||
$query->where('models.model_number', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
$query->whereHas(
|
||||
'model', function ($query) use ($search_val) {
|
||||
$query->where('models.model_number', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if ($fieldname == 'company') {
|
||||
$query->where(
|
||||
function ($query) use ($search_val) {
|
||||
$query->whereHas(
|
||||
'company', function ($query) use ($search_val) {
|
||||
$query->where('companies.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
$query->whereHas(
|
||||
'company', function ($query) use ($search_val) {
|
||||
$query->where('companies.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ($fieldname == 'supplier') {
|
||||
$query->where(
|
||||
function ($query) use ($search_val) {
|
||||
$query->whereHas(
|
||||
'supplier', function ($query) use ($search_val) {
|
||||
$query->where('suppliers.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
$query->whereHas(
|
||||
'supplier', function ($query) use ($search_val) {
|
||||
$query->where('suppliers.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ($fieldname == 'status_label') {
|
||||
$query->whereHas(
|
||||
'assetstatus', function ($query) use ($search_val) {
|
||||
$query->where('status_labels.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,18 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Http\Traits\TwoColumnUniqueUndeletedTrait;
|
||||
use App\Models\Traits\HasUploads;
|
||||
use App\Models\Traits\Loggable;
|
||||
use App\Models\Traits\Requestable;
|
||||
use App\Models\Traits\Searchable;
|
||||
use App\Presenters\AssetModelPresenter;
|
||||
use App\Presenters\Presentable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Watson\Validating\ValidatingTrait;
|
||||
use \App\Presenters\AssetModelPresenter;
|
||||
use App\Http\Traits\TwoColumnUniqueUndeletedTrait;
|
||||
|
||||
/**
|
||||
* Model for Asset Models. Asset Models contain higher level
|
||||
@@ -122,6 +124,22 @@ class AssetModel extends SnipeModel
|
||||
return $this->hasMany(\App\Models\Asset::class, 'model_id');
|
||||
}
|
||||
|
||||
|
||||
public function availableAssets()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Asset::class, 'model_id')->RTD();
|
||||
}
|
||||
|
||||
public function assignedAssets()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Asset::class, 'model_id')->Deployed();
|
||||
}
|
||||
|
||||
public function archivedAssets()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Asset::class, 'model_id')->Archived();
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes the model -> category relationship
|
||||
*
|
||||
@@ -240,6 +258,57 @@ class AssetModel extends SnipeModel
|
||||
* -----------------------------------------------
|
||||
**/
|
||||
|
||||
/**
|
||||
* Query builder scope to search on text filters for complex Bootstrap Tables API
|
||||
*
|
||||
* @param \Illuminate\Database\Query\Builder $query Query builder instance
|
||||
* @param text $filter JSON array of search keys and terms
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder Modified query builder
|
||||
*/
|
||||
public function scopeByFilter($query, $filter)
|
||||
{
|
||||
return $query->where(
|
||||
function ($query) use ($filter) {
|
||||
foreach ($filter as $fieldname => $search_val) {
|
||||
|
||||
if ($fieldname == 'name') {
|
||||
$query->where('models.name', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'notes') {
|
||||
$query->where('models.notes', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'model_number') {
|
||||
$query->where('models.model_number', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'category') {
|
||||
$query->whereHas(
|
||||
'category', function ($query) use ($search_val) {
|
||||
$query->where('categories.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ($fieldname == 'manufacturer') {
|
||||
$query->whereHas(
|
||||
'manufacturer', function ($query) use ($search_val) {
|
||||
$query->where('manufacturers.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* scopeInCategory
|
||||
* Get all models that are in the array of category ids
|
||||
|
||||
@@ -290,6 +290,35 @@ class Category extends SnipeModel
|
||||
* -----------------------------------------------
|
||||
**/
|
||||
|
||||
/**
|
||||
* Query builder scope to search on text filters for complex Bootstrap Tables API
|
||||
*
|
||||
* @param \Illuminate\Database\Query\Builder $query Query builder instance
|
||||
* @param text $filter JSON array of search keys and terms
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder Modified query builder
|
||||
*/
|
||||
public function scopeByFilter($query, $filter)
|
||||
{
|
||||
return $query->where(
|
||||
function ($query) use ($filter) {
|
||||
foreach ($filter as $fieldname => $search_val) {
|
||||
|
||||
if ($fieldname == 'name') {
|
||||
$query->where('categories.name', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'category_type') {
|
||||
$query->where('categories.category_type', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query builder scope for whether or not the category requires acceptance
|
||||
*
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use TCPDF;
|
||||
|
||||
class CheckoutAcceptance extends Model
|
||||
{
|
||||
@@ -129,8 +132,7 @@ class CheckoutAcceptance extends Model
|
||||
/**
|
||||
* Filter checkout acceptences by the user
|
||||
*
|
||||
* @param Illuminate\Database\Eloquent\Builder $query
|
||||
* @param User $user
|
||||
* @param User $user
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeForUser(Builder $query, User $user)
|
||||
@@ -141,7 +143,6 @@ class CheckoutAcceptance extends Model
|
||||
/**
|
||||
* Filter to only get pending acceptances
|
||||
*
|
||||
* @param Illuminate\Database\Eloquent\Builder $query
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopePending(Builder $query)
|
||||
@@ -153,4 +154,110 @@ class CheckoutAcceptance extends Model
|
||||
{
|
||||
return $query->whereNull('accepted_at')->whereNotNull('declined_at');
|
||||
}
|
||||
|
||||
protected function displayCheckoutableType(): Attribute
|
||||
{
|
||||
return Attribute:: make(
|
||||
get: fn(mixed $value) => strtolower(str_replace('App\Models\\', '', $this->checkoutable_type)),
|
||||
);
|
||||
}
|
||||
|
||||
public function generateAcceptancePdf($data, $pdf_filename) {
|
||||
|
||||
// set some language dependent data:
|
||||
$lg = Array();
|
||||
$lg['a_meta_charset'] = 'UTF-8';
|
||||
$lg['w_page'] = 'page';
|
||||
|
||||
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
|
||||
$pdf->setRTL(false);
|
||||
$pdf->setLanguageArray($lg);
|
||||
$pdf->SetFontSubsetting(true);
|
||||
$pdf->SetCreator('Snipe-IT Asset Management System');
|
||||
$pdf->SetAuthor($data['assigned_to']);
|
||||
$pdf->SetTitle('Asset Acceptance: '.$data['item_tag']);
|
||||
$pdf->SetSubject('Asset Acceptance: '.$data['item_tag']);
|
||||
$pdf->SetKeywords('Snipe-IT, assets, acceptance, eula, tos');
|
||||
$pdf->SetFont('dejavusans', '', 8, '', true);
|
||||
$pdf->SetPrintHeader(false);
|
||||
$pdf->SetPrintFooter(false);
|
||||
|
||||
$pdf->AddPage();
|
||||
if ($data['logo'] != null) {
|
||||
$pdf->writeHTML('<img src="@'.$data['logo'].'">', true, 0, true, 0, '');
|
||||
} else {
|
||||
$pdf->writeHTML('<h3>'.$data['site_name'].'</h3><br /><br />', true, 0, true, 0, 'C');
|
||||
}
|
||||
|
||||
$pdf->Ln();
|
||||
|
||||
// Check for CJK in the translation string for date. (This is a good proxy for the rest of the document)
|
||||
Helper::hasRtl(trans('general.date')) ? $pdf->setRTL(true) : $pdf->setRTL(false);
|
||||
Helper::isCjk(trans('general.date')) ? $pdf->SetFont('cid0cs', '', 9) : $pdf->SetFont('dejavusans', '', 8, '', true);
|
||||
|
||||
$pdf->writeHTML(trans('general.date') . ': ' . Helper::getFormattedDateObject(now(), 'datetime', false), true, 0, true, 0, '');
|
||||
|
||||
if ($data['company_name'] != null) {
|
||||
$pdf->writeHTML(trans('general.company') . ': ' . e($data['company_name']), true, 0, true, 0, '');
|
||||
}
|
||||
if ($data['item_tag'] != null) {
|
||||
$pdf->writeHTML(trans('general.asset_tag') . ': ' . e($data['item_tag']), true, 0, true, 0, '');
|
||||
}
|
||||
if ($data['item_name'] != null) {
|
||||
$pdf->writeHTML(trans('general.name') . ': ' . e($data['item_name']), true, 0, true, 0, '');
|
||||
}
|
||||
if ($data['item_model'] != null) {
|
||||
$pdf->writeHTML(trans('general.asset_model') . ': ' . e($data['item_model']), true, 0, true, 0, '');
|
||||
}
|
||||
if ($data['item_serial'] != null) {
|
||||
$pdf->writeHTML(trans('admin/hardware/form.serial').': '.e($data['item_serial']), true, 0, true, 0, '');
|
||||
}
|
||||
if (($data['qty'] != null) && ($data['qty'] > 1)) {
|
||||
$pdf->writeHTML(trans('general.qty').': '.e($data['qty']), true, 0, true, 0, '');
|
||||
}
|
||||
$pdf->writeHTML(trans('general.assignee').': '.e($data['assigned_to']) . ($data['employee_num'] ? ' ('.$data['employee_num'].')' : ''), true, 0, true, 0, '');
|
||||
if ($data['email'] != null) {
|
||||
$pdf->writeHTML(trans('general.email').': '.e($data['email']), true, 0, true, 0, '');
|
||||
}
|
||||
$pdf->Ln();
|
||||
$pdf->writeHTML('<hr>', true, 0, true, 0, '');
|
||||
|
||||
|
||||
// Break the EULA into lines based on newlines, and check each line for RTL or CJK characters
|
||||
$eula_lines = preg_split("/\r\n|\n|\r/", $data['eula']);
|
||||
|
||||
foreach ($eula_lines as $eula_line) {
|
||||
Helper::hasRtl($eula_line) ? $pdf->setRTL(true) : $pdf->setRTL(false);
|
||||
Helper::isCjk($eula_line) ? $pdf->SetFont('cid0cs', '', 9) : $pdf->SetFont('dejavusans', '', 8, '', true);
|
||||
$pdf->writeHTML(Helper::parseEscapedMarkedown($eula_line), true, 0, true, 0, '');
|
||||
}
|
||||
$pdf->Ln();
|
||||
$pdf->Ln();
|
||||
$pdf->setRTL(false);
|
||||
$pdf->Ln();
|
||||
|
||||
if ($data['signature'] != null) {
|
||||
$pdf->writeHTML('<img src="@'.$data['signature'].'">', true, 0, true, 0, '');
|
||||
$pdf->writeHTML('<hr>', true, 0, true, 0, '');
|
||||
$pdf->writeHTML(e($data['assigned_to']), true, 0, true, 0, 'C');
|
||||
$pdf->Ln();
|
||||
}
|
||||
|
||||
Helper::hasRtl(trans('general.notes')) ? $pdf->setRTL(true) : $pdf->setRTL(false);
|
||||
Helper::isCjk(trans('general.notes')) ? $pdf->SetFont('cid0cs', '', 9) : $pdf->SetFont('dejavusans', '', 8, '', true);
|
||||
|
||||
if ($data['note'] != null) {
|
||||
Helper::isCjk(trans('general.notes')) ? $pdf->SetFont('cid0cs', '', 9) : $pdf->SetFont('dejavusans', '', 8, '', true);
|
||||
$pdf->writeHTML(trans('general.notes') . ': ' . e($data['note']), true, 0, true, 0, '');
|
||||
$pdf->Ln();
|
||||
}
|
||||
|
||||
|
||||
$pdf->writeHTML(trans('general.assigned_date').': '.e($data['check_out_date']), true, 0, true, 0, '');
|
||||
$pdf->writeHTML(trans('general.accepted_date').': '.e($data['accepted_date']), true, 0, true, 0, '');
|
||||
|
||||
return $pdf->Output($pdf_filename, 'S');
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
79
app/Models/Checkoutable.php
Normal file
79
app/Models/Checkoutable.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class Checkoutable
|
||||
{
|
||||
public function __construct(
|
||||
public int $acceptance_id,
|
||||
public string $company,
|
||||
public string $category,
|
||||
public string $model,
|
||||
public string $asset_tag,
|
||||
public string $name,
|
||||
public string $type,
|
||||
public object $acceptance,
|
||||
public object $assignee,
|
||||
public readonly string $plain_text_category,
|
||||
public readonly string $plain_text_model,
|
||||
public readonly string $plain_text_name,
|
||||
public readonly string $plain_text_company,
|
||||
){}
|
||||
|
||||
public static function fromAcceptance(CheckoutAcceptance $unaccepted): self
|
||||
{
|
||||
$unaccepted_row = $unaccepted->checkoutable;
|
||||
$acceptance = $unaccepted;
|
||||
|
||||
$assignee = $acceptance->assignedTo;
|
||||
$company = $unaccepted_row->company ? optional($unaccepted_row->company)->present()->nameUrl() : '';
|
||||
$category = $model = $name = $tag = '';
|
||||
$type = $acceptance->checkoutable_item_type ?? '';
|
||||
|
||||
|
||||
if($unaccepted_row instanceof Asset){
|
||||
$category = optional($unaccepted_row->model?->category?->present())->nameUrl() ?? '';
|
||||
$model = optional($unaccepted_row->present())->modelUrl() ?? '';
|
||||
$name = optional($unaccepted_row->present())->nameUrl() ?? '';
|
||||
$tag = (string) ($unaccepted_row->asset_tag ?? '');
|
||||
}
|
||||
if($unaccepted_row instanceof Accessory){
|
||||
$category = optional($unaccepted_row->category?->present())->nameUrl() ?? '';
|
||||
$model = $unaccepted_row->model_number ?? '';
|
||||
$name = optional($unaccepted_row->present())->nameUrl() ?? '';
|
||||
}
|
||||
if($unaccepted_row instanceof LicenseSeat){
|
||||
$category = optional($unaccepted_row->license->category?->present())->nameUrl() ?? '';
|
||||
$company = optional($unaccepted_row->license->company?->present())?->nameUrl() ?? '';
|
||||
$model = '';
|
||||
$name = $unaccepted_row->license->present()->nameUrl() ?? '';
|
||||
}
|
||||
if($unaccepted_row instanceof Consumable){
|
||||
$category = optional($unaccepted_row->category?->present())->nameUrl() ?? '';
|
||||
$model = $unaccepted_row->model_number ?? '';
|
||||
$name = $unaccepted_row?->present()?->nameUrl() ?? '';
|
||||
}
|
||||
if($unaccepted_row instanceof Component){
|
||||
$category = optional($unaccepted_row->category?->present())->nameUrl() ?? '';
|
||||
$model = $unaccepted_row->model_number ?? '';
|
||||
$name = $unaccepted_row?->present()?->nameUrl() ?? '';
|
||||
}
|
||||
|
||||
return new self(
|
||||
acceptance_id: $acceptance->id,
|
||||
company: $company,
|
||||
category: $category,
|
||||
model: $model,
|
||||
asset_tag: $tag,
|
||||
name: $name,
|
||||
type: $type,
|
||||
acceptance: $acceptance,
|
||||
assignee: $assignee,
|
||||
//plain text for CSVs
|
||||
plain_text_category: ($unaccepted_row->model?->category?->name ?? $unaccepted_row->license->category?->name ?? $unaccepted_row->category?->name ?? ''),
|
||||
plain_text_model: ($unaccepted_row->model?->name ?? $unaccepted_row->model_number ?? ''),
|
||||
plain_text_name: ($unaccepted_row->name ?? $unaccepted_row->license?->name ?? ''),
|
||||
plain_text_company: ($unaccepted_row->company)->name ?? $unaccepted_row->license->company?->name ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Models;
|
||||
use App\Helpers\Helper;
|
||||
use App\Models\Traits\CompanyableTrait;
|
||||
use App\Models\Traits\HasUploads;
|
||||
use App\Models\Traits\Loggable;
|
||||
use App\Models\Traits\Searchable;
|
||||
use App\Presenters\Presentable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -274,7 +275,19 @@ class Component extends SnipeModel
|
||||
{
|
||||
return $this->category?->checkin_email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of checkouts for this License
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v2.0]
|
||||
* @return \Illuminate\Database\Eloquent\Relations\Relation
|
||||
*/
|
||||
public function checkouts()
|
||||
{
|
||||
return $this->assetlog()->where('action_type', '=', 'checkout')
|
||||
->orderBy('created_at', 'desc')
|
||||
->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check how many items within a component are remaining
|
||||
@@ -288,7 +301,10 @@ class Component extends SnipeModel
|
||||
return $this->qty - $this->numCheckedOut();
|
||||
}
|
||||
|
||||
public function totalCostSum() {
|
||||
|
||||
return $this->purchase_cost !== null ? $this->qty * $this->purchase_cost : null;
|
||||
}
|
||||
/**
|
||||
* -----------------------------------------------
|
||||
* BEGIN MUTATORS
|
||||
@@ -318,6 +334,97 @@ class Component extends SnipeModel
|
||||
* -----------------------------------------------
|
||||
**/
|
||||
|
||||
/**
|
||||
* Query builder scope to search on text filters for complex Bootstrap Tables API
|
||||
*
|
||||
* @param \Illuminate\Database\Query\Builder $query Query builder instance
|
||||
* @param text $filter JSON array of search keys and terms
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder Modified query builder
|
||||
*/
|
||||
public function scopeByFilter($query, $filter)
|
||||
{
|
||||
return $query->where(
|
||||
function ($query) use ($filter) {
|
||||
foreach ($filter as $fieldname => $search_val) {
|
||||
|
||||
if ($fieldname == 'name') {
|
||||
$query->where('components.name', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'notes') {
|
||||
$query->where('components.notes', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'model_number') {
|
||||
$query->where('components.model_number', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'order_number') {
|
||||
$query->where('components.order_number', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'serial') {
|
||||
$query->where('components.serial', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'serial') {
|
||||
$query->where('components.serial', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'purchase_cost') {
|
||||
$query->where('components.purchase_cost', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'location') {
|
||||
$query->whereHas(
|
||||
'location', function ($query) use ($search_val) {
|
||||
$query->where('locations.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ($fieldname == 'manufacturer') {
|
||||
$query->whereHas(
|
||||
'manufacturer', function ($query) use ($search_val) {
|
||||
$query->where('manufacturers.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if ($fieldname == 'supplier') {
|
||||
$query->whereHas(
|
||||
'supplier', function ($query) use ($search_val) {
|
||||
$query->where('suppliers.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if ($fieldname == 'category') {
|
||||
$query->whereHas(
|
||||
'category', function ($query) use ($search_val) {
|
||||
$query->where('categories.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ($fieldname == 'company') {
|
||||
$query->whereHas(
|
||||
'company', function ($query) use ($search_val) {
|
||||
$query->where('companies.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query builder scope to order on company
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Helpers\Helper;
|
||||
use App\Models\Traits\Acceptable;
|
||||
use App\Models\Traits\CompanyableTrait;
|
||||
use App\Models\Traits\HasUploads;
|
||||
use App\Models\Traits\Loggable;
|
||||
use App\Models\Traits\Searchable;
|
||||
use App\Presenters\ConsumablePresenter;
|
||||
use App\Presenters\Presentable;
|
||||
@@ -312,6 +313,23 @@ class Consumable extends SnipeModel
|
||||
|
||||
return $remaining;
|
||||
}
|
||||
public function totalCostSum() {
|
||||
|
||||
return $this->purchase_cost !== null ? $this->qty * $this->purchase_cost : null;
|
||||
}
|
||||
/**
|
||||
* Get the list of checkouts for this consumable
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v2.0]
|
||||
* @return \Illuminate\Database\Eloquent\Relations\Relation
|
||||
*/
|
||||
public function checkouts()
|
||||
{
|
||||
return $this->assetlog()->where('action_type', '=', 'checkout')
|
||||
->orderBy('created_at', 'desc')
|
||||
->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------------------------
|
||||
@@ -342,6 +360,99 @@ class Consumable extends SnipeModel
|
||||
* -----------------------------------------------
|
||||
**/
|
||||
|
||||
/**
|
||||
* Query builder scope to search on text filters for complex Bootstrap Tables API
|
||||
*
|
||||
* @param \Illuminate\Database\Query\Builder $query Query builder instance
|
||||
* @param text $filter JSON array of search keys and terms
|
||||
*
|
||||
* @return \Illuminate\Database\Query\Builder Modified query builder
|
||||
*/
|
||||
public function scopeByFilter($query, $filter)
|
||||
{
|
||||
return $query->where(
|
||||
function ($query) use ($filter) {
|
||||
foreach ($filter as $fieldname => $search_val) {
|
||||
|
||||
if ($fieldname == 'name') {
|
||||
$query->where('consumables.name', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'notes') {
|
||||
$query->where('consumables.notes', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'model_number') {
|
||||
$query->where('consumables.model_number', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'order_number') {
|
||||
$query->where('consumables.order_number', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'item_no') {
|
||||
$query->where('consumables.item_no', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'serial') {
|
||||
$query->where('consumables.serial', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'purchase_cost') {
|
||||
$query->where('consumables.purchase_cost', 'LIKE', '%' . $search_val . '%');
|
||||
}
|
||||
|
||||
if ($fieldname == 'location') {
|
||||
$query->whereHas(
|
||||
'location', function ($query) use ($search_val) {
|
||||
$query->where('locations.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ($fieldname == 'manufacturer') {
|
||||
$query->whereHas(
|
||||
'manufacturer', function ($query) use ($search_val) {
|
||||
$query->where('manufacturers.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if ($fieldname == 'supplier') {
|
||||
$query->whereHas(
|
||||
'supplier', function ($query) use ($search_val) {
|
||||
$query->where('suppliers.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if ($fieldname == 'category') {
|
||||
$query->whereHas(
|
||||
'category', function ($query) use ($search_val) {
|
||||
$query->where('categories.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ($fieldname == 'company') {
|
||||
$query->whereHas(
|
||||
'company', function ($query) use ($search_val) {
|
||||
$query->where('companies.name', 'LIKE', '%'.$search_val.'%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query builder scope to order on company
|
||||
*
|
||||
|
||||
@@ -31,10 +31,13 @@ class Department extends SnipeModel
|
||||
];
|
||||
|
||||
protected $rules = [
|
||||
'name' => 'required|max:255|is_unique_department',
|
||||
'location_id' => 'numeric|nullable',
|
||||
'company_id' => 'numeric|nullable',
|
||||
'manager_id' => 'numeric|nullable',
|
||||
'name' => 'required|max:255|is_unique_across_company_and_location:departments,name',
|
||||
'location_id' => 'numeric|nullable|exists:locations,id',
|
||||
'company_id' => 'numeric|nullable|exists:companies,id',
|
||||
'manager_id' => 'numeric|nullable|exists:users,id',
|
||||
'phone' => 'string|max:255|nullable',
|
||||
'fax' => 'string|max:255|nullable',
|
||||
'notes' => 'string|max:255|nullable',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -93,6 +93,9 @@ class Group extends SnipeModel
|
||||
|
||||
if (!is_integer($permission)) {
|
||||
$permissions[$permission] = (int) $value;
|
||||
if ($permission == 'superuser') {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
\Log::info('Weird data here - skipping it');
|
||||
unset($permissions[$permission]);
|
||||
|
||||
107
app/Models/Labels/Tapes/Brother/TZe_24mm_E.php
Normal file
107
app/Models/Labels/Tapes/Brother/TZe_24mm_E.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Labels\Tapes\Brother;
|
||||
|
||||
class TZe_24mm_E extends TZe_24mm
|
||||
{
|
||||
private const BARCODE_MARGIN = 1.75;
|
||||
private const TAG_SIZE = 2.00;
|
||||
private const TITLE_SIZE = 2.80;
|
||||
private const TITLE_MARGIN = 0.50;
|
||||
private const LABEL_SIZE = 2.00;
|
||||
private const LABEL_MARGIN = - 0.75;
|
||||
private const FIELD_SIZE = 2.80;
|
||||
private const FIELD_MARGIN = 0.15;
|
||||
private const BARCODE1D_SIZE = - 2.25;
|
||||
|
||||
public function getUnit() { return 'mm'; }
|
||||
public function getWidth() { return 45.0; }
|
||||
public function getHeight() { return 15; }
|
||||
public function getSupportAssetTag() { return true; }
|
||||
public function getSupport1DBarcode() { return true; }
|
||||
public function getSupport2DBarcode() { return true; }
|
||||
public function getSupportFields() { return 3; }
|
||||
public function getSupportLogo() { return false; }
|
||||
public function getSupportTitle() { return true; }
|
||||
|
||||
public function preparePDF($pdf) {}
|
||||
|
||||
public function write($pdf, $record) {
|
||||
$pa = $this->getPrintableArea();
|
||||
|
||||
$currentX = $pa->x1;
|
||||
$currentY = $pa->y1;
|
||||
$usableWidth = $pa->w;
|
||||
|
||||
|
||||
$usableHeight = $pa->h - self::BARCODE1D_SIZE;
|
||||
$barcodeSize = $usableHeight - self::TAG_SIZE;
|
||||
|
||||
if ($record->has('barcode2d')) {
|
||||
static::writeText(
|
||||
$pdf, $record->get('tag'),
|
||||
$pa->x1, $pa->y2 - self::TAG_SIZE - self::BARCODE1D_SIZE,
|
||||
'freesans', 'b', self::TAG_SIZE, 'C',
|
||||
$barcodeSize, self::TAG_SIZE, true, 0
|
||||
);
|
||||
static::write2DBarcode(
|
||||
$pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type,
|
||||
$currentX, $currentY,
|
||||
$barcodeSize, $barcodeSize
|
||||
);
|
||||
$currentX += $barcodeSize + self::BARCODE_MARGIN;
|
||||
$usableWidth -= $barcodeSize + self::BARCODE_MARGIN;
|
||||
} else {
|
||||
static::writeText(
|
||||
$pdf, $record->get('tag'),
|
||||
$pa->x1, $pa->y2 - self::TAG_SIZE - self::BARCODE1D_SIZE,
|
||||
'freesans', 'B', self::TAG_SIZE, 'R',
|
||||
$usableWidth, self::TAG_SIZE, true, 0
|
||||
);
|
||||
}
|
||||
|
||||
if ($record->has('title')) {
|
||||
static::writeText(
|
||||
$pdf, $record->get('title'),
|
||||
$currentX, $currentY,
|
||||
'freesans', 'B', self::TITLE_SIZE, 'L',
|
||||
$usableWidth, self::TITLE_SIZE, true, 0
|
||||
);
|
||||
$currentY += self::TITLE_SIZE + self::TITLE_MARGIN;
|
||||
}
|
||||
|
||||
foreach ($record->get('fields') as $field) {
|
||||
// Write label and value on the same line
|
||||
// Calculate label width with proportional character spacing
|
||||
$labelWidth = $pdf->GetStringWidth($field['label'], 'freesans', '', self::LABEL_SIZE);
|
||||
$charCount = strlen($field['label']);
|
||||
$spacingPerChar = 0.5;
|
||||
$totalSpacing = $charCount * $spacingPerChar;
|
||||
$adjustedWidth = $labelWidth + $totalSpacing;
|
||||
|
||||
static::writeText(
|
||||
$pdf, $field['label'],
|
||||
$currentX, $currentY,
|
||||
'freesans', 'B', self::LABEL_SIZE, 'L',
|
||||
$adjustedWidth, self::LABEL_SIZE, true, 0, $spacingPerChar
|
||||
);
|
||||
|
||||
static::writeText(
|
||||
$pdf, $field['value'],
|
||||
$currentX + $adjustedWidth + 2, $currentY,
|
||||
'freesans', 'B', self::FIELD_SIZE, 'L',
|
||||
$usableWidth - $adjustedWidth - 2, self::FIELD_SIZE, true, 0, 0.3
|
||||
);
|
||||
|
||||
$currentY += max(self::LABEL_SIZE, self::FIELD_SIZE) + self::FIELD_MARGIN;
|
||||
}
|
||||
|
||||
|
||||
if ($record->has('barcode1d')) {
|
||||
static::write1DBarcode(
|
||||
$pdf, $record->get('barcode1d')->content, $record->get('barcode1d')->type,
|
||||
$currentX, $barcodeSize + self::BARCODE_MARGIN, $usableWidth - self::TAG_SIZE, self::TAG_SIZE
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,78 @@ class Ldap extends Model
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds user via Admin search *first*, and _then_ try to bind as that user, returning the user attributes on success,
|
||||
* or false on failure. This enables login when the DN is harder to programmatically 'guess' due to having users in
|
||||
* various different OU's or other LDAP entities.
|
||||
*/
|
||||
public static function findAndBindMultiOU(string $baseDn, string $filterQuery, string $password, int $slow_failure = 3): array|false
|
||||
{
|
||||
/**
|
||||
* If you *don't* set the slow_failure variable, do note that we might permit timing attacks in here - if
|
||||
* your find results come back 'slow' when a user *does* exist, but fast if they *don't* exist, then you
|
||||
* can use this to enumerate users.
|
||||
*
|
||||
* Even if that's *not* true, we still might have an issue: if we don't find the user, then we don't even _try_
|
||||
* to bind as them. Again, that could permit a timing attack.
|
||||
*
|
||||
* Instead of checking every little thing, we just wrap everything in a try/catch in order to unify the
|
||||
* 'slow_failure' treatment. All failures are re-raised as exceptions so that all failures exit from the
|
||||
* same place.
|
||||
*/
|
||||
$connection = null;
|
||||
$admin_conn = null;
|
||||
try {
|
||||
/**
|
||||
* First we get an 'admin' connection, which will need search permissions. That was already a requirement
|
||||
* here, so that's not a big lift. But it _is_ possible to configure LDAP to only login, and *not* to be
|
||||
* able to import lists of users. In that case, this function *will not work* - and you should use the
|
||||
* legacy 'findAndBindUserLdap' method, below. Otherwise, it looks like this would attempt an anonymous
|
||||
* bind - which you might want, but you probably don't.
|
||||
*
|
||||
**/
|
||||
$admin_conn = self::connectToLdap();
|
||||
self::bindAdminToLdap($admin_conn);
|
||||
$results = ldap_search($admin_conn, $baseDn, $filterQuery);
|
||||
$entry_count = ldap_count_entries($admin_conn, $results);
|
||||
if ($entry_count != 1) {
|
||||
throw new \Exception('Wrong number of entries found: ' . $entry_count);
|
||||
}
|
||||
$entry = ldap_first_entry($admin_conn, $results);
|
||||
$user = ldap_get_attributes($admin_conn, $entry);
|
||||
$userDn = ldap_get_dn($admin_conn, $entry);
|
||||
if (!$userDn) {
|
||||
throw new \Exception("No user DN found");
|
||||
}
|
||||
\Log::debug("FOUND DN IS: $userDn");
|
||||
// The temptation now is to do ldap_unbind on the $admin_conn, but that gets handled in the 'finally' below.
|
||||
// I don't know if that means a separate 'connection' is maintained to the LDAP server or not, and would
|
||||
// definitely prefer to not do that if we can avoid it. But I don't know enough about the LDAP protocol to
|
||||
// be certain that that happens.
|
||||
|
||||
//now we try to log in (bind) as that found user
|
||||
$connection = self::connectToLdap();
|
||||
$bind_results = ldap_bind($connection, $userDn, $password);
|
||||
if (!$bind_results) {
|
||||
throw new \Exception("Unable to bind as user");
|
||||
}
|
||||
return array_change_key_case($user);
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug("Exception on fast find-and-bind: " . $e->getMessage());
|
||||
if ($slow_failure) {
|
||||
sleep($slow_failure);
|
||||
}
|
||||
return false; //TODO - make this null instead for a slightly nicer type signature
|
||||
} finally {
|
||||
if ($admin_conn) {
|
||||
ldap_unbind($admin_conn);
|
||||
}
|
||||
if ($connection) {
|
||||
ldap_unbind($connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Binds/authenticates the user to LDAP, and returns their attributes.
|
||||
@@ -147,25 +219,27 @@ class Ldap extends Model
|
||||
|
||||
Log::debug('Filter query: '.$filterQuery);
|
||||
|
||||
// only try this if we have an Admin username set; otherwise use the 'legacy' method
|
||||
if (($settings->ldap_uname) && ($baseDn)) {
|
||||
// in the fallowing call, we pick a slow-failure of 0 because we might need to fall through to 'legacy'
|
||||
$fast_bind = self::findAndBindMultiOU($baseDn, $filterQuery, $password, 0);
|
||||
if ($fast_bind) {
|
||||
\Log::debug("Fast bind worked");
|
||||
return $fast_bind;
|
||||
}
|
||||
\Log::debug("Fast bind failed; falling through to legacy bind");
|
||||
}
|
||||
|
||||
if (! $ldapbind = @ldap_bind($connection, $userDn, $password)) {
|
||||
Log::debug("Status of binding user: $userDn to directory: (directly!) ".($ldapbind ? "success" : "FAILURE"));
|
||||
if (! $ldapbind = self::bindAdminToLdap($connection)) {
|
||||
/*
|
||||
* TODO PLEASE:
|
||||
*
|
||||
* this isn't very clear, so it's important to note: the $ldapbind value is never correctly returned - we never 'return true' from self::bindAdminToLdap() (the function
|
||||
* just "falls off the end" without ever explictly returning 'true')
|
||||
*
|
||||
* but it *does* have an interesting side-effect of checking for the LDAP password being incorrectly encrypted with the wrong APP_KEY, so I'm leaving it in for now.
|
||||
*
|
||||
* If it *did* correctly return 'true' on a succesful bind, it would _probably_ allow users to log in with an incorrect password. Which would be horrible!
|
||||
*
|
||||
* Let's definitely fix this at the next refactor!!!!
|
||||
*
|
||||
*/
|
||||
Log::debug("Status of binding Admin user: $userDn to directory instead: ".($ldapbind ? "success" : "FAILURE"));
|
||||
return false;
|
||||
// replicate the old bad-decryption-key detection behavior here
|
||||
try {
|
||||
Crypt::decrypt(Setting::getSettings()->ldap_pword);
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception('Your app key has changed! Could not decrypt LDAP password using your current app key, so LDAP authentication has been disabled. Login with a local account, update the LDAP password and re-enable it in Admin > Settings.');
|
||||
}
|
||||
//regardless of anything else; stuff isn't working. Return false.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $results = ldap_search($connection, $baseDn, $filterQuery)) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user