diff --git a/.all-contributorsrc b/.all-contributorsrc
index 698e03e21ff3..088a20081ead 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -2590,10 +2590,10 @@
]
},
{
- "login": "QveenSi",
+ "login": "qveensi",
"name": "Yevhenii Huzii",
"avatar_url": "https://avatars.githubusercontent.com/u/19945501?v=4",
- "profile": "https://github.com/QveenSi",
+ "profile": "https://github.com/qveensi",
"contributions": [
"code"
]
@@ -2607,15 +2607,6 @@
"code"
]
},
- {
- "login": "QveenSi",
- "name": "Yevhenii Huzii",
- "avatar_url": "https://avatars.githubusercontent.com/u/19945501?v=4",
- "profile": "https://github.com/QveenSi",
- "contributions": [
- "code"
- ]
- },
{
"login": "chrisweirich",
"name": "Christian Weirich",
@@ -3271,6 +3262,978 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "addex12",
+ "name": "Adugna Gizaw",
+ "avatar_url": "https://avatars.githubusercontent.com/u/18550946?v=4",
+ "profile": "https://orbalia.pythonanywhere.com/",
+ "contributions": [
+ "translation"
+ ]
+ },
+ {
+ "login": "jostrander",
+ "name": "Jesse Ostrander",
+ "avatar_url": "https://avatars.githubusercontent.com/u/760989?v=4",
+ "profile": "https://github.com/jostrander",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "azmcnutt",
+ "name": "James M",
+ "avatar_url": "https://avatars.githubusercontent.com/u/31522486?v=4",
+ "profile": "https://github.com/azmcnutt",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Fiala06",
+ "name": "Fiala06",
+ "avatar_url": "https://avatars.githubusercontent.com/u/5183146?v=4",
+ "profile": "https://github.com/Fiala06",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "ntaylor-86",
+ "name": "Nathan Taylor",
+ "avatar_url": "https://avatars.githubusercontent.com/u/28693782?v=4",
+ "profile": "https://github.com/ntaylor-86",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "fvollmer",
+ "name": "fvollmer",
+ "avatar_url": "https://avatars.githubusercontent.com/u/16699443?v=4",
+ "profile": "https://github.com/fvollmer",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "36864",
+ "name": "36864",
+ "avatar_url": "https://avatars.githubusercontent.com/u/109086466?v=4",
+ "profile": "https://github.com/36864",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "CloCkWeRX",
+ "name": "Daniel O'Connor",
+ "avatar_url": "https://avatars.githubusercontent.com/u/365751?v=4",
+ "profile": "http://clockwerx.blogspot.com/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "BeatSpark",
+ "name": "BeatSpark",
+ "avatar_url": "https://avatars.githubusercontent.com/u/102852568?v=4",
+ "profile": "https://github.com/BeatSpark",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "mrdahbi",
+ "name": "mrdahbi",
+ "avatar_url": "https://avatars.githubusercontent.com/u/59203607?v=4",
+ "profile": "https://github.com/mrdahbi",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "chfsx",
+ "name": "Fabian Schmid",
+ "avatar_url": "https://avatars.githubusercontent.com/u/6661332?v=4",
+ "profile": "http://sr.solutions",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "realchrisolin",
+ "name": "Chris Olin",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1288116?v=4",
+ "profile": "https://www.chrisolin.com",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "mnemonicly",
+ "name": "Dan",
+ "avatar_url": "https://avatars.githubusercontent.com/u/3803132?v=4",
+ "profile": "https://github.com/mnemonicly",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "NebelKreis",
+ "name": "Nebel",
+ "avatar_url": "https://avatars.githubusercontent.com/u/43917728?v=4",
+ "profile": "https://github.com/NebelKreis",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "test1337ahp",
+ "name": "test1337ahp",
+ "avatar_url": "https://avatars.githubusercontent.com/u/132433803?v=4",
+ "profile": "https://github.com/test1337ahp",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "JonathonReinhart",
+ "name": "Jonathon Reinhart",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1916566?v=4",
+ "profile": "https://github.com/JonathonReinhart",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "aranar-pro",
+ "name": "aranar-pro",
+ "avatar_url": "https://avatars.githubusercontent.com/u/484742?v=4",
+ "profile": "https://github.com/aranar-pro",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "phil-flip",
+ "name": "Phil",
+ "avatar_url": "https://avatars.githubusercontent.com/u/27019397?v=4",
+ "profile": "https://github.com/phil-flip",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "fe80",
+ "name": "Steffy Fort",
+ "avatar_url": "https://avatars.githubusercontent.com/u/6473460?v=4",
+ "profile": "https://fe80.fr/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "sorvani",
+ "name": "Jared Busch",
+ "avatar_url": "https://avatars.githubusercontent.com/u/3302372?v=4",
+ "profile": "https://github.com/sorvani",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "seanborg-codethink",
+ "name": "seanborg-codethink",
+ "avatar_url": "https://avatars.githubusercontent.com/u/111956991?v=4",
+ "profile": "https://github.com/seanborg-codethink",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "dkaatz",
+ "name": "dkaatz",
+ "avatar_url": "https://avatars.githubusercontent.com/u/160669961?v=4",
+ "profile": "https://github.com/dkaatz",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "DanielRuf",
+ "name": "Daniel Ruf",
+ "avatar_url": "https://avatars.githubusercontent.com/u/827205?v=4",
+ "profile": "https://threema.id/74SF7MW6?text=",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "ahpaleus",
+ "name": "ahpaleus",
+ "avatar_url": "https://avatars.githubusercontent.com/u/38883201?v=4",
+ "profile": "https://github.com/ahpaleus",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "mink-adao-duy",
+ "name": "Anh DAO-DUY",
+ "avatar_url": "https://avatars.githubusercontent.com/u/22906055?v=4",
+ "profile": "https://github.com/mink-adao-duy",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Serdnad",
+ "name": "Andres Gutierrez",
+ "avatar_url": "https://avatars.githubusercontent.com/u/4723453?v=4",
+ "profile": "https://github.com/Serdnad",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "wewhite",
+ "name": "Warren White",
+ "avatar_url": "https://avatars.githubusercontent.com/u/111083379?v=4",
+ "profile": "https://github.com/wewhite",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "robintemme",
+ "name": "Robin Temme",
+ "avatar_url": "https://avatars.githubusercontent.com/u/2809241?v=4",
+ "profile": "https://robintemme.de/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "herroworrd",
+ "name": "herroworrd",
+ "avatar_url": "https://avatars.githubusercontent.com/u/47008367?v=4",
+ "profile": "https://github.com/herroworrd",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "vicleos",
+ "name": "vicleos",
+ "avatar_url": "https://avatars.githubusercontent.com/u/28558609?v=4",
+ "profile": "https://mubiu.com/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "thinkl33t",
+ "name": "Bob Clough",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1016780?v=4",
+ "profile": "http://thinkl33t.co.uk/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "brandon-bailey",
+ "name": "Brandon Daniel Bailey",
+ "avatar_url": "https://avatars.githubusercontent.com/u/10648463?v=4",
+ "profile": "https://github.com/brandon-bailey",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "marcquark",
+ "name": "Marc Bartelt",
+ "avatar_url": "https://avatars.githubusercontent.com/u/23556080?v=4",
+ "profile": "https://github.com/marcquark",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "manu-crealytics",
+ "name": "manu-crealytics",
+ "avatar_url": "https://avatars.githubusercontent.com/u/18286893?v=4",
+ "profile": "https://github.com/manu-crealytics",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Galaxy102",
+ "name": "Konstantin Köhring",
+ "avatar_url": "https://avatars.githubusercontent.com/u/18245993?v=4",
+ "profile": "https://www.galaxy102.de/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "deloz",
+ "name": "Deloz",
+ "avatar_url": "https://avatars.githubusercontent.com/u/685167?v=4",
+ "profile": "https://deloz.net/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "mbrrg",
+ "name": "Martin Berg",
+ "avatar_url": "https://avatars.githubusercontent.com/u/2682426?v=4",
+ "profile": "https://github.com/mbrrg",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Nothing4You",
+ "name": "Richard Schwab",
+ "avatar_url": "https://avatars.githubusercontent.com/u/3694534?v=4",
+ "profile": "https://github.com/Nothing4You",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "rickheil",
+ "name": "Rick Heil",
+ "avatar_url": "https://avatars.githubusercontent.com/u/8959676?v=4",
+ "profile": "https://rickheil.com/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "rosscdh",
+ "name": "Ross Crawford-d'Heureuse",
+ "avatar_url": "https://avatars.githubusercontent.com/u/397106?v=4",
+ "profile": "https://github.com/rosscdh",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "McG800",
+ "name": "Ryan McGuire",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1621107?v=4",
+ "profile": "https://github.com/McG800",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "SBrown2021",
+ "name": "SBrown2021",
+ "avatar_url": "https://avatars.githubusercontent.com/u/77835667?v=4",
+ "profile": "https://github.com/SBrown2021",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "serkanerip",
+ "name": "Serkan",
+ "avatar_url": "https://avatars.githubusercontent.com/u/8780913?v=4",
+ "profile": "https://github.com/serkanerip",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Shankschn",
+ "name": "Shanks",
+ "avatar_url": "https://avatars.githubusercontent.com/u/63188620?v=4",
+ "profile": "https://www.yudelei.com/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "cendai-mis",
+ "name": "cendai-mis",
+ "avatar_url": "https://avatars.githubusercontent.com/u/198525698?v=4",
+ "profile": "https://github.com/cendai-mis",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "smcpeck",
+ "name": "Shaun McPeck",
+ "avatar_url": "https://avatars.githubusercontent.com/u/8724583?v=4",
+ "profile": "https://smcpeck.github.io/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "snazy2000",
+ "name": "Stephen",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1378836?v=4",
+ "profile": "https://github.com/snazy2000",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Nevets82",
+ "name": "Steven",
+ "avatar_url": "https://avatars.githubusercontent.com/u/4462739?v=4",
+ "profile": "http://nevets82.github.io/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Mateus-Romera",
+ "name": "Mateus Villar",
+ "avatar_url": "https://avatars.githubusercontent.com/u/29017267?v=4",
+ "profile": "https://mateusvillar.com/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "mzack5020",
+ "name": "Matthew Zackschewski",
+ "avatar_url": "https://avatars.githubusercontent.com/u/12749393?v=4",
+ "profile": "https://github.com/mzack5020",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "firefrei",
+ "name": "Matthias Frei",
+ "avatar_url": "https://avatars.githubusercontent.com/u/12660103?v=4",
+ "profile": "https://www.frei.media/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "nticaric",
+ "name": "Nenad Ticaric",
+ "avatar_url": "https://avatars.githubusercontent.com/u/824840?v=4",
+ "profile": "https://github.com/nticaric",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Scorcher",
+ "name": "Nikolay Didenko",
+ "avatar_url": "https://avatars.githubusercontent.com/u/706439?v=4",
+ "profile": "https://github.com/Scorcher",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "nunomaduro",
+ "name": "Nuno Maduro",
+ "avatar_url": "https://avatars.githubusercontent.com/u/5457236?v=4",
+ "profile": "https://nunomaduro.com/sponsorships",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "owalerys",
+ "name": "Oliver Walerys",
+ "avatar_url": "https://avatars.githubusercontent.com/u/8883074?v=4",
+ "profile": "https://tektikhq.com/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "rcmcdonald91",
+ "name": "R. Christian McDonald",
+ "avatar_url": "https://avatars.githubusercontent.com/u/3102039?v=4",
+ "profile": "https://keybase.io/rcmcdonald91",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "nixn",
+ "name": "nix",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1525581?v=4",
+ "profile": "https://nnix.net/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "octobunny",
+ "name": "octobunny",
+ "avatar_url": "https://avatars.githubusercontent.com/u/55462380?v=4",
+ "profile": "https://github.com/octobunny",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "sreyemnayr",
+ "name": "Ryan",
+ "avatar_url": "https://avatars.githubusercontent.com/u/8558670?v=4",
+ "profile": "https://github.com/sreyemnayr",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "p3nj",
+ "name": "p3nj",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1501022?v=4",
+ "profile": "https://benji.ltd/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "timwsuqld",
+ "name": "Tim White",
+ "avatar_url": "https://avatars.githubusercontent.com/u/6201617?v=4",
+ "profile": "https://github.com/timwsuqld",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "yannikp",
+ "name": "yannikp",
+ "avatar_url": "https://avatars.githubusercontent.com/u/22473767?v=4",
+ "profile": "https://github.com/yannikp",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "viclou",
+ "name": "victoria",
+ "avatar_url": "https://avatars.githubusercontent.com/u/20525448?v=4",
+ "profile": "https://github.com/viclou",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "valentyntu",
+ "name": "Valentyn Tulub",
+ "avatar_url": "https://avatars.githubusercontent.com/u/40685314?v=4",
+ "profile": "https://github.com/valentyntu",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Wouter0100",
+ "name": "Wouter van Os",
+ "avatar_url": "https://avatars.githubusercontent.com/u/864520?v=4",
+ "profile": "http://wouter0100.nl/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "xWyatt",
+ "name": "Wyatt Teeter",
+ "avatar_url": "https://avatars.githubusercontent.com/u/3946540?v=4",
+ "profile": "https://www.linkedin.com/in/wyatt-teeter",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "terwey",
+ "name": "Yorick Terweijden",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1596124?v=4",
+ "profile": "https://github.com/terwey",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "bmkalle",
+ "name": "bmkalle",
+ "avatar_url": "https://avatars.githubusercontent.com/u/69298836?v=4",
+ "profile": "https://github.com/bmkalle",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "bricelabelle",
+ "name": "bricelabelle",
+ "avatar_url": "https://avatars.githubusercontent.com/u/28403467?v=4",
+ "profile": "https://github.com/bricelabelle",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "corydlamb",
+ "name": "corydlamb",
+ "avatar_url": "https://avatars.githubusercontent.com/u/97770090?v=4",
+ "profile": "https://github.com/corydlamb",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "splashx",
+ "name": "Diogenes S. Jesus",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1154133?v=4",
+ "profile": "http://twitter.com/splash",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "dkmansion",
+ "name": "D M",
+ "avatar_url": "https://avatars.githubusercontent.com/u/5826629?v=4",
+ "profile": "https://github.com/dkmansion",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Jarli01",
+ "name": "Dustin B",
+ "avatar_url": "https://avatars.githubusercontent.com/u/14837699?v=4",
+ "profile": "https://github.com/Jarli01",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "fabiang",
+ "name": "Fabian Grutschus",
+ "avatar_url": "https://avatars.githubusercontent.com/u/348344?v=4",
+ "profile": "https://github.com/fabiang",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "MelonSmasher",
+ "name": "MelonSmasher",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1491053?v=4",
+ "profile": "https://github.com/MelonSmasher",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "AlexanderWPapyrus",
+ "name": "AlexanderWPapyrus",
+ "avatar_url": "https://avatars.githubusercontent.com/u/80526133?v=4",
+ "profile": "https://github.com/AlexanderWPapyrus",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "disc",
+ "name": "Alexandr Hacicheant",
+ "avatar_url": "https://avatars.githubusercontent.com/u/306231?v=4",
+ "profile": "https://github.com/disc",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "hex128",
+ "name": "Hex",
+ "avatar_url": "https://avatars.githubusercontent.com/u/3032891?v=4",
+ "profile": "https://hex128.io/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "arukompas",
+ "name": "Arunas Skirius",
+ "avatar_url": "https://avatars.githubusercontent.com/u/8697942?v=4",
+ "profile": "https://github.com/arukompas",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "benperiton",
+ "name": "Ben Periton",
+ "avatar_url": "https://avatars.githubusercontent.com/u/104396?v=4",
+ "profile": "https://github.com/benperiton",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "byronwolfman",
+ "name": "Byron Wolfman",
+ "avatar_url": "https://avatars.githubusercontent.com/u/11906832?v=4",
+ "profile": "https://wolfman.dev/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "CalvinSchwartz",
+ "name": "Calvin",
+ "avatar_url": "https://avatars.githubusercontent.com/u/56485508?v=4",
+ "profile": "https://github.com/CalvinSchwartz",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "juanfont",
+ "name": "Juan Font",
+ "avatar_url": "https://avatars.githubusercontent.com/u/181059?v=4",
+ "profile": "https://github.com/juanfont",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "juhotaipale",
+ "name": "Juho Taipale",
+ "avatar_url": "https://avatars.githubusercontent.com/u/13137708?v=4",
+ "profile": "https://github.com/juhotaipale",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "KorvinSzanto",
+ "name": "Korvin Szanto",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1007419?v=4",
+ "profile": "https://github.com/KorvinSzanto",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "sniff122",
+ "name": "Lewis Foster",
+ "avatar_url": "https://avatars.githubusercontent.com/u/8513053?v=4",
+ "profile": "https://lewisfoster.foo/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "loganswartz",
+ "name": "Logan Swartzendruber",
+ "avatar_url": "https://avatars.githubusercontent.com/u/33877541?v=4",
+ "profile": "https://github.com/loganswartz",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "lopezio",
+ "name": "Lorenzo P.",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1156208?v=4",
+ "profile": "https://github.com/lopezio",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "m4us1ne",
+ "name": "Lukas Jung",
+ "avatar_url": "https://avatars.githubusercontent.com/u/33946590?v=4",
+ "profile": "https://github.com/m4us1ne",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "LeafedFox",
+ "name": "Ellie",
+ "avatar_url": "https://avatars.githubusercontent.com/u/10965027?v=4",
+ "profile": "https://leafedfox.xyz/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "gastamper",
+ "name": "GA Stamper",
+ "avatar_url": "https://avatars.githubusercontent.com/u/20960555?v=4",
+ "profile": "https://github.com/gastamper",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "gl-pup",
+ "name": "Guillaume Lefranc",
+ "avatar_url": "https://avatars.githubusercontent.com/u/206553556?v=4",
+ "profile": "https://github.com/gl-pup",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "dasjoe",
+ "name": "Hajo Möller",
+ "avatar_url": "https://avatars.githubusercontent.com/u/733892?v=4",
+ "profile": "https://github.com/dasjoe",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "pottom",
+ "name": "Istvan Basa",
+ "avatar_url": "https://avatars.githubusercontent.com/u/3420063?v=4",
+ "profile": "https://github.com/pottom",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "jjasghar",
+ "name": "JJ Asghar",
+ "avatar_url": "https://avatars.githubusercontent.com/u/810824?v=4",
+ "profile": "https://jjasghar.github.io/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "JemCdo",
+ "name": "James E. Msenga",
+ "avatar_url": "https://avatars.githubusercontent.com/u/40404495?v=4",
+ "profile": "https://github.com/JemCdo",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "jfwiebe",
+ "name": "Jan Felix Wiebe",
+ "avatar_url": "https://avatars.githubusercontent.com/u/6865786?v=4",
+ "profile": "https://github.com/jfwiebe",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "drexljo",
+ "name": "Jo Drexl",
+ "avatar_url": "https://avatars.githubusercontent.com/u/43412008?v=4",
+ "profile": "https://www.nfon.com/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "austinsasko",
+ "name": "Austin Sasko",
+ "avatar_url": "https://avatars.githubusercontent.com/u/4807843?v=4",
+ "profile": "https://github.com/austinsasko",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "JassonCordones",
+ "name": "Jasson",
+ "avatar_url": "https://avatars.githubusercontent.com/u/4875039?v=4",
+ "profile": "http://jassoncordones.github.io",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Tinyblargon",
+ "name": "Okean",
+ "avatar_url": "https://avatars.githubusercontent.com/u/76069640?v=4",
+ "profile": "https://github.com/Tinyblargon",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "amedranogil",
+ "name": "Alejandro Medrano",
+ "avatar_url": "https://avatars.githubusercontent.com/u/6515064?v=4",
+ "profile": "https://www.lst.tfo.upm.es/alejandro-medrano/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "lukaskraic",
+ "name": "Lukas Kraic",
+ "avatar_url": "https://avatars.githubusercontent.com/u/58696401?v=4",
+ "profile": "https://github.com/lukaskraic",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "mckaygerhard",
+ "name": "Герхард PICCORO Lenz McKAY ",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1571724?v=4",
+ "profile": "https://github-readme-stats.vercel.app/api?username=mckaygerhard",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "FlorestanII",
+ "name": "Johannes Pollitt",
+ "avatar_url": "https://avatars.githubusercontent.com/u/15015119?v=4",
+ "profile": "https://github.com/FlorestanII",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "strobelm",
+ "name": "Michael Strobel",
+ "avatar_url": "https://avatars.githubusercontent.com/u/14185442?v=4",
+ "profile": "https://strobelm.de",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "nickwest",
+ "name": "Nicky West",
+ "avatar_url": "https://avatars.githubusercontent.com/u/634790?v=4",
+ "profile": "http://nickwest.me",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "akaspeh1",
+ "name": "akaspeh1",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1347327?v=4",
+ "profile": "https://github.com/akaspeh1",
+ "contributions": [
+ "code"
+ ]
}
]
}
diff --git a/.env.dev.docker b/.env.dev.docker
index 308a6ce0b1df..f47e69ce4be8 100644
--- a/.env.dev.docker
+++ b/.env.dev.docker
@@ -16,7 +16,7 @@ APP_DEBUG=true
APP_KEY=base64:3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ=
APP_URL=http://localhost:8000
APP_TIMEZONE='UTC'
-APP_LOCALE=en
+APP_LOCALE=en-US
MAX_RESULTS=500
# --------------------------------------------
@@ -35,6 +35,7 @@ DB_USERNAME=snipeit
DB_PASSWORD=changeme1234
DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
+DB_DUMP_SKIP_SSL=true
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
@@ -78,6 +79,13 @@ MAIL_BACKUP_NOTIFICATION_DRIVER=null
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true
+# --------------------------------------------
+# OPTIONAL: CHANGE PHP UPLOAD LIMITS (UNCOMMENT WHEN NEEDING TO BE CHANGED)
+# --------------------------------------------
+#PHP_UPLOAD_LIMIT=10
+#PHP_POST_MAX_SIZE=10
+#PHP_UPLOAD_MAX_FILESIZE=10
+#PHP_MEMORY_LIMIT=10
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
diff --git a/.env.docker b/.env.docker
index b6c1e52231fa..9eae34385ed7 100644
--- a/.env.docker
+++ b/.env.docker
@@ -28,6 +28,7 @@ PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=db
+DB_SOCKET=null
DB_PORT='3306'
DB_DATABASE=snipeit
DB_USERNAME=snipeit
@@ -35,6 +36,7 @@ DB_PASSWORD=changeme1234
MYSQL_ROOT_PASSWORD=changeme1234
DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
+DB_DUMP_SKIP_SSL=true
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
@@ -83,6 +85,15 @@ MAIL_BACKUP_NOTIFICATION_DRIVER=null
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true
+# --------------------------------------------
+# OPTIONAL: CHANGE PHP UPLOAD LIMITS (UNCOMMENT WHEN NEEDING TO BE CHANGED)
+# --------------------------------------------
+#PHP_UPLOAD_LIMIT=10
+#PHP_POST_MAX_SIZE=10
+#PHP_UPLOAD_MAX_FILESIZE=10
+#PHP_MEMORY_LIMIT=10
+
+
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
# --------------------------------------------
@@ -158,6 +169,7 @@ AWS_DEFAULT_REGION=null
LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_DURATION=60
RESET_PASSWORD_LINK_EXPIRES=900
+INVITE_PASSWORD_LINK_EXPIRES=1500
# --------------------------------------------
# OPTIONAL: MISC
diff --git a/.env.example b/.env.example
index fe6ff8a8a049..6e423d4fe27a 100644
--- a/.env.example
+++ b/.env.example
@@ -24,12 +24,14 @@ PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
+DB_SOCKET=null
DB_PORT=3306
DB_DATABASE=null
DB_USERNAME=null
DB_PASSWORD=null
DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
+DB_DUMP_SKIP_SSL=false
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
DB_SANITIZE_BY_DEFAULT=false
@@ -99,7 +101,7 @@ PASSPORT_COOKIE_NAME='snipeit_passport_token'
COOKIE_DOMAIN=null
SECURE_COOKIES=false
API_TOKEN_EXPIRATION_YEARS=15
-BS_TABLE_STORAGE=cookieStorage
+BS_TABLE_STORAGE=localStorage
BS_TABLE_DEEPLINK=true
# --------------------------------------------
@@ -173,11 +175,13 @@ LOGIN_AUTOCOMPLETE=false
RESET_PASSWORD_LINK_EXPIRES=15
PASSWORD_CONFIRM_TIMEOUT=10800
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
+INVITE_PASSWORD_LINK_EXPIRES=1500
# --------------------------------------------
# OPTIONAL: MISC
# --------------------------------------------
LOG_CHANNEL=single
+LOG_DEPRECATIONS=false
LOG_MAX_DAYS=10
APP_LOCKED=false
APP_CIPHER=AES-256-CBC
@@ -189,11 +193,17 @@ LDAP_TIME_LIM=600
IMPORT_TIME_LIMIT=600
IMPORT_MEMORY_LIMIT=500M
REPORT_TIME_LIMIT=12000
-REQUIRE_SAML=false
API_THROTTLE_PER_MINUTE=120
CSV_ESCAPE_FORMULAS=true
LIVEWIRE_URL_PREFIX=null
+
+# --------------------------------------------
+# OPTIONAL: SAML SETTINGS
+# --------------------------------------------
+REQUIRE_SAML=false
+SAML_KEY_SIZE=2048
+
# --------------------------------------------
# OPTIONAL: HASHING
# --------------------------------------------
diff --git a/.github/workflows/SA-codeql.yml b/.github/workflows/SA-codeql.yml
index 29f3e1b1f1ac..007d07d37a88 100644
--- a/.github/workflows/SA-codeql.yml
+++ b/.github/workflows/SA-codeql.yml
@@ -26,7 +26,7 @@ jobs:
language: [ 'javascript' ]
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
diff --git a/.github/workflows/codacy-analysis.yml b/.github/workflows/codacy-analysis.yml
index e3e935642593..fee3bab64c01 100644
--- a/.github/workflows/codacy-analysis.yml
+++ b/.github/workflows/codacy-analysis.yml
@@ -10,10 +10,10 @@ name: Codacy Security Scan
on:
push:
- branches: [ master ]
+ branches: [ develop ]
pull_request:
# The branches below must be a subset of the branches above
- branches: [ master ]
+ branches: [ develop ]
schedule:
- cron: '36 23 * * 3'
@@ -22,21 +22,21 @@ permissions:
jobs:
codacy-security-scan:
- # Ensure schedule job never runs on forked repos. It's only executed for 'snipe/snipe-it'
+ # Ensure schedule job never runs on forked repos. It's only executed for 'grokability/snipe-it'
permissions:
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
- if: (github.repository == 'snipe/snipe-it') || ((github.repository != 'snipe/snipe-it') && (github.event_name != 'schedule'))
+ if: (github.repository == 'grokability/snipe-it') || ((github.repository != 'grokability/snipe-it') && (github.event_name != 'schedule'))
name: Codacy Security Scan
runs-on: ubuntu-latest
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
- name: Run Codacy Analysis CLI
- uses: codacy/codacy-analysis-cli-action@v4.4.5
+ uses: codacy/codacy-analysis-cli-action@v4.4.7
with:
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
# You can also omit the token and run the tools that support default configurations
diff --git a/.github/workflows/crowdin-upload.yml b/.github/workflows/crowdin-upload.yml
index 7b9331c97d04..b2e798d56255 100644
--- a/.github/workflows/crowdin-upload.yml
+++ b/.github/workflows/crowdin-upload.yml
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Crowdin push
uses: crowdin/github-action@v2
diff --git a/.github/workflows/docker-alpine.yml b/.github/workflows/docker-alpine.yml
index bd46f9567b82..a65d48d2e2f1 100644
--- a/.github/workflows/docker-alpine.yml
+++ b/.github/workflows/docker-alpine.yml
@@ -20,8 +20,8 @@ permissions:
jobs:
docker:
- # Ensure this job never runs on forked repos. It's only executed for 'snipe/snipe-it'
- if: github.repository == 'snipe/snipe-it'
+ # Ensure this job never runs on forked repos. It's only executed for 'grokability/snipe-it'
+ if: github.repository == 'grokability/snipe-it'
runs-on: ubuntu-latest
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
@@ -32,7 +32,7 @@ jobs:
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine
type=ref,event=tag,suffix=-alpine
- type=semver,pattern=v{{major}}-latest-alpine
+ type=semver,pattern=v{{major}}-latest-alpine
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default.
@@ -42,7 +42,7 @@ jobs:
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
diff --git a/.github/workflows/docker-ubuntu.yml b/.github/workflows/docker-ubuntu.yml
new file mode 100644
index 000000000000..c345105b7eaf
--- /dev/null
+++ b/.github/workflows/docker-ubuntu.yml
@@ -0,0 +1,86 @@
+# Snipe-IT Docker image build for hub.docker.com
+name: Docker images (Ubuntu)
+
+# Run this Build for all pushes to 'master' or develop branch, or tagged releases.
+# Also run for PRs to ensure PR doesn't break Docker build process
+on:
+ push:
+ branches:
+ - master
+ - develop
+ tags:
+ - 'v**'
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ docker:
+ # Ensure this job never runs on forked repos. It's only executed for 'grokability/snipe-it'
+ if: github.repository == 'grokability/snipe-it'
+ runs-on: ubuntu-latest
+ env:
+ # Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
+ # For a new commit on default branch (master), use the literal tag 'latest' on Docker image.
+ # For a new commit on other branches, use the branch name as the tag for Docker image.
+ # For a new tag, copy that tag name as the tag for Docker image.
+ IMAGE_TAGS: |
+ type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
+ type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
+ type=ref,event=tag
+ type=semver,pattern=v{{major}}-latest
+ # Define default tag "flavor" for docker/metadata-action per
+ # https://github.com/docker/metadata-action#flavor-input
+ # We turn off 'latest' tag by default.
+ TAGS_FLAVOR: |
+ latest=false
+
+ steps:
+ # https://github.com/actions/checkout
+ - name: Checkout codebase
+ uses: actions/checkout@v5
+
+ # https://github.com/docker/setup-buildx-action
+ - name: Setup Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ # https://github.com/docker/login-action
+ - name: Login to DockerHub
+ # Only login if not a PR, as PRs only trigger a Docker build and not a push
+ if: github.event_name != 'pull_request'
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
+
+ ###############################################
+ # Build/Push the 'snipe/snipe-it' image
+ ###############################################
+ # https://github.com/docker/metadata-action
+ # Get Metadata for docker_build step below
+ - name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
+ id: meta_build
+ uses: docker/metadata-action@v5
+ with:
+ images: snipe/snipe-it
+ tags: ${{ env.IMAGE_TAGS }}
+ flavor: ${{ env.TAGS_FLAVOR }}
+
+ # https://github.com/docker/build-push-action
+ - name: Build and push 'snipe-it' image
+ id: docker_build
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: ./Dockerfile
+ platforms: linux/amd64,linux/arm64
+ # For pull requests, we run the Docker build (to ensure no PR changes break the build),
+ # but we ONLY do an image push to DockerHub if it's NOT a PR
+ push: ${{ github.event_name != 'pull_request' }}
+ # Use tags / labels provided by 'docker/metadata-action' above
+ tags: ${{ steps.meta_build.outputs.tags }}
+ labels: ${{ steps.meta_build.outputs.labels }}
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
deleted file mode 100644
index adb87f3a5da5..000000000000
--- a/.github/workflows/docker.yml
+++ /dev/null
@@ -1,86 +0,0 @@
-# Snipe-IT Docker image build for hub.docker.com
-name: Docker images
-
-# Run this Build for all pushes to 'master' or develop branch, or tagged releases.
-# Also run for PRs to ensure PR doesn't break Docker build process
-on:
- push:
- branches:
- - master
- - develop
- tags:
- - 'v**'
- # Allows you to run this workflow manually from the Actions tab
- workflow_dispatch:
-
- pull_request:
-
-permissions:
- contents: read
-
-jobs:
- docker:
- # Ensure this job never runs on forked repos. It's only executed for 'snipe/snipe-it'
- if: github.repository == 'snipe/snipe-it'
- runs-on: ubuntu-latest
- env:
- # Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
- # For a new commit on default branch (master), use the literal tag 'latest' on Docker image.
- # For a new commit on other branches, use the branch name as the tag for Docker image.
- # For a new tag, copy that tag name as the tag for Docker image.
- IMAGE_TAGS: |
- type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
- type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
- type=ref,event=tag
- type=semver,pattern=v{{major}}-latest
- # Define default tag "flavor" for docker/metadata-action per
- # https://github.com/docker/metadata-action#flavor-input
- # We turn off 'latest' tag by default.
- TAGS_FLAVOR: |
- latest=false
-
- steps:
- # https://github.com/actions/checkout
- - name: Checkout codebase
- uses: actions/checkout@v4
-
- # https://github.com/docker/setup-buildx-action
- - name: Setup Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- # https://github.com/docker/login-action
- - name: Login to DockerHub
- # Only login if not a PR, as PRs only trigger a Docker build and not a push
- if: github.event_name != 'pull_request'
- uses: docker/login-action@v3
- with:
- username: ${{ secrets.DOCKER_USERNAME }}
- password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
-
- ###############################################
- # Build/Push the 'snipe/snipe-it' image
- ###############################################
- # https://github.com/docker/metadata-action
- # Get Metadata for docker_build step below
- - name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
- id: meta_build
- uses: docker/metadata-action@v5
- with:
- images: snipe/snipe-it
- tags: ${{ env.IMAGE_TAGS }}
- flavor: ${{ env.TAGS_FLAVOR }}
-
- # https://github.com/docker/build-push-action
- - name: Build and push 'snipe-it' image
- id: docker_build
- uses: docker/build-push-action@v6
- with:
- context: .
- file: ./Dockerfile
- platforms: linux/amd64,linux/arm64
- # For pull requests, we run the Docker build (to ensure no PR changes break the build),
- # but we ONLY do an image push to DockerHub if it's NOT a PR
- push: ${{ github.event_name != 'pull_request' }}
- # Use tags / labels provided by 'docker/metadata-action' above
- tags: ${{ steps.meta_build.outputs.tags }}
- labels: ${{ steps.meta_build.outputs.labels }}
diff --git a/.github/workflows/dockerhub-description.yml b/.github/workflows/dockerhub-description.yml
index f9064dec95e0..8b5782339b5d 100644
--- a/.github/workflows/dockerhub-description.yml
+++ b/.github/workflows/dockerhub-description.yml
@@ -11,7 +11,7 @@ jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Docker Hub Description
uses: grokability/dockerhub-description@7ea9d275c7cdbe2b676a093a0308c50665e3b8b4
diff --git a/.github/workflows/tests-mysql.yml b/.github/workflows/tests-mysql.yml
index 310414cda68a..bc1c9275bb4a 100644
--- a/.github/workflows/tests-mysql.yml
+++ b/.github/workflows/tests-mysql.yml
@@ -25,9 +25,9 @@ jobs:
fail-fast: false
matrix:
php-version:
- - "8.1"
- "8.2"
- "8.3"
+ - "8.4"
name: PHP ${{ matrix.php-version }}
@@ -37,7 +37,7 @@ jobs:
php-version: "${{ matrix.php-version }}"
coverage: none
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Get Composer Cache Directory
id: composer-cache
@@ -67,7 +67,7 @@ jobs:
run: |
php artisan key:generate
php artisan migrate --force
- php artisan passport:install
+ php artisan passport:install --no-interaction
chmod -R 777 storage bootstrap/cache
- name: Execute tests (Unit and Feature tests) via PHPUnit
@@ -76,4 +76,16 @@ jobs:
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.mysql.ports[3306] }}
DB_USERNAME: root
+ LOG_CHANNEL: single
+ LOG_LEVEL: debug
run: php artisan test
+
+ - name: Upload Laravel logs as artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
+ path: |
+ storage/logs/*.log
+ if-no-files-found: ignore
+ retention-days: 7
diff --git a/.github/workflows/tests-postgres.yml b/.github/workflows/tests-postgres.yml
index ae48277be3e3..5d7d4d00011d 100644
--- a/.github/workflows/tests-postgres.yml
+++ b/.github/workflows/tests-postgres.yml
@@ -21,9 +21,10 @@ jobs:
fail-fast: false
matrix:
php-version:
- - "8.1"
- "8.2"
- "8.3"
+ - "8.4"
+
name: PHP ${{ matrix.php-version }}
@@ -33,7 +34,7 @@ jobs:
php-version: "${{ matrix.php-version }}"
coverage: none
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Get Composer Cache Directory
id: composer-cache
@@ -64,7 +65,7 @@ jobs:
run: |
php artisan key:generate
php artisan migrate --force
- php artisan passport:install
+ php artisan passport:install --no-interaction
chmod -R 777 storage bootstrap/cache
- name: Execute tests (Unit and Feature tests) via PHPUnit
@@ -74,4 +75,16 @@ jobs:
DB_PORT: ${{ job.services.postgresql.ports[5432] }}
DB_USERNAME: snipeit
DB_PASSWORD: password
+ LOG_CHANNEL: single
+ LOG_LEVEL: debug
run: php artisan test
+
+ - name: Upload Laravel logs as artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
+ path: |
+ storage/logs/*.log
+ if-no-files-found: ignore
+ retention-days: 7
diff --git a/.github/workflows/tests-sqlite.yml b/.github/workflows/tests-sqlite.yml
index 220194314fc8..e00e0f73198e 100644
--- a/.github/workflows/tests-sqlite.yml
+++ b/.github/workflows/tests-sqlite.yml
@@ -15,7 +15,7 @@ jobs:
fail-fast: false
matrix:
php-version:
- - "8.1.1"
+ - "8.3"
name: PHP ${{ matrix.php-version }}
@@ -25,7 +25,7 @@ jobs:
php-version: "${{ matrix.php-version }}"
coverage: none
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Get Composer Cache Directory
id: composer-cache
@@ -61,4 +61,16 @@ jobs:
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: sqlite
+ LOG_CHANNEL: single
+ LOG_LEVEL: debug
run: php artisan test
+
+ - name: Upload Laravel logs as artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
+ path: |
+ storage/logs/*.log
+ if-no-files-found: ignore
+ retention-days: 7
diff --git a/.pa11yci.json b/.pa11yci.json
new file mode 100644
index 000000000000..73b84a8140bd
--- /dev/null
+++ b/.pa11yci.json
@@ -0,0 +1,240 @@
+{
+ "standard": "WCAG2AA",
+ "level": "error",
+ "defaults": {
+ "useIncognitoBrowserContext": false,
+ "timeout": 500000,
+ "wait": 5000,
+ "ignore" : [
+ "WCAG2AA.Principle1.Guideline1_4.1_4_3.G145.Fail",
+ "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
+ ],
+
+ "viewport": {
+ "width": 1280,
+ "height": 1024
+ }
+ },
+ "urls": [
+ {
+ "__NOTE" : "this should always be FIRST (if browser context is preserved)",
+ "url": "https://snipe-it.test/login",
+ "actions": [
+ "navigate to https://snipe-it.test/login",
+ "screen capture tests/pa11y/login.png",
+ "set field input[name='username'] to admin",
+ "set field input[name='password'] to password",
+ "click element button[type=submit]",
+ "wait for url to be https://snipe-it.test/",
+ "screen capture tests/pa11y/dashboard.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/admin",
+ "actions" : [
+ "navigate to https://snipe-it.test/admin",
+ "screen capture tests/pa11y/admin-settings.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/admin/branding",
+ "actions" : [
+ "navigate to https://snipe-it.test/admin/branding",
+ "screen capture tests/pa11y/admin-branding.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/admin/general",
+ "actions" : [
+ "navigate to https://snipe-it.test/admin/general",
+ "screen capture tests/pa11y/admin-general.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/hardware/create",
+ "actions" : [
+ "navigate to https://snipe-it.test/hardware/create",
+ "screen capture tests/pa11y/asset-create.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/hardware",
+ "actions" : [
+ "navigate to https://snipe-it.test/hardware",
+ "screen capture tests/pa11y/asset-list.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/hardware/1",
+ "actions" : [
+ "navigate to https://snipe-it.test/hardware/1",
+ "screen capture tests/pa11y/asset-detail.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/account/view-assets",
+ "actions" : [
+ "navigate to https://snipe-it.test/account/view-assets",
+ "screen capture tests/pa11y/profile.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/licences",
+ "actions" : [
+ "navigate to https://snipe-it.test/licenses",
+ "screen capture tests/pa11y/license-list.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/licences/create",
+ "actions" : [
+ "navigate to https://snipe-it.test/licenses/create",
+ "screen capture tests/pa11y/license-create.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/licences/1",
+ "actions" : [
+ "navigate to https://snipe-it.test/licenses/1",
+ "screen capture tests/pa11y/license-view.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/consumables",
+ "actions" : [
+ "navigate to https://snipe-it.test/consumables",
+ "screen capture tests/pa11y/consumable-list.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/consumables/create",
+ "actions" : [
+ "navigate to https://snipe-it.test/consumables/create",
+ "screen capture tests/pa11y/consumable-create.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/consumables/1",
+ "actions" : [
+ "navigate to https://snipe-it.test/consumables/1",
+ "screen capture tests/pa11y/consumable-view.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/accessories",
+ "actions" : [
+ "navigate to https://snipe-it.test/accessories",
+ "screen capture tests/pa11y/accessory-list.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/accessories/create",
+ "actions" : [
+ "navigate to https://snipe-it.test/accessories/create",
+ "screen capture tests/pa11y/accessory-create.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/accessories/1",
+ "actions" : [
+ "navigate to https://snipe-it.test/accessories/1",
+ "screen capture tests/pa11y/accessory-view.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/locations",
+ "actions" : [
+ "navigate to https://snipe-it.test/locations",
+ "screen capture tests/pa11y/location-list.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/locations/create",
+ "actions" : [
+ "navigate to https://snipe-it.test/locations/create",
+ "screen capture tests/pa11y/location-create.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/locations/1",
+ "actions" : [
+ "navigate to https://snipe-it.test/locations/1",
+ "screen capture tests/pa11y/location-view.png"
+ ]
+ },
+
+ {
+ "url" : "https://snipe-it.test/models",
+ "actions" : [
+ "navigate to https://snipe-it.test/models",
+ "screen capture tests/pa11y/model-list.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/models/create",
+ "actions" : [
+ "navigate to https://snipe-it.test/models/create",
+ "screen capture tests/pa11y/model-create.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/models/1",
+ "actions" : [
+ "navigate to https://snipe-it.test/models/1",
+ "screen capture tests/pa11y/model-view.png"
+ ]
+ },
+
+ {
+ "url" : "https://snipe-it.test/companies",
+ "actions" : [
+ "navigate to https://snipe-it.test/companies",
+ "screen capture tests/pa11y/company-list.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/companies/create",
+ "actions" : [
+ "navigate to https://snipe-it.test/companies/create",
+ "screen capture tests/pa11y/company-create.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/companies/1",
+ "actions" : [
+ "navigate to https://snipe-it.test/companies/1",
+ "screen capture tests/pa11y/company-view.png"
+ ]
+ },
+
+ {
+ "url" : "https://snipe-it.test/departments",
+ "actions" : [
+ "navigate to https://snipe-it.test/departments",
+ "screen capture tests/pa11y/department-list.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/departments/create",
+ "actions" : [
+ "navigate to https://snipe-it.test/departments/create",
+ "screen capture tests/pa11y/department-create.png"
+ ]
+ },
+ {
+ "url" : "https://snipe-it.test/departments/1",
+ "actions" : [
+ "navigate to https://snipe-it.test/departments/1",
+ "screen capture tests/pa11y/department-view.png"
+ ]
+ },
+
+ {
+ "url" : "https://snipe-it.test/invalid-url",
+ "actions" : [
+ "navigate to https://snipe-it.test/invalid-url",
+ "screen capture tests/pa11y/404.png"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.upgrade_requirements.json b/.upgrade_requirements.json
index 53d7337b6266..9c4ece093bd6 100644
--- a/.upgrade_requirements.json
+++ b/.upgrade_requirements.json
@@ -3,8 +3,8 @@
"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",
"DOC4": "You should really just ignore it and run upgrade.php. Really",
- "php_min_version": "8.1.0",
- "php_max_major_minor": "8.3",
- "php_max_wontwork": "8.4.0",
- "current_snipeit_version": "7.0"
+ "php_min_version": "8.2.0",
+ "php_max_major_minor": "8.4",
+ "php_max_wontwork": "8.5.0",
+ "current_snipeit_version": "8.0"
}
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 8d94339ba16c..c2011a5423da 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -42,18 +42,33 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [
Evan Taylor](https://github.com/Delta5)
[💻](https://github.com/snipe/snipe-it/commits?author=Delta5 "Code") | [
Petri Asikainen](https://github.com/PetriAsi)
[💻](https://github.com/snipe/snipe-it/commits?author=PetriAsi "Code") | [
derdeagle](https://github.com/derdeagle)
[💻](https://github.com/snipe/snipe-it/commits?author=derdeagle "Code") | [
Mike Frysinger](https://wh0rd.org/)
[💻](https://github.com/snipe/snipe-it/commits?author=vapier "Code") | [
ALPHA](https://github.com/AL4AL)
[💻](https://github.com/snipe/snipe-it/commits?author=AL4AL "Code") | [
FliegenKLATSCH](https://www.ifern.de)
[💻](https://github.com/snipe/snipe-it/commits?author=FliegenKLATSCH "Code") | [
Jeremy Price](https://github.com/jerm)
[💻](https://github.com/snipe/snipe-it/commits?author=jerm "Code") |
| [
Toreg87](https://github.com/Toreg87)
[💻](https://github.com/snipe/snipe-it/commits?author=Toreg87 "Code") | [
Matthew Nickson](https://github.com/Computroniks)
[💻](https://github.com/snipe/snipe-it/commits?author=Computroniks "Code") | [
Jethro Nederhof](https://jethron.id.au)
[💻](https://github.com/snipe/snipe-it/commits?author=jethron "Code") | [
Oskar Stenberg](https://github.com/01ste02)
[💻](https://github.com/snipe/snipe-it/commits?author=01ste02 "Code") | [
Robert-Azelis](https://github.com/Robert-Azelis)
[💻](https://github.com/snipe/snipe-it/commits?author=Robert-Azelis "Code") | [
Alexander William Smith](https://github.com/alwism)
[💻](https://github.com/snipe/snipe-it/commits?author=alwism "Code") | [
LEITWERK AG](https://www.leitwerk.de/)
[💻](https://github.com/snipe/snipe-it/commits?author=leitwerk-ag "Code") |
| [
Adam](http://www.aboutcher.co.uk)
[💻](https://github.com/snipe/snipe-it/commits?author=adamboutcher "Code") | [
Ian](https://snksrv.com)
[💻](https://github.com/snipe/snipe-it/commits?author=sneak-it "Code") | [
Shao Yu-Lung (Allen)](http://blog.bestlong.idv.tw/)
[💻](https://github.com/snipe/snipe-it/commits?author=bestlong "Code") | [
Haxatron](https://github.com/Haxatron)
[💻](https://github.com/snipe/snipe-it/commits?author=Haxatron "Code") | [
PlaneNuts](https://github.com/PlaneNuts)
[💻](https://github.com/snipe/snipe-it/commits?author=PlaneNuts "Code") | [
Bradley Coudriet](http://bjcpgd.cias.rit.edu)
[💻](https://github.com/snipe/snipe-it/commits?author=exula "Code") | [
Dalton Durst](https://daltondur.st)
[💻](https://github.com/snipe/snipe-it/commits?author=UniversalSuperBox "Code") |
-| [
Alex Janes](https://adagiohealth.org)
[💻](https://github.com/snipe/snipe-it/commits?author=adagioajanes "Code") | [
Nuraeil](https://github.com/nuraeil)
[💻](https://github.com/snipe/snipe-it/commits?author=nuraeil "Code") | [
TenOfTens](https://github.com/TenOfTens)
[💻](https://github.com/snipe/snipe-it/commits?author=TenOfTens "Code") | [
waffle](https://ditisjens.be/)
[💻](https://github.com/snipe/snipe-it/commits?author=insert-waffle "Code") | [
Yevhenii Huzii](https://github.com/QveenSi)
[💻](https://github.com/snipe/snipe-it/commits?author=QveenSi "Code") | [
Achmad Fienan Rahardianto](https://github.com/veenone)
[💻](https://github.com/snipe/snipe-it/commits?author=veenone "Code") | [
Yevhenii Huzii](https://github.com/QveenSi)
[💻](https://github.com/snipe/snipe-it/commits?author=QveenSi "Code") |
-| [
Christian Weirich](https://github.com/chrisweirich)
[💻](https://github.com/snipe/snipe-it/commits?author=chrisweirich "Code") | [
denzfarid](https://github.com/denzfarid)
| [
ntbutler-nbcs](https://github.com/ntbutler-nbcs)
[💻](https://github.com/snipe/snipe-it/commits?author=ntbutler-nbcs "Code") | [
Naveen](https://naveensrinivasan.dev)
[💻](https://github.com/snipe/snipe-it/commits?author=naveensrinivasan "Code") | [
Mike Roquemore](https://github.com/mikeroq)
[💻](https://github.com/snipe/snipe-it/commits?author=mikeroq "Code") | [
Daniel Reeder](https://github.com/reederda)
[🌍](#translation-reederda "Translation") [🌍](#translation-reederda "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=reederda "Code") | [
vickyjaura183](https://github.com/vickyjaura183)
[💻](https://github.com/snipe/snipe-it/commits?author=vickyjaura183 "Code") |
-| [
Peace](https://github.com/julian-piehl)
[💻](https://github.com/snipe/snipe-it/commits?author=julian-piehl "Code") | [
Kyle Gordon](https://github.com/kylegordon)
[💻](https://github.com/snipe/snipe-it/commits?author=kylegordon "Code") | [
Katharina Drexel](http://www.bfh.ch)
[💻](https://github.com/snipe/snipe-it/commits?author=sunflowerbofh "Code") | [
David Sferruzza](https://david.sferruzza.fr/)
[💻](https://github.com/snipe/snipe-it/commits?author=dsferruzza "Code") | [
Rick Nelson](https://github.com/rnelsonee)
[💻](https://github.com/snipe/snipe-it/commits?author=rnelsonee "Code") | [
BasO12](https://github.com/BasO12)
[💻](https://github.com/snipe/snipe-it/commits?author=BasO12 "Code") | [
Vautia](https://github.com/Vautia)
[💻](https://github.com/snipe/snipe-it/commits?author=Vautia "Code") |
-| [
Chris Hartjes](http://www.littlehart.net/atthekeyboard)
[💻](https://github.com/snipe/snipe-it/commits?author=chartjes "Code") | [
geo-chen](https://github.com/geo-chen)
[💻](https://github.com/snipe/snipe-it/commits?author=geo-chen "Code") | [
Phan Nguyen](https://github.com/nh314)
[💻](https://github.com/snipe/snipe-it/commits?author=nh314 "Code") | [
Iisakki Jaakkola](https://github.com/StarlessNights)
[💻](https://github.com/snipe/snipe-it/commits?author=StarlessNights "Code") | [
Ikko Ashimine](https://bandism.net/)
[💻](https://github.com/snipe/snipe-it/commits?author=eltociear "Code") | [
Lukas Fehling](https://github.com/lukasfehling)
[💻](https://github.com/snipe/snipe-it/commits?author=lukasfehling "Code") | [
Fernando Almeida](https://github.com/fernando-almeida)
[💻](https://github.com/snipe/snipe-it/commits?author=fernando-almeida "Code") |
-| [
akemidx](https://github.com/akemidx)
[💻](https://github.com/snipe/snipe-it/commits?author=akemidx "Code") | [
Oguz Bilgic](http://oguz.site)
[💻](https://github.com/snipe/snipe-it/commits?author=oguzbilgic "Code") | [
Scooter Crawford](https://github.com/scoo73r)
[💻](https://github.com/snipe/snipe-it/commits?author=scoo73r "Code") | [
subdriven](https://github.com/subdriven)
[💻](https://github.com/snipe/snipe-it/commits?author=subdriven "Code") | [
Andrew Savinykh](https://github.com/AndrewSav)
[💻](https://github.com/snipe/snipe-it/commits?author=AndrewSav "Code") | [
Tadayuki Onishi](https://kenchan0130.github.io)
[💻](https://github.com/snipe/snipe-it/commits?author=kenchan0130 "Code") | [
Florian](https://github.com/floschoepfer)
[💻](https://github.com/snipe/snipe-it/commits?author=floschoepfer "Code") |
-| [
Spencer Long](http://spencerlong.com)
[💻](https://github.com/snipe/snipe-it/commits?author=spencerrlongg "Code") | [
Marcus Moore](https://github.com/marcusmoore)
[💻](https://github.com/snipe/snipe-it/commits?author=marcusmoore "Code") | [
Martin Meredith](https://github.com/Mezzle)
| [
dboth](http://dboth.de)
[💻](https://github.com/snipe/snipe-it/commits?author=dboth "Code") | [
Zachary Fleck](https://github.com/zacharyfleck)
[💻](https://github.com/snipe/snipe-it/commits?author=zacharyfleck "Code") | [
VIKAAS-A](https://github.com/vikaas-cyper)
[💻](https://github.com/snipe/snipe-it/commits?author=vikaas-cyper "Code") | [
Abdul Kareem](https://github.com/ak-piracha)
[💻](https://github.com/snipe/snipe-it/commits?author=ak-piracha "Code") |
-| [
NojoudAlshehri](https://github.com/NojoudAlshehri)
[💻](https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri "Code") | [
Stefan Stidl](https://github.com/stefanstidlffg)
[💻](https://github.com/snipe/snipe-it/commits?author=stefanstidlffg "Code") | [
Quentin Aymard](https://github.com/qay21)
[💻](https://github.com/snipe/snipe-it/commits?author=qay21 "Code") | [
Grant Le Roux](https://github.com/cram42)
[💻](https://github.com/snipe/snipe-it/commits?author=cram42 "Code") | [
Bogdan](http://@singrity)
[💻](https://github.com/snipe/snipe-it/commits?author=Singrity "Code") | [
mmanjos](https://github.com/mmanjos)
[💻](https://github.com/snipe/snipe-it/commits?author=mmanjos "Code") | [
Abdelaziz Faki](https://azooz2014.github.io/)
[💻](https://github.com/snipe/snipe-it/commits?author=Azooz2014 "Code") |
-| [
bilias](https://github.com/bilias)
[💻](https://github.com/snipe/snipe-it/commits?author=bilias "Code") | [
coach1988](https://github.com/coach1988)
[💻](https://github.com/snipe/snipe-it/commits?author=coach1988 "Code") | [
MrM](https://github.com/mauro-miatello)
[💻](https://github.com/snipe/snipe-it/commits?author=mauro-miatello "Code") | [
koiakoia](https://github.com/koiakoia)
[💻](https://github.com/snipe/snipe-it/commits?author=koiakoia "Code") | [
Mustafa Online](https://github.com/mustafa-online)
[💻](https://github.com/snipe/snipe-it/commits?author=mustafa-online "Code") | [
franceslui](https://github.com/franceslui)
[💻](https://github.com/snipe/snipe-it/commits?author=franceslui "Code") | [
Q4kK](https://github.com/Q4kK)
[💻](https://github.com/snipe/snipe-it/commits?author=Q4kK "Code") |
-| [
squintfox](https://github.com/squintfox)
[💻](https://github.com/snipe/snipe-it/commits?author=squintfox "Code") | [
Jeff Clay](https://github.com/jeffclay)
[💻](https://github.com/snipe/snipe-it/commits?author=jeffclay "Code") | [
Phil J R](https://github.com/PP-JN-RL)
[💻](https://github.com/snipe/snipe-it/commits?author=PP-JN-RL "Code") | [
i_virus](https://www.corelight.com/)
[💻](https://github.com/snipe/snipe-it/commits?author=chandanchowdhury "Code") | [
Paul Grime](https://github.com/gitgrimbo)
[💻](https://github.com/snipe/snipe-it/commits?author=gitgrimbo "Code") | [
Lee Porte](https://leeporte.co.uk)
[💻](https://github.com/snipe/snipe-it/commits?author=LeePorte "Code") | [
BRYAN ](https://github.com/bryanlopezinc)
[💻](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Tests") |
-| [
U-H-T](https://github.com/U-H-T)
[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") | [
Matt Tyree](https://github.com/Tyree)
[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [
Florent Bervas](http://spoontux.net)
[💻](https://github.com/snipe/snipe-it/commits?author=FlorentDotMe "Code") | [
Daniel Albertsen](https://ditscheri.com)
[💻](https://github.com/snipe/snipe-it/commits?author=dbakan "Code") | [
r-xyz](https://github.com/r-xyz)
[💻](https://github.com/snipe/snipe-it/commits?author=r-xyz "Code") | [
Steven Mainor](https://github.com/DrekiDegga)
[💻](https://github.com/snipe/snipe-it/commits?author=DrekiDegga "Code") | [
arne-kroeger](https://github.com/arne-kroeger)
[💻](https://github.com/snipe/snipe-it/commits?author=arne-kroeger "Code") |
-| [
Glukose1](https://github.com/Glukose1)
[💻](https://github.com/snipe/snipe-it/commits?author=Glukose1 "Code") | [
Scarzy](https://github.com/Scarzy)
[💻](https://github.com/snipe/snipe-it/commits?author=Scarzy "Code") | [
setpill](https://github.com/setpill)
[💻](https://github.com/snipe/snipe-it/commits?author=setpill "Code") | [
swift2512](https://github.com/swift2512)
[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aswift2512 "Bug reports") | [
Darren Rainey](https://darrenraineys.co.uk)
[💻](https://github.com/snipe/snipe-it/commits?author=DarrenRainey "Code") | [
maciej-poleszczyk](https://github.com/maciej-poleszczyk)
[💻](https://github.com/snipe/snipe-it/commits?author=maciej-poleszczyk "Code") | [
Sebastian Groß](https://github.com/sgross-emlix)
[💻](https://github.com/snipe/snipe-it/commits?author=sgross-emlix "Code") |
-| [
Anouar Touati](https://github.com/AnouarTouati)
[💻](https://github.com/snipe/snipe-it/commits?author=AnouarTouati "Code") | [
aHVzY2g](https://github.com/aHVzY2g)
[💻](https://github.com/snipe/snipe-it/commits?author=aHVzY2g "Code") | [
林博仁 Buo-ren Lin](https://brlin.me)
[💻](https://github.com/snipe/snipe-it/commits?author=brlin-tw "Code") |
+| [
Alex Janes](https://adagiohealth.org)
[💻](https://github.com/snipe/snipe-it/commits?author=adagioajanes "Code") | [
Nuraeil](https://github.com/nuraeil)
[💻](https://github.com/snipe/snipe-it/commits?author=nuraeil "Code") | [
TenOfTens](https://github.com/TenOfTens)
[💻](https://github.com/snipe/snipe-it/commits?author=TenOfTens "Code") | [
waffle](https://ditisjens.be/)
[💻](https://github.com/snipe/snipe-it/commits?author=insert-waffle "Code") | [
Yevhenii Huzii](https://github.com/qveensi)
[💻](https://github.com/snipe/snipe-it/commits?author=qveensi "Code") | [
Achmad Fienan Rahardianto](https://github.com/veenone)
[💻](https://github.com/snipe/snipe-it/commits?author=veenone "Code") | [
Christian Weirich](https://github.com/chrisweirich)
[💻](https://github.com/snipe/snipe-it/commits?author=chrisweirich "Code") |
+| [
denzfarid](https://github.com/denzfarid)
| [
ntbutler-nbcs](https://github.com/ntbutler-nbcs)
[💻](https://github.com/snipe/snipe-it/commits?author=ntbutler-nbcs "Code") | [
Naveen](https://naveensrinivasan.dev)
[💻](https://github.com/snipe/snipe-it/commits?author=naveensrinivasan "Code") | [
Mike Roquemore](https://github.com/mikeroq)
[💻](https://github.com/snipe/snipe-it/commits?author=mikeroq "Code") | [
Daniel Reeder](https://github.com/reederda)
[🌍](#translation-reederda "Translation") [🌍](#translation-reederda "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=reederda "Code") | [
vickyjaura183](https://github.com/vickyjaura183)
[💻](https://github.com/snipe/snipe-it/commits?author=vickyjaura183 "Code") | [
Peace](https://github.com/julian-piehl)
[💻](https://github.com/snipe/snipe-it/commits?author=julian-piehl "Code") |
+| [
Kyle Gordon](https://github.com/kylegordon)
[💻](https://github.com/snipe/snipe-it/commits?author=kylegordon "Code") | [
Katharina Drexel](http://www.bfh.ch)
[💻](https://github.com/snipe/snipe-it/commits?author=sunflowerbofh "Code") | [
David Sferruzza](https://david.sferruzza.fr/)
[💻](https://github.com/snipe/snipe-it/commits?author=dsferruzza "Code") | [
Rick Nelson](https://github.com/rnelsonee)
[💻](https://github.com/snipe/snipe-it/commits?author=rnelsonee "Code") | [
BasO12](https://github.com/BasO12)
[💻](https://github.com/snipe/snipe-it/commits?author=BasO12 "Code") | [
Vautia](https://github.com/Vautia)
[💻](https://github.com/snipe/snipe-it/commits?author=Vautia "Code") | [
Chris Hartjes](http://www.littlehart.net/atthekeyboard)
[💻](https://github.com/snipe/snipe-it/commits?author=chartjes "Code") |
+| [
geo-chen](https://github.com/geo-chen)
[💻](https://github.com/snipe/snipe-it/commits?author=geo-chen "Code") | [
Phan Nguyen](https://github.com/nh314)
[💻](https://github.com/snipe/snipe-it/commits?author=nh314 "Code") | [
Iisakki Jaakkola](https://github.com/StarlessNights)
[💻](https://github.com/snipe/snipe-it/commits?author=StarlessNights "Code") | [
Ikko Ashimine](https://bandism.net/)
[💻](https://github.com/snipe/snipe-it/commits?author=eltociear "Code") | [
Lukas Fehling](https://github.com/lukasfehling)
[💻](https://github.com/snipe/snipe-it/commits?author=lukasfehling "Code") | [
Fernando Almeida](https://github.com/fernando-almeida)
[💻](https://github.com/snipe/snipe-it/commits?author=fernando-almeida "Code") | [
akemidx](https://github.com/akemidx)
[💻](https://github.com/snipe/snipe-it/commits?author=akemidx "Code") |
+| [
Oguz Bilgic](http://oguz.site)
[💻](https://github.com/snipe/snipe-it/commits?author=oguzbilgic "Code") | [
Scooter Crawford](https://github.com/scoo73r)
[💻](https://github.com/snipe/snipe-it/commits?author=scoo73r "Code") | [
subdriven](https://github.com/subdriven)
[💻](https://github.com/snipe/snipe-it/commits?author=subdriven "Code") | [
Andrew Savinykh](https://github.com/AndrewSav)
[💻](https://github.com/snipe/snipe-it/commits?author=AndrewSav "Code") | [
Tadayuki Onishi](https://kenchan0130.github.io)
[💻](https://github.com/snipe/snipe-it/commits?author=kenchan0130 "Code") | [
Florian](https://github.com/floschoepfer)
[💻](https://github.com/snipe/snipe-it/commits?author=floschoepfer "Code") | [
Spencer Long](http://spencerlong.com)
[💻](https://github.com/snipe/snipe-it/commits?author=spencerrlongg "Code") |
+| [
Marcus Moore](https://github.com/marcusmoore)
[💻](https://github.com/snipe/snipe-it/commits?author=marcusmoore "Code") | [
Martin Meredith](https://github.com/Mezzle)
| [
dboth](http://dboth.de)
[💻](https://github.com/snipe/snipe-it/commits?author=dboth "Code") | [
Zachary Fleck](https://github.com/zacharyfleck)
[💻](https://github.com/snipe/snipe-it/commits?author=zacharyfleck "Code") | [
VIKAAS-A](https://github.com/vikaas-cyper)
[💻](https://github.com/snipe/snipe-it/commits?author=vikaas-cyper "Code") | [
Abdul Kareem](https://github.com/ak-piracha)
[💻](https://github.com/snipe/snipe-it/commits?author=ak-piracha "Code") | [
NojoudAlshehri](https://github.com/NojoudAlshehri)
[💻](https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri "Code") |
+| [
Stefan Stidl](https://github.com/stefanstidlffg)
[💻](https://github.com/snipe/snipe-it/commits?author=stefanstidlffg "Code") | [
Quentin Aymard](https://github.com/qay21)
[💻](https://github.com/snipe/snipe-it/commits?author=qay21 "Code") | [
Grant Le Roux](https://github.com/cram42)
[💻](https://github.com/snipe/snipe-it/commits?author=cram42 "Code") | [
Bogdan](http://@singrity)
[💻](https://github.com/snipe/snipe-it/commits?author=Singrity "Code") | [
mmanjos](https://github.com/mmanjos)
[💻](https://github.com/snipe/snipe-it/commits?author=mmanjos "Code") | [
Abdelaziz Faki](https://azooz2014.github.io/)
[💻](https://github.com/snipe/snipe-it/commits?author=Azooz2014 "Code") | [
bilias](https://github.com/bilias)
[💻](https://github.com/snipe/snipe-it/commits?author=bilias "Code") |
+| [
coach1988](https://github.com/coach1988)
[💻](https://github.com/snipe/snipe-it/commits?author=coach1988 "Code") | [
MrM](https://github.com/mauro-miatello)
[💻](https://github.com/snipe/snipe-it/commits?author=mauro-miatello "Code") | [
koiakoia](https://github.com/koiakoia)
[💻](https://github.com/snipe/snipe-it/commits?author=koiakoia "Code") | [
Mustafa Online](https://github.com/mustafa-online)
[💻](https://github.com/snipe/snipe-it/commits?author=mustafa-online "Code") | [
franceslui](https://github.com/franceslui)
[💻](https://github.com/snipe/snipe-it/commits?author=franceslui "Code") | [
Q4kK](https://github.com/Q4kK)
[💻](https://github.com/snipe/snipe-it/commits?author=Q4kK "Code") | [
squintfox](https://github.com/squintfox)
[💻](https://github.com/snipe/snipe-it/commits?author=squintfox "Code") |
+| [
Jeff Clay](https://github.com/jeffclay)
[💻](https://github.com/snipe/snipe-it/commits?author=jeffclay "Code") | [
Phil J R](https://github.com/PP-JN-RL)
[💻](https://github.com/snipe/snipe-it/commits?author=PP-JN-RL "Code") | [
i_virus](https://www.corelight.com/)
[💻](https://github.com/snipe/snipe-it/commits?author=chandanchowdhury "Code") | [
Paul Grime](https://github.com/gitgrimbo)
[💻](https://github.com/snipe/snipe-it/commits?author=gitgrimbo "Code") | [
Lee Porte](https://leeporte.co.uk)
[💻](https://github.com/snipe/snipe-it/commits?author=LeePorte "Code") | [
BRYAN ](https://github.com/bryanlopezinc)
[💻](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Tests") | [
U-H-T](https://github.com/U-H-T)
[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") |
+| [
Matt Tyree](https://github.com/Tyree)
[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [
Florent Bervas](http://spoontux.net)
[💻](https://github.com/snipe/snipe-it/commits?author=FlorentDotMe "Code") | [
Daniel Albertsen](https://ditscheri.com)
[💻](https://github.com/snipe/snipe-it/commits?author=dbakan "Code") | [
r-xyz](https://github.com/r-xyz)
[💻](https://github.com/snipe/snipe-it/commits?author=r-xyz "Code") | [
Steven Mainor](https://github.com/DrekiDegga)
[💻](https://github.com/snipe/snipe-it/commits?author=DrekiDegga "Code") | [
arne-kroeger](https://github.com/arne-kroeger)
[💻](https://github.com/snipe/snipe-it/commits?author=arne-kroeger "Code") | [
Glukose1](https://github.com/Glukose1)
[💻](https://github.com/snipe/snipe-it/commits?author=Glukose1 "Code") |
+| [
Scarzy](https://github.com/Scarzy)
[💻](https://github.com/snipe/snipe-it/commits?author=Scarzy "Code") | [
setpill](https://github.com/setpill)
[💻](https://github.com/snipe/snipe-it/commits?author=setpill "Code") | [
swift2512](https://github.com/swift2512)
[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aswift2512 "Bug reports") | [
Darren Rainey](https://darrenraineys.co.uk)
[💻](https://github.com/snipe/snipe-it/commits?author=DarrenRainey "Code") | [
maciej-poleszczyk](https://github.com/maciej-poleszczyk)
[💻](https://github.com/snipe/snipe-it/commits?author=maciej-poleszczyk "Code") | [
Sebastian Groß](https://github.com/sgross-emlix)
[💻](https://github.com/snipe/snipe-it/commits?author=sgross-emlix "Code") | [
Anouar Touati](https://github.com/AnouarTouati)
[💻](https://github.com/snipe/snipe-it/commits?author=AnouarTouati "Code") |
+| [
aHVzY2g](https://github.com/aHVzY2g)
[💻](https://github.com/snipe/snipe-it/commits?author=aHVzY2g "Code") | [
林博仁 Buo-ren Lin](https://brlin.me)
[💻](https://github.com/snipe/snipe-it/commits?author=brlin-tw "Code") | [
Adugna Gizaw](https://orbalia.pythonanywhere.com/)
[🌍](#translation-addex12 "Translation") | [
Jesse Ostrander](https://github.com/jostrander)
[💻](https://github.com/snipe/snipe-it/commits?author=jostrander "Code") | [
James M](https://github.com/azmcnutt)
[💻](https://github.com/snipe/snipe-it/commits?author=azmcnutt "Code") | [
Fiala06](https://github.com/Fiala06)
[💻](https://github.com/snipe/snipe-it/commits?author=Fiala06 "Code") | [
Nathan Taylor](https://github.com/ntaylor-86)
[💻](https://github.com/snipe/snipe-it/commits?author=ntaylor-86 "Code") |
+| [
fvollmer](https://github.com/fvollmer)
[💻](https://github.com/snipe/snipe-it/commits?author=fvollmer "Code") | [
36864](https://github.com/36864)
[💻](https://github.com/snipe/snipe-it/commits?author=36864 "Code") | [
Daniel O'Connor](http://clockwerx.blogspot.com/)
[💻](https://github.com/snipe/snipe-it/commits?author=CloCkWeRX "Code") | [
BeatSpark](https://github.com/BeatSpark)
[💻](https://github.com/snipe/snipe-it/commits?author=BeatSpark "Code") | [
mrdahbi](https://github.com/mrdahbi)
[💻](https://github.com/snipe/snipe-it/commits?author=mrdahbi "Code") | [
Fabian Schmid](http://sr.solutions)
[💻](https://github.com/snipe/snipe-it/commits?author=chfsx "Code") | [
Chris Olin](https://www.chrisolin.com)
[💻](https://github.com/snipe/snipe-it/commits?author=realchrisolin "Code") |
+| [
Dan](https://github.com/mnemonicly)
[💻](https://github.com/snipe/snipe-it/commits?author=mnemonicly "Code") | [
Nebel](https://github.com/NebelKreis)
[💻](https://github.com/snipe/snipe-it/commits?author=NebelKreis "Code") | [
test1337ahp](https://github.com/test1337ahp)
[💻](https://github.com/snipe/snipe-it/commits?author=test1337ahp "Code") | [
Jonathon Reinhart](https://github.com/JonathonReinhart)
[💻](https://github.com/snipe/snipe-it/commits?author=JonathonReinhart "Code") | [
aranar-pro](https://github.com/aranar-pro)
[💻](https://github.com/snipe/snipe-it/commits?author=aranar-pro "Code") | [
Phil](https://github.com/phil-flip)
[💻](https://github.com/snipe/snipe-it/commits?author=phil-flip "Code") | [
Steffy Fort](https://fe80.fr/)
[💻](https://github.com/snipe/snipe-it/commits?author=fe80 "Code") |
+| [
Jared Busch](https://github.com/sorvani)
[💻](https://github.com/snipe/snipe-it/commits?author=sorvani "Code") | [
seanborg-codethink](https://github.com/seanborg-codethink)
[💻](https://github.com/snipe/snipe-it/commits?author=seanborg-codethink "Code") | [
dkaatz](https://github.com/dkaatz)
[💻](https://github.com/snipe/snipe-it/commits?author=dkaatz "Code") | [
Daniel Ruf](https://threema.id/74SF7MW6?text=)
[💻](https://github.com/snipe/snipe-it/commits?author=DanielRuf "Code") | [
ahpaleus](https://github.com/ahpaleus)
[💻](https://github.com/snipe/snipe-it/commits?author=ahpaleus "Code") | [
Anh DAO-DUY](https://github.com/mink-adao-duy)
[💻](https://github.com/snipe/snipe-it/commits?author=mink-adao-duy "Code") | [
Andres Gutierrez](https://github.com/Serdnad)
[💻](https://github.com/snipe/snipe-it/commits?author=Serdnad "Code") |
+| [
Warren White](https://github.com/wewhite)
[💻](https://github.com/snipe/snipe-it/commits?author=wewhite "Code") | [
Robin Temme](https://robintemme.de/)
[💻](https://github.com/snipe/snipe-it/commits?author=robintemme "Code") | [
herroworrd](https://github.com/herroworrd)
[💻](https://github.com/snipe/snipe-it/commits?author=herroworrd "Code") | [
vicleos](https://mubiu.com/)
[💻](https://github.com/snipe/snipe-it/commits?author=vicleos "Code") | [
Bob Clough](http://thinkl33t.co.uk/)
[💻](https://github.com/snipe/snipe-it/commits?author=thinkl33t "Code") | [
Brandon Daniel Bailey](https://github.com/brandon-bailey)
[💻](https://github.com/snipe/snipe-it/commits?author=brandon-bailey "Code") | [
Marc Bartelt](https://github.com/marcquark)
[💻](https://github.com/snipe/snipe-it/commits?author=marcquark "Code") |
+| [
manu-crealytics](https://github.com/manu-crealytics)
[💻](https://github.com/snipe/snipe-it/commits?author=manu-crealytics "Code") | [
Konstantin Köhring](https://www.galaxy102.de/)
[💻](https://github.com/snipe/snipe-it/commits?author=Galaxy102 "Code") | [
Deloz](https://deloz.net/)
[💻](https://github.com/snipe/snipe-it/commits?author=deloz "Code") | [
Martin Berg](https://github.com/mbrrg)
[💻](https://github.com/snipe/snipe-it/commits?author=mbrrg "Code") | [
Richard Schwab](https://github.com/Nothing4You)
[💻](https://github.com/snipe/snipe-it/commits?author=Nothing4You "Code") | [
Rick Heil](https://rickheil.com/)
[💻](https://github.com/snipe/snipe-it/commits?author=rickheil "Code") | [
Ross Crawford-d'Heureuse](https://github.com/rosscdh)
[💻](https://github.com/snipe/snipe-it/commits?author=rosscdh "Code") |
+| [
Ryan McGuire](https://github.com/McG800)
[💻](https://github.com/snipe/snipe-it/commits?author=McG800 "Code") | [
SBrown2021](https://github.com/SBrown2021)
[💻](https://github.com/snipe/snipe-it/commits?author=SBrown2021 "Code") | [
Serkan](https://github.com/serkanerip)
[💻](https://github.com/snipe/snipe-it/commits?author=serkanerip "Code") | [
Shanks](https://www.yudelei.com/)
[💻](https://github.com/snipe/snipe-it/commits?author=Shankschn "Code") | [
cendai-mis](https://github.com/cendai-mis)
[💻](https://github.com/snipe/snipe-it/commits?author=cendai-mis "Code") | [
Shaun McPeck](https://smcpeck.github.io/)
[💻](https://github.com/snipe/snipe-it/commits?author=smcpeck "Code") | [
Stephen](https://github.com/snazy2000)
[💻](https://github.com/snipe/snipe-it/commits?author=snazy2000 "Code") |
+| [
Steven](http://nevets82.github.io/)
[💻](https://github.com/snipe/snipe-it/commits?author=Nevets82 "Code") | [
Mateus Villar](https://mateusvillar.com/)
[💻](https://github.com/snipe/snipe-it/commits?author=Mateus-Romera "Code") | [
Matthew Zackschewski](https://github.com/mzack5020)
[💻](https://github.com/snipe/snipe-it/commits?author=mzack5020 "Code") | [
Matthias Frei](https://www.frei.media/)
[💻](https://github.com/snipe/snipe-it/commits?author=firefrei "Code") | [
Nenad Ticaric](https://github.com/nticaric)
[💻](https://github.com/snipe/snipe-it/commits?author=nticaric "Code") | [
Nikolay Didenko](https://github.com/Scorcher)
[💻](https://github.com/snipe/snipe-it/commits?author=Scorcher "Code") | [
Nuno Maduro](https://nunomaduro.com/sponsorships)
[💻](https://github.com/snipe/snipe-it/commits?author=nunomaduro "Code") |
+| [
Oliver Walerys](https://tektikhq.com/)
[💻](https://github.com/snipe/snipe-it/commits?author=owalerys "Code") | [
R. Christian McDonald](https://keybase.io/rcmcdonald91)
[💻](https://github.com/snipe/snipe-it/commits?author=rcmcdonald91 "Code") | [
nix](https://nnix.net/)
[💻](https://github.com/snipe/snipe-it/commits?author=nixn "Code") | [
octobunny](https://github.com/octobunny)
[💻](https://github.com/snipe/snipe-it/commits?author=octobunny "Code") | [
Ryan](https://github.com/sreyemnayr)
[💻](https://github.com/snipe/snipe-it/commits?author=sreyemnayr "Code") | [
p3nj](https://benji.ltd/)
[💻](https://github.com/snipe/snipe-it/commits?author=p3nj "Code") | [
Tim White](https://github.com/timwsuqld)
[💻](https://github.com/snipe/snipe-it/commits?author=timwsuqld "Code") |
+| [
yannikp](https://github.com/yannikp)
[💻](https://github.com/snipe/snipe-it/commits?author=yannikp "Code") | [
victoria](https://github.com/viclou)
[💻](https://github.com/snipe/snipe-it/commits?author=viclou "Code") | [
Valentyn Tulub](https://github.com/valentyntu)
[💻](https://github.com/snipe/snipe-it/commits?author=valentyntu "Code") | [
Wouter van Os](http://wouter0100.nl/)
[💻](https://github.com/snipe/snipe-it/commits?author=Wouter0100 "Code") | [
Wyatt Teeter](https://www.linkedin.com/in/wyatt-teeter)
[💻](https://github.com/snipe/snipe-it/commits?author=xWyatt "Code") | [
Yorick Terweijden](https://github.com/terwey)
[💻](https://github.com/snipe/snipe-it/commits?author=terwey "Code") | [
bmkalle](https://github.com/bmkalle)
[💻](https://github.com/snipe/snipe-it/commits?author=bmkalle "Code") |
+| [
bricelabelle](https://github.com/bricelabelle)
[💻](https://github.com/snipe/snipe-it/commits?author=bricelabelle "Code") | [
corydlamb](https://github.com/corydlamb)
[💻](https://github.com/snipe/snipe-it/commits?author=corydlamb "Code") | [
Diogenes S. Jesus](http://twitter.com/splash)
[💻](https://github.com/snipe/snipe-it/commits?author=splashx "Code") | [
D M](https://github.com/dkmansion)
[💻](https://github.com/snipe/snipe-it/commits?author=dkmansion "Code") | [
Dustin B](https://github.com/Jarli01)
[💻](https://github.com/snipe/snipe-it/commits?author=Jarli01 "Code") | [
Fabian Grutschus](https://github.com/fabiang)
[💻](https://github.com/snipe/snipe-it/commits?author=fabiang "Code") | [
MelonSmasher](https://github.com/MelonSmasher)
[💻](https://github.com/snipe/snipe-it/commits?author=MelonSmasher "Code") |
+| [
AlexanderWPapyrus](https://github.com/AlexanderWPapyrus)
[💻](https://github.com/snipe/snipe-it/commits?author=AlexanderWPapyrus "Code") | [
Alexandr Hacicheant](https://github.com/disc)
[💻](https://github.com/snipe/snipe-it/commits?author=disc "Code") | [
Hex](https://hex128.io/)
[💻](https://github.com/snipe/snipe-it/commits?author=hex128 "Code") | [
Arunas Skirius](https://github.com/arukompas)
[💻](https://github.com/snipe/snipe-it/commits?author=arukompas "Code") | [
Ben Periton](https://github.com/benperiton)
[💻](https://github.com/snipe/snipe-it/commits?author=benperiton "Code") | [
Byron Wolfman](https://wolfman.dev/)
[💻](https://github.com/snipe/snipe-it/commits?author=byronwolfman "Code") | [
Calvin](https://github.com/CalvinSchwartz)
[💻](https://github.com/snipe/snipe-it/commits?author=CalvinSchwartz "Code") |
+| [
Juan Font](https://github.com/juanfont)
[💻](https://github.com/snipe/snipe-it/commits?author=juanfont "Code") | [
Juho Taipale](https://github.com/juhotaipale)
[💻](https://github.com/snipe/snipe-it/commits?author=juhotaipale "Code") | [
Korvin Szanto](https://github.com/KorvinSzanto)
[💻](https://github.com/snipe/snipe-it/commits?author=KorvinSzanto "Code") | [
Lewis Foster](https://lewisfoster.foo/)
[💻](https://github.com/snipe/snipe-it/commits?author=sniff122 "Code") | [
Logan Swartzendruber](https://github.com/loganswartz)
[💻](https://github.com/snipe/snipe-it/commits?author=loganswartz "Code") | [
Lorenzo P.](https://github.com/lopezio)
[💻](https://github.com/snipe/snipe-it/commits?author=lopezio "Code") | [
Lukas Jung](https://github.com/m4us1ne)
[💻](https://github.com/snipe/snipe-it/commits?author=m4us1ne "Code") |
+| [
Ellie](https://leafedfox.xyz/)
[💻](https://github.com/snipe/snipe-it/commits?author=LeafedFox "Code") | [
GA Stamper](https://github.com/gastamper)
[💻](https://github.com/snipe/snipe-it/commits?author=gastamper "Code") | [
Guillaume Lefranc](https://github.com/gl-pup)
[💻](https://github.com/snipe/snipe-it/commits?author=gl-pup "Code") | [
Hajo Möller](https://github.com/dasjoe)
[💻](https://github.com/snipe/snipe-it/commits?author=dasjoe "Code") | [
Istvan Basa](https://github.com/pottom)
[💻](https://github.com/snipe/snipe-it/commits?author=pottom "Code") | [
JJ Asghar](https://jjasghar.github.io/)
[💻](https://github.com/snipe/snipe-it/commits?author=jjasghar "Code") | [
James E. Msenga](https://github.com/JemCdo)
[💻](https://github.com/snipe/snipe-it/commits?author=JemCdo "Code") |
+| [
Jan Felix Wiebe](https://github.com/jfwiebe)
[💻](https://github.com/snipe/snipe-it/commits?author=jfwiebe "Code") | [
Jo Drexl](https://www.nfon.com/)
[💻](https://github.com/snipe/snipe-it/commits?author=drexljo "Code") | [
Austin Sasko](https://github.com/austinsasko)
[💻](https://github.com/snipe/snipe-it/commits?author=austinsasko "Code") | [
Jasson](http://jassoncordones.github.io)
[💻](https://github.com/snipe/snipe-it/commits?author=JassonCordones "Code") | [
Okean](https://github.com/Tinyblargon)
[💻](https://github.com/snipe/snipe-it/commits?author=Tinyblargon "Code") | [
Alejandro Medrano](https://www.lst.tfo.upm.es/alejandro-medrano/)
[💻](https://github.com/snipe/snipe-it/commits?author=amedranogil "Code") | [
Lukas Kraic](https://github.com/lukaskraic)
[💻](https://github.com/snipe/snipe-it/commits?author=lukaskraic "Code") |
+| [
Герхард PICCORO Lenz McKAY ](https://github-readme-stats.vercel.app/api?username=mckaygerhard)
[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") | [
Johannes Pollitt](https://github.com/FlorestanII)
[💻](https://github.com/snipe/snipe-it/commits?author=FlorestanII "Code") | [
Michael Strobel](https://strobelm.de)
[💻](https://github.com/snipe/snipe-it/commits?author=strobelm "Code") | [
Nicky West](http://nickwest.me)
[💻](https://github.com/snipe/snipe-it/commits?author=nickwest "Code") | [
akaspeh1](https://github.com/akaspeh1)
[💻](https://github.com/snipe/snipe-it/commits?author=akaspeh1 "Code") |
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
diff --git a/Dockerfile b/Dockerfile
index bd363ccd18c0..b4c4e8a35751 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,8 +1,8 @@
-FROM ubuntu:22.04
+FROM ubuntu:24.04
LABEL maintainer="Brady Wetherington "
# No need to add `apt-get clean` here, reference:
-# - https://github.com/snipe/snipe-it/pull/9201
+# - https://github.com/grokability/snipe-it/pull/9201
# - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#apt-get
RUN export DEBIAN_FRONTEND=noninteractive; \
@@ -14,16 +14,16 @@ RUN export DEBIAN_FRONTEND=noninteractive; \
apt-utils \
apache2 \
apache2-bin \
-libapache2-mod-php8.1 \
-php8.1-curl \
-php8.1-ldap \
-php8.1-mysql \
-php8.1-gd \
-php8.1-xml \
-php8.1-mbstring \
-php8.1-zip \
-php8.1-bcmath \
-php8.1-redis \
+libapache2-mod-php8.3 \
+php8.3-curl \
+php8.3-ldap \
+php8.3-mysql \
+php8.3-gd \
+php8.3-xml \
+php8.3-mbstring \
+php8.3-zip \
+php8.3-bcmath \
+php8.3-redis \
php-memcached \
patch \
curl \
@@ -40,8 +40,7 @@ autoconf \
libc-dev \
libldap-common \
pkg-config \
-libmcrypt-dev \
-php8.1-dev \
+php8.3-dev \
ca-certificates \
unzip \
dnsutils \
@@ -51,18 +50,13 @@ dnsutils \
RUN curl -L -O https://github.com/pear/pearweb_phars/raw/master/go-pear.phar
RUN php go-pear.phar
-RUN pecl install mcrypt
-
-RUN bash -c "echo extension=/usr/lib/php/20210902/mcrypt.so > /etc/php/8.1/mods-available/mcrypt.ini"
-
-RUN phpenmod mcrypt
RUN phpenmod gd
RUN phpenmod bcmath
-RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/8.1/apache2/php.ini
-RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/8.1/cli/php.ini
+RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/8.3/apache2/php.ini
+RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/8.3/cli/php.ini
-RUN useradd -m --uid 1000 --gid 50 docker
+RUN useradd -m --uid 10000 --gid 50 docker
RUN echo export APACHE_RUN_USER=docker >> /etc/apache2/envvars
RUN echo export APACHE_RUN_GROUP=staff >> /etc/apache2/envvars
@@ -116,7 +110,7 @@ COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Get dependencies
USER docker
-RUN composer install --no-dev --working-dir=/var/www/html
+RUN COMPOSER_CACHE_DIR=/dev/null composer install --no-dev --working-dir=/var/www/html && rm -rf /var/www/html/vendor/*/*/.git
USER root
############### APPLICATION INSTALL/INIT #################
diff --git a/Dockerfile.fpm-alpine b/Dockerfile.fpm-alpine
index b7fb27298580..0b896722679d 100644
--- a/Dockerfile.fpm-alpine
+++ b/Dockerfile.fpm-alpine
@@ -70,7 +70,7 @@ COPY --from=composer /usr/bin/composer /usr/local/bin
ARG COMPOSER_ALLOW_SUPERUSER=1
RUN set -eux; \
# Download and extract snipeit tarball
- curl -o snipeit.tar.gz -fL "https://github.com/snipe/snipe-it/archive/v$SNIPEIT_RELEASE.tar.gz"; \
+ curl -o snipeit.tar.gz -fL "https://github.com/grokability/snipe-it/archive/v$SNIPEIT_RELEASE.tar.gz"; \
tar -xzf snipeit.tar.gz --strip-components=1 -C /var/www/html/; \
rm snipeit.tar.gz; \
# Install composer php dependencies
diff --git a/README.md b/README.md
index 7914b1583445..062be60453bc 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,15 @@
-
+
-[](https://crowdin.com/project/snipe-it) [](https://hub.docker.com/r/snipe/snipe-it/) [](https://app.codacy.com/gh/snipe/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [](https://github.com/snipe/snipe-it/actions/workflows/tests.yml)
+[](https://crowdin.com/project/snipe-it) [](https://hub.docker.com/r/snipe/snipe-it/) [](https://app.codacy.com/gh/grokability/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [](https://github.com/grokability/snipe-it/actions/workflows/tests.yml)
[](#contributing) [](https://discord.gg/yZFtShAcKk)
## Snipe-IT - Open Source Asset Management System
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 10](http://laravel.com).
+It is built on [Laravel 11](http://laravel.com).
-Snipe-IT is actively developed and we [release quite frequently](https://github.com/snipe/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/).)
> [!TIP]
> __This is web-based software__. This means there is no executable file (aka no .exe files), and it must be run on a web server and accessed through a web browser. It runs on any Mac OSX, any flavor of Linux, as well as Windows, and we have a [Docker image](https://snipe-it.readme.io/docs/docker) available if that's what you're into.
@@ -44,7 +44,7 @@ For help using Snipe-IT, check out the [user's manual](https://snipe-it.readme.i
-----
### Bug Reports & Feature Requests
-Feel free to check out the [GitHub Issues for this project](https://github.com/snipe/snipe-it/issues) to open a bug report or see what open issues you can help with. Please search through existing issues (open *and* closed) to see if your question has already been answered before opening a new issue.
+Feel free to check out the [GitHub Issues for this project](https://github.com/grokability/snipe-it/issues) to open a bug report or see what open issues you can help with. Please search through existing issues (open *and* closed) to see if your question has already been answered before opening a new issue.
> [!IMPORTANT]
> **PLEASE see the [Getting Help Guidelines](https://snipe-it.readme.io/docs/getting-help) and [Common Issues](https://snipe-it.readme.io/docs/common-issues) before opening a ticket, and be sure to complete all of the questions in the Github Issue template to help us to help you as quickly as possible.**
@@ -76,23 +76,36 @@ Since the release of the JSON REST API, several third-party developers have been
> [!NOTE]
> As these were created by third-parties, Snipe-IT cannot provide support for these project, and you should contact the developers directly if you need assistance. Additionally, Snipe-IT makes no guarantees as to the reliability, accuracy or maintainability of these libraries. Use at your own risk. :)
-- [Python Module](https://github.com/jbloomer/SnipeIT-PythonAPI) by [@jbloomer](https://github.com/jbloomer)
+#### Libraries & Modules
+
- [SnipeSharp - .NET module in C#](https://github.com/barrycarey/SnipeSharp) by [@barrycarey](https://github.com/barrycarey)
-- [InQRy -unmaintained-](https://github.com/Microsoft/InQRy) by [@Microsoft](https://github.com/Microsoft)
- [SnipeitPS](https://github.com/snazy2000/SnipeitPS) by [@snazy2000](https://github.com/snazy2000) - Powershell API Wrapper for Snipe-it
- [jamf2snipe](https://github.com/grokability/jamf2snipe) - Python script to sync assets between a JAMFPro instance and a Snipe-IT instance
- [jamf-snipe-rename](https://macblog.org/jamf-snipe-rename/) - Python script to rename computers in Jamf from Snipe-IT
-- [Marksman](https://github.com/Scope-IT/marksman) - A Windows agent for Snipe-IT
- [Snipe-IT plugin for Jira Service Desk](https://marketplace.atlassian.com/apps/1220964/snipe-it-for-jira)
- [Python 3 CSV importer](https://github.com/gastamper/snipeit-csvimporter) - allows importing assets into Snipe-IT based on Item Name rather than Asset Tag.
- [Snipe-IT Kubernetes Helm Chart](https://github.com/t3n/helm-charts/tree/master/snipeit) - For more information, [click here](https://hub.helm.sh/charts/t3n/snipeit).
- [Snipe-IT Bulk Edit](https://github.com/bricelabelle/snipe-it-bulkedit) - Google Script files to use Google Sheets as a bulk checkout/checkin/edit tool for Snipe-IT.
- [MosyleSnipeSync](https://github.com/RodneyLeeBrands/MosyleSnipeSync) by [@Karpadiem](https://github.com/Karpadiem) - Python script to synchronize information between Mosyle and Snipe-IT.
- [WWW::SnipeIT](https://github.com/SEDC/perl-www-snipeit) by [@SEDC](https://github.com/SEDC) - perl module for accessing the API
-- [UniFi to Snipe-IT](https://github.com/RodneyLeeBrands/UnifiSnipeSync) by [@karpadiem](https://github.com/karpadiem) - Python script that synchronizes UniFi devices with Snipe-IT.
+- [UniFi to Snipe-IT](https://www.edtechirl.com/p/snipe-it-and-azure-asset-management) originally by [@karpadiem](https://github.com/karpadiem) - Python script that synchronizes UniFi devices with Snipe-IT.
- [Kandji2Snipe](https://github.com/grokability/kandji2snipe) by [@briangoldstein](https://github.com/briangoldstein) - Python script that synchronizes Kandji with Snipe-IT.
- [SnipeAgent](https://github.com/ReticentRobot/SnipeAgent) by [@ReticentRobot](https://github.com/ReticentRobot) - Windows agent for Snipe-IT.
- [Gate Pass Generator](https://github.com/cha7uraAE/snipe-it-gate-pass-system) by [@cha7uraAE](https://github.com/cha7uraAE) - A Streamlit application for generating gate passes based on hardware data from a Snipe-IT API.
+- [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
+- [Python Module (archived)](https://github.com/jbloomer/SnipeIT-PythonAPI) by [@jbloomer](https://github.com/jbloomer)
+
+We also have a handful of [Google Apps scripts](https://github.com/grokability/google-apps-scripts-for-snipe-it) to help with various tasks.
+
+#### Mobile Apps
+
+We're currently working on our own mobile app, but in the meantime, check out these third-party apps that work with Snipe-IT:
+
+- [SnipeMate](https://snipemate.app/) (iOS, Google Play, Huawei AppGallery) by Mars Technology
+- [Snipe-Scan](https://apps.apple.com/do/app/snipe-scan/id6744179400?uo=2) (iOS) by Nicolas Maton
+- [Snipe-IT Assets Management](https://play.google.com/store/apps/details?id=com.diegogarciadev.assetsmanager.snipeit&hl=en&pli=1) (Google Play) by DiegoGarciaDEV
+- [AssetX](https://apps.apple.com/my/app/assetx-for-snipe-it/id6741996196?uo=2) (iOS) for Snipe-IT by Rishi Gupta
-----
@@ -120,9 +133,15 @@ The ERD is available [online here](https://drawsql.app/templates/snipe-it).
Be sure to check out all of the [amazing people](CONTRIBUTORS.md) that have contributed to Snipe-IT over the years!
+-----
+
+### Star History
+
+[](https://www.star-history.com/#grokability/snipe-it&Date)
+
------
### Announcement List
-To be notified of important news (such as new releases, security advisories, etc), [sign up for our list](http://eepurl.com/XyZKz). We'll never sell or give away your info, and we'll only email you when it's important.
-
+To be notified of important news (such as new releases, security advisories, etc), [sign up for our list](http://eepurl.com/XyZKz). We'll never sell or give away your info, and we'll only email you when it's important.
+We also usually make smaller announcements on our social accounts, our Discord, and our blog, so be sure to subscribe to those if you're looking for more granular announcements.
diff --git a/SECURITY.md b/SECURITY.md
index e7ea43159205..e052527bd1fd 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -11,6 +11,7 @@ make it impossible to backport security fixes on older versions.
| Version | Supported |
|---------| ------------------ |
+| 8.x | :white_check_mark: |
| 7.x | :white_check_mark: |
| 6.x | :x: |
| 5.1.x | :x: |
diff --git a/Vagrantfile b/Vagrantfile
index 27efef7b1ad8..7c12ed642b1d 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -1,7 +1,7 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
-SNIPEIT_SH_URL= "https://raw.githubusercontent.com/snipe/snipe-it/master/snipeit.sh"
+SNIPEIT_SH_URL= "https://raw.githubusercontent.com/grokability/snipe-it/master/snipeit.sh"
NETWORK_BRIDGE= "en0: Wi-Fi (AirPort)"
Vagrant.configure("2") do |config|
diff --git a/app.json b/app.json
index 0bf523f5e402..24d874114ace 100644
--- a/app.json
+++ b/app.json
@@ -6,7 +6,7 @@
"it asset"
],
"website": "https://snipeitapp.com/",
- "repository": "https://github.com/snipe/snipe-it",
+ "repository": "https://github.com/grokability/snipe-it",
"logo": "https://pbs.twimg.com/profile_images/976748875733020672/K-HnZCCK_400x400.jpg",
"success_url": "/setup",
"env": {
diff --git a/app/Actions/CheckoutRequests/CancelCheckoutRequestAction.php b/app/Actions/CheckoutRequests/CancelCheckoutRequestAction.php
new file mode 100644
index 000000000000..2d6dd68a0837
--- /dev/null
+++ b/app/Actions/CheckoutRequests/CancelCheckoutRequestAction.php
@@ -0,0 +1,48 @@
+cancelRequest();
+
+ $asset->decrement('requests_counter', 1);
+
+ $data['item'] = $asset;
+ $data['target'] = $user;
+ $data['item_quantity'] = 1;
+ $settings = Setting::getSettings();
+
+ $logaction = new Actionlog();
+ $logaction->item_id = $data['asset_id'] = $asset->id;
+ $logaction->item_type = $data['item_type'] = Asset::class;
+ $logaction->created_at = $data['requested_date'] = date('Y-m-d H:i:s');
+ $logaction->target_id = $data['user_id'] = auth()->id();
+ $logaction->target_type = User::class;
+ $logaction->location_id = $user->location_id ?? null;
+ $logaction->logaction('request canceled');
+
+ try {
+ $settings->notify(new RequestAssetCancelation($data));
+ } catch (\Exception $e) {
+ \Log::warning($e);
+ }
+
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/app/Actions/CheckoutRequests/CreateCheckoutRequestAction.php b/app/Actions/CheckoutRequests/CreateCheckoutRequestAction.php
new file mode 100644
index 000000000000..6870cfba2d4f
--- /dev/null
+++ b/app/Actions/CheckoutRequests/CreateCheckoutRequestAction.php
@@ -0,0 +1,54 @@
+find($asset->id))) {
+ throw new AssetNotRequestable($asset);
+ }
+ if (!Company::isCurrentUserHasAccess($asset)) {
+ throw new AuthorizationException();
+ }
+
+ $data['item'] = $asset;
+ $data['target'] = $user;
+ $data['item_quantity'] = 1;
+ $settings = Setting::getSettings();
+
+ $logaction = new Actionlog();
+ $logaction->item_id = $data['asset_id'] = $asset->id;
+ $logaction->item_type = $data['item_type'] = Asset::class;
+ $logaction->created_at = $data['requested_date'] = date('Y-m-d H:i:s');
+ $logaction->target_id = $data['user_id'] = auth()->id();
+ $logaction->target_type = User::class;
+ $logaction->location_id = $user->location_id ?? null;
+ $logaction->logaction('requested');
+
+ $asset->request();
+ $asset->increment('requests_counter', 1);
+ try {
+ $settings->notify(new RequestAssetNotification($data));
+ } catch (\Exception $e) {
+ Log::warning($e);
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/app/Console/Commands/CleanIncorrectCheckoutAcceptances.php b/app/Console/Commands/CleanIncorrectCheckoutAcceptances.php
new file mode 100644
index 000000000000..30dca964c723
--- /dev/null
+++ b/app/Console/Commands/CleanIncorrectCheckoutAcceptances.php
@@ -0,0 +1,68 @@
+withProgressBar(CheckoutAcceptance::all(), function ($checkoutAcceptance) use (&$deletions, &$skips) {
+ $item = $checkoutAcceptance->checkoutable;
+ $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?
+ }
+ if(get_class($item) == LicenseSeat::class) {
+ $item = $item->license;
+ }
+ 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?
+ //I'm picking `created_at` over `action_date` because I'm more interested in when the actionlogs
+ //were _created_, not when they were alleged to have happened - those created_at times need to be within 'X' seconds of
+ //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;
+ } else {
+ //$this->info("The two records are too far apart");
+ }
+ } else {
+ //$this->info("No match! checkout to id: " . $checkout_to_id." target_id: ".$assetlog->target_id." target_type: ".$assetlog->target_type);
+ }
+ }
+ $skips++;
+ });
+ $this->error("Final deletion count: $deletions, and skip count: $skips");
+ }
+}
diff --git a/app/Console/Commands/CleanOldCheckoutRequests.php b/app/Console/Commands/CleanOldCheckoutRequests.php
new file mode 100644
index 000000000000..a96a58a34984
--- /dev/null
+++ b/app/Console/Commands/CleanOldCheckoutRequests.php
@@ -0,0 +1,74 @@
+ function ($query) {
+ $query->withTrashed();
+ },
+ 'requestedItem' => function ($query) {
+ $query->withTrashed();
+ },
+ ])->get();
+
+ $this->info("Processing {$requests->count()} checkout requests");
+
+ $this->withProgressBar($requests, function ($request) {
+ if ($this->shouldForceDelete($request)) {
+ $request->forceDelete();
+ $this->deletions++;
+ return;
+ }
+
+ if ($this->shouldSoftDelete($request)) {
+ $request->delete();
+ $this->deletions++;
+ return;
+ }
+
+ $this->skips++;
+ });
+
+ $this->info("Final deletion count: $this->deletions, and skip count: $this->skips");
+
+ return 0;
+ }
+
+ private function shouldForceDelete(CheckoutRequest $request)
+ {
+ // check if the requestable or user relationship is null
+ return !$request->requestable || !$request->user;
+ }
+
+ private function shouldSoftDelete(CheckoutRequest $request)
+ {
+ return $request->requestable->trashed() || $request->user->trashed();
+ }
+}
diff --git a/app/Console/Commands/DisableSAML.php b/app/Console/Commands/DisableSAML.php
new file mode 100644
index 000000000000..795e65f1c7ec
--- /dev/null
+++ b/app/Console/Commands/DisableSAML.php
@@ -0,0 +1,53 @@
+confirm("\n****************************************************\nThis will disable SAML support. You will not be able \nto login with an account that does not exist \nlocally in the Snipe-IT local database. \n****************************************************\n\nDo you wish to continue? [y|N]")) {
+ $setting = Setting::getSettings();
+ $setting->saml_enabled = 0;
+ if ($setting->save()) {
+ $this->info('SAML has been set to disabled.');
+ } else {
+ $this->info('Unable to disable SAML.');
+ }
+ } else {
+ $this->info('Canceled. No actions taken.');
+ }
+ }
+}
diff --git a/app/Console/Commands/FixBulkAccessoryCheckinActionLogEntries.php b/app/Console/Commands/FixBulkAccessoryCheckinActionLogEntries.php
new file mode 100644
index 000000000000..694d8d3b30ee
--- /dev/null
+++ b/app/Console/Commands/FixBulkAccessoryCheckinActionLogEntries.php
@@ -0,0 +1,151 @@
+skipBackup = $this->option('skip-backup');
+ $this->dryrun = $this->option('dry-run');
+
+ if ($this->dryrun) {
+ $this->info('This is a DRY RUN - no changes will be saved.');
+ $this->newLine();
+ }
+
+ $logs = Actionlog::query()
+ // only look for accessory checkin logs
+ ->where('item_type', Accessory::class)
+ // that were part of a bulk checkin
+ ->where('note', 'Bulk checkin items')
+ // logs that were improperly timestamped should have created_at in the 1970s
+ ->whereYear('created_at', '1970')
+ ->get();
+
+ if ($logs->isEmpty()) {
+ $this->info('No logs found with incorrect timestamps.');
+ return 0;
+ }
+
+ $this->info('Found ' . $logs->count() . ' logs with incorrect timestamps:');
+
+ $this->table(
+ ['ID', 'Created By', 'Created At', 'Updated At'],
+ $logs->map(function ($log) {
+ return [
+ $log->id,
+ $log->created_by,
+ $log->created_at,
+ $log->updated_at,
+ ];
+ })
+ );
+
+ if (!$this->dryrun && !$this->confirm('Update these logs?')) {
+ return 0;
+ }
+
+ if (!$this->dryrun && !$this->skipBackup) {
+ $this->info('Backing up the database before making changes...');
+ $this->call('snipeit:backup');
+ }
+
+ if ($this->dryrun) {
+ $this->newLine();
+ $this->info('DRY RUN. NOT ACTUALLY UPDATING LOGS.');
+ }
+
+ foreach ($logs as $log) {
+ $this->newLine();
+ $this->info('Processing log id:' . $log->id);
+
+ // created_by was not being set for accessory bulk checkins
+ // so let's see if there was another bulk checkin log
+ // with the same timestamp and a created_by value we can use.
+ if (is_null($log->created_by)) {
+ $createdByFromSimilarLog = $this->getCreatedByAttributeFromSimilarLog($log);
+
+ if ($createdByFromSimilarLog) {
+ $this->line(vsprintf('Updating log id:%s created_by to %s', [$log->id, $createdByFromSimilarLog]));
+ $log->created_by = $createdByFromSimilarLog;
+ } else {
+ $this->warn(vsprintf('No created_by found for log id:%s', [$log->id]));
+ $this->warn('Skipping updating this log since no similar log was found to update created_by from.');
+
+ // If we can't find a similar log then let's skip updating it
+ continue;
+ }
+ }
+
+ $this->line(vsprintf('Updating log id:%s from %s to %s', [$log->id, $log->created_at, $log->updated_at]));
+ $log->created_at = $log->updated_at;
+
+ if (!$this->dryrun) {
+ Model::withoutTimestamps(function () use ($log) {
+ $log->saveQuietly();
+ });
+ }
+ }
+
+ $this->newLine();
+
+ if ($this->dryrun) {
+ $this->info('DRY RUN. NO CHANGES WERE ACTUALLY MADE.');
+ }
+
+ return 0;
+ }
+
+ /**
+ * Hopefully the bulk checkin included other items like assets or licenses
+ * so we can use one of those logs to get the correct created_by value.
+ *
+ * This method attempts to find a bulk check in log that was
+ * created at the same time as the log passed in.
+ */
+ private function getCreatedByAttributeFromSimilarLog(Actionlog $log): null|int
+ {
+ $similarLog = Actionlog::query()
+ ->whereNotNull('created_by')
+ ->where([
+ 'action_type' => 'checkin from',
+ 'note' => 'Bulk checkin items',
+ 'target_id' => $log->target_id,
+ 'target_type' => $log->target_type,
+ 'created_at' => $log->updated_at,
+ ])
+ ->first();
+
+ if ($similarLog) {
+ return $similarLog->created_by;
+ }
+
+ return null;
+ }
+}
diff --git a/app/Console/Commands/FixUpAssignedTypeWithoutAssignedTo.php b/app/Console/Commands/FixUpAssignedTypeWithoutAssignedTo.php
new file mode 100644
index 000000000000..f368929f160c
--- /dev/null
+++ b/app/Console/Commands/FixUpAssignedTypeWithoutAssignedTo.php
@@ -0,0 +1,32 @@
+whereNotNull('assigned_type')->whereNull('assigned_to')->update(['assigned_type' => null]);
+ $this->info("Assets with an assigned_type but no assigned_to are fixed");
+ }
+}
diff --git a/app/Console/Commands/GeneratePersonalAccessToken.php b/app/Console/Commands/GeneratePersonalAccessToken.php
index c5f531213cad..63b246978e10 100644
--- a/app/Console/Commands/GeneratePersonalAccessToken.php
+++ b/app/Console/Commands/GeneratePersonalAccessToken.php
@@ -2,11 +2,9 @@
namespace App\Console\Commands;
-use App\Helpers\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Laravel\Passport\TokenRepository;
-use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Support\Facades\DB;
class GeneratePersonalAccessToken extends Command
@@ -43,9 +41,8 @@ class GeneratePersonalAccessToken extends Command
*
* @return void
*/
- public function __construct(TokenRepository $tokenRepository, ValidationFactory $validation)
+ public function __construct(TokenRepository $tokenRepository)
{
- $this->validation = $validation;
$this->tokenRepository = $tokenRepository;
parent::__construct();
}
@@ -76,7 +73,7 @@ public function handle()
} else {
- $this->warn('Your API Token has been created. Be sure to copy this token now, as it will not be accessible again.');
+ $this->warn('Your API Token has been created. Be sure to copy this token now, as it WILL NOT be accessible again.');
if ($token = DB::table('oauth_access_tokens')->where('user_id', '=', $user->id)->where('name','=',$accessTokenName)->orderBy('created_at', 'desc')->first()) {
$this->info('API Token ID: '.$token->id);
diff --git a/app/Console/Commands/LdapSync.php b/app/Console/Commands/LdapSync.php
index c7efa967a4b6..381ae900d974 100644
--- a/app/Console/Commands/LdapSync.php
+++ b/app/Console/Commands/LdapSync.php
@@ -55,6 +55,8 @@ public function handle()
ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); //600 seconds = 10 minutes
ini_set('memory_limit', env('LDAP_MEM_LIM', '500M'));
+
+ // Map the LDAP attributes to the Snipe-IT user fields.
$ldap_map = [
"username" => Setting::getSettings()->ldap_username_field,
"last_name" => Setting::getSettings()->ldap_lname_field,
@@ -63,11 +65,17 @@ public function handle()
"emp_num" => Setting::getSettings()->ldap_emp_num,
"email" => Setting::getSettings()->ldap_email,
"phone" => Setting::getSettings()->ldap_phone_field,
+ "mobile" => Setting::getSettings()->ldap_mobile,
"jobtitle" => Setting::getSettings()->ldap_jobtitle,
+ "address" => Setting::getSettings()->ldap_address,
+ "city" => Setting::getSettings()->ldap_city,
+ "state" => Setting::getSettings()->ldap_state,
+ "zip" => Setting::getSettings()->ldap_zip,
"country" => Setting::getSettings()->ldap_country,
"location" => Setting::getSettings()->ldap_location,
"dept" => Setting::getSettings()->ldap_dept,
"manager" => Setting::getSettings()->ldap_manager,
+ "display_name" => Setting::getSettings()->ldap_display_name,
];
$ldap_default_group = Setting::getSettings()->ldap_default_group;
@@ -125,6 +133,10 @@ public function handle()
*/
$attributes = array_values(array_filter($ldap_map));
+ if (Setting::getSettings()->is_ad === 1 && is_null($ldap_map['active_flag'])) {
+ $attributes[] = 'useraccountcontrol';
+ }
+
$results = Ldap::findLdapUsers($search_base, -1, $filter, $attributes);
} catch (\Exception $e) {
@@ -178,7 +190,7 @@ public function handle()
// Inject location information fields
for ($i = 0; $i < $results['count']; $i++) {
$results[$i]['ldap_location_override'] = false;
- $results[$i]['location_id'] = 0;
+ $results[$i]['location_id'] = null;
}
// Grab subsets based on location-specific DNs, and overwrite location for these users.
@@ -230,9 +242,11 @@ public function handle()
}
+ // Assign the mapped LDAP attributes for each user to the Snipe-IT user fields
for ($i = 0; $i < $results['count']; $i++) {
$item = [];
$item['username'] = $results[$i][$ldap_map["username"]][0] ?? '';
+ $item['display_name'] = $results[$i][$ldap_map["display_name"]][0] ?? '';
$item['employee_number'] = $results[$i][$ldap_map["emp_num"]][0] ?? '';
$item['lastname'] = $results[$i][$ldap_map["last_name"]][0] ?? '';
$item['firstname'] = $results[$i][$ldap_map["first_name"]][0] ?? '';
@@ -240,8 +254,13 @@ public function handle()
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? '';
$item['location_id'] = $results[$i]['location_id'] ?? '';
$item['telephone'] = $results[$i][$ldap_map["phone"]][0] ?? '';
+ $item['mobile'] = $results[$i][$ldap_map["mobile"]][0] ?? '';
$item['jobtitle'] = $results[$i][$ldap_map["jobtitle"]][0] ?? '';
+ $item['address'] = $results[$i][$ldap_map["address"]][0] ?? '';
+ $item['city'] = $results[$i][$ldap_map["city"]][0] ?? '';
+ $item['state'] = $results[$i][$ldap_map["state"]][0] ?? '';
$item['country'] = $results[$i][$ldap_map["country"]][0] ?? '';
+ $item['zip'] = $results[$i][$ldap_map["zip"]][0] ?? '';
$item['department'] = $results[$i][$ldap_map["dept"]][0] ?? '';
$item['manager'] = $results[$i][$ldap_map["manager"]][0] ?? '';
$item['location'] = $results[$i][$ldap_map["location"]][0] ?? '';
@@ -274,6 +293,9 @@ public function handle()
if($ldap_map["username"] != null){
$user->username = $item['username'];
}
+ if($ldap_map["display_name"] != null){
+ $user->display_name = $item['display_name'];
+ }
if($ldap_map["last_name"] != null){
$user->last_name = $item['lastname'];
}
@@ -289,6 +311,9 @@ public function handle()
if($ldap_map["phone"] != null){
$user->phone = $item['telephone'];
}
+ if($ldap_map["mobile"] != null){
+ $user->mobile = $item['mobile'];
+ }
if($ldap_map["jobtitle"] != null){
$user->jobtitle = $item['jobtitle'];
}
@@ -357,9 +382,15 @@ public function handle()
// (Specifically, we don't handle a value of '0.0' correctly)
$raw_value = @$results[$i][$ldap_map["active_flag"]][0];
$filter_var = filter_var($raw_value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
+
$boolean_cast = (bool) $raw_value;
-
- $user->activated = $filter_var ?? $boolean_cast; // if filter_var() was true or false, use that. If it's null, use the $boolean_cast
+
+ if (Setting::getSettings()->ldap_invert_active_flag === 1) {
+ // Because ldap_active_flag is set, if filter_var is true or boolean_cast is true, then user is suspended
+ $user->activated = !($filter_var ?? $boolean_cast);
+ }else{
+ $user->activated = $filter_var ?? $boolean_cast; // if filter_var() was true or false, use that. If it's null, use the $boolean_cast
+ }
} elseif (array_key_exists('useraccountcontrol', $results[$i])) {
// ....otherwise, (ie if no 'active' LDAP flag is defined), IF the UAC setting exists,
@@ -424,8 +455,12 @@ public function handle()
$item['note'] = $item['createorupdate'];
$item['status'] = 'success';
if ($item['createorupdate'] === 'created' && $ldap_default_group) {
- $user->groups()->attach($ldap_default_group);
+ // Check if the relationship already exists
+ if (!$user->groups()->where('group_id', $ldap_default_group)->exists()) {
+ $user->groups()->attach($ldap_default_group);
+ }
}
+
//updates assets location based on user's location
if ($user->wasChanged('location_id')) {
foreach ($user->assets as $asset) {
diff --git a/app/Console/Commands/LdapTroubleshooter.php b/app/Console/Commands/LdapTroubleshooter.php
index 5bb3cdd366c2..cb19ff8c5381 100644
--- a/app/Console/Commands/LdapTroubleshooter.php
+++ b/app/Console/Commands/LdapTroubleshooter.php
@@ -6,6 +6,7 @@
use App\Models\Setting;
use Exception;
use Illuminate\Support\Facades\Crypt;
+use App\Models\Ldap;
/**
* Check if a given ip is in a network
@@ -160,7 +161,15 @@ public function handle()
$output[] = "-x";
$output[] = "-b ".escapeshellarg($settings->ldap_basedn);
$output[] = "-D ".escapeshellarg($settings->ldap_uname);
- $output[] = "-w ".escapeshellarg(Crypt::Decrypt($settings->ldap_pword));
+
+ try {
+ $w = Crypt::Decrypt($settings->ldap_pword);
+ } catch (\Exception $e) {
+ $this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
+ exit(0);
+ }
+
+ $output[] = "-w ". escapeshellarg($w);
$output[] = escapeshellarg(parenthesized_filter($settings->ldap_filter));
if($settings->ldap_tls) {
$this->line("# adding STARTTLS option");
@@ -171,6 +180,23 @@ public function handle()
$this->line(implode(" \\\n",$output));
exit(0);
}
+
+ //PHP Version check for warning
+ $php_version = phpversion();
+ list($major, $minor, $patch) = explode('.', $php_version);
+ if (
+ $major < 8 ||
+ ($major == 8 && $minor < 3) ||
+ ($major == 8 && $minor == 3 && $patch < 21) ||
+ ($major == 8 && $minor == 4 && $patch < 7)
+ ) {
+ $this->warn("PHP Version: $php_version WARNING - Versions before 8.3.21 or 8.4.7 will return INCONSISTENT results!");
+ if (!$this->confirm("Are you sure you wish to continue?")) {
+ $this->warn("ABORTING");
+ exit(-1);
+ }
+ }
+
if(!$this->option('force')) {
$confirmation = $this->confirm('WARNING: This command will make several attempts to connect to your LDAP server. Are you sure this is ok?');
if(!$confirmation) {
@@ -179,7 +205,7 @@ public function handle()
}
}
//$this->line(print_r($settings,true));
- $this->info("STAGE 1: Checking settings");
+ $this->line("STAGE 1: Checking settings");
if(!$settings->ldap_enabled) {
$this->error("WARNING: Snipe-IT's LDAP setting is not turned on. (That may be OK if you're still trying to figure out settings)");
}
@@ -210,32 +236,40 @@ public function handle()
$this->info("Determined LDAP hostname to be: ".$parsed['host']);
}
- $this->info("Performing DNS lookup of: ".$parsed['host']);
- $ips = dns_get_record($parsed['host']);
$raw_ips = [];
- //$this->info("Host IP is: ".print_r($ips,true));
+ if (inet_pton($parsed['host']) !== false) {
+ $this->line($parsed['host'] . " already looks like an address; skipping DNS lookup");
+ $raw_ips[] = $parsed['host'];
+ } else {
+ $this->line("Performing DNS lookup of: " . $parsed['host']);
+ $ips = dns_get_record($parsed['host']);
- if(!$ips || count($ips) == 0) {
- $this->error("ERROR: DNS lookup of host: ".$parsed['host']." has failed. ABORTING.");
- exit(-1);
- }
- $this->debugout("IP's? ".print_r($ips,true));
- foreach($ips as $ip) {
- if(!isset($ip['ip'])) {
- continue;
+ //$this->info("Host IP is: ".print_r($ips,true));
+
+ if (!$ips || count($ips) == 0) {
+ $this->error("ERROR: DNS lookup of host: " . $parsed['host'] . " has failed. ABORTING.");
+ exit(-1);
+ }
+ $this->debugout("IP's? " . print_r($ips, true));
+ foreach ($ips as $ip) {
+ if (!isset($ip['ip'])) {
+ continue;
+ }
+ $raw_ips[] = $ip['ip'];
}
- $raw_ips[]=$ip['ip'];
- if($ip['ip'] == "127.0.0.1") {
+ }
+ foreach ($raw_ips as $ip) {
+ if ($ip == "127.0.0.1") {
$this->error("WARNING: Using the localhost IP as the LDAP server. This is usually wrong");
}
- if(ip_in_range($ip['ip'],'10.0.0.0/8') || ip_in_range($ip['ip'],'192.168.0.0/16') || ip_in_range($ip['ip'], '172.16.0.0/12')) {
+ if (ip_in_range($ip, '10.0.0.0/8') || ip_in_range($ip, '192.168.0.0/16') || ip_in_range($ip, '172.16.0.0/12')) {
$this->error("WARNING: Using an RFC1918 Private address for LDAP server. This may be correct, but it can be a problem if your Snipe-IT instance is not hosted on your private network");
}
}
- $this->info("STAGE 2: Checking basic network connectivity");
- $ports = [389,636];
+ $this->line("STAGE 2: Checking basic network connectivity");
+ $ports = [636, 389];
if(@$parsed['port'] && !in_array($parsed['port'],$ports)) {
$ports[] = $parsed['port'];
}
@@ -246,7 +280,7 @@ public function handle()
$errstr = '';
$timeout = 30.0;
$result = '';
- $this->info("Attempting to connect to port: ".$port." - may take up to $timeout seconds");
+ $this->line("Attempting to connect to port: " . $port . " - may take up to $timeout seconds");
try {
$result = fsockopen($parsed['host'], $port, $errno, $errstr, 30.0);
} catch(Exception $e) {
@@ -265,9 +299,9 @@ public function handle()
exit(-1);
}
- $this->info("STAGE 3: Determine encryption algorithm, if any");
+ $this->line("STAGE 3: Determine encryption algorithm, if any");
- $ldap_urls = [];
+ $ldap_urls = []; // [url, cert-check?, start_tls?]
$pretty_ldap_urls = [];
foreach($open_ports as $port) {
$this->line("Trying TLS first for port $port");
@@ -275,35 +309,46 @@ public function handle()
if($this->test_anonymous_bind($ldap_url)) {
$this->info("Anonymous bind succesful to $ldap_url!");
$ldap_urls[] = [ $ldap_url, true, false ];
- $pretty_ldap_urls[] = [ $ldap_url, "YES", "no" ];
+ $pretty_ldap_urls[] = [$ldap_url, "enabled", "n/a (no)"];
continue; // TODO - lots of copypasta in these if(test_anonymous_bind()) routines...
} else {
$this->error("WARNING: Failed to bind to $ldap_url - trying without certificate checks.");
}
if($this->test_anonymous_bind($ldap_url, false)) {
- $this->info("Anonymous bind succesful to $ldap_url with certifcate-checks disabled");
- $ldap_urls[] = [ $ldap_url, false, false ];
- $pretty_ldap_urls[] = [ $ldap_url, "no", "no" ];
+ $this->info("Anonymous bind successful to $ldap_url with certificate-checks disabled");
+ $ldap_urls[] = [$ldap_url, false, false];
+ $pretty_ldap_urls[] = [$ldap_url, "DISABLED", "n/a (no)"];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url with certificate checks disabled. Trying unencrypted with STARTTLS");
}
+ // now switching to ldap:// URL's from ldaps://
$ldap_url = "ldap://".$parsed['host'].":$port";
+
if($this->test_anonymous_bind($ldap_url, true, true)) {
$this->info("Plain connection to $ldap_url with STARTTLS succesful!");
$ldap_urls[] = [ $ldap_url, true, true ];
- $pretty_ldap_urls[] = [ $ldap_url, "YES", "YES" ];
+ $pretty_ldap_urls[] = [$ldap_url, "enabled", "STARTTLS ENABLED"];
+ continue;
+ } else {
+ $this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled. Trying without certificate checks.");
+ }
+
+ if ($this->test_anonymous_bind($ldap_url, false, true)) {
+ $this->info("Plain connection to $ldap_url with STARTTLS and cert checks *disabled* successful!");
+ $ldap_urls[] = [$ldap_url, false, true];
+ $pretty_ldap_urls[] = [$ldap_url, "DISABLED", "STARTTLS ENABLED"];
continue;
} else {
- $this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled. Trying without STARTTLS");
+ $this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled, and cert checks disabled. Trying without STARTTLS");
}
if($this->test_anonymous_bind($ldap_url)) {
$this->info("Plain connection to $ldap_url succesful!");
$ldap_urls[] = [ $ldap_url, true, false ];
- $pretty_ldap_urls[] = [ $ldap_url, "YES", "no" ];
+ $pretty_ldap_urls[] = [$ldap_url, "n/a", "starttls disabled"];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url. Giving up on port $port");
@@ -313,23 +358,29 @@ public function handle()
$this->debugout(print_r($ldap_urls,true));
if(count($ldap_urls) > 0 ) {
- $this->info("Found working LDAP URL's: ");
+ $this->debugout("Found working LDAP URL's: ");
foreach($ldap_urls as $ldap_url) { // TODO maybe do this as a $this->table() instead?
- $this->info("LDAP URL: ".$ldap_url[0]);
- $this->info($ldap_url[0]. ($ldap_url[1] ? " certificate checks enabled" : " certificate checks disabled"). ($ldap_url[2] ? " STARTTLS Enabled ": " STARTTLS Disabled"));
+ $this->debugout("LDAP URL: " . $ldap_url[0]);
+ $this->debugout($ldap_url[0] . ($ldap_url[1] ? " certificate checks enabled" : " certificate checks disabled") . ($ldap_url[2] ? " STARTTLS Enabled " : " STARTTLS Disabled"));
}
- $this->table(["URL", "Cert Checks Enabled?", "STARTTLS Enabled?"],$pretty_ldap_urls);
+ $this->table(["URL", "Cert Checks?", "STARTTLS?"], $pretty_ldap_urls);
} else {
$this->error("ERROR - no valid LDAP URL's available - ABORTING");
exit(1);
}
- $this->info("STAGE 4: Test Administrative Bind for LDAP Sync");
+ $this->line("STAGE 4: Test Administrative Bind for LDAP Sync");
foreach($ldap_urls AS $ldap_url) {
- $this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $settings->ldap_uname, Crypt::decrypt($settings->ldap_pword));
+ try {
+ $w = Crypt::Decrypt($settings->ldap_pword);
+ } catch (\Exception $e) {
+ $this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
+ exit(0);
+ }
+ $this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $settings->ldap_uname, $w);
}
- $this->info("STAGE 5: Test BaseDN");
+ $this->line("STAGE 5: Test BaseDN");
//grab all LDAP_ constants and fill up a reversed array mapping from weird LDAP dotted-strings to (Constant Name)
$all_defined_constants = get_defined_constants();
$ldap_constants = [];
@@ -341,16 +392,23 @@ public function handle()
$this->debugout("LDAP constants are: ".print_r($ldap_constants,true));
foreach($ldap_urls AS $ldap_url) {
- if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,Crypt::decrypt($settings->ldap_pword),$settings)) {
+ try {
+ $w = Crypt::Decrypt($settings->ldap_pword);
+ } catch (\Exception $e) {
+ $this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
+ exit(0);
+ }
+
+ if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,$w,$settings)) {
$this->info("Success getting informational bind!");
} else {
$this->error("Unable to get information from bind.");
}
}
- $this->info("STAGE 6: Test LDAP Login to Snipe-IT");
+ $this->line("STAGE 6: Test LDAP Login to Snipe-IT");
foreach($ldap_urls AS $ldap_url) {
- $this->info("Starting auth to ".$ldap_url[0]);
+ $this->line("Starting auth to " . $ldap_url[0]);
while(true) {
$with_tls = $ldap_url[1] ? "with": "without";
$with_startssl = $ldap_url[2] ? "using": "not using";
@@ -359,7 +417,12 @@ public function handle()
}
$username = $this->ask("Username");
$password = $this->secret("Password");
- $this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $username, $password); // FIXME - should do some other stuff here, maybe with the concatenating or something? maybe? and/or should put up some results?
+ $results = $this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $username, $password); // FIXME - should do some other stuff here, maybe with the concatenating or something? maybe? and/or should put up some results?
+ if ($results) {
+ $this->info("Success authenticating with " . $username);
+ } else {
+ $this->error("Unable to authenticate with " . $username);
+ }
}
}
@@ -368,14 +431,17 @@ public function handle()
public function connect_to_ldap($ldap_url, $check_cert, $start_tls)
{
+ if ($check_cert) {
+ $this->line("we *ARE* checking certs");
+ Ldap::ignoreCertificates(false);
+
+ } else {
+ $this->line("we are IGNORING certs");
+ Ldap::ignoreCertificates(true);
+ }
$lconn = ldap_connect($ldap_url);
ldap_set_option($lconn, LDAP_OPT_PROTOCOL_VERSION, 3); // should we 'test' different protocol versions here? Does anyone even use anything other than LDAPv3?
// no - it's formally deprecated: https://tools.ietf.org/html/rfc3494
- if(!$check_cert) {
- putenv('LDAPTLS_REQCERT=never'); // This is horrible; is this *really* the only way to do it?
- } else {
- putenv('LDAPTLS_REQCERT'); // have to very explicitly and manually *UN* set the env var here to ensure it works
- }
if($this->settings->ldap_client_tls_cert && $this->settings->ldap_client_tls_key) {
// client-side TLS certificate support for LDAP (Google Secure LDAP)
putenv('LDAPTLS_CERT=storage/ldap_client_tls.cert');
@@ -404,9 +470,10 @@ public function test_anonymous_bind($ldap_url, $check_cert = true, $start_tls =
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert , $start_tls) {
try {
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
- $this->info("gonna try to bind now, this can take a while if we mess it up");
+ $this->line("Attempting to bind now, this can take a while if we mess it up");
$bind_results = ldap_bind($lconn);
- $this->info("Bind results are: ".$bind_results." which translate into boolean: ".(bool)$bind_results);
+ $this->line("Bind results are: " . $bind_results . " which translate into boolean: " . (bool)$bind_results);
+ ldap_close($lconn);
return (bool)$bind_results;
} catch (Exception $e) {
$this->error("WARNING: Exception caught during bind - ".$e->getMessage());
@@ -421,6 +488,7 @@ public function test_authed_bind($ldap_url, $check_cert, $start_tls, $username,
try {
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
$bind_results = ldap_bind($lconn, $username, $password);
+ ldap_close($lconn);
if(!$bind_results) {
$this->error("WARNING: Failed to bind to $ldap_url as $username");
return false;
@@ -446,22 +514,62 @@ public function test_informational_bind($ldap_url, $check_cert, $start_tls, $use
return false;
}
$this->info("SUCCESS - Able to bind to $ldap_url as $username");
- $result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/);
- $results = ldap_get_entries($conn, $result);
- $cleaned_results = $this->ldap_results_cleaner($results);
- $this->line(print_r($cleaned_results,true));
- //okay, great - now how do we display those results? I have no idea.
+ $cleaned_results = [];
+ try {
+ // This _may_ only work for Active Directory?
+ $result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/);
+ $results = ldap_get_entries($conn, $result);
+ $cleaned_results = $this->ldap_results_cleaner($results);
+ //$this->line(print_r($cleaned_results,true));
+ $default_naming_contexts = $cleaned_results[0]['namingcontexts'];
+ $this->info("Default Naming Contexts:");
+ $this->info(implode(", ", $default_naming_contexts));
+ //okay, great - now how do we display those results? I have no idea.
+ } catch (\Exception $e) {
+ $this->error("Unable to get base naming contexts - here's what we *did* get:");
+ $this->line(print_r($cleaned_results, true));
+ }
// I don't see why this throws an Exception for Google LDAP, but I guess we ought to try and catch it?
- $this->comment("I guess we're trying to do the ldap search here, but sometimes it takes too long?");
+ $this->debugout("I guess we're trying to do the ldap search here, but sometimes it takes too long?");
$this->debugout("Base DN is: ".$settings->ldap_basedn." and filter is: ".parenthesized_filter($settings->ldap_filter));
$search_results = ldap_search($conn, $settings->ldap_basedn, parenthesized_filter($settings->ldap_filter));
+ $entries = ldap_get_entries($conn, $search_results);
$this->info("Printing first 10 results: ");
- for($i=0;$i<10;$i++) {
- $this->info($search_results[$i]);
+ $pretty_data = array_slice($this->ldap_results_cleaner($entries), 0, 10);
+ //print_r($data);
+ $headers = [];
+ foreach ($pretty_data as $row) {
+ //populate headers
+ foreach ($row as $key => $value) {
+ //skip objectsid and objectguid because it junks up output
+ if ($key == "objectsid" || $key == "objectguid") {
+ continue;
+ }
+ if (!in_array($key, $headers)) {
+ $headers[] = $key;
+ }
+ }
}
+ $table = [];
+ //repeat again to populate table
+ foreach ($pretty_data as $row) {
+ $newrow = [];
+ foreach ($headers as $header) {
+ if (is_array(@$row[$header])) {
+ $newrow[] = "[" . implode(", ", $row[$header]) . "]";
+ } else {
+ $newrow[] = @$row[$header];
+ }
+ }
+ $table[] = $newrow;
+ }
+
+ $this->table($headers, $table);
} catch (\Exception $e) {
$this->error("WARNING: Exception caught during Authed bind to $username - ".$e->getMessage());
return false;
+ } finally {
+ ldap_close($conn);
}
});
}
@@ -477,7 +585,7 @@ private function timed_boolean_execute($function)
{
if(!(function_exists('pcntl_sigtimedwait') && function_exists('posix_getpid') && function_exists('pcntl_fork') && function_exists('posix_kill') && function_exists('pcntl_wifsignaled'))) {
// POSIX functions needed for forking aren't present, just run the function inline (ignoring timeout)
- $this->info('WARNING: Unable to execute POSIX fork() commands, timeout may not be respected');
+ $this->line('WARNING: Unable to execute POSIX fork() commands, timeout may not be respected');
return $function();
} else {
$parent_pid = posix_getpid();
@@ -514,4 +622,6 @@ private function timed_boolean_execute($function)
}
}
+
+
}
diff --git a/app/Console/Commands/MoveUploadsToNewDisk.php b/app/Console/Commands/MoveUploadsToNewDisk.php
index a27b06feed60..615c12a545f5 100644
--- a/app/Console/Commands/MoveUploadsToNewDisk.php
+++ b/app/Console/Commands/MoveUploadsToNewDisk.php
@@ -96,7 +96,7 @@ public function handle()
$private_uploads['assets'] = glob('storage/private_uploads/assets'."/*.*");
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'."/*.*");
$private_uploads['audits'] = glob('storage/private_uploads/audits'."/*.*");
- $private_uploads['assetmodels'] = glob('storage/private_uploads/assetmodels'."/*.*");
+ $private_uploads['assetmodels'] = glob('storage/private_uploads/models'."/*.*");
$private_uploads['imports'] = glob('storage/private_uploads/imports'."/*.*");
$private_uploads['licenses'] = glob('storage/private_uploads/licenses'."/*.*");
$private_uploads['users'] = glob('storage/private_uploads/users'."/*.*");
diff --git a/app/Console/Commands/PaveIt.php b/app/Console/Commands/PaveIt.php
index 09c846ea68a0..8c6bc89a5174 100644
--- a/app/Console/Commands/PaveIt.php
+++ b/app/Console/Commands/PaveIt.php
@@ -4,7 +4,7 @@
use App\Models\Asset;
use App\Models\CustomField;
-use Schema;
+use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\Command;
@@ -51,8 +51,7 @@ public function handle()
}
// List all the tables in the database so we don't have to worry about missing some as the app grows
- $tables = DB::connection()->getDoctrineSchemaManager()->listTableNames();
-
+ $tables = Schema::getTables();
$except_tables = [
'oauth_access_tokens',
'oauth_clients',
@@ -60,6 +59,9 @@ public function handle()
'migrations',
'settings',
'users',
+ 'telescope_entries',
+ 'telescope_entries_tags',
+ 'telescope_monitoring',
];
// We only need to find out what these are so we can nuke these columns on the assets table.
@@ -67,14 +69,15 @@ public function handle()
foreach ($custom_fields as $custom_field) {
$this->info('DROP the '.$custom_field->db_column.' column from assets as well.');
- if (\Schema::hasColumn('assets', $custom_field->db_column)) {
- \Schema::table('assets', function ($table) use ($custom_field) {
+ if (Schema::hasColumn('assets', $custom_field->db_column)) {
+ Schema::table('assets', function ($table) use ($custom_field) {
$table->dropColumn($custom_field->db_column);
});
}
}
- foreach ($tables as $table) {
+ foreach ($tables as $table_obj) {
+ $table = $table_obj['name'];
if (in_array($table, $except_tables)) {
$this->info($table. ' is SKIPPED.');
} else {
@@ -84,8 +87,8 @@ public function handle()
}
// Leave in the demo oauth keys so we don't have to reset them every day in the demos
- \DB::statement('delete from oauth_clients WHERE id > 2');
- \DB::statement('delete from oauth_access_tokens WHERE id > 2');
+ DB::statement('delete from oauth_clients WHERE id > 2');
+ DB::statement('delete from oauth_access_tokens WHERE user_id > 2');
}
}
\ No newline at end of file
diff --git a/app/Console/Commands/Purge.php b/app/Console/Commands/Purge.php
index 1dd2aaa51ddf..4db7bac1472f 100644
--- a/app/Console/Commands/Purge.php
+++ b/app/Console/Commands/Purge.php
@@ -62,19 +62,19 @@ public function handle()
$assetcount = $assets->count();
$this->info($assets->count().' assets purged.');
$asset_assoc = 0;
- $asset_maintenances = 0;
+ $maintenances = 0;
foreach ($assets as $asset) {
- $this->info('- Asset "'.$asset->present()->name().'" deleted.');
+ $this->info('- Asset "'.$asset->display_name.'" deleted.');
$asset_assoc += $asset->assetlog()->count();
$asset->assetlog()->forceDelete();
- $asset_maintenances += $asset->assetmaintenances()->count();
- $asset->assetmaintenances()->forceDelete();
+ $maintenances += $asset->maintenances()->count();
+ $asset->maintenances()->forceDelete();
$asset->forceDelete();
}
$this->info($asset_assoc.' corresponding log records purged.');
- $this->info($asset_maintenances.' corresponding maintenance records purged.');
+ $this->info($maintenances.' corresponding maintenance records purged.');
$locations = Location::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($locations->count().' locations purged.');
diff --git a/app/Console/Commands/RecryptFromMcrypt.php b/app/Console/Commands/RecryptFromMcrypt.php
deleted file mode 100644
index 33c8ae65c9a3..000000000000
--- a/app/Console/Commands/RecryptFromMcrypt.php
+++ /dev/null
@@ -1,157 +0,0 @@
-error('ERROR: You do not have a LEGACY_APP_KEY set in your .env file. Please locate your old APP_KEY and ADD a line to your .env file like: LEGACY_APP_KEY=YOUR_OLD_APP_KEY');
-
- return false;
- }
-
- // Do some basic legacy app key length checks
- if (strlen($legacy_key) == 32) {
- $legacy_length_check = true;
- } elseif (array_key_exists('1', $key_parts) && (strlen($key_parts[1]) == 44)) {
- $legacy_key = base64_decode($key_parts[1], true);
- $legacy_length_check = true;
- } else {
- $legacy_length_check = false;
- }
-
- // Check that the app key is 32 characters
- if ($legacy_length_check === true) {
- $this->comment('INFO: Your LEGACY_APP_KEY looks correct. Okay to continue.');
- } else {
- $this->error('ERROR: Your LEGACY_APP_KEY is not the correct length (32 characters or base64 followed by 44 characters for later versions). Please locate your old APP_KEY and use that as your LEGACY_APP_KEY in your .env file to continue.');
-
- return false;
- }
-
- $this->error('================================!!!! WARNING !!!!================================');
- $this->error('================================!!!! WARNING !!!!================================');
- $this->comment("This tool will attempt to decrypt your old Snipe-IT (mcrypt, now deprecated) encrypted data and re-encrypt it using OpenSSL. \n\nYou should only continue if you have backed up any and all old APP_KEYs and have backed up your data.");
-
- $force = ($this->option('force')) ? true : false;
-
- if ($force || ($this->confirm('Are you SURE you wish to continue?'))) {
- $backup_file = 'backups/env-backups/'.'app_key-'.date('Y-m-d-gis');
-
- try {
- Storage::disk('local')->put($backup_file, 'APP_KEY: '.config('app.key'));
- Storage::disk('local')->append($backup_file, 'LEGACY_APP_KEY: '.$legacy_key);
- } catch (\Exception $e) {
- $this->info('WARNING: Could not backup app keys');
- }
-
- if ($legacy_cipher) {
- $mcrypter = new McryptEncrypter($legacy_key, $legacy_cipher);
- } else {
- $mcrypter = new McryptEncrypter($legacy_key);
- }
- $settings = Setting::getSettings();
-
- if ($settings->ldap_pword == '') {
- $this->comment('INFO: No LDAP password found. Skipping... ');
- } else {
- $decrypted_ldap_pword = $mcrypter->decrypt($settings->ldap_pword);
- $settings->ldap_pword = Crypt::encrypt($decrypted_ldap_pword);
- $settings->save();
- }
- /** @var CustomField[] $custom_fields */
- $custom_fields = CustomField::where('field_encrypted', '=', 1)->get();
- $this->comment('INFO: Retrieving encrypted custom fields...');
-
- $query = Asset::withTrashed();
-
- foreach ($custom_fields as $custom_field) {
- $this->comment('FIELD TO RECRYPT: '.$custom_field->name.' ('.$custom_field->db_column.')');
- $query->orWhereNotNull($custom_field->db_column);
- }
-
- // Get all assets with a value in any of the fields that were encrypted
- /** @var Asset[] $assets */
- $assets = $query->get();
-
- $bar = $this->output->createProgressBar(count($assets));
-
- foreach ($assets as $asset) {
- foreach ($custom_fields as $encrypted_field) {
- $columnName = $encrypted_field->db_column;
-
- // Make sure the value isn't null
- if ($asset->{$columnName} != '') {
- // Try to decrypt the payload using the legacy app key
- try {
- $decrypted_field = $mcrypter->decrypt($asset->{$columnName});
- $asset->{$columnName} = Crypt::encrypt($decrypted_field);
- $this->comment($decrypted_field);
- } catch (\Exception $e) {
- $errors[] = ' - ERROR: Could not decrypt field ['.$encrypted_field->name.']: '.$e->getMessage();
- }
- }
- }
- $asset->save();
- $bar->advance();
- }
-
- $bar->finish();
-
- if (count($errors) > 0) {
- $this->comment("\n\n");
- $this->error("The decrypter encountered some errors: \n");
- foreach ($errors as $error) {
- $this->error($error);
- }
- }
- }
- }
-}
diff --git a/app/Console/Commands/RestoreFromBackup.php b/app/Console/Commands/RestoreFromBackup.php
index 0478bcd9292f..b5109c25ece0 100644
--- a/app/Console/Commands/RestoreFromBackup.php
+++ b/app/Console/Commands/RestoreFromBackup.php
@@ -243,12 +243,15 @@ public function handle()
$private_dirs = [
'storage/private_uploads/accessories',
'storage/private_uploads/assetmodels',
+ 'storage/private_uploads/maintenances',
+ 'storage/private_uploads/models',
'storage/private_uploads/assets', // these are asset _files_, not the pictures.
'storage/private_uploads/audits',
'storage/private_uploads/components',
'storage/private_uploads/consumables',
'storage/private_uploads/eula-pdfs',
'storage/private_uploads/imports',
+ 'storage/private_uploads/locations',
'storage/private_uploads/licenses',
'storage/private_uploads/signatures',
'storage/private_uploads/users',
@@ -259,9 +262,10 @@ public function handle()
];
$public_dirs = [
'public/uploads/accessories',
+ 'public/uploads/assetmodels',
+ 'public/uploads/maintenances',
'public/uploads/assets', // these are asset _pictures_, not asset files
'public/uploads/avatars',
- //'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
'public/uploads/categories',
'public/uploads/companies',
'public/uploads/components',
@@ -289,6 +293,7 @@ public function handle()
$interesting_files = [];
$boring_files = [];
+ $unsafe_files = [];
for ($i = 0; $i < $za->numFiles; $i++) {
$stat_results = $za->statIndex($i);
@@ -327,8 +332,9 @@ public function handle()
}
}
}
- $good_extensions = ['png', 'gif', 'jpg', 'svg', 'jpeg', 'doc', 'docx', 'pdf', 'txt',
- 'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico',];
+
+ $good_extensions = config('filesystems.allowed_upload_extensions_array');
+
foreach (array_merge($private_files, $public_files) as $file) {
$has_wildcard = (strpos($file, '*') !== false);
if ($has_wildcard) {
@@ -338,7 +344,9 @@ public function handle()
if ($last_pos !== false) {
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
if (!in_array($extension, $good_extensions)) {
- $this->warn('Potentially unsafe file ' . $raw_path . ' is being skipped');
+ // gathering potentially unsafe files here to return at exit
+ $unsafe_files[] = $raw_path;
+ Log::debug('Potentially unsafe file '.$raw_path.' is being skipped');
$boring_files[] = $raw_path;
continue 2;
}
@@ -372,6 +380,7 @@ public function handle()
if ($this->option('sanitize-guess-prefix')) {
$prefix = SQLStreamer::guess_prefix($sql_contents);
$this->line($prefix);
+
return $this->info("Re-run this command with '--sanitize-with-prefix=".$prefix."' to see an attempt to sanitize your SQL.");
}
@@ -505,6 +514,11 @@ public function handle()
} else {
$this->info(count($interesting_files).' files were succesfully transferred');
}
+ if (count($unsafe_files) > 0) {
+ foreach ($unsafe_files as $unsafe_file) {
+ $this->warn('Potentially unsafe file '.$unsafe_file.' was skipped');
+ }
+ }
foreach ($boring_files as $boring_file) {
$this->warn($boring_file.' was skipped.');
}
diff --git a/app/Console/Commands/SendAcceptanceReminder.php b/app/Console/Commands/SendAcceptanceReminder.php
index f4ab2c5b68e8..67efecbb3453 100644
--- a/app/Console/Commands/SendAcceptanceReminder.php
+++ b/app/Console/Commands/SendAcceptanceReminder.php
@@ -64,28 +64,46 @@ public function handle()
->groupBy(function($item) {
return $item['acceptance']->assignedTo ? $item['acceptance']->assignedTo->id : '';
});
+ $no_email_list= [];
foreach($unacceptedAssetGroups as $unacceptedAssetGroup) {
// The [0] is weird, but it allows for the item_count to work and grabs the appropriate info for each user.
// Collapsing and flattening the collection doesn't work above.
$acceptance = $unacceptedAssetGroup[0]['acceptance'];
+
$locale = $acceptance->assignedTo?->locale;
$email = $acceptance->assignedTo?->email;
+
if(!$email){
- $this->info($acceptance->assignedTo?->present()->fullName().' has no email address.');
+ $no_email_list[] = [
+ 'id' => $acceptance->assignedTo?->id,
+ 'name' => $acceptance->assignedTo?->display_name,
+ ];
+ } else {
+ $count++;
}
$item_count = $unacceptedAssetGroup->count();
if ($locale && $email) {
Mail::to($email)->send((new UnacceptedAssetReminderMail($acceptance, $item_count))->locale($locale));
-
} elseif ($email) {
Mail::to($email)->send((new UnacceptedAssetReminderMail($acceptance, $item_count)));
}
- $count++;
+
}
$this->info($count.' users notified.');
+ $headers = ['ID', 'Name'];
+ $rows = [];
+
+ foreach ($no_email_list as $user) {
+ $rows[] = [$user['id'], $user['name']];
+ }
+
+ if (!empty($rows)) {
+ $this->info("The following users do not have an email address:");
+ $this->table($headers, $rows);
+ }
return 0;
}
diff --git a/app/Console/Commands/SendExpirationAlerts.php b/app/Console/Commands/SendExpirationAlerts.php
index 1220e0cd9d22..64aff907fb6c 100644
--- a/app/Console/Commands/SendExpirationAlerts.php
+++ b/app/Console/Commands/SendExpirationAlerts.php
@@ -42,27 +42,28 @@ public function __construct()
public function handle()
{
$settings = Setting::getSettings();
- $threshold = $settings->alert_interval;
+ $alert_interval = $settings->alert_interval;
if (($settings->alert_email != '') && ($settings->alerts_enabled == 1)) {
// Send a rollup to the admin, if settings dictate
$recipients = collect(explode(',', $settings->alert_email))
->map(fn($item) => trim($item)) // Trim each email
+ ->filter(fn($item) => !empty($item))
->all();
// Expiring Assets
- $assets = Asset::getExpiringWarrantee($threshold);
+ $assets = Asset::getExpiringWarrantee($alert_interval);
if ($assets->count() > 0) {
- $this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $threshold]));
- Mail::to($recipients)->send(new ExpiringAssetsMail($assets, $threshold));
+ $this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $alert_interval]));
+ Mail::to($recipients)->send(new ExpiringAssetsMail($assets, $alert_interval));
}
// Expiring licenses
- $licenses = License::getExpiringLicenses($threshold);
+ $licenses = License::getExpiringLicenses($alert_interval);
if ($licenses->count() > 0) {
- $this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $threshold]));
- Mail::to($recipients)->send(new ExpiringLicenseMail($licenses, $threshold));
+ $this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $alert_interval]));
+ Mail::to($recipients)->send(new ExpiringLicenseMail($licenses, $alert_interval));
}
} else {
if ($settings->alert_email == '') {
diff --git a/app/Console/Commands/SendUpcomingAuditReport.php b/app/Console/Commands/SendUpcomingAuditReport.php
index 7c81a37d3a28..854488adc6c7 100644
--- a/app/Console/Commands/SendUpcomingAuditReport.php
+++ b/app/Console/Commands/SendUpcomingAuditReport.php
@@ -2,13 +2,12 @@
namespace App\Console\Commands;
+use App\Mail\SendUpcomingAuditMail;
use App\Models\Asset;
-use App\Models\Recipients\AlertRecipient;
use App\Models\Setting;
-use App\Notifications\SendUpcomingAuditNotification;
use Carbon\Carbon;
-use Illuminate\Support\Facades\DB;
use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Mail;
class SendUpcomingAuditReport extends Command
{
@@ -48,19 +47,20 @@ public function handle()
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval);
- $assets = Asset::whereNull('deleted_at')->DueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'desc')->get();
- $this->info($assets->count().' assets must be audited in on or before '.$interval_date.' is deadline');
+ $assets = Asset::whereNull('deleted_at')->dueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'desc')->get();
+ $this->info($assets->count() . ' assets must be audited in on or before ' . $interval_date . ' is deadline');
- if (($assets) && ($assets->count() > 0) && ($settings->alert_email != '')) {
+ if ((count($assets) !== 0) && ($assets->count() > 0) && ($settings->alert_email != '')) {
// Send a rollup to the admin, if settings dictate
- $recipients = collect(explode(',', $settings->alert_email))->map(function ($item) {
- return new AlertRecipient($item);
- });
+ $recipients = collect(explode(',', $settings->alert_email))
+ ->map(fn($item) => trim($item))
+ ->filter(fn($item) => !empty($item))
+ ->all();
- $this->info('Sending Admin SendUpcomingAuditNotification to: '.$settings->alert_email);
- \Notification::send($recipients, new SendUpcomingAuditNotification($assets, $settings->audit_warning_days));
+ $this->info('Sending Admin SendUpcomingAuditNotification to: ' . $settings->alert_email);
+ Mail::to($recipients)->send(new SendUpcomingAuditMail($assets, $settings->audit_warning_days));
}
}
diff --git a/app/Console/Commands/TestLocationsFMCS.php b/app/Console/Commands/TestLocationsFMCS.php
new file mode 100644
index 000000000000..70eface2de4c
--- /dev/null
+++ b/app/Console/Commands/TestLocationsFMCS.php
@@ -0,0 +1,51 @@
+info('This script checks for company ID inconsistencies if Full Multiple Company Support with scoped locations will be used.');
+ $this->info('This could take a few moments if have a very large dataset.');
+ $this->newLine();
+
+ // if parameter location_id is set, only test this location
+ $location_id = null;
+ if ($this->option('location_id')) {
+ $location_id = $this->option('location_id');
+ }
+
+ $mismatched = Helper::test_locations_fmcs(true, $location_id);
+ $this->warn(trans_choice('admin/settings/message.location_scoping.mismatch', count($mismatched)));
+ $this->newLine();
+ $this->info('Edit your locations to associate them with the correct company.');
+
+ $header = ['Type', 'ID', 'Name', 'Checkout Type', 'Company ID', 'Item Company', 'Item Location', 'Location Company', 'Location Company ID'];
+ sort($mismatched);
+
+ $this->table($header, $mismatched);
+
+ }
+
+}
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 8d512f303b30..ca85459b672c 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -5,6 +5,7 @@
use App\Console\Commands\ImportLocations;
use App\Console\Commands\ReEncodeCustomFieldNames;
use App\Console\Commands\RestoreDeletedUsers;
+use App\Models\Setting;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -18,12 +19,14 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
- $schedule->command('snipeit:inventory-alerts')->daily();
- $schedule->command('snipeit:expiring-alerts')->daily();
- $schedule->command('snipeit:expected-checkin')->daily();
+ if(Setting::getSettings()?->alerts_enabled === 1) {
+ $schedule->command('snipeit:inventory-alerts')->daily();
+ $schedule->command('snipeit:expiring-alerts')->daily();
+ $schedule->command('snipeit:expected-checkin')->daily();
+ $schedule->command('snipeit:upcoming-audits')->daily();
+ }
$schedule->command('snipeit:backup')->weekly();
$schedule->command('backup:clean')->daily();
- $schedule->command('snipeit:upcoming-audits')->daily();
$schedule->command('auth:clear-resets')->everyFifteenMinutes();
$schedule->command('saml:clear_expired_nonces')->weekly();
}
diff --git a/app/Events/CheckoutableCheckedIn.php b/app/Events/CheckoutableCheckedIn.php
index 48aed2a64d7e..fedbd49fbef9 100644
--- a/app/Events/CheckoutableCheckedIn.php
+++ b/app/Events/CheckoutableCheckedIn.php
@@ -28,7 +28,7 @@ public function __construct($checkoutable, $checkedOutTo, User $checkedInBy, $no
$this->checkedOutTo = $checkedOutTo;
$this->checkedInBy = $checkedInBy;
$this->note = $note;
- $this->action_date = $action_date ?? date('Y-m-d');
+ $this->action_date = $action_date ?? date('Y-m-d H:i:s');
$this->originalValues = $originalValues;
}
}
diff --git a/app/Events/NoteAdded.php b/app/Events/NoteAdded.php
deleted file mode 100644
index de5f91ce78bf..000000000000
--- a/app/Events/NoteAdded.php
+++ /dev/null
@@ -1,28 +0,0 @@
-itemNoteAddedOn = $itemNoteAddedOn;
- $this->note = $note;
- $this->noteAddedBy = $noteAddedBy;
- }
-}
\ No newline at end of file
diff --git a/app/Exceptions/AssetNotRequestable.php b/app/Exceptions/AssetNotRequestable.php
new file mode 100644
index 000000000000..4067dda38541
--- /dev/null
+++ b/app/Exceptions/AssetNotRequestable.php
@@ -0,0 +1,9 @@
+getStatusCode();
+ // API throttle requests are handled in the RouteServiceProvider configureRateLimiting() method, so we don't need to handle them here
switch ($e->getStatusCode()) {
case '404':
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode . ' endpoint not found'), 404);
- case '429':
- return response()->json(Helper::formatStandardApiResponse('error', null, 'Too many requests'), 429);
case '405':
return response()->json(Helper::formatStandardApiResponse('error', null, 'Method not allowed'), 405);
default:
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode), $statusCode);
-
}
+
+ }
+
+ // This handles API validation exceptions that happen at the Form Request level, so they
+ // never even get to the controller where we normally nicely format JSON responses
+ if ($e instanceof ValidationException) {
+ $response = $this->invalidJson($request, $e);
+ return response()->json(Helper::formatStandardApiResponse('error', null, $e->errors()), 200);
}
+
}
+ // This is traaaaash but it handles models that are not found while using route model binding :(
+ // The only alternative is to set that at *each* route, which is crazypants
+ if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
+ $ids = method_exists($e, 'getIds') ? $e->getIds() : [];
+
+ if (in_array('bulkedit', $ids, true)) {
+ $error_array = session()->get('bulk_asset_errors');
+ return redirect()
+ ->route('hardware.index')
+ ->withErrors($error_array, 'bulk_asset_errors')
+ ->withInput();
+ }
+
+ // This gets the MVC model name from the exception and formats in a way that's less fugly
+ $model_name = trim(strtolower(implode(" ", preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel()))))));
+ $route = str_plural(strtolower(last(explode('\\', $e->getModel())))).'.index';
+
+ // Sigh.
+ if ($route == 'assets.index') {
+ $route = 'hardware.index';
+ } elseif ($route == 'reporttemplates.index') {
+ $route = 'reports/custom';
+ } elseif ($route == 'assetmodels.index') {
+ $route = 'models.index';
+ } elseif ($route == 'predefinedkits.index') {
+ $route = 'kits.index';
+ } elseif ($route == 'assetmaintenances.index') {
+ $route = 'maintenances.index';
+ } elseif ($route === 'licenseseats.index') {
+ $route = 'licenses.index';
+ } elseif (($route === 'customfieldsets.index') || ($route === 'customfields.index')) {
+ $route = 'fields.index';
+ }
+
+ return redirect()
+ ->route($route)
+ ->withError(trans('general.generic_model_not_found', ['model' => $model_name]));
+ }
if ($this->isHttpException($e) && (isset($statusCode)) && ($statusCode == '404' )) {
@@ -174,8 +220,9 @@ protected function invalidJson($request, ValidationException $exception)
*/
public function register()
{
+
$this->reportable(function (Throwable $e) {
//
});
}
-}
\ No newline at end of file
+}
diff --git a/app/Exceptions/UserDoestExistException.php b/app/Exceptions/UserDoestExistException.php
new file mode 100644
index 000000000000..e14069dca4af
--- /dev/null
+++ b/app/Exceptions/UserDoestExistException.php
@@ -0,0 +1,10 @@
+fields->count() > 0)) {
+ $fields_array = [];
+
+ foreach ($fieldset->fields as $field) {
+ if ($field->isFieldDecryptable($item->{$field->db_column})) {
+ $decrypted = Helper::gracefulDecrypt($field, $item->{$field->db_column});
+ $value = (Gate::allows('assets.view.encrypted_custom_fields')) ? $decrypted : strtoupper(trans('admin/custom_fields/general.encrypted'));
+
+ if ($field->format == 'DATE'){
+ if (Gate::allows('assets.view.encrypted_custom_fields')){
+ $value = Helper::getFormattedDateObject($value, 'date', false);
+ } else {
+ $value = strtoupper(trans('admin/custom_fields/general.encrypted'));
+ }
+ }
+
+ $fields_array[$field->name] = [
+ 'field' => e($field->db_column),
+ 'value' => e($value),
+ 'field_format' => $field->format,
+ 'element' => $field->element,
+ ];
+
+ } else {
+ $value = $item->{$field->db_column};
+
+ if (($field->format == 'DATE') && (!is_null($value)) && ($value!='')){
+ $value = Helper::getFormattedDateObject($value, 'date', false);
+ }
+
+ $fields_array[$field->name] = [
+ 'field' => e($field->db_column),
+ 'value' => e($value),
+ 'field_format' => $field->format,
+ 'element' => $field->element,
+ ];
+ }
+
+ return $fields_array;
+ }
+ } else {
+ return new \stdClass; // HACK to force generation of empty object instead of empty list
+ }
+ }
+
+ static function present($field) {
+ return [
+ 'field' => 'custom_fields.'.$field->db_column,
+ 'searchable' => true,
+ 'sortable' => true,
+ 'switchable' => true,
+ 'title' => $field->name,
+ 'formatter'=> 'customFieldsFormatter',
+ 'escape' => true,
+ 'class' => ($field->field_encrypted == '1') ? 'css-padlock' : '',
+ 'visible' => ($field->show_in_listview == '1') ? true : false,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Helpers/Helper.php b/app/Helpers/Helper.php
index 95a344dce9e7..722222b75257 100644
--- a/app/Helpers/Helper.php
+++ b/app/Helpers/Helper.php
@@ -12,6 +12,8 @@
use App\Models\Setting;
use App\Models\Statuslabel;
use App\Models\License;
+use App\Models\Location;
+use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Contracts\Encryption\DecryptException;
use Carbon\Carbon;
@@ -653,6 +655,17 @@ public static function customFieldsetList()
return $customfields;
}
+ /**
+ * Get all of the different types of custom fields there are
+ * TODO - how to make this more general? Or more useful? or more dynamic?
+ * idea - key of classname, *value* of trans? (thus having to make this a method, which is fine)
+ */
+ static $itemtypes_having_custom_fields = [
+ 0 => \App\Models\Asset::class,
+ 1 => \App\Models\User::class,
+ // 2 => \App\Models\Accessory::class
+ ];
+
/**
* Get the list of custom field formats in an array to make a dropdown menu
*
@@ -721,8 +734,8 @@ public static function deprecationCheck() : array {
// The check and message that the user is still using the deprecated version
$deprecations = [
'ms_teams_deprecated' => array(
- 'check' => !Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows'),
- 'message' => 'The Microsoft Teams webhook URL being used will be deprecated Jan 31st, 2025. Change webhook endpoint'),
+ 'check' => !Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows') && (Setting::getSettings()->webhook_selected === 'microsoft'),
+ 'message' => 'The Microsoft Teams webhook URL being used will be deprecated Dec 31st, 2025. Change webhook endpoint'),
];
// if item of concern is being used and its being used with the deprecated values return the notification array.
@@ -868,7 +881,49 @@ public static function checkUploadIsImage($file)
$filetype = @finfo_file($finfo, $file);
finfo_close($finfo);
- if (($filetype == 'image/jpeg') || ($filetype == 'image/jpg') || ($filetype == 'image/png') || ($filetype == 'image/bmp') || ($filetype == 'image/gif') || ($filetype == 'image/avif')) {
+ if (($filetype == 'image/jpeg') || ($filetype == 'image/jpg') || ($filetype == 'image/png') || ($filetype == 'image/bmp') || ($filetype == 'image/gif') || ($filetype == 'image/avif') || ($filetype == 'image/webp')) {
+ return $filetype;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if the file is a video, so we can show a preview
+ *
+ * @param File $file
+ * @return string | Boolean
+ * @author [B. Wetherington] []
+ * @since [v8.1.18]
+ */
+ public static function checkUploadIsVideo($file)
+ {
+ $finfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
+ $filetype = @finfo_file($finfo, $file);
+ finfo_close($finfo);
+
+ if (($filetype == 'video/mp4') || ($filetype == 'video/quicktime') || ($filetype == 'video/mpeg') || ($filetype == 'video/ogg') || ($filetype == 'video/webm') || ($filetype == 'video/x-msvide')) {
+ return $filetype;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if the file is audio, so we can show a preview
+ *
+ * @param File $file
+ * @return string | Boolean
+ * @author [A. Gianotto] []
+ * @since [v3.0]
+ */
+ public static function checkUploadIsAudio($file)
+ {
+ $finfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
+ $filetype = @finfo_file($finfo, $file);
+ finfo_close($finfo);
+
+ if (($filetype == 'audio/mpeg') || ($filetype == 'audio/ogg')) {
return $filetype;
}
@@ -895,6 +950,12 @@ public static function checkUploadIsImage($file)
public static function selectedPermissionsArray($permissions, $selected_arr = [])
{
$permissions_arr = [];
+ if (is_array($permissions)) {
+ $permissions = json_encode($permissions);
+ }
+
+ // Set default to empty JSON if the value is null
+ $permissions = json_decode($permissions ?? '{}', JSON_OBJECT_AS_ARRAY);
foreach ($permissions as $permission) {
for ($x = 0; $x < count($permission); $x++) {
@@ -905,13 +966,13 @@ public static function selectedPermissionsArray($permissions, $selected_arr = []
if (is_array($selected_arr)) {
if (array_key_exists($permission_name, $selected_arr)) {
- $permissions_arr[$permission_name] = $selected_arr[$permission_name];
+ $permissions_arr[$permission_name] = (int) $selected_arr[$permission_name];
} else {
- $permissions_arr[$permission_name] = '0';
+ $permissions_arr[$permission_name] = 0;
}
} else {
- $permissions_arr[$permission_name] = '0';
+ $permissions_arr[$permission_name] = 0;
}
}
}
@@ -1147,22 +1208,42 @@ public static function filetype_icon($filename)
'webp' => 'far fa-image',
'avif' => 'far fa-image',
'svg' => 'fas fa-vector-square',
+
// word
'doc' => 'far fa-file-word',
'docx' => 'far fa-file-word',
+
// Excel
'xls' => 'far fa-file-excel',
'xlsx' => 'far fa-file-excel',
+ 'ods' => 'far fa-file-excel',
+
+ // Presentation
+ 'ppt' => 'far fa-file-powerpoint',
+ 'odp' => 'far fa-file-powerpoint',
+
// archive
'zip' => 'fas fa-file-archive',
'rar' => 'fas fa-file-archive',
+
//Text
+ 'odt' => 'far fa-file-alt',
'txt' => 'far fa-file-alt',
'rtf' => 'far fa-file-alt',
'xml' => 'fas fa-code',
+
// Misc
'pdf' => 'far fa-file-pdf',
'lic' => 'far fa-save',
+
+ // video
+ 'mov' => 'fa-solid fa-video',
+ 'mp4' => 'fa-solid fa-video',
+
+ // audio
+ 'ogg' => 'fa-solid fa-file-audio',
+ 'mp3' => 'fa-solid fa-file-audio',
+ 'wav' => 'fa-solid fa-file-audio',
];
if ($extension && array_key_exists($extension, $allowedExtensionMap)) {
@@ -1306,25 +1387,24 @@ public static function iconTypeByItem($item) {
switch ($item) {
case 'asset':
return 'fas fa-barcode';
- break;
case 'accessory':
return 'fas fa-keyboard';
- break;
case 'component':
return 'fas fa-hdd';
- break;
case 'consumable':
return 'fas fa-tint';
- break;
case 'license':
return 'far fa-save';
- break;
case 'location':
return 'fas fa-map-marker-alt';
- break;
case 'user':
return 'fas fa-user';
- break;
+ case 'supplier':
+ return 'fa-solid fa-store';
+ case 'manufacturer':
+ return 'fa-solid fa-building';
+ case 'category':
+ return 'fa-solid fa-table-columns';
}
}
@@ -1474,59 +1554,168 @@ public static function determineLanguageDirection() {
}
- static public function getRedirectOption($request, $id, $table, $item_id = null)
+ static public function getRedirectOption($request, $id, $table, $item_id = null) : RedirectResponse
{
- $redirect_option = Session::get('redirect_option');
- $checkout_to_type = Session::get('checkout_to_type');
+ $redirect_option = Session::get('redirect_option') ?? $request->redirect_option;
+ $checkout_to_type = Session::get('checkout_to_type') ?? null;
+ $checkedInFrom = Session::get('checkedInFrom');
+ $other_redirect = Session::get('other_redirect');
+ $backUrl = Session::pull('back_url', route('home'));
+
+ // return to previous page
+ if ($redirect_option === 'back') {
+ return redirect()->to($backUrl);
+ }
// return to index
if ($redirect_option == 'index') {
- switch ($table) {
- case "Assets":
- return route('hardware.index');
- case "Users":
- return route('users.index');
- case "Licenses":
- return route('licenses.index');
- case "Accessories":
- return route('accessories.index');
- case "Components":
- return route('components.index');
- case "Consumables":
- return route('consumables.index');
- }
+ return match ($table) {
+ 'Assets' => redirect()->route('hardware.index'),
+ 'Users' => redirect()->route('users.index'),
+ 'Licenses' => redirect()->route('licenses.index'),
+ 'Accessories' => redirect()->route('accessories.index'),
+ 'Components' => redirect()->route('components.index'),
+ 'Consumables' => redirect()->route('consumables.index'),
+ };
}
// return to thing being assigned
if ($redirect_option == 'item') {
- switch ($table) {
- case "Assets":
- return route('hardware.show', $id ?? $item_id);
- case "Users":
- return route('users.show', $id ?? $item_id);
- case "Licenses":
- return route('licenses.show', $id ?? $item_id);
- case "Accessories":
- return route('accessories.show', $id ?? $item_id);
- case "Components":
- return route('components.show', $id ?? $item_id);
- case "Consumables":
- return route('consumables.show', $id ?? $item_id);
- }
+ return match ($table) {
+ 'Assets' => redirect()->route('hardware.show', $id ?? $item_id),
+ 'Users' => redirect()->route('users.show', $id ?? $item_id),
+ 'Licenses' => redirect()->route('licenses.show', $id ?? $item_id),
+ 'Accessories' => redirect()->route('accessories.show', $id ?? $item_id),
+ 'Components' => redirect()->route('components.show', $id ?? $item_id),
+ 'Consumables' => redirect()->route('consumables.show', $id ?? $item_id),
+ };
}
// return to assignment target
if ($redirect_option == 'target') {
- switch ($checkout_to_type) {
- case 'user':
- return route('users.show', ['user' => $request->assigned_user]);
- case 'location':
- return route('locations.show', ['location' => $request->assigned_location]);
- case 'asset':
- return route('hardware.show', ['hardware' => $request->assigned_asset]);
- }
+ return match ($checkout_to_type) {
+ 'user' => redirect()->route('users.show', $request->assigned_user ?? $checkedInFrom),
+ 'location' => redirect()->route('locations.show', $request->assigned_location ?? $checkedInFrom),
+ 'asset' => redirect()->route('hardware.show', $request->assigned_asset ?? $checkedInFrom),
+ };
+ }
+
+ // return to somewhere else
+ if ($redirect_option == 'other_redirect') {
+ return match ($other_redirect) {
+ 'audit' => redirect()->route('assets.audit.due'),
+ 'model' => redirect()->route('models.show', $request->model_id),
+ };
+
}
+
return redirect()->back()->with('error', trans('admin/hardware/message.checkout.error'));
}
+
+ /**
+ * Check for inconsistencies before activating scoped locations with FullMultipleCompanySupport
+ * If there are locations with different companies than related objects unforseen problems could arise
+ *
+ * @author T. Regnery
+ * @since 7.0
+ *
+ * @param $artisan when false, bail out on first inconsistent entry
+ * @param $location_id when set, only test this specific location
+ * @param $new_company_id in case of updating a location, this is the newly requested company_id
+ * @return string []
+ */
+ static public function test_locations_fmcs($artisan, $location_id = null, $new_company_id = null) {
+ $mismatched = [];
+
+ if ($location_id) {
+ $location = Location::find($location_id);
+ if ($location) {
+ $locations = collect([])->push(Location::find($location_id));
+ }
+ } else {
+ $locations = Location::all();
+ }
+
+ // Bail out early if there are no locations
+ if ($locations->count() == 0) {
+ return [];
+ }
+
+ foreach($locations as $location) {
+ // in case of an update of a single location, use the newly requested company_id
+ if ($new_company_id) {
+ $location_company = $new_company_id;
+ } else {
+ $location_company = $location->company_id;
+ }
+
+ // Depending on the relationship, we must use different operations to retrieve the objects
+ $keywords_relation = [
+ 'many' => [
+ 'accessories',
+ 'assets',
+ 'assignedAccessories',
+ 'assignedAssets',
+ 'components',
+ 'consumables',
+ 'rtd_assets',
+ 'users',
+ ],
+ 'one' => [
+ 'manager',
+ 'parent',
+ ]];
+
+ // In case of a single location, the children must be checked as well, because we don't walk every location
+ if ($location_id) {
+ $keywords_relation['many'][] = 'children';
+ }
+
+ foreach ($keywords_relation as $relation => $keywords) {
+ foreach($keywords as $keyword) {
+ if ($relation == 'many') {
+ $items = $location->{$keyword}->all();
+ } else {
+ $items = collect([])->push($location->$keyword);
+ }
+
+ $count = 0;
+ foreach ($items as $item) {
+
+
+ if ($item && $item->company_id != $location_company) {
+
+ $mismatched[] = [
+ class_basename(get_class($item)),
+ $item->id,
+ $item->name ?? $item->asset_tag ?? $item->serial ?? $item->username,
+ $item->assigned_type ? str_replace('App\\Models\\', '', $item->assigned_type) : null,
+ $item->company_id ?? null,
+ $item->company->name ?? null,
+// $item->defaultLoc->id ?? null,
+// $item->defaultLoc->name ?? null,
+// $item->defaultLoc->company->id ?? null,
+// $item->defaultLoc->company->name ?? null,
+ $item->location->name ?? null,
+ $item->location->company->name ?? null,
+ $location_company ?? null,
+ ];
+
+ $count++;
+
+ // Bail early if this is not being run via artisan
+ if ((!$artisan) && ($count > 0)) {
+ return $mismatched;
+ }
+
+
+
+ }
+ }
+ }
+ }
+ }
+ return $mismatched;
+ }
}
diff --git a/app/Helpers/IconHelper.php b/app/Helpers/IconHelper.php
index 0b243d69a559..8172f2bbbbde 100644
--- a/app/Helpers/IconHelper.php
+++ b/app/Helpers/IconHelper.php
@@ -16,6 +16,7 @@ public static function icon($type) {
case 'clone':
return 'far fa-clone';
case 'delete':
+ case 'upload deleted':
return 'fas fa-trash';
case 'create':
return 'fa-solid fa-plus';
@@ -43,6 +44,8 @@ public static function icon($type) {
return 'fa-regular fa-envelope';
case 'phone':
return 'fa-solid fa-phone';
+ case 'mobile':
+ return 'fas fa-mobile-screen-button';
case 'long-arrow-right':
return 'fas fa-long-arrow-alt-right';
case 'download':
@@ -59,6 +62,8 @@ public static function icon($type) {
return 'fas fa-cog';
case 'angle-left':
return 'fas fa-angle-left';
+ case 'angle-right':
+ return 'fas fa-angle-right';
case 'warning':
return 'fas fa-exclamation-triangle';
case 'kits':
@@ -149,6 +154,7 @@ public static function icon($type) {
case 'location':
return 'fas fa-map-marker-alt';
case 'superadmin':
+ case 'admin':
return 'fas fa-crown';
case 'print':
return 'fa-solid fa-print';
@@ -184,6 +190,8 @@ public static function icon($type) {
return 'fa-regular fa-id-card';
case 'department' :
return 'fa-solid fa-building-user';
+ case 'home' :
+ return 'fa-solid fa-house';
case 'note':
case 'notes':
return 'fas fa-sticky-note';
diff --git a/app/Helpers/StorageHelper.php b/app/Helpers/StorageHelper.php
index 47700f913ac5..cbd801d30208 100644
--- a/app/Helpers/StorageHelper.php
+++ b/app/Helpers/StorageHelper.php
@@ -16,38 +16,84 @@ public static function downloader($filename, $disk = 'default') : BinaryFileResp
$disk = config('filesystems.default');
}
switch (config("filesystems.disks.$disk.driver")) {
- case 'local':
- return response()->download(Storage::disk($disk)->path($filename)); //works for PRIVATE or public?!
+ case 'local':
+ return response()->download(Storage::disk($disk)->path($filename)); //works for PRIVATE or public?!
- case 's3':
- return redirect()->away(Storage::disk($disk)->temporaryUrl($filename, now()->addMinutes(5))); //works for private or public, I guess?
+ case 's3':
+ return redirect()->away(Storage::disk($disk)->temporaryUrl($filename, now()->addMinutes(5))); //works for private or public, I guess?
- default:
- return Storage::disk($disk)->download($filename);
+ default:
+ return Storage::disk($disk)->download($filename);
}
}
+ public static function getMediaType($file_with_path) {
+
+ // Get the file extension and determine the media type
+ if (Storage::exists($file_with_path)) {
+ $fileinfo = pathinfo($file_with_path);
+ $extension = strtolower($fileinfo['extension']);
+ switch ($extension) {
+ case 'avif':
+ case 'jpg':
+ case 'png':
+ case 'gif':
+ case 'svg':
+ case 'webp':
+ return 'image';
+ case 'pdf':
+ return 'pdf';
+ case 'mp3':
+ case 'wav':
+ case 'ogg':
+ return 'audio';
+ case 'mp4':
+ case 'webm':
+ case 'mov':
+ return 'video';
+ case 'doc':
+ case 'docx':
+ return 'document';
+ case 'txt':
+ return 'text';
+ case 'xls':
+ case 'xlsx':
+ case 'ods':
+ return 'spreadsheet';
+ default:
+ return $extension; // Default for unknown types
+ }
+ }
+ return null;
+ }
/**
* This determines the file types that should be allowed inline and checks their fileinfo extension
* to determine that they are safe to display inline.
*
* @author [
- * @since v7.0.14
- * @param $file_with_path
+ * @since v7.0.14
+ * @param $file_with_path
* @return bool
*/
- public static function allowSafeInline($file_with_path) {
+ public static function allowSafeInline($file_with_path)
+ {
$allowed_inline = [
- 'pdf',
- 'svg',
- 'jpg',
+ 'avif',
+ 'gif',
'gif',
+ 'jpg',
+ 'mov',
+ 'mp3',
+ 'mp4',
+ 'ogg',
+ 'pdf',
+ 'png',
'svg',
- 'avif',
+ 'wav',
+ 'webm',
'webp',
- 'png',
];
@@ -59,10 +105,24 @@ public static function allowSafeInline($file_with_path) {
}
+ public static function getFiletype($file_with_path)
+ {
+
+ // The file exists and is allowed to be displayed inline
+ if (Storage::exists($file_with_path)) {
+ return pathinfo($file_with_path, PATHINFO_EXTENSION);
+ }
+
+ return null;
+
+ }
+
+
/**
* Decide whether to show the file inline or download it.
*/
- public static function showOrDownloadFile($file, $filename) {
+ public static function showOrDownloadFile($file, $filename)
+ {
$headers = [];
diff --git a/app/Http/Controllers/Accessories/AccessoriesController.php b/app/Http/Controllers/Accessories/AccessoriesController.php
index f7ddd5394094..00cfce05163f 100755
--- a/app/Http/Controllers/Accessories/AccessoriesController.php
+++ b/app/Http/Controllers/Accessories/AccessoriesController.php
@@ -77,13 +77,30 @@ public function store(ImageUploadRequest $request) : RedirectResponse
$accessory->supplier_id = request('supplier_id');
$accessory->notes = request('notes');
- $accessory = $request->handleImages($accessory);
+ if ($request->has('use_cloned_image')) {
+ $cloned_model_img = Accessory::select('image')->find($request->input('clone_image_from_id'));
+ if ($cloned_model_img) {
+ $new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
+ $new_image = 'accessories/'.$new_image_name;
+ Storage::disk('public')->copy('accessories/'.$cloned_model_img->image, $new_image);
+ $accessory->image = $new_image_name;
+ }
+
+ } else {
+ $accessory = $request->handleImages($accessory);
+ }
+
+ if($request->get('redirect_option') === 'back'){
+ session()->put(['redirect_option' => 'index']);
+ } else {
+ session()->put(['redirect_option' => $request->get('redirect_option')]);
+ }
- session()->put(['redirect_option' => $request->get('redirect_option')]);
// Was the accessory created?
if ($accessory->save()) {
// Redirect to the new accessory page
- return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.create.success'));
+ return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
+ ->with('success', trans('admin/accessories/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($accessory->getErrors());
@@ -95,16 +112,10 @@ public function store(ImageUploadRequest $request) : RedirectResponse
* @author [A. Gianotto] []
* @param int $accessoryId
*/
- public function edit($accessoryId = null) : View | RedirectResponse
+ public function edit(Accessory $accessory) : View | RedirectResponse
{
-
- if ($item = Accessory::find($accessoryId)) {
- $this->authorize($item);
- return view('accessories.edit', compact('item'))->with('category_type', 'accessory');
- }
-
- return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
-
+ $this->authorize('update', Accessory::class);
+ return view('accessories.edit')->with('item', $accessory)->with('category_type', 'accessory');
}
/**
@@ -114,24 +125,18 @@ public function edit($accessoryId = null) : View | RedirectResponse
* @param int $accessoryId
* @since [v6.0]
*/
- public function getClone($accessoryId = null) : View | RedirectResponse
+ public function getClone(Accessory $accessory) : View | RedirectResponse
{
$this->authorize('create', Accessory::class);
-
- // Check if the asset exists
- if (is_null($accessory_to_clone = Accessory::find($accessoryId))) {
- // Redirect to the asset management page
- return redirect()->route('accessories.index')
- ->with('error', trans('admin/accessories/message.does_not_exist', ['id' => $accessoryId]));
- }
-
- $accessory = clone $accessory_to_clone;
- $accessory->id = null;
- $accessory->location_id = null;
+ $cloned = clone $accessory;
+ $accessory_to_clone = $accessory;
+ $cloned->id = null;
+ $cloned->deleted_at = '';
return view('accessories/edit')
- ->with('item', $accessory);
+ ->with('cloned_model', $accessory_to_clone)
+ ->with('item', $cloned);
}
@@ -142,9 +147,9 @@ public function getClone($accessoryId = null) : View | RedirectResponse
* @param ImageUploadRequest $request
* @param int $accessoryId
*/
- public function update(ImageUploadRequest $request, $accessoryId = null) : RedirectResponse
+ public function update(ImageUploadRequest $request, Accessory $accessory) : RedirectResponse
{
- if ($accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessoryId)) {
+ if ($accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessory->id)) {
$this->authorize($accessory);
@@ -180,7 +185,8 @@ public function update(ImageUploadRequest $request, $accessoryId = null) : Redir
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($accessory->save()) {
- return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.update.success'));
+ return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
+ ->with('success', trans('admin/accessories/message.update.success'));
}
} else {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
@@ -197,15 +203,15 @@ public function update(ImageUploadRequest $request, $accessoryId = null) : Redir
*/
public function destroy($accessoryId) : RedirectResponse
{
- if (is_null($accessory = Accessory::find($accessoryId))) {
+ if (is_null($accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessoryId))) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
}
$this->authorize($accessory);
- if ($accessory->hasUsers() > 0) {
- return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.assoc_users', ['count'=> $accessory->hasUsers()]));
+ if ($accessory->checkouts_count > 0) {
+ return redirect()->route('accessories.index')->with('error', trans('admin/accessories/general.delete_disabled'));
}
if ($accessory->image) {
@@ -231,14 +237,13 @@ public function destroy($accessoryId) : RedirectResponse
* @see AccessoriesController::getDataView() method that generates the JSON response
* @since [v1.0]
*/
- public function show($accessoryID = null) : View | RedirectResponse
+ public function show(Accessory $accessory) : View | RedirectResponse
{
- $accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessoryID);
- $this->authorize('view', $accessory);
- if (isset($accessory->id)) {
- return view('accessories.view', compact('accessory'));
- }
+ $accessory->loadCount('checkouts as checkouts_count');
- return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist', ['id' => $accessoryID]));
+ $accessory->load(['adminuser' => fn($query) => $query->withTrashed()]);
+
+ $this->authorize('view', $accessory);
+ return view('accessories.view', compact('accessory'));
}
}
diff --git a/app/Http/Controllers/Accessories/AccessoriesFilesController.php b/app/Http/Controllers/Accessories/AccessoriesFilesController.php
deleted file mode 100644
index 9dbb16d83aed..000000000000
--- a/app/Http/Controllers/Accessories/AccessoriesFilesController.php
+++ /dev/null
@@ -1,132 +0,0 @@
-]
- * @since [v1.0]
- * @todo Switch to using the AssetFileRequest form request validator.
- */
- public function store(UploadFileRequest $request, $accessoryId = null) : RedirectResponse
- {
-
- if (config('app.lock_passwords')) {
- return redirect()->route('accessories.show', ['accessory'=>$accessoryId])->with('error', trans('general.feature_disabled'));
- }
-
- $accessory = Accessory::find($accessoryId);
-
- if (isset($accessory->id)) {
- $this->authorize('accessories.files', $accessory);
-
- if ($request->hasFile('file')) {
- if (! Storage::exists('private_uploads/accessories')) {
- Storage::makeDirectory('private_uploads/accessories', 775);
- }
-
- foreach ($request->file('file') as $file) {
-
- $file_name = $request->handleFile('private_uploads/accessories/', 'accessory-'.$accessory->id, $file);
- //Log the upload to the log
- $accessory->logUpload($file_name, e($request->input('notes')));
- }
-
-
- return redirect()->route('accessories.show', $accessory->id)->withFragment('files')->with('success', trans('general.file_upload_success'));
-
- }
-
- return redirect()->route('accessories.show', $accessory->id)->withFragment('files')->with('error', trans('general.no_files_uploaded'));
- }
- // Prepare the error message
- return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
-
- }
-
- /**
- * Deletes the selected accessory file.
- *
- * @author [A. Gianotto] []
- * @since [v1.0]
- * @param int $accessoryId
- * @param int $fileId
- */
- public function destroy($accessoryId = null, $fileId = null) : RedirectResponse
- {
- if ($accessory = Accessory::find($accessoryId)) {
- $this->authorize('update', $accessory);
-
- if ($log = Actionlog::find($fileId)) {
-
- if (Storage::exists('private_uploads/accessories/'.$log->filename)) {
- try {
- Storage::delete('private_uploads/accessories/' . $log->filename);
- $log->delete();
- return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
- } catch (\Exception $e) {
- Log::debug($e);
- return redirect()->route('accessories.index')->with('error', trans('general.file_does_not_exist'));
- }
- }
-
- }
- return redirect()->route('accessories.show', ['accessory' => $accessory])->withFragment('files')->with('error', trans('general.log_record_not_found'));
- }
-
- return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
- }
-
- /**
- * Allows the selected file to be viewed.
- *
- * @author [A. Gianotto] []
- * @since [v1.4]
- * @param int $accessoryId
- * @param int $fileId
- */
- public function show($accessoryId = null, $fileId = null) : View | RedirectResponse | Response | BinaryFileResponse | StreamedResponse
- {
-
-
- // the accessory is valid
- if ($accessory = Accessory::find($accessoryId)) {
- $this->authorize('view', $accessory);
- $this->authorize('accessories.files', $accessory);
-
- if ($log = Actionlog::whereNotNull('filename')->where('item_id', $accessory->id)->find($fileId)) {
- $file = 'private_uploads/accessories/'.$log->filename;
-
- try {
- return StorageHelper::showOrDownloadFile($file, $log->filename);
- } catch (\Exception $e) {
- return redirect()->route('accessories.show', ['accessory' => $accessory])->with('error', trans('general.file_not_found'));
- }
- }
-
- return redirect()->route('accessories.show', ['accessory' => $accessory])->withFragment('files')->with('error', trans('general.log_record_not_found'));
-
- }
-
- return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
-
- }
-}
diff --git a/app/Http/Controllers/Accessories/AccessoryCheckinController.php b/app/Http/Controllers/Accessories/AccessoryCheckinController.php
index e36f8a240db8..a7655a278b58 100644
--- a/app/Http/Controllers/Accessories/AccessoryCheckinController.php
+++ b/app/Http/Controllers/Accessories/AccessoryCheckinController.php
@@ -7,7 +7,6 @@
use App\Http\Controllers\Controller;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
-use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use \Illuminate\Contracts\View\View;
@@ -30,9 +29,17 @@ public function create($accessoryUserId = null, $backto = null) : View | Redirec
}
$accessory = Accessory::find($accessory_user->accessory_id);
+
+ //based on what the accessory is checked out to the target redirect option will be displayed accordingly.
+ $target_option = match ($accessory_user->assigned_type) {
+ 'App\Models\Asset' => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.asset')]),
+ 'App\Models\Location' => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.location')]),
+ default => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.user')]),
+ };
$this->authorize('checkin', $accessory);
- return view('accessories/checkin', compact('accessory'))->with('backto', $backto);
+ return view('accessories/checkin', compact('accessory', 'target_option'))->with('backto', $backto);
+
}
/**
@@ -51,8 +58,14 @@ public function store(Request $request, $accessoryCheckoutId = null, $backto = n
$accessory = Accessory::find($accessory_checkout->accessory_id);
- $this->authorize('checkin', $accessory);
+ session()->put('checkedInFrom', $accessory_checkout->assigned_to);
+ session()->put('checkout_to_type', match ($accessory_checkout->assigned_type) {
+ 'App\Models\User' => 'user',
+ 'App\Models\Location' => 'location',
+ 'App\Models\Asset' => 'asset',
+ });
+ $this->authorize('checkin', $accessory);
$checkin_hours = date('H:i:s');
$checkin_at = date('Y-m-d H:i:s');
if ($request->filled('checkin_at')) {
@@ -65,7 +78,8 @@ public function store(Request $request, $accessoryCheckoutId = null, $backto = n
session()->put(['redirect_option' => $request->get('redirect_option')]);
- return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.checkin.success'));
+ return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
+ ->with('success', trans('admin/accessories/message.checkin.success'));
}
// Redirect to the accessory management page with error
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkin.error'));
diff --git a/app/Http/Controllers/Accessories/AccessoryCheckoutController.php b/app/Http/Controllers/Accessories/AccessoryCheckoutController.php
index 58ce78724566..9ed8c0fe4577 100644
--- a/app/Http/Controllers/Accessories/AccessoryCheckoutController.php
+++ b/app/Http/Controllers/Accessories/AccessoryCheckoutController.php
@@ -71,6 +71,7 @@ public function store(AccessoryCheckoutRequest $request, Accessory $accessory) :
$this->authorize('checkout', $accessory);
$target = $this->determineCheckoutTarget();
+ session()->put(['checkout_to_type' => $target]);
$accessory->checkout_qty = $request->input('checkout_qty', 1);
@@ -97,7 +98,7 @@ public function store(AccessoryCheckoutRequest $request, Accessory $accessory) :
// Redirect to the new accessory page
- return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))
+ return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
->with('success', trans('admin/accessories/message.checkout.success'));
}
}
diff --git a/app/Http/Controllers/Account/AcceptanceController.php b/app/Http/Controllers/Account/AcceptanceController.php
index 278d7e208106..f79ec1842f71 100644
--- a/app/Http/Controllers/Account/AcceptanceController.php
+++ b/app/Http/Controllers/Account/AcceptanceController.php
@@ -7,6 +7,7 @@
use App\Events\ItemAccepted;
use App\Events\ItemDeclined;
use App\Http\Controllers\Controller;
+use App\Mail\CheckoutAcceptanceResponseMail;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
@@ -20,9 +21,12 @@
use App\Models\Component;
use App\Models\Consumable;
use App\Notifications\AcceptanceAssetAcceptedNotification;
+use App\Notifications\AcceptanceAssetAcceptedToUserNotification;
use App\Notifications\AcceptanceAssetDeclinedNotification;
+use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use App\Http\Controllers\SettingsController;
@@ -148,6 +152,8 @@ public function store(Request $request, $id) : RedirectResponse
}
}
+
+ $assigned_user = User::find($acceptance->assigned_to_id);
// this is horrible
switch($acceptance->checkoutable_type){
case 'App\Models\Asset':
@@ -157,35 +163,30 @@ public function store(Request $request, $id) : RedirectResponse
return redirect()->back()->with('error', trans('admin/models/message.does_not_exist'));
}
$display_model = $asset_model->name;
- $assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Accessory':
$pdf_view_route ='account.accept.accept-accessory-eula';
$accessory = Accessory::find($item->id);
$display_model = $accessory->name;
- $assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\LicenseSeat':
$pdf_view_route ='account.accept.accept-license-eula';
$license = License::find($item->license_id);
$display_model = $license->name;
- $assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Component':
$pdf_view_route ='account.accept.accept-component-eula';
$component = Component::find($item->id);
$display_model = $component->name;
- $assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
case 'App\Models\Consumable':
$pdf_view_route ='account.accept.accept-consumable-eula';
$consumable = Consumable::find($item->id);
$display_model = $consumable->name;
- $assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
break;
}
// if ($acceptance->checkoutable_type == 'App\Models\Asset') {
@@ -208,9 +209,12 @@ public function store(Request $request, $id) : RedirectResponse
*/
$branding_settings = SettingsController::getPDFBranding();
- if (is_null($branding_settings->logo)){
- $path_logo = "";
- } else {
+ $path_logo = "";
+
+ // Check for the PDF logo path and use that, otherwise use the regular logo path
+ if (!is_null($branding_settings->acceptance_pdf_logo)) {
+ $path_logo = public_path() . '/uploads/' . $branding_settings->acceptance_pdf_logo;
+ } elseif (!is_null($branding_settings->logo)) {
$path_logo = public_path() . '/uploads/' . $branding_settings->logo;
}
@@ -223,11 +227,12 @@ public function store(Request $request, $id) : RedirectResponse
'note' => $request->input('note'),
'check_out_date' => Carbon::parse($acceptance->created_at)->format('Y-m-d'),
'accepted_date' => Carbon::parse($acceptance->accepted_at)->format('Y-m-d'),
- 'assigned_to' => $assigned_to,
+ 'assigned_to' => $assigned_user->present()->fullName,
'company_name' => $branding_settings->site_name,
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'logo' => $path_logo,
'date_settings' => $branding_settings->date_display_format,
+ 'admin' => auth()->user()->present()?->fullName,
];
if ($pdf_view_route!='') {
@@ -237,8 +242,21 @@ public function store(Request $request, $id) : RedirectResponse
}
$acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note'));
+
+ // Send the PDF to the signing user
+ if (($request->input('send_copy') == '1') && ($assigned_user->email !='')) {
+
+ // Add the attachment for the signing user into the $data array
+ $data['file'] = $pdf_filename;
+ $locale = $assigned_user->locale;
+ try {
+ $assigned_user->notify((new AcceptanceAssetAcceptedToUserNotification($data))->locale($locale));
+ } catch (\Exception $e) {
+ Log::warning($e);
+ }
+ }
try {
- $acceptance->notify(new AcceptanceAssetAcceptedNotification($data));
+ $acceptance->notify((new AcceptanceAssetAcceptedNotification($data))->locale(Setting::getSettings()->locale));
} catch (\Exception $e) {
Log::warning($e);
}
@@ -330,10 +348,29 @@ public function store(Request $request, $id) : RedirectResponse
$acceptance->decline($sig_filename, $request->input('note'));
$acceptance->notify(new AcceptanceAssetDeclinedNotification($data));
+ Log::debug('New event acceptance.');
event(new CheckoutDeclined($acceptance));
$return_msg = trans('admin/users/message.declined');
}
+ if ($acceptance->alert_on_response_id) {
+ try {
+ $recipient = User::find($acceptance->alert_on_response_id);
+
+ if ($recipient) {
+ Log::debug('Attempting to send email acceptance.');
+ Mail::to($recipient)->send(new CheckoutAcceptanceResponseMail(
+ $acceptance,
+ $recipient,
+ $request->input('asset_acceptance') === 'accepted',
+ ));
+ Log::debug('Send email notification sucess on checkout acceptance response.');
+ }
+ } catch (Exception $e) {
+ Log::error($e->getMessage());
+ Log::warning($e);
+ }
+ }
return redirect()->to('account/accept')->with('success', $return_msg);
diff --git a/app/Http/Controllers/Api/AccessoriesController.php b/app/Http/Controllers/Api/AccessoriesController.php
index 76e69591d4f1..7933b19057ae 100644
--- a/app/Http/Controllers/Api/AccessoriesController.php
+++ b/app/Http/Controllers/Api/AccessoriesController.php
@@ -66,7 +66,7 @@ public function index(Request $request)
}
if ($request->filled('company_id')) {
- $accessories->where('company_id', '=', $request->input('company_id'));
+ $accessories->where('accessories.company_id', '=', $request->input('company_id'));
}
if ($request->filled('category_id')) {
@@ -249,11 +249,11 @@ public function update(ImageUploadRequest $request, $id)
public function destroy($id)
{
$this->authorize('delete', Accessory::class);
- $accessory = Accessory::findOrFail($id);
+ $accessory = Accessory::withCount('checkouts as checkouts_count')->findOrFail($id);
$this->authorize($accessory);
- if ($accessory->hasUsers() > 0) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.assoc_users', ['count'=> $accessory->hasUsers()])));
+ if ($accessory->checkouts_count > 0) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/general.delete_disabled')));
}
$accessory->delete();
@@ -288,32 +288,42 @@ public function checkout(AccessoryCheckoutRequest $request, Accessory $accessory
'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')));
- return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success')));
+ return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkout.success')));
}
/**
* Check in the item so that it can be checked out again to someone else
*
- * @uses Accessory::checkin_email() to determine if an email can and should be sent
- * @author [A. Gianotto] []
* @param Request $request
* @param int $accessoryUserId
* @param string $backto
- * @return \Illuminate\Http\RedirectResponse
+ * @return JsonResponse
+ * @uses Accessory::checkin_email() to determine if an email can and should be sent
+ * @author [A. Gianotto] []
* @internal param int $accessoryId
*/
public function checkin(Request $request, $accessoryUserId = null)
{
if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryUserId))) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist')));
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist', ['id' => $accessoryUserId])));
}
$accessory = Accessory::find($accessory_checkout->accessory_id);
@@ -327,7 +337,14 @@ public function checkin(Request $request, $accessoryUserId = null)
$user = User::find($accessory_checkout->assigned_to);
}
- return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkin.success')));
+ $payload = [
+ 'accessory_id' => $accessory->id,
+ 'note' => $request->input('note'),
+ 'created_by' => auth()->id(),
+ 'pivot' => $accessory_checkout->id,
+ ];
+
+ return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkin.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.checkin.error')));
diff --git a/app/Http/Controllers/Api/AssetFilesController.php b/app/Http/Controllers/Api/AssetFilesController.php
deleted file mode 100644
index 4369d287d5c2..000000000000
--- a/app/Http/Controllers/Api/AssetFilesController.php
+++ /dev/null
@@ -1,200 +0,0 @@
-
- *
- * @version v1.0
- * @author [T. Scarsbrook] []
- */
-class AssetFilesController extends Controller
-{
- /**
- * Accepts a POST to upload a file to the server.
- *
- * @param \App\Http\Requests\UploadFileRequest $request
- * @param int $assetId
- * @since [v6.0]
- * @author [T. Scarsbrook] []
- */
- public function store(UploadFileRequest $request, $assetId = null) : JsonResponse
- {
- // Start by checking if the asset being acted upon exists
- if (! $asset = Asset::find($assetId)) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
- }
-
- // Make sure we are allowed to update this asset
- $this->authorize('update', $asset);
-
- if ($request->hasFile('file')) {
- // If the file storage directory doesn't exist; create it
- if (! Storage::exists('private_uploads/assets')) {
- Storage::makeDirectory('private_uploads/assets', 775);
- }
-
- // Loop over the attached files and add them to the asset
- foreach ($request->file('file') as $file) {
- $file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
-
- $asset->logUpload($file_name, e($request->get('notes')));
- }
-
- // All done - report success
- return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.upload.success')));
- }
-
- // We only reach here if no files were included in the POST, so tell the user this
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.upload.nofiles')), 500);
- }
-
- /**
- * List the files for an asset.
- *
- * @param int $assetId
- * @since [v6.0]
- * @author [T. Scarsbrook] []
- */
- public function list($assetId = null) : JsonResponse
- {
- // Start by checking if the asset being acted upon exists
- if (! $asset = Asset::find($assetId)) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
- }
-
- // the asset is valid
- if (isset($asset->id)) {
- $this->authorize('view', $asset);
-
- // Check that there are some uploads on this asset that can be listed
- if ($asset->uploads->count() > 0) {
- $files = array();
- foreach ($asset->uploads as $upload) {
- array_push($files, $upload);
- }
- // Give the list of files back to the user
- return response()->json(Helper::formatStandardApiResponse('success', $files, trans('admin/hardware/message.upload.success')));
- }
-
- // There are no files.
- return response()->json(Helper::formatStandardApiResponse('success', array(), trans('admin/hardware/message.upload.success')));
- }
-
- // Send back an error message
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.error')), 500);
- }
-
- /**
- * Check for permissions and display the file.
- *
- * @param int $assetId
- * @param int $fileId
- * @return \Illuminate\Http\JsonResponse
- * @throws \Illuminate\Auth\Access\AuthorizationException
- * @since [v6.0]
- * @author [T. Scarsbrook] []
- */
- public function show($assetId = null, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
- {
- // Start by checking if the asset being acted upon exists
- if (! $asset = Asset::find($assetId)) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
- }
-
- // the asset is valid
- if (isset($asset->id)) {
- $this->authorize('view', $asset);
-
- // Check that the file being requested exists for the asset
- if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $asset->id)->find($fileId)) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.no_match', ['id' => $fileId])), 404);
- }
-
- // Form the full filename with path
- $file = 'private_uploads/assets/'.$log->filename;
- Log::debug('Checking for '.$file);
-
- if ($log->action_type == 'audit') {
- $file = 'private_uploads/audits/'.$log->filename;
- }
-
- // Check the file actually exists on the filesystem
- if (! Storage::exists($file)) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.does_not_exist', ['id' => $fileId])), 404);
- }
-
- if (request('inline') == 'true') {
-
- $headers = [
- 'Content-Disposition' => 'inline',
- ];
-
- return Storage::download($file, $log->filename, $headers);
- }
-
- return StorageHelper::downloader($file);
- }
-
- // Send back an error message
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.error', ['id' => $fileId])), 500);
- }
-
- /**
- * Delete the associated file
- *
- * @param int $assetId
- * @param int $fileId
- * @since [v6.0]
- * @author [T. Scarsbrook] []
- */
- public function destroy($assetId = null, $fileId = null) : JsonResponse
- {
- // Start by checking if the asset being acted upon exists
- if (! $asset = Asset::find($assetId)) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
- }
-
- $rel_path = 'private_uploads/assets';
-
- // the asset is valid
- if (isset($asset->id)) {
- $this->authorize('update', $asset);
-
- // Check for the file
- $log = Actionlog::find($fileId);
- if ($log) {
- // Check the file actually exists, and delete it
- if (Storage::exists($rel_path.'/'.$log->filename)) {
- Storage::delete($rel_path.'/'.$log->filename);
- }
- // Delete the record of the file
- $log->delete();
-
- // All deleting done - notify the user of success
- return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.deletefile.success')), 200);
- }
-
- // The file doesn't seem to really exist, so report an error
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
- }
-
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
- }
-}
diff --git a/app/Http/Controllers/Api/AssetMaintenancesController.php b/app/Http/Controllers/Api/AssetMaintenancesController.php
deleted file mode 100644
index b4e9b441960b..000000000000
--- a/app/Http/Controllers/Api/AssetMaintenancesController.php
+++ /dev/null
@@ -1,222 +0,0 @@
-
- * @version v1.0
- * @since [v1.8]
- */
- public function index(Request $request) : JsonResponse | array
- {
- $this->authorize('view', Asset::class);
-
- $maintenances = AssetMaintenance::select('asset_maintenances.*')
- ->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.assetstatus', 'adminuser');
-
- if ($request->filled('search')) {
- $maintenances = $maintenances->TextSearch($request->input('search'));
- }
-
- if ($request->filled('asset_id')) {
- $maintenances->where('asset_id', '=', $request->input('asset_id'));
- }
-
- if ($request->filled('supplier_id')) {
- $maintenances->where('asset_maintenances.supplier_id', '=', $request->input('supplier_id'));
- }
-
- if ($request->filled('created_by')) {
- $maintenances->where('asset_maintenances.created_by', '=', $request->input('created_by'));
- }
-
- if ($request->filled('asset_maintenance_type')) {
- $maintenances->where('asset_maintenance_type', '=', $request->input('asset_maintenance_type'));
- }
-
-
- // 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'));
- $limit = app('api_limit_value');
-
- $allowed_columns = [
- 'id',
- 'title',
- 'asset_maintenance_time',
- 'asset_maintenance_type',
- 'cost',
- 'start_date',
- 'completion_date',
- 'notes',
- 'asset_tag',
- 'asset_name',
- 'serial',
- 'created_by',
- 'supplier',
- 'is_warranty',
- 'status_label',
- ];
-
- $order = $request->input('order') === 'asc' ? 'asc' : 'desc';
- $sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
-
- switch ($sort) {
- case 'created_by':
- $maintenances = $maintenances->OrderByCreatedBy($order);
- break;
- case 'supplier':
- $maintenances = $maintenances->OrderBySupplier($order);
- break;
- case 'asset_tag':
- $maintenances = $maintenances->OrderByTag($order);
- break;
- case 'asset_name':
- $maintenances = $maintenances->OrderByAssetName($order);
- break;
- case 'serial':
- $maintenances = $maintenances->OrderByAssetSerial($order);
- break;
- case 'status_label':
- $maintenances = $maintenances->OrderStatusName($order);
- break;
- default:
- $maintenances = $maintenances->orderBy($sort, $order);
- break;
- }
-
- $total = $maintenances->count();
- $maintenances = $maintenances->skip($offset)->take($limit)->get();
- return (new AssetMaintenancesTransformer())->transformAssetMaintenances($maintenances, $total);
-
-
- }
-
-
- /**
- * Validates and stores the new asset maintenance
- *
- * @see AssetMaintenancesController::getCreate() method for the form
- * @author Vincent Sposato
- * @version v1.0
- * @since [v1.8]
- */
- public function store(Request $request) : JsonResponse | array
- {
- $this->authorize('update', Asset::class);
- // create a new model instance
- $maintenance = new AssetMaintenance();
- $maintenance->fill($request->all());
- $maintenance->created_by = auth()->id();
-
- // Was the asset maintenance created?
- if ($maintenance->save()) {
- return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.create.success')));
-
- }
-
- return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
-
- }
-
- /**
- * Validates and stores an update to an asset maintenance
- *
- * @author A. Gianotto
- * @param int $id
- * @param int $request
- * @version v1.0
- * @since [v4.0]
- */
- public function update(Request $request, $id) : JsonResponse | array
- {
- $this->authorize('update', Asset::class);
-
- if ($maintenance = AssetMaintenance::with('asset')->find($id)) {
-
- // Can this user manage this asset?
- if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/asset_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) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('general.asset'), 'id' => $id])));
- }
-
- $maintenance->fill($request->all());
-
- if ($maintenance->save()) {
- return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.edit.success')));
- }
-
- return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
- }
-
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id])));
-
- }
-
- /**
- * Delete an asset maintenance
- *
- * @author A. Gianotto
- * @param int $assetMaintenanceId
- * @version v1.0
- * @since [v4.0]
- */
- public function destroy($assetMaintenanceId) : JsonResponse | array
- {
- $this->authorize('update', Asset::class);
- // Check if the asset maintenance exists
-
- $assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
-
- $assetMaintenance->delete();
-
- return response()->json(Helper::formatStandardApiResponse('success', $assetMaintenance, trans('admin/asset_maintenances/message.delete.success')));
-
-
- }
-
- /**
- * View an asset maintenance
- *
- * @author A. Gianotto
- * @param int $assetMaintenanceId
- * @version v1.0
- * @since [v4.0]
- */
- public function show($assetMaintenanceId) : JsonResponse | array
- {
- $this->authorize('view', Asset::class);
- $assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
- if (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
- return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot view a maintenance for that asset'));
- }
-
- return (new AssetMaintenancesTransformer())->transformAssetMaintenance($assetMaintenance);
-
- }
-}
diff --git a/app/Http/Controllers/Api/AssetModelFilesController.php b/app/Http/Controllers/Api/AssetModelFilesController.php
deleted file mode 100644
index 7f0f06c63514..000000000000
--- a/app/Http/Controllers/Api/AssetModelFilesController.php
+++ /dev/null
@@ -1,179 +0,0 @@
-
- *
- * @version v1.0
- * @author [T. Scarsbrook] []
- */
-class AssetModelFilesController extends Controller
-{
- /**
- * Accepts a POST to upload a file to the server.
- *
- * @param \App\Http\Requests\UploadFileRequest $request
- * @param int $assetModelId
- * @since [v7.0.12]
- * @author [r-xyz]
- */
- public function store(UploadFileRequest $request, $assetModelId = null) : JsonResponse
- {
- // Start by checking if the asset being acted upon exists
- if (! $assetModel = AssetModel::find($assetModelId)) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
- }
-
- // Make sure we are allowed to update this asset
- $this->authorize('update', $assetModel);
-
- if ($request->hasFile('file')) {
- // If the file storage directory doesn't exist; create it
- if (! Storage::exists('private_uploads/assetmodels')) {
- Storage::makeDirectory('private_uploads/assetmodels', 775);
- }
-
- // Loop over the attached files and add them to the asset
- foreach ($request->file('file') as $file) {
- $file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$assetModel->id, $file);
-
- $assetModel->logUpload($file_name, e($request->get('notes')));
- }
-
- // All done - report success
- return response()->json(Helper::formatStandardApiResponse('success', $assetModel, trans('admin/models/message.upload.success')));
- }
-
- // We only reach here if no files were included in the POST, so tell the user this
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.upload.nofiles')), 500);
- }
-
- /**
- * List the files for an asset.
- *
- * @param int $assetmodel
- * @since [v7.0.12]
- * @author [r-xyz]
- */
- public function list($assetmodel_id) : JsonResponse | array
- {
- $assetmodel = AssetModel::with('uploads')->find($assetmodel_id);
- $this->authorize('view', $assetmodel);
- return (new AssetModelsTransformer)->transformAssetModelFiles($assetmodel, $assetmodel->uploads()->count());
- }
-
- /**
- * Check for permissions and display the file.
- *
- * @param int $assetModelId
- * @param int $fileId
- * @return \Illuminate\Http\JsonResponse
- * @throws \Illuminate\Auth\Access\AuthorizationException
- * @since [v7.0.12]
- * @author [r-xyz]
- */
- public function show($assetModelId = null, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
- {
- // Start by checking if the asset being acted upon exists
- if (! $assetModel = AssetModel::find($assetModelId)) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
- }
-
- // the asset is valid
- if (isset($assetModel->id)) {
- $this->authorize('view', $assetModel);
-
- // Check that the file being requested exists for the asset
- if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $assetModel->id)->find($fileId)) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.no_match', ['id' => $fileId])), 404);
- }
-
- // Form the full filename with path
- $file = 'private_uploads/assetmodels/'.$log->filename;
- Log::debug('Checking for '.$file);
-
- if ($log->action_type == 'audit') {
- $file = 'private_uploads/audits/'.$log->filename;
- }
-
- // Check the file actually exists on the filesystem
- if (! Storage::exists($file)) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.does_not_exist', ['id' => $fileId])), 404);
- }
-
- if (request('inline') == 'true') {
-
- $headers = [
- 'Content-Disposition' => 'inline',
- ];
-
- return Storage::download($file, $log->filename, $headers);
- }
-
- return StorageHelper::downloader($file);
- }
-
- // Send back an error message
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.error', ['id' => $fileId])), 500);
- }
-
- /**
- * Delete the associated file
- *
- * @param int $assetModelId
- * @param int $fileId
- * @since [v7.0.12]
- * @author [r-xyz]
- */
- public function destroy($assetModelId = null, $fileId = null) : JsonResponse
- {
- // Start by checking if the asset being acted upon exists
- if (! $assetModel = AssetModel::find($assetModelId)) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
- }
-
- $rel_path = 'private_uploads/assetmodels';
-
- // the asset is valid
- if (isset($assetModel->id)) {
- $this->authorize('update', $assetModel);
-
- // Check for the file
- $log = Actionlog::find($fileId);
- if ($log) {
- // Check the file actually exists, and delete it
- if (Storage::exists($rel_path.'/'.$log->filename)) {
- Storage::delete($rel_path.'/'.$log->filename);
- }
- // Delete the record of the file
- $log->delete();
-
- // All deleting done - notify the user of success
- return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/models/message.deletefile.success')), 200);
- }
-
- // The file doesn't seem to really exist, so report an error
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
- }
-
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
- }
-}
diff --git a/app/Http/Controllers/Api/AssetModelsController.php b/app/Http/Controllers/Api/AssetModelsController.php
index 33a00d1d3f52..c78bb7dafa3c 100644
--- a/app/Http/Controllers/Api/AssetModelsController.php
+++ b/app/Http/Controllers/Api/AssetModelsController.php
@@ -50,6 +50,7 @@ public function index(Request $request) : JsonResponse | array
'fieldset',
'deleted_at',
'updated_at',
+ 'require_serial',
];
$assetmodels = AssetModel::select([
@@ -69,14 +70,33 @@ public function index(Request $request) : JsonResponse | array
'models.fieldset_id',
'models.deleted_at',
'models.updated_at',
+ 'models.require_serial'
])
- ->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues', 'adminuser')
+ ->with('category', 'depreciation', 'manufacturer', 'adminuser')
->withCount('assets as assets_count');
if ($request->input('status')=='deleted') {
$assetmodels->onlyTrashed();
}
+ if ($request->filled('name')) {
+ $assetmodels = $assetmodels->where('models.name', '=', $request->input('name'));
+ }
+
+ if ($request->filled('model_number')) {
+ $assetmodels = $assetmodels->where('models.model_number', '=', $request->input('model_number'));
+ }
+
+ if ($request->input('requestable') == 'true') {
+ $assetmodels = $assetmodels->where('models.requestable', '=', '1');
+ } elseif ($request->input('requestable') == 'false') {
+ $assetmodels = $assetmodels->where('models.requestable', '=', '0');
+ }
+
+ if ($request->filled('notes')) {
+ $assetmodels = $assetmodels->where('models.notes', '=', $request->input('notes'));
+ }
+
if ($request->filled('category_id')) {
$assetmodels = $assetmodels->where('models.category_id', '=', $request->input('category_id'));
}
@@ -136,7 +156,7 @@ public function store(StoreAssetModelRequest $request) : JsonResponse
$assetmodel = $request->handleImages($assetmodel);
if ($assetmodel->save()) {
- return response()->json(Helper::formatStandardApiResponse('success', $assetmodel, trans('admin/models/message.create.success')));
+ return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));
@@ -189,7 +209,7 @@ public function update(StoreAssetModelRequest $request, $id) : JsonResponse
$assetmodel = AssetModel::findOrFail($id);
$assetmodel->fill($request->all());
$assetmodel = $request->handleImages($assetmodel);
-
+
/**
* Allow custom_fieldset_id to override and populate fieldset_id.
* This is stupid, but required for legacy API support.
@@ -204,7 +224,7 @@ public function update(StoreAssetModelRequest $request, $id) : JsonResponse
if ($assetmodel->save()) {
- return response()->json(Helper::formatStandardApiResponse('success', $assetmodel, trans('admin/models/message.update.success')));
+ return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.update.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));
diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php
index cfa7652501a9..5ef41174cea2 100644
--- a/app/Http/Controllers/Api/AssetsController.php
+++ b/app/Http/Controllers/Api/AssetsController.php
@@ -34,6 +34,7 @@
use Illuminate\Support\Facades\Route;
use App\View\Label;
use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Validator;
/**
@@ -113,17 +114,23 @@ public function index(Request $request, $action = null, $upcoming_status = null)
'byod',
'asset_eol_date',
'requestable',
+ 'jobtitle',
];
+ $all_custom_fields = CustomField::where('type', Asset::class); //used as a 'cache' of custom fields throughout this page load
+
+ foreach ($all_custom_fields as $field) {
+ $allowed_columns[] = $field->db_column_name();
+ }
+
$filter = [];
if ($request->filled('filter')) {
$filter = json_decode($request->input('filter'), true);
- }
- $all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
- foreach ($all_custom_fields as $field) {
- $allowed_columns[] = $field->db_column_name();
+ $filter = array_filter($filter, function ($key) use ($allowed_columns) {
+ return in_array($key, $allowed_columns);
+ }, ARRAY_FILTER_USE_KEY);
}
$assets = Asset::select('assets.*')
@@ -139,6 +146,7 @@ public function index(Request $request, $action = null, $upcoming_status = null)
'model.category',
'model.manufacturer',
'model.fieldset',
+ 'model.depreciation',
'supplier'
); // it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users.
@@ -299,7 +307,13 @@ public function index(Request $request, $action = null, $upcoming_status = null)
}
if ($request->filled('model_id')) {
- $assets->InModelList([$request->input('model_id')]);
+ // If model_id is already an array, just use it as-is
+ if (is_array($request->input('model_id'))) {
+ $assets->InModelList($request->input('model_id'));
+ } else {
+ // Otherwise, turn it into an array
+ $assets->InModelList([$request->input('model_id')]);
+ }
}
if ($request->filled('category_id')) {
@@ -388,6 +402,9 @@ public function index(Request $request, $action = null, $upcoming_status = null)
case 'assigned_to':
$assets->OrderAssigned($order);
break;
+ case 'jobtitle':
+ $assets->OrderByJobTitle($order);
+ break;
case 'created_by':
$assets->OrderByCreatedByName($order);
break;
@@ -435,12 +452,6 @@ public function index(Request $request, $action = null, $upcoming_status = null)
}]);
}
-
-
- /**
- * Here we're just determining which Transformer (via $transformer) to use based on the
- * variables we set earlier on in this method - we default to AssetsTransformer.
- */
return (new $transformer)->transformAssets($assets, $total, $request);
}
@@ -491,15 +502,32 @@ public function showByTag(Request $request, $tag): JsonResponse | array
public function showBySerial(Request $request, $serial): JsonResponse | array
{
$this->authorize('index', Asset::class);
- $assets = Asset::where('serial', $serial)->with('assetstatus')->with('assignedTo');
+ $assets = Asset::where('serial', $serial)->with([
+ 'assetstatus',
+ 'assignedTo',
+ 'company',
+ 'defaultLoc',
+ 'location',
+ 'model.category',
+ 'model.depreciation',
+ 'model.fieldset',
+ 'model.manufacturer',
+ 'supplier',
+ ]);
// Check if they've passed ?deleted=true
if ($request->input('deleted', 'false') == 'true') {
$assets = $assets->withTrashed();
}
- if (($assets = $assets->get()) && ($assets->count()) > 0) {
- return (new AssetsTransformer)->transformAssets($assets, $assets->count());
+ $offset = ($request->input('offset') > $assets->count()) ? $assets->count() : app('api_offset_value');
+ $limit = app('api_limit_value');
+
+ $total = $assets->count();
+ $assets = $assets->skip($offset)->take($limit)->get();
+
+ if (($assets) && ($assets->count()) > 0) {
+ return (new AssetsTransformer)->transformAssets($assets, $total);
}
// If there are 0 results, return the "no such asset" response
@@ -556,7 +584,12 @@ public function selectlist(Request $request): array
'assets.assigned_to',
'assets.assigned_type',
'assets.status_id',
- ])->with('model', 'assetstatus', 'assignedTo')->NotArchived();
+ ])->with('model', 'assetstatus', 'assignedTo')
+ ->NotArchived();
+
+ if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($request->filled('companyId'))) {
+ $assets->where('assets.company_id', $request->input('companyId'));
+ }
if ($request->filled('assetStatusType') && $request->input('assetStatusType') === 'RTD') {
$assets = $assets->RTD();
@@ -566,7 +599,6 @@ public function selectlist(Request $request): array
$assets = $assets->AssignedSearch($request->input('search'));
}
-
$assets = $assets->paginate(50);
// Loop through and set some custom properties for the transformer to use.
@@ -578,7 +610,7 @@ public function selectlist(Request $request): array
$asset->use_text = $asset->present()->fullName;
if (($asset->checkedOutToUser()) && ($asset->assigned)) {
- $asset->use_text .= ' → ' . $asset->assigned->getFullNameAttribute();
+ $asset->use_text .= ' → ' . $asset->assigned->display_name;
}
@@ -618,48 +650,8 @@ public function store(StoreAssetRequest $request): JsonResponse
$asset = $request->handleImages($asset);
- // Update custom fields in the database.
- $model = AssetModel::find($request->input('model_id'));
+ $asset->customFill($request, Auth::user(), true);
- // Check that it's an object and not a collection
- // (Sometimes people send arrays here and they shouldn't
- if (($model) && ($model instanceof AssetModel) && ($model->fieldset)) {
- foreach ($model->fieldset->fields as $field) {
-
- // Set the field value based on what was sent in the request
- $field_val = $request->input($field->db_column, null);
-
- // If input value is null, use custom field's default value
- if ($field_val == null) {
- Log::debug('Field value for ' . $field->db_column . ' is null');
- $field_val = $field->defaultValue($request->get('model_id'));
- Log::debug('Use the default fieldset value of ' . $field->defaultValue($request->get('model_id')));
- }
-
- // if the field is set to encrypted, make sure we encrypt the value
- if ($field->field_encrypted == '1') {
- Log::debug('This model field is encrypted in this fieldset.');
-
- if (Gate::allows('assets.view.encrypted_custom_fields')) {
-
- // If input value is null, use custom field's default value
- if (($field_val == null) && ($request->has('model_id') != '')) {
- $field_val = Crypt::encrypt($field->defaultValue($request->get('model_id')));
- } else {
- $field_val = Crypt::encrypt($request->input($field->db_column));
- }
- }
- }
- if ($field->element == 'checkbox') {
- if (is_array($field_val)) {
- $field_val = implode(',', $field_val);
- }
- }
-
-
- $asset->{$field->db_column} = $field_val;
- }
- }
if ($asset->save()) {
if ($request->get('assigned_user')) {
@@ -679,7 +671,9 @@ public function store(StoreAssetRequest $request): JsonResponse
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.create.success')));
- return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.create.success')));
+ // below is what we want the _eventual_ return to look like - in a more standardized format.
+ // return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.create.success')));
+
}
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
@@ -721,29 +715,8 @@ public function update(UpdateAssetRequest $request, Asset $asset): JsonResponse
$model = $asset->model;
// Update custom fields
- $problems_updating_encrypted_custom_fields = false;
- if (($model) && (isset($model->fieldset))) {
- foreach ($model->fieldset->fields as $field) {
- $field_val = $request->input($field->db_column, null);
-
- if ($request->has($field->db_column)) {
- if ($field->element == 'checkbox') {
- if (is_array($field_val)) {
- $field_val = implode(',', $field_val);
- }
- }
- if ($field->field_encrypted == '1') {
- if (Gate::allows('assets.view.encrypted_custom_fields')) {
- $field_val = Crypt::encrypt($field_val);
- } else {
- $problems_updating_encrypted_custom_fields = true;
- continue;
- }
- }
- $asset->{$field->db_column} = $field_val;
- }
- }
- }
+ $problems_updating_encrypted_custom_fields = !$asset->customFill($request, Auth::user());
+
if ($asset->save()) {
if (($request->filled('assigned_user')) && ($target = User::find($request->get('assigned_user')))) {
$location = $target->location_id;
@@ -1047,7 +1020,7 @@ public function checkinByTag(Request $request, $tag = null): JsonResponse
* @param int $id
* @since [v4.0]
*/
- public function audit(Request $request): JsonResponse
+ public function audit(Request $request, Asset $asset): JsonResponse
{
$this->authorize('audit', Asset::class);
@@ -1055,19 +1028,76 @@ public function audit(Request $request): JsonResponse
$settings = Setting::getSettings();
$dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
- // No tag passed - return an error
- if (!$request->filled('asset_tag')) {
- return response()->json(Helper::formatStandardApiResponse('error', [
- 'asset_tag' => '',
- 'error' => trans('admin/hardware/message.no_tag'),
- ], trans('admin/hardware/message.no_tag')), 200);
+ // Allow the asset tag to be passed in the payload (legacy method)
+ if ($request->filled('asset_tag')) {
+ $asset = Asset::where('asset_tag', '=', $request->input('asset_tag'))->first();
}
+ if ($asset) {
- $asset = Asset::where('asset_tag', '=', $request->input('asset_tag'))->first();
+ $originalValues = $asset->getRawOriginal();
+ $asset->next_audit_date = $dt;
+
+ if ($request->filled('next_audit_date')) {
+ $asset->next_audit_date = $request->input('next_audit_date');
+ }
+
+ // Check to see if they checked the box to update the physical location,
+ // not just note it in the audit notes
+ if ($request->input('update_location') == '1') {
+ $asset->location_id = $request->input('location_id');
+ }
+
+ $asset->last_audit_date = date('Y-m-d H:i:s');
+
+ // Set up the payload for re-display in the API response
+ $payload = [
+ 'id' => $asset->id,
+ 'asset_tag' => $asset->asset_tag,
+ 'note' => $request->input('note'),
+ 'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date),
+ ];
+
+
+ /**
+ * Update custom fields in the database.
+ * Validation for these fields is handled through the AssetRequest form request
+ * $model = AssetModel::find($request->get('model_id'));
+ */
+ if (($asset->model) && ($asset->model->fieldset)) {
+ $payload['custom_fields'] = [];
+ foreach ($asset->model->fieldset->fields as $field) {
+ if (($field->display_audit == '1') && ($request->has($field->db_column))) {
+ if ($field->field_encrypted == '1') {
+ if (Gate::allows('assets.view.encrypted_custom_fields')) {
+ if (is_array($request->input($field->db_column))) {
+ $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
+ } else {
+ $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
+ }
+ }
+ } else {
+ if (is_array($request->input($field->db_column))) {
+ $asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
+ } else {
+ $asset->{$field->db_column} = $request->input($field->db_column);
+ }
+ }
+ $payload['custom_fields'][$field->db_column] = $request->input($field->db_column);
+ }
+
+ }
+ }
+
+ // Invoke the validation to see if the audit will complete successfully
+ $asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
+
+ // Validate the rest of the data before we turn off the event dispatcher
+ if ($asset->isInvalid()) {
+ return response()->json(Helper::formatStandardApiResponse('error', ['asset_tag' => $asset->asset_tag], $asset->getErrors()));
+ }
- if ($asset) {
/**
* Even though we do a save() further down, we don't want to log this as a "normal" asset update,
@@ -1077,55 +1107,33 @@ public function audit(Request $request): JsonResponse
*
* To prevent this double-logging (one for update and one for audit), we skip the observer and bypass
* that de-normed update log entry by using unsetEventDispatcher(), BUT invoking unsetEventDispatcher()
- * will bypass normal model-level validation that's usually handled at the observer )
+ * will bypass normal model-level validation that's usually handled at the observer)
*
* We handle validation on the save() by checking if the asset is valid via the ->isValid() method,
* which manually invokes Watson Validating to make sure the asset's model is valid.
*
* @see \App\Observers\AssetObserver::updating()
+ * @see \App\Models\Asset::save()
*/
- $asset->unsetEventDispatcher();
- $asset->next_audit_date = $dt;
- if ($request->filled('next_audit_date')) {
- $asset->next_audit_date = $request->input('next_audit_date');
- }
-
- // Check to see if they checked the box to update the physical location,
- // not just note it in the audit notes
- if ($request->input('update_location') == '1') {
- $asset->location_id = $request->input('location_id');
- }
+ $asset->unsetEventDispatcher();
- $asset->last_audit_date = date('Y-m-d H:i:s');
/**
* Invoke Watson Validating to check the asset itself and check to make sure it saved correctly.
* We have to invoke this manually because of the unsetEventDispatcher() above.)
*/
if ($asset->isValid() && $asset->save()) {
- $asset->logAudit(request('note'), request('location_id'));
-
- return response()->json(Helper::formatStandardApiResponse('success', [
- 'asset_tag' => e($asset->asset_tag),
- 'note' => e($request->input('note')),
- 'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date),
- ], trans('admin/hardware/message.audit.success')));
+ $asset->logAudit(request('note'), request('location_id'), null, $originalValues);
+ return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/hardware/message.audit.success')));
}
- // Asset failed validation or was not able to be saved
- return response()->json(Helper::formatStandardApiResponse('error', [
- 'asset_tag' => e($asset->asset_tag),
- 'error' => $asset->getErrors()->first(),
- ], trans('admin/hardware/message.audit.error', ['error' => $asset->getErrors()->first()])), 200);
}
// No matching asset for the asset tag that was passed.
- return response()->json(Helper::formatStandardApiResponse('error', [
- 'asset_tag' => e($request->input('asset_tag')),
- 'error' => trans('admin/hardware/message.audit.error'),
- ], trans('admin/hardware/message.audit.error', ['error' => trans('admin/hardware/message.does_not_exist')])), 200);
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
+
}
@@ -1230,7 +1238,10 @@ public function assignedAccessories(Request $request, Asset $asset) : JsonRespon
{
$this->authorize('view', Asset::class);
$this->authorize('view', $asset);
- $accessory_checkouts = AccessoryCheckout::AssetsAssigned()->with('adminuser')->with('accessories');
+ $accessory_checkouts = AccessoryCheckout::AssetsAssigned()
+ ->where('assigned_to', $asset->id)
+ ->with('adminuser')
+ ->with('accessories');
$offset = ($request->input('offset') > $accessory_checkouts->count()) ? $accessory_checkouts->count() : app('api_offset_value');
$limit = app('api_limit_value');
@@ -1239,6 +1250,8 @@ public function assignedAccessories(Request $request, Asset $asset) : JsonRespon
$accessory_checkouts = $accessory_checkouts->skip($offset)->take($limit)->get();
return (new AssetsTransformer)->transformCheckedoutAccessories($accessory_checkouts, $total);
}
+
+
/**
* Generate asset labels by tag
*
diff --git a/app/Http/Controllers/Api/CategoriesController.php b/app/Http/Controllers/Api/CategoriesController.php
index 319b51dd116f..85cb70b12612 100644
--- a/app/Http/Controllers/Api/CategoriesController.php
+++ b/app/Http/Controllers/Api/CategoriesController.php
@@ -56,7 +56,7 @@ public function index(Request $request) : array
'notes',
])
->with('adminuser')
- ->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count');
+ ->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count');
/*
@@ -212,7 +212,7 @@ public function update(ImageUploadRequest $request, $id) : JsonResponse
public function destroy($id) : JsonResponse
{
$this->authorize('delete', Category::class);
- $category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count')->findOrFail($id);
+ $category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($id);
if (! $category->isDeletable()) {
return response()->json(
diff --git a/app/Http/Controllers/Api/CheckoutRequest.php b/app/Http/Controllers/Api/CheckoutRequest.php
new file mode 100644
index 000000000000..9b66531ae40c
--- /dev/null
+++ b/app/Http/Controllers/Api/CheckoutRequest.php
@@ -0,0 +1,44 @@
+user());
+ return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.requests.success')));
+ } catch (AssetNotRequestable $e) {
+ return response()->json(Helper::formatStandardApiResponse('error', 'Asset is not requestable'));
+ } catch (AuthorizationException $e) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.insufficient_permissions')));
+ } catch (Exception $e) {
+ report($e);
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
+ }
+ }
+
+ public function destroy(Asset $asset): JsonResponse
+ {
+ try {
+ CancelCheckoutRequestAction::run($asset, auth()->user());
+ return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.requests.canceled')));
+ } catch (AuthorizationException $e) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.insufficient_permissions')));
+ } catch (Exception $e) {
+ report($e);
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
+ }
+ }
+}
diff --git a/app/Http/Controllers/Api/CompaniesController.php b/app/Http/Controllers/Api/CompaniesController.php
index fd7f57ddce30..aee38301f428 100644
--- a/app/Http/Controllers/Api/CompaniesController.php
+++ b/app/Http/Controllers/Api/CompaniesController.php
@@ -43,7 +43,10 @@ public function index(Request $request) : JsonResponse | array
$companies = Company::withCount(['assets as assets_count' => function ($query) {
$query->AssetsForShow();
- }])->withCount('licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count');
+ }])
+ ->with('adminuser')
+ ->withCount('licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count');
+
if ($request->filled('search')) {
$companies->TextSearch($request->input('search'));
@@ -119,6 +122,7 @@ public function show($id) : array
{
$this->authorize('view', Company::class);
$company = Company::findOrFail($id);
+ $this->authorize('view', $company);
return (new CompaniesTransformer)->transformCompany($company);
}
@@ -136,6 +140,7 @@ public function update(ImageUploadRequest $request, $id) : JsonResponse
{
$this->authorize('update', Company::class);
$company = Company::findOrFail($id);
+ $this->authorize('update', $company);
$company->fill($request->all());
$company = $request->handleImages($company);
@@ -188,6 +193,7 @@ public function selectlist(Request $request) : array
'companies.image',
]);
+
if ($request->filled('search')) {
$companies = $companies->where('companies.name', 'LIKE', '%'.$request->get('search').'%');
}
diff --git a/app/Http/Controllers/Api/ComponentsController.php b/app/Http/Controllers/Api/ComponentsController.php
index 0f594f5e75d1..8881628b391c 100644
--- a/app/Http/Controllers/Api/ComponentsController.php
+++ b/app/Http/Controllers/Api/ComponentsController.php
@@ -48,7 +48,8 @@ public function index(Request $request) : JsonResponse | array
];
$components = Component::select('components.*')
- ->with('company', 'location', 'category', 'assets', 'supplier', 'adminuser', 'manufacturer');
+ ->with('company', 'location', 'category', 'assets', 'supplier', 'adminuser', 'manufacturer', 'uncontrainedAssets')
+ ->withSum('uncontrainedAssets', 'components_assets.assigned_qty');
if ($request->filled('search')) {
$components = $components->TextSearch($request->input('search'));
@@ -59,7 +60,7 @@ public function index(Request $request) : JsonResponse | array
}
if ($request->filled('company_id')) {
- $components->where('company_id', '=', $request->input('company_id'));
+ $components->where('components.company_id', '=', $request->input('company_id'));
}
if ($request->filled('category_id')) {
@@ -197,6 +198,11 @@ public function destroy($id) : JsonResponse
$this->authorize('delete', Component::class);
$component = Component::findOrFail($id);
$this->authorize('delete', $component);
+
+ if ($component->numCheckedOut() > 0) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.delete.error_qty')));
+ }
+
$component->delete();
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.delete.success')));
diff --git a/app/Http/Controllers/Api/ConsumablesController.php b/app/Http/Controllers/Api/ConsumablesController.php
index 7ff676c7bef3..e163f080aa4b 100644
--- a/app/Http/Controllers/Api/ConsumablesController.php
+++ b/app/Http/Controllers/Api/ConsumablesController.php
@@ -40,7 +40,7 @@ public function index(Request $request) : array
}
if ($request->filled('company_id')) {
- $consumables->where('company_id', '=', $request->input('company_id'));
+ $consumables->where('consumables.company_id', '=', $request->input('company_id'));
}
if ($request->filled('category_id')) {
@@ -228,11 +228,16 @@ public function getDataView($consumableId) : array
foreach ($consumable->consumableAssignments as $consumable_assignment) {
$rows[] = [
'avatar' => ($consumable_assignment->user) ? e($consumable_assignment->user->present()->gravatar) : '',
- 'name' => ($consumable_assignment->user) ? $consumable_assignment->user->present()->nameUrl() : 'Deleted User',
+ 'user' => ($consumable_assignment->user) ? [
+ 'id' => (int) $consumable_assignment->user->id,
+ 'name'=> e($consumable_assignment->user->display_name),
+ ] : null,
'created_at' => Helper::getFormattedDateObject($consumable_assignment->created_at, 'datetime'),
'note' => ($consumable_assignment->note) ? e($consumable_assignment->note) : null,
- 'admin' => ($consumable_assignment->adminuser) ? $consumable_assignment->adminuser->present()->nameUrl() : null, // legacy, so we don't change the shape of the response
- 'created_by' => ($consumable_assignment->adminuser) ? $consumable_assignment->adminuser->present()->nameUrl() : null,
+ 'created_by' => ($consumable_assignment->adminuser) ? [
+ 'id' => (int) $consumable_assignment->adminuser->id,
+ 'name'=> e($consumable_assignment->adminuser->display_name),
+ ] : null,
];
}
diff --git a/app/Http/Controllers/Api/CustomFieldsetsController.php b/app/Http/Controllers/Api/CustomFieldsetsController.php
index 5dbd507917a5..dbfd87c237a9 100644
--- a/app/Http/Controllers/Api/CustomFieldsetsController.php
+++ b/app/Http/Controllers/Api/CustomFieldsetsController.php
@@ -33,7 +33,7 @@ class CustomFieldsetsController extends Controller
public function index() : array
{
$this->authorize('index', CustomField::class);
- $fieldsets = CustomFieldset::withCount('fields as fields_count', 'models as models_count')->get();
+ $fieldsets = CustomFieldset::withCount('fields as fields_count')->get();
return (new CustomFieldsetsTransformer)->transformCustomFieldsets($fieldsets, $fieldsets->count());
}
@@ -119,7 +119,7 @@ public function destroy($id) : JsonResponse
$this->authorize('delete', CustomField::class);
$fieldset = CustomFieldset::findOrFail($id);
- $modelsCount = $fieldset->models->count();
+ $modelsCount = $fieldset->customizables()->count();
$fieldsCount = $fieldset->fields->count();
if (($modelsCount > 0) || ($fieldsCount > 0)) {
diff --git a/app/Http/Controllers/Api/GroupsController.php b/app/Http/Controllers/Api/GroupsController.php
index 5f2e222c70a8..0f474ece8368 100644
--- a/app/Http/Controllers/Api/GroupsController.php
+++ b/app/Http/Controllers/Api/GroupsController.php
@@ -76,7 +76,7 @@ public function store(Request $request) : JsonResponse
$this->authorize('superadmin');
$group = new Group;
// Get all the available permissions
- $permissions = config('permissions');
+ $permissions = json_encode(config('permissions'));
$groupPermissions = Helper::selectedPermissionsArray($permissions, $permissions);
$group->name = $request->input('name');
diff --git a/app/Http/Controllers/Api/ImportController.php b/app/Http/Controllers/Api/ImportController.php
index 2a4d91c3d98e..79bffd1206b1 100644
--- a/app/Http/Controllers/Api/ImportController.php
+++ b/app/Http/Controllers/Api/ImportController.php
@@ -66,25 +66,41 @@ public function store() : JsonResponse
if (! ini_get('auto_detect_line_endings')) {
ini_set('auto_detect_line_endings', '1');
}
- $file_contents = $file->getContent(); //TODO - this *does* load the whole file in RAM, but we need that to be able to 'iconv' it?
- $encoding = $detector->getEncoding($file_contents);
- $reader = null;
- if (strcasecmp($encoding, 'UTF-8') != 0) {
- $transliterated = iconv($encoding, 'UTF-8', $file_contents);
- if ($transliterated !== false) {
- $tmpname = tempnam(sys_get_temp_dir(), '');
- $tmpresults = file_put_contents($tmpname, $transliterated);
- if ($tmpresults !== false) {
+ if (function_exists('iconv')) {
+ $file_contents = $file->getContent(); //TODO - this *does* load the whole file in RAM, but we need that to be able to 'iconv' it?
+ $encoding = $detector->getEncoding($file_contents);
+ \Log::warning("Discovered encoding: $encoding in uploaded CSV");
+ $reader = null;
+ if (strcasecmp($encoding, 'UTF-8') != 0) {
+ $transliterated = false;
+ try {
+ $transliterated = iconv(strtoupper($encoding), 'UTF-8', $file_contents);
+ } catch (\Exception $e) {
+ $transliterated = false; //blank out the partially-decoded string
+ return response()->json(
+ Helper::formatStandardApiResponse(
+ 'error',
+ null,
+ trans('admin/hardware/message.import.transliterate_failure', ["encoding" => $encoding])
+ ),
+ 422
+ );
+ }
+ if ($transliterated !== false) {
+ $tmpname = tempnam(sys_get_temp_dir(), '');
+ $tmpresults = file_put_contents($tmpname, $transliterated);
$transliterated = null; //save on memory?
- $newfile = new UploadedFile($tmpname, $file->getClientOriginalName(), null, null, true); //WARNING: this is enabling 'test mode' - which is gross, but otherwise the file won't be treated as 'uploaded'
- if ($newfile->isValid()) {
- $file = $newfile;
+ if ($tmpresults !== false) {
+ $newfile = new UploadedFile($tmpname, $file->getClientOriginalName(), null, null, true); //WARNING: this is enabling 'test mode' - which is gross, but otherwise the file won't be treated as 'uploaded'
+ if ($newfile->isValid()) {
+ $file = $newfile;
+ }
}
}
}
+ $file_contents = null; //try to save on memory, I guess?
}
$reader = Reader::createFromFileObject($file->openFile('r')); //file pointer leak?
- $file_contents = null; //try to save on memory, I guess?
try {
$import->header_row = $reader->fetchOne(0);
@@ -179,7 +195,7 @@ public function process(ItemImportRequest $request, $import_id) : JsonResponse
// Run a backup immediately before processing
if ($request->get('run-backup')) {
Log::debug('Backup manually requested via importer');
- Artisan::call('snipeit:backup', ['--filename' => 'pre-import-backup-'.date('Y-m-d-H:i:s')]);
+ Artisan::call('snipeit:backup', ['--filename' => 'pre-import-backup-'.date('Y-m-d-H-i-s')]);
} else {
Log::debug('NO BACKUP requested via importer');
}
@@ -218,6 +234,15 @@ public function process(ItemImportRequest $request, $import_id) : JsonResponse
case 'location':
$redirectTo = 'locations.index';
break;
+ case 'supplier':
+ $redirectTo = 'suppliers.index';
+ break;
+ case 'manufacturer':
+ $redirectTo = 'manufacturers.index';
+ break;
+ case 'category':
+ $redirectTo = 'categories.index';
+ break;
}
if ($errors) { //Failure
diff --git a/app/Http/Controllers/Api/LicenseSeatsController.php b/app/Http/Controllers/Api/LicenseSeatsController.php
index 2ed709732236..247f71ff2624 100644
--- a/app/Http/Controllers/Api/LicenseSeatsController.php
+++ b/app/Http/Controllers/Api/LicenseSeatsController.php
@@ -29,12 +29,21 @@ public function index(Request $request, $licenseId) : JsonResponse | array
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department')
->where('license_seats.license_id', $licenseId);
+ if ($request->input('status') == 'available') {
+ $seats->whereNull('license_seats.assigned_to');
+ }
+
+ if ($request->input('status') == 'assigned') {
+ $seats->ByAssigned();
+ }
+
+
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
if ($request->input('sort') == 'department') {
$seats->OrderDepartments($order);
} else {
- $seats->orderBy('id', $order);
+ $seats->orderBy('updated_at', $order);
}
$total = $seats->count();
@@ -119,7 +128,9 @@ public function update(Request $request, $licenseId, $seatId) : JsonResponse | a
// nothing to update
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
}
-
+ if( $touched && $licenseSeat->unreassignable_seat) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.unavailable')));
+ }
// the logging functions expect only one "target". if both asset and user are present in the request,
// we simply let assets take precedence over users...
if ($licenseSeat->isDirty('assigned_to')) {
@@ -136,13 +147,17 @@ public function update(Request $request, $licenseId, $seatId) : JsonResponse | a
if ($licenseSeat->save()) {
if ($is_checkin) {
- $licenseSeat->logCheckin($target, $request->input('note'));
+ if(!$licenseSeat->license->reassignable){
+ $licenseSeat->unreassignable_seat = true;
+ $licenseSeat->save();
+ }
+ $licenseSeat->logCheckin($target, $licenseSeat->notes);
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
}
// 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('note'), $target);
+ $licenseSeat->logCheckout($request->input('notes'), $target);
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
}
diff --git a/app/Http/Controllers/Api/LocationsController.php b/app/Http/Controllers/Api/LocationsController.php
index 6a312e5bcf85..b5c911a6cad6 100644
--- a/app/Http/Controllers/Api/LocationsController.php
+++ b/app/Http/Controllers/Api/LocationsController.php
@@ -12,7 +12,9 @@
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\Asset;
+use App\Models\Company;
use App\Models\Location;
+use App\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
@@ -46,6 +48,7 @@ public function index(Request $request) : JsonResponse | array
'id',
'image',
'ldap_ou',
+ 'company_id',
'manager_id',
'name',
'rtd_assets_count',
@@ -74,15 +77,23 @@ public function index(Request $request) : JsonResponse | array
'locations.image',
'locations.ldap_ou',
'locations.currency',
+ 'locations.company_id',
'locations.notes',
])
+ ->withCount('assignedAssets as assigned_assets_count')
->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('assignedAccessories as assigned_accessories_count')
->withCount('accessories as accessories_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
- ->withCount('users as users_count');
+ ->withCount('users as users_count')
+ ->with('adminuser');
+
+ // Only scope locations if the setting is enabled
+ if (Setting::getSettings()->scope_locations_fmcs) {
+ $locations = Company::scopeCompanyables($locations);
+ }
if ($request->filled('search')) {
$locations = $locations->TextSearch($request->input('search'));
@@ -116,6 +127,10 @@ public function index(Request $request) : JsonResponse | array
$locations->where('locations.manager_id', '=', $request->input('manager_id'));
}
+ if ($request->filled('company_id')) {
+ $locations->where('locations.company_id', '=', $request->input('company_id'));
+ }
+
// 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');
@@ -132,6 +147,9 @@ public function index(Request $request) : JsonResponse | array
case 'manager':
$locations->OrderManager($order);
break;
+ case 'company':
+ $locations->OrderCompany($order);
+ break;
default:
$locations->orderBy($sort, $order);
break;
@@ -159,6 +177,15 @@ public function store(ImageUploadRequest $request) : JsonResponse
$location->fill($request->all());
$location = $request->handleImages($location);
+ // Only scope location if the setting is enabled
+ if (Setting::getSettings()->scope_locations_fmcs) {
+ $location->company_id = Company::getIdForCurrentUser($request->get('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'));
+ }
+ }
+
if ($location->save()) {
return response()->json(Helper::formatStandardApiResponse('success', (new LocationsTransformer)->transformLocation($location), trans('admin/locations/message.create.success')));
}
@@ -176,7 +203,7 @@ public function store(ImageUploadRequest $request) : JsonResponse
public function show($id) : JsonResponse | array
{
$this->authorize('view', Location::class);
- $location = Location::with('parent', 'manager', 'children')
+ $location = Location::with('parent', 'manager', 'children', 'company')
->select([
'locations.id',
'locations.name',
@@ -192,6 +219,7 @@ public function show($id) : JsonResponse | array
'locations.updated_at',
'locations.image',
'locations.currency',
+ 'locations.company_id',
'locations.notes',
])
->withCount('assignedAssets as assigned_assets_count')
@@ -220,6 +248,19 @@ public function update(ImageUploadRequest $request, $id) : JsonResponse
$location->fill($request->all());
$location = $request->handleImages($location);
+ if ($request->filled('company_id')) {
+ // Only scope location if the setting is enabled
+ if (Setting::getSettings()->scope_locations_fmcs) {
+ $location->company_id = Company::getIdForCurrentUser($request->get('company_id'));
+ // check if there are related objects with different company
+ if (Helper::test_locations_fmcs(false, $id, $location->company_id)) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, 'error scoped locations'));
+ }
+ } else {
+ $location->company_id = $request->get('company_id');
+ }
+ }
+
if ($location->isValid()) {
$location->save();
@@ -340,6 +381,11 @@ public function selectlist(Request $request) : array
'locations.image',
]);
+ // Only scope locations if the setting is enabled
+ if (Setting::getSettings()->scope_locations_fmcs) {
+ $locations = Company::scopeCompanyables($locations);
+ }
+
$page = 1;
if ($request->filled('page')) {
$page = $request->input('page');
diff --git a/app/Http/Controllers/Api/MaintenancesController.php b/app/Http/Controllers/Api/MaintenancesController.php
new file mode 100644
index 000000000000..86f561c86c34
--- /dev/null
+++ b/app/Http/Controllers/Api/MaintenancesController.php
@@ -0,0 +1,227 @@
+
+ * @version v1.0
+ * @since [v1.8]
+ */
+ public function index(Request $request) : JsonResponse | array
+ {
+ $this->authorize('view', Asset::class);
+
+ $maintenances = Maintenance::select('maintenances.*')
+ ->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.assetstatus', 'adminuser');
+
+ if ($request->filled('search')) {
+ $maintenances = $maintenances->TextSearch($request->input('search'));
+ }
+
+ if ($request->filled('asset_id')) {
+ $maintenances->where('asset_id', '=', $request->input('asset_id'));
+ }
+
+ if ($request->filled('supplier_id')) {
+ $maintenances->where('maintenances.supplier_id', '=', $request->input('supplier_id'));
+ }
+
+ if ($request->filled('created_by')) {
+ $maintenances->where('maintenances.created_by', '=', $request->input('created_by'));
+ }
+
+ if ($request->filled('asset_maintenance_type')) {
+ $maintenances->where('asset_maintenance_type', '=', $request->input('asset_maintenance_type'));
+ }
+
+
+ // 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'));
+ $limit = app('api_limit_value');
+
+ $allowed_columns = [
+ 'id',
+ 'name',
+ 'asset_maintenance_time',
+ 'asset_maintenance_type',
+ 'cost',
+ 'start_date',
+ 'completion_date',
+ 'notes',
+ 'asset_tag',
+ 'asset_name',
+ 'serial',
+ 'created_by',
+ 'supplier',
+ 'location',
+ 'is_warranty',
+ 'status_label',
+ ];
+
+ $order = $request->input('order') === 'asc' ? 'asc' : 'desc';
+ $sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
+
+ switch ($sort) {
+ case 'created_by':
+ $maintenances = $maintenances->OrderByCreatedBy($order);
+ break;
+ case 'supplier':
+ $maintenances = $maintenances->OrderBySupplier($order);
+ break;
+ case 'asset_tag':
+ $maintenances = $maintenances->OrderByTag($order);
+ break;
+ case 'asset_name':
+ $maintenances = $maintenances->OrderByAssetName($order);
+ break;
+ case 'serial':
+ $maintenances = $maintenances->OrderByAssetSerial($order);
+ break;
+ case 'location':
+ $maintenances = $maintenances->OrderLocationName($order);
+ break;
+ case 'status_label':
+ $maintenances = $maintenances->OrderStatusName($order);
+ break;
+ default:
+ $maintenances = $maintenances->orderBy($sort, $order);
+ break;
+ }
+
+ $total = $maintenances->count();
+ $maintenances = $maintenances->skip($offset)->take($limit)->get();
+ return (new MaintenancesTransformer())->transformMaintenances($maintenances, $total);
+
+
+ }
+
+
+ /**
+ * Validates and stores the new asset maintenance
+ *
+ * @see MaintenancesController::getCreate() method for the form
+ * @author Vincent Sposato
+ * @version v1.0
+ * @since [v1.8]
+ */
+ public function store(ImageUploadRequest $request) : JsonResponse | array
+ {
+ $this->authorize('update', Asset::class);
+
+ // create a new model instance
+ $maintenance = new Maintenance();
+ $maintenance->fill($request->all());
+ $maintenance->created_by = auth()->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')));
+
+ }
+
+ return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
+
+ }
+
+ /**
+ * Validates and stores an update to an asset maintenance
+ *
+ * @author A. Gianotto
+ * @param int $id
+ * @param int $request
+ * @version v1.0
+ * @since [v4.0]
+ */
+ public function update(Request $request, $id) : JsonResponse | array
+ {
+ $this->authorize('update', Asset::class);
+
+ if ($maintenance = Maintenance::with('asset')->find($id)) {
+
+ // Can this user manage this 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')])));
+ }
+
+ // The asset this miantenance is attached to is not valid or has been deleted
+ if (!$maintenance->asset) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('general.asset'), 'id' => $id])));
+ }
+
+ $maintenance->fill($request->all());
+
+ if ($maintenance->save()) {
+ return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.edit.success')));
+ }
+
+ return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
+ }
+
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('admin/maintenances/general.maintenance'), 'id' => $id])));
+
+ }
+
+ /**
+ * Delete an asset maintenance
+ *
+ * @author A. Gianotto
+ * @param int $maintenanceId
+ * @version v1.0
+ * @since [v4.0]
+ */
+ public function destroy($maintenanceId) : JsonResponse | array
+ {
+ $this->authorize('update', Asset::class);
+ // Check if the asset maintenance exists
+
+ $maintenance = Maintenance::findOrFail($maintenanceId);
+
+ $maintenance->delete();
+
+ return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.delete.success')));
+
+
+ }
+
+ /**
+ * View an asset maintenance
+ *
+ * @author A. Gianotto
+ * @param int $maintenanceId
+ * @version v1.0
+ * @since [v4.0]
+ */
+ public function show($maintenanceId) : JsonResponse | array
+ {
+ $this->authorize('view', Asset::class);
+ $maintenance = Maintenance::findOrFail($maintenanceId);
+ if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot view a maintenance for that asset'));
+ }
+
+ return (new MaintenancesTransformer())->transformMaintenance($maintenance);
+
+ }
+}
diff --git a/app/Http/Controllers/Api/NotesController.php b/app/Http/Controllers/Api/NotesController.php
index 97b7ca41340f..c1a16fd4d658 100644
--- a/app/Http/Controllers/Api/NotesController.php
+++ b/app/Http/Controllers/Api/NotesController.php
@@ -2,42 +2,94 @@
namespace App\Http\Controllers\Api;
-use App\Events\NoteAdded;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
+use App\Models\Actionlog;
use App\Models\Asset;
+use Illuminate\Database\Eloquent\ModelNotFoundException;
+use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
-use Illuminate\Validation\Rule;
+use Illuminate\Support\Facades\Log;
+/**
+ * This class controls all API actions related to notes for
+ * the Snipe-IT Asset Management application.
+ */
class NotesController extends Controller
{
- public function store(Request $request)
+ /**
+ * Retrieve a list of manual notes (action logs) for a given asset.
+ *
+ * Checks authorization to view assets, attempts to find the asset by ID,
+ * and fetches related action log entries of type 'note added', including
+ * user information for each note. Returns a JSON response with the notes or errors.
+ *
+ * @param \Illuminate\Http\Request $request The incoming HTTP request.
+ * @param Asset $asset The ID of the asset whose notes to retrieve.
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function index(Asset $asset): JsonResponse
{
- $validated = $request->validate([
- 'note' => 'required|string|max:500',
- 'type' => [
- 'required',
- Rule::in(['asset']),
- ],
- ]);
-
- // This can be made dynamic by using $request->input('type') to determine which model type to add the note to.
- // For now, we are only placing this on Assets
- $item = Asset::findOrFail($request->input("id"));
- $this->authorize('update', $item);
-
- event(new NoteAdded($item, Auth::user(), $validated['note']));
-
- return response()->json(Helper::formatStandardApiResponse('success'));
- }
+ $this->authorize('view', $asset);
- public function update(Request $request)
- {
+ // Get the manual notes for the asset
+ $notes = ActionLog::with('user:id,username')
+ ->where('item_type', Asset::class)
+ ->where('item_id', $asset->id)
+ ->where('action_type', 'note added')
+ ->orderBy('created_at', 'desc')
+ ->get(['id', 'created_at', 'note', 'created_by', 'item_id', 'item_type', 'action_type', 'target_id', 'target_type']);
+
+ $notesArray = $notes->map(function ($note) {
+ return [
+ 'id' => $note->id,
+ 'created_at' => $note->created_at,
+ 'note' => $note->note,
+ 'created_by' => $note->created_by,
+ 'username' => $note->user?->username, // adding the username
+ 'item_id' => $note->item_id,
+ 'item_type' => $note->item_type,
+ 'action_type' => $note->action_type,
+ ];
+ });
+ // Return a success response
+ return response()->json(Helper::formatStandardApiResponse('success', ['notes' => $notesArray, 'asset_id' => $asset->id]));
}
- public function destroy(Request $request)
+
+ /**
+ * Store a manual note on a specified asset and log the action.
+ *
+ * Checks authorization for updating assets, validates the presence of the 'note',
+ * attempts to find the asset by ID, and creates a new ActionLog entry if successful.
+ * Returns JSON responses indicating success or failure with appropriate HTTP status codes.
+ *
+ * @param \Illuminate\Http\Request $request The incoming HTTP request containing the 'note'.
+ * @param Asset $asset The ID of the asset to attach the note to.
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function store(Request $request, Asset $asset): JsonResponse
{
+ $this->authorize('update', $asset);
+
+ if ($request->input('note', '') == '') {
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('validation.required', ['attribute' => 'note'])), 422);
+ }
+
+ // Create the note
+ $logaction = new ActionLog();
+ $logaction->item_type = get_class($asset);
+ $logaction->created_by = Auth::id();
+ $logaction->item_id = $asset->id;
+ $logaction->note = $request->input('note', '');
+
+ if ($logaction->logaction('note added')) {
+ // Return a success response
+ return response()->json(Helper::formatStandardApiResponse('success', ['note' => $logaction->note, 'item_id' => $asset->id], trans('general.note_added')));
+ }
+ // Return an error response if something went wrong
+ return response()->json(Helper::formatStandardApiResponse('error', null, 'Something went wrong'), 500);
}
}
diff --git a/app/Http/Controllers/Api/ProfileController.php b/app/Http/Controllers/Api/ProfileController.php
index f7f91e094e68..69db8aae0463 100644
--- a/app/Http/Controllers/Api/ProfileController.php
+++ b/app/Http/Controllers/Api/ProfileController.php
@@ -4,15 +4,19 @@
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
+use App\Http\Transformers\ProfileTransformer;
use App\Models\CheckoutRequest;
+use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Storage;
use Laravel\Passport\TokenRepository;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Support\Facades\Gate;
use App\Models\CustomField;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\JsonResponse;
+use Symfony\Component\HttpFoundation\BinaryFileResponse;
class ProfileController extends Controller
{
@@ -167,6 +171,22 @@ public function showApiTokens() : JsonResponse
}
+ /**
+ * Display the EULAs accepted by the user.
+ *
+ * @param \App\Http\Transformers\ActionlogsTransformer $transformer
+ * @return \Illuminate\Http\JsonResponse
+ *@since [v8.1.16]
+ * @author [Godfrey Martinez] []
+ */
+ public function eulas(ProfileTransformer $transformer)
+ {
+ // Only return this user's EULAs
+ $eulas = auth()->user()->eulas;
+ return response()->json(
+ $transformer->transformFiles($eulas, $eulas->count())
+ );
+ }
}
diff --git a/app/Http/Controllers/Api/ReportsController.php b/app/Http/Controllers/Api/ReportsController.php
index 494c75104f6d..c03dddf6b777 100644
--- a/app/Http/Controllers/Api/ReportsController.php
+++ b/app/Http/Controllers/Api/ReportsController.php
@@ -5,6 +5,8 @@
use App\Http\Controllers\Controller;
use App\Http\Transformers\ActionlogsTransformer;
use App\Models\Actionlog;
+use App\Models\Company;
+use App\Models\Setting;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
@@ -18,10 +20,11 @@ class ReportsController extends Controller
*/
public function index(Request $request) : JsonResponse | array
{
- $this->authorize('reports.view');
+ $this->authorize('activity.view');
$actionlogs = Actionlog::with('item', 'user', 'adminuser', 'target', 'location');
+
if ($request->filled('search')) {
$actionlogs = $actionlogs->TextSearch(e($request->input('search')));
}
diff --git a/app/Http/Controllers/Api/SettingsController.php b/app/Http/Controllers/Api/SettingsController.php
index 7eb28a4815a8..f24dd25b1791 100644
--- a/app/Http/Controllers/Api/SettingsController.php
+++ b/app/Http/Controllers/Api/SettingsController.php
@@ -3,7 +3,6 @@
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
-use App\Helpers\StorageHelper;
use App\Http\Transformers\DatatablesTransformer;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
@@ -51,10 +50,22 @@ public function ldaptest() : JsonResponse
})->slice(0, 10)->map(function ($item) use ($settings) {
return (object) [
'username' => $item[$settings['ldap_username_field']][0] ?? null,
+ 'display_name' => $item[$settings['ldap_display_name']][0] ?? null,
'employee_number' => $item[$settings['ldap_emp_num']][0] ?? null,
'lastname' => $item[$settings['ldap_lname_field']][0] ?? null,
'firstname' => $item[$settings['ldap_fname_field']][0] ?? null,
'email' => $item[$settings['ldap_email']][0] ?? null,
+ 'phone' => $item[$settings['ldap_phone_field']][0] ?? null,
+ 'mobile' => $item[$settings['ldap_mobile']][0] ?? null,
+ 'jobtitle' => $item[$settings['ldap_jobtitle']][0] ?? null,
+ 'department' => $item[$settings['ldap_department']][0] ?? null,
+ 'manager' => $item[$settings['ldap_manager']][0] ?? null,
+ 'address' => $item[$settings['ldap_address']][0] ?? null,
+ 'city' => $item[$settings['ldap_city']][0] ?? null,
+ 'state' => $item[$settings['ldap_state']][0] ?? null,
+ 'zip' => $item[$settings['ldap_zip']][0] ?? null,
+ 'country' => $item[$settings['ldap_country']][0] ?? null,
+ 'location' => $item[$settings['ldap_location']][0] ?? null,
];
});
if ($users->count() > 0) {
@@ -78,7 +89,7 @@ public function ldaptest() : JsonResponse
}
} catch (\Exception $e) {
Log::debug('Connection failed but we cannot debug it any further on our end.');
- return response()->json(['message' => $e->getMessage()], 500);
+ return response()->json(['message' => $e->getMessage()], 400);
}
@@ -150,8 +161,11 @@ public function ajaxTestEmail() : JsonResponse
if (!config('app.lock_passwords')) {
try {
Notification::send(Setting::first(), new MailTest());
+ Log::debug('Attempting to sending to '.config('mail.reply_to.address'));
return response()->json(['message' => 'Mail sent to '.config('mail.reply_to.address')], 200);
} catch (\Exception $e) {
+ Log::error('Mail sent error using '.config('mail.reply_to.address') .': '. $e->getMessage());
+ Log::debug($e);
return response()->json(['message' => $e->getMessage()], 500);
}
}
@@ -315,4 +329,4 @@ public function downloadLatestBackup() : JsonResponse | BinaryFileResponse
}
-}
\ No newline at end of file
+}
diff --git a/app/Http/Controllers/Api/StatuslabelsController.php b/app/Http/Controllers/Api/StatuslabelsController.php
index 7e4851ff5ae1..b3503ff15230 100644
--- a/app/Http/Controllers/Api/StatuslabelsController.php
+++ b/app/Http/Controllers/Api/StatuslabelsController.php
@@ -290,10 +290,12 @@ public function assets(Request $request, $id) : array
/**
* Returns a boolean response based on whether the status label
- * is one that is deployable.
+ * is one that is deployable or pending.
*
* This is used by the hardware create/edit view to determine whether
- * we should provide a dropdown of users for them to check the asset out to.
+ * we should provide a dropdown of users for them to check the asset out to,
+ * and whether we show a warning that the asset will be checked in if it's already
+ * assigned but the status is changed to one that isn't pending or deployable
*
* @author [A. Gianotto] []
* @since [v4.0]
@@ -301,7 +303,7 @@ public function assets(Request $request, $id) : array
public function checkIfDeployable($id) : string
{
$statuslabel = Statuslabel::findOrFail($id);
- if ($statuslabel->getStatuslabelType() == 'deployable') {
+ if (($statuslabel->getStatuslabelType() == 'pending') || ($statuslabel->getStatuslabelType() == 'deployable')) {
return '1';
}
diff --git a/app/Http/Controllers/Api/SuppliersController.php b/app/Http/Controllers/Api/SuppliersController.php
index f752f222411a..6784ee82c16e 100644
--- a/app/Http/Controllers/Api/SuppliersController.php
+++ b/app/Http/Controllers/Api/SuppliersController.php
@@ -24,10 +24,15 @@ class SuppliersController extends Controller
public function index(Request $request): array
{
$this->authorize('view', Supplier::class);
- $allowed_columns = ['
- id',
+ $allowed_columns = [
+ 'id',
'name',
'address',
+ 'address2',
+ 'city',
+ 'state',
+ 'country',
+ 'zip',
'phone',
'contact',
'fax',
@@ -39,21 +44,24 @@ public function index(Request $request): array
'components_count',
'consumables_count',
'url',
+ 'notes',
];
$suppliers = Supplier::select(
- ['id', 'name', 'address', 'address2', 'city', 'state', 'country', 'fax', 'phone', 'email', 'contact', 'created_at', 'updated_at', 'deleted_at', 'image', 'notes', 'url'])
+ ['id', 'name', 'address', 'address2', 'city', 'state', 'country', 'fax', 'phone', 'email', 'contact', 'created_at', 'created_by', 'updated_at', 'deleted_at', 'image', 'notes', 'url', 'zip'])
->withCount('assets as assets_count')
->withCount('licenses as licenses_count')
->withCount('accessories as accessories_count')
->withCount('components as components_count')
- ->withCount('consumables as consumables_count');
+ ->withCount('consumables as consumables_count')
+ ->with('adminuser');
if ($request->filled('search')) {
- $suppliers = $suppliers->TextSearch($request->input('search'));
+ $suppliers->TextSearch($request->input('search'));
}
+
if ($request->filled('name')) {
$suppliers->where('name', '=', $request->input('name'));
}
@@ -100,7 +108,15 @@ public function index(Request $request): array
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
- $suppliers->orderBy($sort, $order);
+
+ switch ($request->input('sort')) {
+ case 'created_by':
+ $suppliers->OrderByCreatedByName($order);
+ break;
+ default:
+ $suppliers->orderBy($sort, $order);
+ break;
+ }
$total = $suppliers->count();
$suppliers = $suppliers->skip($offset)->take($limit)->get();
@@ -178,7 +194,7 @@ public function update(ImageUploadRequest $request, $id) : JsonResponse
public function destroy($id) : JsonResponse
{
$this->authorize('delete', Supplier::class);
- $supplier = Supplier::with('asset_maintenances', 'assets', 'licenses')->withCount('asset_maintenances as asset_maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->findOrFail($id);
+ $supplier = Supplier::with('maintenances', 'assets', 'licenses')->withCount('maintenances as maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->findOrFail($id);
$this->authorize('delete', $supplier);
@@ -186,8 +202,8 @@ public function destroy($id) : JsonResponse
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count])));
}
- if ($supplier->asset_maintenances_count > 0) {
- return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_maintenances', ['asset_maintenances_count' => $supplier->asset_maintenances_count])));
+ if ($supplier->maintenances_count > 0) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_maintenances', ['maintenances_count' => $supplier->maintenances_count])));
}
if ($supplier->licenses_count > 0) {
diff --git a/app/Http/Controllers/Api/UploadedFilesController.php b/app/Http/Controllers/Api/UploadedFilesController.php
new file mode 100644
index 000000000000..a4a2de05209a
--- /dev/null
+++ b/app/Http/Controllers/Api/UploadedFilesController.php
@@ -0,0 +1,216 @@
+]
+ */
+ public function index(Request $request, $object_type, $id) : JsonResponse | array
+ {
+
+ // Check the permissions to make sure the user can view the object
+ $object = self::$map_object_type[$object_type]::withTrashed()->find($id);
+ $this->authorize('view', $object);
+
+ if (!$object) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
+ }
+
+ // Columns allowed for sorting
+ $allowed_columns =
+ [
+ 'id',
+ 'filename',
+ 'action_type',
+ 'action_date',
+ 'note',
+ 'created_at',
+ ];
+
+
+ $uploads = self::$map_object_type[$object_type]::withTrashed()->find($id)->uploads()
+ ->with('adminuser');
+
+ $offset = ($request->input('offset') > $uploads->count()) ? $uploads->count() : abs($request->input('offset'));
+ $limit = app('api_limit_value');
+ $order = $request->input('order') === 'asc' ? 'asc' : 'desc';
+ $sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
+
+ // Text search on action_logs fields
+ // We could use the normal Actionlogs text scope, but it's a very heavy query since it's searching across all relations
+ // and we generally won't need that here
+ if ($request->filled('search')) {
+
+ $uploads->where(
+ function ($query) use ($request) {
+ $query->where('filename', 'LIKE', '%' . $request->input('search') . '%')
+ ->orWhere('note', 'LIKE', '%' . $request->input('search') . '%');
+ }
+ );
+ }
+
+ $total = $uploads->count();
+ $uploads = $uploads->skip($offset)->take($limit)->orderBy($sort, $order)->get();
+
+ return (new UploadedFilesTransformer())->transformFiles($uploads, $total);
+ }
+
+
+ /**
+ * Accepts a POST to upload a file to the server.
+ *
+ * @param \App\Http\Requests\UploadFileRequest $request
+ * @param string $object_type the type of object to upload the file to
+ * @param int $id the ID of the object to store so we can check permisisons
+ * @since [v8.1.17]
+ * @author [A. Gianotto ]
+ */
+ public function store(UploadFileRequest $request, $object_type, $id) : JsonResponse
+ {
+
+ // Check the permissions to make sure the user can view the object
+ $object = self::$map_object_type[$object_type]::withTrashed()->find($id);
+ $this->authorize('view', $object);
+
+ if (!$object) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
+ }
+
+ // If the file storage directory doesn't exist, create it
+ if (! Storage::exists(self::$map_storage_path[$object_type])) {
+ Storage::makeDirectory(self::$map_storage_path[$object_type], 775);
+ }
+
+
+ if ($request->hasFile('file')) {
+ // Loop over the attached files and add them to the object
+ foreach ($request->file('file') as $file) {
+ $file_name = $request->handleFile(self::$map_storage_path[$object_type], self::$map_file_prefix[$object_type].'-'.$object->id, $file);
+ $files[] = $file_name;
+ $object->logUpload($file_name, $request->get('notes'));
+ }
+
+ $files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')
+ ->where('item_type', '=', self::$map_object_type[$object_type])
+ ->where('item_id', '=', $id)->whereIn('filename', $files)
+ ->get();
+
+ return response()->json(Helper::formatStandardApiResponse('success', (new UploadedFilesTransformer())->transformFiles($files, count($files)), trans_choice('general.file_upload_status.upload.success', count($files))));
+ }
+
+ // No files were submitted
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.nofiles')));
+ }
+
+
+
+ /**
+ * Check for permissions and display the file.
+ *
+ * @param \App\Http\Requests\UploadFileRequest $request
+ * @param string $object_type the type of object to upload the file to
+ * @param int $id the ID of the object to delete from so we can check permisisons
+ * @param $file_id the ID of the file to delete from the action_logs table
+ * @since [v8.1.17]
+ * @author [A. Gianotto ]
+ */
+ public function show($object_type, $id, $file_id) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
+ {
+ // Check the permissions to make sure the user can view the object
+ $object = self::$map_object_type[$object_type]::withTrashed()->find($id);
+ $this->authorize('view', $object);
+
+ if (!$object) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
+ }
+
+
+ // Check that the file being requested exists for the object
+ if (! $log = Actionlog::whereNotNull('filename')->where('item_type', self::$map_object_type[$object_type])->where('item_id', $object->id)->find($file_id)
+ ) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_id')), 200);
+ }
+
+
+ if (! Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.file_not_found'), 200));
+ }
+
+ if (request('inline') == 'true') {
+ $headers = [
+ 'Content-Disposition' => 'inline',
+ ];
+ return Storage::download(self::$map_storage_path[$object_type].'/'.$log->filename, $log->filename, $headers);
+ }
+
+ return StorageHelper::downloader(self::$map_storage_path[$object_type].'/'.$log->filename);
+
+ }
+
+ /**
+ * Delete the associated file
+ *
+ * @param \App\Http\Requests\UploadFileRequest $request
+ * @param string $object_type the type of object to upload the file to
+ * @param int $id the ID of the object to delete from so we can check permisisons
+ * @param $file_id the ID of the file to delete from the action_logs table
+ * @since [v8.1.17]
+ * @author [A. Gianotto ]
+ */
+ public function destroy($object_type, $id, $file_id) : JsonResponse
+ {
+
+ // Check the permissions to make sure the user can view the object
+ $object = self::$map_object_type[$object_type]::withTrashed()->find($id);
+ $this->authorize('update', self::$map_object_type[$object_type]);
+
+ if (!$object) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
+ }
+
+
+ // Check for the file
+ $log = Actionlog::find($file_id)->where('item_type', self::$map_object_type[$object_type])
+ ->where('item_id', $object->id)->first();
+
+ if ($log) {
+ // Check the file actually exists, and delete it
+ if (Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
+ Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
+ }
+ // Delete the record of the file
+ if ($log->logUploadDelete($object, $log->filename)) {
+ return response()->json(Helper::formatStandardApiResponse('success', null, trans_choice('general.file_upload_status.delete.success', 1)), 200);
+ }
+
+
+ }
+
+ // The file doesn't seem to really exist, so report an error
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('general.file_upload_status.delete.error', 1)), 500);
+
+ }
+}
\ No newline at end of file
diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php
index 1d34f90a39e5..aab7fd9d0303 100644
--- a/app/Http/Controllers/Api/UsersController.php
+++ b/app/Http/Controllers/Api/UsersController.php
@@ -6,6 +6,7 @@
use App\Http\Controllers\Controller;
use App\Http\Requests\SaveUserRequest;
use App\Http\Transformers\AccessoriesTransformer;
+use App\Http\Transformers\ActionlogsTransformer;
use App\Http\Transformers\AssetsTransformer;
use App\Http\Transformers\ConsumablesTransformer;
use App\Http\Transformers\LicensesTransformer;
@@ -16,12 +17,16 @@
use App\Models\Accessory;
use App\Models\Company;
use App\Models\Consumable;
+use App\Models\CustomField;
use App\Models\License;
use App\Models\User;
use App\Notifications\CurrentInventory;
+use App\Notifications\WelcomeNotification;
+use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
@@ -42,7 +47,7 @@ public function index(Request $request) : array
{
$this->authorize('view', User::class);
- $users = User::select([
+ $allowed_columns = [
'users.activated',
'users.address',
'users.avatar',
@@ -61,12 +66,14 @@ public function index(Request $request) : array
'users.jobtitle',
'users.last_login',
'users.last_name',
+ 'users.display_name',
'users.locale',
'users.location_id',
'users.manager_id',
'users.notes',
'users.permissions',
'users.phone',
+ 'users.mobile',
'users.state',
'users.two_factor_enrolled',
'users.two_factor_optin',
@@ -80,7 +87,12 @@ public function index(Request $request) : array
'users.autoassign_licenses',
'users.website',
- ])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations')
+ ];
+
+ foreach (CustomField::where('type', User::class)->get() as $field) {
+ $allowed_columns[] = $field->db_column_name();
+ }
+ $users = User::select($allowed_columns)->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations')
->withCount([
'assets as assets_count' => function(Builder $query) {
$query->withoutTrashed();
@@ -101,10 +113,26 @@ public function index(Request $request) : array
$users = $users->where('users.activated', '=', $request->input('activated'));
}
+ if ($request->input('admins') == 'true') {
+ $users = $users->OnlyAdminsAndSuperAdmins();
+ }
+
+ if ($request->input('superadmins') == 'true') {
+ $users = $users->OnlySuperAdmins();
+ }
+
if ($request->filled('company_id')) {
$users = $users->where('users.company_id', '=', $request->input('company_id'));
}
+ if ($request->filled('phone')) {
+ $users = $users->where('users.phone', '=', $request->input('phone'));
+ }
+
+ if ($request->filled('mobile')) {
+ $users = $users->where('users.mobile', '=', $request->input('mobile'));
+ }
+
if ($request->filled('location_id')) {
$users = $users->where('users.location_id', '=', $request->input('location_id'));
}
@@ -129,6 +157,10 @@ public function index(Request $request) : array
$users = $users->where('users.last_name', '=', $request->input('last_name'));
}
+ if ($request->filled('display_name')) {
+ $users = $users->where('users.display_name', '=', $request->input('display_name'));
+ }
+
if ($request->filled('employee_num')) {
$users = $users->where('users.employee_num', '=', $request->input('employee_num'));
}
@@ -206,11 +238,11 @@ public function index(Request $request) : array
}
if ($request->filled('manages_users_count')) {
- $users->has('manages_users_count', '=', $request->input('manages_users_count'));
+ $users->has('managesUsers', '=', $request->input('manages_users_count'));
}
if ($request->filled('manages_locations_count')) {
- $users->has('manages_locations_count', '=', $request->input('manages_locations_count'));
+ $users->has('managedLocations', '=', $request->input('manages_locations_count'));
}
if ($request->filled('autoassign_licenses')) {
@@ -259,6 +291,7 @@ public function index(Request $request) : array
[
'last_name',
'first_name',
+ 'display_name',
'email',
'jobtitle',
'username',
@@ -277,6 +310,7 @@ public function index(Request $request) : array
'manages_users_count',
'manages_locations_count',
'phone',
+ 'mobile',
'address',
'city',
'state',
@@ -329,6 +363,7 @@ public function selectlist(Request $request) : array
'users.employee_num',
'users.first_name',
'users.last_name',
+ 'users.display_name',
'users.gravatar',
'users.avatar',
'users.email',
@@ -339,19 +374,17 @@ public function selectlist(Request $request) : array
$users = $users->where(function ($query) use ($request) {
$query->SimpleNameSearch($request->get('search'))
->orWhere('username', 'LIKE', '%'.$request->get('search').'%')
+ ->orWhere('display_name', 'LIKE', '%'.$request->get('search').'%')
+ ->orWhere('email', 'LIKE', '%'.$request->get('search').'%')
->orWhere('employee_num', 'LIKE', '%'.$request->get('search').'%');
});
}
- $users = $users->orderBy('last_name', 'asc')->orderBy('first_name', 'asc');
+ $users = $users->orderBy('display_name', 'asc')->orderBy('last_name', 'asc')->orderBy('first_name', 'asc');
$users = $users->paginate(50);
foreach ($users as $user) {
- $name_str = '';
- if ($user->last_name != '') {
- $name_str .= $user->last_name.', ';
- }
- $name_str .= $user->first_name;
+ $name_str = $user->display_name;
if ($user->username != '') {
$name_str .= ' ('.$user->username.')';
@@ -403,9 +436,22 @@ public function store(SaveUserRequest $request) : JsonResponse
$user->password = $user->noPassword();
}
- app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
-
+ app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
+
+ $user->customFill($request,Auth::user());
+
if ($user->save()) {
+
+ if (($user->activated == '1') && ($user->email != '') && ($request->input('send_welcome') == '1')) {
+
+ try {
+ $user->notify(new WelcomeNotification($user));
+ } catch (\Exception $e) {
+ Log::warning('Could not send welcome notification for user: ' . $e->getMessage());
+ }
+
+ }
+
if ($request->filled('groups')) {
$user->groups()->sync($request->input('groups'));
} else {
@@ -473,10 +519,37 @@ public function update(SaveUserRequest $request, User $user): JsonResponse
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
}
- if ($request->filled('password')) {
- $user->password = bcrypt($request->input('password'));
+ // check for permissions related fields and pull them out if the current user cannot edit them
+ if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
+
+ if ($request->filled('password')) {
+ $user->password = bcrypt($request->input('password'));
+ }
+
+ if ($request->filled('username')) {
+ $user->username = $request->input('username');
+ }
+
+ if ($request->filled('display_name')) {
+ $user->display_name = $request->input('display_name');
+ }
+
+ if ($request->filled('email')) {
+ $user->email = $request->input('email');
+ }
+
+ if ($request->filled('activated')) {
+ $user->activated = $request->input('activated');
+ }
+
}
+ app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
+
+ $user->customFill($request,Auth::user());
+
+ if ($user->save()) {
+
// We need to use has() instead of filled()
// here because we need to overwrite permissions
// if someone needs to null them out
@@ -496,7 +569,7 @@ public function update(SaveUserRequest $request, User $user): JsonResponse
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
}
- app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
+ app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
if ($user->save()) {
// Check if the request has groups passed and has a value, AND that the user us a superuser
@@ -675,7 +748,6 @@ public function licenses($id) : JsonResponse | array
$this->authorize('view', License::class);
if ($user = User::where('id', $id)->withTrashed()->first()) {
- $this->authorize('update', $user);
$licenses = $user->licenses()->get();
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
}
@@ -735,6 +807,25 @@ public function getCurrentUserInfo(Request $request) : array
return (new UsersTransformer)->transformUser($request->user());
}
+ /**
+ * Display the EULAs accepted by the user.
+ *
+ * @param \App\Models\User $user
+ * @param \App\Http\Transformers\ActionlogsTransformer $transformer
+ * @return \Illuminate\Http\JsonResponse
+ *@since [v8.1.16]
+ * @author [Godfrey Martinez] []
+ */
+ public function eulas(User $user, ActionlogsTransformer $transformer)
+ {
+ $this->authorize('view', User::class);
+
+ $eulas = $user->eulas;
+ return response()->json(
+ $transformer->transformActionlogs($eulas, $eulas->count())
+ );
+ }
+
/**
* Restore a soft-deleted user.
*
@@ -771,4 +862,37 @@ public function restore($userId) : JsonResponse
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')), 200);
}
+
+
+ /**
+ * Run the LDAP sync command to import users from LDAP via API.
+ *
+ * @author A. Gianotto
+ * @since 8.2.2
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function syncLdapUsers(Request $request)
+ {
+ $this->authorize('update', User::class);
+ // Call Artisan LDAP import command.
+
+ Artisan::call('snipeit:ldap-sync', ['--location_id' => $request->input('location_id'), '--json_summary' => true]);
+
+ // Collect and parse JSON summary.
+ $ldap_results_json = Artisan::output();
+ $ldap_results = json_decode($ldap_results_json, true);
+
+ if (!$ldap_results) {
+ return response()->json(Helper::formatStandardApiResponse('error', null,trans('general.no_results')), 200);
+ }
+
+ // Direct user to appropriate status page.
+ if ($ldap_results['error']) {
+ return response()->json(Helper::formatStandardApiResponse('error', null, $ldap_results['error_message']), 200);
+ }
+
+ return response()->json(Helper::formatStandardApiResponse('success', null, $ldap_results['summary']), 200);
+
+ }
}
diff --git a/app/Http/Controllers/AssetMaintenancesController.php b/app/Http/Controllers/AssetMaintenancesController.php
deleted file mode 100644
index 360db45262d3..000000000000
--- a/app/Http/Controllers/AssetMaintenancesController.php
+++ /dev/null
@@ -1,289 +0,0 @@
-
- * @version v1.0
- * @since [v1.8]
- */
- private static function getInsufficientPermissionsRedirect(): RedirectResponse
- {
- return redirect()->route('maintenances.index')
- ->with('error', trans('general.insufficient_permissions'));
- }
-
- /**
- * Returns a view that invokes the ajax tables which actually contains
- * the content for the asset maintenances listing, which is generated in getDatatable.
- *
- * @todo This should be replaced with middleware and/or policies
- * @see AssetMaintenancesController::getDatatable() method that generates the JSON response
- * @author Vincent Sposato
- * @version v1.0
- * @since [v1.8]
- */
- public function index() : View
- {
- $this->authorize('view', Asset::class);
- return view('asset_maintenances/index');
- }
-
- /**
- * Returns a form view to create a new asset maintenance.
- *
- * @see AssetMaintenancesController::postCreate() method that stores the data
- * @author Vincent Sposato
- * @version v1.0
- * @since [v1.8]
- * @return mixed
- */
- public function create() : View
- {
- $this->authorize('update', Asset::class);
- $asset = null;
-
- if ($asset = Asset::find(request('asset_id'))) {
- // We have to set this so that the correct property is set in the select2 ajax dropdown
- $asset->asset_id = $asset->id;
- }
-
- // Prepare Asset Maintenance Type List
- $assetMaintenanceType = [
- '' => 'Select an asset maintenance type',
- ] + AssetMaintenance::getImprovementOptions();
- // Mark the selected asset, if it came in
-
- return view('asset_maintenances/edit')
- ->with('asset', $asset)
- ->with('assetMaintenanceType', $assetMaintenanceType)
- ->with('item', new AssetMaintenance);
- }
-
- /**
- * Validates and stores the new asset maintenance
- *
- * @see AssetMaintenancesController::getCreate() method for the form
- * @author Vincent Sposato
- * @version v1.0
- * @since [v1.8]
- */
- public function store(Request $request) : RedirectResponse
- {
- $this->authorize('update', Asset::class);
- // create a new model instance
- $assetMaintenance = new AssetMaintenance();
- $assetMaintenance->supplier_id = $request->input('supplier_id');
- $assetMaintenance->is_warranty = $request->input('is_warranty');
- $assetMaintenance->cost = $request->input('cost');
- $assetMaintenance->notes = $request->input('notes');
- $asset = Asset::find($request->input('asset_id'));
-
- if ((! Company::isCurrentUserHasAccess($asset)) && ($asset != null)) {
- return static::getInsufficientPermissionsRedirect();
- }
-
- // Save the asset maintenance data
- $assetMaintenance->asset_id = $request->input('asset_id');
- $assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
- $assetMaintenance->title = $request->input('title');
- $assetMaintenance->start_date = $request->input('start_date');
- $assetMaintenance->completion_date = $request->input('completion_date');
- $assetMaintenance->created_by = auth()->id();
-
- if (($assetMaintenance->completion_date !== null)
- && ($assetMaintenance->start_date !== '')
- && ($assetMaintenance->start_date !== '0000-00-00')
- ) {
- $startDate = Carbon::parse($assetMaintenance->start_date);
- $completionDate = Carbon::parse($assetMaintenance->completion_date);
- $assetMaintenance->asset_maintenance_time = $completionDate->diffInDays($startDate);
- }
-
- // Was the asset maintenance created?
- if ($assetMaintenance->save()) {
- // Redirect to the new asset maintenance page
- return redirect()->route('maintenances.index')
- ->with('success', trans('admin/asset_maintenances/message.create.success'));
- }
-
- return redirect()->back()->withInput()->withErrors($assetMaintenance->getErrors());
- }
-
- /**
- * Returns a form view to edit a selected asset maintenance.
- *
- * @see AssetMaintenancesController::postEdit() method that stores the data
- * @author Vincent Sposato
- * @param int $assetMaintenanceId
- * @version v1.0
- * @since [v1.8]
- */
- public function edit($assetMaintenanceId = null) : View | RedirectResponse
- {
- $this->authorize('update', Asset::class);
- // Check if the asset maintenance exists
- $this->authorize('update', Asset::class);
- // Check if the asset maintenance exists
- if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
- // Redirect to the asset maintenance management page
- return redirect()->route('maintenances.index')->with('error', trans('admin/asset_maintenances/message.not_found'));
- } elseif ((!$assetMaintenance->asset) || ($assetMaintenance->asset->deleted_at!='')) {
- // Redirect to the asset maintenance management page
- return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
- } elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
- return static::getInsufficientPermissionsRedirect();
- }
-
- // Prepare Improvement Type List
- $assetMaintenanceType = ['' => 'Select an improvement type'] + AssetMaintenance::getImprovementOptions();
-
- return view('asset_maintenances/edit')
- ->with('selectedAsset', null)
- ->with('assetMaintenanceType', $assetMaintenanceType)
- ->with('item', $assetMaintenance);
- }
-
- /**
- * Validates and stores an update to an asset maintenance
- *
- * @see AssetMaintenancesController::postEdit() method that stores the data
- * @author Vincent Sposato
- * @param Request $request
- * @param int $assetMaintenanceId
- * @version v1.0
- * @since [v1.8]
- */
- public function update(Request $request, $assetMaintenanceId = null) : View | RedirectResponse
- {
- $this->authorize('update', Asset::class);
- // Check if the asset maintenance exists
- if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
- // Redirect to the asset maintenance management page
- return redirect()->route('maintenances.index')->with('error', trans('admin/asset_maintenances/message.not_found'));
- } elseif ((!$assetMaintenance->asset) || ($assetMaintenance->asset->deleted_at!='')) {
- // Redirect to the asset maintenance management page
- return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
- } elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
- return static::getInsufficientPermissionsRedirect();
- }
-
- $assetMaintenance->supplier_id = $request->input('supplier_id');
- $assetMaintenance->is_warranty = $request->input('is_warranty');
- $assetMaintenance->cost = $request->input('cost');
- $assetMaintenance->notes = $request->input('notes');
-
- $asset = Asset::find(request('asset_id'));
-
- if (! Company::isCurrentUserHasAccess($asset)) {
- return static::getInsufficientPermissionsRedirect();
- }
-
- // Save the asset maintenance data
- $assetMaintenance->asset_id = $request->input('asset_id');
- $assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
- $assetMaintenance->title = $request->input('title');
- $assetMaintenance->start_date = $request->input('start_date');
- $assetMaintenance->completion_date = $request->input('completion_date');
-
- if (($assetMaintenance->completion_date == null)
- ) {
- if (($assetMaintenance->asset_maintenance_time !== 0)
- || (! is_null($assetMaintenance->asset_maintenance_time))
- ) {
- $assetMaintenance->asset_maintenance_time = null;
- }
- }
-
- if (($assetMaintenance->completion_date !== null)
- && ($assetMaintenance->start_date !== '')
- && ($assetMaintenance->start_date !== '0000-00-00')
- ) {
- $startDate = Carbon::parse($assetMaintenance->start_date);
- $completionDate = Carbon::parse($assetMaintenance->completion_date);
- $assetMaintenance->asset_maintenance_time = $completionDate->diffInDays($startDate);
- }
-
- // Was the asset maintenance created?
- if ($assetMaintenance->save()) {
-
- // Redirect to the new asset maintenance page
- return redirect()->route('maintenances.index')
- ->with('success', trans('admin/asset_maintenances/message.edit.success'));
- }
-
- return redirect()->back()->withInput()->withErrors($assetMaintenance->getErrors());
- }
-
- /**
- * Delete an asset maintenance
- *
- * @author Vincent Sposato
- * @param int $assetMaintenanceId
- * @version v1.0
- * @since [v1.8]
- */
- public function destroy($assetMaintenanceId) : RedirectResponse
- {
- $this->authorize('update', Asset::class);
- // Check if the asset maintenance exists
- if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
- // Redirect to the asset maintenance management page
- return redirect()->route('maintenances.index')
- ->with('error', trans('admin/asset_maintenances/message.not_found'));
- } elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
- return static::getInsufficientPermissionsRedirect();
- }
-
- // Delete the asset maintenance
- $assetMaintenance->delete();
-
- // Redirect to the asset_maintenance management page
- return redirect()->route('maintenances.index')
- ->with('success', trans('admin/asset_maintenances/message.delete.success'));
- }
-
- /**
- * View an asset maintenance
- *
- * @author Vincent Sposato
- * @param int $assetMaintenanceId
- * @version v1.0
- * @since [v1.8]
- */
- public function show($assetMaintenanceId) : View | RedirectResponse
- {
- $this->authorize('view', Asset::class);
-
- // Check if the asset maintenance exists
- if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
- // Redirect to the asset maintenance management page
- return redirect()->route('maintenances.index')
- ->with('error', trans('admin/asset_maintenances/message.not_found'));
- } elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
- return static::getInsufficientPermissionsRedirect();
- }
-
- return view('asset_maintenances/view')->with('assetMaintenance', $assetMaintenance);
- }
-}
diff --git a/app/Http/Controllers/AssetModelsController.php b/app/Http/Controllers/AssetModelsController.php
index aa083e1ae329..ecbd2f06bef5 100755
--- a/app/Http/Controllers/AssetModelsController.php
+++ b/app/Http/Controllers/AssetModelsController.php
@@ -6,10 +6,12 @@
use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\StoreAssetModelRequest;
use App\Models\Actionlog;
+use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\CustomField;
use App\Models\SnipeModel;
use App\Models\User;
+use App\Models\DefaultValuesForCustomFields;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
@@ -82,12 +84,26 @@ public function store(StoreAssetModelRequest $request) : RedirectResponse
$model->notes = $request->input('notes');
$model->created_by = auth()->id();
$model->requestable = $request->has('requestable');
+ $model->require_serial = $request->input('require_serial', 0);
if ($request->input('fieldset_id') != '') {
$model->fieldset_id = $request->input('fieldset_id');
}
- $model = $request->handleImages($model);
+ if ($request->has('use_cloned_image')) {
+ $cloned_model_img = AssetModel::select('image')->find($request->input('clone_image_from_id'));
+ if ($cloned_model_img) {
+ $new_image_name = 'clone-' . date('U') . '-' . $cloned_model_img->image;
+ $new_image = 'models/' . $new_image_name;
+ Storage::disk('public')->copy('models/' . $cloned_model_img->image, $new_image);
+ $model->image = $new_image_name;
+ }
+
+ } else {
+ $model = $request->handleImages($model);
+ }
+
+
if ($model->save()) {
if ($this->shouldAddDefaultValues($request->input())) {
@@ -109,16 +125,11 @@ public function store(StoreAssetModelRequest $request) : RedirectResponse
* @since [v1.0]
* @param int $modelId
*/
- public function edit($modelId = null) : View | RedirectResponse
+ public function edit(AssetModel $model): View|RedirectResponse
{
$this->authorize('update', AssetModel::class);
- if ($item = AssetModel::find($modelId)) {
- $category_type = 'asset';
- return view('models/edit', compact('item', 'category_type'))->with('depreciation_list', Helper::depreciationList());
-
- }
-
- return redirect()->route('models.index')->with('error', trans('admin/models/message.does_not_exist'));
+ $category_type = 'asset';
+ return view('models/edit', compact('category_type'))->with('item', $model)->with('depreciation_list', Helper::depreciationList());
}
@@ -133,16 +144,11 @@ public function edit($modelId = null) : View | RedirectResponse
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function update(StoreAssetModelRequest $request, $modelId) : RedirectResponse
+ public function update(StoreAssetModelRequest $request, AssetModel $model): RedirectResponse
{
$this->authorize('update', AssetModel::class);
- if (is_null($model = AssetModel::find($modelId))) {
- return redirect()->route('models.index')->with('error', trans('admin/models/message.does_not_exist'));
- }
-
$model = $request->handleImages($model);
-
$model->depreciation_id = $request->input('depreciation_id');
$model->eol = $request->input('eol');
$model->name = $request->input('name');
@@ -153,6 +159,9 @@ public function update(StoreAssetModelRequest $request, $modelId) : RedirectResp
$model->notes = $request->input('notes');
$model->requestable = $request->input('requestable', '0');
+ DefaultValuesForCustomFields::forPivot($model, Asset::class)->delete(); //FIXME - this doesn't seem right
+
+ $model->require_serial = $request->input('require_serial', 0);
$model->fieldset_id = $request->input('fieldset_id');
if ($model->save()) {
@@ -188,28 +197,16 @@ public function update(StoreAssetModelRequest $request, $modelId) : RedirectResp
* @since [v1.0]
* @param int $modelId
*/
- public function destroy($modelId) : RedirectResponse
+ public function destroy(AssetModel $model): RedirectResponse
{
$this->authorize('delete', AssetModel::class);
- // Check if the model exists
- if (is_null($model = AssetModel::find($modelId))) {
- return redirect()->route('models.index')->with('error', trans('admin/models/message.does_not_exist'));
- }
+
if ($model->assets()->count() > 0) {
// Throw an error that this model is associated with assets
return redirect()->route('models.index')->with('error', trans('admin/models/message.assoc_users'));
}
- if ($model->image) {
- try {
- Storage::disk('public')->delete('models/'.$model->image);
- $model->update(['image' => null]);
- } catch (\Exception $e) {
- Log::info($e);
- }
- }
-
// Delete the model
$model->delete();
@@ -267,16 +264,10 @@ public function getRestore($id) : RedirectResponse
* @since [v1.0]
* @param int $modelId
*/
- public function show($modelId = null) : View | RedirectResponse
+ public function show(AssetModel $model): View|RedirectResponse
{
$this->authorize('view', AssetModel::class);
- $model = AssetModel::withTrashed()->find($modelId);
-
- if (isset($model->id)) {
- return view('models/view', compact('model'));
- }
-
- return redirect()->route('models.index')->with('error', trans('admin/models/message.does_not_exist'));
+ return view('models/view', compact('model'));
}
/**
@@ -286,23 +277,20 @@ public function show($modelId = null) : View | RedirectResponse
* @since [v1.0]
* @param int $modelId
*/
- public function getClone($modelId = null) : View | RedirectResponse
+ public function getClone(AssetModel $model): View|RedirectResponse
{
$this->authorize('create', AssetModel::class);
- // Check if the model exists
- if (is_null($model_to_clone = AssetModel::find($modelId))) {
- return redirect()->route('models.index')->with('error', trans('admin/models/message.does_not_exist'));
- }
- $model = clone $model_to_clone;
+ $cloned_model = clone $model;
$model->id = null;
+ $model->deleted_at = null;
// Show the page
return view('models/edit')
->with('depreciation_list', Helper::depreciationList())
->with('item', $model)
- ->with('model_id', $model_to_clone->id)
- ->with('clone_model', $model_to_clone);
+ ->with('model_id', $model->id)
+ ->with('cloned_model', $cloned_model);
}
@@ -321,7 +309,7 @@ public function getCustomFields($modelId) : View
/**
- * Returns a view that allows the user to bulk edit model attrbutes
+ * Returns a view that allows the user to bulk edit model attributes
*
* @author [A. Gianotto] []
* @since [v1.7]
@@ -456,7 +444,7 @@ private function shouldAddDefaultValues(array $input) : bool
}
/**
- * Adds default values to a model (as long as they are truthy)
+ * Adds default values to a model (as long as they are truthy) (does this mean I cannot set a default value of 0?)
*
* @param AssetModel $model
* @param array $defaultValues
@@ -496,11 +484,10 @@ private function assignCustomFieldsDefaultValues(AssetModel|SnipeModel $model, a
}
foreach ($defaultValues as $customFieldId => $defaultValue) {
- if(is_array($defaultValue)){
- $model->defaultValues()->attach($customFieldId, ['default_value' => implode(', ', $defaultValue)]);
- }elseif ($defaultValue) {
- $model->defaultValues()->attach($customFieldId, ['default_value' => $defaultValue]);
+ if (is_array($defaultValue)) {
+ $defaultValue = implode(', ', $defaultValue);
}
+ DefaultValuesForCustomFields::updateOrCreate(['custom_field_id' => $customFieldId, 'item_pivot_id' => $model->id], ['default_value' => $defaultValue]);
}
return true;
}
diff --git a/app/Http/Controllers/AssetModelsFilesController.php b/app/Http/Controllers/AssetModelsFilesController.php
deleted file mode 100644
index 11ede43c640b..000000000000
--- a/app/Http/Controllers/AssetModelsFilesController.php
+++ /dev/null
@@ -1,131 +0,0 @@
-]
- */
- public function store(UploadFileRequest $request, $modelId = null) : RedirectResponse
- {
- if (! $model = AssetModel::find($modelId)) {
- return redirect()->route('models.index')->with('error', trans('admin/hardware/message.does_not_exist'));
- }
-
- $this->authorize('update', $model);
-
- if ($request->hasFile('file')) {
- if (! Storage::exists('private_uploads/assetmodels')) {
- Storage::makeDirectory('private_uploads/assetmodels', 775);
- }
-
- foreach ($request->file('file') as $file) {
-
- $file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$model->id,$file);
-
- $model->logUpload($file_name, $request->get('notes'));
- }
-
- return redirect()->back()->withFragment('files')->with('success', trans('general.file_upload_success'));
- }
-
- return redirect()->back()->withFragment('files')->with('error', trans('admin/hardware/message.upload.nofiles'));
- }
-
- /**
- * Check for permissions and display the file.
- *
- * @author [A. Gianotto] []
- * @param int $modelId
- * @param int $fileId
- * @since [v1.0]
- */
- public function show($modelId = null, $fileId = null) : StreamedResponse | Response | RedirectResponse | BinaryFileResponse
- {
- $model = AssetModel::find($modelId);
- // the asset is valid
- if (isset($model->id)) {
- $this->authorize('view', $model);
-
- if (! $log = Actionlog::find($fileId)) {
- return response('No matching record for that model/file', 500)
- ->header('Content-Type', 'text/plain');
- }
-
- $file = 'private_uploads/assetmodels/'.$log->filename;
-
- if (! Storage::exists($file)) {
- return response('File '.$file.' not found on server', 404)
- ->header('Content-Type', 'text/plain');
- }
-
- if (request('inline') == 'true') {
-
- $headers = [
- 'Content-Disposition' => 'inline',
- ];
-
- return Storage::download($file, $log->filename, $headers);
- }
-
- return StorageHelper::downloader($file);
- }
- // Prepare the error message
- $error = trans('admin/hardware/message.does_not_exist', ['id' => $fileId]);
-
- // Redirect to the hardware management page
- return redirect()->route('hardware.index')->with('error', $error);
- }
-
- /**
- * Delete the associated file
- *
- * @author [A. Gianotto] []
- * @param int $modelId
- * @param int $fileId
- * @since [v1.0]
- */
- public function destroy($modelId = null, $fileId = null) : RedirectResponse
- {
- $model = AssetModel::find($modelId);
- $this->authorize('update', $model);
- $rel_path = 'private_uploads/assetmodels';
-
- // the asset is valid
- if (isset($model->id)) {
- $this->authorize('update', $model);
- $log = Actionlog::find($fileId);
- if ($log) {
- if (Storage::exists($rel_path.'/'.$log->filename)) {
- Storage::delete($rel_path.'/'.$log->filename);
- }
- $log->delete();
-
- return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
- }
-
- return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
- }
-
- // Redirect to the hardware management page
- return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
- }
-}
diff --git a/app/Http/Controllers/Assets/AssetCheckinController.php b/app/Http/Controllers/Assets/AssetCheckinController.php
index f84a468a60e7..62e8e59ca8e1 100644
--- a/app/Http/Controllers/Assets/AssetCheckinController.php
+++ b/app/Http/Controllers/Assets/AssetCheckinController.php
@@ -14,6 +14,7 @@
use Illuminate\Support\Facades\Log;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
+use Illuminate\Support\Facades\Validator;
class AssetCheckinController extends Controller
{
@@ -27,18 +28,12 @@ class AssetCheckinController extends Controller
* @param string $backto
* @since [v1.0]
*/
- public function create($assetId, $backto = null) : View | RedirectResponse
+ public function create(Asset $asset, $backto = null) : View | RedirectResponse
{
- // Check if the asset exists
- if (is_null($asset = Asset::find($assetId))) {
- // Redirect to the asset management page with error
- return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
- }
$this->authorize('checkin', $asset);
// This asset is already checked in, redirect
-
if (is_null($asset->assignedTo)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkin.already_checked_in'));
}
@@ -47,7 +42,23 @@ public function create($assetId, $backto = null) : View | RedirectResponse
return redirect()->route('hardware.show', $asset->id)->with('error', trans('admin/hardware/general.model_invalid_fix'));
}
- return view('hardware/checkin', compact('asset'))->with('statusLabel_list', Helper::statusLabelList())->with('backto', $backto)->with('table_name', 'Assets');
+ // Invoke the validation to see if the audit will complete successfully
+ $asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
+
+ if ($asset->isInvalid()) {
+ return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
+ }
+
+ $target_option = match ($asset->assigned_type) {
+ 'App\Models\Asset' => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.asset_previous')]),
+ 'App\Models\Location' => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.location')]),
+ default => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.user')]),
+ };
+ return view('hardware/checkin', compact('asset', 'target_option'))
+ ->with('item', $asset)
+ ->with('statusLabel_list', Helper::statusLabelList())
+ ->with('backto', $backto)
+ ->with('table_name', 'Assets');
}
/**
@@ -77,12 +88,14 @@ public function store(AssetCheckinRequest $request, $assetId = null, $backto = n
$this->authorize('checkin', $asset);
- if ($asset->assignedType() == Asset::USER) {
- $user = $asset->assignedTo;
- }
+ session()->put('checkedInFrom', $asset->assignedTo->id);
+ session()->put('checkout_to_type', match ($asset->assigned_type) {
+ 'App\Models\User' => 'user',
+ 'App\Models\Location' => 'location',
+ 'App\Models\Asset' => 'asset',
+ });
$asset->expected_checkin = null;
- $asset->last_checkin = now();
$asset->assignedTo()->disassociate($asset);
$asset->accepted = null;
$asset->name = $request->get('name');
@@ -91,6 +104,9 @@ public function store(AssetCheckinRequest $request, $assetId = null, $backto = n
$asset->status_id = e($request->get('status_id'));
}
+ // Add any custom fields that should be included in the checkout
+ $asset->customFieldsForCheckinCheckout('display_checkin');
+
$this->migrateLegacyLocations($asset);
$asset->location_id = $asset->rtd_location_id;
@@ -106,11 +122,14 @@ public function store(AssetCheckinRequest $request, $assetId = null, $backto = n
$originalValues = $asset->getRawOriginal();
+ // Handle last checkin date
$checkin_at = date('Y-m-d H:i:s');
if (($request->filled('checkin_at')) && ($request->get('checkin_at') != date('Y-m-d'))) {
$originalValues['action_date'] = $checkin_at;
$checkin_at = $request->get('checkin_at');
+
}
+ $asset->last_checkin = $checkin_at;
$asset->licenseseats->each(function (LicenseSeat $seat) {
$seat->update(['assigned_to' => null]);
@@ -128,10 +147,14 @@ function (Builder $query) use ($asset) {
session()->put('redirect_option', $request->get('redirect_option'));
+ // Add any custom fields that should be included in the checkout
+ $asset->customFieldsForCheckinCheckout('display_checkin');
+
if ($asset->save()) {
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues));
- return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))->with('success', trans('admin/hardware/message.checkin.success'));
+ return Helper::getRedirectOption($request, $asset->id, 'Assets')
+ ->with('success', trans('admin/hardware/message.checkin.success'));
}
// Redirect to the asset management page with error
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkin.error').$asset->getErrors());
diff --git a/app/Http/Controllers/Assets/AssetCheckoutController.php b/app/Http/Controllers/Assets/AssetCheckoutController.php
index 05b766916be8..bfbbbfcad428 100644
--- a/app/Http/Controllers/Assets/AssetCheckoutController.php
+++ b/app/Http/Controllers/Assets/AssetCheckoutController.php
@@ -12,6 +12,7 @@
use Illuminate\Support\Facades\Session;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
+use Illuminate\Support\Facades\Validator;
class AssetCheckoutController extends Controller
{
@@ -26,27 +27,33 @@ class AssetCheckoutController extends Controller
* @since [v1.0]
* @return \Illuminate\Contracts\View\View
*/
- public function create($assetId) : View | RedirectResponse
+ public function create(Asset $asset) : View | RedirectResponse
{
- // Check if the asset exists
- if (is_null($asset = Asset::with('company')->find(e($assetId)))) {
- return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
- }
$this->authorize('checkout', $asset);
if (!$asset->model) {
- return redirect()->route('hardware.show', $asset->id)->with('error', trans('admin/hardware/general.model_invalid_fix'));
+ return redirect()->route('hardware.show', $asset)
+ ->with('error', trans('admin/hardware/general.model_invalid_fix'));
}
+ // Invoke the validation to see if the audit will complete successfully
+ $asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
+
+ if ($asset->isInvalid()) {
+ return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
+ }
+
+
if ($asset->availableForCheckout()) {
return view('hardware/checkout', compact('asset'))
->with('statusLabel_list', Helper::deployableStatusLabelList())
- ->with('table_name', 'Assets');
+ ->with('table_name', 'Assets')
+ ->with('item', $asset);
}
-
- return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkout.not_available'));
+ return redirect()->route('hardware.index')
+ ->with('error', trans('admin/hardware/message.checkout.not_available'));
}
/**
@@ -58,6 +65,8 @@ public function create($assetId) : View | RedirectResponse
*/
public function store(AssetCheckoutRequest $request, $assetId) : RedirectResponse
{
+
+
try {
// Check if the asset exists
if (! $asset = Asset::find($assetId)) {
@@ -68,12 +77,13 @@ public function store(AssetCheckoutRequest $request, $assetId) : RedirectRespons
$this->authorize('checkout', $asset);
if (!$asset->model) {
- return redirect()->route('hardware.show', $asset->id)->with('error', trans('admin/hardware/general.model_invalid_fix'));
+ return redirect()->route('hardware.show', $asset)->with('error', trans('admin/hardware/general.model_invalid_fix'));
}
$admin = auth()->user();
$target = $this->determineCheckoutTarget();
+ session()->put(['checkout_to_type' => $target]);
$asset = $this->updateAssetLocation($asset, $target);
@@ -91,6 +101,7 @@ public function store(AssetCheckoutRequest $request, $assetId) : RedirectRespons
$asset->status_id = $request->get('status_id');
}
+
if(!empty($asset->licenseseats->all())){
if(request('checkout_to_type') == 'user') {
foreach ($asset->licenseseats as $seat){
@@ -100,23 +111,26 @@ public function store(AssetCheckoutRequest $request, $assetId) : RedirectRespons
}
}
+ // Add any custom fields that should be included in the checkout
+ $asset->customFieldsForCheckinCheckout('display_checkout');
+
$settings = \App\Models\Setting::getSettings();
// We have to check whether $target->company_id is null here since locations don't have a company yet
if (($settings->full_multiple_companies_support) && ((!is_null($target->company_id)) && (!is_null($asset->company_id)))) {
if ($target->company_id != $asset->company_id){
- return redirect()->to("hardware/$assetId/checkout")->with('error', trans('general.error_user_company'));
+ return redirect()->route('hardware.checkout.create', $asset)->with('error', trans('general.error_user_company'));
}
}
- session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
+ session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, $request->get('note'), $request->get('name'))) {
- return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
+ return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success', trans('admin/hardware/message.checkout.success'));
}
// Redirect to the asset management page with error
- return redirect()->to("hardware/$assetId/checkout")->with('error', trans('admin/hardware/message.checkout.error').$asset->getErrors());
+ return redirect()->route("hardware.checkout.create", $asset)->with('error', trans('admin/hardware/message.checkout.error').$asset->getErrors());
} catch (ModelNotFoundException $e) {
return redirect()->back()->with('error', trans('admin/hardware/message.checkout.error'))->withErrors($asset->getErrors());
} catch (CheckoutNotAllowed $e) {
diff --git a/app/Http/Controllers/Assets/AssetFilesController.php b/app/Http/Controllers/Assets/AssetFilesController.php
deleted file mode 100644
index 96011728f99a..000000000000
--- a/app/Http/Controllers/Assets/AssetFilesController.php
+++ /dev/null
@@ -1,117 +0,0 @@
-]
- */
- public function store(UploadFileRequest $request, $assetId = null) : RedirectResponse
- {
- if (! $asset = Asset::find($assetId)) {
- return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
- }
-
- $this->authorize('update', $asset);
-
- if ($request->hasFile('file')) {
- if (! Storage::exists('private_uploads/assets')) {
- Storage::makeDirectory('private_uploads/assets', 775);
- }
-
- foreach ($request->file('file') as $file) {
- $file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
-
- $asset->logUpload($file_name, $request->get('notes'));
- }
-
- return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.upload.success'));
- }
-
- return redirect()->back()->with('error', trans('admin/hardware/message.upload.nofiles'));
- }
-
- /**
- * Check for permissions and display the file.
- *
- * @author [A. Gianotto] []
- * @param int $assetId
- * @param int $fileId
- * @since [v1.0]
- */
- public function show($assetId = null, $fileId = null) : View | RedirectResponse | Response | StreamedResponse | BinaryFileResponse
- {
- if ($asset = Asset::find($assetId)) {
-
- $this->authorize('view', $asset);
-
- if ($log = Actionlog::whereNotNull('filename')->where('item_id', $asset->id)->find($fileId)) {
- $file = 'private_uploads/assets/'.$log->filename;
-
- if ($log->action_type == 'audit') {
- $file = 'private_uploads/audits/'.$log->filename;
- }
-
- try {
- return StorageHelper::showOrDownloadFile($file, $log->filename);
- } catch (\Exception $e) {
- return redirect()->route('hardware.show', ['hardware' => $asset])->with('error', trans('general.file_not_found'));
- }
-
- }
-
- return redirect()->route('hardware.show', ['hardware' => $asset])->with('error', trans('general.log_record_not_found'));
- }
-
- return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
-
- }
-
- /**
- * Delete the associated file
- *
- * @author [A. Gianotto] []
- * @param int $assetId
- * @param int $fileId
- * @since [v1.0]
- */
- public function destroy($assetId = null, $fileId = null) : RedirectResponse
- {
- if ($asset = Asset::find($assetId)) {
- $this->authorize('update', $asset);
- $rel_path = 'private_uploads/assets';
-
- if ($log = Actionlog::find($fileId)) {
- if (Storage::exists($rel_path.'/'.$log->filename)) {
- Storage::delete($rel_path.'/'.$log->filename);
- }
- $log->delete();
- return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
- }
-
- return redirect()->route('hardware.show', ['hardware' => $asset])->with('error', trans('general.log_record_not_found'));
- }
-
- return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
- }
-}
diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php
index 9cb88cfb93f6..05ad21117972 100755
--- a/app/Http/Controllers/Assets/AssetsController.php
+++ b/app/Http/Controllers/Assets/AssetsController.php
@@ -6,6 +6,7 @@
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
+use App\Http\Requests\UpdateAssetRequest;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use Illuminate\Support\Facades\Log;
@@ -30,6 +31,7 @@
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
+use TypeError;
/**
* This class controls all actions related to assets for
@@ -108,17 +110,35 @@ public function store(ImageUploadRequest $request) : RedirectResponse
// This is only necessary on create, not update, since bulk editing is handled
// differently
$asset_tags = $request->input('asset_tags');
+ $model = AssetModel::find($request->input('model_id'));
+ $serial_errors = [];
+ $serials = $request->input('serials');
$settings = Setting::getSettings();
+ //Validate required serial based on model setting
+ for ($a = 1, $aMax = count($asset_tags); $a <= $aMax; $a++) {
+ if ($model && $model->require_serial === 1 && empty($serials[$a])) {
+ $serial_errors["serials.$a"] = trans('admin/hardware/form.serial_required', ['number' => $a]);
+ }
+
+ }
+
+ if (!empty($serial_errors)) {
+ return redirect()->back()
+ ->withInput()
+ ->withErrors($serial_errors);
+ }
+
+ $asset = null;
+ $companyId = Company::getIdForCurrentUser($request->input('company_id'));
$successes = [];
$failures = [];
- $serials = $request->input('serials');
- $asset = null;
- for ($a = 1; $a <= count($asset_tags); $a++) {
+ for ($a = 1, $aMax = count($asset_tags); $a <= $aMax; $a++) {
$asset = new Asset();
- $asset->model()->associate(AssetModel::find($request->input('model_id')));
+
+ $asset->model()->associate($model);
$asset->name = $request->input('name');
// Check for a corresponding serial
@@ -130,7 +150,7 @@ public function store(ImageUploadRequest $request) : RedirectResponse
$asset->asset_tag = $asset_tags[$a];
}
- $asset->company_id = Company::getIdForCurrentUser($request->input('company_id'));
+ $asset->company_id = $companyId;
$asset->model_id = $request->input('model_id');
$asset->order_number = $request->input('order_number');
$asset->notes = $request->input('notes');
@@ -147,7 +167,7 @@ public function store(ImageUploadRequest $request) : RedirectResponse
$asset->byod = request('byod', 0);
if (! empty($settings->audit_interval)) {
- $asset->next_audit_date = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
+ $asset->next_audit_date = Carbon::now()->addMonths((int)$settings->audit_interval)->toDateString();
}
// Set location_id to rtd_location_id ONLY if the asset isn't being checked out
@@ -155,45 +175,48 @@ public function store(ImageUploadRequest $request) : RedirectResponse
$asset->location_id = $request->input('rtd_location_id', null);
}
- // Create the image (if one was chosen.)
- if ($request->has('image')) {
+ if ($request->has('use_cloned_image')) {
+ $cloned_model_img = Asset::select('image')->find($request->input('clone_image_from_id'));
+ if ($cloned_model_img) {
+ $new_image_name = 'clone-' . date('U') . '-' . $cloned_model_img->image;
+ $new_image = 'assets/' . $new_image_name;
+ Storage::disk('public')->copy('assets/' . $cloned_model_img->image, $new_image);
+ $asset->image = $new_image_name;
+ }
+
+ } else {
$asset = $request->handleImages($asset);
}
- // Update custom fields in the database.
- // Validation for these fields is handled through the AssetRequest form request
- $model = AssetModel::find($request->get('model_id'));
-
- if (($model) && ($model->fieldset)) {
- foreach ($model->fieldset->fields as $field) {
- if ($field->field_encrypted == '1') {
- if (Gate::allows('assets.view.encrypted_custom_fields')) {
- if (is_array($request->input($field->db_column))) {
- $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
- } else {
- $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
- }
- }
- } else {
- if (is_array($request->input($field->db_column))) {
- $asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
- } else {
- $asset->{$field->db_column} = $request->input($field->db_column);
- }
- }
- }
- }
+ $asset->customFill($request, Auth::user()); // Update custom fields in the database.
// Validate the asset before saving
if ($asset->isValid() && $asset->save()) {
- if (request('assigned_user')) {
- $target = User::find(request('assigned_user'));
+ $target = null;
+ $location = null;
+
+ if ($userId = request('assigned_user')) {
+ $target = User::find($userId);
+
+ if (!$target) {
+ return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.user'));
+ }
$location = $target->location_id;
- } elseif (request('assigned_asset')) {
- $target = Asset::find(request('assigned_asset'));
+
+ } elseif ($assetId = request('assigned_asset')) {
+ $target = Asset::find($assetId);
+
+ if (!$target) {
+ return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.asset'));
+ }
$location = $target->location_id;
- } elseif (request('assigned_location')) {
- $target = Location::find(request('assigned_location'));
+
+ } elseif ($locationId = request('assigned_location')) {
+ $target = Location::find($locationId);
+
+ if (!$target) {
+ return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.location'));
+ }
$location = $target->id;
}
@@ -201,31 +224,38 @@ public function store(ImageUploadRequest $request) : RedirectResponse
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), $request->input('expected_checkin', null), 'Checked out on asset creation', $request->get('name'), $location);
}
- $successes[] = " $asset->id]) . "' style='color: white;'>" . e($asset->asset_tag) . "";
+ $successes[] = "" . e($asset->asset_tag) . "";
} else {
$failures[] = join(",", $asset->getErrors()->all());
}
}
+ if ($request->get('redirect_option') === 'back') {
+ session()->put(['redirect_option' => 'index']);
+ } else {
+ session()->put(['redirect_option' => $request->get('redirect_option')]);
+ }
+
+ session()->put(['checkout_to_type' => $request->get('checkout_to_type'),
+ 'other_redirect' => 'model']);
- session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
if ($successes) {
if ($failures) {
//some succeeded, some failed
- return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets')) //FIXME - not tested
+ return Helper::getRedirectOption($request, $asset->id, 'Assets') //FIXME - not tested
->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => join(", ", $successes)]))
->with('warning', trans_choice('admin/hardware/message.create.partial_failure', $failures, ['failures' => join("; ", $failures)]));
} else {
if (count($successes) == 1) {
//the most common case, keeping it so we don't have to make every use of that translation string be trans_choice'ed
//and re-translated
- return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
- ->with('success-unescaped', trans('admin/hardware/message.create.success_linked', ['link' => route('hardware.show', ['hardware' => $asset->id]), 'id', 'tag' => e($asset->asset_tag)]));
+ return Helper::getRedirectOption($request, $asset->id, 'Assets')
+ ->with('success-unescaped', trans('admin/hardware/message.create.success_linked', ['link' => route('hardware.show', $asset), 'id', 'tag' => e($asset->asset_tag)]));
} else {
//multi-success
- return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
+ return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => join(", ", $successes)]));
}
}
@@ -240,20 +270,15 @@ public function store(ImageUploadRequest $request) : RedirectResponse
* Returns a view that presents a form to edit an existing asset.
*
* @author [A. Gianotto] []
- * @param int $assetId
* @since [v1.0]
* @return \Illuminate\Contracts\View\View
*/
- public function edit($assetId = null) : View | RedirectResponse
+ public function edit(Asset $asset): View|RedirectResponse
{
- if (! $item = Asset::find($assetId)) {
- // Redirect to the asset management page with error
- return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
- }
- //Handles company checks and permissions.
- $this->authorize($item);
-
- return view('hardware/edit', compact('item'))
+ $this->authorize($asset);
+ session()->put('back_url', url()->previous());
+ return view('hardware/edit')
+ ->with('item', $asset)
->with('statuslabel_list', Helper::statusLabelList())
->with('statuslabel_types', Helper::statusTypeList());
}
@@ -267,15 +292,14 @@ public function edit($assetId = null) : View | RedirectResponse
* @since [v1.0]
* @return \Illuminate\Contracts\View\View
*/
- public function show($assetId = null) : View | RedirectResponse
+ public function show(Asset $asset): View|RedirectResponse
{
- $asset = Asset::withTrashed()->find($assetId);
$this->authorize('view', $asset);
$settings = Setting::getSettings();
if (isset($asset)) {
$audit_log = Actionlog::where('action_type', '=', 'audit')
- ->where('item_id', '=', $assetId)
+ ->where('item_id', '=', $asset->id)
->where('item_type', '=', Asset::class)
->orderBy('created_at', 'DESC')->first();
@@ -291,7 +315,7 @@ public function show($assetId = null) : View | RedirectResponse
$qr_code = (object) [
'display' => $settings->qr_code == '1',
- 'url' => route('qr_code/hardware', $asset->id),
+ 'url' => route('qr_code/hardware', $asset),
];
return view('hardware/view', compact('asset', 'qr_code', 'settings'))
@@ -308,14 +332,9 @@ public function show($assetId = null) : View | RedirectResponse
* @since [v1.0]
* @author [A. Gianotto] []
*/
- public function update(ImageUploadRequest $request, $assetId = null) : RedirectResponse
+ public function update(ImageUploadRequest $request, Asset $asset): RedirectResponse
{
- // Check if the asset exists
- if (! $asset = Asset::find($assetId)) {
- // Redirect to the asset management page with error
- return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
- }
$this->authorize($asset);
$asset->status_id = $request->input('status_id', null);
@@ -329,7 +348,7 @@ public function update(ImageUploadRequest $request, $assetId = null) : RedirectR
$asset->eol_explicit = false;
} elseif ($request->filled('asset_eol_date')) {
$asset->asset_eol_date = $request->input('asset_eol_date', null);
- $months = Carbon::parse($asset->asset_eol_date)->diffInMonths($asset->purchase_date);
+ $months = (int)Carbon::parse($asset->asset_eol_date)->diffInMonths($asset->purchase_date, true);
if($asset->model->eol) {
if($months != $asset->model->eol > 0) {
$asset->eol_explicit = true;
@@ -351,22 +370,16 @@ public function update(ImageUploadRequest $request, $assetId = null) : RedirectR
$status = Statuslabel::find($request->input('status_id'));
- // This is a non-deployable status label - we should check the asset back in.
- if (($status && $status->getStatuslabelType() != 'deployable') && ($target = $asset->assignedTo)) {
-
+ // This is an archived or undeployable - we should check the asset back in.
+ // Pending is allowed here
+ if (($status) && (($status->getStatuslabelType() != 'pending') && ($status->getStatuslabelType() != 'deployable')) && ($target = $asset->assignedTo)) {
$originalValues = $asset->getRawOriginal();
$asset->assigned_to = null;
$asset->assigned_type = null;
$asset->accepted = null;
-
- event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on asset update', date('Y-m-d H:i:s'), $originalValues));
- }
-
- if ($asset->assigned_to == '') {
- $asset->location_id = $request->input('rtd_location_id', null);
+ event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on asset update with ' . $status->getStatuslabelType() . ' status', date('Y-m-d H:i:s'), $originalValues));
}
-
if ($request->filled('image_delete')) {
try {
unlink(public_path().'/uploads/assets/'.$asset->image);
@@ -402,35 +415,49 @@ public function update(ImageUploadRequest $request, $assetId = null) : RedirectR
$asset = $request->handleImages($asset);
// Update custom fields in the database.
- // Validation for these fields is handlded through the AssetRequest form request
// FIXME: No idea why this is returning a Builder error on db_column_name.
// Need to investigate and fix. Using static method for now.
$model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
-
- if ($field->field_encrypted == '1') {
- if (Gate::allows('assets.view.encrypted_custom_fields')) {
+ if ($field->element == 'checkbox' && !$request->has($field->db_column)) {
+ $asset->{$field->db_column} = null;
+ }
+ if ($request->has($field->db_column)) {
+ if ($field->field_encrypted == '1') {
+ if (Gate::allows('assets.view.encrypted_custom_fields')) {
+ if (is_array($request->input($field->db_column))) {
+ $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
+ } else {
+ $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
+ }
+ }
+ } else {
if (is_array($request->input($field->db_column))) {
- $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
+ $asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
} else {
- $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
+ $asset->{$field->db_column} = $request->input($field->db_column);
}
}
- } else {
- if (is_array($request->input($field->db_column))) {
- $asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
- } else {
- $asset->{$field->db_column} = $request->input($field->db_column);
- }
}
}
}
-
- session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
-
+ session()->put([
+ 'redirect_option' => $request->get('redirect_option'),
+ 'checkout_to_type' => $request->get('checkout_to_type'),
+ 'other_redirect' => $request->get('redirect_option') === 'other_redirect' ? 'model' : null,
+ ]);
+
+
+ //Validate required serial based on model setting
+ if ($model && $model->require_serial === 1 && empty($serial[1])) {
+ return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
+ ->with('warning', trans('admin/hardware/form.serial_required_post_model_update', [
+ 'asset_model' => $model->name
+ ]));
+ }
if ($asset->save()) {
- return redirect()->to(Helper::getRedirectOption($request, $assetId, 'Assets'))
+ return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success', trans('admin/hardware/message.update.success'));
}
@@ -462,7 +489,7 @@ public function destroy(Request $request, $assetId) : RedirectResponse
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on delete', $checkin_at, $originalValues));
DB::table('assets')
->where('id', $asset->id)
- ->update(['assigned_to' => null]);
+ ->update(['assigned_to' => null, 'assigned_type' => null]);
}
@@ -474,6 +501,7 @@ public function destroy(Request $request, $assetId) : RedirectResponse
}
}
+
$asset->delete();
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.delete.success'));
@@ -531,12 +559,12 @@ public function getAssetByTag(Request $request, $tag=null) : RedirectResponse
* @param int $assetId
* @since [v1.0]
*/
- public function getQrCode($assetId = null) : Response | BinaryFileResponse | string | bool
+ public function getQrCode(Asset $asset): Response|BinaryFileResponse|string|bool
{
$settings = Setting::getSettings();
- if (($settings->qr_code == '1') && ($settings->label2_2d_type !== 'none')) {
- $asset = Asset::withTrashed()->find($assetId);
+ if ($settings->label2_2d_type !== 'none') {
+
if ($asset) {
$size = Helper::barcodeDimensions($settings->label2_2d_type);
$qr_file = public_path().'/uploads/barcodes/qr-'.str_slug($asset->asset_tag).'-'.str_slug($asset->id).'.png';
@@ -590,7 +618,7 @@ public function getBarCode($assetId = null)
file_put_contents($barcode_file, $barcode_obj->getPngData());
return response($barcode_obj->getPngData())->header('Content-type', 'image/png');
- } catch (\Exception $e) {
+ } catch (\Exception|TypeError $e) {
Log::debug('The barcode format is invalid.');
return response(file_get_contents(public_path('uploads/barcodes/invalid_barcode.gif')))->header('Content-type', 'image/gif');
@@ -634,8 +662,9 @@ public function getLabel($assetId = null)
*/
public function getClone(Asset $asset)
{
- $this->authorize('create', $asset);
+ $this->authorize('create', Asset::class);
$cloned = clone $asset;
+ $cloned_model = $asset;
$cloned->id = null;
$cloned->asset_tag = '';
$cloned->serial = '';
@@ -645,6 +674,7 @@ public function getClone(Asset $asset)
return view('hardware/edit')
->with('statuslabel_list', Helper::statusLabelList())
->with('statuslabel_types', Helper::statusTypeList())
+ ->with('cloned_model', $cloned_model)
->with('item', $cloned);
}
@@ -770,7 +800,7 @@ public function postImportHistory(Request $request)
'item_id' => $asset->id,
'item_type' => Asset::class,
'created_by' => auth()->id(),
- 'note' => 'Checkout imported by '.auth()->user()->present()->fullName().' from history importer',
+ 'note' => 'Checkout imported by ' . auth()->user()->display_name . ' from history importer',
'target_id' => $item[$asset_tag][$batch_counter]['user_id'],
'target_type' => User::class,
'created_at' => $item[$asset_tag][$batch_counter]['checkout_date'],
@@ -798,7 +828,7 @@ public function postImportHistory(Request $request)
'item_id' => $item[$asset_tag][$batch_counter]['asset_id'],
'item_type' => Asset::class,
'created_by' => auth()->id(),
- 'note' => 'Checkin imported by '.auth()->user()->present()->fullName().' from history importer',
+ 'note' => 'Checkin imported by ' . auth()->user()->display_name . ' from history importer',
'target_id' => null,
'created_at' => $checkin_date,
'action_type' => 'checkin',
@@ -877,14 +907,6 @@ public function quickScanCheckin()
return view('hardware/quickscan-checkin')->with('statusLabel_list', Helper::statusLabelList());
}
- public function audit($id)
- {
- $settings = Setting::getSettings();
- $this->authorize('audit', Asset::class);
- $dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
- $asset = Asset::findOrFail($id);
- return view('hardware/audit')->with('asset', $asset)->with('next_audit_date', $dt)->with('locations_list');
- }
public function dueForAudit()
{
@@ -901,22 +923,73 @@ public function dueForCheckin()
}
- public function auditStore(UploadFileRequest $request, $id)
+ public function audit(Asset $asset): View|RedirectResponse
{
$this->authorize('audit', Asset::class);
+ $settings = Setting::getSettings();
+
+
+ // Invoke the validation to see if the audit will complete successfully
+ $asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
+
+ if ($asset->isInvalid()) {
+ return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
+ }
+
+ $dt = Carbon::now()->addMonths((int)$settings->audit_interval)->toDateString();
+ return view('hardware/audit')->with('asset', $asset)->with('item', $asset)->with('next_audit_date', $dt)->with('locations_list');
+ }
- $rules = [
- 'location_id' => 'exists:locations,id|nullable|numeric',
- 'next_audit_date' => 'date|nullable',
- ];
+ public function auditStore(UploadFileRequest $request, Asset $asset)
+ {
+
+ $this->authorize('audit', Asset::class);
+
+ session()->put('redirect_option', $request->get('redirect_option'));
+ session()->put('other_redirect', 'audit');
+
+
+ $originalValues = $asset->getRawOriginal();
+
+ $asset->next_audit_date = $request->input('next_audit_date');
+ $asset->last_audit_date = date('Y-m-d H:i:s');
- $validator = Validator::make($request->all(), $rules);
+ // Check to see if they checked the box to update the physical location,
+ // not just note it in the audit notes
+ if ($request->input('update_location') == '1') {
+ $asset->location_id = $request->input('location_id');
+ }
- if ($validator->fails()) {
- return response()->json(Helper::formatStandardApiResponse('error', null, $validator->errors()->all()));
+ // Update custom fields in the database
+ if (($asset->model) && ($asset->model->fieldset)) {
+ foreach ($asset->model->fieldset->fields as $field) {
+ if (($field->display_audit == '1') && ($request->has($field->db_column))) {
+ if ($field->field_encrypted == '1') {
+ if (Gate::allows('assets.view.encrypted_custom_fields')) {
+ if (is_array($request->input($field->db_column))) {
+ $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
+ } else {
+ $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
+ }
+ }
+ } else {
+ if (is_array($request->input($field->db_column))) {
+ $asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
+ } else {
+ $asset->{$field->db_column} = $request->input($field->db_column);
+ }
+ }
+ }
+ }
}
- $asset = Asset::findOrFail($id);
+ // Invoke the validation to see if the audit will complete successfully
+ $asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
+
+ // Validate the rest of the data before we turn off the event dispatcher
+ if ($asset->isInvalid()) {
+ return redirect()->back()->withInput()->withErrors($asset->getErrors());
+ }
/**
* Even though we do a save() further down, we don't want to log this as a "normal" asset update,
@@ -932,18 +1005,11 @@ public function auditStore(UploadFileRequest $request, $id)
* which manually invokes Watson Validating to make sure the asset's model is valid.
*
* @see \App\Observers\AssetObserver::updating()
+ * @see \App\Models\Asset::save()
*/
- $asset->unsetEventDispatcher();
- $asset->next_audit_date = $request->input('next_audit_date');
- $asset->last_audit_date = date('Y-m-d H:i:s');
+ $asset->unsetEventDispatcher();
- // Check to see if they checked the box to update the physical location,
- // not just note it in the audit notes
- if ($request->input('update_location') == '1') {
- $asset->location_id = $request->input('location_id');
- }
-
/**
* Invoke Watson Validating to check the asset itself and check to make sure it saved correctly.
@@ -957,8 +1023,8 @@ public function auditStore(UploadFileRequest $request, $id)
$file_name = $request->handleFile('private_uploads/audits/', 'audit-'.$asset->id, $request->file('image'));
}
- $asset->logAudit($request->input('note'), $request->input('location_id'), $file_name);
- return redirect()->route('assets.audit.due')->with('success', trans('admin/hardware/message.audit.success'));
+ $asset->logAudit($request->input('note'), $request->input('location_id'), $file_name, $originalValues);
+ return Helper::getRedirectOption($request, $asset->id, 'Assets')->with('success', trans('admin/hardware/message.audit.success'));
}
return redirect()->back()->withInput()->withErrors($asset->getErrors());
diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php
index 93f7255c0bf1..6c75ae06db05 100644
--- a/app/Http/Controllers/Assets/BulkAssetsController.php
+++ b/app/Http/Controllers/Assets/BulkAssetsController.php
@@ -52,11 +52,26 @@ public function edit(Request $request) : View | RedirectResponse
}
$asset_ids = $request->input('ids');
+
if ($request->input('bulk_actions') === 'checkout') {
+ $status_check =$this->hasUndeployableStatus($asset_ids);
+ if($status_check && $status_check['status'] === true){
+
+ $asset_tags = implode(', ', array_column($status_check['tags'], 'asset_tag'));
+ $asset_ids = $status_check['asset_ids'];
+
+ session()->flash('warning', trans('admin/hardware/message.undeployable', ['asset_tags' => $asset_tags]));
+ }
+
$request->session()->flashInput(['selected_assets' => $asset_ids]);
return redirect()->route('hardware.bulkcheckout.show');
}
+ if ($request->input('bulk_actions') === 'maintenance') {
+ $request->session()->flashInput(['selected_assets' => $asset_ids]);
+ return redirect()->route('maintenances.create');
+ }
+
// Figure out where we need to send the user after the update is complete, and store that in the session
$bulk_back_url = request()->headers->get('referer');
session(['bulk_back_url' => $bulk_back_url]);
@@ -97,11 +112,47 @@ public function edit(Request $request) : View | RedirectResponse
// This handles all of the pivot sorting below (versus the assets.* fields in the allowed_columns array)
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'assets.id';
- $assets = Asset::with('assignedTo', 'location', 'model')
+ $query = Asset::with('assignedTo', 'location', 'model')
->whereIn('assets.id', $asset_ids)
->withTrashed();
- $assets = $assets->get();
+
+ switch ($sort_override) {
+ case 'model':
+ $query->OrderModels($order);
+ break;
+ case 'model_number':
+ $query->OrderModelNumber($order);
+ break;
+ case 'category':
+ $query->OrderCategory($order);
+ break;
+ case 'manufacturer':
+ $query->OrderManufacturer($order);
+ break;
+ case 'company':
+ $query->OrderCompany($order);
+ break;
+ case 'location':
+ $query->OrderLocation($order);
+ break;
+ case 'rtd_location':
+ $query->OrderRtdLocation($order);
+ break;
+ case 'status_label':
+ $query->OrderStatus($order);
+ break;
+ case 'supplier':
+ $query->OrderSupplier($order);
+ break;
+ case 'assigned_to':
+ $query->OrderAssigned($order);
+ break;
+ default:
+ $query->orderBy($column_sort, $order);
+ break;
+ }
+ $assets = $query->get();
if ($assets->isEmpty()) {
Log::debug('No assets were found for the provided IDs', ['ids' => $asset_ids]);
@@ -110,6 +161,7 @@ public function edit(Request $request) : View | RedirectResponse
$models = $assets->unique('model_id');
$modelNames = [];
+
foreach($models as $model) {
$modelNames[] = $model->model->name;
}
@@ -145,7 +197,6 @@ public function edit(Request $request) : View | RedirectResponse
case 'edit':
$this->authorize('update', Asset::class);
-
return view('hardware/bulk')
->with('assets', $asset_ids)
->with('statuslabel_list', Helper::statusLabelList())
@@ -154,40 +205,7 @@ public function edit(Request $request) : View | RedirectResponse
}
}
- switch ($sort_override) {
- case 'model':
- $assets->OrderModels($order);
- break;
- case 'model_number':
- $assets->OrderModelNumber($order);
- break;
- case 'category':
- $assets->OrderCategory($order);
- break;
- case 'manufacturer':
- $assets->OrderManufacturer($order);
- break;
- case 'company':
- $assets->OrderCompany($order);
- break;
- case 'location':
- $assets->OrderLocation($order);
- case 'rtd_location':
- $assets->OrderRtdLocation($order);
- break;
- case 'status_label':
- $assets->OrderStatus($order);
- break;
- case 'supplier':
- $assets->OrderSupplier($order);
- break;
- case 'assigned_to':
- $assets->OrderAssigned($order);
- break;
- default:
- $assets->orderBy($column_sort, $order);
- break;
- }
+
return redirect()->back()->with('error', 'No action selected');
}
@@ -206,14 +224,26 @@ public function update(Request $request) : RedirectResponse
$error_array = array();
// Get the back url from the session and then destroy the session
- $bulk_back_url = route('hardware.index');
- if ($request->session()->has('bulk_back_url')) {
- $bulk_back_url = $request->session()->pull('bulk_back_url');
- }
+ $bulk_back_url = $request->session()->pull('bulk_back_url', url()->previous());
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray();
+ // find custom field input attributes that start with 'null_'
+ $null_custom_fields_inputs = array_filter($request->all(), function ($key) {
+ // filter out all keys that start with 'null_'
+ return (strpos($key, 'null_') === 0);
+ }, ARRAY_FILTER_USE_KEY);;
+ // remove 'null' from the keys
+ $custom_fields_to_null = [];
+ foreach ($null_custom_fields_inputs as $key => $value) {
+ $custom_fields_to_null[str_replace('null', '', $key)] = $value;
+ }
+
+
+
+
+
if (! $request->filled('ids') || count($request->input('ids')) == 0) {
return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected'));
@@ -251,7 +281,9 @@ public function update(Request $request) : RedirectResponse
|| ($request->filled('null_expected_checkin_date'))
|| ($request->filled('null_next_audit_date'))
|| ($request->filled('null_asset_eol_date'))
+ || ($request->filled('null_notes'))
|| ($request->anyFilled($custom_field_columns))
+ || ($request->anyFilled(array_keys($null_custom_fields_inputs)))
) {
// Let's loop through those assets and build an update array
@@ -274,10 +306,14 @@ public function update(Request $request) : RedirectResponse
->conditionallyAddItem('supplier_id')
->conditionallyAddItem('warranty_months')
->conditionallyAddItem('next_audit_date')
- ->conditionallyAddItem('asset_eol_date');
+ ->conditionallyAddItem('asset_eol_date')
+ ->conditionallyAddItem('notes');
foreach ($custom_field_columns as $key => $custom_field_column) {
$this->conditionallyAddItem($custom_field_column);
}
+ foreach ($custom_fields_to_null as $key => $custom_field_to_null) {
+ $this->conditionallyAddItem($key);
+ }
if (!($asset->eol_explicit)) {
if ($request->filled('model_id')) {
@@ -328,6 +364,10 @@ public function update(Request $request) : RedirectResponse
}
}
+ if ($request->input('null_notes')=='1') {
+ $this->update_array['notes'] = null;
+ }
+
if ($request->filled('purchase_cost')) {
@@ -358,16 +398,22 @@ public function update(Request $request) : RedirectResponse
* to someone/something.
*/
if ($request->filled('status_id')) {
- $updated_status = Statuslabel::find($request->input('status_id'));
+ try {
+ $updated_status = Statuslabel::findOrFail($request->input('status_id'));
+ } catch (ModelNotFoundException $e) {
+ return redirect($bulk_back_url)->with('error', trans('admin/statuslabels/message.does_not_exist'));
+ }
// We cannot assign a non-deployable status type if the asset is already assigned.
// This could probably be added to a form request.
// If the asset isn't assigned, we don't care what the status is.
// Otherwise we need to make sure the status type is still a deployable one.
- if (
- ($asset->assigned_to == '')
- || ($updated_status->deployable == '1') && ($asset->assetstatus->deployable == '1')
- ) {
+
+ $unassigned = $asset->assigned_to == '';
+ $deployable = $updated_status->deployable == '1' && $asset->assetstatus?->deployable == '1';
+ $pending = $updated_status->pending === 1;
+
+ if ($unassigned || $deployable || $pending) {
$this->update_array['status_id'] = $updated_status->id;
}
@@ -419,6 +465,7 @@ public function update(Request $request) : RedirectResponse
}
/**
+ *
* Start all the custom fields shenanigans
*/
@@ -426,6 +473,15 @@ public function update(Request $request) : RedirectResponse
if ($asset->model->fieldset) {
foreach ($asset->model->fieldset->fields as $field) {
+ // null custom fields
+ if ($custom_fields_to_null) {
+ foreach ($custom_fields_to_null as $key => $custom_field_to_null) {
+ if ($field->db_column == $key) {
+ $this->update_array[$field->db_column] = null;
+ }
+ }
+ }
+
if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted == '1')) {
if (Gate::allows('admin')) {
$decrypted_old = Helper::gracefulDecrypt($field, $asset->{$field->db_column});
@@ -484,7 +540,13 @@ public function update(Request $request) : RedirectResponse
} // end asset foreach
if ($has_errors > 0) {
- return redirect($bulk_back_url)->with('bulk_asset_errors', $error_array);
+ session()->put('bulkedit_ids', $request->input('ids'));
+ session()->put('bulk_asset_errors',$error_array);
+
+ return redirect()
+ ->route('hardware.index')
+ ->with('bulk_asset_errors', $error_array)
+ ->withInput();
}
return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.update.success'));
@@ -525,21 +587,31 @@ public function destroy(Request $request) : RedirectResponse
$this->authorize('delete', Asset::class);
$bulk_back_url = route('hardware.index');
+
if ($request->session()->has('bulk_back_url')) {
$bulk_back_url = $request->session()->pull('bulk_back_url');
}
+ $assetIds = $request->get('ids');
- if ($request->filled('ids')) {
- $assets = Asset::find($request->get('ids'));
- foreach ($assets as $asset) {
+ if(empty($assetIds)) {
+ return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.delete.nothing_updated'));
+ }
+
+ $assignedAssets = Asset::whereIn('id', $assetIds)->whereNotNull('assigned_to')->get();
+ if($assignedAssets->isNotEmpty()) {
+
+ //if assets are checked out, return a list of asset tags that would need to be checked in first.
+ $assetTags = $assignedAssets->pluck('asset_tag')->implode(', ');
+ return redirect($bulk_back_url)->with('error', trans_choice('admin/hardware/message.delete.assigned_to_error', $assignedAssets->count(), ['asset_tag' => $assetTags] ));
+ }
+
+ foreach (Asset::wherein('id', $assetIds)->get() as $asset) {
$asset->delete();
- } // endforeach
+ }
- return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.delete.success'));
+ return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.delete.success'));
// no values given, nothing to update
- }
- return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.delete.nothing_updated'));
}
/**
@@ -548,7 +620,10 @@ public function destroy(Request $request) : RedirectResponse
public function showCheckout() : View
{
$this->authorize('checkout', Asset::class);
- return view('hardware/bulk-checkout');
+
+ $do_not_change = ['' => trans('general.do_not_change')];
+ $status_label_list = $do_not_change + Helper::deployableStatusLabelList();
+ return view('hardware/bulk-checkout')->with('statusLabel_list', $status_label_list);
}
/**
@@ -556,13 +631,13 @@ public function showCheckout() : View
*/
public function storeCheckout(AssetCheckoutRequest $request) : RedirectResponse | ModelNotFoundException
{
-
$this->authorize('checkout', Asset::class);
try {
$admin = auth()->user();
$target = $this->determineCheckoutTarget();
+ session()->put(['checkout_to_type' => $target]);
if (! is_array($request->get('selected_assets'))) {
return redirect()->route('hardware.bulkcheckout.show')->withInput()->with('error', trans('admin/hardware/message.checkout.no_assets_selected'));
@@ -570,6 +645,8 @@ public function storeCheckout(AssetCheckoutRequest $request) : RedirectResponse
$asset_ids = array_filter($request->get('selected_assets'));
+ $assets = Asset::findOrFail($asset_ids);
+
if (request('checkout_to_type') == 'asset') {
foreach ($asset_ids as $asset_id) {
if ($target->id == $asset_id) {
@@ -579,21 +656,25 @@ public function storeCheckout(AssetCheckoutRequest $request) : RedirectResponse
}
$checkout_at = date('Y-m-d H:i:s');
if (($request->filled('checkout_at')) && ($request->get('checkout_at') != date('Y-m-d'))) {
- $checkout_at = e($request->get('checkout_at'));
+ $checkout_at = $request->get('checkout_at');
}
$expected_checkin = '';
if ($request->filled('expected_checkin')) {
- $expected_checkin = e($request->get('expected_checkin'));
+ $expected_checkin = $request->get('expected_checkin');
}
$errors = [];
- DB::transaction(function () use ($target, $admin, $checkout_at, $expected_checkin, &$errors, $asset_ids, $request) { //NOTE: $errors is passsed by reference!
- foreach ($asset_ids as $asset_id) {
- $asset = Asset::findOrFail($asset_id);
+ DB::transaction(function () use ($target, $admin, $checkout_at, $expected_checkin, &$errors, $assets, $request) { //NOTE: $errors is passsed by reference!
+ foreach ($assets as $asset) {
$this->authorize('checkout', $asset);
+ // See if there is a status label passed
+ if ($request->filled('status_id')) {
+ $asset->status_id = $request->get('status_id');
+ }
+
$checkout_success = $asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e($request->get('note')), $asset->name, null);
//TODO - I think this logic is duplicated in the checkOut method?
@@ -618,7 +699,7 @@ public function storeCheckout(AssetCheckoutRequest $request) : RedirectResponse
// Redirect to the asset management page with error
return redirect()->route('hardware.bulkcheckout.show')->withInput()->with('error', trans_choice('admin/hardware/message.multi-checkout.error', $asset_ids))->withErrors($errors);
} catch (ModelNotFoundException $e) {
- return redirect()->route('hardware.bulkcheckout.show')->with('error', $e->getErrors());
+ return redirect()->route('hardware.bulkcheckout.show')->withInput()->with('error', trans_choice('admin/hardware/message.multi-checkout.error', $request->input('selected_assets')));
}
}
@@ -637,4 +718,54 @@ public function restore(Request $request) : RedirectResponse
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.restore.success'));
}
}
+ public function hasUndeployableStatus (array $asset_ids)
+ {
+ $undeployable = Asset::whereIn('id', $asset_ids)
+ ->undeployable()
+ ->get();
+
+ $undeployableTags = $undeployable->map(function ($asset) {
+ return [
+ 'id' => $asset->id,
+ 'asset_tag' => $asset->asset_tag,
+ ];
+ })->toArray();
+
+ $undeployableIds = array_column($undeployableTags, 'id');
+ $filtered_ids = array_diff($asset_ids, $undeployableIds);
+
+ if($undeployable->isNotEmpty()) {
+ return ['status' => true, 'tags' => $undeployableTags, 'asset_ids' => $filtered_ids];
+ }
+ return false;
+ }
+
+ public function bulkEditForm(): View|RedirectResponse
+ {
+ $this->authorize('update', Asset::class);
+
+ $asset_ids = session()->pull('bulkedit_ids', []);
+
+ if (empty($asset_ids)) {
+ return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.update.no_assets_selected'));
+ }
+
+ $assets = Asset::with('model')->withTrashed()->whereIn('id', $asset_ids)->get();
+
+ if ($assets->isEmpty()) {
+ return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.update.assets_do_not_exist_or_are_invalid'));
+ }
+
+ $models = $assets->unique('model_id');
+ $modelNames = [];
+ foreach ($models as $model) {
+ $modelNames[] = $model->model->name;
+ }
+
+ return view('hardware/bulk')
+ ->with('assets', $asset_ids)
+ ->with('statuslabel_list', Helper::statusLabelList())
+ ->with('models', $models->pluck(['model']))
+ ->with('modelNames', $modelNames);
+ }
}
diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php
index e7b10877cfd1..0b8786545514 100644
--- a/app/Http/Controllers/Auth/LoginController.php
+++ b/app/Http/Controllers/Auth/LoginController.php
@@ -206,6 +206,7 @@ private function loginViaLdap(Request $request): User
$user->password = bcrypt($request->input('password'));
}
+ $user->last_login = \Carbon::now();
$user->email = $ldap_attr['email'];
$user->first_name = $ldap_attr['firstname'];
$user->last_name = $ldap_attr['lastname']; //FIXME (or TODO?) - do we need to map additional fields that we now support? E.g. country, phone, etc.
@@ -226,7 +227,7 @@ private function loginViaRemoteUser(Request $request)
$strip_prefixes = [
// IIS/AD
- // https://github.com/snipe/snipe-it/pull/5862
+ // https://github.com/grokability/snipe-it/pull/5862
'\\',
// Google Cloud IAP
@@ -283,8 +284,11 @@ public function login(Request $request)
return redirect()->back()->withInput()->withErrors($validator);
}
- $this->maxLoginAttempts = config('auth.passwords.users.throttle.max_attempts');
- $this->lockoutTime = config('auth.passwords.users.throttle.lockout_duration');
+ // Set the custom lockout attempts from the env and sett the custom lockout throttle from the env.
+ // We divide decayMinutes by 60 here to get minutes, since Laravel changed the default from minutes
+ // to seconds, and we don't want to break limits on existing systems
+ $this->maxAttempts = config('auth.passwords.users.throttle.max_attempts');
+ $this->decayMinutes = (config('auth.passwords.users.throttle.lockout_duration') / 60);
if ($lockedOut = $this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
@@ -354,7 +358,7 @@ public function getTwoFactorEnroll()
// We wouldn't normally see this page if 2FA isn't enforced via the
// \App\Http\Middleware\CheckForTwoFactor middleware AND if a device isn't enrolled,
- // but let's check check anyway in case there's a browser history or back button thing.
+ // but let's check anyway in case there's a browser history or back button thing.
// While you can access this page directly, enrolling a device when 2FA isn't enforced
// won't cause any harm.
@@ -432,6 +436,7 @@ public function postTwoFactorAuth(Request $request)
if (Google2FA::verifyKey($user->two_factor_secret, $secret)) {
$user->two_factor_enrolled = 1;
+ $user->last_login = \Carbon::now();
$user->saveQuietly();
$request->session()->put('2fa_authed', $user->id);
@@ -479,6 +484,7 @@ public function logout(Request $request)
}
$request->session()->regenerate(true);
+ $request->session()->forget('2fa_authed');
if ($request->session()->has('password_hash_'.Auth::getDefaultDriver())){
$request->session()->remove('password_hash_'.Auth::getDefaultDriver());
@@ -519,45 +525,6 @@ public function username()
return 'username';
}
- /**
- * Redirect the user after determining they are locked out.
- *
- * @param \Illuminate\Http\Request $request
- * @return \Illuminate\Http\RedirectResponse
- */
- protected function sendLockoutResponse(Request $request)
- {
- $seconds = $this->limiter()->availableIn(
- $this->throttleKey($request)
- );
-
- $minutes = round($seconds / 60);
-
- $message = trans('auth/message.throttle', ['minutes' => $minutes]);
-
- return redirect()->back()
- ->withInput($request->only($this->username(), 'remember'))
- ->withErrors([$this->username() => $message]);
- }
-
-
- /**
- * Override the lockout time and duration
- *
- * @param \Illuminate\Http\Request $request
- * @return bool
- */
- protected function hasTooManyLoginAttempts(Request $request)
- {
- $lockoutTime = config('auth.passwords.users.throttle.lockout_duration');
- $maxLoginAttempts = config('auth.passwords.users.throttle.max_attempts');
-
- return $this->limiter()->tooManyAttempts(
- $this->throttleKey($request),
- $maxLoginAttempts,
- $lockoutTime
- );
- }
public function legacyAuthRedirect()
{
diff --git a/app/Http/Controllers/BulkAssetModelsController.php b/app/Http/Controllers/BulkAssetModelsController.php
index 36b21178b03f..c1ecf309fbc7 100644
--- a/app/Http/Controllers/BulkAssetModelsController.php
+++ b/app/Http/Controllers/BulkAssetModelsController.php
@@ -71,20 +71,30 @@ public function update(Request $request): View | RedirectResponse
if (($request->filled('manufacturer_id') && ($request->input('manufacturer_id') != 'NC'))) {
$update_array['manufacturer_id'] = $request->input('manufacturer_id');
}
+
if (($request->filled('category_id') && ($request->input('category_id') != 'NC'))) {
$update_array['category_id'] = $request->input('category_id');
}
+
if ($request->input('fieldset_id') != 'NC') {
$update_array['fieldset_id'] = $request->input('fieldset_id');
}
+
if ($request->input('depreciation_id') != 'NC') {
$update_array['depreciation_id'] = $request->input('depreciation_id');
}
- if ($request->filled('requestable') != '') {
+ if ($request->input('requestable') != '') {
$update_array['requestable'] = $request->input('requestable');
}
+ if ($request->filled('min_amt')) {
+ $update_array['min_amt'] = $request->input('min_amt');
+ }
+
+ if ($request->filled('require_serial')) {
+ $update_array['require_serial'] = $request->input('require_serial');
+ }
if (count($update_array) > 0) {
AssetModel::whereIn('id', $models_raw_array)->update($update_array);
diff --git a/app/Http/Controllers/CategoriesController.php b/app/Http/Controllers/CategoriesController.php
index 1b42037f3bc7..3e902541b330 100755
--- a/app/Http/Controllers/CategoriesController.php
+++ b/app/Http/Controllers/CategoriesController.php
@@ -68,6 +68,7 @@ public function store(ImageUploadRequest $request) : RedirectResponse
$category->eula_text = $request->input('eula_text');
$category->use_default_eula = $request->input('use_default_eula', '0');
$category->require_acceptance = $request->input('require_acceptance', '0');
+ $category->alert_on_response = $request->input('alert_on_response', '0');
$category->checkin_email = $request->input('checkin_email', '0');
$category->notes = $request->input('notes');
$category->created_by = auth()->id();
@@ -88,14 +89,10 @@ public function store(ImageUploadRequest $request) : RedirectResponse
* @param int $categoryId
* @since [v1.0]
*/
- public function edit($categoryId = null) : RedirectResponse | View
+ public function edit(Category $category) : RedirectResponse | View
{
$this->authorize('update', Category::class);
- if (is_null($item = Category::find($categoryId))) {
- return redirect()->route('categories.index')->with('error', trans('admin/categories/message.does_not_exist'));
- }
-
- return view('categories/edit', compact('item'))
+ return view('categories/edit')->with('item', $category)
->with('category_types', Helper::categoryTypeList());
}
@@ -108,19 +105,10 @@ public function edit($categoryId = null) : RedirectResponse | View
* @param int $categoryId
* @since [v1.0]
*/
- public function update(ImageUploadRequest $request, $categoryId = null) : RedirectResponse
+ public function update(ImageUploadRequest $request, Category $category) : RedirectResponse
{
$this->authorize('update', Category::class);
- if (is_null($category = Category::find($categoryId))) {
- // Redirect to the categories management page
- return redirect()->route('categories.index')->with('error', trans('admin/categories/message.does_not_exist'));
- }
-
- // Update the category data
$category->name = $request->input('name');
- // If the item count is > 0, we disable the category type in the edit. Disabled items
- // don't POST, so if the category_type is blank we just set it to the default.
-
// Don't allow the user to change the category_type once it's been created
if (($request->filled('category_type') && ($category->itemCount() > 0))) {
@@ -134,6 +122,7 @@ public function update(ImageUploadRequest $request, $categoryId = null) : Redire
$category->eula_text = $request->input('eula_text');
$category->use_default_eula = $request->input('use_default_eula', '0');
$category->require_acceptance = $request->input('require_acceptance', '0');
+ $category->alert_on_response = $request->input('alert_on_response', '0');
$category->checkin_email = $request->input('checkin_email', '0');
$category->notes = $request->input('notes');
@@ -158,7 +147,7 @@ public function destroy($categoryId) : RedirectResponse
{
$this->authorize('delete', Category::class);
// Check if the category exists
- if (is_null($category = Category::findOrFail($categoryId))) {
+ if (is_null($category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($categoryId))) {
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.not_found'));
}
@@ -168,7 +157,6 @@ public function destroy($categoryId) : RedirectResponse
Storage::disk('public')->delete('categories'.'/'.$category->image);
$category->delete();
- // Redirect to the locations management page
return redirect()->route('categories.index')->with('success', trans('admin/categories/message.delete.success'));
}
@@ -181,10 +169,10 @@ public function destroy($categoryId) : RedirectResponse
* @param $id
* @since [v1.8]
*/
- public function show($id) : View | RedirectResponse
+ public function show(Category $category) : View | RedirectResponse
{
$this->authorize('view', Category::class);
- if ($category = Category::find($id)) {
+
if ($category->category_type == 'asset') {
$category_type = 'hardware';
$category_type_route = 'assets';
@@ -199,8 +187,5 @@ public function show($id) : View | RedirectResponse
return view('categories/view', compact('category'))
->with('category_type', $category_type)
->with('category_type_route', $category_type_route);
- }
-
- return redirect()->route('categories.index')->with('error', trans('admin/categories/message.does_not_exist'));
}
}
diff --git a/app/Http/Controllers/CompaniesController.php b/app/Http/Controllers/CompaniesController.php
index 12655cb2a5e3..db6118d37f3d 100644
--- a/app/Http/Controllers/CompaniesController.php
+++ b/app/Http/Controllers/CompaniesController.php
@@ -80,16 +80,10 @@ public function store(ImageUploadRequest $request) : RedirectResponse
* @since [v1.8]
* @param int $companyId
*/
- public function edit($companyId) : View | RedirectResponse
+ public function edit(Company $company) : View | RedirectResponse
{
- if (is_null($item = Company::find($companyId))) {
- return redirect()->route('companies.index')
- ->with('error', trans('admin/companies/message.does_not_exist'));
- }
-
- $this->authorize('update', $item);
-
- return view('companies/edit')->with('item', $item);
+ $this->authorize('update', $company);
+ return view('companies/edit')->with('item', $company);
}
/**
@@ -100,14 +94,10 @@ public function edit($companyId) : View | RedirectResponse
* @param ImageUploadRequest $request
* @param int $companyId
*/
- public function update(ImageUploadRequest $request, $companyId) : RedirectResponse
+ public function update(ImageUploadRequest $request, Company $company) : RedirectResponse
{
- if (is_null($company = Company::find($companyId))) {
- return redirect()->route('companies.index')->with('error', trans('admin/companies/message.does_not_exist'));
- }
$this->authorize('update', $company);
-
$company->name = $request->input('name');
$company->phone = $request->input('phone');
$company->fax = $request->input('fax');
@@ -133,11 +123,13 @@ public function update(ImageUploadRequest $request, $companyId) : RedirectRespon
*/
public function destroy($companyId) : RedirectResponse
{
+
if (is_null($company = Company::find($companyId))) {
return redirect()->route('companies.index')
->with('error', trans('admin/companies/message.not_found'));
}
+
$this->authorize('delete', $company);
if (! $company->isDeletable()) {
return redirect()->route('companies.index')
@@ -158,15 +150,9 @@ public function destroy($companyId) : RedirectResponse
->with('success', trans('admin/companies/message.delete.success'));
}
- public function show($id) : View | RedirectResponse
+ public function show(Company $company) : View | RedirectResponse
{
$this->authorize('view', Company::class);
-
- if (is_null($company = Company::find($id))) {
- return redirect()->route('companies.index')
- ->with('error', trans('admin/companies/message.not_found'));
- }
-
return view('companies/view')->with('company', $company);
}
}
diff --git a/app/Http/Controllers/Components/ComponentCheckinController.php b/app/Http/Controllers/Components/ComponentCheckinController.php
index 379882c3c5e5..b7845761220a 100644
--- a/app/Http/Controllers/Components/ComponentCheckinController.php
+++ b/app/Http/Controllers/Components/ComponentCheckinController.php
@@ -100,8 +100,8 @@ public function store(Request $request, $component_asset_id, $backto = null)
session()->put(['redirect_option' => $request->get('redirect_option')]);
- return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success',
- trans('admin/components/message.checkin.success'));
+ return Helper::getRedirectOption($request, $component->id, 'Components')
+ ->with('success', trans('admin/components/message.checkin.success'));
}
return redirect()->route('components.index')->with('error', trans('admin/components/message.does_not_exist'));
diff --git a/app/Http/Controllers/Components/ComponentCheckoutController.php b/app/Http/Controllers/Components/ComponentCheckoutController.php
index b40d592369f7..4abf426de33a 100644
--- a/app/Http/Controllers/Components/ComponentCheckoutController.php
+++ b/app/Http/Controllers/Components/ComponentCheckoutController.php
@@ -120,6 +120,7 @@ public function store(Request $request, $componentId)
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
- return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.checkout.success'));
+ return Helper::getRedirectOption($request, $component->id, 'Components')
+ ->with('success', trans('admin/components/message.checkout.success'));
}
}
diff --git a/app/Http/Controllers/Components/ComponentsController.php b/app/Http/Controllers/Components/ComponentsController.php
index 62dc25cf1d32..ff1b0061b4ec 100644
--- a/app/Http/Controllers/Components/ComponentsController.php
+++ b/app/Http/Controllers/Components/ComponentsController.php
@@ -88,10 +88,16 @@ public function store(ImageUploadRequest $request)
$component = $request->handleImages($component);
- session()->put(['redirect_option' => $request->get('redirect_option')]);
+ if($request->get('redirect_option') === 'back'){
+ session()->put(['redirect_option' => 'index']);
+ } else {
+ session()->put(['redirect_option' => $request->get('redirect_option')]);
+ }
+
if ($component->save()) {
- return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.create.success'));
+ return Helper::getRedirectOption($request, $component->id, 'Components')
+ ->with('success', trans('admin/components/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($component->getErrors());
@@ -107,15 +113,14 @@ public function store(ImageUploadRequest $request)
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function edit($componentId = null)
+ public function edit(Component $component)
{
- if ($item = Component::find($componentId)) {
- $this->authorize('update', $item);
- return view('components/edit', compact('item'))->with('category_type', 'component');
- }
-
- return redirect()->route('components.index')->with('error', trans('admin/components/message.does_not_exist'));
+ $this->authorize('update', $component);
+ session()->put('back_url', url()->previous());
+ return view('components/edit')
+ ->with('item', $component)
+ ->with('category_type', 'component');
}
@@ -130,11 +135,8 @@ public function edit($componentId = null)
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v3.0]
*/
- public function update(ImageUploadRequest $request, $componentId = null)
+ public function update(ImageUploadRequest $request, Component $component)
{
- if (is_null($component = Component::find($componentId))) {
- return redirect()->route('components.index')->with('error', trans('admin/components/message.does_not_exist'));
- }
$min = $component->numCheckedOut();
$validator = Validator::make($request->all(), [
'qty' => "required|numeric|min:$min",
@@ -169,7 +171,8 @@ public function update(ImageUploadRequest $request, $componentId = null)
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($component->save()) {
- return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.update.success'));
+ return Helper::getRedirectOption($request, $component->id, 'Components')
+ ->with('success', trans('admin/components/message.update.success'));
}
return redirect()->back()->withInput()->withErrors($component->getErrors());
@@ -201,6 +204,10 @@ public function destroy($componentId)
}
}
+ if ($component->numCheckedOut() > 0) {
+ return redirect()->route('components.index')->with('error', trans('admin/components/message.delete.error_qty'));
+ }
+
$component->delete();
return redirect()->route('components.index')->with('success', trans('admin/components/message.delete.success'));
@@ -216,17 +223,9 @@ public function destroy($componentId)
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function show($componentId = null)
+ public function show(Component $component)
{
- $component = Component::find($componentId);
-
- if (isset($component->id)) {
$this->authorize('view', $component);
-
return view('components/view', compact('component'));
- }
- // Redirect to the user management page
- return redirect()->route('components.index')
- ->with('error', trans('admin/components/message.does_not_exist'));
}
}
diff --git a/app/Http/Controllers/Components/ComponentsFilesController.php b/app/Http/Controllers/Components/ComponentsFilesController.php
deleted file mode 100644
index b5e30aa694fd..000000000000
--- a/app/Http/Controllers/Components/ComponentsFilesController.php
+++ /dev/null
@@ -1,138 +0,0 @@
-]
- * @since [v1.0]
- * @todo Switch to using the AssetFileRequest form request validator.
- */
- public function store(UploadFileRequest $request, $componentId = null)
- {
-
- if (config('app.lock_passwords')) {
- return redirect()->route('components.show', ['component'=>$componentId])->with('error', trans('general.feature_disabled'));
- }
-
- $component = Component::find($componentId);
-
- if (isset($component->id)) {
- $this->authorize('update', $component);
-
- if ($request->hasFile('file')) {
- if (! Storage::exists('private_uploads/components')) {
- Storage::makeDirectory('private_uploads/components', 775);
- }
-
- foreach ($request->file('file') as $file) {
- $file_name = $request->handleFile('private_uploads/components/','component-'.$component->id, $file);
-
- //Log the upload to the log
- $component->logUpload($file_name, e($request->input('notes')));
- }
-
-
- return redirect()->route('components.show', $component->id)->withFragment('files')->with('success', trans('general.file_upload_success'));
-
- }
-
- return redirect()->route('components.show', $component->id)->with('error', trans('general.no_files_uploaded'));
- }
- // Prepare the error message
- return redirect()->route('components.index')
- ->with('error', trans('general.file_does_not_exist'));
- }
-
- /**
- * Deletes the selected component file.
- *
- * @author [A. Gianotto] []
- * @since [v1.0]
- * @param int $componentId
- * @param int $fileId
- * @return \Illuminate\Http\RedirectResponse
- * @throws \Illuminate\Auth\Access\AuthorizationException
- */
- public function destroy($componentId = null, $fileId = null)
- {
- $component = Component::find($componentId);
-
- // the asset is valid
- if (isset($component->id)) {
- $this->authorize('update', $component);
- $log = Actionlog::find($fileId);
-
- // Remove the file if one exists
- if (Storage::exists('components/'.$log->filename)) {
- try {
- Storage::delete('components/'.$log->filename);
- } catch (\Exception $e) {
- Log::debug($e);
- }
- }
-
- $log->delete();
-
- return redirect()->back()->withFragment('files')
- ->with('success', trans('admin/hardware/message.deletefile.success'));
- }
-
- // Redirect to the licence management page
- return redirect()->route('components.index')->with('error', trans('general.file_does_not_exist'));
- }
-
- /**
- * Allows the selected file to be viewed.
- *
- * @author [A. Gianotto] []
- * @since [v1.4]
- * @param int $componentId
- * @param int $fileId
- * @return \Symfony\Component\HttpFoundation\Response
- * @throws \Illuminate\Auth\Access\AuthorizationException
- */
- public function show($componentId = null, $fileId = null)
- {
- Log::debug('Private filesystem is: '.config('filesystems.default'));
-
-
- // the component is valid
- if ($component = Component::find($componentId)) {
- $this->authorize('view', $component);
- $this->authorize('components.files', $component);
-
- if ($log = Actionlog::whereNotNull('filename')->where('item_id', $component->id)->find($fileId)) {
-
- $file = 'private_uploads/components/'.$log->filename;
-
- try {
- return StorageHelper::showOrDownloadFile($file, $log->filename);
- } catch (\Exception $e) {
- return redirect()->route('components.show', ['component' => $component])->with('error', trans('general.file_not_found'));
- }
- }
- return redirect()->route('components.show', ['component' => $component])->with('error', trans('general.log_record_not_found'));
-
- }
-
- return redirect()->route('components.index')->with('error', trans('general.file_does_not_exist', ['id' => $fileId]));
- }
-}
diff --git a/app/Http/Controllers/Consumables/ConsumableCheckoutController.php b/app/Http/Controllers/Consumables/ConsumableCheckoutController.php
index e08da4122972..3e972e10855c 100644
--- a/app/Http/Controllers/Consumables/ConsumableCheckoutController.php
+++ b/app/Http/Controllers/Consumables/ConsumableCheckoutController.php
@@ -111,6 +111,7 @@ public function store(Request $request, $consumableId)
// Redirect to the new consumable page
- return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.checkout.success'));
+ return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
+ ->with('success', trans('admin/consumables/message.checkout.success'));
}
}
diff --git a/app/Http/Controllers/Consumables/ConsumablesController.php b/app/Http/Controllers/Consumables/ConsumablesController.php
index 47a9ad71b43a..2601b69edd3b 100644
--- a/app/Http/Controllers/Consumables/ConsumablesController.php
+++ b/app/Http/Controllers/Consumables/ConsumablesController.php
@@ -7,7 +7,7 @@
use App\Http\Requests\ImageUploadRequest;
use App\Models\Company;
use App\Models\Consumable;
-use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View;
@@ -81,16 +81,33 @@ public function store(StoreConsumableRequest $request)
$consumable->purchase_date = $request->input('purchase_date');
$consumable->purchase_cost = $request->input('purchase_cost');
$consumable->qty = $request->input('qty');
- $consumable->created_by = auth()->id();
+ $consumable->created_by = auth()->id();
$consumable->notes = $request->input('notes');
- $consumable = $request->handleImages($consumable);
+ if ($request->has('use_cloned_image')) {
+ $cloned_model_img = Consumable::select('image')->find($request->input('clone_image_from_id'));
+ if ($cloned_model_img) {
+ $new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
+ $new_image = 'consumables/'.$new_image_name;
+ Storage::disk('public')->copy('consumables/'.$cloned_model_img->image, $new_image);
+ $consumable->image = $new_image_name;
+ }
+
+ } else {
+ $consumable = $request->handleImages($consumable);
+ }
+
+ if($request->get('redirect_option') === 'back'){
+ session()->put(['redirect_option' => 'index']);
+ } else {
+ session()->put(['redirect_option' => $request->get('redirect_option')]);
+ }
- session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($consumable->save()) {
- return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.create.success'));
+ return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
+ ->with('success', trans('admin/consumables/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($consumable->getErrors());
@@ -104,15 +121,14 @@ public function store(StoreConsumableRequest $request)
* @see ConsumablesController::postEdit() method that stores the form data.
* @since [v1.0]
*/
- public function edit($consumableId = null) : View | RedirectResponse
+ public function edit(Consumable $consumable) : View | RedirectResponse
{
- if ($item = Consumable::find($consumableId)) {
- $this->authorize($item);
+ $this->authorize($consumable);
+ session()->put('back_url', url()->previous());
+ return view('consumables/edit')
+ ->with('item', $consumable)
+ ->with('category_type', 'consumable');
- return view('consumables/edit', compact('item'))->with('category_type', 'consumable');
- }
-
- return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.does_not_exist'));
}
/**
@@ -126,11 +142,8 @@ public function edit($consumableId = null) : View | RedirectResponse
* @see ConsumablesController::getEdit() method that stores the form data.
* @since [v1.0]
*/
- public function update(StoreConsumableRequest $request, $consumableId = null)
+ public function update(StoreConsumableRequest $request, Consumable $consumable)
{
- if (is_null($consumable = Consumable::find($consumableId))) {
- return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.does_not_exist'));
- }
$min = $consumable->numCheckedOut();
$validator = Validator::make($request->all(), [
@@ -165,7 +178,8 @@ public function update(StoreConsumableRequest $request, $consumableId = null)
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($consumable->save()) {
- return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.update.success'));
+ return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
+ ->with('success', trans('admin/consumables/message.update.success'));
}
return redirect()->back()->withInput()->withErrors($consumable->getErrors());
@@ -202,16 +216,11 @@ public function destroy($consumableId)
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function show($consumableId = null)
+ public function show(Consumable $consumable)
{
- $consumable = Consumable::withCount('users as users_consumables')->find($consumableId);
+ $consumable = Consumable::withCount('users as users_consumables')->find($consumable->id);
$this->authorize($consumable);
- if (isset($consumable->id)) {
- return view('consumables/view', compact('consumable'));
- }
-
- return redirect()->route('consumables.index')
- ->with('error', trans('admin/consumables/message.does_not_exist'));
+ return view('consumables/view', compact('consumable'));
}
public function clone(Consumable $consumable) : View
@@ -220,9 +229,10 @@ public function clone(Consumable $consumable) : View
$consumable_to_close = $consumable;
$consumable = clone $consumable_to_close;
$consumable->id = null;
- $consumable->image = null;
$consumable->created_by = null;
- return view('consumables/edit')->with('item', $consumable);
+ return view('consumables/edit')
+ ->with('cloned_model', $consumable_to_close)
+ ->with('item', $consumable);
}
}
diff --git a/app/Http/Controllers/Consumables/ConsumablesFilesController.php b/app/Http/Controllers/Consumables/ConsumablesFilesController.php
deleted file mode 100644
index 545b008dc0b1..000000000000
--- a/app/Http/Controllers/Consumables/ConsumablesFilesController.php
+++ /dev/null
@@ -1,134 +0,0 @@
-]
- * @since [v1.0]
- * @todo Switch to using the AssetFileRequest form request validator.
- */
- public function store(UploadFileRequest $request, $consumableId = null)
- {
- if (config('app.lock_passwords')) {
- return redirect()->route('consumables.show', ['consumable'=>$consumableId])->with('error', trans('general.feature_disabled'));
- }
-
- $consumable = Consumable::find($consumableId);
-
- if (isset($consumable->id)) {
- $this->authorize('update', $consumable);
-
- if ($request->hasFile('file')) {
- if (! Storage::exists('private_uploads/consumables')) {
- Storage::makeDirectory('private_uploads/consumables', 775);
- }
-
- foreach ($request->file('file') as $file) {
- $file_name = $request->handleFile('private_uploads/consumables/','consumable-'.$consumable->id, $file);
-
- //Log the upload to the log
- $consumable->logUpload($file_name, e($request->input('notes')));
- }
-
-
- return redirect()->route('consumables.show', $consumable->id)->withFragment('files')->with('success', trans('general.file_upload_success'));
-
- }
-
- return redirect()->route('consumables.show', $consumable->id)->with('error', trans('general.no_files_uploaded'));
- }
- // Prepare the error message
- return redirect()->route('consumables.index')
- ->with('error', trans('general.file_does_not_exist'));
- }
-
- /**
- * Deletes the selected consumable file.
- *
- * @author [A. Gianotto] []
- * @since [v1.0]
- * @param int $consumableId
- * @param int $fileId
- * @return \Illuminate\Http\RedirectResponse
- * @throws \Illuminate\Auth\Access\AuthorizationException
- */
- public function destroy($consumableId = null, $fileId = null)
- {
- $consumable = Consumable::find($consumableId);
-
- // the asset is valid
- if (isset($consumable->id)) {
- $this->authorize('update', $consumable);
- $log = Actionlog::find($fileId);
-
- // Remove the file if one exists
- if (Storage::exists('consumables/'.$log->filename)) {
- try {
- Storage::delete('consumables/'.$log->filename);
- } catch (\Exception $e) {
- Log::debug($e);
- }
- }
-
- $log->delete();
-
- return redirect()->back()->withFragment('files')
- ->with('success', trans('admin/hardware/message.deletefile.success'));
- }
-
- // Redirect to the licence management page
- return redirect()->route('consumables.index')->with('error', trans('general.file_does_not_exist'));
- }
-
- /**
- * Allows the selected file to be viewed.
- *
- * @author [A. Gianotto] []
- * @since [v1.4]
- * @param int $consumableId
- * @param int $fileId
- * @throws \Illuminate\Auth\Access\AuthorizationException
- */
- public function show($consumableId = null, $fileId = null)
- {
- $consumable = Consumable::find($consumableId);
-
- // the consumable is valid
- if (isset($consumable->id)) {
- $this->authorize('view', $consumable);
- $this->authorize('consumables.files', $consumable);
-
- if ($log = Actionlog::whereNotNull('filename')->where('item_id', $consumable->id)->find($fileId)) {
- $file = 'private_uploads/consumables/'.$log->filename;
-
- try {
- return StorageHelper::showOrDownloadFile($file, $log->filename);
- } catch (\Exception $e) {
- return redirect()->route('consumables.show', ['consumable' => $consumable])->with('error', trans('general.file_not_found'));
- }
- }
- // The log record doesn't exist somehow
- return redirect()->route('consumables.show', ['consumable' => $consumable])->with('error', trans('general.log_record_not_found'));
-
- }
-
- return redirect()->route('consumables.index')->with('error', trans('general.file_does_not_exist', ['id' => $fileId]));
- }
-}
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index 74fff19a3733..266769ffb2a9 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -22,6 +22,15 @@
namespace App\Http\Controllers;
+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\Location;
+use App\Models\Maintenance;
+use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
@@ -32,6 +41,45 @@ abstract class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
+ static $map_object_type = [
+ 'accessories' => Accessory::class,
+ 'maintenances' => Maintenance::class,
+ 'assets' => Asset::class,
+ 'components' => Component::class,
+ 'consumables' => Consumable::class,
+ 'hardware' => Asset::class,
+ 'licenses' => License::class,
+ 'locations' => Location::class,
+ 'models' => AssetModel::class,
+ 'users' => User::class,
+ ];
+
+ static $map_storage_path = [
+ 'accessories' => 'private_uploads/accessories/',
+ 'maintenances' => 'private_uploads/maintenances/',
+ 'assets' => 'private_uploads/assets/',
+ 'components' => 'private_uploads/components/',
+ 'consumables' => 'private_uploads/consumables/',
+ 'hardware' => 'private_uploads/assets/',
+ 'licenses' => 'private_uploads/licenses/',
+ 'locations' => 'private_uploads/locations/',
+ 'models' => 'private_uploads/models/',
+ 'users' => 'private_uploads/users/',
+ ];
+
+ static $map_file_prefix= [
+ 'accessories' => 'accessory',
+ 'maintenances' => 'maintenance',
+ 'assets' => 'asset',
+ 'components' => 'component',
+ 'consumables' => 'consumable',
+ 'hardware' => 'asset',
+ 'licenses' => 'license',
+ 'locations' => 'location',
+ 'models' => 'model',
+ 'users' => 'user',
+ ];
+
public function __construct()
{
view()->share('signedIn', Auth::check());
diff --git a/app/Http/Controllers/CustomFieldsController.php b/app/Http/Controllers/CustomFieldsController.php
index 53c30b88be3c..bccdd652324a 100644
--- a/app/Http/Controllers/CustomFieldsController.php
+++ b/app/Http/Controllers/CustomFieldsController.php
@@ -6,11 +6,13 @@
use App\Http\Requests\CustomFieldRequest;
use App\Models\CustomField;
use App\Models\CustomFieldset;
+use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View;
+
/**
* This controller handles all actions related to Custom Asset Fields for
* the Snipe-IT Asset Management application.
@@ -22,18 +24,23 @@
*/
class CustomFieldsController extends Controller
{
+
/**
* Returns a view with a listing of custom fields.
*
* @author [Brady Wetherington] []
* @since [v1.8]
*/
- public function index() : View
+ public function index(Request $request): View
{
$this->authorize('view', CustomField::class);
+ if ($request->input('tab') == 1) {
+ // Users section, make sure to auto-create the first fieldset if so
+ CustomFieldset::firstOrCreate(['type' => Helper::$itemtypes_having_custom_fields[1]], ['name' => 'default']);
+ }
- $fieldsets = CustomFieldset::with('fields', 'models')->get();
- $fields = CustomField::with('fieldset')->get();
+ $fieldsets = CustomFieldset::with('fields')->where("type", Helper::$itemtypes_having_custom_fields[$request->get('tab', 0)])->get(); //cannot eager-load 'customizable' because it's not a relation
+ $fields = CustomField::with('fieldset')->where("type", Helper::$itemtypes_having_custom_fields[$request->get('tab', 0)])->get();
return view('custom_fields.index')->with('custom_fieldsets', $fieldsets)->with('custom_fields', $fields);
}
@@ -62,7 +69,7 @@ public function show() : RedirectResponse
public function create(Request $request) : View
{
$this->authorize('create', CustomField::class);
- $fieldsets = CustomFieldset::get();
+ $fieldsets = CustomFieldset::where('type', Helper::$itemtypes_having_custom_fields[$request->get('tab')])->get();
return view('custom_fields.fields.edit', [
'predefinedFormats' => Helper::predefined_formats(),
@@ -83,29 +90,34 @@ public function store(CustomFieldRequest $request) : RedirectResponse
{
$this->authorize('create', CustomField::class);
- $show_in_email = $request->get("show_in_email", 0);
- $display_in_user_view = $request->get("display_in_user_view", 0);
+ $show_in_email = $request->input("show_in_email", 0);
+ $display_in_user_view = $request->input("display_in_user_view", 0);
// Override the display settings if the field is encrypted
- if ($request->get("field_encrypted") == '1') {
+ if ($request->input("field_encrypted") == '1') {
$show_in_email = '0';
$display_in_user_view = '0';
}
$field = new CustomField([
- "name" => trim($request->get("name")),
- "element" => $request->get("element"),
- "help_text" => $request->get("help_text"),
- "field_values" => $request->get("field_values"),
- "field_encrypted" => $request->get("field_encrypted", 0),
+ "name" => trim($request->input("name")),
+ "element" => $request->input("element"),
+ "help_text" => $request->input("help_text"),
+ "field_values" => $request->input("field_values"),
+ "field_encrypted" => $request->input("field_encrypted", 0),
"show_in_email" => $show_in_email,
- "is_unique" => $request->get("is_unique", 0),
+ "is_unique" => $request->input("is_unique", 0),
"display_in_user_view" => $display_in_user_view,
- "auto_add_to_fieldsets" => $request->get("auto_add_to_fieldsets", 0),
- "show_in_listview" => $request->get("show_in_listview", 0),
- "show_in_requestable_list" => $request->get("show_in_requestable_list", 0),
+ "auto_add_to_fieldsets" => $request->input("auto_add_to_fieldsets", 0),
+ "show_in_listview" => $request->input("show_in_listview", 0),
+ "show_in_requestable_list" => $request->input("show_in_requestable_list", 0),
+ "display_checkin" => $request->input("display_checkin", 0),
+ "display_checkout" => $request->input("display_checkout", 0),
+ "display_audit" => $request->input("display_audit", 0),
"created_by" => auth()->id()
]);
+ // not mass-assignable; must be manual
+ $field->type = Helper::$itemtypes_having_custom_fields[$request->get('tab')];
if ($request->filled('custom_format')) {
@@ -118,14 +130,17 @@ public function store(CustomFieldRequest $request) : RedirectResponse
// Sync fields with fieldsets
$fieldset_array = $request->input('associate_fieldsets');
- if ($request->has('associate_fieldsets') && (is_array($fieldset_array))) {
+ if ($request->get('tab') == 1) {
+ $fieldset_array = [CustomFieldset::firstOrCreate(['type' => User::class], ['name' => 'default'])->id => true];
+ }
+ if (($request->has('associate_fieldsets') || $request->get('tab') == 1) && (is_array($fieldset_array))) {
$field->fieldset()->sync(array_keys($fieldset_array));
} else {
$field->fieldset()->sync([]);
}
- return redirect()->route('fields.index')->with('success', trans('admin/custom_fields/message.field.create.success'));
+ return redirect()->route('fields.index', ['tab' => $request->get('tab', 0)])->with('success', trans('admin/custom_fields/message.field.create.success'));
}
return redirect()->back()->with('selected_fieldsets', $request->input('associate_fieldsets'))->withInput()
@@ -141,10 +156,9 @@ public function store(CustomFieldRequest $request) : RedirectResponse
*/
public function deleteFieldFromFieldset($field_id, $fieldset_id) : RedirectResponse
{
+ $this->authorize('update', CustomField::class);
$field = CustomField::find($field_id);
- $this->authorize('update', $field);
-
// Check that the field exists - this is mostly related to the demo, where we
// rewrite the data every x minutes, so it's possible someone might be disassociating
// a field from a fieldset just as we're wiping the database
@@ -154,11 +168,12 @@ public function deleteFieldFromFieldset($field_id, $fieldset_id) : RedirectRespo
return redirect()->route('fieldsets.show', ['fieldset' => $fieldset_id])
->with('success', trans('admin/custom_fields/message.field.delete.success'));
} else {
- return redirect()->back()->withErrors(['message' => "Field is in use and cannot be deleted."]);
+ return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.error'))
+ ->withInput();
}
}
- return redirect()->back()->withErrors(['message' => "Error deleting field from fieldset"]);
+ return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.error'));
}
@@ -169,20 +184,20 @@ public function deleteFieldFromFieldset($field_id, $fieldset_id) : RedirectRespo
* @author [Brady Wetherington] []
* @since [v1.8]
*/
- public function destroy($field_id) : RedirectResponse
+ public function destroy(CustomField $field): RedirectResponse
{
- if ($field = CustomField::find($field_id)) {
- $this->authorize('delete', $field);
+ $this->authorize('delete', CustomField::class);
- if (($field->fieldset) && ($field->fieldset->count() > 0)) {
- return redirect()->back()->withErrors(['message' => 'Field is in-use']);
- }
- $field->delete();
- return redirect()->route("fields.index")
- ->with("success", trans('admin/custom_fields/message.field.delete.success'));
+ if ($field->type == User::class) {
+ $field->fieldset()->detach(); // remove from 'default' group (and others, if they exist in the future!)
}
-
- return redirect()->back()->withErrors(['message' => 'Field does not exist']);
+ if (($field->fieldset) && ($field->fieldset->count() > 0)) {
+ return redirect()->back()->withErrors(['message' => trans('admin/custom_fields/message.field.delete.in_use')]);
+ }
+ $type = $field->type;
+ $field->delete();
+ return redirect()->route('fields.index', ['tab' => array_search($type, Helper::$itemtypes_having_custom_fields)])
+ ->with('success', trans('admin/custom_fields/message.field.delete.success'));
}
@@ -193,11 +208,9 @@ public function destroy($field_id) : RedirectResponse
* @param int $id
* @since [v4.0]
*/
- public function edit(Request $request, $id) : View | RedirectResponse
+ public function edit(Request $request, CustomField $field): View|RedirectResponse
{
- if ($field = CustomField::find($id)) {
-
- $this->authorize('update', $field);
+ $this->authorize('update', CustomField::class);
$fieldsets = CustomFieldset::get();
$customFormat = '';
if ((stripos($field->format, 'regex') === 0) && ($field->format !== CustomField::PREDEFINED_FORMATS['MAC'])) {
@@ -210,11 +223,7 @@ public function edit(Request $request, $id) : View | RedirectResponse
'fieldsets' => $fieldsets,
'predefinedFormats' => Helper::predefined_formats(),
]);
- }
- return redirect()->route("fields.index")
- ->with("error", trans('admin/custom_fields/message.field.invalid'));
-
}
@@ -229,13 +238,9 @@ public function edit(Request $request, $id) : View | RedirectResponse
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function update(CustomFieldRequest $request, $id) : RedirectResponse
+ public function update(CustomFieldRequest $request, CustomField $field): RedirectResponse
{
- $field = CustomField::find($id);
-
- $this->authorize('update', $field);
-
-
+ $this->authorize('update', CustomField::class);
$show_in_email = $request->get("show_in_email", 0);
$display_in_user_view = $request->get("display_in_user_view", 0);
@@ -244,9 +249,9 @@ public function update(CustomFieldRequest $request, $id) : RedirectResponse
$show_in_email = '0';
$display_in_user_view = '0';
}
-
- $field->name = trim(e($request->get("name")));
- $field->element = e($request->get("element"));
+
+ $field->name = trim($request->get("name"));
+ $field->element = $request->get("element");
$field->field_values = $request->get("field_values");
$field->created_by = auth()->id();
$field->help_text = $request->get("help_text");
@@ -256,11 +261,14 @@ public function update(CustomFieldRequest $request, $id) : RedirectResponse
$field->auto_add_to_fieldsets = $request->get("auto_add_to_fieldsets", 0);
$field->show_in_listview = $request->get("show_in_listview", 0);
$field->show_in_requestable_list = $request->get("show_in_requestable_list", 0);
+ $field->display_checkin = $request->get("display_checkin", 0);
+ $field->display_checkout = $request->get("display_checkout", 0);
+ $field->display_audit = $request->get("display_audit", 0);
if ($request->get('format') == 'CUSTOM REGEX') {
- $field->format = e($request->get('custom_format'));
+ $field->format = $request->get('custom_format');
} else {
- $field->format = e($request->get('format'));
+ $field->format = $request->get('format');
}
if ($field->element == 'checkbox' || $field->element == 'radio'){
@@ -269,7 +277,6 @@ public function update(CustomFieldRequest $request, $id) : RedirectResponse
if ($field->save()) {
-
// Sync fields with fieldsets
$fieldset_array = $request->input('associate_fieldsets');
if ($request->has('associate_fieldsets') && (is_array($fieldset_array))) {
@@ -278,7 +285,7 @@ public function update(CustomFieldRequest $request, $id) : RedirectResponse
$field->fieldset()->sync([]);
}
- return redirect()->route('fields.index')->with('success', trans('admin/custom_fields/message.field.update.success'));
+ return redirect()->route('fields.index', ['tab' => $request->get('tab', 0)])->with('success', trans('admin/custom_fields/message.field.update.success'));
}
return redirect()->back()->withInput()->with('error', trans('admin/custom_fields/message.field.update.error'));
diff --git a/app/Http/Controllers/CustomFieldsetsController.php b/app/Http/Controllers/CustomFieldsetsController.php
index 1d887db29a8f..072702ee01d0 100644
--- a/app/Http/Controllers/CustomFieldsetsController.php
+++ b/app/Http/Controllers/CustomFieldsetsController.php
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
+use App\Helpers\Helper;
use App\Models\AssetModel;
use App\Models\CustomField;
use App\Models\CustomFieldset;
@@ -35,15 +36,17 @@ public function index() : RedirectResponse
* @param int $id
* @since [v1.8]
*/
- public function show($id) : View | RedirectResponse
+ public function show(CustomFieldset $fieldset) : View | RedirectResponse
{
$cfset = CustomFieldset::with('fields')
- ->where('id', '=', $id)->orderBy('id', 'ASC')->first();
+ ->where('id', '=', $fieldset->id)
+ ->orderBy('id', 'ASC')
+ ->first();
$this->authorize('view', $cfset);
if ($cfset) {
- $custom_fields_list = ['' => 'Add New Field to Fieldset'] + CustomField::pluck('name', 'id')->toArray();
+ $custom_fields_list = ['' => 'Add New Field to Fieldset'] + CustomField::where('type', $cfset->type)->pluck('name', 'id')->toArray();
$maxid = 0;
foreach ($cfset->fields as $field) {
@@ -91,6 +94,8 @@ public function store(Request $request) : RedirectResponse
$fieldset = new CustomFieldset([
'name' => $request->get('name'),
'created_by' => auth()->id(),
+ 'type' => Helper::$itemtypes_having_custom_fields[$request->get('tab')]
+ // 'sub' =>
]);
$validator = Validator::make($request->all(), $fieldset->rules);
@@ -122,16 +127,10 @@ public function store(Request $request) : RedirectResponse
* @param int $id
* @since [v6.0.14]
*/
- public function edit($id) : View | RedirectResponse
+ public function edit(CustomFieldset $fieldset) : View | RedirectResponse
{
$this->authorize('create', CustomField::class);
-
- if ($fieldset = CustomFieldset::find($id)) {
- return view('custom_fields.fieldsets.edit')->with('item', $fieldset);
- }
-
- return redirect()->route('fields.index')->with('error', trans('admin/custom_fields/general.fieldset_does_not_exist', ['id' => $id]));
-
+ return view('custom_fields.fieldsets.edit')->with('item', $fieldset);
}
/**
@@ -141,23 +140,18 @@ public function edit($id) : View | RedirectResponse
* @param int $id
* @since [v6.0.14]
*/
- public function update(Request $request, $id) : RedirectResponse
+ public function update(Request $request, CustomFieldset $fieldset) : RedirectResponse
{
$this->authorize('create', CustomField::class);
- if ($fieldset = CustomFieldset::find($id)) {
-
- $fieldset->name = $request->input('name');
-
- if ($fieldset->save()) {
- return redirect()->route('fields.index')->with('success', trans('admin/custom_fields/general.fieldset_updated'));
- }
-
- return redirect()->back()->withInput()->withErrors($fieldset->getErrors());
+ $fieldset->name = $request->input('name');
+ if ($fieldset->save()) {
+ return redirect()->route('fields.index')->with('success', trans('admin/custom_fields/general.fieldset_updated'));
}
- return redirect()->route('fields.index')->with('error', trans('admin/custom_fields/general.fieldset_does_not_exist', ['id' => $id]));
+ return redirect()->back()->withInput()->withErrors($fieldset->getErrors());
+
}
/**
diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php
index af9c7ee4464e..a2ccb62475ac 100755
--- a/app/Http/Controllers/DashboardController.php
+++ b/app/Http/Controllers/DashboardController.php
@@ -40,7 +40,7 @@ public function index() : View | RedirectResponse
if ((! file_exists(storage_path().'/oauth-private.key')) || (! file_exists(storage_path().'/oauth-public.key'))) {
Artisan::call('migrate', ['--force' => true]);
- \Artisan::call('passport:install');
+ Artisan::call('passport:install', ['--no-interaction' => true]);
}
return view('dashboard')->with('asset_stats', $asset_stats)->with('counts', $counts);
diff --git a/app/Http/Controllers/DepartmentsController.php b/app/Http/Controllers/DepartmentsController.php
index fe90162c4861..f49242127eca 100644
--- a/app/Http/Controllers/DepartmentsController.php
+++ b/app/Http/Controllers/DepartmentsController.php
@@ -73,17 +73,10 @@ public function store(ImageUploadRequest $request) : RedirectResponse
* @param int $id
* @since [v4.0]
*/
- public function show($id) : View | RedirectResponse
+ public function show(Department $department) : View | RedirectResponse
{
- $department = Department::find($id);
-
$this->authorize('view', $department);
-
- if (isset($department->id)) {
- return view('departments/view', compact('department'));
- }
-
- return redirect()->route('departments.index')->with('error', trans('admin/departments/message.does_not_exist'));
+ return view('departments/view', compact('department'));
}
/**
@@ -139,15 +132,10 @@ public function destroy($id) : RedirectResponse
* @param int $departmentId
* @since [v1.0]
*/
- public function edit($departmentId = null) : View | RedirectResponse
+ public function edit(Department $department) : View | RedirectResponse
{
- if (is_null($item = Department::find($departmentId))) {
- return redirect()->back()->with('error', trans('admin/locations/message.does_not_exist'));
- }
-
- $this->authorize('update', $item);
-
- return view('departments/edit', compact('item'));
+ $this->authorize('update', $department);
+ return view('departments/edit')->with('item', $department);
}
/**
@@ -158,11 +146,8 @@ public function edit($departmentId = null) : View | RedirectResponse
* @param int $departmentId
* @since [v1.0]
*/
- public function update(ImageUploadRequest $request, $id) : RedirectResponse
+ public function update(ImageUploadRequest $request, Department $department) : RedirectResponse
{
- if (is_null($department = Department::find($id))) {
- return redirect()->route('departments.index')->with('error', trans('admin/departments/message.does_not_exist'));
- }
$this->authorize('update', $department);
diff --git a/app/Http/Controllers/DepreciationsController.php b/app/Http/Controllers/DepreciationsController.php
index 5f4a5ca10d61..2c98e240b9de 100755
--- a/app/Http/Controllers/DepreciationsController.php
+++ b/app/Http/Controllers/DepreciationsController.php
@@ -95,17 +95,11 @@ function ($attribute, $value, $fail) use ($request) {
* @param int $depreciationId
* @since [v1.0]
*/
- public function edit($depreciationId = null) : RedirectResponse | View
+ public function edit(Depreciation $depreciation) : RedirectResponse | View
{
- // Check if the depreciation exists
- if (is_null($item = Depreciation::find($depreciationId))) {
- // Redirect to the blogs management page
- return redirect()->route('depreciations.index')->with('error', trans('admin/depreciations/message.does_not_exist'));
- }
- $this->authorize('update', $item);
-
- return view('depreciations/edit', compact('item'));
+ $this->authorize('update', $depreciation);
+ return view('depreciations/edit')->with('item', $depreciation);
}
/**
@@ -117,17 +111,10 @@ public function edit($depreciationId = null) : RedirectResponse | View
* @param int $depreciationId
* @since [v1.0]
*/
- public function update(Request $request, $depreciationId = null) : RedirectResponse
+ public function update(Request $request, Depreciation $depreciation) : RedirectResponse
{
- // Check if the depreciation exists
- if (is_null($depreciation = Depreciation::find($depreciationId))) {
- // Redirect to the blogs management page
- return redirect()->route('depreciations.index')->with('error', trans('admin/depreciations/message.does_not_exist'));
- }
$this->authorize('update', $depreciation);
-
- // Depreciation data
$depreciation->name = $request->input('name');
$depreciation->months = $request->input('months');
@@ -191,12 +178,12 @@ public function destroy($depreciationId) : RedirectResponse
* @param int $depreciationId
* @since [v1.0]
*/
- public function show($id) : View | RedirectResponse
+ public function show(Depreciation $depreciation) : View | RedirectResponse
{
$depreciation = Depreciation::withCount('assets as assets_count')
->withCount('models as models_count')
->withCount('licenses as licenses_count')
- ->find($id);
+ ->find($depreciation->id);
$this->authorize('view', $depreciation);
diff --git a/app/Http/Controllers/GroupsController.php b/app/Http/Controllers/GroupsController.php
index 6aaf58ebde22..577030390b67 100755
--- a/app/Http/Controllers/GroupsController.php
+++ b/app/Http/Controllers/GroupsController.php
@@ -79,19 +79,16 @@ public function store(Request $request) : RedirectResponse
* @param int $id
* @since [v1.0]
*/
- public function edit($id) : View | RedirectResponse
+ public function edit(Group $group) : View | RedirectResponse
{
- $group = Group::find($id);
-
- if ($group) {
- $permissions = config('permissions');
- $groupPermissions = $group->decodePermissions();
- $selected_array = Helper::selectedPermissionsArray($permissions, $groupPermissions);
+ $permissions = config('permissions');
+ $groupPermissions = $group->decodePermissions();
- return view('groups.edit', compact('group', 'permissions', 'selected_array', 'groupPermissions'));
+ if ((!is_array($groupPermissions)) || (!$groupPermissions)) {
+ $groupPermissions = [];
}
-
- return redirect()->route('groups.index')->with('error', trans('admin/groups/message.group_not_found', ['id' => $id]));
+ $selected_array = Helper::selectedPermissionsArray($permissions, $groupPermissions);
+ return view('groups.edit', compact('group', 'permissions', 'selected_array', 'groupPermissions'));
}
/**
@@ -102,11 +99,8 @@ public function edit($id) : View | RedirectResponse
* @param int $id
* @since [v1.0]
*/
- public function update(Request $request, $id = null) : RedirectResponse
+ public function update(Request $request, Group $group) : RedirectResponse
{
- if (! $group = Group::find($id)) {
- return redirect()->route('groups.index')->with('error', trans('admin/groups/message.group_not_found', ['id' => $id]));
- }
$group->name = $request->input('name');
$group->permissions = json_encode($request->input('permission'));
$group->notes = $request->input('notes');
@@ -151,14 +145,8 @@ public function destroy($id) : RedirectResponse
* @param $id
* @since [v4.0.11]
*/
- public function show($id) : View | RedirectResponse
+ public function show(Group $group) : View | RedirectResponse
{
- $group = Group::find($id);
-
- if ($group) {
- return view('groups/view', compact('group'));
- }
-
- return redirect()->route('groups.index')->with('error', trans('admin/groups/message.group_not_found', ['id' => $id]));
+ return view('groups/view', compact('group'));
}
}
diff --git a/app/Http/Controllers/Kits/CheckoutKitController.php b/app/Http/Controllers/Kits/CheckoutKitController.php
index bf4f64a8d648..f34dbdc2c08b 100644
--- a/app/Http/Controllers/Kits/CheckoutKitController.php
+++ b/app/Http/Controllers/Kits/CheckoutKitController.php
@@ -4,10 +4,8 @@
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller;
-use App\Models\PredefinedKit;
use App\Models\Asset;
-use App\Models\PredefinedLicence;
-use App\Models\PredefinedModel;
+use App\Models\PredefinedKit;
use App\Models\User;
use App\Services\PredefinedKitCheckoutService;
use Illuminate\Http\Request;
@@ -35,12 +33,9 @@ public function __construct(PredefinedKitCheckoutService $kitService)
* @author [D. Minaev.] []
* @return \Illuminate\Contracts\View\View View to checkout
*/
- public function showCheckout($kit_id)
+ public function showCheckout(PredefinedKit $kit)
{
$this->authorize('checkout', Asset::class);
-
- $kit = PredefinedKit::findOrFail($kit_id);
-
return view('kits/checkout')->with('kit', $kit);
}
diff --git a/app/Http/Controllers/Kits/PredefinedKitsController.php b/app/Http/Controllers/Kits/PredefinedKitsController.php
index 54f7514510ab..96e4ec4b0a09 100644
--- a/app/Http/Controllers/Kits/PredefinedKitsController.php
+++ b/app/Http/Controllers/Kits/PredefinedKitsController.php
@@ -76,17 +76,15 @@ public function store(ImageUploadRequest $request)
* @param int $kit_id
* @return \Illuminate\Contracts\View\View
*/
- public function edit($kit_id = null)
+ public function edit(PredefinedKit $kit)
{
$this->authorize('update', PredefinedKit::class);
- if ($kit = PredefinedKit::find($kit_id)) {
+
return view('kits/edit')
->with('item', $kit)
->with('models', $kit->models)
->with('licenses', $kit->licenses);
- }
- return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
/**
@@ -98,15 +96,9 @@ public function edit($kit_id = null)
* @param int $kit_id
* @return \Illuminate\Http\RedirectResponse
*/
- public function update(ImageUploadRequest $request, $kit_id = null)
+ public function update(ImageUploadRequest $request, PredefinedKit $kit)
{
$this->authorize('update', PredefinedKit::class);
- // Check if the kit exists
- if (is_null($kit = PredefinedKit::find($kit_id))) {
- // Redirect to the kits management page
- return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
- }
-
$kit->name = $request->input('name');
if ($kit->save()) {
@@ -153,9 +145,9 @@ public function destroy($kit_id)
* @param int $modelId
* @return \Illuminate\Contracts\View\View
*/
- public function show($kit_id = null)
+ public function show(PredefinedKit $kit)
{
- return $this->edit($kit_id);
+ return $this->edit($kit);
}
/**
diff --git a/app/Http/Controllers/LabelsController.php b/app/Http/Controllers/LabelsController.php
index 8e6ba5e2cd7b..ce5bf7d24990 100755
--- a/app/Http/Controllers/LabelsController.php
+++ b/app/Http/Controllers/LabelsController.php
@@ -38,6 +38,7 @@ public function show(string $labelName)
$exampleAsset->order_number = '12345';
$exampleAsset->purchase_date = '2023-01-01';
$exampleAsset->status_id = 1;
+ $exampleAsset->location_id = 1;
$exampleAsset->company = new Company([
'name' => trans('admin/labels/table.example_company'),
diff --git a/app/Http/Controllers/Licenses/LicenseCheckinController.php b/app/Http/Controllers/Licenses/LicenseCheckinController.php
index 373a167642d0..2bb2d5e68e62 100644
--- a/app/Http/Controllers/Licenses/LicenseCheckinController.php
+++ b/app/Http/Controllers/Licenses/LicenseCheckinController.php
@@ -28,16 +28,11 @@ class LicenseCheckinController extends Controller
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function create($seatId = null, $backTo = null)
+ public function create(LicenseSeat $licenseSeat, $backTo = null)
{
// Check if the asset exists
- if (is_null($licenseSeat = LicenseSeat::find($seatId)) || is_null($license = License::find($licenseSeat->license_id))) {
- // Redirect to the asset management page with error
- return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.not_found'));
- }
-
+ $license = License::find($licenseSeat->license_id);
$this->authorize('checkout', $license);
-
return view('licenses/checkin', compact('licenseSeat'))->with('backto', $backTo);
}
@@ -69,12 +64,7 @@ public function store(Request $request, $seatId = null, $backTo = null)
$this->authorize('checkout', $license);
- if (! $license->reassignable) {
- // Not allowed to checkin
- Session::flash('error', trans('admin/licenses/message.checkin.not_reassignable') . '.');
- return redirect()->back()->withInput();
- }
// Declare the rules for the form validation
$rules = [
@@ -91,7 +81,10 @@ public function store(Request $request, $seatId = null, $backTo = null)
}
if($licenseSeat->assigned_to != null){
- $return_to = User::find($licenseSeat->assigned_to);
+ $return_to = User::withTrashed()->find($licenseSeat->assigned_to);
+ if ($return_to) {
+ session()->put('checkedInFrom', $return_to->id);
+ }
} else {
$return_to = Asset::find($licenseSeat->asset_id);
}
@@ -100,16 +93,22 @@ public function store(Request $request, $seatId = null, $backTo = null)
$licenseSeat->assigned_to = null;
$licenseSeat->asset_id = null;
$licenseSeat->notes = $request->input('notes');
+ if (! $licenseSeat->license->reassignable) {
+ $licenseSeat->unreassignable_seat = true;
+ }
session()->put(['redirect_option' => $request->get('redirect_option')]);
-
+ if ($request->get('redirect_option') === 'target'){
+ session()->put(['checkout_to_type' => 'user']);
+ }
// Was the asset updated?
if ($licenseSeat->save()) {
- event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $request->input('notes')));
+ event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $licenseSeat->notes));
- return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.checkin.success'));
+ return Helper::getRedirectOption($request, $license->id, 'Licenses')
+ ->with('success', trans('admin/licenses/message.checkin.success'));
}
// Redirect to the license page with error
@@ -131,21 +130,17 @@ public function bulkCheckin(Request $request, $licenseId) {
$license = License::findOrFail($licenseId);
$this->authorize('checkin', $license);
- if (! $license->reassignable) {
- // Not allowed to checkin
- Session::flash('error', 'License not reassignable.');
-
- return redirect()->back()->withInput();
- }
-
$licenseSeatsByUser = LicenseSeat::where('license_id', '=', $licenseId)
->whereNotNull('assigned_to')
- ->with('user')
+ ->with('user', 'license')
->get();
+ $license = $licenseSeatsByUser->first()?->license;
foreach ($licenseSeatsByUser as $user_seat) {
$user_seat->assigned_to = null;
-
+ if ($license && ! $license->reassignable) {
+ $user_seat->unreassignable_seat = true;
+ }
if ($user_seat->save()) {
Log::debug('Checking in '.$license->name.' from user '.$user_seat->username);
$user_seat->logCheckin($user_seat->user, trans('admin/licenses/general.bulk.checkin_all.log_msg'));
@@ -158,9 +153,12 @@ public function bulkCheckin(Request $request, $licenseId) {
->get();
$count = 0;
+ $license = $licenseSeatsByAsset->first()?->license;
foreach ($licenseSeatsByAsset as $asset_seat) {
$asset_seat->asset_id = null;
-
+ if ($license && ! $license->reassignable) {
+ $asset_seat->unreassignable_seat = true;
+ }
if ($asset_seat->save()) {
Log::debug('Checking in '.$license->name.' from asset '.$asset_seat->asset_tag);
$asset_seat->logCheckin($asset_seat->asset, trans('admin/licenses/general.bulk.checkin_all.log_msg'));
diff --git a/app/Http/Controllers/Licenses/LicenseCheckoutController.php b/app/Http/Controllers/Licenses/LicenseCheckoutController.php
index 0f31db144961..5b2d344ba598 100644
--- a/app/Http/Controllers/Licenses/LicenseCheckoutController.php
+++ b/app/Http/Controllers/Licenses/LicenseCheckoutController.php
@@ -28,33 +28,29 @@ class LicenseCheckoutController extends Controller
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function create($id)
+ public function create(License $license)
{
+ $this->authorize('checkout', $license);
- if ($license = License::find($id)) {
-
- $this->authorize('checkout', $license);
-
- if ($license->category) {
-
- // Make sure there is at least one available to checkout
- if ($license->availCount()->count() < 1){
- return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats'));
- }
+ if ($license->category) {
- // Return the checkout view
- return view('licenses/checkout', compact('license'));
+ // Make sure there is at least one available to checkout
+ if ($license->availCount()->count() < 1) {
+ return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats'));
}
- // Invalid category
- return redirect()->route('licenses.edit', ['license' => $license->id])
- ->with('error', trans('general.invalid_item_category_single', ['type' => trans('general.license')]));
+ // We don't currently allow checking out licenses to locations, so we'll reset that to user if needed
+ if (session()->get('checkout_to_type') == 'location') {
+ session()->put(['checkout_to_type' => 'user']);
+ }
+ // Return the checkout view
+ return view('licenses/checkout', compact('license'));
}
- // Not found
- return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.not_found'));
-
+ // Invalid category
+ return redirect()->route('licenses.edit', ['license' => $license->id])
+ ->with('error', trans('general.invalid_item_category_single', ['type' => trans('general.license')]));
}
@@ -79,17 +75,15 @@ public function store(LicenseCheckoutRequest $request, $licenseId, $seatId = nul
$licenseSeat = $this->findLicenseSeatToCheckout($license, $seatId);
$licenseSeat->created_by = auth()->id();
$licenseSeat->notes = $request->input('notes');
-
-
- $checkoutMethod = 'checkoutTo'.ucwords(request('checkout_to_type'));
if ($request->filled('asset_id')) {
-
+ session()->put(['checkout_to_type' => 'asset']);
$checkoutTarget = $this->checkoutToAsset($licenseSeat);
$request->request->add(['assigned_asset' => $checkoutTarget->id]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => 'asset']);
} elseif ($request->filled('assigned_to')) {
+ session()->put(['checkout_to_type' => 'user']);
$checkoutTarget = $this->checkoutToUser($licenseSeat);
$request->request->add(['assigned_user' => $checkoutTarget->id]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => 'user']);
@@ -98,7 +92,9 @@ public function store(LicenseCheckoutRequest $request, $licenseId, $seatId = nul
if ($checkoutTarget) {
- return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.checkout.success'));
+
+ return Helper::getRedirectOption($request, $license->id, 'Licenses')
+ ->with('success', trans('admin/licenses/message.checkout.success'));
}
diff --git a/app/Http/Controllers/Licenses/LicenseFilesController.php b/app/Http/Controllers/Licenses/LicenseFilesController.php
deleted file mode 100644
index 6ab3cb7703aa..000000000000
--- a/app/Http/Controllers/Licenses/LicenseFilesController.php
+++ /dev/null
@@ -1,132 +0,0 @@
-]
- * @since [v1.0]
- * @todo Switch to using the AssetFileRequest form request validator.
- */
- public function store(UploadFileRequest $request, $licenseId = null)
- {
- $license = License::find($licenseId);
-
- if (isset($license->id)) {
- $this->authorize('update', $license);
-
- if ($request->hasFile('file')) {
- if (! Storage::exists('private_uploads/licenses')) {
- Storage::makeDirectory('private_uploads/licenses', 775);
- }
-
- foreach ($request->file('file') as $file) {
- $file_name = $request->handleFile('private_uploads/licenses/','license-'.$license->id, $file);
-
- //Log the upload to the log
- $license->logUpload($file_name, e($request->input('notes')));
- }
-
-
- return redirect()->route('licenses.show', $license->id)->with('success', trans('admin/licenses/message.upload.success'));
-
- }
-
- return redirect()->route('licenses.show', $license->id)->with('error', trans('admin/licenses/message.upload.nofiles'));
- }
- // Prepare the error message
- return redirect()->route('licenses.index')
- ->with('error', trans('admin/licenses/message.does_not_exist'));
- }
-
- /**
- * Deletes the selected license file.
- *
- * @author [A. Gianotto] []
- * @since [v1.0]
- * @param int $licenseId
- * @param int $fileId
- * @return \Illuminate\Http\RedirectResponse
- * @throws \Illuminate\Auth\Access\AuthorizationException
- */
- public function destroy($licenseId = null, $fileId = null)
- {
- if ($license = License::find($licenseId)) {
-
- $this->authorize('update', $license);
-
- if ($log = Actionlog::find($fileId)) {
-
- // Remove the file if one exists
- if (Storage::exists('licenses/'.$log->filename)) {
- try {
- Storage::delete('licenses/'.$log->filename);
- } catch (\Exception $e) {
- Log::debug($e);
- }
- }
-
- $log->delete();
-
- return redirect()->back()
- ->with('success', trans('admin/hardware/message.deletefile.success'));
- }
-
- return redirect()->route('licenses.index')->with('error', trans('general.log_does_not_exist'));
- }
-
- return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.does_not_exist'));
- }
-
- /**
- * Allows the selected file to be viewed.
- *
- * @author [A. Gianotto] []
- * @since [v1.4]
- * @param int $licenseId
- * @param int $fileId
- * @return \Symfony\Component\HttpFoundation\Response
- * @throws \Illuminate\Auth\Access\AuthorizationException
- */
- public function show($licenseId = null, $fileId = null, $download = true)
- {
- $license = License::find($licenseId);
-
- // the license is valid
- if (isset($license->id)) {
- $this->authorize('view', $license);
- $this->authorize('licenses.files', $license);
-
- if ($log = Actionlog::whereNotNull('filename')->where('item_id', $license->id)->find($fileId)) {
- $file = 'private_uploads/licenses/'.$log->filename;
-
- try {
- return StorageHelper::showOrDownloadFile($file, $log->filename);
- } catch (\Exception $e) {
- return redirect()->route('licenses.show', ['licenses' => $license])->with('error', trans('general.file_not_found'));
- }
- }
-
- // The log record doesn't exist somehow
- return redirect()->route('licenses.show', ['licenses' => $license])->with('error', trans('general.log_record_not_found'));
-
- }
-
- return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.does_not_exist', ['id' => $fileId]));
- }
-}
diff --git a/app/Http/Controllers/Licenses/LicensesController.php b/app/Http/Controllers/Licenses/LicensesController.php
index 6098423ba3e6..b1728469b4be 100755
--- a/app/Http/Controllers/Licenses/LicensesController.php
+++ b/app/Http/Controllers/Licenses/LicensesController.php
@@ -102,10 +102,15 @@ public function store(Request $request)
$license->created_by = auth()->id();
$license->min_amt = $request->input('min_amt');
- session()->put(['redirect_option' => $request->get('redirect_option')]);
+ if($request->get('redirect_option') === 'back'){
+ session()->put(['redirect_option' => 'index']);
+ } else {
+ session()->put(['redirect_option' => $request->get('redirect_option')]);
+ }
if ($license->save()) {
- return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.create.success'));
+ return Helper::getRedirectOption($request, $license->id, 'Licenses')
+ ->with('success', trans('admin/licenses/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($license->getErrors());
@@ -121,21 +126,19 @@ public function store(Request $request)
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function edit($licenseId = null)
+ public function edit(License $license)
{
- if (is_null($item = License::find($licenseId))) {
- return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.does_not_exist'));
- }
-
- $this->authorize('update', $item);
+ $this->authorize('update', $license);
+ session()->put('back_url', url()->previous());
$maintained_list = [
'' => 'Maintained',
'1' => 'Yes',
'0' => 'No',
];
- return view('licenses/edit', compact('item'))
+ return view('licenses/edit')
+ ->with('item', $license)
->with('depreciation_list', Helper::depreciationList())
->with('maintained_list', $maintained_list);
}
@@ -153,11 +156,9 @@ public function edit($licenseId = null)
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function update(Request $request, $licenseId = null)
+ public function update(Request $request, License $license)
{
- if (is_null($license = License::find($licenseId))) {
- return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.does_not_exist'));
- }
+
$this->authorize('update', $license);
@@ -185,7 +186,8 @@ public function update(Request $request, $licenseId = null)
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($license->save()) {
- return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.update.success'));
+ return Helper::getRedirectOption($request, $license->id, 'Licenses')
+ ->with('success', trans('admin/licenses/message.update.success'));
}
// If we can't adjust the number of seats, the error is flashed to the session by the event handler in License.php
return redirect()->back()->withInput()->withErrors($license->getErrors());
@@ -201,10 +203,10 @@ public function update(Request $request, $licenseId = null)
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function destroy($licenseId)
+ public function destroy(License $license)
{
// Check if the license exists
- if (is_null($license = License::find($licenseId))) {
+ if (is_null($license = License::find($license->id))) {
// Redirect to the license management page
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.not_found'));
}
@@ -238,26 +240,30 @@ public function destroy($licenseId)
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function show($licenseId = null)
+ public function show(License $license)
{
- $license = License::with('assignedusers')->find($licenseId);
-
- if (!$license) {
- return redirect()->route('licenses.index')
- ->with('error', trans('admin/licenses/message.does_not_exist'));
- }
+ $license = License::with('assignedusers')->find($license->id);
$users_count = User::where('autoassign_licenses', '1')->count();
- $total_seats_count = $license->totalSeatsByLicenseID();
+
+ $total_seats_count = (int) $license->totalSeatsByLicenseID();
$available_seats_count = $license->availCount()->count();
- $checkedout_seats_count = ($total_seats_count - $available_seats_count);
+ $unreassignable_seats_count = License::unReassignableCount($license);
+
+ if(!$license->reassignable){
+ $checkedout_seats_count = ($total_seats_count - $available_seats_count - $unreassignable_seats_count );
+ }
+ else {
+ $checkedout_seats_count = ($total_seats_count - $available_seats_count);
+ }
$this->authorize('view', $license);
return view('licenses.view', compact('license'))
->with('users_count', $users_count)
->with('total_seats_count', $total_seats_count)
->with('available_seats_count', $available_seats_count)
- ->with('checkedout_seats_count', $checkedout_seats_count);
+ ->with('checkedout_seats_count', $checkedout_seats_count)
+ ->with('unreassignable_seats_count', $unreassignable_seats_count);
}
@@ -267,10 +273,10 @@ public function show($licenseId = null)
*
* @author [A. Gianotto] []
* @param int $licenseId
- * @return \Illuminate\Http\RedirectResponse
+ * @return \Illuminate\Http\RedirectResponse | \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function getClone($licenseId = null)
+ public function getClone($licenseId = null) : \Illuminate\Contracts\View\View | \Illuminate\Http\RedirectResponse
{
if (is_null($license_to_clone = License::find($licenseId))) {
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.does_not_exist'));
@@ -311,13 +317,16 @@ public function getExportLicensesCsv()
$response = new StreamedResponse(function () {
// Open output stream
$handle = fopen('php://output', 'w');
- $licenses= License::with('company',
+ $licenses = License::with('company',
'manufacturer',
'category',
'supplier',
'adminuser',
- 'assignedusers')
- ->orderBy('created_at', 'DESC');
+ 'assignedusers');
+ if (request()->filled('category_id')) {
+ $licenses = $licenses->where('category_id', request()->input('category_id'));
+ }
+ $licenses = $licenses->orderBy('created_at', 'DESC');
Company::scopeCompanyables($licenses)
->chunk(500, function ($licenses) use ($handle) {
$headers = [
@@ -364,7 +373,7 @@ public function getExportLicensesCsv()
$license->order_number,
$license->free_seat_count,
$license->seats,
- ($license->adminuser ? $license->adminuser->present()->fullName() : trans('admin/reports/general.deleted_user')),
+ ($license->adminuser ? $license->adminuser->display_name : trans('admin/reports/general.deleted_user')),
$license->depreciation ? $license->depreciation->name: '',
$license->updated_at,
$license->deleted_at,
diff --git a/app/Http/Controllers/LocationsController.php b/app/Http/Controllers/LocationsController.php
index e250e5cfa5da..46eff73df708 100755
--- a/app/Http/Controllers/LocationsController.php
+++ b/app/Http/Controllers/LocationsController.php
@@ -2,10 +2,13 @@
namespace App\Http\Controllers;
+use App\Helpers\Helper;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Actionlog;
use App\Models\Asset;
+use App\Models\Company;
use App\Models\Location;
+use App\Models\Setting;
use App\Models\User;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\Request;
@@ -63,6 +66,7 @@ public function create() : View
public function store(ImageUploadRequest $request) : RedirectResponse
{
$this->authorize('create', Location::class);
+
$location = new Location();
$location->name = $request->input('name');
$location->parent_id = $request->input('parent_id', null);
@@ -79,8 +83,31 @@ public function store(ImageUploadRequest $request) : RedirectResponse
$location->phone = request('phone');
$location->fax = request('fax');
$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) {
+ $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 {
+ $location->company_id = $request->input('company_id');
+ }
- $location = $request->handleImages($location);
+ if ($request->has('use_cloned_image')) {
+ $cloned_model_img = Location::select('image')->find($request->input('clone_image_from_id'));
+ if ($cloned_model_img) {
+ $new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
+ $new_image = 'locations/'.$new_image_name;
+ Storage::disk('public')->copy('locations/'.$cloned_model_img->image, $new_image);
+ $location->image = $new_image_name;
+ }
+
+ } else {
+ $location = $request->handleImages($location);
+ }
if ($location->save()) {
return redirect()->route('locations.index')->with('success', trans('admin/locations/message.create.success'));
@@ -97,15 +124,10 @@ public function store(ImageUploadRequest $request) : RedirectResponse
* @param int $locationId
* @since [v1.0]
*/
- public function edit($locationId = null) : View | RedirectResponse
+ public function edit(Location $location) : View | RedirectResponse
{
$this->authorize('update', Location::class);
- // Check if the location exists
- if (is_null($item = Location::find($locationId))) {
- return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
- }
-
- return view('locations/edit', compact('item'));
+ return view('locations/edit')->with('item', $location);
}
/**
@@ -117,15 +139,10 @@ public function edit($locationId = null) : View | RedirectResponse
* @param int $locationId
* @since [v1.0]
*/
- public function update(ImageUploadRequest $request, $locationId = null) : RedirectResponse
+ public function update(ImageUploadRequest $request, Location $location) : RedirectResponse
{
$this->authorize('update', Location::class);
- // Check if the location exists
- if (is_null($location = Location::find($locationId))) {
- return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
- }
- // Update the location data
$location->name = $request->input('name');
$location->parent_id = $request->input('parent_id', null);
$location->currency = $request->input('currency', '$');
@@ -141,6 +158,17 @@ public function update(ImageUploadRequest $request, $locationId = null) : Redire
$location->manager_id = $request->input('manager_id');
$location->notes = $request->input('notes');
+ // Only scope the location if the setting is enabled
+ if (Setting::getSettings()->scope_locations_fmcs) {
+ $location->company_id = Company::getIdForCurrentUser($request->input('company_id'));
+ // check if there are related objects with different company
+ if (Helper::test_locations_fmcs(false, $location->id, $location->company_id)) {
+ return redirect()->back()->withInput()->withInput()->with('error', 'error scoped locations');
+ }
+ } else {
+ $location->company_id = $request->input('company_id');
+ }
+
$location = $request->handleImages($location);
if ($location->save()) {
@@ -160,6 +188,7 @@ public function update(ImageUploadRequest $request, $locationId = null) : Redire
public function destroy($locationId) : RedirectResponse
{
$this->authorize('delete', Location::class);
+
if (is_null($location = Location::find($locationId))) {
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.does_not_exist'));
}
@@ -194,15 +223,17 @@ public function destroy($locationId) : RedirectResponse
* @param int $id
* @since [v1.0]
*/
- public function show($id = null) : View | RedirectResponse
+ public function show(Location $location) : View | RedirectResponse
{
+ $this->authorize('view', Location::class);
+
$location = Location::withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count')
->withTrashed()
- ->find($id);
+ ->find($location->id);
if (isset($location->id)) {
return view('locations/view', compact('location'));
@@ -213,20 +244,24 @@ public function show($id = null) : View | RedirectResponse
public function print_assigned($id) : View | RedirectResponse
{
+ $this->authorize('view', Location::class);
if ($location = Location::where('id', $id)->first()) {
$parent = Location::where('id', $location->parent_id)->first();
$manager = User::where('id', $location->manager_id)->first();
+ $company = Company::where('id', $location->company_id)->first();
$users = User::where('location_id', $id)->with('company', 'department', 'location')->get();
$assets = Asset::where('assigned_to', $id)->where('assigned_type', Location::class)->with('model', 'model.category')->get();
- return view('locations/print')->with('assets', $assets)->with('users', $users)->with('location', $location)->with('parent', $parent)->with('manager', $manager);
-
+ return view('locations/print')
+ ->with('assets', $assets)
+ ->with('users',$users)
+ ->with('location', $location)
+ ->with('parent', $parent)
+ ->with('manager', $manager)
+ ->with('company', $company);
}
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
-
-
-
}
@@ -251,9 +286,9 @@ public function getClone($locationId = null) : View | RedirectResponse
// unset these values
$location->id = null;
- $location->image = null;
return view('locations/edit')
+ ->with('cloned_model', $location_to_clone)
->with('item', $location);
}
@@ -295,13 +330,20 @@ public function postRestore($id) : RedirectResponse
}
public function print_all_assigned($id) : View | RedirectResponse
{
+ $this->authorize('view', Location::class);
if ($location = Location::where('id', $id)->first()) {
$parent = Location::where('id', $location->parent_id)->first();
$manager = User::where('id', $location->manager_id)->first();
+ $company = Company::where('id', $location->company_id)->first();
$users = User::where('location_id', $id)->with('company', 'department', 'location')->get();
$assets = Asset::where('location_id', $id)->with('model', 'model.category')->get();
- return view('locations/print')->with('assets', $assets)->with('users', $users)->with('location', $location)->with('parent', $parent)->with('manager', $manager);
-
+ return view('locations/print')
+ ->with('assets', $assets)
+ ->with('users',$users)
+ ->with('location', $location)
+ ->with('parent', $parent)
+ ->with('manager', $manager)
+ ->with('company', $company);
}
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
}
@@ -315,6 +357,8 @@ public function print_all_assigned($id) : View | RedirectResponse
*/
public function postBulkDelete(Request $request) : View | RedirectResponse
{
+ $this->authorize('update', Location::class);
+
$locations_raw_array = $request->input('ids');
// Make sure some IDs have been selected
@@ -348,6 +392,8 @@ public function postBulkDelete(Request $request) : View | RedirectResponse
*/
public function postBulkDeleteStore(Request $request) : RedirectResponse
{
+ $this->authorize('delete', Location::class);
+
$locations_raw_array = $request->input('ids');
if ((is_array($locations_raw_array)) && (count($locations_raw_array) > 0)) {
diff --git a/app/Http/Controllers/MaintenancesController.php b/app/Http/Controllers/MaintenancesController.php
new file mode 100644
index 000000000000..e893b75f39e3
--- /dev/null
+++ b/app/Http/Controllers/MaintenancesController.php
@@ -0,0 +1,216 @@
+authorize('view', Asset::class);
+ return view('maintenances.index');
+ }
+
+ /**
+ * Returns a form view to create a new asset maintenance.
+ *
+ * @see MaintenancesController::postCreate() method that stores the data
+ * @author Vincent Sposato
+ * @version v1.0
+ * @since [v1.8]
+ * @return mixed
+ */
+ public function create() : View
+ {
+ $this->authorize('update', Asset::class);
+ $asset = null;
+
+ if ($asset = Asset::find(request('asset_id'))) {
+ // We have to set this so that the correct property is set in the select2 ajax dropdown
+ $asset->asset_id = $asset->id;
+ }
+
+ return view('maintenances/edit')
+ ->with('maintenanceType', Maintenance::getImprovementOptions())
+ ->with('asset', $asset)
+ ->with('item', new Maintenance);
+ }
+
+ /**
+ * Validates and stores the new asset maintenance
+ *
+ * @see MaintenancesController::getCreate() method for the form
+ * @author Vincent Sposato
+ * @version v1.0
+ * @since [v1.8]
+ */
+ public function store(ImageUploadRequest $request) : RedirectResponse
+ {
+ $this->authorize('update', Asset::class);
+
+ $assets = Asset::whereIn('id', $request->input('selected_assets'))->get();
+
+ // Loop through the selected assets
+ foreach ($assets as $asset) {
+
+ $maintenance = new Maintenance();
+ $maintenance->supplier_id = $request->input('supplier_id');
+ $maintenance->is_warranty = $request->input('is_warranty');
+ $maintenance->cost = $request->input('cost');
+ $maintenance->notes = $request->input('notes');
+
+ // Save the asset maintenance data
+ $maintenance->asset_id = $asset->id;
+ $maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
+ $maintenance->name = $request->input('name');
+ $maintenance->start_date = $request->input('start_date');
+ $maintenance->completion_date = $request->input('completion_date');
+ $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);
+ }
+
+ $maintenance = $request->handleImages($maintenance);
+
+ // Was the asset maintenance created?
+ if (!$maintenance->save()) {
+ return redirect()->back()->withInput()->withErrors($maintenance->getErrors());
+ }
+ }
+
+ return redirect()->route('maintenances.index')
+ ->with('success', trans('admin/maintenances/message.create.success'));
+
+ }
+
+ /**
+ * Returns a form view to edit a selected asset maintenance.
+ *
+ * @see MaintenancesController::postEdit() method that stores the data
+ * @author Vincent Sposato
+ * @version v1.0
+ * @since [v1.8]
+ */
+ public function edit(Maintenance $maintenance) : View | RedirectResponse
+ {
+ $this->authorize('update', Asset::class);
+ $this->authorize('update', $maintenance->asset);
+
+ return view('maintenances/edit')
+ ->with('selected_assets', $maintenance->asset->pluck('id')->toArray())
+ ->with('asset_ids', request()->input('asset_ids', []))
+ ->with('maintenanceType', Maintenance::getImprovementOptions())
+ ->with('item', $maintenance);
+ }
+
+ /**
+ * Validates and stores an update to an asset maintenance
+ *
+ * @see MaintenancesController::postEdit() method that stores the data
+ * @author Vincent Sposato
+ * @param Request $request
+ * @param int $maintenanceId
+ * @version v1.0
+ * @since [v1.8]
+ */
+ public function update(ImageUploadRequest $request, Maintenance $maintenance) : View | RedirectResponse
+ {
+ $this->authorize('update', Asset::class);
+ $this->authorize('update', $maintenance->asset);
+
+ $maintenance->supplier_id = $request->input('supplier_id');
+ $maintenance->is_warranty = $request->input('is_warranty', 0);
+ $maintenance->cost = $request->input('cost');
+ $maintenance->notes = $request->input('notes');
+ $maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
+ $maintenance->name = $request->input('name');
+ $maintenance->start_date = $request->input('start_date');
+ $maintenance->completion_date = $request->input('completion_date');
+
+
+ // 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);
+ }
+ $maintenance = $request->handleImages($maintenance);
+
+ if ($maintenance->save()) {
+ return redirect()->route('maintenances.index')
+ ->with('success', trans('admin/maintenances/message.edit.success'));
+ }
+
+ return redirect()->back()->withInput()->withErrors($maintenance->getErrors());
+ }
+
+ /**
+ * Delete an asset maintenance
+ *
+ * @author Vincent Sposato
+ * @param int $maintenanceId
+ * @version v1.0
+ * @since [v1.8]
+ */
+ public function destroy(Maintenance $maintenance) : RedirectResponse
+ {
+ $this->authorize('update', Asset::class);
+ $this->authorize('update', $maintenance->asset);
+ // Delete the asset maintenance
+ $maintenance->delete();
+ // Redirect to the asset_maintenance management page
+ return redirect()->route('maintenances.index')
+ ->with('success', trans('admin/maintenances/message.delete.success'));
+ }
+
+ /**
+ * View an asset maintenance
+ *
+ * @author Vincent Sposato
+ * @param int $maintenanceId
+ * @version v1.0
+ * @since [v1.8]
+ */
+ public function show(Maintenance $maintenance) : View | RedirectResponse
+ {
+ return view('maintenances.view')->with('maintenance', $maintenance);
+ }
+}
diff --git a/app/Http/Controllers/ManufacturersController.php b/app/Http/Controllers/ManufacturersController.php
index eb8c80d87716..ac0b2818f44c 100755
--- a/app/Http/Controllers/ManufacturersController.php
+++ b/app/Http/Controllers/ManufacturersController.php
@@ -6,6 +6,7 @@
use App\Models\Actionlog;
use App\Models\Manufacturer;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
@@ -31,7 +32,30 @@ class ManufacturersController extends Controller
public function index() : View
{
$this->authorize('index', Manufacturer::class);
- return view('manufacturers/index');
+ $manufacturer_count = Manufacturer::withTrashed()->count();
+ return view('manufacturers/index')->with('manufacturer_count', $manufacturer_count);
+ }
+
+ /**
+ * Returns a view that invokes the ajax tables which actually contains
+ * the content for the manufacturers listing, which is generated in getDatatable.
+ *
+ * @author [A. Gianotto] []
+ * @see Api\ManufacturersController::index() method that generates the JSON response
+ * @since [v1.0]
+ */
+ public function seed() : RedirectResponse
+ {
+ $this->authorize('index', Manufacturer::class);
+
+ $manufacturers_count = Manufacturer::withTrashed()->count();
+
+ if ($manufacturers_count == 0) {
+ Artisan::call('db:seed', ['--class' => 'Database\\Seeders\\ManufacturerSeeder', '--force' => true]);
+ return redirect()->route('manufacturers.index')->with('success', trans('general.seeding.manufacturers.success'));
+ }
+
+ return redirect()->route('manufacturers.index')->with('error', trans_choice('general.seeding.manufacturers.error', ['count' => $manufacturers_count]));
}
/**
@@ -85,18 +109,10 @@ public function store(ImageUploadRequest $request) : RedirectResponse
* @param int $manufacturerId
* @since [v1.0]
*/
- public function edit($manufacturerId = null) : View | RedirectResponse
+ public function edit(Manufacturer $manufacturer) : View | RedirectResponse
{
- // Handles manufacturer checks and permissions.
$this->authorize('update', Manufacturer::class);
-
- // Check if the manufacturer exists
- if (! $item = Manufacturer::find($manufacturerId)) {
- return redirect()->route('manufacturers.index')->with('error', trans('admin/manufacturers/message.does_not_exist'));
- }
-
- // Show the page
- return view('manufacturers/edit', compact('item'));
+ return view('manufacturers/edit')->with('item', $manufacturer);
}
/**
@@ -108,16 +124,10 @@ public function edit($manufacturerId = null) : View | RedirectResponse
* @param int $manufacturerId
* @since [v1.0]
*/
- public function update(ImageUploadRequest $request, $manufacturerId = null) : RedirectResponse
+ public function update(ImageUploadRequest $request, Manufacturer $manufacturer) : RedirectResponse
{
$this->authorize('update', Manufacturer::class);
- // Check if the manufacturer exists
- if (is_null($manufacturer = Manufacturer::find($manufacturerId))) {
- // Redirect to the manufacturer page
- return redirect()->route('manufacturers.index')->with('error', trans('admin/manufacturers/message.does_not_exist'));
- }
- // Save the data
$manufacturer->name = $request->input('name');
$manufacturer->url = $request->input('url');
$manufacturer->support_url = $request->input('support_url');
@@ -185,18 +195,10 @@ public function destroy($manufacturerId) : RedirectResponse
* @param int $manufacturerId
* @since [v1.0]
*/
- public function show($manufacturerId = null) : View | RedirectResponse
+ public function show(Manufacturer $manufacturer) : View | RedirectResponse
{
$this->authorize('view', Manufacturer::class);
- $manufacturer = Manufacturer::find($manufacturerId);
-
- if (isset($manufacturer->id)) {
- return view('manufacturers/view', compact('manufacturer'));
- }
-
- $error = trans('admin/manufacturers/message.does_not_exist');
- // Redirect to the user management page
- return redirect()->route('manufacturers.index')->with('error', $error);
+ return view('manufacturers/view', compact('manufacturer'));
}
/**
diff --git a/app/Http/Controllers/ModalController.php b/app/Http/Controllers/ModalController.php
index 5b8d5e0ea2eb..10b17ec8e21f 100644
--- a/app/Http/Controllers/ModalController.php
+++ b/app/Http/Controllers/ModalController.php
@@ -41,10 +41,11 @@ public function show ($type, $itemId = null) {
$view = view("modals.${type}");
if ($type == "statuslabel") {
- $view->with('statuslabel_types', Helper::statusTypeList());
- }
- if (in_array($type, ['kit-model', 'kit-license', 'kit-consumable', 'kit-accessory'])) {
- $view->with('kitId', $itemId);
+ $view->with('statuslabel_types', Helper::statusTypeList());
+ }
+
+ if (in_array($type, ['kit-model', 'kit-license', 'kit-consumable', 'kit-accessory'])) {
+ $view->with('kitId', $itemId);
}
return $view;
}
diff --git a/app/Http/Controllers/NotesController.php b/app/Http/Controllers/NotesController.php
new file mode 100644
index 000000000000..c5de66526e15
--- /dev/null
+++ b/app/Http/Controllers/NotesController.php
@@ -0,0 +1,42 @@
+authorize('update', Asset::class);
+
+ $validated = $request->validate([
+ 'id' => 'required',
+ 'note' => 'required|string|max:50000',
+ 'type' => [
+ 'required',
+ Rule::in(['asset']),
+ ],
+ ]);
+
+ $item = Asset::findOrFail($validated['id']);
+
+ $this->authorize('update', $item);
+
+ $logaction = new Actionlog;
+ $logaction->item_id = $item->id;
+ $logaction->item_type = get_class($item);
+ $logaction->note = $validated['note'];
+ $logaction->created_by = Auth::id();
+ $logaction->logaction('note added');
+
+ return redirect()
+ ->route('hardware.show', $validated['id'])
+ ->withFragment('history')
+ ->with('success', trans('general.note_added'));
+ }
+}
diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php
index 1a9bd0035612..a2fe612b6efb 100755
--- a/app/Http/Controllers/ProfileController.php
+++ b/app/Http/Controllers/ProfileController.php
@@ -3,15 +3,21 @@
namespace App\Http\Controllers;
use App\Http\Requests\ImageUploadRequest;
+use App\Http\Transformers\ProfileTransformer;
+use App\Models\Actionlog;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\CurrentInventory;
+use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View;
+use Illuminate\Support\Facades\Storage;
+use Symfony\Component\HttpFoundation\BinaryFileResponse;
+
/**
* This controller handles all actions related to User Profiles for
* the Snipe-IT Asset Management application.
@@ -53,7 +59,7 @@ public function postIndex(ImageUploadRequest $request) : RedirectResponse
$user->enable_confetti = $request->input('enable_confetti', false);
if (! config('app.lock_passwords')) {
- $user->locale = $request->input('locale', 'en-US');
+ $user->locale = $request->input('locale');
}
if ((Gate::allows('self.two_factor')) && ((Setting::getSettings()->two_factor_enabled == '1') && (! config('app.lock_passwords')))) {
@@ -136,7 +142,7 @@ public function passwordSave(Request $request) : RedirectResponse
}
// This checks to make sure that the user's password isn't the same as their username,
- // email address, first name or last name (see https://github.com/snipe/snipe-it/issues/8661)
+ // email address, first name or last name (see https://github.com/grokability/snipe-it/issues/8661)
// While this is handled via SaveUserRequest form request in other places, we have to do this manually
// here because we don't have the username, etc form fields available in the profile password change
// form.
@@ -220,7 +226,7 @@ public function emailAssetList() : RedirectResponse
if (!$user = User::find(auth()->id())) {
return redirect()->back()
- ->with('error', trans('admin/users/message.user_not_found', ['id' => $id]));
+ ->with('error', trans('admin/users/message.user_not_found', ['id' => auth()->id()]));
}
if (empty($user->email)) {
return redirect()->back()->with('error', trans('admin/users/message.user_has_no_email'));
@@ -234,4 +240,28 @@ public function emailAssetList() : RedirectResponse
return redirect()->back()->with('success', trans('admin/users/general.user_notified'));
}
+
+
+
+ public function getStoredEula($filename) : Response | BinaryFileResponse | RedirectResponse
+ {
+
+ $logentry = Actionlog::where('filename', $filename)->first();
+
+ // Make sure the user has permission to view this file
+ if (auth()->id() != $logentry->target_id) {
+ return redirect()->route('account')->with('error', trans('general.generic_model_not_found', ['model' => 'file']));
+ }
+
+ if (config('filesystems.default') == 's3_private') {
+ return redirect()->away(Storage::disk('s3_private')->temporaryUrl('private_uploads/eula-pdfs/'.$filename, now()->addMinutes(5)));
+ }
+
+ if (Storage::exists('private_uploads/eula-pdfs/'.$filename)) {
+ return response()->download(config('app.private_uploads').'/eula-pdfs/'.$filename);
+ }
+
+ return redirect()->back()->with('error', trans('general.file_does_not_exist'));
+
+ }
}
diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php
index f0bbfb74a26a..89a0a07a2135 100644
--- a/app/Http/Controllers/ReportsController.php
+++ b/app/Http/Controllers/ReportsController.php
@@ -9,7 +9,7 @@
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Category;
-use App\Models\AssetMaintenance;
+use App\Models\Maintenance;
use App\Models\CheckoutAcceptance;
use App\Models\Company;
use App\Models\CustomField;
@@ -17,13 +17,11 @@
use App\Models\License;
use App\Models\ReportTemplate;
use App\Models\Setting;
-use App\Notifications\CheckoutAssetNotification;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Mail;
-use Illuminate\Support\Facades\Notification;
use \Illuminate\Contracts\View\View;
use League\Csv\Reader;
use Symfony\Component\HttpFoundation\StreamedResponse;
@@ -184,7 +182,7 @@ public function exportDeprecationReport() : Response
$currency = e(Setting::getSettings()->default_currency);
}
- $row[] = $asset->purchase_date;
+ $row[] = Helper::getFormattedDateObject($asset->purchase_date, 'date', false);
$row[] = $currency.Helper::formatCurrencyOutput($asset->purchase_cost);
$row[] = $currency.Helper::formatCurrencyOutput($asset->getDepreciatedValue());
$row[] = $currency.Helper::formatCurrencyOutput(($asset->purchase_cost - $asset->getDepreciatedValue()));
@@ -243,7 +241,7 @@ public function postActivityReport(Request $request) : StreamedResponse
$header = [
trans('general.date'),
- trans('general.admin'),
+ trans('general.created_by'),
trans('general.action'),
trans('general.type'),
trans('general.item'),
@@ -266,7 +264,7 @@ public function postActivityReport(Request $request) : StreamedResponse
$actionlogs = Actionlog::with('item', 'user', 'target', 'location', 'adminuser')
->orderBy('created_at', 'DESC')
- ->chunk(20, function ($actionlogs) use ($handle) {
+ ->chunk(500, function ($actionlogs) use ($handle) {
$executionTime = microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'];
Log::debug('Walking results: '.$executionTime);
$count = 0;
@@ -277,7 +275,7 @@ public function postActivityReport(Request $request) : StreamedResponse
if ($actionlog->target) {
if ($actionlog->targetType() == 'user') {
- $target_name = $actionlog->target->getFullNameAttribute();
+ $target_name = $actionlog->target->display_name;
} else {
$target_name = $actionlog->target->getDisplayNameAttribute();
}
@@ -291,7 +289,7 @@ public function postActivityReport(Request $request) : StreamedResponse
$row = [
$actionlog->created_at,
- ($actionlog->adminuser) ? e($actionlog->adminuser->getFullNameAttribute()) : '',
+ ($actionlog->adminuser) ? e($actionlog->adminuser->display_name) : '',
$actionlog->present()->actionType(),
e($actionlog->itemType()),
($actionlog->itemType() == 'user') ? $actionlog->filename : $item_name,
@@ -485,7 +483,7 @@ public function postCustom(CustomAssetReportRequest $request) : StreamedResponse
$header[] = trans('admin/hardware/table.purchase_date');
}
- if (($request->filled('purchase_cost')) || ($request->filled('depreciation'))) {
+ if ($request->filled('purchase_cost')) {
$header[] = trans('admin/hardware/table.purchase_cost');
}
@@ -493,6 +491,17 @@ public function postCustom(CustomAssetReportRequest $request) : StreamedResponse
$header[] = trans('admin/hardware/table.eol');
}
+ if ($request->filled('warranty')) {
+ $header[] = trans('admin/hardware/form.warranty');
+ $header[] = trans('admin/hardware/form.warranty_expires');
+ }
+
+ if ($request->filled('depreciation')) {
+ $header[] = trans('admin/hardware/table.book_value');
+ $header[] = trans('admin/hardware/table.diff');
+ $header[] = trans('admin/hardware/form.fully_depreciated');
+ }
+
if ($request->filled('order')) {
$header[] = trans('admin/hardware/form.order');
}
@@ -579,17 +588,6 @@ public function postCustom(CustomAssetReportRequest $request) : StreamedResponse
$header[] = trans('general.status');
}
- if ($request->filled('warranty')) {
- $header[] = trans('admin/hardware/form.warranty');
- $header[] = trans('admin/hardware/form.warranty_expires');
- }
-
- if ($request->filled('depreciation')) {
- $header[] = trans('admin/hardware/table.book_value');
- $header[] = trans('admin/hardware/table.diff');
- $header[] = trans('admin/hardware/form.fully_depreciated');
- }
-
if ($request->filled('checkout_date')) {
$header[] = trans('admin/hardware/table.checkout_date');
}
@@ -737,6 +735,11 @@ public function postCustom(CustomAssetReportRequest $request) : StreamedResponse
if (($request->filled('next_audit_start')) && ($request->filled('next_audit_end'))) {
$assets->whereBetween('assets.next_audit_date', [$request->input('next_audit_start'), $request->input('next_audit_end')]);
}
+
+ if (($request->filled('last_updated_start')) && ($request->filled('last_updated_end'))) {
+ $assets->whereBetween('assets.updated_at', [$request->input('last_updated_start'), $request->input('last_updated_end')]);
+ }
+
if ($request->filled('exclude_archived')) {
$assets->notArchived();
}
@@ -748,7 +751,7 @@ public function postCustom(CustomAssetReportRequest $request) : StreamedResponse
}
Log::debug($assets->toSql());
- $assets->orderBy('assets.id', 'ASC')->chunk(20, function ($assets) use ($handle, $customfields, $request) {
+ $assets->orderBy('assets.id', 'ASC')->chunk(500, function ($assets) use ($handle, $customfields, $request) {
$executionTime = microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'];
Log::debug('Walking results: '.$executionTime);
@@ -805,6 +808,19 @@ public function postCustom(CustomAssetReportRequest $request) : StreamedResponse
$row[] = ($asset->purchase_date != '') ? $asset->asset_eol_date : '';
}
+ if ($request->filled('warranty')) {
+ $row[] = ($asset->warranty_months) ? $asset->warranty_months : '';
+ $row[] = $asset->present()->warranty_expires();
+ }
+
+ if ($request->filled('depreciation')) {
+ $depreciation = $asset->getDepreciatedValue();
+ $diff = ($asset->purchase_cost - $depreciation);
+ $row[] = Helper::formatCurrencyOutput($depreciation);
+ $row[] = Helper::formatCurrencyOutput($diff);
+ $row[] = (($asset->depreciation) && ($asset->depreciated_date())) ? $asset->depreciated_date()->format('Y-m-d') : '';
+ }
+
if ($request->filled('order')) {
$row[] = ($asset->order_number) ? $asset->order_number : '';
}
@@ -814,7 +830,7 @@ public function postCustom(CustomAssetReportRequest $request) : StreamedResponse
}
if ($request->filled('location')) {
- $row[] = ($asset->location) ? $asset->location->present()->name() : '';
+ $row[] = ($asset->location) ? $asset->location->display_name : '';
}
if ($request->filled('location_address')) {
@@ -827,7 +843,7 @@ public function postCustom(CustomAssetReportRequest $request) : StreamedResponse
}
if ($request->filled('rtd_location')) {
- $row[] = ($asset->defaultLoc) ? $asset->defaultLoc->present()->name() : '';
+ $row[] = ($asset->defaultLoc) ? $asset->defaultLoc->display_name : '';
}
if ($request->filled('rtd_location_address')) {
@@ -840,7 +856,7 @@ public function postCustom(CustomAssetReportRequest $request) : StreamedResponse
}
if ($request->filled('assigned_to')) {
- $row[] = ($asset->checkedOutToUser() && $asset->assigned) ? $asset->assigned->getFullNameAttribute() : ($asset->assigned ? $asset->assigned->display_name : '');
+ $row[] = ($asset->checkedOutToUser() && $asset->assigned) ?? $asset->assigned->display_name;
$row[] = ($asset->checkedOutToUser() && $asset->assigned) ? 'user' : $asset->assignedType();
}
@@ -938,19 +954,6 @@ public function postCustom(CustomAssetReportRequest $request) : StreamedResponse
$row[] = ($asset->assetstatus) ? $asset->assetstatus->name.' ('.$asset->present()->statusMeta.')' : '';
}
- if ($request->filled('warranty')) {
- $row[] = ($asset->warranty_months) ? $asset->warranty_months : '';
- $row[] = $asset->present()->warranty_expires();
- }
-
- if ($request->filled('depreciation')) {
- $depreciation = $asset->getDepreciatedValue();
- $diff = ($asset->purchase_cost - $depreciation);
- $row[] = Helper::formatCurrencyOutput($depreciation);
- $row[] = Helper::formatCurrencyOutput($diff);
- $row[] = (($asset->depreciation) && ($asset->depreciated_date())) ? $asset->depreciated_date()->format('Y-m-d') : '';
- }
-
if ($request->filled('checkout_date')) {
$row[] = ($asset->last_checkout) ? $asset->last_checkout : '';
}
@@ -1033,11 +1036,11 @@ public function postCustom(CustomAssetReportRequest $request) : StreamedResponse
* @author Vincent Sposato
* @version v1.0
*/
- public function getAssetMaintenancesReport() : View
+ public function getMaintenancesReport() : View
{
$this->authorize('reports.view');
- return view('reports.asset_maintenances');
+ return view('reports.maintenances');
}
/**
@@ -1046,11 +1049,11 @@ public function getAssetMaintenancesReport() : View
* @author Vincent Sposato
* @version v1.0
*/
- public function exportAssetMaintenancesReport() : Response
+ public function exportMaintenancesReport() : Response
{
$this->authorize('reports.view');
// Grab all the improvements
- $assetMaintenances = AssetMaintenance::with('asset', 'supplier')
+ $Maintenances = Maintenance::with('asset', 'supplier')
->orderBy('created_at', 'DESC')
->get();
@@ -1058,36 +1061,36 @@ public function exportAssetMaintenancesReport() : Response
$header = [
trans('admin/hardware/table.asset_tag'),
- trans('admin/asset_maintenances/table.asset_name'),
+ trans('admin/maintenances/table.asset_name'),
trans('general.supplier'),
- trans('admin/asset_maintenances/form.asset_maintenance_type'),
- trans('admin/asset_maintenances/form.title'),
- trans('admin/asset_maintenances/form.start_date'),
- trans('admin/asset_maintenances/form.completion_date'),
- trans('admin/asset_maintenances/form.asset_maintenance_time'),
- trans('admin/asset_maintenances/form.cost'),
+ trans('admin/maintenances/form.asset_maintenance_type'),
+ trans('admin/maintenances/form.title'),
+ trans('admin/maintenances/form.start_date'),
+ trans('admin/maintenances/form.completion_date'),
+ trans('admin/maintenances/form.asset_maintenance_time'),
+ trans('admin/maintenances/form.cost'),
];
$header = array_map('trim', $header);
$rows[] = implode(',', $header);
- foreach ($assetMaintenances as $assetMaintenance) {
+ foreach ($Maintenances as $maintenance) {
$row = [];
- $row[] = str_replace(',', '', e($assetMaintenance->asset->asset_tag));
- $row[] = str_replace(',', '', e($assetMaintenance->asset->name));
- $row[] = str_replace(',', '', e($assetMaintenance->supplier->name));
- $row[] = e($assetMaintenance->improvement_type);
- $row[] = e($assetMaintenance->title);
- $row[] = e($assetMaintenance->start_date);
- $row[] = e($assetMaintenance->completion_date);
- if (is_null($assetMaintenance->asset_maintenance_time)) {
- $improvementTime = intval(Carbon::now()
- ->diffInDays(Carbon::parse($assetMaintenance->start_date)));
+ $row[] = str_replace(',', '', e($maintenance->asset->asset_tag));
+ $row[] = str_replace(',', '', e($maintenance->asset->name));
+ $row[] = str_replace(',', '', e($maintenance->supplier->name));
+ $row[] = e($maintenance->improvement_type);
+ $row[] = e($maintenance->name);
+ $row[] = e($maintenance->start_date);
+ $row[] = e($maintenance->completion_date);
+ if (is_null($maintenance->asset_maintenance_time)) {
+ $improvementTime = (int) Carbon::now()
+ ->diffInDays(Carbon::parse($maintenance->start_date), true);
} else {
- $improvementTime = intval($assetMaintenance->asset_maintenance_time);
+ $improvementTime = (int) $maintenance->asset_maintenance_time;
}
$row[] = $improvementTime;
- $row[] = trans('general.currency') . Helper::formatCurrencyOutput($assetMaintenance->cost);
+ $row[] = trans('general.currency') . Helper::formatCurrencyOutput($maintenance->cost);
$rows[] = implode(',', $row);
}
@@ -1175,18 +1178,13 @@ public function sentAssetAcceptanceReminder(Request $request) : RedirectResponse
}
$email = $assetItem->assignedTo?->email;
$locale = $assetItem->assignedTo?->locale;
- // Only send notification if assigned
- if ($locale && $email) {
- Mail::to($email)->send((new CheckoutAssetMail($assetItem, $assetItem->assignedTo, $logItem->user, $acceptance, $logItem->note))->locale($locale));
- } elseif ($email) {
- Mail::to($email)->send((new CheckoutAssetMail($assetItem, $assetItem->assignedTo, $logItem->user, $acceptance, $logItem->note)));
- }
-
- if ($email == ''){
+ if (is_null($email) || $email === '') {
return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.no_email'));
}
+ Mail::to($email)->send((new CheckoutAssetMail($assetItem, $assetItem->assignedTo, $logItem->user, $acceptance, $logItem->note, firstTimeSending: false))->locale($locale));
+
return redirect()->route('reports/unaccepted_assets')->with('success', trans('admin/reports/general.reminder_sent'));
}
diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php
old mode 100755
new mode 100644
index 7d2d2660abc0..8c57efa5eb20
--- a/app/Http/Controllers/SettingsController.php
+++ b/app/Http/Controllers/SettingsController.php
@@ -14,6 +14,7 @@
use App\Http\Requests\StoreSecuritySettings;
use App\Models\CustomField;
use App\Models\Group;
+use App\Models\Labels\Label as LabelModel;
use App\Models\Setting;
use App\Models\Asset;
use App\Models\User;
@@ -256,7 +257,7 @@ public function getSetupMigrate() : View
Artisan::call('migrate', ['--force' => true]);
if ((! file_exists(storage_path().'/oauth-private.key')) || (! file_exists(storage_path().'/oauth-public.key'))) {
Artisan::call('migrate', ['--path' => 'vendor/laravel/passport/database/migrations', '--force' => true]);
- Artisan::call('passport:install');
+ Artisan::call('passport:install', ['--no-interaction' => true]);
}
return view('setup/migrate')
@@ -290,7 +291,6 @@ public function index() : View
public function getSettings() : View
{
$setting = Setting::getSettings();
-
return view('settings/general', compact('setting'));
}
@@ -314,7 +314,23 @@ public function postSettings(Request $request) : RedirectResponse
$setting->modellist_displays = implode(',', $request->input('show_in_model_list'));
}
+ $old_locations_fmcs = $setting->scope_locations_fmcs;
$setting->full_multiple_companies_support = $request->input('full_multiple_companies_support', '0');
+ $setting->scope_locations_fmcs = $request->input('scope_locations_fmcs', '0');
+
+ // Backward compatibility for locations makes no sense without FullMultipleCompanySupport
+ if (!$setting->full_multiple_companies_support) {
+ $setting->scope_locations_fmcs = '0';
+ }
+
+ // check for inconsistencies when activating scoped locations
+ if ($old_locations_fmcs == '0' && $setting->scope_locations_fmcs == '1') {
+ $mismatched = Helper::test_locations_fmcs(false);
+ if (count($mismatched) != 0) {
+ return redirect()->back()->withInput()->with('error', trans_choice('admin/settings/message.location_scoping.mismatch', count($mismatched)).' '.trans('admin/settings/message.location_scoping.not_saved'));
+ }
+ }
+
$setting->unique_serial = $request->input('unique_serial', '0');
$setting->shortcuts_enabled = $request->input('shortcuts_enabled', '0');
$setting->show_images_in_email = $request->input('show_images_in_email', '0');
@@ -336,6 +352,7 @@ public function postSettings(Request $request) : RedirectResponse
$setting->dash_chart_type = $request->input('dash_chart_type');
$setting->profile_edit = $request->input('profile_edit', 0);
$setting->require_checkinout_notes = $request->input('require_checkinout_notes', 0);
+ $setting->manager_view_enabled = $request->input('manager_view_enabled', 0);
if ($request->input('per_page') != '') {
@@ -428,6 +445,13 @@ public function postBranding(ImageUploadRequest $request) : RedirectResponse
$setting->label_logo = null;
}
+ // Acceptance PDF upload
+ $setting = $request->handleImages($setting, 600, 'acceptance_pdf_logo', '', 'acceptance_pdf_logo');
+ if ('1' == $request->input('clear_acceptance_pdf_logo')) {
+ $setting = $request->deleteExistingImage($setting, '', 'acceptance_pdf_logo');
+ $setting->acceptance_pdf_logo = null;
+ }
+
// Favicon upload
$setting = $request->handleImages($setting, 100, 'favicon', '', 'favicon');
if ('1' == $request->input('clear_favicon')) {
@@ -435,6 +459,7 @@ public function postBranding(ImageUploadRequest $request) : RedirectResponse
$setting->favicon = null;
}
+
// Default avatar upload
$setting = $request->handleImages($setting, 500, 'default_avatar', 'avatars', 'default_avatar');
if ($request->input('clear_default_avatar') == '1') {
@@ -626,6 +651,7 @@ public function postAlerts(StoreNotificationSettings $request) : RedirectRespons
$setting->alert_email = $alert_email;
$setting->admin_cc_email = $admin_cc_email;
+ $setting->admin_cc_always = $request->validated('admin_cc_always');
$setting->alerts_enabled = $request->input('alerts_enabled', '0');
$setting->alert_interval = $request->input('alert_interval');
$setting->alert_threshold = $request->input('alert_threshold');
@@ -748,6 +774,7 @@ public function postLabels(StoreLabelSettings $request) : RedirectResponse
$setting->label2_2d_type = $request->input('label2_2d_type');
$setting->label2_2d_target = $request->input('label2_2d_target');
$setting->label2_fields = $request->input('label2_fields');
+ $setting->label2_empty_row_count = $request->input('label2_empty_row_count');
$setting->labels_per_page = $request->input('labels_per_page');
$setting->labels_width = $request->input('labels_width');
$setting->labels_height = $request->input('labels_height');
@@ -846,11 +873,13 @@ public function postLdapSettings(StoreLdapSettings $request) : RedirectResponse
$setting->ldap_default_group = $request->input('ldap_default_group');
$setting->ldap_filter = $request->input('ldap_filter');
$setting->ldap_username_field = $request->input('ldap_username_field');
+ $setting->ldap_display_name = $request->input('ldap_display_name');
$setting->ldap_lname_field = $request->input('ldap_lname_field');
$setting->ldap_fname_field = $request->input('ldap_fname_field');
$setting->ldap_auth_filter_query = $request->input('ldap_auth_filter_query');
$setting->ldap_version = $request->input('ldap_version', 3);
- $setting->ldap_active_flag = $request->input('ldap_active_flag');
+ $setting->ldap_active_flag = $request->input('ldap_active_flag', 0);
+ $setting->ldap_invert_active_flag = $request->input('ldap_invert_active_flag', 0);
$setting->ldap_emp_num = $request->input('ldap_emp_num');
$setting->ldap_email = $request->input('ldap_email');
$setting->ldap_manager = $request->input('ldap_manager');
@@ -861,7 +890,12 @@ public function postLdapSettings(StoreLdapSettings $request) : RedirectResponse
$setting->ldap_pw_sync = $request->input('ldap_pw_sync', '0');
$setting->custom_forgot_pass_url = $request->input('custom_forgot_pass_url');
$setting->ldap_phone_field = $request->input('ldap_phone');
+ $setting->ldap_mobile = $request->input('ldap_mobile');
$setting->ldap_jobtitle = $request->input('ldap_jobtitle');
+ $setting->ldap_address = $request->input('ldap_address');
+ $setting->ldap_city = $request->input('ldap_city');
+ $setting->ldap_state = $request->input('ldap_state');
+ $setting->ldap_zip = $request->input('ldap_zip');
$setting->ldap_country = $request->input('ldap_country');
$setting->ldap_location = $request->input('ldap_location');
$setting->ldap_dept = $request->input('ldap_dept');
@@ -896,7 +930,7 @@ public function getSamlSettings() : View
* @since v5.0.0
*/
public function postSamlSettings(SettingsSamlRequest $request) : RedirectResponse
- {
+ {
if (is_null($setting = Setting::getSettings())) {
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));
}
@@ -1056,6 +1090,7 @@ public function downloadFile($filename = null) : RedirectResponse | BinaryFileRe
if (! config('app.lock_passwords')) {
if (Storage::exists($path.'/'.$filename)) {
+ Log::warning('User '.auth()->user()->username.' is attempting to download backup file: '.$filename);
return StorageHelper::downloader($path.'/'.$filename);
} else {
// Redirect to the backup page
@@ -1083,6 +1118,7 @@ public function deleteFile($filename = null) : RedirectResponse
if (Storage::exists($path . '/' . $filename)) {
try {
+ Log::warning('User '.auth()->user()->username.' is attempting to delete backup file: '.$filename);
Storage::delete($path . '/' . $filename);
return redirect()->route('settings.backups.index')->with('success', trans('admin/settings/message.backup.file_deleted'));
} catch (\Exception $e) {
@@ -1162,7 +1198,7 @@ public function postRestore(Request $request, $filename = null): RedirectRespons
'--force' => true,
]);
- Log::debug('Attempting to restore from: '. storage_path($path).'/'.$filename);
+ Log::warning('User '.auth()->user()->username.' is attempting to restore from: '. storage_path($path).'/'.$filename);
$restore_params = [
'--force' => true,
@@ -1311,9 +1347,11 @@ public function ajaxTestEmail() : JsonResponse
'name' => config('mail.from.name'),
'email' => config('mail.from.address'),
])->notify(new MailTest());
-
+ Log::debug('Attempting to send mail to '.config('mail.from.address'));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('mail_sent.mail_sent')));
} catch (\Exception $e) {
+ Log::error('Mail sent from '.config('mail.from.address') .' with errors '. $e->getMessage());
+ Log::debug($e);
return response()->json(Helper::formatStandardApiResponse('success', null, $e->getMessage()));
}
}
diff --git a/app/Http/Controllers/StatuslabelsController.php b/app/Http/Controllers/StatuslabelsController.php
index 21a7c798b97f..6fd261206c9e 100755
--- a/app/Http/Controllers/StatuslabelsController.php
+++ b/app/Http/Controllers/StatuslabelsController.php
@@ -26,14 +26,10 @@ public function index() : View
return view('statuslabels.index');
}
- public function show($id) : View | RedirectResponse
+ public function show(Statuslabel $statuslabel) : View | RedirectResponse
{
$this->authorize('view', Statuslabel::class);
- if ($statuslabel = Statuslabel::find($id)) {
- return view('statuslabels.view')->with('statuslabel', $statuslabel);
- }
-
- return redirect()->route('statuslabels.index')->with('error', trans('admin/statuslabels/message.does_not_exist'));
+ return view('statuslabels.view')->with('statuslabel', $statuslabel);
}
/**
@@ -91,20 +87,15 @@ public function store(Request $request) : RedirectResponse
*
* @param int $statuslabelId
*/
- public function edit($statuslabelId = null) : View | RedirectResponse
+ public function edit(Statuslabel $statuslabel) : View | RedirectResponse
{
$this->authorize('update', Statuslabel::class);
- // Check if the Statuslabel exists
- if (is_null($item = Statuslabel::find($statuslabelId))) {
- // Redirect to the blogs management page
- return redirect()->route('statuslabels.index')->with('error', trans('admin/statuslabels/message.does_not_exist'));
- }
-
- $use_statuslabel_type = $item->getStatuslabelType();
$statuslabel_types = ['' => trans('admin/hardware/form.select_statustype')] + ['undeployable' => trans('admin/hardware/general.undeployable')] + ['pending' => trans('admin/hardware/general.pending')] + ['archived' => trans('admin/hardware/general.archived')] + ['deployable' => trans('admin/hardware/general.deployable')];
- return view('statuslabels/edit', compact('item', 'statuslabel_types'))->with('use_statuslabel_type', $use_statuslabel_type);
+ return view('statuslabels/edit', compact('statuslabel_types'))
+ ->with('item', $statuslabel)
+ ->with('use_statuslabel_type', $statuslabel);
}
/**
@@ -112,14 +103,9 @@ public function edit($statuslabelId = null) : View | RedirectResponse
*
* @param int $statuslabelId
*/
- public function update(Request $request, $statuslabelId = null) : RedirectResponse
+ public function update(Request $request, Statuslabel $statuslabel) : RedirectResponse
{
$this->authorize('update', Statuslabel::class);
- // Check if the Statuslabel exists
- if (is_null($statuslabel = Statuslabel::find($statuslabelId))) {
- // Redirect to the blogs management page
- return redirect()->route('statuslabels.index')->with('error', trans('admin/statuslabels/message.does_not_exist'));
- }
if (! $request->filled('statuslabel_types')) {
return redirect()->back()->withInput()->withErrors(['statuslabel_types' => trans('validation.statuslabel_type')]);
diff --git a/app/Http/Controllers/SuppliersController.php b/app/Http/Controllers/SuppliersController.php
index 605bb66f6d1b..2a3c73bebe8a 100755
--- a/app/Http/Controllers/SuppliersController.php
+++ b/app/Http/Controllers/SuppliersController.php
@@ -4,7 +4,6 @@
use App\Http\Requests\ImageUploadRequest;
use App\Models\Supplier;
-use Illuminate\Support\Facades\Auth;
use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View;
@@ -77,17 +76,10 @@ public function store(ImageUploadRequest $request) : RedirectResponse
*
* @param int $supplierId
*/
- public function edit($supplierId = null) : View | RedirectResponse
+ public function edit(Supplier $supplier) : View | RedirectResponse
{
$this->authorize('update', Supplier::class);
- // Check if the supplier exists
- if (is_null($item = Supplier::find($supplierId))) {
- // Redirect to the supplier page
- return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.does_not_exist'));
- }
-
- // Show the page
- return view('suppliers/edit', compact('item'));
+ return view('suppliers/edit')->with('item', $supplier);
}
/**
@@ -95,14 +87,9 @@ public function edit($supplierId = null) : View | RedirectResponse
*
* @param int $supplierId
*/
- public function update($supplierId, ImageUploadRequest $request) : RedirectResponse
+ public function update(ImageUploadRequest $request, Supplier $supplier) : RedirectResponse
{
$this->authorize('update', Supplier::class);
-
- if (is_null($supplier = Supplier::find($supplierId))) {
- return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.does_not_exist'));
- }
-
// Save the data
$supplier->name = request('name');
$supplier->address = request('address');
@@ -134,7 +121,7 @@ public function update($supplierId, ImageUploadRequest $request) : RedirectRespo
public function destroy($supplierId) : RedirectResponse
{
$this->authorize('delete', Supplier::class);
- if (is_null($supplier = Supplier::with('asset_maintenances', 'assets', 'licenses')->withCount('asset_maintenances as asset_maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->find($supplierId))) {
+ if (is_null($supplier = Supplier::with('maintenances', 'assets', 'licenses')->withCount('maintenances as maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->find($supplierId))) {
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.not_found'));
}
@@ -142,8 +129,8 @@ public function destroy($supplierId) : RedirectResponse
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count]));
}
- if ($supplier->asset_maintenances_count > 0) {
- return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_maintenances', ['asset_maintenances_count' => $supplier->asset_maintenances_count]));
+ if ($supplier->maintenances_count > 0) {
+ return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_maintenances', ['maintenances_count' => $supplier->maintenances_count]));
}
if ($supplier->licenses_count > 0) {
@@ -163,15 +150,10 @@ public function destroy($supplierId) : RedirectResponse
* @param null $supplierId
* @internal param int $assetId
*/
- public function show($supplierId = null) : View | RedirectResponse
+ public function show(Supplier $supplier) : View | RedirectResponse
{
$this->authorize('view', Supplier::class);
- $supplier = Supplier::find($supplierId);
-
- if (isset($supplier->id)) {
- return view('suppliers/view', compact('supplier'));
- }
+ return view('suppliers/view', compact('supplier'));
- return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.does_not_exist'));
}
}
diff --git a/app/Http/Controllers/UploadedFilesController.php b/app/Http/Controllers/UploadedFilesController.php
new file mode 100644
index 000000000000..8a9f4304adfc
--- /dev/null
+++ b/app/Http/Controllers/UploadedFilesController.php
@@ -0,0 +1,162 @@
+]
+ */
+ public function store(UploadFileRequest $request, $object_type, $id) : RedirectResponse
+ {
+
+ // Check the permissions to make sure the user can view the object
+ $object = self::$map_object_type[$object_type]::withTrashed()->find($id);
+ $this->authorize('update', $object);
+
+ if (!$object) {
+ return redirect()->back()->withFragment('files')->with('error',trans('general.file_upload_status.invalid_object'));
+ }
+
+ // If the file storage directory doesn't exist, create it
+ if (! Storage::exists(self::$map_storage_path[$object_type])) {
+ Storage::makeDirectory(self::$map_storage_path[$object_type], 775);
+ }
+
+
+ if ($request->hasFile('file')) {
+ // Loop over the attached files and add them to the object
+ foreach ($request->file('file') as $file) {
+ $file_name = $request->handleFile(self::$map_storage_path[$object_type], self::$map_file_prefix[$object_type].'-'.$object->id, $file);
+ $files[] = $file_name;
+ $object->logUpload($file_name, $request->get('notes'));
+ }
+
+ $files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')
+ ->where('item_type', '=', self::$map_object_type[$object_type])
+ ->where('item_id', '=', $id)->whereIn('filename', $files)
+ ->get();
+
+ return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.upload.success', count($files)));
+ }
+
+ // No files were submitted
+ return redirect()->back()->withFragment('files')->with('error', trans('general.file_upload_status.nofiles'));
+ }
+
+
+
+ /**
+ * Check for permissions and display the file.
+ * This isn't currently used, but is here for future use.
+ *
+ * @param \App\Http\Requests\UploadFileRequest $request
+ * @param string $object_type the type of object to upload the file to
+ * @param int $id the ID of the object to delete from so we can check permisisons
+ * @param $file_id the ID of the file to show from the action_logs table
+ * @since [v8.2.2]
+ * @author [A. Gianotto ]
+ */
+ public function show($object_type, $id, $file_id) : RedirectResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
+ {
+ // Check the permissions to make sure the user can view the object
+ $object = self::$map_object_type[$object_type]::withTrashed()->find($id);
+ $this->authorize('view', $object);
+
+ if (!$object) {
+ return redirect()->back()->withFragment('files')->with('error',trans('general.file_upload_status.invalid_object'));
+ }
+
+
+ // Check that the file being requested exists for the object
+ if (! $log = Actionlog::whereNotNull('filename')->where('item_type', self::$map_object_type[$object_type])->where('item_id', $object->id)->find($file_id))
+ {
+ return redirect()->back()->withFragment('files')->with('error', trans('general.file_upload_status.invalid_id'));
+ }
+
+
+ if (! Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename))
+ {
+ return redirect()->back()->withFragment('files')->with('error', trans('general.file_upload_status.file_not_found'));
+ }
+
+ if (request('inline') == 'true') {
+ $headers = [
+ 'Content-Disposition' => 'inline',
+ ];
+ return Storage::download(self::$map_storage_path[$object_type].'/'.$log->filename, $log->filename, $headers);
+ }
+
+ return StorageHelper::downloader(self::$map_storage_path[$object_type].'/'.$log->filename);
+
+ }
+
+ /**
+ * Delete the associated file
+ *
+ * @param \App\Http\Requests\UploadFileRequest $request
+ * @param string $object_type the type of object to upload the file to
+ * @param int $id the ID of the object to delete from so we can check permisisons
+ * @param $file_id the ID of the file to delete from the action_logs table
+ * @since [v8.2.2]
+ * @author [A. Gianotto ]
+ */
+ public function destroy($object_type, $id, $file_id) : RedirectResponse
+ {
+
+ // Check the permissions to make sure the user can view the object
+ $object = self::$map_object_type[$object_type]::withTrashed()->find($id);
+ $this->authorize('update', self::$map_object_type[$object_type]);
+
+ if (!$object) {
+ return redirect()->back()->withFragment('files')->with('error',trans('general.file_upload_status.invalid_object'));
+ }
+
+
+ // Check for the file
+ $log = Actionlog::where('id',$file_id)->where('item_type', self::$map_object_type[$object_type])
+ ->where('item_id', $object->id)->first();
+
+ if ($log) {
+ // Check the file actually exists, and delete it
+ if (Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
+ Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
+ }
+ // Delete the record of the file
+ if ($log->logUploadDelete($object, $log->filename)) {
+ return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.delete.success', 1));
+ }
+
+ }
+
+ // The file doesn't seem to really exist, so report an error
+ return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.delete.error', 1));
+
+ }
+
+}
diff --git a/app/Http/Controllers/Users/BulkUsersController.php b/app/Http/Controllers/Users/BulkUsersController.php
index 9ded0ee8ec8c..0269f3fd0cf8 100644
--- a/app/Http/Controllers/Users/BulkUsersController.php
+++ b/app/Http/Controllers/Users/BulkUsersController.php
@@ -15,6 +15,7 @@
use App\Models\Consumable;
use App\Models\Setting;
use App\Models\User;
+use App\Notifications\CurrentInventory;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
@@ -52,6 +53,28 @@ public function edit(Request $request)
return view('users/bulk-edit', compact('users'))
->with('groups', Group::pluck('name', 'id'));
+ // bulk send assigned inventory
+ } elseif ($request->input('bulk_actions') == 'send_assigned') {
+ $this->authorize('update', User::class);
+
+ $users_without_email = 0;
+ foreach ($users as $user) {
+ if (empty($user->email)) {
+ $users_without_email++;
+ } else {
+ $user->notify((new CurrentInventory($user)));
+ }
+ }
+
+ if ($users_without_email == 0) {
+ return redirect()->back()->with('success', trans_choice('admin/users/general.users_notified', $users->count()));
+ } else {
+ return redirect()->back()->with('warning', trans_choice('admin/users/general.users_notified_warning', $users->count(), ['no_email' => $users_without_email]));
+ }
+
+
+
+
// bulk delete, display the bulk delete confirmation form
} elseif ($request->input('bulk_actions') == 'delete') {
$this->authorize('delete', User::class);
@@ -275,7 +298,6 @@ public function destroy(Request $request)
$this->logItemCheckinAndDelete($assets, Asset::class);
$this->logAccessoriesCheckin($accessoryUserRows);
$this->logItemCheckinAndDelete($licenses, License::class);
- $this->logConsumablesCheckin($consumableUserRows);
Asset::whereIn('id', $assets->pluck('id'))->update([
'status_id' => e(request('status_id')),
@@ -337,21 +359,7 @@ private function logAccessoriesCheckin(Collection $accessoryUserRows): void
$logAction->item_type = Accessory::class;
$logAction->target_id = $accessoryUserRow->assigned_to;
$logAction->target_type = User::class;
- $logAction->created_at = auth()->id();
- $logAction->note = 'Bulk checkin items';
- $logAction->logaction('checkin from');
- }
- }
-
- private function logConsumablesCheckin(Collection $consumableUserRows): void
- {
- foreach ($consumableUserRows as $consumableUserRow) {
- $logAction = new Actionlog();
- $logAction->item_id = $consumableUserRow->consumable_id;
- $logAction->item_type = Consumable::class;
- $logAction->target_id = $consumableUserRow->assigned_to;
- $logAction->target_type = User::class;
- $logAction->created_at = auth()->id();
+ $logAction->created_by = auth()->id();
$logAction->note = 'Bulk checkin items';
$logAction->logaction('checkin from');
}
diff --git a/app/Http/Controllers/Users/UserFilesController.php b/app/Http/Controllers/Users/UserFilesController.php
deleted file mode 100644
index cae4074dd5cb..000000000000
--- a/app/Http/Controllers/Users/UserFilesController.php
+++ /dev/null
@@ -1,146 +0,0 @@
-]
- * @since [v1.6]
- */
- public function store(UploadFileRequest $request, $userId = null)
- {
- $user = User::find($userId);
- $destinationPath = config('app.private_uploads').'/users';
-
- if (isset($user->id)) {
- $this->authorize('update', $user);
-
- $logActions = [];
- $files = $request->file('file');
-
- if (is_null($files)) {
- return redirect()->back()->with('error', trans('admin/users/message.upload.nofiles'));
- }
- foreach ($files as $file) {
- $file_name = $request->handleFile('private_uploads/users/', 'user-'.$user->id, $file);
-
- //Log the uploaded file to the log
- $logAction = new Actionlog();
- $logAction->item_id = $user->id;
- $logAction->item_type = User::class;
- $logAction->created_by = auth()->id();
- $logAction->note = $request->input('notes');
- $logAction->target_id = null;
- $logAction->created_at = date("Y-m-d H:i:s");
- $logAction->filename = $file_name;
- $logAction->action_type = 'uploaded';
-
- if (! $logAction->save()) {
- return JsonResponse::create(['error' => 'Failed validation: '.print_r($logAction->getErrors(), true)], 500);
- }
- $logActions[] = $logAction;
- }
- // dd($logActions);
- return redirect()->back()->withFragment('files')->with('success', trans('admin/users/message.upload.success'));
- }
- return redirect()->back()->with('error', trans('admin/users/message.upload.nofiles'));
-
-
- }
-
- /**
- * Delete file
- *
- * @author [A. Gianotto] []
- * @since [v1.6]
- * @param int $userId
- * @param int $fileId
- * @return \Illuminate\Http\RedirectResponse
- * @throws \Illuminate\Auth\Access\AuthorizationException
- */
- public function destroy($userId = null, $fileId = null)
- {
- if ($user = User::find($userId)) {
-
- $this->authorize('delete', $user);
- $rel_path = 'private_uploads/users';
-
-
- if ($log = Actionlog::find($fileId)) {
- $filename = $log->filename;
- $log->delete();
-
- if (Storage::exists($rel_path.'/'.$filename)) {
- Storage::delete($rel_path.'/'.$filename);
- return redirect()->back()->withFragment('files')->with('success', trans('admin/users/message.deletefile.success'));
- }
-
- }
-
- // The log record doesn't exist somehow
- return redirect()->back()->with('success', trans('admin/users/message.deletefile.success'));
- }
-
- return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', ['id' => $userId]));
-
- }
-
- /**
- * Display/download the uploaded file
- *
- * @author [A. Gianotto] []
- * @since [v1.6]
- * @param int $userId
- * @param int $fileId
- * @return mixed
- * @throws \Illuminate\Auth\Access\AuthorizationException
- */
- public function show($userId = null, $fileId = null)
- {
-
-
- if (empty($fileId)) {
- return redirect()->route('users.show')->with('error', 'Invalid file request');
- }
-
- if ($user = User::find($userId)) {
-
- $this->authorize('view', $user);
-
- if ($log = Actionlog::whereNotNull('filename')->where('item_id', $user->id)->find($fileId)) {
- $file = 'private_uploads/users/'.$log->filename;
-
- try {
- return StorageHelper::showOrDownloadFile($file, $log->filename);
- } catch (\Exception $e) {
- return redirect()->route('users.show', ['user' => $user])->with('error', trans('general.file_not_found'));
- }
- }
-
- // The log record doesn't exist somehow
- return redirect()->route('users.show', ['user' => $user])->with('error', trans('general.log_record_not_found'));
-
-
- return redirect()->back()->with('error', trans('general.file_not_found'));
- }
-
- // Redirect to the user management page if the user doesn't exist
- return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', ['id' => $userId]));
- }
-
-}
diff --git a/app/Http/Controllers/Users/UsersController.php b/app/Http/Controllers/Users/UsersController.php
index 397bfd16d8b7..f07f9de20400 100755
--- a/app/Http/Controllers/Users/UsersController.php
+++ b/app/Http/Controllers/Users/UsersController.php
@@ -14,14 +14,9 @@
use App\Models\Setting;
use App\Models\User;
use App\Notifications\WelcomeNotification;
-use Illuminate\Support\Facades\Auth;
-use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password;
-use Illuminate\Support\Facades\Storage;
-use Redirect;
-use Str;
use Symfony\Component\HttpFoundation\StreamedResponse;
use App\Notifications\CurrentInventory;
@@ -95,6 +90,7 @@ public function store(SaveUserRequest $request)
//Username, email, and password need to be handled specially because the need to respect config values on an edit.
$user->email = trim($request->input('email'));
$user->username = trim($request->input('username'));
+ $user->display_name = $request->input('display_name');
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
@@ -105,6 +101,7 @@ public function store(SaveUserRequest $request)
$user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle');
$user->phone = $request->input('phone');
+ $user->mobile = $request->input('mobile');
$user->location_id = $request->input('location_id', null);
$user->department_id = $request->input('department_id', null);
$user->company_id = Company::getIdForUser($request->input('company_id', null));
@@ -130,31 +127,38 @@ public function store(SaveUserRequest $request)
}
$user->permissions = json_encode($permissions_array);
- // we have to invoke the
+ // we have to invoke the form request here to handle image uploads
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
- session()->put(['redirect_option' => $request->get('redirect_option')]);
+ \Log::info("About to call customFill, in the 'store' controller!!!");
+ $user->customFill($request, Auth::user());
+ if ($request->get('redirect_option') === 'back') {
+ session()->put(['redirect_option' => 'index']);
+ } else {
+ session()->put(['redirect_option' => $request->get('redirect_option')]);
+ }
if ($user->save()) {
+
+ if (($user->activated == '1') && ($user->email != '') && ($request->input('send_welcome') == '1')) {
+
+ try {
+ $user->notify(new WelcomeNotification($user));
+ } catch (\Exception $e) {
+ Log::warning('Could not send welcome notification for user: ' . $e->getMessage());
+ }
+
+
+ }
+
if ($request->filled('groups')) {
$user->groups()->sync($request->input('groups'));
} else {
$user->groups()->sync([]);
}
- if (($request->input('email_user') == 1) && ($request->filled('email'))) {
- // Send the credentials through email
- $data = [];
- $data['email'] = e($request->input('email'));
- $data['username'] = e($request->input('username'));
- $data['first_name'] = e($request->input('first_name'));
- $data['last_name'] = e($request->input('last_name'));
- $data['password'] = e($request->input('password'));
-
- $user->notify(new WelcomeNotification($data));
- }
-
- return redirect()->to(Helper::getRedirectOption($request, $user->id, 'Users'))->with('success', trans('admin/users/message.success.create'));
+ return Helper::getRedirectOption($request, $user->id, 'Users')
+ ->with('success', trans('admin/users/message.success.create'));
}
return redirect()->back()->withInput()->withErrors($user->getErrors());
@@ -175,21 +179,26 @@ private function filterDisplayable($permissions)
/**
* Returns a view that displays the edit user form
*
- * @author [A. Gianotto] []
- * @since [v1.0]
* @param $permissions
- * @return \Illuminate\Contracts\View\View
- * @internal param int $id
+ * @return \Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
+ *@since [v1.0]
+ * @internal param int $id
+ * @author [A. Gianotto] []
*/
- public function edit($id)
+ public function edit(User $user)
{
$this->authorize('update', User::class);
- $user = User::with(['assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc'])->withTrashed()->find($id);
+ session()->put('back_url', url()->previous());
+ $user = User::with(['assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc'])->withTrashed()->find($user->id);
if ($user) {
+ if ($user->trashed()) {
+ return redirect()->route('users.show', $user->id);
+ }
+
$permissions = config('permissions');
$groups = Group::pluck('name', 'id');
@@ -201,7 +210,6 @@ public function edit($id)
return view('users/edit', compact('user', 'groups', 'userGroups', 'permissions', 'userPermissions'))->with('item', $user);
}
- return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', compact('id')));
}
/**
@@ -243,22 +251,18 @@ public function update(SaveUserRequest $request, User $user)
}
}
- // Only save groups if the user is a superuser
- if (auth()->user()->isSuperUser()) {
- $user->groups()->sync($request->input('groups'));
- }
// Update the user fields
- $user->username = trim($request->input('username'));
- $user->email = trim($request->input('email'));
+
$user->first_name = $request->input('first_name');
$user->last_name = $request->input('last_name');
+ $user->display_name = $request->input('display_name');
$user->two_factor_optin = $request->input('two_factor_optin') ?: 0;
$user->locale = $request->input('locale');
$user->employee_num = $request->input('employee_num');
- $user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle', null);
$user->phone = $request->input('phone');
+ $user->mobile = $request->input('mobile');
$user->location_id = $request->input('location_id', null);
$user->company_id = Company::getIdForUser($request->input('company_id', null));
$user->manager_id = $request->input('manager_id', null);
@@ -268,8 +272,6 @@ public function update(SaveUserRequest $request, User $user)
$user->city = $request->input('city', null);
$user->state = $request->input('state', null);
$user->country = $request->input('country', null);
- // if a user is editing themselves we should always keep activated true
- $user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0);
$user->zip = $request->input('zip', null);
$user->remote = $request->input('remote', 0);
$user->vip = $request->input('vip', 0);
@@ -278,16 +280,44 @@ public function update(SaveUserRequest $request, User $user)
$user->end_date = $request->input('end_date', null);
$user->autoassign_licenses = $request->input('autoassign_licenses', 0);
+ // Set this here so that we can overwrite it later if the user is an admin or superadmin
+ $user->activated = $request->input('activated', auth()->user()->is($user) ? 1 : $user->activated);
+
+
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)
->update(['location_id' => $request->input('location_id', null)]);
- // Do we want to update the user password?
- if ($request->filled('password')) {
- $user->password = bcrypt($request->input('password'));
+ // check for permissions related fields and only set them if the user has permission to edit them
+ if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
+
+ $user->username = trim($request->input('username'));
+ $user->email = trim($request->input('email'));
+ $user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0);
+
+ // Do we want to update the user password?
+ if ($request->filled('password')) {
+ $user->password = bcrypt($request->input('password'));
+ }
+
+ $permissions_array = $request->input('permission');
+
+ // Strip out the superuser permission if the user isn't a superadmin
+ if (!auth()->user()->isSuperUser()) {
+ unset($permissions_array['superuser']);
+ $permissions_array['superuser'] = $orig_superuser;
+ }
+
+ $user->permissions = json_encode($permissions_array);
+
+ // Only save groups if the user is a superuser
+ if (auth()->user()->isSuperUser()) {
+ $user->groups()->sync($request->input('groups'));
+ }
}
+
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)
@@ -301,6 +331,14 @@ public function update(SaveUserRequest $request, User $user)
$permissions_array['superuser'] = $orig_superuser;
}
+ $permissions_array = $request->input('permission');
+
+ // Strip out the superuser permission if the user isn't a superadmin
+ if (!Auth::user()->isSuperUser()) {
+ unset($permissions_array['superuser']);
+ $permissions_array['superuser'] = $orig_superuser;
+ }
+
$user->permissions = json_encode($permissions_array);
// Handle uploaded avatar
@@ -309,9 +347,21 @@ public function update(SaveUserRequest $request, User $user)
if ($user->save()) {
// Redirect to the user page
- return redirect()->to(Helper::getRedirectOption($request, $user->id, 'Users'))
+ return Helper::getRedirectOption($request, $user->id, 'Users')
->with('success', trans('admin/users/message.success.update'));
}
+
+ \Log::debug("calling custom fill from the UPDATE method!");
+ $user->customFill($request, Auth::user());
+ //\Log::debug(print_r($user, true));
+
+ // Was the user updated?
+ if ($user->save()) {
+ // Redirect to the user page
+ return redirect()->route('users.index')
+ ->with('success', trans('admin/users/message.success.update'));
+ }
+
return redirect()->back()->withInput()->withErrors($user->getErrors());
}
@@ -324,7 +374,7 @@ public function update(SaveUserRequest $request, User $user)
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function destroy(DeleteUserRequest $request, $id = null)
+ public function destroy(DeleteUserRequest $request, $id)
{
$this->authorize('delete', User::class);
@@ -333,13 +383,6 @@ public function destroy(DeleteUserRequest $request, $id = null)
$this->authorize('delete', $user);
if ($user->delete()) {
- if (Storage::disk('public')->exists('avatars/' . $user->avatar)) {
- try {
- Storage::disk('public')->delete('avatars/' . $user->avatar);
- } catch (\Exception $e) {
- Log::debug($e);
- }
- }
return redirect()->route('users.index')->with('success', trans('admin/users/message.success.delete'));
}
}
@@ -398,23 +441,27 @@ public function getRestore($id = null)
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function show($userId = null)
+ public function show(User $user)
{
// Make sure the user can view users at all
$this->authorize('view', User::class);
- $user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($userId);
+ $user = User::with([
+ 'consumables',
+ 'accessories',
+ 'licenses',
+ 'userloc',
+ ])
+ ->withTrashed()
+ ->find($user->id);
// Make sure they can view this particular user
$this->authorize('view', $user);
- if ($user) {
- $userlog = $user->userlog->load('item');
- return view('users/view', compact('user', 'userlog'))->with('settings', Setting::getSettings());
- }
-
- return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', ['id' => $userId]));
-
+ return view('users/view', [
+ 'user' => $user,
+ 'settings' => Setting::getSettings(),
+ ]);
}
@@ -428,7 +475,7 @@ public function show($userId = null)
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
- public function getClone(Request $request, $id = null)
+ public function getClone(Request $request, User $user)
{
$this->authorize('create', User::class);
@@ -438,7 +485,7 @@ public function getClone(Request $request, $id = null)
app('request')->request->set('permissions', $permissions);
- $user_to_clone = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($id);
+ $user_to_clone = User::with('userloc')->withTrashed()->find($user->id);
// Make sure they can view this particular user
$this->authorize('view', $user_to_clone);
@@ -453,6 +500,8 @@ public function getClone(Request $request, $id = null)
$user->last_name = '';
$user->email = substr($user->email, ($pos = strpos($user->email, '@')) !== false ? $pos : 0);
$user->id = null;
+ $user->username = null;
+ $user->avatar = null;
// Get this user's groups
$userGroups = $user_to_clone->groups()->pluck('name', 'id');
@@ -468,10 +517,10 @@ public function getClone(Request $request, $id = null)
->with('user', $user)
->with('groups', Group::pluck('name', 'id'))
->with('userGroups', $userGroups)
- ->with('clone_user', $user_to_clone);
+ ->with('cloned_model', $user_to_clone)
+ ->with('item', $user);
}
- return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', compact('id')));
}
@@ -510,6 +559,8 @@ public function getExportUserCsv()
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'),
@@ -555,10 +606,12 @@ public function getExportUserCsv()
($user->company) ? $user->company->name : '',
$user->jobtitle,
$user->employee_num,
- $user->present()->fullName(),
+ $user->first_name,
+ $user->last_name,
+ $user->display_name,
$user->username,
$user->email,
- ($user->manager) ? $user->manager->present()->fullName() : '',
+ ($user->manager) ? $user->manager->display_name : '',
($user->userloc) ? $user->userloc->name : '',
($user->department) ? $user->department->name : '',
$user->assets->count(),
@@ -598,18 +651,18 @@ public function printInventory($id)
$user = User::where('id', $id)
->with([
- 'assets.assetlog',
- 'assets.assignedAssets.assetlog',
+ 'assets.log' => fn($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
+ 'assets.assignedAssets.log' => fn($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
'assets.assignedAssets.defaultLoc',
'assets.assignedAssets.location',
'assets.assignedAssets.model.category',
'assets.defaultLoc',
'assets.location',
'assets.model.category',
- 'accessories.assetlog',
+ 'accessories.log' => fn($query) => $query->withTrashed()->where('target_type', User::class)->where('target_id', $id)->where('action_type', 'accepted'),
'accessories.category',
'accessories.manufacturer',
- 'consumables.assetlog',
+ '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',
diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php
index 12c300e5bd74..2b767650ade3 100755
--- a/app/Http/Controllers/ViewAssetsController.php
+++ b/app/Http/Controllers/ViewAssetsController.php
@@ -2,18 +2,21 @@
namespace App\Http\Controllers;
+use App\Actions\CheckoutRequests\CancelCheckoutRequestAction;
+use App\Actions\CheckoutRequests\CreateCheckoutRequestAction;
+use App\Exceptions\AssetNotRequestable;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\AssetModel;
-use App\Models\Company;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\RequestAssetCancelation;
use App\Notifications\RequestAssetNotification;
+use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View;
-use Log;
+use Exception;
/**
* This controller handles all actions related to the ability for users
@@ -24,50 +27,126 @@
class ViewAssetsController extends Controller
{
/**
- * Redirect to the profile page.
+ * Extract custom fields that should be displayed in user view.
*
+ * @param User $user
+ * @return array
*/
- public function getIndex() : View | RedirectResponse
+ private function extractCustomFields(User $user): array
{
- $user = User::with(
- 'assets',
- 'assets.model',
- 'assets.model.fieldset.fields',
- 'consumables',
- 'accessories',
- 'licenses',
- )->find(auth()->id());
-
- $field_array = array();
-
- // Loop through all the custom fields that are applied to any model the user has assigned
+ $fieldArray = [];
foreach ($user->assets as $asset) {
-
- // Make sure the model has a custom fieldset before trying to loop through the associated fields
- if ($asset->model->fieldset) {
-
+ if ($asset->model && $asset->model->fieldset) {
foreach ($asset->model->fieldset->fields as $field) {
- // check and make sure they're allowed to see the value of the custom field
if ($field->display_in_user_view == '1') {
- $field_array[$field->db_column] = $field->name;
+ $fieldArray[$field->db_column] = $field->name;
}
-
}
}
+ }
+ return array_unique($fieldArray);
+ }
+
+ /**
+ * Get list of users viewable by the current user.
+ *
+ * @param User $authUser
+ * @return \Illuminate\Support\Collection
+ */
+ private function getViewableUsers(User $authUser): \Illuminate\Support\Collection
+ {
+ // SuperAdmin sees all users
+ if ($authUser->isSuperUser()) {
+ return User::select('id', 'first_name', 'last_name', 'username')
+ ->where('activated', 1)
+ ->orderBy('last_name')
+ ->orderBy('first_name')
+ ->get();
+ }
+
+ // Regular manager sees only their subordinates + self
+ $managedUsers = $authUser->getAllSubordinates();
+
+ // If user has subordinates, show them with self at beginning
+ if ($managedUsers->count() > 0) {
+ return collect([$authUser])->merge($managedUsers)
+ ->sortBy('last_name')
+ ->sortBy('first_name');
+ }
+
+ // User has no subordinates, only sees themselves
+ return collect([$authUser]);
+ }
+
+ /**
+ * Get the selected user ID from request or default to current user.
+ *
+ * @param Request $request
+ * @param \Illuminate\Support\Collection $subordinates
+ * @param int $defaultUserId
+ * @return int
+ */
+ private function getSelectedUserId(Request $request, \Illuminate\Support\Collection $subordinates, int $defaultUserId): int
+ {
+ // If no subordinates or no user_id in request, return default
+ if ($subordinates->count() <= 1 || !$request->filled('user_id')) {
+ return $defaultUserId;
+ }
+
+ $requestedUserId = (int) $request->input('user_id');
+
+ // Validate if the requested user is allowed
+ if ($subordinates->contains('id', $requestedUserId)) {
+ return $requestedUserId;
+ }
+
+ // If invalid ID or not authorized, return default
+ return $defaultUserId;
+ }
+
+ /**
+ * Show user's assigned assets with optional manager view functionality.
+ *
+ */
+ public function getIndex(Request $request) : View | RedirectResponse
+ {
+ $authUser = auth()->user();
+ $settings = Setting::getSettings();
+ $subordinates = collect();
+ $selectedUserId = $authUser->id;
+ // Process manager view if enabled
+ if ($settings->manager_view_enabled) {
+ $subordinates = $this->getViewableUsers($authUser);
+ $selectedUserId = $this->getSelectedUserId($request, $subordinates, $authUser->id);
}
- // Since some models may re-use the same fieldsets/fields, let's make the array unique so we don't repeat columns
- array_unique($field_array);
+ // Load the data for the user to be viewed (either auth user or selected subordinate)
+ $userToView = User::with([
+ 'assets',
+ 'assets.model',
+ 'assets.model.fieldset.fields',
+ 'consumables',
+ 'accessories',
+ 'licenses'
+ ])->find($selectedUserId);
- if (isset($user->id)) {
- return view('account/view-assets', compact('user', 'field_array' ))
- ->with('settings', Setting::getSettings());
+ // If the user to view couldn't be found (shouldn't happen with proper logic), redirect with error
+ if (!$userToView) {
+ return redirect()->route('view-assets')->with('error', trans('admin/users/message.user_not_found'));
}
- // Redirect to the user management page
- return redirect()->route('users.index')
- ->with('error', trans('admin/users/message.user_not_found', $user->id));
+ // Process custom fields for the user being viewed
+ $fieldArray = $this->extractCustomFields($userToView);
+
+ // Pass the necessary data to the view
+ return view('account/view-assets', [
+ 'user' => $userToView, // Use 'user' for compatibility with the existing view
+ 'field_array' => $fieldArray,
+ 'settings' => $settings,
+ 'subordinates' => $subordinates,
+ 'selectedUserId' => $selectedUserId
+ ]);
}
/**
@@ -81,7 +160,7 @@ public function getRequestableIndex() : View
return view('account/requestable-assets', compact('assets', 'models'));
}
- public function getRequestItem(Request $request, $itemType, $itemId = null, $cancel_by_admin = false, $requestingUser = null) : RedirectResponse
+ public function getRequestItem(Request $request, $itemType, $itemId = null, $cancel_by_admin = false, $requestingUser = null): RedirectResponse
{
$item = null;
$fullItemType = 'App\\Models\\'.studly_case($itemType);
@@ -106,7 +185,7 @@ public function getRequestItem(Request $request, $itemType, $itemId = null, $can
$logaction->target_type = User::class;
$data['item_quantity'] = $request->has('request-quantity') ? e($request->input('request-quantity')) : 1;
- $data['requested_by'] = $user->present()->fullName();
+ $data['requested_by'] = $user->display_name;
$data['item'] = $item;
$data['item_type'] = $itemType;
$data['target'] = auth()->user();
@@ -144,63 +223,33 @@ public function getRequestItem(Request $request, $itemType, $itemId = null, $can
* Process a specific requested asset
* @param null $assetId
*/
- public function getRequestAsset($assetId = null) : RedirectResponse
+ public function store(Asset $asset): RedirectResponse
{
- $user = auth()->user();
-
- // Check if the asset exists and is requestable
- if (is_null($asset = Asset::RequestableAssets()->find($assetId))) {
- return redirect()->route('requestable-assets')
- ->with('error', trans('admin/hardware/message.does_not_exist_or_not_requestable'));
- }
- if (! Company::isCurrentUserHasAccess($asset)) {
- return redirect()->route('requestable-assets')
- ->with('error', trans('general.insufficient_permissions'));
- }
-
- $data['item'] = $asset;
- $data['target'] = auth()->user();
- $data['item_quantity'] = 1;
- $settings = Setting::getSettings();
-
- $logaction = new Actionlog();
- $logaction->item_id = $data['asset_id'] = $asset->id;
- $logaction->item_type = $data['item_type'] = Asset::class;
- $logaction->created_at = $data['requested_date'] = date('Y-m-d H:i:s');
-
- if ($user->location_id) {
- $logaction->location_id = $user->location_id;
- }
- $logaction->target_id = $data['user_id'] = auth()->id();
- $logaction->target_type = User::class;
-
- // If it's already requested, cancel the request.
- if ($asset->isRequestedBy(auth()->user())) {
- $asset->cancelRequest();
- $asset->decrement('requests_counter', 1);
-
- $logaction->logaction('request canceled');
- try {
- $settings->notify(new RequestAssetCancelation($data));
- } catch (\Exception $e) {
- Log::warning($e);
- }
- return redirect()->route('requestable-assets')
- ->with('success')->with('success', trans('admin/hardware/message.requests.canceled'));
+ try {
+ CreateCheckoutRequestAction::run($asset, auth()->user());
+ return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.success'));
+ } catch (AssetNotRequestable $e) {
+ return redirect()->back()->with('error', 'Asset is not requestable');
+ } catch (AuthorizationException $e) {
+ return redirect()->back()->with('error', trans('admin/hardware/message.requests.error'));
+ } catch (Exception $e) {
+ report($e);
+ return redirect()->back()->with('error', trans('general.something_went_wrong'));
}
+ }
- $logaction->logaction('requested');
- $asset->request();
- $asset->increment('requests_counter', 1);
+ public function destroy(Asset $asset): RedirectResponse
+ {
try {
- $settings->notify(new RequestAssetNotification($data));
- } catch (\Exception $e) {
- Log::warning($e);
+ CancelCheckoutRequestAction::run($asset, auth()->user());
+ return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.canceled'));
+ } catch (Exception $e) {
+ report($e);
+ return redirect()->back()->with('error', trans('general.something_went_wrong'));
}
-
- return redirect()->route('requestable-assets')->with('success')->with('success', trans('admin/hardware/message.requests.success'));
}
+
public function getRequestedAssets() : View
{
return view('account/requested');
diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
index b69e22e4f912..729fe75175b3 100644
--- a/app/Http/Kernel.php
+++ b/app/Http/Kernel.php
@@ -73,6 +73,7 @@ class Kernel extends HttpKernel
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
+ 'api-throttle' => \App\Http\Middleware\SetAPIResponseHeaders::class,
'health' => null,
];
}
diff --git a/app/Http/Middleware/SecurityHeaders.php b/app/Http/Middleware/SecurityHeaders.php
index 8e6c17b4e753..e740c766788d 100644
--- a/app/Http/Middleware/SecurityHeaders.php
+++ b/app/Http/Middleware/SecurityHeaders.php
@@ -26,7 +26,6 @@ public function handle($request, Closure $next)
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
- $response->headers->set('X-XSS-Protection', '1; mode=block');
// Ugh. Feature-Policy is dumb and clumsy and mostly irrelevant for Snipe-IT,
// since we don't provide any way to IFRAME anything in in the first place.
diff --git a/app/Http/Middleware/SetAPIResponseHeaders.php b/app/Http/Middleware/SetAPIResponseHeaders.php
new file mode 100644
index 000000000000..ac277e785c4e
--- /dev/null
+++ b/app/Http/Middleware/SetAPIResponseHeaders.php
@@ -0,0 +1,82 @@
+headers->get('X-RateLimit-Remaining')) &&
+ (int) $response->headers->get('X-RateLimit-Remaining') <= (int) $remainingAttempts) {
+ $headers = [];
+ $headers['Retry-After'] = $retryAfter; // this is the only line we changed
+ $headers['X-RateLimit-Reset'] = $retryAfter; // this is the only line we changed
+ $headers['X-RateLimit-Reset-Timestamp'] = $this->availableAt($retryAfter); // this is the only line we changed
+ return $headers;
+ }
+
+ $headers = [
+ 'X-RateLimit-Limit' => $maxAttempts,
+ 'X-RateLimit-Remaining' => $remainingAttempts,
+ ];
+
+ if (! is_null($retryAfter)) {
+ $headers['Retry-After'] = $retryAfter;
+ $headers['X-RateLimit-Reset'] = $retryAfter; // this is the only line we changed
+ $headers['X-RateLimit-Reset-Timestamp'] = $this->availableAt($retryAfter); // this is the only line we changed
+ }
+
+ return $headers;
+ }
+
+
+
+ /**
+ * Handle an incoming request.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @param \Closure $next
+ * @return mixed
+ */
+ protected function handleRequest($request, Closure $next, array $limits)
+ {
+ foreach ($limits as $limit) {
+ if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) {
+ throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
+ }
+
+ $this->limiter->hit($limit->key, $limit->decaySeconds);
+ }
+
+ $response = $next($request);
+
+ foreach ($limits as $limit) {
+ $response = $this->addHeaders(
+ $response,
+ $limit->maxAttempts,
+ $this->calculateRemainingAttempts($limit->key, $limit->maxAttempts),
+ $this->getTimeUntilNextRetry($limit->key) // this is the only line we changed
+ );
+ }
+
+ return $response;
+ }
+
+}
\ No newline at end of file
diff --git a/app/Http/Requests/CustomFieldRequest.php b/app/Http/Requests/CustomFieldRequest.php
index 0c2ec0ae60bd..c8da020803d5 100644
--- a/app/Http/Requests/CustomFieldRequest.php
+++ b/app/Http/Requests/CustomFieldRequest.php
@@ -34,12 +34,14 @@ public function rules(Request $request)
case 'POST':
{
$rules['name'] = 'required|unique:custom_fields';
+ $rules['tab'] = 'required';
break;
}
// Save all fields
case 'PUT':
$rules['name'] = 'required';
+ $rules['tab'] = 'required';
break;
// Save only what's passed
diff --git a/app/Http/Requests/DeleteUserRequest.php b/app/Http/Requests/DeleteUserRequest.php
index 99574950d880..6d387917c633 100644
--- a/app/Http/Requests/DeleteUserRequest.php
+++ b/app/Http/Requests/DeleteUserRequest.php
@@ -24,7 +24,7 @@ public function authorize(): bool
public function prepareForValidation(): void
{
- $user_to_delete = User::withTrashed()->find(request()->route('user'));
+ $user_to_delete = User::withTrashed()->with('managesUsers')->find(request()->route('user'));
if ($user_to_delete) {
$this->merge([
@@ -61,7 +61,8 @@ public function rules(): array
public function messages(): array
{
- $user_to_delete = User::withTrashed()->find(request()->route('user'));
+ $user_to_delete = User::withTrashed()->with('managesUsers')->find(request()->route('user'));
+
$messages = [];
if ($user_to_delete) {
diff --git a/app/Http/Requests/ImageUploadRequest.php b/app/Http/Requests/ImageUploadRequest.php
index abb0cee5f75f..3a62212e3646 100644
--- a/app/Http/Requests/ImageUploadRequest.php
+++ b/app/Http/Requests/ImageUploadRequest.php
@@ -11,6 +11,7 @@
use Intervention\Image\Exception\NotReadableException;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
+use Illuminate\Support\Str;
class ImageUploadRequest extends Request
{
@@ -70,19 +71,25 @@ protected function base64FileKeys(): array
public function handleImages($item, $w = 600, $form_fieldname = 'image', $path = null, $db_fieldname = 'image')
{
- $type = strtolower(class_basename(get_class($item)));
+ $type = class_basename(get_class($item));
if (is_null($path)) {
- $path = str_plural($type);
+ $path = strtolower(str_plural($type));
- if ($type == 'assetmodel') {
+ if ($type == 'AssetModel') {
$path = 'models';
}
if ($type == 'user') {
$path = 'avatars';
}
+
+ }
+
+
+ if (!Storage::disk('public')->exists($path)) {
+ Storage::disk('public')->makeDirectory($path);
}
if ($this->offsetGet($form_fieldname) instanceof UploadedFile) {
@@ -93,10 +100,9 @@ public function handleImages($item, $w = 600, $form_fieldname = 'image', $path =
if (isset($image)) {
- if (!config('app.lock_passwords')) {
$ext = $image->guessExtension();
- $file_name = $type.'-'.$form_fieldname.'-'.$item->id.'-'.str_random(10).'.'.$ext;
+ $file_name = $type.'-'.$form_fieldname.($item->id ?? '-'.$item->id).'-'.str_random(10).'.'.$ext;
if (($image->getMimeType() == 'image/vnd.microsoft.icon') || ($image->getMimeType() == 'image/x-icon') || ($image->getMimeType() == 'image/avif') || ($image->getMimeType() == 'image/webp')) {
// If the file is an icon, webp or avif, we need to just move it since gd doesn't support resizing
@@ -138,7 +144,7 @@ public function handleImages($item, $w = 600, $form_fieldname = 'image', $path =
// Remove Current image if exists
$item = $this->deleteExistingImage($item, $path, $db_fieldname);
$item->{$db_fieldname} = $file_name;
- }
+
// If the user isn't uploading anything new but wants to delete their old image, do so
diff --git a/app/Http/Requests/SaveUserRequest.php b/app/Http/Requests/SaveUserRequest.php
index 5a47362cfc47..4051e9804395 100644
--- a/app/Http/Requests/SaveUserRequest.php
+++ b/app/Http/Requests/SaveUserRequest.php
@@ -33,9 +33,9 @@ public function response(array $errors)
public function rules()
{
$rules = [
- 'department_id' => 'nullable|exists:departments,id',
+ 'department_id' => 'nullable|integer|exists:departments,id',
'manager_id' => 'nullable|exists:users,id',
- 'company_id' => ['nullable','exists:companies,id']
+ 'company_id' => ['nullable', 'integer', 'exists:companies,id']
];
switch ($this->method()) {
diff --git a/app/Http/Requests/SettingsSamlRequest.php b/app/Http/Requests/SettingsSamlRequest.php
index 2ab876141a73..53a35210183d 100644
--- a/app/Http/Requests/SettingsSamlRequest.php
+++ b/app/Http/Requests/SettingsSamlRequest.php
@@ -41,6 +41,7 @@ public function rules()
public function withValidator($validator)
{
$validator->after(function ($validator) {
+ $setting = Setting::getSettings();
if ($this->input('saml_enabled') == '1') {
$idpMetadata = $this->input('saml_idp_metadata');
if (! empty($idpMetadata)) {
@@ -56,7 +57,7 @@ public function withValidator($validator)
}
}
- $was_custom_x509cert = strpos(Setting::getSettings()->saml_custom_settings, 'sp_x509cert') !== false;
+ $was_custom_x509cert = strpos($setting->saml_custom_settings, 'sp_x509cert') !== false;
$custom_x509cert = '';
$custom_privateKey = '';
@@ -108,7 +109,7 @@ public function withValidator($validator)
];
$pkey = openssl_pkey_new([
- 'private_key_bits' => 2048,
+ 'private_key_bits' => config('app.saml_key_size'),
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
@@ -126,10 +127,14 @@ public function withValidator($validator)
}
if (! (empty($x509cert) && empty($privateKey))) {
- $this->merge([
- 'saml_sp_x509cert' => $x509cert,
- 'saml_sp_privatekey' => $privateKey,
- ]);
+// $this->merge([
+// 'saml_sp_x509cert' => $x509cert,
+// 'saml_sp_privatekey' => $privateKey,
+// ]);
+ $setting->saml_sp_x509cert = $x509cert;
+ $setting->saml_sp_privatekey = $privateKey;
+ $setting->save();
+
}
} else {
$validator->errors()->add('saml_integration', 'openssl.cnf is missing/invalid');
@@ -145,15 +150,21 @@ public function withValidator($validator)
}
if (! empty($x509certNew)) {
- $this->merge([
- 'saml_sp_x509certNew' => $x509certNew,
- ]);
+// $this->merge([
+// 'saml_sp_x509certNew' => $x509certNew,
+// ]);
+ $setting->saml_sp_x509certNew = $x509certNew;
+ $setting->save();
}
} else {
- $this->merge([
- 'saml_sp_x509certNew' => '',
- ]);
+// $this->merge([
+// 'saml_sp_x509certNew' => '',
+// ]);
+ $setting->saml_sp_x509certNew = '';
+ $setting->save();
}
+
+
});
}
}
diff --git a/app/Http/Requests/StoreAssetModelRequest.php b/app/Http/Requests/StoreAssetModelRequest.php
index 635d45cf890d..0da47b0c7258 100644
--- a/app/Http/Requests/StoreAssetModelRequest.php
+++ b/app/Http/Requests/StoreAssetModelRequest.php
@@ -19,6 +19,7 @@ public function authorize(): bool
public function prepareForValidation(): void
{
+ parent::prepareForValidation();
if ($this->category_id) {
if ($category = Category::find($this->category_id)) {
diff --git a/app/Http/Requests/StoreAssetRequest.php b/app/Http/Requests/StoreAssetRequest.php
index fb7469ac88f9..66179ac739c4 100644
--- a/app/Http/Requests/StoreAssetRequest.php
+++ b/app/Http/Requests/StoreAssetRequest.php
@@ -39,7 +39,6 @@ public function prepareForValidation(): void
$this->merge([
'asset_tag' => $this->asset_tag ?? Asset::autoincrement_asset(),
'company_id' => $idForCurrentUser,
- 'assigned_to' => $assigned_to ?? null,
]);
}
diff --git a/app/Http/Requests/StoreLabelSettings.php b/app/Http/Requests/StoreLabelSettings.php
index 6a39418a6719..2b6fd83c6ff4 100644
--- a/app/Http/Requests/StoreLabelSettings.php
+++ b/app/Http/Requests/StoreLabelSettings.php
@@ -29,10 +29,16 @@ public function rules(): array
return $label->getName();
})->values()->toArray();
+ if (empty($this->input('label2_template'))) {
+ $this->merge([
+ 'label2_template' => 'DefaultLabel',
+ ]);
+ }
+
return [
'labels_per_page' => 'numeric',
- 'labels_width' => 'numeric',
- 'labels_height' => 'numeric',
+ 'labels_width' => 'numeric|min:0.1',
+ 'labels_height' => 'numeric|min:0.1',
'labels_pmargin_left' => 'numeric|nullable',
'labels_pmargin_right' => 'numeric|nullable',
'labels_pmargin_top' => 'numeric|nullable',
diff --git a/app/Http/Requests/StoreLdapSettings.php b/app/Http/Requests/StoreLdapSettings.php
index 41971450461d..ce0fa41bf545 100644
--- a/app/Http/Requests/StoreLdapSettings.php
+++ b/app/Http/Requests/StoreLdapSettings.php
@@ -27,8 +27,6 @@ public function rules(): array
'ldap_auth_filter_query' => 'not_in:uid=samaccountname|required_if:ldap_enabled,1',
'ldap_filter' => 'nullable|regex:"^[^(]"|required_if:ldap_enabled,1',
'ldap_server' => 'nullable|required_if:ldap_enabled,1|starts_with:ldap://,ldaps://',
- 'ldap_uname' => 'nullable|required_if:ldap_enabled,1',
- 'ldap_pword' => 'nullable|required_if:ldap_enabled,1',
'ldap_basedn' => 'nullable|required_if:ldap_enabled,1',
'ldap_fname_field' => 'nullable|required_if:ldap_enabled,1',
'custom_forgot_pass_url' => 'nullable|url',
diff --git a/app/Http/Requests/StoreNotificationSettings.php b/app/Http/Requests/StoreNotificationSettings.php
index 13ce5478ed41..f58d014c7687 100644
--- a/app/Http/Requests/StoreNotificationSettings.php
+++ b/app/Http/Requests/StoreNotificationSettings.php
@@ -5,6 +5,7 @@
use App\Models\Accessory;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
+use Illuminate\Validation\Rule;
class StoreNotificationSettings extends FormRequest
{
@@ -26,9 +27,12 @@ public function rules(): array
return [
'alert_email' => 'email_array|nullable',
'admin_cc_email' => 'email_array|nullable',
- 'alert_threshold' => 'numeric|nullable|gt:0',
+ 'admin_cc_always' => [
+ Rule::in('0', '1'),
+ ],
+ 'alert_threshold' => 'numeric|nullable',
'alert_interval' => 'numeric|nullable|gt:0',
- 'audit_warning_days' => 'numeric|nullable|gt:0',
+ 'audit_warning_days' => 'numeric|nullable',
'due_checkin_days' => 'numeric|nullable|gt:0',
'audit_interval' => 'numeric|nullable|gt:0',
];
diff --git a/app/Http/Requests/Traits/MayContainCustomFields.php b/app/Http/Requests/Traits/MayContainCustomFields.php
index bbdf62893d06..2b12ccff6ea7 100644
--- a/app/Http/Requests/Traits/MayContainCustomFields.php
+++ b/app/Http/Requests/Traits/MayContainCustomFields.php
@@ -10,19 +10,36 @@ trait MayContainCustomFields
// this gets called automatically on a form request
public function withValidator($validator)
{
- // find the model
- if ($this->method() == 'POST') {
- $asset_model = AssetModel::find($this->model_id);
- }
- if ($this->method() == 'PATCH' || $this->method() == 'PUT') {
- $asset_model = $this->asset->model;
+
+ // In case the model is being changed via form
+ if (request()->has('model_id')!='') {
+
+ $asset_model = AssetModel::find(request()->input('model_id'));
+
+ // or if we have it available to route-model-binding
+ } elseif ((request()->route('asset') && (request()->route('asset')->model_id))) {
+
+ $asset_model = AssetModel::find(request()->route('asset')->model_id);
+
+ } else {
+
+ if ($this->method() == 'POST') {
+ $asset_model = AssetModel::find($this->model_id);
+ }
+
+ if ($this->method() == 'PATCH' || $this->method() == 'PUT') {
+ $asset_model = $this->asset->model;
+ }
}
+
+
// collect the custom fields in the request
$validator->after(function ($validator) use ($asset_model) {
$request_fields = $this->collect()->keys()->filter(function ($attributes) {
return str_starts_with($attributes, '_snipeit_');
});
- // if there are custom fields, find the one's that don't exist on the model's fieldset and add an error to the validator's error bag
+
+ // if there are custom fields, find the ones that don't exist on the model's fieldset and add an error to the validator's error bag
if (count($request_fields) > 0 && $validator->errors()->isEmpty()) {
$request_fields->diff($asset_model?->fieldset?->fields?->pluck('db_column'))
->each(function ($request_field_name) use ($request_fields, $validator) {
diff --git a/app/Http/Requests/UploadFileRequest.php b/app/Http/Requests/UploadFileRequest.php
index e58f1a1be37d..82f4a35be1a6 100644
--- a/app/Http/Requests/UploadFileRequest.php
+++ b/app/Http/Requests/UploadFileRequest.php
@@ -6,6 +6,7 @@
use enshrined\svgSanitize\Sanitizer;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
+use \App\Helpers\Helper;
class UploadFileRequest extends Request
{
@@ -27,44 +28,76 @@ public function authorize()
*/
public function rules()
{
- $max_file_size = \App\Helpers\Helper::file_upload_max_size();
+ $max_file_size = Helper::file_upload_max_size();
return [
- 'file.*' => 'required|mimes:png,gif,jpg,svg,jpeg,doc,docx,pdf,txt,zip,rar,xls,xlsx,lic,xml,rtf,json,webp,avif|max:'.$max_file_size,
+ 'file.*' => 'required|mimes:'.config('filesystems.allowed_upload_extensions_for_validator').'|max:'.$max_file_size,
];
}
/**
* Sanitizes (if needed) and Saves a file to the appropriate location
* Returns the 'short' (storage-relative) filename
- *
- * TODO - this has a lot of similarities to UploadImageRequest's handleImage; is there
- * a way to merge them or extend one into the other?
*/
public function handleFile(string $dirname, string $name_prefix, $file): string
{
+
$extension = $file->getClientOriginalExtension();
$file_name = $name_prefix.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$file->guessExtension();
// Check for SVG and sanitize it
if ($file->getMimeType() === 'image/svg+xml') {
- Log::debug('This is an SVG');
- Log::debug($file_name);
-
- $sanitizer = new Sanitizer();
- $dirtySVG = file_get_contents($file->getRealPath());
- $cleanSVG = $sanitizer->sanitize($dirtySVG);
-
- try {
- Storage::put($dirname.$file_name, $cleanSVG);
- } catch (\Exception $e) {
- Log::debug('Upload no workie :( ');
- Log::debug($e);
- }
-
+ $uploaded_file = $this->handleSVG($file);
} else {
- $put_results = Storage::put($dirname.$file_name, file_get_contents($file));
+ $uploaded_file = file_get_contents($file);
}
+
+ try {
+ Storage::put($dirname.$file_name, $uploaded_file);
+ } catch (\Exception $e) {
+ Log::debug($e);
+ }
+
return $file_name;
}
-}
+
+ public function handleSVG($file)
+ {
+ $sanitizer = new Sanitizer();
+ $dirtySVG = file_get_contents($file->getRealPath());
+ return $sanitizer->sanitize($dirtySVG);
+ }
+
+
+ /**
+ * Get the validation error messages that apply to the request, but
+ * replace the attribute name with the name of the file that was attempted and failed
+ * to make it clearer to the user which file is the bad one.
+ *
+ * @return array
+ */
+ public function attributes(): array
+ {
+ $attributes = [];
+
+ if (($this->file) && (is_array($this->file))) {
+
+ for ($i = 0; $i < count($this->file); $i++) {
+
+ try {
+
+ if ($this->file[$i]) {
+ $attributes['file.'.$i] = $this->file[$i]->getClientOriginalName();
+ }
+
+ } catch (\Exception $e) {
+ $attributes['file.'.$i] = 'Invalid file';
+ }
+
+ }
+ }
+
+ return $attributes;
+
+ }
+}
\ No newline at end of file
diff --git a/app/Http/Transformers/AccessoriesTransformer.php b/app/Http/Transformers/AccessoriesTransformer.php
index 58858cf8976d..491871e122f4 100644
--- a/app/Http/Transformers/AccessoriesTransformer.php
+++ b/app/Http/Transformers/AccessoriesTransformer.php
@@ -37,12 +37,14 @@ public function transformAccessory(Accessory $accessory)
'purchase_date' => ($accessory->purchase_date) ? Helper::getFormattedDateObject($accessory->purchase_date, 'date') : null,
'purchase_cost' => Helper::formatCurrencyOutput($accessory->purchase_cost),
'order_number' => ($accessory->order_number) ? e($accessory->order_number) : null,
- 'min_qty' => ($accessory->min_amt) ? (int) $accessory->min_amt : null,
- 'remaining_qty' => (int) ($accessory->qty - $accessory->checkouts_count),
+ 'min_qty' => ($accessory->min_amt) ? (int) $accessory->min_amt : null, // Legacy - should phase out - replaced by below, for the bootstrap table formatter
+ 'min_amt' => ($accessory->min_amt) ? (int) $accessory->min_amt : null,
+ 'remaining_qty' => (int) ($accessory->qty - $accessory->checkouts_count), // Legacy - should phase out - replaced by below, for the bootstrap table formatter
+ 'remaining' => (int) ($accessory->qty - $accessory->checkouts_count),
'checkouts_count' => $accessory->checkouts_count,
'created_by' => ($accessory->adminuser) ? [
'id' => (int) $accessory->adminuser->id,
- 'name'=> e($accessory->adminuser->present()->fullName()),
+ 'name'=> e($accessory->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($accessory->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($accessory->updated_at, 'datetime'),
@@ -53,7 +55,7 @@ public function transformAccessory(Accessory $accessory)
'checkout' => Gate::allows('checkout', Accessory::class),
'checkin' => false,
'update' => Gate::allows('update', Accessory::class),
- 'delete' => Gate::allows('delete', Accessory::class),
+ 'delete' => $accessory->checkouts_count === 0 && Gate::allows('delete', Accessory::class),
'clone' => Gate::allows('create', Accessory::class),
];
@@ -92,6 +94,10 @@ public function transformCheckedoutAccessory($accessory_checkouts, $total)
public function transformAssignedTo($accessoryCheckout)
{
+ if (is_null($accessoryCheckout->assigned)) {
+ return null;
+ }
+
if ($accessoryCheckout->checkedOutToUser()) {
return (new UsersTransformer)->transformUserCompact($accessoryCheckout->assigned);
} elseif ($accessoryCheckout->checkedOutToLocation()) {
diff --git a/app/Http/Transformers/ActionlogsTransformer.php b/app/Http/Transformers/ActionlogsTransformer.php
index 4e6341c8f33f..c62e3031f557 100644
--- a/app/Http/Transformers/ActionlogsTransformer.php
+++ b/app/Http/Transformers/ActionlogsTransformer.php
@@ -2,6 +2,7 @@
namespace App\Http\Transformers;
use App\Helpers\Helper;
+use App\Helpers\StorageHelper;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\CustomField;
@@ -16,6 +17,7 @@
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Storage;
class ActionlogsTransformer
{
@@ -48,17 +50,20 @@ private function clean_field($value)
public function transformActionlog (Actionlog $actionlog, $settings = null)
{
+
$icon = $actionlog->present()->icon();
+ if (($actionlog->filename!='') && ($actionlog->action_type!='upload deleted')) {
+ $icon = Helper::filetype_icon($actionlog->filename);
+ }
+
static $custom_fields = false;
if ($custom_fields === false) {
$custom_fields = CustomField::all();
}
- if ($actionlog->filename!='') {
- $icon = Helper::filetype_icon($actionlog->filename);
- }
+
// This is necessary since we can't escape special characters within a JSON object
if (($actionlog->log_meta) && ($actionlog->log_meta!='')) {
@@ -113,8 +118,8 @@ public function transformActionlog (Actionlog $actionlog, $settings = null)
// Display the changes if the user is an admin or superadmin
if (Gate::allows('admin')) {
- $clean_meta[$fieldname]['old'] = ($enc_old) ? unserialize($enc_old): '';
- $clean_meta[$fieldname]['new'] = ($enc_new) ? unserialize($enc_new): '';
+ $clean_meta[$fieldname]['old'] = ($enc_old) ? unserialize($enc_old, ['allowed_classes' => false]) : '';
+ $clean_meta[$fieldname]['new'] = ($enc_new) ? unserialize($enc_new, ['allowed_classes' => false]) : '';
}
}
@@ -133,24 +138,6 @@ public function transformActionlog (Actionlog $actionlog, $settings = null)
$clean_meta= $this->changedInfo($clean_meta);
}
- $file_url = '';
- if($actionlog->filename!='') {
- if ($actionlog->action_type == 'accepted') {
- $file_url = route('log.storedeula.download', ['filename' => $actionlog->filename]);
- } else {
- if ($actionlog->item) {
- if ($actionlog->itemType() == 'asset') {
- $file_url = route('show/assetfile', ['assetId' => $actionlog->item->id, 'fileId' => $actionlog->id]);
- } elseif ($actionlog->itemType() == 'accessory') {
- $file_url = route('show.accessoryfile', ['accessoryId' => $actionlog->item->id, 'fileId' => $actionlog->id]);
- } elseif ($actionlog->itemType() == 'license') {
- $file_url = route('show.licensefile', ['licenseId' => $actionlog->item->id, 'fileId' => $actionlog->id]);
- } elseif ($actionlog->itemType() == 'user') {
- $file_url = route('show/userfile', ['userId' => $actionlog->item->id, 'fileId' => $actionlog->id]);
- }
- }
- }
- }
$array = [
'id' => (int) $actionlog->id,
@@ -158,13 +145,15 @@ public function transformActionlog (Actionlog $actionlog, $settings = null)
'file' => ($actionlog->filename!='')
?
[
- 'url' => $file_url,
+ 'url' => $actionlog->uploads_file_url(),
'filename' => $actionlog->filename,
+ 'inlineable' => StorageHelper::allowSafeInline($actionlog->uploads_file_url()),
+ 'exists_on_disk' => Storage::exists($actionlog->uploads_file_path()) ? true : false,
] : null,
'item' => ($actionlog->item) ? [
'id' => (int) $actionlog->item->id,
- 'name' => ($actionlog->itemType()=='user') ? e($actionlog->item->getFullNameAttribute()) : e($actionlog->item->getDisplayNameAttribute()),
+ 'name' => e($actionlog->item->display_name) ?? null,
'type' => e($actionlog->itemType()),
'serial' =>e($actionlog->item->serial) ? e($actionlog->item->serial) : null
] : null,
@@ -179,27 +168,27 @@ public function transformActionlog (Actionlog $actionlog, $settings = null)
'action_type' => $actionlog->present()->actionType(),
'admin' => ($actionlog->adminuser) ? [
'id' => (int) $actionlog->adminuser->id,
- 'name' => e($actionlog->adminuser->getFullNameAttribute()),
+ 'name' => e($actionlog->adminuser->display_name),
'first_name'=> e($actionlog->adminuser->first_name),
'last_name'=> e($actionlog->adminuser->last_name)
] : null,
'created_by' => ($actionlog->adminuser) ? [
'id' => (int) $actionlog->adminuser->id,
- 'name' => e($actionlog->adminuser->getFullNameAttribute()),
+ 'name' => e($actionlog->adminuser->display_name),
'first_name'=> e($actionlog->adminuser->first_name),
'last_name'=> e($actionlog->adminuser->last_name)
] : null,
'target' => ($actionlog->target) ? [
'id' => (int) $actionlog->target->id,
- 'name' => ($actionlog->targetType()=='user') ? e($actionlog->target->getFullNameAttribute()) : e($actionlog->target->getDisplayNameAttribute()),
+ 'name' => ($actionlog->target->display_name) ?? null,
'type' => e($actionlog->targetType()),
] : null,
'note' => ($actionlog->note) ? Helper::parseEscapedMarkedownInline($actionlog->note): null,
'signature_file' => ($actionlog->accept_signature) ? route('log.signature.view', ['filename' => $actionlog->accept_signature ]) : null,
'log_meta' => ((isset($clean_meta)) && (is_array($clean_meta))) ? $clean_meta: null,
- 'remote_ip' => ($actionlog->remote_ip) ?? null,
- 'user_agent' => ($actionlog->user_agent) ?? null,
+ 'remote_ip' => e($actionlog->remote_ip) ?? null,
+ 'user_agent' => e($actionlog->user_agent) ?? null,
'action_source' => ($actionlog->action_source) ?? null,
'action_date' => ($actionlog->action_date) ? Helper::getFormattedDateObject($actionlog->action_date, 'datetime'): Helper::getFormattedDateObject($actionlog->created_at, 'datetime'),
];
diff --git a/app/Http/Transformers/AssetMaintenancesTransformer.php b/app/Http/Transformers/AssetMaintenancesTransformer.php
deleted file mode 100644
index ab044260f782..000000000000
--- a/app/Http/Transformers/AssetMaintenancesTransformer.php
+++ /dev/null
@@ -1,91 +0,0 @@
-transformDatatables($array, $total);
- }
-
- public function transformAssetMaintenance(AssetMaintenance $assetmaintenance)
- {
- $array = [
- 'id' => (int) $assetmaintenance->id,
- 'asset' => ($assetmaintenance->asset) ? [
- 'id' => (int) $assetmaintenance->asset->id,
- 'name'=> ($assetmaintenance->asset->name) ? e($assetmaintenance->asset->name) : null,
- 'asset_tag'=> e($assetmaintenance->asset->asset_tag),
- 'serial'=> e($assetmaintenance->asset->serial),
- 'deleted_at'=> Helper::getFormattedDateObject($assetmaintenance->asset->deleted_at, 'datetime'),
- 'created_at' => Helper::getFormattedDateObject($assetmaintenance->asset->created_at, 'datetime'),
- 'updated_at' => Helper::getFormattedDateObject($assetmaintenance->asset->updated_at, 'datetime'),
- ] : null,
- 'model' => (($assetmaintenance->asset) && ($assetmaintenance->asset->model)) ? [
- 'id' => (int) $assetmaintenance->asset->model->id,
- 'name'=> ($assetmaintenance->asset->model->name) ? e($assetmaintenance->asset->model->name).' '.e($assetmaintenance->asset->model->model_number) : null,
- ] : null,
- 'status_label' => (($assetmaintenance->asset) && ($assetmaintenance->asset->assetstatus)) ? [
- 'id' => (int) $assetmaintenance->asset->assetstatus->id,
- 'name'=> e($assetmaintenance->asset->assetstatus->name),
- 'status_type'=> e($assetmaintenance->asset->assetstatus->getStatuslabelType()),
- 'status_meta' => e($assetmaintenance->asset->present()->statusMeta),
- ] : null,
- 'company' => (($assetmaintenance->asset) && ($assetmaintenance->asset->company)) ? [
- 'id' => (int) $assetmaintenance->asset->company->id,
- 'name'=> ($assetmaintenance->asset->company->name) ? e($assetmaintenance->asset->company->name) : null,
-
- ] : null,
- 'title' => ($assetmaintenance->title) ? e($assetmaintenance->title) : null,
- 'location' => (($assetmaintenance->asset) && ($assetmaintenance->asset->location)) ? [
- 'id' => (int) $assetmaintenance->asset->location->id,
- 'name'=> e($assetmaintenance->asset->location->name),
-
- ] : null,
- 'rtd_location' => (($assetmaintenance->asset) && ($assetmaintenance->asset->defaultLoc)) ? [
- 'id' => (int) $assetmaintenance->asset->defaultLoc->id,
- 'name'=> e($assetmaintenance->asset->defaultLoc->name),
- ] : null,
- 'notes' => ($assetmaintenance->notes) ? Helper::parseEscapedMarkedownInline($assetmaintenance->notes) : null,
- 'supplier' => ($assetmaintenance->supplier) ? ['id' => $assetmaintenance->supplier->id, 'name'=> e($assetmaintenance->supplier->name)] : null,
- 'cost' => Helper::formatCurrencyOutput($assetmaintenance->cost),
- 'asset_maintenance_type' => e($assetmaintenance->asset_maintenance_type),
- 'start_date' => Helper::getFormattedDateObject($assetmaintenance->start_date, 'date'),
- 'asset_maintenance_time' => $assetmaintenance->asset_maintenance_time,
- 'completion_date' => Helper::getFormattedDateObject($assetmaintenance->completion_date, 'date'),
- 'user_id' => ($assetmaintenance->adminuser) ? [
- 'id' => $assetmaintenance->adminuser->id,
- 'name'=> e($assetmaintenance->adminuser->present()->fullName())
- ] : null, // legacy to not change the shape of the API
- 'created_by' => ($assetmaintenance->adminuser) ? [
- 'id' => (int) $assetmaintenance->adminuser->id,
- 'name'=> e($assetmaintenance->adminuser->present()->fullName()),
- ] : null,
- 'created_at' => Helper::getFormattedDateObject($assetmaintenance->created_at, 'datetime'),
- 'updated_at' => Helper::getFormattedDateObject($assetmaintenance->updated_at, 'datetime'),
- 'is_warranty'=> $assetmaintenance->is_warranty,
-
- ];
-
- $permissions_array['available_actions'] = [
- 'update' => (Gate::allows('update', Asset::class) && ((($assetmaintenance->asset) && $assetmaintenance->asset->deleted_at==''))) ? true : false,
- 'delete' => Gate::allows('delete', Asset::class),
- ];
-
- $array += $permissions_array;
-
- return $array;
- }
-}
diff --git a/app/Http/Transformers/AssetModelsTransformer.php b/app/Http/Transformers/AssetModelsTransformer.php
index d95120dbe224..793ab645978a 100644
--- a/app/Http/Transformers/AssetModelsTransformer.php
+++ b/app/Http/Transformers/AssetModelsTransformer.php
@@ -46,8 +46,9 @@ public function transformAssetModel(AssetModel $assetmodel)
'name'=> e($assetmodel->manufacturer->name),
] : null,
'image' => ($assetmodel->image != '') ? Storage::disk('public')->url('models/'.e($assetmodel->image)) : null,
- 'model_number' => e($assetmodel->model_number),
+ 'model_number' => ($assetmodel->model_number ? e($assetmodel->model_number): null),
'min_amt' => ($assetmodel->min_amt) ? (int) $assetmodel->min_amt : null,
+ 'remaining' => (int) ($assetmodel->assets_count - $assetmodel->min_amt),
'depreciation' => ($assetmodel->depreciation) ? [
'id' => (int) $assetmodel->depreciation->id,
'name'=> e($assetmodel->depreciation->name),
@@ -64,10 +65,11 @@ public function transformAssetModel(AssetModel $assetmodel)
'default_fieldset_values' => $default_field_values,
'eol' => ($assetmodel->eol > 0) ? $assetmodel->eol.' months' : 'None',
'requestable' => ($assetmodel->requestable == '1') ? true : false,
+ 'require_serial' => $assetmodel->require_serial,
'notes' => Helper::parseEscapedMarkedownInline($assetmodel->notes),
'created_by' => ($assetmodel->adminuser) ? [
'id' => (int) $assetmodel->adminuser->id,
- 'name'=> e($assetmodel->adminuser->present()->fullName()),
+ 'name'=> e($assetmodel->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($assetmodel->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($assetmodel->updated_at, 'datetime'),
@@ -104,6 +106,7 @@ public function transformAssetModelFile($file, $assetmodel)
$array = [
'id' => (int) $file->id,
'filename' => e($file->filename),
+ 'note' => $file->note,
'url' => route('show/modelfile', [$assetmodel->id, $file->id]),
'created_by' => ($file->adminuser) ? [
'id' => (int) $file->adminuser->id,
diff --git a/app/Http/Transformers/AssetsTransformer.php b/app/Http/Transformers/AssetsTransformer.php
index c5110ac8ee6b..2862e58ea366 100644
--- a/app/Http/Transformers/AssetsTransformer.php
+++ b/app/Http/Transformers/AssetsTransformer.php
@@ -2,6 +2,7 @@
namespace App\Http\Transformers;
+use App\Helpers\CustomFieldHelper;
use App\Helpers\Helper;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
@@ -42,7 +43,7 @@ public function transformAsset(Asset $asset)
'requestable' => ($asset->requestable ? true : false),
'model_number' => (($asset->model) && ($asset->model->model_number)) ? e($asset->model->model_number) : null,
- 'eol' => (($asset->asset_eol_date != '') && ($asset->purchase_date != '')) ? Carbon::parse($asset->asset_eol_date)->diffInMonths($asset->purchase_date).' months' : null,
+ 'eol' => (($asset->asset_eol_date != '') && ($asset->purchase_date != '')) ? (int) Carbon::parse($asset->asset_eol_date)->diffInMonths($asset->purchase_date, true) . ' months' : null,
'asset_eol_date' => ($asset->asset_eol_date != '') ? Helper::getFormattedDateObject($asset->asset_eol_date, 'date') : null,
'status_label' => ($asset->assetstatus) ? [
'id' => (int) $asset->assetstatus->id,
@@ -58,6 +59,13 @@ public function transformAsset(Asset $asset)
'id' => (int) $asset->model->manufacturer->id,
'name'=> e($asset->model->manufacturer->name),
] : null,
+ 'depreciation' => (($asset->model) && ($asset->model->depreciation)) ? [
+ 'id' => (int) $asset->model->depreciation->id,
+ 'name'=> e($asset->model->depreciation->name),
+ 'months'=> (int) $asset->model->depreciation->months,
+ 'type'=> e($asset->model->depreciation->depreciation_type),
+ 'minimum'=> ($asset->model->depreciation->depreciation_min) ? (int) $asset->model->depreciation->depreciation_min : null,
+ ] : null,
'supplier' => ($asset->supplier) ? [
'id' => (int) $asset->supplier->id,
'name'=> e($asset->supplier->name),
@@ -84,7 +92,7 @@ public function transformAsset(Asset $asset)
'warranty_expires' => ($asset->warranty_months > 0) ? Helper::getFormattedDateObject($asset->warranty_expires, 'date') : null,
'created_by' => ($asset->adminuser) ? [
'id' => (int) $asset->adminuser->id,
- 'name'=> e($asset->adminuser->present()->fullName()),
+ 'name'=> e($asset->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($asset->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($asset->updated_at, 'datetime'),
@@ -101,53 +109,11 @@ public function transformAsset(Asset $asset)
'checkout_counter' => (int) $asset->checkout_counter,
'requests_counter' => (int) $asset->requests_counter,
'user_can_checkout' => (bool) $asset->availableForCheckout(),
- 'book_value' => Helper::formatCurrencyOutput($asset->getLinearDepreciatedValue()),
+ 'book_value' => Helper::formatCurrencyOutput($asset->getDepreciatedValue()),
];
- if (($asset->model) && ($asset->model->fieldset) && ($asset->model->fieldset->fields->count() > 0)) {
- $fields_array = [];
-
- foreach ($asset->model->fieldset->fields as $field) {
- if ($field->isFieldDecryptable($asset->{$field->db_column})) {
- $decrypted = Helper::gracefulDecrypt($field, $asset->{$field->db_column});
- $value = (Gate::allows('assets.view.encrypted_custom_fields')) ? $decrypted : strtoupper(trans('admin/custom_fields/general.encrypted'));
-
- if ($field->format == 'DATE'){
- if (Gate::allows('assets.view.encrypted_custom_fields')){
- $value = Helper::getFormattedDateObject($value, 'date', false);
- } else {
- $value = strtoupper(trans('admin/custom_fields/general.encrypted'));
- }
- }
-
- $fields_array[$field->name] = [
- 'field' => e($field->db_column),
- 'value' => e($value),
- 'field_format' => $field->format,
- 'element' => $field->element,
- ];
-
- } else {
- $value = $asset->{$field->db_column};
-
- if (($field->format == 'DATE') && (!is_null($value)) && ($value!='')){
- $value = Helper::getFormattedDateObject($value, 'date', false);
- }
-
- $fields_array[$field->name] = [
- 'field' => e($field->db_column),
- 'value' => e($value),
- 'field_format' => $field->format,
- 'element' => $field->element,
- ];
- }
-
- $array['custom_fields'] = $fields_array;
- }
- } else {
- $array['custom_fields'] = new \stdClass; // HACK to force generation of empty object instead of empty list
- }
+ $array['custom_fields'] = CustomFieldHelper::transform($asset->model->fieldset,$asset);
$permissions_array['available_actions'] = [
'checkout' => ($asset->deleted_at=='' && Gate::allows('checkout', Asset::class)) ? true : false,
@@ -155,6 +121,7 @@ public function transformAsset(Asset $asset)
'clone' => Gate::allows('create', Asset::class) ? true : false,
'restore' => ($asset->deleted_at!='' && Gate::allows('create', Asset::class)) ? true : false,
'update' => ($asset->deleted_at=='' && Gate::allows('update', Asset::class)) ? true : false,
+ 'audit' => Gate::allows('audit', Asset::class) ? true : false,
'delete' => ($asset->deleted_at=='' && $asset->assigned_to =='' && Gate::allows('delete', Asset::class) && ($asset->deleted_at == '')) ? true : false,
];
@@ -202,6 +169,7 @@ public function transformAssignedTo($asset)
'last_name'=> ($asset->assigned->last_name) ? e($asset->assigned->last_name) : null,
'email'=> ($asset->assigned->email) ? e($asset->assigned->email) : null,
'employee_number' => ($asset->assigned->employee_num) ? e($asset->assigned->employee_num) : null,
+ 'jobtitle' => $asset->assigned->jobtitle ? e($asset->assigned->jobtitle) : null,
'type' => 'user',
] : null;
}
@@ -278,7 +246,7 @@ public function transformAssetCompact(Asset $asset)
'id' => (int) $asset->id,
'image' => ($asset->getImageUrl()) ? $asset->getImageUrl() : null,
'type' => 'asset',
- 'name' => e($asset->present()->fullName()),
+ 'name' => e($asset->display_name),
'model' => ($asset->model) ? e($asset->model->name) : null,
'model_number' => (($asset->model) && ($asset->model->model_number)) ? e($asset->model->model_number) : null,
'asset_tag' => e($asset->asset_tag),
@@ -302,29 +270,32 @@ public function transformCheckedoutAccessories($accessory_checkouts, $total)
public function transformCheckedoutAccessory(AccessoryCheckout $accessory_checkout)
{
-
- $array = [
- 'id' => $accessory_checkout->id,
- 'accessory' => [
- 'id' => $accessory_checkout->accessory->id,
- 'name' => $accessory_checkout->accessory->name,
- ],
- 'image' => ($accessory_checkout->accessory->image) ? Storage::disk('public')->url('accessories/'.e($accessory_checkout->accessory->image)) : null,
- 'note' => $accessory_checkout->note ? e($accessory_checkout->note) : null,
- 'created_by' => $accessory_checkout->adminuser ? [
- 'id' => (int) $accessory_checkout->adminuser->id,
- 'name'=> e($accessory_checkout->adminuser->present()->fullName),
- ]: null,
- 'created_at' => Helper::getFormattedDateObject($accessory_checkout->created_at, 'datetime'),
- ];
-
- $permissions_array['available_actions'] = [
- 'checkout' => false,
- 'checkin' => Gate::allows('checkin', Accessory::class),
- ];
-
- $array += $permissions_array;
- return $array;
+ if ($accessory_checkout->accessory) {
+ $array = [
+ 'id' => $accessory_checkout->id,
+ 'accessory' => [
+ 'id' => $accessory_checkout->accessory->id,
+ 'name' => $accessory_checkout->accessory->name,
+ ],
+ 'assigned_to' => $accessory_checkout->assigned_to,
+ 'image' => ($accessory_checkout->accessory->image) ? Storage::disk('public')->url('accessories/' . e($accessory_checkout->accessory->image)) : null,
+ 'note' => $accessory_checkout->note ? e($accessory_checkout->note) : null,
+ 'created_by' => $accessory_checkout->adminuser ? [
+ 'id' => (int)$accessory_checkout->adminuser->id,
+ 'name' => e($accessory_checkout->adminuser->present()->fullName),
+ ] : null,
+ 'created_at' => Helper::getFormattedDateObject($accessory_checkout->created_at, 'datetime'),
+ 'deleted_at' => Helper::getFormattedDateObject($accessory_checkout->deleted_at, 'datetime'),
+ ];
+
+ $permissions_array['available_actions'] = [
+ 'checkout' => false,
+ 'checkin' => Gate::allows('checkin', Accessory::class),
+ ];
+
+ $array += $permissions_array;
+ return $array;
+ }
}
}
diff --git a/app/Http/Transformers/CategoriesTransformer.php b/app/Http/Transformers/CategoriesTransformer.php
index 0d1834649da5..348c5d45522f 100644
--- a/app/Http/Transformers/CategoriesTransformer.php
+++ b/app/Http/Transformers/CategoriesTransformer.php
@@ -64,7 +64,7 @@ public function transformCategory(Category $category = null)
'licenses_count' => (int) $category->licenses_count,
'created_by' => ($category->adminuser) ? [
'id' => (int) $category->adminuser->id,
- 'name'=> e($category->adminuser->present()->fullName()),
+ 'name'=> e($category->adminuser->display_name),
] : null,
'notes' => Helper::parseEscapedMarkedownInline($category->notes),
'created_at' => Helper::getFormattedDateObject($category->created_at, 'datetime'),
diff --git a/app/Http/Transformers/CompaniesTransformer.php b/app/Http/Transformers/CompaniesTransformer.php
index 8ca5344de6be..13f9a05e270a 100644
--- a/app/Http/Transformers/CompaniesTransformer.php
+++ b/app/Http/Transformers/CompaniesTransformer.php
@@ -38,7 +38,7 @@ public function transformCompany(Company $company = null)
'users_count' => (int) $company->users_count,
'created_by' => ($company->adminuser) ? [
'id' => (int) $company->adminuser->id,
- 'name'=> e($company->adminuser->present()->fullName()),
+ 'name'=> e($company->adminuser->display_name),
] : null,
'notes' => Helper::parseEscapedMarkedownInline($company->notes),
'created_at' => Helper::getFormattedDateObject($company->created_at, 'datetime'),
diff --git a/app/Http/Transformers/ComponentsTransformer.php b/app/Http/Transformers/ComponentsTransformer.php
index f98edd6e3fb1..f7f8c337bf6e 100644
--- a/app/Http/Transformers/ComponentsTransformer.php
+++ b/app/Http/Transformers/ComponentsTransformer.php
@@ -51,7 +51,7 @@ public function transformComponent(Component $component)
'notes' => ($component->notes) ? Helper::parseEscapedMarkedownInline($component->notes) : null,
'created_by' => ($component->adminuser) ? [
'id' => (int) $component->adminuser->id,
- 'name'=> e($component->adminuser->present()->fullName()),
+ 'name'=> e($component->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($component->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($component->updated_at, 'datetime'),
@@ -62,7 +62,7 @@ public function transformComponent(Component $component)
'checkout' => Gate::allows('checkout', Component::class),
'checkin' => Gate::allows('checkin', Component::class),
'update' => Gate::allows('update', Component::class),
- 'delete' => Gate::allows('delete', Component::class),
+ 'delete' => $component->isDeletable(),
];
$array += $permissions_array;
@@ -76,7 +76,7 @@ public function transformCheckedoutComponents(Collection $components_assets, $to
$array[] = [
'assigned_pivot_id' => $asset->pivot->id,
'id' => (int) $asset->id,
- 'name' => e($asset->model->present()->name).' '.e($asset->present()->name),
+ 'name' => e($asset->model->display_name).' '.e($asset->display_name),
'qty' => $asset->pivot->assigned_qty,
'note' => $asset->pivot->note,
'type' => 'asset',
diff --git a/app/Http/Transformers/ConsumablesTransformer.php b/app/Http/Transformers/ConsumablesTransformer.php
index b31e31ac96d8..4c7dbf9cc59b 100644
--- a/app/Http/Transformers/ConsumablesTransformer.php
+++ b/app/Http/Transformers/ConsumablesTransformer.php
@@ -25,7 +25,7 @@ public function transformConsumable(Consumable $consumable)
$array = [
'id' => (int) $consumable->id,
'name' => e($consumable->name),
- 'image' => ($consumable->image) ? Storage::disk('public')->url('consumables/'.e($consumable->image)) : null,
+ 'image' => ($consumable->getImageUrl()) ? ($consumable->getImageUrl()) : null,
'category' => ($consumable->category) ? ['id' => $consumable->category->id, 'name' => e($consumable->category->name)] : null,
'company' => ($consumable->company) ? ['id' => (int) $consumable->company->id, 'name' => e($consumable->company->name)] : null,
'item_no' => e($consumable->item_no),
@@ -42,7 +42,7 @@ public function transformConsumable(Consumable $consumable)
'notes' => ($consumable->notes) ? Helper::parseEscapedMarkedownInline($consumable->notes) : null,
'created_by' => ($consumable->adminuser) ? [
'id' => (int) $consumable->adminuser->id,
- 'name'=> e($consumable->adminuser->present()->fullName()),
+ 'name'=> e($consumable->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($consumable->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($consumable->updated_at, 'datetime'),
diff --git a/app/Http/Transformers/CustomFieldsTransformer.php b/app/Http/Transformers/CustomFieldsTransformer.php
index d6401a3e5e25..501d264b5572 100644
--- a/app/Http/Transformers/CustomFieldsTransformer.php
+++ b/app/Http/Transformers/CustomFieldsTransformer.php
@@ -50,6 +50,9 @@ public function transformCustomField(CustomField $field)
'display_in_user_view' => ($field->display_in_user_view =='1') ? true : false,
'auto_add_to_fieldsets' => ($field->auto_add_to_fieldsets == '1') ? true : false,
'show_in_listview' => ($field->show_in_listview == '1') ? true : false,
+ 'display_checkin' => ($field->display_checkin == '1') ? true : false,
+ 'display_checkout' => ($field->display_checkout == '1') ? true : false,
+ 'display_audit' => ($field->display_audit == '1') ? true : false,
'created_at' => Helper::getFormattedDateObject($field->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($field->updated_at, 'datetime'),
];
diff --git a/app/Http/Transformers/CustomFieldsetsTransformer.php b/app/Http/Transformers/CustomFieldsetsTransformer.php
index 61e42486ab43..18f1f79632e2 100644
--- a/app/Http/Transformers/CustomFieldsetsTransformer.php
+++ b/app/Http/Transformers/CustomFieldsetsTransformer.php
@@ -3,6 +3,8 @@
namespace App\Http\Transformers;
use App\Helpers\Helper;
+use App\Models\Asset;
+use App\Models\AssetModel;
use App\Models\CustomFieldset;
use Illuminate\Database\Eloquent\Collection;
@@ -21,8 +23,13 @@ public function transformCustomFieldsets(Collection $fieldsets, $total)
public function transformCustomFieldset(CustomFieldset $fieldset)
{
$fields = $fieldset->fields;
- $models = $fieldset->models;
+ $models = [];
$modelsArray = [];
+ if ($fieldset->type == Asset::class) {
+ \Log::debug("Item pivot id is: ".$fieldset->item_pivot_id);
+ $models = AssetModel::where('fieldset_id', $fieldset->id)->get();
+ \Log::debug("And the models object count is: ".$models->count());
+ }
foreach ($models as $model) {
$modelsArray[] = [
@@ -30,15 +37,21 @@ public function transformCustomFieldset(CustomFieldset $fieldset)
'name' => e($model->name),
];
}
+ \Log::debug("Models array is: ".print_r($modelsArray,true));
$array = [
'id' => (int) $fieldset->id,
'name' => e($fieldset->name),
'fields' => (new CustomFieldsTransformer)->transformCustomFields($fields, $fieldset->fields_count),
- 'models' => (new DatatablesTransformer)->transformDatatables($modelsArray, $fieldset->models_count),
+ 'customizables' => (new DatatablesTransformer)->transformDatatables($fieldset->customizables(),count($fieldset->customizables())),
'created_at' => Helper::getFormattedDateObject($fieldset->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($fieldset->updated_at, 'datetime'),
+ 'type' => $fieldset->type,
];
+ if ($fieldset->type == Asset::class) {
+ // TODO - removeme - legacy column just for Assets?
+ $array['models'] = (new DatatablesTransformer)->transformDatatables($modelsArray, count($modelsArray));
+ }
return $array;
}
diff --git a/app/Http/Transformers/DatatablesTransformer.php b/app/Http/Transformers/DatatablesTransformer.php
index 0e69109391fc..2ec993536d34 100644
--- a/app/Http/Transformers/DatatablesTransformer.php
+++ b/app/Http/Transformers/DatatablesTransformer.php
@@ -4,6 +4,10 @@
class DatatablesTransformer
{
+
+ /**
+ * Transform data for bootstrap tables and API responses for lists of things
+ **/
public function transformDatatables($objects, $total = null)
{
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
@@ -11,4 +15,15 @@ public function transformDatatables($objects, $total = null)
return $objects_array;
}
-}
+
+ /**
+ * Transform data for returning the status of items within a bulk action
+ **/
+ public function transformBulkResponseWithStatusAndObjects($objects, $total)
+ {
+ (isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
+ $objects_array['rows'] = $objects;
+
+ return $objects_array;
+ }
+}
\ No newline at end of file
diff --git a/app/Http/Transformers/DepartmentsTransformer.php b/app/Http/Transformers/DepartmentsTransformer.php
index 3d1e4c6f90c4..e072585a125b 100644
--- a/app/Http/Transformers/DepartmentsTransformer.php
+++ b/app/Http/Transformers/DepartmentsTransformer.php
@@ -35,7 +35,7 @@ public function transformDepartment(Department $department = null)
] : null,
'manager' => ($department->manager) ? [
'id' => (int) $department->manager->id,
- 'name' => e($department->manager->getFullNameAttribute()),
+ 'name' => e($department->manager->display_name),
'first_name'=> e($department->manager->first_name),
'last_name'=> e($department->manager->last_name),
] : null,
diff --git a/app/Http/Transformers/DepreciationsTransformer.php b/app/Http/Transformers/DepreciationsTransformer.php
index 64d4c88f7edc..3b0d68392cf7 100644
--- a/app/Http/Transformers/DepreciationsTransformer.php
+++ b/app/Http/Transformers/DepreciationsTransformer.php
@@ -26,14 +26,14 @@ public function transformDepreciation(Depreciation $depreciation)
$array = [
'id' => (int) $depreciation->id,
'name' => e($depreciation->name),
- 'months' => $depreciation->months.' '.trans('general.months'),
+ 'months' => trans_choice('general.months_plural', $depreciation->months),
'depreciation_min' => $depreciation->depreciation_type === 'percent' ? $depreciation->depreciation_min.'%' : $depreciation->depreciation_min,
- 'assets_count' => $depreciation->assets_count,
- 'models_count' => $depreciation->models_count,
- 'licenses_count' => $depreciation->licenses_count,
+ 'assets_count' => ($depreciation->assets_count > 0) ? (int) $depreciation->assets_count : 0,
+ 'models_count' => ($depreciation->models_count > 0) ? (int) $depreciation->models_count : 0,
+ 'licenses_count' => ($depreciation->licenses_count > 0) ? (int) $depreciation->licenses_count : 0,
'created_by' => ($depreciation->adminuser) ? [
'id' => (int) $depreciation->adminuser->id,
- 'name'=> e($depreciation->adminuser->present()->fullName()),
+ 'name'=> e($depreciation->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($depreciation->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($depreciation->updated_at, 'datetime')
diff --git a/app/Http/Transformers/GroupsTransformer.php b/app/Http/Transformers/GroupsTransformer.php
index 920c856b1a49..759392615545 100644
--- a/app/Http/Transformers/GroupsTransformer.php
+++ b/app/Http/Transformers/GroupsTransformer.php
@@ -24,12 +24,12 @@ public function transformGroup(Group $group)
$array = [
'id' => (int) $group->id,
'name' => e($group->name),
- 'permissions' => json_decode($group->permissions),
+ 'permissions' => $group->decodePermissions(),
'users_count' => (int) $group->users_count,
'notes' => Helper::parseEscapedMarkedownInline($group->notes),
'created_by' => ($group->adminuser) ? [
'id' => (int) $group->adminuser->id,
- 'name'=> e($group->adminuser->present()->fullName()),
+ 'name'=> e($group->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($group->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($group->updated_at, 'datetime'),
diff --git a/app/Http/Transformers/LicenseSeatsTransformer.php b/app/Http/Transformers/LicenseSeatsTransformer.php
index 7ae68e9e449c..17025e7f9f6c 100644
--- a/app/Http/Transformers/LicenseSeatsTransformer.php
+++ b/app/Http/Transformers/LicenseSeatsTransformer.php
@@ -2,30 +2,30 @@
namespace App\Http\Transformers;
+use App\Helpers\Helper;
use App\Models\License;
use App\Models\LicenseSeat;
use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection;
-
class LicenseSeatsTransformer
{
public function transformLicenseSeats(Collection $seats, $total)
{
$array = [];
- $seat_count = 0;
+
foreach ($seats as $seat) {
- $seat_count++;
- $array[] = self::transformLicenseSeat($seat, $seat_count);
+ $array[] = self::transformLicenseSeat($seat);
}
return (new DatatablesTransformer)->transformDatatables($array, $total);
}
- public function transformLicenseSeat(LicenseSeat $seat, $seat_count = 0)
+ public function transformLicenseSeat(LicenseSeat $seat)
{
$array = [
'id' => (int) $seat->id,
'license_id' => (int) $seat->license->id,
+ 'updated_at' => Helper::getFormattedDateObject($seat->updated_at, 'datetime'), // we use updated_at here because the record gets updated when it's checked in or out
'assigned_user' => ($seat->user) ? [
'id' => (int) $seat->user->id,
'name'=> e($seat->user->present()->fullName),
@@ -36,24 +36,24 @@ public function transformLicenseSeat(LicenseSeat $seat, $seat_count = 0)
'name' => e($seat->user->department->name),
] : null,
+ 'created_at' => Helper::getFormattedDateObject($seat->created_at, 'datetime'),
] : null,
'assigned_asset' => ($seat->asset) ? [
'id' => (int) $seat->asset->id,
'name'=> e($seat->asset->present()->fullName),
+ 'created_at' => Helper::getFormattedDateObject($seat->created_at, 'datetime'),
] : null,
'location' => ($seat->location()) ? [
'id' => (int) $seat->location()->id,
'name'=> e($seat->location()->name),
+ 'created_at' => Helper::getFormattedDateObject($seat->created_at, 'datetime'),
] : null,
'reassignable' => (bool) $seat->license->reassignable,
'notes' => e($seat->notes),
'user_can_checkout' => (($seat->assigned_to == '') && ($seat->asset_id == '')),
+ 'disabled' => $seat->unreassignable_seat,
];
- if ($seat_count != 0) {
- $array['name'] = trans('admin/licenses/general.seat_count', ['count' => $seat_count]);
- }
-
$permissions_array['available_actions'] = [
'checkout' => Gate::allows('checkout', License::class),
'checkin' => Gate::allows('checkin', License::class),
diff --git a/app/Http/Transformers/LicensesTransformer.php b/app/Http/Transformers/LicensesTransformer.php
index 673ac06b3dab..24822efeca7d 100644
--- a/app/Http/Transformers/LicensesTransformer.php
+++ b/app/Http/Transformers/LicensesTransformer.php
@@ -37,7 +37,8 @@ public function transformLicense(License $license)
'notes' => Helper::parseEscapedMarkedownInline($license->notes),
'expiration_date' => Helper::getFormattedDateObject($license->expiration_date, 'date'),
'seats' => (int) $license->seats,
- 'free_seats_count' => (int) $license->free_seats_count,
+ 'free_seats_count' => (int) $license->free_seats_count - License::unReassignableCount($license),
+ 'remaining' => (int) $license->free_seats_count,
'min_amt' => ($license->min_amt) ? (int) ($license->min_amt) : null,
'license_name' => ($license->license_name) ? e($license->license_name) : null,
'license_email' => ($license->license_email) ? e($license->license_email) : null,
@@ -47,7 +48,7 @@ public function transformLicense(License $license)
'category' => ($license->category) ? ['id' => (int) $license->category->id, 'name'=> e($license->category->name)] : null,
'created_by' => ($license->adminuser) ? [
'id' => (int) $license->adminuser->id,
- 'name'=> e($license->adminuser->present()->fullName()),
+ 'name'=> e($license->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($license->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($license->updated_at, 'datetime'),
@@ -61,7 +62,7 @@ public function transformLicense(License $license)
'checkin' => Gate::allows('checkin', License::class),
'clone' => Gate::allows('create', License::class),
'update' => Gate::allows('update', License::class),
- 'delete' => (Gate::allows('delete', License::class) && ($license->free_seats_count > 0)) ? true : false,
+ 'delete' => (Gate::allows('delete', License::class) && ($license->free_seats_count == $license->seats)) ? true : false,
];
$array += $permissions_array;
diff --git a/app/Http/Transformers/LocationsTransformer.php b/app/Http/Transformers/LocationsTransformer.php
index 331f3839b200..4965ff99d570 100644
--- a/app/Http/Transformers/LocationsTransformer.php
+++ b/app/Http/Transformers/LocationsTransformer.php
@@ -57,12 +57,20 @@ public function transformLocation(Location $location = null)
'ldap_ou' => ($location->ldap_ou) ? e($location->ldap_ou) : null,
'notes' => Helper::parseEscapedMarkedownInline($location->notes),
'created_at' => Helper::getFormattedDateObject($location->created_at, 'datetime'),
+ 'created_by' => $location->adminuser ? [
+ 'id' => (int) $location->adminuser->id,
+ 'name'=> e($location->adminuser->present()->fullName),
+ ]: null,
'updated_at' => Helper::getFormattedDateObject($location->updated_at, 'datetime'),
'parent' => ($location->parent) ? [
'id' => (int) $location->parent->id,
'name'=> e($location->parent->name),
] : null,
'manager' => ($location->manager) ? (new UsersTransformer)->transformUser($location->manager) : null,
+ 'company' => ($location->company) ? [
+ 'id' => (int) $location->company->id,
+ 'name'=> e($location->company->name)
+ ] : null,
'children' => $children_arr,
];
@@ -100,11 +108,9 @@ public function transformCheckedoutAccessory(AccessoryCheckout $accessory_checko
$array = [
'id' => $accessory_checkout->id,
- 'accessory' => [
- 'id' => $accessory_checkout->accessory->id,
- 'name' => $accessory_checkout->accessory->name,
- ],
- 'image' => ($accessory_checkout->accessory->image) ? Storage::disk('public')->url('accessories/'.e($accessory_checkout->accessory->image)) : null,
+ 'assigned_to' => $accessory_checkout->assigned_to,
+ 'accessory' => $this->transformAccessory($accessory_checkout->accessory),
+ 'image' => ($accessory_checkout?->accessory?->image) ? Storage::disk('public')->url('accessories/' . e($accessory_checkout->accessory->image)) : null,
'note' => $accessory_checkout->note ? e($accessory_checkout->note) : null,
'created_by' => $accessory_checkout->adminuser ? [
'id' => (int) $accessory_checkout->adminuser->id,
@@ -152,4 +158,16 @@ public function transformLocationCompact(Location $location = null)
return $array;
}
}
-}
\ No newline at end of file
+
+ private function transformAccessory(?Accessory $accessory): ?array
+ {
+ if ($accessory) {
+ return [
+ 'id' => $accessory->id,
+ 'name' => $accessory->name,
+ ];
+ }
+
+ return null;
+ }
+}
diff --git a/app/Http/Transformers/MaintenancesTransformer.php b/app/Http/Transformers/MaintenancesTransformer.php
new file mode 100644
index 000000000000..c20c2548697e
--- /dev/null
+++ b/app/Http/Transformers/MaintenancesTransformer.php
@@ -0,0 +1,97 @@
+transformDatatables($array, $total);
+ }
+
+ public function transformMaintenance(Maintenance $assetmaintenance)
+ {
+ $array = [
+ 'id' => (int) $assetmaintenance->id,
+ 'asset' => ($assetmaintenance->asset) ? [
+ 'id' => (int) $assetmaintenance->asset->id,
+ 'name'=> ($assetmaintenance->asset->name) ? e($assetmaintenance->asset->name) : null,
+ 'asset_tag'=> e($assetmaintenance->asset->asset_tag),
+ 'serial'=> e($assetmaintenance->asset->serial),
+ 'deleted_at'=> Helper::getFormattedDateObject($assetmaintenance->asset->deleted_at, 'datetime'),
+ 'created_at' => Helper::getFormattedDateObject($assetmaintenance->asset->created_at, 'datetime'),
+ 'updated_at' => Helper::getFormattedDateObject($assetmaintenance->asset->updated_at, 'datetime'),
+ ] : null,
+ 'image' => ($assetmaintenance->image != '') ? Storage::disk('public')->url('maintenances/'.e($assetmaintenance->image)) : null,
+ 'model' => (($assetmaintenance->asset) && ($assetmaintenance->asset->model)) ? [
+ 'id' => (int) $assetmaintenance->asset->model->id,
+ 'name'=> ($assetmaintenance->asset->model->name) ? e($assetmaintenance->asset->model->name).' '.e($assetmaintenance->asset->model->model_number) : null,
+ ] : null,
+ 'status_label' => (($assetmaintenance->asset) && ($assetmaintenance->asset->assetstatus)) ? [
+ 'id' => (int) $assetmaintenance->asset->assetstatus->id,
+ 'name'=> e($assetmaintenance->asset->assetstatus->name),
+ 'status_type'=> e($assetmaintenance->asset->assetstatus->getStatuslabelType()),
+ 'status_meta' => e($assetmaintenance->asset->present()->statusMeta),
+ ] : null,
+ 'company' => (($assetmaintenance->asset) && ($assetmaintenance->asset->company)) ? [
+ 'id' => (int) $assetmaintenance->asset->company->id,
+ 'name'=> ($assetmaintenance->asset->company->name) ? e($assetmaintenance->asset->company->name) : null,
+
+ ] : null,
+ 'name' => ($assetmaintenance->name) ? e($assetmaintenance->name) : null,
+ 'title' => ($assetmaintenance->name) ? e($assetmaintenance->name) : null, // legacy to not change the shape of the API
+ 'location' => (($assetmaintenance->asset) && ($assetmaintenance->asset->location)) ? [
+ 'id' => (int) $assetmaintenance->asset->location->id,
+ 'name'=> e($assetmaintenance->asset->location->name),
+
+ ] : null,
+ 'rtd_location' => (($assetmaintenance->asset) && ($assetmaintenance->asset->defaultLoc)) ? [
+ 'id' => (int) $assetmaintenance->asset->defaultLoc->id,
+ 'name'=> e($assetmaintenance->asset->defaultLoc->name),
+ ] : null,
+ 'notes' => ($assetmaintenance->notes) ? Helper::parseEscapedMarkedownInline($assetmaintenance->notes) : null,
+ 'supplier' => ($assetmaintenance->supplier) ? [
+ 'id' => $assetmaintenance->supplier->id,
+ 'name'=> e($assetmaintenance->supplier->name)
+ ] : null,
+ 'cost' => Helper::formatCurrencyOutput($assetmaintenance->cost),
+ 'asset_maintenance_type' => e($assetmaintenance->asset_maintenance_type),
+ 'start_date' => Helper::getFormattedDateObject($assetmaintenance->start_date, 'date'),
+ 'asset_maintenance_time' => $assetmaintenance->asset_maintenance_time,
+ 'completion_date' => Helper::getFormattedDateObject($assetmaintenance->completion_date, 'date'),
+ 'user_id' => ($assetmaintenance->adminuser) ? [
+ 'id' => $assetmaintenance->adminuser->id,
+ 'name'=> e($assetmaintenance->adminuser->display_name)
+ ] : null, // legacy to not change the shape of the API
+ 'created_by' => ($assetmaintenance->adminuser) ? [
+ 'id' => (int) $assetmaintenance->adminuser->id,
+ 'name'=> e($assetmaintenance->adminuser->display_name),
+ ] : null,
+ 'created_at' => Helper::getFormattedDateObject($assetmaintenance->created_at, 'datetime'),
+ 'updated_at' => Helper::getFormattedDateObject($assetmaintenance->updated_at, 'datetime'),
+ 'is_warranty'=> $assetmaintenance->is_warranty,
+
+ ];
+
+ $permissions_array['available_actions'] = [
+ 'update' => (Gate::allows('update', Asset::class) && ((($assetmaintenance->asset) && $assetmaintenance->asset->deleted_at==''))) ? true : false,
+ 'delete' => Gate::allows('delete', Asset::class),
+ ];
+
+ $array += $permissions_array;
+
+ return $array;
+ }
+}
diff --git a/app/Http/Transformers/ManufacturersTransformer.php b/app/Http/Transformers/ManufacturersTransformer.php
index cf17eb77646a..0d1373414c81 100644
--- a/app/Http/Transformers/ManufacturersTransformer.php
+++ b/app/Http/Transformers/ManufacturersTransformer.php
@@ -40,7 +40,7 @@ public function transformManufacturer(Manufacturer $manufacturer = null)
'notes' => Helper::parseEscapedMarkedownInline($manufacturer->notes),
'created_by' => ($manufacturer->adminuser) ? [
'id' => (int) $manufacturer->adminuser->id,
- 'name'=> e($manufacturer->adminuser->present()->fullName()),
+ 'name'=> e($manufacturer->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($manufacturer->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($manufacturer->updated_at, 'datetime'),
diff --git a/app/Http/Transformers/PredefinedKitsTransformer.php b/app/Http/Transformers/PredefinedKitsTransformer.php
index 61c9e476a99d..3660ff269e2f 100644
--- a/app/Http/Transformers/PredefinedKitsTransformer.php
+++ b/app/Http/Transformers/PredefinedKitsTransformer.php
@@ -34,7 +34,7 @@ public function transformPredefinedKit(PredefinedKit $kit)
'name' => e($kit->name),
'created_by' => ($kit->adminuser) ? [
'id' => (int) $kit->adminuser->id,
- 'name'=> e($kit->adminuser->present()->fullName()),
+ 'name'=> e($kit->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($kit->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($kit->updated_at, 'datetime'),
diff --git a/app/Http/Transformers/ProfileTransformer.php b/app/Http/Transformers/ProfileTransformer.php
new file mode 100644
index 000000000000..3b8e58e1337e
--- /dev/null
+++ b/app/Http/Transformers/ProfileTransformer.php
@@ -0,0 +1,42 @@
+transformDatatables($array, $total);
+ }
+
+
+ public function transformFile(Actionlog $file)
+ {
+ $array = [
+ 'id' => (int) $file->id,
+ 'icon' => Helper::filetype_icon($file->filename),
+ 'item' => ($file->item) ? [
+ 'name' => $file->item->display_name ? e($file->item->display_name) : null,
+ 'type' => e($file->itemType()),
+ ] : null,
+ 'filename' => e($file->filename),
+ 'signature_file' => ($file->accept_signature) ? route('profile.signature.view', ['filename' => $file->accept_signature ]) : null,
+ 'note' => e($file->note),
+ 'url' => route('profile.storedeula.download', ['filename' => $file->filename]),
+ 'file' => route('profile.storedeula.download', ['filename' => $file->filename]),
+ 'created_at' => Helper::getFormattedDateObject($file->created_at, 'datetime'),
+ ];
+
+ return $array;
+ }
+
+}
diff --git a/app/Http/Transformers/StatuslabelsTransformer.php b/app/Http/Transformers/StatuslabelsTransformer.php
index 751edb7016b1..64097959946a 100644
--- a/app/Http/Transformers/StatuslabelsTransformer.php
+++ b/app/Http/Transformers/StatuslabelsTransformer.php
@@ -32,7 +32,7 @@ public function transformStatuslabel(Statuslabel $statuslabel)
'notes' => e($statuslabel->notes),
'created_by' => ($statuslabel->adminuser) ? [
'id' => (int) $statuslabel->adminuser->id,
- 'name'=> e($statuslabel->adminuser->present()->fullName()),
+ 'name'=> e($statuslabel->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($statuslabel->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($statuslabel->updated_at, 'datetime'),
diff --git a/app/Http/Transformers/SuppliersTransformer.php b/app/Http/Transformers/SuppliersTransformer.php
index 1fdc93c193b8..750c969c636a 100644
--- a/app/Http/Transformers/SuppliersTransformer.php
+++ b/app/Http/Transformers/SuppliersTransformer.php
@@ -45,6 +45,10 @@ public function transformSupplier(Supplier $supplier = null)
'components_count' => (int) $supplier->components_count,
'notes' => ($supplier->notes) ? Helper::parseEscapedMarkedownInline($supplier->notes) : null,
'created_at' => Helper::getFormattedDateObject($supplier->created_at, 'datetime'),
+ 'created_by' => $supplier->adminuser ? [
+ 'id' => (int) $supplier->adminuser->id,
+ 'name'=> e($supplier->adminuser->present()->fullName),
+ ]: null,
'updated_at' => Helper::getFormattedDateObject($supplier->updated_at, 'datetime'),
];
diff --git a/app/Http/Transformers/UploadedFilesTransformer.php b/app/Http/Transformers/UploadedFilesTransformer.php
new file mode 100644
index 000000000000..e32aabc8e58d
--- /dev/null
+++ b/app/Http/Transformers/UploadedFilesTransformer.php
@@ -0,0 +1,61 @@
+transformDatatables($array, $total);
+ }
+
+
+ public function transformFile(Actionlog $file)
+ {
+ $snipeModel = $file->item_type;
+
+ $array = [
+ 'id' => (int) $file->id,
+ 'icon' => Helper::filetype_icon($file->filename),
+ 'name' => e($file->filename),
+ 'item' => ($file->item_type) ? [
+ 'id' => (int) $file->item_id,
+ 'type' => str_plural(strtolower(class_basename($file->item_type))),
+ ] : null,
+ 'filename' => e($file->filename),
+ 'filetype' => StorageHelper::getFiletype($file->uploads_file_path()),
+ 'mediatype' => StorageHelper::getMediaType($file->uploads_file_path()),
+ 'url' => $file->uploads_file_url(),
+ 'note' => ($file->note) ? e($file->note) : null,
+ 'created_by' => ($file->adminuser) ? [
+ 'id' => (int) $file->adminuser->id,
+ 'name'=> e($file->adminuser->present()->fullName),
+ ] : null,
+ 'created_at' => Helper::getFormattedDateObject($file->created_at, 'datetime'),
+ 'deleted_at' => Helper::getFormattedDateObject($file->deleted_at, 'datetime'),
+ 'inlineable' => StorageHelper::allowSafeInline($file->uploads_file_path()) ?? false,
+ 'exists_on_disk' => (Storage::exists($file->uploads_file_path()) ? true : false),
+ ];
+
+ $permissions_array['available_actions'] = [
+ 'delete' => (Gate::allows('update', $snipeModel) && ($file->deleted_at == '')),
+ ];
+
+ $array += $permissions_array;
+ return $array;
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/Http/Transformers/UsersTransformer.php b/app/Http/Transformers/UsersTransformer.php
index 3bf3ee970265..01fab4d624be 100644
--- a/app/Http/Transformers/UsersTransformer.php
+++ b/app/Http/Transformers/UsersTransformer.php
@@ -2,7 +2,10 @@
namespace App\Http\Transformers;
+use App\Helpers\CustomFieldHelper;
use App\Helpers\Helper;
+use App\Models\CustomField;
+use App\Models\CustomFieldset;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Gate;
@@ -22,23 +25,31 @@ public function transformUsers(Collection $users, $total)
public function transformUser(User $user)
{
+ $role = null;
+ if ($user->isSuperUser()) {
+ $role = 'superadmin';
+ } elseif ($user->isAdmin()) {
+ $role = 'admin';
+ }
$array = [
'id' => (int) $user->id,
'avatar' => e($user->present()->gravatar) ?? null,
- 'name' => e($user->getFullNameAttribute()),
- 'first_name' => e($user->first_name),
- 'last_name' => e($user->last_name),
- 'username' => e($user->username),
+ 'name' => e($user->getFullNameAttribute()) ?? null,
+ 'first_name' => e($user->first_name) ?? null,
+ 'last_name' => e($user->last_name) ?? null,
+ 'display_name' => ($user->getRawOriginal('display_name')) ? e($user->getRawOriginal('display_name')) : null,
+ 'username' => e($user->username) ?? null,
'remote' => ($user->remote == '1') ? true : false,
'locale' => ($user->locale) ? e($user->locale) : null,
'employee_num' => ($user->employee_num) ? e($user->employee_num) : null,
'manager' => ($user->manager) ? [
'id' => (int) $user->manager->id,
- 'name'=> e($user->manager->first_name).' '.e($user->manager->last_name),
+ 'name'=> e($user->manager->display_name),
] : null,
'jobtitle' => ($user->jobtitle) ? e($user->jobtitle) : null,
'vip' => ($user->vip == '1') ? true : false,
'phone' => ($user->phone) ? e($user->phone) : null,
+ 'mobile' => ($user->mobile) ? e($user->mobile) : null,
'website' => ($user->website) ? e($user->website) : null,
'address' => ($user->address) ? e($user->address) : null,
'city' => ($user->city) ? e($user->city) : null,
@@ -50,11 +61,16 @@ public function transformUser(User $user)
'id' => (int) $user->department->id,
'name'=> e($user->department->name),
] : null,
+ 'department_manager' => ($user->department?->manager) ? [
+ 'id' => (int) $user->department->manager->id,
+ 'name'=> e($user->department->manager->display_name),
+ ] : null,
'location' => ($user->userloc) ? [
'id' => (int) $user->userloc->id,
'name'=> e($user->userloc->name),
] : null,
'notes'=> Helper::parseEscapedMarkedownInline($user->notes),
+ 'role' => $role,
'permissions' => $user->decodePermissions(),
'activated' => ($user->activated == '1') ? true : false,
'autoassign_licenses' => ($user->autoassign_licenses == '1') ? true : false,
@@ -70,7 +86,7 @@ public function transformUser(User $user)
'company' => ($user->company) ? ['id' => (int) $user->company->id, 'name'=> e($user->company->name)] : null,
'created_by' => ($user->createdBy) ? [
'id' => (int) $user->createdBy->id,
- 'name'=> e($user->createdBy->present()->fullName),
+ 'name'=> e($user->createdBy->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($user->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($user->updated_at, 'datetime'),
@@ -80,6 +96,8 @@ public function transformUser(User $user)
'deleted_at' => ($user->deleted_at) ? Helper::getFormattedDateObject($user->deleted_at, 'datetime') : null,
];
+ $array['custom_fields'] = CustomFieldHelper::transform(CustomFieldset::where('type',User::class)->first(), $user);
+
$permissions_array['available_actions'] = [
'update' => (Gate::allows('update', User::class) && ($user->deleted_at == '')),
'delete' => $user->isDeletable(),
@@ -126,6 +144,7 @@ public function transformUserCompact(User $user) : array
'first_name' => e($user->first_name),
'last_name' => e($user->last_name),
'username' => e($user->username),
+ 'display_name' => e($user->display_name),
'created_by' => $user->adminuser ? [
'id' => (int) $user->adminuser->id,
'name'=> e($user->adminuser->present()->fullName),
diff --git a/app/Importer/AssetImporter.php b/app/Importer/AssetImporter.php
index 1112a04e3508..dabe3035ad45 100644
--- a/app/Importer/AssetImporter.php
+++ b/app/Importer/AssetImporter.php
@@ -16,10 +16,22 @@ public function __construct($filename)
{
parent::__construct($filename);
- $this->defaultStatusLabelId = Statuslabel::first()->id;
-
+ $this->defaultStatusLabelId = Statuslabel::first()?->id;
+
if (!is_null(Statuslabel::deployable()->first())) {
- $this->defaultStatusLabelId = Statuslabel::deployable()->first()->id;
+ $this->defaultStatusLabelId = Statuslabel::deployable()->first()?->id;
+ }
+
+ if (is_null($this->defaultStatusLabelId)) {
+ $defaultLabel = Statuslabel::create([
+ 'name' => 'Default Status',
+ 'deployable' => 0,
+ 'pending' => 1,
+ 'archived' => 0,
+ 'notes' => 'Default status label created by AssetImporter',
+ ]);
+
+ $this->defaultStatusLabelId = $defaultLabel->id;
}
}
@@ -28,9 +40,10 @@ protected function handle($row)
// ItemImporter handles the general fetching.
parent::handle($row);
+ // FIXME : YUP!!!!! This shit needs to go (?) Yeah?
if ($this->customFields) {
foreach ($this->customFields as $customField) {
- $customFieldValue = $this->array_smart_custom_field_fetch($row, $customField);
+ $customFieldValue = $this->array_smart_custom_field_fetch($row, $customField); // TODO/FIXME - this might require a new 'mode' on customFill()?
if ($customFieldValue) {
if ($customField->field_encrypted == 1) {
@@ -40,7 +53,7 @@ protected function handle($row)
$this->item['custom_fields'][$customField->db_column_name()] = $customFieldValue;
$this->log('Custom Field '.$customField->name.': '.$customFieldValue);
}
- } else {
+ } else { // FIXME - think this through? Do we want to blank this? Is that how other stuff works?
// Clear out previous data.
$this->item['custom_fields'][$customField->db_column_name()] = null;
}
@@ -68,7 +81,16 @@ public function createAssetIfNotExists(array $row)
$asset_tag = Asset::autoincrement_asset();
}
- $asset = Asset::where(['asset_tag'=> (string) $asset_tag])->first();
+
+
+ if ($this->findCsvMatch($row, 'id')!='') {
+ // Override asset if an ID was given
+ \Log::debug('Finding asset by ID: '.$this->findCsvMatch($row, 'id'));
+ $asset = Asset::find($this->findCsvMatch($row, 'id'));
+ } else {
+ $asset = Asset::where(['asset_tag'=> (string) $asset_tag])->first();
+ }
+
if ($asset) {
if (! $this->updating) {
$exists_error = trans('general.import_asset_tag_exists', ['asset_tag' => $asset_tag]);
diff --git a/app/Importer/AssetModelImporter.php b/app/Importer/AssetModelImporter.php
index 7cfd8a530dbb..b60ad1d0fbfc 100644
--- a/app/Importer/AssetModelImporter.php
+++ b/app/Importer/AssetModelImporter.php
@@ -66,6 +66,7 @@ public function createAssetModelIfNotExists(array $row)
$this->item['fieldset'] = trim($this->findCsvMatch($row, 'fieldset'));
$this->item['depreciation'] = trim($this->findCsvMatch($row, 'depreciation'));
$this->item['requestable'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'requestable'))) == 1) ? 1 : 0;
+ $this->item['require_serial'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'require_serial'))) == 1) ? 1 : 0;
if (!empty($this->item['category'])) {
if ($category = $this->createOrFetchCategory($this->item['category'])) {
diff --git a/app/Importer/CategoryImporter.php b/app/Importer/CategoryImporter.php
new file mode 100644
index 000000000000..39b477c96a78
--- /dev/null
+++ b/app/Importer/CategoryImporter.php
@@ -0,0 +1,99 @@
+createCategoryIfNotExists($row);
+ }
+
+ /**
+ * Create a category if a duplicate does not exist.
+ * @todo Investigate how this should interact with Importer::createCategoryIfNotExists
+ *
+ * @author A. Gianotto
+ * @since 6.1.0
+ * @param array $row
+ */
+ public function createCategoryIfNotExists(array $row)
+ {
+
+ $editingCategory = false;
+
+ $category = Category::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
+
+ if ($this->findCsvMatch($row, 'id')!='') {
+ // Override category if an ID was given
+ \Log::debug('Finding category by ID: '.$this->findCsvMatch($row, 'id'));
+ $category = Category::find($this->findCsvMatch($row, 'id'));
+ }
+
+
+ if ($category) {
+ if (! $this->updating) {
+ $this->log('A matching Category '.$this->item['name'].' already exists');
+ return;
+ }
+
+ $this->log('Updating Category');
+ $editingCategory = true;
+ } else {
+ $this->log('No Matching Category, Create a new one');
+ $category = new Category;
+ $category->created_by = auth()->id();
+ }
+
+ // Pull the records from the CSV to determine their values
+ $this->item['name'] = trim($this->findCsvMatch($row, 'name'));
+ $this->item['notes'] = trim($this->findCsvMatch($row, 'notes'));
+ $this->item['eula_text'] = trim($this->findCsvMatch($row, 'eula_text'));
+ $this->item['category_type'] = trim(strtolower($this->findCsvMatch($row, 'category_type')));
+ $this->item['use_default_eula'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'use_default_eula'))) == 1) ? 1 : 0;
+ $this->item['require_acceptance'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'require_acceptance'))) == 1) ? 1 : 0;
+ $this->item['checkin_email'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'checkin_email'))) == 1) ? 1 : 0;
+
+
+ Log::debug('Item array is: ');
+ Log::debug(print_r($this->item, true));
+
+
+ if ($editingCategory) {
+ Log::debug('Updating existing category');
+ $category->update($this->sanitizeItemForUpdating($category));
+ } else {
+ Log::debug('Creating category');
+ $category->fill($this->sanitizeItemForStoring($category));
+ }
+
+ if ($category->save()) {
+ $this->log('Category '.$category->name.' created or updated from CSV import');
+ return $category;
+
+ } else {
+ Log::debug($category->getErrors());
+ $this->logError($category, 'Category "'.$this->item['name'].'"');
+ return $category->errors;
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/app/Importer/ConsumableImporter.php b/app/Importer/ConsumableImporter.php
index 10ffaaedfb5a..40c597e89664 100644
--- a/app/Importer/ConsumableImporter.php
+++ b/app/Importer/ConsumableImporter.php
@@ -28,9 +28,9 @@ public function createConsumableIfNotExists($row)
{
$consumable = Consumable::where('name', trim($this->item['name']))->first();
if ($consumable) {
+
if (! $this->updating) {
$this->log('A matching Consumable '.$this->item['name'].' already exists. ');
-
return;
}
$this->log('Updating Consumable');
@@ -39,12 +39,10 @@ public function createConsumableIfNotExists($row)
return;
}
+
$this->log('No matching consumable, creating one');
$consumable = new Consumable();
$consumable->created_by = auth()->id();
- $this->item['model_number'] = trim($this->findCsvMatch($row, 'model_number'));
- $this->item['item_no'] = trim($this->findCsvMatch($row, 'item_number'));
- $this->item['min_amt'] = trim($this->findCsvMatch($row, "min_amt"));
$consumable->fill($this->sanitizeItemForStoring($consumable));
// This sets an attribute on the Loggable trait for the action log
diff --git a/app/Importer/Importer.php b/app/Importer/Importer.php
index 0d4b8d493290..afe0a5720855 100644
--- a/app/Importer/Importer.php
+++ b/app/Importer/Importer.php
@@ -72,6 +72,7 @@ abstract class Importer
'termination_date' => 'termination date',
'warranty_months' => 'warranty',
'full_name' => 'full name',
+ 'display_name' => 'display name',
'email' => 'email',
'username' => 'username',
'address' => 'address',
@@ -88,6 +89,7 @@ abstract class Importer
'department' => 'department',
'manager_name' => 'manager full name',
'manager_username' => 'manager username',
+ 'manager_employee_num' => 'manager employee number',
'min_amt' => 'minimum quantity',
'remote' => 'remote',
'vip' => 'vip',
@@ -132,7 +134,7 @@ public function __construct($file)
} else {
$this->csv = Reader::createFromString($file);
}
- $this->tempPassword = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 40);
+ $this->tempPassword = '*** NO PASSWORD - IMPORTED VIA CSV ***';
}
// Cached Values for import lookups
@@ -183,7 +185,7 @@ abstract protected function handle($row);
* @author Daniel Meltzer
* @since 5.0
*/
- protected function populateCustomFields($headerRow)
+ protected function populateCustomFields($headerRow) // FIXME - what in the actual fuck is this.
{
// Stolen From https://adamwathan.me/2016/07/14/customizing-keys-when-mapping-collections/
// This 'inverts' the fields such that we have a collection of fields indexed by name.
@@ -298,6 +300,7 @@ protected function createOrFetchUser($row, $type = 'user')
'full_name' => $this->findCsvMatch($row, 'full_name'),
'first_name' => $this->findCsvMatch($row, 'first_name'),
'last_name' => $this->findCsvMatch($row, 'last_name'),
+ 'display_name' => $this->findCsvMatch($row, 'display_name'),
'email' => $this->findCsvMatch($row, 'email'),
'manager_id'=> '',
'department_id' => '',
@@ -368,6 +371,7 @@ protected function createOrFetchUser($row, $type = 'user')
$user->first_name = $user_array['first_name'];
$user->last_name = $user_array['last_name'];
$user->username = $user_array['username'];
+ $user->display_name = $user_array['display_name'] ?? null;
$user->email = $user_array['email'];
$user->manager_id = $user_array['manager_id'] ?? null;
$user->department_id = $user_array['department_id'] ?? null;
diff --git a/app/Importer/ItemImporter.php b/app/Importer/ItemImporter.php
index 435f081aba2a..92f44d991d89 100644
--- a/app/Importer/ItemImporter.php
+++ b/app/Importer/ItemImporter.php
@@ -24,6 +24,12 @@ public function __construct($filename)
protected function handle($row)
{
+
+ /**
+ * This section adds the most common fields into the $item array so we don't have to manually add them to
+ * things like accessories, consumables, etc.
+ */
+
// Need to reset this between iterations or we'll have stale data.
$this->item = [];
@@ -73,29 +79,20 @@ protected function handle($row)
$this->item['notes'] = $this->findCsvMatch($row, 'notes');
$this->item['order_number'] = $this->findCsvMatch($row, 'order_number');
$this->item['purchase_cost'] = $this->findCsvMatch($row, 'purchase_cost');
+ $this->item['model_number'] = trim($this->findCsvMatch($row, 'model_number'));
+ $this->item['min_amt'] = $this->findCsvMatch($row, 'min_amt');
+ $this->item['qty'] = $this->findCsvMatch($row, 'quantity');
+ $this->item['requestable'] = $this->findCsvMatch($row, 'requestable');
+ $this->item['created_by'] = auth()->id();
+ $this->item['serial'] = $this->findCsvMatch($row, 'serial');
+ $this->item['item_no'] = trim($this->findCsvMatch($row, 'item_no'));
+
$this->item['purchase_date'] = null;
if ($this->findCsvMatch($row, 'purchase_date') != '') {
$this->item['purchase_date'] = date('Y-m-d', strtotime($this->findCsvMatch($row, 'purchase_date')));
}
-// $this->item['asset_eol_date'] = null;
-// if ($this->findCsvMatch($row, 'asset_eol_date') != '') {
-// $csvMatch = $this->findCsvMatch($row, 'asset_eol_date');
-// \Log::warning('EOL Date for $csvMatch is '.$csvMatch);
-// try {
-// $this->item['asset_eol_date'] = CarbonImmutable::parse($csvMatch)->format('Y-m-d');
-// } catch (\Exception $e) {
-// Log::info($e->getMessage());
-// $this->log('Unable to parse date: '.$csvMatch);
-// }
-// }
-
-
- $this->item['qty'] = $this->findCsvMatch($row, 'quantity');
- $this->item['requestable'] = $this->findCsvMatch($row, 'requestable');
- $this->item['created_by'] = auth()->id();
- $this->item['serial'] = $this->findCsvMatch($row, 'serial');
// NO need to call this method if we're running the user import.
// TODO: Merge these methods.
$this->item['checkout_class'] = $this->findCsvMatch($row, 'checkout_class');
@@ -113,7 +110,7 @@ protected function handle($row)
protected function determineCheckout($row)
{
// Locations don't get checked out to anyone/anything
- if ((get_class($this) == LocationImporter::class) || (get_class($this) == AssetModelImporter::class)) {
+ if ((get_class($this) == LocationImporter::class) || (get_class($this) == AssetModelImporter::class) || (get_class($this) == SupplierImporter::class) || (get_class($this) == ManufacturerImporter::class) || (get_class($this) == CategoryImporter::class)) {
return;
}
@@ -356,16 +353,27 @@ public function createOrFetchCompany($asset_company_name)
* @param $user_manager string
* @return int id of company created/found
*/
- public function fetchManager($user_manager_first_name, $user_manager_last_name)
+ public function fetchManager($user_manager_username = null, $user_manager_employee_num = null, $user_manager_first_name = null, $user_manager_last_name = null)
{
- $manager = User::where('first_name', '=', $user_manager_first_name)
- ->where('last_name', '=', $user_manager_last_name)->first();
+ if ($user_manager_username!='') {
+ $manager = User::where('username', '=', $user_manager_username)->first();
+ $this->log('Checking on username '.$user_manager_username);
+ } elseif ($user_manager_employee_num!='') {
+ $manager = User::where('employee_num', '=', $user_manager_employee_num)->first();
+ $this->log('Checking on employee_num '.$user_manager_employee_num);
+ } else {
+ $manager = User::where('first_name', '=', $user_manager_first_name)
+ ->where('last_name', '=', $user_manager_last_name)->first();
+ $this->log('Checking on full name');
+ }
+
if ($manager) {
$this->log('A matching Manager '.$user_manager_first_name.' '.$user_manager_last_name.' already exists');
return $manager->id;
}
- $this->log('No matching Manager '.$user_manager_first_name.' '.$user_manager_last_name.' found. If their user account is being created through this import, you should re-process this file again. ');
+
+ $this->log('No matching Manager found. If their user account is being created through this import, you should re-process this file again. ');
return null;
}
@@ -512,7 +520,6 @@ public function createOrFetchSupplier($item_supplier)
if ($supplier->save()) {
$this->log('Supplier '.$item_supplier.' was created');
-
return $supplier->id;
}
$this->logError($supplier, 'Supplier');
diff --git a/app/Importer/LicenseImporter.php b/app/Importer/LicenseImporter.php
index 0dc7475478b8..d5df4aba3c59 100644
--- a/app/Importer/LicenseImporter.php
+++ b/app/Importer/LicenseImporter.php
@@ -68,6 +68,7 @@ public function createLicenseIfNotExists(array $row)
$this->item['order_number'] = trim($this->findCsvMatch($row, 'order_number'));
$this->item['reassignable'] = trim($this->findCsvMatch($row, 'reassignable'));
$this->item['manufacturer'] = $this->createOrFetchManufacturer(trim($this->findCsvMatch($row, 'manufacturer')));
+ $this->item['min_amt'] = trim($this->findCsvMatch($row, 'min_amt'));
if($this->item['reassignable'] == "")
{
diff --git a/app/Importer/ManufacturerImporter.php b/app/Importer/ManufacturerImporter.php
new file mode 100644
index 000000000000..4c627e9d99d1
--- /dev/null
+++ b/app/Importer/ManufacturerImporter.php
@@ -0,0 +1,101 @@
+createManufacturerIfNotExists($row);
+ }
+
+ /**
+ * Create a manufacturer if a duplicate does not exist.
+ * @todo Investigate how this should interact with Importer::createManufacturerIfNotExists
+ *
+ * @author A. Gianotto
+ * @since 6.1.0
+ * @param array $row
+ */
+ public function createManufacturerIfNotExists(array $row)
+ {
+
+ $editingManufacturer = false;
+
+ $manufacturer = Manufacturer::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
+
+ if ($this->findCsvMatch($row, 'id')!='') {
+ // Override manufacturer if an ID was given
+ \Log::debug('Finding manufacturer by ID: '.$this->findCsvMatch($row, 'id'));
+ $manufacturer = Manufacturer::find($this->findCsvMatch($row, 'id'));
+ }
+
+
+ if ($manufacturer) {
+ if (! $this->updating) {
+ $this->log('A matching Manufacturer '.$this->item['name'].' already exists');
+ return;
+ }
+
+ $this->log('Updating Manufacturer');
+ $editingManufacturer = true;
+ } else {
+ $this->log('No Matching Manufacturer, Create a new one');
+ $manufacturer = new Manufacturer;
+ $manufacturer->created_by = auth()->id();
+ }
+
+ // Pull the records from the CSV to determine their values
+ $this->item['name'] = trim($this->findCsvMatch($row, 'name'));
+ $this->item['support_phone'] = trim($this->findCsvMatch($row, 'support_phone'));
+ $this->item['fax'] = trim($this->findCsvMatch($row, 'fax'));
+ $this->item['support_email'] = trim($this->findCsvMatch($row, 'support_email'));
+ $this->item['contact'] = trim($this->findCsvMatch($row, 'contact'));
+ $this->item['url'] = trim($this->findCsvMatch($row, 'url'));
+ $this->item['support_url'] = trim($this->findCsvMatch($row, 'support_url'));
+ $this->item['warranty_lookup_url'] = trim($this->findCsvMatch($row, 'warranty_lookup_url'));
+ $this->item['notes'] = trim($this->findCsvMatch($row, 'notes'));
+
+
+ Log::debug('Item array is: ');
+ Log::debug(print_r($this->item, true));
+
+
+ if ($editingManufacturer) {
+ Log::debug('Updating existing manufacturer');
+ $manufacturer->update($this->sanitizeItemForUpdating($manufacturer));
+ } else {
+ Log::debug('Creating manufacturer');
+ $manufacturer->fill($this->sanitizeItemForStoring($manufacturer));
+ }
+
+ if ($manufacturer->save()) {
+ $this->log('Manufacturer '.$manufacturer->name.' created or updated from CSV import');
+ return $manufacturer;
+
+ } else {
+ Log::debug($manufacturer->getErrors());
+ $this->logError($manufacturer, 'Manufacturer "'.$this->item['name'].'"');
+ return $manufacturer->errors;
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/app/Importer/SupplierImporter.php b/app/Importer/SupplierImporter.php
new file mode 100644
index 000000000000..7878aff83521
--- /dev/null
+++ b/app/Importer/SupplierImporter.php
@@ -0,0 +1,105 @@
+createSupplierIfNotExists($row);
+ }
+
+ /**
+ * Create a supplier if a duplicate does not exist.
+ * @todo Investigate how this should interact with Importer::createSupplierIfNotExists
+ *
+ * @author A. Gianotto
+ * @since 6.1.0
+ * @param array $row
+ */
+ public function createSupplierIfNotExists(array $row)
+ {
+
+ $editingSupplier = false;
+
+ $supplier = Supplier::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
+
+ if ($this->findCsvMatch($row, 'id')!='') {
+ // Override supplier if an ID was given
+ \Log::debug('Finding supplier by ID: '.$this->findCsvMatch($row, 'id'));
+ $supplier = Supplier::find($this->findCsvMatch($row, 'id'));
+ }
+
+
+ if ($supplier) {
+ if (! $this->updating) {
+ $this->log('A matching Supplier '.$this->item['name'].' already exists');
+ return;
+ }
+
+ $this->log('Updating Supplier');
+ $editingSupplier = true;
+ } else {
+ $this->log('No Matching Supplier, Create a new one');
+ $supplier = new Supplier;
+ $supplier->created_by = auth()->id();
+ }
+
+ // Pull the records from the CSV to determine their values
+ $this->item['name'] = trim($this->findCsvMatch($row, 'name'));
+ $this->item['address'] = trim($this->findCsvMatch($row, 'address'));
+ $this->item['address2'] = trim($this->findCsvMatch($row, 'address2'));
+ $this->item['city'] = trim($this->findCsvMatch($row, 'city'));
+ $this->item['state'] = trim($this->findCsvMatch($row, 'state'));
+ $this->item['country'] = trim($this->findCsvMatch($row, 'country'));
+ $this->item['zip'] = trim($this->findCsvMatch($row, 'zip'));
+ $this->item['phone'] = trim($this->findCsvMatch($row, 'phone'));
+ $this->item['fax'] = trim($this->findCsvMatch($row, 'fax'));
+ $this->item['email'] = trim($this->findCsvMatch($row, 'email'));
+ $this->item['contact'] = trim($this->findCsvMatch($row, 'contact'));
+ $this->item['url'] = trim($this->findCsvMatch($row, 'url'));
+ $this->item['notes'] = trim($this->findCsvMatch($row, 'notes'));
+
+
+ Log::debug('Item array is: ');
+ Log::debug(print_r($this->item, true));
+
+
+ if ($editingSupplier) {
+ Log::debug('Updating existing supplier');
+ $supplier->update($this->sanitizeItemForUpdating($supplier));
+ } else {
+ Log::debug('Creating supplier');
+ $supplier->fill($this->sanitizeItemForStoring($supplier));
+ }
+
+ if ($supplier->save()) {
+ $this->log('Supplier '.$supplier->name.' created or updated from CSV import');
+ return $supplier;
+
+ } else {
+ Log::debug($supplier->getErrors());
+ $this->logError($supplier, 'Supplier "'.$this->item['name'].'"');
+ return $supplier->errors;
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/app/Importer/UserImporter.php b/app/Importer/UserImporter.php
index 77317b3d09db..942f1cf4a21b 100644
--- a/app/Importer/UserImporter.php
+++ b/app/Importer/UserImporter.php
@@ -7,7 +7,9 @@
use App\Models\Setting;
use App\Models\User;
use App\Notifications\WelcomeNotification;
+use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Password;
/**
* This is ONLY used for the User Import. When we are importing users
@@ -45,11 +47,13 @@ public function createUserIfNotExists(array $row)
// Pull the records from the CSV to determine their values
$this->item['id'] = trim($this->findCsvMatch($row, 'id'));
$this->item['username'] = trim($this->findCsvMatch($row, 'username'));
+ $this->item['display_name'] = trim($this->findCsvMatch($row, 'display_name'));
$this->item['first_name'] = trim($this->findCsvMatch($row, 'first_name'));
$this->item['last_name'] = trim($this->findCsvMatch($row, 'last_name'));
$this->item['email'] = trim($this->findCsvMatch($row, 'email'));
$this->item['gravatar'] = trim($this->findCsvMatch($row, 'gravatar'));
$this->item['phone'] = trim($this->findCsvMatch($row, 'phone_number'));
+ $this->item['mobile'] = trim($this->findCsvMatch($row, 'mobile_number'));
$this->item['website'] = trim($this->findCsvMatch($row, 'website'));
$this->item['jobtitle'] = trim($this->findCsvMatch($row, 'jobtitle'));
$this->item['address'] = trim($this->findCsvMatch($row, 'address'));
@@ -62,7 +66,7 @@ public function createUserIfNotExists(array $row)
$this->item['activated'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'activated'))) == 1) ? '1' : 0;
$this->item['employee_num'] = trim($this->findCsvMatch($row, 'employee_num'));
$this->item['department_id'] = trim($this->createOrFetchDepartment(trim($this->findCsvMatch($row, 'department'))));
- $this->item['manager_id'] = $this->fetchManager(trim($this->findCsvMatch($row, 'manager_first_name')), trim($this->findCsvMatch($row, 'manager_last_name')));
+ $this->item['manager_id'] = $this->fetchManager(trim($this->findCsvMatch($row, 'manager_username')), trim($this->findCsvMatch($row, 'manager_employee_num')), trim($this->findCsvMatch($row, 'manager_first_name')), trim($this->findCsvMatch($row, 'manager_last_name')));
$this->item['remote'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'remote'))) == 1 ) ? '1' : 0;
$this->item['vip'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'vip'))) ==1 ) ? '1' : 0;
$this->item['autoassign_licenses'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'autoassign_licenses'))) ==1 ) ? '1' : 0;
@@ -80,6 +84,7 @@ public function createUserIfNotExists(array $row)
$this->item['username'] = $user_formatted_array['username'];
}
+
// Check if a numeric ID was passed. If it does, use that above all else.
if ((array_key_exists('id', $this->item) && ($this->item['id'] != "") && (is_numeric($this->item['id'])))) {
$user = User::find($this->item['id']);
@@ -89,12 +94,25 @@ public function createUserIfNotExists(array $row)
if ($user) {
+ // If the user does not want to update existing values, only add new ones, bail out
if (! $this->updating) {
Log::debug('A matching User '.$this->item['name'].' already exists. ');
return;
}
+
$this->log('Updating User');
+
+ // Todo - check that this works
+ if (!Gate::allows('canEditAuthFields', $user)) {
+ unset($user->username);
+ unset($user->email);
+ unset($user->password);
+ unset($user->activated);
+ }
+
$user->update($this->sanitizeItemForUpdating($user));
+
+ // Why do we have to do this twice? Update should
$user->save();
// Update the location of any assets checked out to this user
@@ -110,28 +128,32 @@ public function createUserIfNotExists(array $row)
// This needs to be applied after the update logic, otherwise we'll overwrite user passwords
// Issue #5408
- $this->item['password'] = bcrypt($this->tempPassword);
+ $this->item['password'] = $this->tempPassword;
$this->log('No matching user, creating one');
$user = new User();
$user->created_by = auth()->id();
+
$user->fill($this->sanitizeItemForStoring($user));
+ // TODO - check for gate here I guess
+
+
if ($user->save()) {
$this->log('User '.$this->item['name'].' was created');
if (($user->email) && ($user->activated == '1')) {
- $data = [
- 'email' => $user->email,
- 'username' => $user->username,
- 'first_name' => $user->first_name,
- 'last_name' => $user->last_name,
- 'password' => $this->tempPassword,
- ];
if ($this->send_welcome) {
- $user->notify(new WelcomeNotification($data));
+
+ try {
+ $user->notify(new WelcomeNotification($user));
+ } catch (\Exception $e) {
+ Log::warning('Could not send welcome notification for user: ' . $e->getMessage());
+ }
+
}
+
}
$user = null;
$this->item = null;
@@ -140,9 +162,9 @@ public function createUserIfNotExists(array $row)
}
$this->logError($user, 'User');
- return;
}
+
/**
* Fetch an existing department, or create new if it doesn't exist
*
diff --git a/app/LegacyEncrypter/BaseEncrypter.php b/app/LegacyEncrypter/BaseEncrypter.php
deleted file mode 100644
index adc1e451d4bc..000000000000
--- a/app/LegacyEncrypter/BaseEncrypter.php
+++ /dev/null
@@ -1,81 +0,0 @@
-key);
- }
-
- /**
- * Get the JSON array from the given payload.
- *
- * @param string $payload
- * @return array
- *
- * @throws \Illuminate\Contracts\Encryption\DecryptException
- */
- protected function getJsonPayload($payload)
- {
- $payload = json_decode(base64_decode($payload), true);
-
- // If the payload is not valid JSON or does not have the proper keys set we will
- // assume it is invalid and bail out of the routine since we will not be able
- // to decrypt the given value. We'll also check the MAC for this encryption.
- if (! $payload || $this->invalidPayload($payload)) {
- throw new DecryptException('The payload is invalid.');
- }
-
- if (! $this->validMac($payload)) {
- throw new DecryptException('The MAC is invalid.');
- }
-
- return $payload;
- }
-
- /**
- * Verify that the encryption payload is valid.
- *
- * @param array|mixed $data
- * @return bool
- */
- protected function invalidPayload($data)
- {
- return ! is_array($data) || ! isset($data['iv']) || ! isset($data['value']) || ! isset($data['mac']);
- }
-
- /**
- * Determine if the MAC for the given payload is valid.
- *
- * @param array $payload
- * @return bool
- *
- * @throws \RuntimeException
- */
- protected function validMac(array $payload)
- {
- $bytes = random_bytes(16);
-
- $calcMac = hash_hmac('sha256', $this->hash($payload['iv'], $payload['value']), $bytes, true);
-
- return hash_equals(hash_hmac('sha256', $payload['mac'], $bytes, true), $calcMac);
- }
-}
diff --git a/app/LegacyEncrypter/McryptEncrypter.php b/app/LegacyEncrypter/McryptEncrypter.php
deleted file mode 100644
index ac0a49fc5869..000000000000
--- a/app/LegacyEncrypter/McryptEncrypter.php
+++ /dev/null
@@ -1,214 +0,0 @@
-key = $key;
- $this->cipher = $cipher;
- $this->block = mcrypt_get_iv_size($this->cipher, MCRYPT_MODE_CBC);
- } else {
- throw new RuntimeException('The only supported ciphers are MCRYPT_RIJNDAEL_128 and MCRYPT_RIJNDAEL_256.');
- }
- }
-
- /**
- * Determine if the given key and cipher combination is valid.
- *
- * @param string $key
- * @param string $cipher
- * @return bool
- */
- public static function supported($key, $cipher)
- {
- return defined('MCRYPT_RIJNDAEL_128') &&
- ($cipher === MCRYPT_RIJNDAEL_128 || $cipher === MCRYPT_RIJNDAEL_256);
- }
-
- /**
- * Encrypt the given value.
- *
- * @param string $value
- * @return string
- *
- * @throws \Illuminate\Contracts\Encryption\EncryptException
- */
- public function encrypt($value, $serialize = true)
- {
- $iv = mcrypt_create_iv($this->getIvSize(), $this->getRandomizer());
-
- $value = base64_encode($this->padAndMcrypt($value, $iv));
-
- // Once we have the encrypted value we will go ahead base64_encode the input
- // vector and create the MAC for the encrypted value so we can verify its
- // authenticity. Then, we'll JSON encode the data in a "payload" array.
- $mac = $this->hash($iv = base64_encode($iv), $value);
-
- $json = json_encode(compact('iv', 'value', 'mac'));
-
- if (! is_string($json)) {
- throw new EncryptException('Could not encrypt the data.');
- }
-
- return base64_encode($json);
- }
-
- /**
- * Pad and use mcrypt on the given value and input vector.
- *
- * @param string $value
- * @param string $iv
- * @return string
- */
- protected function padAndMcrypt($value, $iv)
- {
- $value = $this->addPadding(serialize($value));
-
- return mcrypt_encrypt($this->cipher, $this->key, $value, MCRYPT_MODE_CBC, $iv);
- }
-
- /**
- * Decrypt the given value.
- *
- * @param string $payload
- * @return string
- */
- public function decrypt($payload, $unserialize = true)
- {
- $payload = $this->getJsonPayload($payload);
-
- // We'll go ahead and remove the PKCS7 padding from the encrypted value before
- // we decrypt it. Once we have the de-padded value, we will grab the vector
- // and decrypt the data, passing back the unserialized from of the value.
- $value = base64_decode($payload['value']);
-
- $iv = base64_decode($payload['iv']);
-
- return unserialize($this->stripPadding($this->mcryptDecrypt($value, $iv)));
- }
-
- /**
- * Run the mcrypt decryption routine for the value.
- *
- * @param string $value
- * @param string $iv
- * @return string
- *
- * @throws \Illuminate\Contracts\Encryption\DecryptException
- */
- protected function mcryptDecrypt($value, $iv)
- {
- try {
- return mcrypt_decrypt($this->cipher, $this->key, $value, MCRYPT_MODE_CBC, $iv);
- } catch (Exception $e) {
- throw new DecryptException($e->getMessage());
- }
- }
-
- /**
- * Add PKCS7 padding to a given value.
- *
- * @param string $value
- * @return string
- */
- protected function addPadding($value)
- {
- $pad = $this->block - (strlen($value) % $this->block);
-
- return $value.str_repeat(chr($pad), $pad);
- }
-
- /**
- * Remove the padding from the given value.
- *
- * @param string $value
- * @return string
- */
- protected function stripPadding($value)
- {
- $pad = ord($value[($len = strlen($value)) - 1]);
-
- return $this->paddingIsValid($pad, $value) ? substr($value, 0, $len - $pad) : $value;
- }
-
- /**
- * Determine if the given padding for a value is valid.
- *
- * @param string $pad
- * @param string $value
- * @return bool
- */
- protected function paddingIsValid($pad, $value)
- {
- $beforePad = strlen($value) - $pad;
-
- return substr($value, $beforePad) == str_repeat(substr($value, -1), $pad);
- }
-
- /**
- * Get the IV size for the cipher.
- *
- * @return int
- */
- protected function getIvSize()
- {
- return mcrypt_get_iv_size($this->cipher, MCRYPT_MODE_CBC);
- }
-
- /**
- * Get the random data source available for the OS.
- *
- * @return int
- */
- protected function getRandomizer()
- {
- if (defined('MCRYPT_DEV_URANDOM')) {
- return MCRYPT_DEV_URANDOM;
- }
-
- if (defined('MCRYPT_DEV_RANDOM')) {
- return MCRYPT_DEV_RANDOM;
- }
-
- mt_srand();
-
- return MCRYPT_RAND;
- }
-}
diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php
index 0811e9ee57ac..9674bd63a3ed 100644
--- a/app/Listeners/CheckoutableListener.php
+++ b/app/Listeners/CheckoutableListener.php
@@ -4,14 +4,17 @@
use App\Events\CheckoutableCheckedOut;
use App\Mail\CheckinAccessoryMail;
+use App\Mail\CheckinComponentMail;
use App\Mail\CheckinLicenseMail;
use App\Mail\CheckoutAccessoryMail;
use App\Mail\CheckoutAssetMail;
use App\Mail\CheckinAssetMail;
+use App\Mail\CheckoutComponentMail;
use App\Mail\CheckoutConsumableMail;
use App\Mail\CheckoutLicenseMail;
use App\Models\Accessory;
use App\Models\Asset;
+use App\Models\Category;
use App\Models\CheckoutAcceptance;
use App\Models\Component;
use App\Models\Consumable;
@@ -21,12 +24,15 @@
use App\Models\User;
use App\Notifications\CheckinAccessoryNotification;
use App\Notifications\CheckinAssetNotification;
+use App\Notifications\CheckinComponentNotification;
use App\Notifications\CheckinLicenseSeatNotification;
use App\Notifications\CheckoutAccessoryNotification;
use App\Notifications\CheckoutAssetNotification;
+use App\Notifications\CheckoutComponentNotification;
use App\Notifications\CheckoutConsumableNotification;
use App\Notifications\CheckoutLicenseSeatNotification;
use GuzzleHttp\Exception\ClientException;
+use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;
use Exception;
@@ -37,9 +43,27 @@
class CheckoutableListener
{
private array $skipNotificationsFor = [
- Component::class,
+// Component::class,
];
+ /**
+ * Register the listeners for the subscriber.
+ *
+ * @param Illuminate\Events\Dispatcher $events
+ */
+ public function subscribe($events)
+ {
+ $events->listen(
+ \App\Events\CheckoutableCheckedIn::class,
+ 'App\Listeners\CheckoutableListener@onCheckedIn'
+ );
+
+ $events->listen(
+ \App\Events\CheckoutableCheckedOut::class,
+ 'App\Listeners\CheckoutableListener@onCheckedOut'
+ );
+ }
+
/**
* Notify the user and post to webhook about the checked out checkoutable
* and add a record to the checkout_requests table.
@@ -50,86 +74,71 @@ public function onCheckedOut($event)
return;
}
- /**
- * Make a checkout acceptance and attach it in the notification
- */
- $settings = Setting::getSettings();
$acceptance = $this->getCheckoutAcceptance($event);
- $adminCcEmailsArray = [];
- if ($settings->admin_cc_email !== '') {
- $adminCcEmail = $settings->admin_cc_email;
- $adminCcEmailsArray = array_map('trim', explode(',', $adminCcEmail));
- }
- $ccEmails = array_filter($adminCcEmailsArray);
- $mailable = $this->getCheckoutMailType($event, $acceptance);
- $notifiable = $this->getNotifiables($event);
+ $shouldSendEmailToUser = $this->shouldSendCheckoutEmailToUser($event->checkoutable);
+ $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress($acceptance);
+ $shouldSendWebhookNotification = $this->shouldSendWebhookNotification();
- if ($event->checkedOutTo->locale) {
- $mailable->locale($event->checkedOutTo->locale);
+ if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) {
+ return;
}
- // Send email notifications
- try {
- /**
- * Send an email if any of the following conditions are met:
- * 1. The asset requires acceptance
- * 2. The item has a EULA
- * 3. The item should send an email at check-in/check-out
- */
- if ($event->checkoutable->requireAcceptance() || $event->checkoutable->getEula() ||
- $this->checkoutableShouldSendEmail($event)) {
- Log::info('Sending checkout email, Locale: ' . ($event->checkedOutTo->locale ?? 'default'));
- if (!empty($notifiable)) {
- Mail::to($notifiable)->cc($ccEmails)->send($mailable);
- } elseif (!empty($ccEmails)) {
- Mail::cc($ccEmails)->send($mailable);
+ if ($shouldSendEmailToUser || $shouldSendEmailToAlertAddress) {
+ $mailable = $this->getCheckoutMailType($event, $acceptance);
+ $notifiable = $this->getNotifiableUser($event);
+
+ $notifiableHasEmail = $notifiable instanceof User && $notifiable->email;
+
+ $shouldSendEmailToUser = $shouldSendEmailToUser && $notifiableHasEmail;
+
+ [$to, $cc] = $this->generateEmailRecipients($shouldSendEmailToUser, $shouldSendEmailToAlertAddress, $notifiable);
+
+ if (!empty($to)) {
+ try {
+ Mail::to(array_flatten($to))->send($mailable->locale($notifiable->locale));
+ Mail::to(array_flatten($cc))->send($mailable->locale(Setting::getSettings()->locale));
+ Log::info('Checkout Mail sent to checkout target');
+ } catch (ClientException $e) {
+ Log::debug("Exception caught during checkout email: " . $e->getMessage());
+ } catch (Exception $e) {
+ Log::debug("Exception caught during checkout email: " . $e->getMessage());
}
- Log::info('Checkout Mail sent.');
}
- } catch (ClientException $e) {
- Log::debug("Exception caught during checkout email: " . $e->getMessage());
- } catch (Exception $e) {
- Log::debug("Exception caught during checkout email: " . $e->getMessage());
}
-// Send Webhook notification
- try {
- if ($this->shouldSendWebhookNotification()) {
+
+ if ($shouldSendWebhookNotification) {
+ try {
if ($this->newMicrosoftTeamsWebhookEnabled()) {
$message = $this->getCheckoutNotification($event)->toMicrosoftTeams();
$notification = new TeamsNotification(Setting::getSettings()->webhook_endpoint);
$notification->success()->sendMessage($message[0], $message[1]); // Send the message to Microsoft Teams
} else {
-
Notification::route($this->webhookSelected(), Setting::getSettings()->webhook_endpoint)
->notify($this->getCheckoutNotification($event, $acceptance));
}
+ } catch (ClientException $e) {
+ if (strpos($e->getMessage(), 'channel_not_found') !== false) {
+ Log::warning(Setting::getSettings()->webhook_selected . " notification failed: " . $e->getMessage());
+ return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_channel_not_found'));
+ } else {
+ Log::error("ClientException caught during checkin notification: " . $e->getMessage());
+ }
+ return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
+ } catch (Exception $e) {
+ Log::warning(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [
+ 'error' => $e->getMessage(),
+ 'webhook_endpoint' => Setting::getSettings()->webhook_endpoint,
+ 'event' => $event,
+ ]);
+ return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
}
- } catch (ClientException $e) {
- if (strpos($e->getMessage(), 'channel_not_found') !== false) {
- Log::warning(Setting::getSettings()->webhook_selected." notification failed: " . $e->getMessage());
- return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_channel_not_found') );
- }
- else {
- Log::error("ClientException caught during checkin notification: " . $e->getMessage());
- }
- return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_fail') );
- } catch (Exception $e) {
- Log::error(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [
- 'error' => $e->getMessage(),
- 'webhook_endpoint' => Setting::getSettings()->webhook_endpoint,
- 'event' => $event,
- ]);
- return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
}
}
-
-
-
/**
* Notify the user and post to webhook about the checked in checkoutable
- */
+ */
public function onCheckedIn($event)
{
Log::debug('onCheckedIn in the Checkoutable listener fired');
@@ -138,60 +147,54 @@ public function onCheckedIn($event)
return;
}
- /**
- * Send the appropriate notification
- */
- if ($event->checkedOutTo && $event->checkoutable){
- $acceptances = CheckoutAcceptance::where('checkoutable_id', $event->checkoutable->id)
- ->where('assigned_to_id', $event->checkedOutTo->id)
- ->get();
-
- foreach($acceptances as $acceptance){
- if($acceptance->isPending()){
- $acceptance->delete();
- }
- }
+ $shouldSendEmailToUser = $this->checkoutableCategoryShouldSendEmail($event->checkoutable);
+ $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress();
+ $shouldSendWebhookNotification = $this->shouldSendWebhookNotification();
+ if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) {
+ return;
}
- $settings = Setting::getSettings();
- $adminCcEmailsArray = [];
- if($settings->admin_cc_email !== '') {
- $adminCcEmail = $settings->admin_cc_email;
- $adminCcEmailsArray = array_map('trim', explode(',', $adminCcEmail));
- }
- $ccEmails = array_filter($adminCcEmailsArray);
- $mailable = $this->getCheckinMailType($event);
- $notifiable = $this->getNotifiables($event);
- if ($event->checkedOutTo?->locale) {
- $mailable->locale($event->checkedOutTo->locale);
- }
- // Send email notifications
- try {
+ if ($shouldSendEmailToUser || $shouldSendEmailToAlertAddress) {
/**
- * Send an email if any of the following conditions are met:
- * 1. The asset requires acceptance
- * 2. The item has a EULA
- * 3. The item should send an email at check-in/check-out
+ * Send the appropriate notification
*/
- if ($event->checkoutable->requireAcceptance() || $event->checkoutable->getEula() ||
- $this->checkoutableShouldSendEmail($event)) {
- Log::info('Sending checkin email, Locale: ' . ($event->checkedOutTo->locale ?? 'default'));
- if (!empty($notifiable)) {
- Mail::to($notifiable)->cc($ccEmails)->send($mailable);
- } elseif (!empty($ccEmails)){
- Mail::cc($ccEmails)->send($mailable);
+ if ($event->checkedOutTo && $event->checkoutable) {
+ $acceptances = CheckoutAcceptance::where('checkoutable_id', $event->checkoutable->id)
+ ->where('assigned_to_id', $event->checkedOutTo->id)
+ ->get();
+
+ foreach ($acceptances as $acceptance) {
+ if ($acceptance->isPending()) {
+ $acceptance->delete();
}
- Log::info('Checkin Mail sent.');
}
- } catch (ClientException $e) {
- Log::debug("Exception caught during checkin email: " . $e->getMessage());
- } catch (Exception $e) {
- Log::debug("Exception caught during checkin email: " . $e->getMessage());
+ }
+
+ $mailable = $this->getCheckinMailType($event);
+ $notifiable = $this->getNotifiableUser($event);
+
+ $notifiableHasEmail = $notifiable instanceof User && $notifiable->email;
+
+ $shouldSendEmailToUser = $shouldSendEmailToUser && $notifiableHasEmail;
+
+ [$to, $cc] = $this->generateEmailRecipients($shouldSendEmailToUser, $shouldSendEmailToAlertAddress, $notifiable);
+
+ try {
+ if (!empty($to)) {
+ Mail::to(array_flatten($to))->send($mailable->locale($notifiable->locale));
+ Mail::to(array_flatten($cc))->send($mailable->locale(Setting::getSettings()->locale));
+ Log::info('Checkin Mail sent to CC addresses');
+ }
+ } catch (ClientException $e) {
+ Log::debug("Exception caught during checkin email: " . $e->getMessage());
+ } catch (Exception $e) {
+ Log::debug("Exception caught during checkin email: " . $e->getMessage());
+ }
}
- // Send Webhook notification
- try {
- if ($this->shouldSendWebhookNotification()) {
+ if ($shouldSendWebhookNotification) {
+ // Send Webhook notification
+ try {
if ($this->newMicrosoftTeamsWebhookEnabled()) {
$message = $this->getCheckinNotification($event)->toMicrosoftTeams();
$notification = new TeamsNotification(Setting::getSettings()->webhook_endpoint);
@@ -200,25 +203,24 @@ public function onCheckedIn($event)
Notification::route($this->webhookSelected(), Setting::getSettings()->webhook_endpoint)
->notify($this->getCheckinNotification($event));
}
- }
- } catch (ClientException $e) {
- if (strpos($e->getMessage(), 'channel_not_found') !== false) {
- Log::warning(Setting::getSettings()->webhook_selected." notification failed: " . $e->getMessage());
- return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_channel_not_found') );
- }
- else {
- Log::error("ClientException caught during checkin notification: " . $e->getMessage());
+ } catch (ClientException $e) {
+ if (strpos($e->getMessage(), 'channel_not_found') !== false) {
+ Log::warning(Setting::getSettings()->webhook_selected . " notification failed: " . $e->getMessage());
+ return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_channel_not_found'));
+ } else {
+ Log::error("ClientException caught during checkin notification: " . $e->getMessage());
+ return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
+ }
+ } catch (Exception $e) {
+ Log::warning(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [
+ 'error' => $e->getMessage(),
+ 'webhook_endpoint' => Setting::getSettings()->webhook_endpoint,
+ 'event' => $event,
+ ]);
return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail'));
}
- } catch (Exception $e) {
- Log::error(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [
- 'error' => $e->getMessage(),
- 'webhook_endpoint' => Setting::getSettings()->webhook_endpoint,
- 'event' => $event,
- ]);
- return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_fail'));
}
- }
+ }
/**
* Generates a checkout acceptance
@@ -231,6 +233,7 @@ private function getCheckoutAcceptance($event)
if ($checkedOutToType != "App\Models\User") {
return null;
}
+
if (!$event->checkoutable->requireAcceptance()) {
return null;
}
@@ -238,15 +241,22 @@ private function getCheckoutAcceptance($event)
$acceptance = new CheckoutAcceptance;
$acceptance->checkoutable()->associate($event->checkoutable);
$acceptance->assignedTo()->associate($event->checkedOutTo);
+
+ $category = $this->getCategoryFromCheckoutable($event->checkoutable);
+
+ if ($category?->alert_on_response) {
+ $acceptance->alert_on_response_id = auth()->id();
+ }
+
$acceptance->save();
- return $acceptance;
+ return $acceptance;
}
/**
* Get the appropriate notification for the event
- *
- * @param CheckoutableCheckedIn $event
+ *
+ * @param CheckoutableCheckedIn $event
* @return Notification
*/
private function getCheckinNotification($event)
@@ -260,17 +270,19 @@ private function getCheckinNotification($event)
break;
case Asset::class:
$notificationClass = CheckinAssetNotification::class;
- break;
+ break;
case LicenseSeat::class:
$notificationClass = CheckinLicenseSeatNotification::class;
break;
+ case Component::class:
+ $notificationClass = CheckinComponentNotification::class;
+ break;
}
Log::debug('Notification class: '.$notificationClass);
- return new $notificationClass($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note);
+ return new $notificationClass($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note);
}
-
/**
* Get the appropriate notification for the event
*
@@ -295,6 +307,9 @@ private function getCheckoutNotification($event, $acceptance = null)
case LicenseSeat::class:
$notificationClass = CheckoutLicenseSeatNotification::class;
break;
+ case Component::class:
+ $notificationClass = CheckoutComponentNotification::class;
+ break;
}
@@ -306,37 +321,53 @@ private function getCheckoutMailType($event, $acceptance){
Asset::class => CheckoutAssetMail::class,
LicenseSeat::class => CheckoutLicenseMail::class,
Consumable::class => CheckoutConsumableMail::class,
+ Component::class => CheckoutComponentMail::class,
];
$mailable= $lookup[get_class($event->checkoutable)];
return new $mailable($event->checkoutable, $event->checkedOutTo, $event->checkedOutBy, $acceptance, $event->note);
}
+
private function getCheckinMailType($event){
$lookup = [
Accessory::class => CheckinAccessoryMail::class,
Asset::class => CheckinAssetMail::class,
LicenseSeat::class => CheckinLicenseMail::class,
+ Component::class => CheckinComponentMail::class,
];
-
$mailable= $lookup[get_class($event->checkoutable)];
return new $mailable($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note);
}
- private function getNotifiables($event){
- if($event->checkedOutTo instanceof Asset){
+ /**
+ * This gets the recipient objects based on the type of checkoutable.
+ * The 'name' property for users is set in the boot method in the User model.
+ *
+ * @see \App\Models\User::boot()
+ * @param $event
+ * @return mixed
+ */
+ private function getNotifiableUser($event)
+ {
+
+ // If it's assigned to an asset, get that asset's assignedTo object
+ if ($event->checkedOutTo instanceof Asset){
$event->checkedOutTo->load('assignedTo');
- return $event->checkedOutTo->assignedto?->email ?? '';
- }
- else if($event->checkedOutTo instanceof Location) {
- return $event->checkedOutTo->manager?->email ?? '';
- }
- else{
- return $event->checkedOutTo?->email ?? '';
+ return $event->checkedOutTo->assignedto;
+
+ // If it's assigned to a location, get that location's manager object
+ } elseif ($event->checkedOutTo instanceof Location) {
+ return $event->checkedOutTo->manager;
+
+ // Otherwise just return the assigned to object
+ } else {
+ return $event->checkedOutTo;
}
}
+
private function webhookSelected(){
if(Setting::getSettings()->webhook_selected === 'slack' || Setting::getSettings()->webhook_selected === 'general'){
return 'slack';
@@ -345,24 +376,6 @@ private function webhookSelected(){
return Setting::getSettings()->webhook_selected;
}
- /**
- * Register the listeners for the subscriber.
- *
- * @param Illuminate\Events\Dispatcher $events
- */
- public function subscribe($events)
- {
- $events->listen(
- \App\Events\CheckoutableCheckedIn::class,
- 'App\Listeners\CheckoutableListener@onCheckedIn'
- );
-
- $events->listen(
- \App\Events\CheckoutableCheckedOut::class,
- 'App\Listeners\CheckoutableListener@onCheckedOut'
- );
- }
-
private function shouldNotSendAnyNotifications($checkoutable): bool
{
return in_array(get_class($checkoutable), $this->skipNotificationsFor);
@@ -373,16 +386,104 @@ private function shouldSendWebhookNotification(): bool
return Setting::getSettings() && Setting::getSettings()->webhook_endpoint;
}
- private function checkoutableShouldSendEmail($event): bool
+ private function checkoutableCategoryShouldSendEmail(Model $checkoutable): bool
{
- if($event->checkoutable instanceof LicenseSeat){
- return $event->checkoutable->license->checkin_email();
+ if ($checkoutable instanceof LicenseSeat) {
+ return $checkoutable->license->checkin_email();
}
- return (method_exists($event->checkoutable, 'checkin_email') && $event->checkoutable->checkin_email());
+ return (method_exists($checkoutable, 'checkin_email') && $checkoutable->checkin_email());
}
private function newMicrosoftTeamsWebhookEnabled(): bool
{
return Setting::getSettings()->webhook_selected === 'microsoft' && Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows');
}
+
+ private function shouldSendCheckoutEmailToUser(Model $checkoutable): bool
+ {
+ /**
+ * Send an email if any of the following conditions are met:
+ * 1. The asset requires acceptance
+ * 2. The item has a EULA
+ * 3. The item should send an email at check-in/check-out
+ */
+
+ if ($checkoutable->requireAcceptance()) {
+ return true;
+ }
+
+ if ($checkoutable->getEula()) {
+ return true;
+ }
+
+ if ($this->checkoutableCategoryShouldSendEmail($checkoutable)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private function shouldSendEmailToAlertAddress($acceptance = null): bool
+ {
+ $setting = Setting::getSettings();
+
+ if (!$setting) {
+ return false;
+ }
+
+ if (is_null($acceptance) && !$setting->admin_cc_always) {
+ return false;
+ }
+
+ return (bool) $setting->admin_cc_email;
+ }
+
+ private function getFormattedAlertAddresses(): array
+ {
+ $alertAddresses = Setting::getSettings()->admin_cc_email;
+
+ if ($alertAddresses !== '') {
+ return array_filter(array_map('trim', explode(',', $alertAddresses)));
+ }
+
+ return [];
+ }
+
+ private function generateEmailRecipients(
+ bool $shouldSendEmailToUser,
+ bool $shouldSendEmailToAlertAddress,
+ mixed $notifiable
+ ): array {
+ $to = [];
+ $cc = [];
+
+ // if user && cc: to user, cc admin
+ if ($shouldSendEmailToUser && $shouldSendEmailToAlertAddress) {
+ $to[] = $notifiable;
+ $cc[] = $this->getFormattedAlertAddresses();
+ }
+
+ // if user && no cc: to user
+ if ($shouldSendEmailToUser && !$shouldSendEmailToAlertAddress) {
+ $to[] = $notifiable;
+ }
+
+ // if no user && cc: to admin
+ if (!$shouldSendEmailToUser && $shouldSendEmailToAlertAddress) {
+ $to[] = $this->getFormattedAlertAddresses();
+ }
+
+ return array($to, $cc);
+ }
+
+ private function getCategoryFromCheckoutable(Model $checkoutable): ?Category
+ {
+ return match (true) {
+ $checkoutable instanceof Asset => $checkoutable->model->category,
+ $checkoutable instanceof Accessory,
+ $checkoutable instanceof Consumable,
+ $checkoutable instanceof Component => $checkoutable->category,
+ $checkoutable instanceof LicenseSeat => $checkoutable->license->category,
+ };
+ }
}
diff --git a/app/Listeners/LogListener.php b/app/Listeners/LogListener.php
index d0da80614c8d..d7973e2103e7 100644
--- a/app/Listeners/LogListener.php
+++ b/app/Listeners/LogListener.php
@@ -17,7 +17,6 @@
use App\Events\ItemDeclined;
use App\Events\LicenseCheckedIn;
use App\Events\LicenseCheckedOut;
-use App\Events\NoteAdded;
use App\Models\Actionlog;
use App\Models\User;
use App\Models\LicenseSeat;
@@ -66,6 +65,7 @@ public function onCheckoutAccepted(CheckoutAccepted $event)
$logaction->filename = $event->acceptance->stored_eula_file;
$logaction->note = $event->acceptance->note;
$logaction->action_type = 'accepted';
+ $logaction->action_date = $event->acceptance->accepted_at;
// TODO: log the actual license seat that was checked out
if ($event->acceptance->checkoutable instanceof LicenseSeat) {
@@ -83,6 +83,7 @@ public function onCheckoutDeclined(CheckoutDeclined $event)
$logaction->accept_signature = $event->acceptance->signature_filename;
$logaction->note = $event->acceptance->note;
$logaction->action_type = 'declined';
+ $logaction->action_date = $event->acceptance->declined_at;
// TODO: log the actual license seat that was checked out
if ($event->acceptance->checkoutable instanceof LicenseSeat) {
@@ -129,23 +130,6 @@ public function onUserMerged(UserMerged $event)
}
-
- /**
- * Note is added to action log
- *
- */
- public function onNoteAdded(NoteAdded $event)
- {
- $logaction = new Actionlog();
- $logaction->item_id = $event->itemNoteAddedOn->id;
- $logaction->item_type = get_class($event->itemNoteAddedOn);
- $logaction->note = $event->note; //this is the received alphanumeric text from the box
- $logaction->created_by = $event->noteAddedBy->id;
- $logaction->action_type = 'note_added';
- $logaction->save();
- }
-
-
/**
* Register the listeners for the subscriber.
*
diff --git a/app/Livewire/CategoryEditForm.php b/app/Livewire/CategoryEditForm.php
index 28a06657c1da..98e505c8df0f 100644
--- a/app/Livewire/CategoryEditForm.php
+++ b/app/Livewire/CategoryEditForm.php
@@ -6,17 +6,19 @@
class CategoryEditForm extends Component
{
+ public bool $alertOnResponse;
+
public $defaultEulaText;
public $eulaText;
public $originalSendCheckInEmailValue;
- public $requireAcceptance;
+ public bool $requireAcceptance;
- public $sendCheckInEmail;
+ public bool $sendCheckInEmail;
- public $useDefaultEula;
+ public bool $useDefaultEula;
public function mount()
{
diff --git a/app/Livewire/Importer.php b/app/Livewire/Importer.php
index eea319a4001f..ebde00fd879d 100644
--- a/app/Livewire/Importer.php
+++ b/app/Livewire/Importer.php
@@ -35,10 +35,14 @@ class Importer extends Component
public $accessories_fields;
public $assets_fields;
public $users_fields;
+ public $assetmodels_fields;
+ public $suppliers_fields;
public $licenses_fields;
public $locations_fields;
public $consumables_fields;
public $components_fields;
+ public $manufacturers_fields;
+ public $categories_fields;
public $aliases_fields;
protected $rules = [
@@ -85,9 +89,6 @@ private function getColumns($type)
case 'component':
$results = $this->components_fields;
break;
- case 'consumable':
- $results = $this->consumables_fields;
- break;
case 'license':
$results = $this->licenses_fields;
break;
@@ -97,8 +98,14 @@ private function getColumns($type)
case 'location':
$results = $this->locations_fields;
break;
- case 'user':
- $results = $this->users_fields;
+ case 'supplier':
+ $results = $this->suppliers_fields;
+ break;
+ case 'manufacturer':
+ $results = $this->manufacturers_fields;
+ break;
+ case 'category':
+ $results = $this->categories_fields;
break;
default:
$results = [];
@@ -108,7 +115,7 @@ private function getColumns($type)
if ($type == "asset") {
// add Custom Fields after a horizontal line
$results['-'] = "———" . trans('admin/custom_fields/general.custom_fields') . "———’";
- foreach (CustomField::orderBy('name')->get() as $field) {
+ foreach (CustomField::where('type', \App\Models\Asset::class)->orderBy('name')->get() as $field) { // TODO - generalize?
$results[$field->db_column_name()] = $field->name;
}
}
@@ -128,7 +135,7 @@ public function updatingTypeOfImport($type)
//yes, this key *is* valid. Continue on to the next field.
continue;
} else {
- //no, this key is *INVALID* for this import type. Better set it to null
+ //no, this key is *INVALID* for this import type. Better set it to null,
// and we'll hope that the $aliases_fields or something else picks it up.
$this->field_map[$i] = null; // fingers crossed! But it's not likely, tbh.
} // TODO - strictly speaking, this isn't necessary here I don't think.
@@ -149,7 +156,7 @@ public function updatingTypeOfImport($type)
// in "Accessories"!)
if (array_key_exists($key, $this->columnOptions[$type])) {
$this->field_map[$i] = $key;
- continue 3; // bust out of both of these loops; as well as the surrounding one - e.g. move on to the next header
+ continue 3; // bust out of both of these loops and the surrounding one - e.g. move on to the next header
}
}
}
@@ -171,49 +178,53 @@ public function mount()
'license' => trans('general.licenses'),
'location' => trans('general.locations'),
'user' => trans('general.users'),
+ 'supplier' => trans('general.suppliers'),
+ 'manufacturer' => trans('general.manufacturers'),
+ 'category' => trans('general.categories'),
];
/**
* These are the item-type specific columns
*/
$this->accessories_fields = [
+ 'category' => trans('general.category'),
'company' => trans('general.company'),
- 'location' => trans('general.location'),
- 'quantity' => trans('general.qty'),
'item_name' => trans('general.item_name_var', ['item' => trans('general.accessory')]),
+ 'location' => trans('general.location'),
+ 'manufacturer' => trans('general.manufacturer'),
+ 'min_amt' => trans('mail.min_QTY'),
'model_number' => trans('general.model_no'),
'notes' => trans('general.notes'),
- 'category' => trans('general.category'),
- 'supplier' => trans('general.supplier'),
- 'min_amt' => trans('mail.min_QTY'),
+ 'order_number' => trans('general.order_number'),
'purchase_cost' => trans('general.purchase_cost'),
'purchase_date' => trans('general.purchase_date'),
- 'manufacturer' => trans('general.manufacturer'),
- 'order_number' => trans('general.order_number'),
+ 'quantity' => trans('general.qty'),
+ 'supplier' => trans('general.supplier'),
];
$this->assets_fields = [
- 'company' => trans('general.company'),
- 'location' => trans('general.location'),
- 'item_name' => trans('general.item_name_var', ['item' => trans('general.asset')]),
- 'asset_tag' => trans('general.asset_tag'),
+ 'id' => trans('general.id'),
+ 'asset_eol_date' => trans('admin/hardware/form.eol_date'),
'asset_model' => trans('general.model_name'),
+ 'asset_notes' => trans('general.item_notes', ['item' => trans('admin/hardware/general.asset')]),
+ 'asset_tag' => trans('general.asset_tag'),
'byod' => trans('general.byod'),
- 'model_number' => trans('general.model_no'),
- 'status' => trans('general.status'),
- 'warranty_months' => trans('admin/hardware/form.warranty'),
'category' => trans('general.category'),
+ 'company' => trans('general.company'),
+ 'image' => trans('general.importer.image_filename'),
+ 'item_name' => trans('general.item_name_var', ['item' => trans('general.asset')]),
+ 'location' => trans('general.location'),
+ 'manufacturer' => trans('general.manufacturer'),
+ 'model_notes' => trans('general.item_notes', ['item' => trans('admin/hardware/form.model')]),
+ 'model_number' => trans('general.model_no'),
+ 'order_number' => trans('general.order_number'),
+ 'purchase_cost' => trans('general.purchase_cost'),
+ 'purchase_date' => trans('general.purchase_date'),
'requestable' => trans('admin/hardware/general.requestable'),
'serial' => trans('general.serial_number'),
+ 'status' => trans('general.status'),
'supplier' => trans('general.supplier'),
- 'purchase_cost' => trans('general.purchase_cost'),
- 'purchase_date' => trans('general.purchase_date'),
- 'asset_notes' => trans('general.item_notes', ['item' => trans('admin/hardware/general.asset')]),
- 'model_notes' => trans('general.item_notes', ['item' => trans('admin/hardware/form.model')]),
- 'manufacturer' => trans('general.manufacturer'),
- 'order_number' => trans('general.order_number'),
- 'image' => trans('general.importer.image_filename'),
- 'asset_eol_date' => trans('admin/hardware/form.eol_date'),
+ 'warranty_months' => trans('admin/hardware/form.warranty'),
/**
* Checkout fields:
* Assets can be checked out to other assets, people, or locations, but we currently
@@ -238,123 +249,170 @@ public function mount()
];
$this->consumables_fields = [
+ 'category' => trans('general.category'),
+ 'checkout_class' => trans('general.importer.checkout_type'),
'company' => trans('general.company'),
- 'location' => trans('general.location'),
- 'quantity' => trans('general.qty'),
'item_name' => trans('general.item_name_var', ['item' => trans('general.consumable')]),
+ 'item_no' => trans('admin/consumables/general.item_no'),
+ 'location' => trans('general.location'),
+ 'manufacturer' => trans('general.manufacturer'),
+ 'min_amt' => trans('general.min_amt'),
'model_number' => trans('general.model_no'),
'notes' => trans('general.notes'),
- 'min_amt' => trans('mail.min_QTY'),
- 'category' => trans('general.category'),
+ 'order_number' => trans('general.order_number'),
'purchase_cost' => trans('general.purchase_cost'),
'purchase_date' => trans('general.purchase_date'),
- 'checkout_class' => trans('general.importer.checkout_type'),
+ 'quantity' => trans('general.qty'),
'supplier' => trans('general.supplier'),
- 'manufacturer' => trans('general.manufacturer'),
- 'order_number' => trans('general.order_number'),
- 'item_no' => trans('admin/consumables/general.item_no'),
];
$this->components_fields = [
+ 'category' => trans('general.category'),
'company' => trans('general.company'),
- 'location' => trans('general.location'),
- 'quantity' => trans('general.qty'),
'item_name' => trans('general.item_name_var', ['item' => trans('general.component')]),
+ 'location' => trans('general.location'),
+ 'manufacturer' => trans('general.manufacturer'),
+ 'min_amt' => trans('mail.min_QTY'),
'model_number' => trans('general.model_no'),
'notes' => trans('general.notes'),
- 'category' => trans('general.category'),
- 'supplier' => trans('general.supplier'),
- 'min_amt' => trans('mail.min_QTY'),
+ 'order_number' => trans('general.order_number'),
'purchase_cost' => trans('general.purchase_cost'),
'purchase_date' => trans('general.purchase_date'),
- 'manufacturer' => trans('general.manufacturer'),
- 'order_number' => trans('general.order_number'),
+ 'quantity' => trans('general.qty'),
'serial' => trans('general.serial_number'),
+ 'supplier' => trans('general.supplier'),
];
$this->licenses_fields = [
- 'company' => trans('general.company'),
- 'location' => trans('general.location'),
- 'item_name' => trans('general.item_name_var', ['item' => trans('general.license')]),
'asset_tag' => trans('general.importer.checked_out_to_tag'),
+ 'category' => trans('general.category'),
+ 'checkout_class' => trans('general.importer.checkout_type'),
+ 'company' => trans('general.company'),
+ 'email' => trans('general.importer.checked_out_to_email'),
'expiration_date' => trans('admin/licenses/form.expiration'),
'full_name' => trans('general.importer.checked_out_to_fullname'),
+ 'item_name' => trans('general.item_name_var', ['item' => trans('general.license')]),
'license_email' => trans('admin/licenses/form.to_email'),
'license_name' => trans('admin/licenses/form.to_name'),
- 'purchase_order' => trans('admin/licenses/form.purchase_order'),
- 'order_number' => trans('general.order_number'),
- 'reassignable' => trans('admin/licenses/form.reassignable'),
- 'seats' => trans('admin/licenses/form.seats'),
+ 'location' => trans('general.location'),
+ 'maintained' => trans('admin/licenses/form.maintained'),
+ 'manufacturer' => trans('general.manufacturer'),
+ 'min_amt' => trans('general.min_amt'),
'notes' => trans('general.notes'),
- 'category' => trans('general.category'),
- 'supplier' => trans('general.supplier'),
+ 'order_number' => trans('general.order_number'),
'purchase_cost' => trans('general.purchase_cost'),
'purchase_date' => trans('general.purchase_date'),
- 'maintained' => trans('admin/licenses/form.maintained'),
- 'checkout_class' => trans('general.importer.checkout_type'),
+ 'purchase_order' => trans('admin/licenses/form.purchase_order'),
+ 'reassignable' => trans('admin/licenses/form.reassignable'),
+ 'seats' => trans('admin/licenses/form.seats'),
'serial' => trans('general.license_serial'),
- 'email' => trans('general.importer.checked_out_to_email'),
+ 'supplier' => trans('general.supplier'),
+ 'termination_date' => trans('admin/licenses/form.termination_date'),
'username' => trans('general.importer.checked_out_to_username'),
- 'manufacturer' => trans('general.manufacturer'),
];
$this->users_fields = [
'id' => trans('general.id'),
+ 'activated' => trans('general.activated'),
+ 'address' => trans('general.address'),
+ 'avatar' => trans('general.image'),
+ 'city' => trans('general.city'),
'company' => trans('general.company'),
- 'location' => trans('general.location'),
+ 'country' => trans('general.country'),
'department' => trans('general.department'),
+ 'email' => trans('admin/users/table.email'),
+ 'employee_num' => trans('general.employee_number'),
+ 'end_date' => trans('general.end_date'),
'first_name' => trans('general.first_name'),
- 'last_name' => trans('general.last_name'),
- 'notes' => trans('general.notes'),
- 'username' => trans('admin/users/table.username'),
+ 'gravatar' => trans('general.importer.gravatar'),
'jobtitle' => trans('admin/users/table.title'),
- 'phone_number' => trans('admin/users/table.phone'),
+ 'last_name' => trans('general.last_name'),
+ 'location' => trans('general.location'),
'manager_first_name' => trans('general.importer.manager_first_name'),
'manager_last_name' => trans('general.importer.manager_last_name'),
- 'activated' => trans('general.activated'),
- 'address' => trans('general.address'),
- 'city' => trans('general.city'),
+ 'manager_employee_num' => trans('general.importer.manager_employee_num'),
+ 'manager_username' => trans('general.importer.manager_username'),
+ 'notes' => trans('general.notes'),
+ 'phone_number' => trans('admin/users/table.phone'),
+ 'mobile_number' => trans('admin/users/table.mobile'),
+ 'remote' => trans('admin/users/general.remote'),
+ 'start_date' => trans('general.start_date'),
'state' => trans('general.state'),
- 'country' => trans('general.country'),
- 'zip' => trans('general.zip'),
+ 'username' => trans('admin/users/table.username'),
+ 'display_name' => trans('admin/users/table.display_name'),
'vip' => trans('general.importer.vip'),
- 'remote' => trans('admin/users/general.remote'),
- 'email' => trans('admin/users/table.email'),
'website' => trans('general.website'),
- 'avatar' => trans('general.image'),
- 'gravatar' => trans('general.importer.gravatar'),
- 'start_date' => trans('general.start_date'),
- 'end_date' => trans('general.end_date'),
- 'employee_num' => trans('general.employee_number'),
+ 'zip' => trans('general.zip'),
];
$this->locations_fields = [
'id' => trans('general.id'),
- 'name' => trans('general.item_name_var', ['item' => trans('general.location')]),
+ 'name' => trans('general.name'),
'address' => trans('general.address'),
'address2' => trans('general.importer.address2'),
'city' => trans('general.city'),
- 'state' => trans('general.state'),
'country' => trans('general.country'),
- 'zip' => trans('general.zip'),
'currency' => trans('general.importer.currency'),
'ldap_ou' => trans('admin/locations/table.ldap_ou'),
- 'manager_username' => trans('general.importer.manager_username'),
'manager' => trans('general.importer.manager_full_name'),
+ 'manager_username' => trans('general.importer.manager_username'),
+ 'notes' => trans('general.notes'),
'parent_location' => trans('admin/locations/table.parent'),
+ 'state' => trans('general.state'),
+ 'zip' => trans('general.zip'),
+ ];
+
+ $this->suppliers_fields = [
+ 'id' => trans('general.id'),
+ 'name' => trans('general.name'),
+ 'address' => trans('general.address'),
+ 'address2' => trans('general.importer.address2'),
+ 'city' => trans('general.city'),
'notes' => trans('general.notes'),
+ 'state' => trans('general.state'),
+ 'zip' => trans('general.zip'),
+ 'phone' => trans('general.phone'),
+ 'fax' => trans('general.fax'),
+ 'url' => trans('general.url'),
+ 'contact' => trans('general.contact'),
+ 'email' => trans('general.email'),
];
+ $this->manufacturers_fields = [
+ 'id' => trans('general.id'),
+ 'name' => trans('general.name'),
+ 'notes' => trans('general.notes'),
+ 'support_phone' => trans('admin/manufacturers/table.support_phone'),
+ 'support_url' => trans('admin/manufacturers/table.support_url'),
+ 'support_email' => trans('admin/manufacturers/table.support_email'),
+ 'warranty_lookup_url' => trans('admin/manufacturers/table.warranty_lookup_url'),
+ 'url' => trans('general.url'),
+ ];
+
+ $this->categories_fields = [
+ 'id' => trans('general.id'),
+ 'name' => trans('general.name'),
+ 'notes' => trans('general.notes'),
+ 'category_type' => trans('admin/categories/general.import_category_type'),
+ 'eula_text' => trans('admin/categories/general.import_eula_text'),
+ 'use_default_eula' => trans('admin/categories/general.use_default_eula_column'),
+ 'require_acceptance' => trans('admin/categories/general.import_require_acceptance'),
+ 'checkin_email' => trans('admin/categories/general.import_checkin_email'),
+ ];
+
+
+
$this->assetmodels_fields = [
- 'item_name' => trans('general.item_name_var', ['item' => trans('general.asset_model')]),
'category' => trans('general.category'),
+ 'eol' => trans('general.eol'),
+ 'fieldset' => trans('admin/models/general.fieldset'),
+ 'item_name' => trans('general.item_name_var', ['item' => trans('general.asset_model')]),
'manufacturer' => trans('general.manufacturer'),
+ 'min_amt' => trans('mail.min_QTY'),
'model_number' => trans('general.model_no'),
'notes' => trans('general.item_notes', ['item' => trans('admin/hardware/form.model')]),
- 'min_amt' => trans('mail.min_QTY'),
- 'fieldset' => trans('admin/models/general.fieldset'),
- 'eol' => trans('general.eol'),
'requestable' => trans('admin/models/general.requestable'),
+ 'require_serial' => trans('admin/hardware/general.require_serial'),
];
@@ -369,6 +427,8 @@ public function mount()
'consumable name',
'component name',
'name',
+ 'supplier name',
+ 'location name',
],
'item_no' => [
'item number',
@@ -427,6 +487,13 @@ public function mount()
'username',
trans('general.importer.checked_out_to_username'),
],
+ 'display_name' =>
+ [
+ 'display name',
+ 'displayName',
+ 'display',
+ trans('admin/users/table.display_name'),
+ ],
'first_name' =>
[
'first name',
@@ -453,6 +520,13 @@ public function mount()
'telephone',
'tel.',
],
+ 'mobile_number' =>
+ [
+ 'mobile',
+ 'mobile number',
+ 'cell',
+ 'cellphone',
+ ],
'serial' =>
[
@@ -462,6 +536,10 @@ public function mount()
'product key',
'key',
],
+ 'require_serial' =>
+ [
+ 'serial required',
+ ],
'model_number' =>
[
'model',
diff --git a/app/Livewire/LocationScopeCheck.php b/app/Livewire/LocationScopeCheck.php
new file mode 100644
index 000000000000..24beb9413f69
--- /dev/null
+++ b/app/Livewire/LocationScopeCheck.php
@@ -0,0 +1,29 @@
+mismatched = Helper::test_locations_fmcs(false);
+ $this->is_tested = true;
+ }
+
+ public function mount() {
+ $this->setting = Setting::getSettings();
+ }
+
+ public function render()
+ {
+ return view('livewire.location-scope-check');
+ }
+}
diff --git a/app/Livewire/OauthClients.php b/app/Livewire/OauthClients.php
index 017e789060b1..e114c2278ed4 100644
--- a/app/Livewire/OauthClients.php
+++ b/app/Livewire/OauthClients.php
@@ -47,7 +47,7 @@ public function deleteClient(Client $clientId): void
{
// test for safety
// ->delete must be of type Client - thus the model binding
- if ($clientId->created_by == auth()->id()) {
+ if ($clientId->user_id == auth()->id()) {
app(ClientRepository::class)->delete($clientId);
} else {
Log::warning('User ' . auth()->id() . ' attempted to delete client ' . $clientId->id . ' which belongs to user ' . $clientId->created_by);
diff --git a/app/Livewire/SlackSettingsForm.php b/app/Livewire/SlackSettingsForm.php
index 7487f30961b5..535a83413fae 100644
--- a/app/Livewire/SlackSettingsForm.php
+++ b/app/Livewire/SlackSettingsForm.php
@@ -71,12 +71,12 @@ public function mount() {
$this->setting = Setting::getSettings();
$this->save_button = trans('general.save');
- $this->webhook_selected = $this->setting->webhook_selected;
- $this->webhook_name = $this->webhook_text[$this->setting->webhook_selected]["name"];
- $this->webhook_icon = $this->webhook_text[$this->setting->webhook_selected]["icon"];
- $this->webhook_placeholder = $this->webhook_text[$this->setting->webhook_selected]["placeholder"];
- $this->webhook_link = $this->webhook_text[$this->setting->webhook_selected]["link"];
- $this->webhook_test = $this->webhook_text[$this->setting->webhook_selected]["test"];
+ $this->webhook_selected = ($this->setting->webhook_selected !== '') ? $this->setting->webhook_selected : 'slack';
+ $this->webhook_name = $this->webhook_text[$this->setting->webhook_selected]["name"] ?? $this->webhook_text['slack']["name"];
+ $this->webhook_icon = $this->webhook_text[$this->setting->webhook_selected]["icon"] ?? $this->webhook_text['slack']["icon"];
+ $this->webhook_placeholder = $this->webhook_text[$this->setting->webhook_selected]["placeholder"] ?? $this->webhook_text['slack']["placeholder"];
+ $this->webhook_link = $this->webhook_text[$this->setting->webhook_selected]["link"] ?? $this->webhook_text['slack']["link"];
+ $this->webhook_test = $this->webhook_text[$this->setting->webhook_selected]["test"] ?? $this->webhook_text['slack']["test"];
$this->webhook_endpoint = $this->setting->webhook_endpoint;
$this->webhook_channel = $this->setting->webhook_channel;
$this->webhook_botname = $this->setting->webhook_botname;
@@ -90,7 +90,7 @@ public function mount() {
$this->isDisabled= '';
}
if($this->webhook_selected === 'microsoft' && $this->teams_webhook_deprecated) {
- session()->flash('warning', 'The selected Microsoft Teams webhook URL will be deprecated Jan 31st, 2025. Please use a workflow URL. Microsofts Documentation on creating a workflow can be found here.');
+ session()->flash('warning', trans('admin/settings/message.webhook.ms_teams_deprecation'));
}
}
public function updated($field) {
diff --git a/app/Mail/CheckinAssetMail.php b/app/Mail/CheckinAssetMail.php
index 355c2f9f1343..72f129dd6d8f 100644
--- a/app/Mail/CheckinAssetMail.php
+++ b/app/Mail/CheckinAssetMail.php
@@ -47,7 +47,7 @@ public function envelope(): Envelope
return new Envelope(
from: $from,
- subject: trans('mail.Asset_Checkin_Notification'),
+ subject: trans('mail.Asset_Checkin_Notification', ['tag' => $this->item->asset_tag]),
);
}
diff --git a/app/Mail/CheckinComponentMail.php b/app/Mail/CheckinComponentMail.php
new file mode 100644
index 000000000000..5b62a2c4b8d9
--- /dev/null
+++ b/app/Mail/CheckinComponentMail.php
@@ -0,0 +1,71 @@
+item = $component;
+ $this->target = $checkedOutTo;
+ $this->admin = $checkedInby;
+ $this->note = $note;
+ $this->settings = Setting::getSettings();
+ }
+
+ /**
+ * Get the message envelope.
+ */
+ public function envelope(): Envelope
+ {
+ $from = new Address(config('mail.from.address'), config('mail.from.name'));
+
+ return new Envelope(
+ from: $from,
+ subject: trans('mail.Confirm_component_checkin'),
+ );
+ }
+
+ /**
+ * Get the message content definition.
+ */
+ public function content(): Content
+ {
+ return new Content(
+ markdown: 'mail.markdown.checkin-component',
+ with: [
+ 'item' => $this->item,
+ 'admin' => $this->admin,
+ 'note' => $this->note,
+ 'target' => $this->target,
+ ]
+ );
+ }
+
+ /**
+ * Get the attachments for the message.
+ *
+ * @return array
+ */
+ public function attachments(): array
+ {
+ return [];
+ }
+}
diff --git a/app/Mail/CheckoutAcceptanceResponseMail.php b/app/Mail/CheckoutAcceptanceResponseMail.php
new file mode 100644
index 000000000000..fba6dd3de28a
--- /dev/null
+++ b/app/Mail/CheckoutAcceptanceResponseMail.php
@@ -0,0 +1,79 @@
+acceptance = $acceptance;
+ $this->recipient = $recipient;
+ $this->wasAccepted = $wasAccepted;
+ }
+
+ /**
+ * Get the message envelope.
+ */
+ public function envelope(): Envelope
+ {
+ $subject = $this->wasAccepted
+ ? trans('mail.initiated_accepted')
+ : trans('mail.initiated_declined');
+
+ return new Envelope(
+ subject: $subject,
+ );
+ }
+
+ /**
+ * Get the message content definition.
+ */
+ public function content(): Content
+ {
+ return new Content(
+ markdown: 'mail.markdown.checkout-acceptance-response',
+ with: [
+ 'assignedTo' => $this->acceptance->assignedTo,
+ 'introduction' => $this->introduction(),
+ 'item' => $this->acceptance->checkoutable,
+ 'note' => $this->acceptance->note,
+ 'recipient' => $this->recipient,
+ ]
+ );
+ }
+
+ /**
+ * Get the attachments for the message.
+ *
+ * @return array
+ */
+ public function attachments(): array
+ {
+ return [];
+ }
+
+ private function introduction(): string
+ {
+ return $this->wasAccepted
+ ? trans('mail.following_accepted')
+ : trans('mail.following_declined');
+ }
+}
diff --git a/app/Mail/CheckoutAccessoryMail.php b/app/Mail/CheckoutAccessoryMail.php
index 64c02e31ed1c..92ce84a2d6e5 100644
--- a/app/Mail/CheckoutAccessoryMail.php
+++ b/app/Mail/CheckoutAccessoryMail.php
@@ -3,6 +3,8 @@
namespace App\Mail;
use App\Models\Accessory;
+use App\Models\Asset;
+use App\Models\Location;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Bus\Queueable;
@@ -41,7 +43,7 @@ public function envelope(): Envelope
return new Envelope(
from: $from,
- subject: (trans('mail.Accessory_Checkout_Notification')),
+ subject: trans('mail.Accessory_Checkout_Notification'),
);
}
@@ -54,6 +56,17 @@ public function content(): Content
$eula = $this->item->getEula();
$req_accept = $this->item->requireAcceptance();
$accept_url = is_null($this->acceptance) ? null : route('account.accept.item', $this->acceptance);
+ $name = null;
+
+ if($this->target instanceof User){
+ $name = $this->target->display_name;
+ }
+ else if($this->target instanceof Asset){
+ $name = $this->target->assignedto?->display_name;
+ }
+ else if($this->target instanceof Location){
+ $name = $this->target->manager->name;
+ }
return new Content(
markdown: 'mail.markdown.checkout-accessory',
@@ -61,14 +74,35 @@ public function content(): Content
'item' => $this->item,
'admin' => $this->admin,
'note' => $this->note,
- 'target' => $this->target,
+ 'target' => $name,
'eula' => $eula,
'req_accept' => $req_accept,
'accept_url' => $accept_url,
'checkout_qty' => $this->checkout_qty,
+ 'introduction_line' => $this->introductionLine(),
],
);
}
+ private function introductionLine(): string
+ {
+ if ($this->target instanceof Location) {
+ return trans('mail.new_item_checked_location', ['location' => $this->target->name ]);
+ }
+ if ($this->requiresAcceptance()) {
+ return trans('mail.new_item_checked_with_acceptance');
+ }
+
+ if (!$this->requiresAcceptance()) {
+ return trans('mail.new_item_checked');
+ }
+
+ // we shouldn't get here but let's send a default message just in case
+ return trans('new_item_checked');
+ }
+ private function requiresAcceptance(): int|bool
+ {
+ return method_exists($this->item, 'requireAcceptance') ? $this->item->requireAcceptance() : 0;
+ }
/**
* Get the attachments for the message.
diff --git a/app/Mail/CheckoutAssetMail.php b/app/Mail/CheckoutAssetMail.php
index 680014dcd189..0dcd568bab8c 100644
--- a/app/Mail/CheckoutAssetMail.php
+++ b/app/Mail/CheckoutAssetMail.php
@@ -4,6 +4,7 @@
use App\Helpers\Helper;
use App\Models\Asset;
+use App\Models\Location;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Bus\Queueable;
@@ -20,22 +21,27 @@ class CheckoutAssetMail extends Mailable
{
use Queueable, SerializesModels;
+ private bool $firstTimeSending;
+
/**
* Create a new message instance.
+ * @throws \Exception
*/
- public function __construct(Asset $asset, $checkedOutTo, User $checkedOutBy, $acceptance, $note)
+ public function __construct(Asset $asset, $checkedOutTo, User $checkedOutBy, $acceptance, $note, bool $firstTimeSending = true)
{
$this->item = $asset;
$this->admin = $checkedOutBy;
$this->note = $note;
- $this->target = $checkedOutTo;
$this->acceptance = $acceptance;
$this->settings = Setting::getSettings();
+ $this->target = $checkedOutTo;
$this->last_checkout = '';
$this->expected_checkin = '';
+ $this->firstTimeSending = $firstTimeSending;
+
if ($this->item->last_checkout) {
$this->last_checkout = Helper::getFormattedDateObject($this->item->last_checkout, 'date',
false);
@@ -56,7 +62,7 @@ public function envelope(): Envelope
return new Envelope(
from: $from,
- subject: trans('mail.Asset_Checkout_Notification'),
+ subject: $this->getSubject(),
);
}
@@ -70,8 +76,19 @@ public function content(): Content
{
$this->item->load('assetstatus');
$eula = method_exists($this->item, 'getEula') ? $this->item->getEula() : '';
- $req_accept = method_exists($this->item, 'requireAcceptance') ? $this->item->requireAcceptance() : 0;
+ $req_accept = $this->requiresAcceptance();
$fields = [];
+ $name = null;
+
+ if($this->target instanceof User){
+ $name = $this->target->display_name;
+ }
+ else if($this->target instanceof Asset){
+ $name = $this->target->assignedto?->display_name;
+ }
+ else if($this->target instanceof Location){
+ $name = $this->target->manager->name;
+ }
// Check if the item has custom fields associated with it
if (($this->item->model) && ($this->item->model->fieldset)) {
@@ -87,13 +104,14 @@ public function content(): Content
'admin' => $this->admin,
'status' => $this->item->assetstatus?->name,
'note' => $this->note,
- 'target' => $this->target,
+ 'target' => $name,
'fields' => $fields,
'eula' => $eula,
'req_accept' => $req_accept,
'accept_url' => $accept_url,
'last_checkout' => $this->last_checkout,
'expected_checkin' => $this->expected_checkin,
+ 'introduction_line' => $this->introductionLine(),
],
);
}
@@ -107,4 +125,39 @@ public function attachments(): array
{
return [];
}
+
+ private function getSubject(): string
+ {
+ if ($this->firstTimeSending) {
+ return trans('mail.Asset_Checkout_Notification', ['tag' => $this->item->asset_tag]);
+ }
+
+ return trans('mail.unaccepted_asset_reminder');
+ }
+
+ private function introductionLine(): string
+ {
+ if ($this->firstTimeSending && $this->target instanceof Location) {
+ return trans('mail.new_item_checked_location', ['location' => $this->target->name ]);
+ }
+ if ($this->firstTimeSending && $this->requiresAcceptance()) {
+ return trans('mail.new_item_checked_with_acceptance');
+ }
+
+ if ($this->firstTimeSending && !$this->requiresAcceptance()) {
+ return trans('mail.new_item_checked');
+ }
+
+ if (!$this->firstTimeSending && $this->requiresAcceptance()) {
+ return trans('mail.recent_item_checked');
+ }
+
+ // we shouldn't get here but let's send a default message just in case
+ return trans('new_item_checked');
+ }
+
+ private function requiresAcceptance(): int|bool
+ {
+ return method_exists($this->item, 'requireAcceptance') ? $this->item->requireAcceptance() : 0;
+ }
}
diff --git a/app/Mail/CheckoutComponentMail.php b/app/Mail/CheckoutComponentMail.php
new file mode 100644
index 000000000000..e914d1419694
--- /dev/null
+++ b/app/Mail/CheckoutComponentMail.php
@@ -0,0 +1,82 @@
+item = $component;
+ $this->admin = $checkedOutBy;
+ $this->note = $note;
+ $this->target = $checkedOutTo;
+ $this->acceptance = $acceptance;
+ $this->qty = $component->assets->first()?->pivot?->assigned_qty;
+
+ $this->settings = Setting::getSettings();
+ }
+
+ /**
+ * Get the message envelope.
+ */
+ public function envelope(): Envelope
+ {
+ $from = new Address(config('mail.from.address'), config('mail.from.name'));
+
+ return new Envelope(
+ from: $from,
+ subject: trans('mail.Confirm_component_delivery'),
+ );
+ }
+
+ /**
+ * Get the message content definition.
+ */
+ public function content(): Content
+ {
+
+ $eula = $this->item->getEula();
+ $req_accept = $this->item->requireAcceptance();
+
+ $accept_url = is_null($this->acceptance) ? null : route('account.accept.item', $this->acceptance);
+
+ return new Content(
+ markdown: 'mail.markdown.checkout-component',
+ with: [
+ 'item' => $this->item,
+ 'admin' => $this->admin,
+ 'note' => $this->note,
+ 'target' => $this->target,
+ 'eula' => $eula,
+ 'req_accept' => $req_accept,
+ 'accept_url' => $accept_url,
+ 'qty' => $this->qty,
+ ]
+ );
+ }
+
+ /**
+ * Get the attachments for the message.
+ *
+ * @return array
+ */
+ public function attachments(): array
+ {
+ return [];
+ }
+}
diff --git a/app/Mail/CheckoutLicenseMail.php b/app/Mail/CheckoutLicenseMail.php
index 9462c6c332c4..f3688bae5a61 100644
--- a/app/Mail/CheckoutLicenseMail.php
+++ b/app/Mail/CheckoutLicenseMail.php
@@ -2,6 +2,7 @@
namespace App\Mail;
+use App\Models\Asset;
use App\Models\LicenseSeat;
use App\Models\Setting;
use App\Models\User;
@@ -25,9 +26,16 @@ public function __construct(LicenseSeat $licenseSeat, $checkedOutTo, User $check
$this->item = $licenseSeat;
$this->admin = $checkedOutBy;
$this->note = $note;
- $this->target = $checkedOutTo;
$this->acceptance = $acceptance;
$this->settings = Setting::getSettings();
+ $this->target = $checkedOutTo;
+
+ if($this->target instanceof User){
+ $this->target = $this->target->display_name;
+ }
+ elseif($this->target instanceof Asset){
+ $this->target = $this->target->display_name;
+ }
}
/**
diff --git a/app/Mail/SendUpcomingAuditMail.php b/app/Mail/SendUpcomingAuditMail.php
new file mode 100644
index 000000000000..03faa081ebaf
--- /dev/null
+++ b/app/Mail/SendUpcomingAuditMail.php
@@ -0,0 +1,65 @@
+assets = $params;
+ $this->threshold = $threshold;
+ }
+
+ /**
+ * Get the message envelope.
+ */
+ public function envelope(): Envelope
+ {
+ $from = new Address(config('mail.from.address'), config('mail.from.name'));
+
+ return new Envelope(
+ from: $from,
+ subject: trans_choice('mail.upcoming-audits', $this->assets->count(), ['count' => $this->assets->count(), 'threshold' => $this->threshold]),
+ );
+ }
+
+ /**
+ * Get the message content definition.
+ */
+ public function content(): Content
+ {
+
+
+ return new Content(
+
+ markdown: 'notifications.markdown.upcoming-audits',
+ with: [
+ 'assets' => $this->assets,
+ 'threshold' => $this->threshold,
+ ],
+ );
+ }
+
+ /**
+ * Get the attachments for the message.
+ *
+ * @return array
+ */
+ public function attachments(): array
+ {
+ return [];
+ }
+}
diff --git a/app/Mail/UnacceptedAssetReminderMail.php b/app/Mail/UnacceptedAssetReminderMail.php
index 1436bbc84e08..0e4473aaadf2 100644
--- a/app/Mail/UnacceptedAssetReminderMail.php
+++ b/app/Mail/UnacceptedAssetReminderMail.php
@@ -19,9 +19,10 @@ class UnacceptedAssetReminderMail extends Mailable
*/
public function __construct($checkout_info, $count)
{
+
$this->count = $count;
- $this->target = $checkout_info['acceptance']?->assignedTo;
- $this->acceptance = $checkout_info['acceptance'];
+ $this->target = $checkout_info?->assignedTo;
+ $this->acceptance = $checkout_info;
}
/**
diff --git a/app/Models/Accessory.php b/app/Models/Accessory.php
index fc1bb36ab40d..33a1c6a5700d 100755
--- a/app/Models/Accessory.php
+++ b/app/Models/Accessory.php
@@ -4,6 +4,8 @@
use App\Helpers\Helper;
use App\Models\Traits\Acceptable;
+use App\Models\Traits\CompanyableTrait;
+use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -14,7 +16,7 @@
/**
* Model for Accessories.
*
- * @version v1.0
+ * @version v1.0
*/
class Accessory extends SnipeModel
{
@@ -22,6 +24,7 @@ class Accessory extends SnipeModel
protected $presenter = \App\Presenters\AccessoryPresenter::class;
use CompanyableTrait;
+ use HasUploads;
use Loggable, Presentable;
use SoftDeletes;
@@ -54,26 +57,27 @@ class Accessory extends SnipeModel
];
/**
- * Accessory validation rules
- */
+ * Accessory validation rules
+ */
public $rules = [
'name' => 'required|min:3|max:255',
'qty' => 'required|integer|min:1',
'category_id' => 'required|integer|exists:categories,id',
'company_id' => 'integer|nullable',
+ 'location_id' => 'exists:locations,id|nullable|fmcs_location',
'min_amt' => 'integer|min:0|nullable',
- 'purchase_cost' => 'numeric|nullable|gte:0|max:9999999999999',
+ 'purchase_cost' => 'numeric|nullable|gte:0|max:99999999999999999.99',
'purchase_date' => 'date_format:Y-m-d|nullable',
];
/**
- * Whether the model should inject it's identifier to the unique
- * validation rules before attempting validation. If this property
- * is not set in the model it will default to true.
- *
+ * Whether the model should inject it's identifier to the unique
+ * validation rules before attempting validation. If this property
+ * is not set in the model it will default to true.
+ *
* @var bool
- */
+ */
protected $injectUniqueIdentifier = true;
use ValidatingTrait;
@@ -101,29 +105,11 @@ class Accessory extends SnipeModel
];
-
- /**
- * Establishes the accessories -> action logs -> uploads relationship
- *
- * @author A. Gianotto
- * @since [v6.1.13]
- * @return \Illuminate\Database\Eloquent\Relations\Relation
- */
- public function uploads()
- {
- return $this->hasMany(\App\Models\Actionlog::class, 'item_id')
- ->where('item_type', '=', self::class)
- ->where('action_type', '=', 'uploaded')
- ->whereNotNull('filename')
- ->orderBy('created_at', 'desc');
- }
-
-
/**
* Establishes the accessory -> supplier relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function supplier()
@@ -136,7 +122,7 @@ public function supplier()
* Sets the requestable attribute on the accessory
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return void
*/
public function setRequestableAttribute($value)
@@ -151,7 +137,7 @@ public function setRequestableAttribute($value)
* Establishes the accessory -> company relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function company()
@@ -163,7 +149,7 @@ public function company()
* Establishes the accessory -> location relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function location()
@@ -175,7 +161,7 @@ public function location()
* Establishes the accessory -> category relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function category()
@@ -187,7 +173,7 @@ public function category()
* Returns the action logs associated with the accessory
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assetlog()
@@ -216,8 +202,8 @@ public function assetlog()
*
* It's super-mega-assy, but it's the best I could do for now.
*
- * @author A. Gianotto
- * @since v5.0.0
+ * @author A. Gianotto
+ * @since v5.0.0
*
* @see \App\Http\Controllers\Api\AccessoriesController\checkedout()
*/
@@ -234,7 +220,7 @@ public function lastCheckout()
* presenter or service provider
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return string
*/
public function getImageUrl()
@@ -250,7 +236,7 @@ public function getImageUrl()
* Establishes the accessory -> users relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function checkouts()
@@ -263,7 +249,7 @@ public function checkouts()
* Establishes the accessory -> admin user relationship
*
* @author A. Gianotto
- * @since [v7.0.13]
+ * @since [v7.0.13]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
@@ -275,7 +261,7 @@ public function adminuser()
* Checks whether or not the accessory has users
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return int
*/
public function hasUsers()
@@ -289,7 +275,7 @@ public function hasUsers()
* Establishes the accessory -> manufacturer relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function manufacturer()
@@ -302,12 +288,12 @@ public function manufacturer()
* accessory based on the category it belongs to.
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return bool
*/
public function checkin_email()
{
- return $this->category->checkin_email;
+ return $this->category?->checkin_email;
}
/**
@@ -315,7 +301,7 @@ public function checkin_email()
* accept it via email.
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return bool
*/
public function requireAcceptance()
@@ -328,7 +314,7 @@ public function requireAcceptance()
* checks for a settings level EULA
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return string
*/
public function getEula()
@@ -348,7 +334,7 @@ public function getEula()
* Check how many items within an accessory are checked out
*
* @author [A. Gianotto] []
- * @since [v5.0]
+ * @since [v5.0]
* @return int
*/
public function numCheckedOut()
@@ -365,7 +351,7 @@ public function numCheckedOut()
* bad things happen.
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return int
*/
public function numRemaining()
@@ -380,8 +366,8 @@ public function numRemaining()
/**
* Run after the checkout acceptance was declined by the user
*
- * @param User $acceptedBy
- * @param string $signature
+ * @param User $acceptedBy
+ * @param string $signature
*/
public function declinedCheckout(User $declinedBy, $signature)
{
@@ -407,8 +393,8 @@ public function declinedCheckout(User $declinedBy, $signature)
* This simply checks that there is a value for quantity, and if there isn't, set it to 0.
*
* @author A. Gianotto
- * @since v6.3.4
- * @param $value
+ * @since v6.3.4
+ * @param $value
* @return void
*/
public function setQtyAttribute($value)
@@ -425,7 +411,6 @@ public function setQtyAttribute($value)
/**
* Query builder scope to order on created_by name
- *
*/
public function scopeOrderByCreatedByName($query, $order)
{
@@ -433,68 +418,68 @@ public function scopeOrderByCreatedByName($query, $order)
}
/**
- * Query builder scope to order on company
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope to order on company
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeOrderCompany($query, $order)
{
return $query->leftJoin('companies', 'accessories.company_id', '=', 'companies.id')
- ->orderBy('companies.name', $order);
+ ->orderBy('companies.name', $order);
}
/**
- * Query builder scope to order on category
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope to order on category
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeOrderCategory($query, $order)
{
return $query->leftJoin('categories', 'accessories.category_id', '=', 'categories.id')
- ->orderBy('categories.name', $order);
+ ->orderBy('categories.name', $order);
}
/**
- * Query builder scope to order on location
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope to order on location
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeOrderLocation($query, $order)
{
return $query->leftJoin('locations', 'accessories.location_id', '=', 'locations.id')
- ->orderBy('locations.name', $order);
+ ->orderBy('locations.name', $order);
}
/**
- * Query builder scope to order on manufacturer
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope to order on manufacturer
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeOrderManufacturer($query, $order)
{
return $query->leftJoin('manufacturers', 'accessories.manufacturer_id', '=', 'manufacturers.id')->orderBy('manufacturers.name', $order);
}
/**
- * Query builder scope to order on supplier
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope to order on supplier
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeOrderSupplier($query, $order)
{
return $query->leftJoin('suppliers', 'accessories.supplier_id', '=', 'suppliers.id')->orderBy('suppliers.name', $order);
diff --git a/app/Models/AccessoryCheckout.php b/app/Models/AccessoryCheckout.php
index 2dc51160ae5f..9f49354389fb 100755
--- a/app/Models/AccessoryCheckout.php
+++ b/app/Models/AccessoryCheckout.php
@@ -16,7 +16,7 @@
/**
* Model for Accessories.
*
- * @version v1.0
+ * @version v1.0
*/
class AccessoryCheckout extends Model
{
@@ -36,7 +36,7 @@ class AccessoryCheckout extends Model
* Establishes the accessory checkout -> accessory relationship
*
* @author [A. Kroeger]
- * @since [v7.0.9]
+ * @since [v7.0.9]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function accessory()
@@ -52,19 +52,19 @@ public function accessories()
* Establishes the accessory checkout -> user relationship
*
* @author [A. Kroeger]
- * @since [v7.0.9]
+ * @since [v7.0.9]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
{
- return $this->hasOne(\App\Models\User::class, 'created_by');
+ return $this->hasOne(\App\Models\User::class, 'id', 'created_by');
}
/**
* Get the target this asset is checked out to
*
* @author [A. Kroeger]
- * @since [v7.0]
+ * @since [v7.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assignedTo()
@@ -76,7 +76,7 @@ public function assignedTo()
* Gets the lowercased name of the type of target the asset is assigned to
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return string
*/
public function assignedType()
@@ -91,7 +91,7 @@ public function assignedType()
* this method is an easy way of seeing if we are checked out to a user.
*
* @author [A. Kroeger]
- * @since [v7.0]
+ * @since [v7.0]
*/
public function checkedOutToUser(): bool
{
@@ -118,7 +118,7 @@ public function scopeLocationAssigned(Builder $query): void
$query->where('assigned_type', '=', Location::class);
}
- public function scopeAssetAssigned(Builder $query): void
+ public function scopeAssetsAssigned(Builder $query): void
{
$query->where('assigned_type', '=', Asset::class);
}
@@ -127,50 +127,64 @@ public function scopeAssetAssigned(Builder $query): void
* Run additional, advanced searches.
*
* @param \Illuminate\Database\Eloquent\Builder $query
- * @param array $terms The search terms
+ * @param array $terms The search terms
* @return \Illuminate\Database\Eloquent\Builder
*/
public function advancedTextSearch(Builder $query, array $terms)
{
- $userQuery = User::where(function ($query) use ($terms) {
- foreach ($terms as $term) {
- $search_str = '%' . $term . '%';
- $query->where('first_name', 'like', $search_str)
- ->orWhere('last_name', 'like', $search_str)
- ->orWhere('note', 'like', $search_str);
+ $userQuery = User::where(
+ function ($query) use ($terms) {
+ foreach ($terms as $term) {
+ $search_str = '%' . $term . '%';
+ $query->where('first_name', 'like', $search_str)
+ ->orWhere('last_name', 'like', $search_str)
+ ->orWhere('note', 'like', $search_str);
+ }
}
- })->select('id');
-
- $locationQuery = Location::where(function ($query) use ($terms) {
- foreach ($terms as $term) {
- $search_str = '%' . $term . '%';
- $query->where('name', 'like', $search_str);
+ )->select('id');
+
+ $locationQuery = Location::where(
+ function ($query) use ($terms) {
+ foreach ($terms as $term) {
+ $search_str = '%' . $term . '%';
+ $query->where('name', 'like', $search_str);
+ }
+ }
+ )->select('id');
+
+ $assetQuery = Asset::where(
+ function ($query) use ($terms) {
+ foreach ($terms as $term) {
+ $search_str = '%' . $term . '%';
+ $query->where('name', 'like', $search_str);
+ }
}
- })->select('id');
+ )->select('id');
- $assetQuery = Asset::where(function ($query) use ($terms) {
- foreach ($terms as $term) {
- $search_str = '%' . $term . '%';
- $query->where('name', 'like', $search_str);
+ $query->where(
+ function ($query) use ($userQuery) {
+ $query->where('assigned_type', User::class)
+ ->whereIn('assigned_to', $userQuery);
+ }
+ )->orWhere(
+ function ($query) use ($locationQuery) {
+ $query->where('assigned_type', Location::class)
+ ->whereIn('assigned_to', $locationQuery);
+ }
+ )->orWhere(
+ function ($query) use ($assetQuery) {
+ $query->where('assigned_type', Asset::class)
+ ->whereIn('assigned_to', $assetQuery);
}
- })->select('id');
-
- $query->where(function ($query) use ($userQuery) {
- $query->where('assigned_type', User::class)
- ->whereIn('assigned_to', $userQuery);
- })->orWhere(function($query) use ($locationQuery) {
- $query->where('assigned_type', Location::class)
- ->whereIn('assigned_to', $locationQuery);
- })->orWhere(function($query) use ($assetQuery) {
- $query->where('assigned_type', Asset::class)
- ->whereIn('assigned_to', $assetQuery);
- })->orWhere(function($query) use ($terms) {
- foreach ($terms as $term) {
- $search_str = '%' . $term . '%';
- $query->where('note', 'like', $search_str);
+ )->orWhere(
+ function ($query) use ($terms) {
+ foreach ($terms as $term) {
+ $search_str = '%' . $term . '%';
+ $query->where('note', 'like', $search_str);
+ }
}
- });
+ );
return $query;
}
diff --git a/app/Models/Actionlog.php b/app/Models/Actionlog.php
index 31d9b74bbf0c..59e7b8244aa4 100755
--- a/app/Models/Actionlog.php
+++ b/app/Models/Actionlog.php
@@ -2,17 +2,19 @@
namespace App\Models;
+use App\Models\Traits\CompanyableTrait;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Support\Str;
/**
* Model for the Actionlog (the table that keeps a historical log of
* checkouts, checkins, and updates).
*
- * @version v1.0
+ * @version v1.0
*/
class Actionlog extends SnipeModel
{
@@ -57,7 +59,9 @@ class Actionlog extends SnipeModel
'user_agent',
'item_type',
'target_type',
- 'action_source'
+ 'action_source',
+ 'created_at',
+ 'action_date',
];
/**
@@ -67,33 +71,60 @@ class Actionlog extends SnipeModel
*/
protected $searchableRelations = [
'company' => ['name'],
- 'adminuser' => ['first_name','last_name','username', 'email'],
- 'user' => ['first_name','last_name','username', 'email'],
- 'assets' => ['asset_tag','name', 'serial'],
+ 'adminuser' => ['first_name','last_name','username', 'email', 'employee_num'],
+ 'user' => ['first_name','last_name','username', 'email', 'employee_num'],
+ 'assets' => ['asset_tag','name', 'serial', 'order_number', 'notes', 'purchase_date'],
+ 'assets.model' => ['name', 'model_number', 'eol', 'notes'],
+ 'assets.model.category' => ['name', 'notes'],
+ 'assets.location' => ['name'],
+ 'assets.defaultLoc' => ['name'],
+ 'assets.model.manufacturer' => ['name', 'notes'],
+ 'licenses' => ['name', 'serial', 'notes', 'order_number', 'license_email', 'license_name', 'purchase_order', 'purchase_date'],
+ 'licenses.category' => ['name', 'notes'],
+ 'licenses.supplier' => ['name'],
+ 'consumables' => ['name', 'notes', 'order_number', 'model_number', 'item_no', 'purchase_date'],
+ 'consumables.category' => ['name', 'notes'],
+ 'consumables.location' => ['name', 'notes'],
+ 'consumables.supplier' => ['name', 'notes'],
+ 'components' => ['name', 'notes', 'purchase_date'],
+ 'components.category' => ['name', 'notes'],
+ 'components.location' => ['name', 'notes'],
+ 'components.supplier' => ['name', 'notes'],
+ 'accessories' => ['name', 'purchase_date'],
+ 'accessories.category' => ['name'],
+ 'accessories.location' => ['name', 'notes'],
+ 'accessories.supplier' => ['name', 'notes'],
];
/**
* Override from Builder to automatically add the company
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public static function boot()
{
parent::boot();
- static::creating(function (self $actionlog) {
- // If the admin is a superadmin, let's see if the target instead has a company.
- if (auth()->user() && auth()->user()->isSuperUser()) {
- if ($actionlog->target) {
- $actionlog->company_id = $actionlog->target->company_id;
- } elseif ($actionlog->item) {
- $actionlog->company_id = $actionlog->item->company_id;
+ static::creating(
+ function (self $actionlog) {
+ // If the admin is a superadmin, let's see if the target instead has a company.
+ if (auth()->user() && auth()->user()->isSuperUser()) {
+ if ($actionlog->target) {
+ $actionlog->company_id = $actionlog->target->company_id;
+ } elseif ($actionlog->item) {
+ $actionlog->company_id = $actionlog->item->company_id;
+ }
+ } elseif (auth()->user() && auth()->user()->company) {
+ $actionlog->company_id = auth()->user()->company_id;
+ }
+
+ if ($actionlog->action_date == '') {
+ $actionlog->action_date = Carbon::now();
}
- } elseif (auth()->user() && auth()->user()->company) {
- $actionlog->company_id = auth()->user()->company_id;
+
}
- });
+ );
}
@@ -101,7 +132,7 @@ public static function boot()
* Establishes the actionlog -> item relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function item()
@@ -113,7 +144,7 @@ public function item()
* Establishes the actionlog -> company relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function company()
@@ -126,7 +157,7 @@ public function company()
* Establishes the actionlog -> asset relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assets()
@@ -134,11 +165,59 @@ public function assets()
return $this->hasMany(\App\Models\Asset::class, 'id', 'item_id');
}
+ /**
+ * Establishes the actionlog -> license relationship
+ *
+ * @author [A. Gianotto] []
+ * @since [v3.0]
+ * @return \Illuminate\Database\Eloquent\Relations\Relation
+ */
+ public function licenses()
+ {
+ return $this->hasMany(\App\Models\License::class, 'id', 'item_id');
+ }
+
+ /**
+ * Establishes the actionlog -> consumable relationship
+ *
+ * @author [A. Gianotto] []
+ * @since [v3.0]
+ * @return \Illuminate\Database\Eloquent\Relations\Relation
+ */
+ public function consumables()
+ {
+ return $this->hasMany(\App\Models\Consumable::class, 'id', 'item_id');
+ }
+
+ /**
+ * Establishes the actionlog -> consumable relationship
+ *
+ * @author [A. Gianotto] []
+ * @since [v3.0]
+ * @return \Illuminate\Database\Eloquent\Relations\Relation
+ */
+ public function accessories()
+ {
+ return $this->hasMany(\App\Models\Accessory::class, 'id', 'item_id');
+ }
+
+ /**
+ * Establishes the actionlog -> components relationship
+ *
+ * @author [A. Gianotto] []
+ * @since [v3.0]
+ * @return \Illuminate\Database\Eloquent\Relations\Relation
+ */
+ public function components()
+ {
+ return $this->hasMany(\App\Models\Component::class, 'id', 'item_id');
+ }
+
/**
* Establishes the actionlog -> item type relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function itemType()
@@ -154,7 +233,7 @@ public function itemType()
* Establishes the actionlog -> target type relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function targetType()
@@ -167,25 +246,12 @@ public function targetType()
}
- /**
- * Establishes the actionlog -> uploads relationship
- *
- * @author [A. Gianotto] []
- * @since [v3.0]
- * @return \Illuminate\Database\Eloquent\Relations\Relation
- */
- public function uploads()
- {
- return $this->morphTo('item')
- ->where('action_type', '=', 'uploaded')
- ->withTrashed();
- }
/**
* Establishes the actionlog -> userlog relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function userlog()
@@ -197,20 +263,20 @@ public function userlog()
* Establishes the actionlog -> admin user relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
{
return $this->belongsTo(User::class, 'created_by')
- ->withTrashed();
+ ->withTrashed();
}
/**
* Establishes the actionlog -> user relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function user()
@@ -223,7 +289,7 @@ public function user()
* Establishes the actionlog -> target relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function target()
@@ -235,7 +301,7 @@ public function target()
* Establishes the actionlog -> location relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function location()
@@ -248,7 +314,7 @@ public function location()
* Check if the file exists, and if it does, force a download
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return string | false
*/
public function get_src($type = 'assets', $fieldname = 'filename')
@@ -266,7 +332,7 @@ public function get_src($type = 'assets', $fieldname = 'filename')
* Saves the log record with the action type
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return bool
*/
public function logaction($actiontype)
@@ -287,20 +353,26 @@ public function logaction($actiontype)
* Calculate the number of days until the next audit
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return int
*/
public function daysUntilNextAudit($monthInterval = 12, $asset = null)
{
$now = Carbon::now();
- $last_audit_date = $this->created_at;
- $next_audit = $last_audit_date->addMonth($monthInterval);
- $next_audit_days = $now->diffInDays($next_audit);
+ $last_audit_date = $this->created_at; // this is the action log's created at, not the asset itself
+ $next_audit = $last_audit_date->addMonth($monthInterval); // this actually *modifies* the $last_audit_date
+ $next_audit_days = (int) round($now->diffInDays($next_audit, true));
+ $override_default_next = $next_audit;
// Override the default setting for interval if the asset has its own next audit date
if (($asset) && ($asset->next_audit_date)) {
- $override_default_next = \Carbon::parse($asset->next_audit_date);
- $next_audit_days = $override_default_next->diffInDays($now);
+ $override_default_next = Carbon::parse($asset->next_audit_date);
+ $next_audit_days = (int) round($override_default_next->diffInDays($now, true));
+ }
+
+ // Show as negative number if the next audit date is before the audit date we're looking at
+ if ($this->created_at > $override_default_next) {
+ $next_audit_days = '-'.$next_audit_days;
}
return $next_audit_days;
@@ -310,7 +382,7 @@ public function daysUntilNextAudit($monthInterval = 12, $asset = null)
* Calculate the date of the next audit
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return \Datetime
*/
public function calcNextAuditDate($monthInterval = 12, $asset = null)
@@ -327,24 +399,24 @@ public function calcNextAuditDate($monthInterval = 12, $asset = null)
/**
* Gets action logs in chronological order, excluding uploads
*
- * @author Vincent Sposato
- * @since v1.0
+ * @author Vincent Sposato
+ * @since v1.0
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getListingOfActionLogsChronologicalOrder()
{
return $this->all()
- ->where('action_type', '!=', 'uploaded')
- ->orderBy('item_id', 'asc')
- ->orderBy('created_at', 'asc')
- ->get();
+ ->where('action_type', '!=', 'uploaded')
+ ->orderBy('item_id', 'asc')
+ ->orderBy('created_at', 'asc')
+ ->get();
}
/**
* Determines what the type of request is so we can log it to the action_log
*
* @author A. Gianotto
- * @since v6.3.0
+ * @since v6.3.0
* @return string
*/
public function determineActionSource(): string
@@ -356,11 +428,12 @@ public function determineActionSource(): string
// This is an API call
if (((request()->header('content-type') && (request()->header('accept'))=='application/json'))
- && (starts_with(request()->header('authorization'), 'Bearer '))) {
+ && (starts_with(request()->header('authorization'), 'Bearer '))
+ ) {
return 'api';
}
- // This is probably NOT an API call
+ // This is probably NOT an API call
if (request()->filled('_token')) {
return 'gui';
}
@@ -370,6 +443,81 @@ public function determineActionSource(): string
}
+
+ /**
+ * @author Godfrey Martinez
+ * @since [v8.0.4]
+ * @return \App\Models\Actionlog
+ */
+ public function logUploadDelete($object, $filename)
+ {
+ $log = new Actionlog;
+ $log->item_type = $object instanceof SnipeModel ? get_class($object) : $object;
+ $log->item_id = $object->id;
+ $log->created_by = auth()->id();
+ $log->target_id = null;
+ $log->filename = $filename;
+ $log->created_at = date('Y-m-d H:i:s');
+ $log->logaction('upload deleted');
+
+ return $log;
+ }
+
+ public function uploads_file_url()
+ {
+
+
+
+ if (($this->action_type == 'accepted') || ($this->action_type == 'declined')) {
+ return route('log.storedeula.download', ['filename' => $this->filename]);
+ }
+
+ $object = Str::snake(str_plural(str_replace("App\Models\\", '', $this->item_type)));
+
+ if ($object == 'asset_models') {
+ $object = 'models';
+ }
+
+ return route('ui.files.show', [
+ 'object_type' => $object,
+ 'id' => $this->item_id,
+ 'file_id' => $this->id,
+ ]);
+
+ }
+
+ public function uploads_file_path()
+ {
+
+ if (($this->action_type == 'accepted') || ($this->action_type == 'declined')) {
+ return 'private_uploads/eula-pdfs/'.$this->filename;
+ }
+
+ switch ($this->item_type) {
+ case Accessory::class:
+ return 'private_uploads/accessories/'.$this->filename;
+ case Asset::class:
+ return 'private_uploads/assets/'.$this->filename;
+ case AssetModel::class:
+ return 'private_uploads/models/'.$this->filename;
+ case Consumable::class:
+ return 'private_uploads/consumables/'.$this->filename;
+ case Component::class:
+ return 'private_uploads/components/'.$this->filename;
+ case License::class:
+ return 'private_uploads/licenses/'.$this->filename;
+ case Location::class:
+ return 'private_uploads/locations/'.$this->filename;
+ case Maintenance::class:
+ return 'private_uploads/maintenances/'.$this->filename;
+ case User::class:
+ return 'private_uploads/users/'.$this->filename;
+ default:
+ return null;
+ }
+ }
+
+
// Manually sets $this->source for determineActionSource()
public function setActionSource($source = null): void
{
@@ -380,4 +528,4 @@ public function scopeOrderByCreatedBy($query, $order)
{
return $query->leftJoin('users as admin_sort', 'action_logs.created_by', '=', 'admin_sort.id')->select('action_logs.*')->orderBy('admin_sort.first_name', $order)->orderBy('admin_sort.last_name', $order);
}
-}
+}
\ No newline at end of file
diff --git a/app/Models/Asset.php b/app/Models/Asset.php
index ce8b870eb2e0..e9f15bbb1f3e 100644
--- a/app/Models/Asset.php
+++ b/app/Models/Asset.php
@@ -7,24 +7,28 @@
use App\Helpers\Helper;
use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\Traits\Acceptable;
+use App\Models\Traits\Customizable;
+use App\Models\Traits\HasCustomFields;
+use App\Models\Traits\CompanyableTrait;
+use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable;
-use App\Presenters\Presentable;
use App\Presenters\AssetPresenter;
-use Illuminate\Support\Facades\Auth;
+use App\Presenters\Presentable;
use Carbon\Carbon;
-use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Crypt;
+use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
-use Illuminate\Database\Eloquent\Casts\Attribute;
-use Illuminate\Database\Eloquent\Model;
/**
* Model for Assets.
*
- * @version v1.0
+ * @version v1.0
*/
class Asset extends Depreciable
{
@@ -33,33 +37,47 @@ class Asset extends Depreciable
protected $with = ['model', 'adminuser'];
use CompanyableTrait;
+ use HasUploads;
use HasFactory, Loggable, Requestable, Presentable, SoftDeletes, ValidatingTrait, UniqueUndeletedTrait;
public const LOCATION = 'location';
public const ASSET = 'asset';
public const USER = 'user';
- use Acceptable;
+ use Acceptable, HasCustomFields;
+
+ public function getFieldsetKey(): object|int|null
+ {
+ return $this->model;
+ }
+ public static function getFieldsetUsers(int $fieldset_id): array
+ {
+ $models = [];
+ foreach (AssetModel::where("fieldset_id", $fieldset_id)->get() as $model) {
+ $models[route('models.show', $model->id)] = $model->name . (($model->model_number) ? ' (' . $model->model_number . ')' : '');
+ }
+ return $models;
+ }
/**
* Run after the checkout acceptance was declined by the user
*
- * @param User $acceptedBy
- * @param string $signature
+ * @param User $acceptedBy
+ * @param string $signature
*/
public function declinedCheckout(User $declinedBy, $signature)
{
- $this->assigned_to = null;
- $this->assigned_type = null;
- $this->accepted = null;
- $this->save();
+ $this->assigned_to = null;
+ $this->assigned_type = null;
+ $this->accepted = null;
+ $this->save();
}
/**
- * The database table used by the model.
- *
- * @var string
- */
+ * The database table used by the model.
+ *
+ * @var string
+ */
protected $table = 'assets';
/**
@@ -69,12 +87,12 @@ public function declinedCheckout(User $declinedBy, $signature)
// protected $with = ['model'];
/**
- * Whether the model should inject it's identifier to the unique
- * validation rules before attempting validation. If this property
- * is not set in the model it will default to true.
- *
+ * Whether the model should inject it's identifier to the unique
+ * validation rules before attempting validation. If this property
+ * is not set in the model it will default to true.
+ *
* @var bool
- */
+ */
protected $injectUniqueIdentifier = true;
protected $casts = [
@@ -108,30 +126,31 @@ public function declinedCheckout(User $declinedBy, $signature)
'expected_checkin' => ['nullable', 'date'],
'last_audit_date' => ['nullable', 'date_format:Y-m-d H:i:s'],
'next_audit_date' => ['nullable', 'date'],
- 'location_id' => ['nullable', 'exists:locations,id'],
- 'rtd_location_id' => ['nullable', 'exists:locations,id'],
+ 'location_id' => ['nullable', 'exists:locations,id', 'fmcs_location'],
+ 'rtd_location_id' => ['nullable', 'exists:locations,id', 'fmcs_location'],
'purchase_date' => ['nullable', 'date', 'date_format:Y-m-d'],
- 'serial' => ['nullable', 'unique_undeleted:assets,serial'],
- 'purchase_cost' => ['nullable', 'numeric', 'gte:0', 'max:9999999999999'],
+ 'serial' => ['nullable', 'string', 'unique_undeleted:assets,serial'],
+ 'purchase_cost' => ['nullable', 'numeric', 'gte:0', 'max:99999999999999999.99'],
'supplier_id' => ['nullable', 'exists:suppliers,id'],
'asset_eol_date' => ['nullable', 'date'],
'eol_explicit' => ['nullable', 'boolean'],
'byod' => ['nullable', 'boolean'],
'order_number' => ['nullable', 'string', 'max:191'],
'notes' => ['nullable', 'string', 'max:65535'],
- 'assigned_to' => ['nullable', 'integer'],
+ 'assigned_to' => ['nullable', 'integer', 'required_with:assigned_type'],
+ 'assigned_type' => ['nullable', 'required_with:assigned_to', 'in:' . User::class . "," . Location::class . "," . Asset::class],
'requestable' => ['nullable', 'boolean'],
- 'assigned_user' => ['nullable', 'exists:users,id,deleted_at,NULL'],
- 'assigned_location' => ['nullable', 'exists:locations,id,deleted_at,NULL'],
- 'assigned_asset' => ['nullable', 'exists:assets,id,deleted_at,NULL']
+ 'assigned_user' => ['integer', 'nullable', 'exists:users,id,deleted_at,NULL'],
+ 'assigned_location' => ['integer', 'nullable', 'exists:locations,id,deleted_at,NULL', 'fmcs_location'],
+ 'assigned_asset' => ['integer', 'nullable', 'exists:assets,id,deleted_at,NULL']
];
/**
- * The attributes that are mass assignable.
- *
- * @var array
- */
+ * The attributes that are mass assignable.
+ *
+ * @var array
+ */
protected $fillable = [
'asset_tag',
'assigned_to',
@@ -204,6 +223,17 @@ public function declinedCheckout(User $declinedBy, $signature)
'model.manufacturer' => ['name'],
];
+ protected static function booted(): void
+ {
+ static::forceDeleted(function (Asset $asset) {
+ $asset->requests()->forceDelete();
+ });
+
+ static::softDeleted(function (Asset $asset) {
+ $asset->requests()->delete();
+ });
+ }
+
// To properly set the expected checkin as Y-m-d
public function setExpectedCheckinAttribute($value)
{
@@ -213,39 +243,31 @@ public function setExpectedCheckinAttribute($value)
$this->attributes['expected_checkin'] = $value;
}
- /**
- * This handles the custom field validation for assets
- *
- * @var array
- */
- public function save(array $params = [])
+
+ public function customFieldValidationRules()
{
- if ($this->model_id != '') {
- $model = AssetModel::find($this->model_id);
- if (($model) && ($model->fieldset)) {
+ $customFieldValidationRules = [];
- foreach ($model->fieldset->fields as $field){
- if($field->format == 'BOOLEAN'){
- $this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN);
- }
- }
+ if (($this->model) && ($this->model->fieldset)) {
- $this->rules += $model->fieldset->validation_rules();
+ foreach ($this->model->fieldset->fields as $field) {
- if ($this->model->fieldset){
- foreach ($this->model->fieldset->fields as $field){
- if($field->format == 'BOOLEAN'){
- $this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN);
- }
- }
+ // this just casts booleans that may come through as strings to an actual boolean type
+ // adding !$field->field_encrypted because when the encrypted value comes through it
+ // screws things up for the encrypted validation rules (and the encrypted string
+ // is not a valid boolean type)
+ if ($field->format == 'BOOLEAN' && !$field->field_encrypted) {
+ $this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN);
}
}
+
+ $customFieldValidationRules += $this->model->fieldset->validation_rules();
}
- return parent::save($params);
- }
+ return $customFieldValidationRules;
+ }
public function getDisplayNameAttribute()
{
@@ -254,7 +276,8 @@ public function getDisplayNameAttribute()
/**
* Returns the warranty expiration date as Carbon object
- * @return \Carbon|null
+ *
+ * @return \Carbon\Carbon|null
*/
public function getWarrantyExpiresAttribute()
{
@@ -276,9 +299,9 @@ public function getWarrantyExpiresAttribute()
/**
* Establishes the asset -> company relationship
*
- * @author [A. Gianotto] []
- * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ *@since [v3.0]
+ * @author [A. Gianotto] []
*/
public function company()
{
@@ -291,9 +314,9 @@ public function company()
* OR if the assigned_to and deleted_at fields on the asset are empty AND
* that the status is deployable
*
- * @author [A. Gianotto] []
- * @since [v3.0]
* @return bool
+ *@since [v3.0]
+ * @author [A. Gianotto] []
*/
public function availableForCheckout()
{
@@ -303,8 +326,8 @@ public function availableForCheckout()
// The asset status is not archived and is deployable
if (($this->assetstatus) && ($this->assetstatus->archived == '0')
- && ($this->assetstatus->deployable == '1'))
- {
+ && ($this->assetstatus->deployable == '1')
+ ) {
return true;
}
@@ -316,18 +339,18 @@ public function availableForCheckout()
/**
* Checks the asset out to the target
*
- * @todo The admin parameter is never used. Can probably be removed.
- *
- * @author [A. Gianotto] []
- * @param User $user
- * @param User $admin
- * @param Carbon $checkout_at
- * @param Carbon $expected_checkin
+ * @param User $user
+ * @param User $admin
+ * @param Carbon $checkout_at
+ * @param Carbon $expected_checkin
* @param string $note
* @param null $name
* @return bool
- * @since [v3.0]
* @return bool
+ * @todo The admin parameter is never used. Can probably be removed.
+ *
+ * @author [A. Gianotto] []
+ * @since [v3.0]
*/
public function checkOut($target, $admin = null, $checkout_at = null, $expected_checkin = null, $note = null, $name = null, $location = null)
{
@@ -386,9 +409,9 @@ public function checkOut($target, $admin = null, $checkout_at = null, $expected_
/**
* Sets the detailedNameAttribute
*
- * @author [A. Gianotto] []
- * @since [v3.0]
* @return string
+ *@since [v3.0]
+ * @author [A. Gianotto] []
*/
public function getDetailedNameAttribute()
{
@@ -404,35 +427,69 @@ public function getDetailedNameAttribute()
/**
* Pulls in the validation rules
*
- * @author [A. Gianotto] []
- * @since [v3.0]
* @return array
+ *@since [v3.0]
+ * @author [A. Gianotto] []
*/
public function validationRules()
{
return $this->rules;
}
+ public function customFieldsForCheckinCheckout($checkin_checkout)
+ {
+ // Check to see if any of the custom fields were included on the form and if they have any values
+ if (($this->model) && ($this->model->fieldset) && ($this->model->fieldset->fields)) {
+
+ foreach ($this->model->fieldset->fields as $field) {
+
+ if (($field->{$checkin_checkout} == 1) && (request()->has($field->db_column))) {
+
+ if ($field->field_encrypted == '1') {
+
+ if (Gate::allows('assets.view.encrypted_custom_fields')) {
+ if (is_array(request()->input($field->db_column))) {
+ $this->{$field->db_column} = Crypt::encrypt(implode(', ', request()->input($field->db_column)));
+ } else {
+ $this->{$field->db_column} = Crypt::encrypt(request()->get($field->db_column));
+ }
+ }
+
+ } else {
+
+ if (is_array(request()->input($field->db_column))) {
+ $this->{$field->db_column} = implode(', ', request()->input($field->db_column));
+ } else {
+ $this->{$field->db_column} = request()->input($field->db_column);
+ }
+
+ }
+ }
+ }
+ }
+
+ }
+
/**
* Establishes the asset -> depreciation relationship
*
- * @author [A. Gianotto] []
- * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v3.0]
+ * @author [A. Gianotto] []
*/
public function depreciation()
{
- return $this->hasOneThrough(\App\Models\Depreciation::class,\App\Models\AssetModel::class,'id','id','model_id','depreciation_id');
+ return $this->hasOneThrough(\App\Models\Depreciation::class, \App\Models\AssetModel::class, 'id', 'id', 'model_id', 'depreciation_id');
}
/**
* Get components assigned to this asset
*
- * @author [A. Gianotto] []
- * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v4.0]
+ * @author [A. Gianotto] []
*/
public function components()
{
@@ -443,11 +500,11 @@ public function components()
/**
* Get depreciation attribute from associated asset model
*
+ * @return \Illuminate\Database\Eloquent\Relations\Relation
+ *@author [A. Gianotto] []
+ * @since [v4.0]
* @todo Is this still needed?
*
- * @author [A. Gianotto] []
- * @since [v4.0]
- * @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function get_depreciation()
{
@@ -457,52 +514,36 @@ public function get_depreciation()
}
- /**
- * Get uploads for this asset
- *
- * @author [A. Gianotto] []
- * @since [v4.0]
- * @return \Illuminate\Database\Eloquent\Relations\Relation
- */
- public function uploads()
- {
- return $this->hasMany('\App\Models\Actionlog', 'item_id')
- ->where('item_type', '=', Asset::class)
- ->where('action_type', '=', 'uploaded')
- ->whereNotNull('filename')
- ->orderBy('created_at', 'desc');
- }
-
/**
* Determines whether the asset is checked out to a user
*
- * Even though we allow allow for checkout to things beyond users
+ * Even though we allow for checkout to things beyond users
* this method is an easy way of seeing if we are checked out to a user.
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
*/
public function checkedOutToUser(): bool
{
- return $this->assignedType() === self::USER;
+ return $this->assignedType() === self::USER;
}
public function checkedOutToLocation(): bool
{
- return $this->assignedType() === self::LOCATION;
+ return $this->assignedType() === self::LOCATION;
}
public function checkedOutToAsset(): bool
{
- return $this->assignedType() === self::ASSET;
+ return $this->assignedType() === self::ASSET;
}
/**
* Get the target this asset is checked out to
*
- * @author [A. Gianotto] []
- * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v4.0]
+ * @author [A. Gianotto] []
*/
public function assignedTo()
{
@@ -514,15 +555,27 @@ public function assignedTo()
*
* Sigh.
*
- * @author [A. Gianotto] []
- * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v4.0]
+ * @author [A. Gianotto] []
*/
public function assignedAssets()
{
return $this->morphMany(self::class, 'assigned', 'assigned_type', 'assigned_to')->withTrashed();
}
+ /**
+ * Establishes the accessory -> asset assignment relationship
+ *
+ * @author A. Gianotto
+ * @since [v3.0]
+ * @return \Illuminate\Database\Eloquent\Relations\Relation
+ */
+ public function assignedAccessories()
+ {
+ return $this->morphMany(\App\Models\AccessoryCheckout::class, 'assigned', 'assigned_type', 'assigned_to');
+ }
+
/**
* Get the asset's location based on the assigned user
@@ -530,7 +583,7 @@ public function assignedAssets()
* @todo Refactor this if possible. It's awful.
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return \ArrayObject
*/
public function assetLoc($iterations = 1, $first_asset = null)
@@ -570,9 +623,9 @@ public function assetLoc($iterations = 1, $first_asset = null)
/**
* Gets the lowercased name of the type of target the asset is assigned to
*
- * @author [A. Gianotto] []
- * @since [v4.0]
* @return string
+ *@since [v4.0]
+ * @author [A. Gianotto] []
*/
public function assignedType()
{
@@ -583,11 +636,12 @@ public function assignedType()
/**
* This is annoying, but because we don't say "assets" in our route names, we have to make an exception here
- * @todo - normalize the route names - API endpoint URLS can stay the same
*
- * @author [A. Gianotto] []
- * @since [v6.1.0]
* @return string
+ *@author [A. Gianotto] []
+ * @since [v6.1.0]
+ * @todo - normalize the route names - API endpoint URLS can stay the same
+ *
*/
public function targetShowRoute()
{
@@ -604,9 +658,9 @@ public function targetShowRoute()
/**
* Get the asset's location based on default RTD location
*
- * @author [A. Gianotto] []
- * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v2.0]
+ * @author [A. Gianotto] []
*/
public function defaultLoc()
{
@@ -619,9 +673,9 @@ public function defaultLoc()
* Check first to see if there is a specific image uploaded to the asset,
* and if not, check for an image uploaded to the asset model.
*
- * @author [A. Gianotto] []
- * @since [v2.0]
* @return string | false
+ *@since [v2.0]
+ * @author [A. Gianotto] []
*/
public function getImageUrl()
{
@@ -629,6 +683,8 @@ public function getImageUrl()
return Storage::disk('public')->url(app('assets_upload_path').e($this->image));
} elseif ($this->model && ! empty($this->model->image)) {
return Storage::disk('public')->url(app('models_upload_path').e($this->model->image));
+ } elseif ($this->model?->category && !empty($this->model->category->image)) {
+ return Storage::disk('public')->url(app('categories_upload_path') . e($this->model->category->image));
}
return false;
@@ -638,24 +694,24 @@ public function getImageUrl()
/**
* Get the asset's logs
*
- * @author [A. Gianotto] []
- * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v2.0]
+ * @author [A. Gianotto] []
*/
public function assetlog()
{
return $this->hasMany(\App\Models\Actionlog::class, 'item_id')
- ->where('item_type', '=', self::class)
- ->orderBy('created_at', 'desc')
- ->withTrashed();
+ ->where('item_type', '=', self::class)
+ ->orderBy('created_at', 'desc')
+ ->withTrashed();
}
/**
* Get the list of checkouts for this asset
*
- * @author [A. Gianotto] []
- * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v2.0]
+ * @author [A. Gianotto] []
*/
public function checkouts()
{
@@ -664,12 +720,27 @@ public function checkouts()
->withTrashed();
}
+
/**
- * Get the list of checkins for this asset
+ * Get the list of audits for this asset
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
+ * @return \Illuminate\Database\Eloquent\Relations\Relation
+ */
+ public function audits()
+ {
+ return $this->assetlog()->where('action_type', '=', 'audit')
+ ->orderBy('created_at', 'desc')
+ ->withTrashed();
+ }
+
+ /**
+ * Get the list of checkins for this asset
+ *
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v2.0]
+ * @author [A. Gianotto] []
*/
public function checkins()
{
@@ -682,9 +753,9 @@ public function checkins()
/**
* Get the asset's user requests
*
- * @author [A. Gianotto] []
- * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v2.0]
+ * @author [A. Gianotto] []
*/
public function userRequests()
{
@@ -698,22 +769,22 @@ public function userRequests()
/**
* Get maintenances for this asset
*
- * @author Vincent Sposato
- * @since 1.0
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since 1.0
+ * @author Vincent Sposato
*/
- public function assetmaintenances()
+ public function maintenances()
{
- return $this->hasMany(\App\Models\AssetMaintenance::class, 'asset_id')
- ->orderBy('created_at', 'desc');
+ return $this->hasMany(\App\Models\Maintenance::class, 'asset_id')
+ ->orderBy('created_at', 'desc');
}
/**
* Get user who created the item
*
- * @author [A. Gianotto] []
- * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v1.0]
+ * @author [A. Gianotto] []
*/
public function adminuser()
{
@@ -725,9 +796,9 @@ public function adminuser()
/**
* Establishes the asset -> status relationship
*
- * @author [A. Gianotto] []
- * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v1.0]
+ * @author [A. Gianotto] []
*/
public function assetstatus()
{
@@ -737,9 +808,9 @@ public function assetstatus()
/**
* Establishes the asset -> model relationship
*
- * @author [A. Gianotto] []
- * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v1.0]
+ * @author [A. Gianotto] []
*/
public function model()
{
@@ -749,10 +820,10 @@ public function model()
/**
* Return the assets with a warranty expiring within x days
*
- * @param $days
- * @author [A. Gianotto] []
- * @since [v2.0]
+ * @param $days
* @return mixed
+ *@since [v2.0]
+ * @author [A. Gianotto] []
*/
public static function getExpiringWarrantee($days = 30)
{
@@ -762,9 +833,12 @@ public static function getExpiringWarrantee($days = 30)
->whereNotNull('warranty_months')
->whereNotNull('purchase_date')
->whereNull('deleted_at')
- ->whereRaw('DATE_ADD(`purchase_date`,INTERVAL `warranty_months` MONTH) <= DATE(NOW() + INTERVAL '
- . $days
- . ' DAY) AND DATE_ADD(`purchase_date`, INTERVAL `warranty_months` MONTH) > NOW()')
+ ->NotArchived()
+ ->whereRaw(
+ 'DATE_ADD(`purchase_date`, INTERVAL `warranty_months` MONTH) <= DATE_ADD(NOW(), INTERVAL '
+ . $days
+ . ' DAY) AND DATE_ADD(`purchase_date`, INTERVAL `warranty_months` MONTH) > NOW()'
+ )
->orderByRaw('DATE_ADD(`purchase_date`,INTERVAL `warranty_months` MONTH)')
->get();
}
@@ -773,9 +847,9 @@ public static function getExpiringWarrantee($days = 30)
/**
* Establishes the asset -> assigned licenses relationship
*
- * @author [A. Gianotto] []
- * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ *@since [v4.0]
+ * @author [A. Gianotto] []
*/
public function licenses()
{
@@ -785,9 +859,9 @@ public function licenses()
/**
* Establishes the asset -> license seats relationship
*
- * @author [A. Gianotto] []
- * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ *@since [v4.0]
+ * @author [A. Gianotto] []
*/
public function licenseseats()
{
@@ -797,9 +871,9 @@ public function licenseseats()
/**
* Establishes the asset -> aupplier relationship
*
- * @author [A. Gianotto] []
- * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ *@since [v2.0]
+ * @author [A. Gianotto] []
*/
public function supplier()
{
@@ -809,9 +883,9 @@ public function supplier()
/**
* Establishes the asset -> location relationship
*
- * @author [A. Gianotto] []
- * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ *@since [v2.0]
+ * @author [A. Gianotto] []
*/
public function location()
{
@@ -822,9 +896,9 @@ public function location()
/**
* Get the next autoincremented asset tag
*
- * @author [A. Gianotto] []
- * @since [v4.0]
* @return string | false
+ *@since [v4.0]
+ * @author [A. Gianotto] []
*/
public static function autoincrement_asset(int $additional_increment = 0)
{
@@ -849,7 +923,7 @@ public static function autoincrement_asset(int $additional_increment = 0)
* We'll add the zerofill and prefixes on the fly as we generate the number.
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return int
*/
public static function nextAutoIncrement($assets)
@@ -860,12 +934,10 @@ public static function nextAutoIncrement($assets)
foreach ($assets as $asset) {
$results = preg_match("/\d+$/", $asset['asset_tag'], $matches);
- if ($results)
- {
+ if ($results) {
$number = $matches[0];
- if ($number > $max)
- {
+ if ($number > $max) {
$max = $number;
}
}
@@ -881,9 +953,9 @@ public static function nextAutoIncrement($assets)
*
* We'll add the zerofill and prefixes on the fly as we generate the number.
*
- * @author [A. Gianotto] []
- * @since [v4.0]
* @return string
+ *@since [v4.0]
+ * @author [A. Gianotto] []
*/
public static function zerofill($num, $zerofill = 3)
{
@@ -895,7 +967,7 @@ public static function zerofill($num, $zerofill = 3)
* asset model category
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return bool
*/
public function checkin_email()
@@ -909,7 +981,7 @@ public function checkin_email()
* Determine whether this asset requires acceptance by the assigned user
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return bool
*/
public function requireAcceptance()
@@ -918,6 +990,7 @@ public function requireAcceptance()
return $this->model->category->require_acceptance;
}
+ return false;
}
@@ -925,7 +998,7 @@ public function requireAcceptance()
* Determine whether this asset's next audit date is before the last audit date
*
* @return bool
- * @since [v6.4.1]
+ * @since [v6.4.1]
* @author [A. Gianotto] []
* */
public function checkInvalidNextAuditDate()
@@ -952,17 +1025,17 @@ public function checkInvalidNextAuditDate()
* Checks for a category-specific EULA, and if that doesn't exist,
* checks for a settings level EULA
*
- * @author [A. Gianotto] []
- * @since [v4.0]
* @return string | false
+ *@since [v4.0]
+ * @author [A. Gianotto] []
*/
public function getEula()
{
if (($this->model) && ($this->model->category)) {
- if (($this->model->category->eula_text) && ($this->model->category->use_default_eula === 0)) {
+ if (($this->model->category->eula_text) && ($this->model->category->use_default_eula == 0)) {
return Helper::parseEscapedMarkedown($this->model->category->eula_text);
- } elseif ($this->model->category->use_default_eula === 1) {
+ } elseif ($this->model->category->use_default_eula == 1) {
return Helper::parseEscapedMarkedown(Setting::getSettings()->default_eula_text);
} else {
@@ -972,7 +1045,8 @@ public function getEula()
return false;
}
- public function getComponentCost(){
+ public function getComponentCost()
+ {
$cost = 0;
foreach($this->components as $component) {
$cost += $component->pivot->assigned_qty*$component->purchase_cost;
@@ -992,7 +1066,7 @@ public function getComponentCost(){
* This is kind of dumb and confusing, since we already cast it that way AND it's a date field
* in the database, but here we are.
*
- * @param $value
+ * @param $value
* @return void
*/
@@ -1042,7 +1116,7 @@ protected function assetEolDate(): Attribute
*
* This will also correctly parse a 1/0 if "true"/"false" is passed.
*
- * @param $value
+ * @param $value
* @return void
*/
@@ -1056,16 +1130,16 @@ protected function requestable(): Attribute
/**
- * -----------------------------------------------
- * BEGIN QUERY SCOPES
- * -----------------------------------------------
- **/
+ * -----------------------------------------------
+ * BEGIN QUERY SCOPES
+ * -----------------------------------------------
+ **/
/**
* Run additional, advanced searches.
*
* @param \Illuminate\Database\Eloquent\Builder $query
- * @param array $terms The search terms
+ * @param array $terms The search terms
* @return \Illuminate\Database\Eloquent\Builder
*/
public function advancedTextSearch(Builder $query, array $terms)
@@ -1074,31 +1148,38 @@ public function advancedTextSearch(Builder $query, array $terms)
/**
* Assigned user
*/
- $query = $query->leftJoin('users as assets_users', function ($leftJoin) {
+ $query = $query->leftJoin(
+ 'users as assets_users', function ($leftJoin) {
$leftJoin->on('assets_users.id', '=', 'assets.assigned_to')
->where('assets.assigned_type', '=', User::class);
- });
+ }
+ );
foreach ($terms as $term) {
$query = $query
->orWhere('assets_users.first_name', 'LIKE', '%'.$term.'%')
->orWhere('assets_users.last_name', 'LIKE', '%'.$term.'%')
+ ->orWhere('assets_users.jobtitle', 'LIKE', '%' . $term . '%')
->orWhere('assets_users.username', 'LIKE', '%'.$term.'%')
->orWhere('assets_users.employee_num', 'LIKE', '%'.$term.'%')
- ->orWhereMultipleColumns([
- 'assets_users.first_name',
+ ->orWhereMultipleColumns(
+ [
+ 'assets_users.first_name',
'assets_users.last_name',
- ], $term);
+ ], $term
+ );
}
/**
* Assigned location
*/
- $query = $query->leftJoin('locations as assets_locations', function ($leftJoin) {
+ $query = $query->leftJoin(
+ 'locations as assets_locations', function ($leftJoin) {
$leftJoin->on('assets_locations.id', '=', 'assets.assigned_to')
->where('assets.assigned_type', '=', Location::class);
- });
+ }
+ );
foreach ($terms as $term) {
@@ -1108,10 +1189,12 @@ public function advancedTextSearch(Builder $query, array $terms)
/**
* Assigned assets
*/
- $query = $query->leftJoin('assets as assigned_assets', function ($leftJoin) {
+ $query = $query->leftJoin(
+ 'assets as assigned_assets', function ($leftJoin) {
$leftJoin->on('assigned_assets.id', '=', 'assets.assigned_to')
->where('assets.assigned_type', '=', self::class);
- });
+ }
+ );
foreach ($terms as $term) {
$query = $query->orWhere('assigned_assets.name', 'LIKE', '%'.$term.'%');
@@ -1123,12 +1206,12 @@ public function advancedTextSearch(Builder $query, array $terms)
/**
- * Query builder scope for hardware
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope for hardware
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeHardware($query)
{
@@ -1136,101 +1219,121 @@ public function scopeHardware($query)
}
/**
- * Query builder scope for pending assets
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope for pending assets
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopePending($query)
{
- return $query->whereHas('assetstatus', function ($query) {
+ return $query->whereHas(
+ 'assetstatus', function ($query) {
$query->where('deployable', '=', 0)
->where('pending', '=', 1)
->where('archived', '=', 0);
- });
+ }
+ );
}
/**
- * Query builder scope for searching location
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope for searching location
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeAssetsByLocation($query, $location)
{
- return $query->where(function ($query) use ($location) {
- $query->whereHas('assignedTo', function ($query) use ($location) {
- $query->where([
- ['users.location_id', '=', $location->id],
- ['assets.assigned_type', '=', User::class],
- ])->orWhere([
- ['locations.id', '=', $location->id],
- ['assets.assigned_type', '=', Location::class],
- ])->orWhere([
- ['assets.rtd_location_id', '=', $location->id],
- ['assets.assigned_type', '=', self::class],
- ]);
- })->orWhere(function ($query) use ($location) {
- $query->where('assets.rtd_location_id', '=', $location->id);
- $query->whereNull('assets.assigned_to');
- });
- });
+ return $query->where(
+ function ($query) use ($location) {
+ $query->whereHas(
+ 'assignedTo', function ($query) use ($location) {
+ $query->where(
+ [
+ ['users.location_id', '=', $location->id],
+ ['assets.assigned_type', '=', User::class],
+ ]
+ )->orWhere(
+ [
+ ['locations.id', '=', $location->id],
+ ['assets.assigned_type', '=', Location::class],
+ ]
+ )->orWhere(
+ [
+ ['assets.rtd_location_id', '=', $location->id],
+ ['assets.assigned_type', '=', self::class],
+ ]
+ );
+ }
+ )->orWhere(
+ function ($query) use ($location) {
+ $query->where('assets.rtd_location_id', '=', $location->id);
+ $query->whereNull('assets.assigned_to');
+ }
+ );
+ }
+ );
}
/**
- * Query builder scope for RTD assets
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope for RTD assets
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeRTD($query)
{
return $query->whereNull('assets.assigned_to')
- ->whereHas('assetstatus', function ($query) {
- $query->where('deployable', '=', 1)
- ->where('pending', '=', 0)
- ->where('archived', '=', 0);
- });
+ ->whereHas(
+ 'assetstatus', function ($query) {
+ $query->where('deployable', '=', 1)
+ ->where('pending', '=', 0)
+ ->where('archived', '=', 0);
+ }
+ );
}
- /**
- * Query builder scope for Undeployable assets
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ /**
+ * Query builder scope for Undeployable assets
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeUndeployable($query)
{
- return $query->whereHas('assetstatus', function ($query) {
+ return $query->whereHas(
+ 'assetstatus', function ($query) {
$query->where('deployable', '=', 0)
->where('pending', '=', 0)
->where('archived', '=', 0);
- });
+ }
+ );
}
/**
* Query builder scope for non-Archived assets
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeNotArchived($query)
{
- return $query->whereHas('assetstatus', function ($query) {
+ return $query->whereHas(
+ 'assetstatus', function ($query) {
$query->where('archived', '=', 0);
- });
+ }
+ );
}
/**
@@ -1249,16 +1352,16 @@ public function scopeNotArchived($query)
* threshold for alerts = 30 days
* now = May 4, 2019
*
- * @author A. Gianotto
- * @since v4.6.16
- * @param Setting $settings
+ * @param Setting $settings
*
* @return \Illuminate\Database\Query\Builder Modified query builder
+ * @author A. Gianotto
+ * @since v4.6.16
*/
public function scopeDueForAudit($query, $settings)
{
- $interval = $settings->audit_warning_days ?? 0;
+ $interval = (int) $settings->audit_warning_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval)->format('Y-m-d');
@@ -1275,11 +1378,11 @@ public function scopeDueForAudit($query, $settings)
* This is/will be used in the artisan command snipeit:upcoming-audits and also
* for an upcoming API call for retrieving a report on overdue assets.
*
- * @author A. Gianotto
- * @since v4.6.16
- * @param Setting $settings
+ * @param Setting $settings
*
* @return \Illuminate\Database\Query\Builder Modified query builder
+ * @author A. Gianotto
+ * @since v4.6.16
*/
public function scopeOverdueForAudit($query)
@@ -1297,21 +1400,25 @@ public function scopeOverdueForAudit($query)
* This is/will be used in the artisan command snipeit:upcoming-audits and also
* for an upcoming API call for retrieving a report on assets that will need to be audited.
*
- * @author A. Gianotto
- * @since v4.6.16
- * @param Setting $settings
+ * @param Setting $settings
*
* @return \Illuminate\Database\Query\Builder Modified query builder
+ * @author A. Gianotto
+ * @since v4.6.16
*/
public function scopeDueOrOverdueForAudit($query, $settings)
{
- return $query->where(function ($query) {
- $query->OverdueForAudit();
- })->orWhere(function ($query) use ($settings) {
- $query->DueForAudit($settings);
- });
+ return $query->where(
+ function ($query) {
+ $query->OverdueForAudit();
+ }
+ )->orWhere(
+ function ($query) use ($settings) {
+ $query->DueForAudit($settings);
+ }
+ );
}
@@ -1319,14 +1426,14 @@ public function scopeDueOrOverdueForAudit($query, $settings)
* Query builder scope for Assets that are DUE for checkin, based on the assets.expected_checkin
* and settings.audit_warning_days. It checks to see if assets.expected_checkin is now
*
- * @author A. Gianotto
- * @since v6.4.0
* @return \Illuminate\Database\Query\Builder Modified query builder
+ * @since v6.4.0
+ * @author A. Gianotto
*/
public function scopeDueForCheckin($query, $settings)
{
- $interval = $settings->due_checkin_days ?? 0;
+ $interval = (int) $settings->due_checkin_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval)->format('Y-m-d');
@@ -1340,9 +1447,9 @@ public function scopeDueForCheckin($query, $settings)
/**
* Query builder scope for Assets that are overdue for checkin OR overdue
*
- * @author A. Gianotto
- * @since v6.4.0
* @return \Illuminate\Database\Query\Builder Modified query builder
+ * @since v6.4.0
+ * @author A. Gianotto
*/
public function scopeOverdueForCheckin($query)
{
@@ -1356,17 +1463,21 @@ public function scopeOverdueForCheckin($query)
/**
* Query builder scope for Assets that are due for checkin OR overdue
*
- * @author A. Gianotto
- * @since v6.4.0
* @return \Illuminate\Database\Query\Builder Modified query builder
+ * @since v6.4.0
+ * @author A. Gianotto
*/
public function scopeDueOrOverdueForCheckin($query, $settings)
{
- return $query->where(function ($query) {
- $query->OverdueForCheckin();
- })->orWhere(function ($query) use ($settings) {
- $query->DueForCheckin($settings);
- });
+ return $query->where(
+ function ($query) {
+ $query->OverdueForCheckin();
+ }
+ )->orWhere(
+ function ($query) use ($settings) {
+ $query->DueForCheckin($settings);
+ }
+ );
}
@@ -1377,7 +1488,7 @@ public function scopeDueOrOverdueForCheckin($query, $settings)
* has chosen to not display archived assets in their regular lists
* and views, it will return the correct number.
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -1386,115 +1497,123 @@ public function scopeAssetsForShow($query)
{
if (Setting::getSettings()->show_archived_in_list!=1) {
- return $query->whereHas('assetstatus', function ($query) {
+ return $query->whereHas(
+ 'assetstatus', function ($query) {
$query->where('archived', '=', 0);
- });
+ }
+ );
} else {
return $query;
}
}
- /**
- * Query builder scope for Archived assets
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ /**
+ * Query builder scope for Archived assets
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeArchived($query)
{
- return $query->whereHas('assetstatus', function ($query) {
+ return $query->whereHas(
+ 'assetstatus', function ($query) {
$query->where('deployable', '=', 0)
->where('pending', '=', 0)
->where('archived', '=', 1);
- });
+ }
+ );
}
- /**
- * Query builder scope for Deployed assets
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ /**
+ * Query builder scope for Deployed assets
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeDeployed($query)
{
return $query->where('assigned_to', '>', '0');
}
- /**
- * Query builder scope for Requestable assets
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ /**
+ * Query builder scope for Requestable assets
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
- public function scopeRequestableAssets($query)
+ public function scopeRequestableAssets($query): Builder
{
$table = $query->getModel()->getTable();
return Company::scopeCompanyables($query->where($table.'.requestable', '=', 1))
- ->whereHas('assetstatus', function ($query) {
- $query->where(function ($query) {
- $query->where('deployable', '=', 1)
- ->where('archived', '=', 0); // you definitely can't request something that's archived
- })->orWhere('pending', '=', 1); // we've decided that even though an asset may be 'pending', you can still request it
- });
+ ->whereHas(
+ 'assetstatus', function ($query) {
+ $query->where(
+ function ($query) {
+ $query->where('deployable', '=', 1)
+ ->where('archived', '=', 0); // you definitely can't request something that's archived
+ }
+ )->orWhere('pending', '=', 1); // we've decided that even though an asset may be 'pending', you can still request it
+ }
+ );
}
/**
- * scopeInModelList
- * Get all assets in the provided listing of model ids
- *
- * @param $query
- * @param array $modelIdListing
- *
- * @return mixed
- * @author Vincent Sposato
- * @version v1.0
- */
+ * scopeInModelList
+ * Get all assets in the provided listing of model ids
+ *
+ * @param $query
+ * @param array $modelIdListing
+ *
+ * @return mixed
+ * @author Vincent Sposato
+ * @version v1.0
+ */
public function scopeInModelList($query, array $modelIdListing)
{
return $query->whereIn('assets.model_id', $modelIdListing);
}
- /**
- * Query builder scope to get not-yet-accepted assets
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ /**
+ * Query builder scope to get not-yet-accepted assets
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeNotYetAccepted($query)
{
return $query->where('accepted', '=', 'pending');
}
- /**
- * Query builder scope to get rejected assets
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ /**
+ * Query builder scope to get rejected assets
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeRejected($query)
{
return $query->where('accepted', '=', 'rejected');
}
- /**
- * Query builder scope to get accepted assets
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ /**
+ * Query builder scope to get accepted assets
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeAccepted($query)
{
return $query->where('accepted', '=', 'accepted');
@@ -1503,8 +1622,8 @@ public function scopeAccepted($query)
/**
* Query builder scope to search on text for complex Bootstrap Tables API.
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $search Search term
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $search Search term
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -1512,259 +1631,328 @@ public function scopeAssignedSearch($query, $search)
{
$search = explode(' OR ', $search);
- return $query->leftJoin('users as assets_users', function ($leftJoin) {
+ return $query->leftJoin(
+ 'users as assets_users', function ($leftJoin) {
$leftJoin->on('assets_users.id', '=', 'assets.assigned_to')
->where('assets.assigned_type', '=', User::class);
- })->leftJoin('locations as assets_locations', function ($leftJoin) {
+ }
+ )->leftJoin(
+ 'locations as assets_locations', function ($leftJoin) {
$leftJoin->on('assets_locations.id', '=', 'assets.assigned_to')
->where('assets.assigned_type', '=', Location::class);
- })->leftJoin('assets as assigned_assets', function ($leftJoin) {
+ }
+ )->leftJoin(
+ 'assets as assigned_assets', function ($leftJoin) {
$leftJoin->on('assigned_assets.id', '=', 'assets.assigned_to')
->where('assets.assigned_type', '=', self::class);
- })->where(function ($query) use ($search) {
- foreach ($search as $search) {
- $query->whereHas('model', function ($query) use ($search) {
- $query->whereHas('category', function ($query) use ($search) {
- $query->where(function ($query) use ($search) {
- $query->where('categories.name', 'LIKE', '%'.$search.'%')
- ->orWhere('models.name', 'LIKE', '%'.$search.'%')
- ->orWhere('models.model_number', 'LIKE', '%'.$search.'%');
- });
- });
- })->orWhereHas('model', function ($query) use ($search) {
- $query->whereHas('manufacturer', function ($query) use ($search) {
- $query->where(function ($query) use ($search) {
- $query->where('manufacturers.name', 'LIKE', '%'.$search.'%');
- });
- });
- })->orWhere(function ($query) use ($search) {
- $query->where('assets_users.first_name', 'LIKE', '%'.$search.'%')
- ->orWhere('assets_users.last_name', 'LIKE', '%'.$search.'%')
- ->orWhereMultipleColumns([
- 'assets_users.first_name',
- 'assets_users.last_name',
- ], $search)
- ->orWhere('assets_users.username', 'LIKE', '%'.$search.'%')
- ->orWhere('assets_locations.name', 'LIKE', '%'.$search.'%')
- ->orWhere('assigned_assets.name', 'LIKE', '%'.$search.'%');
- })->orWhere('assets.name', 'LIKE', '%'.$search.'%')
- ->orWhere('assets.asset_tag', 'LIKE', '%'.$search.'%')
- ->orWhere('assets.serial', 'LIKE', '%'.$search.'%')
- ->orWhere('assets.order_number', 'LIKE', '%'.$search.'%')
- ->orWhere('assets.notes', 'LIKE', '%'.$search.'%');
- }
+ }
+ )->where(
+ function ($query) use ($search) {
+ foreach ($search as $search) {
+ $query->whereHas(
+ 'model', function ($query) use ($search) {
+ $query->whereHas(
+ 'category', function ($query) use ($search) {
+ $query->where(
+ function ($query) use ($search) {
+ $query->where('categories.name', 'LIKE', '%' . $search . '%')
+ ->orWhere('models.name', 'LIKE', '%' . $search . '%')
+ ->orWhere('models.model_number', 'LIKE', '%' . $search . '%');
+ }
+ );
+ }
+ );
+ }
+ )->orWhereHas(
+ 'model', function ($query) use ($search) {
+ $query->whereHas(
+ 'manufacturer', function ($query) use ($search) {
+ $query->where(
+ function ($query) use ($search) {
+ $query->where('manufacturers.name', 'LIKE', '%' . $search . '%');
+ }
+ );
+ }
+ );
+ }
+ )->orWhere(
+ function ($query) use ($search) {
+ $query->where('assets_users.first_name', 'LIKE', '%' . $search . '%')
+ ->orWhere('assets_users.last_name', 'LIKE', '%' . $search . '%')
+ ->orWhere('assets_users.username', 'LIKE', '%' . $search . '%')
+ ->orWhere('assets_users.jobtitle', 'LIKE', '%' . $search . '%')
+ ->orWhereMultipleColumns(
+ [
+ 'assets_users.first_name',
+ 'assets_users.last_name',
+ 'assets_users.jobtitle',
+ ], $search
+ )
+ ->orWhere('assets_locations.name', 'LIKE', '%' . $search . '%')
+ ->orWhere('assigned_assets.name', 'LIKE', '%' . $search . '%');
+ }
+ )->orWhere('assets.name', 'LIKE', '%' . $search . '%')
+ ->orWhere('assets.asset_tag', 'LIKE', '%' . $search . '%')
+ ->orWhere('assets.serial', 'LIKE', '%' . $search . '%')
+ ->orWhere('assets.order_number', 'LIKE', '%' . $search . '%')
+ ->orWhere('assets.notes', 'LIKE', '%' . $search . '%');
+ }
- })->withTrashed()->whereNull('assets.deleted_at'); //workaround for laravel bug
+ }
+ )->withTrashed()->whereNull('assets.deleted_at'); //workaround for laravel bug
}
/**
* Query builder scope to search the department ID of users assigned to assets
*
- * @author [A. Gianotto] []
- * @since [v5.0]
* @return string | false
*
* @return \Illuminate\Database\Query\Builder Modified query builder
+ *@author [A. Gianotto] []
+ * @since [v5.0]
*/
public function scopeCheckedOutToTargetInDepartment($query, $search)
{
- return $query->leftJoin('users as assets_dept_users', function ($leftJoin) {
+ return $query->leftJoin(
+ 'users as assets_dept_users', function ($leftJoin) {
$leftJoin->on('assets_dept_users.id', '=', 'assets.assigned_to')
->where('assets.assigned_type', '=', User::class);
- })->where(function ($query) use ($search) {
- $query->whereIn('assets_dept_users.department_id', $search);
+ }
+ )->where(
+ function ($query) use ($search) {
+ $query->whereIn('assets_dept_users.department_id', $search);
- })->withTrashed()->whereNull('assets.deleted_at'); //workaround for laravel bug
+ }
+ )->withTrashed()->whereNull('assets.deleted_at'); //workaround for laravel bug
}
-
/**
* Query builder scope to search on text filters for complex Bootstrap Tables API
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $filter JSON array of search keys and terms
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $filter JSON array of search keys and terms
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeByFilter($query, $filter)
{
- return $query->where(function ($query) use ($filter) {
- foreach ($filter as $key => $search_val) {
+ return $query->where(
+ function ($query) use ($filter) {
+ foreach ($filter as $key => $search_val) {
- $fieldname = str_replace('custom_fields.', '', $key);
+ $fieldname = str_replace('custom_fields.', '', $key);
- if ($fieldname == 'asset_tag') {
- $query->where('assets.asset_tag', 'LIKE', '%'.$search_val.'%');
- }
+ if ($fieldname == 'asset_tag') {
+ $query->where('assets.asset_tag', 'LIKE', '%' . $search_val . '%');
+ }
- if ($fieldname == 'name') {
- $query->where('assets.name', 'LIKE', '%'.$search_val.'%');
- }
+ if ($fieldname == 'name') {
+ $query->where('assets.name', 'LIKE', '%' . $search_val . '%');
+ }
- if ($fieldname =='serial') {
- $query->where('assets.serial', 'LIKE', '%'.$search_val.'%');
- }
+ if ($fieldname == 'serial') {
+ $query->where('assets.serial', 'LIKE', '%' . $search_val . '%');
+ }
- if ($fieldname == 'purchase_date') {
- $query->where('assets.purchase_date', 'LIKE', '%'.$search_val.'%');
- }
+ if ($fieldname == 'purchase_date') {
+ $query->where('assets.purchase_date', 'LIKE', '%' . $search_val . '%');
+ }
- if ($fieldname == 'purchase_cost') {
- $query->where('assets.purchase_cost', 'LIKE', '%'.$search_val.'%');
- }
+ if ($fieldname == 'purchase_cost') {
+ $query->where('assets.purchase_cost', 'LIKE', '%' . $search_val . '%');
+ }
- if ($fieldname == 'notes') {
- $query->where('assets.notes', 'LIKE', '%'.$search_val.'%');
- }
+ if ($fieldname == 'notes') {
+ $query->where('assets.notes', 'LIKE', '%' . $search_val . '%');
+ }
- if ($fieldname == 'order_number') {
- $query->where('assets.order_number', 'LIKE', '%'.$search_val.'%');
- }
+ if ($fieldname == 'order_number') {
+ $query->where('assets.order_number', 'LIKE', '%' . $search_val . '%');
+ }
- if ($fieldname == 'status_label') {
- $query->whereHas('assetstatus', function ($query) use ($search_val) {
- $query->where('status_labels.name', 'LIKE', '%'.$search_val.'%');
- });
- }
+ if ($fieldname == 'status_label') {
+ $query->whereHas(
+ 'assetstatus', function ($query) use ($search_val) {
+ $query->where('status_labels.name', 'LIKE', '%' . $search_val . '%');
+ }
+ );
+ }
- if ($fieldname == 'location') {
- $query->whereHas('location', function ($query) use ($search_val) {
- $query->where('locations.name', 'LIKE', '%'.$search_val.'%');
- });
- }
+ if ($fieldname == 'location') {
+ $query->whereHas(
+ 'location', function ($query) use ($search_val) {
+ $query->where('locations.name', 'LIKE', '%' . $search_val . '%');
+ }
+ );
+ }
- if ($fieldname == 'rtd_location') {
- $query->whereHas('defaultLoc', function ($query) use ($search_val) {
- $query->where('locations.name', 'LIKE', '%'.$search_val.'%');
- });
- }
+ if ($fieldname == 'rtd_location') {
+ $query->whereHas(
+ 'defaultLoc', function ($query) use ($search_val) {
+ $query->where('locations.name', 'LIKE', '%' . $search_val . '%');
+ }
+ );
+ }
- if ($fieldname =='assigned_to') {
- $query->whereHasMorph('assignedTo', [User::class], function ($query) use ($search_val) {
- $query->where(function ($query) use ($search_val) {
- $query->where('users.first_name', 'LIKE', '%'.$search_val.'%')
- ->orWhere('users.last_name', 'LIKE', '%'.$search_val.'%');
- });
- });
- }
+ if ($fieldname == 'assigned_to') {
+ $query->whereHasMorph(
+ 'assignedTo', [User::class], function ($query) use ($search_val) {
+ $query->where(
+ function ($query) use ($search_val) {
+ $query->where('users.first_name', 'LIKE', '%' . $search_val . '%')
+ ->orWhere('users.last_name', 'LIKE', '%' . $search_val . '%');
+ }
+ );
+ }
+ );
+ }
- if ($fieldname == 'manufacturer') {
- $query->whereHas('model', function ($query) use ($search_val) {
- $query->whereHas('manufacturer', function ($query) use ($search_val) {
- $query->where(function ($query) use ($search_val) {
- $query->where('manufacturers.name', 'LIKE', '%'.$search_val.'%');
- });
- });
- });
- }
+ if ($fieldname == 'manufacturer') {
+ $query->whereHas(
+ 'model', function ($query) use ($search_val) {
+ $query->whereHas(
+ 'manufacturer', function ($query) use ($search_val) {
+ $query->where(
+ function ($query) use ($search_val) {
+ $query->where('manufacturers.name', 'LIKE', '%' . $search_val . '%');
+ }
+ );
+ }
+ );
+ }
+ );
+ }
- if ($fieldname == 'category') {
- $query->whereHas('model', function ($query) use ($search_val) {
- $query->whereHas('category', function ($query) use ($search_val) {
- $query->where(function ($query) use ($search_val) {
- $query->where('categories.name', 'LIKE', '%'.$search_val.'%')
- ->orWhere('models.name', 'LIKE', '%'.$search_val.'%')
- ->orWhere('models.model_number', 'LIKE', '%'.$search_val.'%');
- });
- });
- });
- }
+ if ($fieldname == 'category') {
+ $query->whereHas(
+ 'model', function ($query) use ($search_val) {
+ $query->whereHas(
+ 'category', function ($query) use ($search_val) {
+ $query->where(
+ function ($query) use ($search_val) {
+ $query->where('categories.name', 'LIKE', '%' . $search_val . '%')
+ ->orWhere('models.name', 'LIKE', '%' . $search_val . '%')
+ ->orWhere('models.model_number', 'LIKE', '%' . $search_val . '%');
+ }
+ );
+ }
+ );
+ }
+ );
+ }
- if ($fieldname == 'model') {
- $query->where(function ($query) use ($search_val) {
- $query->whereHas('model', function ($query) use ($search_val) {
- $query->where('models.name', 'LIKE', '%'.$search_val.'%');
- });
- });
- }
+ if ($fieldname == 'model') {
+ $query->where(
+ function ($query) use ($search_val) {
+ $query->whereHas(
+ 'model', function ($query) use ($search_val) {
+ $query->where('models.name', 'LIKE', '%' . $search_val . '%');
+ }
+ );
+ }
+ );
+ }
- if ($fieldname == 'model_number') {
- $query->where(function ($query) use ($search_val) {
- $query->whereHas('model', function ($query) use ($search_val) {
- $query->where('models.model_number', 'LIKE', '%'.$search_val.'%');
- });
- });
- }
+ if ($fieldname == 'model_number') {
+ $query->where(
+ function ($query) use ($search_val) {
+ $query->whereHas(
+ 'model', function ($query) use ($search_val) {
+ $query->where('models.model_number', 'LIKE', '%' . $search_val . '%');
+ }
+ );
+ }
+ );
+ }
- if ($fieldname == 'company') {
- $query->where(function ($query) use ($search_val) {
- $query->whereHas('company', function ($query) use ($search_val) {
- $query->where('companies.name', 'LIKE', '%'.$search_val.'%');
- });
- });
- }
+ if ($fieldname == 'company') {
+ $query->where(
+ function ($query) use ($search_val) {
+ $query->whereHas(
+ 'company', function ($query) use ($search_val) {
+ $query->where('companies.name', 'LIKE', '%' . $search_val . '%');
+ }
+ );
+ }
+ );
+ }
- if ($fieldname == 'supplier') {
- $query->where(function ($query) use ($search_val) {
- $query->whereHas('supplier', function ($query) use ($search_val) {
- $query->where('suppliers.name', 'LIKE', '%'.$search_val.'%');
- });
- });
- }
+ if ($fieldname == 'supplier') {
+ $query->where(
+ function ($query) use ($search_val) {
+ $query->whereHas(
+ 'supplier', function ($query) use ($search_val) {
+ $query->where('suppliers.name', 'LIKE', '%' . $search_val . '%');
+ }
+ );
+ }
+ );
+ }
- /**
- * THIS CLUNKY BIT IS VERY IMPORTANT
- *
- * Although inelegant, this section matters a lot when querying against fields that do not
- * exist on the asset table. There's probably a better way to do this moving forward, for
- * example using the Schema:: methods to determine whether or not a column actually exists,
- * or even just using the $searchableRelations variable earlier in this file.
- *
- * In short, this set of statements tells the query builder to ONLY query against an
- * actual field that's being passed if it doesn't meet known relational fields. This
- * allows us to query custom fields directly in the assets table
- * (regardless of their name) and *skip* any fields that we already know can only be
- * searched through relational searches that we do earlier in this method.
- *
- * For example, we do not store "location" as a field on the assets table, we store
- * that relationship through location_id on the assets table, therefore querying
- * assets.location would fail, as that field doesn't exist -- plus we're already searching
- * against those relationships earlier in this method.
- *
- * - snipe
- *
- */
-
- if (($fieldname!='category') && ($fieldname!='model_number') && ($fieldname!='rtd_location') && ($fieldname!='location') && ($fieldname!='supplier')
- && ($fieldname!='status_label') && ($fieldname!='assigned_to') && ($fieldname!='model') && ($fieldname!='company') && ($fieldname!='manufacturer')) {
- $query->where('assets.'.$fieldname, 'LIKE', '%' . $search_val . '%');
- }
+ /**
+ * THIS CLUNKY BIT IS VERY IMPORTANT
+ *
+ * Although inelegant, this section matters a lot when querying against fields that do not
+ * exist on the asset table. There's probably a better way to do this moving forward, for
+ * example using the Schema:: methods to determine whether or not a column actually exists,
+ * or even just using the $searchableRelations variable earlier in this file.
+ *
+ * In short, this set of statements tells the query builder to ONLY query against an
+ * actual field that's being passed if it doesn't meet known relational fields. This
+ * allows us to query custom fields directly in the assets table
+ * (regardless of their name) and *skip* any fields that we already know can only be
+ * searched through relational searches that we do earlier in this method.
+ *
+ * For example, we do not store "location" as a field on the assets table, we store
+ * that relationship through location_id on the assets table, therefore querying
+ * assets.location would fail, as that field doesn't exist -- plus we're already searching
+ * against those relationships earlier in this method.
+ *
+ * - snipe
+ */
+
+ if (($fieldname!= 'category') && ($fieldname != 'model_number') && ($fieldname != 'rtd_location') && ($fieldname != 'location') && ($fieldname != 'supplier')
+ && ($fieldname != 'status_label') && ($fieldname != 'assigned_to') && ($fieldname != 'model') && ($fieldname != 'company') && ($fieldname != 'manufacturer')
+ ) {
+ $query->where('assets.' . $fieldname, 'LIKE', '%' . $search_val . '%');
+ }
- }
+ }
- });
+ }
+ );
}
/**
- * Query builder scope to order on model
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope to order on model
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeOrderModels($query, $order)
{
return $query->join('models as asset_models', 'assets.model_id', '=', 'asset_models.id')->orderBy('asset_models.name', $order);
}
/**
- * Query builder scope to order on model number
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope to order on model number
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeOrderModelNumber($query, $order)
{
return $query->leftJoin('models as model_number_sort', 'assets.model_id', '=', 'model_number_sort.id')->orderBy('model_number_sort.model_number', $order);
@@ -1774,8 +1962,8 @@ public function scopeOrderModelNumber($query, $order)
/**
* Query builder scope to order on created_by name
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -1786,39 +1974,39 @@ public function scopeOrderByCreatedByName($query, $order)
/**
- * Query builder scope to order on assigned user
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope to order on assigned user
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeOrderAssigned($query, $order)
{
return $query->leftJoin('users as users_sort', 'assets.assigned_to', '=', 'users_sort.id')->select('assets.*')->orderBy('users_sort.first_name', $order)->orderBy('users_sort.last_name', $order);
}
/**
- * Query builder scope to order on status
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope to order on status
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeOrderStatus($query, $order)
{
return $query->join('status_labels as status_sort', 'assets.status_id', '=', 'status_sort.id')->orderBy('status_sort.name', $order);
}
/**
- * Query builder scope to order on company
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope to order on company
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeOrderCompany($query, $order)
{
return $query->leftJoin('companies as company_sort', 'assets.company_id', '=', 'company_sort.id')->orderBy('company_sort.name', $order);
@@ -1828,8 +2016,8 @@ public function scopeOrderCompany($query, $order)
/**
* Query builder scope to return results of a category
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -1837,34 +2025,33 @@ public function scopeInCategory($query, $category_id)
{
return $query->join('models as category_models', 'assets.model_id', '=', 'category_models.id')
->join('categories', 'category_models.category_id', '=', 'categories.id')
- ->whereIn('category_models.category_id', (!is_array($category_id) ? explode(',',$category_id): $category_id));
- //->whereIn('category_models.category_id', $category_id);
+ ->whereIn('category_models.category_id', (!is_array($category_id) ? explode(',', $category_id) : $category_id));
+ //->whereIn('category_models.category_id', $category_id);
}
/**
* Query builder scope to return results of a manufacturer
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeByManufacturer($query, $manufacturer_id)
{
return $query->join('models', 'assets.model_id', '=', 'models.id')
- ->join('manufacturers', 'models.manufacturer_id', '=', 'manufacturers.id')->whereIn('models.manufacturer_id', (!is_array($manufacturer_id) ? explode(',',$manufacturer_id): $manufacturer_id));
+ ->join('manufacturers', 'models.manufacturer_id', '=', 'manufacturers.id')->whereIn('models.manufacturer_id', (!is_array($manufacturer_id) ? explode(',', $manufacturer_id) : $manufacturer_id));
}
-
/**
- * Query builder scope to order on category
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ * Query builder scope to order on category
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeOrderCategory($query, $order)
{
return $query->join('models as order_model_category', 'assets.model_id', '=', 'order_model_category.id')
@@ -1876,8 +2063,8 @@ public function scopeOrderCategory($query, $order)
/**
* Query builder scope to order on manufacturer
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -1888,14 +2075,14 @@ public function scopeOrderManufacturer($query, $order)
->orderBy('manufacturer_order.name', $order);
}
- /**
- * Query builder scope to order on location
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
+ /**
+ * Query builder scope to order on location
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
public function scopeOrderLocation($query, $order)
{
return $query->leftJoin('locations as asset_locations', 'asset_locations.id', '=', 'assets.location_id')->orderBy('asset_locations.name', $order);
@@ -1903,8 +2090,9 @@ public function scopeOrderLocation($query, $order)
/**
* Query builder scope to order on default
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -1917,8 +2105,8 @@ public function scopeOrderRtdLocation($query, $order)
/**
* Query builder scope to order on supplier name
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -1927,29 +2115,47 @@ public function scopeOrderSupplier($query, $order)
return $query->leftJoin('suppliers as suppliers_assets', 'assets.supplier_id', '=', 'suppliers_assets.id')->orderBy('suppliers_assets.name', $order);
}
+ /**
+ * Query builder scope to order on supplier name
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
+ *
+ * @return \Illuminate\Database\Query\Builder Modified query builder
+ */
+ public function scopeOrderByJobTitle($query, $order)
+ {
+ return $query->leftJoin('users as users_sort', 'assets.assigned_to', '=', 'users_sort.id')->select('assets.*')->orderBy('users_sort.jobtitle', $order);
+ }
+
/**
* Query builder scope to search on location ID
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $search Search term
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $search Search term
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeByLocationId($query, $search)
{
- return $query->where(function ($query) use ($search) {
- $query->whereHas('location', function ($query) use ($search) {
- $query->where('locations.id', '=', $search);
- });
- });
+ return $query->where(
+ function ($query) use ($search) {
+ $query->whereHas(
+ 'location', function ($query) use ($search) {
+ $query->where('locations.id', '=', $search);
+ }
+ );
+ }
+ );
}
/**
* Query builder scope to search on depreciation name
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $search Search term
+ *
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $search Search term
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
diff --git a/app/Models/AssetMaintenance.php b/app/Models/AssetMaintenance.php
deleted file mode 100644
index 246220f5c7a3..000000000000
--- a/app/Models/AssetMaintenance.php
+++ /dev/null
@@ -1,275 +0,0 @@
- 'required|integer',
- 'supplier_id' => 'required|integer',
- 'asset_maintenance_type' => 'required',
- 'title' => 'required|max:100',
- 'is_warranty' => 'boolean',
- 'start_date' => 'required|date_format:Y-m-d',
- 'completion_date' => 'date_format:Y-m-d|nullable',
- 'notes' => 'string|nullable',
- 'cost' => 'numeric|nullable',
- ];
-
-
- /**
- * The attributes that are mass assignable.
- *
- * @var array
- */
- protected $fillable = [
- 'title',
- 'asset_id',
- 'supplier_id',
- 'asset_maintenance_type',
- 'is_warranty',
- 'start_date',
- 'completion_date',
- 'asset_maintenance_time',
- 'notes',
- 'cost',
- ];
-
- use Searchable;
-
- /**
- * The attributes that should be included when searching the model.
- *
- * @var array
- */
- protected $searchableAttributes =
- [
- 'title',
- 'notes',
- 'asset_maintenance_type',
- 'cost',
- 'start_date',
- 'completion_date'
- ];
-
- /**
- * The relations and their attributes that should be included when searching the model.
- *
- * @var array
- */
- protected $searchableRelations = [
- 'asset' => ['name', 'asset_tag', 'serial'],
- 'asset.model' => ['name', 'model_number'],
- 'asset.supplier' => ['name'],
- 'asset.assetstatus' => ['name'],
- 'supplier' => ['name'],
- ];
-
- public function getCompanyableParents()
- {
- return ['asset'];
- }
-
- /**
- * getImprovementOptions
- *
- * @return array
- * @author Vincent Sposato
- * @version v1.0
- */
- public static function getImprovementOptions()
- {
- return [
- trans('admin/asset_maintenances/general.maintenance') => trans('admin/asset_maintenances/general.maintenance'),
- trans('admin/asset_maintenances/general.repair') => trans('admin/asset_maintenances/general.repair'),
- trans('admin/asset_maintenances/general.upgrade') => trans('admin/asset_maintenances/general.upgrade'),
- trans('admin/asset_maintenances/general.pat_test') => trans('admin/asset_maintenances/general.pat_test'),
- trans('admin/asset_maintenances/general.calibration') => trans('admin/asset_maintenances/general.calibration'),
- trans('admin/asset_maintenances/general.software_support') => trans('admin/asset_maintenances/general.software_support'),
- trans('admin/asset_maintenances/general.hardware_support') => trans('admin/asset_maintenances/general.hardware_support'),
- trans('admin/asset_maintenances/general.configuration_change') => trans('admin/asset_maintenances/general.configuration_change'),
- ];
- }
-
- public function setIsWarrantyAttribute($value)
- {
- if ($value == '') {
- $value = 0;
- }
- $this->attributes['is_warranty'] = $value;
- }
-
- /**
- * @param $value
- */
- public function setCostAttribute($value)
- {
- $value = Helper::ParseCurrency($value);
- if ($value == 0) {
- $value = null;
- }
- $this->attributes['cost'] = $value;
- }
-
- /**
- * @param $value
- */
- public function setNotesAttribute($value)
- {
- if ($value == '') {
- $value = null;
- }
- $this->attributes['notes'] = $value;
- }
-
- /**
- * @param $value
- */
- public function setCompletionDateAttribute($value)
- {
- if ($value == '' || $value == '0000-00-00') {
- $value = null;
- }
- $this->attributes['completion_date'] = $value;
- }
-
- /**
- * asset
- * Get asset for this improvement
- *
- * @return mixed
- * @author Vincent Sposato
- * @version v1.0
- */
- public function asset()
- {
- return $this->belongsTo(\App\Models\Asset::class, 'asset_id')
- ->withTrashed();
- }
-
- /**
- * Get the admin who created the maintenance
- *
- * @return mixed
- * @author A. Gianotto
- * @version v3.0
- */
- public function adminuser()
- {
- return $this->belongsTo(\App\Models\User::class, 'created_by')
- ->withTrashed();
- }
-
- public function supplier()
- {
- return $this->belongsTo(\App\Models\Supplier::class, 'supplier_id')
- ->withTrashed();
- }
-
- /**
- * -----------------------------------------------
- * BEGIN QUERY SCOPES
- * -----------------------------------------------
- **/
-
- /**
- * Query builder scope to order on a supplier
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param string $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
- public function scopeOrderBySupplier($query, $order)
- {
- return $query->leftJoin('suppliers as suppliers_maintenances', 'asset_maintenances.supplier_id', '=', 'suppliers_maintenances.id')
- ->orderBy('suppliers_maintenances.name', $order);
- }
-
-
-
- /**
- * Query builder scope to order on asset tag
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param string $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
- public function scopeOrderByTag($query, $order)
- {
- return $query->leftJoin('assets', 'asset_maintenances.asset_id', '=', 'assets.id')
- ->orderBy('assets.asset_tag', $order);
- }
-
- /**
- * Query builder scope to order on asset tag
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param string $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
- public function scopeOrderByAssetName($query, $order)
- {
- return $query->leftJoin('assets', 'asset_maintenances.asset_id', '=', 'assets.id')
- ->orderBy('assets.name', $order);
- }
-
- /**
- * Query builder scope to order on serial
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param string $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
- public function scopeOrderByAssetSerial($query, $order)
- {
- return $query->leftJoin('assets', 'asset_maintenances.asset_id', '=', 'assets.id')
- ->orderBy('assets.serial', $order);
- }
-
- /**
- * Query builder scope to order on status label name
- *
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
- *
- * @return \Illuminate\Database\Query\Builder Modified query builder
- */
- public function scopeOrderStatusName($query, $order)
- {
- return $query->join('assets as maintained_asset', 'asset_maintenances.asset_id', '=', 'maintained_asset.id')
- ->leftjoin('status_labels as maintained_asset_status', 'maintained_asset_status.id', '=', 'maintained_asset.status_id')
- ->orderBy('maintained_asset_status.name', $order);
- }
-
- /**
- * Query builder scope to order on the user that created it
- */
- public function scopeOrderByCreatedBy($query, $order)
- {
- return $query->leftJoin('users as admin_sort', 'asset_maintenances.created_by', '=', 'admin_sort.id')->select('asset_maintenances.*')->orderBy('admin_sort.first_name', $order)->orderBy('admin_sort.last_name', $order);
- }
-}
diff --git a/app/Models/AssetModel.php b/app/Models/AssetModel.php
index 02b5df40d13b..52ecda86a04b 100755
--- a/app/Models/AssetModel.php
+++ b/app/Models/AssetModel.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -16,7 +17,7 @@
* Model for Asset Models. Asset Models contain higher level
* attributes that are common among the same type of asset.
*
- * @version v1.0
+ * @version v1.0
*/
class AssetModel extends SnipeModel
{
@@ -24,6 +25,7 @@ class AssetModel extends SnipeModel
use SoftDeletes;
use Loggable, Requestable, Presentable;
use TwoColumnUniqueUndeletedTrait;
+ use HasUploads;
/**
* Whether the model should inject its identifier to the unique
@@ -69,6 +71,7 @@ class AssetModel extends SnipeModel
'name',
'notes',
'requestable',
+ 'require_serial'
];
use Searchable;
@@ -96,15 +99,23 @@ class AssetModel extends SnipeModel
'manufacturer' => ['name'],
];
+ protected static function booted(): void
+ {
+ static::forceDeleted(function (AssetModel $assetModel) {
+ $assetModel->requests()->forceDelete();
+ });
-
+ static::softDeleted(function (AssetModel $assetModel) {
+ $assetModel->requests()->delete();
+ });
+ }
/**
* Establishes the model -> assets relationship
*
- * @author [A. Gianotto] []
- * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ *@since [v1.0]
+ * @author [A. Gianotto] []
*/
public function assets()
{
@@ -114,9 +125,9 @@ public function assets()
/**
* Establishes the model -> category relationship
*
- * @author [A. Gianotto] []
- * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ *@since [v1.0]
+ * @author [A. Gianotto] []
*/
public function category()
{
@@ -126,9 +137,9 @@ public function category()
/**
* Establishes the model -> depreciation relationship
*
- * @author [A. Gianotto] []
- * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v1.0]
+ * @author [A. Gianotto] []
*/
public function depreciation()
{
@@ -138,9 +149,9 @@ public function depreciation()
/**
* Establishes the model -> manufacturer relationship
*
- * @author [A. Gianotto] []
- * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v1.0]
+ * @author [A. Gianotto] []
*/
public function manufacturer()
{
@@ -150,40 +161,29 @@ public function manufacturer()
/**
* Establishes the model -> fieldset relationship
*
- * @author [A. Gianotto] []
- * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v2.0]
+ * @author [A. Gianotto] []
*/
public function fieldset()
{
+ // this is actually OK - we don't *need* to do this, but it's okay to make references from Model to fieldset
return $this->belongsTo(\App\Models\CustomFieldset::class, 'fieldset_id');
}
public function customFields()
{
- return $this->fieldset()->first()->fields();
- }
-
- /**
- * Establishes the model -> custom field default values relationship
- *
- * @author hannah tinkler
- * @since [v4.3]
- * @return \Illuminate\Database\Eloquent\Relations\Relation
- */
- public function defaultValues()
- {
- return $this->belongsToMany(\App\Models\CustomField::class, 'models_custom_fields')->withPivot('default_value');
+ return $this->fieldset()->first()->fields();
}
/**
* Gets the full url for the image
*
+ * @return \Illuminate\Database\Eloquent\Relations\Relation
+ *@author [A. Gianotto] []
+ * @since [v2.0]
* @todo this should probably be moved
*
- * @author [A. Gianotto] []
- * @since [v2.0]
- * @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function getImageUrl()
{
@@ -198,9 +198,9 @@ public function getImageUrl()
/**
* Checks if the model is deletable
*
- * @author A. Gianotto
- * @since [v6.3.4]
* @return bool
+ *@since [v6.3.4]
+ * @author A. Gianotto
*/
public function isDeletable()
{
@@ -209,32 +209,17 @@ public function isDeletable()
&& ($this->deleted_at == '');
}
- /**
- * Get uploads for this model
- *
- * @author [A. Gianotto] []
- * @since [v4.0]
- * @return \Illuminate\Database\Eloquent\Relations\Relation
- */
- public function uploads()
- {
- return $this->hasMany('\App\Models\Actionlog', 'item_id')
- ->where('item_type', '=', AssetModel::class)
- ->where('action_type', '=', 'uploaded')
- ->whereNotNull('filename')
- ->orderBy('created_at', 'desc');
- }
/**
* Get user who created the item
*
- * @author [A. Gianotto] []
- * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
+ * @since [v1.0]
+ * @author [A. Gianotto] []
*/
public function adminuser()
{
- return $this->belongsTo(\App\Models\User::class, 'created_by');
+ return $this->belongsTo(\App\Models\User::class, 'created_by')->withTrashed();
}
@@ -248,10 +233,10 @@ public function adminuser()
* scopeInCategory
* Get all models that are in the array of category ids
*
- * @param $query
+ * @param $query
* @param array $categoryIdListing
*
- * @return mixed
+ * @return mixed
* @author Vincent Sposato
* @version v1.0
*/
@@ -264,9 +249,9 @@ public function scopeInCategory($query, array $categoryIdListing)
* scopeRequestable
* Get all models that are requestable by a user.
*
- * @param $query
+ * @param $query
*
- * @return $query
+ * @return $query
* @author Daniel Meltzer
* @version v3.5
*/
@@ -278,8 +263,8 @@ public function scopeRequestableModels($query)
/**
* Query builder scope to search on text, including catgeory and manufacturer name
*
- * @param Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $search Search term
+ * @param Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
@@ -287,23 +272,31 @@ public function scopeSearchByManufacturerOrCat($query, $search)
{
return $query->where('models.name', 'LIKE', "%$search%")
->orWhere('model_number', 'LIKE', "%$search%")
- ->orWhere(function ($query) use ($search) {
- $query->whereHas('category', function ($query) use ($search) {
- $query->where('categories.name', 'LIKE', '%'.$search.'%');
- });
- })
- ->orWhere(function ($query) use ($search) {
- $query->whereHas('manufacturer', function ($query) use ($search) {
- $query->where('manufacturers.name', 'LIKE', '%'.$search.'%');
- });
- });
+ ->orWhere(
+ function ($query) use ($search) {
+ $query->whereHas(
+ 'category', function ($query) use ($search) {
+ $query->where('categories.name', 'LIKE', '%' . $search . '%');
+ }
+ );
+ }
+ )
+ ->orWhere(
+ function ($query) use ($search) {
+ $query->whereHas(
+ 'manufacturer', function ($query) use ($search) {
+ $query->where('manufacturers.name', 'LIKE', '%' . $search . '%');
+ }
+ );
+ }
+ );
}
/**
* Query builder scope to order on manufacturer
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -315,8 +308,8 @@ public function scopeOrderManufacturer($query, $order)
/**
* Query builder scope to order on category name
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param text $order Order
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param text $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -332,7 +325,6 @@ public function scopeOrderFieldset($query, $order)
/**
* Query builder scope to order on created_by name
- *
*/
public function scopeOrderByCreatedByName($query, $order)
{
diff --git a/app/Models/Category.php b/app/Models/Category.php
index cfa83328ab2e..7d9d7a14aead 100755
--- a/app/Models/Category.php
+++ b/app/Models/Category.php
@@ -18,7 +18,7 @@
* to require acceptance from the user, whether or not to
* send a EULA to the user, etc.
*
- * @version v1.0
+ * @version v1.0
*/
class Category extends SnipeModel
{
@@ -32,6 +32,7 @@ class Category extends SnipeModel
protected $hidden = ['created_by', 'deleted_at'];
protected $casts = [
+ 'alert_on_response' => 'boolean',
'created_by' => 'integer',
];
@@ -69,6 +70,7 @@ class Category extends SnipeModel
'eula_text',
'name',
'require_acceptance',
+ 'alert_on_response',
'use_default_eula',
'created_by',
'notes',
@@ -94,12 +96,20 @@ class Category extends SnipeModel
* Checks if category can be deleted
*
* @author [Dan Meltzer] []
- * @since [v5.0]
+ * @since [v5.0]
* @return bool
*/
public function isDeletable()
{
+ // We have to check for models as well if the category type is asset
+ if ($this->category_type == 'asset') {
+ return Gate::allows('delete', $this)
+ && ($this->itemCount() == 0)
+ && ($this->models_count == 0)
+ && ($this->deleted_at == '');
+ }
+
return Gate::allows('delete', $this)
&& ($this->itemCount() == 0)
&& ($this->deleted_at == '');
@@ -109,7 +119,7 @@ public function isDeletable()
* Establishes the category -> accessories relationship
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function accessories()
@@ -121,7 +131,7 @@ public function accessories()
* Establishes the category -> licenses relationship
*
* @author [A. Gianotto] []
- * @since [v4.3]
+ * @since [v4.3]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function licenses()
@@ -133,7 +143,7 @@ public function licenses()
* Establishes the category -> consumables relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function consumables()
@@ -145,7 +155,7 @@ public function consumables()
* Establishes the category -> consumables relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function components()
@@ -160,7 +170,7 @@ public function components()
* It should only be used in a single category context.
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return int
*/
public function itemCount()
@@ -171,18 +181,18 @@ public function itemCount()
}
switch ($this->category_type) {
- case 'asset':
- return $this->assets->count();
- case 'accessory':
- return $this->accessories->count();
- case 'component':
- return $this->components->count();
- case 'consumable':
- return $this->consumables->count();
- case 'license':
- return $this->licenses->count();
- default:
- return 0;
+ case 'asset':
+ return $this->assets->count();
+ case 'accessory':
+ return $this->accessories->count();
+ case 'component':
+ return $this->components->count();
+ case 'consumable':
+ return $this->consumables->count();
+ case 'license':
+ return $this->licenses->count();
+ default:
+ return 0;
}
}
@@ -191,7 +201,7 @@ public function itemCount()
* Establishes the category -> assets relationship
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assets()
@@ -208,8 +218,8 @@ public function assets()
* by their category.
*
* @author [A. Gianotto] []
- * @since [v6.1.0]
- * @see \App\Models\Asset::scopeAssetsForShow()
+ * @since [v6.1.0]
+ * @see \App\Models\Asset::scopeAssetsForShow()
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function showableAssets()
@@ -221,7 +231,7 @@ public function showableAssets()
* Establishes the category -> models relationship
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function models()
@@ -239,7 +249,7 @@ public function adminuser()
* checks for a settings level EULA
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return string | null
*/
public function getEula()
@@ -266,7 +276,7 @@ public function getEula()
*
* This will also correctly parse a 1/0 if "true"/"false" is passed.
*
- * @param $value
+ * @param $value
* @return void
*/
public function setCheckinEmailAttribute($value)
@@ -283,9 +293,9 @@ public function setCheckinEmailAttribute($value)
/**
* Query builder scope for whether or not the category requires acceptance
*
- * @author Vincent Sposato
+ * @author Vincent Sposato
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeRequiresAcceptance($query)
diff --git a/app/Models/CheckoutAcceptance.php b/app/Models/CheckoutAcceptance.php
index e44a330ebc91..f65fb219a88a 100644
--- a/app/Models/CheckoutAcceptance.php
+++ b/app/Models/CheckoutAcceptance.php
@@ -15,6 +15,7 @@ class CheckoutAcceptance extends Model
protected $casts = [
'accepted_at' => 'datetime',
'declined_at' => 'datetime',
+ 'alert_on_response_id' => 'integer',
];
/**
@@ -31,7 +32,19 @@ public function routeNotificationForMail()
return array_filter($recipients);
}
-
+ public function getCheckoutableItemTypeAttribute(): string
+ {
+ $type = $this->checkoutable_type;
+
+ return match ($type) {
+ Asset::class => trans('general.asset'),
+ LicenseSeat::class => trans('general.license'),
+ Accessory::class => trans('general.accessory'),
+ Component::class => trans('general.component'),
+ Consumable::class => trans('general.consumable'),
+ default => class_basename($type),
+ };
+ }
/**
* The resource that was is out
*
@@ -65,7 +78,7 @@ public function isPending()
/**
* Was the checkoutable checked out to this user?
*
- * @param User $user
+ * @param User $user
* @return bool
*/
public function isCheckedOutTo(User $user)
@@ -78,7 +91,7 @@ public function isCheckedOutTo(User $user)
* Do not add stuff here that doesn't have a corresponding column in the
* checkout_acceptances table or you'll get an error.
*
- * @param string $signature_filename
+ * @param string $signature_filename
*/
public function accept($signature_filename, $eula = null, $filename = null, $note = null)
{
@@ -98,7 +111,7 @@ public function accept($signature_filename, $eula = null, $filename = null, $not
/**
* Decline the checkout acceptance
*
- * @param string $signature_filename
+ * @param string $signature_filename
*/
public function decline($signature_filename, $note = null)
{
@@ -115,8 +128,9 @@ public function decline($signature_filename, $note = null)
/**
* Filter checkout acceptences by the user
+ *
* @param Illuminate\Database\Eloquent\Builder $query
- * @param User $user
+ * @param User $user
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeForUser(Builder $query, User $user)
@@ -126,6 +140,7 @@ public function scopeForUser(Builder $query, User $user)
/**
* Filter to only get pending acceptances
+ *
* @param Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
diff --git a/app/Models/CheckoutRequest.php b/app/Models/CheckoutRequest.php
index d6a85f297233..42512d8fda23 100644
--- a/app/Models/CheckoutRequest.php
+++ b/app/Models/CheckoutRequest.php
@@ -2,11 +2,13 @@
namespace App\Models;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class CheckoutRequest extends Model
{
+ use HasFactory;
use SoftDeletes;
protected $fillable = ['user_id'];
protected $table = 'checkout_requests';
diff --git a/app/Models/Company.php b/app/Models/Company.php
index 8886da77f6c0..72a12aebc66d 100644
--- a/app/Models/Company.php
+++ b/app/Models/Company.php
@@ -2,22 +2,26 @@
namespace App\Models;
+use App\Models\Traits\CompanyableTrait;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
-use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
-use Watson\Validating\ValidatingTrait;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
+use Watson\Validating\ValidatingTrait;
+
/**
* Model for Companies.
*
- * @version v1.8
+ * @version v1.8
*/
final class Company extends SnipeModel
{
use HasFactory;
+ use CompanyableTrait;
+
protected $table = 'companies';
@@ -26,19 +30,19 @@ final class Company extends SnipeModel
'name' => 'required|min:1|max:255|unique:companies,name',
'fax' => 'min:7|max:35|nullable',
'phone' => 'min:7|max:35|nullable',
- 'email' => 'email|max:150|nullable',
+ 'email' => 'email|max:150|nullable',
];
protected $presenter = \App\Presenters\CompanyPresenter::class;
use Presentable;
/**
- * Whether the model should inject it's identifier to the unique
- * validation rules before attempting validation. If this property
- * is not set in the model it will default to true.
- *
+ * Whether the model should inject it's identifier to the unique
+ * validation rules before attempting validation. If this property
+ * is not set in the model it will default to true.
+ *
* @var bool
- */
+ */
protected $injectUniqueIdentifier = true;
use ValidatingTrait;
use Searchable;
@@ -67,7 +71,8 @@ final class Company extends SnipeModel
'phone',
'fax',
'email',
- 'created_by'
+ 'created_by',
+ 'notes',
];
private static function isFullMultipleCompanySupportEnabled()
@@ -99,7 +104,7 @@ public static function getIdFromInput($unescaped_input)
* account the full multiple company support setting
* and if the current user is a super user.
*
- * @param $unescaped_input
+ * @param $unescaped_input
* @return int|mixed|string|null
*/
public static function getIdForCurrentUser($unescaped_input)
@@ -126,7 +131,7 @@ public static function getIdForCurrentUser($unescaped_input)
* Check to see if the current user should have access to the model.
* I hate this method and I think it should be refactored.
*
- * @param $companyable
+ * @param $companyable
* @return bool|void
*/
public static function isCurrentUserHasAccess($companyable)
@@ -145,10 +150,10 @@ public static function isCurrentUserHasAccess($companyable)
if (!is_string($companyable)) {
$company_table = $companyable->getModel()->getTable();
try {
- // This is primary for the gate:allows-check in location->isDeletable()
+ // This is primarily for the gate:allows-check in location->isDeletable()
// Locations don't have a company_id so without this it isn't possible to delete locations with FullMultipleCompanySupport enabled
// because this function is called by SnipePermissionsPolicy->before()
- if (!$companyable instanceof Company && !Schema::hasColumn($company_table, 'company_id')) {
+ if (!Schema::hasColumn($company_table, 'company_id')) {
return true;
}
@@ -159,12 +164,19 @@ public static function isCurrentUserHasAccess($companyable)
if (auth()->user()) {
- Log::warning('Companyable is '.$companyable);
+ // Log::warning('Companyable is '.$companyable);
$current_user_company_id = auth()->user()->company_id;
$companyable_company_id = $companyable->company_id;
- return $current_user_company_id == null || $current_user_company_id == $companyable_company_id || auth()->user()->isSuperUser();
+
+ // Set this to check companyable on company
+ if ($companyable instanceof Company) {
+ $companyable_company_id = $companyable->id;
+ }
+ return ($current_user_company_id == null) || ($current_user_company_id == $companyable_company_id) || auth()->user()->isSuperUser();
}
+ return false;
+
}
public static function isCurrentUserAuthorized()
@@ -182,7 +194,7 @@ public static function canManageUsersCompanies()
* Checks if company can be deleted
*
* @author [Dan Meltzer] []
- * @since [v5.0]
+ * @since [v5.0]
* @return bool
*/
public function isDeletable()
@@ -199,7 +211,7 @@ public function isDeletable()
}
/**
- * @param $unescaped_input
+ * @param $unescaped_input
* @return int|mixed|string|null
*/
public static function getIdForUser($unescaped_input)
@@ -255,14 +267,14 @@ public function components()
* @todo - refactor that trait to handle the user's model as well.
*
* @author [A. Gianotto]
- * @param $query
- * @param $column
- * @param $table_name
+ * @param $query
+ * @param $column
+ * @param $table_name
* @return mixed
*/
public static function scopeCompanyables($query, $column = 'company_id', $table_name = null)
{
- // If not logged in and hitting this, assume we are on the command line and don't scope?'
+ // If not logged in and hitting this, assume we are on the command line and don't scope?
if (! static::isFullMultipleCompanySupportEnabled() || (Auth::hasUser() && auth()->user()->isSuperUser()) || (! Auth::hasUser())) {
return $query;
} else {
@@ -279,11 +291,16 @@ public static function scopeCompanyables($query, $column = 'company_id', $table_
private static function scopeCompanyablesDirectly($query, $column = 'company_id', $table_name = null)
{
+ $company_id = null;
// Get the company ID of the logged-in user, or set it to null if there is no company associated with the user
if (Auth::hasUser()) {
$company_id = auth()->user()->company_id;
- } else {
- $company_id = null;
+ }
+
+
+ // If we are scoping the companies table itself, look for the company.id
+ if ($query->getModel()->getTable() == 'companies') {
+ return $query->where('companies.id', '=', $company_id);
}
@@ -296,6 +313,8 @@ private static function scopeCompanyablesDirectly($query, $column = 'company_id'
return $query->where($table.$column, '=', $company_id);
}
+
+
}
public function adminuser()
@@ -310,8 +329,8 @@ public function adminuser()
* This gets invoked by CompanyableChildScope, but I'm not sure what it does.
*
* @author [A. Gianotto]
- * @param array $companyable_names
- * @param $query
+ * @param array $companyable_names
+ * @param $query
* @return mixed
*/
public static function scopeCompanyableChildren(array $companyable_names, $query)
@@ -323,17 +342,18 @@ public static function scopeCompanyableChildren(array $companyable_names, $query
return $query;
} else {
$f = function ($q) {
- Log::debug('scopeCompanyablesDirectly firing ');
static::scopeCompanyablesDirectly($q);
};
- $q = $query->where(function ($q) use ($companyable_names, $f) {
- $q2 = $q->whereHas($companyable_names[0], $f);
+ $q = $query->where(
+ function ($q) use ($companyable_names, $f) {
+ $q2 = $q->whereHas($companyable_names[0], $f);
- for ($i = 1; $i < count($companyable_names); $i++) {
- $q2 = $q2->orWhereHas($companyable_names[$i], $f);
+ for ($i = 1; $i < count($companyable_names); $i++) {
+ $q2 = $q2->orWhereHas($companyable_names[$i], $f);
+ }
}
- });
+ );
return $q;
}
diff --git a/app/Models/CompanyableChildScope.php b/app/Models/CompanyableChildScope.php
index 4077ebd596bf..35f2049e8bad 100644
--- a/app/Models/CompanyableChildScope.php
+++ b/app/Models/CompanyableChildScope.php
@@ -9,15 +9,15 @@
/**
* Handle query scoping for full company support.
*
- * @todo Move this to a more Laravel 5.2 esque way
- * @version v1.0
+ * @todo Move this to a more Laravel 5.2 esque way
+ * @version v1.0
*/
final class CompanyableChildScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*
- * @param \Illuminate\Database\Eloquent\Builder $builder
+ * @param \Illuminate\Database\Eloquent\Builder $builder
* @return void
*/
public function apply(Builder $builder, Model $model)
@@ -31,7 +31,7 @@ public function apply(Builder $builder, Model $model)
* @todo IMPLEMENT
* Remove the scope from the given Eloquent query builder.
*
- * @param \Illuminate\Database\Eloquent\Builder $builder
+ * @param \Illuminate\Database\Eloquent\Builder $builder
* @return void
*/
public function remove(Builder $builder)
diff --git a/app/Models/CompanyableScope.php b/app/Models/CompanyableScope.php
index 4bbe7d63963f..bbc0b5e0e678 100644
--- a/app/Models/CompanyableScope.php
+++ b/app/Models/CompanyableScope.php
@@ -9,15 +9,15 @@
/**
* Handle query scoping for full company support.
*
- * @todo Move this to a more Laravel 5.2 esque way
- * @version v1.0
+ * @todo Move this to a more Laravel 5.2 esque way
+ * @version v1.0
*/
final class CompanyableScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*
- * @param \Illuminate\Database\Eloquent\Builder $builder
+ * @param \Illuminate\Database\Eloquent\Builder $builder
* @return void
*/
public function apply(Builder $builder, Model $model)
@@ -29,7 +29,7 @@ public function apply(Builder $builder, Model $model)
* @todo IMPLEMENT
* Remove the scope from the given Eloquent query builder.
*
- * @param \Illuminate\Database\Eloquent\Builder $builder
+ * @param \Illuminate\Database\Eloquent\Builder $builder
* @return void
*/
public function remove(Builder $builder)
diff --git a/app/Models/CompanyableTrait.php b/app/Models/CompanyableTrait.php
deleted file mode 100644
index 04a620d8e3d5..000000000000
--- a/app/Models/CompanyableTrait.php
+++ /dev/null
@@ -1,18 +0,0 @@
- 'required|integer|exists:categories,id',
'supplier_id' => 'nullable|integer|exists:suppliers,id',
'company_id' => 'integer|nullable|exists:companies,id',
+ 'location_id' => 'exists:locations,id|nullable|fmcs_location',
'min_amt' => 'integer|min:0|nullable',
'purchase_date' => 'date_format:Y-m-d|nullable',
- 'purchase_cost' => 'numeric|nullable|gte:0|max:9999999999999',
+ 'purchase_cost' => 'numeric|nullable|gte:0|max:99999999999999999.99',
'manufacturer_id' => 'integer|exists:manufacturers,id|nullable',
];
@@ -104,28 +110,20 @@ class Component extends SnipeModel
];
- /**
- * Establishes the components -> action logs -> uploads relationship
- *
- * @author A. Gianotto
- * @since [v6.1.13]
- * @return \Illuminate\Database\Eloquent\Relations\Relation
- */
- public function uploads()
+ public function isDeletable()
{
- return $this->hasMany(\App\Models\Actionlog::class, 'item_id')
- ->where('item_type', '=', self::class)
- ->where('action_type', '=', 'uploaded')
- ->whereNotNull('filename')
- ->orderBy('created_at', 'desc');
+ return Gate::allows('delete', $this)
+ && ($this->numCheckedOut() === 0)
+ && ($this->deleted_at == '');
}
+
/**
* Establishes the component -> location relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function location()
@@ -137,7 +135,7 @@ public function location()
* Establishes the component -> assets relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assets()
@@ -151,7 +149,7 @@ public function assets()
* @todo this is probably not needed - refactor
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
@@ -163,7 +161,7 @@ public function adminuser()
* Establishes the component -> company relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function company()
@@ -175,7 +173,7 @@ public function company()
* Establishes the component -> category relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function category()
@@ -187,7 +185,7 @@ public function category()
* Establishes the item -> supplier relationship
*
* @author [A. Gianotto] []
- * @since [v6.1.1]
+ * @since [v6.1.1]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function supplier()
@@ -200,19 +198,49 @@ public function supplier()
* Establishes the item -> manufacturer relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function manufacturer()
{
return $this->belongsTo(\App\Models\Manufacturer::class, 'manufacturer_id');
}
+ /**
+ * Determine whether this asset requires acceptance by the assigned user
+ *
+ * @author [A. Gianotto] []
+ * @since [v4.0]
+ * @return bool
+ */
+ public function requireAcceptance()
+ {
+ return $this->category->require_acceptance;
+ }
+
+ /**
+ * Checks for a category-specific EULA, and if that doesn't exist,
+ * checks for a settings level EULA
+ *
+ * @author [A. Gianotto] []
+ * @since [v4.0]
+ * @return string | false
+ */
+ public function getEula()
+ {
+ if ($this->category->eula_text) {
+ return Helper::parseEscapedMarkedown($this->category->eula_text);
+ } elseif ((Setting::getSettings()->default_eula_text) && ($this->category->use_default_eula == '1')) {
+ return Helper::parseEscapedMarkedown(Setting::getSettings()->default_eula_text);
+ } else {
+ return null;
+ }
+ }
/**
* Establishes the component -> action logs relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assetlog()
@@ -224,7 +252,7 @@ public function assetlog()
* Check how many items within a component are checked out
*
* @author [A. Gianotto] []
- * @since [v5.0]
+ * @since [v5.0]
* @return int
*/
public function numCheckedOut()
@@ -234,18 +262,43 @@ public function numCheckedOut()
// In case there are elements checked out to assets that belong to a different company
// than this asset and full multiple company support is on we'll remove the global scope,
// so they are included in the count.
- foreach ($this->assets()->withoutGlobalScope(new CompanyableScope)->get() as $checkout) {
- $checkedout += $checkout->pivot->assigned_qty;
- }
+ return $this->uncontrainedAssets->sum('pivot.assigned_qty');
+ }
+
+
+ /**
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
+ *
+ * This allows us to get the assets with assigned components without the company restriction
+ */
+ public function uncontrainedAssets()
+ {
+
+ return $this->belongsToMany(\App\Models\Asset::class, 'components_assets')
+ ->withPivot('id', 'assigned_qty', 'created_at', 'created_by', 'note')
+ ->withoutGlobalScope(new CompanyableScope);
- return $checkedout;
}
+ /**
+ * Determine whether to send a checkin/checkout email based on
+ * asset model category
+ *
+ * @author [A. Gianotto] []
+ * @since [v4.0]
+ * @return bool
+ */
+ public function checkin_email()
+ {
+ return $this->category?->checkin_email;
+ }
+
+
/**
* Check how many items within a component are remaining
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return int
*/
public function numRemaining()
@@ -268,8 +321,8 @@ public function numRemaining()
* This simply checks that there is a value for quantity, and if there isn't, set it to 0.
*
* @author A. Gianotto