diff --git a/.all-contributorsrc b/.all-contributorsrc
index 22cecb3852..088a20081e 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",
@@ -3328,7 +3319,7 @@
},
{
"login": "36864",
- "name": 36864,
+ "name": "36864",
"avatar_url": "https://avatars.githubusercontent.com/u/109086466?v=4",
"profile": "https://github.com/36864",
"contributions": [
@@ -3361,6 +3352,888 @@
"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.docker b/.env.docker
index 88b65b8f79..9eae34385e 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
@@ -168,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 81de24ead1..6e423d4fe2 100644
--- a/.env.example
+++ b/.env.example
@@ -24,6 +24,7 @@ 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
@@ -174,6 +175,7 @@ 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
@@ -191,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 29f3e1b1f1..007d07d37a 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 0788d6eaa2..fee3bab64c 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'
@@ -32,11 +32,11 @@ jobs:
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 7b9331c97d..b2e798d562 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
new file mode 100644
index 0000000000..a65d48d2e2
--- /dev/null
+++ b/.github/workflows/docker-alpine.yml
@@ -0,0 +1,86 @@
+# Snipe-IT (Alpine) Docker image build for hub.docker.com
+name: Docker images (Alpine)
+
+# 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) }},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
+ # 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.alpine
+ 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-arm.yml b/.github/workflows/docker-arm.yml
deleted file mode 100644
index 9d883b54bf..0000000000
--- a/.github/workflows/docker-arm.yml
+++ /dev/null
@@ -1,151 +0,0 @@
-# Snipe-IT Docker image build for hub.docker.com
-name: Docker ARM64 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-ubuntu-arm:
- # 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-24.04-arm
- 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 'grokability/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: grokability/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/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 }}
- docker-alpine-arm:
- # 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-24.04-arm
- 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) }},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
- # 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 'grokability/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: grokability/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.alpine
- platforms: 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-intel.yml b/.github/workflows/docker-intel.yml
deleted file mode 100644
index d025c2a18f..0000000000
--- a/.github/workflows/docker-intel.yml
+++ /dev/null
@@ -1,151 +0,0 @@
-# Snipe-IT Docker image build for hub.docker.com
-name: Docker Intel/amd64 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-ubuntu-intel:
- # 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@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 'grokability/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: grokability/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
- # 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 }}
- docker-alpine-intel:
- # 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) }},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
- # 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 'grokability/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: grokability/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.alpine
- platforms: linux/amd64
- # 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-ubuntu.yml b/.github/workflows/docker-ubuntu.yml
new file mode 100644
index 0000000000..c345105b7e
--- /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/dockerhub-description.yml b/.github/workflows/dockerhub-description.yml
index f9064dec95..8b5782339b 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 16190900b0..237220b337 100644
--- a/.github/workflows/tests-mysql.yml
+++ b/.github/workflows/tests-mysql.yml
@@ -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
diff --git a/.github/workflows/tests-postgres.yml b/.github/workflows/tests-postgres.yml
index d111fc87a5..97379ec2bc 100644
--- a/.github/workflows/tests-postgres.yml
+++ b/.github/workflows/tests-postgres.yml
@@ -34,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
diff --git a/.github/workflows/tests-sqlite.yml b/.github/workflows/tests-sqlite.yml
index cd06805e01..fdf4ea2ce9 100644
--- a/.github/workflows/tests-sqlite.yml
+++ b/.github/workflows/tests-sqlite.yml
@@ -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
diff --git a/.pa11yci.json b/.pa11yci.json
new file mode 100644
index 0000000000..73b84a8140
--- /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/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 122382894e..c2011a5423 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -1,60 +1,74 @@
Thanks goes to all of these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)) who have helped Snipe-IT get this far:
-| [
snipe](http://www.snipe.net)
[💻](https://github.com/grokability/snipe-it/commits?author=snipe "Code") [🚇](#infra-snipe "Infrastructure (Hosting, Build-Tools, etc)") [📖](https://github.com/grokability/snipe-it/commits?author=snipe "Documentation") [⚠️](https://github.com/grokability/snipe-it/commits?author=snipe "Tests") [🐛](https://github.com/grokability/snipe-it/issues?q=author%3Asnipe "Bug reports") [🎨](#design-snipe "Design") [👀](#review-snipe "Reviewed Pull Requests") | [
Brady Wetherington](http://www.uberbrady.com)
[💻](https://github.com/grokability/snipe-it/commits?author=uberbrady "Code") [📖](https://github.com/grokability/snipe-it/commits?author=uberbrady "Documentation") [🚇](#infra-uberbrady "Infrastructure (Hosting, Build-Tools, etc)") [👀](#review-uberbrady "Reviewed Pull Requests") | [
Daniel Meltzer](https://github.com/dmeltzer)
[💻](https://github.com/grokability/snipe-it/commits?author=dmeltzer "Code") [⚠️](https://github.com/grokability/snipe-it/commits?author=dmeltzer "Tests") [📖](https://github.com/grokability/snipe-it/commits?author=dmeltzer "Documentation") | [
Michael T](http://www.tuckertechonline.com)
[💻](https://github.com/grokability/snipe-it/commits?author=mtucker6784 "Code") | [
madd15](https://github.com/madd15)
[📖](https://github.com/grokability/snipe-it/commits?author=madd15 "Documentation") [💬](#question-madd15 "Answering Questions") | [
Vincent Sposato](https://github.com/vsposato)
[💻](https://github.com/grokability/snipe-it/commits?author=vsposato "Code") | [
Andrea Bergamasco](https://github.com/vjandrea)
[💻](https://github.com/grokability/snipe-it/commits?author=vjandrea "Code") |
-| :---: | :---: | :---: |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| :---: |
-| [
Karol](https://github.com/kpawelski)
[🌍](#translation-kpawelski "Translation") [💻](https://github.com/grokability/snipe-it/commits?author=kpawelski "Code") | [
morph027](http://blog.morph027.de/)
[💻](https://github.com/grokability/snipe-it/commits?author=morph027 "Code") | [
fvleminckx](https://github.com/fvleminckx)
[🚇](#infra-fvleminckx "Infrastructure (Hosting, Build-Tools, etc)") | [
itsupportcmsukorg](https://github.com/itsupportcmsukorg)
[💻](https://github.com/grokability/snipe-it/commits?author=itsupportcmsukorg "Code") [🐛](https://github.com/grokability/snipe-it/issues?q=author%3Aitsupportcmsukorg "Bug reports") | [
Frank](https://override.io)
[💻](https://github.com/grokability/snipe-it/commits?author=base-zero "Code") | [
Deleted user](https://github.com/ghost)
[🌍](#translation-ghost "Translation") [💻](https://github.com/grokability/snipe-it/commits?author=ghost "Code") | [
tiagom62](https://github.com/tiagom62)
[💻](https://github.com/grokability/snipe-it/commits?author=tiagom62 "Code") [🚇](#infra-tiagom62 "Infrastructure (Hosting, Build-Tools, etc)") |
-| [
Ryan Stafford](https://github.com/rystaf)
[💻](https://github.com/grokability/snipe-it/commits?author=rystaf "Code") | [
Eammon Hanlon](https://github.com/ehanlon)
[💻](https://github.com/grokability/snipe-it/commits?author=ehanlon "Code") | [
zjean](https://github.com/zjean)
[💻](https://github.com/grokability/snipe-it/commits?author=zjean "Code") | [
Matthias Frei](http://www.frei.media)
[💻](https://github.com/grokability/snipe-it/commits?author=FREImedia "Code") | [
opsydev](https://github.com/opsydev)
[💻](https://github.com/grokability/snipe-it/commits?author=opsydev "Code") | [
Daniel Dreier](http://www.ddreier.com)
[💻](https://github.com/grokability/snipe-it/commits?author=ddreier "Code") | [
Nikolai Prokoschenko](http://rassie.org)
[💻](https://github.com/grokability/snipe-it/commits?author=rassie "Code") |
-| [
Drew](https://github.com/YetAnotherCodeMonkey)
[💻](https://github.com/grokability/snipe-it/commits?author=YetAnotherCodeMonkey "Code") | [
Walter](https://github.com/merid14)
[💻](https://github.com/grokability/snipe-it/commits?author=merid14 "Code") | [
Petr Baloun](https://github.com/balous)
[💻](https://github.com/grokability/snipe-it/commits?author=balous "Code") | [
reidblomquist](https://github.com/reidblomquist)
[📖](https://github.com/grokability/snipe-it/commits?author=reidblomquist "Documentation") | [
Mathieu Kooiman](https://github.com/mathieuk)
[💻](https://github.com/grokability/snipe-it/commits?author=mathieuk "Code") | [
csayre](https://github.com/csayre)
[📖](https://github.com/grokability/snipe-it/commits?author=csayre "Documentation") | [
Adam Dunson](https://github.com/adamdunson)
[💻](https://github.com/grokability/snipe-it/commits?author=adamdunson "Code") |
-| [
Hereward](https://github.com/thehereward)
[💻](https://github.com/grokability/snipe-it/commits?author=thehereward "Code") | [
swoopdk](https://github.com/swoopdk)
[💻](https://github.com/grokability/snipe-it/commits?author=swoopdk "Code") | [
Abdullah Alansari](https://linkedin.com/in/ahimta)
[💻](https://github.com/grokability/snipe-it/commits?author=Ahimta "Code") | [
Micael Rodrigues](https://github.com/MicaelRodrigues)
[💻](https://github.com/grokability/snipe-it/commits?author=MicaelRodrigues "Code") | [
Patrick Gallagher](http://macadmincorner.com)
[📖](https://github.com/grokability/snipe-it/commits?author=patgmac "Documentation") | [
Miliamber](https://github.com/Miliamber)
[💻](https://github.com/grokability/snipe-it/commits?author=Miliamber "Code") | [
hawk554](https://github.com/hawk554)
[💻](https://github.com/grokability/snipe-it/commits?author=hawk554 "Code") |
-| [
Justin Kerr](http://jbirdkerr.net)
[💻](https://github.com/grokability/snipe-it/commits?author=jbirdkerr "Code") | [
Ira W. Snyder](http://www.irasnyder.com/devel/)
[📖](https://github.com/grokability/snipe-it/commits?author=irasnyd "Documentation") | [
Aladin Alaily](https://github.com/aalaily)
[💻](https://github.com/grokability/snipe-it/commits?author=aalaily "Code") | [
Chase Hansen](https://github.com/kobie-chasehansen)
[💻](https://github.com/grokability/snipe-it/commits?author=kobie-chasehansen "Code") [💬](#question-kobie-chasehansen "Answering Questions") [🐛](https://github.com/grokability/snipe-it/issues?q=author%3Akobie-chasehansen "Bug reports") | [
IDM Helpdesk](https://github.com/IDM-Helpdesk)
[💻](https://github.com/grokability/snipe-it/commits?author=IDM-Helpdesk "Code") | [
Kai](http://balticer.de)
[💻](https://github.com/grokability/snipe-it/commits?author=balticer "Code") | [
Michael Daniels](http://www.michaeldaniels.me)
[💻](https://github.com/grokability/snipe-it/commits?author=mdaniels5757 "Code") |
-| [
Tom Castleman](http://tomcastleman.me)
[💻](https://github.com/grokability/snipe-it/commits?author=tomcastleman "Code") | [
Daniel Nemanic](https://github.com/DanielNemanic)
[💻](https://github.com/grokability/snipe-it/commits?author=DanielNemanic "Code") | [
SouthWolf](https://github.com/southwolf)
[💻](https://github.com/grokability/snipe-it/commits?author=southwolf "Code") | [
Ivar Nesje](https://github.com/ivarne)
[💻](https://github.com/grokability/snipe-it/commits?author=ivarne "Code") | [
Jérémy Benoist](http://www.j0k3r.net)
[📖](https://github.com/grokability/snipe-it/commits?author=j0k3r "Documentation") | [
Chris Leathley](https://github.com/cleathley)
[🚇](#infra-cleathley "Infrastructure (Hosting, Build-Tools, etc)") | [
splaer](https://github.com/splaer)
[🐛](https://github.com/grokability/snipe-it/issues?q=author%3Asplaer "Bug reports") [💻](https://github.com/grokability/snipe-it/commits?author=splaer "Code") |
-| [
Joe Ferguson](http://www.joeferguson.me)
[💻](https://github.com/grokability/snipe-it/commits?author=svpernova09 "Code") | [
diwanicki](https://github.com/diwanicki)
[💻](https://github.com/grokability/snipe-it/commits?author=diwanicki "Code") [📖](https://github.com/grokability/snipe-it/commits?author=diwanicki "Documentation") | [
Lee Thoong Ching](https://github.com/pakkua80)
[📖](https://github.com/grokability/snipe-it/commits?author=pakkua80 "Documentation") [💻](https://github.com/grokability/snipe-it/commits?author=pakkua80 "Code") | [
Marek Šuppa](http://shu.io)
[💻](https://github.com/grokability/snipe-it/commits?author=mrshu "Code") | [
Juan J. Martinez](https://github.com/mizar1616)
[🌍](#translation-mizar1616 "Translation") | [
R Ryan Dial](https://github.com/rrdial)
[🌍](#translation-rrdial "Translation") | [
Andrej Manduch](https://github.com/burlito)
[📖](https://github.com/grokability/snipe-it/commits?author=burlito "Documentation") |
-| [
Jay Richards](http://www.cordeos.com)
[💻](https://github.com/grokability/snipe-it/commits?author=technogenus "Code") | [
Alexander Innes](https://necurity.co.uk)
[💻](https://github.com/grokability/snipe-it/commits?author=leostat "Code") | [
Danny Garcia](https://buzzedword.codes)
[💻](https://github.com/grokability/snipe-it/commits?author=buzzedword "Code") | [
archpoint](https://github.com/archpoint)
[💻](https://github.com/grokability/snipe-it/commits?author=archpoint "Code") | [
Jake McGraw](http://www.jakemcgraw.com)
[💻](https://github.com/grokability/snipe-it/commits?author=jakemcgraw "Code") | [
FleischKarussel](https://github.com/FleischKarussel)
[📖](https://github.com/grokability/snipe-it/commits?author=FleischKarussel "Documentation") | [
Dylan Yi](https://github.com/feeva)
[💻](https://github.com/grokability/snipe-it/commits?author=feeva "Code") |
-| [
Gil Rutkowski](http://FlashingCursor.com)
[💻](https://github.com/grokability/snipe-it/commits?author=flashingcursor "Code") | [
Desmond Morris](http://www.desmondmorris.com)
[💻](https://github.com/grokability/snipe-it/commits?author=desmondmorris "Code") | [
Nick Peelman](http://peelman.us)
[💻](https://github.com/grokability/snipe-it/commits?author=peelman "Code") | [
Abraham Vegh](https://abrahamvegh.com)
[💻](https://github.com/grokability/snipe-it/commits?author=abrahamvegh "Code") | [
Mohamed Rashid](https://github.com/rashivkp)
[📖](https://github.com/grokability/snipe-it/commits?author=rashivkp "Documentation") | [
Kasey](http://hinchk.github.io)
[💻](https://github.com/grokability/snipe-it/commits?author=HinchK "Code") | [
Brett](https://github.com/BrettFagerlund)
[⚠️](https://github.com/grokability/snipe-it/commits?author=BrettFagerlund "Tests") |
-| [
Jason Spriggs](http://jasonspriggs.com)
[💻](https://github.com/grokability/snipe-it/commits?author=jasonspriggs "Code") | [
Nate Felton](http://n8felton.wordpress.com)
[💻](https://github.com/grokability/snipe-it/commits?author=n8felton "Code") | [
Manasses Ferreira](http://homepages.dcc.ufmg.br/~manassesferreira)
[💻](https://github.com/grokability/snipe-it/commits?author=manassesferreira "Code") | [
Steve](https://github.com/steveelwood)
[⚠️](https://github.com/grokability/snipe-it/commits?author=steveelwood "Tests") | [
matc](http://twitter.com/matc)
[⚠️](https://github.com/grokability/snipe-it/commits?author=matc "Tests") | [
Cole R. Davis](http://www.davisracingteam.com)
[⚠️](https://github.com/grokability/snipe-it/commits?author=VanillaNinjaD "Tests") | [
gibsonjoshua55](https://github.com/gibsonjoshua55)
[💻](https://github.com/grokability/snipe-it/commits?author=gibsonjoshua55 "Code") |
-| [
Robin Temme](https://github.com/zwerch)
[💻](https://github.com/grokability/snipe-it/commits?author=zwerch "Code") | [
Iman](https://github.com/imanghafoori1)
[💻](https://github.com/grokability/snipe-it/commits?author=imanghafoori1 "Code") | [
Richard Hofman](https://github.com/richardhofman6)
[💻](https://github.com/grokability/snipe-it/commits?author=richardhofman6 "Code") | [
gizzmojr](https://github.com/gizzmojr)
[💻](https://github.com/grokability/snipe-it/commits?author=gizzmojr "Code") | [
Jenny Li](https://github.com/imjennyli)
[📖](https://github.com/grokability/snipe-it/commits?author=imjennyli "Documentation") | [
Geoff Young](https://github.com/GeoffYoung)
[💻](https://github.com/grokability/snipe-it/commits?author=GeoffYoung "Code") | [
Elliot Blackburn](http://www.elliotblackburn.com)
[📖](https://github.com/grokability/snipe-it/commits?author=BlueHatbRit "Documentation") |
-| [
Tõnis Ormisson](http://andmemasin.eu)
[💻](https://github.com/grokability/snipe-it/commits?author=TonisOrmisson "Code") | [
Nicolai Essig](http://www.nicolai-essig.de)
[💻](https://github.com/grokability/snipe-it/commits?author=thakilla "Code") | [
Danielle](https://github.com/techincolor)
[📖](https://github.com/grokability/snipe-it/commits?author=techincolor "Documentation") | [
Lawrence](https://github.com/TheVakman)
[⚠️](https://github.com/grokability/snipe-it/commits?author=TheVakman "Tests") [🐛](https://github.com/grokability/snipe-it/issues?q=author%3ATheVakman "Bug reports") | [
uknzaeinozpas](https://github.com/uknzaeinozpas)
[⚠️](https://github.com/grokability/snipe-it/commits?author=uknzaeinozpas "Tests") [💻](https://github.com/grokability/snipe-it/commits?author=uknzaeinozpas "Code") | [
Ryan](https://github.com/Gelob)
[📖](https://github.com/grokability/snipe-it/commits?author=Gelob "Documentation") | [
vcordes79](https://github.com/vcordes79)
[💻](https://github.com/grokability/snipe-it/commits?author=vcordes79 "Code") |
-| [
fordster78](https://github.com/fordster78)
[💻](https://github.com/grokability/snipe-it/commits?author=fordster78 "Code") | [
CronKz](https://github.com/CronKz)
[💻](https://github.com/grokability/snipe-it/commits?author=CronKz "Code") [🌍](#translation-CronKz "Translation") | [
Tim Bishop](https://github.com/tdb)
[💻](https://github.com/grokability/snipe-it/commits?author=tdb "Code") | [
Sean McIlvenna](https://www.seanmcilvenna.com)
[💻](https://github.com/grokability/snipe-it/commits?author=seanmcilvenna "Code") | [
cepacs](https://github.com/cepacs)
[🐛](https://github.com/grokability/snipe-it/issues?q=author%3Acepacs "Bug reports") [📖](https://github.com/grokability/snipe-it/commits?author=cepacs "Documentation") | [
lea-mink](https://github.com/lea-mink)
[💻](https://github.com/grokability/snipe-it/commits?author=lea-mink "Code") | [
Hannah Tinkler](https://github.com/hannahtinkler)
[💻](https://github.com/grokability/snipe-it/commits?author=hannahtinkler "Code") |
-| [
Doeke Zanstra](https://github.com/doekman)
[💻](https://github.com/grokability/snipe-it/commits?author=doekman "Code") | [
Djamon Staal](https://www.sdhd.nl/)
[💻](https://github.com/grokability/snipe-it/commits?author=SjamonDaal "Code") | [
Earl Ramirez](https://github.com/EarlRamirez)
[💻](https://github.com/grokability/snipe-it/commits?author=EarlRamirez "Code") | [
Richard Ray Thomas](https://github.com/RichardRay)
[💻](https://github.com/grokability/snipe-it/commits?author=RichardRay "Code") | [
Ryan Kuba](https://www.taisun.io/)
[💻](https://github.com/grokability/snipe-it/commits?author=thelamer "Code") | [
Brian Monroe](https://github.com/ParadoxGuitarist)
[💻](https://github.com/grokability/snipe-it/commits?author=ParadoxGuitarist "Code") | [
plexorama](https://github.com/plexorama)
[💻](https://github.com/grokability/snipe-it/commits?author=plexorama "Code") |
-| [
Till Deeke](https://tilldeeke.de)
[💻](https://github.com/grokability/snipe-it/commits?author=tilldeeke "Code") | [
5quirrel](https://github.com/5quirrel)
[💻](https://github.com/grokability/snipe-it/commits?author=5quirrel "Code") | [
Jason](https://github.com/jasonlshelton)
[💻](https://github.com/grokability/snipe-it/commits?author=jasonlshelton "Code") | [
Antti](https://github.com/chemfy)
[💻](https://github.com/grokability/snipe-it/commits?author=chemfy "Code") | [
DeusMaximus](https://github.com/DeusMaximus)
[💻](https://github.com/grokability/snipe-it/commits?author=DeusMaximus "Code") | [
a-royal](https://github.com/A-ROYAL)
[🌍](#translation-A-ROYAL "Translation") | [
Alberto Aldrigo](https://github.com/albertoaldrigo)
[🌍](#translation-albertoaldrigo "Translation") |
-| [
Alex Stanev](http://alex.stanev.org/blog)
[🌍](#translation-RealEnder "Translation") | [
Andreas Rehm](http://devel.itsolution2.de)
[🌍](#translation-sirrus "Translation") | [
Andreas Erhard](https://github.com/xelan)
[🌍](#translation-xelan "Translation") | [
Andrés Vanegas Jiménez](https://github.com/angeldeejay)
[🌍](#translation-angeldeejay "Translation") | [
Antonio Schiavon](https://github.com/aschiavon91)
[🌍](#translation-aschiavon91 "Translation") | [
benunter](https://github.com/benunter)
[🌍](#translation-benunter "Translation") | [
Borys Żmuda](http://catweb24.pl)
[🌍](#translation-rudashi "Translation") |
-| [
chibacityblues](https://github.com/chibacityblues)
[🌍](#translation-chibacityblues "Translation") | [
Chien Wei Lin](https://github.com/cwlin0416)
[🌍](#translation-cwlin0416 "Translation") | [
Christian Schuster](https://github.com/Againstreality)
[🌍](#translation-Againstreality "Translation") | [
Christian Stefanus](http://chriss.webhostid.com)
[🌍](#translation-kopi-item "Translation") | [
wxcafé](http://wxcafe.net)
[🌍](#translation-wxcafe "Translation") | [
dpyroc](https://github.com/dpyroc)
[🌍](#translation-dpyroc "Translation") | [
Daniel Friedlmaier](http://www.friedlmaier.net)
[🌍](#translation-da-friedl "Translation") |
-| [
Daniel Heene](https://github.com/danielheene)
[🌍](#translation-danielheene "Translation") | [
danielcb](https://github.com/danielcb)
[🌍](#translation-danielcb "Translation") | [
Dominik Senti](https://github.com/dominiksenti)
[🌍](#translation-dominiksenti "Translation") | [
Eric Gautheron](http://www.konectik.com)
[🌍](#translation-EpixFr "Translation") | [
Erlend Pilø](https://erlpil.com)
[🌍](#translation-Erlpil "Translation") | [
Fabio Rapposelli](http://fabio.technology)
[🌍](#translation-frapposelli "Translation") | [
Felipe Barros](https://github.com/fgbs)
[🌍](#translation-fgbs "Translation") |
-| [
Fernando Possebon](https://github.com/possebon)
[🌍](#translation-possebon "Translation") | [
gdraque](https://github.com/gdraque)
[🌍](#translation-gdraque "Translation") | [
Georg Wallisch](https://github.com/georgwallisch)
[🌍](#translation-georgwallisch "Translation") | [
Gerardo Robles](https://github.com/jgroblesr85)
[🌍](#translation-jgroblesr85 "Translation") | [
Gluek](https://t.me/Gluek)
[🌍](#translation-mrgluek "Translation") | [
AdnanAbuShahad](https://github.com/AdnanAbuShahad)
[🌍](#translation-AdnanAbuShahad "Translation") | [
Hafidzi My](https://hafidzi.my)
[🌍](#translation-hafidzi "Translation") |
-| [
Harim Park](https://github.com/fofwisdom)
[🌍](#translation-fofwisdom "Translation") | [
Henrik Kentsson](http://www.kentsson.se)
[🌍](#translation-Kentsson "Translation") | [
Husnul Yaqien](https://github.com/husnulyaqien)
[🌍](#translation-husnulyaqien "Translation") | [
Ibrahim](http://abaalkhail.org)
[🌍](#translation-abaalkh "Translation") | [
igolman](https://github.com/igolman)
[🌍](#translation-igolman "Translation") | [
itangiang](https://github.com/itangiang)
[🌍](#translation-itangiang "Translation") | [
jarby1211](https://github.com/jarby1211)
[🌍](#translation-jarby1211 "Translation") |
-| [
Jhonn Willker](http://jwillker.com)
[🌍](#translation-JohnWillker "Translation") | [
Jose](https://github.com/joxelito94)
[🌍](#translation-joxelito94 "Translation") | [
laopangzi](https://github.com/laopangzi)
[🌍](#translation-laopangzi "Translation") | [
Lars Strojny](http://usrportage.de)
[🌍](#translation-lstrojny "Translation") | [
MarcosBL](http://twitter.com/marcosbl)
[🌍](#translation-MarcosBL "Translation") | [
marie joy cajes](https://github.com/mariejoyacajes)
[🌍](#translation-mariejoyacajes "Translation") | [
Mark S. Johansen](http://www.markjohansen.dk)
[🌍](#translation-msjohansen "Translation") |
-| [
Martin Stub](http://martinstub.dk)
[🌍](#translation-stubben "Translation") | [
Meyer Flavio](https://github.com/meyerf99)
[🌍](#translation-meyerf99 "Translation") | [
Micael Rodrigues](https://github.com/MicaelRodrigues)
[🌍](#translation-MicaelRodrigues "Translation") | [
Mikael Rasmussen](http://rubixy.com/)
[🌍](#translation-mikaelssen "Translation") | [
IxFail](https://github.com/IxFail)
[🌍](#translation-IxFail "Translation") | [
Mohammed Fota](http://www.mohammedfota.com)
[🌍](#translation-MohammedFota "Translation") | [
Moayad Alserihi](https://github.com/omego)
[🌍](#translation-omego "Translation") |
-| [
saymd](https://github.com/saymd)
[🌍](#translation-saymd "Translation") | [
Patrik Larsson](https://nordsken.se)
[🌍](#translation-pooot "Translation") | [
drcryo](https://github.com/drcryo)
[🌍](#translation-drcryo "Translation") | [
pawel1615](https://github.com/pawel1615)
[🌍](#translation-pawel1615 "Translation") | [
bodrovics](https://github.com/bodrovics)
[🌍](#translation-bodrovics "Translation") | [
priatna](https://github.com/priatna)
[🌍](#translation-priatna "Translation") | [
Fan Jiang](https://amayume.net)
[🌍](#translation-ProfFan "Translation") |
-| [
ragnarcx](https://github.com/ragnarcx)
[🌍](#translation-ragnarcx "Translation") | [
Rein van Haaren](http://www.reinvanhaaren.nl/)
[🌍](#translation-reinvanhaaren "Translation") | [
Teguh Dwicaksana](http://dheche.songolimo.net)
[🌍](#translation-dheche "Translation") | [
fraccie](https://github.com/FRaccie)
[🌍](#translation-FRaccie "Translation") | [
vinzruzell](https://github.com/vinzruzell)
[🌍](#translation-vinzruzell "Translation") | [
Kevin Austin](http://kevinaustin.com)
[🌍](#translation-vipsystem "Translation") | [
Wira Sandy](http://azuraweb.xyz)
[🌍](#translation-wira-sandy "Translation") |
-| [
Илья](https://github.com/GrayHoax)
[🌍](#translation-GrayHoax "Translation") | [
GodUseVPN](https://github.com/godusevpn)
[🌍](#translation-godusevpn "Translation") | [
周周](https://github.com/EngrZhou)
[🌍](#translation-EngrZhou "Translation") | [
Sam](https://github.com/takuy)
[💻](https://github.com/grokability/snipe-it/commits?author=takuy "Code") | [
Azerothian](https://www.illisian.com.au)
[💻](https://github.com/grokability/snipe-it/commits?author=Azerothian "Code") | [
Wes Hulette](http://macfoo.wordpress.com/)
[💻](https://github.com/grokability/snipe-it/commits?author=jwhulette "Code") | [
patrict](https://github.com/patrict)
[💻](https://github.com/grokability/snipe-it/commits?author=patrict "Code") |
-| [
Dmitriy Minaev](https://github.com/VELIKII-DIVAN)
[💻](https://github.com/grokability/snipe-it/commits?author=VELIKII-DIVAN "Code") | [
liquidhorse](https://github.com/liquidhorse)
[💻](https://github.com/grokability/snipe-it/commits?author=liquidhorse "Code") | [
Jordi Boggiano](https://seld.be/)
[💻](https://github.com/grokability/snipe-it/commits?author=Seldaek "Code") | [
Ivan Nieto](https://github.com/inietov)
[💻](https://github.com/grokability/snipe-it/commits?author=inietov "Code") | [
Ben RUBSON](https://github.com/benrubson)
[💻](https://github.com/grokability/snipe-it/commits?author=benrubson "Code") | [
NMathar](https://github.com/NMathar)
[💻](https://github.com/grokability/snipe-it/commits?author=NMathar "Code") | [
Steffen](https://github.com/smb)
[💻](https://github.com/grokability/snipe-it/commits?author=smb "Code") |
-| [
Sxderp](https://github.com/Sxderp)
[💻](https://github.com/grokability/snipe-it/commits?author=Sxderp "Code") | [
fanta8897](https://github.com/fanta8897)
[💻](https://github.com/grokability/snipe-it/commits?author=fanta8897 "Code") | [
Andrey Bolonin](https://andreybolonin.com/phpconsulting/)
[💻](https://github.com/grokability/snipe-it/commits?author=andreybolonin "Code") | [
shinayoshi](http://www.shinayoshi.net/)
[💻](https://github.com/grokability/snipe-it/commits?author=shinayoshi "Code") | [
Hubert](https://github.com/reuser)
[💻](https://github.com/grokability/snipe-it/commits?author=reuser "Code") | [
KeenRivals](https://brashear.me)
[💻](https://github.com/grokability/snipe-it/commits?author=KeenRivals "Code") | [
omyno](https://github.com/omyno)
[💻](https://github.com/grokability/snipe-it/commits?author=omyno "Code") |
-| [
Evgeny](https://github.com/jackka)
[💻](https://github.com/grokability/snipe-it/commits?author=jackka "Code") | [
Colin Campbell](https://digitalist.se)
[💻](https://github.com/grokability/snipe-it/commits?author=colin-campbell "Code") | [
Ľubomír Kučera](https://github.com/lubo)
[💻](https://github.com/grokability/snipe-it/commits?author=lubo "Code") | [
Martin Meredith](https://www.sourceguru.net)
[💻](https://github.com/grokability/snipe-it/commits?author=Mezzle "Code") | [
Tim Farmer](https://github.com/timothyfarmer)
[💻](https://github.com/grokability/snipe-it/commits?author=timothyfarmer "Code") | [
Marián Skrip](https://github.com/mskrip)
[💻](https://github.com/grokability/snipe-it/commits?author=mskrip "Code") | [
Godfrey Martinez](https://github.com/Godmartinz)
[💻](https://github.com/grokability/snipe-it/commits?author=Godmartinz "Code") |
-| [
bigtreeEdo](https://github.com/bigtreeEdo)
[💻](https://github.com/grokability/snipe-it/commits?author=bigtreeEdo "Code") | [
Colin McNeil](https://colinmcneil.me/)
[💻](https://github.com/grokability/snipe-it/commits?author=ColinMcNeil "Code") | [
JoKneeMo](https://github.com/JoKneeMo)
[💻](https://github.com/grokability/snipe-it/commits?author=JoKneeMo "Code") | [
Joshi](http://www.redbridge.se)
[💻](https://github.com/grokability/snipe-it/commits?author=joshi-redbridge "Code") | [
Anthony Burns](https://github.com/anthonypburns)
[💻](https://github.com/grokability/snipe-it/commits?author=anthonypburns "Code") | [
johnson-yi](https://github.com/johnson-yi)
[💻](https://github.com/grokability/snipe-it/commits?author=johnson-yi "Code") | [
Sanjay Govind](https://tangentmc.net)
[💻](https://github.com/grokability/snipe-it/commits?author=sanjay900 "Code") |
-| [
Peter Upfold](https://peter.upfold.org.uk/)
[💻](https://github.com/grokability/snipe-it/commits?author=PeterUpfold "Code") | [
Jared Biel](https://github.com/jbiel)
[💻](https://github.com/grokability/snipe-it/commits?author=jbiel "Code") | [
Dampfklon](https://github.com/dampfklon)
[💻](https://github.com/grokability/snipe-it/commits?author=dampfklon "Code") | [
Charles Hamilton](https://communityclosing.com)
[💻](https://github.com/grokability/snipe-it/commits?author=chamilton-ccn "Code") | [
Giuseppe Iannello](https://github.com/giannello)
[💻](https://github.com/grokability/snipe-it/commits?author=giannello "Code") | [
Peter Dave Hello](https://www.peterdavehello.org/)
[💻](https://github.com/grokability/snipe-it/commits?author=PeterDaveHello "Code") | [
sigmoidal](https://github.com/sigmoidal)
[💻](https://github.com/grokability/snipe-it/commits?author=sigmoidal "Code") |
-| [
Vincent Lainé](https://github.com/phenixdotnet)
[💻](https://github.com/grokability/snipe-it/commits?author=phenixdotnet "Code") | [
Lucas Pleß](http://www.lucas-pless.com)
[💻](https://github.com/grokability/snipe-it/commits?author=derlucas "Code") | [
Ian Littman](http://twitter.com/iansltx)
[💻](https://github.com/grokability/snipe-it/commits?author=iansltx "Code") | [
João Paulo](https://github.com/PauloLuna)
[💻](https://github.com/grokability/snipe-it/commits?author=PauloLuna "Code") | [
ThoBur](https://github.com/ThoBur)
[💻](https://github.com/grokability/snipe-it/commits?author=ThoBur "Code") | [
Alexander Chibrikin](http://phpprofi.ru/)
[💻](https://github.com/grokability/snipe-it/commits?author=alek13 "Code") | [
Anthony Winstanley](https://github.com/winstan)
[💻](https://github.com/grokability/snipe-it/commits?author=winstan "Code") |
-| [
Folke](https://github.com/fashberg)
[💻](https://github.com/grokability/snipe-it/commits?author=fashberg "Code") | [
Bennett Blodinger](https://github.com/benwa)
[💻](https://github.com/grokability/snipe-it/commits?author=benwa "Code") | [
NMC](https://nmc.dev)
[💻](https://github.com/grokability/snipe-it/commits?author=ncareau "Code") | [
andres-baller](https://github.com/andres-baller)
[💻](https://github.com/grokability/snipe-it/commits?author=andres-baller "Code") | [
sean-borg](https://github.com/sean-borg)
[💻](https://github.com/grokability/snipe-it/commits?author=sean-borg "Code") | [
EDVLeer](https://github.com/EDVLeer)
[💻](https://github.com/grokability/snipe-it/commits?author=EDVLeer "Code") | [
Kurokat](https://github.com/Kurokat)
[💻](https://github.com/grokability/snipe-it/commits?author=Kurokat "Code") |
-| [
Kevin Köllmann](https://www.kevinkoellmann.de)
[💻](https://github.com/grokability/snipe-it/commits?author=koelle25 "Code") | [
sw-mreyes](https://github.com/sw-mreyes)
[💻](https://github.com/grokability/snipe-it/commits?author=sw-mreyes "Code") | [
Joel Pittet](https://pittet.ca)
[💻](https://github.com/grokability/snipe-it/commits?author=joelpittet "Code") | [
Eli Young](https://elyscape.com)
[💻](https://github.com/grokability/snipe-it/commits?author=elyscape "Code") | [
Raell Dottin](https://github.com/raelldottin)
[💻](https://github.com/grokability/snipe-it/commits?author=raelldottin "Code") | [
Tom Misilo](https://github.com/misilot)
[💻](https://github.com/grokability/snipe-it/commits?author=misilot "Code") | [
David Davenne](http://david.davenne.be)
[💻](https://github.com/grokability/snipe-it/commits?author=JuustoMestari "Code") |
-| [
Mark Stenglein](https://markstenglein.com)
[💻](https://github.com/grokability/snipe-it/commits?author=ocelotsloth "Code") | [
ajsy](https://github.com/ajsy)
[💻](https://github.com/grokability/snipe-it/commits?author=ajsy "Code") | [
Jan Kiesewetter](https://github.com/t3easy)
[💻](https://github.com/grokability/snipe-it/commits?author=t3easy "Code") | [
Tetrachloromethane250](https://github.com/Tetrachloromethane250)
[💻](https://github.com/grokability/snipe-it/commits?author=Tetrachloromethane250 "Code") | [
Lars Kajes](https://www.kajes.se/)
[💻](https://github.com/grokability/snipe-it/commits?author=kajes "Code") | [
Joly0](https://github.com/Joly0)
[💻](https://github.com/grokability/snipe-it/commits?author=Joly0 "Code") | [
theburger](https://github.com/limeless)
[💻](https://github.com/grokability/snipe-it/commits?author=limeless "Code") |
-| [
David Valin Alonso](https://github.com/deivishome)
[💻](https://github.com/grokability/snipe-it/commits?author=deivishome "Code") | [
andreaci](https://github.com/andreaci)
[💻](https://github.com/grokability/snipe-it/commits?author=andreaci "Code") | [
Jelle Sebreghts](http://www.jellesebreghts.be)
[💻](https://github.com/grokability/snipe-it/commits?author=Jelle-S "Code") | [
Michael Pietsch](https://github.com/Skywalker-11)
| [
Masudul Haque Shihab](https://github.com/sh1hab)
[💻](https://github.com/grokability/snipe-it/commits?author=sh1hab "Code") | [
Supapong Areeprasertkul](http://www.freedomdive.com/)
[💻](https://github.com/grokability/snipe-it/commits?author=zybersup "Code") | [
Peter Sarossy](https://github.com/psarossy)
[💻](https://github.com/grokability/snipe-it/commits?author=psarossy "Code") |
-| [
Renee Margaret McConahy](https://github.com/nepella)
[💻](https://github.com/grokability/snipe-it/commits?author=nepella "Code") | [
JohnnyPicnic](https://github.com/JohnnyPicnic)
[💻](https://github.com/grokability/snipe-it/commits?author=JohnnyPicnic "Code") | [
markbrule](https://github.com/markbrule)
[💻](https://github.com/grokability/snipe-it/commits?author=markbrule "Code") | [
Mike Campbell](https://github.com/mikecmpbll)
[💻](https://github.com/grokability/snipe-it/commits?author=mikecmpbll "Code") | [
tbrconnect](https://github.com/tbrconnect)
[💻](https://github.com/grokability/snipe-it/commits?author=tbrconnect "Code") | [
kcoyo](https://github.com/kcoyo)
[💻](https://github.com/grokability/snipe-it/commits?author=kcoyo "Code") | [
Travis Miller](https://travismiller.com/)
[💻](https://github.com/grokability/snipe-it/commits?author=travismiller "Code") |
-| [
Evan Taylor](https://github.com/Delta5)
[💻](https://github.com/grokability/snipe-it/commits?author=Delta5 "Code") | [
Petri Asikainen](https://github.com/PetriAsi)
[💻](https://github.com/grokability/snipe-it/commits?author=PetriAsi "Code") | [
derdeagle](https://github.com/derdeagle)
[💻](https://github.com/grokability/snipe-it/commits?author=derdeagle "Code") | [
Mike Frysinger](https://wh0rd.org/)
[💻](https://github.com/grokability/snipe-it/commits?author=vapier "Code") | [
ALPHA](https://github.com/AL4AL)
[💻](https://github.com/grokability/snipe-it/commits?author=AL4AL "Code") | [
FliegenKLATSCH](https://www.ifern.de)
[💻](https://github.com/grokability/snipe-it/commits?author=FliegenKLATSCH "Code") | [
Jeremy Price](https://github.com/jerm)
[💻](https://github.com/grokability/snipe-it/commits?author=jerm "Code") |
-| [
Toreg87](https://github.com/Toreg87)
[💻](https://github.com/grokability/snipe-it/commits?author=Toreg87 "Code") | [
Matthew Nickson](https://github.com/Computroniks)
[💻](https://github.com/grokability/snipe-it/commits?author=Computroniks "Code") | [
Jethro Nederhof](https://jethron.id.au)
[💻](https://github.com/grokability/snipe-it/commits?author=jethron "Code") | [
Oskar Stenberg](https://github.com/01ste02)
[💻](https://github.com/grokability/snipe-it/commits?author=01ste02 "Code") | [
Robert-Azelis](https://github.com/Robert-Azelis)
[💻](https://github.com/grokability/snipe-it/commits?author=Robert-Azelis "Code") | [
Alexander William Smith](https://github.com/alwism)
[💻](https://github.com/grokability/snipe-it/commits?author=alwism "Code") | [
LEITWERK AG](https://www.leitwerk.de/)
[💻](https://github.com/grokability/snipe-it/commits?author=leitwerk-ag "Code") |
-| [
Adam](http://www.aboutcher.co.uk)
[💻](https://github.com/grokability/snipe-it/commits?author=adamboutcher "Code") | [
Ian](https://snksrv.com)
[💻](https://github.com/grokability/snipe-it/commits?author=sneak-it "Code") | [
Shao Yu-Lung (Allen)](http://blog.bestlong.idv.tw/)
[💻](https://github.com/grokability/snipe-it/commits?author=bestlong "Code") | [
Haxatron](https://github.com/Haxatron)
[💻](https://github.com/grokability/snipe-it/commits?author=Haxatron "Code") | [
PlaneNuts](https://github.com/PlaneNuts)
[💻](https://github.com/grokability/snipe-it/commits?author=PlaneNuts "Code") | [
Bradley Coudriet](http://bjcpgd.cias.rit.edu)
[💻](https://github.com/grokability/snipe-it/commits?author=exula "Code") | [
Dalton Durst](https://daltondur.st)
[💻](https://github.com/grokability/snipe-it/commits?author=UniversalSuperBox "Code") |
-| [
Alex Janes](https://adagiohealth.org)
[💻](https://github.com/grokability/snipe-it/commits?author=adagioajanes "Code") | [
Nuraeil](https://github.com/nuraeil)
[💻](https://github.com/grokability/snipe-it/commits?author=nuraeil "Code") | [
TenOfTens](https://github.com/TenOfTens)
[💻](https://github.com/grokability/snipe-it/commits?author=TenOfTens "Code") | [
waffle](https://ditisjens.be/)
[💻](https://github.com/grokability/snipe-it/commits?author=insert-waffle "Code") | [
Yevhenii Huzii](https://github.com/QveenSi)
[💻](https://github.com/grokability/snipe-it/commits?author=QveenSi "Code") | [
Achmad Fienan Rahardianto](https://github.com/veenone)
[💻](https://github.com/grokability/snipe-it/commits?author=veenone "Code") | [
Yevhenii Huzii](https://github.com/QveenSi)
[💻](https://github.com/grokability/snipe-it/commits?author=QveenSi "Code") |
-| [
Christian Weirich](https://github.com/chrisweirich)
[💻](https://github.com/grokability/snipe-it/commits?author=chrisweirich "Code") | [
denzfarid](https://github.com/denzfarid)
| [
ntbutler-nbcs](https://github.com/ntbutler-nbcs)
[💻](https://github.com/grokability/snipe-it/commits?author=ntbutler-nbcs "Code") | [
Naveen](https://naveensrinivasan.dev)
[💻](https://github.com/grokability/snipe-it/commits?author=naveensrinivasan "Code") | [
Mike Roquemore](https://github.com/mikeroq)
[💻](https://github.com/grokability/snipe-it/commits?author=mikeroq "Code") | [
Daniel Reeder](https://github.com/reederda)
[🌍](#translation-reederda "Translation") [🌍](#translation-reederda "Translation") [💻](https://github.com/grokability/snipe-it/commits?author=reederda "Code") | [
vickyjaura183](https://github.com/vickyjaura183)
[💻](https://github.com/grokability/snipe-it/commits?author=vickyjaura183 "Code") |
-| [
Peace](https://github.com/julian-piehl)
[💻](https://github.com/grokability/snipe-it/commits?author=julian-piehl "Code") | [
Kyle Gordon](https://github.com/kylegordon)
[💻](https://github.com/grokability/snipe-it/commits?author=kylegordon "Code") | [
Katharina Drexel](http://www.bfh.ch)
[💻](https://github.com/grokability/snipe-it/commits?author=sunflowerbofh "Code") | [
David Sferruzza](https://david.sferruzza.fr/)
[💻](https://github.com/grokability/snipe-it/commits?author=dsferruzza "Code") | [
Rick Nelson](https://github.com/rnelsonee)
[💻](https://github.com/grokability/snipe-it/commits?author=rnelsonee "Code") | [
BasO12](https://github.com/BasO12)
[💻](https://github.com/grokability/snipe-it/commits?author=BasO12 "Code") | [
Vautia](https://github.com/Vautia)
[💻](https://github.com/grokability/snipe-it/commits?author=Vautia "Code") |
-| [
Chris Hartjes](http://www.littlehart.net/atthekeyboard)
[💻](https://github.com/grokability/snipe-it/commits?author=chartjes "Code") | [
geo-chen](https://github.com/geo-chen)
[💻](https://github.com/grokability/snipe-it/commits?author=geo-chen "Code") | [
Phan Nguyen](https://github.com/nh314)
[💻](https://github.com/grokability/snipe-it/commits?author=nh314 "Code") | [
Iisakki Jaakkola](https://github.com/StarlessNights)
[💻](https://github.com/grokability/snipe-it/commits?author=StarlessNights "Code") | [
Ikko Ashimine](https://bandism.net/)
[💻](https://github.com/grokability/snipe-it/commits?author=eltociear "Code") | [
Lukas Fehling](https://github.com/lukasfehling)
[💻](https://github.com/grokability/snipe-it/commits?author=lukasfehling "Code") | [
Fernando Almeida](https://github.com/fernando-almeida)
[💻](https://github.com/grokability/snipe-it/commits?author=fernando-almeida "Code") |
-| [
akemidx](https://github.com/akemidx)
[💻](https://github.com/grokability/snipe-it/commits?author=akemidx "Code") | [
Oguz Bilgic](http://oguz.site)
[💻](https://github.com/grokability/snipe-it/commits?author=oguzbilgic "Code") | [
Scooter Crawford](https://github.com/scoo73r)
[💻](https://github.com/grokability/snipe-it/commits?author=scoo73r "Code") | [
subdriven](https://github.com/subdriven)
[💻](https://github.com/grokability/snipe-it/commits?author=subdriven "Code") | [
Andrew Savinykh](https://github.com/AndrewSav)
[💻](https://github.com/grokability/snipe-it/commits?author=AndrewSav "Code") | [
Tadayuki Onishi](https://kenchan0130.github.io)
[💻](https://github.com/grokability/snipe-it/commits?author=kenchan0130 "Code") | [
Florian](https://github.com/floschoepfer)
[💻](https://github.com/grokability/snipe-it/commits?author=floschoepfer "Code") |
-| [
Spencer Long](http://spencerlong.com)
[💻](https://github.com/grokability/snipe-it/commits?author=spencerrlongg "Code") | [
Marcus Moore](https://github.com/marcusmoore)
[💻](https://github.com/grokability/snipe-it/commits?author=marcusmoore "Code") | [
Martin Meredith](https://github.com/Mezzle)
| [
dboth](http://dboth.de)
[💻](https://github.com/grokability/snipe-it/commits?author=dboth "Code") | [
Zachary Fleck](https://github.com/zacharyfleck)
[💻](https://github.com/grokability/snipe-it/commits?author=zacharyfleck "Code") | [
VIKAAS-A](https://github.com/vikaas-cyper)
[💻](https://github.com/grokability/snipe-it/commits?author=vikaas-cyper "Code") | [
Abdul Kareem](https://github.com/ak-piracha)
[💻](https://github.com/grokability/snipe-it/commits?author=ak-piracha "Code") |
-| [
NojoudAlshehri](https://github.com/NojoudAlshehri)
[💻](https://github.com/grokability/snipe-it/commits?author=NojoudAlshehri "Code") | [
Stefan Stidl](https://github.com/stefanstidlffg)
[💻](https://github.com/grokability/snipe-it/commits?author=stefanstidlffg "Code") | [
Quentin Aymard](https://github.com/qay21)
[💻](https://github.com/grokability/snipe-it/commits?author=qay21 "Code") | [
Grant Le Roux](https://github.com/cram42)
[💻](https://github.com/grokability/snipe-it/commits?author=cram42 "Code") | [
Bogdan](http://@singrity)
[💻](https://github.com/grokability/snipe-it/commits?author=Singrity "Code") | [
mmanjos](https://github.com/mmanjos)
[💻](https://github.com/grokability/snipe-it/commits?author=mmanjos "Code") | [
Abdelaziz Faki](https://azooz2014.github.io/)
[💻](https://github.com/grokability/snipe-it/commits?author=Azooz2014 "Code") |
-| [
bilias](https://github.com/bilias)
[💻](https://github.com/grokability/snipe-it/commits?author=bilias "Code") | [
coach1988](https://github.com/coach1988)
[💻](https://github.com/grokability/snipe-it/commits?author=coach1988 "Code") | [
MrM](https://github.com/mauro-miatello)
[💻](https://github.com/grokability/snipe-it/commits?author=mauro-miatello "Code") | [
koiakoia](https://github.com/koiakoia)
[💻](https://github.com/grokability/snipe-it/commits?author=koiakoia "Code") | [
Mustafa Online](https://github.com/mustafa-online)
[💻](https://github.com/grokability/snipe-it/commits?author=mustafa-online "Code") | [
franceslui](https://github.com/franceslui)
[💻](https://github.com/grokability/snipe-it/commits?author=franceslui "Code") | [
Q4kK](https://github.com/Q4kK)
[💻](https://github.com/grokability/snipe-it/commits?author=Q4kK "Code") |
-| [
squintfox](https://github.com/squintfox)
[💻](https://github.com/grokability/snipe-it/commits?author=squintfox "Code") | [
Jeff Clay](https://github.com/jeffclay)
[💻](https://github.com/grokability/snipe-it/commits?author=jeffclay "Code") | [
Phil J R](https://github.com/PP-JN-RL)
[💻](https://github.com/grokability/snipe-it/commits?author=PP-JN-RL "Code") | [
i_virus](https://www.corelight.com/)
[💻](https://github.com/grokability/snipe-it/commits?author=chandanchowdhury "Code") | [
Paul Grime](https://github.com/gitgrimbo)
[💻](https://github.com/grokability/snipe-it/commits?author=gitgrimbo "Code") | [
Lee Porte](https://leeporte.co.uk)
[💻](https://github.com/grokability/snipe-it/commits?author=LeePorte "Code") | [
BRYAN ](https://github.com/bryanlopezinc)
[💻](https://github.com/grokability/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/grokability/snipe-it/commits?author=bryanlopezinc "Tests") |
-| [
U-H-T](https://github.com/U-H-T)
[💻](https://github.com/grokability/snipe-it/commits?author=U-H-T "Code") | [
Matt Tyree](https://github.com/Tyree)
[📖](https://github.com/grokability/snipe-it/commits?author=Tyree "Documentation") | [
Florent Bervas](http://spoontux.net)
[💻](https://github.com/grokability/snipe-it/commits?author=FlorentDotMe "Code") | [
Daniel Albertsen](https://ditscheri.com)
[💻](https://github.com/grokability/snipe-it/commits?author=dbakan "Code") | [
r-xyz](https://github.com/r-xyz)
[💻](https://github.com/grokability/snipe-it/commits?author=r-xyz "Code") | [
Steven Mainor](https://github.com/DrekiDegga)
[💻](https://github.com/grokability/snipe-it/commits?author=DrekiDegga "Code") | [
arne-kroeger](https://github.com/arne-kroeger)
[💻](https://github.com/grokability/snipe-it/commits?author=arne-kroeger "Code") |
-| [
Glukose1](https://github.com/Glukose1)
[💻](https://github.com/grokability/snipe-it/commits?author=Glukose1 "Code") | [
Scarzy](https://github.com/Scarzy)
[💻](https://github.com/grokability/snipe-it/commits?author=Scarzy "Code") | [
setpill](https://github.com/setpill)
[💻](https://github.com/grokability/snipe-it/commits?author=setpill "Code") | [
swift2512](https://github.com/swift2512)
[🐛](https://github.com/grokability/snipe-it/issues?q=author%3Aswift2512 "Bug reports") | [
Darren Rainey](https://darrenraineys.co.uk)
[💻](https://github.com/grokability/snipe-it/commits?author=DarrenRainey "Code") | [
maciej-poleszczyk](https://github.com/maciej-poleszczyk)
[💻](https://github.com/grokability/snipe-it/commits?author=maciej-poleszczyk "Code") | [
Sebastian Groß](https://github.com/sgross-emlix)
[💻](https://github.com/grokability/snipe-it/commits?author=sgross-emlix "Code") |
-| [
Anouar Touati](https://github.com/AnouarTouati)
[💻](https://github.com/grokability/snipe-it/commits?author=AnouarTouati "Code") | [
aHVzY2g](https://github.com/aHVzY2g)
[💻](https://github.com/grokability/snipe-it/commits?author=aHVzY2g "Code") | [
林博仁 Buo-ren Lin](https://brlin.me)
[💻](https://github.com/grokability/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/grokability/snipe-it/commits?author=jostrander "Code") | [
James M](https://github.com/azmcnutt)
[💻](https://github.com/grokability/snipe-it/commits?author=azmcnutt "Code") | [
Fiala06](https://github.com/Fiala06)
[💻](https://github.com/grokability/snipe-it/commits?author=Fiala06 "Code") |
-| [
Nathan Taylor](https://github.com/ntaylor-86)
[💻](https://github.com/grokability/snipe-it/commits?author=ntaylor-86 "Code") | [
fvollmer](https://github.com/fvollmer)
[💻](https://github.com/grokability/snipe-it/commits?author=fvollmer "Code") |[
36864](https://github.com/36864)
[💻](https://github.com/grokability/snipe-it/commits?author=36864 "Code") |[
CloCkWeRX](https://github.com/CloCkWeRX)
[💻](https://github.com/grokability/snipe-it/commits?author=CloCkWeRX "Code")|[
BeatSpark](https://github.com/BeatSpark)
[💻](https://github.com/grokability/snipe-it/commits?author=BeatSpark "Code") | [
mrdahbi](https://github.com/mrdahbi)
[💻](https://github.com/grokability/snipe-it/commits?author=mrdahbi "Code") |
+| [
snipe](http://www.snipe.net)
[💻](https://github.com/snipe/snipe-it/commits?author=snipe "Code") [🚇](#infra-snipe "Infrastructure (Hosting, Build-Tools, etc)") [📖](https://github.com/snipe/snipe-it/commits?author=snipe "Documentation") [⚠️](https://github.com/snipe/snipe-it/commits?author=snipe "Tests") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Asnipe "Bug reports") [🎨](#design-snipe "Design") [👀](#review-snipe "Reviewed Pull Requests") | [
Brady Wetherington](http://www.uberbrady.com)
[💻](https://github.com/snipe/snipe-it/commits?author=uberbrady "Code") [📖](https://github.com/snipe/snipe-it/commits?author=uberbrady "Documentation") [🚇](#infra-uberbrady "Infrastructure (Hosting, Build-Tools, etc)") [👀](#review-uberbrady "Reviewed Pull Requests") | [
Daniel Meltzer](https://github.com/dmeltzer)
[💻](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Tests") [📖](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Documentation") | [
Michael T](http://www.tuckertechonline.com)
[💻](https://github.com/snipe/snipe-it/commits?author=mtucker6784 "Code") | [
madd15](https://github.com/madd15)
[📖](https://github.com/snipe/snipe-it/commits?author=madd15 "Documentation") [💬](#question-madd15 "Answering Questions") | [
Vincent Sposato](https://github.com/vsposato)
[💻](https://github.com/snipe/snipe-it/commits?author=vsposato "Code") | [
Andrea Bergamasco](https://github.com/vjandrea)
[💻](https://github.com/snipe/snipe-it/commits?author=vjandrea "Code") |
+| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
+| [
Karol](https://github.com/kpawelski)
[🌍](#translation-kpawelski "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=kpawelski "Code") | [
morph027](http://blog.morph027.de/)
[💻](https://github.com/snipe/snipe-it/commits?author=morph027 "Code") | [
fvleminckx](https://github.com/fvleminckx)
[🚇](#infra-fvleminckx "Infrastructure (Hosting, Build-Tools, etc)") | [
itsupportcmsukorg](https://github.com/itsupportcmsukorg)
[💻](https://github.com/snipe/snipe-it/commits?author=itsupportcmsukorg "Code") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aitsupportcmsukorg "Bug reports") | [
Frank](https://override.io)
[💻](https://github.com/snipe/snipe-it/commits?author=base-zero "Code") | [
Deleted user](https://github.com/ghost)
[🌍](#translation-ghost "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=ghost "Code") | [
tiagom62](https://github.com/tiagom62)
[💻](https://github.com/snipe/snipe-it/commits?author=tiagom62 "Code") [🚇](#infra-tiagom62 "Infrastructure (Hosting, Build-Tools, etc)") |
+| [
Ryan Stafford](https://github.com/rystaf)
[💻](https://github.com/snipe/snipe-it/commits?author=rystaf "Code") | [
Eammon Hanlon](https://github.com/ehanlon)
[💻](https://github.com/snipe/snipe-it/commits?author=ehanlon "Code") | [
zjean](https://github.com/zjean)
[💻](https://github.com/snipe/snipe-it/commits?author=zjean "Code") | [
Matthias Frei](http://www.frei.media)
[💻](https://github.com/snipe/snipe-it/commits?author=FREImedia "Code") | [
opsydev](https://github.com/opsydev)
[💻](https://github.com/snipe/snipe-it/commits?author=opsydev "Code") | [
Daniel Dreier](http://www.ddreier.com)
[💻](https://github.com/snipe/snipe-it/commits?author=ddreier "Code") | [
Nikolai Prokoschenko](http://rassie.org)
[💻](https://github.com/snipe/snipe-it/commits?author=rassie "Code") |
+| [
Drew](https://github.com/YetAnotherCodeMonkey)
[💻](https://github.com/snipe/snipe-it/commits?author=YetAnotherCodeMonkey "Code") | [
Walter](https://github.com/merid14)
[💻](https://github.com/snipe/snipe-it/commits?author=merid14 "Code") | [
Petr Baloun](https://github.com/balous)
[💻](https://github.com/snipe/snipe-it/commits?author=balous "Code") | [
reidblomquist](https://github.com/reidblomquist)
[📖](https://github.com/snipe/snipe-it/commits?author=reidblomquist "Documentation") | [
Mathieu Kooiman](https://github.com/mathieuk)
[💻](https://github.com/snipe/snipe-it/commits?author=mathieuk "Code") | [
csayre](https://github.com/csayre)
[📖](https://github.com/snipe/snipe-it/commits?author=csayre "Documentation") | [
Adam Dunson](https://github.com/adamdunson)
[💻](https://github.com/snipe/snipe-it/commits?author=adamdunson "Code") |
+| [
Hereward](https://github.com/thehereward)
[💻](https://github.com/snipe/snipe-it/commits?author=thehereward "Code") | [
swoopdk](https://github.com/swoopdk)
[💻](https://github.com/snipe/snipe-it/commits?author=swoopdk "Code") | [
Abdullah Alansari](https://linkedin.com/in/ahimta)
[💻](https://github.com/snipe/snipe-it/commits?author=Ahimta "Code") | [
Micael Rodrigues](https://github.com/MicaelRodrigues)
[💻](https://github.com/snipe/snipe-it/commits?author=MicaelRodrigues "Code") | [
Patrick Gallagher](http://macadmincorner.com)
[📖](https://github.com/snipe/snipe-it/commits?author=patgmac "Documentation") | [
Miliamber](https://github.com/Miliamber)
[💻](https://github.com/snipe/snipe-it/commits?author=Miliamber "Code") | [
hawk554](https://github.com/hawk554)
[💻](https://github.com/snipe/snipe-it/commits?author=hawk554 "Code") |
+| [
Justin Kerr](http://jbirdkerr.net)
[💻](https://github.com/snipe/snipe-it/commits?author=jbirdkerr "Code") | [
Ira W. Snyder](http://www.irasnyder.com/devel/)
[📖](https://github.com/snipe/snipe-it/commits?author=irasnyd "Documentation") | [
Aladin Alaily](https://github.com/aalaily)
[💻](https://github.com/snipe/snipe-it/commits?author=aalaily "Code") | [
Chase Hansen](https://github.com/kobie-chasehansen)
[💻](https://github.com/snipe/snipe-it/commits?author=kobie-chasehansen "Code") [💬](#question-kobie-chasehansen "Answering Questions") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Akobie-chasehansen "Bug reports") | [
IDM Helpdesk](https://github.com/IDM-Helpdesk)
[💻](https://github.com/snipe/snipe-it/commits?author=IDM-Helpdesk "Code") | [
Kai](http://balticer.de)
[💻](https://github.com/snipe/snipe-it/commits?author=balticer "Code") | [
Michael Daniels](http://www.michaeldaniels.me)
[💻](https://github.com/snipe/snipe-it/commits?author=mdaniels5757 "Code") |
+| [
Tom Castleman](http://tomcastleman.me)
[💻](https://github.com/snipe/snipe-it/commits?author=tomcastleman "Code") | [
Daniel Nemanic](https://github.com/DanielNemanic)
[💻](https://github.com/snipe/snipe-it/commits?author=DanielNemanic "Code") | [
SouthWolf](https://github.com/southwolf)
[💻](https://github.com/snipe/snipe-it/commits?author=southwolf "Code") | [
Ivar Nesje](https://github.com/ivarne)
[💻](https://github.com/snipe/snipe-it/commits?author=ivarne "Code") | [
Jérémy Benoist](http://www.j0k3r.net)
[📖](https://github.com/snipe/snipe-it/commits?author=j0k3r "Documentation") | [
Chris Leathley](https://github.com/cleathley)
[🚇](#infra-cleathley "Infrastructure (Hosting, Build-Tools, etc)") | [
splaer](https://github.com/splaer)
[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Asplaer "Bug reports") [💻](https://github.com/snipe/snipe-it/commits?author=splaer "Code") |
+| [
Joe Ferguson](http://www.joeferguson.me)
[💻](https://github.com/snipe/snipe-it/commits?author=svpernova09 "Code") | [
diwanicki](https://github.com/diwanicki)
[💻](https://github.com/snipe/snipe-it/commits?author=diwanicki "Code") [📖](https://github.com/snipe/snipe-it/commits?author=diwanicki "Documentation") | [
Lee Thoong Ching](https://github.com/pakkua80)
[📖](https://github.com/snipe/snipe-it/commits?author=pakkua80 "Documentation") [💻](https://github.com/snipe/snipe-it/commits?author=pakkua80 "Code") | [
Marek Šuppa](http://shu.io)
[💻](https://github.com/snipe/snipe-it/commits?author=mrshu "Code") | [
Juan J. Martinez](https://github.com/mizar1616)
[🌍](#translation-mizar1616 "Translation") | [
R Ryan Dial](https://github.com/rrdial)
[🌍](#translation-rrdial "Translation") | [
Andrej Manduch](https://github.com/burlito)
[📖](https://github.com/snipe/snipe-it/commits?author=burlito "Documentation") |
+| [
Jay Richards](http://www.cordeos.com)
[💻](https://github.com/snipe/snipe-it/commits?author=technogenus "Code") | [
Alexander Innes](https://necurity.co.uk)
[💻](https://github.com/snipe/snipe-it/commits?author=leostat "Code") | [
Danny Garcia](https://buzzedword.codes)
[💻](https://github.com/snipe/snipe-it/commits?author=buzzedword "Code") | [
archpoint](https://github.com/archpoint)
[💻](https://github.com/snipe/snipe-it/commits?author=archpoint "Code") | [
Jake McGraw](http://www.jakemcgraw.com)
[💻](https://github.com/snipe/snipe-it/commits?author=jakemcgraw "Code") | [
FleischKarussel](https://github.com/FleischKarussel)
[📖](https://github.com/snipe/snipe-it/commits?author=FleischKarussel "Documentation") | [
Dylan Yi](https://github.com/feeva)
[💻](https://github.com/snipe/snipe-it/commits?author=feeva "Code") |
+| [
Gil Rutkowski](http://FlashingCursor.com)
[💻](https://github.com/snipe/snipe-it/commits?author=flashingcursor "Code") | [
Desmond Morris](http://www.desmondmorris.com)
[💻](https://github.com/snipe/snipe-it/commits?author=desmondmorris "Code") | [
Nick Peelman](http://peelman.us)
[💻](https://github.com/snipe/snipe-it/commits?author=peelman "Code") | [
Abraham Vegh](https://abrahamvegh.com)
[💻](https://github.com/snipe/snipe-it/commits?author=abrahamvegh "Code") | [
Mohamed Rashid](https://github.com/rashivkp)
[📖](https://github.com/snipe/snipe-it/commits?author=rashivkp "Documentation") | [
Kasey](http://hinchk.github.io)
[💻](https://github.com/snipe/snipe-it/commits?author=HinchK "Code") | [
Brett](https://github.com/BrettFagerlund)
[⚠️](https://github.com/snipe/snipe-it/commits?author=BrettFagerlund "Tests") |
+| [
Jason Spriggs](http://jasonspriggs.com)
[💻](https://github.com/snipe/snipe-it/commits?author=jasonspriggs "Code") | [
Nate Felton](http://n8felton.wordpress.com)
[💻](https://github.com/snipe/snipe-it/commits?author=n8felton "Code") | [
Manasses Ferreira](http://homepages.dcc.ufmg.br/~manassesferreira)
[💻](https://github.com/snipe/snipe-it/commits?author=manassesferreira "Code") | [
Steve](https://github.com/steveelwood)
[⚠️](https://github.com/snipe/snipe-it/commits?author=steveelwood "Tests") | [
matc](http://twitter.com/matc)
[⚠️](https://github.com/snipe/snipe-it/commits?author=matc "Tests") | [
Cole R. Davis](http://www.davisracingteam.com)
[⚠️](https://github.com/snipe/snipe-it/commits?author=VanillaNinjaD "Tests") | [
gibsonjoshua55](https://github.com/gibsonjoshua55)
[💻](https://github.com/snipe/snipe-it/commits?author=gibsonjoshua55 "Code") |
+| [
Robin Temme](https://github.com/zwerch)
[💻](https://github.com/snipe/snipe-it/commits?author=zwerch "Code") | [
Iman](https://github.com/imanghafoori1)
[💻](https://github.com/snipe/snipe-it/commits?author=imanghafoori1 "Code") | [
Richard Hofman](https://github.com/richardhofman6)
[💻](https://github.com/snipe/snipe-it/commits?author=richardhofman6 "Code") | [
gizzmojr](https://github.com/gizzmojr)
[💻](https://github.com/snipe/snipe-it/commits?author=gizzmojr "Code") | [
Jenny Li](https://github.com/imjennyli)
[📖](https://github.com/snipe/snipe-it/commits?author=imjennyli "Documentation") | [
Geoff Young](https://github.com/GeoffYoung)
[💻](https://github.com/snipe/snipe-it/commits?author=GeoffYoung "Code") | [
Elliot Blackburn](http://www.elliotblackburn.com)
[📖](https://github.com/snipe/snipe-it/commits?author=BlueHatbRit "Documentation") |
+| [
Tõnis Ormisson](http://andmemasin.eu)
[💻](https://github.com/snipe/snipe-it/commits?author=TonisOrmisson "Code") | [
Nicolai Essig](http://www.nicolai-essig.de)
[💻](https://github.com/snipe/snipe-it/commits?author=thakilla "Code") | [
Danielle](https://github.com/techincolor)
[📖](https://github.com/snipe/snipe-it/commits?author=techincolor "Documentation") | [
Lawrence](https://github.com/TheVakman)
[⚠️](https://github.com/snipe/snipe-it/commits?author=TheVakman "Tests") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3ATheVakman "Bug reports") | [
uknzaeinozpas](https://github.com/uknzaeinozpas)
[⚠️](https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas "Tests") [💻](https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas "Code") | [
Ryan](https://github.com/Gelob)
[📖](https://github.com/snipe/snipe-it/commits?author=Gelob "Documentation") | [
vcordes79](https://github.com/vcordes79)
[💻](https://github.com/snipe/snipe-it/commits?author=vcordes79 "Code") |
+| [
fordster78](https://github.com/fordster78)
[💻](https://github.com/snipe/snipe-it/commits?author=fordster78 "Code") | [
CronKz](https://github.com/CronKz)
[💻](https://github.com/snipe/snipe-it/commits?author=CronKz "Code") [🌍](#translation-CronKz "Translation") | [
Tim Bishop](https://github.com/tdb)
[💻](https://github.com/snipe/snipe-it/commits?author=tdb "Code") | [
Sean McIlvenna](https://www.seanmcilvenna.com)
[💻](https://github.com/snipe/snipe-it/commits?author=seanmcilvenna "Code") | [
cepacs](https://github.com/cepacs)
[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Acepacs "Bug reports") [📖](https://github.com/snipe/snipe-it/commits?author=cepacs "Documentation") | [
lea-mink](https://github.com/lea-mink)
[💻](https://github.com/snipe/snipe-it/commits?author=lea-mink "Code") | [
Hannah Tinkler](https://github.com/hannahtinkler)
[💻](https://github.com/snipe/snipe-it/commits?author=hannahtinkler "Code") |
+| [
Doeke Zanstra](https://github.com/doekman)
[💻](https://github.com/snipe/snipe-it/commits?author=doekman "Code") | [
Djamon Staal](https://www.sdhd.nl/)
[💻](https://github.com/snipe/snipe-it/commits?author=SjamonDaal "Code") | [
Earl Ramirez](https://github.com/EarlRamirez)
[💻](https://github.com/snipe/snipe-it/commits?author=EarlRamirez "Code") | [
Richard Ray Thomas](https://github.com/RichardRay)
[💻](https://github.com/snipe/snipe-it/commits?author=RichardRay "Code") | [
Ryan Kuba](https://www.taisun.io/)
[💻](https://github.com/snipe/snipe-it/commits?author=thelamer "Code") | [
Brian Monroe](https://github.com/ParadoxGuitarist)
[💻](https://github.com/snipe/snipe-it/commits?author=ParadoxGuitarist "Code") | [
plexorama](https://github.com/plexorama)
[💻](https://github.com/snipe/snipe-it/commits?author=plexorama "Code") |
+| [
Till Deeke](https://tilldeeke.de)
[💻](https://github.com/snipe/snipe-it/commits?author=tilldeeke "Code") | [
5quirrel](https://github.com/5quirrel)
[💻](https://github.com/snipe/snipe-it/commits?author=5quirrel "Code") | [
Jason](https://github.com/jasonlshelton)
[💻](https://github.com/snipe/snipe-it/commits?author=jasonlshelton "Code") | [
Antti](https://github.com/chemfy)
[💻](https://github.com/snipe/snipe-it/commits?author=chemfy "Code") | [
DeusMaximus](https://github.com/DeusMaximus)
[💻](https://github.com/snipe/snipe-it/commits?author=DeusMaximus "Code") | [
a-royal](https://github.com/A-ROYAL)
[🌍](#translation-A-ROYAL "Translation") | [
Alberto Aldrigo](https://github.com/albertoaldrigo)
[🌍](#translation-albertoaldrigo "Translation") |
+| [
Alex Stanev](http://alex.stanev.org/blog)
[🌍](#translation-RealEnder "Translation") | [
Andreas Rehm](http://devel.itsolution2.de)
[🌍](#translation-sirrus "Translation") | [
Andreas Erhard](https://github.com/xelan)
[🌍](#translation-xelan "Translation") | [
Andrés Vanegas Jiménez](https://github.com/angeldeejay)
[🌍](#translation-angeldeejay "Translation") | [
Antonio Schiavon](https://github.com/aschiavon91)
[🌍](#translation-aschiavon91 "Translation") | [
benunter](https://github.com/benunter)
[🌍](#translation-benunter "Translation") | [
Borys Żmuda](http://catweb24.pl)
[🌍](#translation-rudashi "Translation") |
+| [
chibacityblues](https://github.com/chibacityblues)
[🌍](#translation-chibacityblues "Translation") | [
Chien Wei Lin](https://github.com/cwlin0416)
[🌍](#translation-cwlin0416 "Translation") | [
Christian Schuster](https://github.com/Againstreality)
[🌍](#translation-Againstreality "Translation") | [
Christian Stefanus](http://chriss.webhostid.com)
[🌍](#translation-kopi-item "Translation") | [
wxcafé](http://wxcafe.net)
[🌍](#translation-wxcafe "Translation") | [
dpyroc](https://github.com/dpyroc)
[🌍](#translation-dpyroc "Translation") | [
Daniel Friedlmaier](http://www.friedlmaier.net)
[🌍](#translation-da-friedl "Translation") |
+| [
Daniel Heene](https://github.com/danielheene)
[🌍](#translation-danielheene "Translation") | [
danielcb](https://github.com/danielcb)
[🌍](#translation-danielcb "Translation") | [
Dominik Senti](https://github.com/dominiksenti)
[🌍](#translation-dominiksenti "Translation") | [
Eric Gautheron](http://www.konectik.com)
[🌍](#translation-EpixFr "Translation") | [
Erlend Pilø](https://erlpil.com)
[🌍](#translation-Erlpil "Translation") | [
Fabio Rapposelli](http://fabio.technology)
[🌍](#translation-frapposelli "Translation") | [
Felipe Barros](https://github.com/fgbs)
[🌍](#translation-fgbs "Translation") |
+| [
Fernando Possebon](https://github.com/possebon)
[🌍](#translation-possebon "Translation") | [
gdraque](https://github.com/gdraque)
[🌍](#translation-gdraque "Translation") | [
Georg Wallisch](https://github.com/georgwallisch)
[🌍](#translation-georgwallisch "Translation") | [
Gerardo Robles](https://github.com/jgroblesr85)
[🌍](#translation-jgroblesr85 "Translation") | [
Gluek](https://t.me/Gluek)
[🌍](#translation-mrgluek "Translation") | [
AdnanAbuShahad](https://github.com/AdnanAbuShahad)
[🌍](#translation-AdnanAbuShahad "Translation") | [
Hafidzi My](https://hafidzi.my)
[🌍](#translation-hafidzi "Translation") |
+| [
Harim Park](https://github.com/fofwisdom)
[🌍](#translation-fofwisdom "Translation") | [
Henrik Kentsson](http://www.kentsson.se)
[🌍](#translation-Kentsson "Translation") | [
Husnul Yaqien](https://github.com/husnulyaqien)
[🌍](#translation-husnulyaqien "Translation") | [
Ibrahim](http://abaalkhail.org)
[🌍](#translation-abaalkh "Translation") | [
igolman](https://github.com/igolman)
[🌍](#translation-igolman "Translation") | [
itangiang](https://github.com/itangiang)
[🌍](#translation-itangiang "Translation") | [
jarby1211](https://github.com/jarby1211)
[🌍](#translation-jarby1211 "Translation") |
+| [
Jhonn Willker](http://jwillker.com)
[🌍](#translation-JohnWillker "Translation") | [
Jose](https://github.com/joxelito94)
[🌍](#translation-joxelito94 "Translation") | [
laopangzi](https://github.com/laopangzi)
[🌍](#translation-laopangzi "Translation") | [
Lars Strojny](http://usrportage.de)
[🌍](#translation-lstrojny "Translation") | [
MarcosBL](http://twitter.com/marcosbl)
[🌍](#translation-MarcosBL "Translation") | [
marie joy cajes](https://github.com/mariejoyacajes)
[🌍](#translation-mariejoyacajes "Translation") | [
Mark S. Johansen](http://www.markjohansen.dk)
[🌍](#translation-msjohansen "Translation") |
+| [
Martin Stub](http://martinstub.dk)
[🌍](#translation-stubben "Translation") | [
Meyer Flavio](https://github.com/meyerf99)
[🌍](#translation-meyerf99 "Translation") | [
Micael Rodrigues](https://github.com/MicaelRodrigues)
[🌍](#translation-MicaelRodrigues "Translation") | [
Mikael Rasmussen](http://rubixy.com/)
[🌍](#translation-mikaelssen "Translation") | [
IxFail](https://github.com/IxFail)
[🌍](#translation-IxFail "Translation") | [
Mohammed Fota](http://www.mohammedfota.com)
[🌍](#translation-MohammedFota "Translation") | [
Moayad Alserihi](https://github.com/omego)
[🌍](#translation-omego "Translation") |
+| [
saymd](https://github.com/saymd)
[🌍](#translation-saymd "Translation") | [
Patrik Larsson](https://nordsken.se)
[🌍](#translation-pooot "Translation") | [
drcryo](https://github.com/drcryo)
[🌍](#translation-drcryo "Translation") | [
pawel1615](https://github.com/pawel1615)
[🌍](#translation-pawel1615 "Translation") | [
bodrovics](https://github.com/bodrovics)
[🌍](#translation-bodrovics "Translation") | [
priatna](https://github.com/priatna)
[🌍](#translation-priatna "Translation") | [
Fan Jiang](https://amayume.net)
[🌍](#translation-ProfFan "Translation") |
+| [
ragnarcx](https://github.com/ragnarcx)
[🌍](#translation-ragnarcx "Translation") | [
Rein van Haaren](http://www.reinvanhaaren.nl/)
[🌍](#translation-reinvanhaaren "Translation") | [
Teguh Dwicaksana](http://dheche.songolimo.net)
[🌍](#translation-dheche "Translation") | [
fraccie](https://github.com/FRaccie)
[🌍](#translation-FRaccie "Translation") | [
vinzruzell](https://github.com/vinzruzell)
[🌍](#translation-vinzruzell "Translation") | [
Kevin Austin](http://kevinaustin.com)
[🌍](#translation-vipsystem "Translation") | [
Wira Sandy](http://azuraweb.xyz)
[🌍](#translation-wira-sandy "Translation") |
+| [
Илья](https://github.com/GrayHoax)
[🌍](#translation-GrayHoax "Translation") | [
GodUseVPN](https://github.com/godusevpn)
[🌍](#translation-godusevpn "Translation") | [
周周](https://github.com/EngrZhou)
[🌍](#translation-EngrZhou "Translation") | [
Sam](https://github.com/takuy)
[💻](https://github.com/snipe/snipe-it/commits?author=takuy "Code") | [
Azerothian](https://www.illisian.com.au)
[💻](https://github.com/snipe/snipe-it/commits?author=Azerothian "Code") | [
Wes Hulette](http://macfoo.wordpress.com/)
[💻](https://github.com/snipe/snipe-it/commits?author=jwhulette "Code") | [
patrict](https://github.com/patrict)
[💻](https://github.com/snipe/snipe-it/commits?author=patrict "Code") |
+| [
Dmitriy Minaev](https://github.com/VELIKII-DIVAN)
[💻](https://github.com/snipe/snipe-it/commits?author=VELIKII-DIVAN "Code") | [
liquidhorse](https://github.com/liquidhorse)
[💻](https://github.com/snipe/snipe-it/commits?author=liquidhorse "Code") | [
Jordi Boggiano](https://seld.be/)
[💻](https://github.com/snipe/snipe-it/commits?author=Seldaek "Code") | [
Ivan Nieto](https://github.com/inietov)
[💻](https://github.com/snipe/snipe-it/commits?author=inietov "Code") | [
Ben RUBSON](https://github.com/benrubson)
[💻](https://github.com/snipe/snipe-it/commits?author=benrubson "Code") | [
NMathar](https://github.com/NMathar)
[💻](https://github.com/snipe/snipe-it/commits?author=NMathar "Code") | [
Steffen](https://github.com/smb)
[💻](https://github.com/snipe/snipe-it/commits?author=smb "Code") |
+| [
Sxderp](https://github.com/Sxderp)
[💻](https://github.com/snipe/snipe-it/commits?author=Sxderp "Code") | [
fanta8897](https://github.com/fanta8897)
[💻](https://github.com/snipe/snipe-it/commits?author=fanta8897 "Code") | [
Andrey Bolonin](https://andreybolonin.com/phpconsulting/)
[💻](https://github.com/snipe/snipe-it/commits?author=andreybolonin "Code") | [
shinayoshi](http://www.shinayoshi.net/)
[💻](https://github.com/snipe/snipe-it/commits?author=shinayoshi "Code") | [
Hubert](https://github.com/reuser)
[💻](https://github.com/snipe/snipe-it/commits?author=reuser "Code") | [
KeenRivals](https://brashear.me)
[💻](https://github.com/snipe/snipe-it/commits?author=KeenRivals "Code") | [
omyno](https://github.com/omyno)
[💻](https://github.com/snipe/snipe-it/commits?author=omyno "Code") |
+| [
Evgeny](https://github.com/jackka)
[💻](https://github.com/snipe/snipe-it/commits?author=jackka "Code") | [
Colin Campbell](https://digitalist.se)
[💻](https://github.com/snipe/snipe-it/commits?author=colin-campbell "Code") | [
Ľubomír Kučera](https://github.com/lubo)
[💻](https://github.com/snipe/snipe-it/commits?author=lubo "Code") | [
Martin Meredith](https://www.sourceguru.net)
[💻](https://github.com/snipe/snipe-it/commits?author=Mezzle "Code") | [
Tim Farmer](https://github.com/timothyfarmer)
[💻](https://github.com/snipe/snipe-it/commits?author=timothyfarmer "Code") | [
Marián Skrip](https://github.com/mskrip)
[💻](https://github.com/snipe/snipe-it/commits?author=mskrip "Code") | [
Godfrey Martinez](https://github.com/Godmartinz)
[💻](https://github.com/snipe/snipe-it/commits?author=Godmartinz "Code") |
+| [
bigtreeEdo](https://github.com/bigtreeEdo)
[💻](https://github.com/snipe/snipe-it/commits?author=bigtreeEdo "Code") | [
Colin McNeil](https://colinmcneil.me/)
[💻](https://github.com/snipe/snipe-it/commits?author=ColinMcNeil "Code") | [
JoKneeMo](https://github.com/JoKneeMo)
[💻](https://github.com/snipe/snipe-it/commits?author=JoKneeMo "Code") | [
Joshi](http://www.redbridge.se)
[💻](https://github.com/snipe/snipe-it/commits?author=joshi-redbridge "Code") | [
Anthony Burns](https://github.com/anthonypburns)
[💻](https://github.com/snipe/snipe-it/commits?author=anthonypburns "Code") | [
johnson-yi](https://github.com/johnson-yi)
[💻](https://github.com/snipe/snipe-it/commits?author=johnson-yi "Code") | [
Sanjay Govind](https://tangentmc.net)
[💻](https://github.com/snipe/snipe-it/commits?author=sanjay900 "Code") |
+| [
Peter Upfold](https://peter.upfold.org.uk/)
[💻](https://github.com/snipe/snipe-it/commits?author=PeterUpfold "Code") | [
Jared Biel](https://github.com/jbiel)
[💻](https://github.com/snipe/snipe-it/commits?author=jbiel "Code") | [
Dampfklon](https://github.com/dampfklon)
[💻](https://github.com/snipe/snipe-it/commits?author=dampfklon "Code") | [
Charles Hamilton](https://communityclosing.com)
[💻](https://github.com/snipe/snipe-it/commits?author=chamilton-ccn "Code") | [
Giuseppe Iannello](https://github.com/giannello)
[💻](https://github.com/snipe/snipe-it/commits?author=giannello "Code") | [
Peter Dave Hello](https://www.peterdavehello.org/)
[💻](https://github.com/snipe/snipe-it/commits?author=PeterDaveHello "Code") | [
sigmoidal](https://github.com/sigmoidal)
[💻](https://github.com/snipe/snipe-it/commits?author=sigmoidal "Code") |
+| [
Vincent Lainé](https://github.com/phenixdotnet)
[💻](https://github.com/snipe/snipe-it/commits?author=phenixdotnet "Code") | [
Lucas Pleß](http://www.lucas-pless.com)
[💻](https://github.com/snipe/snipe-it/commits?author=derlucas "Code") | [
Ian Littman](http://twitter.com/iansltx)
[💻](https://github.com/snipe/snipe-it/commits?author=iansltx "Code") | [
João Paulo](https://github.com/PauloLuna)
[💻](https://github.com/snipe/snipe-it/commits?author=PauloLuna "Code") | [
ThoBur](https://github.com/ThoBur)
[💻](https://github.com/snipe/snipe-it/commits?author=ThoBur "Code") | [
Alexander Chibrikin](http://phpprofi.ru/)
[💻](https://github.com/snipe/snipe-it/commits?author=alek13 "Code") | [
Anthony Winstanley](https://github.com/winstan)
[💻](https://github.com/snipe/snipe-it/commits?author=winstan "Code") |
+| [
Folke](https://github.com/fashberg)
[💻](https://github.com/snipe/snipe-it/commits?author=fashberg "Code") | [
Bennett Blodinger](https://github.com/benwa)
[💻](https://github.com/snipe/snipe-it/commits?author=benwa "Code") | [
NMC](https://nmc.dev)
[💻](https://github.com/snipe/snipe-it/commits?author=ncareau "Code") | [
andres-baller](https://github.com/andres-baller)
[💻](https://github.com/snipe/snipe-it/commits?author=andres-baller "Code") | [
sean-borg](https://github.com/sean-borg)
[💻](https://github.com/snipe/snipe-it/commits?author=sean-borg "Code") | [
EDVLeer](https://github.com/EDVLeer)
[💻](https://github.com/snipe/snipe-it/commits?author=EDVLeer "Code") | [
Kurokat](https://github.com/Kurokat)
[💻](https://github.com/snipe/snipe-it/commits?author=Kurokat "Code") |
+| [
Kevin Köllmann](https://www.kevinkoellmann.de)
[💻](https://github.com/snipe/snipe-it/commits?author=koelle25 "Code") | [
sw-mreyes](https://github.com/sw-mreyes)
[💻](https://github.com/snipe/snipe-it/commits?author=sw-mreyes "Code") | [
Joel Pittet](https://pittet.ca)
[💻](https://github.com/snipe/snipe-it/commits?author=joelpittet "Code") | [
Eli Young](https://elyscape.com)
[💻](https://github.com/snipe/snipe-it/commits?author=elyscape "Code") | [
Raell Dottin](https://github.com/raelldottin)
[💻](https://github.com/snipe/snipe-it/commits?author=raelldottin "Code") | [
Tom Misilo](https://github.com/misilot)
[💻](https://github.com/snipe/snipe-it/commits?author=misilot "Code") | [
David Davenne](http://david.davenne.be)
[💻](https://github.com/snipe/snipe-it/commits?author=JuustoMestari "Code") |
+| [
Mark Stenglein](https://markstenglein.com)
[💻](https://github.com/snipe/snipe-it/commits?author=ocelotsloth "Code") | [
ajsy](https://github.com/ajsy)
[💻](https://github.com/snipe/snipe-it/commits?author=ajsy "Code") | [
Jan Kiesewetter](https://github.com/t3easy)
[💻](https://github.com/snipe/snipe-it/commits?author=t3easy "Code") | [
Tetrachloromethane250](https://github.com/Tetrachloromethane250)
[💻](https://github.com/snipe/snipe-it/commits?author=Tetrachloromethane250 "Code") | [
Lars Kajes](https://www.kajes.se/)
[💻](https://github.com/snipe/snipe-it/commits?author=kajes "Code") | [
Joly0](https://github.com/Joly0)
[💻](https://github.com/snipe/snipe-it/commits?author=Joly0 "Code") | [
theburger](https://github.com/limeless)
[💻](https://github.com/snipe/snipe-it/commits?author=limeless "Code") |
+| [
David Valin Alonso](https://github.com/deivishome)
[💻](https://github.com/snipe/snipe-it/commits?author=deivishome "Code") | [
andreaci](https://github.com/andreaci)
[💻](https://github.com/snipe/snipe-it/commits?author=andreaci "Code") | [
Jelle Sebreghts](http://www.jellesebreghts.be)
[💻](https://github.com/snipe/snipe-it/commits?author=Jelle-S "Code") | [
Michael Pietsch](https://github.com/Skywalker-11)
| [
Masudul Haque Shihab](https://github.com/sh1hab)
[💻](https://github.com/snipe/snipe-it/commits?author=sh1hab "Code") | [
Supapong Areeprasertkul](http://www.freedomdive.com/)
[💻](https://github.com/snipe/snipe-it/commits?author=zybersup "Code") | [
Peter Sarossy](https://github.com/psarossy)
[💻](https://github.com/snipe/snipe-it/commits?author=psarossy "Code") |
+| [
Renee Margaret McConahy](https://github.com/nepella)
[💻](https://github.com/snipe/snipe-it/commits?author=nepella "Code") | [
JohnnyPicnic](https://github.com/JohnnyPicnic)
[💻](https://github.com/snipe/snipe-it/commits?author=JohnnyPicnic "Code") | [
markbrule](https://github.com/markbrule)
[💻](https://github.com/snipe/snipe-it/commits?author=markbrule "Code") | [
Mike Campbell](https://github.com/mikecmpbll)
[💻](https://github.com/snipe/snipe-it/commits?author=mikecmpbll "Code") | [
tbrconnect](https://github.com/tbrconnect)
[💻](https://github.com/snipe/snipe-it/commits?author=tbrconnect "Code") | [
kcoyo](https://github.com/kcoyo)
[💻](https://github.com/snipe/snipe-it/commits?author=kcoyo "Code") | [
Travis Miller](https://travismiller.com/)
[💻](https://github.com/snipe/snipe-it/commits?author=travismiller "Code") |
+| [
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") | [
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/README.md b/README.md
index 34a5ee405c..062be60453 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@

-[](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/grokability/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
@@ -133,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/app/Console/Commands/CleanIncorrectCheckoutAcceptances.php b/app/Console/Commands/CleanIncorrectCheckoutAcceptances.php
new file mode 100644
index 0000000000..30dca964c7
--- /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 0000000000..a96a58a349
--- /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/FixUpAssignedTypeWithoutAssignedTo.php b/app/Console/Commands/FixUpAssignedTypeWithoutAssignedTo.php
new file mode 100644
index 0000000000..f368929f16
--- /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/LdapSync.php b/app/Console/Commands/LdapSync.php
index f90aeafd8d..381ae900d9 100644
--- a/app/Console/Commands/LdapSync.php
+++ b/app/Console/Commands/LdapSync.php
@@ -55,6 +55,8 @@ class LdapSync extends Command
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 @@ class LdapSync extends Command
"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;
@@ -182,7 +190,7 @@ class LdapSync extends Command
// 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.
@@ -234,9 +242,11 @@ class LdapSync extends Command
}
+ // 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] ?? '';
@@ -244,8 +254,13 @@ class LdapSync extends Command
$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] ?? '';
@@ -278,6 +293,9 @@ class LdapSync extends Command
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'];
}
@@ -293,6 +311,9 @@ class LdapSync extends Command
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'];
}
diff --git a/app/Console/Commands/LdapTroubleshooter.php b/app/Console/Commands/LdapTroubleshooter.php
index 5bb3cdd366..cb19ff8c53 100644
--- a/app/Console/Commands/LdapTroubleshooter.php
+++ b/app/Console/Commands/LdapTroubleshooter.php
@@ -6,6 +6,7 @@ use Illuminate\Console\Command;
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 @@ class LdapTroubleshooter extends Command
$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 @@ class LdapTroubleshooter extends Command
$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 @@ class LdapTroubleshooter extends Command
}
}
//$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 @@ class LdapTroubleshooter extends Command
$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);
}
- $raw_ips[]=$ip['ip'];
- if($ip['ip'] == "127.0.0.1") {
+ $this->debugout("IP's? " . print_r($ips, true));
+ foreach ($ips as $ip) {
+ if (!isset($ip['ip'])) {
+ continue;
+ }
+ $raw_ips[] = $ip['ip'];
+ }
+ }
+ 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 @@ class LdapTroubleshooter extends Command
$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 @@ class LdapTroubleshooter extends Command
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 @@ class LdapTroubleshooter extends Command
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 STARTTLS");
+ $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, 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 @@ class LdapTroubleshooter extends Command
$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 @@ class LdapTroubleshooter extends Command
$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 @@ class LdapTroubleshooter extends Command
}
$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 @@ class LdapTroubleshooter extends Command
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 @@ class LdapTroubleshooter extends Command
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 @@ class LdapTroubleshooter extends Command
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 @@ class LdapTroubleshooter extends Command
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 @@ class LdapTroubleshooter extends Command
{
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 @@ class LdapTroubleshooter extends Command
}
}
+
+
}
diff --git a/app/Console/Commands/MoveUploadsToNewDisk.php b/app/Console/Commands/MoveUploadsToNewDisk.php
index a27b06feed..615c12a545 100644
--- a/app/Console/Commands/MoveUploadsToNewDisk.php
+++ b/app/Console/Commands/MoveUploadsToNewDisk.php
@@ -96,7 +96,7 @@ class MoveUploadsToNewDisk extends Command
$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 ef69d25a5b..8c6bc89a51 100644
--- a/app/Console/Commands/PaveIt.php
+++ b/app/Console/Commands/PaveIt.php
@@ -4,7 +4,7 @@ namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\CustomField;
-use Schema;
+use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\Command;
@@ -59,6 +59,9 @@ class PaveIt extends Command
'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.
@@ -66,8 +69,8 @@ class PaveIt extends Command
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);
});
}
@@ -84,8 +87,8 @@ class PaveIt extends Command
}
// 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 1dd2aaa51d..4db7bac147 100644
--- a/app/Console/Commands/Purge.php
+++ b/app/Console/Commands/Purge.php
@@ -62,19 +62,19 @@ class Purge extends Command
$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/RestoreFromBackup.php b/app/Console/Commands/RestoreFromBackup.php
index 0bee8dcdcf..b5109c25ec 100644
--- a/app/Console/Commands/RestoreFromBackup.php
+++ b/app/Console/Commands/RestoreFromBackup.php
@@ -243,12 +243,15 @@ class RestoreFromBackup extends Command
$private_dirs = [
'storage/private_uploads/accessories',
'storage/private_uploads/assetmodels',
+ 'storage/private_uploads/maintenances',
+ 'storage/private_uploads/models',
'storage/private_uploads/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 @@ class RestoreFromBackup extends Command
];
$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',
@@ -328,9 +332,9 @@ class RestoreFromBackup extends Command
}
}
}
- $good_extensions = ['png', 'gif', 'jpg', 'svg', 'jpeg', 'doc', 'docx', 'pdf', 'txt',
- 'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico', 'avif'
- ];
+
+ $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) {
diff --git a/app/Console/Commands/SendAcceptanceReminder.php b/app/Console/Commands/SendAcceptanceReminder.php
index 250b08abf9..67efecbb34 100644
--- a/app/Console/Commands/SendAcceptanceReminder.php
+++ b/app/Console/Commands/SendAcceptanceReminder.php
@@ -77,7 +77,7 @@ class SendAcceptanceReminder extends Command
if(!$email){
$no_email_list[] = [
'id' => $acceptance->assignedTo?->id,
- 'name' => $acceptance->assignedTo?->present()->fullName(),
+ 'name' => $acceptance->assignedTo?->display_name,
];
} else {
$count++;
@@ -99,8 +99,11 @@ class SendAcceptanceReminder extends Command
foreach ($no_email_list as $user) {
$rows[] = [$user['id'], $user['name']];
}
- $this->info("The following users do not have an email address:");
- $this->table($headers, $rows);
+
+ 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/TestLocationsFMCS.php b/app/Console/Commands/TestLocationsFMCS.php
index f14c78063a..70eface2de 100644
--- a/app/Console/Commands/TestLocationsFMCS.php
+++ b/app/Console/Commands/TestLocationsFMCS.php
@@ -27,7 +27,7 @@ class TestLocationsFMCS extends Command
public function handle()
{
$this->info('This script checks for company ID inconsistencies if Full Multiple Company Support with scoped locations will be used.');
- $this->info('This could take few moments if have a very large dataset.');
+ $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
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 664c8edc62..ca85459b67 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -19,7 +19,7 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
- if(Setting::getSettings()->alerts_enabled === 1) {
+ if(Setting::getSettings()?->alerts_enabled === 1) {
$schedule->command('snipeit:inventory-alerts')->daily();
$schedule->command('snipeit:expiring-alerts')->daily();
$schedule->command('snipeit:expected-checkin')->daily();
diff --git a/app/Events/CheckoutableCheckedIn.php b/app/Events/CheckoutableCheckedIn.php
index 48aed2a64d..fedbd49fbe 100644
--- a/app/Events/CheckoutableCheckedIn.php
+++ b/app/Events/CheckoutableCheckedIn.php
@@ -28,7 +28,7 @@ class CheckoutableCheckedIn
$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/Exceptions/Handler.php b/app/Exceptions/Handler.php
index e340d70b09..d68418ce59 100644
--- a/app/Exceptions/Handler.php
+++ b/app/Exceptions/Handler.php
@@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Log;
use Throwable;
use JsonException;
use Carbon\Exceptions\InvalidFormatException;
+use Illuminate\Http\Exceptions\ThrottleRequestsException;
class Handler extends ExceptionHandler
{
@@ -107,27 +108,43 @@ class Handler extends ExceptionHandler
$statusCode = $e->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 = strtolower(implode(" ", preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel())))));
+ $model_name = trim(strtolower(implode(" ", preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel()))))));
$route = str_plural(strtolower(last(explode('\\', $e->getModel())))).'.index';
// Sigh.
@@ -143,6 +160,8 @@ class Handler extends ExceptionHandler
$route = 'maintenances.index';
} elseif ($route === 'licenseseats.index') {
$route = 'licenses.index';
+ } elseif (($route === 'customfieldsets.index') || ($route === 'customfields.index')) {
+ $route = 'fields.index';
}
return redirect()
@@ -201,6 +220,7 @@ class Handler extends ExceptionHandler
*/
public function register()
{
+
$this->reportable(function (Throwable $e) {
//
});
diff --git a/app/Helpers/Helper.php b/app/Helpers/Helper.php
index 3352f0a6a0..5e954a24c5 100644
--- a/app/Helpers/Helper.php
+++ b/app/Helpers/Helper.php
@@ -13,6 +13,7 @@ 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;
@@ -722,8 +723,8 @@ class Helper
// 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.
@@ -876,6 +877,48 @@ class Helper
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;
+ }
+
+ return false;
+ }
+
/**
* Walks through the permissions in the permissions config file and determines if
* permissions are granted based on a $selected_arr array.
@@ -1154,22 +1197,42 @@ class Helper
'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)) {
@@ -1313,25 +1376,24 @@ class Helper
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';
}
}
@@ -1481,60 +1543,62 @@ class Helper
}
- 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', $request->assigned_user ?? $checkedInFrom);
- case 'location':
- return route('locations.show', $request->assigned_location ?? $checkedInFrom);
- case 'asset':
- return route('hardware.show', $request->assigned_asset ?? $checkedInFrom);
- }
+ 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'));
}
@@ -1562,6 +1626,11 @@ class Helper
$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) {
@@ -1600,14 +1669,17 @@ class Helper
$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,
- str_replace('App\\Models\\', '', $item->assigned_type) ?? null,
+ $item->assigned_type ? str_replace('App\\Models\\', '', $item->assigned_type) : null,
$item->company_id ?? null,
$item->company->name ?? null,
// $item->defaultLoc->id ?? null,
@@ -1619,6 +1691,15 @@ class Helper
$location_company ?? null,
];
+ $count++;
+
+ // Bail early if this is not being run via artisan
+ if ((!$artisan) && ($count > 0)) {
+ return $mismatched;
+ }
+
+
+
}
}
}
diff --git a/app/Helpers/IconHelper.php b/app/Helpers/IconHelper.php
index 7c8e2a7456..8172f2bbbb 100644
--- a/app/Helpers/IconHelper.php
+++ b/app/Helpers/IconHelper.php
@@ -16,6 +16,7 @@ class IconHelper
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 @@ class IconHelper
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':
@@ -151,6 +154,7 @@ class IconHelper
case 'location':
return 'fas fa-map-marker-alt';
case 'superadmin':
+ case 'admin':
return 'fas fa-crown';
case 'print':
return 'fa-solid fa-print';
diff --git a/app/Helpers/StorageHelper.php b/app/Helpers/StorageHelper.php
index 47700f913a..cbd801d302 100644
--- a/app/Helpers/StorageHelper.php
+++ b/app/Helpers/StorageHelper.php
@@ -16,38 +16,84 @@ class StorageHelper
$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',
- 'gif',
- 'svg',
'avif',
- 'webp',
+ 'gif',
+ 'gif',
+ 'jpg',
+ 'mov',
+ 'mp3',
+ 'mp4',
+ 'ogg',
+ 'pdf',
'png',
+ 'svg',
+ 'wav',
+ 'webm',
+ 'webp',
];
@@ -59,10 +105,24 @@ class StorageHelper
}
+ 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 bc1ac56fc9..00cfce0516 100755
--- a/app/Http/Controllers/Accessories/AccessoriesController.php
+++ b/app/Http/Controllers/Accessories/AccessoriesController.php
@@ -77,13 +77,30 @@ class AccessoriesController extends Controller
$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());
@@ -113,11 +130,12 @@ class AccessoriesController extends Controller
$this->authorize('create', Accessory::class);
$cloned = clone $accessory;
+ $accessory_to_clone = $accessory;
$cloned->id = null;
$cloned->deleted_at = '';
- $cloned->location_id = null;
return view('accessories/edit')
+ ->with('cloned_model', $accessory_to_clone)
->with('item', $cloned);
}
@@ -167,7 +185,8 @@ class AccessoriesController extends Controller
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'));
@@ -220,7 +239,10 @@ class AccessoriesController extends Controller
*/
public function show(Accessory $accessory) : View | RedirectResponse
{
- $accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessory->id);
+ $accessory->loadCount('checkouts as checkouts_count');
+
+ $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 9dbb16d83a..0000000000
--- 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 ab24200d78..a7655a278b 100644
--- a/app/Http/Controllers/Accessories/AccessoryCheckinController.php
+++ b/app/Http/Controllers/Accessories/AccessoryCheckinController.php
@@ -78,7 +78,8 @@ class AccessoryCheckinController extends Controller
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 58ce787245..9ed8c0fe45 100644
--- a/app/Http/Controllers/Accessories/AccessoryCheckoutController.php
+++ b/app/Http/Controllers/Accessories/AccessoryCheckoutController.php
@@ -71,6 +71,7 @@ class AccessoryCheckoutController extends Controller
$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 @@ class AccessoryCheckoutController extends Controller
// 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 b20f7d97e8..f79ec1842f 100644
--- a/app/Http/Controllers/Account/AcceptanceController.php
+++ b/app/Http/Controllers/Account/AcceptanceController.php
@@ -7,6 +7,7 @@ use App\Events\CheckoutDeclined;
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\License;
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 @@ class AcceptanceController extends Controller
}
}
+
+ $assigned_user = User::find($acceptance->assigned_to_id);
// this is horrible
switch($acceptance->checkoutable_type){
case 'App\Models\Asset':
@@ -157,35 +163,30 @@ class AcceptanceController extends Controller
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') {
@@ -226,11 +227,12 @@ class AcceptanceController extends Controller
'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!='') {
@@ -240,8 +242,21 @@ class AcceptanceController extends Controller
}
$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);
}
@@ -333,10 +348,29 @@ class AcceptanceController extends Controller
$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 90486b40f2..7933b19057 100644
--- a/app/Http/Controllers/Api/AccessoriesController.php
+++ b/app/Http/Controllers/Api/AccessoriesController.php
@@ -288,32 +288,42 @@ class AccessoriesController extends Controller
'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 @@ class AccessoriesController extends Controller
$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 fabe9ebbb3..0000000000
--- 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(Asset $asset, Request $request) : JsonResponse | array
- {
-
- $this->authorize('view', $asset);
-
- $allowed_columns =
- [
- 'id',
- 'filename',
- 'eol',
- 'notes',
- 'created_at',
- 'updated_at',
- ];
-
- $files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')->where('item_type', '=', Asset::class)->where('item_id', '=', $asset->id);
-
- if ($request->filled('search')) {
- $files = $files->TextSearch($request->input('search'));
- }
-
- // Make sure the offset and limit are actually integers and do not exceed system limits
- $offset = ($request->input('offset') > $files->count()) ? $files->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';
- $files = $files->orderBy($sort, $order);
-
- $files = $files->skip($offset)->take($limit)->get();
- return (new UploadedFilesTransformer())->transformFiles($files, $files->count());
-
- }
-
- /**
- * 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(Asset $asset, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
- {
-
- // 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(Asset $asset, $fileId = null) : JsonResponse
- {
-
- $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/AssetModelFilesController.php b/app/Http/Controllers/Api/AssetModelFilesController.php
deleted file mode 100644
index 7f0f06c635..0000000000
--- 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 954da30870..8b8bc01124 100644
--- a/app/Http/Controllers/Api/AssetModelsController.php
+++ b/app/Http/Controllers/Api/AssetModelsController.php
@@ -50,6 +50,7 @@ class AssetModelsController extends Controller
'fieldset',
'deleted_at',
'updated_at',
+ 'require_serial',
];
$assetmodels = AssetModel::select([
@@ -69,6 +70,7 @@ class AssetModelsController extends Controller
'models.fieldset_id',
'models.deleted_at',
'models.updated_at',
+ 'models.require_serial'
])
->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues', 'adminuser')
->withCount('assets as assets_count');
@@ -85,6 +87,12 @@ class AssetModelsController extends Controller
$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'));
}
@@ -148,7 +156,7 @@ class AssetModelsController extends Controller
$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()));
@@ -201,7 +209,7 @@ class AssetModelsController extends Controller
$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.
@@ -216,7 +224,7 @@ class AssetModelsController extends Controller
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 bd95c9cce5..bfe071a6b5 100644
--- a/app/Http/Controllers/Api/AssetsController.php
+++ b/app/Http/Controllers/Api/AssetsController.php
@@ -114,17 +114,23 @@ class AssetsController extends Controller
'byod',
'asset_eol_date',
'requestable',
+ 'jobtitle',
];
+ $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 = [];
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.*')
@@ -140,6 +146,7 @@ class AssetsController extends Controller
'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.
@@ -298,9 +305,15 @@ class AssetsController extends Controller
if ($request->input('requestable') == 'true') {
$assets->where('assets.requestable', '=', '1');
}
-
+
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')) {
@@ -389,6 +402,9 @@ class AssetsController extends Controller
case 'assigned_to':
$assets->OrderAssigned($order);
break;
+ case 'jobtitle':
+ $assets->OrderByJobTitle($order);
+ break;
case 'created_by':
$assets->OrderByCreatedByName($order);
break;
@@ -594,7 +610,7 @@ class AssetsController extends Controller
$asset->use_text = $asset->present()->fullName;
if (($asset->checkedOutToUser()) && ($asset->assigned)) {
- $asset->use_text .= ' → ' . $asset->assigned->getFullNameAttribute();
+ $asset->use_text .= ' → ' . $asset->assigned->display_name;
}
@@ -695,7 +711,9 @@ class AssetsController extends Controller
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);
@@ -1133,12 +1151,12 @@ class AssetsController extends Controller
}
}
- // Validate custom fields
- Validator::make($asset->toArray(), $asset->customFieldValidationRules())->validate();
+ // 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', null, $asset->getErrors()));
+ return response()->json(Helper::formatStandardApiResponse('error', ['asset_tag' => $asset->asset_tag], $asset->getErrors()));
}
diff --git a/app/Http/Controllers/Api/CategoriesController.php b/app/Http/Controllers/Api/CategoriesController.php
index 319b51dd11..85cb70b126 100644
--- a/app/Http/Controllers/Api/CategoriesController.php
+++ b/app/Http/Controllers/Api/CategoriesController.php
@@ -56,7 +56,7 @@ class CategoriesController extends Controller
'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 @@ class CategoriesController extends Controller
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/CompaniesController.php b/app/Http/Controllers/Api/CompaniesController.php
index fd7f57ddce..aee38301f4 100644
--- a/app/Http/Controllers/Api/CompaniesController.php
+++ b/app/Http/Controllers/Api/CompaniesController.php
@@ -43,7 +43,10 @@ class CompaniesController extends Controller
$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 @@ class CompaniesController extends Controller
{
$this->authorize('view', Company::class);
$company = Company::findOrFail($id);
+ $this->authorize('view', $company);
return (new CompaniesTransformer)->transformCompany($company);
}
@@ -136,6 +140,7 @@ class CompaniesController extends Controller
{
$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 @@ class CompaniesController extends Controller
'companies.image',
]);
+
if ($request->filled('search')) {
$companies = $companies->where('companies.name', 'LIKE', '%'.$request->get('search').'%');
}
diff --git a/app/Http/Controllers/Api/ConsumablesController.php b/app/Http/Controllers/Api/ConsumablesController.php
index 7bddde070c..e163f080aa 100644
--- a/app/Http/Controllers/Api/ConsumablesController.php
+++ b/app/Http/Controllers/Api/ConsumablesController.php
@@ -228,11 +228,16 @@ class ConsumablesController extends Controller
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/ImportController.php b/app/Http/Controllers/Api/ImportController.php
index 41597cd2c5..79bffd1206 100644
--- a/app/Http/Controllers/Api/ImportController.php
+++ b/app/Http/Controllers/Api/ImportController.php
@@ -195,7 +195,7 @@ class ImportController extends Controller
// 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');
}
@@ -234,6 +234,15 @@ class ImportController extends Controller
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 d8a0dc119b..247f71ff26 100644
--- a/app/Http/Controllers/Api/LicenseSeatsController.php
+++ b/app/Http/Controllers/Api/LicenseSeatsController.php
@@ -29,12 +29,21 @@ class LicenseSeatsController extends Controller
$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();
diff --git a/app/Http/Controllers/Api/LocationsController.php b/app/Http/Controllers/Api/LocationsController.php
index 638765928b..b5c911a6ca 100644
--- a/app/Http/Controllers/Api/LocationsController.php
+++ b/app/Http/Controllers/Api/LocationsController.php
@@ -87,7 +87,8 @@ class LocationsController extends Controller
->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) {
@@ -218,6 +219,7 @@ class LocationsController extends Controller
'locations.updated_at',
'locations.image',
'locations.currency',
+ 'locations.company_id',
'locations.notes',
])
->withCount('assignedAssets as assigned_assets_count')
diff --git a/app/Http/Controllers/Api/AssetMaintenancesController.php b/app/Http/Controllers/Api/MaintenancesController.php
similarity index 75%
rename from app/Http/Controllers/Api/AssetMaintenancesController.php
rename to app/Http/Controllers/Api/MaintenancesController.php
index b4e9b44196..86f561c86c 100644
--- a/app/Http/Controllers/Api/AssetMaintenancesController.php
+++ b/app/Http/Controllers/Api/MaintenancesController.php
@@ -4,11 +4,11 @@ namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
-use App\Http\Transformers\AssetMaintenancesTransformer;
+use App\Http\Requests\ImageUploadRequest;
+use App\Http\Transformers\MaintenancesTransformer;
use App\Models\Asset;
-use App\Models\AssetMaintenance;
+use App\Models\Maintenance;
use App\Models\Company;
-use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
@@ -18,13 +18,13 @@ use Illuminate\Http\JsonResponse;
*
* @version v2.0
*/
-class AssetMaintenancesController extends Controller
+class MaintenancesController extends Controller
{
/**
* Generates the JSON response for asset maintenances listing view.
*
- * @see AssetMaintenancesController::getIndex() method that generates view
+ * @see MaintenancesController::getIndex() method that generates view
* @author Vincent Sposato
* @version v1.0
* @since [v1.8]
@@ -33,7 +33,7 @@ class AssetMaintenancesController extends Controller
{
$this->authorize('view', Asset::class);
- $maintenances = AssetMaintenance::select('asset_maintenances.*')
+ $maintenances = Maintenance::select('maintenances.*')
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.assetstatus', 'adminuser');
if ($request->filled('search')) {
@@ -45,11 +45,11 @@ class AssetMaintenancesController extends Controller
}
if ($request->filled('supplier_id')) {
- $maintenances->where('asset_maintenances.supplier_id', '=', $request->input('supplier_id'));
+ $maintenances->where('maintenances.supplier_id', '=', $request->input('supplier_id'));
}
if ($request->filled('created_by')) {
- $maintenances->where('asset_maintenances.created_by', '=', $request->input('created_by'));
+ $maintenances->where('maintenances.created_by', '=', $request->input('created_by'));
}
if ($request->filled('asset_maintenance_type')) {
@@ -63,7 +63,7 @@ class AssetMaintenancesController extends Controller
$allowed_columns = [
'id',
- 'title',
+ 'name',
'asset_maintenance_time',
'asset_maintenance_type',
'cost',
@@ -75,6 +75,7 @@ class AssetMaintenancesController extends Controller
'serial',
'created_by',
'supplier',
+ 'location',
'is_warranty',
'status_label',
];
@@ -98,6 +99,9 @@ class AssetMaintenancesController extends Controller
case 'serial':
$maintenances = $maintenances->OrderByAssetSerial($order);
break;
+ case 'location':
+ $maintenances = $maintenances->OrderLocationName($order);
+ break;
case 'status_label':
$maintenances = $maintenances->OrderStatusName($order);
break;
@@ -108,7 +112,7 @@ class AssetMaintenancesController extends Controller
$total = $maintenances->count();
$maintenances = $maintenances->skip($offset)->take($limit)->get();
- return (new AssetMaintenancesTransformer())->transformAssetMaintenances($maintenances, $total);
+ return (new MaintenancesTransformer())->transformMaintenances($maintenances, $total);
}
@@ -117,22 +121,23 @@ class AssetMaintenancesController extends Controller
/**
* Validates and stores the new asset maintenance
*
- * @see AssetMaintenancesController::getCreate() method for the form
+ * @see MaintenancesController::getCreate() method for the form
* @author Vincent Sposato
* @version v1.0
* @since [v1.8]
*/
- public function store(Request $request) : JsonResponse | array
+ public function store(ImageUploadRequest $request) : JsonResponse | array
{
$this->authorize('update', Asset::class);
+
// create a new model instance
- $maintenance = new AssetMaintenance();
+ $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/asset_maintenances/message.create.success')));
+ return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.create.success')));
}
@@ -153,11 +158,11 @@ class AssetMaintenancesController extends Controller
{
$this->authorize('update', Asset::class);
- if ($maintenance = AssetMaintenance::with('asset')->find($id)) {
+ 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/asset_maintenances/general.maintenance'), 'id' => $id, 'action' => trans('general.edit')])));
+ 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
@@ -168,13 +173,13 @@ class AssetMaintenancesController extends Controller
$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('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/asset_maintenances/general.maintenance'), 'id' => $id])));
+ return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('admin/maintenances/general.maintenance'), 'id' => $id])));
}
@@ -182,20 +187,20 @@ class AssetMaintenancesController extends Controller
* Delete an asset maintenance
*
* @author A. Gianotto
- * @param int $assetMaintenanceId
+ * @param int $maintenanceId
* @version v1.0
* @since [v4.0]
*/
- public function destroy($assetMaintenanceId) : JsonResponse | array
+ public function destroy($maintenanceId) : JsonResponse | array
{
$this->authorize('update', Asset::class);
// Check if the asset maintenance exists
- $assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
+ $maintenance = Maintenance::findOrFail($maintenanceId);
- $assetMaintenance->delete();
+ $maintenance->delete();
- return response()->json(Helper::formatStandardApiResponse('success', $assetMaintenance, trans('admin/asset_maintenances/message.delete.success')));
+ return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.delete.success')));
}
@@ -204,19 +209,19 @@ class AssetMaintenancesController extends Controller
* View an asset maintenance
*
* @author A. Gianotto
- * @param int $assetMaintenanceId
+ * @param int $maintenanceId
* @version v1.0
* @since [v4.0]
*/
- public function show($assetMaintenanceId) : JsonResponse | array
+ public function show($maintenanceId) : JsonResponse | array
{
$this->authorize('view', Asset::class);
- $assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
- if (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
+ $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 AssetMaintenancesTransformer())->transformAssetMaintenance($assetMaintenance);
+ return (new MaintenancesTransformer())->transformMaintenance($maintenance);
}
}
diff --git a/app/Http/Controllers/Api/NotesController.php b/app/Http/Controllers/Api/NotesController.php
new file mode 100644
index 0000000000..c1a16fd4d6
--- /dev/null
+++ b/app/Http/Controllers/Api/NotesController.php
@@ -0,0 +1,95 @@
+authorize('view', $asset);
+
+ // 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]));
+ }
+
+ /**
+ * 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 f7f91e094e..69db8aae04 100644
--- a/app/Http/Controllers/Api/ProfileController.php
+++ b/app/Http/Controllers/Api/ProfileController.php
@@ -4,15 +4,19 @@ namespace App\Http\Controllers\Api;
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 @@ class ProfileController extends Controller
}
+ /**
+ * 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 494c75104f..c03dddf6b7 100644
--- a/app/Http/Controllers/Api/ReportsController.php
+++ b/app/Http/Controllers/Api/ReportsController.php
@@ -5,6 +5,8 @@ namespace App\Http\Controllers\Api;
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 7eb28a4815..f24dd25b17 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 @@ class SettingsController extends Controller
})->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 @@ class SettingsController extends Controller
}
} 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 @@ class SettingsController extends Controller
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 @@ class SettingsController extends Controller
}
-}
\ No newline at end of file
+}
diff --git a/app/Http/Controllers/Api/SuppliersController.php b/app/Http/Controllers/Api/SuppliersController.php
index f752f22241..6784ee82c1 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 @@ class SuppliersController extends Controller
'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 @@ class SuppliersController extends Controller
$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 @@ class SuppliersController extends Controller
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 @@ class SuppliersController extends Controller
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 0000000000..a4a2de0520
--- /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 09dadbbd3c..1b06f5364f 100644
--- a/app/Http/Controllers/Api/UsersController.php
+++ b/app/Http/Controllers/Api/UsersController.php
@@ -6,6 +6,7 @@ use App\Helpers\Helper;
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;
@@ -19,9 +20,12 @@ use App\Models\Consumable;
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;
@@ -61,12 +65,14 @@ class UsersController extends Controller
'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 +86,12 @@ class UsersController extends Controller
'users.autoassign_licenses',
'users.website',
- ])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations')
+ ])->with('manager')
+ ->with('groups')
+ ->with('userloc')
+ ->with('company')
+ ->with('department')
+ ->with('createdBy')
->withCount([
'assets as assets_count' => function(Builder $query) {
$query->withoutTrashed();
@@ -101,10 +112,26 @@ class UsersController extends Controller
$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 +156,10 @@ class UsersController extends Controller
$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 +237,11 @@ class UsersController extends Controller
}
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 +290,7 @@ class UsersController extends Controller
[
'last_name',
'first_name',
+ 'display_name',
'email',
'jobtitle',
'username',
@@ -277,6 +309,7 @@ class UsersController extends Controller
'manages_users_count',
'manages_locations_count',
'phone',
+ 'mobile',
'address',
'city',
'state',
@@ -329,6 +362,7 @@ class UsersController extends Controller
'users.employee_num',
'users.first_name',
'users.last_name',
+ 'users.display_name',
'users.gravatar',
'users.avatar',
'users.email',
@@ -339,20 +373,17 @@ class UsersController extends Controller
$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.')';
@@ -404,9 +435,20 @@ class UsersController extends Controller
$user->password = $user->noPassword();
}
- app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
+ app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
if ($user->save()) {
+
+ if (($user->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 {
@@ -474,8 +516,29 @@ class UsersController extends Controller
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');
+ }
+
}
// We need to use has() instead of filled()
@@ -497,7 +560,7 @@ class UsersController extends Controller
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
@@ -676,7 +739,6 @@ class UsersController extends Controller
$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());
}
@@ -736,6 +798,25 @@ class UsersController extends Controller
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.
*
@@ -772,4 +853,37 @@ class UsersController extends Controller
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 8ab710b03b..0000000000
--- a/app/Http/Controllers/AssetMaintenancesController.php
+++ /dev/null
@@ -1,272 +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 = (int) $completionDate->diffInDays($startDate, true);
- }
-
- // 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(AssetMaintenance $maintenance) : View | RedirectResponse
- {
- $this->authorize('update', Asset::class);
- if ((!$maintenance->asset) || ($maintenance->asset->deleted_at!='')) {
- return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
- } elseif (! Company::isCurrentUserHasAccess($maintenance->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', $maintenance);
- }
-
- /**
- * 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, AssetMaintenance $maintenance) : View | RedirectResponse
- {
- $this->authorize('update', Asset::class);
-
- if ((!$maintenance->asset) || ($maintenance->asset->deleted_at!='')) {
- return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
- } elseif (! Company::isCurrentUserHasAccess($maintenance->asset)) {
- return static::getInsufficientPermissionsRedirect();
- }
-
- $maintenance->supplier_id = $request->input('supplier_id');
- $maintenance->is_warranty = $request->input('is_warranty');
- $maintenance->cost = $request->input('cost');
- $maintenance->notes = $request->input('notes');
-
- $asset = Asset::find(request('asset_id'));
-
- if (! Company::isCurrentUserHasAccess($asset)) {
- return static::getInsufficientPermissionsRedirect();
- }
-
- // Save the asset maintenance data
- $maintenance->asset_id = $request->input('asset_id');
- $maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
- $maintenance->title = $request->input('title');
- $maintenance->start_date = $request->input('start_date');
- $maintenance->completion_date = $request->input('completion_date');
-
- 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);
- }
-
- // Was the asset maintenance created?
- if ($maintenance->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($maintenance->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(AssetMaintenance $maintenance) : View | RedirectResponse
- {
- $this->authorize('view', Asset::class);
- if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
- return static::getInsufficientPermissionsRedirect();
- }
-
- return view('asset_maintenances/view')->with('assetMaintenance', $maintenance);
- }
-}
diff --git a/app/Http/Controllers/AssetModelsController.php b/app/Http/Controllers/AssetModelsController.php
index 0795588776..a74365298e 100755
--- a/app/Http/Controllers/AssetModelsController.php
+++ b/app/Http/Controllers/AssetModelsController.php
@@ -82,12 +82,26 @@ class AssetModelsController extends Controller
$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())) {
@@ -142,7 +156,7 @@ class AssetModelsController extends Controller
$model->category_id = $request->input('category_id');
$model->notes = $request->input('notes');
$model->requestable = $request->input('requestable', '0');
-
+ $model->require_serial = $request->input('require_serial', 0);
$model->fieldset_id = $request->input('fieldset_id');
if ($model->save()) {
@@ -271,7 +285,7 @@ class AssetModelsController extends Controller
->with('depreciation_list', Helper::depreciationList())
->with('item', $model)
->with('model_id', $model->id)
- ->with('clone_model', $cloned_model);
+ ->with('cloned_model', $cloned_model);
}
diff --git a/app/Http/Controllers/AssetModelsFilesController.php b/app/Http/Controllers/AssetModelsFilesController.php
deleted file mode 100644
index 14b2c1fc0b..0000000000
--- a/app/Http/Controllers/AssetModelsFilesController.php
+++ /dev/null
@@ -1,115 +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(AssetModel $model, $fileId = null) : StreamedResponse | Response | RedirectResponse | BinaryFileResponse
- {
-
- $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);
- }
-
- /**
- * Delete the associated file
- *
- * @author [A. Gianotto] []
- * @param int $modelId
- * @param int $fileId
- * @since [v1.0]
- */
- public function destroy(AssetModel $model, $fileId = null) : RedirectResponse
- {
- $rel_path = 'private_uploads/assetmodels';
- $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'));
-
- }
-}
diff --git a/app/Http/Controllers/Assets/AssetCheckinController.php b/app/Http/Controllers/Assets/AssetCheckinController.php
index cf881b57c7..62e8e59ca8 100644
--- a/app/Http/Controllers/Assets/AssetCheckinController.php
+++ b/app/Http/Controllers/Assets/AssetCheckinController.php
@@ -14,6 +14,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Log;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
+use Illuminate\Support\Facades\Validator;
class AssetCheckinController extends Controller
{
@@ -40,6 +41,14 @@ class AssetCheckinController extends Controller
if (!$asset->model) {
return redirect()->route('hardware.show', $asset->id)->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());
+ }
+
$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')]),
@@ -87,7 +96,6 @@ class AssetCheckinController extends Controller
});
$asset->expected_checkin = null;
- $asset->last_checkin = now();
$asset->assignedTo()->disassociate($asset);
$asset->accepted = null;
$asset->name = $request->get('name');
@@ -114,11 +122,14 @@ class AssetCheckinController extends Controller
$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]);
@@ -142,7 +153,8 @@ class AssetCheckinController extends Controller
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 4d8c9ffda2..bfbbbfcad4 100644
--- a/app/Http/Controllers/Assets/AssetCheckoutController.php
+++ b/app/Http/Controllers/Assets/AssetCheckoutController.php
@@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Session;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
+use Illuminate\Support\Facades\Validator;
class AssetCheckoutController extends Controller
{
@@ -36,6 +37,14 @@ class AssetCheckoutController extends Controller
->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())
@@ -56,6 +65,8 @@ class AssetCheckoutController extends Controller
*/
public function store(AssetCheckoutRequest $request, $assetId) : RedirectResponse
{
+
+
try {
// Check if the asset exists
if (! $asset = Asset::find($assetId)) {
@@ -72,6 +83,7 @@ class AssetCheckoutController extends Controller
$admin = auth()->user();
$target = $this->determineCheckoutTarget();
+ session()->put(['checkout_to_type' => $target]);
$asset = $this->updateAssetLocation($asset, $target);
@@ -114,7 +126,7 @@ class AssetCheckoutController extends Controller
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
diff --git a/app/Http/Controllers/Assets/AssetFilesController.php b/app/Http/Controllers/Assets/AssetFilesController.php
deleted file mode 100644
index cf119edddc..0000000000
--- a/app/Http/Controllers/Assets/AssetFilesController.php
+++ /dev/null
@@ -1,108 +0,0 @@
-]
- */
- public function store(UploadFileRequest $request, Asset $asset) : RedirectResponse
- {
-
- $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(Asset $asset, $fileId = null) : View | RedirectResponse | Response | StreamedResponse | BinaryFileResponse
- {
-
- $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', $asset)->with('error', trans('general.file_not_found'));
- }
-
- }
-
- return redirect()->route('hardware.show', $asset)->with('error', trans('general.log_record_not_found'));
-
-
- }
-
- /**
- * Delete the associated file
- *
- * @author [A. Gianotto] []
- * @param int $assetId
- * @param int $fileId
- * @since [v1.0]
- */
- public function destroy(Asset $asset, $fileId = null) : RedirectResponse
- {
- $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', $asset)->with('error', trans('general.log_record_not_found'));
- }
-
-}
diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php
index 6391a3dd9f..13201c51ec 100755
--- a/app/Http/Controllers/Assets/AssetsController.php
+++ b/app/Http/Controllers/Assets/AssetsController.php
@@ -110,17 +110,35 @@ class AssetsController extends Controller
// 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
@@ -132,7 +150,7 @@ class AssetsController extends Controller
$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');
@@ -149,7 +167,7 @@ class AssetsController extends Controller
$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
@@ -157,14 +175,21 @@ class AssetsController extends Controller
$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) {
@@ -188,14 +213,31 @@ class AssetsController extends Controller
// 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;
}
@@ -209,25 +251,32 @@ class AssetsController extends Controller
$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'))
+ 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)]));
}
}
@@ -248,6 +297,7 @@ class AssetsController extends Controller
public function edit(Asset $asset) : View | RedirectResponse
{
$this->authorize($asset);
+ session()->put('back_url', url()->previous());
return view('hardware/edit')
->with('item', $asset)
->with('statuslabel_list', Helper::statusLabelList())
@@ -391,6 +441,9 @@ class AssetsController extends Controller
$model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
+ 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')) {
@@ -410,11 +463,22 @@ class AssetsController extends Controller
}
}
}
+ 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,
+ ]);
- session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
- if ($asset->save()) {
+ //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 Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success', trans('admin/hardware/message.update.success'));
}
@@ -446,7 +510,7 @@ class AssetsController extends Controller
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]);
}
@@ -458,6 +522,7 @@ class AssetsController extends Controller
}
}
+
$asset->delete();
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.delete.success'));
@@ -519,7 +584,7 @@ class AssetsController extends Controller
{
$settings = Setting::getSettings();
- if (($settings->qr_code == '1') && ($settings->label2_2d_type !== 'none')) {
+ if ($settings->label2_2d_type !== 'none') {
if ($asset) {
$size = Helper::barcodeDimensions($settings->label2_2d_type);
@@ -618,8 +683,9 @@ class AssetsController extends Controller
*/
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 = '';
@@ -629,6 +695,7 @@ class AssetsController extends Controller
return view('hardware/edit')
->with('statuslabel_list', Helper::statusLabelList())
->with('statuslabel_types', Helper::statusTypeList())
+ ->with('cloned_model', $cloned_model)
->with('item', $cloned);
}
@@ -754,7 +821,7 @@ class AssetsController extends Controller
'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'],
@@ -782,7 +849,7 @@ class AssetsController extends Controller
'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,11 +944,20 @@ class AssetsController extends Controller
}
- public function audit(Asset $asset)
+ public function audit(Asset $asset): View | RedirectResponse
{
- $settings = Setting::getSettings();
$this->authorize('audit', Asset::class);
- $dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
+ $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');
}
@@ -890,6 +966,10 @@ class AssetsController extends Controller
$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');
@@ -924,8 +1004,8 @@ class AssetsController extends Controller
}
}
- // Validate custom fields
- Validator::make($asset->toArray(), $asset->customFieldValidationRules())->validate();
+ // 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()) {
@@ -965,7 +1045,7 @@ class AssetsController extends Controller
}
$asset->logAudit($request->input('note'), $request->input('location_id'), $file_name, $originalValues);
- return redirect()->route('assets.audit.due')->with('success', trans('admin/hardware/message.audit.success'));
+ 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 e433475d39..6c75ae06db 100644
--- a/app/Http/Controllers/Assets/BulkAssetsController.php
+++ b/app/Http/Controllers/Assets/BulkAssetsController.php
@@ -52,11 +52,26 @@ class BulkAssetsController extends Controller
}
$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 @@ class BulkAssetsController extends Controller
// 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 @@ class BulkAssetsController extends Controller
$models = $assets->unique('model_id');
$modelNames = [];
+
foreach($models as $model) {
$modelNames[] = $model->model->name;
}
@@ -145,7 +197,6 @@ class BulkAssetsController extends Controller
case 'edit':
$this->authorize('update', Asset::class);
-
return view('hardware/bulk')
->with('assets', $asset_ids)
->with('statuslabel_list', Helper::statusLabelList())
@@ -154,40 +205,7 @@ class BulkAssetsController extends Controller
}
}
- 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 @@ class BulkAssetsController extends Controller
$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 @@ class BulkAssetsController extends Controller
|| ($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 @@ class BulkAssetsController extends Controller
->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 @@ class BulkAssetsController extends Controller
}
}
+ if ($request->input('null_notes')=='1') {
+ $this->update_array['notes'] = null;
+ }
+
if ($request->filled('purchase_cost')) {
@@ -368,10 +408,12 @@ class BulkAssetsController extends Controller
// 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;
}
@@ -423,6 +465,7 @@ class BulkAssetsController extends Controller
}
/**
+ *
* Start all the custom fields shenanigans
*/
@@ -430,6 +473,15 @@ class BulkAssetsController extends Controller
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});
@@ -488,7 +540,13 @@ class BulkAssetsController extends Controller
} // 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'));
@@ -562,7 +620,10 @@ class BulkAssetsController extends Controller
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);
}
/**
@@ -576,6 +637,7 @@ class BulkAssetsController extends Controller
$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'));
@@ -594,13 +656,13 @@ class BulkAssetsController extends Controller
}
$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 = [];
@@ -608,6 +670,11 @@ class BulkAssetsController extends Controller
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?
@@ -651,4 +718,54 @@ class BulkAssetsController extends Controller
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 749abd6c1e..0b87865455 100644
--- a/app/Http/Controllers/Auth/LoginController.php
+++ b/app/Http/Controllers/Auth/LoginController.php
@@ -484,6 +484,7 @@ class LoginController extends Controller
}
$request->session()->regenerate(true);
+ $request->session()->forget('2fa_authed');
if ($request->session()->has('password_hash_'.Auth::getDefaultDriver())){
$request->session()->remove('password_hash_'.Auth::getDefaultDriver());
diff --git a/app/Http/Controllers/BulkAssetModelsController.php b/app/Http/Controllers/BulkAssetModelsController.php
index 5f64ea0838..c1ecf309fb 100644
--- a/app/Http/Controllers/BulkAssetModelsController.php
+++ b/app/Http/Controllers/BulkAssetModelsController.php
@@ -92,7 +92,9 @@ class BulkAssetModelsController extends Controller
$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 da8b57a552..3e902541b3 100755
--- a/app/Http/Controllers/CategoriesController.php
+++ b/app/Http/Controllers/CategoriesController.php
@@ -68,6 +68,7 @@ class CategoriesController extends Controller
$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();
@@ -121,6 +122,7 @@ class CategoriesController extends Controller
$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');
@@ -145,7 +147,7 @@ class CategoriesController extends Controller
{
$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'));
}
@@ -155,7 +157,6 @@ class CategoriesController extends Controller
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'));
}
diff --git a/app/Http/Controllers/CompaniesController.php b/app/Http/Controllers/CompaniesController.php
index 96a80e87e6..db6118d37f 100644
--- a/app/Http/Controllers/CompaniesController.php
+++ b/app/Http/Controllers/CompaniesController.php
@@ -123,11 +123,13 @@ final class CompaniesController extends Controller
*/
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')
diff --git a/app/Http/Controllers/Components/ComponentCheckinController.php b/app/Http/Controllers/Components/ComponentCheckinController.php
index 379882c3c5..b784576122 100644
--- a/app/Http/Controllers/Components/ComponentCheckinController.php
+++ b/app/Http/Controllers/Components/ComponentCheckinController.php
@@ -100,8 +100,8 @@ class ComponentCheckinController extends Controller
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 b40d592369..4abf426de3 100644
--- a/app/Http/Controllers/Components/ComponentCheckoutController.php
+++ b/app/Http/Controllers/Components/ComponentCheckoutController.php
@@ -120,6 +120,7 @@ class ComponentCheckoutController extends Controller
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 74594d312b..ff1b0061b4 100644
--- a/app/Http/Controllers/Components/ComponentsController.php
+++ b/app/Http/Controllers/Components/ComponentsController.php
@@ -88,10 +88,16 @@ class ComponentsController extends Controller
$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());
@@ -111,6 +117,7 @@ class ComponentsController extends Controller
{
$this->authorize('update', $component);
+ session()->put('back_url', url()->previous());
return view('components/edit')
->with('item', $component)
->with('category_type', 'component');
@@ -164,7 +171,8 @@ class ComponentsController extends Controller
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());
diff --git a/app/Http/Controllers/Components/ComponentsFilesController.php b/app/Http/Controllers/Components/ComponentsFilesController.php
deleted file mode 100644
index b5e30aa694..0000000000
--- 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 e08da41229..3e972e1085 100644
--- a/app/Http/Controllers/Consumables/ConsumableCheckoutController.php
+++ b/app/Http/Controllers/Consumables/ConsumableCheckoutController.php
@@ -111,6 +111,7 @@ class ConsumableCheckoutController extends Controller
// 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 c96c2db975..2601b69edd 100644
--- a/app/Http/Controllers/Consumables/ConsumablesController.php
+++ b/app/Http/Controllers/Consumables/ConsumablesController.php
@@ -7,7 +7,7 @@ use App\Http\Controllers\Controller;
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 @@ class ConsumablesController extends Controller
$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());
@@ -107,6 +124,7 @@ class ConsumablesController extends Controller
public function edit(Consumable $consumable) : View | RedirectResponse
{
$this->authorize($consumable);
+ session()->put('back_url', url()->previous());
return view('consumables/edit')
->with('item', $consumable)
->with('category_type', 'consumable');
@@ -160,7 +178,8 @@ class ConsumablesController extends Controller
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());
@@ -210,9 +229,10 @@ class ConsumablesController extends Controller
$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 545b008dc0..0000000000
--- 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 74fff19a37..266769ffb2 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 4c63179d59..73e17f8942 100644
--- a/app/Http/Controllers/CustomFieldsController.php
+++ b/app/Http/Controllers/CustomFieldsController.php
@@ -83,30 +83,30 @@ class CustomFieldsController extends Controller
{
$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),
- "display_checkin" => $request->get("display_checkin", 0),
- "display_checkout" => $request->get("display_checkout", 0),
- "display_audit" => $request->get("display_audit", 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()
]);
@@ -144,10 +144,9 @@ class CustomFieldsController extends Controller
*/
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
@@ -157,11 +156,12 @@ class CustomFieldsController extends Controller
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'));
}
@@ -172,20 +172,16 @@ class CustomFieldsController extends Controller
* @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->fieldset) && ($field->fieldset->count() > 0)) {
+ return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.in_use'));
}
-
- return redirect()->back()->withErrors(['message' => 'Field does not exist']);
+ $field->delete();
+ return redirect()->route("fields.index")
+ ->with("success", trans('admin/custom_fields/message.field.delete.success'));
}
@@ -198,7 +194,7 @@ class CustomFieldsController extends Controller
*/
public function edit(Request $request, CustomField $field) : View | RedirectResponse
{
- $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'])) {
@@ -228,7 +224,7 @@ class CustomFieldsController extends Controller
*/
public function update(CustomFieldRequest $request, CustomField $field) : RedirectResponse
{
- $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);
@@ -238,8 +234,8 @@ class CustomFieldsController extends Controller
$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");
@@ -254,9 +250,9 @@ class CustomFieldsController extends Controller
$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'){
@@ -265,7 +261,6 @@ class CustomFieldsController extends Controller
if ($field->save()) {
-
// Sync fields with fieldsets
$fieldset_array = $request->input('associate_fieldsets');
if ($request->has('associate_fieldsets') && (is_array($fieldset_array))) {
diff --git a/app/Http/Controllers/Licenses/LicenseCheckinController.php b/app/Http/Controllers/Licenses/LicenseCheckinController.php
index 4f7f7397ef..2bb2d5e68e 100644
--- a/app/Http/Controllers/Licenses/LicenseCheckinController.php
+++ b/app/Http/Controllers/Licenses/LicenseCheckinController.php
@@ -81,8 +81,10 @@ class LicenseCheckinController extends Controller
}
if($licenseSeat->assigned_to != null){
- $return_to = User::find($licenseSeat->assigned_to);
- session()->put('checkedInFrom', $return_to->id);
+ $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);
}
@@ -96,14 +98,17 @@ class LicenseCheckinController extends Controller
}
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(), $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
diff --git a/app/Http/Controllers/Licenses/LicenseCheckoutController.php b/app/Http/Controllers/Licenses/LicenseCheckoutController.php
index 564ce97a89..5b2d344ba5 100644
--- a/app/Http/Controllers/Licenses/LicenseCheckoutController.php
+++ b/app/Http/Controllers/Licenses/LicenseCheckoutController.php
@@ -39,6 +39,11 @@ class LicenseCheckoutController extends Controller
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats'));
}
+ // 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'));
}
@@ -70,17 +75,15 @@ class LicenseCheckoutController extends Controller
$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']);
@@ -89,7 +92,9 @@ class LicenseCheckoutController extends Controller
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 6ab3cb7703..0000000000
--- 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 53841e5030..b1728469b4 100755
--- a/app/Http/Controllers/Licenses/LicensesController.php
+++ b/app/Http/Controllers/Licenses/LicensesController.php
@@ -102,10 +102,15 @@ class LicensesController extends Controller
$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());
@@ -125,7 +130,7 @@ class LicensesController extends Controller
{
$this->authorize('update', $license);
-
+ session()->put('back_url', url()->previous());
$maintained_list = [
'' => 'Maintained',
'1' => 'Yes',
@@ -181,7 +186,8 @@ class LicensesController extends Controller
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());
@@ -311,13 +317,16 @@ class LicensesController extends Controller
$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 @@ class LicensesController extends Controller
$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 da4e6a7e44..46eff73df7 100755
--- a/app/Http/Controllers/LocationsController.php
+++ b/app/Http/Controllers/LocationsController.php
@@ -96,7 +96,18 @@ class LocationsController extends Controller
$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'));
@@ -275,9 +286,9 @@ class LocationsController extends Controller
// unset these values
$location->id = null;
- $location->image = null;
return view('locations/edit')
+ ->with('cloned_model', $location_to_clone)
->with('item', $location);
}
diff --git a/app/Http/Controllers/LocationsFilesController.php b/app/Http/Controllers/LocationsFilesController.php
deleted file mode 100644
index 3aaec0e089..0000000000
--- a/app/Http/Controllers/LocationsFilesController.php
+++ /dev/null
@@ -1,111 +0,0 @@
-]
- */
- public function store(UploadFileRequest $request, Location $location) : RedirectResponse
- {
- $this->authorize('update', $location);
-
- if ($request->hasFile('file')) {
-
- if (! Storage::exists('private_uploads/locations')) {
- Storage::makeDirectory('private_uploads/locations', 775);
- }
-
- foreach ($request->file('file') as $file) {
- $file_name = $request->handleFile('private_uploads/locations/','location-'.$location->id, $file);
- $location->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(Location $location, $fileId = null) : StreamedResponse | Response | RedirectResponse | BinaryFileResponse
- {
-
- $this->authorize('view', $location);
-
- if (! $log = Actionlog::find($fileId)) {
- return redirect()->back()->withFragment('files')->with('error', 'No matching file record');
- }
-
- $file = 'private_uploads/locations/'.$log->filename;
-
- if (! Storage::exists($file)) {
- return redirect()->back()->withFragment('files')->with('error', 'No matching file on server');
- }
-
- if (request('inline') == 'true') {
-
- $headers = [
- 'Content-Disposition' => 'inline',
- ];
-
- return Storage::download($file, $log->filename, $headers);
- }
-
- return StorageHelper::downloader($file);
- }
-
- /**
- * Delete the associated file
- *
- * @author [A. Gianotto] []
- * @param int $modelId
- * @param int $fileId
- * @since [v1.0]
- */
- public function destroy(Location $location, $fileId = null) : RedirectResponse
- {
- $rel_path = 'private_uploads/locations';
- $this->authorize('update', $location);
- $log = Actionlog::find($fileId);
-
- if ($log) {
-
- // This should be moved to purge
-// 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'));
-
- }
-}
diff --git a/app/Http/Controllers/MaintenancesController.php b/app/Http/Controllers/MaintenancesController.php
new file mode 100644
index 0000000000..e893b75f39
--- /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 985ec769fc..ac0b2818f4 100755
--- a/app/Http/Controllers/ManufacturersController.php
+++ b/app/Http/Controllers/ManufacturersController.php
@@ -6,6 +6,7 @@ use App\Http\Requests\ImageUploadRequest;
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]));
}
/**
diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php
index bbbc46da6f..a2fe612b6e 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.
@@ -220,7 +226,7 @@ class ProfileController extends Controller
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 @@ class ProfileController extends Controller
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 33afac5312..89a0a07a21 100644
--- a/app/Http/Controllers/ReportsController.php
+++ b/app/Http/Controllers/ReportsController.php
@@ -9,7 +9,7 @@ use App\Models\Actionlog;
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\Depreciation;
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 @@ class ReportsController extends Controller
$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()));
@@ -277,7 +275,7 @@ class ReportsController extends Controller
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 @@ class ReportsController extends Controller
$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 @@ class ReportsController extends Controller
$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');
}
@@ -737,6 +735,11 @@ class ReportsController extends Controller
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();
}
@@ -827,7 +830,7 @@ class ReportsController extends Controller
}
if ($request->filled('location')) {
- $row[] = ($asset->location) ? $asset->location->present()->name() : '';
+ $row[] = ($asset->location) ? $asset->location->display_name : '';
}
if ($request->filled('location_address')) {
@@ -840,7 +843,7 @@ class ReportsController extends Controller
}
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')) {
@@ -853,7 +856,7 @@ class ReportsController extends Controller
}
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();
}
@@ -1033,11 +1036,11 @@ class ReportsController extends Controller
* @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 @@ class ReportsController extends Controller
* @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 @@ class ReportsController extends Controller
$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)) {
+ $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($assetMaintenance->start_date), true);
+ ->diffInDays(Carbon::parse($maintenance->start_date), true);
} else {
- $improvementTime = (int) $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);
}
diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php
index a6de4466dd..8c57efa5eb 100644
--- a/app/Http/Controllers/SettingsController.php
+++ b/app/Http/Controllers/SettingsController.php
@@ -14,6 +14,7 @@ use App\Http\Requests\StoreLabelSettings;
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;
@@ -290,7 +291,6 @@ class SettingsController extends Controller
public function getSettings() : View
{
$setting = Setting::getSettings();
-
return view('settings/general', compact('setting'));
}
@@ -352,6 +352,7 @@ class SettingsController extends Controller
$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') != '') {
@@ -650,6 +651,7 @@ class SettingsController extends Controller
$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');
@@ -772,6 +774,7 @@ class SettingsController extends Controller
$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');
@@ -870,6 +873,7 @@ class SettingsController extends Controller
$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');
@@ -886,7 +890,12 @@ class SettingsController extends Controller
$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');
@@ -921,7 +930,7 @@ class SettingsController extends Controller
* @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'));
}
@@ -1081,6 +1090,7 @@ class SettingsController extends Controller
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
@@ -1108,6 +1118,7 @@ class SettingsController extends Controller
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) {
@@ -1187,7 +1198,7 @@ class SettingsController extends Controller
'--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,
@@ -1336,9 +1347,11 @@ class SettingsController extends Controller
'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/SuppliersController.php b/app/Http/Controllers/SuppliersController.php
index d96031a9aa..2a3c73bebe 100755
--- a/app/Http/Controllers/SuppliersController.php
+++ b/app/Http/Controllers/SuppliersController.php
@@ -4,7 +4,6 @@ namespace App\Http\Controllers;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Supplier;
-use Illuminate\Support\Facades\Auth;
use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View;
@@ -122,7 +121,7 @@ class SuppliersController extends Controller
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'));
}
@@ -130,8 +129,8 @@ class SuppliersController extends Controller
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) {
diff --git a/app/Http/Controllers/UploadedFilesController.php b/app/Http/Controllers/UploadedFilesController.php
new file mode 100644
index 0000000000..8a9f4304ad
--- /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/UserFilesController.php b/app/Http/Controllers/Users/UserFilesController.php
deleted file mode 100644
index 45bd0c6329..0000000000
--- a/app/Http/Controllers/Users/UserFilesController.php
+++ /dev/null
@@ -1,129 +0,0 @@
-]
- * @since [v1.6]
- */
- public function store(UploadFileRequest $request, User $user)
- {
- $this->authorize('update', $user);
- $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);
- }
-
- return redirect()->back()->withFragment('files')->with('success', trans('admin/users/message.upload.success'));
- }
-
-
- }
-
- /**
- * 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(User $user, $fileId = null)
- {
-
-
- if (empty($fileId)) {
- return redirect()->route('users.show')->with('error', 'Invalid file request');
- }
-
- $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'));
-
- }
-
-}
diff --git a/app/Http/Controllers/Users/UsersController.php b/app/Http/Controllers/Users/UsersController.php
index 613524d5c3..aa42415065 100755
--- a/app/Http/Controllers/Users/UsersController.php
+++ b/app/Http/Controllers/Users/UsersController.php
@@ -14,14 +14,9 @@ use App\Models\Group;
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 @@ class UsersController extends Controller
//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 @@ class UsersController extends Controller
$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,37 @@ class UsersController extends Controller
}
$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')]);
+ 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());
@@ -178,7 +181,7 @@ class UsersController extends Controller
* @author [A. Gianotto] []
* @since [v1.0]
* @param $permissions
- * @return \Illuminate\Contracts\View\View
+ * @return \Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
* @internal param int $id
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
@@ -186,10 +189,15 @@ class UsersController extends Controller
{
$this->authorize('update', User::class);
+ 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');
@@ -242,22 +250,18 @@ class UsersController extends Controller
}
}
- // 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);
@@ -267,8 +271,6 @@ class UsersController extends Controller
$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);
@@ -277,30 +279,49 @@ class UsersController extends Controller
$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)
->update(['location_id' => $user->location_id]);
- $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
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
@@ -308,7 +329,7 @@ class UsersController extends Controller
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'));
}
return redirect()->back()->withInput()->withErrors($user->getErrors());
@@ -434,7 +455,7 @@ class UsersController extends Controller
app('request')->request->set('permissions', $permissions);
- $user_to_clone = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($user->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);
@@ -449,6 +470,8 @@ class UsersController extends Controller
$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');
@@ -464,7 +487,7 @@ class UsersController extends Controller
->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);
}
@@ -506,6 +529,8 @@ class UsersController extends Controller
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'),
@@ -551,10 +576,12 @@ class UsersController extends Controller
($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(),
diff --git a/app/Http/Controllers/ViewAssetsController.php b/app/Http/Controllers/ViewAssetsController.php
index bbff6ba4f7..2b767650ad 100755
--- a/app/Http/Controllers/ViewAssetsController.php
+++ b/app/Http/Controllers/ViewAssetsController.php
@@ -27,50 +27,126 @@ use Exception;
class ViewAssetsController extends Controller
{
/**
- * Redirect to the profile page.
+ * Extract custom fields that should be displayed in user view.
+ *
+ * @param User $user
+ * @return array
+ */
+ private function extractCustomFields(User $user): array
+ {
+ $fieldArray = [];
+ foreach ($user->assets as $asset) {
+ if ($asset->model && $asset->model->fieldset) {
+ foreach ($asset->model->fieldset->fields as $field) {
+ if ($field->display_in_user_view == '1') {
+ $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() : View | RedirectResponse
+ public function getIndex(Request $request) : View | RedirectResponse
{
- $user = User::with(
+ $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);
+ }
+
+ // 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(auth()->id());
-
- $field_array = array();
-
- // Loop through all the custom fields that are applied to any model the user has assigned
- 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) {
-
- 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;
- }
-
- }
- }
+ 'licenses'
+ ])->find($selectedUserId);
+ // 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'));
}
- // 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);
+ // Process custom fields for the user being viewed
+ $fieldArray = $this->extractCustomFields($userToView);
- if (isset($user->id)) {
- return view('account/view-assets', compact('user', 'field_array' ))
- ->with('settings', Setting::getSettings());
- }
-
- // Redirect to the user management page
- return redirect()->route('users.index')
- ->with('error', trans('admin/users/message.user_not_found', $user->id));
+ // 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
+ ]);
}
/**
@@ -109,7 +185,7 @@ class ViewAssetsController extends Controller
$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();
diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
index b69e22e4f9..729fe75175 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 8e6c17b4e7..e740c76678 100644
--- a/app/Http/Middleware/SecurityHeaders.php
+++ b/app/Http/Middleware/SecurityHeaders.php
@@ -26,7 +26,6 @@ class SecurityHeaders
$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 0000000000..ac277e785c
--- /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/ImageUploadRequest.php b/app/Http/Requests/ImageUploadRequest.php
index abb0cee5f7..3a62212e36 100644
--- a/app/Http/Requests/ImageUploadRequest.php
+++ b/app/Http/Requests/ImageUploadRequest.php
@@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Storage;
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 @@ class ImageUploadRequest extends Request
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 @@ class ImageUploadRequest extends Request
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 @@ class ImageUploadRequest extends Request
// 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/SettingsSamlRequest.php b/app/Http/Requests/SettingsSamlRequest.php
index 2ab876141a..53a3521018 100644
--- a/app/Http/Requests/SettingsSamlRequest.php
+++ b/app/Http/Requests/SettingsSamlRequest.php
@@ -41,6 +41,7 @@ class SettingsSamlRequest extends FormRequest
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 @@ class SettingsSamlRequest extends FormRequest
}
}
- $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 @@ class SettingsSamlRequest extends FormRequest
];
$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 @@ class SettingsSamlRequest extends FormRequest
}
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 @@ class SettingsSamlRequest extends FormRequest
}
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 635d45cf89..0da47b0c72 100644
--- a/app/Http/Requests/StoreAssetModelRequest.php
+++ b/app/Http/Requests/StoreAssetModelRequest.php
@@ -19,6 +19,7 @@ class StoreAssetModelRequest extends ImageUploadRequest
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 fb7469ac88..66179ac739 100644
--- a/app/Http/Requests/StoreAssetRequest.php
+++ b/app/Http/Requests/StoreAssetRequest.php
@@ -39,7 +39,6 @@ class StoreAssetRequest extends ImageUploadRequest
$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 f9ede7ea89..2b6fd83c6f 100644
--- a/app/Http/Requests/StoreLabelSettings.php
+++ b/app/Http/Requests/StoreLabelSettings.php
@@ -37,8 +37,8 @@ class StoreLabelSettings extends FormRequest
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/StoreNotificationSettings.php b/app/Http/Requests/StoreNotificationSettings.php
index bf5f5b3d4e..f58d014c76 100644
--- a/app/Http/Requests/StoreNotificationSettings.php
+++ b/app/Http/Requests/StoreNotificationSettings.php
@@ -5,6 +5,7 @@ namespace App\Http\Requests;
use App\Models\Accessory;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
+use Illuminate\Validation\Rule;
class StoreNotificationSettings extends FormRequest
{
@@ -26,6 +27,9 @@ class StoreNotificationSettings extends FormRequest
return [
'alert_email' => 'email_array|nullable',
'admin_cc_email' => 'email_array|nullable',
+ 'admin_cc_always' => [
+ Rule::in('0', '1'),
+ ],
'alert_threshold' => 'numeric|nullable',
'alert_interval' => 'numeric|nullable|gt:0',
'audit_warning_days' => 'numeric|nullable',
diff --git a/app/Http/Requests/UploadFileRequest.php b/app/Http/Requests/UploadFileRequest.php
index e58f1a1be3..82f4a35be1 100644
--- a/app/Http/Requests/UploadFileRequest.php
+++ b/app/Http/Requests/UploadFileRequest.php
@@ -6,6 +6,7 @@ use App\Http\Traits\ConvertsBase64ToFiles;
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 @@ class UploadFileRequest extends Request
*/
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 b502490884..491871e122 100644
--- a/app/Http/Transformers/AccessoriesTransformer.php
+++ b/app/Http/Transformers/AccessoriesTransformer.php
@@ -44,7 +44,7 @@ class AccessoriesTransformer
'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'),
diff --git a/app/Http/Transformers/ActionlogsTransformer.php b/app/Http/Transformers/ActionlogsTransformer.php
index 702ea123d8..c62e3031f5 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\Contracts\Encryption\DecryptException;
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 @@ class ActionlogsTransformer
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 @@ class ActionlogsTransformer
// 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 @@ class ActionlogsTransformer
$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', ['asset' => $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', ['user' => $actionlog->item->id, 'fileId' => $actionlog->id]);
- }
- }
- }
- }
$array = [
'id' => (int) $actionlog->id,
@@ -158,13 +145,15 @@ class ActionlogsTransformer
'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 @@ class ActionlogsTransformer
'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/AssetModelsTransformer.php b/app/Http/Transformers/AssetModelsTransformer.php
index 2d47ca47db..793ab64597 100644
--- a/app/Http/Transformers/AssetModelsTransformer.php
+++ b/app/Http/Transformers/AssetModelsTransformer.php
@@ -65,10 +65,11 @@ class AssetModelsTransformer
'default_fieldset_values' => $default_field_values,
'eol' => ($assetmodel->eol > 0) ? $assetmodel->eol.' months' : 'None',
'requestable' => ($assetmodel->requestable == '1') ? true : false,
+ 'require_serial' => $assetmodel->require_serial,
'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'),
diff --git a/app/Http/Transformers/AssetsTransformer.php b/app/Http/Transformers/AssetsTransformer.php
index 8b16a9a726..e971f1e7ae 100644
--- a/app/Http/Transformers/AssetsTransformer.php
+++ b/app/Http/Transformers/AssetsTransformer.php
@@ -58,6 +58,13 @@ class AssetsTransformer
'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 +91,7 @@ class AssetsTransformer
'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,7 +108,7 @@ class AssetsTransformer
'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()),
];
@@ -155,6 +162,7 @@ class AssetsTransformer
'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 +210,7 @@ class AssetsTransformer
'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 +287,7 @@ class AssetsTransformer
'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),
diff --git a/app/Http/Transformers/CategoriesTransformer.php b/app/Http/Transformers/CategoriesTransformer.php
index 0d1834649d..348c5d4552 100644
--- a/app/Http/Transformers/CategoriesTransformer.php
+++ b/app/Http/Transformers/CategoriesTransformer.php
@@ -64,7 +64,7 @@ class CategoriesTransformer
'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 8ca5344de6..13f9a05e27 100644
--- a/app/Http/Transformers/CompaniesTransformer.php
+++ b/app/Http/Transformers/CompaniesTransformer.php
@@ -38,7 +38,7 @@ class CompaniesTransformer
'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 90d10ba9a5..f7f8c337bf 100644
--- a/app/Http/Transformers/ComponentsTransformer.php
+++ b/app/Http/Transformers/ComponentsTransformer.php
@@ -51,7 +51,7 @@ class ComponentsTransformer
'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'),
@@ -76,7 +76,7 @@ class ComponentsTransformer
$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 b31e31ac96..4c7dbf9cc5 100644
--- a/app/Http/Transformers/ConsumablesTransformer.php
+++ b/app/Http/Transformers/ConsumablesTransformer.php
@@ -25,7 +25,7 @@ class ConsumablesTransformer
$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 @@ class ConsumablesTransformer
'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/DatatablesTransformer.php b/app/Http/Transformers/DatatablesTransformer.php
index 0e69109391..2ec993536d 100644
--- a/app/Http/Transformers/DatatablesTransformer.php
+++ b/app/Http/Transformers/DatatablesTransformer.php
@@ -4,6 +4,10 @@ namespace App\Http\Transformers;
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 @@ class DatatablesTransformer
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 3d1e4c6f90..e072585a12 100644
--- a/app/Http/Transformers/DepartmentsTransformer.php
+++ b/app/Http/Transformers/DepartmentsTransformer.php
@@ -35,7 +35,7 @@ class DepartmentsTransformer
] : 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 64d4c88f7e..3b0d68392c 100644
--- a/app/Http/Transformers/DepreciationsTransformer.php
+++ b/app/Http/Transformers/DepreciationsTransformer.php
@@ -26,14 +26,14 @@ class DepreciationsTransformer
$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 9495aeeecc..7593926155 100644
--- a/app/Http/Transformers/GroupsTransformer.php
+++ b/app/Http/Transformers/GroupsTransformer.php
@@ -29,7 +29,7 @@ class GroupsTransformer
'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 9868876295..17025e7f9f 100644
--- a/app/Http/Transformers/LicenseSeatsTransformer.php
+++ b/app/Http/Transformers/LicenseSeatsTransformer.php
@@ -2,6 +2,7 @@
namespace App\Http\Transformers;
+use App\Helpers\Helper;
use App\Models\License;
use App\Models\LicenseSeat;
use Illuminate\Support\Facades\Gate;
@@ -11,20 +12,20 @@ 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),
@@ -35,14 +36,17 @@ class LicenseSeatsTransformer
'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),
@@ -50,10 +54,6 @@ class LicenseSeatsTransformer
'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 d8c6a56ca5..24822efeca 100644
--- a/app/Http/Transformers/LicensesTransformer.php
+++ b/app/Http/Transformers/LicensesTransformer.php
@@ -48,7 +48,7 @@ class LicensesTransformer
'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'),
@@ -62,7 +62,7 @@ class LicensesTransformer
'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 b1553c69f4..4965ff99d5 100644
--- a/app/Http/Transformers/LocationsTransformer.php
+++ b/app/Http/Transformers/LocationsTransformer.php
@@ -57,6 +57,10 @@ class LocationsTransformer
'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,
diff --git a/app/Http/Transformers/AssetMaintenancesTransformer.php b/app/Http/Transformers/MaintenancesTransformer.php
similarity index 79%
rename from app/Http/Transformers/AssetMaintenancesTransformer.php
rename to app/Http/Transformers/MaintenancesTransformer.php
index ab044260f7..c20c254869 100644
--- a/app/Http/Transformers/AssetMaintenancesTransformer.php
+++ b/app/Http/Transformers/MaintenancesTransformer.php
@@ -4,23 +4,24 @@ namespace App\Http\Transformers;
use App\Helpers\Helper;
use App\Models\Asset;
-use App\Models\AssetMaintenance;
+use App\Models\Maintenance;
use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Facades\Storage;
-class AssetMaintenancesTransformer
+class MaintenancesTransformer
{
- public function transformAssetMaintenances(Collection $assetmaintenances, $total)
+ public function transformMaintenances(Collection $maintenances, $total)
{
$array = [];
- foreach ($assetmaintenances as $assetmaintenance) {
- $array[] = self::transformAssetMaintenance($assetmaintenance);
+ foreach ($maintenances as $assetmaintenance) {
+ $array[] = self::transformMaintenance($assetmaintenance);
}
return (new DatatablesTransformer)->transformDatatables($array, $total);
}
- public function transformAssetMaintenance(AssetMaintenance $assetmaintenance)
+ public function transformMaintenance(Maintenance $assetmaintenance)
{
$array = [
'id' => (int) $assetmaintenance->id,
@@ -33,6 +34,7 @@ class AssetMaintenancesTransformer
'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,
@@ -48,7 +50,8 @@ class AssetMaintenancesTransformer
'name'=> ($assetmaintenance->asset->company->name) ? e($assetmaintenance->asset->company->name) : null,
] : null,
- 'title' => ($assetmaintenance->title) ? e($assetmaintenance->title) : 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),
@@ -59,7 +62,10 @@ class AssetMaintenancesTransformer
'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,
+ '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'),
@@ -67,11 +73,11 @@ class AssetMaintenancesTransformer
'completion_date' => Helper::getFormattedDateObject($assetmaintenance->completion_date, 'date'),
'user_id' => ($assetmaintenance->adminuser) ? [
'id' => $assetmaintenance->adminuser->id,
- 'name'=> e($assetmaintenance->adminuser->present()->fullName())
+ '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->present()->fullName()),
+ 'name'=> e($assetmaintenance->adminuser->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($assetmaintenance->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($assetmaintenance->updated_at, 'datetime'),
diff --git a/app/Http/Transformers/ManufacturersTransformer.php b/app/Http/Transformers/ManufacturersTransformer.php
index cf17eb7764..0d1373414c 100644
--- a/app/Http/Transformers/ManufacturersTransformer.php
+++ b/app/Http/Transformers/ManufacturersTransformer.php
@@ -40,7 +40,7 @@ class ManufacturersTransformer
'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 61c9e476a9..3660ff269e 100644
--- a/app/Http/Transformers/PredefinedKitsTransformer.php
+++ b/app/Http/Transformers/PredefinedKitsTransformer.php
@@ -34,7 +34,7 @@ class PredefinedKitsTransformer
'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 0000000000..3b8e58e133
--- /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 751edb7016..6409795994 100644
--- a/app/Http/Transformers/StatuslabelsTransformer.php
+++ b/app/Http/Transformers/StatuslabelsTransformer.php
@@ -32,7 +32,7 @@ class StatuslabelsTransformer
'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 1fdc93c193..750c969c63 100644
--- a/app/Http/Transformers/SuppliersTransformer.php
+++ b/app/Http/Transformers/SuppliersTransformer.php
@@ -45,6 +45,10 @@ class SuppliersTransformer
'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
index a18c9f9b65..e32aabc8e5 100644
--- a/app/Http/Transformers/UploadedFilesTransformer.php
+++ b/app/Http/Transformers/UploadedFilesTransformer.php
@@ -3,10 +3,10 @@
namespace App\Http\Transformers;
use App\Helpers\Helper;
+use App\Helpers\StorageHelper;
use App\Models\Actionlog;
-use App\Models\Asset;
-use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
class UploadedFilesTransformer
@@ -26,23 +26,27 @@ class UploadedFilesTransformer
{
$snipeModel = $file->item_type;
-
- // This will be used later as we extend out this transformer to handle more types of uploads
- if ($file->item_type == Asset::class) {
- $file_url = route('show/assetfile', [$file->item_id, $file->id]);
- }
-
$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),
- 'url' => $file_url,
+ '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'),
- 'updated_at' => Helper::getFormattedDateObject($file->updated_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'] = [
@@ -53,4 +57,5 @@ class UploadedFilesTransformer
return $array;
}
-}
+
+}
\ No newline at end of file
diff --git a/app/Http/Transformers/UsersTransformer.php b/app/Http/Transformers/UsersTransformer.php
index 3bf3ee9702..1de71615cf 100644
--- a/app/Http/Transformers/UsersTransformer.php
+++ b/app/Http/Transformers/UsersTransformer.php
@@ -22,23 +22,31 @@ class UsersTransformer
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 +58,16 @@ class UsersTransformer
'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 +83,7 @@ class UsersTransformer
'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'),
@@ -126,6 +139,7 @@ class UsersTransformer
'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 ad69028055..1d03d6f95b 100644
--- a/app/Importer/AssetImporter.php
+++ b/app/Importer/AssetImporter.php
@@ -80,7 +80,16 @@ class AssetImporter extends ItemImporter
$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 7cfd8a530d..b60ad1d0fb 100644
--- a/app/Importer/AssetModelImporter.php
+++ b/app/Importer/AssetModelImporter.php
@@ -66,6 +66,7 @@ class AssetModelImporter extends ItemImporter
$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 0000000000..39b477c96a
--- /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/Importer.php b/app/Importer/Importer.php
index 0d4b8d4932..a5142f4380 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 @@ abstract class Importer
} 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
@@ -298,6 +300,7 @@ abstract class Importer
'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 @@ abstract class Importer
$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 dfcd644c9d..92f44d991d 100644
--- a/app/Importer/ItemImporter.php
+++ b/app/Importer/ItemImporter.php
@@ -110,7 +110,7 @@ class ItemImporter extends Importer
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;
}
@@ -353,16 +353,27 @@ class ItemImporter extends Importer
* @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;
}
diff --git a/app/Importer/ManufacturerImporter.php b/app/Importer/ManufacturerImporter.php
new file mode 100644
index 0000000000..4c627e9d99
--- /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 0000000000..7878aff835
--- /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 77317b3d09..942f1cf4a2 100644
--- a/app/Importer/UserImporter.php
+++ b/app/Importer/UserImporter.php
@@ -7,7 +7,9 @@ use App\Models\Department;
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 @@ class UserImporter extends ItemImporter
// 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 @@ class UserImporter extends ItemImporter
$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 @@ class UserImporter extends ItemImporter
$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 @@ class UserImporter extends ItemImporter
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 @@ class UserImporter extends ItemImporter
// 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 @@ class UserImporter extends ItemImporter
}
$this->logError($user, 'User');
- return;
}
+
/**
* Fetch an existing department, or create new if it doesn't exist
*
diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php
index 2bf93afc94..9674bd63a3 100644
--- a/app/Listeners/CheckoutableListener.php
+++ b/app/Listeners/CheckoutableListener.php
@@ -4,14 +4,17 @@ namespace App\Listeners;
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\Setting;
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 @@ use Osama\LaravelTeamsNotification\TeamsNotification;
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,93 +74,71 @@ class CheckoutableListener
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));
+ $shouldSendEmailToUser = $this->shouldSendCheckoutEmailToUser($event->checkoutable);
+ $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress($acceptance);
+ $shouldSendWebhookNotification = $this->shouldSendWebhookNotification();
+
+ if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) {
+ return;
}
- $ccEmails = array_filter($adminCcEmailsArray);
- $mailable = $this->getCheckoutMailType($event, $acceptance);
- $notifiable = $this->getNotifiableUsers($event);
+ if ($shouldSendEmailToUser || $shouldSendEmailToAlertAddress) {
+ $mailable = $this->getCheckoutMailType($event, $acceptance);
+ $notifiable = $this->getNotifiableUser($event);
- // 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
- * 4. If the admin CC email is set, even if the item being checked out doesn't have an email address (location, etc)
- */
+ $notifiableHasEmail = $notifiable instanceof User && $notifiable->email;
- if ($event->checkoutable->requireAcceptance() || $event->checkoutable->getEula() ||
- $this->checkoutableShouldSendEmail($event)) {
+ $shouldSendEmailToUser = $shouldSendEmailToUser && $notifiableHasEmail;
+ [$to, $cc] = $this->generateEmailRecipients($shouldSendEmailToUser, $shouldSendEmailToAlertAddress, $notifiable);
- // Send a checkout email to the admin CC addresses, even if the target has no email
- if (!empty($ccEmails)) {
- Mail::to($ccEmails)->send($mailable);
- Log::info('Checkout Mail sent to CC addresses');
- }
-
- // Send a checkout email to the target if it has an email
- if (!empty($notifiable->email)) {
- Mail::to($notifiable)->send($mailable);
+ 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());
}
-
}
- } 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 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::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'));
}
}
-
-
-
/**
* Notify the user and post to webhook about the checked in checkoutable
- */
+ */
public function onCheckedIn($event)
{
Log::debug('onCheckedIn in the Checkoutable listener fired');
@@ -145,61 +147,54 @@ class CheckoutableListener
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();
+ $shouldSendEmailToUser = $this->checkoutableCategoryShouldSendEmail($event->checkoutable);
+ $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress();
+ $shouldSendWebhookNotification = $this->shouldSendWebhookNotification();
+ if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) {
+ return;
+ }
- foreach($acceptances as $acceptance){
- if($acceptance->isPending()){
- $acceptance->delete();
+ if ($shouldSendEmailToUser || $shouldSendEmailToAlertAddress) {
+ /**
+ * 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();
+ }
}
}
- }
- $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->getNotifiableUsers($event);
+ $mailable = $this->getCheckinMailType($event);
+ $notifiable = $this->getNotifiableUser($event);
- // 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
- * 4. If the admin CC email is set, even if the item being checked in doesn't have an email address (location, etc)
- */
+ $notifiableHasEmail = $notifiable instanceof User && $notifiable->email;
- // Send a checkout email to the admin's CC addresses, even if the target has no email
- if (!empty($ccEmails)) {
- Mail::to($ccEmails)->send($mailable);
- Log::info('Checkin Mail sent to CC addresses');
+ $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 a checkout email to the target if it has an email
- if (!empty($notifiable->email)) {
- Mail::to($notifiable)->send($mailable);
- Log::info('Checkin Mail sent to checkout target');
- }
- } catch (ClientException $e) {
- Log::debug("Exception caught during checkin email: " . $e->getMessage());
- } catch (Exception $e) {
- Log::debug("Exception caught during checkin email: " . $e->getMessage());
}
- // 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);
@@ -208,25 +203,24 @@ class CheckoutableListener
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::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'));
}
- }
+ }
/**
* Generates a checkout acceptance
@@ -239,6 +233,7 @@ class CheckoutableListener
if ($checkedOutToType != "App\Models\User") {
return null;
}
+
if (!$event->checkoutable->requireAcceptance()) {
return null;
}
@@ -246,15 +241,22 @@ class CheckoutableListener
$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)
@@ -268,17 +270,19 @@ class CheckoutableListener
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
*
@@ -303,6 +307,9 @@ class CheckoutableListener
case LicenseSeat::class:
$notificationClass = CheckoutLicenseSeatNotification::class;
break;
+ case Component::class:
+ $notificationClass = CheckoutComponentNotification::class;
+ break;
}
@@ -314,19 +321,21 @@ class CheckoutableListener
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);
@@ -341,7 +350,8 @@ class CheckoutableListener
* @param $event
* @return mixed
*/
- private function getNotifiableUsers($event){
+ private function getNotifiableUser($event)
+ {
// If it's assigned to an asset, get that asset's assignedTo object
if ($event->checkedOutTo instanceof Asset){
@@ -357,6 +367,7 @@ class CheckoutableListener
return $event->checkedOutTo;
}
}
+
private function webhookSelected(){
if(Setting::getSettings()->webhook_selected === 'slack' || Setting::getSettings()->webhook_selected === 'general'){
return 'slack';
@@ -365,60 +376,114 @@ class CheckoutableListener
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
{
- if(in_array(get_class($checkoutable), $this->skipNotificationsFor)) {
- return true;
- }
- //runs a check if the category wants to send checkin/checkout emails to users
- $category = match (true) {
- $checkoutable instanceof Asset => $checkoutable->model->category,
- $checkoutable instanceof Accessory,
- $checkoutable instanceof Consumable => $checkoutable->category,
- $checkoutable instanceof LicenseSeat => $checkoutable->license->category,
- default => null,
- };
-
- if (!$category->checkin_email) {
- return true;
- }
- return false;
+ return in_array(get_class($checkoutable), $this->skipNotificationsFor);
}
-
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/Livewire/CategoryEditForm.php b/app/Livewire/CategoryEditForm.php
index fd8bef6489..98e505c8df 100644
--- a/app/Livewire/CategoryEditForm.php
+++ b/app/Livewire/CategoryEditForm.php
@@ -6,6 +6,8 @@ use Livewire\Component;
class CategoryEditForm extends Component
{
+ public bool $alertOnResponse;
+
public $defaultEulaText;
public $eulaText;
diff --git a/app/Livewire/Importer.php b/app/Livewire/Importer.php
index 5f30dca7be..58da61a3bf 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 @@ class Importer extends Component
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 @@ class Importer extends Component
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 = [];
@@ -128,7 +135,7 @@ class Importer extends Component
//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 @@ class Importer extends Component
// 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,6 +178,9 @@ class Importer extends Component
'license' => trans('general.licenses'),
'location' => trans('general.locations'),
'user' => trans('general.users'),
+ 'supplier' => trans('general.suppliers'),
+ 'manufacturer' => trans('general.manufacturers'),
+ 'category' => trans('general.categories'),
];
/**
@@ -193,6 +203,7 @@ class Importer extends Component
];
$this->assets_fields = [
+ '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')]),
@@ -319,12 +330,16 @@ class Importer extends Component
'location' => trans('general.location'),
'manager_first_name' => trans('general.importer.manager_first_name'),
'manager_last_name' => trans('general.importer.manager_last_name'),
+ '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'),
'username' => trans('admin/users/table.username'),
+ 'display_name' => trans('admin/users/table.display_name'),
'vip' => trans('general.importer.vip'),
'website' => trans('general.website'),
'zip' => trans('general.zip'),
@@ -332,6 +347,7 @@ class Importer extends Component
$this->locations_fields = [
'id' => trans('general.id'),
+ 'name' => trans('general.name'),
'address' => trans('general.address'),
'address2' => trans('general.importer.address2'),
'city' => trans('general.city'),
@@ -340,13 +356,52 @@ class Importer extends Component
'ldap_ou' => trans('admin/locations/table.ldap_ou'),
'manager' => trans('general.importer.manager_full_name'),
'manager_username' => trans('general.importer.manager_username'),
- 'name' => trans('general.item_name_var', ['item' => trans('general.location')]),
'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 = [
'category' => trans('general.category'),
'eol' => trans('general.eol'),
@@ -357,6 +412,7 @@ class Importer extends Component
'model_number' => trans('general.model_no'),
'notes' => trans('general.item_notes', ['item' => trans('admin/hardware/form.model')]),
'requestable' => trans('admin/models/general.requestable'),
+ 'require_serial' => trans('admin/hardware/general.require_serial'),
];
@@ -371,6 +427,8 @@ class Importer extends Component
'consumable name',
'component name',
'name',
+ 'supplier name',
+ 'location name',
],
'item_no' => [
'item number',
@@ -429,6 +487,13 @@ class Importer extends Component
'username',
trans('general.importer.checked_out_to_username'),
],
+ 'display_name' =>
+ [
+ 'display name',
+ 'displayName',
+ 'display',
+ trans('admin/users/table.display_name'),
+ ],
'first_name' =>
[
'first name',
@@ -455,6 +520,13 @@ class Importer extends Component
'telephone',
'tel.',
],
+ 'mobile_number' =>
+ [
+ 'mobile',
+ 'mobile number',
+ 'cell',
+ 'cellphone',
+ ],
'serial' =>
[
@@ -464,6 +536,10 @@ class Importer extends Component
'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 0000000000..24beb9413f
--- /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 017e789060..e114c2278e 100644
--- a/app/Livewire/OauthClients.php
+++ b/app/Livewire/OauthClients.php
@@ -47,7 +47,7 @@ class OauthClients extends Component
{
// 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 7487f30961..535a83413f 100644
--- a/app/Livewire/SlackSettingsForm.php
+++ b/app/Livewire/SlackSettingsForm.php
@@ -71,12 +71,12 @@ class SlackSettingsForm extends Component
$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 @@ class SlackSettingsForm extends Component
$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 355c2f9f13..72f129dd6d 100644
--- a/app/Mail/CheckinAssetMail.php
+++ b/app/Mail/CheckinAssetMail.php
@@ -47,7 +47,7 @@ class CheckinAssetMail extends Mailable
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 0000000000..5b62a2c4b8
--- /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 0000000000..fba6dd3de2
--- /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 64c02e31ed..92ce84a2d6 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 @@ class CheckoutAccessoryMail extends Mailable
return new Envelope(
from: $from,
- subject: (trans('mail.Accessory_Checkout_Notification')),
+ subject: trans('mail.Accessory_Checkout_Notification'),
);
}
@@ -54,6 +56,17 @@ class CheckoutAccessoryMail extends Mailable
$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 @@ class CheckoutAccessoryMail extends Mailable
'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 7ac20861ed..0dcd568bab 100644
--- a/app/Mail/CheckoutAssetMail.php
+++ b/app/Mail/CheckoutAssetMail.php
@@ -4,6 +4,7 @@ namespace App\Mail;
use App\Helpers\Helper;
use App\Models\Asset;
+use App\Models\Location;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Bus\Queueable;
@@ -24,16 +25,17 @@ class CheckoutAssetMail extends Mailable
/**
* Create a new message instance.
+ * @throws \Exception
*/
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 = '';
@@ -76,6 +78,17 @@ class CheckoutAssetMail extends Mailable
$eula = method_exists($this->item, 'getEula') ? $this->item->getEula() : '';
$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)) {
@@ -91,7 +104,7 @@ class CheckoutAssetMail extends Mailable
'admin' => $this->admin,
'status' => $this->item->assetstatus?->name,
'note' => $this->note,
- 'target' => $this->target,
+ 'target' => $name,
'fields' => $fields,
'eula' => $eula,
'req_accept' => $req_accept,
@@ -116,7 +129,7 @@ class CheckoutAssetMail extends Mailable
private function getSubject(): string
{
if ($this->firstTimeSending) {
- return trans('mail.Asset_Checkout_Notification');
+ return trans('mail.Asset_Checkout_Notification', ['tag' => $this->item->asset_tag]);
}
return trans('mail.unaccepted_asset_reminder');
@@ -124,6 +137,9 @@ class CheckoutAssetMail extends Mailable
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');
}
diff --git a/app/Mail/CheckoutComponentMail.php b/app/Mail/CheckoutComponentMail.php
new file mode 100644
index 0000000000..e914d14196
--- /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 9462c6c332..f3688bae5a 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 @@ class CheckoutLicenseMail extends Mailable
$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/Models/Accessory.php b/app/Models/Accessory.php
index 039f8692f6..33a1c6a570 100755
--- a/app/Models/Accessory.php
+++ b/app/Models/Accessory.php
@@ -4,6 +4,8 @@ namespace App\Models;
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 @@ use Watson\Validating\ValidatingTrait;
/**
* 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,8 +57,8 @@ class Accessory extends SnipeModel
];
/**
- * Accessory validation rules
- */
+ * Accessory validation rules
+ */
public $rules = [
'name' => 'required|min:3|max:255',
'qty' => 'required|integer|min:1',
@@ -63,18 +66,18 @@ class Accessory extends SnipeModel
'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;
@@ -102,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()
@@ -137,7 +122,7 @@ class Accessory extends SnipeModel
* Sets the requestable attribute on the accessory
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return void
*/
public function setRequestableAttribute($value)
@@ -152,7 +137,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> company relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function company()
@@ -164,7 +149,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> location relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function location()
@@ -176,7 +161,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> category relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function category()
@@ -188,7 +173,7 @@ class Accessory extends SnipeModel
* 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()
@@ -217,8 +202,8 @@ class Accessory extends SnipeModel
*
* 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()
*/
@@ -235,7 +220,7 @@ class Accessory extends SnipeModel
* presenter or service provider
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return string
*/
public function getImageUrl()
@@ -251,7 +236,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> users relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function checkouts()
@@ -264,7 +249,7 @@ class Accessory extends SnipeModel
* 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()
@@ -276,7 +261,7 @@ class Accessory extends SnipeModel
* Checks whether or not the accessory has users
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return int
*/
public function hasUsers()
@@ -290,7 +275,7 @@ class Accessory extends SnipeModel
* Establishes the accessory -> manufacturer relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function manufacturer()
@@ -303,12 +288,12 @@ class Accessory extends SnipeModel
* 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;
}
/**
@@ -316,7 +301,7 @@ class Accessory extends SnipeModel
* accept it via email.
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return bool
*/
public function requireAcceptance()
@@ -329,7 +314,7 @@ class Accessory extends SnipeModel
* checks for a settings level EULA
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return string
*/
public function getEula()
@@ -349,7 +334,7 @@ class Accessory extends SnipeModel
* Check how many items within an accessory are checked out
*
* @author [A. Gianotto] []
- * @since [v5.0]
+ * @since [v5.0]
* @return int
*/
public function numCheckedOut()
@@ -366,7 +351,7 @@ class Accessory extends SnipeModel
* bad things happen.
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return int
*/
public function numRemaining()
@@ -381,8 +366,8 @@ class Accessory extends SnipeModel
/**
* 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)
{
@@ -408,8 +393,8 @@ class Accessory extends SnipeModel
* 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)
@@ -426,7 +411,6 @@ class Accessory extends SnipeModel
/**
* Query builder scope to order on created_by name
- *
*/
public function scopeOrderByCreatedByName($query, $order)
{
@@ -434,68 +418,68 @@ class Accessory extends SnipeModel
}
/**
- * 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 61ffcd08e5..9f49354389 100755
--- a/app/Models/AccessoryCheckout.php
+++ b/app/Models/AccessoryCheckout.php
@@ -16,7 +16,7 @@ use Watson\Validating\ValidatingTrait;
/**
* 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,7 +52,7 @@ class AccessoryCheckout extends Model
* 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()
@@ -64,7 +64,7 @@ class AccessoryCheckout extends Model
* 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 @@ class AccessoryCheckout extends Model
* 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 @@ class AccessoryCheckout extends Model
* 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
{
@@ -127,50 +127,64 @@ class AccessoryCheckout extends Model
* 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');
+ )->select('id');
- $locationQuery = Location::where(function ($query) use ($terms) {
- foreach ($terms as $term) {
- $search_str = '%' . $term . '%';
- $query->where('name', 'like', $search_str);
+ $locationQuery = Location::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);
+ $assetQuery = Asset::where(
+ function ($query) use ($terms) {
+ foreach ($terms as $term) {
+ $search_str = '%' . $term . '%';
+ $query->where('name', 'like', $search_str);
+ }
}
- })->select('id');
+ )->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);
+ $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);
+ }
+ }
+ );
return $query;
}
diff --git a/app/Models/Actionlog.php b/app/Models/Actionlog.php
index dd86ae25c6..59e7b8244a 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
{
@@ -69,11 +71,13 @@ class Actionlog extends SnipeModel
*/
protected $searchableRelations = [
'company' => ['name'],
- 'adminuser' => ['first_name','last_name','username', 'email'],
- 'user' => ['first_name','last_name','username', 'email'],
+ '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'],
@@ -96,24 +100,31 @@ class Actionlog extends SnipeModel
* 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;
}
- } elseif (auth()->user() && auth()->user()->company) {
- $actionlog->company_id = auth()->user()->company_id;
+
+ if ($actionlog->action_date == '') {
+ $actionlog->action_date = Carbon::now();
+ }
+
}
- });
+ );
}
@@ -121,7 +132,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> item relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function item()
@@ -133,7 +144,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> company relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function company()
@@ -146,7 +157,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> asset relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assets()
@@ -158,7 +169,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> license relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function licenses()
@@ -170,7 +181,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> consumable relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function consumables()
@@ -182,7 +193,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> consumable relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function accessories()
@@ -194,7 +205,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> components relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function components()
@@ -206,7 +217,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> item type relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function itemType()
@@ -222,7 +233,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> target type relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function targetType()
@@ -235,25 +246,12 @@ class Actionlog extends SnipeModel
}
- /**
- * 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()
@@ -265,20 +263,20 @@ class Actionlog extends SnipeModel
* 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()
@@ -291,7 +289,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> target relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function target()
@@ -303,7 +301,7 @@ class Actionlog extends SnipeModel
* Establishes the actionlog -> location relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function location()
@@ -316,7 +314,7 @@ class Actionlog extends SnipeModel
* 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')
@@ -334,7 +332,7 @@ class Actionlog extends SnipeModel
* Saves the log record with the action type
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return bool
*/
public function logaction($actiontype)
@@ -355,7 +353,7 @@ class Actionlog extends SnipeModel
* 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)
@@ -376,7 +374,7 @@ class Actionlog extends SnipeModel
if ($this->created_at > $override_default_next) {
$next_audit_days = '-'.$next_audit_days;
}
-
+
return $next_audit_days;
}
@@ -384,7 +382,7 @@ class Actionlog extends SnipeModel
* 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)
@@ -401,24 +399,24 @@ class Actionlog extends SnipeModel
/**
* 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
@@ -430,11 +428,12 @@ class Actionlog extends SnipeModel
// 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';
}
@@ -444,6 +443,81 @@ class Actionlog extends SnipeModel
}
+
+ /**
+ * @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
{
@@ -454,4 +528,4 @@ class Actionlog extends SnipeModel
{
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 ac4fecac34..d5b640e40a 100644
--- a/app/Models/Asset.php
+++ b/app/Models/Asset.php
@@ -7,24 +7,25 @@ use App\Exceptions\CheckoutNotAllowed;
use App\Helpers\Helper;
use App\Http\Traits\UniqueUndeletedTrait;
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 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\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,6 +34,7 @@ class Asset extends Depreciable
protected $with = ['model', 'adminuser'];
use CompanyableTrait;
+ use HasUploads;
use HasFactory, Loggable, Requestable, Presentable, SoftDeletes, ValidatingTrait, UniqueUndeletedTrait;
public const LOCATION = 'location';
@@ -44,22 +46,22 @@ class Asset extends Depreciable
/**
* 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 +71,12 @@ class Asset extends Depreciable
// 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 = [
@@ -111,27 +113,28 @@ class Asset extends Depreciable
'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', 'fmcs_location'],
- '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 +207,17 @@ class Asset extends Depreciable
'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)
{
@@ -220,18 +234,22 @@ class Asset extends Depreciable
$customFieldValidationRules = [];
- if (($this->model) && ($this->model->fieldset)) {
+ if (($this->model) && ($this->model->fieldset)) {
- foreach ($this->model->fieldset->fields as $field) {
+ 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();
}
+ $customFieldValidationRules += $this->model->fieldset->validation_rules();
+ }
+
return $customFieldValidationRules;
}
@@ -257,6 +275,7 @@ class Asset extends Depreciable
/**
* Returns the warranty expiration date as Carbon object
+ *
* @return \Carbon\Carbon|null
*/
public function getWarrantyExpiresAttribute()
@@ -280,7 +299,7 @@ class Asset extends Depreciable
* Establishes the asset -> company relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function company()
@@ -295,7 +314,7 @@ class Asset extends Depreciable
* that the status is deployable
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return bool
*/
public function availableForCheckout()
@@ -306,8 +325,8 @@ class Asset extends Depreciable
// 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;
}
@@ -322,14 +341,14 @@ class Asset extends Depreciable
* @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 string $note
- * @param null $name
+ * @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]
+ * @since [v3.0]
* @return bool
*/
public function checkOut($target, $admin = null, $checkout_at = null, $expected_checkin = null, $note = null, $name = null, $location = null)
@@ -390,7 +409,7 @@ class Asset extends Depreciable
* Sets the detailedNameAttribute
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return string
*/
public function getDetailedNameAttribute()
@@ -408,7 +427,7 @@ class Asset extends Depreciable
* Pulls in the validation rules
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return array
*/
public function validationRules()
@@ -416,15 +435,38 @@ class Asset extends Depreciable
return $this->rules;
}
- public function customFieldsForCheckinCheckout($checkin_checkout) {
+ 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))){
- $this->{$field->db_column} = request()->get($field->db_column);
+
+ 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);
+ }
+
+ }
}
}
}
+
}
@@ -432,12 +474,12 @@ class Asset extends Depreciable
* Establishes the asset -> depreciation relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
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');
}
@@ -445,7 +487,7 @@ class Asset extends Depreciable
* Get components assigned to this asset
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function components()
@@ -460,7 +502,7 @@ class Asset extends Depreciable
* @todo Is this still needed?
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function get_depreciation()
@@ -471,51 +513,35 @@ class Asset extends Depreciable
}
- /**
- * 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]
+ * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assignedTo()
@@ -529,7 +555,7 @@ class Asset extends Depreciable
* Sigh.
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assignedAssets()
@@ -541,7 +567,7 @@ class Asset extends Depreciable
* Establishes the accessory -> asset assignment relationship
*
* @author A. Gianotto
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assignedAccessories()
@@ -556,7 +582,7 @@ class Asset extends Depreciable
* @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)
@@ -597,7 +623,7 @@ class Asset extends Depreciable
* 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()
@@ -609,10 +635,11 @@ class Asset extends Depreciable
/**
* 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]
+ * @since [v6.1.0]
* @return string
*/
public function targetShowRoute()
@@ -631,7 +658,7 @@ class Asset extends Depreciable
* Get the asset's location based on default RTD location
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function defaultLoc()
@@ -646,7 +673,7 @@ class Asset extends Depreciable
* and if not, check for an image uploaded to the asset model.
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return string | false
*/
public function getImageUrl()
@@ -655,6 +682,8 @@ class Asset extends Depreciable
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;
@@ -665,22 +694,22 @@ class Asset extends Depreciable
* Get the asset's logs
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
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]
+ * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function checkouts()
@@ -695,7 +724,7 @@ class Asset extends Depreciable
* 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()
@@ -709,7 +738,7 @@ class Asset extends Depreciable
* Get the list of checkins for this asset
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function checkins()
@@ -724,7 +753,7 @@ class Asset extends Depreciable
* Get the asset's user requests
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function userRequests()
@@ -739,21 +768,21 @@ class Asset extends Depreciable
/**
* Get maintenances for this asset
*
- * @author Vincent Sposato
- * @since 1.0
+ * @author Vincent Sposato
+ * @since 1.0
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
- 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]
+ * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
@@ -767,7 +796,7 @@ class Asset extends Depreciable
* Establishes the asset -> status relationship
*
* @author [A. Gianotto] []
- * @since [v1.0]
+ * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assetstatus()
@@ -779,7 +808,7 @@ class Asset extends Depreciable
* Establishes the asset -> model relationship
*
* @author [A. Gianotto] []
- * @since [v1.0]
+ * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function model()
@@ -790,9 +819,9 @@ class Asset extends Depreciable
/**
* Return the assets with a warranty expiring within x days
*
- * @param $days
+ * @param $days
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return mixed
*/
public static function getExpiringWarrantee($days = 30)
@@ -803,9 +832,12 @@ class Asset extends Depreciable
->whereNotNull('warranty_months')
->whereNotNull('purchase_date')
->whereNull('deleted_at')
- ->whereRaw('DATE_ADD(`purchase_date`, INTERVAL `warranty_months` MONTH) <= DATE_ADD(NOW(), INTERVAL '
+ ->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()')
+ . ' DAY) AND DATE_ADD(`purchase_date`, INTERVAL `warranty_months` MONTH) > NOW()'
+ )
->orderByRaw('DATE_ADD(`purchase_date`,INTERVAL `warranty_months` MONTH)')
->get();
}
@@ -815,7 +847,7 @@ class Asset extends Depreciable
* Establishes the asset -> assigned licenses relationship
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function licenses()
@@ -827,7 +859,7 @@ class Asset extends Depreciable
* Establishes the asset -> license seats relationship
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function licenseseats()
@@ -839,7 +871,7 @@ class Asset extends Depreciable
* Establishes the asset -> aupplier relationship
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function supplier()
@@ -851,7 +883,7 @@ class Asset extends Depreciable
* Establishes the asset -> location relationship
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function location()
@@ -864,7 +896,7 @@ class Asset extends Depreciable
* Get the next autoincremented asset tag
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return string | false
*/
public static function autoincrement_asset(int $additional_increment = 0)
@@ -890,7 +922,7 @@ class Asset extends Depreciable
* 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)
@@ -901,12 +933,10 @@ class Asset extends Depreciable
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;
}
}
@@ -923,7 +953,7 @@ class Asset extends Depreciable
* 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 string
*/
public static function zerofill($num, $zerofill = 3)
@@ -936,7 +966,7 @@ class Asset extends Depreciable
* asset model category
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return bool
*/
public function checkin_email()
@@ -950,7 +980,7 @@ class Asset extends Depreciable
* Determine whether this asset requires acceptance by the assigned user
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return bool
*/
public function requireAcceptance()
@@ -959,6 +989,7 @@ class Asset extends Depreciable
return $this->model->category->require_acceptance;
}
+ return false;
}
@@ -966,7 +997,7 @@ class Asset extends Depreciable
* 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()
@@ -994,16 +1025,16 @@ class Asset extends Depreciable
* checks for a settings level EULA
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return string | false
*/
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 {
@@ -1013,7 +1044,8 @@ class Asset extends Depreciable
return false;
}
- public function getComponentCost(){
+ public function getComponentCost()
+ {
$cost = 0;
foreach($this->components as $component) {
$cost += $component->pivot->assigned_qty*$component->purchase_cost;
@@ -1033,7 +1065,7 @@ class Asset extends Depreciable
* 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
*/
@@ -1083,7 +1115,7 @@ class Asset extends Depreciable
*
* This will also correctly parse a 1/0 if "true"/"false" is passed.
*
- * @param $value
+ * @param $value
* @return void
*/
@@ -1097,16 +1129,16 @@ class Asset extends Depreciable
/**
- * -----------------------------------------------
- * 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)
@@ -1115,31 +1147,38 @@ class Asset extends Depreciable
/**
* Assigned user
*/
- $query = $query->leftJoin('users as assets_users', function ($leftJoin) {
- $leftJoin->on('assets_users.id', '=', 'assets.assigned_to')
- ->where('assets.assigned_type', '=', User::class);
- });
+ $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([
+ ->orWhereMultipleColumns(
+ [
'assets_users.first_name',
'assets_users.last_name',
- ], $term);
+ ], $term
+ );
}
/**
* Assigned location
*/
- $query = $query->leftJoin('locations as assets_locations', function ($leftJoin) {
- $leftJoin->on('assets_locations.id', '=', 'assets.assigned_to')
- ->where('assets.assigned_type', '=', Location::class);
- });
+ $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) {
@@ -1149,10 +1188,12 @@ class Asset extends Depreciable
/**
* Assigned assets
*/
- $query = $query->leftJoin('assets as assigned_assets', function ($leftJoin) {
- $leftJoin->on('assigned_assets.id', '=', 'assets.assigned_to')
- ->where('assets.assigned_type', '=', self::class);
- });
+ $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.'%');
@@ -1164,12 +1205,12 @@ class Asset extends Depreciable
/**
- * 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)
{
@@ -1177,101 +1218,121 @@ class Asset extends Depreciable
}
/**
- * 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) {
- $query->where('deployable', '=', 0)
- ->where('pending', '=', 1)
- ->where('archived', '=', 0);
- });
+ 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) {
- $query->where('deployable', '=', 0)
- ->where('pending', '=', 0)
- ->where('archived', '=', 0);
- });
+ 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) {
- $query->where('archived', '=', 0);
- });
+ return $query->whereHas(
+ 'assetstatus', function ($query) {
+ $query->where('archived', '=', 0);
+ }
+ );
}
/**
@@ -1291,15 +1352,15 @@ class Asset extends Depreciable
* now = May 4, 2019
*
* @author A. Gianotto
- * @since v4.6.16
- * @param Setting $settings
+ * @since v4.6.16
+ * @param Setting $settings
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
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');
@@ -1317,8 +1378,8 @@ class Asset extends Depreciable
* for an upcoming API call for retrieving a report on overdue assets.
*
* @author A. Gianotto
- * @since v4.6.16
- * @param Setting $settings
+ * @since v4.6.16
+ * @param Setting $settings
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -1339,8 +1400,8 @@ class Asset extends Depreciable
* 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
+ * @since v4.6.16
+ * @param Setting $settings
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -1348,11 +1409,15 @@ class Asset extends Depreciable
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);
+ }
+ );
}
@@ -1361,13 +1426,13 @@ class Asset extends Depreciable
* and settings.audit_warning_days. It checks to see if assets.expected_checkin is now
*
* @author A. Gianotto
- * @since v6.4.0
+ * @since v6.4.0
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
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');
@@ -1382,7 +1447,7 @@ class Asset extends Depreciable
* Query builder scope for Assets that are overdue for checkin OR overdue
*
* @author A. Gianotto
- * @since v6.4.0
+ * @since v6.4.0
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOverdueForCheckin($query)
@@ -1398,16 +1463,20 @@ class Asset extends Depreciable
* Query builder scope for Assets that are due for checkin OR overdue
*
* @author A. Gianotto
- * @since v6.4.0
+ * @since v6.4.0
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
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);
+ }
+ );
}
@@ -1418,7 +1487,7 @@ class Asset extends Depreciable
* 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
*/
@@ -1427,115 +1496,123 @@ class Asset extends Depreciable
{
if (Setting::getSettings()->show_archived_in_list!=1) {
- return $query->whereHas('assetstatus', function ($query) {
- $query->where('archived', '=', 0);
- });
+ 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) {
- $query->where('deployable', '=', 0)
- ->where('pending', '=', 0)
- ->where('archived', '=', 1);
- });
+ 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): 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');
@@ -1544,8 +1621,8 @@ class Asset extends Depreciable
/**
* 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
*/
@@ -1553,69 +1630,99 @@ class Asset extends Depreciable
{
$search = explode(' OR ', $search);
- 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->on('assets_locations.id', '=', 'assets.assigned_to')
- ->where('assets.assigned_type', '=', Location::class);
- })->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.'%');
+ 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->on('assets_locations.id', '=', 'assets.assigned_to')
+ ->where('assets.assigned_type', '=', Location::class);
+ }
+ )->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.'%')
+ ->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]
+ * @since [v5.0]
* @return string | false
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeCheckedOutToTargetInDepartment($query, $search)
{
- 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) {
+ 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);
- })->withTrashed()->whereNull('assets.deleted_at'); //workaround for laravel bug
+ }
+ )->withTrashed()->whereNull('assets.deleted_at'); //workaround for laravel bug
}
@@ -1623,189 +1730,229 @@ class Asset extends Depreciable
/**
* 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);
+
+ if ($fieldname == 'asset_tag') {
+ $query->where('assets.asset_tag', '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 == 'purchase_date') {
+ $query->where('assets.purchase_date', '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 == '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 == '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 =='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 == '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_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 == '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 . '%');
+ }
- $fieldname = str_replace('custom_fields.', '', $key);
- if ($fieldname == 'asset_tag') {
- $query->where('assets.asset_tag', '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 == 'purchase_date') {
- $query->where('assets.purchase_date', '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 == '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 == '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 =='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 == '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_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 == '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 . '%');
- }
-
}
-
-
- });
+ );
}
/**
- * 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);
@@ -1815,8 +1962,8 @@ class Asset extends Depreciable
/**
* 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
*/
@@ -1827,39 +1974,39 @@ class Asset extends Depreciable
/**
- * 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);
@@ -1869,8 +2016,8 @@ class Asset extends Depreciable
/**
* 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
*/
@@ -1878,34 +2025,34 @@ class Asset extends Depreciable
{
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', (!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')
@@ -1917,8 +2064,8 @@ class Asset extends Depreciable
/**
* 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
*/
@@ -1929,14 +2076,14 @@ class Asset extends Depreciable
->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);
@@ -1944,8 +2091,9 @@ class Asset extends Depreciable
/**
* 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
*/
@@ -1958,8 +2106,8 @@ class Asset extends Depreciable
/**
* 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
*/
@@ -1968,29 +2116,47 @@ class Asset extends Depreciable
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/AssetModel.php b/app/Models/AssetModel.php
index 8d60474b96..29308fdab3 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 @@ use App\Http\Traits\TwoColumnUniqueUndeletedTrait;
* 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,14 +99,22 @@ 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]
+ * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assets()
@@ -115,7 +126,7 @@ class AssetModel extends SnipeModel
* Establishes the model -> category relationship
*
* @author [A. Gianotto] []
- * @since [v1.0]
+ * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function category()
@@ -127,7 +138,7 @@ class AssetModel extends SnipeModel
* Establishes the model -> depreciation relationship
*
* @author [A. Gianotto] []
- * @since [v1.0]
+ * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function depreciation()
@@ -139,7 +150,7 @@ class AssetModel extends SnipeModel
* Establishes the model -> manufacturer relationship
*
* @author [A. Gianotto] []
- * @since [v1.0]
+ * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function manufacturer()
@@ -151,7 +162,7 @@ class AssetModel extends SnipeModel
* Establishes the model -> fieldset relationship
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function fieldset()
@@ -161,14 +172,14 @@ class AssetModel extends SnipeModel
public function customFields()
{
- return $this->fieldset()->first()->fields();
+ return $this->fieldset()->first()->fields();
}
/**
* Establishes the model -> custom field default values relationship
*
* @author hannah tinkler
- * @since [v4.3]
+ * @since [v4.3]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function defaultValues()
@@ -182,7 +193,7 @@ class AssetModel extends SnipeModel
* @todo this should probably be moved
*
* @author [A. Gianotto] []
- * @since [v2.0]
+ * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function getImageUrl()
@@ -199,7 +210,7 @@ class AssetModel extends SnipeModel
* Checks if the model is deletable
*
* @author A. Gianotto
- * @since [v6.3.4]
+ * @since [v6.3.4]
* @return bool
*/
public function isDeletable()
@@ -209,27 +220,12 @@ class AssetModel extends SnipeModel
&& ($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]
+ * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
@@ -248,10 +244,10 @@ class AssetModel extends SnipeModel
* 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 +260,9 @@ class AssetModel extends SnipeModel
* 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 +274,8 @@ class AssetModel extends SnipeModel
/**
* 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 +283,31 @@ class AssetModel extends SnipeModel
{
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 +319,8 @@ class AssetModel extends SnipeModel
/**
* 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 +336,6 @@ class AssetModel extends SnipeModel
/**
* 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 cfa83328ab..7d9d7a14ae 100755
--- a/app/Models/Category.php
+++ b/app/Models/Category.php
@@ -18,7 +18,7 @@ use Illuminate\Support\Str;
* 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 @@ class Category extends SnipeModel
* 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 @@ class Category extends SnipeModel
* 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 @@ class Category extends SnipeModel
* 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 @@ class Category extends SnipeModel
* 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 @@ class Category extends SnipeModel
* 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 @@ class Category extends SnipeModel
}
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 @@ class Category extends SnipeModel
* 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 @@ class Category extends SnipeModel
* 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 @@ class Category extends SnipeModel
* 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 @@ class Category extends SnipeModel
* 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 @@ class Category extends SnipeModel
*
* 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 @@ class Category extends SnipeModel
/**
* 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 e44a330ebc..f65fb219a8 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 @@ class CheckoutAcceptance extends Model
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 @@ class CheckoutAcceptance extends Model
/**
* 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 @@ class CheckoutAcceptance extends Model
* 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 @@ class CheckoutAcceptance extends Model
/**
* Decline the checkout acceptance
*
- * @param string $signature_filename
+ * @param string $signature_filename
*/
public function decline($signature_filename, $note = null)
{
@@ -115,8 +128,9 @@ class CheckoutAcceptance extends Model
/**
* Filter checkout acceptences by the user
+ *
* @param Illuminate\Database\Eloquent\Builder $query
- * @param User $user
+ * @param User $user
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeForUser(Builder $query, User $user)
@@ -126,6 +140,7 @@ class CheckoutAcceptance extends Model
/**
* 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 d6a85f2972..42512d8fda 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 43fd396069..72a12aebc6 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;
@@ -100,7 +104,7 @@ final class Company extends SnipeModel
* 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)
@@ -127,7 +131,7 @@ final class Company extends SnipeModel
* 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)
@@ -146,10 +150,10 @@ final class Company extends SnipeModel
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;
}
@@ -160,12 +164,19 @@ final class Company extends SnipeModel
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()
@@ -183,7 +194,7 @@ final class Company extends SnipeModel
* Checks if company can be deleted
*
* @author [Dan Meltzer] []
- * @since [v5.0]
+ * @since [v5.0]
* @return bool
*/
public function isDeletable()
@@ -200,7 +211,7 @@ final class Company extends SnipeModel
}
/**
- * @param $unescaped_input
+ * @param $unescaped_input
* @return int|mixed|string|null
*/
public static function getIdForUser($unescaped_input)
@@ -256,14 +267,14 @@ final class Company extends SnipeModel
* @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 {
@@ -280,11 +291,16 @@ final class Company extends SnipeModel
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);
}
@@ -297,6 +313,8 @@ final class Company extends SnipeModel
return $query->where($table.$column, '=', $company_id);
}
+
+
}
public function adminuser()
@@ -311,8 +329,8 @@ final class Company extends SnipeModel
* 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)
@@ -324,17 +342,18 @@ final class Company extends SnipeModel
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 4077ebd596..35f2049e8b 100644
--- a/app/Models/CompanyableChildScope.php
+++ b/app/Models/CompanyableChildScope.php
@@ -9,15 +9,15 @@ use Illuminate\Database\Eloquent\Scope;
/**
* 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 @@ final class CompanyableChildScope implements Scope
* @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 4bbe7d6396..bbc0b5e0e6 100644
--- a/app/Models/CompanyableScope.php
+++ b/app/Models/CompanyableScope.php
@@ -9,15 +9,15 @@ use Illuminate\Database\Eloquent\Scope;
/**
* 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 @@ final class CompanyableScope implements Scope
* @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/Component.php b/app/Models/Component.php
index 0208fb9f68..09ca41830e 100644
--- a/app/Models/Component.php
+++ b/app/Models/Component.php
@@ -2,6 +2,9 @@
namespace App\Models;
+use App\Helpers\Helper;
+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;
@@ -12,7 +15,7 @@ use Watson\Validating\ValidatingTrait;
/**
* Model for Components.
*
- * @version v1.0
+ * @version v1.0
*/
class Component extends SnipeModel
{
@@ -20,6 +23,7 @@ class Component extends SnipeModel
protected $presenter = \App\Presenters\ComponentPresenter::class;
use CompanyableTrait;
+ use HasUploads;
use Loggable, Presentable;
use SoftDeletes;
protected $casts = [
@@ -39,7 +43,7 @@ class Component extends SnipeModel
'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',
];
@@ -113,28 +117,13 @@ class Component extends SnipeModel
&& ($this->deleted_at == '');
}
- /**
- * Establishes the components -> 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 component -> location relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function location()
@@ -146,7 +135,7 @@ class Component extends SnipeModel
* Establishes the component -> assets relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assets()
@@ -160,7 +149,7 @@ class Component extends SnipeModel
* @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()
@@ -172,7 +161,7 @@ class Component extends SnipeModel
* Establishes the component -> company relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function company()
@@ -184,7 +173,7 @@ class Component extends SnipeModel
* Establishes the component -> category relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function category()
@@ -196,7 +185,7 @@ class Component extends SnipeModel
* 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()
@@ -209,19 +198,49 @@ class Component extends SnipeModel
* 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()
@@ -233,7 +252,7 @@ class Component extends SnipeModel
* Check how many items within a component are checked out
*
* @author [A. Gianotto] []
- * @since [v5.0]
+ * @since [v5.0]
* @return int
*/
public function numCheckedOut()
@@ -252,20 +271,34 @@ class Component extends SnipeModel
*
* This allows us to get the assets with assigned components without the company restriction
*/
- public function uncontrainedAssets() {
+ public function uncontrainedAssets()
+ {
return $this->belongsToMany(\App\Models\Asset::class, 'components_assets')
- ->withPivot('id', 'assigned_qty', 'created_at', 'created_by', 'note')
- ->withoutGlobalScope(new CompanyableScope);
+ ->withPivot('id', 'assigned_qty', 'created_at', 'created_by', 'note')
+ ->withoutGlobalScope(new CompanyableScope);
}
+ /**
+ * 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()
@@ -288,8 +321,8 @@ class Component extends SnipeModel
* 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)
@@ -307,8 +340,8 @@ class Component extends SnipeModel
/**
* Query builder scope to order on company
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param string $order Order
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -320,8 +353,8 @@ class Component extends SnipeModel
/**
* Query builder scope to order on company
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param string $order Order
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -333,8 +366,8 @@ class Component extends SnipeModel
/**
* Query builder scope to order on company
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param string $order Order
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -346,8 +379,8 @@ class Component extends SnipeModel
/**
* Query builder scope to order on supplier
*
- * @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
*/
@@ -359,8 +392,8 @@ class Component extends SnipeModel
/**
* 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
*/
diff --git a/app/Models/Consumable.php b/app/Models/Consumable.php
index c83aa6106e..c7cdff0fe3 100644
--- a/app/Models/Consumable.php
+++ b/app/Models/Consumable.php
@@ -4,21 +4,16 @@ namespace App\Models;
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\ConsumablePresenter;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
-use Illuminate\Database\Eloquent\Relations\Relation;
-use App\Presenters\ConsumablePresenter;
-use App\Models\Actionlog;
-use App\Models\ConsumableAssignment;
-use App\Models\User;
-use App\Models\Location;
-use App\Models\Manufacturer;
-use App\Models\Supplier;
-use App\Models\Category;
class Consumable extends SnipeModel
{
@@ -29,6 +24,7 @@ class Consumable extends SnipeModel
use Loggable, Presentable;
use SoftDeletes;
use Acceptable;
+ use HasUploads;
protected $table = 'consumables';
protected $casts = [
@@ -51,7 +47,7 @@ class Consumable extends SnipeModel
'company_id' => 'integer|nullable',
'location_id' => 'exists:locations,id|nullable|fmcs_location',
'min_amt' => 'integer|min:0|max:99999|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',
];
@@ -111,21 +107,6 @@ class Consumable 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()
- {
- return $this->hasMany(Actionlog::class, 'item_id')
- ->where('item_type', '=', self::class)
- ->where('action_type', '=', 'uploaded')
- ->whereNotNull('filename')
- ->orderBy('created_at', 'desc');
- }
/**
@@ -138,7 +119,7 @@ class Consumable extends SnipeModel
* @todo Update this comment once it's been implemented
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function setRequestableAttribute($value)
@@ -153,7 +134,7 @@ class Consumable extends SnipeModel
* Establishes the consumable -> admin user relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
@@ -165,7 +146,7 @@ class Consumable extends SnipeModel
* Establishes the component -> assignments relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function consumableAssignments()
@@ -177,7 +158,7 @@ class Consumable extends SnipeModel
* Establishes the component -> company relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function company()
@@ -189,7 +170,7 @@ class Consumable extends SnipeModel
* Establishes the component -> manufacturer relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function manufacturer()
@@ -201,7 +182,7 @@ class Consumable extends SnipeModel
* Establishes the component -> location relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function location()
@@ -213,7 +194,7 @@ class Consumable extends SnipeModel
* Establishes the component -> category relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function category()
@@ -226,7 +207,7 @@ class Consumable extends SnipeModel
* Establishes the component -> action logs relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assetlog()
@@ -238,23 +219,28 @@ class Consumable extends SnipeModel
* Gets the full image url for the consumable
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return string | false
*/
public function getImageUrl()
{
+ // If there is a consumable image, use that
if ($this->image) {
return Storage::disk('public')->url(app('consumables_upload_path').$this->image);
- }
- return false;
+ // Otherwise check for a category image
+ } elseif (($this->category) && ($this->category->image)) {
+ return Storage::disk('public')->url(app('categories_upload_path').e($this->category->image));
+ }
+
+ return false;
}
/**
* Establishes the component -> users relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
*/
public function users() : Relation
{
@@ -265,7 +251,7 @@ class Consumable extends SnipeModel
* 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()
@@ -279,19 +265,19 @@ class Consumable extends SnipeModel
* asset model category
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return bool
*/
public function checkin_email()
{
- return $this->category->checkin_email;
+ return $this->category?->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()
@@ -304,7 +290,7 @@ class Consumable extends SnipeModel
* checks for a settings level EULA
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return string | false
*/
public function getEula()
@@ -322,7 +308,7 @@ class Consumable extends SnipeModel
* Check how many items within a consumable are checked out
*
* @author [A. Gianotto] []
- * @since [v5.0]
+ * @since [v5.0]
* @return int
*/
public function numCheckedOut()
@@ -334,7 +320,7 @@ class Consumable extends SnipeModel
* Checks the number of available consumables
*
* @author [A. Gianotto] []
- * @since [v4.0]
+ * @since [v4.0]
* @return int
*/
public function numRemaining()
@@ -360,8 +346,8 @@ class Consumable extends SnipeModel
* 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)
@@ -378,8 +364,8 @@ class Consumable extends SnipeModel
/**
* Query builder scope to order on company
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param string $order Order
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -391,8 +377,8 @@ class Consumable extends SnipeModel
/**
* Query builder scope to order on location
*
- * @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
*/
@@ -404,8 +390,8 @@ class Consumable extends SnipeModel
/**
* Query builder scope to order on manufacturer
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param string $order Order
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -417,8 +403,8 @@ class Consumable extends SnipeModel
/**
* Query builder scope to order on company
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param string $order Order
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -430,8 +416,8 @@ class Consumable extends SnipeModel
/**
* Query builder scope to order on remaining
*
- * @param \Illuminate\Database\Query\Builder $query Query builder instance
- * @param string $order Order
+ * @param \Illuminate\Database\Query\Builder $query Query builder instance
+ * @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
@@ -444,8 +430,8 @@ class Consumable extends SnipeModel
/**
* Query builder scope to order on supplier
*
- * @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
*/
diff --git a/app/Models/ConsumableAssignment.php b/app/Models/ConsumableAssignment.php
index 4c9a19703e..0e634f580f 100644
--- a/app/Models/ConsumableAssignment.php
+++ b/app/Models/ConsumableAssignment.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Models\Traits\CompanyableTrait;
use Illuminate\Database\Eloquent\Model;
use Watson\Validating\ValidatingTrait;
diff --git a/app/Models/CustomField.php b/app/Models/CustomField.php
index 4954e2f17b..0e8845cfb3 100644
--- a/app/Models/CustomField.php
+++ b/app/Models/CustomField.php
@@ -16,6 +16,7 @@ class CustomField extends Model
UniqueUndeletedTrait;
/**
+ *
* Custom field predfined formats
*
* @var array
@@ -79,6 +80,9 @@ class CustomField extends Model
'auto_add_to_fieldsets',
'show_in_listview',
'show_in_email',
+ 'display_checkout',
+ 'display_checkin',
+ 'display_audit',
'show_in_requestable_list',
];
@@ -89,7 +93,7 @@ class CustomField extends Model
* table instead of the assets table.
*
* @author [Brady Wetherington] []
- * @since [v3.0]
+ * @since [v3.0]
*/
public static $table_name = 'assets';
@@ -100,7 +104,7 @@ class CustomField extends Model
* do with previously existing values. - @snipe
*
* @author [A. Gianotto] []
- * @since [v3.4]
+ * @since [v3.4]
* @return string
*/
public static function name_to_db_name($name)
@@ -117,83 +121,132 @@ class CustomField extends Model
* to do it in the controllers.
*
* @author [A. Gianotto] []
- * @since [v3.4]
+ * @since [v3.4]
* @return bool
*/
public static function boot()
{
parent::boot();
- self::created(function ($custom_field) {
+ self::created(
+ function ($custom_field) {
- // Column already exists on the assets table - nothing to do here.
- // This *shouldn't* happen in the wild.
- if (Schema::hasColumn(self::$table_name, $custom_field->db_column)) {
- return false;
+ // Column already exists on the assets table - nothing to do here.
+ // This *shouldn't* happen in the wild.
+ if (Schema::hasColumn(self::$table_name, $custom_field->db_column)) {
+ return false;
+ }
+
+ // Update the column name in the assets table
+ Schema::table(
+ self::$table_name, function ($table) use ($custom_field) {
+ $table->text($custom_field->convertUnicodeDbSlug())->nullable();
+ }
+ );
+
+ // Update the db_column property in the custom fields table
+ $custom_field->db_column = $custom_field->convertUnicodeDbSlug();
+ $custom_field->save();
}
+ );
- // Update the column name in the assets table
- Schema::table(self::$table_name, function ($table) use ($custom_field) {
- $table->text($custom_field->convertUnicodeDbSlug())->nullable();
- });
+ self::updating(
+ function ($custom_field) {
- // Update the db_column property in the custom fields table
- $custom_field->db_column = $custom_field->convertUnicodeDbSlug();
- $custom_field->save();
- });
+ // Column already exists on the assets table - nothing to do here.
+ if ($custom_field->isDirty('name')) {
+ if (Schema::hasColumn(self::$table_name, $custom_field->convertUnicodeDbSlug())) {
+ return true;
+ }
- self::updating(function ($custom_field) {
+ // Rename the field if the name has changed
+ Schema::table(
+ self::$table_name, function ($table) use ($custom_field) {
+ $table->renameColumn($custom_field->convertUnicodeDbSlug($custom_field->getOriginal('name')), $custom_field->convertUnicodeDbSlug());
+ }
+ );
+
+ // Save the updated column name to the custom fields table
+ $custom_field->db_column = $custom_field->convertUnicodeDbSlug();
+ $custom_field->save();
- // Column already exists on the assets table - nothing to do here.
- if ($custom_field->isDirty('name')) {
- if (Schema::hasColumn(self::$table_name, $custom_field->convertUnicodeDbSlug())) {
return true;
}
- // Rename the field if the name has changed
- Schema::table(self::$table_name, function ($table) use ($custom_field) {
- $table->renameColumn($custom_field->convertUnicodeDbSlug($custom_field->getOriginal('name')), $custom_field->convertUnicodeDbSlug());
- });
-
- // Save the updated column name to the custom fields table
- $custom_field->db_column = $custom_field->convertUnicodeDbSlug();
- $custom_field->save();
-
return true;
}
-
- return true;
- });
+ );
// Drop the assets column if we've deleted it from custom fields
- self::deleting(function ($custom_field) {
- return Schema::table(self::$table_name, function ($table) use ($custom_field) {
- $table->dropColumn($custom_field->db_column);
- });
- });
+ self::deleting(
+ function ($custom_field) {
+ return Schema::table(
+ self::$table_name, function ($table) use ($custom_field) {
+ $table->dropColumn($custom_field->db_column);
+ }
+ );
+ }
+ );
}
/**
* Establishes the customfield -> fieldset relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function fieldset()
{
return $this->belongsToMany(\App\Models\CustomFieldset::class);
}
-
+
+ public function displayFieldInCheckinForm()
+ {
+ if ($this->display_checkin == '1') {
+ return true;
+ }
+ return false;
+ }
+
+ public function displayFieldInCheckoutForm()
+ {
+ if ($this->display_checkout == '1') {
+ return true;
+ }
+ return false;
+ }
+
+ public function displayFieldInAuditForm()
+ {
+ if ($this->display_audit == '1') {
+ return true;
+ }
+ return false;
+ }
+
+ public function displayFieldInCurrentForm($form_type = null)
+ {
+ switch ($form_type) {
+ case 'audit':
+ return $this->displayFieldInAuditForm();
+ case 'checkin':
+ return $this->displayFieldInCheckinForm();
+ case 'checkout':
+ return $this->displayFieldInCheckoutForm();
+ }
+ }
+
+
public function assetModels()
{
- return $this->fieldset()->with('models')->get()->pluck('models')->flatten()->unique('id');
+ return $this->fieldset()->with('models')->get()->pluck('models')->flatten()->unique('id');
}
/**
* Establishes the customfield -> admin user relationship
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function user()
@@ -205,7 +258,7 @@ class CustomField extends Model
* Establishes the customfield -> default values relationship
*
* @author Hannah Tinkler
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function defaultValues()
@@ -222,19 +275,23 @@ class CustomField extends Model
*/
public function defaultValue($modelId)
{
- return $this->defaultValues->filter(function ($item) use ($modelId) {
- return $item->pivot->asset_model_id == $modelId;
- })->map(function ($item) {
- return $item->pivot->default_value;
- })->first();
+ return $this->defaultValues->filter(
+ function ($item) use ($modelId) {
+ return $item->pivot->asset_model_id == $modelId;
+ }
+ )->map(
+ function ($item) {
+ return $item->pivot->default_value;
+ }
+ )->first();
}
/**
* Checks the format of the attribute
*
* @author [A. Gianotto] []
- * @param $value string
- * @since [v3.0]
+ * @param $value string
+ * @since [v3.0]
* @return bool
*/
public function check_format($value)
@@ -246,7 +303,7 @@ class CustomField extends Model
* Gets the DB column name.
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return string
*/
public function db_column_name()
@@ -262,7 +319,7 @@ class CustomField extends Model
* user-friendly text in the dropdowns, and in the custom fields display.
*
* @author [A. Gianotto] []
- * @since [v3.4]
+ * @since [v3.4]
* @return string
*/
public function getFormatAttribute($value)
@@ -280,7 +337,7 @@ class CustomField extends Model
* Format a value string as an array for select boxes and checkboxes.
*
* @author [A. Gianotto] []
- * @since [v3.4]
+ * @since [v3.4]
* @return array
*/
public function setFormatAttribute($value)
@@ -296,7 +353,7 @@ class CustomField extends Model
* Format a value string as an array for select boxes and checkboxes.
*
* @author [A. Gianotto] []
- * @since [v3.4]
+ * @since [v3.4]
* @return array
*/
public function formatFieldValuesAsArray()
@@ -326,7 +383,7 @@ class CustomField extends Model
* Check whether the field is encrypted
*
* @author [A. Gianotto] []
- * @since [v3.4]
+ * @since [v3.4]
* @return bool
*/
public function isFieldDecryptable($string)
@@ -343,7 +400,7 @@ class CustomField extends Model
* won't break the database.
*
* @author [A. Gianotto] []
- * @since [v3.4]
+ * @since [v3.4]
* @return string
*/
public function convertUnicodeDbSlug($original = null)
@@ -352,7 +409,7 @@ class CustomField extends Model
$id = $this->id ? $this->id : 'xx';
if (! function_exists('transliterator_transliterate')) {
- $long_slug = '_snipeit_'.str_slug(mb_convert_encoding(trim($name),"UTF-8"), '_');
+ $long_slug = '_snipeit_'.str_slug(mb_convert_encoding(trim($name), "UTF-8"), '_');
} else {
$long_slug = '_snipeit_'.Utf8Slugger::slugify($name, '_');
}
@@ -362,9 +419,10 @@ class CustomField extends Model
/**
* Get validation rules for custom fields to use with Validator
+ *
* @author [V. Cordes] []
- * @param int $id
- * @since [v4.1.10]
+ * @param int $id
+ * @since [v4.1.10]
* @return array
*/
public function validationRules($regex_format = null)
@@ -378,6 +436,7 @@ class CustomField extends Model
/**
* Check to see if there is a custom regex format type
+ *
* @see https://github.com/grokability/snipe-it/issues/5896
*
* @author Wes Hulette
diff --git a/app/Models/CustomFieldset.php b/app/Models/CustomFieldset.php
index d6bd7a1bef..f27b838647 100644
--- a/app/Models/CustomFieldset.php
+++ b/app/Models/CustomFieldset.php
@@ -3,12 +3,19 @@
namespace App\Models;
use App\Rules\AlphaEncrypted;
+use App\Rules\BooleanEncrypted;
+use App\Rules\DateEncrypted;
+use App\Rules\EmailEncrypted;
+use App\Rules\IPEncrypted;
+use App\Rules\IPv4Encrypted;
+use App\Rules\IPv6Encrypted;
+use App\Rules\MacEncrypted;
use App\Rules\NumericEncrypted;
+use App\Rules\RegexEncrypted;
+use App\Rules\UrlEncrypted;
use Gate;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
-use Illuminate\Support\Facades\Log;
-use Illuminate\Validation\Rule;
use Watson\Validating\ValidatingTrait;
class CustomFieldset extends Model
@@ -20,6 +27,7 @@ class CustomFieldset extends Model
/**
* Validation rules
+ *
* @var array
*/
public $rules = [
@@ -39,7 +47,7 @@ class CustomFieldset extends Model
* Establishes the fieldset -> field relationship
*
* @author [Brady Wetherington] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function fields()
@@ -51,7 +59,7 @@ class CustomFieldset extends Model
* Establishes the fieldset -> models relationship
*
* @author [Brady Wetherington] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function models()
@@ -63,7 +71,7 @@ class CustomFieldset extends Model
* Establishes the fieldset -> admin user relationship
*
* @author [Brady Wetherington] []
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function user()
@@ -71,22 +79,42 @@ class CustomFieldset extends Model
return $this->belongsTo(\App\Models\User::class); //WARNING - not all CustomFieldsets have a User!!
}
+ public function displayAnyFieldsInForm($form_type = null)
+ {
+ if ($this->fields) {
+
+ switch ($form_type) {
+ case 'audit':
+ return $this->fields->where('display_audit', '1')->count() > 0;
+ case 'checkin':
+ return $this->fields->where('display_checkin', '1')->count() > 0;
+ case 'checkout':
+ return $this->fields->where('display_checkout', '1')->count() > 0;
+ default:
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* Determine the validation rules we should apply based on the
* custom field format
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return array
*/
- public function validation_rules()
+ public function validation_rules(): array
{
$rules = [];
foreach ($this->fields as $field) {
$rule = [];
- if (($field->field_encrypted != '1') ||
- (($field->field_encrypted == '1') && (Gate::allows('admin')))) {
+ if (($field->field_encrypted != '1')
+ || (($field->field_encrypted == '1') && (Gate::allows('admin')))
+ ) {
$rule[] = ($field->pivot->required == '1') ? 'required' : 'nullable';
}
@@ -94,21 +122,71 @@ class CustomFieldset extends Model
$rule[] = 'unique_undeleted';
}
- array_push($rule, $field->attributes['format']);
+ if ($field->attributes['format']!='') {
+ array_push($rule, $field->attributes['format']);
+ }
+
$rules[$field->db_column_name()] = $rule;
-
- // these are to replace the standard 'numeric' and 'alpha' rules if the custom field is also encrypted.
- // the values need to be decrypted first, because encrypted strings are alphanumeric
- if ($field->format === 'NUMERIC' && $field->field_encrypted) {
+ // this is to switch the rules to rules specially made for encrypted custom fields that decrypt the value before validating
+ if ($field->field_encrypted) {
$numericKey = array_search('numeric', $rules[$field->db_column_name()]);
- $rules[$field->db_column_name()][$numericKey] = new NumericEncrypted;
+ $alphaKey = array_search('alpha', $rules[$field->db_column_name()]);
+ $emailKey = array_search('email', $rules[$field->db_column_name()]);
+ $dateKey = array_search('date', $rules[$field->db_column_name()]);
+ $urlKey = array_search('url', $rules[$field->db_column_name()]);
+ $ipKey = array_search('ip', $rules[$field->db_column_name()]);
+ $ipv4Key = array_search('ipv4', $rules[$field->db_column_name()]);
+ $ipv6Key = array_search('ipv6', $rules[$field->db_column_name()]);
+ $macKey = array_search('regex:/^[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}$/', $rules[$field->db_column_name()]);
+ $booleanKey = array_search('boolean', $rules[$field->db_column_name()]);
+ // find objects in array that start with "regex:"
+ // collect because i couldn't figure how to do this
+ // with array filter and get keys out of it
+ $regexCollect = collect($rules[$field->db_column_name()]);
+ $regexKeys = $regexCollect->filter(function ($value, $key) {
+ return starts_with($value, 'regex:');
+ })->keys()->values()->toArray();
+
+ switch ($field->format) {
+ case 'NUMERIC':
+ $rules[$field->db_column_name()][$numericKey] = new NumericEncrypted;
+ break;
+ case 'ALPHA':
+ $rules[$field->db_column_name()][$alphaKey] = new AlphaEncrypted;
+ break;
+ case 'EMAIL':
+ $rules[$field->db_column_name()][$emailKey] = new EmailEncrypted;
+ break;
+ case 'DATE':
+ $rules[$field->db_column_name()][$dateKey] = new DateEncrypted;
+ break;
+ case 'URL':
+ $rules[$field->db_column_name()][$urlKey] = new UrlEncrypted;
+ break;
+ case 'IP':
+ $rules[$field->db_column_name()][$ipKey] = new IPEncrypted;
+ break;
+ case 'IPV4':
+ $rules[$field->db_column_name()][$ipv4Key] = new IPv4Encrypted;
+ break;
+ case 'IPV6':
+ $rules[$field->db_column_name()][$ipv6Key] = new IPv6Encrypted;
+ break;
+ case 'MAC':
+ $rules[$field->db_column_name()][$macKey] = new MacEncrypted;
+ break;
+ case 'BOOLEAN':
+ $rules[$field->db_column_name()][$booleanKey] = new BooleanEncrypted;
+ break;
+ case starts_with($field->format, 'regex'):
+ foreach ($regexKeys as $regexKey) {
+ $rules[$field->db_column_name()][$regexKey] = new RegexEncrypted;
+ }
+ break;
+ }
}
- if ($field->format === 'ALPHA' && $field->field_encrypted) {
- $alphaKey = array_search('alpha', $rules[$field->db_column_name()]);
- $rules[$field->db_column_name()][$alphaKey] = new AlphaEncrypted;
- }
// add not_array to rules for all fields but checkboxes
if ($field->element != 'checkbox') {
diff --git a/app/Models/Department.php b/app/Models/Department.php
index 592fd840b1..1569081fdd 100644
--- a/app/Models/Department.php
+++ b/app/Models/Department.php
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Http\Traits\UniqueUndeletedTrait;
+use App\Models\Traits\CompanyableTrait;
use App\Models\Traits\Searchable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Watson\Validating\ValidatingTrait;
@@ -72,7 +73,7 @@ class Department extends SnipeModel
* Establishes the department -> company relationship
*
* @author A. Gianotto
- * @since [v4.0]
+ * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function company()
@@ -84,7 +85,7 @@ class Department extends SnipeModel
* Establishes the department -> users relationship
*
* @author A. Gianotto
- * @since [v4.0]
+ * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function users()
@@ -96,7 +97,7 @@ class Department extends SnipeModel
* Establishes the department -> manager relationship
*
* @author A. Gianotto
- * @since [v4.0]
+ * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function manager()
@@ -108,7 +109,7 @@ class Department extends SnipeModel
* Establishes the department -> location relationship
*
* @author A. Gianotto
- * @since [v4.0]
+ * @since [v4.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function location()
@@ -119,8 +120,8 @@ class Department extends SnipeModel
/**
* Query builder scope to order on location 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
*/
@@ -132,8 +133,8 @@ class Department extends SnipeModel
/**
* Query builder scope to order on manager 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
*/
@@ -145,8 +146,8 @@ class Department extends SnipeModel
/**
* Query builder scope to order on company
*
- * @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
*/
diff --git a/app/Models/Depreciable.php b/app/Models/Depreciable.php
index 0c513a3d33..b4cd40e1d1 100644
--- a/app/Models/Depreciable.php
+++ b/app/Models/Depreciable.php
@@ -48,15 +48,15 @@ class Depreciable extends SnipeModel
$depreciation = 0;
$setting = Setting::getSettings();
switch ($setting->depreciation_method) {
- case 'half_1':
+ case 'half_1':
$depreciation = $this->getHalfYearDepreciatedValue(true);
break;
- case 'half_2':
+ case 'half_2':
$depreciation = $this->getHalfYearDepreciatedValue(false);
break;
- default:
+ default:
$depreciation = $this->getLinearDepreciatedValue();
}
@@ -74,7 +74,7 @@ class Depreciable extends SnipeModel
return null;
}
- if ($months_passed >= $this->get_depreciation()->months){
+ if ($months_passed >= $this->get_depreciation()->months) {
//if there is a floor use it
if($this->get_depreciation()->depreciation_min) {
@@ -93,14 +93,15 @@ class Depreciable extends SnipeModel
return $current_value;
}
- public function getMonthlyDepreciation(){
+ public function getMonthlyDepreciation()
+ {
return ($this->purchase_cost-$this->calculateDepreciation())/$this->get_depreciation()->months;
}
/**
- * @param onlyHalfFirstYear Boolean always applied only second half of the first year
+ * @param onlyHalfFirstYear Boolean always applied only second half of the first year
* @return float|int
*/
public function getHalfYearDepreciatedValue($onlyHalfFirstYear = false)
@@ -131,7 +132,7 @@ class Depreciable extends SnipeModel
}
/**
- * @param \DateTime $date
+ * @param \DateTime $date
* @return int
*/
protected function get_fiscal_year($date)
@@ -146,7 +147,7 @@ class Depreciable extends SnipeModel
}
/**
- * @param \DateTime $date
+ * @param \DateTime $date
* @return bool
*/
protected function is_first_half_of_year($date)
diff --git a/app/Models/Depreciation.php b/app/Models/Depreciation.php
index 11ee82c16a..6e01c6d782 100755
--- a/app/Models/Depreciation.php
+++ b/app/Models/Depreciation.php
@@ -16,7 +16,7 @@ class Depreciation extends SnipeModel
// Declare the rules for the form validation
protected $rules = [
'name' => 'required|min:3|max:255|unique:depreciations,name',
- 'months' => 'required|max:3600|integer|gt:0',
+ 'months' => 'required|max:3600|integer',
];
/**
@@ -56,7 +56,7 @@ class Depreciation extends SnipeModel
* Establishes the depreciation -> models relationship
*
* @author A. Gianotto
- * @since [v5.0]
+ * @since [v5.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function models()
@@ -68,7 +68,7 @@ class Depreciation extends SnipeModel
* Establishes the depreciation -> licenses relationship
*
* @author A. Gianotto
- * @since [v5.0]
+ * @since [v5.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function licenses()
@@ -80,7 +80,7 @@ class Depreciation extends SnipeModel
* Establishes the depreciation -> assets relationship
*
* @author A. Gianotto
- * @since [v5.0]
+ * @since [v5.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assets()
@@ -92,7 +92,7 @@ class Depreciation extends SnipeModel
* Get the user that created the depreciation
*
* @author A. Gianotto
- * @since [v7.0.13]
+ * @since [v7.0.13]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
diff --git a/app/Models/Group.php b/app/Models/Group.php
index 253d47fbb9..9f4f2e2e56 100755
--- a/app/Models/Group.php
+++ b/app/Models/Group.php
@@ -51,7 +51,7 @@ class Group extends SnipeModel
* Establishes the groups -> users relationship
*
* @author A. Gianotto
- * @since [v1.0]
+ * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function users()
@@ -63,7 +63,7 @@ class Group extends SnipeModel
* Get the user that created the group
*
* @author A. Gianotto
- * @since [v6.3.0]
+ * @since [v6.3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
@@ -75,29 +75,33 @@ class Group extends SnipeModel
* Decode JSON permissions into array
*
* @author A. Gianotto
- * @since [v1.0]
- * @return array
+ * @since [v1.0]
+ * @return array | \stdClass
*/
public function decodePermissions()
{
- // Set default to empty JSON if the value is null
+ // If the permissions are an array, convert it to JSON
if (is_array($this->permissions)) {
$this->permissions = json_encode($this->permissions);
}
+
$permissions = json_decode($this->permissions ?? '{}', JSON_OBJECT_AS_ARRAY);
- // If there are no permissions, return an empty array
- if (!$permissions) {
- return [];
- }
-
// Otherwise, loop through the permissions and cast the values as integers
- foreach ($permissions as $permission => $value) {
- $permissions[$permission] = (int) $value;
+ if ((is_array($permissions)) && ($permissions)) {
+ foreach ($permissions as $permission => $value) {
+
+ if (!is_integer($permission)) {
+ $permissions[$permission] = (int) $value;
+ } else {
+ \Log::info('Weird data here - skipping it');
+ unset($permissions[$permission]);
+ }
+ }
+ return $permissions ?: new \stdClass;
}
+ return new \stdClass;
-
- return $permissions;
}
/**
diff --git a/app/Models/Import.php b/app/Models/Import.php
index d824a3840c..4ea259ba05 100644
--- a/app/Models/Import.php
+++ b/app/Models/Import.php
@@ -19,7 +19,7 @@ class Import extends Model
* Establishes the license -> admin user relationship
*
* @author A. Gianotto
- * @since [v2.0]
+ * @since [v2.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function adminuser()
diff --git a/app/Models/Labels/DefaultLabel.php b/app/Models/Labels/DefaultLabel.php
index 9f7059bcd5..fa60dafab3 100644
--- a/app/Models/Labels/DefaultLabel.php
+++ b/app/Models/Labels/DefaultLabel.php
@@ -38,13 +38,14 @@ class DefaultLabel extends RectangleSheet
private int $rows;
- public function __construct() {
+ public function __construct()
+ {
$settings = Setting::getSettings();
$this->textSize = Helper::convertUnit($settings->labels_fontsize, 'pt', 'in');
- $this->labelWidth = $settings->labels_width;
- $this->labelHeight = $settings->labels_height;
+ $this->labelWidth = $this->setLabelWidth($settings);
+ $this->labelHeight = $this->setLabelHeight($settings);
$this->labelSpacingH = $settings->labels_display_sgutter;
$this->labelSpacingV = $settings->labels_display_bgutter;
@@ -74,41 +75,116 @@ class DefaultLabel extends RectangleSheet
}
- public function getUnit() { return 'in'; }
+ public function getUnit()
+ {
+ return 'in';
+ }
- public function getPageWidth() { return $this->pageWidth; }
- public function getPageHeight() { return $this->pageHeight; }
+ public function getPageWidth()
+ {
+ return $this->pageWidth;
+ }
+ public function getPageHeight()
+ {
+ return $this->pageHeight;
+ }
- public function getPageMarginTop() { return $this->pageMarginTop; }
- public function getPageMarginBottom() { return $this->pageMarginBottom; }
- public function getPageMarginLeft() { return $this->pageMarginLeft; }
- public function getPageMarginRight() { return $this->pageMarginRight; }
+ public function getPageMarginTop()
+ {
+ return $this->pageMarginTop;
+ }
+ public function getPageMarginBottom()
+ {
+ return $this->pageMarginBottom;
+ }
+ public function getPageMarginLeft()
+ {
+ return $this->pageMarginLeft;
+ }
+ public function getPageMarginRight()
+ {
+ return $this->pageMarginRight;
+ }
- public function getColumns() { return $this->columns; }
- public function getRows() { return $this->rows; }
- public function getLabelBorder() { return 0; }
+ public function getColumns()
+ {
+ return $this->columns;
+ }
+ public function getRows()
+ {
+ return $this->rows;
+ }
+ public function getLabelBorder()
+ {
+ return 0;
+ }
- public function getLabelWidth() { return $this->labelWidth; }
- public function getLabelHeight() { return $this->labelHeight; }
+ public function getLabelWidth()
+ {
+ return $this->labelWidth;
+ }
+ public function getLabelHeight()
+ {
+ return $this->labelHeight;
+ }
- public function getLabelMarginTop() { return 0; }
- public function getLabelMarginBottom() { return 0; }
- public function getLabelMarginLeft() { return 0; }
- public function getLabelMarginRight() { return 0; }
+ public function getLabelMarginTop()
+ {
+ return 0;
+ }
+ public function getLabelMarginBottom()
+ {
+ return 0;
+ }
+ public function getLabelMarginLeft()
+ {
+ return 0;
+ }
+ public function getLabelMarginRight()
+ {
+ return 0;
+ }
- public function getLabelColumnSpacing() { return $this->labelSpacingH; }
- public function getLabelRowSpacing() { return $this->labelSpacingV; }
+ public function getLabelColumnSpacing()
+ {
+ return $this->labelSpacingH;
+ }
+ public function getLabelRowSpacing()
+ {
+ return $this->labelSpacingV;
+ }
- public function getSupportAssetTag() { return false; }
- public function getSupport1DBarcode() { return true; }
- public function getSupport2DBarcode() { return true; }
- public function getSupportFields() { return 4; }
- public function getSupportTitle() { return true; }
- public function getSupportLogo() { return true; }
+ public function getSupportAssetTag()
+ {
+ return false;
+ }
+ public function getSupport1DBarcode()
+ {
+ return true;
+ }
+ public function getSupport2DBarcode()
+ {
+ return true;
+ }
+ public function getSupportFields()
+ {
+ return 4;
+ }
+ public function getSupportTitle()
+ {
+ return true;
+ }
+ public function getSupportLogo()
+ {
+ return true;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$asset = $record->get('asset');
$settings = Setting::getSettings();
@@ -181,6 +257,25 @@ class DefaultLabel extends RectangleSheet
}
}
-}
+ private function setLabelWidth(Setting $settings)
+ {
+ $labelWidth = $settings->labels_width;
-?>
\ No newline at end of file
+ if ($labelWidth == 0) {
+ $labelWidth = 0.1;
+ }
+
+ return $labelWidth;
+ }
+
+ private function setLabelHeight(?Setting $settings)
+ {
+ $labelHeight = $settings->labels_height;
+
+ if ($labelHeight == 0) {
+ $labelHeight = 0.1;
+ }
+
+ return $labelHeight;
+ }
+}
diff --git a/app/Models/Labels/Field.php b/app/Models/Labels/Field.php
index c023f54175..cdd40cac49 100644
--- a/app/Models/Labels/Field.php
+++ b/app/Models/Labels/Field.php
@@ -5,21 +5,30 @@ namespace App\Models\Labels;
use App\Models\Asset;
use Illuminate\Support\Collection;
-class Field {
+class Field
+{
protected Collection $options;
- public function getOptions() { return $this->options; }
- public function setOptions($options) {
+ public function getOptions()
+ {
+ return $this->options;
+ }
+ public function setOptions($options)
+ {
$tempCollect = collect($options);
if (!$tempCollect->contains(fn($o) => !is_subclass_of($o, FieldOption::class))) {
$this->options = $options;
}
}
- public function toArray(Asset $asset) { return Field::makeArray($this, $asset); }
+ public function toArray(Asset $asset)
+ {
+ return Field::makeArray($this, $asset);
+ }
/* Statics */
- public static function makeArray(Field $field, Asset $asset) {
+ public static function makeArray(Field $field, Asset $asset)
+ {
return $field->getOptions()
// filter out any FieldOptions that are accidentally null
->filter()
@@ -27,11 +36,13 @@ class Field {
->filter(fn($result) => $result['value'] != null);
}
- public static function makeString(Field $option) {
+ public static function makeString(Field $option)
+ {
return implode('|', $option->getOptions());
}
- public static function fromString(string $theString) {
+ public static function fromString(string $theString)
+ {
$field = new Field();
$field->options = collect(explode('|', $theString))
->filter(fn($optionString) => !empty($optionString))
diff --git a/app/Models/Labels/FieldOption.php b/app/Models/Labels/FieldOption.php
index 94394eda23..cf65af57ba 100644
--- a/app/Models/Labels/FieldOption.php
+++ b/app/Models/Labels/FieldOption.php
@@ -5,14 +5,22 @@ namespace App\Models\Labels;
use App\Models\Asset;
use Illuminate\Support\Collection;
-class FieldOption {
+class FieldOption
+{
protected string $label;
- public function getLabel() { return $this->label; }
+ public function getLabel()
+ {
+ return $this->label;
+ }
protected string $dataSource;
- public function getDataSource() { return $this->dataSource; }
+ public function getDataSource()
+ {
+ return $this->dataSource;
+ }
- public function getValue(Asset $asset) {
+ public function getValue(Asset $asset)
+ {
$dataPath = collect(explode('.', $this->dataSource));
// assignedTo directly on the asset is a special case where
@@ -22,10 +30,10 @@ class FieldOption {
if ($asset->relationLoaded('assignedTo')) {
// If the "assignedTo" relationship was eager loaded then the way to get the
// relationship changes from $asset->assignedTo to $asset->assigned.
- return $asset->assigned ? $asset->assigned->present()->fullName() : null;
+ return $asset->assigned ? $asset->assigned->display_name : null;
}
- return $asset->assignedTo ? $asset->assignedTo->present()->fullName() : null;
+ return $asset->assignedTo ? $asset->assignedTo->display_name : null;
}
// Handle Laravel's stupid Carbon datetime casting
@@ -33,18 +41,29 @@ class FieldOption {
return $asset->purchase_date ? $asset->purchase_date->format('Y-m-d') : null;
}
- return $dataPath->reduce(function ($myValue, $path) {
- try { return $myValue ? $myValue->{$path} : ${$myValue}; }
- catch (\Exception $e) { return $myValue; }
- }, $asset);
+ return $dataPath->reduce(
+ function ($myValue, $path) {
+ try { return $myValue ? $myValue->{$path} : ${$myValue};
+ }
+ catch (\Exception $e) { return $myValue;
+ }
+ }, $asset
+ );
}
- public function toArray(Asset $asset=null) { return FieldOption::makeArray($this, $asset); }
- public function toString() { return FieldOption::makeString($this); }
+ public function toArray(Asset $asset=null)
+ {
+ return FieldOption::makeArray($this, $asset);
+ }
+ public function toString()
+ {
+ return FieldOption::makeString($this);
+ }
/* Statics */
- public static function makeArray(FieldOption $option, Asset $asset=null) {
+ public static function makeArray(FieldOption $option, Asset $asset=null)
+ {
return [
'label' => $option->getLabel(),
'dataSource' => $option->getDataSource(),
@@ -52,11 +71,13 @@ class FieldOption {
];
}
- public static function makeString(FieldOption $option) {
+ public static function makeString(FieldOption $option)
+ {
return $option->getLabel() . '=' . $option->getDataSource();
}
- public static function fromString(string $theString) {
+ public static function fromString(string $theString)
+ {
$parts = explode('=', $theString);
if (count($parts) == 2) {
$option = new FieldOption();
diff --git a/app/Models/Labels/Label.php b/app/Models/Labels/Label.php
index 2405da5401..cff859f359 100644
--- a/app/Models/Labels/Label.php
+++ b/app/Models/Labels/Label.php
@@ -13,7 +13,7 @@ use Illuminate\Support\Facades\Log;
/**
* Model for Labels.
*
- * @version v1.0
+ * @version v1.0
*/
abstract class Label
{
@@ -32,7 +32,8 @@ abstract class Label
*
* @return int
*/
- public function getRotation() {
+ public function getRotation()
+ {
return 0;
}
@@ -123,29 +124,32 @@ abstract class Label
/**
* Make changes to the PDF properties here. OPTIONAL.
*
- * @param TCPDF $pdf The TCPDF instance
+ * @param TCPDF $pdf The TCPDF instance
*/
public abstract function preparePDF(TCPDF $pdf);
/**
* Write single data record as content here.
*
- * @param TCPDF $pdf The TCPDF instance
- * @param Collection $record A data record
+ * @param TCPDF $pdf The TCPDF instance
+ * @param Collection $record A data record
*/
public abstract function write(TCPDF $pdf, Collection $record);
/**
* Handle the data here. Override for multiple-per-page handling
*
- * @param TCPDF $pdf The TCPDF instance
- * @param Collection $data The data
+ * @param TCPDF $pdf The TCPDF instance
+ * @param Collection $data The data
*/
- public function writeAll(TCPDF $pdf, Collection $data) {
- $data->each(function ($record, $index) use ($pdf) {
- $pdf->AddPage();
- $this->write($pdf, $record);
- });
+ public function writeAll(TCPDF $pdf, Collection $data)
+ {
+ $data->each(
+ function ($record, $index) use ($pdf) {
+ $pdf->AddPage();
+ $this->write($pdf, $record);
+ }
+ );
}
/**
@@ -153,7 +157,8 @@ abstract class Label
*
* @return string
*/
- public final function getName() {
+ public final function getName()
+ {
$refClass = new \ReflectionClass(Label::class);
return str_replace($refClass->getNamespaceName() . '\\', '', get_class($this));
}
@@ -165,7 +170,8 @@ abstract class Label
*
* @return string
*/
- public final function getOrientation() {
+ public final function getOrientation()
+ {
return ($this->getWidth() >= $this->getHeight()) ? 'L' : 'P';
}
@@ -174,7 +180,8 @@ abstract class Label
*
* @return object [ 'x1'=>0.00, 'y1'=>0.00, 'x2'=>0.00, 'y2'=>0.00, 'w'=>0.00, 'h'=>0.00 ]
*/
- public final function getPrintableArea() {
+ public final function getPrintableArea()
+ {
return (object)[
'x1' => $this->getMarginLeft(),
'y1' => $this->getMarginTop(),
@@ -188,21 +195,22 @@ abstract class Label
/**
* Write a text cell.
*
- * @param TCPDF $pdf The TCPDF instance
- * @param string $text The text to write. Supports 'some **bold** text'.
- * @param float $x X position of top-left
- * @param float $y Y position of top-left
- * @param string $font The font family
- * @param string $style The font style
- * @param int $size The font size in getUnit() units
- * @param string $align Align text in the box. 'L' left, 'R' right, 'C' center.
- * @param float $width Force text box width. NULL to auto-fit.
- * @param float $height Force text box height. NULL to auto-fit.
- * @param bool $squash Squash text if it's too big
- * @param int $border Thickness of border. Default = 0.
- * @param int $spacing Letter spacing. Default = 0.
+ * @param TCPDF $pdf The TCPDF instance
+ * @param string $text The text to write. Supports 'some **bold** text'.
+ * @param float $x X position of top-left
+ * @param float $y Y position of top-left
+ * @param string $font The font family
+ * @param string $style The font style
+ * @param int $size The font size in getUnit() units
+ * @param string $align Align text in the box. 'L' left, 'R' right, 'C' center.
+ * @param float $width Force text box width. NULL to auto-fit.
+ * @param float $height Force text box height. NULL to auto-fit.
+ * @param bool $squash Squash text if it's too big
+ * @param int $border Thickness of border. Default = 0.
+ * @param int $spacing Letter spacing. Default = 0.
*/
- public final function writeText(TCPDF $pdf, $text, $x, $y, $font=null, $style=null, $size=null, $align='L', $width=null, $height=null, $squash=false, $border=0, $spacing=0) {
+ public final function writeText(TCPDF $pdf, $text, $x, $y, $font=null, $style=null, $size=null, $align='L', $width=null, $height=null, $squash=false, $border=0, $spacing=0)
+ {
$prevFamily = $pdf->getFontFamily();
$prevStyle = $pdf->getFontStyle();
$prevSizePt = $pdf->getFontSizePt();
@@ -211,33 +219,42 @@ abstract class Label
$fontFamily = !empty($font) ? $font : $prevFamily;
$fontStyle = !empty($style) ? $style : $prevStyle;
- if ($size) $fontSizePt = Helper::convertUnit($size, $this->getUnit(), 'pt', true);
- else $fontSizePt = $prevSizePt;
+ if ($size) { $fontSizePt = Helper::convertUnit($size, $this->getUnit(), 'pt', true);
+ } else { $fontSizePt = $prevSizePt;
+ }
$pdf->SetFontSpacing($spacing);
$parts = collect(explode('**', $text))
- ->map(function ($part, $index) use ($pdf, $fontFamily, $fontStyle, $fontSizePt) {
- $modStyle = ($index % 2 == 1) ? 'B' : $fontStyle;
- $pdf->setFont($fontFamily, $modStyle, $fontSizePt);
- return [
+ ->map(
+ function ($part, $index) use ($pdf, $fontFamily, $fontStyle, $fontSizePt) {
+ $modStyle = ($index % 2 == 1) ? 'B' : $fontStyle;
+ $pdf->setFont($fontFamily, $modStyle, $fontSizePt);
+ return [
'text' => $part,
'text_width' => $pdf->GetStringWidth($part),
'font_family' => $fontFamily,
'font_style' => $modStyle,
'font_size' => $fontSizePt,
- ];
- });
+ ];
+ }
+ );
- $textWidth = $parts->reduce(function ($carry, $part) { return $carry += $part['text_width']; });
+ $textWidth = $parts->reduce(
+ function ($carry, $part) {
+ return $carry += $part['text_width'];
+ }
+ );
$cellWidth = !empty($width) ? $width : $textWidth;
if ($squash && ($textWidth > 0)) {
$scaleFactor = min(1.0, $cellWidth / $textWidth);
- $parts = $parts->map(function ($part, $index) use ($scaleFactor) {
- $part['text_width'] = $part['text_width'] * $scaleFactor;
- return $part;
- });
+ $parts = $parts->map(
+ function ($part, $index) use ($scaleFactor) {
+ $part['text_width'] = $part['text_width'] * $scaleFactor;
+ return $part;
+ }
+ );
}
$cellHeight = !empty($height) ? $height : Helper::convertUnit($fontSizePt, 'pt', $this->getUnit());
@@ -249,18 +266,23 @@ abstract class Label
}
switch($align) {
- case 'R': $startX = ($x + $cellWidth) - min($cellWidth, $textWidth); break;
- case 'C': $startX = ($x + ($cellWidth / 2)) - (min($cellWidth, $textWidth) / 2); break;
- case 'L':
- default: $startX = $x; break;
+ case 'R': $startX = ($x + $cellWidth) - min($cellWidth, $textWidth);
+ break;
+ case 'C': $startX = ($x + ($cellWidth / 2)) - (min($cellWidth, $textWidth) / 2);
+ break;
+ case 'L':
+ default: $startX = $x;
+ break;
}
- $parts->reduce(function ($currentX, $part) use ($pdf, $y, $cellHeight) {
- $pdf->SetXY($currentX, $y);
- $pdf->setFont($part['font_family'], $part['font_style'], $part['font_size']);
- $pdf->Cell($part['text_width'], $cellHeight, $part['text'], 0, 0, '', false, '', 1, true);
- return $currentX += $part['text_width'];
- }, $startX);
+ $parts->reduce(
+ function ($currentX, $part) use ($pdf, $y, $cellHeight) {
+ $pdf->SetXY($currentX, $y);
+ $pdf->setFont($part['font_family'], $part['font_style'], $part['font_size']);
+ $pdf->Cell($part['text_width'], $cellHeight, $part['text'], 0, 0, '', false, '', 1, true);
+ return $currentX += $part['text_width'];
+ }, $startX
+ );
$pdf->SetFont($prevFamily, $prevStyle, $prevSizePt);
$pdf->SetFontSpacing(0);
@@ -269,27 +291,30 @@ abstract class Label
/**
* Write an image.
*
- * @param TCPDF $pdf The TCPDF instance
- * @param string $image The image to write
- * @param float $x X position of top-left
- * @param float $y Y position of top-left
- * @param float $width The container width
- * @param float $height The container height
- * @param string $halign Align text in the box. 'L' left, 'R' right, 'C' center. Default 'L'.
- * @param string $valign Align text in the box. 'T' top, 'B' bottom, 'C' center. Default 'T'.
- * @param int $dpi Pixels per inch
- * @param bool $resize Resize to fit container
- * @param bool $stretch Stretch (vs Scale) to fit container
- * @param int $border Thickness of border. Default = 0.
+ * @param TCPDF $pdf The TCPDF instance
+ * @param string $image The image to write
+ * @param float $x X position of top-left
+ * @param float $y Y position of top-left
+ * @param float $width The container width
+ * @param float $height The container height
+ * @param string $halign Align text in the box. 'L' left, 'R' right, 'C' center. Default 'L'.
+ * @param string $valign Align text in the box. 'T' top, 'B' bottom, 'C' center. Default 'T'.
+ * @param int $dpi Pixels per inch
+ * @param bool $resize Resize to fit container
+ * @param bool $stretch Stretch (vs Scale) to fit container
+ * @param int $border Thickness of border. Default = 0.
*
* @return array Returns the final calculated size [w,h]
*/
- public final function writeImage(TCPDF $pdf, $image, $x, $y, $width=null, $height=null, $halign='L', $valign='L', $dpi=300, $resize=false, $stretch=false, $border=0) {
+ public final function writeImage(TCPDF $pdf, $image, $x, $y, $width=null, $height=null, $halign='L', $valign='L', $dpi=300, $resize=false, $stretch=false, $border=0)
+ {
- if (empty($image)) return [0,0];
+ if (empty($image)) { return [0,0];
+ }
$imageInfo = getimagesize($image);
- if (!$imageInfo) return [0,0]; // TODO: SVG or other
+ if (!$imageInfo) { return [0,0]; // TODO: SVG or other
+ }
$imageWidthPx = $imageInfo[0];
$imageHeightPx = $imageInfo[1];
@@ -342,18 +367,24 @@ abstract class Label
// Horizontal Position
switch ($halign) {
- case 'R': $originX = ($x + $containerWidth) - $outputWidth; break;
- case 'C': $originX = ($x + ($containerWidth / 2)) - ($outputWidth / 2); break;
- case 'L':
- default: $originX = $x; break;
+ case 'R': $originX = ($x + $containerWidth) - $outputWidth;
+ break;
+ case 'C': $originX = ($x + ($containerWidth / 2)) - ($outputWidth / 2);
+ break;
+ case 'L':
+ default: $originX = $x;
+ break;
}
// Vertical Position
switch ($valign) {
- case 'B': $originY = ($y + $containerHeight) - $outputHeight; break;
- case 'C': $originY = ($y + ($containerHeight / 2)) - ($outputHeight / 2); break;
- case 'T':
- default: $originY = $y; break;
+ case 'B': $originY = ($y + $containerHeight) - $outputHeight;
+ break;
+ case 'C': $originY = ($y + ($containerHeight / 2)) - ($outputHeight / 2);
+ break;
+ case 'T':
+ default: $originY = $y;
+ break;
}
// Actual Image
@@ -373,16 +404,18 @@ abstract class Label
/**
* Write a 1D barcode.
*
- * @param TCPDF $pdf The TCPDF instance
- * @param string $value The barcode content
- * @param string $type The barcode type
- * @param float $x X position of top-left
- * @param float $y Y position of top-left
- * @param float $width The container width
- * @param float $height The container height
+ * @param TCPDF $pdf The TCPDF instance
+ * @param string $value The barcode content
+ * @param string $type The barcode type
+ * @param float $x X position of top-left
+ * @param float $y Y position of top-left
+ * @param float $width The container width
+ * @param float $height The container height
*/
- public final function write1DBarcode(TCPDF $pdf, $value, $type, $x, $y, $width, $height) {
- if (empty($value)) return;
+ public final function write1DBarcode(TCPDF $pdf, $value, $type, $x, $y, $width, $height)
+ {
+ if (empty($value)) { return;
+ }
try {
$pdf->write1DBarcode($value, $type, $x, $y, $width, $height, null, ['stretch'=>true]);
} catch (\Exception|TypeError $e) {
@@ -393,16 +426,18 @@ abstract class Label
/**
* Write a 2D barcode.
*
- * @param TCPDF $pdf The TCPDF instance
- * @param string $value The barcode content
- * @param string $type The barcode type
- * @param float $x X position of top-left
- * @param float $y Y position of top-left
- * @param float $width The container width
- * @param float $height The container height
+ * @param TCPDF $pdf The TCPDF instance
+ * @param string $value The barcode content
+ * @param string $type The barcode type
+ * @param float $x X position of top-left
+ * @param float $y Y position of top-left
+ * @param float $width The container width
+ * @param float $height The container height
*/
- public final function write2DBarcode(TCPDF $pdf, $value, $type, $x, $y, $width, $height) {
- if (empty($value)) return;
+ public final function write2DBarcode(TCPDF $pdf, $value, $type, $x, $y, $width, $height)
+ {
+ if (empty($value)) { return;
+ }
$pdf->write2DBarcode($value, $type, $x, $y, $width, $height, null, ['stretch'=>true]);
}
@@ -411,127 +446,180 @@ abstract class Label
/**
* Checks the template is internally valid
*/
- public final function validate() : void {
+ public final function validate() : void
+ {
$this->validateUnits();
$this->validateSize();
$this->validateMargins();
$this->validateSupport();
}
- private function validateUnits() : void {
+ private function validateUnits() : void
+ {
$validUnits = [ 'pt', 'mm', 'cm', 'in' ];
$unit = $this->getUnit();
if (!in_array(strtolower($unit), $validUnits)) {
- throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_value', [
- 'name' => 'getUnit()',
- 'expected' => '[ \''.implode('\', \'', $validUnits).'\' ]',
- 'actual' => '\''.$unit.'\''
- ]));
+ throw new \UnexpectedValueException(
+ trans(
+ 'admin/labels/message.invalid_return_value', [
+ 'name' => 'getUnit()',
+ 'expected' => '[ \''.implode('\', \'', $validUnits).'\' ]',
+ 'actual' => '\''.$unit.'\''
+ ]
+ )
+ );
}
}
- private function validateSize() : void {
+ private function validateSize() : void
+ {
$width = $this->getWidth();
if (!is_numeric($width) || is_string($width)) {
- throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
- 'name' => 'getWidth()',
- 'expected' => 'float',
- 'actual' => gettype($width)
- ]));
+ throw new \UnexpectedValueException(
+ trans(
+ 'admin/labels/message.invalid_return_type', [
+ 'name' => 'getWidth()',
+ 'expected' => 'float',
+ 'actual' => gettype($width)
+ ]
+ )
+ );
}
$height = $this->getHeight();
if (!is_numeric($height) || is_string($height)) {
- throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
- 'name' => 'getHeight()',
- 'expected' => 'float',
- 'actual' => gettype($height)
- ]));
+ throw new \UnexpectedValueException(
+ trans(
+ 'admin/labels/message.invalid_return_type', [
+ 'name' => 'getHeight()',
+ 'expected' => 'float',
+ 'actual' => gettype($height)
+ ]
+ )
+ );
}
}
- private function validateMargins() : void {
+ private function validateMargins() : void
+ {
$marginTop = $this->getMarginTop();
if (!is_numeric($marginTop) || is_string($marginTop)) {
- throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
- 'name' => 'getMarginTop()',
- 'expected' => 'float',
- 'actual' => gettype($marginTop)
- ]));
+ throw new \UnexpectedValueException(
+ trans(
+ 'admin/labels/message.invalid_return_type', [
+ 'name' => 'getMarginTop()',
+ 'expected' => 'float',
+ 'actual' => gettype($marginTop)
+ ]
+ )
+ );
}
$marginBottom = $this->getMarginBottom();
if (!is_numeric($marginBottom) || is_string($marginBottom)) {
- throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
- 'name' => 'getMarginBottom()',
- 'expected' => 'float',
- 'actual' => gettype($marginBottom)
- ]));
+ throw new \UnexpectedValueException(
+ trans(
+ 'admin/labels/message.invalid_return_type', [
+ 'name' => 'getMarginBottom()',
+ 'expected' => 'float',
+ 'actual' => gettype($marginBottom)
+ ]
+ )
+ );
}
$marginLeft = $this->getMarginLeft();
if (!is_numeric($marginLeft) || is_string($marginLeft)) {
- throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
- 'name' => 'getMarginLeft()',
- 'expected' => 'float',
- 'actual' => gettype($marginLeft)
- ]));
+ throw new \UnexpectedValueException(
+ trans(
+ 'admin/labels/message.invalid_return_type', [
+ 'name' => 'getMarginLeft()',
+ 'expected' => 'float',
+ 'actual' => gettype($marginLeft)
+ ]
+ )
+ );
}
$marginRight = $this->getMarginRight();
if (!is_numeric($marginRight) || is_string($marginRight)) {
- throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
- 'name' => 'getMarginRight()',
- 'expected' => 'float',
- 'actual' => gettype($marginRight)
- ]));
+ throw new \UnexpectedValueException(
+ trans(
+ 'admin/labels/message.invalid_return_type', [
+ 'name' => 'getMarginRight()',
+ 'expected' => 'float',
+ 'actual' => gettype($marginRight)
+ ]
+ )
+ );
}
}
- private function validateSupport() : void {
+ private function validateSupport() : void
+ {
$support1D = $this->getSupport1DBarcode();
if (!is_bool($support1D)) {
- throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
- 'name' => 'getSupport1DBarcode()',
- 'expected' => 'boolean',
- 'actual' => gettype($support1D)
- ]));
+ throw new \UnexpectedValueException(
+ trans(
+ 'admin/labels/message.invalid_return_type', [
+ 'name' => 'getSupport1DBarcode()',
+ 'expected' => 'boolean',
+ 'actual' => gettype($support1D)
+ ]
+ )
+ );
}
$support2D = $this->getSupport2DBarcode();
if (!is_bool($support2D)) {
- throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
- 'name' => 'getSupport2DBarcode()',
- 'expected' => 'boolean',
- 'actual' => gettype($support2D)
- ]));
+ throw new \UnexpectedValueException(
+ trans(
+ 'admin/labels/message.invalid_return_type', [
+ 'name' => 'getSupport2DBarcode()',
+ 'expected' => 'boolean',
+ 'actual' => gettype($support2D)
+ ]
+ )
+ );
}
$supportFields = $this->getSupportFields();
if (!is_int($supportFields)) {
- throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
- 'name' => 'getSupportFields()',
- 'expected' => 'integer',
- 'actual' => gettype($supportFields)
- ]));
+ throw new \UnexpectedValueException(
+ trans(
+ 'admin/labels/message.invalid_return_type', [
+ 'name' => 'getSupportFields()',
+ 'expected' => 'integer',
+ 'actual' => gettype($supportFields)
+ ]
+ )
+ );
}
$supportLogo = $this->getSupportLogo();
if (!is_bool($supportLogo)) {
- throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
- 'name' => 'getSupportLogo()',
- 'expected' => 'boolean',
- 'actual' => gettype($supportLogo)
- ]));
+ throw new \UnexpectedValueException(
+ trans(
+ 'admin/labels/message.invalid_return_type', [
+ 'name' => 'getSupportLogo()',
+ 'expected' => 'boolean',
+ 'actual' => gettype($supportLogo)
+ ]
+ )
+ );
}
$supportTitle = $this->getSupportTitle();
if (!is_bool($supportTitle)) {
- throw new \UnexpectedValueException(trans('admin/labels/message.invalid_return_type', [
- 'name' => 'getSupportTitle()',
- 'expected' => 'boolean',
- 'actual' => gettype($supportTitle)
- ]));
+ throw new \UnexpectedValueException(
+ trans(
+ 'admin/labels/message.invalid_return_type', [
+ 'name' => 'getSupportTitle()',
+ 'expected' => 'boolean',
+ 'actual' => gettype($supportTitle)
+ ]
+ )
+ );
}
}
@@ -539,23 +627,26 @@ abstract class Label
/**
* Public Static Functions
- */
+ */
/**
* Find size of a page by its format.
*
- * @param string $format Format name (eg: 'A4', 'LETTER', etc.)
- * @param string $orientation 'L' for Landscape, 'P' for Portrait ('L' default)
- * @param string $unit Unit of measure to return in ('mm' default)
+ * @param string $format Format name (eg: 'A4', 'LETTER', etc.)
+ * @param string $orientation 'L' for Landscape, 'P' for Portrait ('L' default)
+ * @param string $unit Unit of measure to return in ('mm' default)
*
* @return object (object)[ 'width' => (float)123.4, 'height' => (float)123.4 ]
*/
- public static function fromFormat($format, $orientation='L', $unit='mm', $round=false) {
+ public static function fromFormat($format, $orientation='L', $unit='mm', $round=false)
+ {
$size = collect(TCPDF_STATIC::getPageSizeFromFormat(strtoupper($format)))
->sort()
- ->map(function ($value) use ($unit) {
- return Helper::convertUnit($value, 'pt', $unit);
- })
+ ->map(
+ function ($value) use ($unit) {
+ return Helper::convertUnit($value, 'pt', $unit);
+ }
+ )
->toArray();
$width = ($orientation == 'L') ? $size[1] : $size[0];
$height = ($orientation == 'L') ? $size[0] : $size[1];
@@ -571,16 +662,19 @@ abstract class Label
* Unlike most Models, these are defined by their existence as non-
* abstract classes stored in Models\Labels.
*
- * @param string|Arrayable|array|null $path Label path[s]
+ * @param string|Arrayable|array|null $path Label path[s]
* @return Collection|Label|null
*/
- public static function find($name=null) {
+ public static function find($name=null)
+ {
// Find many
if (is_array($name) || $name instanceof Arrayable) {
$labels = collect($name)
- ->map(function ($thisname) {
- return static::find($thisname);
- })
+ ->map(
+ function ($thisname) {
+ return static::find($thisname);
+ }
+ )
->whereNotNull();
return ($labels->count() > 0) ? $labels : null;
}
@@ -588,26 +682,36 @@ abstract class Label
// Find one
if ($name !== null) {
return static::find()
- ->sole(function ($label) use ($name) {
- return $label->getName() == $name;
- });
+ ->sole(
+ function ($label) use ($name) {
+ return $label->getName() == $name;
+ }
+ );
}
// Find all
return collect(File::allFiles(__DIR__))
- ->map(function ($file) {
- preg_match_all('/\/*(.+?)(?:\/|\.)/', $file->getRelativePathName(), $matches);
- return __NAMESPACE__ . '\\' . implode('\\', $matches[1]);
- })
- ->filter(function ($name) {
- if (!class_exists($name)) return false;
- $refClass = new \ReflectionClass($name);
- if ($refClass->isAbstract()) return false;
- return $refClass->isSubclassOf(Label::class);
- })
- ->map(function ($name) {
- return new $name();
- });
+ ->map(
+ function ($file) {
+ preg_match_all('/\/*(.+?)(?:\/|\.)/', $file->getRelativePathName(), $matches);
+ return __NAMESPACE__ . '\\' . implode('\\', $matches[1]);
+ }
+ )
+ ->filter(
+ function ($name) {
+ if (!class_exists($name)) { return false;
+ }
+ $refClass = new \ReflectionClass($name);
+ if ($refClass->isAbstract()) { return false;
+ }
+ return $refClass->isSubclassOf(Label::class);
+ }
+ )
+ ->map(
+ function ($name) {
+ return new $name();
+ }
+ );
}
diff --git a/app/Models/Labels/RectangleSheet.php b/app/Models/Labels/RectangleSheet.php
index f5fe5cda98..e9c6f007d9 100644
--- a/app/Models/Labels/RectangleSheet.php
+++ b/app/Models/Labels/RectangleSheet.php
@@ -33,9 +33,13 @@ abstract class RectangleSheet extends Sheet
public abstract function getLabelRowSpacing();
- public function getLabelsPerPage() { return $this->getColumns() * $this->getRows(); }
+ public function getLabelsPerPage()
+ {
+ return $this->getColumns() * $this->getRows();
+ }
- public function getLabelPosition($index) {
+ public function getLabelPosition($index)
+ {
$printIndex = $index + $this->getLabelIndexOffset();
$row = (int)($printIndex / $this->getColumns());
$col = $printIndex - ($row * $this->getColumns());
diff --git a/app/Models/Labels/Sheet.php b/app/Models/Labels/Sheet.php
index 83e363591a..63f9da173a 100644
--- a/app/Models/Labels/Sheet.php
+++ b/app/Models/Labels/Sheet.php
@@ -6,12 +6,30 @@ abstract class Sheet extends Label
{
protected int $indexOffset = 0;
- public function getWidth() { return $this->getPageWidth(); }
- public function getHeight() { return $this->getPageHeight(); }
- public function getMarginTop() { return $this->getPageMarginTop(); }
- public function getMarginBottom() { return $this->getPageMarginBottom(); }
- public function getMarginLeft() { return $this->getPageMarginLeft(); }
- public function getMarginRight() { return $this->getPageMarginRight(); }
+ public function getWidth()
+ {
+ return $this->getPageWidth();
+ }
+ public function getHeight()
+ {
+ return $this->getPageHeight();
+ }
+ public function getMarginTop()
+ {
+ return $this->getPageMarginTop();
+ }
+ public function getMarginBottom()
+ {
+ return $this->getPageMarginBottom();
+ }
+ public function getMarginLeft()
+ {
+ return $this->getPageMarginLeft();
+ }
+ public function getMarginRight()
+ {
+ return $this->getPageMarginRight();
+ }
/**
* Returns the page width in getUnit() units
@@ -107,7 +125,7 @@ abstract class Sheet extends Label
/**
* Returns label position based on its index
*
- * @param int $index
+ * @param int $index
*
* @return array [x,y]
*/
@@ -123,10 +141,11 @@ abstract class Sheet extends Label
/**
* Handle the data here. Override for multiple-per-page handling
*
- * @param TCPDF $pdf The TCPDF instance
- * @param Collection $data The data
+ * @param TCPDF $pdf The TCPDF instance
+ * @param Collection $data The data
*/
- public function writeAll($pdf, $data) {
+ public function writeAll($pdf, $data)
+ {
$prevPageNumber = -1;
foreach ($data->toArray() as $recordIndex => $record) {
@@ -170,7 +189,8 @@ abstract class Sheet extends Label
*
* @return string
*/
- public final function getLabelOrientation() {
+ public final function getLabelOrientation()
+ {
return ($this->getLabelWidth() >= $this->getLabelHeight()) ? 'L' : 'P';
}
@@ -179,7 +199,8 @@ abstract class Sheet extends Label
*
* @return object [ 'x1'=>0.00, 'y1'=>0.00, 'x2'=>0.00, 'y2'=>0.00, 'w'=>0.00, 'h'=>0.00 ]
*/
- public final function getLabelPrintableArea() {
+ public final function getLabelPrintableArea()
+ {
return (object)[
'x1' => $this->getLabelMarginLeft(),
'y1' => $this->getLabelMarginTop(),
@@ -195,15 +216,20 @@ abstract class Sheet extends Label
*
* @return int
*/
- public function getLabelIndexOffset() { return $this->indexOffset; }
+ public function getLabelIndexOffset()
+ {
+ return $this->indexOffset;
+ }
/**
* Sets label index offset (skip positions)
*
- * @param int $offset
- *
+ * @param int $offset
*/
- public function setLabelIndexOffset(int $offset) { $this->indexOffset = $offset; }
+ public function setLabelIndexOffset(int $offset)
+ {
+ $this->indexOffset = $offset;
+ }
}
?>
\ No newline at end of file
diff --git a/app/Models/Labels/Sheets/Avery/L6009.php b/app/Models/Labels/Sheets/Avery/L6009.php
new file mode 100644
index 0000000000..11d6cfffca
--- /dev/null
+++ b/app/Models/Labels/Sheets/Avery/L6009.php
@@ -0,0 +1,110 @@
+getUnit(), 0);
+ $this->pageWidth = $paperSize->width;
+ $this->pageHeight = $paperSize->height;
+
+ $this->pageMarginLeft = Helper::convertUnit(self::COLUMN1_X, 'pt', $this->getUnit());
+ $this->pageMarginTop = Helper::convertUnit(self::ROW1_Y, 'pt', $this->getUnit());
+
+ $columnSpacingPt = self::COLUMN2_X - self::COLUMN1_X - self::LABEL_W;
+ $this->columnSpacing = Helper::convertUnit($columnSpacingPt, 'pt', $this->getUnit());
+ $rowSpacingPt = self::ROW2_Y - self::ROW1_Y - self::LABEL_H;
+ $this->rowSpacing = Helper::convertUnit($rowSpacingPt, 'pt', $this->getUnit());
+
+ $this->labelWidth = Helper::convertUnit(self::LABEL_W, 'pt', $this->getUnit());
+ $this->labelHeight = Helper::convertUnit(self::LABEL_H, 'pt', $this->getUnit());
+ }
+
+ public function getPageWidth()
+ {
+ return $this->pageWidth;
+ }
+ public function getPageHeight()
+ {
+ return $this->pageHeight;
+ }
+
+ public function getPageMarginTop()
+ {
+ return $this->pageMarginTop;
+ }
+ public function getPageMarginBottom()
+ {
+ return $this->pageMarginTop;
+ }
+ public function getPageMarginLeft()
+ {
+ return $this->pageMarginLeft;
+ }
+ public function getPageMarginRight()
+ {
+ return $this->pageMarginLeft;
+ }
+
+ public function getColumns()
+ {
+ return 4;
+ }
+ public function getRows()
+ {
+ return 12;
+ }
+
+ public function getLabelColumnSpacing()
+ {
+ return $this->columnSpacing;
+ }
+ public function getLabelRowSpacing()
+ {
+ return $this->rowSpacing;
+ }
+
+ public function getLabelWidth()
+ {
+ return $this->labelWidth;
+ }
+ public function getLabelHeight()
+ {
+ return $this->labelHeight;
+ }
+
+ public function getLabelBorder()
+ {
+ return 0;
+ }
+}
+
+?>
diff --git a/app/Models/Labels/Sheets/Avery/L6009_A.php b/app/Models/Labels/Sheets/Avery/L6009_A.php
new file mode 100644
index 0000000000..09f3aa8358
--- /dev/null
+++ b/app/Models/Labels/Sheets/Avery/L6009_A.php
@@ -0,0 +1,121 @@
+getLabelPrintableArea();
+
+ $currentX = $pa->x1;
+ $currentY = $pa->y1;
+ $usableWidth = $pa->w;
+ $usableHeight = $pa->h;
+
+ if ($record->has('title')) {
+ static::writeText(
+ $pdf, $record->get('title'),
+ $pa->x1, $pa->y1,
+ 'freesans', '', self::TITLE_SIZE, 'C',
+ $pa->w, self::TITLE_SIZE, true, 0
+ );
+
+ }
+ $currentY += self::TITLE_SIZE + self::TITLE_MARGIN;
+ $usableHeight -= self::TITLE_SIZE + self::TITLE_MARGIN;
+ $barcodeSize = $usableHeight;
+ if ($record->has('barcode2d')) {
+ static::write2DBarcode(
+ $pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type,
+ $currentX, $currentY,
+ $barcodeSize, $barcodeSize
+ );
+ $currentX += $barcodeSize + self::BARCODE_MARGIN;
+ $usableWidth -= $barcodeSize + self::BARCODE_MARGIN;
+ }
+
+ foreach ($record->get('fields') as $field) {
+ static::writeText(
+ $pdf, $field['label'],
+ $currentX, $currentY,
+ 'freesans', '', self::LABEL_SIZE, 'L',
+ $usableWidth, self::LABEL_SIZE, true, 0
+ );
+ $currentY += self::LABEL_SIZE + self::LABEL_MARGIN;
+
+ static::writeText(
+ $pdf, $field['value'],
+ $currentX, $currentY,
+ 'freemono', 'B', self::FIELD_SIZE, 'L',
+ $usableWidth, self::FIELD_SIZE, true, 0, 0.01
+ );
+ $currentY += self::FIELD_SIZE + self::FIELD_MARGIN;
+ }
+
+ }
+}
+
+
+?>
diff --git a/app/Models/Labels/Sheets/Avery/L7162.php b/app/Models/Labels/Sheets/Avery/L7162.php
index e1097db9b2..d0e6c673c7 100644
--- a/app/Models/Labels/Sheets/Avery/L7162.php
+++ b/app/Models/Labels/Sheets/Avery/L7162.php
@@ -31,7 +31,8 @@ abstract class L7162 extends RectangleSheet
private float $labelWidth;
private float $labelHeight;
- public function __construct() {
+ public function __construct()
+ {
$paperSize = static::fromFormat(self::PAPER_FORMAT, self::PAPER_ORIENTATION, $this->getUnit(), 0);
$this->pageWidth = $paperSize->width;
$this->pageHeight = $paperSize->height;
@@ -48,24 +49,63 @@ abstract class L7162 extends RectangleSheet
$this->labelHeight = Helper::convertUnit(self::LABEL_H, 'pt', $this->getUnit());
}
- public function getPageWidth() { return $this->pageWidth; }
- public function getPageHeight() { return $this->pageHeight; }
+ public function getPageWidth()
+ {
+ return $this->pageWidth;
+ }
+ public function getPageHeight()
+ {
+ return $this->pageHeight;
+ }
- public function getPageMarginTop() { return $this->pageMarginTop; }
- public function getPageMarginBottom() { return $this->pageMarginTop; }
- public function getPageMarginLeft() { return $this->pageMarginLeft; }
- public function getPageMarginRight() { return $this->pageMarginLeft; }
+ public function getPageMarginTop()
+ {
+ return $this->pageMarginTop;
+ }
+ public function getPageMarginBottom()
+ {
+ return $this->pageMarginTop;
+ }
+ public function getPageMarginLeft()
+ {
+ return $this->pageMarginLeft;
+ }
+ public function getPageMarginRight()
+ {
+ return $this->pageMarginLeft;
+ }
- public function getColumns() { return 2; }
- public function getRows() { return 8; }
+ public function getColumns()
+ {
+ return 2;
+ }
+ public function getRows()
+ {
+ return 8;
+ }
- public function getLabelColumnSpacing() { return $this->columnSpacing; }
- public function getLabelRowSpacing() { return $this->rowSpacing; }
+ public function getLabelColumnSpacing()
+ {
+ return $this->columnSpacing;
+ }
+ public function getLabelRowSpacing()
+ {
+ return $this->rowSpacing;
+ }
- public function getLabelWidth() { return $this->labelWidth; }
- public function getLabelHeight() { return $this->labelHeight; }
+ public function getLabelWidth()
+ {
+ return $this->labelWidth;
+ }
+ public function getLabelHeight()
+ {
+ return $this->labelHeight;
+ }
- public function getLabelBorder() { return 0; }
+ public function getLabelBorder()
+ {
+ return 0;
+ }
}
?>
\ No newline at end of file
diff --git a/app/Models/Labels/Sheets/Avery/L7162_A.php b/app/Models/Labels/Sheets/Avery/L7162_A.php
index 0b3312ba7c..adfe7f382a 100644
--- a/app/Models/Labels/Sheets/Avery/L7162_A.php
+++ b/app/Models/Labels/Sheets/Avery/L7162_A.php
@@ -14,23 +14,59 @@ class L7162_A extends L7162
private const FIELD_SIZE = 4.60;
private const FIELD_MARGIN = 0.30;
- public function getUnit() { return 'mm'; }
+ public function getUnit()
+ {
+ return 'mm';
+ }
- public function getLabelMarginTop() { return 1.0; }
- public function getLabelMarginBottom() { return 1.0; }
- public function getLabelMarginLeft() { return 1.0; }
- public function getLabelMarginRight() { return 1.0; }
+ public function getLabelMarginTop()
+ {
+ return 1.0;
+ }
+ public function getLabelMarginBottom()
+ {
+ return 1.0;
+ }
+ public function getLabelMarginLeft()
+ {
+ return 1.0;
+ }
+ public function getLabelMarginRight()
+ {
+ return 1.0;
+ }
- public function getSupportAssetTag() { return true; }
- public function getSupport1DBarcode() { return false; }
- public function getSupport2DBarcode() { return true; }
- public function getSupportFields() { return 4; }
- public function getSupportLogo() { return false; }
- public function getSupportTitle() { return true; }
+ public function getSupportAssetTag()
+ {
+ return true;
+ }
+ public function getSupport1DBarcode()
+ {
+ return false;
+ }
+ public function getSupport2DBarcode()
+ {
+ return true;
+ }
+ public function getSupportFields()
+ {
+ return 4;
+ }
+ public function getSupportLogo()
+ {
+ return false;
+ }
+ public function getSupportTitle()
+ {
+ return true;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getLabelPrintableArea();
$usableWidth = $pa->w;
diff --git a/app/Models/Labels/Sheets/Avery/L7162_B.php b/app/Models/Labels/Sheets/Avery/L7162_B.php
index 268754e04f..833653cabf 100644
--- a/app/Models/Labels/Sheets/Avery/L7162_B.php
+++ b/app/Models/Labels/Sheets/Avery/L7162_B.php
@@ -17,23 +17,59 @@ class L7162_B extends L7162
private const FIELD_SIZE = 4.20;
private const FIELD_MARGIN = 0.30;
- public function getUnit() { return 'mm'; }
+ public function getUnit()
+ {
+ return 'mm';
+ }
- public function getLabelMarginTop() { return 1.0; }
- public function getLabelMarginBottom() { return 0; }
- public function getLabelMarginLeft() { return 1.0; }
- public function getLabelMarginRight() { return 1.0; }
+ public function getLabelMarginTop()
+ {
+ return 1.0;
+ }
+ public function getLabelMarginBottom()
+ {
+ return 0;
+ }
+ public function getLabelMarginLeft()
+ {
+ return 1.0;
+ }
+ public function getLabelMarginRight()
+ {
+ return 1.0;
+ }
- public function getSupportAssetTag() { return true; }
- public function getSupport1DBarcode() { return true; }
- public function getSupport2DBarcode() { return false; }
- public function getSupportFields() { return 3; }
- public function getSupportLogo() { return true; }
- public function getSupportTitle() { return true; }
+ public function getSupportAssetTag()
+ {
+ return true;
+ }
+ public function getSupport1DBarcode()
+ {
+ return true;
+ }
+ public function getSupport2DBarcode()
+ {
+ return false;
+ }
+ public function getSupportFields()
+ {
+ return 3;
+ }
+ public function getSupportLogo()
+ {
+ return true;
+ }
+ public function getSupportTitle()
+ {
+ return true;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getLabelPrintableArea();
$usableWidth = $pa->w;
@@ -55,7 +91,7 @@ class L7162_B extends L7162
$pdf, $record->get('logo'),
$pa->x1, $pa->y1,
self::LOGO_MAX_WIDTH, $usableHeight,
- 'L', 'T', 300, true, false, 0.1
+ 'L', 'T', 300, true, false, 0
);
$currentX += $logoSize[0] + self::LOGO_MARGIN;
$usableWidth -= $logoSize[0] + self::LOGO_MARGIN;
@@ -100,4 +136,4 @@ class L7162_B extends L7162
}
-?>
\ No newline at end of file
+?>
diff --git a/app/Models/Labels/Sheets/Avery/L7163.php b/app/Models/Labels/Sheets/Avery/L7163.php
index f143260336..01fe9a78e0 100644
--- a/app/Models/Labels/Sheets/Avery/L7163.php
+++ b/app/Models/Labels/Sheets/Avery/L7163.php
@@ -31,7 +31,8 @@ abstract class L7163 extends RectangleSheet
private float $labelWidth;
private float $labelHeight;
- public function __construct() {
+ public function __construct()
+ {
$paperSize = static::fromFormat(self::PAPER_FORMAT, self::PAPER_ORIENTATION, $this->getUnit(), 0);
$this->pageWidth = $paperSize->width;
$this->pageHeight = $paperSize->height;
@@ -48,24 +49,63 @@ abstract class L7163 extends RectangleSheet
$this->labelHeight = Helper::convertUnit(self::LABEL_H, 'pt', $this->getUnit());
}
- public function getPageWidth() { return $this->pageWidth; }
- public function getPageHeight() { return $this->pageHeight; }
+ public function getPageWidth()
+ {
+ return $this->pageWidth;
+ }
+ public function getPageHeight()
+ {
+ return $this->pageHeight;
+ }
- public function getPageMarginTop() { return $this->pageMarginTop; }
- public function getPageMarginBottom() { return $this->pageMarginTop; }
- public function getPageMarginLeft() { return $this->pageMarginLeft; }
- public function getPageMarginRight() { return $this->pageMarginLeft; }
+ public function getPageMarginTop()
+ {
+ return $this->pageMarginTop;
+ }
+ public function getPageMarginBottom()
+ {
+ return $this->pageMarginTop;
+ }
+ public function getPageMarginLeft()
+ {
+ return $this->pageMarginLeft;
+ }
+ public function getPageMarginRight()
+ {
+ return $this->pageMarginLeft;
+ }
- public function getColumns() { return 2; }
- public function getRows() { return 7; }
+ public function getColumns()
+ {
+ return 2;
+ }
+ public function getRows()
+ {
+ return 7;
+ }
- public function getLabelColumnSpacing() { return $this->columnSpacing; }
- public function getLabelRowSpacing() { return $this->rowSpacing; }
+ public function getLabelColumnSpacing()
+ {
+ return $this->columnSpacing;
+ }
+ public function getLabelRowSpacing()
+ {
+ return $this->rowSpacing;
+ }
- public function getLabelWidth() { return $this->labelWidth; }
- public function getLabelHeight() { return $this->labelHeight; }
+ public function getLabelWidth()
+ {
+ return $this->labelWidth;
+ }
+ public function getLabelHeight()
+ {
+ return $this->labelHeight;
+ }
- public function getLabelBorder() { return 0; }
+ public function getLabelBorder()
+ {
+ return 0;
+ }
}
?>
\ No newline at end of file
diff --git a/app/Models/Labels/Sheets/Avery/L7163_A.php b/app/Models/Labels/Sheets/Avery/L7163_A.php
index 6dc33f64dd..47a44d75db 100644
--- a/app/Models/Labels/Sheets/Avery/L7163_A.php
+++ b/app/Models/Labels/Sheets/Avery/L7163_A.php
@@ -14,23 +14,59 @@ class L7163_A extends L7163
private const FIELD_SIZE = 4.80;
private const FIELD_MARGIN = 0.30;
- public function getUnit() { return 'mm'; }
+ public function getUnit()
+ {
+ return 'mm';
+ }
- public function getLabelMarginTop() { return 1.0; }
- public function getLabelMarginBottom() { return 1.0; }
- public function getLabelMarginLeft() { return 1.0; }
- public function getLabelMarginRight() { return 1.0; }
+ public function getLabelMarginTop()
+ {
+ return 1.0;
+ }
+ public function getLabelMarginBottom()
+ {
+ return 1.0;
+ }
+ public function getLabelMarginLeft()
+ {
+ return 1.0;
+ }
+ public function getLabelMarginRight()
+ {
+ return 1.0;
+ }
- public function getSupportAssetTag() { return true; }
- public function getSupport1DBarcode() { return false; }
- public function getSupport2DBarcode() { return true; }
- public function getSupportFields() { return 4; }
- public function getSupportLogo() { return false; }
- public function getSupportTitle() { return true; }
+ public function getSupportAssetTag()
+ {
+ return true;
+ }
+ public function getSupport1DBarcode()
+ {
+ return false;
+ }
+ public function getSupport2DBarcode()
+ {
+ return true;
+ }
+ public function getSupportFields()
+ {
+ return 4;
+ }
+ public function getSupportLogo()
+ {
+ return false;
+ }
+ public function getSupportTitle()
+ {
+ return true;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getLabelPrintableArea();
$usableWidth = $pa->w;
diff --git a/app/Models/Labels/Sheets/Avery/_3490.php b/app/Models/Labels/Sheets/Avery/_3490.php
new file mode 100644
index 0000000000..c81c980e11
--- /dev/null
+++ b/app/Models/Labels/Sheets/Avery/_3490.php
@@ -0,0 +1,111 @@
+getUnit(), 2);
+ $this->pageWidth = $paperSize->width;
+ $this->pageHeight = $paperSize->height;
+
+ $this->pageMarginLeft = Helper::convertUnit(self::COLUMN1_X, 'pt', $this->getUnit());
+ $this->pageMarginTop = Helper::convertUnit(self::ROW1_Y, 'pt', $this->getUnit());
+
+ $columnSpacingPt = self::COLUMN2_X - self::COLUMN1_X - self::LABEL_W;
+ $this->columnSpacing = Helper::convertUnit($columnSpacingPt, 'pt', $this->getUnit());
+ $rowSpacingPt = self::ROW2_Y - self::ROW1_Y - self::LABEL_H;
+ $this->rowSpacing = Helper::convertUnit($rowSpacingPt, 'pt', $this->getUnit());
+
+ $this->labelWidth = Helper::convertUnit(self::LABEL_W, 'pt', $this->getUnit());
+ $this->labelHeight = Helper::convertUnit(self::LABEL_H, 'pt', $this->getUnit());
+ }
+
+ public function getPageWidth()
+ {
+ return $this->pageWidth;
+ }
+ public function getPageHeight()
+ {
+ return $this->pageHeight;
+ }
+
+ public function getPageMarginTop()
+ {
+ return $this->pageMarginTop;
+ }
+ public function getPageMarginBottom()
+ {
+ return $this->pageMarginTop;
+ }
+ public function getPageMarginLeft()
+ {
+ return $this->pageMarginLeft;
+ }
+ public function getPageMarginRight()
+ {
+ return $this->pageMarginLeft;
+ }
+
+ public function getColumns()
+ {
+ return 3;
+ }
+ public function getRows()
+ {
+ return 10;
+ }
+
+ public function getLabelColumnSpacing()
+ {
+ return $this->columnSpacing;
+ }
+ public function getLabelRowSpacing()
+ {
+ return $this->rowSpacing;
+ }
+
+ public function getLabelWidth()
+ {
+ return $this->labelWidth;
+ }
+ public function getLabelHeight()
+ {
+ return $this->labelHeight;
+ }
+
+ public function getLabelBorder()
+ {
+ return 0;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/app/Models/Labels/Sheets/Avery/_3490_A.php b/app/Models/Labels/Sheets/Avery/_3490_A.php
new file mode 100644
index 0000000000..591f8318aa
--- /dev/null
+++ b/app/Models/Labels/Sheets/Avery/_3490_A.php
@@ -0,0 +1,121 @@
+getLabelPrintableArea();
+
+ $currentX = $pa->x1;
+ $currentY = $pa->y1;
+ $usableWidth = $pa->w;
+ $usableHeight = $pa->h;
+
+ if ($record->has('title')) {
+ static::writeText(
+ $pdf, $record->get('title'),
+ $pa->x1, $pa->y1,
+ 'freesans', '', self::TITLE_SIZE, 'C',
+ $pa->w, self::TITLE_SIZE, true, 0
+ );
+ $currentY += self::TITLE_SIZE + self::TITLE_MARGIN;
+ $usableHeight -= self::TITLE_SIZE + self::TITLE_MARGIN;
+ }
+
+ $barcodeSize = $usableHeight;
+ if ($record->has('barcode2d')) {
+ static::write2DBarcode(
+ $pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type,
+ $currentX, $currentY,
+ $barcodeSize, $barcodeSize
+ );
+ $currentX += $barcodeSize + self::BARCODE_MARGIN;
+ $usableWidth -= $barcodeSize + self::BARCODE_MARGIN;
+ }
+
+ foreach ($record->get('fields') as $field) {
+ static::writeText(
+ $pdf, $field['label'],
+ $currentX, $currentY,
+ 'freesans', '', self::LABEL_SIZE, 'L',
+ $usableWidth, self::LABEL_SIZE, true, 0
+ );
+ $currentY += self::LABEL_SIZE + self::LABEL_MARGIN;
+
+ static::writeText(
+ $pdf, $field['value'],
+ $currentX, $currentY,
+ 'freemono', 'B', self::FIELD_SIZE, 'L',
+ $usableWidth, self::FIELD_SIZE, true, 0, 0.01
+ );
+ $currentY += self::FIELD_SIZE + self::FIELD_MARGIN;
+ }
+
+ }
+}
+
+
+?>
\ No newline at end of file
diff --git a/app/Models/Labels/Sheets/Avery/_5267.php b/app/Models/Labels/Sheets/Avery/_5267.php
index f5f2f13557..882c862691 100644
--- a/app/Models/Labels/Sheets/Avery/_5267.php
+++ b/app/Models/Labels/Sheets/Avery/_5267.php
@@ -31,7 +31,8 @@ abstract class _5267 extends RectangleSheet
private float $labelWidth;
private float $labelHeight;
- public function __construct() {
+ public function __construct()
+ {
$paperSize = static::fromFormat(self::PAPER_FORMAT, self::PAPER_ORIENTATION, $this->getUnit(), 2);
$this->pageWidth = $paperSize->width;
$this->pageHeight = $paperSize->height;
@@ -48,24 +49,63 @@ abstract class _5267 extends RectangleSheet
$this->labelHeight = Helper::convertUnit(self::LABEL_H, 'pt', $this->getUnit());
}
- public function getPageWidth() { return $this->pageWidth; }
- public function getPageHeight() { return $this->pageHeight; }
+ public function getPageWidth()
+ {
+ return $this->pageWidth;
+ }
+ public function getPageHeight()
+ {
+ return $this->pageHeight;
+ }
- public function getPageMarginTop() { return $this->pageMarginTop; }
- public function getPageMarginBottom() { return $this->pageMarginTop; }
- public function getPageMarginLeft() { return $this->pageMarginLeft; }
- public function getPageMarginRight() { return $this->pageMarginLeft; }
+ public function getPageMarginTop()
+ {
+ return $this->pageMarginTop;
+ }
+ public function getPageMarginBottom()
+ {
+ return $this->pageMarginTop;
+ }
+ public function getPageMarginLeft()
+ {
+ return $this->pageMarginLeft;
+ }
+ public function getPageMarginRight()
+ {
+ return $this->pageMarginLeft;
+ }
- public function getColumns() { return 4; }
- public function getRows() { return 20; }
+ public function getColumns()
+ {
+ return 4;
+ }
+ public function getRows()
+ {
+ return 20;
+ }
- public function getLabelColumnSpacing() { return $this->columnSpacing; }
- public function getLabelRowSpacing() { return $this->rowSpacing; }
+ public function getLabelColumnSpacing()
+ {
+ return $this->columnSpacing;
+ }
+ public function getLabelRowSpacing()
+ {
+ return $this->rowSpacing;
+ }
- public function getLabelWidth() { return $this->labelWidth; }
- public function getLabelHeight() { return $this->labelHeight; }
+ public function getLabelWidth()
+ {
+ return $this->labelWidth;
+ }
+ public function getLabelHeight()
+ {
+ return $this->labelHeight;
+ }
- public function getLabelBorder() { return 0; }
+ public function getLabelBorder()
+ {
+ return 0;
+ }
}
?>
\ No newline at end of file
diff --git a/app/Models/Labels/Sheets/Avery/_5267_A.php b/app/Models/Labels/Sheets/Avery/_5267_A.php
index efe0855d5e..1ed6f557f0 100644
--- a/app/Models/Labels/Sheets/Avery/_5267_A.php
+++ b/app/Models/Labels/Sheets/Avery/_5267_A.php
@@ -12,23 +12,59 @@ class _5267_A extends _5267
private const FIELD_SIZE = 0.150;
private const FIELD_MARGIN = 0.012;
- public function getUnit() { return 'in'; }
+ public function getUnit()
+ {
+ return 'in';
+ }
- public function getLabelMarginTop() { return 0.02; }
- public function getLabelMarginBottom() { return 0.00; }
- public function getLabelMarginLeft() { return 0.04; }
- public function getLabelMarginRight() { return 0.04; }
+ public function getLabelMarginTop()
+ {
+ return 0.02;
+ }
+ public function getLabelMarginBottom()
+ {
+ return 0.00;
+ }
+ public function getLabelMarginLeft()
+ {
+ return 0.04;
+ }
+ public function getLabelMarginRight()
+ {
+ return 0.04;
+ }
- public function getSupportAssetTag() { return false; }
- public function getSupport1DBarcode() { return true; }
- public function getSupport2DBarcode() { return false; }
- public function getSupportFields() { return 1; }
- public function getSupportLogo() { return false; }
- public function getSupportTitle() { return true; }
+ public function getSupportAssetTag()
+ {
+ return false;
+ }
+ public function getSupport1DBarcode()
+ {
+ return true;
+ }
+ public function getSupport2DBarcode()
+ {
+ return false;
+ }
+ public function getSupportFields()
+ {
+ return 1;
+ }
+ public function getSupportLogo()
+ {
+ return false;
+ }
+ public function getSupportTitle()
+ {
+ return true;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getLabelPrintableArea();
if ($record->has('barcode1d')) {
diff --git a/app/Models/Labels/Sheets/Avery/_5520.php b/app/Models/Labels/Sheets/Avery/_5520.php
index 00cb0e0687..ae9ff0b039 100644
--- a/app/Models/Labels/Sheets/Avery/_5520.php
+++ b/app/Models/Labels/Sheets/Avery/_5520.php
@@ -31,7 +31,8 @@ abstract class _5520 extends RectangleSheet
private float $labelWidth;
private float $labelHeight;
- public function __construct() {
+ public function __construct()
+ {
$paperSize = static::fromFormat(self::PAPER_FORMAT, self::PAPER_ORIENTATION, $this->getUnit(), 2);
$this->pageWidth = $paperSize->width;
$this->pageHeight = $paperSize->height;
@@ -48,24 +49,63 @@ abstract class _5520 extends RectangleSheet
$this->labelHeight = Helper::convertUnit(self::LABEL_H, 'pt', $this->getUnit());
}
- public function getPageWidth() { return $this->pageWidth; }
- public function getPageHeight() { return $this->pageHeight; }
+ public function getPageWidth()
+ {
+ return $this->pageWidth;
+ }
+ public function getPageHeight()
+ {
+ return $this->pageHeight;
+ }
- public function getPageMarginTop() { return $this->pageMarginTop; }
- public function getPageMarginBottom() { return $this->pageMarginTop; }
- public function getPageMarginLeft() { return $this->pageMarginLeft; }
- public function getPageMarginRight() { return $this->pageMarginLeft; }
+ public function getPageMarginTop()
+ {
+ return $this->pageMarginTop;
+ }
+ public function getPageMarginBottom()
+ {
+ return $this->pageMarginTop;
+ }
+ public function getPageMarginLeft()
+ {
+ return $this->pageMarginLeft;
+ }
+ public function getPageMarginRight()
+ {
+ return $this->pageMarginLeft;
+ }
- public function getColumns() { return 3; }
- public function getRows() { return 10; }
+ public function getColumns()
+ {
+ return 3;
+ }
+ public function getRows()
+ {
+ return 10;
+ }
- public function getLabelColumnSpacing() { return $this->columnSpacing; }
- public function getLabelRowSpacing() { return $this->rowSpacing; }
+ public function getLabelColumnSpacing()
+ {
+ return $this->columnSpacing;
+ }
+ public function getLabelRowSpacing()
+ {
+ return $this->rowSpacing;
+ }
- public function getLabelWidth() { return $this->labelWidth; }
- public function getLabelHeight() { return $this->labelHeight; }
+ public function getLabelWidth()
+ {
+ return $this->labelWidth;
+ }
+ public function getLabelHeight()
+ {
+ return $this->labelHeight;
+ }
- public function getLabelBorder() { return 0; }
+ public function getLabelBorder()
+ {
+ return 0;
+ }
}
?>
\ No newline at end of file
diff --git a/app/Models/Labels/Sheets/Avery/_5520_A.php b/app/Models/Labels/Sheets/Avery/_5520_A.php
index 199566d248..9d2c91502d 100644
--- a/app/Models/Labels/Sheets/Avery/_5520_A.php
+++ b/app/Models/Labels/Sheets/Avery/_5520_A.php
@@ -14,23 +14,59 @@ class _5520_A extends _5520
private const FIELD_SIZE = 0.150;
private const FIELD_MARGIN = 0.012;
- public function getUnit() { return 'in'; }
+ public function getUnit()
+ {
+ return 'in';
+ }
- public function getLabelMarginTop() { return 0.06; }
- public function getLabelMarginBottom() { return 0.06; }
- public function getLabelMarginLeft() { return 0.06; }
- public function getLabelMarginRight() { return 0.06; }
+ public function getLabelMarginTop()
+ {
+ return 0.06;
+ }
+ public function getLabelMarginBottom()
+ {
+ return 0.06;
+ }
+ public function getLabelMarginLeft()
+ {
+ return 0.06;
+ }
+ public function getLabelMarginRight()
+ {
+ return 0.06;
+ }
- public function getSupportAssetTag() { return false; }
- public function getSupport1DBarcode() { return false; }
- public function getSupport2DBarcode() { return true; }
- public function getSupportFields() { return 3; }
- public function getSupportLogo() { return false; }
- public function getSupportTitle() { return true; }
+ public function getSupportAssetTag()
+ {
+ return false;
+ }
+ public function getSupport1DBarcode()
+ {
+ return false;
+ }
+ public function getSupport2DBarcode()
+ {
+ return true;
+ }
+ public function getSupportFields()
+ {
+ return 3;
+ }
+ public function getSupportLogo()
+ {
+ return false;
+ }
+ public function getSupportTitle()
+ {
+ return true;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getLabelPrintableArea();
$currentX = $pa->x1;
diff --git a/app/Models/Labels/Sheets/Avery/_5520_B.php b/app/Models/Labels/Sheets/Avery/_5520_B.php
index eb6494e0aa..9c44e26bf4 100644
--- a/app/Models/Labels/Sheets/Avery/_5520_B.php
+++ b/app/Models/Labels/Sheets/Avery/_5520_B.php
@@ -15,23 +15,59 @@ class _5520_B extends _5520
private const FIELD_SIZE = 0.150;
private const FIELD_MARGIN = 0.012;
- public function getUnit() { return 'in'; }
+ public function getUnit()
+ {
+ return 'in';
+ }
- public function getLabelMarginTop() { return 0.06; }
- public function getLabelMarginBottom() { return 0.06; }
- public function getLabelMarginLeft() { return 0.06; }
- public function getLabelMarginRight() { return 0.06; }
+ public function getLabelMarginTop()
+ {
+ return 0.06;
+ }
+ public function getLabelMarginBottom()
+ {
+ return 0.06;
+ }
+ public function getLabelMarginLeft()
+ {
+ return 0.06;
+ }
+ public function getLabelMarginRight()
+ {
+ return 0.06;
+ }
- public function getSupportAssetTag() { return false; }
- public function getSupport1DBarcode() { return true; }
- public function getSupport2DBarcode() { return false; }
- public function getSupportFields() { return 2; }
- public function getSupportLogo() { return false; }
- public function getSupportTitle() { return true; }
+ public function getSupportAssetTag()
+ {
+ return false;
+ }
+ public function getSupport1DBarcode()
+ {
+ return true;
+ }
+ public function getSupport2DBarcode()
+ {
+ return false;
+ }
+ public function getSupportFields()
+ {
+ return 2;
+ }
+ public function getSupportLogo()
+ {
+ return false;
+ }
+ public function getSupportTitle()
+ {
+ return true;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getLabelPrintableArea();
$currentX = $pa->x1;
diff --git a/app/Models/Labels/Tapes/Brother/TZe_12mm.php b/app/Models/Labels/Tapes/Brother/TZe_12mm.php
index f9196847ce..3cf1f54081 100644
--- a/app/Models/Labels/Tapes/Brother/TZe_12mm.php
+++ b/app/Models/Labels/Tapes/Brother/TZe_12mm.php
@@ -11,9 +11,24 @@ abstract class TZe_12mm extends Label
private const MARGIN_SIDES = 3.20;
private const MARGIN_ENDS = 3.20;
- public function getHeight() { return Helper::convertUnit(self::HEIGHT, 'mm', $this->getUnit()); }
- public function getMarginTop() { return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit()); }
- public function getMarginBottom() { return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit());}
- public function getMarginLeft() { return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit()); }
- public function getMarginRight() { return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit()); }
+ public function getHeight()
+ {
+ return Helper::convertUnit(self::HEIGHT, 'mm', $this->getUnit());
+ }
+ public function getMarginTop()
+ {
+ return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit());
+ }
+ public function getMarginBottom()
+ {
+ return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit());
+ }
+ public function getMarginLeft()
+ {
+ return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit());
+ }
+ public function getMarginRight()
+ {
+ return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit());
+ }
}
\ No newline at end of file
diff --git a/app/Models/Labels/Tapes/Brother/TZe_12mm_A.php b/app/Models/Labels/Tapes/Brother/TZe_12mm_A.php
index f89cfc5d47..5ff9fcd474 100644
--- a/app/Models/Labels/Tapes/Brother/TZe_12mm_A.php
+++ b/app/Models/Labels/Tapes/Brother/TZe_12mm_A.php
@@ -8,18 +8,45 @@ class TZe_12mm_A extends TZe_12mm
private const BARCODE_MARGIN = 0.30;
private const TEXT_SIZE_MOD = 1.00;
- public function getUnit() { return 'mm'; }
- public function getWidth() { return 50.0; }
- public function getSupportAssetTag() { return true; }
- public function getSupport1DBarcode() { return true; }
- public function getSupport2DBarcode() { return false; }
- public function getSupportFields() { return 1; }
- public function getSupportLogo() { return false; }
- public function getSupportTitle() { return false; }
+ public function getUnit()
+ {
+ return 'mm';
+ }
+ public function getWidth()
+ {
+ return 50.0;
+ }
+ public function getSupportAssetTag()
+ {
+ return true;
+ }
+ public function getSupport1DBarcode()
+ {
+ return true;
+ }
+ public function getSupport2DBarcode()
+ {
+ return false;
+ }
+ public function getSupportFields()
+ {
+ return 1;
+ }
+ public function getSupportLogo()
+ {
+ return false;
+ }
+ public function getSupportTitle()
+ {
+ return false;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getPrintableArea();
if ($record->has('barcode1d')) {
diff --git a/app/Models/Labels/Tapes/Brother/TZe_18mm.php b/app/Models/Labels/Tapes/Brother/TZe_18mm.php
index 38c14c7aa4..efcc405bc4 100644
--- a/app/Models/Labels/Tapes/Brother/TZe_18mm.php
+++ b/app/Models/Labels/Tapes/Brother/TZe_18mm.php
@@ -11,9 +11,24 @@ abstract class TZe_18mm extends Label
private const MARGIN_SIDES = 3.20;
private const MARGIN_ENDS = 3.20;
- public function getHeight() { return Helper::convertUnit(self::HEIGHT, 'mm', $this->getUnit()); }
- public function getMarginTop() { return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit()); }
- public function getMarginBottom() { return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit());}
- public function getMarginLeft() { return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit()); }
- public function getMarginRight() { return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit()); }
+ public function getHeight()
+ {
+ return Helper::convertUnit(self::HEIGHT, 'mm', $this->getUnit());
+ }
+ public function getMarginTop()
+ {
+ return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit());
+ }
+ public function getMarginBottom()
+ {
+ return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit());
+ }
+ public function getMarginLeft()
+ {
+ return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit());
+ }
+ public function getMarginRight()
+ {
+ return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit());
+ }
}
\ No newline at end of file
diff --git a/app/Models/Labels/Tapes/Brother/TZe_18mm_A.php b/app/Models/Labels/Tapes/Brother/TZe_18mm_A.php
index 32156f5ee6..fd27a80421 100644
--- a/app/Models/Labels/Tapes/Brother/TZe_18mm_A.php
+++ b/app/Models/Labels/Tapes/Brother/TZe_18mm_A.php
@@ -8,18 +8,45 @@ class TZe_18mm_A extends TZe_18mm
private const BARCODE_MARGIN = 0.30;
private const TEXT_SIZE_MOD = 1.00;
- public function getUnit() { return 'mm'; }
- public function getWidth() { return 50.0; }
- public function getSupportAssetTag() { return true; }
- public function getSupport1DBarcode() { return true; }
- public function getSupport2DBarcode() { return false; }
- public function getSupportFields() { return 1; }
- public function getSupportLogo() { return false; }
- public function getSupportTitle() { return false; }
+ public function getUnit()
+ {
+ return 'mm';
+ }
+ public function getWidth()
+ {
+ return 50.0;
+ }
+ public function getSupportAssetTag()
+ {
+ return true;
+ }
+ public function getSupport1DBarcode()
+ {
+ return true;
+ }
+ public function getSupport2DBarcode()
+ {
+ return false;
+ }
+ public function getSupportFields()
+ {
+ return 1;
+ }
+ public function getSupportLogo()
+ {
+ return false;
+ }
+ public function getSupportTitle()
+ {
+ return false;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getPrintableArea();
if ($record->has('barcode1d')) {
diff --git a/app/Models/Labels/Tapes/Brother/TZe_24mm.php b/app/Models/Labels/Tapes/Brother/TZe_24mm.php
index 3c67bc1614..c9ee228beb 100644
--- a/app/Models/Labels/Tapes/Brother/TZe_24mm.php
+++ b/app/Models/Labels/Tapes/Brother/TZe_24mm.php
@@ -11,9 +11,24 @@ abstract class TZe_24mm extends Label
private const MARGIN_SIDES = 3.20;
private const MARGIN_ENDS = 3.20;
- public function getHeight() { return Helper::convertUnit(self::HEIGHT, 'mm', $this->getUnit()); }
- public function getMarginTop() { return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit()); }
- public function getMarginBottom() { return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit());}
- public function getMarginLeft() { return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit()); }
- public function getMarginRight() { return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit()); }
+ public function getHeight()
+ {
+ return Helper::convertUnit(self::HEIGHT, 'mm', $this->getUnit());
+ }
+ public function getMarginTop()
+ {
+ return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit());
+ }
+ public function getMarginBottom()
+ {
+ return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit());
+ }
+ public function getMarginLeft()
+ {
+ return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit());
+ }
+ public function getMarginRight()
+ {
+ return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit());
+ }
}
\ No newline at end of file
diff --git a/app/Models/Labels/Tapes/Brother/TZe_24mm_A.php b/app/Models/Labels/Tapes/Brother/TZe_24mm_A.php
index ea4c6c9dfb..4210bb2709 100644
--- a/app/Models/Labels/Tapes/Brother/TZe_24mm_A.php
+++ b/app/Models/Labels/Tapes/Brother/TZe_24mm_A.php
@@ -13,18 +13,45 @@ class TZe_24mm_A extends TZe_24mm
private const FIELD_SIZE = 3.20;
private const FIELD_MARGIN = 0.15;
- public function getUnit() { return 'mm'; }
- public function getWidth() { return 65.0; }
- public function getSupportAssetTag() { return true; }
- public function getSupport1DBarcode() { return false; }
- public function getSupport2DBarcode() { return true; }
- public function getSupportFields() { return 3; }
- public function getSupportLogo() { return false; }
- public function getSupportTitle() { return true; }
+ public function getUnit()
+ {
+ return 'mm';
+ }
+ public function getWidth()
+ {
+ return 65.0;
+ }
+ public function getSupportAssetTag()
+ {
+ return true;
+ }
+ public function getSupport1DBarcode()
+ {
+ return false;
+ }
+ public function getSupport2DBarcode()
+ {
+ return true;
+ }
+ public function getSupportFields()
+ {
+ return 3;
+ }
+ public function getSupportLogo()
+ {
+ return false;
+ }
+ public function getSupportTitle()
+ {
+ return true;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
diff --git a/app/Models/Labels/Tapes/Brother/TZe_24mm_B.php b/app/Models/Labels/Tapes/Brother/TZe_24mm_B.php
index cedf5e0cbd..2ea5697316 100644
--- a/app/Models/Labels/Tapes/Brother/TZe_24mm_B.php
+++ b/app/Models/Labels/Tapes/Brother/TZe_24mm_B.php
@@ -15,18 +15,45 @@ class TZe_24mm_B extends TZe_24mm
private const FIELD_SIZE = 3.20;
private const FIELD_MARGIN = 0.15;
- public function getUnit() { return 'mm'; }
- public function getWidth() { return 73.0; }
- public function getSupportAssetTag() { return true; }
- public function getSupport1DBarcode() { return false; }
- public function getSupport2DBarcode() { return true; }
- public function getSupportFields() { return 3; }
- public function getSupportLogo() { return true; }
- public function getSupportTitle() { return true; }
+ public function getUnit()
+ {
+ return 'mm';
+ }
+ public function getWidth()
+ {
+ return 73.0;
+ }
+ public function getSupportAssetTag()
+ {
+ return true;
+ }
+ public function getSupport1DBarcode()
+ {
+ return false;
+ }
+ public function getSupport2DBarcode()
+ {
+ return true;
+ }
+ public function getSupportFields()
+ {
+ return 3;
+ }
+ public function getSupportLogo()
+ {
+ return true;
+ }
+ public function getSupportTitle()
+ {
+ return true;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
diff --git a/app/Models/Labels/Tapes/Brother/TZe_24mm_C.php b/app/Models/Labels/Tapes/Brother/TZe_24mm_C.php
index 65b3676bfc..e100dfa81b 100644
--- a/app/Models/Labels/Tapes/Brother/TZe_24mm_C.php
+++ b/app/Models/Labels/Tapes/Brother/TZe_24mm_C.php
@@ -15,18 +15,45 @@ class TZe_24mm_C extends TZe_24mm
private const FIELD_SIZE = 3.20;
private const FIELD_MARGIN = 0.15;
- public function getUnit() { return 'mm'; }
- public function getWidth() { return 34.0; }
- public function getSupportAssetTag() { return true; }
- public function getSupport1DBarcode() { return false; }
- public function getSupport2DBarcode() { return true; }
- public function getSupportFields() { return 0; }
- public function getSupportLogo() { return true; }
- public function getSupportTitle() { return false; }
+ public function getUnit()
+ {
+ return 'mm';
+ }
+ public function getWidth()
+ {
+ return 34.0;
+ }
+ public function getSupportAssetTag()
+ {
+ return true;
+ }
+ public function getSupport1DBarcode()
+ {
+ return false;
+ }
+ public function getSupport2DBarcode()
+ {
+ return true;
+ }
+ public function getSupportFields()
+ {
+ return 0;
+ }
+ public function getSupportLogo()
+ {
+ return true;
+ }
+ public function getSupportTitle()
+ {
+ return false;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
diff --git a/app/Models/Labels/Tapes/Brother/TZe_24mm_D.php b/app/Models/Labels/Tapes/Brother/TZe_24mm_D.php
index 5ecd86c977..c88e84d246 100644
--- a/app/Models/Labels/Tapes/Brother/TZe_24mm_D.php
+++ b/app/Models/Labels/Tapes/Brother/TZe_24mm_D.php
@@ -14,18 +14,45 @@ class TZe_24mm_D extends TZe_24mm
private const FIELD_MARGIN = 0.35;
private const BARCODE1D_SIZE = 3.00; // Size for the C128 barcode at bottom
- public function getUnit() { return 'mm'; }
- public function getWidth() { return 65.0; }
- public function getSupportAssetTag() { return true; }
- public function getSupport1DBarcode() { return true; }
- public function getSupport2DBarcode() { return true; }
- public function getSupportFields() { return 3; }
- public function getSupportLogo() { return false; }
- public function getSupportTitle() { return true; }
+ public function getUnit()
+ {
+ return 'mm';
+ }
+ public function getWidth()
+ {
+ return 65.0;
+ }
+ public function getSupportAssetTag()
+ {
+ return true;
+ }
+ public function getSupport1DBarcode()
+ {
+ return true;
+ }
+ public function getSupport2DBarcode()
+ {
+ return true;
+ }
+ public function getSupportFields()
+ {
+ return 3;
+ }
+ public function getSupportLogo()
+ {
+ return false;
+ }
+ public function getSupportTitle()
+ {
+ return true;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
@@ -70,27 +97,38 @@ class TZe_24mm_D extends TZe_24mm
}
foreach ($record->get('fields') as $field) {
- // Write label and value on the same line
- // Calculate label width with proportional character spacing
- $labelWidth = $pdf->GetStringWidth($field['label'], 'freemono', '', self::LABEL_SIZE);
- $charCount = strlen($field['label']);
- $spacingPerChar = 0.5;
- $totalSpacing = $charCount * $spacingPerChar;
- $adjustedWidth = $labelWidth + $totalSpacing;
+ if (!empty($field['label']) && $field['label'] !== "\u{200B}") {
+ // Write label and value on the same line
+ // Calculate label width with proportional character spacing
+ $labelWidth = $pdf->GetStringWidth($field['label'], 'freemono', '', self::LABEL_SIZE);
+ $charCount = strlen($field['label']);
+ $spacingPerChar = 0.5;
+ $totalSpacing = $charCount * $spacingPerChar;
+ $adjustedWidth = $labelWidth + $totalSpacing;
- static::writeText(
- $pdf, $field['label'],
- $currentX, $currentY,
- 'freemono', 'B', self::LABEL_SIZE, 'L',
- $adjustedWidth, self::LABEL_SIZE, true, 0, $spacingPerChar
- );
+ static::writeText(
+ $pdf, $field['label'],
+ $currentX, $currentY,
+ 'freemono', 'B', self::LABEL_SIZE, 'L',
+ $adjustedWidth, self::LABEL_SIZE, true, 0, $spacingPerChar
+ );
- static::writeText(
- $pdf, $field['value'],
- $currentX + $adjustedWidth + 2, $currentY,
- 'freemono', 'B', self::FIELD_SIZE, 'L',
- $usableWidth - $adjustedWidth - 2, self::FIELD_SIZE, true, 0, 0.3
- );
+ static::writeText(
+ $pdf, $field['value'],
+ $currentX + $adjustedWidth + 2, $currentY,
+ 'freemono', 'B', self::FIELD_SIZE, 'L',
+ $usableWidth - $adjustedWidth - 2, self::FIELD_SIZE, true, 0, 0.3
+ );
+ } else {
+
+ // Label is empty, so write value only.
+ static::writeText(
+ $pdf, $field['value'],
+ $currentX, $currentY, // No offset
+ 'freemono', 'B', self::FIELD_SIZE, 'L',
+ $usableWidth, self::FIELD_SIZE, true, 0, 0.3
+ );
+ }
$currentY += max(self::LABEL_SIZE, self::FIELD_SIZE) + self::FIELD_MARGIN;
}
diff --git a/app/Models/Labels/Tapes/Brother/TZe_62mm_Landscape.php b/app/Models/Labels/Tapes/Brother/TZe_62mm_Landscape.php
index 2069927a34..1f5ee1df3d 100644
--- a/app/Models/Labels/Tapes/Brother/TZe_62mm_Landscape.php
+++ b/app/Models/Labels/Tapes/Brother/TZe_62mm_Landscape.php
@@ -14,10 +14,28 @@ abstract class TZe_62mm_Landscape extends Label
private const MARGIN_SIDES = 1.50;
private const MARGIN_ENDS = 1.50;
- public function getWidth() { return Helper::convertUnit(self::WIDTH, 'mm', $this->getUnit()); }
- public function getMarginTop() { return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit()); }
- public function getMarginBottom() { return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit());}
- public function getMarginLeft() { return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit()); }
- public function getMarginRight() { return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit()); }
- public function getRotation() { return 90; }
+ public function getWidth()
+ {
+ return Helper::convertUnit(self::WIDTH, 'mm', $this->getUnit());
+ }
+ public function getMarginTop()
+ {
+ return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit());
+ }
+ public function getMarginBottom()
+ {
+ return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit());
+ }
+ public function getMarginLeft()
+ {
+ return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit());
+ }
+ public function getMarginRight()
+ {
+ return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit());
+ }
+ public function getRotation()
+ {
+ return 90;
+ }
}
diff --git a/app/Models/Labels/Tapes/Brother/TZe_62mm_Landscape_A.php b/app/Models/Labels/Tapes/Brother/TZe_62mm_Landscape_A.php
index 3a4d6da2c7..6cf53f8e18 100644
--- a/app/Models/Labels/Tapes/Brother/TZe_62mm_Landscape_A.php
+++ b/app/Models/Labels/Tapes/Brother/TZe_62mm_Landscape_A.php
@@ -4,14 +4,38 @@ namespace App\Models\Labels\Tapes\Brother;
class TZe_62mm_Landscape_A extends TZe_62mm_Landscape
{
- public function getUnit() { return 'mm'; }
- public function getHeight() { return 31.50; }
- public function getSupportAssetTag() { return true; }
- public function getSupport1DBarcode() { return true; }
- public function getSupport2DBarcode() { return true; }
- public function getSupportFields() { return 2; }
- public function getSupportLogo() { return true; }
- public function getSupportTitle() { return true; }
+ public function getUnit()
+ {
+ return 'mm';
+ }
+ public function getHeight()
+ {
+ return 31.50;
+ }
+ public function getSupportAssetTag()
+ {
+ return true;
+ }
+ public function getSupport1DBarcode()
+ {
+ return true;
+ }
+ public function getSupport2DBarcode()
+ {
+ return true;
+ }
+ public function getSupportFields()
+ {
+ return 2;
+ }
+ public function getSupportLogo()
+ {
+ return true;
+ }
+ public function getSupportTitle()
+ {
+ return true;
+ }
private const BARCODE1D_HEIGHT = 3.00;
private const BARCODE1D_MARGIN = 3.00;
@@ -27,9 +51,12 @@ class TZe_62mm_Landscape_A extends TZe_62mm_Landscape
private const FIELD_SIZE = 3.00;
private const FIELD_MARGIN = 0.10;
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
diff --git a/app/Models/Labels/Tapes/Dymo/LabelWriter.php b/app/Models/Labels/Tapes/Dymo/LabelWriter.php
index fa427fd213..8d9e37555d 100644
--- a/app/Models/Labels/Tapes/Dymo/LabelWriter.php
+++ b/app/Models/Labels/Tapes/Dymo/LabelWriter.php
@@ -11,9 +11,24 @@ abstract class LabelWriter extends Label
private const MARGIN_SIDES = 0.1;
private const MARGIN_ENDS = 0.1;
- public function getHeight() { return Helper::convertUnit(self::HEIGHT, 'in', $this->getUnit()); }
- public function getMarginTop() { return Helper::convertUnit(self::MARGIN_SIDES, 'in', $this->getUnit()); }
- public function getMarginBottom() { return Helper::convertUnit(self::MARGIN_SIDES, 'in', $this->getUnit());}
- public function getMarginLeft() { return Helper::convertUnit(self::MARGIN_ENDS, 'in', $this->getUnit()); }
- public function getMarginRight() { return Helper::convertUnit(self::MARGIN_ENDS, 'in', $this->getUnit()); }
+ public function getHeight()
+ {
+ return Helper::convertUnit(self::HEIGHT, 'in', $this->getUnit());
+ }
+ public function getMarginTop()
+ {
+ return Helper::convertUnit(self::MARGIN_SIDES, 'in', $this->getUnit());
+ }
+ public function getMarginBottom()
+ {
+ return Helper::convertUnit(self::MARGIN_SIDES, 'in', $this->getUnit());
+ }
+ public function getMarginLeft()
+ {
+ return Helper::convertUnit(self::MARGIN_ENDS, 'in', $this->getUnit());
+ }
+ public function getMarginRight()
+ {
+ return Helper::convertUnit(self::MARGIN_ENDS, 'in', $this->getUnit());
+ }
}
\ No newline at end of file
diff --git a/app/Models/Labels/Tapes/Dymo/LabelWriter_11354.php b/app/Models/Labels/Tapes/Dymo/LabelWriter_11354.php
new file mode 100644
index 0000000000..08f2fb6d27
--- /dev/null
+++ b/app/Models/Labels/Tapes/Dymo/LabelWriter_11354.php
@@ -0,0 +1,116 @@
+getPrintableArea();
+
+ $currentX = $pa->x1;
+ $currentY = $pa->y1;
+ $usableWidth = $pa->w;
+ $usableHeight = $pa->h;
+
+ // Wide 1D barcode on top
+ if ($record->has('barcode1d')) {
+ static::write1DBarcode(
+ $pdf, $record->get('barcode1d')->content, $record->get('barcode1d')->type,
+ $currentX, $currentY, $usableWidth, self::BARCODE1D_HEIGHT
+ );
+ $currentY += self::BARCODE1D_HEIGHT + self::BARCODE_MARGIN;
+ $usableHeight -= self::BARCODE1D_HEIGHT + self::BARCODE_MARGIN;
+ }
+
+ // 2D Barcode in left column
+ if ($record->has('barcode2d')) {
+ $barcodeSize = $usableHeight - self::TAG_SIZE;
+
+ static::writeText(
+ $pdf, $record->get('tag'),
+ $currentX, $pa->y2 - self::TAG_SIZE,
+ 'freesans', 'b', self::TAG_SIZE, 'C',
+ $barcodeSize, self::TAG_SIZE, true, 0
+ );
+ static::write2DBarcode(
+ $pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type,
+ $currentX, $currentY,
+ $barcodeSize, $barcodeSize
+ );
+ $currentX += $barcodeSize + self::BARCODE_MARGIN;
+ $usableWidth -= $barcodeSize + self::BARCODE_MARGIN;
+ }
+
+ // Right column
+ if ($record->has('title')) {
+ static::writeText(
+ $pdf, $record->get('title'),
+ $currentX, $currentY,
+ 'freesans', 'b', self::TITLE_SIZE, 'L',
+ $usableWidth, self::TITLE_SIZE, true, 0
+ );
+ $currentY += self::TITLE_SIZE + self::TITLE_MARGIN;
+ }
+
+ foreach ($record->get('fields') as $field) {
+ static::writeText(
+ $pdf, (($field['label']) ? $field['label'].' ' : '') . $field['value'],
+ $currentX, $currentY,
+ 'freesans', '', self::FIELD_SIZE, 'L',
+ $usableWidth, self::FIELD_SIZE, true, 0, 0.3
+ );
+ $currentY += self::FIELD_SIZE + self::FIELD_MARGIN;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/Models/Labels/Tapes/Dymo/LabelWriter_1933081.php b/app/Models/Labels/Tapes/Dymo/LabelWriter_1933081.php
index 9f5fa735e4..63c270e865 100644
--- a/app/Models/Labels/Tapes/Dymo/LabelWriter_1933081.php
+++ b/app/Models/Labels/Tapes/Dymo/LabelWriter_1933081.php
@@ -14,19 +14,49 @@ class LabelWriter_1933081 extends LabelWriter
private const FIELD_SIZE = 2.80;
private const FIELD_MARGIN = 0.15;
- public function getUnit() { return 'mm'; }
- public function getWidth() { return 89; }
- public function getHeight() { return 25; }
- public function getSupportAssetTag() { return true; }
- public function getSupport1DBarcode() { return true; }
- public function getSupport2DBarcode() { return true; }
- public function getSupportFields() { return 5; }
- public function getSupportLogo() { return false; }
- public function getSupportTitle() { return true; }
+ public function getUnit()
+ {
+ return 'mm';
+ }
+ public function getWidth()
+ {
+ return 89;
+ }
+ public function getHeight()
+ {
+ return 25;
+ }
+ public function getSupportAssetTag()
+ {
+ return true;
+ }
+ public function getSupport1DBarcode()
+ {
+ return true;
+ }
+ public function getSupport2DBarcode()
+ {
+ return true;
+ }
+ public function getSupportFields()
+ {
+ return 5;
+ }
+ public function getSupportLogo()
+ {
+ return false;
+ }
+ public function getSupportTitle()
+ {
+ return true;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
diff --git a/app/Models/Labels/Tapes/Dymo/LabelWriter_2112283.php b/app/Models/Labels/Tapes/Dymo/LabelWriter_2112283.php
index 117486a8e5..63701d31a1 100644
--- a/app/Models/Labels/Tapes/Dymo/LabelWriter_2112283.php
+++ b/app/Models/Labels/Tapes/Dymo/LabelWriter_2112283.php
@@ -14,19 +14,49 @@ class LabelWriter_2112283 extends LabelWriter
private const FIELD_SIZE = 2.80;
private const FIELD_MARGIN = 0.15;
- public function getUnit() { return 'mm'; }
- public function getWidth() { return 54; }
- public function getHeight() { return 25; }
- public function getSupportAssetTag() { return true; }
- public function getSupport1DBarcode() { return true; }
- public function getSupport2DBarcode() { return true; }
- public function getSupportFields() { return 5; }
- public function getSupportLogo() { return false; }
- public function getSupportTitle() { return true; }
+ public function getUnit()
+ {
+ return 'mm';
+ }
+ public function getWidth()
+ {
+ return 54;
+ }
+ public function getHeight()
+ {
+ return 25;
+ }
+ public function getSupportAssetTag()
+ {
+ return true;
+ }
+ public function getSupport1DBarcode()
+ {
+ return true;
+ }
+ public function getSupport2DBarcode()
+ {
+ return true;
+ }
+ public function getSupportFields()
+ {
+ return 5;
+ }
+ public function getSupportLogo()
+ {
+ return false;
+ }
+ public function getSupportTitle()
+ {
+ return true;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
diff --git a/app/Models/Labels/Tapes/Dymo/LabelWriter_30252.php b/app/Models/Labels/Tapes/Dymo/LabelWriter_30252.php
index ed8074547b..8c3f050242 100644
--- a/app/Models/Labels/Tapes/Dymo/LabelWriter_30252.php
+++ b/app/Models/Labels/Tapes/Dymo/LabelWriter_30252.php
@@ -16,18 +16,45 @@ class LabelWriter_30252 extends LabelWriter
- public function getUnit() { return 'mm'; }
- public function getWidth() { return 96.52; }
- public function getSupportAssetTag() { return true; }
- public function getSupport1DBarcode() { return true; }
- public function getSupport2DBarcode() { return true; }
- public function getSupportFields() { return 3; }
- public function getSupportLogo() { return false; }
- public function getSupportTitle() { return true; }
+ public function getUnit()
+ {
+ return 'mm';
+ }
+ public function getWidth()
+ {
+ return 96.52;
+ }
+ public function getSupportAssetTag()
+ {
+ return true;
+ }
+ public function getSupport1DBarcode()
+ {
+ return true;
+ }
+ public function getSupport2DBarcode()
+ {
+ return true;
+ }
+ public function getSupportFields()
+ {
+ return 3;
+ }
+ public function getSupportLogo()
+ {
+ return false;
+ }
+ public function getSupportTitle()
+ {
+ return true;
+ }
- public function preparePDF($pdf) {}
+ public function preparePDF($pdf)
+ {
+ }
- public function write($pdf, $record) {
+ public function write($pdf, $record)
+ {
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
diff --git a/app/Models/Labels/Tapes/Generic/Continuous_53mm.php b/app/Models/Labels/Tapes/Generic/Continuous_53mm.php
new file mode 100644
index 0000000000..18b12fbc99
--- /dev/null
+++ b/app/Models/Labels/Tapes/Generic/Continuous_53mm.php
@@ -0,0 +1,110 @@
+tapeHeight = $height;
+ }
+
+ public function getBarcodeRatio()
+ {
+ return 0.9; // Barcode should use 90% of available width
+ }
+
+ /**
+ * Calculate the required height for the content
+ *
+ * @param $record The record to calculate height for
+ * @return float The calculated height in mm
+ */
+ protected function calculateRequiredHeight($record)
+ {
+ $height = $this->marginTop + $this->marginBottom;
+
+ // Add title height if present
+ if ($record->has('title') && $this->getSupportTitle()) {
+ $height += $this->titleSize + $this->titleMargin;
+ }
+
+ // Add barcode height if present
+ if (($record->has('barcode2d') && $this->getSupport2DBarcode())
+ || ($record->has('barcode') && $this->getSupport1DBarcode())
+ ) {
+ $pa = $this->getPrintableArea();
+ $usableWidth = $pa->w;
+ $barcodeSize = $usableWidth * $this->getBarcodeRatio();
+ $height += $barcodeSize + $this->barcodeMargin;
+ }
+
+ // Add fields height if present
+ if ($record->has('fields') && $this->getSupportFields() > 0) {
+ foreach ($record->get('fields') as $field) {
+ $height += $this->labelSize + $this->labelMargin;
+ $height += $this->fieldSize + $this->fieldMargin;
+ }
+ }
+
+ // Add a small buffer to ensure everything fits
+ $height += 2.0;
+
+ // Ensure minimum height
+ return max($this->minHeight, $height);
+ }
+
+ /**
+ * Override the writeAll method to support dynamic page sizes for continuous tapes
+ */
+ public function writeAll($pdf, $data)
+ {
+ // Use auto-sizing for continuous tapes, fixed height for die-cut tapes
+ if ($this->continuous) {
+ $data->each(
+ function ($record, $index) use ($pdf) {
+ // Calculate the required height for this record
+ $requiredHeight = $this->calculateRequiredHeight($record);
+
+ // Temporarily update the height property
+ $originalHeight = $this->height;
+ $this->height = $requiredHeight;
+
+ // Add a new page with the calculated dimensions
+ $pdf->AddPage(
+ $this->getOrientation(),
+ [$this->getWidth(), $requiredHeight],
+ false, // Don't reset page number
+ false // Don't reset object ID
+ );
+
+ // Write the content
+ $this->write($pdf, $record);
+
+ // Restore the original height
+ $this->height = $originalHeight;
+ }
+ );
+ } else {
+ // Use the default implementation for non-continuous (die-cut) tapes
+ parent::writeAll($pdf, $data);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Models/Labels/Tapes/Generic/Continuous_53mm_A.php b/app/Models/Labels/Tapes/Generic/Continuous_53mm_A.php
new file mode 100644
index 0000000000..4f40f64627
--- /dev/null
+++ b/app/Models/Labels/Tapes/Generic/Continuous_53mm_A.php
@@ -0,0 +1,96 @@
+SetAutoPageBreak(false);
+ }
+
+ public function write($pdf, $record)
+ {
+ $pa = $this->getPrintableArea();
+
+ $currentX = $pa->x1;
+ $currentY = $pa->y1;
+ $usableWidth = $pa->w;
+ $usableHeight = $pa->h;
+
+ if ($record->has('title')) {
+ static::writeText(
+ $pdf, $record->get('title'),
+ $pa->x1, $pa->y1,
+ 'freesans', '', $this->titleSize, 'C',
+ $pa->w, $this->titleSize, true, 0
+ );
+ $currentY += $this->titleSize + $this->titleMargin;
+ $usableHeight -= $this->titleSize + $this->titleMargin;
+ }
+
+ // Make the barcode as large as possible while still leaving room for fields
+ $barcodeSize = min($usableHeight * 0.8, $usableWidth * $this->getBarcodeRatio());
+
+ if ($record->has('barcode2d')) {
+ $barcodeX = $pa->x1 + ($usableWidth - $barcodeSize) / 2;
+
+ static::write2DBarcode(
+ $pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type,
+ $barcodeX, $currentY,
+ $barcodeSize, $barcodeSize
+ );
+ $currentY += $barcodeSize + $this->barcodeMargin;
+ }
+
+ if ($record->has('fields')) {
+ foreach ($record->get('fields') as $field) {
+ static::writeText(
+ $pdf, $field['label'],
+ $currentX, $currentY,
+ 'freesans', '', $this->labelSize, 'L',
+ $usableWidth, $this->labelSize, true, 0
+ );
+ $currentY += $this->labelSize + $this->labelMargin;
+
+ static::writeText(
+ $pdf, $field['value'],
+ $currentX, $currentY,
+ 'freemono', 'B', $this->fieldSize, 'L',
+ $usableWidth, $this->fieldSize, true, 0, 0.01
+ );
+ $currentY += $this->fieldSize + $this->fieldMargin;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Models/Labels/Tapes/Generic/Continuous_Landscape_0_59in.php b/app/Models/Labels/Tapes/Generic/Continuous_Landscape_0_59in.php
new file mode 100644
index 0000000000..47856823bc
--- /dev/null
+++ b/app/Models/Labels/Tapes/Generic/Continuous_Landscape_0_59in.php
@@ -0,0 +1,178 @@
+tapeHeight = $length;
+
+ $this->marginTop = 0.1;
+ $this->marginBottom = 0.1;
+ // Keep small horizontal margins
+ $this->marginLeft = self::TAPE_WIDTH * 0.2;
+ // $this->marginRight = self::TAPE_WIDTH * 0.1;
+
+ // Override font sizes to make them larger
+ // Calculate a larger base font size (3x the default)
+ $baseFontSize = self::TAPE_WIDTH * 0.16; // 3x the default 0.07
+
+ // Recalculate all element sizing based on the larger base font size
+ $this->titleSize = $baseFontSize; // Same as base font size
+ $this->titleMargin = $baseFontSize * 0.3; // 30% of base font size
+ $this->fieldSize = $baseFontSize * 1.1; // 110% of base font size
+ $this->fieldMargin = $baseFontSize * 0.1; // 10% of base font size
+ $this->labelSize = $baseFontSize * 0.7; // 70% of base font size
+ $this->labelMargin = $baseFontSize * -0.1; // -10% of base font size
+ $this->barcodeMargin = $baseFontSize * 0.9; // 20% of base font size
+ $this->tagSize = $baseFontSize * 0.8; // 80% of base font size
+ }
+
+ public function getBarcodeRatio()
+ {
+ return 1.0; // Barcode should use 100% of available height
+ }
+
+ /**
+ * Calculate the required length for the content
+ *
+ * @param $record The record to calculate length for
+ * @return float The calculated length in inches
+ */
+ protected function calculateRequiredLength($record)
+ {
+
+ // Calculate length needed for barcode and fields side by side
+ $requiredLength = 0;
+
+ // Add barcode length if present
+ if (($record->has('barcode2d') && $this->getSupport2DBarcode())
+ || ($record->has('barcode') && $this->getSupport1DBarcode())
+ ) {
+ // Use full tape width for barcode size
+ $barcodeSize = self::TAPE_WIDTH;
+ $requiredLength += $barcodeSize + $this->barcodeMargin * 0.3; // Minimal margin
+ }
+
+ // Add fields length if present - calculate based on actual content
+ if ($record->has('fields') && $this->getSupportFields() > 0) {
+ $fields = array_slice($record->get('fields')->toArray(), 0, $this->getSupportFields());
+
+ // Base width for field area
+ $fieldsWidth = self::TAPE_WIDTH;
+
+ // Calculate additional width based on text length
+ foreach ($fields as $field) {
+ // Get label and value text
+ $labelText = $field['label'] ?? '';
+ $valueText = $field['value'] ?? '';
+
+ // Calculate approximate width needed based on text length
+ // Increase character width to ensure enough space (0.15 inches per character)
+ $labelWidth = strlen($labelText) * 0.09;
+ $valueWidth = strlen($valueText) * 0.09;
+
+ // Use the longer of the two
+ $textWidth = max($labelWidth, $valueWidth);
+
+ // Ensure minimum width and add to total
+ $fieldsWidth = max($fieldsWidth, $textWidth);
+ }
+
+ // Add the calculated width for fields
+ $requiredLength += $fieldsWidth;
+
+ // Add minimal extra space for field padding
+ // Reduce padding to eliminate extraneous space on right edge
+ // $requiredLength += self::TAPE_WIDTH * 0.1;
+ }
+
+ // Ensure minimum length
+ return max($this->minHeight, $requiredLength);
+ }
+
+ /**
+ * Calculate text width accurately using the PDF object
+ *
+ * @param $pdf The PDF object
+ * @param string $text The text to measure
+ * @param string $font The font to use
+ * @param string $style The font style
+ * @param float $size The font size
+ * @return float The calculated width
+ */
+ protected function calculateTextWidth($pdf, $text, $font, $style, $size)
+ {
+ $originalFont = $pdf->getFontFamily();
+ $originalStyle = $pdf->getFontStyle();
+ $originalSize = $pdf->getFontSizePt();
+
+ $pdf->SetFont($font, $style, Helper::convertUnit($size, $this->getUnit(), 'pt', true));
+ $width = $pdf->GetStringWidth($text);
+
+ // Restore original font settings
+ $pdf->SetFont($originalFont, $originalStyle, $originalSize);
+
+ return $width;
+ }
+
+ /**
+ * Override the writeAll method to support dynamic page sizes for continuous tapes
+ */
+ public function writeAll($pdf, $data)
+ {
+ // Use auto-sizing for continuous tapes, fixed height for die-cut tapes
+ if ($this->continuous) {
+ $data->each(
+ function ($record, $index) use ($pdf) {
+ // Calculate the required length by calling write with calculateOnly=true
+ $requiredLength = $this->write($pdf, $record);
+
+ // If write didn't return a length (old implementation), fall back to calculateRequiredLength
+ if ($requiredLength === null) {
+ $requiredLength = $this->calculateRequiredLength($record);
+ }
+
+ // Temporarily update the height property
+ $originalHeight = $this->height;
+ $this->height = self::TAPE_WIDTH; // Keep height fixed at tape width
+
+ // Add a new page with the calculated dimensions
+ // Keep height fixed at TAPE_WIDTH, use calculated length for width
+ $pdf->AddPage(
+ $this->getOrientation(),
+ [$requiredLength, self::TAPE_WIDTH],
+ false, // Don't reset page number
+ false // Don't reset object ID
+ );
+
+ // Write the content
+ $this->write($pdf, $record);
+
+ // Restore the original height
+ $this->height = $originalHeight;
+ }
+ );
+ } else {
+ // Use the default implementation for non-continuous (die-cut) tapes
+ parent::writeAll($pdf, $data);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Models/Labels/Tapes/Generic/Continuous_Landscape_0_59in_A.php b/app/Models/Labels/Tapes/Generic/Continuous_Landscape_0_59in_A.php
new file mode 100644
index 0000000000..23325aa837
--- /dev/null
+++ b/app/Models/Labels/Tapes/Generic/Continuous_Landscape_0_59in_A.php
@@ -0,0 +1,180 @@
+SetAutoPageBreak(false);
+ }
+
+ public function write($pdf, $record, $calculateOnly = false)
+ {
+ $pa = $this->getPrintableArea();
+
+ $currentX = $pa->x1;
+ $currentY = $pa->y1;
+ $usableWidth = $pa->w;
+ $usableHeight = $pa->h;
+
+ // Calculate required length based on content
+ $requiredLength = 0;
+
+ // Use full usable height for barcode
+ $barcodeSize = $usableHeight;
+
+ // Add barcode width to required length
+ if ($record->has('barcode2d') && $this->getSupport2DBarcode()) {
+ $requiredLength += $barcodeSize;
+ // Add gap between barcode and fields
+ $requiredLength += $this->barcodeMargin;
+ }
+
+ // Calculate fields width using accurate text measurement
+ if ($record->has('fields') && $this->getSupportFields() > 0) {
+ $fields = array_slice($record->get('fields')->toArray(), 0, $this->getSupportFields());
+ $fieldsWidth = 0;
+
+ foreach ($fields as $field) {
+ $labelText = $field['label'] ?? '';
+ $valueText = $field['value'] ?? '';
+
+ // Calculate accurate width using the PDF object
+ $labelWidth = $this->calculateTextWidth($pdf, $labelText, 'freesans', 'B', $this->labelSize * 1.2);
+ $valueWidth = $this->calculateTextWidth($pdf, $valueText, 'freemono', 'B', $this->fieldSize * 1.3);
+
+ // Use the longer of the two
+ $textWidth = max($labelWidth, $valueWidth);
+ $fieldsWidth = max($fieldsWidth, $textWidth);
+ }
+
+ $requiredLength += $fieldsWidth;
+ }
+
+ // Add more padding to prevent text from being cut off
+ // $requiredLength += self::TAPE_WIDTH * 0.8;
+
+ // Ensure minimum length
+ $requiredLength = max($this->minHeight, $requiredLength);
+
+ // If we're just calculating, return the length
+ if ($calculateOnly) {
+ return $requiredLength;
+ }
+
+ // Otherwise, render the content
+ // Position barcode on the left side
+ if ($record->has('barcode2d') && $this->getSupport2DBarcode()) {
+ // Position at top of usable area
+ static::write2DBarcode(
+ $pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type,
+ $currentX, $currentY,
+ $barcodeSize, $barcodeSize
+ );
+ $currentX += $barcodeSize + $this->barcodeMargin;
+ $usableWidth -= $barcodeSize + $this->barcodeMargin;
+ }
+
+ // Position fields to the right of the barcode
+ if ($record->has('fields') && $this->getSupportFields() > 0) {
+ // Limit to the number of supported fields
+ $fields = array_slice($record->get('fields')->toArray(), 0, $this->getSupportFields());
+
+ // Calculate total height needed for fields
+ $totalFieldsHeight = 0;
+ foreach ($fields as $field) {
+ $totalFieldsHeight += $this->labelSize * 1.2 + $this->labelMargin; // Increased label size by 20%
+ $totalFieldsHeight += $this->fieldSize * 1.3 + $this->fieldMargin * 2; // Increased field size by 30% and margin
+ }
+
+ // Start position - respect top margin
+ $fieldY = $currentY; // $currentY already includes the top margin
+ $fieldWidth = $usableWidth;
+
+ // Calculate available height for fields (respecting margins)
+ $availableHeight = $usableHeight;
+
+ // If fields don't fill available height, adjust spacing proportionally
+ // but don't exceed the available height
+ $scaleFactor = 1.0; // Default scale factor
+ if ($totalFieldsHeight < $availableHeight && count($fields) > 0) {
+ // Scale up to fill available height, but not too much
+ $scaleFactor = min(1.5, $availableHeight / $totalFieldsHeight);
+ } else if ($totalFieldsHeight > $availableHeight && count($fields) > 0) {
+ // Scale down to fit within available height
+ $scaleFactor = $availableHeight / $totalFieldsHeight;
+ }
+
+ foreach ($fields as $field) {
+ // Calculate scaled spacing
+ $labelHeight = $this->labelSize * 1.2 * $scaleFactor;
+ $labelSpacing = $this->labelMargin * $scaleFactor;
+ $fieldHeight = $this->fieldSize * 1.3 * $scaleFactor;
+ $fieldSpacing = $this->fieldMargin * 2 * $scaleFactor;
+
+ // Check if label is empty or null
+ $labelText = $field['label'] ?? '';
+ $valueText = $field['value'] ?? '';
+
+ if (empty(trim($labelText))) {
+ // If label is empty, just render the value at the current Y position
+ static::writeText(
+ $pdf, $valueText,
+ $currentX, $fieldY,
+ 'freemono', 'B', $this->fieldSize * 1.3, 'L', // Increased field size by 30%
+ $fieldWidth, $fieldHeight, false, 0, 0.00
+ );
+ $fieldY += ($fieldHeight + $fieldSpacing) + 0.02; // Increased spacing after value
+ } else {
+ // If label has content, render both label and value
+ static::writeText(
+ $pdf, $labelText,
+ $currentX, $fieldY,
+ 'freesans', 'B', $this->labelSize * 1.2, 'L', // Increased label size by 20% and made bold
+ $labelWidth, $labelHeight, false, 0,
+ );
+ $fieldY += ($labelHeight + $labelSpacing) + 0.01;
+
+ // Value
+ static::writeText(
+ $pdf, $valueText,
+ $currentX, $fieldY, // Position value directly below label
+ 'freemono', 'B', $this->fieldSize * 1.3, 'L', // Increased field size by 30%
+ $fieldWidth, $fieldHeight, false, 0, 0.00
+ );
+ $fieldY += ($fieldHeight + $fieldSpacing) + 0.02; // Increased spacing after value
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Models/Labels/Tapes/Generic/GenericTape.php b/app/Models/Labels/Tapes/Generic/GenericTape.php
new file mode 100644
index 0000000000..1c8426a79a
--- /dev/null
+++ b/app/Models/Labels/Tapes/Generic/GenericTape.php
@@ -0,0 +1,127 @@
+width = $width;
+ $this->height = $height;
+ $this->continuous = $continuous;
+ $this->spacing = $spacing;
+
+ // Calculate base font size (7% of tape width)
+ $baseFontSize = static::TAPE_WIDTH * 0.07;
+
+ // Calculate margin (4% of tape width)
+ $margin = static::TAPE_WIDTH * 0.04;
+
+ // Set margins
+ $this->marginTop = $margin;
+ $this->marginBottom = $margin;
+ $this->marginLeft = $margin;
+ $this->marginRight = $margin;
+
+ // Calculate and set element sizing based on base font size
+ $this->titleSize = $baseFontSize; // Same as base font size
+ $this->titleMargin = $baseFontSize * 0.3; // 30% of base font size
+ $this->fieldSize = $baseFontSize * 1.1; // 110% of base font size
+ $this->fieldMargin = $baseFontSize * 0.1; // 10% of base font size
+ $this->labelSize = $baseFontSize * 0.7; // 70% of base font size
+ $this->labelMargin = $baseFontSize * -0.1; // -10% of base font size
+ $this->barcodeMargin = $baseFontSize * 0.5; // 50% of base font size
+ $this->tagSize = $baseFontSize * 0.8; // 80% of base font size
+ }
+
+ // Unit of measurement
+ public function getUnit()
+ {
+ return 'mm';
+ }
+
+ // Label dimensions
+ public function getWidth()
+ {
+ return $this->width;
+ }
+ public function getHeight()
+ {
+ return $this->height;
+ }
+
+ // Margins
+ public function getMarginTop()
+ {
+ return $this->marginTop;
+ }
+ public function getMarginBottom()
+ {
+ return $this->marginBottom;
+ }
+ public function getMarginLeft()
+ {
+ return $this->marginLeft;
+ }
+ public function getMarginRight()
+ {
+ return $this->marginRight;
+ }
+
+
+ /**
+ * Check if this is a continuous tape
+ *
+ * @return bool
+ */
+ public function isContinuous()
+ {
+ return $this->continuous;
+ }
+
+ /**
+ * Get spacing between labels (for die-cut tapes)
+ *
+ * @return float
+ */
+ public function getSpacing()
+ {
+ return $this->spacing;
+ }
+}
\ No newline at end of file
diff --git a/app/Models/Labels/Tapes/Generic/Tape_53mm.php b/app/Models/Labels/Tapes/Generic/Tape_53mm.php
new file mode 100644
index 0000000000..679380f221
--- /dev/null
+++ b/app/Models/Labels/Tapes/Generic/Tape_53mm.php
@@ -0,0 +1,34 @@
+tapeHeight = $height;
+ }
+
+ /**
+ * Get the barcode size ratio for calculations
+ *
+ * @return float
+ */
+ public function getBarcodeRatio()
+ {
+ return 0.9; // Barcode should use 90% of available width
+ }
+}
\ No newline at end of file
diff --git a/app/Models/Labels/Tapes/Generic/Tape_53mm_A.php b/app/Models/Labels/Tapes/Generic/Tape_53mm_A.php
new file mode 100644
index 0000000000..8b7a79dda0
--- /dev/null
+++ b/app/Models/Labels/Tapes/Generic/Tape_53mm_A.php
@@ -0,0 +1,101 @@
+SetAutoPageBreak(false);
+ }
+
+ public function write($pdf, $record)
+ {
+ $pa = $this->getPrintableArea();
+
+ $currentX = $pa->x1;
+ $currentY = $pa->y1;
+ $usableWidth = $pa->w;
+ $usableHeight = $pa->h;
+
+ if ($record->has('title')) {
+ static::writeText(
+ $pdf, $record->get('title'),
+ $pa->x1, $pa->y1,
+ 'freesans', '', $this->titleSize, 'C',
+ $pa->w, $this->titleSize, true, 0
+ );
+ $currentY += $this->titleSize + $this->titleMargin;
+ $usableHeight -= $this->titleSize + $this->titleMargin;
+ }
+
+ // Make the barcode as large as possible while still leaving room for fields
+ $barcodeSize = min($usableHeight * 0.8, $usableWidth * $this->getBarcodeRatio());
+
+ if ($record->has('barcode2d')) {
+ $barcodeX = $pa->x1 + ($usableWidth - $barcodeSize) / 2;
+
+ static::write2DBarcode(
+ $pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type,
+ $barcodeX, $currentY,
+ $barcodeSize, $barcodeSize
+ );
+ $currentY += $barcodeSize + $this->barcodeMargin;
+ }
+
+ if ($record->has('fields')) {
+ foreach ($record->get('fields') as $field) {
+ static::writeText(
+ $pdf, $field['label'],
+ $currentX, $currentY,
+ 'freesans', '', $this->labelSize, 'L',
+ $usableWidth, $this->labelSize, true, 0
+ );
+ $currentY += $this->labelSize + $this->labelMargin;
+
+ static::writeText(
+ $pdf, $field['value'],
+ $currentX, $currentY,
+ 'freemono', 'B', $this->fieldSize, 'L',
+ $usableWidth, $this->fieldSize, true, 0, 0.01
+ );
+ $currentY += $this->fieldSize + $this->fieldMargin;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Models/Ldap.php b/app/Models/Ldap.php
index f71f926a93..c40ad60f16 100644
--- a/app/Models/Ldap.php
+++ b/app/Models/Ldap.php
@@ -27,11 +27,40 @@ use Illuminate\Support\Facades\Crypt;
class Ldap extends Model
{
+ public static function ignoreCertificates(bool $ignore_cert = true)
+ {
+ if (defined('LDAP_OPT_X_TLS_REQUIRE_CERT') && defined('LDAP_OPT_X_TLS_NEVER')) {
+ // TODO - we are currently, as a 'safety', doing *both* the following 'new-style' ldap_set_option calls,
+ // as well as "falling-through" to the 'old-style' putenv() calls.
+ //
+ // I *suspect* we can eventually remove the putenv() calls, but I'm just a little nervous about that.
+ // According to the PHP docs, the LDAP_OPT_X_TLS_REQUIRE_CERT constant has been available since PHP 7.0.
+ // We're currently using PHP versions way, way later than that (v8.2-v8.4 as of this writing). So it's
+ // unlikely that these constants wouldn't be defined - unless you didn't have LDAP support in the first
+ // place. But if that were to happen, I would hope we would've detected that long, long ago, rather than at
+ // this point.
+ if ($ignore_cert) {
+ if (ldap_set_option(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER)) {
+ //return true;
+ }
+ } else {
+ if (ldap_set_option(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_DEMAND)) {
+ //return true;
+ }
+ }
+ }
+ if ($ignore_cert) {
+ return putenv('LDAPTLS_REQCERT=never');
+ } else {
+ return putenv('LDAPTLS_REQCERT');
+ }
+ }
+
/**
* Makes a connection to LDAP using the settings in Admin > Settings.
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
* @return connection
*/
public static function connectToLdap()
@@ -43,15 +72,12 @@ class Ldap extends Model
// If we are ignoring the SSL cert we need to setup the environment variable
// before we create the connection
- if ($ldap_server_cert_ignore == '1') {
- putenv('LDAPTLS_REQCERT=never');
- }
+ self::ignoreCertificates((bool)$ldap_server_cert_ignore);
// If the user specifies where CA Certs are, make sure to use them
if (env('LDAPTLS_CACERT')) {
putenv('LDAPTLS_CACERT='.env('LDAPTLS_CACERT'));
}
-
$connection = @ldap_connect($ldap_host);
if (! $connection) {
@@ -81,10 +107,10 @@ class Ldap extends Model
* Binds/authenticates the user to LDAP, and returns their attributes.
*
* @author [A. Gianotto] []
- * @since [v3.0]
- * @param $username
- * @param $password
- * @param bool|false $user
+ * @since [v3.0]
+ * @param $username
+ * @param $password
+ * @param bool|false $user
* @return bool true if the username and/or password provided are valid
* false if the username and/or password provided are invalid
* array of ldap_attributes if $user is true
@@ -160,8 +186,8 @@ class Ldap extends Model
* Here we also return a better error if the app key is donked.
*
* @author [A. Gianotto] []
- * @since [v3.0]
- * @param bool|false $user
+ * @since [v3.0]
+ * @param bool|false $user
* @return bool true if the username and/or password provided are valid
* false if the username and/or password provided are invalid
*/
@@ -169,37 +195,37 @@ class Ldap extends Model
{
$ldap_username = Setting::getSettings()->ldap_uname;
- if ( $ldap_username ) {
- // Lets return some nicer messages for users who donked their app key, and disable LDAP
- try {
- $ldap_pass = Crypt::decrypt(Setting::getSettings()->ldap_pword);
- } catch (Exception $e) {
- throw new Exception('Your app key has changed! Could not decrypt LDAP password using your current app key, so LDAP authentication has been disabled. Login with a local account, update the LDAP password and re-enable it in Admin > Settings.');
- }
+ if ($ldap_username ) {
+ // Lets return some nicer messages for users who donked their app key, and disable LDAP
+ try {
+ $ldap_pass = Crypt::decrypt(Setting::getSettings()->ldap_pword);
+ } catch (Exception $e) {
+ throw new Exception('Your app key has changed! Could not decrypt LDAP password using your current app key, so LDAP authentication has been disabled. Login with a local account, update the LDAP password and re-enable it in Admin > Settings.');
+ }
- if (! $ldapbind = @ldap_bind($connection, $ldap_username, $ldap_pass)) {
- throw new Exception('Could not bind to LDAP: '.ldap_error($connection));
- }
- // TODO - this just "falls off the end" but the function states that it should return true or false
- // unfortunately, one of the use cases for this function is wrong and *needs* for that failure mode to fire
- // so I don't want to fix this right now.
- // this method MODIFIES STATE on the passed-in $connection and just returns true or false (or, in this case, undefined)
- // at the next refactor, this should be appropriately modified to be more consistent.
- } else {
- // LDAP should also work with anonymous bind (no dn, no password available)
- if (! $ldapbind = @ldap_bind($connection )) {
- throw new Exception('Could not bind to LDAP: '.ldap_error($connection));
- }
- }
- }
+ if (! $ldapbind = @ldap_bind($connection, $ldap_username, $ldap_pass)) {
+ throw new Exception('Could not bind to LDAP: '.ldap_error($connection));
+ }
+ // TODO - this just "falls off the end" but the function states that it should return true or false
+ // unfortunately, one of the use cases for this function is wrong and *needs* for that failure mode to fire
+ // so I don't want to fix this right now.
+ // this method MODIFIES STATE on the passed-in $connection and just returns true or false (or, in this case, undefined)
+ // at the next refactor, this should be appropriately modified to be more consistent.
+ } else {
+ // LDAP should also work with anonymous bind (no dn, no password available)
+ if (! $ldapbind = @ldap_bind($connection)) {
+ throw new Exception('Could not bind to LDAP: '.ldap_error($connection));
+ }
+ }
+ }
/**
* Parse and map LDAP attributes based on settings
*
* @author [A. Gianotto] []
- * @since [v3.0]
+ * @since [v3.0]
*
- * @param $ldapatttibutes
+ * @param $ldapatttibutes
* @return array|bool
*/
public static function parseAndMapLdapAttributes($ldapattributes)
@@ -238,8 +264,8 @@ class Ldap extends Model
* Create user from LDAP attributes
*
* @author [A. Gianotto] []
- * @since [v3.0]
- * @param $ldapatttibutes
+ * @since [v3.0]
+ * @param $ldapatttibutes
* @return User | bool
*/
public static function createUserFromLdap($ldapatttibutes, $password)
@@ -279,11 +305,11 @@ class Ldap extends Model
* Searches LDAP
*
* @author [A. Gianotto] []
- * @since [v3.0]
- * @param $base_dn
- * @param $count
- * @param $filter
- * @param $attributes
+ * @since [v3.0]
+ * @param $base_dn
+ * @param $count
+ * @param $filter
+ * @param $attributes
* @return array|bool
*/
public static function findLdapUsers($base_dn = null, $count = -1, $filter = null, $attributes = [])
@@ -331,7 +357,7 @@ class Ldap extends Model
$errmsg = null;
$referrals = null;
$controls = [];
- ldap_parse_result($ldapconn, $search_results, $errcode , $matcheddn , $errmsg , $referrals, $controls);
+ ldap_parse_result($ldapconn, $search_results, $errcode, $matcheddn, $errmsg, $referrals, $controls);
if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
// You need to pass the cookie from the last call to the next one
$cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
diff --git a/app/Models/License.php b/app/Models/License.php
index 490a8401a4..ecd1b003e3 100755
--- a/app/Models/License.php
+++ b/app/Models/License.php
@@ -3,13 +3,14 @@
namespace App\Models;
use App\Helpers\Helper;
+use App\Models\Traits\CompanyableTrait;
+use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Carbon\Carbon;
-use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
-use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Session;
use Watson\Validating\ValidatingTrait;
@@ -21,6 +22,7 @@ class License extends Depreciable
use SoftDeletes;
use CompanyableTrait;
+ use HasUploads;
use Loggable, Presentable;
protected $injectUniqueIdentifier = true;
use ValidatingTrait;
@@ -43,13 +45,13 @@ class License extends Depreciable
protected $rules = [
'name' => 'required|string|min:3|max:255',
- 'seats' => 'required|min:1|integer',
+ 'seats' => 'required|min:1|integer|limit_change:10000', // limit_change is a "pseudo-rule" that translates into 'between', see prepareLimitChangeRule() below
'license_email' => 'email|nullable|max:120',
'license_name' => 'string|nullable|max:100',
'notes' => 'string|nullable',
'category_id' => 'required|exists:categories,id',
'company_id' => 'integer|nullable',
- 'purchase_cost'=> 'numeric|nullable|gte:0',
+ 'purchase_cost' => 'numeric|nullable|gte:0|max:99999999999999999.99',
'purchase_date' => 'date_format:Y-m-d|nullable|max:10|required_with:depreciation_id',
'expiration_date' => 'date_format:Y-m-d|nullable|max:10',
'termination_date' => 'date_format:Y-m-d|nullable|max:10',
@@ -113,6 +115,7 @@ class License extends Depreciable
'company' => ['name'],
'category' => ['name'],
'depreciation' => ['name'],
+ 'supplier' => ['name'],
];
protected $appends = ['free_seat_count'];
@@ -120,39 +123,51 @@ class License extends Depreciable
* Update seat counts when the license is updated
*
* @author A. Gianotto
- * @since [v3.0]
+ * @since [v3.0]
*/
public static function boot()
{
parent::boot();
// We need to listen for created for the initial setup so that we have a license ID.
- static::created(function ($license) {
- $newSeatCount = $license->getAttributes()['seats'];
+ static::created(
+ function ($license) {
+ $newSeatCount = $license->getAttributes()['seats'];
- return static::adjustSeatCount($license, 0, $newSeatCount);
- });
+ return static::adjustSeatCount($license, 0, $newSeatCount);
+ }
+ );
// However, we listen for updating to be able to prevent the edit if we cannot delete enough seats.
- static::updating(function ($license) {
- $newSeatCount = $license->getAttributes()['seats'];
- //$oldSeatCount = isset($license->getOriginal()['seats']) ? $license->getOriginal()['seats'] : 0;
- /*
- That previous method *did* mostly work, but if you ever managed to get your $license->seats value out of whack
- with your actual count of license_seats *records*, you would never manage to get back 'into whack'.
- The below method actually grabs a count of existing license_seats records, so it will be more accurate.
- This means that if your license_seats are out of whack, you can change the quantity and hit 'save' and it
- will manage to 'true up' and make your counts line up correctly.
- */
- $oldSeatCount = $license->license_seats_count;
+ static::updating(
+ function ($license) {
+ $newSeatCount = $license->getAttributes()['seats'];
+ //$oldSeatCount = isset($license->getOriginal()['seats']) ? $license->getOriginal()['seats'] : 0;
+ /*
+ That previous method *did* mostly work, but if you ever managed to get your $license->seats value out of whack
+ with your actual count of license_seats *records*, you would never manage to get back 'into whack'.
+ The below method actually grabs a count of existing license_seats records, so it will be more accurate.
+ This means that if your license_seats are out of whack, you can change the quantity and hit 'save' and it
+ will manage to 'true up' and make your counts line up correctly.
+ */
+ $oldSeatCount = $license->license_seats_count;
- return static::adjustSeatCount($license, $oldSeatCount, $newSeatCount);
- });
+ return static::adjustSeatCount($license, $oldSeatCount, $newSeatCount);
+ }
+ );
+ }
+
+ public function prepareLimitChangeRule($parameters, $field)
+ {
+ $actual_seat_count = $this->licenseseats()->count(); //we use the *actual* seat count here, in case your license has gone wonky
+ $lower_bound = $actual_seat_count - $parameters[0];
+ $upper_bound = $actual_seat_count + $parameters[0];
+ return ["between", ($lower_bound <= 0 ? 1 : $lower_bound), $upper_bound];
}
/**
* Balance seat counts
*
* @author A. Gianotto
- * @since [v3.0]
+ * @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public static function adjustSeatCount($license, $oldSeats, $newSeats)
@@ -164,21 +179,17 @@ class License extends Depreciable
// On Create, we just make one for each of the seats.
$change = abs($oldSeats - $newSeats);
if ($oldSeats > $newSeats) {
- $license->load('licenseseats.user');
// Need to delete seats... lets see if if we have enough.
- $seatsAvailableForDelete = $license->licenseseats->reject(function ($seat) {
- return ((bool) $seat->assigned_to) || ((bool) $seat->asset_id);
- });
+ $seatsAvailableForDelete = $license->licenseseats()->whereNull('assigned_to')->whereNull('asset_id')->limit($change);
if ($change > $seatsAvailableForDelete->count()) {
Session::flash('error', trans('admin/licenses/message.assoc_users'));
return false;
}
- for ($i = 1; $i <= $change; $i++) {
- $seatsAvailableForDelete->pop()->delete();
- }
+ $seatsAvailableForDelete->delete();
+
// Log Deletion of seats.
$logAction = new Actionlog;
$logAction->item_type = self::class;
@@ -203,11 +214,15 @@ class License extends Depreciable
}
//Chunk and use DB transactions to prevent timeouts.
- collect($licenseInsert)->chunk(1000)->each(function ($chunk) {
- DB::transaction(function () use ($chunk) {
- LicenseSeat::insert($chunk->toArray());
- });
- });
+ collect($licenseInsert)->chunk(1000)->each(
+ function ($chunk) {
+ DB::transaction(
+ function () use ($chunk) {
+ LicenseSeat::insert($chunk->toArray());
+ }
+ );
+ }
+ );
// On initial create, we shouldn't log the addition of seats.
if ($license->id) {
@@ -228,7 +243,7 @@ class License extends Depreciable
* Sets the attribute for whether or not the license is maintained
*
* @author A. Gianotto
- * @since [v1.0]
+ * @since [v1.0]
* @return mixed
*/
public function setMaintainedAttribute($value)
@@ -240,7 +255,7 @@ class License extends Depreciable
* Sets the reassignable attribute
*
* @author A. Gianotto
- * @since [v1.0]
+ * @since [v1.0]
* @return mixed
*/
public function setReassignableAttribute($value)
@@ -252,7 +267,7 @@ class License extends Depreciable
* Sets expiration date attribute
*
* @author A. Gianotto
- * @since [v1.0]
+ * @since [v1.0]
* @return mixed
*/
public function setExpirationDateAttribute($value)
@@ -269,7 +284,7 @@ class License extends Depreciable
* Sets termination date attribute
*
* @author A. Gianotto