Compare commits
627 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da86e919d9 | |||
| 45d6a491cb | |||
| 3dc90f89f6 | |||
| e2bea57146 | |||
| 43a32071f1 | |||
| e3a9872d28 | |||
| 75f86cd669 | |||
| 73f72cbbb0 | |||
| 2033f25386 | |||
| 8d0a6af2aa | |||
| a698ba3082 | |||
| b57d286b15 | |||
| 3cd5e86527 | |||
| bccba46332 | |||
| 70357ada3d | |||
| 043ad713e7 | |||
| bb178b0a5c | |||
| 288bded7d9 | |||
| d12ad3d538 | |||
| 905d498ecd | |||
| 802067f398 | |||
| b40e227ad3 | |||
| b89504e1c3 | |||
| c8ae09cd43 | |||
| 8ebddd95ff | |||
| c14880dfca | |||
| a27c551f64 | |||
| e71453cb5d | |||
| bb19add3b6 | |||
| 9bd6396a15 | |||
| b060327219 | |||
| 8b383df13f | |||
| 6a0ec69451 | |||
| 66bf6275b8 | |||
| 6fc2ff7252 | |||
| 0f6367bb17 | |||
| e3190c3922 | |||
| 53628d6ae3 | |||
| d03f68ae34 | |||
| 87bc834885 | |||
| 9f89dffaae | |||
| ab1a5c0241 | |||
| 758c1cabc5 | |||
| 3dd5358e73 | |||
| 8cc4ad27c9 | |||
| cd1f6b8e73 | |||
| 07e70cf7a9 | |||
| a9d1069705 | |||
| 10703263a8 | |||
| b0aa21bee7 | |||
| 82fa1d7a26 | |||
| be446e97d7 | |||
| c44f3319e3 | |||
| 678d1c1428 | |||
| 535d7c0ff6 | |||
| e430e4e6e2 | |||
| df92076e15 | |||
| e2ba35ee80 | |||
| f4cac96358 | |||
| 5257c2ce84 | |||
| b378cf31f4 | |||
| 0f184840df | |||
| 3df21df85b | |||
| 0d870d540d | |||
| 144772cfbe | |||
| 80c8aa41dc | |||
| 5658cd6dd4 | |||
| 374f426f0c | |||
| 2af0c237a9 | |||
| dafd72af59 | |||
| cbc6dc94a5 | |||
| f74e7510c5 | |||
| d87cd7cbb9 | |||
| 9a8cbd6e00 | |||
| abc4363e83 | |||
| df0ee6020a | |||
| 53599544af | |||
| b5ec9e080d | |||
| 8f98c8a862 | |||
| 0959d87534 | |||
| 1252681d55 | |||
| 9bc4efa5ff | |||
| 5656e4f5b7 | |||
| a966198a75 | |||
| 4ff214ac47 | |||
| 5169d174ad | |||
| 9c849c337f | |||
| d0685464f6 | |||
| 10b5a8ef21 | |||
| f0a9a49753 | |||
| 1afde946d2 | |||
| e8ba1feddc | |||
| 18e9b5c5bf | |||
| f186dc20f6 | |||
| 80a722d465 | |||
| 765487f62e | |||
| 1d186fffaa | |||
| 6295b7726e | |||
| e7c45644b9 | |||
| 356a0d4c12 | |||
| 00d4d6c7a8 | |||
| 371d44b2a7 | |||
| 79732a9151 | |||
| a6e55fb462 | |||
| d032a51a3d | |||
| 9c2495af29 | |||
| d7bc6c45f6 | |||
| 4382e01f57 | |||
| bab5294399 | |||
| a161fa8519 | |||
| 5e5bd7a17d | |||
| 285717ab12 | |||
| 81d91da0b8 | |||
| b017e9382f | |||
| eb5334e865 | |||
| 01b1c3923d | |||
| 780fb76af8 | |||
| ab90fc16e0 | |||
| 990c50c5b9 | |||
| 2e91b3dc9a | |||
| 211bd02786 | |||
| e8d000a17a | |||
| 8fc373abfc | |||
| a473ca737e | |||
| ff6fc68981 | |||
| f133a67550 | |||
| b0a6cdc29f | |||
| edcb429366 | |||
| ba5a674526 | |||
| e84496f8b1 | |||
| 5f9212383a | |||
| a5493f11bc | |||
| ce434b3d04 | |||
| ade07b411b | |||
| 3868e469c0 | |||
| ea939acbd3 | |||
| 522544c131 | |||
| 445fb6f253 | |||
| 7bf8fd5eeb | |||
| c758fb4c83 | |||
| 4145f64399 | |||
| 4120ab6fe6 | |||
| 0170fb7711 | |||
| 42df2f6c31 | |||
| 9b522b69ff | |||
| 135db70b0f | |||
| 048e97f9a9 | |||
| 18d8f257ee | |||
| ec67195014 | |||
| 0d745ad10f | |||
| 89ce71b350 | |||
| 63c1f7922f | |||
| 5809ac7997 | |||
| 92b6e46249 | |||
| 46c11d8599 | |||
| 7651365ff6 | |||
| 35caa0e68d | |||
| e0a7fe443d | |||
| c31190a128 | |||
| de50ec30b7 | |||
| c0fe308d7d | |||
| 0a20141b7c | |||
| 4d0282ca0a | |||
| e61143f746 | |||
| 69dc91d225 | |||
| 6f1c49e14d | |||
| 78acc3685d | |||
| bf5013e527 | |||
| cbd961e922 | |||
| fa26e23383 | |||
| 7abe1bed50 | |||
| 155df0a94d | |||
| 4d06e81768 | |||
| 9bf1e2401d | |||
| 4edf40acaf | |||
| a54ed750a3 | |||
| c02a6c105a | |||
| 4d2416ab96 | |||
| 61ae30528a | |||
| e883eb70b9 | |||
| a5b1379cdb | |||
| ef64210ed2 | |||
| f29846ec20 | |||
| d2b4d84374 | |||
| dfe3f5fb9f | |||
| 8dbb19eb82 | |||
| 45cdff6920 | |||
| c25d56ea85 | |||
| f92a9a6cc6 | |||
| 988729fbeb | |||
| e00f7b5b67 | |||
| 39fbe98313 | |||
| 46d5234fd7 | |||
| dd4117bd5b | |||
| 4dcd5190df | |||
| 48728e83b2 | |||
| 087b895bba | |||
| 2ed28f7f7a | |||
| 9f50328da2 | |||
| 3737b34913 | |||
| aa0eb24e80 | |||
| 9d012dd06d | |||
| df28c80553 | |||
| 2a3a3f7818 | |||
| 15cb7993f6 | |||
| 15529a0c9c | |||
| d2c30dd08c | |||
| 972b27140a | |||
| cac13dd949 | |||
| 112bf498e6 | |||
| 02488a62c1 | |||
| f5313f6ec0 | |||
| 3206549170 | |||
| 59b621500f | |||
| cd5716d66d | |||
| 6a68a38d71 | |||
| f23ea5ce8f | |||
| c893b69b5f | |||
| 269e6c4ef6 | |||
| a0ab9d3a80 | |||
| cdd72cf372 | |||
| e38b8cdd68 | |||
| c44cb23dea | |||
| 84bdfa98d1 | |||
| f3055e7442 | |||
| 9c36ade1e2 | |||
| 4127c6a0c0 | |||
| c133c869ae | |||
| d74197aacc | |||
| c870dd0dae | |||
| 6d1d89105d | |||
| f3a4f5edaa | |||
| 8f61d1e729 | |||
| 4782734ed4 | |||
| a9d65f7e81 | |||
| e59f5d92a4 | |||
| 93576fc435 | |||
| 221ae337f2 | |||
| 1b1d1f77d5 | |||
| d7ef85235c | |||
| 3a714c3ef6 | |||
| 2a69bf903e | |||
| 23c93473c8 | |||
| 266f04b04c | |||
| 9f64a90a45 | |||
| baacf171f4 | |||
| 109e7fff68 | |||
| 816868cfc8 | |||
| c21b44aded | |||
| 0565ec22cb | |||
| 17fc52a237 | |||
| f535b8ffd2 | |||
| 221e495974 | |||
| 8f06902230 | |||
| ff95416a90 | |||
| b1491b524d | |||
| 703c5ca4ed | |||
| ce6c7146ea | |||
| e1e614ebc8 | |||
| 7918653413 | |||
| a23bc89607 | |||
| 6f25f80260 | |||
| 6da5f2e19b | |||
| 518351eba1 | |||
| ce0ce8688b | |||
| 43be1e8364 | |||
| 6e749d34a4 | |||
| 6e55d78c19 | |||
| 884dc926fe | |||
| a383033ffa | |||
| 67fa473281 | |||
| 28b3e34a84 | |||
| 72383fdbd7 | |||
| 44f9101d93 | |||
| 9cab197651 | |||
| db4fcff1f3 | |||
| ea820ce99a | |||
| d21ff001bf | |||
| 69ddde697a | |||
| 3f72d0afd8 | |||
| 1d209155f2 | |||
| 20b2d22991 | |||
| e12ac03dd8 | |||
| 9c73b26cd1 | |||
| cabc842f52 | |||
| d099cbd8e5 | |||
| cfa8069953 | |||
| 45df8ea55e | |||
| 33846b0d61 | |||
| b7df1dcefb | |||
| 3b0278bd3a | |||
| b3be2baf40 | |||
| 3cff19f9ca | |||
| 4380a46d1c | |||
| f4b9138a3f | |||
| f5dbf27592 | |||
| 069912d051 | |||
| 6a1c3e29d0 | |||
| 9fc37cf6b9 | |||
| b37adb8c49 | |||
| 86245ad4ae | |||
| e3afe3b74d | |||
| c8bafdad79 | |||
| ee3ebe32e2 | |||
| 3060fd305b | |||
| 72666cdd47 | |||
| 4fbd6b2f15 | |||
| 10ee84cb26 | |||
| c94fce2367 | |||
| 432e625186 | |||
| 653b1327cb | |||
| d011ad3dde | |||
| 54d01409dc | |||
| d5ce5a82de | |||
| 849b217300 | |||
| b224cc636c | |||
| 371f096e54 | |||
| 5efb21eb0b | |||
| ec24da12a1 | |||
| 6aa8d8e772 | |||
| 424ed48d06 | |||
| 3c44ce8682 | |||
| 948dadc333 | |||
| 0ff2fb5cff | |||
| c0773772f4 | |||
| 3c1b18919a | |||
| 72a11113e7 | |||
| 978c8f81a5 | |||
| ac2162113d | |||
| b0635f24db | |||
| 34b4cf12e2 | |||
| fec0a1b2b5 | |||
| 96088c416e | |||
| c8f3e833e5 | |||
| 4f943d4a7a | |||
| 37361ef52f | |||
| 5307a44fab | |||
| d97f579761 | |||
| afc287b607 | |||
| ded6515cbc | |||
| 1af9b42d82 | |||
| 403f9c848b | |||
| 480d252173 | |||
| d329e5f862 | |||
| d9bc110868 | |||
| e7c80b89eb | |||
| 50ba979840 | |||
| 6fd834e4d2 | |||
| 2d6eb5d80a | |||
| f74fedb226 | |||
| 90e2c105cd | |||
| 5976e93de2 | |||
| 34101c148f | |||
| 048a46b317 | |||
| 6ae09e15fb | |||
| f03b27ec88 | |||
| cc1e0d82dd | |||
| f233bd2d01 | |||
| 7a8b22df26 | |||
| 17df4a08a7 | |||
| c377b41198 | |||
| 0c59ca70cf | |||
| e9e9dfeeab | |||
| f8c084cde7 | |||
| 8f7fa6c0f5 | |||
| f381362130 | |||
| bef4a50720 | |||
| 2a93de675f | |||
| e5f41f8f17 | |||
| b9da8ee55c | |||
| bf525f7213 | |||
| c9ef163142 | |||
| feb3bd58cf | |||
| f9288e450b | |||
| 541128dd7a | |||
| 23b9c881ad | |||
| cacd6f7e9b | |||
| 4db4314f18 | |||
| 51aa66a77d | |||
| aa0b491080 | |||
| c01c9201ee | |||
| 0ad1a5b6ba | |||
| 95909d552a | |||
| a159c3b84e | |||
| 875b0bbdec | |||
| be5b74af90 | |||
| be1f1bd1c5 | |||
| 5dcc8efcca | |||
| c9be696c84 | |||
| 8748ddffd8 | |||
| e19a9b23e5 | |||
| 5752fe68f0 | |||
| 187f160b21 | |||
| a6bbf0edf0 | |||
| 8908b67b3d | |||
| 4a0797d59f | |||
| 07f1f247de | |||
| 4373f761c7 | |||
| 559491d31a | |||
| 1f51155c92 | |||
| 58f7370935 | |||
| 043292ff15 | |||
| a04bf04900 | |||
| 9408f4005c | |||
| f9567af55a | |||
| 62a0c3764e | |||
| 66cab56c47 | |||
| dfa8590a65 | |||
| ee10cc970f | |||
| 96a42d0f33 | |||
| 57b257057a | |||
| c45040818d | |||
| fe2d599099 | |||
| 1d2ba0a8c1 | |||
| 63454f8c63 | |||
| 8e9bd5dbb1 | |||
| 87d6328fb8 | |||
| 5020aec71a | |||
| 39ff553b3e | |||
| aea3877718 | |||
| dbaa900444 | |||
| 751541a54d | |||
| 26382eb0a1 | |||
| 3972799e56 | |||
| 50baed175f | |||
| d870b3625b | |||
| ae2e51c66c | |||
| db2afd0dc7 | |||
| 826bbe37c9 | |||
| 27a637a7a4 | |||
| 460daf71b6 | |||
| 56d5f17dde | |||
| 3074bae47c | |||
| b9c7bcf035 | |||
| 071c46a91e | |||
| 49138f2cb1 | |||
| fd46794350 | |||
| 9305c3e845 | |||
| 058da6bfef | |||
| a65ae59810 | |||
| 1967b3b7a7 | |||
| 30dbf1698b | |||
| 8e1ad53a31 | |||
| a5272968de | |||
| 94e14e5ee9 | |||
| db46e16530 | |||
| 0630ef9f89 | |||
| 0f80950a91 | |||
| aae07bd3a7 | |||
| 2620b60048 | |||
| 1cff2d67aa | |||
| 80418d0b16 | |||
| 14c5cff429 | |||
| 0ea6eb13c2 | |||
| 86cc20034f | |||
| 57f17e80a2 | |||
| 81b1cdc6e9 | |||
| 12c3629c89 | |||
| 0304933c53 | |||
| 0a02c0b81a | |||
| f0d84f5350 | |||
| fdcc3f1968 | |||
| 663bab1f9d | |||
| 8b1e312292 | |||
| 9004211a59 | |||
| 053eb91457 | |||
| 3810513224 | |||
| e50e0f0e34 | |||
| cdf73f9c89 | |||
| bc808cbe46 | |||
| fdc65fb1b2 | |||
| 1ad562f8b9 | |||
| 3db9a15dd3 | |||
| 42bf43d68d | |||
| 4548ed8a45 | |||
| a5cea247f1 | |||
| 407e2d0246 | |||
| 2af7367480 | |||
| 29b9a78f54 | |||
| 571bc39495 | |||
| 382a164b9d | |||
| 19f70656ee | |||
| 9216a7550f | |||
| 4f9ba7c6cc | |||
| afb7c69ac3 | |||
| 8ea78fae21 | |||
| 5f232c0584 | |||
| ed931d497a | |||
| 59278c3f70 | |||
| 179d031bb2 | |||
| dc1410aa70 | |||
| 0f595a8854 | |||
| 70e1dcf1b4 | |||
| 780e3e1cd9 | |||
| 339c93ebbf | |||
| b4bb1556be | |||
| ba96aa5a61 | |||
| 2171556ec4 | |||
| 2d33368063 | |||
| 93a2f74f9e | |||
| ee61084ac8 | |||
| 8d8a1889cd | |||
| f275cb6928 | |||
| db8de1f794 | |||
| d901e821cc | |||
| 34a533b2d6 | |||
| ed6b3c04ab | |||
| d3d37c70ab | |||
| 475e674fc6 | |||
| 01436d0532 | |||
| 96bf7d0c2b | |||
| 529973aa77 | |||
| a4ca0a592f | |||
| f4cd090ac6 | |||
| 6d5e68274d | |||
| 3e002cb940 | |||
| b7ea9a959c | |||
| dc3a16c437 | |||
| 608af84253 | |||
| 90c8689596 | |||
| 161d7e1c2b | |||
| 8627032c4f | |||
| 5bead4fbcc | |||
| ef44ba5f97 | |||
| 2dc0ec9e7e | |||
| afd435e895 | |||
| 80d1bf6a7a | |||
| 737f3ef3db | |||
| d179f47274 | |||
| 1832d95371 | |||
| a614f986f0 | |||
| f398a59d26 | |||
| c8bd104268 | |||
| 7f01bd4c56 | |||
| 6c3c7fdf49 | |||
| bdc8fc8d4a | |||
| 41be127489 | |||
| fdfae9593d | |||
| 6a21eb53c9 | |||
| efde2b4672 | |||
| daaa26cbf4 | |||
| 3e7441562c | |||
| 7b53fa5245 | |||
| d35d46f5b4 | |||
| f4772a9cad | |||
| 4c1bb7e0ac | |||
| 762ea9b4db | |||
| b16970a61e | |||
| dadb9bd81e | |||
| 13dc7de660 | |||
| 003ea36e18 | |||
| f4bd2a68c9 | |||
| be4e75d4f7 | |||
| 538c21ce1e | |||
| 626cd6cb2e | |||
| 2a56f6573d | |||
| 6ee2dc1cd6 | |||
| 3fcde8bd16 | |||
| e2ff7a7bc7 | |||
| c7efd16517 | |||
| f2907f04d9 | |||
| 7d98c267d5 | |||
| 5bc6330c13 | |||
| 1706ed597d | |||
| 6e9ba28ef7 | |||
| 554d1a44de | |||
| c0a8f4c1a4 | |||
| 08be9aac6d | |||
| a51b17fb53 | |||
| 66d5618d60 | |||
| e16c2384fd | |||
| b3323f08a0 | |||
| 7e63c2ef92 | |||
| 7f65b6d598 | |||
| 8fb8f0a4d2 | |||
| 637dbc8d2a | |||
| 978990fdff | |||
| 52a058e511 | |||
| 64bea202c5 | |||
| 37f60993ca | |||
| 32717c67c7 | |||
| 3681e3f025 | |||
| 1d0f055349 | |||
| fb3024ca9c | |||
| 005c0ea9f6 | |||
| 7c3f1f3a84 | |||
| 900e5209d9 | |||
| 4fbf416d16 | |||
| 7b7d2c87fb | |||
| 6debb3a65d | |||
| 315ba49a1d | |||
| ff57855038 | |||
| da6e837578 | |||
| a2d8f89162 | |||
| e36d65e695 | |||
| 34abf14cbe | |||
| dda7a4f22f | |||
| 283a885196 | |||
| d44aa3f16e | |||
| afb37981bf | |||
| 2b6518427a | |||
| 185e0073b3 | |||
| d0794ba71c | |||
| 1b42e2e138 | |||
| b4efabe82e | |||
| 9b37e95b58 | |||
| a92d8eeaab | |||
| e8dbb12ccc | |||
| 8a2cd19ea6 | |||
| afdf86ad0d | |||
| a5dae3f222 | |||
| 97765c08b1 | |||
| 6ad92556a1 | |||
| e2465ca2a7 | |||
| f5644928a8 | |||
| 8747ff32dd | |||
| 4ddd2f1cf8 | |||
| 11c8fd4d4c | |||
| ab04f3de93 | |||
| 4c16796256 | |||
| 516771d948 | |||
| e25ea465c5 | |||
| 30ac3d1a26 | |||
| e47c772230 | |||
| 706b623d95 | |||
| a908a76f53 | |||
| a2ec707f79 |
@@ -4271,6 +4271,15 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "CybotTM",
|
||||||
|
"name": "Sebastian Mendel",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/326348?v=4",
|
||||||
|
"profile": "https://github.com/CybotTM",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -113,7 +113,7 @@ ENABLE_HSTS=false
|
|||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
CACHE_DRIVER=file
|
CACHE_DRIVER=file
|
||||||
SESSION_DRIVER=file
|
SESSION_DRIVER=file
|
||||||
QUEUE_DRIVER=sync
|
QUEUE_CONNECTION=sync
|
||||||
CACHE_PREFIX=snipeit
|
CACHE_PREFIX=snipeit
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|||||||
+2
-1
@@ -37,6 +37,7 @@ MYSQL_ROOT_PASSWORD=changeme1234
|
|||||||
DB_PREFIX=null
|
DB_PREFIX=null
|
||||||
DB_DUMP_PATH='/usr/bin'
|
DB_DUMP_PATH='/usr/bin'
|
||||||
DB_DUMP_SKIP_SSL=true
|
DB_DUMP_SKIP_SSL=true
|
||||||
|
DB_DUMP_SINGLE_TRANSACTION=false
|
||||||
DB_CHARSET=utf8mb4
|
DB_CHARSET=utf8mb4
|
||||||
DB_COLLATION=utf8mb4_unicode_ci
|
DB_COLLATION=utf8mb4_unicode_ci
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ ENABLE_HSTS=false
|
|||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
CACHE_DRIVER=file
|
CACHE_DRIVER=file
|
||||||
SESSION_DRIVER=file
|
SESSION_DRIVER=file
|
||||||
QUEUE_DRIVER=sync
|
QUEUE_CONNECTION=sync
|
||||||
CACHE_PREFIX=snipeit
|
CACHE_PREFIX=snipeit
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|||||||
+1
-1
@@ -72,7 +72,7 @@ CORS_ALLOWED_ORIGINS="*"
|
|||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
CACHE_DRIVER=file
|
CACHE_DRIVER=file
|
||||||
SESSION_DRIVER=file
|
SESSION_DRIVER=file
|
||||||
QUEUE_DRIVER=sync
|
QUEUE_CONNECTION=sync
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
# OPTIONAL: LOGIN THROTTLING
|
# OPTIONAL: LOGIN THROTTLING
|
||||||
|
|||||||
+4
-2
@@ -32,6 +32,7 @@ DB_PASSWORD=null
|
|||||||
DB_PREFIX=null
|
DB_PREFIX=null
|
||||||
DB_DUMP_PATH='/usr/bin'
|
DB_DUMP_PATH='/usr/bin'
|
||||||
DB_DUMP_SKIP_SSL=false
|
DB_DUMP_SKIP_SSL=false
|
||||||
|
DB_DUMP_SINGLE_TRANSACTION=false
|
||||||
DB_CHARSET=utf8mb4
|
DB_CHARSET=utf8mb4
|
||||||
DB_COLLATION=utf8mb4_unicode_ci
|
DB_COLLATION=utf8mb4_unicode_ci
|
||||||
DB_SANITIZE_BY_DEFAULT=false
|
DB_SANITIZE_BY_DEFAULT=false
|
||||||
@@ -133,7 +134,7 @@ BS_TABLE_DEEPLINK=true
|
|||||||
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
|
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
|
||||||
ALLOW_IFRAMING=false
|
ALLOW_IFRAMING=false
|
||||||
REFERRER_POLICY=same-origin
|
REFERRER_POLICY=same-origin
|
||||||
ENABLE_CSP=false
|
ENABLE_CSP=true
|
||||||
ADDITIONAL_CSP_URLS=null
|
ADDITIONAL_CSP_URLS=null
|
||||||
CORS_ALLOWED_ORIGINS=null
|
CORS_ALLOWED_ORIGINS=null
|
||||||
ENABLE_HSTS=false
|
ENABLE_HSTS=false
|
||||||
@@ -142,7 +143,7 @@ ENABLE_HSTS=false
|
|||||||
# OPTIONAL: CACHE SETTINGS
|
# OPTIONAL: CACHE SETTINGS
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
CACHE_DRIVER=file
|
CACHE_DRIVER=file
|
||||||
QUEUE_DRIVER=sync
|
QUEUE_CONNECTION=sync
|
||||||
CACHE_PREFIX=snipeit
|
CACHE_PREFIX=snipeit
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
@@ -210,6 +211,7 @@ LOGIN_AUTOCOMPLETE=false
|
|||||||
RESET_PASSWORD_LINK_EXPIRES=15
|
RESET_PASSWORD_LINK_EXPIRES=15
|
||||||
PASSWORD_CONFIRM_TIMEOUT=10800
|
PASSWORD_CONFIRM_TIMEOUT=10800
|
||||||
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
|
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
|
||||||
|
TWO_FACTOR_MAX_ATTEMPTS_PER_MIN=5
|
||||||
INVITE_PASSWORD_LINK_EXPIRES=1500
|
INVITE_PASSWORD_LINK_EXPIRES=1500
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# This workflow uses actions that are not certified by GitHub.
|
||||||
|
# They are provided by a third-party and are governed by
|
||||||
|
# separate terms of service, privacy policy, and support
|
||||||
|
# documentation.
|
||||||
|
|
||||||
|
# EthicalCheck addresses the critical need to continuously security test APIs in development and in production.
|
||||||
|
|
||||||
|
# EthicalCheck provides the industry’s only free & automated API security testing service that uncovers security vulnerabilities using OWASP API list.
|
||||||
|
# Developers relies on EthicalCheck to evaluate every update and release, ensuring that no APIs go to production with exploitable vulnerabilities.
|
||||||
|
|
||||||
|
# You develop the application and API, we bring complete and continuous security testing to you, accelerating development.
|
||||||
|
|
||||||
|
# Know your API and Applications are secure with EthicalCheck – our free & automated API security testing service.
|
||||||
|
|
||||||
|
# How EthicalCheck works?
|
||||||
|
# EthicalCheck functions in the following simple steps.
|
||||||
|
# 1. Security Testing.
|
||||||
|
# Provide your OpenAPI specification or start with a public Postman collection URL.
|
||||||
|
# EthicalCheck instantly instrospects your API and creates a map of API endpoints for security testing.
|
||||||
|
# It then automatically creates hundreds of security tests that are non-intrusive to comprehensively and completely test for authentication, authorizations, and OWASP bugs your API. The tests addresses the OWASP API Security categories including OAuth 2.0, JWT, Rate Limit etc.
|
||||||
|
|
||||||
|
# 2. Reporting.
|
||||||
|
# EthicalCheck generates security test report that includes all the tested endpoints, coverage graph, exceptions, and vulnerabilities.
|
||||||
|
# Vulnerabilities are fully triaged, it contains CVSS score, severity, endpoint information, and OWASP tagging.
|
||||||
|
|
||||||
|
|
||||||
|
# This is a starter workflow to help you get started with EthicalCheck Actions
|
||||||
|
|
||||||
|
name: EthicalCheck-Workflow
|
||||||
|
|
||||||
|
# Controls when the workflow will run
|
||||||
|
on:
|
||||||
|
# Triggers the workflow on push or pull request events but only for the "master" branch
|
||||||
|
# Customize trigger events based on your DevSecOps processes.
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
schedule:
|
||||||
|
- cron: '35 17 * * 6'
|
||||||
|
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Trigger_EthicalCheck:
|
||||||
|
permissions:
|
||||||
|
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
|
||||||
|
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: EthicalCheck Free & Automated API Security Testing Service
|
||||||
|
uses: apisec-inc/ethicalcheck-action@005fac321dd843682b1af6b72f30caaf9952c641
|
||||||
|
with:
|
||||||
|
# The OpenAPI Specification URL or Swagger Path or Public Postman collection URL.
|
||||||
|
oas-url: "http://netbanking.apisec.ai:8080/v2/api-docs"
|
||||||
|
# The email address to which the penetration test report will be sent.
|
||||||
|
email: "snipe@snipe.net"
|
||||||
|
sarif-result-file: "ethicalcheck-results.sarif"
|
||||||
|
|
||||||
|
- name: Upload sarif file to repository
|
||||||
|
uses: github/codeql-action/upload-sarif@v4
|
||||||
|
with:
|
||||||
|
sarif_file: ./ethicalcheck-results.sarif
|
||||||
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"DOC1": "This file is meant to be pulled from the current HEAD of the desired branch, NOT referenced locally",
|
"DOC1": "This file is meant to be pulled from the current HEAD of the desired branch, NOT referenced locally",
|
||||||
"DOC2": "In other words, what you see locally are the requirements for your _current_ install",
|
"DOC2": "In other words, what you see locally are the requirements for your _current_ install",
|
||||||
"DOC3": "Please don't rely on these versions for planning upgrades unless you've fetched the most recent version",
|
"DOC3": "Please don't rely on these versions for planning upgrades unless you've fetched the most recent version",
|
||||||
"DOC4": "You should really just ignore it and run upgrade.php. Really",
|
"DOC4": "You should really just ignore it and run upgrade.php. Really",
|
||||||
"php_min_version": "8.2.0",
|
"php_min_version": "8.2.0",
|
||||||
"php_max_major_minor": "8.4",
|
"php_max_major_minor": "8.5",
|
||||||
"php_max_wontwork": "8.5.0",
|
"php_max_wontwork": "8.6.0",
|
||||||
"current_snipeit_version": "8.0"
|
"current_snipeit_version": "8.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **PHP 8.2+** / **Laravel 12** (framework), **Laravel Mix** (webpack) for frontend assets
|
||||||
|
- **AdminLTE 2** / **Bootstrap 3** UI — Blade views, no Livewire/Inertia
|
||||||
|
- **Chart.js v2.9.4** — bundled at `public/js/dist/Chart.min.js`; use `horizontalBar` type (v2 API, not v3)
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
php artisan test
|
||||||
|
# or
|
||||||
|
vendor/bin/phpunit
|
||||||
|
|
||||||
|
# Run a single test file
|
||||||
|
php artisan test tests/Feature/Assets/AssetsTest.php
|
||||||
|
|
||||||
|
# Run a specific test method
|
||||||
|
php artisan test --filter testSomeMethod
|
||||||
|
|
||||||
|
# Build frontend assets (dev)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run prod
|
||||||
|
|
||||||
|
# Laravel Mix watch
|
||||||
|
npm run watch
|
||||||
|
|
||||||
|
# Tinker / REPL
|
||||||
|
php artisan tinker
|
||||||
|
|
||||||
|
# Clear caches after config/route changes
|
||||||
|
php artisan optimize:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
Dev server is served via **Laravel Herd** (`herd coverage` for coverage reports).
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Controllers
|
||||||
|
|
||||||
|
Two parallel controller trees:
|
||||||
|
- `app/Http/Controllers/` — web/UI controllers (Blade views)
|
||||||
|
- `app/Http/Controllers/Api/` — REST API controllers (JSON, used by datatables + select2)
|
||||||
|
|
||||||
|
Subdirectory groupings: `Assets/`, `Licenses/`, `Users/`, `Accessories/`, `Consumables/`, `Components/`, `Kits/`, `Account/`, `Auth/`
|
||||||
|
|
||||||
|
### API Pattern
|
||||||
|
|
||||||
|
Every API controller returns data via a **Transformer** (`app/Http/Transformers/`). Never return raw model attributes from API controllers — always pass through the transformer. `DatatablesTransformer` wraps paginated results.
|
||||||
|
|
||||||
|
```php
|
||||||
|
return (new AssetsTransformer)->transformAssets($assets, $assets->count());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
All authorization goes through **Policies** (`app/Policies/`). `CheckoutablePermissionsPolicy` is the base for assets/licenses/accessories/consumables — its `checkout()` / `checkin()` methods accept `$item = null` so you can use `@can('checkout', \App\Models\Asset::class)` without an instance.
|
||||||
|
|
||||||
|
### FMCS (Full Multiple Company Support)
|
||||||
|
|
||||||
|
`Setting::getSettings()->full_multiple_companies_support == '1'` gates company-scoped filtering. The select2 API endpoints (`selectlist()` methods) accept a `companyId` query param — apply it like this:
|
||||||
|
|
||||||
|
```php
|
||||||
|
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($request->filled('companyId'))) {
|
||||||
|
$query->where('table.company_id', $request->input('companyId'));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass `data-company-id="{{ $user->company_id }}"` in Blade to wire it to select2.
|
||||||
|
|
||||||
|
### Select2 AJAX Dropdowns
|
||||||
|
|
||||||
|
Use `class="js-data-ajax"` with `data-endpoint="hardware|licenses|consumables|..."`. `snipeit.js` auto-initializes these, forwarding `data-company-id` as `companyId` and `data-asset-status-type` as `statusType` to the API.
|
||||||
|
|
||||||
|
### Routes
|
||||||
|
|
||||||
|
All routes are in `routes/web.php` (UI) and `routes/api.php` (API). Breadcrumbs are defined inline using `->breadcrumbs(fn (Trail $trail) => ...)` from `tabuna/breadcrumbs`. Every UI route should have a breadcrumb.
|
||||||
|
|
||||||
|
Note: the `reports/unaccepted_assets` route is named with slashes, not dots — use `route('reports/unaccepted_assets')`.
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
String keys live in `resources/lang/en-US/general.php` (and other files in that directory). Always add new UI strings as translation keys rather than hard-coding English.
|
||||||
|
|
||||||
|
### Checkout Redirect Flow
|
||||||
|
|
||||||
|
After checkout, `Helper::getRedirectOption()` reads `$request->redirect_option`. For redirecting back to the assigned user after checkout:
|
||||||
|
- Set `redirect_option=target` in the form
|
||||||
|
- Set `checkout_to_type=user` in the form
|
||||||
|
- Set `assigned_user={{ $user->id }}` in the form
|
||||||
|
|
||||||
|
### Key Helper Methods (`app/Helpers/Helper.php`)
|
||||||
|
|
||||||
|
- `Helper::deployableStatusLabelList()` — status labels for checkout forms
|
||||||
|
- `Helper::defaultChartColors()` — 10-color palette used in charts
|
||||||
|
- `Helper::getRedirectOption($request, $id, $table)` — post-checkout redirect logic
|
||||||
|
|
||||||
|
### Global View Variables
|
||||||
|
|
||||||
|
`$snipeSettings` is injected into all views via a service provider — no need to pass `Setting::getSettings()` from every controller. Use it directly in Blade.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Tests live in `tests/Feature/` (organized by entity) and `tests/Unit/`. Feature tests hit the database; the test environment uses `array` cache/session/mail drivers. Tests use factories for data setup.
|
||||||
+1
-1
@@ -69,7 +69,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
|
|||||||
| [<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/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/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/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/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") | [<img src="https://avatars.githubusercontent.com/u/75509373?v=4" width="110px;"/><br /><sub>Peter Gallwas</sub>](https://www.husky.nz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Husky-Devel "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") | [<img src="https://avatars.githubusercontent.com/u/75509373?v=4" width="110px;"/><br /><sub>Peter Gallwas</sub>](https://www.husky.nz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Husky-Devel "Code") | [<img src="https://avatars.githubusercontent.com/u/326348?v=4" width="110px;"/><br /><sub>Sebastian Mendel</sub>](https://github.com/CybotTM)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CybotTM "Code") |
|
||||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
|
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ COPY --from=mlocati/php-extension-installer:2.1.15 /usr/bin/install-php-extensio
|
|||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
install-php-extensions \
|
install-php-extensions \
|
||||||
bcmath \
|
bcmath \
|
||||||
|
exif \
|
||||||
gd \
|
gd \
|
||||||
ldap \
|
ldap \
|
||||||
mysqli \
|
mysqli \
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
This is a FOSS project for asset management in IT Operations. Knowing who has which laptop, when it was purchased in order to depreciate it correctly, handling software licenses, etc.
|
This is a FOSS project for asset management in IT Operations. Knowing who has which laptop, when it was purchased in order to depreciate it correctly, handling software licenses, etc.
|
||||||
|
|
||||||
It is built on [Laravel 11](http://laravel.com).
|
It is built on [Laravel 12](http://laravel.com).
|
||||||
|
|
||||||
Snipe-IT is actively developed and we [release quite frequently](https://github.com/grokability/snipe-it/releases). ([Check out the live demo here](https://snipeitapp.com/demo/).)
|
Snipe-IT is actively developed and we [release quite frequently](https://github.com/grokability/snipe-it/releases). ([Check out the live demo here](https://snipeitapp.com/demo/).)
|
||||||
|
|
||||||
@@ -98,6 +98,7 @@ Since the release of the JSON REST API, several third-party developers have been
|
|||||||
- [InQRy (archived)](https://github.com/Microsoft/InQRy) by [@Microsoft](https://github.com/Microsoft)
|
- [InQRy (archived)](https://github.com/Microsoft/InQRy) by [@Microsoft](https://github.com/Microsoft)
|
||||||
- [Marksman (archived)](https://github.com/Scope-IT/marksman) - A Windows agent for Snipe-IT
|
- [Marksman (archived)](https://github.com/Scope-IT/marksman) - A Windows agent for Snipe-IT
|
||||||
- [Python Module (archived)](https://github.com/jbloomer/SnipeIT-PythonAPI) by [@jbloomer](https://github.com/jbloomer)
|
- [Python Module (archived)](https://github.com/jbloomer/SnipeIT-PythonAPI) by [@jbloomer](https://github.com/jbloomer)
|
||||||
|
[IT-Tools](https://github.com/chrisnox/Snipeit-it-tools) by @chrisnox - Browser bookmarklets for PDF handover/return protocols, digital signatures, label printing (Zebra ZD410), AirWatch MDM sync and Lansweeper CSV import.
|
||||||
|
|
||||||
We also have a handful of [Google Apps scripts](https://github.com/grokability/google-apps-scripts-for-snipe-it) to help with various tasks.
|
We also have a handful of [Google Apps scripts](https://github.com/grokability/google-apps-scripts-for-snipe-it) to help with various tasks.
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,13 @@ final class PreserveUnauthorizedPrivilegedPermissionsAction
|
|||||||
* @param array<string, mixed> $originalPermissions
|
* @param array<string, mixed> $originalPermissions
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
public static function run(array $requestedPermissions, User $authenticatedUser, array $originalPermissions = []): array
|
public static function run(array $requestedPermissions, User $authenticatedUser, array $originalPermissions = [], ?User $targetUser = null): array
|
||||||
{
|
{
|
||||||
|
// Disallow non-admin/superuser users from modifying their own permissions, but allow them to modify other users' permissions (except for admin/superuser keys).
|
||||||
|
if ($targetUser && ! $authenticatedUser->isSuperUser() && $authenticatedUser->id === $targetUser->id) {
|
||||||
|
return $originalPermissions;
|
||||||
|
}
|
||||||
|
|
||||||
if (! $authenticatedUser->isSuperUser()) {
|
if (! $authenticatedUser->isSuperUser()) {
|
||||||
if (array_key_exists('superuser', $originalPermissions)) {
|
if (array_key_exists('superuser', $originalPermissions)) {
|
||||||
$requestedPermissions['superuser'] = $originalPermissions['superuser'];
|
$requestedPermissions['superuser'] = $originalPermissions['superuser'];
|
||||||
|
|||||||
@@ -0,0 +1,989 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Events\CheckoutableCheckedIn;
|
||||||
|
use App\Mail\BulkDeleteReportMail;
|
||||||
|
use App\Models\Accessory;
|
||||||
|
use App\Models\AccessoryCheckout;
|
||||||
|
use App\Models\Actionlog;
|
||||||
|
use App\Models\Asset;
|
||||||
|
use App\Models\CheckoutAcceptance;
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\Component;
|
||||||
|
use App\Models\Consumable;
|
||||||
|
use App\Models\License;
|
||||||
|
use App\Models\LicenseSeat;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Symfony\Component\Console\Helper\ProgressBar;
|
||||||
|
|
||||||
|
use function Laravel\Prompts\confirm;
|
||||||
|
use function Laravel\Prompts\error;
|
||||||
|
use function Laravel\Prompts\info;
|
||||||
|
use function Laravel\Prompts\multisearch;
|
||||||
|
use function Laravel\Prompts\multiselect;
|
||||||
|
use function Laravel\Prompts\search;
|
||||||
|
use function Laravel\Prompts\select;
|
||||||
|
use function Laravel\Prompts\warning;
|
||||||
|
|
||||||
|
class BulkDelete extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'snipeit:checkin-delete-items';
|
||||||
|
|
||||||
|
protected $description = 'Interactively check in and/or delete items by company and type';
|
||||||
|
|
||||||
|
private const CHECKIN_NOTE = 'Checked in via bulk CLI operation';
|
||||||
|
|
||||||
|
private array $reportLines = [];
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
// Step 1: Dry run?
|
||||||
|
$dryRun = confirm(
|
||||||
|
label: 'Is this a dry run?',
|
||||||
|
default: true,
|
||||||
|
yes: 'Yes — preview only, no changes will be made',
|
||||||
|
no: 'No — LIVE RUN, changes WILL be made',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 2: Who are you?
|
||||||
|
$adminId = search(
|
||||||
|
label: 'Who are you? Search by username, first or last name.',
|
||||||
|
placeholder: 'Type to search users...',
|
||||||
|
options: function (string $value): array {
|
||||||
|
if (strlen($value) < 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return User::where('activated', 1)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->onlySuperAdmins()
|
||||||
|
->where(function ($query) use ($value) {
|
||||||
|
$query->where('username', 'like', "%{$value}%")
|
||||||
|
->orWhere('first_name', 'like', "%{$value}%")
|
||||||
|
->orWhere('last_name', 'like', "%{$value}%")
|
||||||
|
->orWhereRaw("CONCAT(first_name, ' ', last_name) LIKE ?", ["%{$value}%"]);
|
||||||
|
})
|
||||||
|
->get()
|
||||||
|
->mapWithKeys(fn (User $u) => [$u->id => "{$u->first_name} {$u->last_name} ({$u->username})"])
|
||||||
|
->toArray();
|
||||||
|
},
|
||||||
|
validate: fn (mixed $value) => ! $value ? 'A valid active user is required.' : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @var User $admin */
|
||||||
|
$admin = User::findOrFail((int) $adminId);
|
||||||
|
|
||||||
|
// Step 3: Which companies?
|
||||||
|
if (! Company::exists()) {
|
||||||
|
error('No companies found. Please create at least one company before using this command.');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedCompanyKeys = multisearch(
|
||||||
|
label: 'Which companies would you like to check in and delete items for?',
|
||||||
|
placeholder: 'Type to search companies...',
|
||||||
|
options: function (string $value): array {
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
if ($value === '' || str_contains('(no company / unassigned)', strtolower($value))) {
|
||||||
|
$results['__null__'] = '(No Company / Unassigned)';
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = Company::orderBy('name');
|
||||||
|
if ($value !== '') {
|
||||||
|
$query->where('name', 'like', "%{$value}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->get()->each(function (Company $c) use (&$results) {
|
||||||
|
$results[$c->id] = "{$c->name} (ID: {$c->id})";
|
||||||
|
});
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
},
|
||||||
|
scroll: 10,
|
||||||
|
required: 'Please select at least one company.',
|
||||||
|
hint: 'If you\'re searching on several differently named companies, use the up-arrow to go back to the search box to search again. ',
|
||||||
|
);
|
||||||
|
|
||||||
|
$includeNullCompany = in_array('__null__', $selectedCompanyKeys);
|
||||||
|
$selectedCompanyIds = array_values(array_filter(
|
||||||
|
$selectedCompanyKeys,
|
||||||
|
fn ($k) => $k !== '__null__'
|
||||||
|
));
|
||||||
|
|
||||||
|
$companyNamesById = Company::whereIn('id', $selectedCompanyIds)->pluck('name', 'id')->toArray();
|
||||||
|
$selectedCompanyNames = array_map(
|
||||||
|
fn ($id) => $id === '__null__' ? '(No Company)' : ($companyNamesById[$id] ?? "(ID: {$id})"),
|
||||||
|
$selectedCompanyKeys
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 4: Which item types?
|
||||||
|
$rawTypeSelection = multiselect(
|
||||||
|
label: 'What item types would you like to check in and delete?',
|
||||||
|
options: [
|
||||||
|
'all' => 'All Items (assets, licenses, accessories, components, consumables, users)',
|
||||||
|
'assets' => 'Assets',
|
||||||
|
'licenses' => 'Licenses',
|
||||||
|
'accessories' => 'Accessories',
|
||||||
|
'components' => 'Components',
|
||||||
|
'consumables' => 'Consumables',
|
||||||
|
'users' => 'Users',
|
||||||
|
],
|
||||||
|
required: 'Please select at least one item type.',
|
||||||
|
hint: 'Select "All Items" to process every supported type.',
|
||||||
|
);
|
||||||
|
|
||||||
|
$allSubTypes = ['assets', 'licenses', 'accessories', 'components', 'consumables', 'users'];
|
||||||
|
$selectedTypes = in_array('all', $rawTypeSelection)
|
||||||
|
? $allSubTypes
|
||||||
|
: array_values(array_intersect($allSubTypes, $rawTypeSelection));
|
||||||
|
|
||||||
|
// Compute and display counts now so the user can see what will be affected
|
||||||
|
$counts = $this->getCounts($selectedTypes, $selectedCompanyIds, $includeNullCompany);
|
||||||
|
|
||||||
|
$skipAdminUser = false;
|
||||||
|
|
||||||
|
$this->line('');
|
||||||
|
$this->line(' Items that would be affected:');
|
||||||
|
foreach ($counts as $type => $count) {
|
||||||
|
$this->line(sprintf(' %-14s %d', ucfirst($type).':', $count));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array('users', $selectedTypes)) {
|
||||||
|
$userInScope = $this->buildUserQuery($selectedCompanyIds, $includeNullCompany)
|
||||||
|
->where('users.id', $admin->id)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($userInScope) {
|
||||||
|
$skipAdminUser = true;
|
||||||
|
$counts['users'] = max(0, ($counts['users'] ?? 0) - 1);
|
||||||
|
warning(" Your user ({$admin->username}) is within the selected scope and will be skipped during user deletion.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->line('');
|
||||||
|
|
||||||
|
// Step 5: Hard delete, soft delete, or no delete?
|
||||||
|
$deleteType = select(
|
||||||
|
label: 'How should items be deleted?',
|
||||||
|
options: [
|
||||||
|
'soft' => 'Soft delete — items moved to trash (recoverable)',
|
||||||
|
'hard' => 'Hard delete — permanently removed (cannot be recovered)',
|
||||||
|
'none' => 'No delete — check in only, items remain in inventory',
|
||||||
|
],
|
||||||
|
default: 'soft',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 6: Send checkin notifications? (not applicable to users or consumables)
|
||||||
|
$notifiableTypes = array_intersect($selectedTypes, ['assets', 'licenses', 'accessories', 'components']);
|
||||||
|
$sendNotifications = false;
|
||||||
|
|
||||||
|
if (! empty($notifiableTypes)) {
|
||||||
|
$sendNotifications = confirm(
|
||||||
|
label: 'Should we send checkin notifications?',
|
||||||
|
default: true,
|
||||||
|
hint: 'Applies to: '.implode(', ', $notifiableTypes).'. Users and consumables are excluded.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 7: Clear related action_logs?
|
||||||
|
$clearLogs = confirm(
|
||||||
|
label: 'Should we clear related action logs?',
|
||||||
|
default: false,
|
||||||
|
hint: 'This removes all history for affected items, as if the data never existed.',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 8: Delete associated files?
|
||||||
|
$deleteFiles = false;
|
||||||
|
if ($deleteType !== 'none') {
|
||||||
|
$deleteFiles = confirm(
|
||||||
|
label: 'Should we also delete associated image and upload files?',
|
||||||
|
default: $deleteType === 'hard',
|
||||||
|
hint: 'Permanently removes images, avatars, signatures, EULAs, and action log uploads from disk.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 9: Delete the companies themselves?
|
||||||
|
$deleteCompanyType = 'keep';
|
||||||
|
if (! empty($selectedCompanyIds)) {
|
||||||
|
$deleteCompanyType = select(
|
||||||
|
label: 'Should the selected companies also be deleted?',
|
||||||
|
options: [
|
||||||
|
'keep' => 'Keep — do not delete the companies',
|
||||||
|
'soft' => 'Soft delete — companies moved to trash (recoverable)',
|
||||||
|
'hard' => 'Hard delete — permanently removed (cannot be recovered)',
|
||||||
|
],
|
||||||
|
default: 'keep',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 10: Backup first?
|
||||||
|
$doBackup = confirm(
|
||||||
|
label: 'Should we run a backup before proceeding?',
|
||||||
|
default: true,
|
||||||
|
hint: 'Strongly recommended. Saved as backup-before-bulk-delete-cli-[datetime].zip',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 11: Summary + final confirmation
|
||||||
|
$this->line('');
|
||||||
|
$this->line(' ════════════════════════════════════════════════════');
|
||||||
|
$this->line(' SUMMARY OF ACTIONS');
|
||||||
|
$this->line(' ════════════════════════════════════════════════════');
|
||||||
|
$this->line(" Admin user: {$admin->first_name} {$admin->last_name} ({$admin->username})");
|
||||||
|
$this->line(' Companies: '.implode(', ', $selectedCompanyNames));
|
||||||
|
$this->line(' Item types: '.implode(', ', $selectedTypes));
|
||||||
|
$this->line(" Delete mode: {$deleteType}");
|
||||||
|
$this->line(' Notifications: '.($sendNotifications ? 'Yes' : 'No'));
|
||||||
|
$this->line(' Clear logs: '.($clearLogs ? 'Yes' : 'No'));
|
||||||
|
$this->line(' Delete files: '.($deleteFiles ? 'Yes' : 'No'));
|
||||||
|
$this->line(' Delete companies: '.($deleteCompanyType === 'keep' ? 'No' : ucfirst($deleteCompanyType).' delete'));
|
||||||
|
$this->line(' Backup first: '.($doBackup ? 'Yes' : 'No'));
|
||||||
|
$this->line(' Dry run: '.($dryRun ? 'Yes' : 'No'));
|
||||||
|
$this->line('');
|
||||||
|
$this->line(' Items to be processed:');
|
||||||
|
foreach ($counts as $type => $count) {
|
||||||
|
$this->line(sprintf(' %-14s %d', ucfirst($type).':', $count));
|
||||||
|
}
|
||||||
|
if ($skipAdminUser) {
|
||||||
|
$this->line(' * Your user account will be skipped during user deletion.');
|
||||||
|
}
|
||||||
|
$this->line(' ════════════════════════════════════════════════════');
|
||||||
|
$this->line('');
|
||||||
|
|
||||||
|
// Step 10.5: Email report?
|
||||||
|
$sendEmailReport = false;
|
||||||
|
if ($admin->email) {
|
||||||
|
$sendEmailReport = confirm(
|
||||||
|
label: "Send an email report to {$admin->email}?",
|
||||||
|
default: false,
|
||||||
|
hint: 'A summary of all '.($dryRun ? 'would-be ' : '').'actions will be emailed to you.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $dryRun) {
|
||||||
|
$confirmed = confirm(
|
||||||
|
label: 'Are you sure you want to proceed? This cannot be undone.',
|
||||||
|
default: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (! $confirmed) {
|
||||||
|
info('Aborted. No changes were made.');
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run backup if requested
|
||||||
|
if ($doBackup && ! $dryRun) {
|
||||||
|
$backupFilename = 'backup-before-bulk-delete-cli-'.now()->format('Y-m-d-H-i-s');
|
||||||
|
info("Running backup ({$backupFilename}.zip)...");
|
||||||
|
$result = $this->callSilently('snipeit:backup', ['--filename' => $backupFilename]);
|
||||||
|
if ($result === 0) {
|
||||||
|
info("Backup completed: {$backupFilename}.zip");
|
||||||
|
} else {
|
||||||
|
warning("Backup may have failed (exit code {$result}). Proceeding anyway.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 11: Execute with progress bar
|
||||||
|
$totalItems = array_sum($counts);
|
||||||
|
$bar = $this->output->createProgressBar($totalItems > 0 ? $totalItems : 1);
|
||||||
|
$bar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %message%');
|
||||||
|
$bar->setMessage('Starting...');
|
||||||
|
$bar->start();
|
||||||
|
|
||||||
|
foreach ($selectedTypes as $type) {
|
||||||
|
match ($type) {
|
||||||
|
'assets' => $this->processAssets($selectedCompanyIds, $includeNullCompany, $sendNotifications, $admin, $dryRun, $deleteType, $clearLogs, $deleteFiles, $bar),
|
||||||
|
'licenses' => $this->processLicenses($selectedCompanyIds, $includeNullCompany, $sendNotifications, $admin, $dryRun, $deleteType, $clearLogs, $deleteFiles, $bar),
|
||||||
|
'accessories' => $this->processAccessories($selectedCompanyIds, $includeNullCompany, $sendNotifications, $admin, $dryRun, $deleteType, $clearLogs, $deleteFiles, $bar),
|
||||||
|
'components' => $this->processComponents($selectedCompanyIds, $includeNullCompany, $sendNotifications, $admin, $dryRun, $deleteType, $clearLogs, $deleteFiles, $bar),
|
||||||
|
'consumables' => $this->processConsumables($selectedCompanyIds, $includeNullCompany, $dryRun, $deleteType, $clearLogs, $deleteFiles, $bar),
|
||||||
|
'users' => $this->processUsers($selectedCompanyIds, $includeNullCompany, $admin, $skipAdminUser, $dryRun, $deleteType, $clearLogs, $deleteFiles, $bar),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->setMessage('Done.');
|
||||||
|
$bar->finish();
|
||||||
|
$this->line('');
|
||||||
|
$this->line('');
|
||||||
|
|
||||||
|
// Delete companies if requested
|
||||||
|
if ($deleteCompanyType !== 'keep' && ! empty($selectedCompanyIds)) {
|
||||||
|
$companies = Company::whereIn('id', $selectedCompanyIds)->get();
|
||||||
|
foreach ($companies as $company) {
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(" [dry-run] Would {$deleteCompanyType}-delete company {$company->name}");
|
||||||
|
$this->reportLines[] = "Would {$deleteCompanyType}-delete company {$company->name}";
|
||||||
|
} else {
|
||||||
|
if ($deleteCompanyType === 'soft') {
|
||||||
|
$company->delete();
|
||||||
|
} else {
|
||||||
|
$company->forceDelete();
|
||||||
|
}
|
||||||
|
// Remove any remaining pivot associations (e.g. the admin user who was
|
||||||
|
// skipped during user processing but is still a member of this company)
|
||||||
|
DB::table('company_user')->where('company_id', $company->id)->delete();
|
||||||
|
$this->reportLines[] = ucfirst($deleteCompanyType)."-deleted company {$company->name}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
warning('Dry run complete — no changes were made.');
|
||||||
|
} else {
|
||||||
|
info('All actions completed successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sendEmailReport && $admin->email) {
|
||||||
|
Mail::to($admin->email)->send(new BulkDeleteReportMail(
|
||||||
|
admin: $admin,
|
||||||
|
dryRun: $dryRun,
|
||||||
|
companyNames: $selectedCompanyNames,
|
||||||
|
selectedTypes: $selectedTypes,
|
||||||
|
deleteType: $deleteType,
|
||||||
|
reportLines: $this->reportLines,
|
||||||
|
runAt: now(),
|
||||||
|
));
|
||||||
|
info("Report sent to {$admin->email}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCounts(array $types, array $companyIds, bool $includeNull): array
|
||||||
|
{
|
||||||
|
$counts = [];
|
||||||
|
|
||||||
|
if (in_array('assets', $types)) {
|
||||||
|
$counts['assets'] = $this->buildCompanyQuery(Asset::query(), $companyIds, $includeNull)->count();
|
||||||
|
}
|
||||||
|
if (in_array('licenses', $types)) {
|
||||||
|
$counts['licenses'] = $this->buildCompanyQuery(License::query(), $companyIds, $includeNull)->count();
|
||||||
|
}
|
||||||
|
if (in_array('accessories', $types)) {
|
||||||
|
$counts['accessories'] = $this->buildCompanyQuery(Accessory::query(), $companyIds, $includeNull)->count();
|
||||||
|
}
|
||||||
|
if (in_array('components', $types)) {
|
||||||
|
$counts['components'] = $this->buildCompanyQuery(Component::query(), $companyIds, $includeNull)->count();
|
||||||
|
}
|
||||||
|
if (in_array('consumables', $types)) {
|
||||||
|
$counts['consumables'] = $this->buildCompanyQuery(Consumable::query(), $companyIds, $includeNull)->count();
|
||||||
|
}
|
||||||
|
if (in_array('users', $types)) {
|
||||||
|
$counts['users'] = $this->buildUserQuery($companyIds, $includeNull)->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildCompanyQuery(Builder $query, array $companyIds, bool $includeNull): Builder
|
||||||
|
{
|
||||||
|
return $query->where(function (Builder $q) use ($companyIds, $includeNull) {
|
||||||
|
if (! empty($companyIds)) {
|
||||||
|
$q->whereIn('company_id', $companyIds);
|
||||||
|
}
|
||||||
|
if ($includeNull) {
|
||||||
|
$method = ! empty($companyIds) ? 'orWhereNull' : 'whereNull';
|
||||||
|
$q->{$method}('company_id');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildUserQuery(array $companyIds, bool $includeNull): Builder
|
||||||
|
{
|
||||||
|
return User::query()
|
||||||
|
->where('activated', 1)
|
||||||
|
->where(function (Builder $q) use ($companyIds, $includeNull) {
|
||||||
|
if (! empty($companyIds)) {
|
||||||
|
$q->whereIn('company_id', $companyIds);
|
||||||
|
}
|
||||||
|
if ($includeNull) {
|
||||||
|
$method = ! empty($companyIds) ? 'orWhereNull' : 'whereNull';
|
||||||
|
$q->{$method}('company_id');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processAssets(
|
||||||
|
array $companyIds,
|
||||||
|
bool $includeNull,
|
||||||
|
bool $sendNotifications,
|
||||||
|
User $admin,
|
||||||
|
bool $dryRun,
|
||||||
|
string $deleteType,
|
||||||
|
bool $clearLogs,
|
||||||
|
bool $deleteFiles,
|
||||||
|
ProgressBar $bar,
|
||||||
|
): void {
|
||||||
|
$assets = $this->buildCompanyQuery(Asset::query(), $companyIds, $includeNull)->get();
|
||||||
|
|
||||||
|
foreach ($assets as $asset) {
|
||||||
|
$bar->setMessage("Assets: {$asset->asset_tag}");
|
||||||
|
|
||||||
|
if ($asset->assignedTo) {
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(" [dry-run] Would check in asset {$asset->asset_tag} from {$asset->assignedTo->name}");
|
||||||
|
$this->reportLines[] = "Would check in asset {$asset->asset_tag} (assigned to {$asset->assignedTo->name})";
|
||||||
|
} else {
|
||||||
|
$target = $asset->assignedTo;
|
||||||
|
$checkinAt = now()->format('Y-m-d H:i:s');
|
||||||
|
$originalValues = $asset->getRawOriginal();
|
||||||
|
|
||||||
|
if ($sendNotifications) {
|
||||||
|
event(new CheckoutableCheckedIn($asset, $target, $admin, self::CHECKIN_NOTE, $checkinAt, $originalValues));
|
||||||
|
DB::table('assets')->where('id', $asset->id)->update(['assigned_to' => null, 'assigned_type' => null]);
|
||||||
|
} else {
|
||||||
|
DB::table('assets')->where('id', $asset->id)->update(['assigned_to' => null, 'assigned_type' => null]);
|
||||||
|
$asset->logCheckin($target, self::CHECKIN_NOTE, $checkinAt, $originalValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->reportLines[] = "Checked in asset {$asset->asset_tag} from {$target->name}";
|
||||||
|
$asset->licenseseats()->update(['assigned_to' => null]);
|
||||||
|
|
||||||
|
CheckoutAcceptance::where('checkoutable_type', Asset::class)
|
||||||
|
->where('checkoutable_id', $asset->id)
|
||||||
|
->whereNull('accepted_at')
|
||||||
|
->whereNull('declined_at')
|
||||||
|
->forceDelete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $dryRun) {
|
||||||
|
// Collect action log file paths before logs may be cleared
|
||||||
|
$actionLogPaths = $deleteFiles
|
||||||
|
? $asset->assetlog()->whereNotNull('filename')->get()
|
||||||
|
->map(fn (Actionlog $log) => $log->uploads_file_path())
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->toArray()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Delete checkout acceptance files, then hard-remove all acceptances
|
||||||
|
if ($deleteFiles) {
|
||||||
|
CheckoutAcceptance::where('checkoutable_type', Asset::class)
|
||||||
|
->where('checkoutable_id', $asset->id)
|
||||||
|
->get()
|
||||||
|
->each(fn (CheckoutAcceptance $ca) => $this->deleteAcceptanceFiles($ca));
|
||||||
|
}
|
||||||
|
CheckoutAcceptance::where('checkoutable_type', Asset::class)
|
||||||
|
->where('checkoutable_id', $asset->id)
|
||||||
|
->forceDelete();
|
||||||
|
|
||||||
|
// Hard-delete-only cleanup: maintenance records, accessory checkouts to this
|
||||||
|
// asset, and any other assets that were assigned to this one
|
||||||
|
$maintenanceImages = [];
|
||||||
|
if ($deleteType === 'hard') {
|
||||||
|
if ($deleteFiles) {
|
||||||
|
$maintenanceImages = $asset->maintenances()
|
||||||
|
->whereNotNull('image')
|
||||||
|
->pluck('image')
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
$asset->maintenances()->forceDelete();
|
||||||
|
AccessoryCheckout::where('assigned_to', $asset->id)
|
||||||
|
->where('assigned_type', Asset::class)
|
||||||
|
->delete();
|
||||||
|
DB::table('assets')
|
||||||
|
->where('assigned_to', $asset->id)
|
||||||
|
->where('assigned_type', Asset::class)
|
||||||
|
->update(['assigned_to' => null, 'assigned_type' => null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
match ($deleteType) {
|
||||||
|
'soft' => $asset->delete(),
|
||||||
|
'hard' => $asset->forceDelete(),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($deleteType !== 'none') {
|
||||||
|
$this->reportLines[] = ucfirst($deleteType)."-deleted asset {$asset->asset_tag}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($clearLogs) {
|
||||||
|
$asset->assetlog()->forceDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($deleteFiles) {
|
||||||
|
if ($asset->image) {
|
||||||
|
$this->deleteStorageFile('public', app('assets_upload_path').$asset->image);
|
||||||
|
}
|
||||||
|
foreach ($maintenanceImages as $img) {
|
||||||
|
$this->deleteStorageFile('public', app('maintenances_upload_path').$img);
|
||||||
|
}
|
||||||
|
foreach ($actionLogPaths as $path) {
|
||||||
|
$this->deleteStorageFile('local', $path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($deleteType !== 'none') {
|
||||||
|
$this->line(" [dry-run] Would {$deleteType}-delete asset {$asset->asset_tag}");
|
||||||
|
$this->reportLines[] = "Would {$deleteType}-delete asset {$asset->asset_tag}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processLicenses(
|
||||||
|
array $companyIds,
|
||||||
|
bool $includeNull,
|
||||||
|
bool $sendNotifications,
|
||||||
|
User $admin,
|
||||||
|
bool $dryRun,
|
||||||
|
string $deleteType,
|
||||||
|
bool $clearLogs,
|
||||||
|
bool $deleteFiles,
|
||||||
|
ProgressBar $bar,
|
||||||
|
): void {
|
||||||
|
$licenses = $this->buildCompanyQuery(License::query(), $companyIds, $includeNull)->get();
|
||||||
|
|
||||||
|
foreach ($licenses as $license) {
|
||||||
|
$bar->setMessage("Licenses: {$license->name}");
|
||||||
|
|
||||||
|
$seats = LicenseSeat::where('license_id', $license->id)
|
||||||
|
->where(fn ($q) => $q->whereNotNull('assigned_to')->orWhereNotNull('asset_id'))
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($seats as $seat) {
|
||||||
|
$target = $seat->assigned_to ? $seat->user : $seat->asset;
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(" [dry-run] Would check in license seat for {$license->name} from ".($target?->name ?? $target?->asset_tag ?? 'unknown'));
|
||||||
|
$this->reportLines[] = "Would check in license seat for {$license->name} from ".($target?->name ?? $target?->asset_tag ?? 'unknown');
|
||||||
|
} else {
|
||||||
|
$seat->assigned_to = null;
|
||||||
|
$seat->asset_id = null;
|
||||||
|
$seat->save();
|
||||||
|
|
||||||
|
$this->reportLines[] = "Checked in license seat for {$license->name} from ".($target?->name ?? $target?->asset_tag ?? 'unknown');
|
||||||
|
|
||||||
|
if ($target) {
|
||||||
|
if ($sendNotifications) {
|
||||||
|
event(new CheckoutableCheckedIn($seat, $target, $admin, self::CHECKIN_NOTE));
|
||||||
|
} else {
|
||||||
|
$seat->logCheckin($target, self::CHECKIN_NOTE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $dryRun) {
|
||||||
|
// Collect action log file paths before logs may be cleared
|
||||||
|
$actionLogPaths = $deleteFiles
|
||||||
|
? $license->assetlog()->whereNotNull('filename')->get()
|
||||||
|
->map(fn (Actionlog $log) => $log->uploads_file_path())
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->toArray()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if ($deleteType === 'soft') {
|
||||||
|
$license->licenseseats()->delete();
|
||||||
|
$license->delete();
|
||||||
|
$this->reportLines[] = "Soft-deleted license {$license->name}";
|
||||||
|
} elseif ($deleteType === 'hard') {
|
||||||
|
$seatIds = $license->licenseseats()->pluck('id');
|
||||||
|
if ($deleteFiles) {
|
||||||
|
CheckoutAcceptance::where('checkoutable_type', LicenseSeat::class)
|
||||||
|
->whereIn('checkoutable_id', $seatIds)
|
||||||
|
->get()
|
||||||
|
->each(fn (CheckoutAcceptance $ca) => $this->deleteAcceptanceFiles($ca));
|
||||||
|
}
|
||||||
|
CheckoutAcceptance::where('checkoutable_type', LicenseSeat::class)
|
||||||
|
->whereIn('checkoutable_id', $seatIds)
|
||||||
|
->forceDelete();
|
||||||
|
$license->licenseseats()->forceDelete();
|
||||||
|
DB::table('kits_licenses')->where('license_id', $license->id)->delete();
|
||||||
|
$license->forceDelete();
|
||||||
|
$this->reportLines[] = "Hard-deleted license {$license->name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($clearLogs) {
|
||||||
|
$license->assetlog()->forceDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($deleteFiles) {
|
||||||
|
foreach ($actionLogPaths as $path) {
|
||||||
|
$this->deleteStorageFile('local', $path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($deleteType !== 'none') {
|
||||||
|
$this->line(" [dry-run] Would {$deleteType}-delete license {$license->name}");
|
||||||
|
$this->reportLines[] = "Would {$deleteType}-delete license {$license->name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processAccessories(
|
||||||
|
array $companyIds,
|
||||||
|
bool $includeNull,
|
||||||
|
bool $sendNotifications,
|
||||||
|
User $admin,
|
||||||
|
bool $dryRun,
|
||||||
|
string $deleteType,
|
||||||
|
bool $clearLogs,
|
||||||
|
bool $deleteFiles,
|
||||||
|
ProgressBar $bar,
|
||||||
|
): void {
|
||||||
|
$accessories = $this->buildCompanyQuery(Accessory::query(), $companyIds, $includeNull)->get();
|
||||||
|
|
||||||
|
foreach ($accessories as $accessory) {
|
||||||
|
$bar->setMessage("Accessories: {$accessory->name}");
|
||||||
|
|
||||||
|
$checkouts = AccessoryCheckout::where('accessory_id', $accessory->id)->get();
|
||||||
|
|
||||||
|
foreach ($checkouts as $checkout) {
|
||||||
|
$target = $checkout->assignedTo;
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(" [dry-run] Would check in accessory {$accessory->name} from ".($target?->name ?? 'unknown'));
|
||||||
|
$this->reportLines[] = "Would check in accessory {$accessory->name} from ".($target?->name ?? 'unknown');
|
||||||
|
} else {
|
||||||
|
$checkinAt = now()->format('Y-m-d H:i:s');
|
||||||
|
$checkout->delete();
|
||||||
|
|
||||||
|
$this->reportLines[] = "Checked in accessory {$accessory->name} from ".($target?->name ?? 'unknown');
|
||||||
|
|
||||||
|
if ($target) {
|
||||||
|
if ($sendNotifications) {
|
||||||
|
event(new CheckoutableCheckedIn($accessory, $target, $admin, self::CHECKIN_NOTE, $checkinAt));
|
||||||
|
} else {
|
||||||
|
$accessory->logCheckin($target, self::CHECKIN_NOTE, $checkinAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $dryRun) {
|
||||||
|
// Collect action log file paths before logs may be cleared
|
||||||
|
$actionLogPaths = $deleteFiles
|
||||||
|
? $accessory->assetlog()->whereNotNull('filename')->get()
|
||||||
|
->map(fn (Actionlog $log) => $log->uploads_file_path())
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->toArray()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if ($clearLogs) {
|
||||||
|
$accessory->assetlog()->forceDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($deleteType === 'hard') {
|
||||||
|
DB::table('kits_accessories')->where('accessory_id', $accessory->id)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
match ($deleteType) {
|
||||||
|
'soft' => $accessory->delete(),
|
||||||
|
'hard' => $accessory->forceDelete(),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($deleteType !== 'none') {
|
||||||
|
$this->reportLines[] = ucfirst($deleteType)."-deleted accessory {$accessory->name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($deleteFiles) {
|
||||||
|
if ($accessory->image) {
|
||||||
|
$this->deleteStorageFile('public', app('accessories_upload_path').$accessory->image);
|
||||||
|
}
|
||||||
|
foreach ($actionLogPaths as $path) {
|
||||||
|
$this->deleteStorageFile('local', $path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($deleteType !== 'none') {
|
||||||
|
$this->line(" [dry-run] Would {$deleteType}-delete accessory {$accessory->name}");
|
||||||
|
$this->reportLines[] = "Would {$deleteType}-delete accessory {$accessory->name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processComponents(
|
||||||
|
array $companyIds,
|
||||||
|
bool $includeNull,
|
||||||
|
bool $sendNotifications,
|
||||||
|
User $admin,
|
||||||
|
bool $dryRun,
|
||||||
|
string $deleteType,
|
||||||
|
bool $clearLogs,
|
||||||
|
bool $deleteFiles,
|
||||||
|
ProgressBar $bar,
|
||||||
|
): void {
|
||||||
|
$components = $this->buildCompanyQuery(Component::query(), $companyIds, $includeNull)->get();
|
||||||
|
|
||||||
|
foreach ($components as $component) {
|
||||||
|
$bar->setMessage("Components: {$component->name}");
|
||||||
|
|
||||||
|
$assignments = DB::table('components_assets')
|
||||||
|
->where('component_id', $component->id)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($assignments as $assignment) {
|
||||||
|
$asset = Asset::find($assignment->asset_id);
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(" [dry-run] Would check in component {$component->name} from asset ".($asset?->asset_tag ?? 'unknown'));
|
||||||
|
$this->reportLines[] = "Would check in component {$component->name} from asset ".($asset?->asset_tag ?? 'unknown');
|
||||||
|
} else {
|
||||||
|
$checkinAt = now()->format('Y-m-d H:i:s');
|
||||||
|
DB::table('components_assets')->where('id', $assignment->id)->delete();
|
||||||
|
|
||||||
|
$this->reportLines[] = "Checked in component {$component->name} from asset ".($asset?->asset_tag ?? 'unknown');
|
||||||
|
|
||||||
|
if ($asset) {
|
||||||
|
if ($sendNotifications) {
|
||||||
|
event(new CheckoutableCheckedIn($component, $asset, $admin, self::CHECKIN_NOTE, $checkinAt));
|
||||||
|
} else {
|
||||||
|
$component->logCheckin($asset, self::CHECKIN_NOTE, $checkinAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $dryRun) {
|
||||||
|
// Collect action log file paths before logs may be cleared
|
||||||
|
$actionLogPaths = $deleteFiles
|
||||||
|
? $component->assetlog()->whereNotNull('filename')->get()
|
||||||
|
->map(fn (Actionlog $log) => $log->uploads_file_path())
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->toArray()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if ($clearLogs) {
|
||||||
|
$component->assetlog()->forceDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
match ($deleteType) {
|
||||||
|
'soft' => $component->delete(),
|
||||||
|
'hard' => $component->forceDelete(),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($deleteType !== 'none') {
|
||||||
|
$this->reportLines[] = ucfirst($deleteType)."-deleted component {$component->name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($deleteFiles) {
|
||||||
|
if ($component->image) {
|
||||||
|
$this->deleteStorageFile('public', app('components_upload_path').$component->image);
|
||||||
|
}
|
||||||
|
foreach ($actionLogPaths as $path) {
|
||||||
|
$this->deleteStorageFile('local', $path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($deleteType !== 'none') {
|
||||||
|
$this->line(" [dry-run] Would {$deleteType}-delete component {$component->name}");
|
||||||
|
$this->reportLines[] = "Would {$deleteType}-delete component {$component->name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processConsumables(
|
||||||
|
array $companyIds,
|
||||||
|
bool $includeNull,
|
||||||
|
bool $dryRun,
|
||||||
|
string $deleteType,
|
||||||
|
bool $clearLogs,
|
||||||
|
bool $deleteFiles,
|
||||||
|
ProgressBar $bar,
|
||||||
|
): void {
|
||||||
|
$consumables = $this->buildCompanyQuery(Consumable::query(), $companyIds, $includeNull)->get();
|
||||||
|
|
||||||
|
foreach ($consumables as $consumable) {
|
||||||
|
$bar->setMessage("Consumables: {$consumable->name}");
|
||||||
|
|
||||||
|
if (! $dryRun) {
|
||||||
|
// Collect action log file paths before logs may be cleared
|
||||||
|
$actionLogPaths = $deleteFiles
|
||||||
|
? $consumable->assetlog()->whereNotNull('filename')->get()
|
||||||
|
->map(fn (Actionlog $log) => $log->uploads_file_path())
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->toArray()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if ($clearLogs) {
|
||||||
|
$consumable->assetlog()->forceDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($deleteType === 'hard') {
|
||||||
|
DB::table('kits_consumables')->where('consumable_id', $consumable->id)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
match ($deleteType) {
|
||||||
|
'soft' => $consumable->delete(),
|
||||||
|
'hard' => $consumable->forceDelete(),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($deleteType !== 'none') {
|
||||||
|
$this->reportLines[] = ucfirst($deleteType)."-deleted consumable {$consumable->name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($deleteFiles) {
|
||||||
|
if ($consumable->image) {
|
||||||
|
$this->deleteStorageFile('public', app('consumables_upload_path').$consumable->image);
|
||||||
|
}
|
||||||
|
foreach ($actionLogPaths as $path) {
|
||||||
|
$this->deleteStorageFile('local', $path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($deleteType !== 'none') {
|
||||||
|
$this->line(" [dry-run] Would {$deleteType}-delete consumable {$consumable->name}");
|
||||||
|
$this->reportLines[] = "Would {$deleteType}-delete consumable {$consumable->name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processUsers(
|
||||||
|
array $companyIds,
|
||||||
|
bool $includeNull,
|
||||||
|
User $admin,
|
||||||
|
bool $skipAdminUser,
|
||||||
|
bool $dryRun,
|
||||||
|
string $deleteType,
|
||||||
|
bool $clearLogs,
|
||||||
|
bool $deleteFiles,
|
||||||
|
ProgressBar $bar,
|
||||||
|
): void {
|
||||||
|
$users = $this->buildUserQuery($companyIds, $includeNull)->get();
|
||||||
|
|
||||||
|
foreach ($users as $user) {
|
||||||
|
if ($skipAdminUser && $user->id === $admin->id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->setMessage("Users: {$user->username}");
|
||||||
|
|
||||||
|
// If real companies were selected, check whether this user also belongs to
|
||||||
|
// companies outside the selected scope. If so, only remove the selected-company
|
||||||
|
// associations and skip full deletion to avoid orphaning them from their other companies.
|
||||||
|
if (! empty($companyIds)) {
|
||||||
|
$allUserCompanyIds = array_unique(array_filter(array_merge(
|
||||||
|
$user->companies()->pluck('companies.id')->toArray(),
|
||||||
|
$user->company_id ? [$user->company_id] : [],
|
||||||
|
)));
|
||||||
|
$outsideCompanyIds = array_values(array_diff($allUserCompanyIds, $companyIds));
|
||||||
|
|
||||||
|
if (! empty($outsideCompanyIds)) {
|
||||||
|
$outsideNames = Company::whereIn('id', $outsideCompanyIds)->pluck('name')->implode(', ');
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(" [dry-run] Would partially disassociate user {$user->username} (also belongs to: {$outsideNames})");
|
||||||
|
$this->reportLines[] = "Would partially disassociate user {$user->username} — also belongs to: {$outsideNames}";
|
||||||
|
} else {
|
||||||
|
$user->companies()->detach($companyIds);
|
||||||
|
warning(" Skipped full deletion of {$user->username}: they also belong to {$outsideNames}. Removed selected company associations only.");
|
||||||
|
$this->reportLines[] = "Partially disassociated user {$user->username} — also belongs to: {$outsideNames}. Full deletion skipped.";
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->advance();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $dryRun) {
|
||||||
|
// Collect file paths and acceptance records before deleting pivot data
|
||||||
|
$acceptancesToDelete = $deleteFiles
|
||||||
|
? CheckoutAcceptance::where('assigned_to_id', $user->id)->get()
|
||||||
|
: collect();
|
||||||
|
|
||||||
|
$actionLogPaths = $deleteFiles
|
||||||
|
? Actionlog::where('item_type', User::class)
|
||||||
|
->where('item_id', $user->id)
|
||||||
|
->where('action_type', 'uploaded')
|
||||||
|
->whereNotNull('filename')
|
||||||
|
->get()
|
||||||
|
->map(fn (Actionlog $log) => $log->uploads_file_path())
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->toArray()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Clear pivot/assignment data that will orphan on deletion
|
||||||
|
LicenseSeat::where('assigned_to', $user->id)->update(['assigned_to' => null]);
|
||||||
|
AccessoryCheckout::where('assigned_to', $user->id)
|
||||||
|
->where('assigned_type', User::class)
|
||||||
|
->delete();
|
||||||
|
DB::table('consumables_users')->where('assigned_to', $user->id)->delete();
|
||||||
|
CheckoutAcceptance::where('assigned_to_id', $user->id)->forceDelete();
|
||||||
|
if ($deleteType === 'hard') {
|
||||||
|
DB::table('company_user')->where('user_id', $user->id)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($clearLogs) {
|
||||||
|
$user->userlog()->forceDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
match ($deleteType) {
|
||||||
|
'soft' => $user->delete(),
|
||||||
|
'hard' => $user->forceDelete(),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($deleteType !== 'none') {
|
||||||
|
$this->reportLines[] = ucfirst($deleteType)."-deleted user {$user->username}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($deleteFiles) {
|
||||||
|
if ($user->avatar) {
|
||||||
|
$this->deleteStorageFile('public', app('users_upload_path').$user->avatar);
|
||||||
|
}
|
||||||
|
$acceptancesToDelete->each(fn (CheckoutAcceptance $ca) => $this->deleteAcceptanceFiles($ca));
|
||||||
|
foreach ($actionLogPaths as $path) {
|
||||||
|
$this->deleteStorageFile('local', $path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($deleteType !== 'none') {
|
||||||
|
$this->line(" [dry-run] Would {$deleteType}-delete user {$user->username}");
|
||||||
|
$this->reportLines[] = "Would {$deleteType}-delete user {$user->username}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteStorageFile(string $disk, ?string $path): void
|
||||||
|
{
|
||||||
|
if (! $path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$storage = $disk === 'public'
|
||||||
|
? Storage::disk('public')
|
||||||
|
: Storage::disk(config('filesystems.default'));
|
||||||
|
if ($storage->exists($path)) {
|
||||||
|
$storage->delete($path);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::warning("Could not delete file {$path}: ".$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteAcceptanceFiles(CheckoutAcceptance $acceptance): void
|
||||||
|
{
|
||||||
|
if ($acceptance->signature_filename) {
|
||||||
|
$this->deleteStorageFile('local', 'private_uploads/signatures/'.$acceptance->signature_filename);
|
||||||
|
}
|
||||||
|
if ($acceptance->stored_eula_file) {
|
||||||
|
$this->deleteStorageFile('local', 'private_uploads/eula-pdfs/'.$acceptance->stored_eula_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Events\CheckoutableCheckedIn;
|
||||||
|
use App\Models\Accessory;
|
||||||
|
use App\Models\AccessoryCheckout;
|
||||||
|
use App\Models\Asset;
|
||||||
|
use App\Models\CheckoutAcceptance;
|
||||||
|
use App\Models\Component;
|
||||||
|
use App\Models\License;
|
||||||
|
use App\Models\LicenseSeat;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class CheckinAndDeleteItems extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'snipeit:checkin-delete-all
|
||||||
|
{--company-id= : Only process items belonging to this company ID}
|
||||||
|
{--admin-id= : ID of the user credited for the checkins (defaults to first superadmin)}
|
||||||
|
{--no-notifications : Suppress email and webhook notifications}
|
||||||
|
{--type=all : Comma-separated types to process: assets, licenses, accessories, components, or all}
|
||||||
|
{--note= : Note recorded on each checkin action log entry}
|
||||||
|
{--dry-run : Preview what would be processed without making any changes}
|
||||||
|
{--force : Skip the confirmation prompt}';
|
||||||
|
|
||||||
|
protected $description = 'Check in all assigned items and soft-delete them, optionally scoped to a company';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$companyId = $this->option('company-id');
|
||||||
|
$noNotifications = $this->option('no-notifications');
|
||||||
|
$dryRun = $this->option('dry-run');
|
||||||
|
$typeOption = $this->option('type') ?? 'all';
|
||||||
|
$note = $this->option('note') ?: 'Checked in and deleted via CLI';
|
||||||
|
|
||||||
|
$allTypes = ['assets', 'licenses', 'accessories', 'components'];
|
||||||
|
$typesToProcess = $typeOption === 'all'
|
||||||
|
? $allTypes
|
||||||
|
: array_intersect(array_map('trim', explode(',', $typeOption)), $allTypes);
|
||||||
|
|
||||||
|
if (empty($typesToProcess)) {
|
||||||
|
$this->error('Invalid --type value. Use: assets, licenses, accessories, components, or all.');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$admin = null;
|
||||||
|
if (! $dryRun && ! $noNotifications) {
|
||||||
|
if ($this->option('admin-id')) {
|
||||||
|
$admin = User::find($this->option('admin-id'));
|
||||||
|
if (! $admin) {
|
||||||
|
$this->error('No user found with admin-id '.$this->option('admin-id').'.');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$admin = User::onlySuperAdmins()->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $admin) {
|
||||||
|
$this->warn('No admin user found — notifications will be suppressed.');
|
||||||
|
$noNotifications = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scopeMsg = $companyId ? "company ID {$companyId}" : 'all companies';
|
||||||
|
$typesMsg = implode(', ', $typesToProcess);
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->warn('DRY RUN — no changes will be made.');
|
||||||
|
} elseif (! $this->option('force')) {
|
||||||
|
if (! $this->confirm("This will check in and soft-delete all [{$typesMsg}] for [{$scopeMsg}]. Continue?")) {
|
||||||
|
$this->info('Aborted.');
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array('assets', $typesToProcess)) {
|
||||||
|
$this->processAssets($companyId, $noNotifications, $note, $admin, $dryRun);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array('licenses', $typesToProcess)) {
|
||||||
|
$this->processLicenses($companyId, $noNotifications, $note, $admin, $dryRun);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array('accessories', $typesToProcess)) {
|
||||||
|
$this->processAccessories($companyId, $noNotifications, $note, $admin, $dryRun);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array('components', $typesToProcess)) {
|
||||||
|
$this->processComponents($companyId, $noNotifications, $note, $admin, $dryRun);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->warn('Dry run complete — no changes were made.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processAssets(?string $companyId, bool $noNotifications, string $note, ?User $admin, bool $dryRun): void
|
||||||
|
{
|
||||||
|
$query = Asset::query();
|
||||||
|
if ($companyId) {
|
||||||
|
$query->where('company_id', $companyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$assets = $query->get();
|
||||||
|
$checkedIn = 0;
|
||||||
|
$deleted = 0;
|
||||||
|
|
||||||
|
foreach ($assets as $asset) {
|
||||||
|
if ($asset->assignedTo) {
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(' Would check in asset: '.$asset->asset_tag.' (assigned to '.$asset->assignedTo->name.')');
|
||||||
|
} else {
|
||||||
|
$target = $asset->assignedTo;
|
||||||
|
$checkin_at = now()->format('Y-m-d H:i:s');
|
||||||
|
$originalValues = $asset->getRawOriginal();
|
||||||
|
|
||||||
|
if ($noNotifications) {
|
||||||
|
DB::table('assets')->where('id', $asset->id)
|
||||||
|
->update(['assigned_to' => null, 'assigned_type' => null]);
|
||||||
|
$asset->logCheckin($target, $note, $checkin_at, $originalValues);
|
||||||
|
} else {
|
||||||
|
// Fire event before clearing so the log captures the original state
|
||||||
|
event(new CheckoutableCheckedIn($asset, $target, $admin, $note, $checkin_at, $originalValues));
|
||||||
|
DB::table('assets')->where('id', $asset->id)
|
||||||
|
->update(['assigned_to' => null, 'assigned_type' => null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$asset->licenseseats()->update(['assigned_to' => null]);
|
||||||
|
|
||||||
|
CheckoutAcceptance::pending()
|
||||||
|
->whereHasMorph('checkoutable', [Asset::class], fn (Builder $q) => $q->where('id', $asset->id))
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkedIn++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(' Would delete asset: '.$asset->asset_tag);
|
||||||
|
} else {
|
||||||
|
$asset->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $dryRun ? 'would be' : 'were';
|
||||||
|
$this->info("Assets: {$checkedIn} {$action} checked in, {$deleted} {$action} deleted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processLicenses(?string $companyId, bool $noNotifications, string $note, ?User $admin, bool $dryRun): void
|
||||||
|
{
|
||||||
|
$query = License::query();
|
||||||
|
if ($companyId) {
|
||||||
|
$query->where('company_id', $companyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$licenses = $query->get();
|
||||||
|
$seatsCheckedIn = 0;
|
||||||
|
$deleted = 0;
|
||||||
|
|
||||||
|
foreach ($licenses as $license) {
|
||||||
|
$seats = LicenseSeat::where('license_id', $license->id)
|
||||||
|
->where(fn ($q) => $q->whereNotNull('assigned_to')->orWhereNotNull('asset_id'))
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($seats as $seat) {
|
||||||
|
$target = $seat->assigned_to ? $seat->user : $seat->asset;
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(' Would check in license seat for: '.$license->name.' (assigned to '.($target?->name ?? $target?->asset_tag ?? 'unknown').')');
|
||||||
|
} else {
|
||||||
|
$seat->assigned_to = null;
|
||||||
|
$seat->asset_id = null;
|
||||||
|
$seat->save();
|
||||||
|
|
||||||
|
if ($target) {
|
||||||
|
if ($noNotifications) {
|
||||||
|
$seat->logCheckin($target, $note);
|
||||||
|
} else {
|
||||||
|
event(new CheckoutableCheckedIn($seat, $target, $admin, $note));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$seatsCheckedIn++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(' Would delete license: '.$license->name);
|
||||||
|
} else {
|
||||||
|
$license->licenseseats()->delete();
|
||||||
|
$license->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $dryRun ? 'would be' : 'were';
|
||||||
|
$this->info("Licenses: {$seatsCheckedIn} seats {$action} checked in, {$deleted} licenses {$action} deleted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processAccessories(?string $companyId, bool $noNotifications, string $note, ?User $admin, bool $dryRun): void
|
||||||
|
{
|
||||||
|
$query = Accessory::query();
|
||||||
|
if ($companyId) {
|
||||||
|
$query->where('company_id', $companyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$accessories = $query->get();
|
||||||
|
$checkedIn = 0;
|
||||||
|
$deleted = 0;
|
||||||
|
|
||||||
|
foreach ($accessories as $accessory) {
|
||||||
|
$checkouts = AccessoryCheckout::where('accessory_id', $accessory->id)->get();
|
||||||
|
|
||||||
|
foreach ($checkouts as $checkout) {
|
||||||
|
$target = $checkout->assignedTo;
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(' Would check in accessory: '.$accessory->name.' (assigned to '.($target?->name ?? $target?->asset_tag ?? 'unknown').')');
|
||||||
|
} else {
|
||||||
|
$checkin_at = now()->format('Y-m-d H:i:s');
|
||||||
|
$checkout->delete();
|
||||||
|
|
||||||
|
if ($target) {
|
||||||
|
if ($noNotifications) {
|
||||||
|
$accessory->logCheckin($target, $note, $checkin_at);
|
||||||
|
} else {
|
||||||
|
event(new CheckoutableCheckedIn($accessory, $target, $admin, $note, $checkin_at));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkedIn++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(' Would delete accessory: '.$accessory->name);
|
||||||
|
} else {
|
||||||
|
$accessory->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $dryRun ? 'would be' : 'were';
|
||||||
|
$this->info("Accessories: {$checkedIn} {$action} checked in, {$deleted} {$action} deleted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processComponents(?string $companyId, bool $noNotifications, string $note, ?User $admin, bool $dryRun): void
|
||||||
|
{
|
||||||
|
$query = Component::query();
|
||||||
|
if ($companyId) {
|
||||||
|
$query->where('company_id', $companyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$components = $query->get();
|
||||||
|
$checkedIn = 0;
|
||||||
|
$deleted = 0;
|
||||||
|
|
||||||
|
foreach ($components as $component) {
|
||||||
|
$assignments = DB::table('components_assets')
|
||||||
|
->where('component_id', $component->id)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($assignments as $assignment) {
|
||||||
|
$asset = Asset::find($assignment->asset_id);
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(' Would check in component: '.$component->name.' (assigned to '.($asset?->asset_tag ?? 'unknown').')');
|
||||||
|
} else {
|
||||||
|
$checkin_at = now()->format('Y-m-d H:i:s');
|
||||||
|
DB::table('components_assets')->where('id', $assignment->id)->delete();
|
||||||
|
|
||||||
|
if ($asset) {
|
||||||
|
if ($noNotifications) {
|
||||||
|
$component->logCheckin($asset, $note, $checkin_at);
|
||||||
|
} else {
|
||||||
|
event(new CheckoutableCheckedIn($component, $asset, $admin, $note, $checkin_at));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkedIn++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(' Would delete component: '.$component->name);
|
||||||
|
} else {
|
||||||
|
$component->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $dryRun ? 'would be' : 'were';
|
||||||
|
$this->info("Components: {$checkedIn} {$action} checked in, {$deleted} {$action} deleted.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,41 +30,77 @@ class CleanIncorrectCheckoutAcceptances extends Command
|
|||||||
{
|
{
|
||||||
$deletions = 0;
|
$deletions = 0;
|
||||||
$skips = 0;
|
$skips = 0;
|
||||||
|
$total = CheckoutAcceptance::count();
|
||||||
|
|
||||||
// This walks *every* checkoutacceptance. That's gnarly. But necessary
|
$this->info("Processing {$total} checkout acceptances...");
|
||||||
$this->withProgressBar(CheckoutAcceptance::all(), function ($checkoutAcceptance) use (&$deletions, &$skips) {
|
$bar = $this->output->createProgressBar($total);
|
||||||
$item = $checkoutAcceptance->checkoutable;
|
$bar->start();
|
||||||
$checkout_to_id = $checkoutAcceptance->assigned_to_id;
|
|
||||||
if (is_null($item)) {
|
|
||||||
$this->info("'Checkoutable' Item is null, going to next record");
|
|
||||||
|
|
||||||
return; // 'false' allegedly breaks execution entirely, so 'true' maybe doesn't? hrm. just straight return maybe?
|
// Chunk to avoid loading the whole table into memory; eager-load checkoutable
|
||||||
}
|
// to eliminate the N+1 on that relationship.
|
||||||
if (get_class($item) == LicenseSeat::class) {
|
CheckoutAcceptance::with('checkoutable')
|
||||||
$item = $item->license;
|
->chunkById(500, function ($chunk) use (&$deletions, &$skips, $bar) {
|
||||||
}
|
$idsToDelete = [];
|
||||||
foreach ($item->assetlog()->where('action_type', 'checkout')->get() as $assetlog) {
|
|
||||||
if ($assetlog->target_id == $checkout_to_id && $assetlog->target_type != User::class) {
|
|
||||||
// We have a checkout-to an ID for a non-User, which matches to an ID in the checkout_acceptances table
|
|
||||||
|
|
||||||
// now, let's compare the _times_ - are they close?
|
foreach ($chunk as $checkoutAcceptance) {
|
||||||
// I'm picking `created_at` over `action_date` because I'm more interested in when the actionlogs
|
$item = $checkoutAcceptance->checkoutable;
|
||||||
// were _created_, not when they were alleged to have happened - those created_at times need to be within 'X' seconds of
|
$checkout_to_id = $checkoutAcceptance->assigned_to_id;
|
||||||
// each other (currently 5)
|
|
||||||
if ($assetlog->created_at->diffInSeconds($checkoutAcceptance->created_at, true) <= 5) { // we're allowing for five _ish_ seconds of slop
|
|
||||||
$deletions++;
|
|
||||||
$checkoutAcceptance->forceDelete(); // HARD delete this record; it should have never been
|
|
||||||
|
|
||||||
return;
|
if (is_null($item)) {
|
||||||
} else {
|
$skips++;
|
||||||
// $this->info("The two records are too far apart");
|
$bar->advance();
|
||||||
|
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// $this->info("No match! checkout to id: " . $checkout_to_id." target_id: ".$assetlog->target_id." target_type: ".$assetlog->target_type);
|
if (get_class($item) === LicenseSeat::class) {
|
||||||
|
$item = $item->license;
|
||||||
|
if (is_null($item)) {
|
||||||
|
$skips++;
|
||||||
|
$bar->advance();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_null($checkoutAcceptance->created_at)) {
|
||||||
|
$skips++;
|
||||||
|
$bar->advance();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push all filtering (including the ±5-second window) into the DB;
|
||||||
|
// exists() returns as soon as one matching row is found rather than
|
||||||
|
// fetching all checkout logs into PHP.
|
||||||
|
$shouldDelete = $item->assetlog()
|
||||||
|
->where('action_type', 'checkout')
|
||||||
|
->where('target_id', $checkout_to_id)
|
||||||
|
->where('target_type', '!=', User::class)
|
||||||
|
->whereBetween('created_at', [
|
||||||
|
$checkoutAcceptance->created_at->copy()->subSeconds(5),
|
||||||
|
$checkoutAcceptance->created_at->copy()->addSeconds(5),
|
||||||
|
])
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($shouldDelete) {
|
||||||
|
$idsToDelete[] = $checkoutAcceptance->id;
|
||||||
|
$deletions++;
|
||||||
|
} else {
|
||||||
|
$skips++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar->advance();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
$skips++;
|
// Bulk-delete the bad records in one query per chunk instead of one per row.
|
||||||
});
|
if (! empty($idsToDelete)) {
|
||||||
$this->error("Final deletion count: $deletions, and skip count: $skips");
|
CheckoutAcceptance::whereIn('id', $idsToDelete)->forceDelete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$bar->finish();
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("Final deletion count: {$deletions}, and skip count: {$skips}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Console\Commands;
|
|||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
class ResetDemoSettings extends Command
|
class ResetDemoSettings extends Command
|
||||||
{
|
{
|
||||||
@@ -47,7 +48,7 @@ class ResetDemoSettings extends Command
|
|||||||
$settings->auto_increment_assets = 1;
|
$settings->auto_increment_assets = 1;
|
||||||
$settings->logo = 'snipe-logo.png';
|
$settings->logo = 'snipe-logo.png';
|
||||||
$settings->alert_email = 'service@snipe-it.io';
|
$settings->alert_email = 'service@snipe-it.io';
|
||||||
$settings->login_note = 'Use `admin` / `password` to login to the demo.';
|
$settings->login_note = "Use any of the following credentials to login to the demo:\n\n- `admin` / `password`\n- `assets` / `password`\n- `testuser` / `password`";
|
||||||
$settings->header_color = '#3c8dbc';
|
$settings->header_color = '#3c8dbc';
|
||||||
$settings->link_dark_color = '#5fa4cc';
|
$settings->link_dark_color = '#5fa4cc';
|
||||||
$settings->link_light_color = '#296282;';
|
$settings->link_light_color = '#296282;';
|
||||||
@@ -85,6 +86,44 @@ class ResetDemoSettings extends Command
|
|||||||
$user->save();
|
$user->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$assetsUser = User::updateOrCreate(
|
||||||
|
['username' => 'assets'],
|
||||||
|
[
|
||||||
|
'first_name' => 'Assets',
|
||||||
|
'last_name' => 'User',
|
||||||
|
'password' => Hash::make('password'),
|
||||||
|
'activated' => 1,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$assetsUser->permissions = json_encode([
|
||||||
|
'assets.view' => 1,
|
||||||
|
'assets.create' => 1,
|
||||||
|
'assets.edit' => 1,
|
||||||
|
'assets.delete' => 1,
|
||||||
|
'assets.checkout' => 1,
|
||||||
|
'assets.checkin' => 1,
|
||||||
|
'assets.audit' => 1,
|
||||||
|
'assets.files' => 1,
|
||||||
|
'assets.view.requestable' => 1,
|
||||||
|
'assets.view.encrypted_custom_fields' => 1,
|
||||||
|
]);
|
||||||
|
$assetsUser->save();
|
||||||
|
|
||||||
|
$testUser = User::updateOrCreate(
|
||||||
|
['username' => 'testuser'],
|
||||||
|
[
|
||||||
|
'first_name' => 'Test',
|
||||||
|
'last_name' => 'User',
|
||||||
|
'password' => Hash::make('password'),
|
||||||
|
'activated' => 1,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$testUser->permissions = json_encode([
|
||||||
|
'self.checkout_assets' => 1,
|
||||||
|
'assets.view.requestable' => 1,
|
||||||
|
]);
|
||||||
|
$testUser->save();
|
||||||
|
|
||||||
\Storage::disk('public')->put('snipe-logo.png', file_get_contents(public_path('img/demo/snipe-logo.png')));
|
\Storage::disk('public')->put('snipe-logo.png', file_get_contents(public_path('img/demo/snipe-logo.png')));
|
||||||
\Storage::disk('public')->put('snipe-logo-lg.png', file_get_contents(public_path('img/demo/snipe-logo-lg.png')));
|
\Storage::disk('public')->put('snipe-logo-lg.png', file_get_contents(public_path('img/demo/snipe-logo-lg.png')));
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ namespace App\Console\Commands;
|
|||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
use function Laravel\Prompts\info;
|
||||||
|
use function Laravel\Prompts\select;
|
||||||
|
|
||||||
class Version extends Command
|
class Version extends Command
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -11,7 +14,7 @@ class Version extends Command
|
|||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $signature = 'version:update {--branch=master} {--type=patch}';
|
protected $signature = 'version:update';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The console command description.
|
* The console command description.
|
||||||
@@ -37,30 +40,40 @@ class Version extends Command
|
|||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
$use_branch = $this->option('branch');
|
$use_branch = select(
|
||||||
$use_type = $this->option('type');
|
label: 'Which branch?',
|
||||||
|
options: ['master', 'develop'],
|
||||||
|
default: 'develop',
|
||||||
|
);
|
||||||
|
|
||||||
|
$use_type = select(
|
||||||
|
label: 'Which release type?',
|
||||||
|
options: [
|
||||||
|
'hash' => 'Hash bump',
|
||||||
|
'patch' => 'Patch release',
|
||||||
|
'minor' => 'Minor release',
|
||||||
|
'major' => 'Major release',
|
||||||
|
'pre-patch' => 'Pre-patch release',
|
||||||
|
'pre-minor' => 'Pre-minor release',
|
||||||
|
'pre-major' => 'Pre-major release',
|
||||||
|
],
|
||||||
|
default: 'hash',
|
||||||
|
scroll: 7,
|
||||||
|
);
|
||||||
|
|
||||||
$git_branch = trim(shell_exec('git rev-parse --abbrev-ref HEAD'));
|
$git_branch = trim(shell_exec('git rev-parse --abbrev-ref HEAD'));
|
||||||
$build_version = trim(shell_exec('git rev-list --count '.$use_branch));
|
$build_version = trim(shell_exec('git rev-list --count '.$use_branch));
|
||||||
$versionFile = 'config/version.php';
|
$versionFile = 'config/version.php';
|
||||||
$full_hash_version = str_replace("\n", '', shell_exec('git describe master --tags'));
|
$full_hash_version = str_replace("\n", '', shell_exec('git describe master --tags'));
|
||||||
|
|
||||||
$version = explode('-', $full_hash_version);
|
$version = explode('-', $full_hash_version);
|
||||||
$app_version = $current_app_version = $version[0];
|
$app_version = $version[0];
|
||||||
$hash_version = (array_key_exists('2', $version)) ? $version[2] : '';
|
$hash_version = (array_key_exists('2', $version)) ? $version[2] : '';
|
||||||
$prerelease_version = '';
|
$prerelease_version = '';
|
||||||
|
|
||||||
$this->line('Branch is: '.$use_branch);
|
if (array_key_exists('3', $version)) {
|
||||||
$this->line('Type is: '.$use_type);
|
$prerelease_version = $version[1];
|
||||||
$this->line('Current version is: '.$full_hash_version);
|
$hash_version = $version[3];
|
||||||
|
|
||||||
if (count($version) == 3) {
|
|
||||||
$this->line('This does not look like an alpha/beta release.');
|
|
||||||
} else {
|
|
||||||
if (array_key_exists('3', $version)) {
|
|
||||||
$this->line('The current version looks like a beta release.');
|
|
||||||
$prerelease_version = $version[1];
|
|
||||||
$hash_version = $version[3];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$app_version_raw = explode('.', $app_version);
|
$app_version_raw = explode('.', $app_version);
|
||||||
@@ -74,54 +87,52 @@ class Version extends Command
|
|||||||
$patch = $app_version_raw[2];
|
$patch = $app_version_raw[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($use_type == 'major') {
|
if ($use_type === 'major') {
|
||||||
$app_version = 'v'.($maj + 1).".$min.$patch";
|
$app_version = 'v'.($maj + 1).".$min.$patch";
|
||||||
} elseif ($use_type == 'minor') {
|
} elseif ($use_type === 'minor') {
|
||||||
$app_version = 'v'."$maj.".($min + 1).".$patch";
|
$app_version = 'v'."$maj.".($min + 1).".$patch";
|
||||||
} elseif ($use_type == 'pre') {
|
} elseif ($use_type === 'pre-patch') {
|
||||||
$pre_raw = str_replace('beta', '', $prerelease_version);
|
$app_version = 'v'."$maj.$min.".($patch + 1).'-pre';
|
||||||
$pre_raw = str_replace('alpha', '', $pre_raw);
|
} elseif ($use_type === 'pre-minor') {
|
||||||
$pre_raw = str_ireplace('rc', '', $pre_raw);
|
$app_version = 'v'."$maj.".($min + 1).'.0-pre';
|
||||||
$pre_raw = $pre_raw++;
|
} elseif ($use_type === 'pre-major') {
|
||||||
$this->line('Setting the pre-release to '.$prerelease_version.'-'.$pre_raw);
|
$app_version = 'v'.($maj + 1).'.0.0-pre';
|
||||||
$app_version = 'v'."$maj.".($min + 1).".$patch";
|
} elseif ($use_type === 'patch') {
|
||||||
} elseif ($use_type == 'patch') {
|
|
||||||
$app_version = 'v'."$maj.$min.".($patch + 1);
|
$app_version = 'v'."$maj.$min.".($patch + 1);
|
||||||
// If nothing is passed, leave the version as it is, just increment the build
|
|
||||||
} else {
|
|
||||||
$app_version = 'v'."$maj.$min.".$patch;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if this tag already exists, or if this prior to a release
|
if ($use_branch === 'develop' && ! str_ends_with($app_version, '-pre')) {
|
||||||
$this->line('Running: git rev-parse master '.$current_app_version);
|
|
||||||
// $pre_release = trim(shell_exec('git rev-parse '.$use_branch.' '.$current_app_version.' 2>&1 1> /dev/null'));
|
|
||||||
|
|
||||||
if ($use_branch == 'develop') {
|
|
||||||
$app_version = $app_version.'-pre';
|
$app_version = $app_version.'-pre';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$full_hash_version = str_replace($version[0], $app_version, $full_hash_version);
|
||||||
$full_app_version = $app_version.' - build '.$build_version.'-'.$hash_version;
|
$full_app_version = $app_version.' - build '.$build_version.'-'.$hash_version;
|
||||||
|
|
||||||
$array = var_export(
|
$content = <<<PHP
|
||||||
[
|
<?php
|
||||||
'app_version' => $app_version,
|
|
||||||
'full_app_version' => $full_app_version,
|
|
||||||
'build_version' => $build_version,
|
|
||||||
'prerelease_version' => $prerelease_version,
|
|
||||||
'hash_version' => $hash_version,
|
|
||||||
'full_hash' => $full_hash_version,
|
|
||||||
'branch' => $git_branch, ],
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
// Construct our file content
|
return [
|
||||||
$content = <<<CON
|
'app_version' => '$app_version',
|
||||||
<?php
|
'full_app_version' => '$full_app_version',
|
||||||
return $array;
|
'build_version' => '$build_version',
|
||||||
CON;
|
'prerelease_version' => '$prerelease_version',
|
||||||
|
'hash_version' => '$hash_version',
|
||||||
|
'full_hash' => '$full_hash_version',
|
||||||
|
'branch' => '$git_branch',
|
||||||
|
];
|
||||||
|
|
||||||
|
PHP;
|
||||||
|
|
||||||
// And finally write the file and output the current version
|
|
||||||
\File::put($versionFile, $content);
|
\File::put($versionFile, $content);
|
||||||
$this->info('Setting NEW version: '.$full_app_version.' ('.$git_branch.')');
|
info('New version: '.$full_app_version.' ('.$git_branch.')');
|
||||||
|
|
||||||
|
info('Building JS/CSS assets...');
|
||||||
|
passthru('npm run prod', $exitCode);
|
||||||
|
|
||||||
|
if ($exitCode !== 0) {
|
||||||
|
$this->error('Asset build failed with exit code '.$exitCode);
|
||||||
|
} else {
|
||||||
|
info('Assets built successfully.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ enum ActionType: string
|
|||||||
case DeleteSeats = 'delete seats';
|
case DeleteSeats = 'delete seats';
|
||||||
case AddSeats = 'add seats';
|
case AddSeats = 'add seats';
|
||||||
|
|
||||||
|
// Maintenances
|
||||||
|
case MaintenanceComplete = 'completed';
|
||||||
|
|
||||||
// File Uploads
|
// File Uploads
|
||||||
case Uploaded = 'uploaded';
|
case Uploaded = 'uploaded';
|
||||||
case UploadDeleted = 'upload deleted';
|
case UploadDeleted = 'upload deleted';
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ use Illuminate\Validation\ValidationException;
|
|||||||
use Intervention\Image\Exception\NotSupportedException;
|
use Intervention\Image\Exception\NotSupportedException;
|
||||||
use JsonException;
|
use JsonException;
|
||||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||||
|
use Livewire\Exceptions\ComponentNotFoundException;
|
||||||
|
use Livewire\Exceptions\PublicPropertyNotFoundException;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
@@ -41,6 +43,8 @@ class Handler extends ExceptionHandler
|
|||||||
JsonException::class,
|
JsonException::class,
|
||||||
SCIMException::class, // these generally don't need to be reported
|
SCIMException::class, // these generally don't need to be reported
|
||||||
InvalidFormatException::class,
|
InvalidFormatException::class,
|
||||||
|
PublicPropertyNotFoundException::class,
|
||||||
|
ComponentNotFoundException::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,6 +75,17 @@ class Handler extends ExceptionHandler
|
|||||||
public function render($request, Throwable $e)
|
public function render($request, Throwable $e)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// Livewire tried to set a property that doesn't exist (e.g. stale browser state sending a bare "0" as a property name)
|
||||||
|
if ($e instanceof PublicPropertyNotFoundException) {
|
||||||
|
return response()->json(['message' => $e->getMessage()], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A request named a Livewire component that doesn't exist in this app (e.g. bots probing
|
||||||
|
// for Filament endpoints). Return 404 so it doesn't surface as a 500.
|
||||||
|
if ($e instanceof ComponentNotFoundException) {
|
||||||
|
return response()->json(['message' => 'Component not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
// CSRF token mismatch error
|
// CSRF token mismatch error
|
||||||
if ($e instanceof TokenMismatchException) {
|
if ($e instanceof TokenMismatchException) {
|
||||||
return redirect()->back()->with('error', trans('general.token_expired'));
|
return redirect()->back()->with('error', trans('general.token_expired'));
|
||||||
|
|||||||
+87
-9
@@ -14,6 +14,7 @@ use App\Models\License;
|
|||||||
use App\Models\Location;
|
use App\Models\Location;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\Statuslabel;
|
use App\Models\Statuslabel;
|
||||||
|
use App\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Contracts\Encryption\DecryptException;
|
use Illuminate\Contracts\Encryption\DecryptException;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
@@ -663,7 +664,7 @@ class Helper
|
|||||||
*/
|
*/
|
||||||
public static function depreciationList()
|
public static function depreciationList()
|
||||||
{
|
{
|
||||||
$depreciation_list = ['' => 'Do Not Depreciate'] + Depreciation::orderBy('name', 'asc')
|
$depreciation_list = ['' => trans('admin/licenses/form.no_depreciation')] + Depreciation::orderBy('name', 'asc')
|
||||||
->pluck('name', 'id')->toArray();
|
->pluck('name', 'id')->toArray();
|
||||||
|
|
||||||
return $depreciation_list;
|
return $depreciation_list;
|
||||||
@@ -1268,6 +1269,7 @@ class Helper
|
|||||||
$allowedExtensionMap = [
|
$allowedExtensionMap = [
|
||||||
// Images
|
// Images
|
||||||
'jpg' => 'far fa-image',
|
'jpg' => 'far fa-image',
|
||||||
|
'jfif' => 'far fa-image',
|
||||||
'jpeg' => 'far fa-image',
|
'jpeg' => 'far fa-image',
|
||||||
'gif' => 'far fa-image',
|
'gif' => 'far fa-image',
|
||||||
'png' => 'far fa-image',
|
'png' => 'far fa-image',
|
||||||
@@ -1596,7 +1598,17 @@ class Helper
|
|||||||
$checkout_to_type = session('checkout_to_type') ?? null;
|
$checkout_to_type = session('checkout_to_type') ?? null;
|
||||||
$checkedInFrom = session('checkedInFrom');
|
$checkedInFrom = session('checkedInFrom');
|
||||||
$other_redirect = session('other_redirect');
|
$other_redirect = session('other_redirect');
|
||||||
$backUrl = session()->pull('url.intended', 'home');
|
$backUrl = str_replace(["\r", "\n"], '', session()->pull('url.intended', 'home'));
|
||||||
|
|
||||||
|
// Reject any stored back-URL that points off-site. redirect()->intended() performs
|
||||||
|
// no host validation, and url.intended can be written from the SAML RelayState POST
|
||||||
|
// parameter (SamlController), which an attacker-controlled IdP could set to an
|
||||||
|
// off-site URL.
|
||||||
|
$backHost = parse_url($backUrl, PHP_URL_HOST);
|
||||||
|
$appHost = parse_url(config('app.url'), PHP_URL_HOST);
|
||||||
|
if ($backHost && $backHost !== $appHost) {
|
||||||
|
$backUrl = route('home');
|
||||||
|
}
|
||||||
|
|
||||||
// return to previous page
|
// return to previous page
|
||||||
if ($redirect_option == 'back') {
|
if ($redirect_option == 'back') {
|
||||||
@@ -1689,6 +1701,8 @@ class Helper
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$floater = (bool) Setting::getSettings()->null_company_is_floater;
|
||||||
|
|
||||||
foreach ($locations as $location) {
|
foreach ($locations as $location) {
|
||||||
// in case of an update of a single location, use the newly requested company_id
|
// in case of an update of a single location, use the newly requested company_id
|
||||||
if ($new_company_id) {
|
if ($new_company_id) {
|
||||||
@@ -1723,26 +1737,51 @@ class Helper
|
|||||||
foreach ($keywords as $keyword) {
|
foreach ($keywords as $keyword) {
|
||||||
if ($relation == 'many') {
|
if ($relation == 'many') {
|
||||||
$items = $location->{$keyword}->all();
|
$items = $location->{$keyword}->all();
|
||||||
|
// assignedAccessories returns AccessoryCheckout records (no company_id);
|
||||||
|
// resolve each to its parent Accessory so the comparison is valid.
|
||||||
|
if ($keyword === 'assignedAccessories') {
|
||||||
|
$items = collect($items)->map(fn ($checkout) => $checkout->accessory)->filter()->values()->all();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$items = collect([])->push($location->$keyword);
|
$items = collect([])->push($location->$keyword);
|
||||||
}
|
}
|
||||||
|
|
||||||
$count = 0;
|
$count = 0;
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
|
if (! $item) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if ($item && $item->company_id != $location_company) {
|
// Users belong to companies via the many-to-many pivot (company_user).
|
||||||
|
// canReceiveFromCompany() returns true only when the user's pivot
|
||||||
|
// contains the location's company, so !canReceiveFromCompany() is
|
||||||
|
// the correct mismatch signal.
|
||||||
|
if ($item instanceof User) {
|
||||||
|
$isMismatch = ! $item->canReceiveFromCompany((int) $location_company);
|
||||||
|
} elseif ($item->company_id == $location_company) {
|
||||||
|
$isMismatch = false;
|
||||||
|
} elseif (is_null($item->company_id) || is_null($location_company)) {
|
||||||
|
$isMismatch = ! $floater;
|
||||||
|
} else {
|
||||||
|
$isMismatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isMismatch) {
|
||||||
|
if ($item instanceof User) {
|
||||||
|
$itemCompanyIds = $item->companies->pluck('id')->implode(', ');
|
||||||
|
$itemCompanyNames = $item->companies->pluck('name')->implode(', ');
|
||||||
|
} else {
|
||||||
|
$itemCompanyIds = $item->company_id ?? null;
|
||||||
|
$itemCompanyNames = $item->company->name ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
$mismatched[] = [
|
$mismatched[] = [
|
||||||
class_basename(get_class($item)),
|
class_basename(get_class($item)),
|
||||||
$item->id,
|
$item->id,
|
||||||
$item->name ?? $item->asset_tag ?? $item->serial ?? $item->username,
|
$item->name ?? $item->asset_tag ?? $item->serial ?? $item->username,
|
||||||
$item->assigned_type ? str_replace('App\\Models\\', '', $item->assigned_type) : null,
|
$item->assigned_type ? str_replace('App\\Models\\', '', $item->assigned_type) : null,
|
||||||
$item->company_id ?? null,
|
$itemCompanyIds,
|
||||||
$item->company->name ?? null,
|
$itemCompanyNames,
|
||||||
// $item->defaultLoc->id ?? null,
|
|
||||||
// $item->defaultLoc->name ?? null,
|
|
||||||
// $item->defaultLoc->company->id ?? null,
|
|
||||||
// $item->defaultLoc->company->name ?? null,
|
|
||||||
$item->location->name ?? null,
|
$item->location->name ?? null,
|
||||||
$item->location->company->name ?? null,
|
$item->location->company->name ?? null,
|
||||||
$location_company ?? null,
|
$location_company ?? null,
|
||||||
@@ -1856,4 +1895,43 @@ class Helper
|
|||||||
return 'App\\Models\\'.ucwords($model);
|
return 'App\\Models\\'.ucwords($model);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a markdown-textarea value as HTML.
|
||||||
|
*
|
||||||
|
* Soft line breaks (single newlines) are rendered as <br> so that line
|
||||||
|
* breaks typed in the textarea are preserved in the output.
|
||||||
|
*
|
||||||
|
* When $inline is true, block-level elements are suppressed and hard
|
||||||
|
* breaks are pre-processed manually — used for the encrypted reveal span
|
||||||
|
* where block HTML cannot be placed inside a font-size-toggled <span>.
|
||||||
|
*/
|
||||||
|
public static function renderMarkdown(?string $text, bool $inline = false): string
|
||||||
|
{
|
||||||
|
if (empty($text)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($inline) {
|
||||||
|
// Convert newlines to CommonMark hard breaks for inline rendering
|
||||||
|
$text = preg_replace('/(?<! {2})\n/', " \n", $text);
|
||||||
|
|
||||||
|
return Str::inlineMarkdown($text, ['html_input' => 'escape', 'allow_unsafe_links' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = trim(Str::markdown($text, [
|
||||||
|
'html_input' => 'escape',
|
||||||
|
'allow_unsafe_links' => false,
|
||||||
|
'renderer' => ['soft_break' => "<br>\n"],
|
||||||
|
]));
|
||||||
|
|
||||||
|
// If the entire output is a single <p> block, unwrap it so the content
|
||||||
|
// renders inline-ish without the <p> adding unwanted top spacing in the
|
||||||
|
// compact detail-view layout.
|
||||||
|
if (str_starts_with($html, '<p>') && str_ends_with($html, '</p>') && substr_count($html, '<p>') === 1) {
|
||||||
|
return substr($html, 3, -4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class IconHelper
|
|||||||
case 'angle-right':
|
case 'angle-right':
|
||||||
return 'fas fa-angle-right';
|
return 'fas fa-angle-right';
|
||||||
case 'warning':
|
case 'warning':
|
||||||
|
case 'alert':
|
||||||
return 'fas fa-exclamation-triangle';
|
return 'fas fa-exclamation-triangle';
|
||||||
case 'kits':
|
case 'kits':
|
||||||
return 'fas fa-object-group';
|
return 'fas fa-object-group';
|
||||||
@@ -126,6 +127,7 @@ class IconHelper
|
|||||||
case 'dashboard':
|
case 'dashboard':
|
||||||
return 'fas fa-tachometer-alt';
|
return 'fas fa-tachometer-alt';
|
||||||
case 'info-circle':
|
case 'info-circle':
|
||||||
|
case 'info':
|
||||||
return 'fas fa-info-circle';
|
return 'fas fa-info-circle';
|
||||||
case 'caret-right':
|
case 'caret-right':
|
||||||
return 'fa fa-caret-right';
|
return 'fa fa-caret-right';
|
||||||
@@ -156,6 +158,7 @@ class IconHelper
|
|||||||
case 'remote':
|
case 'remote':
|
||||||
return 'fa-solid fa-house-laptop';
|
return 'fa-solid fa-house-laptop';
|
||||||
case 'more-info':
|
case 'more-info':
|
||||||
|
case 'help':
|
||||||
case 'support':
|
case 'support':
|
||||||
return 'far fa-life-ring';
|
return 'far fa-life-ring';
|
||||||
case 'plus':
|
case 'plus':
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ namespace App\Http\Controllers\Accessories;
|
|||||||
|
|
||||||
use App\Events\CheckoutableCheckedOut;
|
use App\Events\CheckoutableCheckedOut;
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
use App\Http\Controllers\CheckInOutRequest;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\AccessoryCheckoutRequest;
|
use App\Http\Requests\AccessoryCheckoutRequest;
|
||||||
|
use App\Http\Traits\CheckInOutTrait;
|
||||||
use App\Models\Accessory;
|
use App\Models\Accessory;
|
||||||
use App\Models\AccessoryCheckout;
|
use App\Models\AccessoryCheckout;
|
||||||
use App\Models\CheckoutAcceptance;
|
use App\Models\CheckoutAcceptance;
|
||||||
@@ -18,7 +18,7 @@ use Illuminate\Http\Request;
|
|||||||
|
|
||||||
class AccessoryCheckoutController extends Controller
|
class AccessoryCheckoutController extends Controller
|
||||||
{
|
{
|
||||||
use CheckInOutRequest;
|
use CheckInOutTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the form to checkout an Accessory to a user.
|
* Return the form to checkout an Accessory to a user.
|
||||||
@@ -66,6 +66,20 @@ class AccessoryCheckoutController extends Controller
|
|||||||
$target = $this->determineCheckoutTarget();
|
$target = $this->determineCheckoutTarget();
|
||||||
session()->put(['checkout_to_type' => $target]);
|
session()->put(['checkout_to_type' => $target]);
|
||||||
|
|
||||||
|
if (! $accessory->canCheckoutTo($target)) {
|
||||||
|
$targetType = match (class_basename($target)) {
|
||||||
|
'User' => trans('general.user'),
|
||||||
|
'Location' => trans('general.location'),
|
||||||
|
default => trans('general.asset'),
|
||||||
|
};
|
||||||
|
|
||||||
|
return redirect()->back()->with('error', trans('general.error_checkout_company_mismatch', [
|
||||||
|
'item' => trans('general.accessory').' "'.$accessory->name.'"',
|
||||||
|
'item_company' => $accessory->company?->name ?? trans('general.unassigned'),
|
||||||
|
'target' => $targetType.' "'.($target->name ?? $target->username ?? $target->id).'"',
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
$accessory->checkout_qty = $request->input('checkout_qty', 1);
|
$accessory->checkout_qty = $request->input('checkout_qty', 1);
|
||||||
|
|
||||||
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
|
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
|
||||||
|
|||||||
@@ -149,6 +149,9 @@ class AcceptanceController extends Controller
|
|||||||
|
|
||||||
$item = $acceptance->checkoutable_type::find($acceptance->checkoutable_id);
|
$item = $acceptance->checkoutable_type::find($acceptance->checkoutable_id);
|
||||||
|
|
||||||
|
$username_slug = Str::slug($assignedUser->username);
|
||||||
|
$asset_tag_slug = ($item instanceof Asset && $item->asset_tag) ? '-'.Str::slug($item->asset_tag) : '';
|
||||||
|
|
||||||
// If signatures are required, make sure we have one
|
// If signatures are required, make sure we have one
|
||||||
if ($requiresSignature) {
|
if ($requiresSignature) {
|
||||||
|
|
||||||
@@ -234,7 +237,7 @@ class AcceptanceController extends Controller
|
|||||||
|
|
||||||
if ($request->input('asset_acceptance') === 'accepted') {
|
if ($request->input('asset_acceptance') === 'accepted') {
|
||||||
|
|
||||||
$pdf_filename = 'accepted-'.$acceptance->checkoutable_id.'-'.$acceptance->display_checkoutable_type.'-eula-'.date('Y-m-d-h-i-s').'.pdf';
|
$pdf_filename = 'accepted-'.$username_slug.$asset_tag_slug.'-'.date('Y-m-d-h-i-s').'.pdf';
|
||||||
|
|
||||||
// Generate the PDF content
|
// Generate the PDF content
|
||||||
$pdf_content = $acceptance->generateAcceptancePdf($data, $acceptance);
|
$pdf_content = $acceptance->generateAcceptancePdf($data, $acceptance);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
use App\Models\Actionlog;
|
use App\Models\Actionlog;
|
||||||
use App\Models\Asset;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
@@ -17,6 +16,9 @@ class ActionlogController extends Controller
|
|||||||
{
|
{
|
||||||
$filename = basename((string) $filename);
|
$filename = basename((string) $filename);
|
||||||
|
|
||||||
|
$actionlog = Actionlog::where('accept_signature', $filename)->with('item')->firstOrFail();
|
||||||
|
$this->authorize('view', $actionlog->item);
|
||||||
|
|
||||||
// PHP doesn't let you handle file not found errors well with
|
// PHP doesn't let you handle file not found errors well with
|
||||||
// file_get_contents, so we set the error reporting for just this class
|
// file_get_contents, so we set the error reporting for just this class
|
||||||
error_reporting(0);
|
error_reporting(0);
|
||||||
@@ -29,7 +31,6 @@ class ActionlogController extends Controller
|
|||||||
|
|
||||||
return redirect()->away(Storage::disk($disk)->temporaryUrl($file, now()->addMinutes(5)));
|
return redirect()->away(Storage::disk($disk)->temporaryUrl($file, now()->addMinutes(5)));
|
||||||
default:
|
default:
|
||||||
$this->authorize('view', Asset::class);
|
|
||||||
$file = config('app.private_uploads').'/signatures/'.$filename;
|
$file = config('app.private_uploads').'/signatures/'.$filename;
|
||||||
$filetype = Helper::checkUploadIsImage($file);
|
$filetype = Helper::checkUploadIsImage($file);
|
||||||
|
|
||||||
|
|||||||
@@ -4,26 +4,28 @@ namespace App\Http\Controllers\Api;
|
|||||||
|
|
||||||
use App\Events\CheckoutableCheckedOut;
|
use App\Events\CheckoutableCheckedOut;
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
use App\Http\Controllers\CheckInOutRequest;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\AccessoryCheckoutRequest;
|
use App\Http\Requests\AccessoryCheckoutRequest;
|
||||||
use App\Http\Requests\ImageUploadRequest;
|
use App\Http\Requests\ImageUploadRequest;
|
||||||
use App\Http\Requests\StoreAccessoryRequest;
|
use App\Http\Requests\StoreAccessoryRequest;
|
||||||
|
use App\Http\Traits\CheckInOutTrait;
|
||||||
use App\Http\Transformers\AccessoriesTransformer;
|
use App\Http\Transformers\AccessoriesTransformer;
|
||||||
use App\Http\Transformers\ActionlogsTransformer;
|
use App\Http\Transformers\ActionlogsTransformer;
|
||||||
use App\Http\Transformers\SelectlistTransformer;
|
use App\Http\Transformers\SelectlistTransformer;
|
||||||
use App\Models\Accessory;
|
use App\Models\Accessory;
|
||||||
use App\Models\AccessoryCheckout;
|
use App\Models\AccessoryCheckout;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class AccessoriesController extends Controller
|
class AccessoriesController extends Controller
|
||||||
{
|
{
|
||||||
use CheckInOutRequest;
|
use CheckInOutTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a listing of the resource.
|
* Display a listing of the resource.
|
||||||
@@ -105,7 +107,7 @@ class AccessoriesController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||||
$offset = ($request->input('offset') > $accessories->count()) ? $accessories->count() : abs($request->input('offset'));
|
$offset = ($request->input('offset') > $accessories->count()) ? $accessories->count() : app('api_offset_value');
|
||||||
$limit = app('api_limit_value');
|
$limit = app('api_limit_value');
|
||||||
|
|
||||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||||
@@ -232,6 +234,10 @@ class AccessoriesController extends Controller
|
|||||||
$total = $accessory_checkouts->count();
|
$total = $accessory_checkouts->count();
|
||||||
$accessory_checkouts = $accessory_checkouts->skip($offset)->take($limit)->get();
|
$accessory_checkouts = $accessory_checkouts->skip($offset)->take($limit)->get();
|
||||||
|
|
||||||
|
$accessory_checkouts->loadMorph('assignedTo', [
|
||||||
|
User::class => ['companies'],
|
||||||
|
]);
|
||||||
|
|
||||||
return (new AccessoriesTransformer)->transformCheckedoutAccessory($accessory_checkouts, $total);
|
return (new AccessoriesTransformer)->transformCheckedoutAccessory($accessory_checkouts, $total);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,40 +306,49 @@ class AccessoriesController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorize('checkout', $accessory);
|
$this->authorize('checkout', $accessory);
|
||||||
$target = $this->determineCheckoutTarget();
|
$target = $this->determineCheckoutTarget();
|
||||||
$accessory->checkout_qty = $request->input('checkout_qty', 1);
|
|
||||||
|
|
||||||
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
|
if ((Setting::getSettings()->full_multiple_companies_support == '1') && (! $target->companies()->where('companies.id', $accessory->company_id)->exists())) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||||
$accessory_checkout = new AccessoryCheckout([
|
|
||||||
'accessory_id' => $accessory->id,
|
|
||||||
'created_at' => Carbon::now(),
|
|
||||||
'assigned_to' => $target->id,
|
|
||||||
'assigned_type' => $target::class,
|
|
||||||
'note' => $request->input('note'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$accessory_checkout->created_by = auth()->id();
|
|
||||||
$accessory_checkout->save();
|
|
||||||
|
|
||||||
$payload = [
|
|
||||||
'accessory_id' => $accessory->id,
|
|
||||||
'assigned_to' => $target->id,
|
|
||||||
'assigned_type' => $target::class,
|
|
||||||
'note' => $request->input('note'),
|
|
||||||
'created_by' => auth()->id(),
|
|
||||||
'pivot' => $accessory_checkout->id,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set this value to be able to pass the qty through to the event
|
$accessory->checkout_qty = $request->input('checkout_qty', 1);
|
||||||
event(new CheckoutableCheckedOut(
|
$payload = null;
|
||||||
$accessory,
|
|
||||||
$target,
|
// Keep checkout rows and checkout log/event atomic to avoid ghost assignments.
|
||||||
auth()->user(),
|
DB::transaction(function () use ($accessory, $request, $target, &$payload): void {
|
||||||
$request->input('note'),
|
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
|
||||||
[],
|
|
||||||
$accessory->checkout_qty,
|
$accessory_checkout = new AccessoryCheckout([
|
||||||
));
|
'accessory_id' => $accessory->id,
|
||||||
|
'created_at' => Carbon::now(),
|
||||||
|
'assigned_to' => $target->id,
|
||||||
|
'assigned_type' => $target::class,
|
||||||
|
'note' => $request->input('note'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$accessory_checkout->created_by = auth()->id();
|
||||||
|
$accessory_checkout->save();
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'accessory_id' => $accessory->id,
|
||||||
|
'assigned_to' => $target->id,
|
||||||
|
'assigned_type' => $target::class,
|
||||||
|
'note' => $request->input('note'),
|
||||||
|
'created_by' => auth()->id(),
|
||||||
|
'pivot' => $accessory_checkout->id,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set this value to be able to pass the qty through to the event.
|
||||||
|
event(new CheckoutableCheckedOut(
|
||||||
|
$accessory,
|
||||||
|
$target,
|
||||||
|
auth()->user(),
|
||||||
|
$request->input('note'),
|
||||||
|
[],
|
||||||
|
$accessory->checkout_qty,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkout.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkout.success')));
|
||||||
|
|
||||||
@@ -390,6 +405,7 @@ class AccessoriesController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function selectlist(Request $request)
|
public function selectlist(Request $request)
|
||||||
{
|
{
|
||||||
|
$this->authorize('view.selectlists');
|
||||||
|
|
||||||
$accessories = Accessory::select([
|
$accessories = Accessory::select([
|
||||||
'accessories.id',
|
'accessories.id',
|
||||||
|
|||||||
@@ -133,7 +133,8 @@ class AssetModelsController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||||
$offset = ($request->input('offset') > $assetmodels->count()) ? $assetmodels->count() : abs($request->input('offset'));
|
$total = $assetmodels->count();
|
||||||
|
$offset = ($request->input('offset') > $total) ? $total : app('api_offset_value');
|
||||||
$limit = app('api_limit_value');
|
$limit = app('api_limit_value');
|
||||||
|
|
||||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||||
@@ -157,7 +158,6 @@ class AssetModelsController extends Controller
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$total = $assetmodels->count();
|
|
||||||
$assetmodels = $assetmodels->skip($offset)->take($limit)->get();
|
$assetmodels = $assetmodels->skip($offset)->take($limit)->get();
|
||||||
|
|
||||||
return (new AssetModelsTransformer)->transformAssetModels($assetmodels, $total);
|
return (new AssetModelsTransformer)->transformAssetModels($assetmodels, $total);
|
||||||
|
|||||||
@@ -371,6 +371,12 @@ class AssetsController extends Controller
|
|||||||
$assets->where('assets.order_number', '=', strval($request->input('order_number')));
|
$assets->where('assets.order_number', '=', strval($request->input('order_number')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($all_custom_fields as $field) {
|
||||||
|
if ($request->filled($field->db_column_name())) {
|
||||||
|
$assets->where($field->db_column_name(), '=', $request->input($field->db_column_name()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This is kinda gross, but we need to do this because the Bootstrap Tables
|
// This is kinda gross, but we need to do this because the Bootstrap Tables
|
||||||
// API passes custom field ordering as custom_fields.fieldname, and we have to strip
|
// API passes custom field ordering as custom_fields.fieldname, and we have to strip
|
||||||
// that out to let the default sorter below order them correctly on the assets table.
|
// that out to let the default sorter below order them correctly on the assets table.
|
||||||
@@ -590,6 +596,7 @@ class AssetsController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function selectlist(Request $request): array
|
public function selectlist(Request $request): array
|
||||||
{
|
{
|
||||||
|
$this->authorize('view.selectlists');
|
||||||
|
|
||||||
$assets = Asset::select([
|
$assets = Asset::select([
|
||||||
'assets.id',
|
'assets.id',
|
||||||
@@ -602,8 +609,20 @@ class AssetsController extends Controller
|
|||||||
])->with('model', 'status', 'assignedTo')
|
])->with('model', 'status', 'assignedTo')
|
||||||
->NotArchived();
|
->NotArchived();
|
||||||
|
|
||||||
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($request->filled('companyId'))) {
|
// When FMCS is enabled, automatically scope to companies the acting user belongs to.
|
||||||
$assets->where('assets.company_id', $request->input('companyId'));
|
// scopeCompanyables is a no-op for superusers and when FMCS is disabled.
|
||||||
|
$assets = Company::scopeCompanyables($assets);
|
||||||
|
|
||||||
|
// Allow further narrowing to a specific company passed via data-company-id on the select.
|
||||||
|
if ((Setting::getSettings()->full_multiple_companies_support == '1') && $request->filled('companyId')) {
|
||||||
|
$companyIds = array_values(array_filter(array_map('intval', explode(',', $request->input('companyId')))));
|
||||||
|
if (! empty($companyIds)) {
|
||||||
|
$assets->whereIn('assets.company_id', $companyIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('excludeId')) {
|
||||||
|
$assets->where('assets.id', '!=', (int) $request->input('excludeId'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('statusType') && $request->input('statusType') === 'RTD') {
|
if ($request->filled('statusType') && $request->input('statusType') === 'RTD') {
|
||||||
@@ -706,18 +725,35 @@ class AssetsController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($asset->save()) {
|
$target = $this->resolveCheckoutTargetForAssetMutation($request);
|
||||||
if ($request->input('assigned_user')) {
|
$requestedCheckout = $request->filled('assigned_user') || $request->filled('assigned_asset') || $request->filled('assigned_location');
|
||||||
$target = User::find(request('assigned_user'));
|
|
||||||
} elseif ($request->input('assigned_asset')) {
|
if ($requestedCheckout && (! $target)) {
|
||||||
$target = Asset::find(request('assigned_asset'));
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')));
|
||||||
} elseif ($request->input('assigned_location')) {
|
}
|
||||||
$target = Location::find(request('assigned_location'));
|
|
||||||
|
if ($requestedCheckout) {
|
||||||
|
$companyMismatchResponse = $this->checkoutCompanyMismatchResponse($asset, $target);
|
||||||
|
if ($companyMismatchResponse) {
|
||||||
|
return $companyMismatchResponse;
|
||||||
}
|
}
|
||||||
if (isset($target)) {
|
}
|
||||||
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset creation', e($request->input('name')));
|
|
||||||
|
$stored = DB::transaction(function () use ($asset, $request, $target, $requestedCheckout): bool {
|
||||||
|
if (! $asset->save()) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($requestedCheckout) {
|
||||||
|
// Keep create + optional checkout side effects atomic.
|
||||||
|
return $asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset creation', e($request->input('name')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($stored) {
|
||||||
|
|
||||||
if ($asset->image) {
|
if ($asset->image) {
|
||||||
$asset->image = $asset->getImageUrl();
|
$asset->image = $asset->getImageUrl();
|
||||||
}
|
}
|
||||||
@@ -792,25 +828,54 @@ class AssetsController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($asset->save()) {
|
$target = $this->resolveCheckoutTargetForAssetMutation($request, $asset->id);
|
||||||
if (($request->filled('assigned_user')) && ($target = User::find($request->input('assigned_user')))) {
|
$requestedCheckout = $request->filled('assigned_user') || $request->filled('assigned_asset') || $request->filled('assigned_location');
|
||||||
$location = $target->location_id;
|
|
||||||
} elseif (($request->filled('assigned_asset')) && ($target = Asset::find($request->input('assigned_asset')))) {
|
|
||||||
$location = $target->location_id;
|
|
||||||
|
|
||||||
Asset::where('assigned_type', Asset::class)->where('assigned_to', $asset->id)
|
if ($requestedCheckout && (! $target)) {
|
||||||
->update(['location_id' => $target->location_id]);
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')));
|
||||||
} elseif (($request->filled('assigned_location')) && ($target = Location::find($request->input('assigned_location')))) {
|
}
|
||||||
$location = $target->id;
|
|
||||||
|
if ($requestedCheckout) {
|
||||||
|
$companyMismatchResponse = $this->checkoutCompanyMismatchResponse($asset, $target);
|
||||||
|
if ($companyMismatchResponse) {
|
||||||
|
return $companyMismatchResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated = DB::transaction(function () use ($asset, $request, $target, $requestedCheckout): bool {
|
||||||
|
if (! $asset->save()) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($target)) {
|
if ($requestedCheckout) {
|
||||||
// Using `->has` preserves the asset name if the name parameter was not included in request.
|
// Using `->has` preserves the asset name if the name parameter was not included in request.
|
||||||
$asset_name = request()->has('name') ? request('name') : $asset->name;
|
$asset_name = request()->has('name') ? request('name') : $asset->name;
|
||||||
|
|
||||||
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset update', $asset_name, $location);
|
$location = null;
|
||||||
|
if ($request->filled('assigned_user')) {
|
||||||
|
$location = $target->location_id;
|
||||||
|
} elseif ($request->filled('assigned_asset')) {
|
||||||
|
$location = $target->location_id;
|
||||||
|
} elseif ($request->filled('assigned_location')) {
|
||||||
|
$location = $target->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep update + optional checkout side effects atomic.
|
||||||
|
if (! $asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset update', $asset_name, $location)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('assigned_asset')) {
|
||||||
|
Asset::where('assigned_type', Asset::class)->where('assigned_to', $asset->id)
|
||||||
|
->update(['location_id' => $target->location_id]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($updated) {
|
||||||
|
|
||||||
if ($asset->image) {
|
if ($asset->image) {
|
||||||
$asset->image = $asset->getImageUrl();
|
$asset->image = $asset->getImageUrl();
|
||||||
}
|
}
|
||||||
@@ -829,6 +894,32 @@ class AssetsController extends Controller
|
|||||||
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
|
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveCheckoutTargetForAssetMutation(Request $request, ?int $assetId = null): User|Asset|Location|null
|
||||||
|
{
|
||||||
|
if ($request->filled('assigned_user')) {
|
||||||
|
return User::withoutGlobalScopes()->find($request->input('assigned_user'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('assigned_asset')) {
|
||||||
|
return Asset::withoutGlobalScopes()->where('id', '!=', $assetId)->find($request->input('assigned_asset'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('assigned_location')) {
|
||||||
|
return Location::withoutGlobalScopes()->find($request->input('assigned_location'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkoutCompanyMismatchResponse(Asset $asset, User|Asset|Location $target): ?JsonResponse
|
||||||
|
{
|
||||||
|
if (! $asset->canCheckoutTo($target)) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a given asset (mark as deleted).
|
* Delete a given asset (mark as deleted).
|
||||||
*
|
*
|
||||||
@@ -905,6 +996,7 @@ class AssetsController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function checkoutByTag(AssetCheckoutRequest $request, $tag): JsonResponse
|
public function checkoutByTag(AssetCheckoutRequest $request, $tag): JsonResponse
|
||||||
{
|
{
|
||||||
|
// Use the same hardened checkout path as ID-based checkout.
|
||||||
if ($asset = Asset::where('asset_tag', $tag)->first()) {
|
if ($asset = Asset::where('asset_tag', $tag)->first()) {
|
||||||
return $this->checkout($request, $asset->id);
|
return $this->checkout($request, $asset->id);
|
||||||
}
|
}
|
||||||
@@ -940,19 +1032,22 @@ class AssetsController extends Controller
|
|||||||
|
|
||||||
// This item is checked out to a location
|
// This item is checked out to a location
|
||||||
if (request('checkout_to_type') == 'location') {
|
if (request('checkout_to_type') == 'location') {
|
||||||
$target = Location::find(request('assigned_location'));
|
// Resolve unscoped target first so FMCS mismatch can be handled explicitly.
|
||||||
|
$target = Location::withoutGlobalScopes()->find(request('assigned_location'));
|
||||||
$asset->location_id = ($target) ? $target->id : '';
|
$asset->location_id = ($target) ? $target->id : '';
|
||||||
$error_payload['target_id'] = $request->input('assigned_location');
|
$error_payload['target_id'] = $request->input('assigned_location');
|
||||||
$error_payload['target_type'] = 'location';
|
$error_payload['target_type'] = 'location';
|
||||||
} elseif (request('checkout_to_type') == 'asset') {
|
} elseif (request('checkout_to_type') == 'asset') {
|
||||||
$target = Asset::where('id', '!=', $asset_id)->find(request('assigned_asset'));
|
// Resolve unscoped target first so FMCS mismatch can be handled explicitly.
|
||||||
|
$target = Asset::withoutGlobalScopes()->where('id', '!=', $asset_id)->find(request('assigned_asset'));
|
||||||
// Override with the asset's location_id if it has one
|
// Override with the asset's location_id if it has one
|
||||||
$asset->location_id = (($target) && (isset($target->location_id))) ? $target->location_id : '';
|
$asset->location_id = (($target) && (isset($target->location_id))) ? $target->location_id : '';
|
||||||
$error_payload['target_id'] = $request->input('assigned_asset');
|
$error_payload['target_id'] = $request->input('assigned_asset');
|
||||||
$error_payload['target_type'] = 'asset';
|
$error_payload['target_type'] = 'asset';
|
||||||
} elseif (request('checkout_to_type') == 'user') {
|
} elseif (request('checkout_to_type') == 'user') {
|
||||||
// Fetch the target and set the asset's new location_id
|
// Fetch the target and set the asset's new location_id
|
||||||
$target = User::find(request('assigned_user'));
|
// Resolve unscoped target first so FMCS mismatch can be handled explicitly.
|
||||||
|
$target = User::withoutGlobalScopes()->find(request('assigned_user'));
|
||||||
$asset->location_id = (($target) && (isset($target->location_id))) ? $target->location_id : '';
|
$asset->location_id = (($target) && (isset($target->location_id))) ? $target->location_id : '';
|
||||||
$error_payload['target_id'] = $request->input('assigned_user');
|
$error_payload['target_id'] = $request->input('assigned_user');
|
||||||
$error_payload['target_type'] = 'user';
|
$error_payload['target_type'] = 'user';
|
||||||
@@ -971,6 +1066,11 @@ class AssetsController extends Controller
|
|||||||
return response()->json(Helper::formatStandardApiResponse('error', $error_payload, 'Checkout target for asset '.e($asset->asset_tag).' is invalid - '.$error_payload['target_type'].' does not exist.'));
|
return response()->json(Helper::formatStandardApiResponse('error', $error_payload, 'Checkout target for asset '.e($asset->asset_tag).' is invalid - '.$error_payload['target_type'].' does not exist.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In FMCS mode, enforce explicit same-company target checks before mutating checkout state.
|
||||||
|
if ($mismatch = $this->checkoutCompanyMismatchResponse($asset, $target)) {
|
||||||
|
return $mismatch;
|
||||||
|
}
|
||||||
|
|
||||||
$checkout_at = request('checkout_at', date('Y-m-d H:i:s'));
|
$checkout_at = request('checkout_at', date('Y-m-d H:i:s'));
|
||||||
$expected_checkin = request('expected_checkin', null);
|
$expected_checkin = request('expected_checkin', null);
|
||||||
$note = request('note', null);
|
$note = request('note', null);
|
||||||
@@ -985,7 +1085,12 @@ class AssetsController extends Controller
|
|||||||
// $asset->location_id = $target->rtd_location_id;
|
// $asset->location_id = $target->rtd_location_id;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
if ($asset->checkOut($target, auth()->user(), $checkout_at, $expected_checkin, $note, $asset_name, $asset->location_id)) {
|
// Keep checkout mutation + checkout logging/event side effects atomic.
|
||||||
|
$wasCheckedOut = DB::transaction(function () use ($asset, $target, $checkout_at, $expected_checkin, $note, $asset_name): bool {
|
||||||
|
return $asset->checkOut($target, auth()->user(), $checkout_at, $expected_checkin, $note, $asset_name, $asset->location_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($wasCheckedOut) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', ['asset' => e($asset->asset_tag)], trans('admin/hardware/message.checkout.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', ['asset' => e($asset->asset_tag)], trans('admin/hardware/message.checkout.success')));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1021,7 +1126,9 @@ class AssetsController extends Controller
|
|||||||
$asset->assignedTo()->disassociate($asset);
|
$asset->assignedTo()->disassociate($asset);
|
||||||
$asset->accepted = null;
|
$asset->accepted = null;
|
||||||
|
|
||||||
if ($request->has('name')) {
|
if ($request->input('clear_name') == '1') {
|
||||||
|
$asset->name = null;
|
||||||
|
} elseif ($request->has('name')) {
|
||||||
$asset->name = $request->input('name');
|
$asset->name = $request->input('name');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1164,6 +1271,10 @@ class AssetsController extends Controller
|
|||||||
|
|
||||||
$asset->last_audit_date = date('Y-m-d H:i:s');
|
$asset->last_audit_date = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
if ($request->input('clear_name') == '1') {
|
||||||
|
$asset->name = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Set up the payload for re-display in the API response
|
// Set up the payload for re-display in the API response
|
||||||
$payload = [
|
$payload = [
|
||||||
'id' => $asset->id,
|
'id' => $asset->id,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Http\Requests\ImageUploadRequest;
|
|||||||
use App\Http\Transformers\CompaniesTransformer;
|
use App\Http\Transformers\CompaniesTransformer;
|
||||||
use App\Http\Transformers\SelectlistTransformer;
|
use App\Http\Transformers\SelectlistTransformer;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
|
use App\Models\Setting;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
@@ -206,6 +207,16 @@ class CompaniesController extends Controller
|
|||||||
'companies.tag_color',
|
'companies.tag_color',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// When FMCS is enabled and the user is not a superuser, restrict the list to
|
||||||
|
// companies they belong to (primary company_id + pivot companies). This lets
|
||||||
|
// non-superusers select a company from their own set when creating assets, etc.
|
||||||
|
if (Setting::getSettings()->full_multiple_companies_support == '1' && ! auth()->user()->isSuperUser()) {
|
||||||
|
$userCompanyIds = auth()->user()->allCompanies()->pluck('id');
|
||||||
|
if ($userCompanyIds->isNotEmpty()) {
|
||||||
|
$companies->whereIn('companies.id', $userCompanyIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
$companies = $companies->where('companies.name', 'LIKE', '%'.$request->input('search').'%');
|
$companies = $companies->where('companies.name', 'LIKE', '%'.$request->input('search').'%');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use App\Http\Transformers\ComponentsTransformer;
|
|||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\Component;
|
use App\Models\Component;
|
||||||
|
use App\Models\Setting;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Query\Builder;
|
use Illuminate\Database\Query\Builder;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
@@ -314,20 +315,33 @@ class ComponentsController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($component->numRemaining() >= $request->input('assigned_qty')) {
|
if ($component->numRemaining() >= $request->input('assigned_qty')) {
|
||||||
|
// Resolve the raw target first, then enforce FMCS explicitly.
|
||||||
|
// Scoped lookup can hide cross-company records and lead to partial writes.
|
||||||
|
$asset = Asset::withoutGlobalScopes()->find($request->input('assigned_to'));
|
||||||
|
|
||||||
$asset = Asset::find($request->input('assigned_to'));
|
if (! $asset) {
|
||||||
$component->assigned_to = $request->input('assigned_to');
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')));
|
||||||
|
}
|
||||||
|
|
||||||
$component->assets()->attach($component->id, [
|
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($component->company_id !== $asset->company_id)) {
|
||||||
'component_id' => $component->id,
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||||
'created_at' => Carbon::now(),
|
}
|
||||||
'assigned_qty' => $request->input('assigned_qty', 1),
|
|
||||||
'created_by' => auth()->id(),
|
|
||||||
'asset_id' => $request->input('assigned_to'),
|
|
||||||
'note' => $request->input('note'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$component->logCheckout($request->input('note'), $asset, null, [], $request->get('assigned_qty', 1));
|
// Keep pivot + action log in one transaction so checkout is all-or-nothing.
|
||||||
|
DB::transaction(function () use ($component, $request, $asset): void {
|
||||||
|
$component->assigned_to = $request->input('assigned_to');
|
||||||
|
|
||||||
|
$component->assets()->attach($component->id, [
|
||||||
|
'component_id' => $component->id,
|
||||||
|
'created_at' => Carbon::now(),
|
||||||
|
'assigned_qty' => $request->input('assigned_qty', 1),
|
||||||
|
'created_by' => auth()->id(),
|
||||||
|
'asset_id' => $request->input('assigned_to'),
|
||||||
|
'note' => $request->input('note'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component->logCheckout($request->input('note'), $asset, null, [], $request->get('assigned_qty', 1));
|
||||||
|
});
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.checkout.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.checkout.success')));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ use App\Http\Transformers\ConsumablesTransformer;
|
|||||||
use App\Http\Transformers\SelectlistTransformer;
|
use App\Http\Transformers\SelectlistTransformer;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\Consumable;
|
use App\Models\Consumable;
|
||||||
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class ConsumablesController extends Controller
|
class ConsumablesController extends Controller
|
||||||
{
|
{
|
||||||
@@ -306,34 +308,42 @@ class ConsumablesController extends Controller
|
|||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/consumables/message.checkout.unavailable', ['requested' => $consumable->checkout_qty, 'remaining' => $consumable->numRemaining()])));
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/consumables/message.checkout.unavailable', ['requested' => $consumable->checkout_qty, 'remaining' => $consumable->numRemaining()])));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user exists - @TODO: this should probably be handled via validation, not here??
|
// Resolve the raw target first, then enforce FMCS explicitly.
|
||||||
if (! $user = User::find($request->input('assigned_to'))) {
|
// Scoped lookup can hide cross-company users and make failures ambiguous.
|
||||||
|
if (! $user = User::withoutGlobalScopes()->find($request->input('assigned_to'))) {
|
||||||
// Return error message
|
// Return error message
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'No user found'));
|
return response()->json(Helper::formatStandardApiResponse('error', null, 'No user found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((Setting::getSettings()->full_multiple_companies_support == '1') && (! $user->companies()->where('companies.id', $consumable->company_id)->exists())) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||||
|
}
|
||||||
|
|
||||||
// Update the consumable data
|
// Update the consumable data
|
||||||
$consumable->assigned_to = $request->input('assigned_to');
|
$consumable->assigned_to = $request->input('assigned_to');
|
||||||
|
|
||||||
for ($i = 0; $i < $consumable->checkout_qty; $i++) {
|
// Keep pivot writes and checkout log/event atomic to avoid partial checkout state.
|
||||||
$consumable->users()->attach($consumable->id,
|
DB::transaction(function () use ($consumable, $request, $user): void {
|
||||||
[
|
for ($i = 0; $i < $consumable->checkout_qty; $i++) {
|
||||||
'consumable_id' => $consumable->id,
|
$consumable->users()->attach($consumable->id,
|
||||||
'created_by' => $user->id,
|
[
|
||||||
'assigned_to' => $request->input('assigned_to'),
|
'consumable_id' => $consumable->id,
|
||||||
'note' => $request->input('note'),
|
'created_by' => $user->id,
|
||||||
]
|
'assigned_to' => $request->input('assigned_to'),
|
||||||
);
|
'note' => $request->input('note'),
|
||||||
}
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
event(new CheckoutableCheckedOut(
|
event(new CheckoutableCheckedOut(
|
||||||
$consumable,
|
$consumable,
|
||||||
$user,
|
$user,
|
||||||
auth()->user(),
|
auth()->user(),
|
||||||
$request->input('note'),
|
$request->input('note'),
|
||||||
[],
|
[],
|
||||||
$consumable->checkout_qty,
|
$consumable->checkout_qty,
|
||||||
));
|
));
|
||||||
|
});
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/consumables/message.checkout.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/consumables/message.checkout.success')));
|
||||||
|
|
||||||
@@ -346,6 +356,8 @@ class ConsumablesController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function selectlist(Request $request): array
|
public function selectlist(Request $request): array
|
||||||
{
|
{
|
||||||
|
$this->authorize('view.selectlists');
|
||||||
|
|
||||||
$consumables = Consumable::select([
|
$consumables = Consumable::select([
|
||||||
'consumables.id',
|
'consumables.id',
|
||||||
'consumables.name',
|
'consumables.name',
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ use App\Http\Transformers\LicenseSeatsTransformer;
|
|||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\License;
|
use App\Models\License;
|
||||||
use App\Models\LicenseSeat;
|
use App\Models\LicenseSeat;
|
||||||
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class LicenseSeatsController extends Controller
|
class LicenseSeatsController extends Controller
|
||||||
{
|
{
|
||||||
@@ -25,7 +27,7 @@ class LicenseSeatsController extends Controller
|
|||||||
if ($license = License::find($licenseId)) {
|
if ($license = License::find($licenseId)) {
|
||||||
$this->authorize('view', $license);
|
$this->authorize('view', $license);
|
||||||
|
|
||||||
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department', 'user.company', 'asset.company')
|
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department', 'user.companies', 'asset.company')
|
||||||
->where('license_seats.license_id', $licenseId);
|
->where('license_seats.license_id', $licenseId);
|
||||||
|
|
||||||
if ($request->input('status') == 'available') {
|
if ($request->input('status') == 'available') {
|
||||||
@@ -106,7 +108,8 @@ class LicenseSeatsController extends Controller
|
|||||||
'prohibits:asset_id',
|
'prohibits:asset_id',
|
||||||
// must be a valid user or null to unassign
|
// must be a valid user or null to unassign
|
||||||
function ($attribute, $value, $fail) {
|
function ($attribute, $value, $fail) {
|
||||||
if (! is_null($value) && ! User::where('id', $value)->whereNull('deleted_at')->exists()) {
|
// Validate existence without company scopes; FMCS checks happen explicitly below.
|
||||||
|
if (! is_null($value) && ! User::withoutGlobalScopes()->where('id', $value)->whereNull('deleted_at')->exists()) {
|
||||||
$fail('The selected assigned_to is invalid.');
|
$fail('The selected assigned_to is invalid.');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -118,7 +121,8 @@ class LicenseSeatsController extends Controller
|
|||||||
'prohibits:assigned_to',
|
'prohibits:assigned_to',
|
||||||
// must be a valid asset or null to unassign
|
// must be a valid asset or null to unassign
|
||||||
function ($attribute, $value, $fail) {
|
function ($attribute, $value, $fail) {
|
||||||
if (! is_null($value) && ! Asset::where('id', $value)->whereNull('deleted_at')->exists()) {
|
// Validate existence without company scopes; FMCS checks happen explicitly below.
|
||||||
|
if (! is_null($value) && ! Asset::withoutGlobalScopes()->where('id', $value)->whereNull('deleted_at')->exists()) {
|
||||||
$fail('The selected asset_id is invalid.');
|
$fail('The selected asset_id is invalid.');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -128,77 +132,141 @@ class LicenseSeatsController extends Controller
|
|||||||
|
|
||||||
$this->authorize('checkout', License::class);
|
$this->authorize('checkout', License::class);
|
||||||
|
|
||||||
$licenseSeat = LicenseSeat::with(['license', 'asset', 'user'])->find($seatId);
|
$errorResponse = null;
|
||||||
|
$updatedSeat = null;
|
||||||
|
|
||||||
if (! $licenseSeat) {
|
// Fetch the seat with a pessimistic lock inside a transaction so concurrent requests
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat not found'));
|
// on the same seat serialise rather than racing to overwrite each other's assignment.
|
||||||
}
|
DB::transaction(function () use ($request, $licenseId, $seatId, $validated, &$errorResponse, &$updatedSeat): void {
|
||||||
|
$licenseSeat = LicenseSeat::with(['license', 'asset', 'user'])
|
||||||
|
->lockForUpdate()
|
||||||
|
->find($seatId);
|
||||||
|
|
||||||
$license = $licenseSeat->license;
|
if (! $licenseSeat) {
|
||||||
if (! $license || $license->id != intval($licenseId)) {
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, 'Seat not found'));
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat does not belong to the specified license'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$oldUser = $licenseSeat->user;
|
return;
|
||||||
$oldAsset = $licenseSeat->asset;
|
}
|
||||||
|
|
||||||
// attempt to update the license seat
|
$license = $licenseSeat->license;
|
||||||
$licenseSeat->fill($validated);
|
if (! $license || $license->id != intval($licenseId)) {
|
||||||
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, 'Seat does not belong to the specified license'));
|
||||||
// check if this update is a checkin operation
|
|
||||||
// 1. are relevant fields touched at all?
|
return;
|
||||||
$assignmentTouched = $licenseSeat->isDirty('assigned_to') || $licenseSeat->isDirty('asset_id');
|
}
|
||||||
$anythingTouched = $licenseSeat->isDirty();
|
|
||||||
|
$targetUser = null;
|
||||||
if (! $anythingTouched) {
|
if (! is_null($request->input('assigned_to'))) {
|
||||||
return response()->json(
|
// Resolve unscoped target so we can return a clean cross-company error instead of a hidden-not-found.
|
||||||
Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success'))
|
$targetUser = User::withoutGlobalScopes()->find($request->input('assigned_to'));
|
||||||
);
|
|
||||||
}
|
if (! $targetUser) {
|
||||||
if ($assignmentTouched && $licenseSeat->unreassignable_seat) {
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, 'Target not found'));
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.unavailable')));
|
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
// 2. are they cleared? if yes then this is a checkin operation
|
|
||||||
$is_checkin = ($assignmentTouched && $licenseSeat->assigned_to === null && $licenseSeat->asset_id === null);
|
if ((Setting::getSettings()->full_multiple_companies_support == '1') && (! $targetUser->companies()->where('companies.id', $license->company_id)->exists())) {
|
||||||
$target = null;
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||||
|
|
||||||
// the logging functions expect only one "target". if both asset and user are present in the request,
|
return;
|
||||||
// we simply let assets take precedence over users...
|
}
|
||||||
if ($licenseSeat->isDirty('assigned_to')) {
|
}
|
||||||
$target = $is_checkin ? $oldUser : User::find($licenseSeat->assigned_to);
|
|
||||||
}
|
$targetAsset = null;
|
||||||
|
if (! is_null($request->input('asset_id'))) {
|
||||||
if ($licenseSeat->isDirty('asset_id')) {
|
// Resolve unscoped target so FMCS company mismatch can be enforced explicitly.
|
||||||
$target = $is_checkin ? $oldAsset : Asset::find($licenseSeat->asset_id);
|
$targetAsset = Asset::withoutGlobalScopes()->find($request->input('asset_id'));
|
||||||
}
|
|
||||||
|
if (! $targetAsset) {
|
||||||
if ($assignmentTouched && is_null($target)) {
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, 'Target not found'));
|
||||||
// if both asset_id and assigned_to are null then we are "checking-in"
|
|
||||||
// a related model that does not exist (possible purged or bad data).
|
return;
|
||||||
if (! is_null($request->input('asset_id')) || ! is_null($request->input('assigned_to'))) {
|
}
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Target not found'));
|
|
||||||
|
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($license->company_id !== $targetAsset->company_id)) {
|
||||||
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$oldUser = $licenseSeat->user;
|
||||||
|
$oldAsset = $licenseSeat->asset;
|
||||||
|
|
||||||
|
$licenseSeat->fill($validated);
|
||||||
|
|
||||||
|
$assignmentTouched = $licenseSeat->isDirty('assigned_to') || $licenseSeat->isDirty('asset_id');
|
||||||
|
$anythingTouched = $licenseSeat->isDirty();
|
||||||
|
|
||||||
|
if (! $anythingTouched) {
|
||||||
|
$updatedSeat = $licenseSeat;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($assignmentTouched && $licenseSeat->unreassignable_seat) {
|
||||||
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.unavailable')));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are the assignment fields cleared? If yes, this is a checkin operation.
|
||||||
|
$is_checkin = ($assignmentTouched && $licenseSeat->assigned_to === null && $licenseSeat->asset_id === null);
|
||||||
|
|
||||||
|
// The logging functions expect only one "target"; assets take precedence over users.
|
||||||
|
$target = null;
|
||||||
|
if ($licenseSeat->isDirty('assigned_to')) {
|
||||||
|
$target = $is_checkin ? $oldUser : $targetUser;
|
||||||
|
}
|
||||||
|
if ($licenseSeat->isDirty('asset_id')) {
|
||||||
|
$target = $is_checkin ? $oldAsset : $targetAsset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($assignmentTouched && is_null($target)) {
|
||||||
|
// Both fields are null but one was provided — the related model is purged or bad data.
|
||||||
|
if (! is_null($request->input('asset_id')) || ! is_null($request->input('assigned_to'))) {
|
||||||
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, 'Target not found'));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $licenseSeat->save()) {
|
||||||
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, $licenseSeat->getErrors()));
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ($licenseSeat->save()) {
|
|
||||||
if ($assignmentTouched) {
|
if ($assignmentTouched) {
|
||||||
if ($is_checkin) {
|
if ($is_checkin) {
|
||||||
if (! $licenseSeat->license->reassignable) {
|
if (! $licenseSeat->license->reassignable) {
|
||||||
$licenseSeat->unreassignable_seat = true;
|
$licenseSeat->unreassignable_seat = true;
|
||||||
$licenseSeat->save();
|
|
||||||
|
if (! $licenseSeat->save()) {
|
||||||
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, $licenseSeat->getErrors()));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// todo: skip if target is null?
|
|
||||||
$licenseSeat->logCheckin($target, $licenseSeat->notes);
|
$licenseSeat->logCheckin($target, $licenseSeat->notes);
|
||||||
} else {
|
} else {
|
||||||
// in this case, relevant fields are touched but it's not a checkin operation. so it must be a checkout operation.
|
|
||||||
$licenseSeat->logCheckout($request->input('notes'), $target);
|
$licenseSeat->logCheckout($request->input('notes'), $target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
|
$updatedSeat = $licenseSeat;
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($errorResponse) {
|
||||||
|
return $errorResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Helper::formatStandardApiResponse('error', null, $licenseSeat->getErrors());
|
if ($updatedSeat) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('success', $updatedSeat, trans('admin/licenses/message.update.success')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, 'An unexpected error occurred'), 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,21 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Events\CheckoutableCheckedIn;
|
||||||
|
use App\Events\CheckoutableCheckedOut;
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\FilterRequest;
|
use App\Http\Requests\FilterRequest;
|
||||||
use App\Http\Transformers\ActionlogsTransformer;
|
use App\Http\Transformers\ActionlogsTransformer;
|
||||||
|
use App\Http\Transformers\LicenseSeatsTransformer;
|
||||||
use App\Http\Transformers\LicensesTransformer;
|
use App\Http\Transformers\LicensesTransformer;
|
||||||
use App\Http\Transformers\SelectlistTransformer;
|
use App\Http\Transformers\SelectlistTransformer;
|
||||||
|
use App\Models\Asset;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\License;
|
use App\Models\License;
|
||||||
|
use App\Models\LicenseSeat;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -28,7 +34,7 @@ class LicensesController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorize('view', License::class);
|
$this->authorize('view', License::class);
|
||||||
|
|
||||||
$licenses = License::with('company', 'manufacturer', 'supplier', 'category', 'adminuser')->withCount('freeSeats as free_seats_count');
|
$licenses = License::with('company', 'manufacturer', 'supplier', 'category', 'adminuser', 'licenseSeatsRelation', 'assignedCount')->withCount('freeSeats as free_seats_count');
|
||||||
$settings = Setting::getSettings();
|
$settings = Setting::getSettings();
|
||||||
|
|
||||||
if ($request->input('status') == 'inactive') {
|
if ($request->input('status') == 'inactive') {
|
||||||
@@ -247,7 +253,7 @@ class LicensesController extends Controller
|
|||||||
if ($license->assigned_seats_count == 0) {
|
if ($license->assigned_seats_count == 0) {
|
||||||
// Delete the license and the associated license seats
|
// Delete the license and the associated license seats
|
||||||
DB::table('license_seats')
|
DB::table('license_seats')
|
||||||
->where('id', $license->id)
|
->where('license_id', $license->id)
|
||||||
->update(['assigned_to' => null, 'asset_id' => null]);
|
->update(['assigned_to' => null, 'asset_id' => null]);
|
||||||
|
|
||||||
$licenseSeats = $license->licenseseats();
|
$licenseSeats = $license->licenseseats();
|
||||||
@@ -261,6 +267,167 @@ class LicensesController extends Controller
|
|||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.assoc_users')));
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.assoc_users')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checkout a license seat to a user or asset.
|
||||||
|
*
|
||||||
|
* Accepts an optional `seat_id`; if omitted the next available free seat is used.
|
||||||
|
* `target_type` must be "user" or "asset". Supply `assigned_to` for users or
|
||||||
|
* `asset_id` for assets.
|
||||||
|
*
|
||||||
|
* This will eventually use the same form request the UI uses, but we need to update the field names first.
|
||||||
|
*
|
||||||
|
* @param int $licenseId
|
||||||
|
*/
|
||||||
|
public function checkout(Request $request, $licenseId): JsonResponse
|
||||||
|
{
|
||||||
|
$license = License::findOrFail($licenseId);
|
||||||
|
$this->authorize('checkout', $license);
|
||||||
|
|
||||||
|
$validated = $this->validate($request, [
|
||||||
|
'seat_id' => 'sometimes|integer|nullable',
|
||||||
|
'target_type' => 'required|in:user,asset',
|
||||||
|
'assigned_to' => 'required_if:target_type,user|integer|nullable',
|
||||||
|
'asset_id' => 'required_if:target_type,asset|integer|nullable',
|
||||||
|
'notes' => 'sometimes|string|nullable',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($license->isInactive()) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.license_is_inactive')));
|
||||||
|
}
|
||||||
|
|
||||||
|
$errorResponse = null;
|
||||||
|
$updatedSeat = null;
|
||||||
|
$target = null;
|
||||||
|
|
||||||
|
DB::transaction(function () use ($license, $validated, &$errorResponse, &$updatedSeat, &$target): void {
|
||||||
|
$seatId = $validated['seat_id'] ?? null;
|
||||||
|
|
||||||
|
$licenseSeat = $seatId
|
||||||
|
? LicenseSeat::where('id', $seatId)->where('license_id', $license->id)->lockForUpdate()->first()
|
||||||
|
: $license->freeSeat(lock: true);
|
||||||
|
|
||||||
|
if (! $licenseSeat) {
|
||||||
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.not_enough_seats')));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($licenseSeat->unreassignable_seat) {
|
||||||
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.unavailable')));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($validated['target_type'] === 'user') {
|
||||||
|
$target = User::withoutGlobalScopes()->whereNull('deleted_at')->find($validated['assigned_to'] ?? null);
|
||||||
|
if (! $target) {
|
||||||
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.user_does_not_exist')));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Company::isFullMultipleCompanySupportEnabled() && ! $target->companies()->where('companies.id', $license->company_id)->exists()) {
|
||||||
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$licenseSeat->assigned_to = $target->id;
|
||||||
|
$licenseSeat->asset_id = null;
|
||||||
|
} else {
|
||||||
|
$target = Asset::withoutGlobalScopes()->whereNull('deleted_at')->find($validated['asset_id'] ?? null);
|
||||||
|
if (! $target) {
|
||||||
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.asset_does_not_exist')));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Company::isFullMultipleCompanySupportEnabled() && $license->company_id && $license->company_id !== $target->company_id) {
|
||||||
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_user_company')));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$licenseSeat->asset_id = $target->id;
|
||||||
|
$licenseSeat->assigned_to = null;
|
||||||
|
|
||||||
|
if ($target->checkedOutToUser()) {
|
||||||
|
$licenseSeat->assigned_to = $target->assigned_to;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$licenseSeat->notes = $validated['notes'] ?? null;
|
||||||
|
$licenseSeat->created_by = auth()->id();
|
||||||
|
|
||||||
|
if (! $licenseSeat->save()) {
|
||||||
|
$errorResponse = response()->json(Helper::formatStandardApiResponse('error', null, $licenseSeat->getErrors()));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event(new CheckoutableCheckedOut($licenseSeat, $target, auth()->user(), $validated['notes'] ?? null));
|
||||||
|
$updatedSeat = $licenseSeat->load('license', 'user', 'asset');
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($errorResponse) {
|
||||||
|
return $errorResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($updatedSeat) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('success', (new LicenseSeatsTransformer)->transformLicenseSeat($updatedSeat), trans('admin/licenses/message.checkout.success')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, 'An unexpected error occurred'), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checkin a license seat.
|
||||||
|
*
|
||||||
|
* `seat_id` is required to identify which seat to check back in.
|
||||||
|
*
|
||||||
|
* @param int $licenseId
|
||||||
|
*/
|
||||||
|
public function checkin(Request $request, $licenseId): JsonResponse
|
||||||
|
{
|
||||||
|
$license = License::findOrFail($licenseId);
|
||||||
|
$this->authorize('checkin', $license);
|
||||||
|
|
||||||
|
$validated = $this->validate($request, [
|
||||||
|
'seat_id' => 'required|integer',
|
||||||
|
'notes' => 'sometimes|string|nullable',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$licenseSeat = LicenseSeat::where('id', $validated['seat_id'])
|
||||||
|
->where('license_id', $license->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $licenseSeat) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.not_found')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_null($licenseSeat->assigned_to) && is_null($licenseSeat->asset_id)) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkin.error')));
|
||||||
|
}
|
||||||
|
|
||||||
|
$target = $licenseSeat->user ?? $licenseSeat->asset;
|
||||||
|
|
||||||
|
$licenseSeat->assigned_to = null;
|
||||||
|
$licenseSeat->asset_id = null;
|
||||||
|
$licenseSeat->notes = $validated['notes'] ?? null;
|
||||||
|
|
||||||
|
if (! $license->reassignable) {
|
||||||
|
$licenseSeat->unreassignable_seat = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $licenseSeat->save()) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, $licenseSeat->getErrors()));
|
||||||
|
}
|
||||||
|
|
||||||
|
event(new CheckoutableCheckedIn($licenseSeat, $target, auth()->user(), $licenseSeat->notes));
|
||||||
|
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('success', (new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat->load('license', 'user', 'asset')), trans('admin/licenses/message.checkin.success')));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a paginated collection for the select2 menus
|
* Gets a paginated collection for the select2 menus
|
||||||
*
|
*
|
||||||
@@ -268,6 +435,8 @@ class LicensesController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function selectlist(Request $request): array
|
public function selectlist(Request $request): array
|
||||||
{
|
{
|
||||||
|
$this->authorize('view.selectlists');
|
||||||
|
|
||||||
$licenses = License::select([
|
$licenses = License::select([
|
||||||
'licenses.id',
|
'licenses.id',
|
||||||
'licenses.name',
|
'licenses.name',
|
||||||
|
|||||||
@@ -67,7 +67,18 @@ class LocationsController extends Controller
|
|||||||
'notes',
|
'notes',
|
||||||
];
|
];
|
||||||
|
|
||||||
$locations = Location::with('parent', 'manager', 'children')->select([
|
$locations = Location::with([
|
||||||
|
'parent',
|
||||||
|
'children',
|
||||||
|
'manager' => fn ($q) => $q->withCount([
|
||||||
|
'assets as assets_count',
|
||||||
|
'accessories as accessories_count',
|
||||||
|
'licenses as licenses_count',
|
||||||
|
'consumables as consumables_count',
|
||||||
|
'managesUsers as manages_users_count',
|
||||||
|
'managedLocations as manages_locations_count',
|
||||||
|
]),
|
||||||
|
])->select([
|
||||||
'locations.id',
|
'locations.id',
|
||||||
'locations.name',
|
'locations.name',
|
||||||
'locations.address',
|
'locations.address',
|
||||||
@@ -103,7 +114,9 @@ class LocationsController extends Controller
|
|||||||
->withCount('components as components_count')
|
->withCount('components as components_count')
|
||||||
->with('adminuser');
|
->with('adminuser');
|
||||||
|
|
||||||
// Only scope locations if the setting is enabled
|
// scope_locations_fmcs is required for location-level company scoping (locations may not
|
||||||
|
// have company_id assigned unless the compatibility check has been completed in Settings).
|
||||||
|
// Without it, locations are visible to all authenticated users regardless of FMCS state.
|
||||||
if (Setting::getSettings()->scope_locations_fmcs) {
|
if (Setting::getSettings()->scope_locations_fmcs) {
|
||||||
$locations = Company::scopeCompanyables($locations);
|
$locations = Company::scopeCompanyables($locations);
|
||||||
}
|
}
|
||||||
@@ -157,8 +170,6 @@ class LocationsController extends Controller
|
|||||||
$locations->where('tag_color', '=', $request->input('locations.tag_color'));
|
$locations->where('tag_color', '=', $request->input('locations.tag_color'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
|
||||||
$offset = ($request->input('offset') > $locations->count()) ? $locations->count() : app('api_offset_value');
|
|
||||||
$limit = app('api_limit_value');
|
$limit = app('api_limit_value');
|
||||||
|
|
||||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||||
@@ -180,6 +191,7 @@ class LocationsController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$total = $locations->count();
|
$total = $locations->count();
|
||||||
|
$offset = ($request->input('offset') > $total) ? $total : app('api_offset_value');
|
||||||
$locations = $locations->skip($offset)->take($limit)->get();
|
$locations = $locations->skip($offset)->take($limit)->get();
|
||||||
|
|
||||||
return (new LocationsTransformer)->transformLocations($locations, $total);
|
return (new LocationsTransformer)->transformLocations($locations, $total);
|
||||||
@@ -199,12 +211,19 @@ class LocationsController extends Controller
|
|||||||
$location->fill($request->all());
|
$location->fill($request->all());
|
||||||
$location = $request->handleImages($location);
|
$location = $request->handleImages($location);
|
||||||
|
|
||||||
// Only scope location if the setting is enabled
|
|
||||||
if (Setting::getSettings()->scope_locations_fmcs) {
|
if (Setting::getSettings()->scope_locations_fmcs) {
|
||||||
$location->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
$location->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||||
// check if parent is set and has a different company
|
}
|
||||||
if ($location->parent_id && Location::find($location->parent_id)->company_id != $location->company_id) {
|
|
||||||
response()->json(Helper::formatStandardApiResponse('error', null, 'different company than parent'));
|
// Parent company check applies whenever FMCS is on, independent of scope_locations_fmcs.
|
||||||
|
if (Setting::getSettings()->full_multiple_companies_support) {
|
||||||
|
$parent = $location->parent_id ? Location::find($location->parent_id) : null;
|
||||||
|
if ($parent && $parent->company_id != $location->company_id) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_location_parent_company', [
|
||||||
|
'parent' => $parent->name,
|
||||||
|
'parent_company' => $parent->company?->name ?? trans('general.unassigned'),
|
||||||
|
'location_company' => $location->company?->name ?? trans('general.unassigned'),
|
||||||
|
])));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +246,19 @@ class LocationsController extends Controller
|
|||||||
public function show($id): JsonResponse|array
|
public function show($id): JsonResponse|array
|
||||||
{
|
{
|
||||||
$this->authorize('view', Location::class);
|
$this->authorize('view', Location::class);
|
||||||
$location = Location::with('parent', 'manager', 'children', 'company')
|
$location = Location::with([
|
||||||
|
'parent',
|
||||||
|
'children',
|
||||||
|
'company',
|
||||||
|
'manager' => fn ($q) => $q->withCount([
|
||||||
|
'assets as assets_count',
|
||||||
|
'accessories as accessories_count',
|
||||||
|
'licenses as licenses_count',
|
||||||
|
'consumables as consumables_count',
|
||||||
|
'managesUsers as manages_users_count',
|
||||||
|
'managedLocations as manages_locations_count',
|
||||||
|
]),
|
||||||
|
])
|
||||||
->select([
|
->select([
|
||||||
'locations.id',
|
'locations.id',
|
||||||
'locations.name',
|
'locations.name',
|
||||||
@@ -279,18 +310,36 @@ class LocationsController extends Controller
|
|||||||
$location = $request->handleImages($location);
|
$location = $request->handleImages($location);
|
||||||
|
|
||||||
if ($request->filled('company_id')) {
|
if ($request->filled('company_id')) {
|
||||||
// Only scope location if the setting is enabled
|
|
||||||
if (Setting::getSettings()->scope_locations_fmcs) {
|
if (Setting::getSettings()->scope_locations_fmcs) {
|
||||||
$location->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
$location->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||||
// check if there are related objects with different company
|
// check if there are related objects with different company
|
||||||
if (Helper::test_locations_fmcs(false, $id, $location->company_id)) {
|
if ($mismatched = Helper::test_locations_fmcs(false, $id, $location->company_id)) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'error scoped locations'));
|
$first = $mismatched[0];
|
||||||
|
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_location_scoped_items', [
|
||||||
|
'item_type' => trans('general.'.strtolower($first[0])),
|
||||||
|
'item_name' => $first[2],
|
||||||
|
'item_company' => $first[5] ?? trans('general.unassigned'),
|
||||||
|
])));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$location->company_id = $request->input('company_id');
|
$location->company_id = $request->input('company_id');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parent company check applies whenever FMCS is on, independent of scope_locations_fmcs.
|
||||||
|
// Runs outside the company_id gate so a parent_id-only update is also validated.
|
||||||
|
if (Setting::getSettings()->full_multiple_companies_support) {
|
||||||
|
$parent = $location->parent_id ? Location::find($location->parent_id) : null;
|
||||||
|
if ($parent && $parent->company_id != $location->company_id) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.error_location_parent_company', [
|
||||||
|
'parent' => $parent->name,
|
||||||
|
'parent_company' => $parent->company?->name ?? trans('general.unassigned'),
|
||||||
|
'location_company' => $location->company?->name ?? trans('general.unassigned'),
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($location->isValid()) {
|
if ($location->isValid()) {
|
||||||
|
|
||||||
$location->save();
|
$location->save();
|
||||||
@@ -422,11 +471,6 @@ class LocationsController extends Controller
|
|||||||
'locations.tag_color',
|
'locations.tag_color',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Only scope locations if the setting is enabled
|
|
||||||
if (Setting::getSettings()->scope_locations_fmcs) {
|
|
||||||
$locations = Company::scopeCompanyables($locations);
|
|
||||||
}
|
|
||||||
|
|
||||||
$page = 1;
|
$page = 1;
|
||||||
if ($request->filled('page')) {
|
if ($request->filled('page')) {
|
||||||
$page = $request->input('page');
|
$page = $request->input('page');
|
||||||
@@ -436,6 +480,10 @@ class LocationsController extends Controller
|
|||||||
$locations = $locations->where('locations.name', 'LIKE', '%'.$request->input('search').'%');
|
$locations = $locations->where('locations.name', 'LIKE', '%'.$request->input('search').'%');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($request->filled('excludeId')) {
|
||||||
|
$locations->where('locations.id', '!=', (int) $request->input('excludeId'));
|
||||||
|
}
|
||||||
|
|
||||||
$locations = $locations->orderBy('name', 'ASC')->get();
|
$locations = $locations->orderBy('name', 'ASC')->get();
|
||||||
|
|
||||||
$locations_with_children = [];
|
$locations_with_children = [];
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Helpers\Helper;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\FilterRequest;
|
||||||
|
use App\Http\Transformers\MaintenanceTypesTransformer;
|
||||||
|
use App\Models\MaintenanceType;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class MaintenanceTypesController extends Controller
|
||||||
|
{
|
||||||
|
public function index(FilterRequest $request): JsonResponse|array
|
||||||
|
{
|
||||||
|
$this->authorize('view', MaintenanceType::class);
|
||||||
|
|
||||||
|
$types = MaintenanceType::select(['id', 'name', 'created_at', 'updated_at', 'deleted_at']);
|
||||||
|
|
||||||
|
if ($request->input('deleted') == 'true') {
|
||||||
|
$types->onlyTrashed();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$types->where('name', 'LIKE', '%'.$request->input('search').'%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('name')) {
|
||||||
|
$types->where('name', '=', $request->input('name'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$offset = ($request->input('offset') > $types->count()) ? $types->count() : app('api_offset_value');
|
||||||
|
$limit = app('api_limit_value');
|
||||||
|
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||||
|
$sort = in_array($request->input('sort'), ['id', 'name', 'created_at', 'updated_at']) ? $request->input('sort') : 'name';
|
||||||
|
|
||||||
|
$total = $types->count();
|
||||||
|
$types = $types->orderBy($sort, $order)->skip($offset)->take($limit)->get();
|
||||||
|
|
||||||
|
return (new MaintenanceTypesTransformer)->transformMaintenanceTypes($types, $total);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(MaintenanceType $maintenanceType): JsonResponse|array
|
||||||
|
{
|
||||||
|
$this->authorize('view', $maintenanceType);
|
||||||
|
|
||||||
|
return (new MaintenanceTypesTransformer)->transformMaintenanceType($maintenanceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->authorize('create', MaintenanceType::class);
|
||||||
|
|
||||||
|
$type = new MaintenanceType;
|
||||||
|
$type->name = $request->input('name');
|
||||||
|
$type->created_by = auth()->id();
|
||||||
|
|
||||||
|
if ($type->save()) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('success', (new MaintenanceTypesTransformer)->transformMaintenanceType($type), trans('admin/maintenance_types/message.create.success')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, $type->getErrors()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, MaintenanceType $maintenanceType): JsonResponse
|
||||||
|
{
|
||||||
|
$this->authorize('update', $maintenanceType);
|
||||||
|
|
||||||
|
$maintenanceType->name = $request->input('name');
|
||||||
|
|
||||||
|
if ($maintenanceType->save()) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('success', (new MaintenanceTypesTransformer)->transformMaintenanceType($maintenanceType), trans('admin/maintenance_types/message.update.success')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenanceType->getErrors()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(MaintenanceType $maintenanceType): JsonResponse
|
||||||
|
{
|
||||||
|
$this->authorize('delete', $maintenanceType);
|
||||||
|
|
||||||
|
$maintenanceType->delete();
|
||||||
|
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/maintenance_types/message.delete.success')));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,15 +2,19 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Enums\ActionType;
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\FilterRequest;
|
use App\Http\Requests\FilterRequest;
|
||||||
use App\Http\Requests\ImageUploadRequest;
|
use App\Http\Requests\ImageUploadRequest;
|
||||||
use App\Http\Transformers\ActionlogsTransformer;
|
use App\Http\Transformers\ActionlogsTransformer;
|
||||||
use App\Http\Transformers\MaintenancesTransformer;
|
use App\Http\Transformers\MaintenancesTransformer;
|
||||||
|
use App\Models\Actionlog;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\Maintenance;
|
use App\Models\Maintenance;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
@@ -38,7 +42,8 @@ class MaintenancesController extends Controller
|
|||||||
$this->authorize('view', Asset::class);
|
$this->authorize('view', Asset::class);
|
||||||
|
|
||||||
$maintenances = Maintenance::select('maintenances.*')
|
$maintenances = Maintenance::select('maintenances.*')
|
||||||
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.status', 'adminuser', 'asset.assignedTo');
|
->whereHas('asset')
|
||||||
|
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.status', 'adminuser', 'asset.assignedTo', 'maintenanceType', 'responsibleParty', 'completedByUser');
|
||||||
|
|
||||||
// This invokes the Searchable model trait scopeTextSearch and will handle input by search or by advanced search filter
|
// This invokes the Searchable model trait scopeTextSearch and will handle input by search or by advanced search filter
|
||||||
if ($request->filled('filter') || $request->filled('search')) {
|
if ($request->filled('filter') || $request->filled('search')) {
|
||||||
@@ -61,22 +66,53 @@ class MaintenancesController extends Controller
|
|||||||
$maintenances->where('maintenances.url', '=', $request->input('url'));
|
$maintenances->where('maintenances.url', '=', $request->input('url'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('asset_maintenance_type')) {
|
if ($request->filled('maintenance_type')) {
|
||||||
$maintenances->where('asset_maintenance_type', '=', $request->input('asset_maintenance_type'));
|
$maintenances->where('maintenance_type', '=', $request->input('maintenance_type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('maintenance_type_id')) {
|
||||||
|
$maintenances->where('maintenance_type_id', '=', $request->input('maintenance_type_id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('responsible_party_id')) {
|
||||||
|
$maintenances->where('responsible_party_id', '=', $request->input('responsible_party_id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('completed')) {
|
||||||
|
if ($request->input('completed') === 'true') {
|
||||||
|
$maintenances->completed();
|
||||||
|
} else {
|
||||||
|
$maintenances->active();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('upcoming_status')) {
|
||||||
|
$settings = Setting::getSettings();
|
||||||
|
switch ($request->input('upcoming_status')) {
|
||||||
|
case 'due':
|
||||||
|
$maintenances->dueForCompletion($settings);
|
||||||
|
break;
|
||||||
|
case 'overdue':
|
||||||
|
$maintenances->overdueForCompletion();
|
||||||
|
break;
|
||||||
|
case 'due-or-overdue':
|
||||||
|
$maintenances->dueOrOverdueForCompletion($settings);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||||
$offset = ($request->input('offset') > $maintenances->count()) ? $maintenances->count() : abs($request->input('offset'));
|
$offset = ($request->input('offset') > $maintenances->count()) ? $maintenances->count() : app('api_offset_value');
|
||||||
$limit = app('api_limit_value');
|
$limit = app('api_limit_value');
|
||||||
|
|
||||||
$allowed_columns = [
|
$allowed_columns = [
|
||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'asset_maintenance_time',
|
'asset_maintenance_time',
|
||||||
'asset_maintenance_type',
|
|
||||||
'cost',
|
'cost',
|
||||||
'start_date',
|
'start_date',
|
||||||
'completion_date',
|
'completion_date',
|
||||||
|
'completed_at',
|
||||||
'notes',
|
'notes',
|
||||||
'asset_tag',
|
'asset_tag',
|
||||||
'asset_name',
|
'asset_name',
|
||||||
@@ -88,6 +124,7 @@ class MaintenancesController extends Controller
|
|||||||
'status_label',
|
'status_label',
|
||||||
'model',
|
'model',
|
||||||
'model_number',
|
'model_number',
|
||||||
|
'maintenance_type',
|
||||||
];
|
];
|
||||||
|
|
||||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||||
@@ -95,31 +132,37 @@ class MaintenancesController extends Controller
|
|||||||
|
|
||||||
switch ($sort) {
|
switch ($sort) {
|
||||||
case 'created_by':
|
case 'created_by':
|
||||||
$maintenances = $maintenances->OrderByCreatedBy($order);
|
$maintenances = $maintenances->orderByCreatedBy($order);
|
||||||
break;
|
break;
|
||||||
case 'supplier':
|
case 'supplier':
|
||||||
$maintenances = $maintenances->OrderBySupplier($order);
|
$maintenances = $maintenances->orderBySupplier($order);
|
||||||
break;
|
break;
|
||||||
case 'asset_tag':
|
case 'asset_tag':
|
||||||
$maintenances = $maintenances->OrderByTag($order);
|
$maintenances = $maintenances->orderByTag($order);
|
||||||
break;
|
break;
|
||||||
case 'asset_name':
|
case 'asset_name':
|
||||||
$maintenances = $maintenances->OrderByAssetName($order);
|
$maintenances = $maintenances->orderByAssetName($order);
|
||||||
break;
|
break;
|
||||||
case 'model':
|
case 'model':
|
||||||
$maintenances = $maintenances->OrderByAssetModelName($order);
|
$maintenances = $maintenances->orderByAssetModelName($order);
|
||||||
break;
|
break;
|
||||||
case 'model_number':
|
case 'model_number':
|
||||||
$maintenances = $maintenances->OrderByAssetModelNumber($order);
|
$maintenances = $maintenances->orderByAssetModelNumber($order);
|
||||||
break;
|
break;
|
||||||
case 'serial':
|
case 'serial':
|
||||||
$maintenances = $maintenances->OrderByAssetSerial($order);
|
$maintenances = $maintenances->orderByAssetSerial($order);
|
||||||
break;
|
break;
|
||||||
case 'location':
|
case 'location':
|
||||||
$maintenances = $maintenances->OrderLocationName($order);
|
$maintenances = $maintenances->orderLocationName($order);
|
||||||
break;
|
break;
|
||||||
case 'status_label':
|
case 'status_label':
|
||||||
$maintenances = $maintenances->OrderStatusName($order);
|
$maintenances = $maintenances->orderStatusName($order);
|
||||||
|
break;
|
||||||
|
case 'maintenance_type':
|
||||||
|
$maintenances = $maintenances->orderByMaintenanceType($order);
|
||||||
|
break;
|
||||||
|
case 'completed_at':
|
||||||
|
$maintenances = $maintenances->orderByCompletedAt($order);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$maintenances = $maintenances->orderBy($sort, $order);
|
$maintenances = $maintenances->orderBy($sort, $order);
|
||||||
@@ -152,19 +195,60 @@ class MaintenancesController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorize('update', Asset::class);
|
$this->authorize('update', Asset::class);
|
||||||
|
|
||||||
// create a new model instance
|
$isBulk = $request->has('asset_ids');
|
||||||
$maintenance = new Maintenance;
|
$assetIds = $isBulk
|
||||||
$maintenance->fill($request->all());
|
? array_values(array_filter((array) $request->input('asset_ids')))
|
||||||
$maintenance->created_by = auth()->id();
|
: [$request->input('asset_id')];
|
||||||
$maintenance = $request->handleImages($maintenance);
|
|
||||||
// Was the asset maintenance created?
|
|
||||||
if ($maintenance->save()) {
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.create.success')));
|
|
||||||
|
|
||||||
|
$created = new EloquentCollection;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach ($assetIds as $assetId) {
|
||||||
|
$asset = Asset::find($assetId);
|
||||||
|
|
||||||
|
if (! $asset) {
|
||||||
|
$errors[] = trans('general.item_not_found', ['item_type' => trans('general.asset'), 'id' => $assetId]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Company::isCurrentUserHasAccess($asset)) {
|
||||||
|
$errors[] = trans('general.action_permission_denied', ['item_type' => trans('general.asset'), 'id' => $assetId, 'action' => trans('general.create')]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$maintenance = new Maintenance;
|
||||||
|
$maintenance->fill($request->except(['asset_id', 'asset_ids']));
|
||||||
|
$maintenance->asset_id = $assetId;
|
||||||
|
$maintenance->created_by = auth()->id();
|
||||||
|
$request->handleImages($maintenance);
|
||||||
|
|
||||||
|
if ($maintenance->save()) {
|
||||||
|
$created->push($maintenance->fresh());
|
||||||
|
} else {
|
||||||
|
$errors[] = $maintenance->getErrors();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
|
if ($isBulk) {
|
||||||
|
if ($created->isEmpty()) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, count($errors) === 1 ? $errors[0] : $errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(Helper::formatStandardApiResponse(
|
||||||
|
'success',
|
||||||
|
(new MaintenancesTransformer)->transformMaintenances($created, $created->count()),
|
||||||
|
trans('admin/maintenances/message.create.success')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single asset_id path — backward compatible response shape
|
||||||
|
if ($created->isNotEmpty()) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('success', $created->first(), trans('admin/maintenances/message.create.success')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, ! empty($errors) ? $errors[0] : null));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -185,17 +269,33 @@ class MaintenancesController extends Controller
|
|||||||
|
|
||||||
if ($maintenance = Maintenance::with('asset')->find($id)) {
|
if ($maintenance = Maintenance::with('asset')->find($id)) {
|
||||||
|
|
||||||
// Can this user manage this asset?
|
// The asset this maintenance is attached to is not valid or has been deleted
|
||||||
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/maintenances/general.maintenance'), 'id' => $id, 'action' => trans('general.edit')])));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The asset this miantenance is attached to is not valid or has been deleted
|
|
||||||
if (! $maintenance->asset) {
|
if (! $maintenance->asset) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('general.asset'), 'id' => $id])));
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('general.asset'), 'id' => $id])));
|
||||||
}
|
}
|
||||||
|
|
||||||
$maintenance->fill($request->all());
|
// Can this user manage the existing asset?
|
||||||
|
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/maintenances/general.maintenance'), 'id' => $id, 'action' => trans('general.edit')])));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the request changes asset_id, verify the new asset is accessible
|
||||||
|
if ($request->filled('asset_id') && (int) $request->input('asset_id') !== $maintenance->asset_id) {
|
||||||
|
$newAsset = Asset::find($request->input('asset_id'));
|
||||||
|
|
||||||
|
if (! $newAsset) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('general.asset'), 'id' => $request->input('asset_id')])));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Company::isCurrentUserHasAccess($newAsset)) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('general.asset'), 'id' => $request->input('asset_id'), 'action' => trans('general.edit')])), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$maintenance->fill($request->except('asset_id'));
|
||||||
|
$maintenance->asset_id = $newAsset->id;
|
||||||
|
} else {
|
||||||
|
$maintenance->fill($request->except('asset_id'));
|
||||||
|
}
|
||||||
|
|
||||||
if ($maintenance->save()) {
|
if ($maintenance->save()) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.edit.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.edit.success')));
|
||||||
@@ -255,6 +355,35 @@ class MaintenancesController extends Controller
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function complete(Request $request, Maintenance $maintenance): JsonResponse
|
||||||
|
{
|
||||||
|
$this->authorize('update', Asset::class);
|
||||||
|
|
||||||
|
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/maintenances/general.maintenance'), 'id' => $maintenance->id, 'action' => trans('admin/maintenances/form.mark_complete')])));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($maintenance->completed_at) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/maintenances/form.already_complete')));
|
||||||
|
}
|
||||||
|
|
||||||
|
$maintenance->completed_at = now();
|
||||||
|
$maintenance->completed_by = auth()->id();
|
||||||
|
$maintenance->asset_maintenance_time = (int) $maintenance->created_at->diffInDays(now(), true);
|
||||||
|
$maintenance->saveQuietly();
|
||||||
|
|
||||||
|
$logAction = new Actionlog;
|
||||||
|
$logAction->item_type = Maintenance::class;
|
||||||
|
$logAction->item_id = $maintenance->id;
|
||||||
|
$logAction->target_type = Asset::class;
|
||||||
|
$logAction->target_id = $maintenance->asset_id;
|
||||||
|
$logAction->created_by = auth()->id();
|
||||||
|
$logAction->note = $request->input('note');
|
||||||
|
$logAction->logaction(ActionType::MaintenanceComplete);
|
||||||
|
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('success', (new MaintenancesTransformer)->transformMaintenance($maintenance->fresh()), trans('admin/maintenances/message.complete.success')));
|
||||||
|
}
|
||||||
|
|
||||||
public function history(Request $request, Maintenance $maintenance): JsonResponse|array
|
public function history(Request $request, Maintenance $maintenance): JsonResponse|array
|
||||||
{
|
{
|
||||||
$this->authorize('history', $maintenance);
|
$this->authorize('history', $maintenance);
|
||||||
@@ -266,4 +395,50 @@ class MaintenancesController extends Controller
|
|||||||
|
|
||||||
return response()->json((new ActionlogsTransformer)->transformActionlogs($history, $total), 200, ['Content-Type' => 'application/json;charset=utf8'], JSON_UNESCAPED_UNICODE);
|
return response()->json((new ActionlogsTransformer)->transformActionlogs($history, $total), 200, ['Content-Type' => 'application/json;charset=utf8'], JSON_UNESCAPED_UNICODE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function notesIndex(Maintenance $maintenance): JsonResponse
|
||||||
|
{
|
||||||
|
$this->authorize('journal', $maintenance);
|
||||||
|
|
||||||
|
$notes = Actionlog::with('user:id,username')
|
||||||
|
->where('item_type', Maintenance::class)
|
||||||
|
->where('item_id', $maintenance->id)
|
||||||
|
->where('action_type', 'note added')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get(['id', 'created_at', 'note', 'created_by', 'item_id', 'item_type', 'action_type']);
|
||||||
|
|
||||||
|
$notesArray = $notes->map(fn ($note) => [
|
||||||
|
'id' => $note->id,
|
||||||
|
'created_at' => $note->created_at,
|
||||||
|
'note' => $note->note,
|
||||||
|
'created_by' => $note->created_by,
|
||||||
|
'username' => $note->user?->username,
|
||||||
|
'item_id' => $note->item_id,
|
||||||
|
'item_type' => $note->item_type,
|
||||||
|
'action_type' => $note->action_type,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('success', ['notes' => $notesArray, 'maintenance_id' => $maintenance->id]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notesStore(Request $request, Maintenance $maintenance): JsonResponse
|
||||||
|
{
|
||||||
|
$this->authorize('update', $maintenance);
|
||||||
|
|
||||||
|
if (! $request->filled('note')) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('validation.required', ['attribute' => 'note'])), 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logaction = new Actionlog;
|
||||||
|
$logaction->item_type = Maintenance::class;
|
||||||
|
$logaction->created_by = auth()->id();
|
||||||
|
$logaction->item_id = $maintenance->id;
|
||||||
|
$logaction->note = $request->input('note');
|
||||||
|
|
||||||
|
if ($logaction->logaction('note added')) {
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('success', ['note' => $logaction->note, 'item_id' => $maintenance->id], trans('general.note_added')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, 'Something went wrong'), 500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ use App\Helpers\Helper;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Transformers\PredefinedKitsTransformer;
|
use App\Http\Transformers\PredefinedKitsTransformer;
|
||||||
use App\Http\Transformers\SelectlistTransformer;
|
use App\Http\Transformers\SelectlistTransformer;
|
||||||
|
use App\Models\Accessory;
|
||||||
|
use App\Models\Consumable;
|
||||||
|
use App\Models\License;
|
||||||
use App\Models\PredefinedKit;
|
use App\Models\PredefinedKit;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -183,6 +186,9 @@ class PredefinedKitsController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$license_id = $request->input('license');
|
$license_id = $request->input('license');
|
||||||
|
$license = License::findOrFail($license_id);
|
||||||
|
$this->authorize('view', $license);
|
||||||
|
|
||||||
$relation = $kit->licenses();
|
$relation = $kit->licenses();
|
||||||
if ($relation->find($license_id)) {
|
if ($relation->find($license_id)) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, ['license' => trans('admin/kits/general.license_error')]));
|
return response()->json(Helper::formatStandardApiResponse('error', null, ['license' => trans('admin/kits/general.license_error')]));
|
||||||
@@ -329,6 +335,9 @@ class PredefinedKitsController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$consumable_id = $request->input('consumable');
|
$consumable_id = $request->input('consumable');
|
||||||
|
$consumable = Consumable::findOrFail($consumable_id);
|
||||||
|
$this->authorize('view', $consumable);
|
||||||
|
|
||||||
$relation = $kit->consumables();
|
$relation = $kit->consumables();
|
||||||
if ($relation->find($consumable_id)) {
|
if ($relation->find($consumable_id)) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, ['consumable' => trans('admin/kits/general.consumable_error')]));
|
return response()->json(Helper::formatStandardApiResponse('error', null, ['consumable' => trans('admin/kits/general.consumable_error')]));
|
||||||
@@ -402,6 +411,9 @@ class PredefinedKitsController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$accessory_id = $request->input('accessory');
|
$accessory_id = $request->input('accessory');
|
||||||
|
$accessory = Accessory::findOrFail($accessory_id);
|
||||||
|
$this->authorize('view', $accessory);
|
||||||
|
|
||||||
$relation = $kit->accessories();
|
$relation = $kit->accessories();
|
||||||
if ($relation->find($accessory_id)) {
|
if ($relation->find($accessory_id)) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, ['accessory' => trans('admin/kits/general.accessory_error')]));
|
return response()->json(Helper::formatStandardApiResponse('error', null, ['accessory' => trans('admin/kits/general.accessory_error')]));
|
||||||
|
|||||||
@@ -6,8 +6,18 @@ use App\Helpers\Helper;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\FilterRequest;
|
use App\Http\Requests\FilterRequest;
|
||||||
use App\Http\Transformers\ActionlogsTransformer;
|
use App\Http\Transformers\ActionlogsTransformer;
|
||||||
|
use App\Models\Accessory;
|
||||||
use App\Models\Actionlog;
|
use App\Models\Actionlog;
|
||||||
|
use App\Models\Asset;
|
||||||
|
use App\Models\Component;
|
||||||
|
use App\Models\Consumable;
|
||||||
|
use App\Models\License;
|
||||||
|
use App\Models\LicenseSeat;
|
||||||
|
use App\Models\Maintenance;
|
||||||
|
use App\Models\User;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
|
||||||
class ReportsController extends Controller
|
class ReportsController extends Controller
|
||||||
@@ -26,18 +36,18 @@ class ReportsController extends Controller
|
|||||||
// then they shouldn't be able to see the activity log for that item or target,
|
// then they shouldn't be able to see the activity log for that item or target,
|
||||||
// but if they have the general activity view permission,
|
// but if they have the general activity view permission,
|
||||||
// then they can see all activity logs regardless of the item or target.
|
// then they can see all activity logs regardless of the item or target.
|
||||||
if ((! Gate::allows('activity.view')) && (($request->filled('target_type')) && ($request->filled('target_id'))) || (($request->filled('item_type')) && ($request->filled('item_id')))) {
|
if ((! Gate::allows('activity.view')) && (($request->filled('target_type') && $request->filled('target_id')) || ($request->filled('item_type') && $request->filled('item_id')))) {
|
||||||
|
|
||||||
if (($request->filled('target_type')) && ($request->filled('target_id'))) {
|
if (($request->filled('target_type')) && ($request->filled('target_id'))) {
|
||||||
$target = Helper::normalizeFullModelName(request()->input('target_type'));
|
$targetClass = Helper::normalizeFullModelName(request()->input('target_type'));
|
||||||
$target::find(request()->input('target_id'))?->withTrashed();
|
$target = $targetClass::withTrashed()->find(request()->input('target_id'));
|
||||||
$this->authorize('view', $target);
|
$this->authorize('view', $target ?? $targetClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($request->filled('item_type')) && ($request->filled('item_id'))) {
|
if (($request->filled('item_type')) && ($request->filled('item_id'))) {
|
||||||
$item = Helper::normalizeFullModelName(request()->input('item_type'));
|
$itemClass = Helper::normalizeFullModelName(request()->input('item_type'));
|
||||||
$item::find(request()->input('item_id'))?->withTrashed();
|
$item = $itemClass::withTrashed()->find(request()->input('item_id'));
|
||||||
$this->authorize('view', $item);
|
$this->authorize('view', $item ?? $itemClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -125,4 +135,141 @@ class ReportsController extends Controller
|
|||||||
return response()->json((new ActionlogsTransformer)->transformActionlogs($actionlogs, $total), 200, ['Content-Type' => 'application/json;charset=utf8'], JSON_UNESCAPED_UNICODE);
|
return response()->json((new ActionlogsTransformer)->transformActionlogs($actionlogs, $total), 200, ['Content-Type' => 'application/json;charset=utf8'], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns time-series data for the reports overview charts.
|
||||||
|
*
|
||||||
|
* Accepts ?days=N (preset, default 30) OR ?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD.
|
||||||
|
* Also returns the immediately preceding period of equal length for comparison lines.
|
||||||
|
*/
|
||||||
|
public function activityChart(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->authorize('reports.view');
|
||||||
|
|
||||||
|
$allowedDays = [7, 14, 30, 60, 90, 180, 365];
|
||||||
|
|
||||||
|
if ($request->filled('start_date') && $request->filled('end_date')) {
|
||||||
|
$curStart = Carbon::parse($request->input('start_date'))->startOfDay();
|
||||||
|
$curEnd = Carbon::parse($request->input('end_date'))->endOfDay();
|
||||||
|
if ($curEnd->lt($curStart)) {
|
||||||
|
[$curStart, $curEnd] = [$curEnd, $curStart];
|
||||||
|
}
|
||||||
|
$days = max(1, (int) $curStart->diffInDays($curEnd) + 1);
|
||||||
|
} else {
|
||||||
|
$days = in_array((int) $request->input('days'), $allowedDays) ? (int) $request->input('days') : 30;
|
||||||
|
$curEnd = Carbon::today()->endOfDay();
|
||||||
|
$curStart = Carbon::today()->subDays($days - 1)->startOfDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
$prevEnd = $curStart->copy()->subSecond()->endOfDay();
|
||||||
|
$prevStart = $prevEnd->copy()->subDays($days - 1)->startOfDay();
|
||||||
|
|
||||||
|
$buildDates = function (Carbon $start, Carbon $end): array {
|
||||||
|
$dates = [];
|
||||||
|
for ($d = $start->copy(); $d->lte($end); $d->addDay()) {
|
||||||
|
$dates[] = $d->toDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dates;
|
||||||
|
};
|
||||||
|
|
||||||
|
$curDates = $buildDates($curStart, $curEnd);
|
||||||
|
$prevDates = $buildDates($prevStart, $prevEnd);
|
||||||
|
|
||||||
|
$pluckAction = function (string $actionType, Carbon $start, Carbon $end): array {
|
||||||
|
return Actionlog::where('action_type', $actionType)
|
||||||
|
->whereBetween('created_at', [$start, $end])
|
||||||
|
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
|
||||||
|
->groupBy('date')
|
||||||
|
->pluck('count', 'date')
|
||||||
|
->toArray();
|
||||||
|
};
|
||||||
|
|
||||||
|
// withTrashed() ensures records deleted after creation still appear in their creation-period counts.
|
||||||
|
$pluckCreated = function (string $modelClass, Carbon $start, Carbon $end): array {
|
||||||
|
return $modelClass::withTrashed()
|
||||||
|
->whereBetween('created_at', [$start, $end])
|
||||||
|
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
|
||||||
|
->groupBy('date')
|
||||||
|
->pluck('count', 'date')
|
||||||
|
->toArray();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Maintenance has no company_id column and no CompanyableTrait, so scope through
|
||||||
|
// its asset relationship — whereHas('asset') applies Asset's FMCS global scope.
|
||||||
|
$pluckMaintenances = function (Carbon $start, Carbon $end): array {
|
||||||
|
return Maintenance::withTrashed()
|
||||||
|
->whereHas('asset')
|
||||||
|
->whereBetween('maintenances.created_at', [$start, $end])
|
||||||
|
->selectRaw('DATE(maintenances.created_at) as date, COUNT(*) as count')
|
||||||
|
->groupBy('date')
|
||||||
|
->pluck('count', 'date')
|
||||||
|
->toArray();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filters by both action_type and item_type for per-category checkout/checkin counts.
|
||||||
|
$pluckActionByType = function (string $actionType, string $modelClass, Carbon $start, Carbon $end): array {
|
||||||
|
return Actionlog::where('action_type', $actionType)
|
||||||
|
->where('item_type', $modelClass)
|
||||||
|
->whereBetween('created_at', [$start, $end])
|
||||||
|
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
|
||||||
|
->groupBy('date')
|
||||||
|
->pluck('count', 'date')
|
||||||
|
->toArray();
|
||||||
|
};
|
||||||
|
|
||||||
|
$pluckDeletedUsers = function (Carbon $start, Carbon $end): array {
|
||||||
|
return User::withTrashed()
|
||||||
|
->whereNotNull('deleted_at')
|
||||||
|
->whereBetween('deleted_at', [$start, $end])
|
||||||
|
->selectRaw('DATE(deleted_at) as date, COUNT(*) as count')
|
||||||
|
->groupBy('date')
|
||||||
|
->pluck('count', 'date')
|
||||||
|
->toArray();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Catches both 'checkin' and 'checkin from' action types used across different item types.
|
||||||
|
$pluckCheckinsByType = function (string $modelClass, Carbon $start, Carbon $end): array {
|
||||||
|
return Actionlog::whereIn('action_type', ['checkin', 'checkin from'])
|
||||||
|
->where('item_type', $modelClass)
|
||||||
|
->whereBetween('created_at', [$start, $end])
|
||||||
|
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
|
||||||
|
->groupBy('date')
|
||||||
|
->pluck('count', 'date')
|
||||||
|
->toArray();
|
||||||
|
};
|
||||||
|
|
||||||
|
$fill = fn (array $raw, array $dates) => array_map(fn ($d) => (int) ($raw[$d] ?? 0), $dates);
|
||||||
|
|
||||||
|
$datasets = [];
|
||||||
|
foreach ([
|
||||||
|
'new_users' => fn ($s, $e) => $pluckCreated(User::class, $s, $e),
|
||||||
|
'deleted_users' => fn ($s, $e) => $pluckDeletedUsers($s, $e),
|
||||||
|
'asset_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', Asset::class, $s, $e),
|
||||||
|
'asset_checkins' => fn ($s, $e) => $pluckCheckinsByType(Asset::class, $s, $e),
|
||||||
|
'new_assets' => fn ($s, $e) => $pluckCreated(Asset::class, $s, $e),
|
||||||
|
'new_maintenances' => fn ($s, $e) => $pluckMaintenances($s, $e),
|
||||||
|
'new_audits' => fn ($s, $e) => $pluckAction('audit', $s, $e),
|
||||||
|
'component_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', Component::class, $s, $e),
|
||||||
|
'component_checkins' => fn ($s, $e) => $pluckCheckinsByType(Component::class, $s, $e),
|
||||||
|
'new_components' => fn ($s, $e) => $pluckCreated(Component::class, $s, $e),
|
||||||
|
'consumable_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', Consumable::class, $s, $e),
|
||||||
|
'consumable_checkins' => fn ($s, $e) => $pluckCheckinsByType(Consumable::class, $s, $e),
|
||||||
|
'new_consumables' => fn ($s, $e) => $pluckCreated(Consumable::class, $s, $e),
|
||||||
|
'license_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', LicenseSeat::class, $s, $e),
|
||||||
|
'license_checkins' => fn ($s, $e) => $pluckCheckinsByType(LicenseSeat::class, $s, $e),
|
||||||
|
'new_licenses' => fn ($s, $e) => $pluckCreated(License::class, $s, $e),
|
||||||
|
'accessory_checkouts' => fn ($s, $e) => $pluckActionByType('checkout', Accessory::class, $s, $e),
|
||||||
|
'accessory_checkins' => fn ($s, $e) => $pluckCheckinsByType(Accessory::class, $s, $e),
|
||||||
|
'new_accessories' => fn ($s, $e) => $pluckCreated(Accessory::class, $s, $e),
|
||||||
|
] as $key => $query) {
|
||||||
|
$datasets[$key] = $fill($query($curStart, $curEnd), $curDates);
|
||||||
|
$datasets['prev_'.$key] = $fill($query($prevStart, $prevEnd), $prevDates);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(array_merge([
|
||||||
|
'labels' => array_map(fn ($d) => Carbon::parse($d)->format('M j'), $curDates),
|
||||||
|
'prev_label' => $prevStart->format('M j').' – '.$prevEnd->format('M j'),
|
||||||
|
], $datasets));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class UploadedFilesController extends Controller
|
|||||||
$uploads = self::$map_object_type[$object_type]::withTrashed()->find($id)->uploads()
|
$uploads = self::$map_object_type[$object_type]::withTrashed()->find($id)->uploads()
|
||||||
->with('adminuser');
|
->with('adminuser');
|
||||||
|
|
||||||
$offset = ($request->input('offset') > $uploads->count()) ? $uploads->count() : abs($request->input('offset'));
|
$offset = ($request->input('offset') > $uploads->count()) ? $uploads->count() : app('api_offset_value');
|
||||||
$limit = app('api_limit_value');
|
$limit = app('api_limit_value');
|
||||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use App\Models\Asset;
|
|||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\Consumable;
|
use App\Models\Consumable;
|
||||||
use App\Models\License;
|
use App\Models\License;
|
||||||
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Notifications\CurrentInventory;
|
use App\Notifications\CurrentInventory;
|
||||||
use App\Notifications\WelcomeNotification;
|
use App\Notifications\WelcomeNotification;
|
||||||
@@ -51,7 +52,6 @@ class UsersController extends Controller
|
|||||||
'users.address',
|
'users.address',
|
||||||
'users.avatar',
|
'users.avatar',
|
||||||
'users.city',
|
'users.city',
|
||||||
'users.company_id',
|
|
||||||
'users.country',
|
'users.country',
|
||||||
'users.created_by',
|
'users.created_by',
|
||||||
'users.created_at',
|
'users.created_at',
|
||||||
@@ -89,7 +89,7 @@ class UsersController extends Controller
|
|||||||
])->with('manager')
|
])->with('manager')
|
||||||
->with('groups')
|
->with('groups')
|
||||||
->with('userloc')
|
->with('userloc')
|
||||||
->with('company')
|
->with('companies')
|
||||||
->with('department')
|
->with('department')
|
||||||
->with('createdBy')
|
->with('createdBy')
|
||||||
->withCount([
|
->withCount([
|
||||||
@@ -191,7 +191,7 @@ class UsersController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('company_id')) {
|
if ($request->filled('company_id')) {
|
||||||
$users = $users->where('users.company_id', '=', $request->input('company_id'));
|
$users = $users->whereHas('companies', fn ($q) => $q->where('companies.id', $request->input('company_id')));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('phone')) {
|
if ($request->filled('phone')) {
|
||||||
@@ -380,6 +380,8 @@ class UsersController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function selectlist(Request $request): array
|
public function selectlist(Request $request): array
|
||||||
{
|
{
|
||||||
|
$this->authorize('view.selectlists');
|
||||||
|
|
||||||
$users = User::select(
|
$users = User::select(
|
||||||
[
|
[
|
||||||
'users.id',
|
'users.id',
|
||||||
@@ -394,6 +396,22 @@ class UsersController extends Controller
|
|||||||
]
|
]
|
||||||
)->where('show_in_list', '=', '1');
|
)->where('show_in_list', '=', '1');
|
||||||
|
|
||||||
|
// When FMCS is enabled, automatically scope to companies the acting user belongs to.
|
||||||
|
// scopeCompanyables is a no-op for superusers and when FMCS is disabled.
|
||||||
|
$users = Company::scopeCompanyables($users, 'company_id', 'users');
|
||||||
|
|
||||||
|
// Allow further narrowing to a specific company passed via data-company-ids on the select.
|
||||||
|
if ((Setting::getSettings()->full_multiple_companies_support == '1') && $request->filled('companyId')) {
|
||||||
|
$companyIds = array_values(array_filter(array_map('intval', explode(',', $request->input('companyId')))));
|
||||||
|
if (! empty($companyIds)) {
|
||||||
|
$users = Company::scopeUsersByCompanyIds($users, $companyIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('excludeId')) {
|
||||||
|
$users->where('users.id', '!=', (int) $request->input('excludeId'));
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
$users = $users->where(function ($query) use ($request) {
|
$users = $users->where(function ($query) use ($request) {
|
||||||
$query->SimpleNameSearch($request->input('search'))
|
$query->SimpleNameSearch($request->input('search'))
|
||||||
@@ -441,7 +459,6 @@ class UsersController extends Controller
|
|||||||
$authenticatedUser = auth()->user();
|
$authenticatedUser = auth()->user();
|
||||||
$user = new User;
|
$user = new User;
|
||||||
$user->fill($request->all());
|
$user->fill($request->all());
|
||||||
$user->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
|
||||||
$user->created_by = auth()->id();
|
$user->created_by = auth()->id();
|
||||||
|
|
||||||
if ($request->has('permissions')) {
|
if ($request->has('permissions')) {
|
||||||
@@ -486,6 +503,12 @@ class UsersController extends Controller
|
|||||||
$user->groups()->sync($request->input('groups'));
|
$user->groups()->sync($request->input('groups'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync company memberships from company_ids[] or fall back to scalar company_id
|
||||||
|
$companyIds = array_filter(
|
||||||
|
(array) ($request->input('company_ids') ?? ($request->filled('company_id') ? [$request->input('company_id')] : []))
|
||||||
|
);
|
||||||
|
$user->syncCompaniesWithLogging(Company::getIdsForCurrentUser(array_map('intval', $companyIds)));
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.create')));
|
return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.create')));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,15 +592,12 @@ class UsersController extends Controller
|
|||||||
requestedPermissions: NormalizePermissionsPayloadAction::run($request->input('permissions')),
|
requestedPermissions: NormalizePermissionsPayloadAction::run($request->input('permissions')),
|
||||||
authenticatedUser: $authenticatedUser,
|
authenticatedUser: $authenticatedUser,
|
||||||
originalPermissions: NormalizePermissionsPayloadAction::run($user->decodePermissions()),
|
originalPermissions: NormalizePermissionsPayloadAction::run($user->decodePermissions()),
|
||||||
|
targetUser: $user,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('company_id')) {
|
|
||||||
$user->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($user->id == $request->input('manager_id')) {
|
if ($user->id == $request->input('manager_id')) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
|
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
|
||||||
}
|
}
|
||||||
@@ -606,6 +626,18 @@ class UsersController extends Controller
|
|||||||
$user->groups()->sync($request->input('groups'));
|
$user->groups()->sync($request->input('groups'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// company_ids (new format) = full replacement sync.
|
||||||
|
// Legacy company_id = add without removing other associations.
|
||||||
|
if ($request->has('company_ids')) {
|
||||||
|
$companyIds = array_filter(array_map('intval', (array) $request->input('company_ids')));
|
||||||
|
$user->syncCompaniesWithLogging(Company::getIdsForCurrentUser($companyIds));
|
||||||
|
} elseif ($request->filled('company_id')) {
|
||||||
|
$filtered = Company::getIdsForCurrentUser([(int) $request->input('company_id')]);
|
||||||
|
if (! empty($filtered)) {
|
||||||
|
$user->companies()->syncWithoutDetaching($filtered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.update')));
|
return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.update')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class AssetCheckinController extends Controller
|
|||||||
public function store(AssetCheckinRequest $request, $assetId = null, $backto = null): RedirectResponse
|
public function store(AssetCheckinRequest $request, $assetId = null, $backto = null): RedirectResponse
|
||||||
{
|
{
|
||||||
// Check if the asset exists
|
// Check if the asset exists
|
||||||
if (is_null($asset = Asset::find($assetId))) {
|
if (is_null($asset = Asset::withTrashed()->find($assetId))) {
|
||||||
// Redirect to the asset management page with error
|
// Redirect to the asset management page with error
|
||||||
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
|
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
|
||||||
}
|
}
|
||||||
@@ -135,12 +135,16 @@ class AssetCheckinController extends Controller
|
|||||||
|
|
||||||
$asset->location_id = $asset->rtd_location_id;
|
$asset->location_id = $asset->rtd_location_id;
|
||||||
|
|
||||||
if ($request->filled('location_id')) {
|
if ($request->has('location_id')) {
|
||||||
Log::debug('NEW Location ID: '.$request->input('location_id'));
|
if ($request->filled('location_id')) {
|
||||||
$asset->location_id = $request->input('location_id');
|
Log::debug('NEW Location ID: '.$request->input('location_id'));
|
||||||
|
$asset->location_id = $request->input('location_id');
|
||||||
if ($request->input('update_default_location') == 0) {
|
if ($request->input('update_default_location') == 0) {
|
||||||
$asset->rtd_location_id = $request->input('location_id');
|
$asset->rtd_location_id = $request->input('location_id');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Explicitly submitted as empty — clear the location
|
||||||
|
$asset->location_id = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ namespace App\Http\Controllers\Assets;
|
|||||||
|
|
||||||
use App\Exceptions\CheckoutNotAllowed;
|
use App\Exceptions\CheckoutNotAllowed;
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
use App\Http\Controllers\CheckInOutRequest;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\AssetCheckoutRequest;
|
use App\Http\Requests\AssetCheckoutRequest;
|
||||||
|
use App\Http\Traits\CheckInOutTrait;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\CheckoutAcceptance;
|
use App\Models\CheckoutAcceptance;
|
||||||
use App\Models\Setting;
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
@@ -17,7 +16,7 @@ use Illuminate\Http\RedirectResponse;
|
|||||||
|
|
||||||
class AssetCheckoutController extends Controller
|
class AssetCheckoutController extends Controller
|
||||||
{
|
{
|
||||||
use CheckInOutRequest;
|
use CheckInOutTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a view that presents a form to check an asset out to a
|
* Returns a view that presents a form to check an asset out to a
|
||||||
@@ -119,13 +118,18 @@ class AssetCheckoutController extends Controller
|
|||||||
// Add any custom fields that should be included in the checkout
|
// Add any custom fields that should be included in the checkout
|
||||||
$asset->customFieldsForCheckinCheckout('display_checkout');
|
$asset->customFieldsForCheckinCheckout('display_checkout');
|
||||||
|
|
||||||
$settings = Setting::getSettings();
|
if (! $asset->canCheckoutTo($target)) {
|
||||||
|
$targetType = match (class_basename($target)) {
|
||||||
|
'User' => trans('general.user'),
|
||||||
|
'Location' => trans('general.location'),
|
||||||
|
default => trans('general.asset'),
|
||||||
|
};
|
||||||
|
|
||||||
// We have to check whether $target->company_id is null here since locations don't have a company yet
|
return redirect()->route('hardware.checkout.create', $asset)->with('error', trans('general.error_checkout_company_mismatch', [
|
||||||
if (($settings->full_multiple_companies_support) && ((! is_null($target->company_id)) && (! is_null($asset->company_id)))) {
|
'item' => trans('general.asset').' "'.$asset->display_name.'"',
|
||||||
if ($target->company_id != $asset->company_id) {
|
'item_company' => $asset->company?->name ?? trans('general.unassigned'),
|
||||||
return redirect()->route('hardware.checkout.create', $asset)->with('error', trans('general.error_user_company'));
|
'target' => $targetType.' "'.($target->name ?? $target->username ?? $target->id).'"',
|
||||||
}
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
session()->put([
|
session()->put([
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ class AssetsController extends Controller
|
|||||||
|
|
||||||
$qr_code = (object) [
|
$qr_code = (object) [
|
||||||
'display' => $settings->qr_code == '1',
|
'display' => $settings->qr_code == '1',
|
||||||
'url' => route('qr_code/hardware', $asset),
|
'url' => route('qr_code/common', ['object_type' => 'hardware', 'id' => $asset->id]),
|
||||||
];
|
];
|
||||||
|
|
||||||
$total_maintenance_cost = $asset->maintenances?->sum('cost');
|
$total_maintenance_cost = $asset->maintenances?->sum('cost');
|
||||||
@@ -443,7 +443,7 @@ class AssetsController extends Controller
|
|||||||
|
|
||||||
if ($request->filled('image_delete')) {
|
if ($request->filled('image_delete')) {
|
||||||
try {
|
try {
|
||||||
unlink(public_path().'/uploads/assets/'.$asset->image);
|
unlink(public_path().'/uploads/assets/'.basename($asset->image));
|
||||||
$asset->image = '';
|
$asset->image = '';
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::info($e);
|
Log::info($e);
|
||||||
@@ -511,7 +511,7 @@ class AssetsController extends Controller
|
|||||||
|
|
||||||
// Validate required serial based on model setting
|
// Validate required serial based on model setting
|
||||||
if ($model && $model->require_serial === 1 && empty($serial[1])) {
|
if ($model && $model->require_serial === 1 && empty($serial[1])) {
|
||||||
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
|
return Helper::getRedirectOption($request, $asset->id, 'Assets')
|
||||||
->with('warning', trans('admin/hardware/form.serial_required_post_model_update', [
|
->with('warning', trans('admin/hardware/form.serial_required_post_model_update', [
|
||||||
'asset_model' => $model->name,
|
'asset_model' => $model->name,
|
||||||
]));
|
]));
|
||||||
@@ -549,7 +549,7 @@ class AssetsController extends Controller
|
|||||||
|
|
||||||
if ($asset->image) {
|
if ($asset->image) {
|
||||||
try {
|
try {
|
||||||
Storage::disk('public')->delete('assets'.'/'.$asset->image);
|
Storage::disk('public')->delete('assets/'.basename($asset->image));
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::debug($e);
|
Log::debug($e);
|
||||||
}
|
}
|
||||||
@@ -567,11 +567,12 @@ class AssetsController extends Controller
|
|||||||
*
|
*
|
||||||
* @since [v3.0]
|
* @since [v3.0]
|
||||||
*/
|
*/
|
||||||
public function getAssetBySerial(Request $request): RedirectResponse
|
public function getAssetBySerial(Request $request, $serial = null): RedirectResponse
|
||||||
{
|
{
|
||||||
|
$serial = $serial ?: $request->input('serial');
|
||||||
$topsearch = ($request->input('topsearch') == 'true');
|
$topsearch = ($request->input('topsearch') == 'true');
|
||||||
|
|
||||||
if (! $asset = Asset::where('serial', '=', $request->input('serial'))->first()) {
|
if (! $asset = Asset::where('serial', '=', $serial)->first()) {
|
||||||
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
|
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
|
||||||
}
|
}
|
||||||
$this->authorize('view', $asset);
|
$this->authorize('view', $asset);
|
||||||
|
|||||||
@@ -2,20 +2,25 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Assets;
|
namespace App\Http\Controllers\Assets;
|
||||||
|
|
||||||
|
use App\Events\CheckoutableCheckedIn;
|
||||||
use App\Events\CheckoutablesCheckedOutInBulk;
|
use App\Events\CheckoutablesCheckedOutInBulk;
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
use App\Http\Controllers\CheckInOutRequest;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\AssetCheckoutRequest;
|
use App\Http\Requests\AssetCheckoutRequest;
|
||||||
|
use App\Http\Traits\CheckInOutTrait;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\AssetModel;
|
use App\Models\AssetModel;
|
||||||
|
use App\Models\CheckoutAcceptance;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\CustomField;
|
use App\Models\CustomField;
|
||||||
|
use App\Models\LicenseSeat;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\Statuslabel;
|
use App\Models\Statuslabel;
|
||||||
|
use App\Models\User;
|
||||||
use App\View\Label;
|
use App\View\Label;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -27,7 +32,7 @@ use Illuminate\Support\Facades\Log;
|
|||||||
|
|
||||||
class BulkAssetsController extends Controller
|
class BulkAssetsController extends Controller
|
||||||
{
|
{
|
||||||
use CheckInOutRequest;
|
use CheckInOutTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the bulk edit page.
|
* Display the bulk edit page.
|
||||||
@@ -73,6 +78,16 @@ class BulkAssetsController extends Controller
|
|||||||
return redirect()->route('hardware.bulkcheckout.show');
|
return redirect()->route('hardware.bulkcheckout.show');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($request->input('bulk_actions') === 'checkin') {
|
||||||
|
$referer = request()->headers->get('referer');
|
||||||
|
if ($referer && parse_url($referer, PHP_URL_HOST) === parse_url(config('app.url'), PHP_URL_HOST)) {
|
||||||
|
redirect()->setIntendedUrl($referer);
|
||||||
|
}
|
||||||
|
$request->session()->flashInput(['selected_assets' => $asset_ids]);
|
||||||
|
|
||||||
|
return redirect()->route('hardware.bulkcheckin.show');
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->input('bulk_actions') === 'maintenance') {
|
if ($request->input('bulk_actions') === 'maintenance') {
|
||||||
$request->session()->flashInput(['selected_assets' => $asset_ids]);
|
$request->session()->flashInput(['selected_assets' => $asset_ids]);
|
||||||
|
|
||||||
@@ -673,18 +688,25 @@ class BulkAssetsController extends Controller
|
|||||||
->with('error', trans('general.error_assets_already_checked_out'));
|
->with('error', trans('general.error_assets_already_checked_out'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent checking out assets across companies if FMCS enabled
|
// Prevent checking out assets across companies if FMCS enabled.
|
||||||
if (Setting::getSettings()->full_multiple_companies_support && $target->company_id) {
|
if (Setting::getSettings()->full_multiple_companies_support) {
|
||||||
$company_ids = $assets->pluck('company_id')->unique();
|
$company_ids = $assets->pluck('company_id')->filter()->unique();
|
||||||
|
|
||||||
// if there is more than one unique company id or the singular company id does not match
|
if ($company_ids->isNotEmpty()) {
|
||||||
// then the checkout is invalid
|
if ($company_ids->count() > 1) {
|
||||||
if ($company_ids->count() > 1 || $company_ids->first() != $target->company_id) {
|
// Selected assets span multiple companies; bulk checkout can't satisfy all of them.
|
||||||
// re-add the asset ids so the assets select is re-populated
|
$mismatch = true;
|
||||||
$request->session()->flashInput(['selected_assets' => $asset_ids]);
|
} else {
|
||||||
|
// All assets share the same company; let the model enforce the checkout rules.
|
||||||
|
$mismatch = ! $assets->first()->canCheckoutTo($target);
|
||||||
|
}
|
||||||
|
|
||||||
return redirect(route('hardware.bulkcheckout.show'))
|
if ($mismatch) {
|
||||||
->with('error', trans('general.error_user_company_multiple'));
|
$request->session()->flashInput(['selected_assets' => $asset_ids]);
|
||||||
|
|
||||||
|
return redirect(route('hardware.bulkcheckout.show'))
|
||||||
|
->with('error', trans('general.error_user_company_multiple'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,6 +781,112 @@ class BulkAssetsController extends Controller
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show Bulk Checkin Page
|
||||||
|
*/
|
||||||
|
public function showCheckin(): View
|
||||||
|
{
|
||||||
|
$this->authorize('checkin', Asset::class);
|
||||||
|
|
||||||
|
$notAssigned = collect();
|
||||||
|
|
||||||
|
if (old('selected_assets') && is_array(old('selected_assets'))) {
|
||||||
|
$assets = Asset::withTrashed()->findMany(old('selected_assets'));
|
||||||
|
|
||||||
|
[$assigned, $notAssigned] = $assets->partition(function (Asset $asset) {
|
||||||
|
return $asset->assigned_to;
|
||||||
|
});
|
||||||
|
|
||||||
|
session()->flashInput(['selected_assets' => $assigned->pluck('id')->values()->toArray()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$do_not_change = ['' => trans('general.do_not_change')];
|
||||||
|
$status_label_list = $do_not_change + Helper::statusLabelList();
|
||||||
|
|
||||||
|
return view('hardware/bulk-checkin', [
|
||||||
|
'statusLabel_list' => $status_label_list,
|
||||||
|
'removed_assets' => $notAssigned,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process Multiple Checkin Request
|
||||||
|
*/
|
||||||
|
public function storeCheckin(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorize('checkin', Asset::class);
|
||||||
|
|
||||||
|
if (! is_array($request->input('selected_assets'))) {
|
||||||
|
return redirect()->route('hardware.bulkcheckin.show')->withInput()->with('error', trans('admin/hardware/message.multi-checkin.no_assets_selected'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$asset_ids = array_filter($request->input('selected_assets'));
|
||||||
|
|
||||||
|
$assets = Asset::withTrashed()->findOrFail($asset_ids);
|
||||||
|
|
||||||
|
$checkin_at = date('Y-m-d H:i:s');
|
||||||
|
if ($request->filled('checkin_at') && $request->input('checkin_at') != date('Y-m-d')) {
|
||||||
|
$checkin_at = $request->input('checkin_at');
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$admin = auth()->user();
|
||||||
|
|
||||||
|
DB::transaction(function () use ($assets, $admin, $checkin_at, $request, &$errors) {
|
||||||
|
foreach ($assets as $asset) {
|
||||||
|
$this->authorize('checkin', $asset);
|
||||||
|
|
||||||
|
if (is_null($asset->assignedTo)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$target = $asset->assignedTo;
|
||||||
|
$originalValues = $asset->getRawOriginal();
|
||||||
|
|
||||||
|
$asset->expected_checkin = null;
|
||||||
|
$asset->assignedTo()->disassociate($asset);
|
||||||
|
$asset->accepted = null;
|
||||||
|
|
||||||
|
if ($request->filled('status_id')) {
|
||||||
|
$asset->status_id = $request->input('status_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
$asset->location_id = $asset->rtd_location_id;
|
||||||
|
$asset->last_checkin = $checkin_at;
|
||||||
|
|
||||||
|
if ($request->boolean('checkin_licenses')) {
|
||||||
|
$asset->licenseseats->each(function (LicenseSeat $seat) {
|
||||||
|
$seat->update(['assigned_to' => null]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckoutAcceptance::pending()->whereHasMorph('checkoutable', [Asset::class], function (Builder $query) use ($asset) {
|
||||||
|
$query->where('id', $asset->id);
|
||||||
|
})->get()->each->delete();
|
||||||
|
|
||||||
|
if ($asset->save()) {
|
||||||
|
if ($request->boolean('checkin_child_assets')) {
|
||||||
|
Asset::where('assigned_type', Asset::class)
|
||||||
|
->where('assigned_to', $asset->id)
|
||||||
|
->update(['location_id' => $asset->location_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
event(new CheckoutableCheckedIn($asset, $target, $admin, $request->input('note'), $checkin_at, $originalValues));
|
||||||
|
} else {
|
||||||
|
$errors = array_merge_recursive($errors, $asset->getErrors()->toArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (! $errors) {
|
||||||
|
return redirect()->intended(route('hardware.index'))->with('success', trans_choice('admin/hardware/message.multi-checkin.success', count($asset_ids)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('hardware.bulkcheckin.show')->withInput()
|
||||||
|
->with('error', trans_choice('admin/hardware/message.multi-checkin.error', count($asset_ids)))
|
||||||
|
->withErrors($errors);
|
||||||
|
}
|
||||||
|
|
||||||
public function restore(Request $request): RedirectResponse
|
public function restore(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->authorize('update', Asset::class);
|
$this->authorize('update', Asset::class);
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class SamlController extends Controller
|
|||||||
{
|
{
|
||||||
$auth = $this->saml->getAuth();
|
$auth = $this->saml->getAuth();
|
||||||
$ssoUrl = $auth->login(session()->get('url.intended'), [], false, false, false, false);
|
$ssoUrl = $auth->login(session()->get('url.intended'), [], false, false, false, false);
|
||||||
|
|
||||||
return redirect()->away($ssoUrl);
|
return redirect()->away($ssoUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ class SamlController extends Controller
|
|||||||
$saml = $this->saml;
|
$saml = $this->saml;
|
||||||
$auth = $saml->getAuth();
|
$auth = $saml->getAuth();
|
||||||
$saml_exception = false;
|
$saml_exception = false;
|
||||||
session()->put('url.intended', $request->post('RelayState'));
|
session()->put('url.intended', str_replace(["\r", "\n"], '', $request->post('RelayState')));
|
||||||
try {
|
try {
|
||||||
$auth->processResponse();
|
$auth->processResponse();
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ class ComponentCheckinController extends Controller
|
|||||||
}
|
}
|
||||||
$this->authorize('checkin', $component);
|
$this->authorize('checkin', $component);
|
||||||
|
|
||||||
return view('components/checkin', compact('component_assets', 'component', 'asset'));
|
return view('components/checkin', compact('component_assets', 'component', 'asset'))
|
||||||
|
->with('snipe_component', $component);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->route('components.index')->with('error', trans('admin/components/messages.not_found'));
|
return redirect()->route('components.index')->with('error', trans('admin/components/messages.not_found'));
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use App\Helpers\Helper;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\Component;
|
use App\Models\Component;
|
||||||
use App\Models\Setting;
|
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
@@ -46,7 +45,8 @@ class ComponentCheckoutController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return the checkout view
|
// Return the checkout view
|
||||||
return view('components/checkout', compact('component'));
|
return view('components/checkout', compact('component'))
|
||||||
|
->with('snipe_component', $component);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalid category
|
// Invalid category
|
||||||
@@ -103,8 +103,12 @@ class ComponentCheckoutController extends Controller
|
|||||||
// Check if the asset exists
|
// Check if the asset exists
|
||||||
$asset = Asset::find($request->input('asset_id'));
|
$asset = Asset::find($request->input('asset_id'));
|
||||||
|
|
||||||
if ((Setting::getSettings()->full_multiple_companies_support) && $component->company_id !== $asset->company_id) {
|
if (! $component->canCheckoutTo($asset)) {
|
||||||
return redirect()->route('components.checkout.show', $componentId)->with('error', trans('general.error_user_company'));
|
return redirect()->route('components.checkout.show', $componentId)->with('error', trans('general.error_checkout_company_mismatch', [
|
||||||
|
'item' => trans('general.component').' "'.$component->name.'"',
|
||||||
|
'item_company' => $component->company?->name ?? trans('general.unassigned'),
|
||||||
|
'target' => trans('general.asset').' "'.$asset->display_name.'"',
|
||||||
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
$component->checkout_qty = $request->input('assigned_qty');
|
$component->checkout_qty = $request->input('assigned_qty');
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ namespace App\Http\Controllers\Components;
|
|||||||
|
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\ImageUploadRequest;
|
use App\Http\Requests\StoreComponentRequest;
|
||||||
|
use App\Http\Requests\UpdateComponentRequest;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\Component;
|
use App\Models\Component;
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
@@ -12,7 +13,6 @@ use Illuminate\Contracts\View\View;
|
|||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Facades\Validator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class controls all actions related to Components for
|
* This class controls all actions related to Components for
|
||||||
@@ -74,7 +74,7 @@ class ComponentsController extends Controller
|
|||||||
*
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*/
|
*/
|
||||||
public function store(ImageUploadRequest $request)
|
public function store(StoreComponentRequest $request)
|
||||||
{
|
{
|
||||||
$this->authorize('create', Component::class);
|
$this->authorize('create', Component::class);
|
||||||
$component = new Component;
|
$component = new Component;
|
||||||
@@ -148,21 +148,10 @@ class ComponentsController extends Controller
|
|||||||
*
|
*
|
||||||
* @since [v3.0]
|
* @since [v3.0]
|
||||||
*/
|
*/
|
||||||
public function update(ImageUploadRequest $request, Component $component)
|
public function update(UpdateComponentRequest $request, Component $component)
|
||||||
{
|
{
|
||||||
$min = $component->numCheckedOut();
|
|
||||||
$validator = Validator::make($request->all(), [
|
|
||||||
'qty' => "required|numeric|min:$min",
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($validator->fails()) {
|
|
||||||
return redirect()->back()
|
|
||||||
->withErrors($validator)
|
|
||||||
->withInput();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->authorize('update', $component);
|
$this->authorize('update', $component);
|
||||||
|
|
||||||
// Update the component data
|
// Update the component data
|
||||||
$component->name = $request->input('name');
|
$component->name = $request->input('name');
|
||||||
$component->category_id = $request->input('category_id');
|
$component->category_id = $request->input('category_id');
|
||||||
|
|||||||
@@ -96,6 +96,14 @@ class ConsumableCheckoutController extends Controller
|
|||||||
return redirect()->route('consumables.checkout.show', $consumable)->with('error', trans('admin/consumables/message.checkout.user_does_not_exist'))->withInput();
|
return redirect()->route('consumables.checkout.show', $consumable)->with('error', trans('admin/consumables/message.checkout.user_does_not_exist'))->withInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! $consumable->canCheckoutTo($user)) {
|
||||||
|
return redirect()->back()->with('error', trans('general.error_checkout_company_mismatch', [
|
||||||
|
'item' => trans('general.consumable').' "'.$consumable->name.'"',
|
||||||
|
'item_company' => $consumable->company?->name ?? trans('general.unassigned'),
|
||||||
|
'target' => trans('general.user').' "'.$user->username.'"',
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
// Update the consumable data
|
// Update the consumable data
|
||||||
$consumable->assigned_to = e($request->input('assigned_to'));
|
$consumable->assigned_to = e($request->input('assigned_to'));
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class GoogleAuthController extends Controller
|
|||||||
Log::debug('Google user '.$socialUser->getEmail().' found in Snipe-IT');
|
Log::debug('Google user '.$socialUser->getEmail().' found in Snipe-IT');
|
||||||
$user->update([
|
$user->update([
|
||||||
'avatar' => $socialUser->avatar,
|
'avatar' => $socialUser->avatar,
|
||||||
|
'last_login' => \Carbon::now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Auth::login($user, true);
|
Auth::login($user, true);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Kits;
|
namespace App\Http\Controllers\Kits;
|
||||||
|
|
||||||
use App\Http\Controllers\CheckInOutRequest;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Traits\CheckInOutTrait;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\PredefinedKit;
|
use App\Models\PredefinedKit;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
@@ -23,7 +23,7 @@ class CheckoutKitController extends Controller
|
|||||||
{
|
{
|
||||||
public $kitService;
|
public $kitService;
|
||||||
|
|
||||||
use CheckInOutRequest;
|
use CheckInOutTrait;
|
||||||
|
|
||||||
public function __construct(PredefinedKitCheckoutService $kitService)
|
public function __construct(PredefinedKitCheckoutService $kitService)
|
||||||
{
|
{
|
||||||
@@ -53,6 +53,8 @@ class CheckoutKitController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function store(Request $request, $kit_id)
|
public function store(Request $request, $kit_id)
|
||||||
{
|
{
|
||||||
|
$this->authorize('checkout', Asset::class);
|
||||||
|
|
||||||
$user_id = e($request->input('user_id'));
|
$user_id = e($request->input('user_id'));
|
||||||
if (is_null($user = User::find($user_id))) {
|
if (is_null($user = User::find($user_id))) {
|
||||||
return redirect()->back()->with('error', trans('admin/users/message.user_not_found'));
|
return redirect()->back()->with('error', trans('admin/users/message.user_not_found'));
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Licenses;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\License;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
|
||||||
|
class BulkLicensesController extends Controller
|
||||||
|
{
|
||||||
|
public function destroy(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorize('delete', License::class);
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$success_count = 0;
|
||||||
|
|
||||||
|
foreach ($request->input('ids', []) as $id) {
|
||||||
|
$license = License::find($id);
|
||||||
|
|
||||||
|
if (is_null($license)) {
|
||||||
|
$errors[] = trans('admin/licenses/message.does_not_exist');
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Gate::allows('delete', $license)) {
|
||||||
|
$errors[] = trans('general.insufficient_permissions');
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($license->assigned_seats_count > 0) {
|
||||||
|
$errors[] = trans('admin/licenses/message.delete.bulk_checkout_warning', ['license_name' => $license->name]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since assigned_seats_count == 0, all seats already have assigned_to and asset_id as null,
|
||||||
|
// so this update is effectively a no-op. It mirrors the single destroy() and is kept as a
|
||||||
|
// safety net. Bypassing Eloquent events here is intentional and safe — there is nothing
|
||||||
|
// assigned to trigger events on. Prior checkout/checkin history is preserved in action_log
|
||||||
|
// (keyed by LicenseSeat item_type/item_id) and remains accessible even after soft-delete.
|
||||||
|
DB::table('license_seats')
|
||||||
|
->where('license_id', $license->id)
|
||||||
|
->update(['assigned_to' => null, 'asset_id' => null]);
|
||||||
|
|
||||||
|
$license->licenseseats()->delete();
|
||||||
|
$license->delete();
|
||||||
|
$success_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($errors) > 0) {
|
||||||
|
if ($success_count > 0) {
|
||||||
|
return redirect()->route('licenses.index')
|
||||||
|
->with('success', trans_choice('admin/licenses/message.delete.partial_success', $success_count, ['count' => $success_count]))
|
||||||
|
->with('multi_error_messages', $errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('licenses.index')->with('multi_error_messages', $errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('licenses.index')->with('success', trans('admin/licenses/message.delete.bulk_success'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ use Illuminate\Auth\Access\AuthorizationException;
|
|||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ class LicenseCheckinController extends Controller
|
|||||||
{
|
{
|
||||||
// Check if the asset exists
|
// Check if the asset exists
|
||||||
$license = License::find($licenseSeat->license_id);
|
$license = License::find($licenseSeat->license_id);
|
||||||
$this->authorize('checkout', $license);
|
$this->authorize('checkin', $license);
|
||||||
|
|
||||||
return view('licenses/checkin', compact('licenseSeat'))->with('backto', $backTo);
|
return view('licenses/checkin', compact('licenseSeat'))->with('backto', $backTo);
|
||||||
}
|
}
|
||||||
@@ -69,7 +70,7 @@ class LicenseCheckinController extends Controller
|
|||||||
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkin.error'));
|
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkin.error'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->authorize('checkout', $license);
|
$this->authorize('checkin', $license);
|
||||||
|
|
||||||
// Declare the rules for the form validation
|
// Declare the rules for the form validation
|
||||||
$rules = [
|
$rules = [
|
||||||
@@ -127,10 +128,45 @@ class LicenseCheckinController extends Controller
|
|||||||
* @see LicenseCheckinController::create() method that provides the form view
|
* @see LicenseCheckinController::create() method that provides the form view
|
||||||
* @since [v6.1.1]
|
* @since [v6.1.1]
|
||||||
*
|
*
|
||||||
* @return RedirectResponse
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*/
|
*/
|
||||||
|
public function bulkCheckinSelected(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorize('checkin', License::class);
|
||||||
|
|
||||||
|
$seatIds = $request->input('ids', []);
|
||||||
|
|
||||||
|
if (empty($seatIds)) {
|
||||||
|
return redirect()->back()->with('warning', trans('admin/licenses/general.bulk.checkin_selected.no_seats_selected'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$seats = LicenseSeat::whereIn('id', $seatIds)
|
||||||
|
->where(function ($query) {
|
||||||
|
$query->whereNotNull('assigned_to')->orWhereNotNull('asset_id');
|
||||||
|
})
|
||||||
|
->with('license', 'user', 'asset')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$count = 0;
|
||||||
|
foreach ($seats as $seat) {
|
||||||
|
if (! $seat->license || ! Gate::allows('checkin', $seat->license)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$target = $seat->user ?? $seat->asset;
|
||||||
|
$seat->assigned_to = null;
|
||||||
|
$seat->asset_id = null;
|
||||||
|
if (! $seat->license->reassignable) {
|
||||||
|
$seat->unreassignable_seat = true;
|
||||||
|
}
|
||||||
|
if ($seat->save()) {
|
||||||
|
event(new CheckoutableCheckedIn($seat, $target, auth()->user(), null));
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', trans_choice('admin/licenses/general.bulk.checkin_selected.success', $count, ['count' => $count]));
|
||||||
|
}
|
||||||
|
|
||||||
public function bulkCheckin(Request $request, $licenseId)
|
public function bulkCheckin(Request $request, $licenseId)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ use App\Models\Asset;
|
|||||||
use App\Models\CheckoutAcceptance;
|
use App\Models\CheckoutAcceptance;
|
||||||
use App\Models\License;
|
use App\Models\License;
|
||||||
use App\Models\LicenseSeat;
|
use App\Models\LicenseSeat;
|
||||||
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class LicenseCheckoutController extends Controller
|
class LicenseCheckoutController extends Controller
|
||||||
@@ -94,23 +96,53 @@ class LicenseCheckoutController extends Controller
|
|||||||
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.license_is_inactive'));
|
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.license_is_inactive'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$licenseSeat = $this->findLicenseSeatToCheckout($license, $seatId);
|
if (Setting::getSettings()->full_multiple_companies_support == '1') {
|
||||||
$licenseSeat->created_by = auth()->id();
|
if ($request->filled('asset_id')) {
|
||||||
$licenseSeat->notes = $request->input('notes');
|
$fmcsTarget = Asset::find($request->input('asset_id'));
|
||||||
|
if ($fmcsTarget && ! $license->canCheckoutTo($fmcsTarget)) {
|
||||||
|
return redirect()->route('licenses.index')->with('error', trans('general.error_checkout_company_mismatch', [
|
||||||
|
'item' => trans('general.license').' "'.$license->name.'"',
|
||||||
|
'item_company' => $license->company?->name ?? trans('general.unassigned'),
|
||||||
|
'target' => trans('general.asset').' "'.$fmcsTarget->display_name.'"',
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
} elseif ($request->filled('assigned_to')) {
|
||||||
|
$fmcsTarget = User::find($request->input('assigned_to'));
|
||||||
|
if ($fmcsTarget && ! $license->canCheckoutTo($fmcsTarget)) {
|
||||||
|
return redirect()->route('licenses.index')->with('error', trans('general.error_checkout_company_mismatch', [
|
||||||
|
'item' => trans('general.license').' "'.$license->name.'"',
|
||||||
|
'item_company' => $license->company?->name ?? trans('general.unassigned'),
|
||||||
|
'target' => trans('general.user').' "'.$fmcsTarget->username.'"',
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$licenseSeat = null;
|
||||||
|
$checkoutTarget = null;
|
||||||
|
|
||||||
|
DB::transaction(function () use ($request, $license, $seatId, &$licenseSeat, &$checkoutTarget): void {
|
||||||
|
$licenseSeat = $this->findLicenseSeatToCheckout($license, $seatId, lock: true);
|
||||||
|
$licenseSeat->created_by = auth()->id();
|
||||||
|
$licenseSeat->notes = $request->input('notes');
|
||||||
|
|
||||||
|
if ($request->filled('asset_id')) {
|
||||||
|
$checkoutTarget = $this->checkoutToAsset($licenseSeat);
|
||||||
|
} elseif ($request->filled('assigned_to')) {
|
||||||
|
$checkoutTarget = $this->checkoutToUser($licenseSeat);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if ($request->filled('asset_id')) {
|
if ($request->filled('asset_id')) {
|
||||||
session()->put(['checkout_to_type' => 'asset']);
|
session()->put(['checkout_to_type' => 'asset']);
|
||||||
$checkoutTarget = $this->checkoutToAsset($licenseSeat);
|
|
||||||
$request->request->add(['assigned_asset' => $checkoutTarget->id]);
|
$request->request->add(['assigned_asset' => $checkoutTarget->id]);
|
||||||
session()->put([
|
session()->put([
|
||||||
'redirect_option' => $request->input('redirect_option'),
|
'redirect_option' => $request->input('redirect_option'),
|
||||||
'checkout_to_type' => 'asset',
|
'checkout_to_type' => 'asset',
|
||||||
'sign_in_place' => $request->boolean('sign_in_place'),
|
'sign_in_place' => $request->boolean('sign_in_place'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} elseif ($request->filled('assigned_to')) {
|
} elseif ($request->filled('assigned_to')) {
|
||||||
session()->put(['checkout_to_type' => 'user']);
|
session()->put(['checkout_to_type' => 'user']);
|
||||||
$checkoutTarget = $this->checkoutToUser($licenseSeat);
|
|
||||||
$request->request->add(['assigned_user' => $checkoutTarget->id]);
|
$request->request->add(['assigned_user' => $checkoutTarget->id]);
|
||||||
session()->put([
|
session()->put([
|
||||||
'redirect_option' => $request->input('redirect_option'),
|
'redirect_option' => $request->input('redirect_option'),
|
||||||
@@ -156,9 +188,11 @@ class LicenseCheckoutController extends Controller
|
|||||||
return redirect()->route('licenses.index')->with('error', trans('Something went wrong handling this checkout.'));
|
return redirect()->route('licenses.index')->with('error', trans('Something went wrong handling this checkout.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function findLicenseSeatToCheckout($license, $seatId)
|
protected function findLicenseSeatToCheckout($license, $seatId, bool $lock = false)
|
||||||
{
|
{
|
||||||
$licenseSeat = LicenseSeat::find($seatId) ?? $license->freeSeat();
|
$licenseSeat = $seatId
|
||||||
|
? LicenseSeat::where('id', $seatId)->when($lock, fn ($q) => $q->lockForUpdate())->first()
|
||||||
|
: $license->freeSeat(lock: $lock);
|
||||||
|
|
||||||
if (! $licenseSeat) {
|
if (! $licenseSeat) {
|
||||||
if ($seatId) {
|
if ($seatId) {
|
||||||
@@ -229,14 +263,10 @@ class LicenseCheckoutController extends Controller
|
|||||||
|
|
||||||
Log::debug('Checking out '.$licenseId.' via bulk');
|
Log::debug('Checking out '.$licenseId.' via bulk');
|
||||||
$license = License::findOrFail($licenseId);
|
$license = License::findOrFail($licenseId);
|
||||||
$this->authorize('checkin', $license);
|
$this->authorize('checkout', $license);
|
||||||
$avail_count = $license->getAvailSeatsCountAttribute();
|
|
||||||
|
|
||||||
$users = User::whereNull('deleted_at')->where('autoassign_licenses', '=', 1)->with('licenses')->get();
|
if ($license->isInactive()) {
|
||||||
Log::debug($avail_count.' will be assigned');
|
return redirect()->back()->with('error', trans('admin/licenses/message.checkout.license_is_inactive'));
|
||||||
|
|
||||||
if ($users->count() > $avail_count) {
|
|
||||||
Log::debug('You do not have enough free seats to complete this task, so we will check out as many as we can. ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the license is valid, check that there is an available seat
|
// If the license is valid, check that there is an available seat
|
||||||
@@ -244,6 +274,19 @@ class LicenseCheckoutController extends Controller
|
|||||||
return redirect()->back()->with('error', trans('admin/licenses/general.bulk.checkout_all.error_no_seats'));
|
return redirect()->back()->with('error', trans('admin/licenses/general.bulk.checkout_all.error_no_seats'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$avail_count = $license->getAvailSeatsCountAttribute();
|
||||||
|
|
||||||
|
$usersQuery = User::whereNull('deleted_at')->where('autoassign_licenses', '=', 1)->with('licenses');
|
||||||
|
if (Setting::getSettings()->full_multiple_companies_support && $license->company_id) {
|
||||||
|
$usersQuery->where('company_id', '=', $license->company_id);
|
||||||
|
}
|
||||||
|
$users = $usersQuery->get();
|
||||||
|
Log::debug($avail_count.' will be assigned');
|
||||||
|
|
||||||
|
if ($users->count() > $avail_count) {
|
||||||
|
Log::debug('You do not have enough free seats to complete this task, so we will check out as many as we can. ');
|
||||||
|
}
|
||||||
|
|
||||||
$assigned_count = 0;
|
$assigned_count = 0;
|
||||||
|
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use Illuminate\Contracts\View\View;
|
|||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use League\Csv\EscapeFormula;
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -388,6 +389,8 @@ class LicensesController extends Controller
|
|||||||
|
|
||||||
fputcsv($handle, $headers);
|
fputcsv($handle, $headers);
|
||||||
|
|
||||||
|
$formatter = new EscapeFormula('`');
|
||||||
|
|
||||||
foreach ($licenses as $license) {
|
foreach ($licenses as $license) {
|
||||||
// Add a new row with data
|
// Add a new row with data
|
||||||
$values = [
|
$values = [
|
||||||
@@ -419,7 +422,14 @@ class LicensesController extends Controller
|
|||||||
$license->created_at,
|
$license->created_at,
|
||||||
];
|
];
|
||||||
|
|
||||||
fputcsv($handle, $values);
|
// CSV_ESCAPE_FORMULAS is set to false in the .env
|
||||||
|
if (config('app.escape_formulas') === false) {
|
||||||
|
fputcsv($handle, $values);
|
||||||
|
|
||||||
|
// CSV_ESCAPE_FORMULAS is set to true or is not set in the .env
|
||||||
|
} else {
|
||||||
|
fputcsv($handle, $formatter->escapeRecord($values));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -89,19 +89,24 @@ class LocationsController extends Controller
|
|||||||
$location->fax = request('fax');
|
$location->fax = request('fax');
|
||||||
$location->tag_color = $request->input('tag_color');
|
$location->tag_color = $request->input('tag_color');
|
||||||
$location->notes = $request->input('notes');
|
$location->notes = $request->input('notes');
|
||||||
$location->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
|
||||||
|
|
||||||
// Only scope the location if the setting is enabled
|
|
||||||
if (Setting::getSettings()->scope_locations_fmcs) {
|
if (Setting::getSettings()->scope_locations_fmcs) {
|
||||||
$location->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
$location->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||||
// check if parent is set and has a different company
|
|
||||||
if ($location->parent_id && Location::find($location->parent_id)->company_id != $location->company_id) {
|
|
||||||
return redirect()->back()->withInput()->withInput()->with('error', 'different company than parent');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$location->company_id = $request->input('company_id');
|
$location->company_id = $request->input('company_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parent company check applies whenever FMCS is on, independent of scope_locations_fmcs.
|
||||||
|
if (Setting::getSettings()->full_multiple_companies_support) {
|
||||||
|
$parent = $location->parent_id ? Location::find($location->parent_id) : null;
|
||||||
|
if ($parent && $parent->company_id != $location->company_id) {
|
||||||
|
return redirect()->back()->withInput()->with('error', trans('general.error_location_parent_company', [
|
||||||
|
'parent' => $parent->name,
|
||||||
|
'parent_company' => $parent->company?->name ?? trans('general.unassigned'),
|
||||||
|
'location_company' => $location->company?->name ?? trans('general.unassigned'),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->has('use_cloned_image')) {
|
if ($request->has('use_cloned_image')) {
|
||||||
$cloned_model_img = Location::select('image')->find($request->input('clone_image_from_id'));
|
$cloned_model_img = Location::select('image')->find($request->input('clone_image_from_id'));
|
||||||
if ($cloned_model_img) {
|
if ($cloned_model_img) {
|
||||||
@@ -171,17 +176,34 @@ class LocationsController extends Controller
|
|||||||
$location->tag_color = $request->input('tag_color');
|
$location->tag_color = $request->input('tag_color');
|
||||||
$location->notes = $request->input('notes');
|
$location->notes = $request->input('notes');
|
||||||
|
|
||||||
// Only scope the location if the setting is enabled
|
|
||||||
if (Setting::getSettings()->scope_locations_fmcs) {
|
if (Setting::getSettings()->scope_locations_fmcs) {
|
||||||
$location->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
$location->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||||
// check if there are related objects with different company
|
// check if there are related objects with different company
|
||||||
if (Helper::test_locations_fmcs(false, $location->id, $location->company_id)) {
|
if ($mismatched = Helper::test_locations_fmcs(false, $location->id, $location->company_id)) {
|
||||||
return redirect()->back()->withInput()->withInput()->with('error', 'error scoped locations');
|
$first = $mismatched[0];
|
||||||
|
|
||||||
|
return redirect()->back()->withInput()->with('error', trans('general.error_location_scoped_items', [
|
||||||
|
'item_type' => trans('general.'.strtolower($first[0])),
|
||||||
|
'item_name' => $first[2],
|
||||||
|
'item_company' => $first[5] ?? trans('general.unassigned'),
|
||||||
|
]));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$location->company_id = $request->input('company_id');
|
$location->company_id = $request->input('company_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parent company check applies whenever FMCS is on, independent of scope_locations_fmcs.
|
||||||
|
if (Setting::getSettings()->full_multiple_companies_support) {
|
||||||
|
$parent = $location->parent_id ? Location::find($location->parent_id) : null;
|
||||||
|
if ($parent && $parent->company_id != $location->company_id) {
|
||||||
|
return redirect()->back()->withInput()->with('error', trans('general.error_location_parent_company', [
|
||||||
|
'parent' => $parent->name,
|
||||||
|
'parent_company' => $parent->company?->name ?? trans('general.unassigned'),
|
||||||
|
'location_company' => $location->company?->name ?? trans('general.unassigned'),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$location = $request->handleImages($location);
|
$location = $request->handleImages($location);
|
||||||
|
|
||||||
if ($location->save()) {
|
if ($location->save()) {
|
||||||
@@ -277,7 +299,7 @@ class LocationsController extends Controller
|
|||||||
->with('assignedAssets', $location->assignedAssets)
|
->with('assignedAssets', $location->assignedAssets)
|
||||||
->with('accessories', $location->accessories)
|
->with('accessories', $location->accessories)
|
||||||
->with('assignedAccessories', $location->assignedAccessories)
|
->with('assignedAccessories', $location->assignedAccessories)
|
||||||
->with('users', $location->users)
|
->with('users', $location->users()->with('companies')->get())
|
||||||
->with('location', $location)
|
->with('location', $location)
|
||||||
->with('consumables', $location->consumables)
|
->with('consumables', $location->consumables)
|
||||||
->with('components', $location->components)
|
->with('components', $location->components)
|
||||||
@@ -297,7 +319,7 @@ class LocationsController extends Controller
|
|||||||
->with('assignedAssets', $location->assignedAssets)
|
->with('assignedAssets', $location->assignedAssets)
|
||||||
->with('accessories', $location->accessories)
|
->with('accessories', $location->accessories)
|
||||||
->with('assignedAccessories', $location->assignedAccessories)
|
->with('assignedAccessories', $location->assignedAccessories)
|
||||||
->with('users', $location->users)
|
->with('users', $location->users()->with('companies')->get())
|
||||||
->with('location', $location)
|
->with('location', $location)
|
||||||
->with('consumables', $location->consumables)
|
->with('consumables', $location->consumables)
|
||||||
->with('components', $location->components)
|
->with('components', $location->components)
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\MaintenanceType;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class MaintenanceTypesController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
$this->authorize('index', MaintenanceType::class);
|
||||||
|
|
||||||
|
return view('maintenance-types.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
$this->authorize('create', MaintenanceType::class);
|
||||||
|
|
||||||
|
return view('maintenance-types.edit')->with('item', new MaintenanceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorize('create', MaintenanceType::class);
|
||||||
|
|
||||||
|
$type = new MaintenanceType;
|
||||||
|
$type->name = $request->input('name');
|
||||||
|
$type->created_by = auth()->id();
|
||||||
|
|
||||||
|
if ($type->save()) {
|
||||||
|
return redirect()->route('maintenance-types.index')
|
||||||
|
->with('success', trans('admin/maintenance_types/message.create.success'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()->withInput()->withErrors($type->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(MaintenanceType $maintenanceType): View
|
||||||
|
{
|
||||||
|
$this->authorize('update', $maintenanceType);
|
||||||
|
|
||||||
|
return view('maintenance-types.edit')->with('item', $maintenanceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, MaintenanceType $maintenanceType): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorize('update', $maintenanceType);
|
||||||
|
|
||||||
|
$maintenanceType->name = $request->input('name');
|
||||||
|
|
||||||
|
if ($maintenanceType->save()) {
|
||||||
|
return redirect()->route('maintenance-types.index')
|
||||||
|
->with('success', trans('admin/maintenance_types/message.update.success'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()->withInput()->withErrors($maintenanceType->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(MaintenanceType $maintenanceType): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorize('delete', $maintenanceType);
|
||||||
|
|
||||||
|
$maintenanceType->delete();
|
||||||
|
|
||||||
|
return redirect()->route('maintenance-types.index')
|
||||||
|
->with('success', trans('admin/maintenance_types/message.delete.success'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Enums\ActionType;
|
||||||
use App\Http\Requests\ImageUploadRequest;
|
use App\Http\Requests\ImageUploadRequest;
|
||||||
use App\Http\Requests\UploadFileRequest;
|
use App\Http\Requests\UploadFileRequest;
|
||||||
|
use App\Models\Actionlog;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
|
use App\Models\Company;
|
||||||
use App\Models\Maintenance;
|
use App\Models\Maintenance;
|
||||||
use Carbon\Carbon;
|
use App\Models\MaintenanceType;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -57,6 +60,7 @@ class MaintenancesController extends Controller
|
|||||||
|
|
||||||
return view('maintenances/edit')
|
return view('maintenances/edit')
|
||||||
->with('maintenanceType', Maintenance::getImprovementOptions())
|
->with('maintenanceType', Maintenance::getImprovementOptions())
|
||||||
|
->with('maintenanceTypes', MaintenanceType::orderBy('name')->get())
|
||||||
->with('asset', $asset)
|
->with('asset', $asset)
|
||||||
->with('item', new Maintenance);
|
->with('item', new Maintenance);
|
||||||
}
|
}
|
||||||
@@ -82,6 +86,10 @@ class MaintenancesController extends Controller
|
|||||||
// Loop through the selected assets
|
// Loop through the selected assets
|
||||||
foreach ($assets as $asset) {
|
foreach ($assets as $asset) {
|
||||||
|
|
||||||
|
if (! Company::isCurrentUserHasAccess($asset)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$maintenance = new Maintenance;
|
$maintenance = new Maintenance;
|
||||||
$maintenance->supplier_id = $request->input('supplier_id');
|
$maintenance->supplier_id = $request->input('supplier_id');
|
||||||
$maintenance->is_warranty = $request->input('is_warranty');
|
$maintenance->is_warranty = $request->input('is_warranty');
|
||||||
@@ -92,20 +100,13 @@ class MaintenancesController extends Controller
|
|||||||
// Save the asset maintenance data
|
// Save the asset maintenance data
|
||||||
$maintenance->asset_id = $asset->id;
|
$maintenance->asset_id = $asset->id;
|
||||||
$maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
|
$maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
|
||||||
|
$maintenance->maintenance_type_id = $request->input('maintenance_type_id');
|
||||||
$maintenance->name = $request->input('name');
|
$maintenance->name = $request->input('name');
|
||||||
$maintenance->start_date = $request->input('start_date');
|
$maintenance->start_date = $request->input('start_date');
|
||||||
$maintenance->completion_date = $request->input('completion_date');
|
$maintenance->completion_date = $request->input('completion_date');
|
||||||
|
$maintenance->responsible_party_id = $request->input('responsible_party_id') ?: auth()->id();
|
||||||
$maintenance->created_by = auth()->id();
|
$maintenance->created_by = auth()->id();
|
||||||
|
|
||||||
if (($maintenance->completion_date !== null)
|
|
||||||
&& ($maintenance->start_date !== '')
|
|
||||||
&& ($maintenance->start_date !== '0000-00-00')
|
|
||||||
) {
|
|
||||||
$startDate = Carbon::parse($maintenance->start_date);
|
|
||||||
$completionDate = Carbon::parse($maintenance->completion_date);
|
|
||||||
$maintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$request->handleImages($maintenance);
|
$request->handleImages($maintenance);
|
||||||
|
|
||||||
// Was the asset maintenance created?
|
// Was the asset maintenance created?
|
||||||
@@ -141,6 +142,7 @@ class MaintenancesController extends Controller
|
|||||||
->with('selected_assets', $maintenance->asset->pluck('id')->toArray())
|
->with('selected_assets', $maintenance->asset->pluck('id')->toArray())
|
||||||
->with('asset_ids', request()->input('asset_ids', []))
|
->with('asset_ids', request()->input('asset_ids', []))
|
||||||
->with('maintenanceType', Maintenance::getImprovementOptions())
|
->with('maintenanceType', Maintenance::getImprovementOptions())
|
||||||
|
->with('maintenanceTypes', MaintenanceType::orderBy('name')->get())
|
||||||
->with('item', $maintenance);
|
->with('item', $maintenance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,28 +171,12 @@ class MaintenancesController extends Controller
|
|||||||
$maintenance->cost = $request->input('cost');
|
$maintenance->cost = $request->input('cost');
|
||||||
$maintenance->notes = $request->input('notes');
|
$maintenance->notes = $request->input('notes');
|
||||||
$maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
|
$maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
|
||||||
|
$maintenance->maintenance_type_id = $request->input('maintenance_type_id');
|
||||||
$maintenance->name = $request->input('name');
|
$maintenance->name = $request->input('name');
|
||||||
$maintenance->start_date = $request->input('start_date');
|
$maintenance->start_date = $request->input('start_date');
|
||||||
$maintenance->completion_date = $request->input('completion_date');
|
$maintenance->completion_date = $request->input('completion_date');
|
||||||
|
$maintenance->responsible_party_id = $request->input('responsible_party_id');
|
||||||
$maintenance->url = $request->input('url');
|
$maintenance->url = $request->input('url');
|
||||||
|
|
||||||
// Todo - put this in a getter/setter?
|
|
||||||
if (($maintenance->completion_date == null)) {
|
|
||||||
if (($maintenance->asset_maintenance_time !== 0)
|
|
||||||
|| (! is_null($maintenance->asset_maintenance_time))
|
|
||||||
) {
|
|
||||||
$maintenance->asset_maintenance_time = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (($maintenance->completion_date !== null)
|
|
||||||
&& ($maintenance->start_date !== '')
|
|
||||||
&& ($maintenance->start_date !== '0000-00-00')
|
|
||||||
) {
|
|
||||||
$startDate = Carbon::parse($maintenance->start_date);
|
|
||||||
$completionDate = Carbon::parse($maintenance->completion_date);
|
|
||||||
$maintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
|
|
||||||
}
|
|
||||||
$request->handleImages($maintenance);
|
$request->handleImages($maintenance);
|
||||||
|
|
||||||
if ($maintenance->save()) {
|
if ($maintenance->save()) {
|
||||||
@@ -253,6 +239,36 @@ class MaintenancesController extends Controller
|
|||||||
)->validate();
|
)->validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a maintenance record as complete, logging who completed it and when.
|
||||||
|
*/
|
||||||
|
public function complete(Request $request, Maintenance $maintenance): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorize('update', $maintenance->asset);
|
||||||
|
|
||||||
|
if ($maintenance->completed_at) {
|
||||||
|
return redirect()->back()
|
||||||
|
->with('warning', trans('admin/maintenances/form.already_complete'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$maintenance->completed_at = now();
|
||||||
|
$maintenance->completed_by = auth()->id();
|
||||||
|
$maintenance->asset_maintenance_time = (int) $maintenance->created_at->diffInDays(now(), true);
|
||||||
|
$maintenance->saveQuietly();
|
||||||
|
|
||||||
|
$logAction = new Actionlog;
|
||||||
|
$logAction->item_type = Maintenance::class;
|
||||||
|
$logAction->item_id = $maintenance->id;
|
||||||
|
$logAction->target_type = Asset::class;
|
||||||
|
$logAction->target_id = $maintenance->asset_id;
|
||||||
|
$logAction->created_by = auth()->id();
|
||||||
|
$logAction->note = $request->input('note');
|
||||||
|
$logAction->logaction(ActionType::MaintenanceComplete);
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->with('success', trans('admin/maintenances/message.complete.success'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete an asset maintenance
|
* Delete an asset maintenance
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class ModalController extends Controller
|
|||||||
'kit-consumable',
|
'kit-consumable',
|
||||||
'kit-accessory',
|
'kit-accessory',
|
||||||
'location',
|
'location',
|
||||||
|
'maintenance-type',
|
||||||
'manufacturer',
|
'manufacturer',
|
||||||
'model',
|
'model',
|
||||||
'statuslabel',
|
'statuslabel',
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Models\Actionlog;
|
use App\Models\Actionlog;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
|
use App\Models\Maintenance;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class NotesController extends Controller
|
class NotesController extends Controller
|
||||||
{
|
{
|
||||||
public function store(Request $request)
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->authorize('update', Asset::class);
|
$this->authorize('update', Asset::class);
|
||||||
|
|
||||||
@@ -19,13 +21,19 @@ class NotesController extends Controller
|
|||||||
'note' => 'required|string|max:50000',
|
'note' => 'required|string|max:50000',
|
||||||
'type' => [
|
'type' => [
|
||||||
'required',
|
'required',
|
||||||
Rule::in(['asset']),
|
Rule::in(['asset', 'maintenance']),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$item = Asset::findOrFail($validated['id']);
|
if ($validated['type'] === 'maintenance') {
|
||||||
|
$item = Maintenance::findOrFail($validated['id']);
|
||||||
$this->authorize('update', $item);
|
$this->authorize('update', $item->asset);
|
||||||
|
$redirect = redirect()->route('maintenances.show', $validated['id']);
|
||||||
|
} else {
|
||||||
|
$item = Asset::findOrFail($validated['id']);
|
||||||
|
$this->authorize('update', $item);
|
||||||
|
$redirect = redirect()->route('hardware.show', $validated['id']);
|
||||||
|
}
|
||||||
|
|
||||||
$logaction = new Actionlog;
|
$logaction = new Actionlog;
|
||||||
$logaction->item_id = $item->id;
|
$logaction->item_id = $item->id;
|
||||||
@@ -34,9 +42,6 @@ class NotesController extends Controller
|
|||||||
$logaction->created_by = Auth::id();
|
$logaction->created_by = Auth::id();
|
||||||
$logaction->logaction('note added');
|
$logaction->logaction('note added');
|
||||||
|
|
||||||
return redirect()
|
return $redirect->withFragment('notes')->with('success', trans('general.note_added'));
|
||||||
->route('hardware.show', $validated['id'])
|
|
||||||
->withFragment('history')
|
|
||||||
->with('success', trans('general.note_added'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Models\Asset;
|
|||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Notifications\CurrentInventory;
|
use App\Notifications\CurrentInventory;
|
||||||
|
use App\Rules\CssColor;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -63,6 +64,12 @@ class ProfileController extends Controller
|
|||||||
|
|
||||||
$user->enable_sounds = $request->input('enable_sounds', false);
|
$user->enable_sounds = $request->input('enable_sounds', false);
|
||||||
$user->enable_confetti = $request->input('enable_confetti', false);
|
$user->enable_confetti = $request->input('enable_confetti', false);
|
||||||
|
$request->validate([
|
||||||
|
'link_light_color' => ['nullable', new CssColor],
|
||||||
|
'link_dark_color' => ['nullable', new CssColor],
|
||||||
|
'nav_link_color' => ['nullable', new CssColor],
|
||||||
|
]);
|
||||||
|
|
||||||
$user->link_light_color = $request->input('link_light_color', '#296282');
|
$user->link_light_color = $request->input('link_light_color', '#296282');
|
||||||
$user->link_dark_color = $request->input('link_dark_color', '#296282');
|
$user->link_dark_color = $request->input('link_dark_color', '#296282');
|
||||||
$user->nav_link_color = $request->input('nav_link_color', '#FFFFFF');
|
$user->nav_link_color = $request->input('nav_link_color', '#FFFFFF');
|
||||||
@@ -211,14 +218,19 @@ class ProfileController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function printInventory(): View
|
public function printInventory(): View
|
||||||
{
|
{
|
||||||
$show_users = User::where('id', auth()->user()->id)->get();
|
$userId = auth()->id();
|
||||||
|
|
||||||
return view('users/print')
|
$show_user = User::withInventoryRelations($userId)->first();
|
||||||
->with('assets', auth()->user()->assets())
|
|
||||||
->with('licenses', auth()->user()->licenses()->get())
|
$indirectItemsCount =
|
||||||
->with('accessories', auth()->user()->accessories()->get())
|
$show_user->assets->flatMap->assignedAssets->count()
|
||||||
->with('consumables', auth()->user()->consumables()->get())
|
+ $show_user->assets->flatMap->components->count()
|
||||||
->with('users', $show_users)
|
+ $show_user->assets->flatMap->licenses->count()
|
||||||
|
+ $show_user->assets->flatMap->assignedAccessories->count();
|
||||||
|
|
||||||
|
return view('users.print')
|
||||||
|
->with('users', [$show_user])
|
||||||
|
->with('indirectItemsCount', $indirectItemsCount)
|
||||||
->with('settings', Setting::getSettings());
|
->with('settings', Setting::getSettings());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Helpers\Helper;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Com\Tecnick\Barcode\Barcode;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
|
||||||
|
class QrCodeController extends Controller
|
||||||
|
{
|
||||||
|
public static $map_show_route = [
|
||||||
|
'accessories' => 'accessories.show',
|
||||||
|
'assets' => 'hardware.show',
|
||||||
|
'companies' => 'companies.show',
|
||||||
|
'components' => 'components.show',
|
||||||
|
'consumables' => 'consumables.show',
|
||||||
|
'hardware' => 'hardware.show',
|
||||||
|
'licenses' => 'licenses.show',
|
||||||
|
'locations' => 'locations.show',
|
||||||
|
'models' => 'models.show',
|
||||||
|
'users' => 'users.show',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function show($object_type, $id): Response|BinaryFileResponse|string|bool
|
||||||
|
{
|
||||||
|
$settings = Setting::getSettings();
|
||||||
|
|
||||||
|
if ($settings->label2_2d_type === 'none') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! array_key_exists($object_type, self::$map_show_route)) {
|
||||||
|
return $object_type.' is not a valid type.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
|
||||||
|
|
||||||
|
if (! $object) {
|
||||||
|
return 'That item is invalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->authorize('view', $object);
|
||||||
|
|
||||||
|
$size = Helper::barcodeDimensions($settings->label2_2d_type);
|
||||||
|
$qr_file = public_path().'/uploads/barcodes/qr-'.str_slug($object_type).'-'.str_slug($id).'.png';
|
||||||
|
|
||||||
|
if (file_exists($qr_file)) {
|
||||||
|
return response()->file($qr_file, ['Content-type' => 'image/png']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$barcode = new Barcode;
|
||||||
|
$barcode_obj = $barcode->getBarcodeObj(
|
||||||
|
$settings->label2_2d_type,
|
||||||
|
route(self::$map_show_route[$object_type], $id),
|
||||||
|
$size['height'],
|
||||||
|
$size['width'],
|
||||||
|
'black',
|
||||||
|
[-2, -2, -2, -2]
|
||||||
|
);
|
||||||
|
file_put_contents($qr_file, $barcode_obj->getPngData());
|
||||||
|
|
||||||
|
return response($barcode_obj->getPngData())->header('Content-type', 'image/png');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,8 +36,6 @@ use Illuminate\Support\Facades\Gate;
|
|||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
use League\Csv\EscapeFormula;
|
use League\Csv\EscapeFormula;
|
||||||
use League\Csv\Reader;
|
|
||||||
use League\Csv\Writer;
|
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,6 +54,31 @@ class ReportsController extends Controller
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
$this->authorize('reports.view');
|
||||||
|
$settings = Setting::getSettings();
|
||||||
|
|
||||||
|
$audit_alert_count = Asset::DueOrOverdueForAudit($settings)->count();
|
||||||
|
$checkin_alert_count = Asset::DueOrOverdueForCheckin($settings)->count();
|
||||||
|
// CheckoutAcceptance has no company_id column; scope through the checkoutable
|
||||||
|
// relationship so each type's CompanyableTrait global scope is applied.
|
||||||
|
$pending_acceptance_count = CheckoutAcceptance::pending()
|
||||||
|
->whereHasMorph('checkoutable', [Asset::class, LicenseSeat::class, Accessory::class, Component::class, Consumable::class])
|
||||||
|
->count();
|
||||||
|
$licenses_low_count = License::withCount(['freeSeats as free_seats_count'])
|
||||||
|
->get()
|
||||||
|
->filter(fn ($l) => $l->free_seats_count <= 0)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
return view('reports/index', compact(
|
||||||
|
'audit_alert_count',
|
||||||
|
'checkin_alert_count',
|
||||||
|
'pending_acceptance_count',
|
||||||
|
'licenses_low_count',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a view that displays the accessories report.
|
* Returns a view that displays the accessories report.
|
||||||
*
|
*
|
||||||
@@ -80,36 +103,46 @@ class ReportsController extends Controller
|
|||||||
* @see ManufacturersController::getDatatable() method that generates the JSON response
|
* @see ManufacturersController::getDatatable() method that generates the JSON response
|
||||||
* @since [v1.0]
|
* @since [v1.0]
|
||||||
*/
|
*/
|
||||||
public function exportAccessoryReport(): Response
|
public function exportAccessoryReport(): StreamedResponse
|
||||||
{
|
{
|
||||||
$this->authorize('reports.view');
|
$this->authorize('reports.view');
|
||||||
$accessories = Accessory::orderBy('created_at', 'DESC')->get();
|
|
||||||
|
|
||||||
$rows = [];
|
$response = new StreamedResponse(function () {
|
||||||
$header = [
|
$handle = fopen('php://output', 'w');
|
||||||
trans('admin/accessories/table.title'),
|
|
||||||
trans('admin/accessories/general.accessory_category'),
|
|
||||||
trans('admin/accessories/general.total'),
|
|
||||||
trans('admin/accessories/general.remaining'),
|
|
||||||
];
|
|
||||||
$header = array_map('trim', $header);
|
|
||||||
$rows[] = implode(', ', $header);
|
|
||||||
|
|
||||||
// Row per accessory
|
$header = [
|
||||||
foreach ($accessories as $accessory) {
|
trans('admin/accessories/table.title'),
|
||||||
$row = [];
|
trans('admin/accessories/general.accessory_category'),
|
||||||
$row[] = e($accessory->accessory_name);
|
trans('admin/accessories/general.total'),
|
||||||
$row[] = e($accessory->accessory_category);
|
trans('admin/accessories/general.remaining'),
|
||||||
$row[] = e($accessory->total);
|
];
|
||||||
$row[] = e($accessory->remaining);
|
fputcsv($handle, $header);
|
||||||
|
|
||||||
$rows[] = implode(',', $row);
|
$formatter = new EscapeFormula('`');
|
||||||
}
|
|
||||||
|
|
||||||
$csv = implode("\n", $rows);
|
Accessory::with('category')->orderBy('created_at', 'DESC')
|
||||||
$response = response()->make($csv, 200);
|
->chunk(500, function ($accessories) use ($handle, $formatter) {
|
||||||
$response->header('Content-Type', 'text/csv');
|
foreach ($accessories as $accessory) {
|
||||||
$response->header('Content-disposition', 'attachment;filename=report.csv');
|
$row = [
|
||||||
|
$accessory->name,
|
||||||
|
$accessory->category?->name,
|
||||||
|
$accessory->qty,
|
||||||
|
$accessory->numRemaining(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (config('app.escape_formulas') === false) {
|
||||||
|
fputcsv($handle, $row);
|
||||||
|
} else {
|
||||||
|
fputcsv($handle, $formatter->escapeRecord($row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fclose($handle);
|
||||||
|
}, 200, [
|
||||||
|
'Content-Type' => 'text/csv',
|
||||||
|
'Content-Disposition' => 'attachment; filename="accessories-report-'.date('Y-m-d-his').'.csv"',
|
||||||
|
]);
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
@@ -138,74 +171,80 @@ class ReportsController extends Controller
|
|||||||
*
|
*
|
||||||
* @since [v1.0]
|
* @since [v1.0]
|
||||||
*/
|
*/
|
||||||
public function exportDeprecationReport(): Response
|
public function exportDeprecationReport(): StreamedResponse
|
||||||
{
|
{
|
||||||
$this->authorize('reports.view');
|
$this->authorize('reports.view');
|
||||||
// Grab all the assets
|
|
||||||
$assets = Asset::with('model', 'assignedTo', 'status', 'defaultLoc', 'assetlog')
|
|
||||||
->orderBy('created_at', 'DESC')->get();
|
|
||||||
|
|
||||||
$csv = Writer::createFromFileObject(new \SplTempFileObject);
|
$response = new StreamedResponse(function () {
|
||||||
$csv->setOutputBOM(Reader::BOM_UTF16_BE);
|
$handle = fopen('php://output', 'w');
|
||||||
|
$formatter = new EscapeFormula('`');
|
||||||
|
|
||||||
$rows = [];
|
$header = [
|
||||||
|
trans('admin/hardware/table.asset_tag'),
|
||||||
|
trans('admin/hardware/table.title'),
|
||||||
|
trans('admin/hardware/table.serial'),
|
||||||
|
trans('admin/hardware/table.checkoutto'),
|
||||||
|
trans('admin/hardware/table.location'),
|
||||||
|
trans('admin/hardware/table.purchase_date'),
|
||||||
|
trans('admin/hardware/table.purchase_cost'),
|
||||||
|
trans('admin/hardware/table.book_value'),
|
||||||
|
trans('admin/hardware/table.diff'),
|
||||||
|
];
|
||||||
|
fputcsv($handle, $header);
|
||||||
|
|
||||||
// Create the header row
|
Asset::with('model', 'assignedTo', 'status', 'defaultLoc', 'assetlog')
|
||||||
$header = [
|
->orderBy('created_at', 'DESC')
|
||||||
trans('admin/hardware/table.asset_tag'),
|
->chunk(500, function ($assets) use ($handle, $formatter) {
|
||||||
trans('admin/hardware/table.title'),
|
foreach ($assets as $asset) {
|
||||||
trans('admin/hardware/table.serial'),
|
$currency = $asset->location
|
||||||
trans('admin/hardware/table.checkoutto'),
|
? $asset->location->currency
|
||||||
trans('admin/hardware/table.location'),
|
: Setting::getSettings()->default_currency;
|
||||||
trans('admin/hardware/table.purchase_date'),
|
|
||||||
trans('admin/hardware/table.purchase_cost'),
|
|
||||||
trans('admin/hardware/table.book_value'),
|
|
||||||
trans('admin/hardware/table.diff'),
|
|
||||||
];
|
|
||||||
|
|
||||||
// we insert the CSV header
|
if ($target = $asset->assignedTo) {
|
||||||
$csv->insertOne($header);
|
$assignedTo = $target->display_name;
|
||||||
|
} else {
|
||||||
|
$assignedTo = '';
|
||||||
|
}
|
||||||
|
|
||||||
// Create a row per asset
|
if (($asset->assigned_to > 0) && ($location = $asset->location)) {
|
||||||
foreach ($assets as $asset) {
|
if ($location->city) {
|
||||||
$row = [];
|
$locationStr = $location->city.', '.$location->state;
|
||||||
$row[] = e($asset->asset_tag);
|
} elseif ($location->name) {
|
||||||
$row[] = e($asset->name);
|
$locationStr = $location->name;
|
||||||
$row[] = e($asset->serial);
|
} else {
|
||||||
|
$locationStr = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$locationStr = '';
|
||||||
|
}
|
||||||
|
|
||||||
if ($target = $asset->assignedTo) {
|
$row = [
|
||||||
$row[] = e($target->display_name);
|
$asset->asset_tag,
|
||||||
} else {
|
$asset->name,
|
||||||
$row[] = ''; // Empty string if unassigned
|
$asset->serial,
|
||||||
}
|
$assignedTo,
|
||||||
|
$locationStr,
|
||||||
|
Helper::getFormattedDateObject($asset->purchase_date, 'date', false),
|
||||||
|
$currency.Helper::formatCurrencyOutput($asset->purchase_cost),
|
||||||
|
$currency.Helper::formatCurrencyOutput($asset->getDepreciatedValue()),
|
||||||
|
$currency.Helper::formatCurrencyOutput($asset->purchase_cost - $asset->getDepreciatedValue()),
|
||||||
|
];
|
||||||
|
|
||||||
if (($asset->assigned_to > 0) && ($location = $asset->location)) {
|
if (config('app.escape_formulas') === false) {
|
||||||
if ($location->city) {
|
fputcsv($handle, $row);
|
||||||
$row[] = e($location->city).', '.e($location->state);
|
} else {
|
||||||
} elseif ($location->name) {
|
fputcsv($handle, $formatter->escapeRecord($row));
|
||||||
$row[] = e($location->name);
|
}
|
||||||
} else {
|
}
|
||||||
$row[] = '';
|
});
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$row[] = ''; // Empty string if location is not set
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($asset->location) {
|
fclose($handle);
|
||||||
$currency = e($asset->location->currency);
|
}, 200, [
|
||||||
} else {
|
'Content-Type' => 'text/csv',
|
||||||
$currency = e(Setting::getSettings()->default_currency);
|
'Content-Disposition' => 'attachment; filename="depreciation-report-'.date('Y-m-d-his').'.csv"',
|
||||||
}
|
]);
|
||||||
|
|
||||||
$row[] = Helper::getFormattedDateObject($asset->purchase_date, 'date', false);
|
return $response;
|
||||||
$row[] = $currency.Helper::formatCurrencyOutput($asset->purchase_cost);
|
|
||||||
$row[] = $currency.Helper::formatCurrencyOutput($asset->getDepreciatedValue());
|
|
||||||
$row[] = $currency.Helper::formatCurrencyOutput(($asset->purchase_cost - $asset->getDepreciatedValue()));
|
|
||||||
$csv->insertOne($row);
|
|
||||||
}
|
|
||||||
|
|
||||||
$csv->output('depreciation-report-'.date('Y-m-d').'.csv');
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -252,6 +291,7 @@ class ReportsController extends Controller
|
|||||||
|
|
||||||
$response = new StreamedResponse(function () {
|
$response = new StreamedResponse(function () {
|
||||||
Log::debug('Starting streamed response');
|
Log::debug('Starting streamed response');
|
||||||
|
Log::debug('CSV escaping is set to: '.config('app.escape_formulas'));
|
||||||
|
|
||||||
// Open output stream
|
// Open output stream
|
||||||
$handle = fopen('php://output', 'w');
|
$handle = fopen('php://output', 'w');
|
||||||
@@ -287,6 +327,8 @@ class ReportsController extends Controller
|
|||||||
Log::debug('Walking results: '.$executionTime);
|
Log::debug('Walking results: '.$executionTime);
|
||||||
$count = 0;
|
$count = 0;
|
||||||
|
|
||||||
|
$formatter = new EscapeFormula('`');
|
||||||
|
|
||||||
foreach ($actionlogs as $actionlog) {
|
foreach ($actionlogs as $actionlog) {
|
||||||
$count++;
|
$count++;
|
||||||
$target_name = '';
|
$target_name = '';
|
||||||
@@ -317,7 +359,15 @@ class ReportsController extends Controller
|
|||||||
$actionlog->action_source,
|
$actionlog->action_source,
|
||||||
$actionlog->log_meta,
|
$actionlog->log_meta,
|
||||||
];
|
];
|
||||||
fputcsv($handle, $row);
|
|
||||||
|
// CSV_ESCAPE_FORMULAS is set to false in the .env
|
||||||
|
if (config('app.escape_formulas') === false) {
|
||||||
|
fputcsv($handle, $row);
|
||||||
|
|
||||||
|
// CSV_ESCAPE_FORMULAS is set to true or is not set in the .env
|
||||||
|
} else {
|
||||||
|
fputcsv($handle, $formatter->escapeRecord($row));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -359,45 +409,52 @@ class ReportsController extends Controller
|
|||||||
*
|
*
|
||||||
* @since [v1.0]
|
* @since [v1.0]
|
||||||
*/
|
*/
|
||||||
public function exportLicenseReport(): Response
|
public function exportLicenseReport(): StreamedResponse
|
||||||
{
|
{
|
||||||
$this->authorize('reports.view');
|
$this->authorize('reports.view');
|
||||||
$licenses = License::orderBy('created_at', 'DESC')->get();
|
|
||||||
|
|
||||||
$rows = [];
|
$response = new StreamedResponse(function () {
|
||||||
$header = [
|
$handle = fopen('php://output', 'w');
|
||||||
trans('admin/licenses/table.title'),
|
$formatter = new EscapeFormula('`');
|
||||||
trans('admin/licenses/table.serial'),
|
|
||||||
trans('admin/licenses/form.seats'),
|
|
||||||
trans('admin/licenses/form.remaining_seats'),
|
|
||||||
trans('admin/licenses/form.expiration'),
|
|
||||||
trans('general.purchase_date'),
|
|
||||||
trans('general.depreciation'),
|
|
||||||
trans('general.purchase_cost'),
|
|
||||||
];
|
|
||||||
|
|
||||||
$header = array_map('trim', $header);
|
$header = [
|
||||||
$rows[] = implode(', ', $header);
|
trans('admin/licenses/table.title'),
|
||||||
|
trans('admin/licenses/table.serial'),
|
||||||
|
trans('admin/licenses/form.seats'),
|
||||||
|
trans('admin/licenses/form.remaining_seats'),
|
||||||
|
trans('admin/licenses/form.expiration'),
|
||||||
|
trans('general.purchase_date'),
|
||||||
|
trans('general.depreciation'),
|
||||||
|
trans('general.purchase_cost'),
|
||||||
|
];
|
||||||
|
fputcsv($handle, $header);
|
||||||
|
|
||||||
// Row per license
|
License::orderBy('created_at', 'DESC')->chunk(500, function ($licenses) use ($handle, $formatter) {
|
||||||
foreach ($licenses as $license) {
|
foreach ($licenses as $license) {
|
||||||
$row = [];
|
$row = [
|
||||||
$row[] = e($license->name);
|
$license->name,
|
||||||
$row[] = e($license->serial);
|
$license->serial,
|
||||||
$row[] = e($license->seats);
|
$license->seats,
|
||||||
$row[] = $license->remaincount();
|
$license->remaincount(),
|
||||||
$row[] = $license->expiration_date;
|
$license->expiration_date,
|
||||||
$row[] = $license->purchase_date;
|
$license->purchase_date,
|
||||||
$row[] = ($license->depreciation != '') ? '' : e($license->depreciation->name);
|
($license->depreciation != '') ? $license->depreciation->name : '',
|
||||||
$row[] = '"'.Helper::formatCurrencyOutput($license->purchase_cost).'"';
|
Helper::formatCurrencyOutput($license->purchase_cost),
|
||||||
|
];
|
||||||
|
|
||||||
$rows[] = implode(',', $row);
|
if (config('app.escape_formulas') === false) {
|
||||||
}
|
fputcsv($handle, $row);
|
||||||
|
} else {
|
||||||
|
fputcsv($handle, $formatter->escapeRecord($row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$csv = implode("\n", $rows);
|
fclose($handle);
|
||||||
$response = response()->make($csv, 200);
|
}, 200, [
|
||||||
$response->header('Content-Type', 'text/csv');
|
'Content-Type' => 'text/csv',
|
||||||
$response->header('Content-disposition', 'attachment;filename=report.csv');
|
'Content-Disposition' => 'attachment; filename="licenses-report-'.date('Y-m-d-his').'.csv"',
|
||||||
|
]);
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
@@ -742,12 +799,11 @@ class ReportsController extends Controller
|
|||||||
$checkout_start = Carbon::parse($request->input('checkout_date_start'))->startOfDay();
|
$checkout_start = Carbon::parse($request->input('checkout_date_start'))->startOfDay();
|
||||||
$checkout_end = Carbon::parse($request->input('checkout_date_end', now()))->endOfDay();
|
$checkout_end = Carbon::parse($request->input('checkout_date_end', now()))->endOfDay();
|
||||||
|
|
||||||
$actionlogassets = Actionlog::where('action_type', '=', 'checkout')
|
$actionlogassets = Actionlog::select('id')->where('action_type', '=', 'checkout')
|
||||||
->where('item_type', 'LIKE', '%Asset%')
|
->where('item_type', '=', Asset::class)
|
||||||
->whereBetween('action_date', [$checkout_start, $checkout_end])
|
->whereBetween('action_date', [$checkout_start, $checkout_end]); // we are *not* doing ->get()...
|
||||||
->pluck('item_id');
|
|
||||||
|
|
||||||
$assets->whereIn('assets.id', $actionlogassets);
|
$assets->whereIn('id', $actionlogassets); // ...because this _should_ act as a 'subquery'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($request->filled('checkin_date_start'))) {
|
if (($request->filled('checkin_date_start'))) {
|
||||||
@@ -852,7 +908,7 @@ class ReportsController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('purchase_date')) {
|
if ($request->filled('purchase_date')) {
|
||||||
$row[] = ($asset->purchase_date) ? $asset->purchase_date : '';
|
$row[] = ($asset->purchase_date) ? Carbon::parse($asset->purchase_date)->format('Y-m-d') : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('purchase_cost')) {
|
if ($request->filled('purchase_cost')) {
|
||||||
@@ -860,7 +916,7 @@ class ReportsController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('eol')) {
|
if ($request->filled('eol')) {
|
||||||
$row[] = ($asset->asset_eol_date != '') ? $asset->asset_eol_date : '';
|
$row[] = ($asset->asset_eol_date != '') ? Carbon::parse($asset->asset_eol_date)->format('Y-m-d') : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('warranty')) {
|
if ($request->filled('warranty')) {
|
||||||
@@ -1136,56 +1192,60 @@ class ReportsController extends Controller
|
|||||||
*
|
*
|
||||||
* @version v1.0
|
* @version v1.0
|
||||||
*/
|
*/
|
||||||
public function exportMaintenancesReport(): Response
|
public function exportMaintenancesReport(): StreamedResponse
|
||||||
{
|
{
|
||||||
$this->authorize('reports.view');
|
$this->authorize('reports.view');
|
||||||
// Grab all the improvements
|
|
||||||
$Maintenances = Maintenance::with('asset', 'supplier')
|
|
||||||
->orderBy('created_at', 'DESC')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$rows = [];
|
$response = new StreamedResponse(function () {
|
||||||
|
$handle = fopen('php://output', 'w');
|
||||||
|
$formatter = new EscapeFormula('`');
|
||||||
|
|
||||||
$header = [
|
$header = [
|
||||||
trans('admin/hardware/table.asset_tag'),
|
trans('admin/hardware/table.asset_tag'),
|
||||||
trans('admin/maintenances/table.asset_name'),
|
trans('admin/maintenances/table.asset_name'),
|
||||||
trans('general.supplier'),
|
trans('general.supplier'),
|
||||||
trans('admin/maintenances/form.asset_maintenance_type'),
|
trans('admin/maintenances/form.asset_maintenance_type'),
|
||||||
trans('admin/maintenances/form.title'),
|
trans('admin/maintenances/form.title'),
|
||||||
trans('admin/maintenances/form.start_date'),
|
trans('admin/maintenances/form.start_date'),
|
||||||
trans('admin/maintenances/form.completion_date'),
|
trans('admin/maintenances/form.completion_date'),
|
||||||
trans('admin/maintenances/form.asset_maintenance_time'),
|
trans('admin/maintenances/form.asset_maintenance_time'),
|
||||||
trans('admin/maintenances/form.cost'),
|
trans('admin/maintenances/form.cost'),
|
||||||
];
|
];
|
||||||
|
fputcsv($handle, $header);
|
||||||
|
|
||||||
$header = array_map('trim', $header);
|
Maintenance::with('asset', 'supplier')
|
||||||
$rows[] = implode(',', $header);
|
->orderBy('created_at', 'DESC')
|
||||||
|
->chunk(500, function ($maintenances) use ($handle, $formatter) {
|
||||||
|
foreach ($maintenances as $maintenance) {
|
||||||
|
$improvementTime = is_null($maintenance->asset_maintenance_time)
|
||||||
|
? (int) Carbon::now()->diffInDays(Carbon::parse($maintenance->start_date), true)
|
||||||
|
: (int) $maintenance->asset_maintenance_time;
|
||||||
|
|
||||||
foreach ($Maintenances as $maintenance) {
|
$row = [
|
||||||
$row = [];
|
$maintenance->asset->asset_tag,
|
||||||
$row[] = str_replace(',', '', e($maintenance->asset->asset_tag));
|
$maintenance->asset->name,
|
||||||
$row[] = str_replace(',', '', e($maintenance->asset->name));
|
$maintenance->supplier->name,
|
||||||
$row[] = str_replace(',', '', e($maintenance->supplier->name));
|
$maintenance->improvement_type,
|
||||||
$row[] = e($maintenance->improvement_type);
|
$maintenance->name,
|
||||||
$row[] = e($maintenance->name);
|
$maintenance->start_date,
|
||||||
$row[] = e($maintenance->start_date);
|
$maintenance->completion_date,
|
||||||
$row[] = e($maintenance->completion_date);
|
$improvementTime,
|
||||||
if (is_null($maintenance->asset_maintenance_time)) {
|
trans('general.currency').Helper::formatCurrencyOutput($maintenance->cost),
|
||||||
$improvementTime = (int) Carbon::now()
|
];
|
||||||
->diffInDays(Carbon::parse($maintenance->start_date), true);
|
|
||||||
} else {
|
|
||||||
$improvementTime = (int) $maintenance->asset_maintenance_time;
|
|
||||||
}
|
|
||||||
$row[] = $improvementTime;
|
|
||||||
$row[] = trans('general.currency').Helper::formatCurrencyOutput($maintenance->cost);
|
|
||||||
$rows[] = implode(',', $row);
|
|
||||||
}
|
|
||||||
|
|
||||||
// spit out a csv
|
if (config('app.escape_formulas') === false) {
|
||||||
$csv = implode("\n", $rows);
|
fputcsv($handle, $row);
|
||||||
$response = response()->make($csv, 200);
|
} else {
|
||||||
$response->header('Content-Type', 'text/csv');
|
fputcsv($handle, $formatter->escapeRecord($row));
|
||||||
$response->header('Content-disposition', 'attachment;filename=report.csv');
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fclose($handle);
|
||||||
|
}, 200, [
|
||||||
|
'Content-Type' => 'text/csv',
|
||||||
|
'Content-Disposition' => 'attachment; filename="maintenances-report-'.date('Y-m-d-his').'.csv"',
|
||||||
|
]);
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
@@ -1200,6 +1260,9 @@ class ReportsController extends Controller
|
|||||||
public function getAssetAcceptanceReport($deleted = false): View
|
public function getAssetAcceptanceReport($deleted = false): View
|
||||||
{
|
{
|
||||||
$this->authorize('reports.view');
|
$this->authorize('reports.view');
|
||||||
|
|
||||||
|
$this->disableDebugbar();
|
||||||
|
|
||||||
$showDeleted = $deleted == 'deleted';
|
$showDeleted = $deleted == 'deleted';
|
||||||
|
|
||||||
$query = CheckoutAcceptance::Pending()
|
$query = CheckoutAcceptance::Pending()
|
||||||
@@ -1261,6 +1324,11 @@ class ReportsController extends Controller
|
|||||||
// Redirect to the unaccepted items 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'));
|
return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.bad_data'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! $this->currentUserCanAccessAcceptance($acceptance)) {
|
||||||
|
return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.insufficient_permissions'));
|
||||||
|
}
|
||||||
|
|
||||||
$item = $acceptance->checkoutable;
|
$item = $acceptance->checkoutable;
|
||||||
$assignee = $acceptance->assignedTo ?? $item->assignedTo ?? null;
|
$assignee = $acceptance->assignedTo ?? $item->assignedTo ?? null;
|
||||||
$email = $assignee?->email;
|
$email = $assignee?->email;
|
||||||
@@ -1295,6 +1363,33 @@ class ReportsController extends Controller
|
|||||||
return redirect()->route('reports/unaccepted_assets')->with('success', trans('admin/reports/general.reminder_sent'));
|
return redirect()->route('reports/unaccepted_assets')->with('success', trans('admin/reports/general.reminder_sent'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function currentUserCanAccessAcceptance(CheckoutAcceptance $acceptance): bool
|
||||||
|
{
|
||||||
|
if (Setting::getSettings()->full_multiple_companies_support != '1') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (! $user->company_id || $user->isSuperUser()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass Eloquent global scopes so cross-company items are still found
|
||||||
|
$checkoutableType = $acceptance->checkoutable_type;
|
||||||
|
$checkoutable = $checkoutableType::withoutGlobalScopes()->find($acceptance->checkoutable_id);
|
||||||
|
|
||||||
|
if ($checkoutable instanceof LicenseSeat) {
|
||||||
|
$itemCompanyId = License::withoutGlobalScopes()
|
||||||
|
->where('id', $checkoutable->license_id)
|
||||||
|
->value('company_id');
|
||||||
|
} else {
|
||||||
|
$itemCompanyId = $checkoutable?->company_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $itemCompanyId === null || (int) $itemCompanyId === (int) $user->company_id;
|
||||||
|
}
|
||||||
|
|
||||||
private function getCheckoutMailType(CheckoutAcceptance $acceptance, $logItem): Mailable
|
private function getCheckoutMailType(CheckoutAcceptance $acceptance, $logItem): Mailable
|
||||||
{
|
{
|
||||||
$lookup = [
|
$lookup = [
|
||||||
@@ -1327,11 +1422,21 @@ class ReportsController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorize('reports.view');
|
$this->authorize('reports.view');
|
||||||
|
|
||||||
if (! $acceptance = CheckoutAcceptance::pending()->find($acceptanceId)) {
|
$acceptance = CheckoutAcceptance::pending()
|
||||||
|
->with(['checkoutable' => function (MorphTo $morphTo) {
|
||||||
|
$morphTo->morphWith([LicenseSeat::class => ['license']]);
|
||||||
|
}])
|
||||||
|
->find($acceptanceId);
|
||||||
|
|
||||||
|
if (! $acceptance) {
|
||||||
// Redirect to the unaccepted assets report page with error
|
// Redirect to the unaccepted assets report page with error
|
||||||
return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.bad_data'));
|
return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.bad_data'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! $this->currentUserCanAccessAcceptance($acceptance)) {
|
||||||
|
return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.insufficient_permissions'));
|
||||||
|
}
|
||||||
|
|
||||||
if ($acceptance->delete()) {
|
if ($acceptance->delete()) {
|
||||||
return redirect()->route('reports/unaccepted_assets')->with('success', trans('admin/reports/general.acceptance_deleted'));
|
return redirect()->route('reports/unaccepted_assets')->with('success', trans('admin/reports/general.acceptance_deleted'));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use App\Models\Group;
|
|||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Notifications\MailTest;
|
use App\Notifications\MailTest;
|
||||||
|
use App\Rules\CssColor;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
@@ -92,10 +93,12 @@ class SettingsController extends Controller
|
|||||||
$old_locations_fmcs = $setting->scope_locations_fmcs;
|
$old_locations_fmcs = $setting->scope_locations_fmcs;
|
||||||
$setting->full_multiple_companies_support = $request->input('full_multiple_companies_support', '0');
|
$setting->full_multiple_companies_support = $request->input('full_multiple_companies_support', '0');
|
||||||
$setting->scope_locations_fmcs = $request->input('scope_locations_fmcs', '0');
|
$setting->scope_locations_fmcs = $request->input('scope_locations_fmcs', '0');
|
||||||
|
$setting->null_company_is_floater = $request->input('null_company_is_floater', '0');
|
||||||
|
|
||||||
// Backward compatibility for locations makes no sense without FullMultipleCompanySupport
|
// These options make no sense without FullMultipleCompanySupport
|
||||||
if (! $setting->full_multiple_companies_support) {
|
if (! $setting->full_multiple_companies_support) {
|
||||||
$setting->scope_locations_fmcs = '0';
|
$setting->scope_locations_fmcs = '0';
|
||||||
|
$setting->null_company_is_floater = '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for inconsistencies when activating scoped locations
|
// check for inconsistencies when activating scoped locations
|
||||||
@@ -189,6 +192,13 @@ class SettingsController extends Controller
|
|||||||
$request->validate(['site_name' => 'required']);
|
$request->validate(['site_name' => 'required']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'header_color' => ['nullable', new CssColor],
|
||||||
|
'link_light_color' => ['nullable', new CssColor],
|
||||||
|
'link_dark_color' => ['nullable', new CssColor],
|
||||||
|
'nav_link_color' => ['nullable', new CssColor],
|
||||||
|
]);
|
||||||
|
|
||||||
$setting->header_color = $request->input('header_color', '#3c8dbc');
|
$setting->header_color = $request->input('header_color', '#3c8dbc');
|
||||||
$setting->link_light_color = $request->input('link_light_color', '#296282');
|
$setting->link_light_color = $request->input('link_light_color', '#296282');
|
||||||
$setting->link_dark_color = $request->input('link_dark_color', '#5fa4cc');
|
$setting->link_dark_color = $request->input('link_dark_color', '#5fa4cc');
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Http\Requests\SetupUserRequest;
|
|||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Notifications\FirstAdminNotification;
|
use App\Notifications\FirstAdminNotification;
|
||||||
|
use App\Rules\CssColor;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
@@ -166,6 +167,12 @@ class SetupController extends Controller
|
|||||||
$settings->alerts_enabled = 1;
|
$settings->alerts_enabled = 1;
|
||||||
$settings->pwd_secure_min = 10;
|
$settings->pwd_secure_min = 10;
|
||||||
$settings->brand = 1;
|
$settings->brand = 1;
|
||||||
|
$request->validate([
|
||||||
|
'link_light_color' => ['nullable', new CssColor],
|
||||||
|
'link_dark_color' => ['nullable', new CssColor],
|
||||||
|
'nav_link_color' => ['nullable', new CssColor],
|
||||||
|
]);
|
||||||
|
|
||||||
$settings->link_light_color = $request->input('link_light_color', '#296282');
|
$settings->link_light_color = $request->input('link_light_color', '#296282');
|
||||||
$settings->link_dark_color = $request->input('link_dark_color', '#296282');
|
$settings->link_dark_color = $request->input('link_dark_color', '#296282');
|
||||||
$settings->nav_link_color = $request->input('nav_link_color', '#FFFFFF');
|
$settings->nav_link_color = $request->input('nav_link_color', '#FFFFFF');
|
||||||
|
|||||||
@@ -101,11 +101,13 @@ class UploadedFilesController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (request('inline') == 'true') {
|
if (request('inline') == 'true') {
|
||||||
$headers = [
|
$path = self::$map_storage_path[$object_type];
|
||||||
'Content-Disposition' => 'inline',
|
|
||||||
];
|
|
||||||
|
|
||||||
return Storage::download(self::$map_storage_path[$object_type].$log->filename, $log->filename, $headers);
|
if (! StorageHelper::allowSafeInline($path.$log->filename)) {
|
||||||
|
return StorageHelper::downloader($path.$log->filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Storage::download($path.$log->filename, $log->filename, ['Content-Disposition' => 'inline']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return StorageHelper::downloader(self::$map_storage_path[$object_type].$log->filename);
|
return StorageHelper::downloader(self::$map_storage_path[$object_type].$log->filename);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Models\Accessory;
|
use App\Models\Accessory;
|
||||||
use App\Models\Actionlog;
|
use App\Models\Actionlog;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
|
use App\Models\Company;
|
||||||
use App\Models\ConsumableAssignment;
|
use App\Models\ConsumableAssignment;
|
||||||
use App\Models\Group;
|
use App\Models\Group;
|
||||||
use App\Models\License;
|
use App\Models\License;
|
||||||
@@ -168,16 +169,21 @@ class BulkUsersController extends Controller
|
|||||||
|
|
||||||
$this->conditionallyAddItem('location_id')
|
$this->conditionallyAddItem('location_id')
|
||||||
->conditionallyAddItem('department_id')
|
->conditionallyAddItem('department_id')
|
||||||
->conditionallyAddItem('company_id')
|
|
||||||
->conditionallyAddItem('locale')
|
->conditionallyAddItem('locale')
|
||||||
->conditionallyAddItem('remote')
|
->conditionallyAddItem('remote')
|
||||||
->conditionallyAddItem('ldap_import')
|
|
||||||
->conditionallyAddItem('activated')
|
|
||||||
->conditionallyAddItem('display_name')
|
->conditionallyAddItem('display_name')
|
||||||
->conditionallyAddItem('start_date')
|
->conditionallyAddItem('start_date')
|
||||||
->conditionallyAddItem('end_date')
|
->conditionallyAddItem('end_date')
|
||||||
->conditionallyAddItem('city')
|
->conditionallyAddItem('city')
|
||||||
->conditionallyAddItem('autoassign_licenses');
|
->conditionallyAddItem('autoassign_licenses')
|
||||||
|
->conditionallyAddItem('phone')
|
||||||
|
->conditionallyAddItem('jobtitle')
|
||||||
|
->conditionallyAddItem('address')
|
||||||
|
->conditionallyAddItem('state')
|
||||||
|
->conditionallyAddItem('country')
|
||||||
|
->conditionallyAddItem('zip')
|
||||||
|
->conditionallyAddItem('website')
|
||||||
|
->conditionallyAddItem('notes');
|
||||||
|
|
||||||
// If the manager_id is one of the users being updated, generate a warning.
|
// If the manager_id is one of the users being updated, generate a warning.
|
||||||
if (array_search($request->input('manager_id'), $user_raw_array)) {
|
if (array_search($request->input('manager_id'), $user_raw_array)) {
|
||||||
@@ -202,7 +208,7 @@ class BulkUsersController extends Controller
|
|||||||
$this->update_array['manager_id'] = null;
|
$this->update_array['manager_id'] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->input('null_company_id') == '1') {
|
if ($request->input('null_company_ids') == '1') {
|
||||||
$this->update_array['company_id'] = null;
|
$this->update_array['company_id'] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +228,46 @@ class BulkUsersController extends Controller
|
|||||||
$this->update_array['display_name'] = null;
|
$this->update_array['display_name'] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($request->input('null_city') == '1') {
|
||||||
|
$this->update_array['city'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->input('null_phone') == '1') {
|
||||||
|
$this->update_array['phone'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->input('null_jobtitle') == '1') {
|
||||||
|
$this->update_array['jobtitle'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->input('null_employee_num') == '1') {
|
||||||
|
$this->update_array['employee_num'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->input('null_address') == '1') {
|
||||||
|
$this->update_array['address'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->input('null_state') == '1') {
|
||||||
|
$this->update_array['state'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->input('null_country') == '1') {
|
||||||
|
$this->update_array['country'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->input('null_zip') == '1') {
|
||||||
|
$this->update_array['zip'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->input('null_website') == '1') {
|
||||||
|
$this->update_array['website'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->input('null_notes') == '1') {
|
||||||
|
$this->update_array['notes'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (! $manager_conflict) {
|
if (! $manager_conflict) {
|
||||||
$this->conditionallyAddItem('manager_id');
|
$this->conditionallyAddItem('manager_id');
|
||||||
}
|
}
|
||||||
@@ -235,11 +281,50 @@ class BulkUsersController extends Controller
|
|||||||
->update(['location_id' => $this->update_array['location_id']]);
|
->update(['location_id' => $this->update_array['location_id']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only sync groups if groups were selected
|
// Handle company pivot sync separately from the mass update.
|
||||||
if ($request->filled('groups')) {
|
// company_ids[] comes from the multi-select; null_company_ids clears all memberships.
|
||||||
|
$bulkCompanyIds = array_filter(array_map('intval', (array) $request->input('company_ids', [])));
|
||||||
|
$clearCompanies = $request->input('null_company_ids') == '1';
|
||||||
|
|
||||||
|
if ($bulkCompanyIds || $clearCompanies) {
|
||||||
|
$allowedIds = Company::getIdsForCurrentUser($bulkCompanyIds);
|
||||||
|
// Also update the scalar company_id column for display/backward compat.
|
||||||
|
$scalarCompanyId = $allowedIds[0] ?? null;
|
||||||
|
User::whereIn('id', $user_raw_array)->where('id', '!=', auth()->id())
|
||||||
|
->update(['company_id' => $scalarCompanyId]);
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
|
if ($clearCompanies && ! auth()->user()->isSuperUser() && Company::isFullMultipleCompanySupportEnabled()) {
|
||||||
|
// Non-superusers can only detach companies they belong to; sync([]) would
|
||||||
|
// also wipe memberships for companies outside their scope.
|
||||||
|
$user->companies()->detach(Company::getIdsForCurrentUser(
|
||||||
|
$user->companies()->pluck('companies.id')->toArray()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$user->companies()->sync($allowedIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields that require canEditAuthFields (non-admins cannot touch admins/superusers,
|
||||||
|
// admins cannot touch superusers) must be applied per-user, not via mass update.
|
||||||
|
foreach ($users as $user) {
|
||||||
|
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
|
||||||
|
$authFieldUpdate = [];
|
||||||
|
if ($request->filled('activated')) {
|
||||||
|
$authFieldUpdate['activated'] = $request->input('activated');
|
||||||
|
}
|
||||||
|
if ($request->filled('ldap_import')) {
|
||||||
|
$authFieldUpdate['ldap_import'] = $request->input('ldap_import');
|
||||||
|
}
|
||||||
|
if ($request->filled('email')) {
|
||||||
|
$authFieldUpdate['email'] = $request->input('email');
|
||||||
|
} elseif ($request->input('null_email') == '1') {
|
||||||
|
$authFieldUpdate['email'] = null;
|
||||||
|
}
|
||||||
|
if (! empty($authFieldUpdate)) {
|
||||||
|
$user->update($authFieldUpdate);
|
||||||
|
}
|
||||||
|
if ($request->filled('groups') && auth()->user()->isSuperUser()) {
|
||||||
$user->groups()->sync($request->input('groups'));
|
$user->groups()->sync($request->input('groups'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,6 +395,31 @@ class BulkUsersController extends Controller
|
|||||||
return redirect()->route('users.index')->with('error', 'No status selected');
|
return redirect()->route('users.index')->with('error', 'No status selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enforce per-item checkin permissions before touching anything (catches FMCS company scoping).
|
||||||
|
foreach ($assets as $asset) {
|
||||||
|
if (auth()->user()->cannot('checkin', $asset)) {
|
||||||
|
return redirect()->route('users.index')->with('error', trans('general.insufficient_permissions'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$licenseModels = License::whereIn('id', $licenses->pluck('license_id')->unique())->get();
|
||||||
|
foreach ($licenseModels as $license) {
|
||||||
|
if (auth()->user()->cannot('checkin', $license)) {
|
||||||
|
return redirect()->route('users.index')->with('error', trans('general.insufficient_permissions'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$accessoryModels = Accessory::whereIn('id', $accessoryUserRows->pluck('accessory_id')->unique())->get();
|
||||||
|
foreach ($accessoryModels as $accessory) {
|
||||||
|
if (auth()->user()->cannot('checkin', $accessory)) {
|
||||||
|
return redirect()->route('users.index')->with('error', trans('general.insufficient_permissions'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->input('delete_user') == '1' && $users->isNotEmpty() && auth()->user()->cannot('delete', User::class)) {
|
||||||
|
return redirect()->route('users.index')->with('error', trans('general.insufficient_permissions'));
|
||||||
|
}
|
||||||
|
|
||||||
$this->logItemCheckinAndDelete($assets, Asset::class);
|
$this->logItemCheckinAndDelete($assets, Asset::class);
|
||||||
$this->logAccessoriesCheckin($accessoryUserRows);
|
$this->logAccessoriesCheckin($accessoryUserRows);
|
||||||
$this->logItemCheckinAndDelete($licenses, License::class);
|
$this->logItemCheckinAndDelete($licenses, License::class);
|
||||||
@@ -398,7 +508,7 @@ class BulkUsersController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function merge(Request $request)
|
public function merge(Request $request)
|
||||||
{
|
{
|
||||||
$this->authorize('update', User::class);
|
$this->authorize('delete', User::class);
|
||||||
|
|
||||||
if (config('app.lock_passwords')) {
|
if (config('app.lock_passwords')) {
|
||||||
return redirect()->route('users.index')->with('error', trans('general.feature_disabled'));
|
return redirect()->route('users.index')->with('error', trans('general.feature_disabled'));
|
||||||
@@ -416,9 +526,17 @@ class BulkUsersController extends Controller
|
|||||||
$users_to_merge = User::whereIn('id', $user_ids_to_merge)->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations', 'uploads', 'acceptances')->get();
|
$users_to_merge = User::whereIn('id', $user_ids_to_merge)->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations', 'uploads', 'acceptances')->get();
|
||||||
$admin = User::find(auth()->id());
|
$admin = User::find(auth()->id());
|
||||||
|
|
||||||
|
if (! auth()->user()->can('canEditAuthFields', $merge_into_user) || ! auth()->user()->can('editableOnDemo')) {
|
||||||
|
return redirect()->route('users.index')->with('error', trans('general.insufficient_permissions'));
|
||||||
|
}
|
||||||
|
|
||||||
// Walk users
|
// Walk users
|
||||||
foreach ($users_to_merge as $user_to_merge) {
|
foreach ($users_to_merge as $user_to_merge) {
|
||||||
|
|
||||||
|
if (! auth()->user()->can('canEditAuthFields', $user_to_merge) || ! auth()->user()->can('editableOnDemo')) {
|
||||||
|
return redirect()->route('users.index')->with('error', trans('general.insufficient_permissions'));
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($user_to_merge->assets as $asset) {
|
foreach ($user_to_merge->assets as $asset) {
|
||||||
Log::debug('Updating asset: '.$asset->asset_tag.' to '.$merge_into_user->id);
|
Log::debug('Updating asset: '.$asset->asset_tag.' to '.$merge_into_user->id);
|
||||||
$asset->assigned_to = $request->input('merge_into_id');
|
$asset->assigned_to = $request->input('merge_into_id');
|
||||||
@@ -461,6 +579,12 @@ class BulkUsersController extends Controller
|
|||||||
$managedLocation->save();
|
$managedLocation->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Carry over company pivot memberships from the merged user into the target.
|
||||||
|
$mergedCompanyIds = $user_to_merge->companies()->pluck('companies.id')->toArray();
|
||||||
|
if (! empty($mergedCompanyIds)) {
|
||||||
|
$merge_into_user->companies()->syncWithoutDetaching($mergedCompanyIds);
|
||||||
|
}
|
||||||
|
|
||||||
$user_to_merge->delete();
|
$user_to_merge->delete();
|
||||||
|
|
||||||
event(new UserMerged($user_to_merge, $merge_into_user, $admin));
|
event(new UserMerged($user_to_merge, $merge_into_user, $admin));
|
||||||
|
|||||||
@@ -10,11 +10,14 @@ use App\Http\Requests\DeleteUserRequest;
|
|||||||
use App\Http\Requests\ImageUploadRequest;
|
use App\Http\Requests\ImageUploadRequest;
|
||||||
use App\Http\Requests\SaveUserRequest;
|
use App\Http\Requests\SaveUserRequest;
|
||||||
use App\Mail\UnacceptedAssetReminderMail;
|
use App\Mail\UnacceptedAssetReminderMail;
|
||||||
|
use App\Models\Accessory;
|
||||||
use App\Models\Actionlog;
|
use App\Models\Actionlog;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\CheckoutAcceptance;
|
use App\Models\CheckoutAcceptance;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
|
use App\Models\Consumable;
|
||||||
use App\Models\Group;
|
use App\Models\Group;
|
||||||
|
use App\Models\License;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Notifications\CurrentInventory;
|
use App\Notifications\CurrentInventory;
|
||||||
@@ -26,6 +29,7 @@ use Illuminate\Http\Request;
|
|||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Illuminate\Support\Facades\Password;
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use League\Csv\EscapeFormula;
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,7 +126,7 @@ class UsersController extends Controller
|
|||||||
$user->mobile = $request->input('mobile');
|
$user->mobile = $request->input('mobile');
|
||||||
$user->location_id = $request->input('location_id', null);
|
$user->location_id = $request->input('location_id', null);
|
||||||
$user->department_id = $request->input('department_id', null);
|
$user->department_id = $request->input('department_id', null);
|
||||||
$user->company_id = Company::getIdForUser($request->input('company_id', null));
|
$companyIds = array_filter(array_map('intval', (array) ($request->input('company_ids') ?? ($request->filled('company_id') ? [$request->input('company_id')] : []))));
|
||||||
$user->manager_id = $request->input('manager_id', null);
|
$user->manager_id = $request->input('manager_id', null);
|
||||||
$user->notes = $request->input('notes');
|
$user->notes = $request->input('notes');
|
||||||
$user->address = $request->input('address', null);
|
$user->address = $request->input('address', null);
|
||||||
@@ -152,6 +156,7 @@ class UsersController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($user->save()) {
|
if ($user->save()) {
|
||||||
|
$user->syncCompaniesWithLogging(Company::getIdsForCurrentUser($companyIds));
|
||||||
|
|
||||||
if (($user->activated == '1') && ($user->email != '') && ($request->input('send_welcome') == '1')) {
|
if (($user->activated == '1') && ($user->email != '') && ($request->input('send_welcome') == '1')) {
|
||||||
|
|
||||||
@@ -163,7 +168,7 @@ class UsersController extends Controller
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
|
if (auth()->user()->isSuperUser() && auth()->user()->can('editableOnDemo')) {
|
||||||
$user->groups()->sync($request->input('groups'));
|
$user->groups()->sync($request->input('groups'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +279,7 @@ class UsersController extends Controller
|
|||||||
$user->phone = $request->input('phone');
|
$user->phone = $request->input('phone');
|
||||||
$user->mobile = $request->input('mobile');
|
$user->mobile = $request->input('mobile');
|
||||||
$user->location_id = $request->input('location_id', null);
|
$user->location_id = $request->input('location_id', null);
|
||||||
$user->company_id = Company::getIdForUser($request->input('company_id', null));
|
$companyIds = array_filter(array_map('intval', (array) ($request->input('company_ids') ?? ($request->filled('company_id') ? [$request->input('company_id')] : []))));
|
||||||
$user->manager_id = $request->input('manager_id', null);
|
$user->manager_id = $request->input('manager_id', null);
|
||||||
$user->notes = $request->input('notes');
|
$user->notes = $request->input('notes');
|
||||||
$user->department_id = $request->input('department_id', null);
|
$user->department_id = $request->input('department_id', null);
|
||||||
@@ -310,11 +315,14 @@ class UsersController extends Controller
|
|||||||
$user->password = bcrypt($request->input('password'));
|
$user->password = bcrypt($request->input('password'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$user->permissions = json_encode(PreserveUnauthorizedPrivilegedPermissionsAction::run(
|
if ($request->has('permission')) {
|
||||||
requestedPermissions: NormalizePermissionsPayloadAction::run($request->input('permission')),
|
$user->permissions = json_encode(PreserveUnauthorizedPrivilegedPermissionsAction::run(
|
||||||
authenticatedUser: $authenticatedUser,
|
requestedPermissions: NormalizePermissionsPayloadAction::run($request->input('permission')),
|
||||||
originalPermissions: $orig_permissions_array,
|
authenticatedUser: $authenticatedUser,
|
||||||
));
|
originalPermissions: $orig_permissions_array,
|
||||||
|
targetUser: $user,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// Only save groups if the user is a superuser
|
// Only save groups if the user is a superuser
|
||||||
if (auth()->user()->isSuperUser()) {
|
if (auth()->user()->isSuperUser()) {
|
||||||
@@ -332,6 +340,8 @@ class UsersController extends Controller
|
|||||||
session()->put(['redirect_option' => $request->input('redirect_option')]);
|
session()->put(['redirect_option' => $request->input('redirect_option')]);
|
||||||
|
|
||||||
if ($user->save()) {
|
if ($user->save()) {
|
||||||
|
$user->syncCompaniesWithLogging(Company::getIdsForCurrentUser($companyIds));
|
||||||
|
|
||||||
// Redirect to the user page
|
// Redirect to the user page
|
||||||
return Helper::getRedirectOption($request, $user->id, 'Users')
|
return Helper::getRedirectOption($request, $user->id, 'Users')
|
||||||
->with('success', trans('admin/users/message.success.update'));
|
->with('success', trans('admin/users/message.success.update'));
|
||||||
@@ -476,7 +486,7 @@ class UsersController extends Controller
|
|||||||
$permissions = $request->input('permissions', []);
|
$permissions = $request->input('permissions', []);
|
||||||
app('request')->request->set('permissions', $permissions);
|
app('request')->request->set('permissions', $permissions);
|
||||||
|
|
||||||
$user_to_clone = User::with('userloc')->withTrashed()->find($user->id);
|
$user_to_clone = User::with('userloc', 'companies')->withTrashed()->find($user->id);
|
||||||
// Make sure they can view this particular user
|
// Make sure they can view this particular user
|
||||||
$this->authorize('view', $user_to_clone);
|
$this->authorize('view', $user_to_clone);
|
||||||
|
|
||||||
@@ -533,52 +543,76 @@ class UsersController extends Controller
|
|||||||
// Open output stream
|
// Open output stream
|
||||||
$handle = fopen('php://output', 'w');
|
$handle = fopen('php://output', 'w');
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
// strtolower to prevent Excel from trying to open it as a SYLK file
|
||||||
|
strtolower(trans('general.id')),
|
||||||
|
trans('admin/companies/table.title'),
|
||||||
|
trans('admin/users/table.title'),
|
||||||
|
trans('general.employee_number'),
|
||||||
|
trans('admin/users/table.first_name'),
|
||||||
|
trans('admin/users/table.last_name'),
|
||||||
|
trans('admin/users/table.name'),
|
||||||
|
trans('admin/users/table.display_name'),
|
||||||
|
trans('admin/users/table.username'),
|
||||||
|
trans('admin/users/table.email'),
|
||||||
|
trans('admin/users/table.phone'),
|
||||||
|
trans('admin/users/table.mobile'),
|
||||||
|
trans('general.website'),
|
||||||
|
trans('general.address'),
|
||||||
|
trans('general.city'),
|
||||||
|
trans('general.state'),
|
||||||
|
trans('general.country'),
|
||||||
|
trans('general.zip'),
|
||||||
|
trans('admin/users/table.manager'),
|
||||||
|
trans('admin/users/table.location'),
|
||||||
|
trans('general.department'),
|
||||||
|
trans('general.assets'),
|
||||||
|
trans('general.licenses'),
|
||||||
|
trans('general.accessories'),
|
||||||
|
trans('general.consumables'),
|
||||||
|
trans('general.groups'),
|
||||||
|
trans('general.permissions'),
|
||||||
|
trans('general.notes'),
|
||||||
|
trans('admin/users/table.activated'),
|
||||||
|
trans('general.created_at'),
|
||||||
|
trans('general.importer.vip'),
|
||||||
|
trans('admin/users/general.remote'),
|
||||||
|
trans('general.language'),
|
||||||
|
trans('general.autoassign_licenses'),
|
||||||
|
trans('general.ldap_sync'),
|
||||||
|
trans('admin/users/general.two_factor_enrolled'),
|
||||||
|
trans('admin/users/general.two_factor_active'),
|
||||||
|
trans('admin/users/table.managed_users'),
|
||||||
|
trans('admin/users/table.managed_locations'),
|
||||||
|
trans('admin/users/general.department_manager'),
|
||||||
|
trans('general.created_by'),
|
||||||
|
trans('general.updated_at'),
|
||||||
|
trans('general.start_date'),
|
||||||
|
trans('general.end_date'),
|
||||||
|
trans('admin/users/table.last_login'),
|
||||||
|
trans('admin/licenses/table.deleted_at'),
|
||||||
|
];
|
||||||
|
|
||||||
|
fputcsv($handle, $headers);
|
||||||
|
|
||||||
$users = User::with(
|
$users = User::with(
|
||||||
'assets',
|
'assets',
|
||||||
'accessories',
|
'accessories',
|
||||||
'consumables',
|
'consumables',
|
||||||
'department',
|
'department.manager',
|
||||||
'licenses',
|
'licenses',
|
||||||
'manager',
|
'manager',
|
||||||
'groups',
|
'groups',
|
||||||
'userloc',
|
'userloc',
|
||||||
'company'
|
'companies',
|
||||||
)->orderBy('created_at', 'DESC')
|
'createdBy'
|
||||||
|
)->withCount(['managesUsers as manages_users_count', 'managedLocations as manages_locations_count'])
|
||||||
|
->orderBy('created_at', 'DESC')
|
||||||
->chunk(500, function ($users) use ($handle) {
|
->chunk(500, function ($users) use ($handle) {
|
||||||
$headers = [
|
|
||||||
// strtolower to prevent Excel from trying to open it as a SYLK file
|
|
||||||
strtolower(trans('general.id')),
|
|
||||||
trans('admin/companies/table.title'),
|
|
||||||
trans('admin/users/table.title'),
|
|
||||||
trans('general.employee_number'),
|
|
||||||
trans('admin/users/table.first_name'),
|
|
||||||
trans('admin/users/table.last_name'),
|
|
||||||
trans('admin/users/table.name'),
|
|
||||||
trans('admin/users/table.username'),
|
|
||||||
trans('admin/users/table.email'),
|
|
||||||
trans('admin/users/table.manager'),
|
|
||||||
trans('admin/users/table.location'),
|
|
||||||
trans('general.department'),
|
|
||||||
trans('general.assets'),
|
|
||||||
trans('general.licenses'),
|
|
||||||
trans('general.accessories'),
|
|
||||||
trans('general.consumables'),
|
|
||||||
trans('general.groups'),
|
|
||||||
trans('general.permissions'),
|
|
||||||
trans('general.notes'),
|
|
||||||
trans('admin/users/table.activated'),
|
|
||||||
trans('general.created_at'),
|
|
||||||
];
|
|
||||||
|
|
||||||
fputcsv($handle, $headers);
|
$formatter = new EscapeFormula('`');
|
||||||
|
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
$user_groups = '';
|
|
||||||
|
|
||||||
foreach ($user->groups as $user_group) {
|
|
||||||
$user_groups .= $user_group->name.', ';
|
|
||||||
}
|
|
||||||
|
|
||||||
$permissionstring = '';
|
$permissionstring = '';
|
||||||
|
|
||||||
if ($user->isSuperUser()) {
|
if ($user->isSuperUser()) {
|
||||||
@@ -592,14 +626,23 @@ class UsersController extends Controller
|
|||||||
// Add a new row with data
|
// Add a new row with data
|
||||||
$values = [
|
$values = [
|
||||||
$user->id,
|
$user->id,
|
||||||
($user->company) ? $user->company->name : '',
|
$user->companies->pluck('name')->implode('|'),
|
||||||
$user->jobtitle,
|
$user->jobtitle,
|
||||||
$user->employee_num,
|
$user->employee_num,
|
||||||
$user->first_name,
|
$user->first_name,
|
||||||
$user->last_name,
|
$user->last_name,
|
||||||
$user->display_name,
|
$user->getFullNameAttribute(),
|
||||||
|
$user->getRawOriginal('display_name'),
|
||||||
$user->username,
|
$user->username,
|
||||||
$user->email,
|
$user->email,
|
||||||
|
$user->phone,
|
||||||
|
$user->mobile,
|
||||||
|
$user->website,
|
||||||
|
$user->address,
|
||||||
|
$user->city,
|
||||||
|
$user->state,
|
||||||
|
$user->country,
|
||||||
|
$user->zip,
|
||||||
($user->manager) ? $user->manager->display_name : '',
|
($user->manager) ? $user->manager->display_name : '',
|
||||||
($user->userloc) ? $user->userloc->name : '',
|
($user->userloc) ? $user->userloc->name : '',
|
||||||
($user->department) ? $user->department->name : '',
|
($user->department) ? $user->department->name : '',
|
||||||
@@ -607,14 +650,37 @@ class UsersController extends Controller
|
|||||||
$user->licenses->count(),
|
$user->licenses->count(),
|
||||||
$user->accessories->count(),
|
$user->accessories->count(),
|
||||||
$user->consumables->count(),
|
$user->consumables->count(),
|
||||||
$user_groups,
|
$user->groups->pluck('name')->implode(', '),
|
||||||
$permissionstring,
|
$permissionstring,
|
||||||
$user->notes,
|
$user->notes,
|
||||||
($user->activated == '1') ? trans('general.yes') : trans('general.no'),
|
($user->activated == '1') ? trans('general.yes') : trans('general.no'),
|
||||||
$user->created_at,
|
$user->created_at,
|
||||||
|
($user->vip == '1') ? trans('general.yes') : trans('general.no'),
|
||||||
|
($user->remote == '1') ? trans('general.yes') : trans('general.no'),
|
||||||
|
$user->locale,
|
||||||
|
($user->autoassign_licenses == '1') ? trans('general.yes') : trans('general.no'),
|
||||||
|
($user->ldap_import == '1') ? trans('general.yes') : trans('general.no'),
|
||||||
|
($user->two_factor_active_and_enrolled()) ? trans('general.yes') : trans('general.no'),
|
||||||
|
($user->two_factor_active()) ? trans('general.yes') : trans('general.no'),
|
||||||
|
$user->manages_users_count,
|
||||||
|
$user->manages_locations_count,
|
||||||
|
($user->department && $user->department->manager) ? $user->department->manager->display_name : '',
|
||||||
|
($user->createdBy) ? $user->createdBy->display_name : '',
|
||||||
|
$user->updated_at,
|
||||||
|
$user->start_date,
|
||||||
|
$user->end_date,
|
||||||
|
$user->last_login,
|
||||||
|
$user->deleted_at,
|
||||||
];
|
];
|
||||||
|
|
||||||
fputcsv($handle, $values);
|
// CSV_ESCAPE_FORMULAS is set to false in the .env
|
||||||
|
if (config('app.escape_formulas') === false) {
|
||||||
|
fputcsv($handle, $values);
|
||||||
|
|
||||||
|
// CSV_ESCAPE_FORMULAS is set to true or is not set in the .env
|
||||||
|
} else {
|
||||||
|
fputcsv($handle, $formatter->escapeRecord($values));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -639,32 +705,24 @@ class UsersController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorize('view', User::class);
|
$this->authorize('view', User::class);
|
||||||
|
|
||||||
$user = User::where('id', $id)
|
$actor = auth()->user();
|
||||||
->with([
|
$canViewLicenses = $actor->can('view', License::class);
|
||||||
'assets.log' => fn ($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
|
$canViewAccessories = $actor->can('view', Accessory::class);
|
||||||
'assets.assignedAssets.log' => fn ($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
|
$canViewConsumables = $actor->can('view', Consumable::class);
|
||||||
'assets.assignedAssets.defaultLoc',
|
|
||||||
'assets.assignedAssets.location',
|
$user = User::withInventoryRelations($id, $canViewLicenses, $canViewAccessories, $canViewConsumables)->first();
|
||||||
'assets.assignedAssets.model.category',
|
|
||||||
'assets.defaultLoc',
|
$indirectItemsCount = $user?->assets?->flatMap->assignedAssets->count()
|
||||||
'assets.location',
|
+ $user?->assets?->flatMap->components->count()
|
||||||
'assets.model.category',
|
+ ($canViewLicenses ? $user?->assets?->flatMap->licenses->count() : 0)
|
||||||
'accessories.log' => fn ($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
|
+ ($canViewAccessories ? $user?->assets?->flatMap->assignedAccessories->count() : 0);
|
||||||
'accessories.category',
|
|
||||||
'accessories.manufacturer',
|
|
||||||
'consumables.log' => fn ($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
|
|
||||||
'consumables.category',
|
|
||||||
'consumables.manufacturer',
|
|
||||||
'licenses.category',
|
|
||||||
])
|
|
||||||
->withTrashed()
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($user) {
|
if ($user) {
|
||||||
$this->authorize('view', $user);
|
$this->authorize('view', $user);
|
||||||
|
|
||||||
return view('users.print')
|
return view('users.print')
|
||||||
->with('users', [$user])
|
->with('users', [$user])
|
||||||
|
->with('indirectItemsCount', $indirectItemsCount)
|
||||||
->with('settings', Setting::getSettings());
|
->with('settings', Setting::getSettings());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use Illuminate\Contracts\View\View;
|
|||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This controller handles all actions related to the ability for users
|
* This controller handles all actions related to the ability for users
|
||||||
@@ -120,6 +121,7 @@ class ViewAssetsController extends Controller
|
|||||||
'consumables',
|
'consumables',
|
||||||
'accessories',
|
'accessories',
|
||||||
'licenses',
|
'licenses',
|
||||||
|
'companies',
|
||||||
])->find($selectedUserId);
|
])->find($selectedUserId);
|
||||||
|
|
||||||
// If the user to view couldn't be found (shouldn't happen with proper logic), redirect with error
|
// If the user to view couldn't be found (shouldn't happen with proper logic), redirect with error
|
||||||
@@ -199,21 +201,39 @@ class ViewAssetsController extends Controller
|
|||||||
|
|
||||||
$settings = Setting::getSettings();
|
$settings = Setting::getSettings();
|
||||||
|
|
||||||
if (($item_request = $item->isRequestedBy($user)) || $cancel_by_admin) {
|
$is_admin = $user->isSuperUser() || $user->isAdmin();
|
||||||
$item->cancelRequest($requestingUser);
|
|
||||||
|
if ($cancel_by_admin && ! $is_admin) {
|
||||||
|
return redirect()->back()->with('error', trans('general.insufficient_permissions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($item_request = $item->isRequestedBy($user)) || ($is_admin && $cancel_by_admin)) {
|
||||||
|
$item->cancelRequest($is_admin && $cancel_by_admin ? $requestingUser : null);
|
||||||
$data['item_quantity'] = ($item_request) ? $item_request->qty : 1;
|
$data['item_quantity'] = ($item_request) ? $item_request->qty : 1;
|
||||||
$logaction->logaction(ActionType::RequestCanceled);
|
$logaction->logaction(ActionType::RequestCanceled);
|
||||||
|
|
||||||
if (($settings->alert_email != '') && ($settings->alerts_enabled == '1') && (! config('app.lock_passwords'))) {
|
if (($settings->alert_email != '') && ($settings->alerts_enabled == '1') && (! config('app.lock_passwords'))) {
|
||||||
$settings->notify((new RequestAssetCancelation($data))->locale($settings->locale));
|
try {
|
||||||
|
$settings->notify((new RequestAssetCancelation($data))->locale($settings->locale));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::warning('Could not send request cancellation notification: '.$e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->back()->with('success')->with('success', trans('admin/hardware/message.requests.canceled'));
|
return redirect()->back()->with('success')->with('success', trans('admin/hardware/message.requests.canceled'));
|
||||||
} else {
|
} else {
|
||||||
|
if ($fullItemType === Asset::class && is_null(Asset::RequestableAssets()->find($item->id))) {
|
||||||
|
return redirect()->back()->with('error', trans('admin/hardware/message.requests.error'));
|
||||||
|
}
|
||||||
|
|
||||||
$item->request();
|
$item->request();
|
||||||
if (($settings->alert_email != '') && ($settings->alerts_enabled == '1') && (! config('app.lock_passwords'))) {
|
if (($settings->alert_email != '') && ($settings->alerts_enabled == '1') && (! config('app.lock_passwords'))) {
|
||||||
$logaction->logaction('requested');
|
$logaction->logaction('requested');
|
||||||
$settings->notify((new RequestAssetNotification($data))->locale($settings->locale));
|
try {
|
||||||
|
$settings->notify((new RequestAssetNotification($data))->locale($settings->locale));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::warning('Could not send asset request notification: '.$e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.success'));
|
return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.success'));
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http;
|
namespace App\Http;
|
||||||
|
|
||||||
use App\Http\Middleware\AssetCountForSidebar;
|
|
||||||
use App\Http\Middleware\CheckColorSettings;
|
use App\Http\Middleware\CheckColorSettings;
|
||||||
use App\Http\Middleware\CheckForDebug;
|
use App\Http\Middleware\CheckForDebug;
|
||||||
use App\Http\Middleware\CheckForSetup;
|
use App\Http\Middleware\CheckForSetup;
|
||||||
@@ -17,6 +16,7 @@ use App\Http\Middleware\PreventBackHistory;
|
|||||||
use App\Http\Middleware\RedirectIfAuthenticated;
|
use App\Http\Middleware\RedirectIfAuthenticated;
|
||||||
use App\Http\Middleware\SecurityHeaders;
|
use App\Http\Middleware\SecurityHeaders;
|
||||||
use App\Http\Middleware\SetAPIResponseHeaders;
|
use App\Http\Middleware\SetAPIResponseHeaders;
|
||||||
|
use App\Http\Middleware\SetPaginationDefaults;
|
||||||
use App\Http\Middleware\TrimStrings;
|
use App\Http\Middleware\TrimStrings;
|
||||||
use App\Http\Middleware\TrustProxies;
|
use App\Http\Middleware\TrustProxies;
|
||||||
use App\Http\Middleware\VerifyCsrfToken;
|
use App\Http\Middleware\VerifyCsrfToken;
|
||||||
@@ -74,7 +74,6 @@ class Kernel extends HttpKernel
|
|||||||
CheckUserIsActivated::class,
|
CheckUserIsActivated::class,
|
||||||
CheckForTwoFactor::class,
|
CheckForTwoFactor::class,
|
||||||
CreateFreshApiToken::class,
|
CreateFreshApiToken::class,
|
||||||
AssetCountForSidebar::class,
|
|
||||||
CheckColorSettings::class,
|
CheckColorSettings::class,
|
||||||
AuthenticateSession::class,
|
AuthenticateSession::class,
|
||||||
SubstituteBindings::class,
|
SubstituteBindings::class,
|
||||||
@@ -84,6 +83,7 @@ class Kernel extends HttpKernel
|
|||||||
'auth:api',
|
'auth:api',
|
||||||
CheckLocale::class,
|
CheckLocale::class,
|
||||||
LogAuthedUserHeader::class,
|
LogAuthedUserHeader::class,
|
||||||
|
SetPaginationDefaults::class,
|
||||||
SubstituteBindings::class,
|
SubstituteBindings::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use App\Models\Asset;
|
|
||||||
use App\Models\Setting;
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class AssetCountForSidebar
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Handle an incoming request.
|
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function handle($request, Closure $next)
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* This needs to be set for the /setup process, since the tables might not exist yet
|
|
||||||
*/
|
|
||||||
$total_assets = 0;
|
|
||||||
$total_due_for_checkin = 0;
|
|
||||||
$total_overdue_for_checkin = 0;
|
|
||||||
$total_due_for_audit = 0;
|
|
||||||
$total_overdue_for_audit = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
$settings = Setting::getSettings();
|
|
||||||
view()->share('settings', $settings);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$total_assets = Asset::AssetsForShow()->count();
|
|
||||||
view()->share('total_assets', $total_assets);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$total_rtd_sidebar = Asset::RTD()->count();
|
|
||||||
view()->share('total_rtd_sidebar', $total_rtd_sidebar);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$total_deployed_sidebar = Asset::Deployed()->count();
|
|
||||||
view()->share('total_deployed_sidebar', $total_deployed_sidebar);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$total_archived_sidebar = Asset::Archived()->count();
|
|
||||||
view()->share('total_archived_sidebar', $total_archived_sidebar);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$total_pending_sidebar = Asset::Pending()->count();
|
|
||||||
view()->share('total_pending_sidebar', $total_pending_sidebar);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$total_undeployable_sidebar = Asset::Undeployable()->count();
|
|
||||||
view()->share('total_undeployable_sidebar', $total_undeployable_sidebar);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$total_byod_sidebar = Asset::where('byod', '=', '1')->count();
|
|
||||||
view()->share('total_byod_sidebar', $total_byod_sidebar);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$total_due_for_audit = Asset::DueForAudit($settings)->count();
|
|
||||||
view()->share('total_due_for_audit', $total_due_for_audit);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$total_overdue_for_audit = Asset::OverdueForAudit()->count();
|
|
||||||
view()->share('total_overdue_for_audit', $total_overdue_for_audit);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$total_due_for_checkin = Asset::DueForCheckin($settings)->count();
|
|
||||||
view()->share('total_due_for_checkin', $total_due_for_checkin);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$total_overdue_for_checkin = Asset::OverdueForCheckin()->count();
|
|
||||||
view()->share('total_overdue_for_checkin', $total_overdue_for_checkin);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
view()->share('total_due_and_overdue_for_checkin', ($total_due_for_checkin + $total_overdue_for_checkin));
|
|
||||||
view()->share('total_due_and_overdue_for_audit', ($total_due_for_audit + $total_overdue_for_audit));
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SetPaginationDefaults
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next)
|
||||||
|
{
|
||||||
|
$limit = config('app.max_results');
|
||||||
|
$intLimit = intval($request->input('limit'));
|
||||||
|
|
||||||
|
if (abs($intLimit) > 0 && $intLimit <= config('app.max_results')) {
|
||||||
|
$limit = abs($intLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
app()->instance('api_limit_value', $limit);
|
||||||
|
|
||||||
|
if ($request->filled('page') && ! $request->filled('offset')) {
|
||||||
|
$page = max(1, intval($request->input('page')));
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
} else {
|
||||||
|
$offset = intval($request->input('offset'));
|
||||||
|
$page = $limit > 0 ? (int) floor($offset / $limit) + 1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
app()->instance('api_offset_value', $offset);
|
||||||
|
app()->instance('api_current_page', $page);
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Helpers\Helper;
|
||||||
use App\Http\Requests\Traits\MayContainCustomFields;
|
use App\Http\Requests\Traits\MayContainCustomFields;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\AssetModel;
|
use App\Models\AssetModel;
|
||||||
@@ -26,6 +27,10 @@ class CreateMultipleAssetRequest extends ImageUploadRequest // should I extend f
|
|||||||
{
|
{
|
||||||
parent::prepareForValidation();
|
parent::prepareForValidation();
|
||||||
|
|
||||||
|
if ($this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))) {
|
||||||
|
$this->merge(['purchase_cost' => Helper::ParseCurrency($this->input('purchase_cost'))]);
|
||||||
|
}
|
||||||
|
|
||||||
if (Setting::getSettings()->full_multiple_companies_support == '1' && ! $this->user()->isSuperUser()) {
|
if (Setting::getSettings()->full_multiple_companies_support == '1' && ! $this->user()->isSuperUser()) {
|
||||||
$this->mergeIfMissing(['company_id' => $this->user()->company_id]);
|
$this->mergeIfMissing(['company_id' => $this->user()->company_id]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,20 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Models\Accessory;
|
||||||
|
use App\Models\Asset;
|
||||||
|
use App\Models\AssetModel;
|
||||||
|
use App\Models\Component;
|
||||||
|
use App\Models\Consumable;
|
||||||
|
use App\Models\License;
|
||||||
|
use App\Models\LicenseSeat;
|
||||||
|
use App\Models\Location;
|
||||||
|
use App\Models\Maintenance;
|
||||||
|
use App\Models\User;
|
||||||
use App\Rules\ValidJson;
|
use App\Rules\ValidJson;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class FilterRequest extends FormRequest
|
class FilterRequest extends FormRequest
|
||||||
{
|
{
|
||||||
@@ -23,8 +34,37 @@ class FilterRequest extends FormRequest
|
|||||||
*/
|
*/
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
|
$allowedTypes = [
|
||||||
|
'accessory',
|
||||||
|
Accessory::class,
|
||||||
|
'asset',
|
||||||
|
Asset::class,
|
||||||
|
'assetmodel',
|
||||||
|
'assetModel',
|
||||||
|
'AssetModel',
|
||||||
|
AssetModel::class,
|
||||||
|
'component',
|
||||||
|
Component::class,
|
||||||
|
'consumable',
|
||||||
|
Consumable::class,
|
||||||
|
'license',
|
||||||
|
License::class,
|
||||||
|
'licenseseat',
|
||||||
|
'licenseSeat',
|
||||||
|
'LicenseSeat',
|
||||||
|
LicenseSeat::class,
|
||||||
|
'location',
|
||||||
|
Location::class,
|
||||||
|
'maintenance',
|
||||||
|
Maintenance::class,
|
||||||
|
'user',
|
||||||
|
User::class,
|
||||||
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'filter' => ['nullable', new ValidJson],
|
'filter' => ['nullable', new ValidJson],
|
||||||
|
'item_type' => ['nullable', Rule::in($allowedTypes)],
|
||||||
|
'target_type' => ['nullable', Rule::in($allowedTypes)],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class ItemImportRequest extends FormRequest
|
|||||||
$classString = "App\\Importer\\{$class}Importer";
|
$classString = "App\\Importer\\{$class}Importer";
|
||||||
$importer = new $classString($filename);
|
$importer = new $classString($filename);
|
||||||
$import->field_map = request('column-mappings');
|
$import->field_map = request('column-mappings');
|
||||||
$import->created_by = auth()->id();
|
$import->created_by = $import->created_by ?? auth()->id();
|
||||||
$import->save();
|
$import->save();
|
||||||
$fieldMappings = [];
|
$fieldMappings = [];
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ class ItemImportRequest extends FormRequest
|
|||||||
|
|
||||||
if (is_null($fieldValue)) {
|
if (is_null($fieldValue)) {
|
||||||
$errorMessage = trans('validation.import_field_empty', ['fieldname' => $field]);
|
$errorMessage = trans('validation.import_field_empty', ['fieldname' => $field]);
|
||||||
$this->errorCallback($import, $field, [$field => $errorMessage]);
|
$this->errorCallback($import, $field, [$field => [$errorMessage]]);
|
||||||
|
|
||||||
return $this->errors;
|
return $this->errors;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class SaveUserRequest extends FormRequest
|
|||||||
'department_id' => 'nullable|integer|exists:departments,id',
|
'department_id' => 'nullable|integer|exists:departments,id',
|
||||||
'manager_id' => 'nullable|integer|exists:users,id',
|
'manager_id' => 'nullable|integer|exists:users,id',
|
||||||
'company_id' => ['nullable', 'integer', 'exists:companies,id'],
|
'company_id' => ['nullable', 'integer', 'exists:companies,id'],
|
||||||
|
'company_ids' => 'nullable|array',
|
||||||
|
'company_ids.*' => 'integer|exists:companies,id',
|
||||||
];
|
];
|
||||||
|
|
||||||
switch ($this->method()) {
|
switch ($this->method()) {
|
||||||
@@ -52,13 +54,13 @@ class SaveUserRequest extends FormRequest
|
|||||||
$rules['first_name'] = 'required|string|min:1';
|
$rules['first_name'] = 'required|string|min:1';
|
||||||
$rules['username'] = 'required_unless:ldap_import,1|string|min:1';
|
$rules['username'] = 'required_unless:ldap_import,1|string|min:1';
|
||||||
$rules['password'] = Setting::passwordComplexityRulesSaving('update').'|confirmed';
|
$rules['password'] = Setting::passwordComplexityRulesSaving('update').'|confirmed';
|
||||||
$rules['company_id'] = [new UserCannotSwitchCompaniesIfItemsAssigned];
|
$rules['company_id'] = ['nullable', 'integer', 'exists:companies,id', new UserCannotSwitchCompaniesIfItemsAssigned];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Save only what's passed
|
// Save only what's passed
|
||||||
case 'PATCH':
|
case 'PATCH':
|
||||||
$rules['password'] = Setting::passwordComplexityRulesSaving('update');
|
$rules['password'] = Setting::passwordComplexityRulesSaving('update');
|
||||||
$rules['company_id'] = [new UserCannotSwitchCompaniesIfItemsAssigned];
|
$rules['company_id'] = ['nullable', 'integer', 'exists:companies,id', new UserCannotSwitchCompaniesIfItemsAssigned];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Helpers\Helper;
|
||||||
use App\Models\Accessory;
|
use App\Models\Accessory;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
@@ -21,6 +22,10 @@ class StoreAccessoryRequest extends ImageUploadRequest
|
|||||||
{
|
{
|
||||||
parent::prepareForValidation();
|
parent::prepareForValidation();
|
||||||
|
|
||||||
|
if ($this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))) {
|
||||||
|
$this->merge(['purchase_cost' => Helper::ParseCurrency($this->input('purchase_cost'))]);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->category_id) {
|
if ($this->category_id) {
|
||||||
if ($category = Category::find($this->category_id)) {
|
if ($category = Category::find($this->category_id)) {
|
||||||
$this->merge([
|
$this->merge([
|
||||||
@@ -28,7 +33,6 @@ class StoreAccessoryRequest extends ImageUploadRequest
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Helpers\Helper;
|
||||||
use App\Http\Requests\Traits\MayContainCustomFields;
|
use App\Http\Requests\Traits\MayContainCustomFields;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\Setting;
|
|
||||||
use App\Rules\AssetCannotBeCheckedOutToNondeployableStatus;
|
use App\Rules\AssetCannotBeCheckedOutToNondeployableStatus;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Carbon\Exceptions\InvalidFormatException;
|
use Carbon\Exceptions\InvalidFormatException;
|
||||||
@@ -39,6 +39,9 @@ class StoreAssetRequest extends ImageUploadRequest
|
|||||||
$this->merge([
|
$this->merge([
|
||||||
'asset_tag' => $this->asset_tag ?? Asset::autoincrement_asset(),
|
'asset_tag' => $this->asset_tag ?? Asset::autoincrement_asset(),
|
||||||
'company_id' => $idForCurrentUser,
|
'company_id' => $idForCurrentUser,
|
||||||
|
'purchase_cost' => $this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))
|
||||||
|
? Helper::ParseCurrency($this->input('purchase_cost'))
|
||||||
|
: $this->input('purchase_cost'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,15 +52,6 @@ class StoreAssetRequest extends ImageUploadRequest
|
|||||||
{
|
{
|
||||||
$modelRules = (new Asset)->getRules();
|
$modelRules = (new Asset)->getRules();
|
||||||
|
|
||||||
if (Setting::getSettings()->digit_separator === '1.234,56' && is_string($this->input('purchase_cost'))) {
|
|
||||||
// If purchase_cost was submitted as a string with a comma separator
|
|
||||||
// then we need to ignore the normal numeric rules.
|
|
||||||
// Since the original rules still live on the model they will be run
|
|
||||||
// right before saving (and after purchase_cost has been
|
|
||||||
// converted to a float via setPurchaseCostAttribute).
|
|
||||||
$modelRules = $this->removeNumericRulesFromPurchaseCost($modelRules);
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_merge(
|
return array_merge(
|
||||||
$modelRules,
|
$modelRules,
|
||||||
['status_id' => [new AssetCannotBeCheckedOutToNondeployableStatus]],
|
['status_id' => [new AssetCannotBeCheckedOutToNondeployableStatus]],
|
||||||
@@ -81,20 +75,4 @@ class StoreAssetRequest extends ImageUploadRequest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function removeNumericRulesFromPurchaseCost(array $rules): array
|
|
||||||
{
|
|
||||||
$purchaseCost = $rules['purchase_cost'];
|
|
||||||
|
|
||||||
// If rule is in "|" format then turn it into an array
|
|
||||||
if (is_string($purchaseCost)) {
|
|
||||||
$purchaseCost = explode('|', $purchaseCost);
|
|
||||||
}
|
|
||||||
|
|
||||||
$rules['purchase_cost'] = array_filter($purchaseCost, function ($rule) {
|
|
||||||
return $rule !== 'numeric' && $rule !== 'gte:0';
|
|
||||||
});
|
|
||||||
|
|
||||||
return $rules;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Helpers\Helper;
|
||||||
|
use App\Models\Component;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
|
||||||
|
class StoreComponentRequest extends ImageUploadRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return Gate::allows('create', Component::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
if ($this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))) {
|
||||||
|
$this->merge(['purchase_cost' => Helper::ParseCurrency($this->input('purchase_cost'))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function response(array $errors)
|
||||||
|
{
|
||||||
|
return $this->redirector->back()->withInput()->withErrors($errors, $this->errorBag);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Helpers\Helper;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use App\Models\Consumable;
|
use App\Models\Consumable;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
@@ -21,6 +22,10 @@ class StoreConsumableRequest extends ImageUploadRequest
|
|||||||
{
|
{
|
||||||
parent::prepareForValidation();
|
parent::prepareForValidation();
|
||||||
|
|
||||||
|
if ($this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))) {
|
||||||
|
$this->merge(['purchase_cost' => Helper::ParseCurrency($this->input('purchase_cost'))]);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->category_id) {
|
if ($this->category_id) {
|
||||||
if ($category = Category::find($this->category_id)) {
|
if ($category = Category::find($this->category_id)) {
|
||||||
$this->merge([
|
$this->merge([
|
||||||
@@ -28,7 +33,6 @@ class StoreConsumableRequest extends ImageUploadRequest
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Helpers\Helper;
|
||||||
use App\Http\Requests\Traits\MayContainCustomFields;
|
use App\Http\Requests\Traits\MayContainCustomFields;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
@@ -22,6 +23,13 @@ class UpdateAssetRequest extends ImageUploadRequest
|
|||||||
return Gate::allows('update', $this->asset);
|
return Gate::allows('update', $this->asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
if ($this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))) {
|
||||||
|
$this->merge(['purchase_cost' => Helper::ParseCurrency($this->input('purchase_cost'))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* Get the validation rules that apply to the request.
|
||||||
*
|
*
|
||||||
@@ -51,12 +59,6 @@ class UpdateAssetRequest extends ImageUploadRequest
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// if the purchase cost is passed in as a string **and** the digit_separator is ',' (as is common in the EU)
|
|
||||||
// then we tweak the purchase_cost rule to make it a string
|
|
||||||
if ($setting->digit_separator === '1.234,56' && is_string($this->input('purchase_cost'))) {
|
|
||||||
$rules['purchase_cost'] = ['nullable', 'string'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rules;
|
return $rules;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Helpers\Helper;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
|
||||||
|
class UpdateComponentRequest extends ImageUploadRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return Gate::allows('update', $this->component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
if ($this->filled('purchase_cost') && ! is_float($this->input('purchase_cost')) && preg_match('/^[\d.,]+$/', (string) $this->input('purchase_cost'))) {
|
||||||
|
$this->merge(['purchase_cost' => Helper::ParseCurrency($this->input('purchase_cost'))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$min = $this->component->numCheckedOut();
|
||||||
|
|
||||||
|
return array_merge(parent::rules(), [
|
||||||
|
'qty' => "required|numeric|min:{$min}",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function response(array $errors)
|
||||||
|
{
|
||||||
|
return $this->redirector->back()->withInput()->withErrors($errors, $this->errorBag);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Traits;
|
||||||
|
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\Location;
|
use App\Models\Location;
|
||||||
use App\Models\SnipeModel;
|
use App\Models\SnipeModel;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
||||||
trait CheckInOutRequest
|
trait CheckInOutTrait
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Find target for checkout
|
* Find target for checkout
|
||||||
@@ -26,6 +26,7 @@ class AccessoriesTransformer
|
|||||||
'id' => $accessory->id,
|
'id' => $accessory->id,
|
||||||
'name' => e($accessory->name),
|
'name' => e($accessory->name),
|
||||||
'image' => ($accessory->image) ? Storage::disk('public')->url('accessories/'.e($accessory->image)) : null,
|
'image' => ($accessory->image) ? Storage::disk('public')->url('accessories/'.e($accessory->image)) : null,
|
||||||
|
'qr_code_url' => route('qr_code/common', ['object_type' => 'accessories', 'id' => $accessory->id]),
|
||||||
'company' => ($accessory->company) ? [
|
'company' => ($accessory->company) ? [
|
||||||
'id' => $accessory->company->id,
|
'id' => $accessory->company->id,
|
||||||
'name' => e($accessory->company->name),
|
'name' => e($accessory->company->name),
|
||||||
|
|||||||
@@ -116,10 +116,10 @@ class ActionlogsTransformer
|
|||||||
$clean_meta[$fieldname]['old'] = '************';
|
$clean_meta[$fieldname]['old'] = '************';
|
||||||
$clean_meta[$fieldname]['new'] = '************';
|
$clean_meta[$fieldname]['new'] = '************';
|
||||||
|
|
||||||
// Display the changes if the user is an admin or superadmin
|
// Display the changes if the user has permission to view encrypted custom fields
|
||||||
if (Gate::allows('admin')) {
|
if (Gate::allows('assets.view.encrypted_custom_fields')) {
|
||||||
$clean_meta[$fieldname]['old'] = ($enc_old) ? unserialize($enc_old, ['allowed_classes' => false]) : '';
|
$clean_meta[$fieldname]['old'] = ($enc_old) ? e(unserialize($enc_old, ['allowed_classes' => false])) : '';
|
||||||
$clean_meta[$fieldname]['new'] = ($enc_new) ? unserialize($enc_new, ['allowed_classes' => false]) : '';
|
$clean_meta[$fieldname]['new'] = ($enc_new) ? e(unserialize($enc_new, ['allowed_classes' => false])) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -293,6 +293,28 @@ class ActionlogsTransformer
|
|||||||
$clean_meta[trans('general.company')] = $clean_meta['company_id'];
|
$clean_meta[trans('general.company')] = $clean_meta['company_id'];
|
||||||
unset($clean_meta['company_id']);
|
unset($clean_meta['company_id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('companies', $clean_meta)) {
|
||||||
|
// clean_field() JSON-encodes array values into a string (e.g. "[14,15]").
|
||||||
|
// Decode them back to integer arrays before resolving names.
|
||||||
|
// Use withoutGlobalScopes so FMCS does not hide companies from the log viewer.
|
||||||
|
$resolveCompanyNames = function ($rawValue): string {
|
||||||
|
$ids = json_decode($rawValue, true);
|
||||||
|
if (empty($ids) || ! is_array($ids)) {
|
||||||
|
return trans('general.unassigned');
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect($ids)
|
||||||
|
->map(fn ($id) => Company::withoutGlobalScopes()->withTrashed()->find($id))
|
||||||
|
->map(fn ($c) => $c ? e($c->name) : trans('general.deleted'))
|
||||||
|
->join(', ');
|
||||||
|
};
|
||||||
|
|
||||||
|
$clean_meta['companies']['old'] = $resolveCompanyNames($clean_meta['companies']['old']);
|
||||||
|
$clean_meta['companies']['new'] = $resolveCompanyNames($clean_meta['companies']['new']);
|
||||||
|
$clean_meta[trans('general.companies')] = $clean_meta['companies'];
|
||||||
|
unset($clean_meta['companies']);
|
||||||
|
}
|
||||||
if (array_key_exists('supplier_id', $clean_meta)) {
|
if (array_key_exists('supplier_id', $clean_meta)) {
|
||||||
|
|
||||||
$oldSupplier = $supplier->find($clean_meta['supplier_id']['old']);
|
$oldSupplier = $supplier->find($clean_meta['supplier_id']['old']);
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class AssetModelsTransformer
|
|||||||
'tag_color' => ($assetmodel->manufacturer->tag_color) ? e($assetmodel->manufacturer->tag_color) : null,
|
'tag_color' => ($assetmodel->manufacturer->tag_color) ? e($assetmodel->manufacturer->tag_color) : null,
|
||||||
] : null,
|
] : null,
|
||||||
'image' => ($assetmodel->image != '') ? Storage::disk('public')->url('models/'.e($assetmodel->image)) : null,
|
'image' => ($assetmodel->image != '') ? Storage::disk('public')->url('models/'.e($assetmodel->image)) : null,
|
||||||
|
'qr_code_url' => route('qr_code/common', ['object_type' => 'models', 'id' => $assetmodel->id]),
|
||||||
'model_number' => ($assetmodel->model_number ? e($assetmodel->model_number) : null),
|
'model_number' => ($assetmodel->model_number ? e($assetmodel->model_number) : null),
|
||||||
'min_amt' => ($assetmodel->min_amt) ? (int) $assetmodel->min_amt : null,
|
'min_amt' => ($assetmodel->min_amt) ? (int) $assetmodel->min_amt : null,
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class AssetsTransformer
|
|||||||
'tag_color' => ($asset->defaultLoc->tag_color) ? e($asset->defaultLoc->tag_color) : null,
|
'tag_color' => ($asset->defaultLoc->tag_color) ? e($asset->defaultLoc->tag_color) : null,
|
||||||
] : null,
|
] : null,
|
||||||
'image' => ($asset->getImageUrl()) ? $asset->getImageUrl() : null,
|
'image' => ($asset->getImageUrl()) ? $asset->getImageUrl() : null,
|
||||||
|
'qr_code_url' => route('qr_code/common', ['object_type' => 'hardware', 'id' => $asset->id]),
|
||||||
'qr' => ($setting->qr_code == '1') ? Storage::disk('public')->url('barcodes/qr-'.str_slug($asset->asset_tag).'-'.str_slug($asset->id).'.png') : null,
|
'qr' => ($setting->qr_code == '1') ? Storage::disk('public')->url('barcodes/qr-'.str_slug($asset->asset_tag).'-'.str_slug($asset->id).'.png') : null,
|
||||||
'alt_barcode' => ($setting->alt_barcode_enabled == '1') ? Storage::disk('public')->url('barcodes/'.str_slug($setting->alt_barcode).'-'.str_slug($asset->asset_tag).'.png') : null,
|
'alt_barcode' => ($setting->alt_barcode_enabled == '1') ? Storage::disk('public')->url('barcodes/'.str_slug($setting->alt_barcode).'-'.str_slug($asset->asset_tag).'.png') : null,
|
||||||
'assigned_to' => $this->transformAssignedTo($asset),
|
'assigned_to' => $this->transformAssignedTo($asset),
|
||||||
@@ -144,7 +145,7 @@ class AssetsTransformer
|
|||||||
|
|
||||||
$fields_array[$field->name] = [
|
$fields_array[$field->name] = [
|
||||||
'field' => e($field->db_column),
|
'field' => e($field->db_column),
|
||||||
'value' => e($value),
|
'value' => ($field->element == 'markdown-textarea' && Gate::allows('assets.view.encrypted_custom_fields')) ? Helper::renderMarkdown($value) : e($value),
|
||||||
'field_format' => $field->format,
|
'field_format' => $field->format,
|
||||||
'element' => $field->element,
|
'element' => $field->element,
|
||||||
];
|
];
|
||||||
@@ -158,7 +159,7 @@ class AssetsTransformer
|
|||||||
|
|
||||||
$fields_array[$field->name] = [
|
$fields_array[$field->name] = [
|
||||||
'field' => e($field->db_column),
|
'field' => e($field->db_column),
|
||||||
'value' => e($value),
|
'value' => ($field->element == 'markdown-textarea') ? Helper::renderMarkdown($value) : e($value),
|
||||||
'field_format' => $field->format,
|
'field_format' => $field->format,
|
||||||
'element' => $field->element,
|
'element' => $field->element,
|
||||||
];
|
];
|
||||||
@@ -274,7 +275,7 @@ class AssetsTransformer
|
|||||||
$value = Helper::getFormattedDateObject($value, 'date', false);
|
$value = Helper::getFormattedDateObject($value, 'date', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
$fields_array[$field->db_column] = e($value);
|
$fields_array[$field->db_column] = ($field->element == 'markdown-textarea') ? Helper::renderMarkdown($value) : e($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
$array['custom_fields'] = $fields_array;
|
$array['custom_fields'] = $fields_array;
|
||||||
@@ -388,6 +389,9 @@ class AssetsTransformer
|
|||||||
$permissions_array['available_actions'] = [
|
$permissions_array['available_actions'] = [
|
||||||
'checkout' => false,
|
'checkout' => false,
|
||||||
'checkin' => Gate::allows('checkin', License::class),
|
'checkin' => Gate::allows('checkin', License::class),
|
||||||
|
'bulk_selectable' => [
|
||||||
|
'checkin' => Gate::allows('checkin', License::class),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$array += $permissions_array;
|
$array += $permissions_array;
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ class CategoriesTransformer
|
|||||||
$permissions_array['available_actions'] = [
|
$permissions_array['available_actions'] = [
|
||||||
'update' => Gate::allows('update', Category::class),
|
'update' => Gate::allows('update', Category::class),
|
||||||
'delete' => $category->isDeletable(),
|
'delete' => $category->isDeletable(),
|
||||||
|
'bulk_selectable' => [
|
||||||
|
'delete' => $category->isDeletable(),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$array += $permissions_array;
|
$array += $permissions_array;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class CompaniesTransformer
|
|||||||
'fax' => ($company->fax != '') ? e($company->fax) : null,
|
'fax' => ($company->fax != '') ? e($company->fax) : null,
|
||||||
'email' => ($company->email != '') ? e($company->email) : null,
|
'email' => ($company->email != '') ? e($company->email) : null,
|
||||||
'image' => ($company->image) ? Storage::disk('public')->url('companies/'.e($company->image)) : null,
|
'image' => ($company->image) ? Storage::disk('public')->url('companies/'.e($company->image)) : null,
|
||||||
|
'qr_code_url' => route('qr_code/common', ['object_type' => 'companies', 'id' => $company->id]),
|
||||||
'assets_count' => (int) $company->assets_count,
|
'assets_count' => (int) $company->assets_count,
|
||||||
'licenses_count' => (int) $company->licenses_count,
|
'licenses_count' => (int) $company->licenses_count,
|
||||||
'accessories_count' => (int) $company->accessories_count,
|
'accessories_count' => (int) $company->accessories_count,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class ComponentsTransformer
|
|||||||
'id' => (int) $component->id,
|
'id' => (int) $component->id,
|
||||||
'name' => e($component->name),
|
'name' => e($component->name),
|
||||||
'image' => ($component->image) ? Storage::disk('public')->url('components/'.e($component->image)) : null,
|
'image' => ($component->image) ? Storage::disk('public')->url('components/'.e($component->image)) : null,
|
||||||
|
'qr_code_url' => route('qr_code/common', ['object_type' => 'components', 'id' => $component->id]),
|
||||||
'serial' => ($component->serial) ? e($component->serial) : null,
|
'serial' => ($component->serial) ? e($component->serial) : null,
|
||||||
'location' => ($component->location) ? [
|
'location' => ($component->location) ? [
|
||||||
'id' => (int) $component->location->id,
|
'id' => (int) $component->location->id,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class ConsumablesTransformer
|
|||||||
'id' => (int) $consumable->id,
|
'id' => (int) $consumable->id,
|
||||||
'name' => e($consumable->name),
|
'name' => e($consumable->name),
|
||||||
'image' => ($consumable->getImageUrl()) ? ($consumable->getImageUrl()) : null,
|
'image' => ($consumable->getImageUrl()) ? ($consumable->getImageUrl()) : null,
|
||||||
|
'qr_code_url' => route('qr_code/common', ['object_type' => 'consumables', 'id' => $consumable->id]),
|
||||||
'category' => ($consumable->category) ? [
|
'category' => ($consumable->category) ? [
|
||||||
'id' => $consumable->category->id,
|
'id' => $consumable->category->id,
|
||||||
'name' => e($consumable->category->name),
|
'name' => e($consumable->category->name),
|
||||||
|
|||||||
@@ -9,8 +9,24 @@ class DatatablesTransformer
|
|||||||
**/
|
**/
|
||||||
public function transformDatatables($objects, $total = null)
|
public function transformDatatables($objects, $total = null)
|
||||||
{
|
{
|
||||||
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
|
$objects_array = [
|
||||||
$objects_array['rows'] = $objects;
|
'total' => $total ?? count($objects),
|
||||||
|
'rows' => $objects,
|
||||||
|
];
|
||||||
|
$current_page = app('api_current_page');
|
||||||
|
$limit = (int) app('api_limit_value');
|
||||||
|
$total_pages = $limit > 0 ? (int) ceil($objects_array['total'] / $limit) : 1;
|
||||||
|
|
||||||
|
$objects_array['current_page'] = $current_page;
|
||||||
|
$objects_array['per_page'] = $limit;
|
||||||
|
$objects_array['total_pages'] = $total_pages;
|
||||||
|
|
||||||
|
$objects_array['prev_page_url'] = $current_page > 1
|
||||||
|
? request()->fullUrlWithQuery(['page' => $current_page - 1])
|
||||||
|
: null;
|
||||||
|
$objects_array['next_page_url'] = $current_page < $total_pages
|
||||||
|
? request()->fullUrlWithQuery(['page' => $current_page + 1])
|
||||||
|
: null;
|
||||||
|
|
||||||
return $objects_array;
|
return $objects_array;
|
||||||
}
|
}
|
||||||
@@ -20,8 +36,10 @@ class DatatablesTransformer
|
|||||||
**/
|
**/
|
||||||
public function transformBulkResponseWithStatusAndObjects($objects, $total)
|
public function transformBulkResponseWithStatusAndObjects($objects, $total)
|
||||||
{
|
{
|
||||||
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
|
$objects_array = [
|
||||||
$objects_array['rows'] = $objects;
|
'total' => $total ?? count($objects),
|
||||||
|
'rows' => $objects,
|
||||||
|
];
|
||||||
|
|
||||||
return $objects_array;
|
return $objects_array;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,13 +38,11 @@ class LicenseSeatsTransformer
|
|||||||
'tag_color' => $seat->user->department->tag_color ? e($seat->user->department->tag_color) : null,
|
'tag_color' => $seat->user->department->tag_color ? e($seat->user->department->tag_color) : null,
|
||||||
|
|
||||||
] : null,
|
] : null,
|
||||||
'company' => ($seat->user->company) ?
|
'companies' => $seat->user->companies->map(fn ($c) => [
|
||||||
[
|
'id' => (int) $c->id,
|
||||||
'id' => (int) $seat->user->company->id,
|
'name' => e($c->name),
|
||||||
'name' => e($seat->user->company->name),
|
'tag_color' => $c->tag_color ? e($c->tag_color) : null,
|
||||||
'tag_color' => $seat->user->company->tag_color ? e($seat->user->company->tag_color) : null,
|
])->values(),
|
||||||
|
|
||||||
] : null,
|
|
||||||
'created_at' => Helper::getFormattedDateObject($seat->created_at, 'datetime'),
|
'created_at' => Helper::getFormattedDateObject($seat->created_at, 'datetime'),
|
||||||
] : null,
|
] : null,
|
||||||
'assigned_asset' => ($seat->asset) ? [
|
'assigned_asset' => ($seat->asset) ? [
|
||||||
@@ -70,6 +68,9 @@ class LicenseSeatsTransformer
|
|||||||
'clone' => Gate::allows('create', License::class),
|
'clone' => Gate::allows('create', License::class),
|
||||||
'update' => Gate::allows('update', License::class),
|
'update' => Gate::allows('update', License::class),
|
||||||
'delete' => Gate::allows('delete', License::class),
|
'delete' => Gate::allows('delete', License::class),
|
||||||
|
'bulk_selectable' => [
|
||||||
|
'checkin' => Gate::allows('checkin', License::class) && ($seat->assigned_to || $seat->asset_id),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$array += $permissions_array;
|
$array += $permissions_array;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class LicensesTransformer
|
|||||||
$array = [
|
$array = [
|
||||||
'id' => (int) $license->id,
|
'id' => (int) $license->id,
|
||||||
'name' => e($license->name),
|
'name' => e($license->name),
|
||||||
|
'qr_code_url' => route('qr_code/common', ['object_type' => 'licenses', 'id' => $license->id]),
|
||||||
'company' => ($license->company) ? ['id' => (int) $license->company->id, 'name' => e($license->company->name)] : null,
|
'company' => ($license->company) ? ['id' => (int) $license->company->id, 'name' => e($license->company->name)] : null,
|
||||||
'manufacturer' => ($license->manufacturer) ? [
|
'manufacturer' => ($license->manufacturer) ? [
|
||||||
'id' => (int) $license->manufacturer->id,
|
'id' => (int) $license->manufacturer->id,
|
||||||
@@ -66,7 +67,6 @@ class LicensesTransformer
|
|||||||
'created_at' => Helper::getFormattedDateObject($license->created_at, 'datetime'),
|
'created_at' => Helper::getFormattedDateObject($license->created_at, 'datetime'),
|
||||||
'updated_at' => Helper::getFormattedDateObject($license->updated_at, 'datetime'),
|
'updated_at' => Helper::getFormattedDateObject($license->updated_at, 'datetime'),
|
||||||
'deleted_at' => Helper::getFormattedDateObject($license->deleted_at, 'datetime'),
|
'deleted_at' => Helper::getFormattedDateObject($license->deleted_at, 'datetime'),
|
||||||
'user_can_checkout' => (bool) ($license->free_seats_count > 0),
|
|
||||||
'disabled' => $license->isInactive(),
|
'disabled' => $license->isInactive(),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -75,7 +75,11 @@ class LicensesTransformer
|
|||||||
'checkin' => Gate::allows('checkin', License::class),
|
'checkin' => Gate::allows('checkin', License::class),
|
||||||
'clone' => Gate::allows('create', License::class),
|
'clone' => Gate::allows('create', License::class),
|
||||||
'update' => Gate::allows('update', License::class),
|
'update' => Gate::allows('update', License::class),
|
||||||
'delete' => (Gate::allows('delete', License::class) && ($license->free_seats_count == $license->seats)) ? true : false,
|
'delete' => $license->isDeletable(),
|
||||||
|
'user_can_checkout' => (bool) (($license->free_seats_count - License::unReassignableCount($license)) > 0),
|
||||||
|
'bulk_selectable' => [
|
||||||
|
'delete' => $license->isDeletable(),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$array += $permissions_array;
|
$array += $permissions_array;
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class LocationsTransformer
|
|||||||
'id' => (int) $location->id,
|
'id' => (int) $location->id,
|
||||||
'name' => e($location->name),
|
'name' => e($location->name),
|
||||||
'image' => ($location->image) ? Storage::disk('public')->url('locations/'.e($location->image)) : null,
|
'image' => ($location->image) ? Storage::disk('public')->url('locations/'.e($location->image)) : null,
|
||||||
|
'qr_code_url' => route('qr_code/common', ['object_type' => 'locations', 'id' => $location->id]),
|
||||||
'address' => ($location->address) ? e($location->address) : null,
|
'address' => ($location->address) ? e($location->address) : null,
|
||||||
'address2' => ($location->address2) ? e($location->address2) : null,
|
'address2' => ($location->address2) ? e($location->address2) : null,
|
||||||
'city' => ($location->city) ? e($location->city) : null,
|
'city' => ($location->city) ? e($location->city) : null,
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Transformers;
|
||||||
|
|
||||||
|
use App\Helpers\Helper;
|
||||||
|
use App\Models\MaintenanceType;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
|
||||||
|
class MaintenanceTypesTransformer
|
||||||
|
{
|
||||||
|
public function transformMaintenanceTypes(Collection $types, int $total): array
|
||||||
|
{
|
||||||
|
$array = [];
|
||||||
|
foreach ($types as $type) {
|
||||||
|
$array[] = self::transformMaintenanceType($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (new DatatablesTransformer)->transformDatatables($array, $total);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transformMaintenanceType(MaintenanceType $type): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => (int) $type->id,
|
||||||
|
'name' => e($type->name),
|
||||||
|
'created_at' => Helper::getFormattedDateObject($type->created_at, 'datetime'),
|
||||||
|
'updated_at' => Helper::getFormattedDateObject($type->updated_at, 'datetime'),
|
||||||
|
'deleted_at' => Helper::getFormattedDateObject($type->deleted_at, 'datetime'),
|
||||||
|
'available_actions' => [
|
||||||
|
'update' => Gate::allows('update', $type),
|
||||||
|
'delete' => $type->isDeletable(),
|
||||||
|
'restore' => Gate::allows('delete', $type),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,6 +82,22 @@ class MaintenancesTransformer
|
|||||||
'id' => (int) $assetmaintenance->adminuser->id,
|
'id' => (int) $assetmaintenance->adminuser->id,
|
||||||
'name' => e($assetmaintenance->adminuser->display_name),
|
'name' => e($assetmaintenance->adminuser->display_name),
|
||||||
] : null,
|
] : null,
|
||||||
|
'maintenance_type' => $assetmaintenance->maintenanceType
|
||||||
|
? e($assetmaintenance->maintenanceType->name)
|
||||||
|
: null,
|
||||||
|
'responsible_party' => ($assetmaintenance->responsibleParty) ? [
|
||||||
|
'id' => (int) $assetmaintenance->responsibleParty->id,
|
||||||
|
'name' => e($assetmaintenance->responsibleParty->display_name),
|
||||||
|
] : null,
|
||||||
|
'checked_out_to_at_creation' => $assetmaintenance->checked_out_to_id ? [
|
||||||
|
'id' => (int) $assetmaintenance->checked_out_to_id,
|
||||||
|
'type' => $assetmaintenance->checked_out_to_type,
|
||||||
|
] : null,
|
||||||
|
'completed_at' => Helper::getFormattedDateObject($assetmaintenance->completed_at, 'datetime'),
|
||||||
|
'completed_by' => ($assetmaintenance->completedByUser) ? [
|
||||||
|
'id' => (int) $assetmaintenance->completedByUser->id,
|
||||||
|
'name' => e($assetmaintenance->completedByUser->display_name),
|
||||||
|
] : null,
|
||||||
'created_at' => Helper::getFormattedDateObject($assetmaintenance->created_at, 'datetime'),
|
'created_at' => Helper::getFormattedDateObject($assetmaintenance->created_at, 'datetime'),
|
||||||
'updated_at' => Helper::getFormattedDateObject($assetmaintenance->updated_at, 'datetime'),
|
'updated_at' => Helper::getFormattedDateObject($assetmaintenance->updated_at, 'datetime'),
|
||||||
'is_warranty' => (bool) $assetmaintenance->is_warranty,
|
'is_warranty' => (bool) $assetmaintenance->is_warranty,
|
||||||
@@ -91,6 +107,7 @@ class MaintenancesTransformer
|
|||||||
$permissions_array['available_actions'] = [
|
$permissions_array['available_actions'] = [
|
||||||
'update' => (Gate::allows('update', Asset::class) && ((($assetmaintenance->asset) && $assetmaintenance->asset->deleted_at == ''))) ? true : false,
|
'update' => (Gate::allows('update', Asset::class) && ((($assetmaintenance->asset) && $assetmaintenance->asset->deleted_at == ''))) ? true : false,
|
||||||
'delete' => Gate::allows('delete', Asset::class),
|
'delete' => Gate::allows('delete', Asset::class),
|
||||||
|
'complete' => Gate::allows('update', Asset::class) && ! $assetmaintenance->completed_at,
|
||||||
];
|
];
|
||||||
|
|
||||||
$array += $permissions_array;
|
$array += $permissions_array;
|
||||||
@@ -128,10 +145,23 @@ class MaintenancesTransformer
|
|||||||
'supplier' => ($assetmaintenance->supplier) ? e($assetmaintenance->supplier?->name) : null,
|
'supplier' => ($assetmaintenance->supplier) ? e($assetmaintenance->supplier?->name) : null,
|
||||||
'url' => ($assetmaintenance->url) ? e($assetmaintenance->url) : null,
|
'url' => ($assetmaintenance->url) ? e($assetmaintenance->url) : null,
|
||||||
'cost' => Helper::formatCurrencyOutput($assetmaintenance->cost),
|
'cost' => Helper::formatCurrencyOutput($assetmaintenance->cost),
|
||||||
|
'maintenance_type' => $assetmaintenance->maintenanceType
|
||||||
|
? e($assetmaintenance->maintenanceType->name)
|
||||||
|
: null,
|
||||||
'asset_maintenance_type' => e($assetmaintenance->asset_maintenance_type),
|
'asset_maintenance_type' => e($assetmaintenance->asset_maintenance_type),
|
||||||
'start_date' => Helper::getFormattedDateObject($assetmaintenance->start_date, 'date'),
|
'start_date' => Helper::getFormattedDateObject($assetmaintenance->start_date, 'date'),
|
||||||
'asset_maintenance_time' => $assetmaintenance->asset_maintenance_time,
|
'asset_maintenance_time' => $assetmaintenance->asset_maintenance_time,
|
||||||
'completion_date' => Helper::getFormattedDateObject($assetmaintenance->completion_date, 'date'),
|
'completion_date' => Helper::getFormattedDateObject($assetmaintenance->completion_date, 'date'),
|
||||||
|
'responsible_party' => ($assetmaintenance->responsibleParty) ? [
|
||||||
|
'id' => (int) $assetmaintenance->responsibleParty->id,
|
||||||
|
'name' => e($assetmaintenance->responsibleParty->display_name),
|
||||||
|
] : null,
|
||||||
|
'checked_out_to_at_creation' => ($assetmaintenance->checkedOutTo) ? e($assetmaintenance->checkedOutTo->display_name) : null,
|
||||||
|
'completed_at' => Helper::getFormattedDateObject($assetmaintenance->completed_at, 'datetime'),
|
||||||
|
'completed_by' => ($assetmaintenance->completedByUser) ? [
|
||||||
|
'id' => (int) $assetmaintenance->completedByUser->id,
|
||||||
|
'name' => e($assetmaintenance->completedByUser->display_name),
|
||||||
|
] : null,
|
||||||
'created_by' => ($assetmaintenance->adminuser) ? e($assetmaintenance->adminuser->display_name) : null,
|
'created_by' => ($assetmaintenance->adminuser) ? e($assetmaintenance->adminuser->display_name) : null,
|
||||||
'created_at' => Helper::getFormattedDateObject($assetmaintenance->created_at, 'datetime'),
|
'created_at' => Helper::getFormattedDateObject($assetmaintenance->created_at, 'datetime'),
|
||||||
'updated_at' => Helper::getFormattedDateObject($assetmaintenance->updated_at, 'datetime'),
|
'updated_at' => Helper::getFormattedDateObject($assetmaintenance->updated_at, 'datetime'),
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ class ManufacturersTransformer
|
|||||||
'update' => (($manufacturer->deleted_at == '') && (Gate::allows('update', Manufacturer::class))),
|
'update' => (($manufacturer->deleted_at == '') && (Gate::allows('update', Manufacturer::class))),
|
||||||
'restore' => (($manufacturer->deleted_at != '') && (Gate::allows('create', Manufacturer::class))),
|
'restore' => (($manufacturer->deleted_at != '') && (Gate::allows('create', Manufacturer::class))),
|
||||||
'delete' => $manufacturer->isDeletable(),
|
'delete' => $manufacturer->isDeletable(),
|
||||||
|
'bulk_selectable' => [
|
||||||
|
'delete' => $manufacturer->isDeletable(),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$array += $permissions_array;
|
$array += $permissions_array;
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ class SuppliersTransformer
|
|||||||
$permissions_array['available_actions'] = [
|
$permissions_array['available_actions'] = [
|
||||||
'update' => Gate::allows('update', Supplier::class),
|
'update' => Gate::allows('update', Supplier::class),
|
||||||
'delete' => (Gate::allows('delete', Supplier::class) && ($supplier->isDeletable())),
|
'delete' => (Gate::allows('delete', Supplier::class) && ($supplier->isDeletable())),
|
||||||
|
'bulk_selectable' => [
|
||||||
|
'delete' => (Gate::allows('delete', Supplier::class) && ($supplier->isDeletable())),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$array += $permissions_array;
|
$array += $permissions_array;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class UsersTransformer
|
|||||||
|
|
||||||
public function transformUser(User $user)
|
public function transformUser(User $user)
|
||||||
{
|
{
|
||||||
|
|
||||||
$role = null;
|
$role = null;
|
||||||
if ($user->isSuperUser()) {
|
if ($user->isSuperUser()) {
|
||||||
$role = 'superadmin';
|
$role = 'superadmin';
|
||||||
@@ -31,6 +30,7 @@ class UsersTransformer
|
|||||||
$array = [
|
$array = [
|
||||||
'id' => (int) $user->id,
|
'id' => (int) $user->id,
|
||||||
'avatar' => e($user->present()->gravatar) ?? null,
|
'avatar' => e($user->present()->gravatar) ?? null,
|
||||||
|
'qr_code_url' => route('qr_code/common', ['object_type' => 'users', 'id' => $user->id]),
|
||||||
'name' => e($user->getFullNameAttribute()) ?? null,
|
'name' => e($user->getFullNameAttribute()) ?? null,
|
||||||
'first_name' => e($user->first_name) ?? null,
|
'first_name' => e($user->first_name) ?? null,
|
||||||
'last_name' => e($user->last_name) ?? null,
|
'last_name' => e($user->last_name) ?? null,
|
||||||
@@ -82,11 +82,17 @@ class UsersTransformer
|
|||||||
'consumables_count' => (int) $user->consumables_count,
|
'consumables_count' => (int) $user->consumables_count,
|
||||||
'manages_users_count' => (int) $user->manages_users_count,
|
'manages_users_count' => (int) $user->manages_users_count,
|
||||||
'manages_locations_count' => (int) $user->manages_locations_count,
|
'manages_locations_count' => (int) $user->manages_locations_count,
|
||||||
'company' => ($user->company) ? [
|
// Legacy field — kept for backward API compatibility; use `companies` for multi-company support.
|
||||||
'id' => (int) $user->company->id,
|
'company' => $user->companies->isNotEmpty() ? [
|
||||||
'name' => e($user->company->name),
|
'id' => (int) $user->companies->first()->id,
|
||||||
'tag_color' => ($user->company->tag_color) ? e($user->company->tag_color) : null,
|
'name' => e($user->companies->first()->name),
|
||||||
|
'tag_color' => ($user->companies->first()->tag_color) ? e($user->companies->first()->tag_color) : null,
|
||||||
] : null,
|
] : null,
|
||||||
|
'companies' => $user->companies->map(fn ($c) => [
|
||||||
|
'id' => (int) $c->id,
|
||||||
|
'name' => e($c->name),
|
||||||
|
'tag_color' => $c->tag_color ? e($c->tag_color) : null,
|
||||||
|
])->values(),
|
||||||
'created_by' => ($user->createdBy) ? [
|
'created_by' => ($user->createdBy) ? [
|
||||||
'id' => (int) $user->createdBy->id,
|
'id' => (int) $user->createdBy->id,
|
||||||
'name' => e($user->createdBy->display_name),
|
'name' => e($user->createdBy->display_name),
|
||||||
@@ -144,6 +150,11 @@ class UsersTransformer
|
|||||||
'last_name' => e($user->last_name),
|
'last_name' => e($user->last_name),
|
||||||
'username' => e($user->username),
|
'username' => e($user->username),
|
||||||
'display_name' => e($user->display_name),
|
'display_name' => e($user->display_name),
|
||||||
|
'companies' => $user->companies->map(fn ($c) => [
|
||||||
|
'id' => (int) $c->id,
|
||||||
|
'name' => e($c->name),
|
||||||
|
'tag_color' => $c->tag_color ? e($c->tag_color) : null,
|
||||||
|
])->values(),
|
||||||
'created_by' => $user->adminuser ? [
|
'created_by' => $user->adminuser ? [
|
||||||
'id' => (int) $user->adminuser->id,
|
'id' => (int) $user->adminuser->id,
|
||||||
'name' => e($user->adminuser->present()->fullName),
|
'name' => e($user->adminuser->present()->fullName),
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ class AccessoryImporter extends ItemImporter
|
|||||||
$this->log('Updating Accessory');
|
$this->log('Updating Accessory');
|
||||||
$this->item['model_number'] = trim($this->findCsvMatch($row, 'model_number'));
|
$this->item['model_number'] = trim($this->findCsvMatch($row, 'model_number'));
|
||||||
$accessory->update($this->sanitizeItemForUpdating($accessory));
|
$accessory->update($this->sanitizeItemForUpdating($accessory));
|
||||||
$accessory->save();
|
// update() already saves the model, no need to call save() again while Model::unguard() is active
|
||||||
|
$accessory->setImported(true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user