diff --git a/CageUI/package-lock.json b/CageUI/package-lock.json index 8d9e2834d..5faf07a69 100644 --- a/CageUI/package-lock.json +++ b/CageUI/package-lock.json @@ -9,13 +9,20 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", "@labkey/api": "1.51.5", "@labkey/components": "7.43.0", + "@mui/icons-material": "^9.0.1", + "@mui/material": "^9.0.1", + "@mui/x-data-grid": "^8.28.6", + "@mui/x-date-pickers": "^9.2.0", "d3": "^7.9.0", "dayjs": "^1.11.21", "react": "~18.3.1", "react-bootstrap": "~2.10.10", "react-dom": "~18.3.1", + "react-is": "^18.3.1", "react-select": "^5.10.2", "react-svg": "^16.4.2" }, @@ -1727,6 +1734,28 @@ "node": ">=6.9.0" } }, + "node_modules/@base-ui/utils": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.3.2.tgz", + "integrity": "sha512-oWy1aq/I2GmYjpl4PhEAhzflF8VPGKgZeq0xAWTbfD5KBWyxcN0ZP2+WHSUm/5Z6lVMBDLReLcoXwSYoRc/zNQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@floating-ui/utils": "^0.2.12", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "@types/react": "^17 || ^18 || ^19", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", @@ -1929,20 +1958,14 @@ "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", "license": "MIT", "dependencies": { - "@emotion/memoize": "0.7.4" + "@emotion/memoize": "^0.9.0" } }, - "node_modules/@emotion/is-prop-valid/node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "license": "MIT" - }, "node_modules/@emotion/memoize": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", @@ -1993,17 +2016,26 @@ "license": "MIT" }, "node_modules/@emotion/styled": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.3.0.tgz", - "integrity": "sha512-GgcUpXBBEU5ido+/p/mCT2/Xx+Oqmp9JzQRuC+a4lYM4i4LBBn/dWvc0rQ19N9ObA8/T4NWMrPNe79kMBDJqoQ==", + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", "dependencies": { - "@emotion/styled-base": "^10.3.0", - "babel-plugin-emotion": "^10.0.27" + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" }, "peerDependencies": { - "@emotion/core": "^10.0.27", - "react": ">=16.3.0" + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@emotion/styled-base": { @@ -2028,6 +2060,15 @@ "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", "license": "MIT" }, + "node_modules/@emotion/styled-base/node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, "node_modules/@emotion/styled-base/node_modules/@emotion/memoize": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", @@ -2153,9 +2194,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, "node_modules/@hello-pangea/dnd": { @@ -2782,6 +2823,581 @@ "dev": true, "license": "MIT" }, + "node_modules/@mui/core-downloads-tracker": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.3.1.tgz", + "integrity": "sha512-IAyAFNQbT7hysJ9HXphiOmWJF7G1OglzHanqCgvQgH9LA2ydxtmaTBDbcBqw6euZesyShiwvpvbnYO1GY1AyXQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.3.1.tgz", + "integrity": "sha512-rZj5ccG7vkpV38o/l4ys+chfE9GFypmvZr9dSNBoYVCNBD9yC6KfKq1TYpEMshWbjic7GKQ8MA1LQlcpGgcq9Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^9.3.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.3.1.tgz", + "integrity": "sha512-NahAEGIXqS1K0bA4th1jeFxBguS59NOcLbMA0vU+fSaPWKjtwGBGGHeTwlc9PSmzMjOZKceeFWESB8fVHr31hA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/core-downloads-tracker": "^9.3.1", + "@mui/system": "^9.3.0", + "@mui/types": "^9.3.0", + "@mui/utils": "^9.3.0", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.8", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^9.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/@mui/private-theming": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.3.0.tgz", + "integrity": "sha512-ERvqk5pejf9aRnQcDILSWGtFmsEMiVxlQ4+xsVCjsEvmK0fV9BiVP/cQwAF5dwyDFbve4lTrlcgeFEecVzTNiA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/utils": "^9.3.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.3.0.tgz", + "integrity": "sha512-x9+KYxhjoHYZ4nioxdKnvQWdw0RScbhoZfQ4tv3Db742683U3wlYSp6zV6Us78dMFnKnyTrPTkQKyutp14gnKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.3.0.tgz", + "integrity": "sha512-0l4LqHJxZj65xSrioniGsxm7VNoGXonPo203oZjhBUvIDPeBqRTb7Mqc45Qxs6sO6WGR7WE/9cJ6lb4lkvHkjg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/private-theming": "^9.3.0", + "@mui/styled-engine": "^9.3.0", + "@mui/types": "^9.3.0", + "@mui/utils": "^9.3.0", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.3.0.tgz", + "integrity": "sha512-2JSxyfpEFWNUB2vKs/T1BvkfyNisMHWph8bLMj8T0uHwmLl/0qfAwQkfwMT6kxLXN9uIum9AEbECXU8er3amIg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.3.0.tgz", + "integrity": "sha512-2HZdHwWJ6eB+7lVGSOHsByGw8jeRulT4g0NZ608Wb8Q57DE2jbNqrWPFuJsvkQQiBiTmlpvQL3i+/62zsiPrkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/types": "^9.3.0", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/@mui/x-data-grid": { + "version": "8.29.2", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.29.2.tgz", + "integrity": "sha512-AnH3yQJZXUB+Dv2CtSx8J1XBU1y4GTbq3MCC+r0CCTtglB032K4UhbyeMXG6FLFqWDn1nKofcBNYVoNv5ELR4A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.5", + "@mui/x-internals": "8.29.2", + "@mui/x-virtualizer": "0.4.1", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid/node_modules/@mui/types": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", + "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid/node_modules/@mui/utils": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz", + "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/types": "^7.4.12", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid/node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/@mui/x-date-pickers": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-9.11.0.tgz", + "integrity": "sha512-3vLmkn1wG+hNBaGobnk/9R8APCRhOiAXBTC78m7wZ2OWZ2NoMqi+O09HwTGlIgK6mScZZCeCQTSj5phNLrK1hw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/utils": "^9.3.0", + "@mui/x-internals": "^9.11.0", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^7.3.0 || ^9.0.0", + "@mui/system": "^7.3.0 || ^9.0.0", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2 || ^3.0.0", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/@mui/x-internals": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-9.11.0.tgz", + "integrity": "sha512-JjKe9k1+gVWNPwMTZLNSc92eVmoZqP5Xq3Ui6mJkstotUrbWIZC0o9+4AfTR1lYqWwnKmqm+VtLLVCa7MEBXWQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@base-ui/utils": "^0.3.1", + "@mui/utils": "^9.3.0", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-internals": { + "version": "8.29.2", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.29.2.tgz", + "integrity": "sha512-TLILyHia5NHh3MGErFDh0bXZ4V6iS45hdStofsV5wklF6dpgZjduo65oJB0h81mI5mexNlhHANEVfw0KV6BxBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.5", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-internals/node_modules/@mui/types": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", + "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals/node_modules/@mui/utils": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz", + "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/types": "^7.4.12", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals/node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/@mui/x-virtualizer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.4.1.tgz", + "integrity": "sha512-EM4lCW9MFRHxAgdqBG6nRQXlrhctlHqOjPm95usht15JBXtuWwtuVdA5q8ta+EwXE9zF0BcKyQsGs8R/vWzmFw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.5", + "@mui/x-internals": "8.29.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-virtualizer/node_modules/@mui/types": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", + "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-virtualizer/node_modules/@mui/utils": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz", + "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/types": "^7.4.12", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-virtualizer/node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, "node_modules/@noble/hashes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", @@ -8929,6 +9545,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -9199,9 +9821,9 @@ } }, "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, "node_modules/react-lifecycles-compat": { @@ -9365,6 +9987,20 @@ "react-dom": ">=16.7.0" } }, + "node_modules/react-treebeard/node_modules/@emotion/styled": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.3.0.tgz", + "integrity": "sha512-GgcUpXBBEU5ido+/p/mCT2/Xx+Oqmp9JzQRuC+a4lYM4i4LBBn/dWvc0rQ19N9ObA8/T4NWMrPNe79kMBDJqoQ==", + "license": "MIT", + "dependencies": { + "@emotion/styled-base": "^10.3.0", + "babel-plugin-emotion": "^10.0.27" + }, + "peerDependencies": { + "@emotion/core": "^10.0.27", + "react": ">=16.3.0" + } + }, "node_modules/reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -9568,6 +10204,12 @@ "dev": true, "license": "MIT" }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", diff --git a/CageUI/package.json b/CageUI/package.json index 28708475a..a9a490598 100644 --- a/CageUI/package.json +++ b/CageUI/package.json @@ -18,13 +18,20 @@ "author": "Board of Regents of the University of Wisconsin System", "license": "Apache-2.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", "@labkey/api": "1.51.5", "@labkey/components": "7.43.0", + "@mui/icons-material": "^9.0.1", + "@mui/material": "^9.0.1", + "@mui/x-data-grid": "^8.28.6", + "@mui/x-date-pickers": "^9.2.0", "d3": "^7.9.0", "dayjs": "^1.11.21", "react": "~18.3.1", "react-bootstrap": "~2.10.10", "react-dom": "~18.3.1", + "react-is": "^18.3.1", "react-select": "^5.10.2", "react-svg": "^16.4.2" }, diff --git a/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.001-26.002.sql b/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.001-26.002.sql index d20ac7610..0e58c0893 100644 --- a/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.001-26.002.sql +++ b/CageUI/resources/schemas/dbscripts/postgresql/cageui-26.001-26.002.sql @@ -40,3 +40,36 @@ select setname, container, 8 as value, 'Caging' as category, 'Ghost Cage' as tit insert into ehr_lookups.lookups (set_name,container,value, title) select setname, container, 'ghostCage' as value, '/cageui/static/cage.svg' as title from ehr_lookups.lookup_sets where setname='cageui_svg_urls'; + +INSERT INTO ehr_lookups.lookup_sets (setname, label, description, keyField, container) +select 'adoption_status' as setname, + 'Adoption Status Field Values' as label, + 'List of possible adoption progress statuses' as description, + 'value' as keyField, + container from ehr_lookups.lookup_sets where setname='ancestry'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 0 as value, 'Start' as title from ehr_lookups.lookup_sets where setname='adoption_status'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 1 as value, 'End' as title from ehr_lookups.lookup_sets where setname='adoption_status'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 2 as value, 'Pause' as title from ehr_lookups.lookup_sets where setname='adoption_status'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 3 as value, 'Resume' as title from ehr_lookups.lookup_sets where setname='adoption_status'; + + +INSERT INTO ehr_lookups.lookup_sets (setname, label, description, keyField, container) +select 'adoption_results' as setname, + 'Adoption Result Field Values' as label, + 'List of possible adoption results' as description, + 'value' as keyField, + container from ehr_lookups.lookup_sets where setname='ancestry'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 0 as value, 'Success' as title from ehr_lookups.lookup_sets where setname='adoption_results'; + +insert into ehr_lookups.lookups (set_name,container,value, title) +select setname, container, 1 as value, 'Failure' as title from ehr_lookups.lookup_sets where setname='adoption_results'; \ No newline at end of file diff --git a/CageUI/src/client/api/labkeyActions.ts b/CageUI/src/client/api/labkeyActions.ts index 7a9451d59..5e6fbbf39 100644 --- a/CageUI/src/client/api/labkeyActions.ts +++ b/CageUI/src/client/api/labkeyActions.ts @@ -19,6 +19,7 @@ import { ActionURL, Ajax, Query, Security, Utils } from '@labkey/api'; import { CageMods, Rack, RackConditionOption, Room, SessionLog } from '../types/typings'; import { buildURL } from '@labkey/components'; import { RackSwitchOption } from '../types/homeTypes'; +import { AdoptionData } from '../types/adoptionFormTypes'; export function labkeyActionSelectWithPromise( options: Query.SelectRowsOptions, @@ -193,4 +194,20 @@ export function updateRackConditionStatus(rack: RackSwitchOption, condition: Rac jsonData: {rack: rack.value.objectId, condition: condition.value}, }); }); +} + +// This function is for submitting a adoption form. +export function startAdoptionSubmission(animals: AdoptionData[]): Promise<{ + success: boolean, + errors: any[] +}> { + return new Promise((resolve, reject) => { + Ajax.request({ + url: buildURL('cageui', 'submitAdoptionForm.api'), + method: 'POST', + success: (res) => resolve(JSON.parse(res.response)), + failure: Utils.getCallbackWrapper((error) => reject(error)), + jsonData: {adoptionData: animals}, + }); + }); } \ No newline at end of file diff --git a/CageUI/src/client/cageui.scss b/CageUI/src/client/cageui.scss index a3b6bc49a..9db3e427d 100644 --- a/CageUI/src/client/cageui.scss +++ b/CageUI/src/client/cageui.scss @@ -673,7 +673,7 @@ } .loading-overlay { - position: absolute; + position: fixed; top: 0; left: 0; width: 100%; @@ -681,22 +681,18 @@ background-color: rgba(0, 0, 0, 0.30); display: flex; justify-content: center; - align-items: start; + align-items: center; border-radius: 8px; z-index: 9999; backdrop-filter: blur(5px); } .loading-content { - position: sticky; - top: 25%; - left: 50%; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 20px; - padding-top: 20px; } .spinner { @@ -1711,9 +1707,6 @@ margin-top: 0px; box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.2); } - - - .modification-editor { } @@ -1724,13 +1717,6 @@ margin-top: 0px; border-bottom: lightgrey 5px solid; } -.modification-editor-input { - width: 100%; - padding: 8px 12px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 1rem; -} .modification-editor-content { margin-bottom: 20px; display: flex; @@ -1738,9 +1724,6 @@ margin-top: 0px; flex-direction: row; } - - - @keyframes fadeIn { from { opacity: 0; @@ -1752,8 +1735,7 @@ margin-top: 0px; } } - -.animal-editor{ +.animal-editor { } @@ -1763,6 +1745,7 @@ margin-top: 0px; } + /* Multi Dropdown Css @@ -2217,3 +2200,39 @@ Multi Dropdown Css height: 100% !important; overflow: hidden !important; } + + +.MuiDataGrid-form-container { + display: grid; + grid-template-columns: minmax(0, 1fr); + position: relative; + width: 100%; + gap: 10px; + + .MuiDataGrid-root { + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + width: 100%; + overflow: hidden; // Ensure DataGrid handles its own internal overflow + + .MuiDataGrid-main { + min-width: 0; + } + + .MuiDataGrid-cell:focus-within { + outline: none; + } + } +} + +.form-actions { + display: flex; + justify-content: flex-end; + gap: 15px; + padding: 20px; + background: white; + border-radius: 8px; + box-shadow: 0 -2px 10px rgba(0,0,0,0.05); + position: sticky; + bottom: 0; +} \ No newline at end of file diff --git a/CageUI/src/client/components/AutoCompleteEditCell.tsx b/CageUI/src/client/components/AutoCompleteEditCell.tsx new file mode 100644 index 000000000..1608fe743 --- /dev/null +++ b/CageUI/src/client/components/AutoCompleteEditCell.tsx @@ -0,0 +1,85 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ +import * as React from 'react'; +import { useState } from 'react'; +import { GridRenderEditCellParams, useGridApiContext } from '@mui/x-data-grid'; +import { Autocomplete, TextField } from '@mui/material'; + +interface AutoCompleteEditCellParams { + options: any[] + required: boolean; + multiple?: boolean; + disableClearable?: boolean; + returnValueOnly?: boolean; +} + +export const AutoCompleteEditCell = (props: GridRenderEditCellParams & AutoCompleteEditCellParams) => { + const { id, field, value, options, required, multiple, disableClearable, returnValueOnly } = props; + const apiRef = useGridApiContext(); + const [open, setOpen] = useState(true); + + const handleChange = (event: any, newValue: any) => { + const val = returnValueOnly && newValue ? newValue.value : newValue; + apiRef.current.setEditCellValue({ id, field, value: val }); + if (!multiple && (newValue || newValue === null)) { + apiRef.current.stopCellEditMode({ id, field }); + } + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Tab') { + apiRef.current.stopCellEditMode({ id, field }); + } + }; + + const isError = required && (value === null || value === undefined || (Array.isArray(value) && value.length === 0) || value === ''); + const selectedOption = multiple ? (value || []) : (options.find(opt => opt.value === value || opt === value || (typeof value === 'object' && value !== null && opt.value === value.value)) || null); + + return ( + option.label || ''} + value={selectedOption} + onChange={handleChange} + open={open} + onOpen={() => setOpen(true)} + onClose={(event, reason) => { + if (reason === 'selectOption' || reason === 'blur' || reason === 'escape') { + setOpen(false); + } + }} + fullWidth + multiple={multiple} + disableClearable={disableClearable} + isOptionEqualToValue={(option, value) => { + const val = (value && typeof value === 'object' && 'value' in value) ? value.value : value; + return option.value === val; + }} + renderInput={(params) => ( + + )} + /> + ); +}; \ No newline at end of file diff --git a/CageUI/src/client/components/DateTimeGridField.tsx b/CageUI/src/client/components/DateTimeGridField.tsx new file mode 100644 index 000000000..bd44ffe0a --- /dev/null +++ b/CageUI/src/client/components/DateTimeGridField.tsx @@ -0,0 +1,180 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ +import * as React from 'react'; +import { + DataGrid, + GridColDef, + GridRowsProp, + useGridApiContext, + GridRenderEditCellParams, + GRID_DATE_COL_DEF, + GRID_DATETIME_COL_DEF, + GridColTypeDef, + GridFilterInputValueProps, + getGridDateOperators, +} from '@mui/x-data-grid'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; // Says this import is unused but will break typescript if removed +import { enUS as locale } from 'date-fns/locale'; +import { format } from 'date-fns/format'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; +import { Dayjs } from 'dayjs'; + +AdapterDateFns; // this is here to prevent intellij/ide "remove unused imports" from cleaning up the required import above. + +// Check out the code used here at this link to explain it further. (MUI X v9.3.0) +// https://mui.com/x/react-data-grid/custom-columns/#date-pickers +/** + * `date` column + */ + +const dateColumnType: GridColTypeDef = { + ...GRID_DATE_COL_DEF, + resizable: false, + renderEditCell: (params) => { + return ; + }, + filterOperators: getGridDateOperators(false).map((item) => ({ + ...item, + InputComponent: GridFilterDateInput, + InputComponentProps: { showTime: false }, + })), + valueFormatter: (value) => { + if (value) { + return format(value, 'MM/dd/yyyy', { locale }); + } + return ''; + }, +}; + +function GridEditDateCell({ + id, + field, + value, + colDef, + hasFocus, + }: GridRenderEditCellParams) { + const apiRef = useGridApiContext(); + const inputRef = React.useRef(null); + const [open, setOpen] = React.useState(true); + const Component = colDef.type === 'dateTime' ? DateTimePicker : DatePicker; + + const handleChange = (newValue: unknown) => { + apiRef.current.setEditCellValue({ id, field, value: newValue }); + }; + + const handleAccept = (newValue: unknown) => { + apiRef.current.setEditCellValue({ id, field, value: newValue }); + apiRef.current.stopCellEditMode({ id, field }); + }; + + const handleClose = () => { + setOpen(false); + apiRef.current.stopCellEditMode({ id, field }); + }; + + useEnhancedEffect(() => { + if (hasFocus) { + inputRef.current!.focus(); + } + }, [hasFocus]); + + return ( + setOpen(true)} + onClose={handleClose} + onChange={handleChange} + onAccept={handleAccept} + closeOnSelect={false} + timeSteps={{ minutes: 1 }} + slotProps={{ + actionBar: { + actions: ['cancel', 'accept'], + }, + textField: { + inputRef, + variant: 'standard', + fullWidth: true, + sx: { + padding: '0 9px', + justifyContent: 'center', + '& .MuiInput-underline:after': { + borderBottomColor: value ? 'primary' : 'error.main', + }, + }, + error: !value, + slotProps: { + input: { + disableUnderline: false, + sx: { fontSize: 'inherit' }, + }, + }, + }, + }} + /> + ); +} + +function GridFilterDateInput( + props: GridFilterInputValueProps & { showTime?: boolean }, +) { + const { item, showTime, applyValue, apiRef } = props; + + const Component = showTime ? DateTimePicker : DatePicker; + + const handleFilterChange = (newValue: unknown) => { + applyValue({ ...item, value: newValue }); + }; + + return ( + + ); +} + +/** + * `dateTime` column + */ + +export const dateTimeColumnType: GridColTypeDef = { + ...GRID_DATETIME_COL_DEF, + resizable: true, + renderEditCell: (params) => { + return ; + }, + filterOperators: getGridDateOperators(true).map((item) => ({ + ...item, + InputComponent: GridFilterDateInput, + InputComponentProps: { showTime: true }, + })), + valueFormatter: (value: Dayjs) => { + if (value) { + return format(value.toDate(), 'MM/dd/yyyy hh:mm a', { locale }); + } + return ''; + }, +}; diff --git a/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx new file mode 100644 index 000000000..0e5a1f865 --- /dev/null +++ b/CageUI/src/client/components/adoptionDataEntry/AdoptionForm.tsx @@ -0,0 +1,445 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + + +import * as React from 'react'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { + DataGrid, + GridAutosizeOptions, + GridColDef, + GridRenderCellParams, + GridRowModel, + useGridApiRef, + useGridApiContext, + GridRenderEditCellParams, GridCellParams +} from '@mui/x-data-grid'; +import { Autocomplete, Box, Button, IconButton, TextField } from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; +import dayjs from 'dayjs'; +import { AdoptionData, AdoptionResult, AdoptionStatus } from '../../types/adoptionFormTypes'; +import { dateTimeColumnType } from '../DateTimeGridField'; +import { generateUUID } from '../../utils/helpers'; +import { ActionURL, Filter, Query } from '@labkey/api'; +import { labkeyActionSelectWithPromise, startAdoptionSubmission } from '../../api/labkeyActions'; +import { AutoCompleteEditCell } from '../AutoCompleteEditCell'; +import { LoadingScreen } from '../LoadingScreen'; +import { LayoutErrors } from '../LayoutErrors'; + +interface AdoptionFormProps { + prevForm?: AdoptionData; +} + +export const AdoptionForm: FC = (props) => { + const {prevForm} = props; + const [animals, setAnimals] = useState(prevForm ? [prevForm] : []); + const [centerAnimals, setCenterAnimals] = useState([]); + const [errorMsg, setErrorMsg] = useState([]); + const [isSaving, setIsSaving] = useState(false); + const apiRef = useGridApiRef(); + const [autoSizeOptions] = useState({ + includeHeaders: true, + includeOutliers: true, + expand: true, + outliersFactor: 1.5, + }); + + useEffect(() => { + if (apiRef.current) { + const timeout = setTimeout(() => { + apiRef.current?.autosizeColumns(autoSizeOptions); + }, 250); + return () => clearTimeout(timeout); + } + }, [apiRef, animals, autoSizeOptions]); + + useEffect(() => { + const config: Query.SelectRowsOptions = { + schemaName: 'study', + queryName: 'demographics', + viewName: 'Alive, at Center', + columns: ['Id'] + }; + + labkeyActionSelectWithPromise(config).then(result => { + if (result.rows.length !== 0) { + const rowOptions: string[] = []; + result.rows.forEach(row => { + rowOptions.push(row.Id); + }); + setCenterAnimals(rowOptions); + } + }).catch(err => { + console.error('Error fetching alive at center animals', err); + }); + }, []); + + const handleAddAnimal = useCallback(() => { + const newAnimal: AdoptionData = { + objectid: generateUUID(), + id: '', + date: dayjs(), + dam: null, + sire: null, + type: { + label: AdoptionStatus[AdoptionStatus.Start] as keyof typeof AdoptionStatus, + value: AdoptionStatus.Start + }, + result: null + }; + setAnimals(prev => [...prev, newAnimal]); + }, []); + + const handleDeleteRow = useCallback((objectid: string) => { + setAnimals(prev => prev.filter(animal => animal.objectid !== objectid)); + }, []); + + const processRowUpdate = useCallback((newRow: GridRowModel, oldRow: GridRowModel) => { + if (newRow.type && newRow.type.value !== AdoptionStatus.End) { + newRow.result = null; + } + + if (!prevForm && newRow.id && newRow.id !== oldRow.id) { + const config: Query.SelectRowsOptions = { + schemaName: 'study', + queryName: 'adoptionsOngoing', + filterArray: [Filter.create('Id', newRow.id, Filter.Types.EQUAL)], + columns: ['dam'] + }; + + labkeyActionSelectWithPromise(config).then(result => { + if (result.rows.length !== 0) { + const damId = result.rows[0].dam; + setAnimals(prev => prev.map(row => (row.objectid === newRow.objectid ? { ...newRow, dam: damId } : row))); + } + }).catch(err => { + console.error('Error fetching ongoing adoption for dam ID', err); + }); + } + + setAnimals(prev => prev.map(row => (row.objectid === newRow.objectid ? newRow : row))); + return newRow; + }, [prevForm]); + + const handleCellClick = useCallback((params: GridCellParams) => { + if (params.isEditable && params.cellMode === 'view') { + apiRef.current.startCellEditMode({ id: params.id, field: params.field }); + } + }, [apiRef]); + + const adoptionStatusOptions = useMemo(() => { + return Object.keys(AdoptionStatus) + .filter(key => isNaN(Number(key))) + .map(key => ({ + label: key, + value: AdoptionStatus[key as keyof typeof AdoptionStatus] + })); + }, []); + + const adoptionResultOptions = useMemo(() => { + return Object.keys(AdoptionResult) + .filter(key => isNaN(Number(key))) + .map(key => ({ + label: key, + value: AdoptionResult[key as keyof typeof AdoptionResult] + })); + }, []); + + const centerAnimalsOptions = useMemo(() => { + return centerAnimals.map(animalId => ({ + label: animalId, + value: animalId + })); + }, [centerAnimals]); + + const columns: GridColDef[] = useMemo(() => [ + { + field: 'id', + headerName: 'Infant Id', + minWidth: 100, + flex: 1, + display: 'flex', + editable: true, + renderEditCell: (params) => ( + + ), + valueFormatter: (value) => { + if (value === undefined || value === null) return ''; + return value; + } + }, + { + field: 'date', + headerName: 'Date', + ...dateTimeColumnType, + minWidth: 180, + flex: 1, + display: 'flex', + editable: true + }, + { + field: 'dam', + headerName: 'Foster Dam', + minWidth: 120, + flex: 1, + display: 'flex', + editable: true, + renderEditCell: (params) => ( + + ), + valueFormatter: (value) => { + if (value === undefined || value === null) return ''; + return value; + } + }, + { + field: 'sire', + headerName: 'Foster Sire', + minWidth: 120, + flex: 1, + display: 'flex', + editable: true, + renderEditCell: (params) => ( + + ), + valueFormatter: (value) => { + if (value === undefined || value === null) return ''; + return value; + } + }, + { + field: 'type', + headerName: 'Type', + minWidth: 120, + flex: 1, + display: 'flex', + editable: true, + renderEditCell: (params) => ( + + ), + valueFormatter: (value) => { + const val = (value as any)?.value !== undefined ? (value as any).value : value; + if (val === undefined || val === null) return ''; + return AdoptionStatus[val as number] || ''; + } + }, + { + field: 'result', + headerName: 'Result', + minWidth: 120, + flex: 1, + display: 'flex', + editable: true, + renderEditCell: (params) => { + if(params.row.type?.value !== AdoptionStatus.End){ + return; + } + return( + + ); + }, + valueFormatter: (value) => { + const val = (value as any)?.value !== undefined ? (value as any).value : value; + if (val === undefined || val === null) return ''; + return AdoptionResult[val as number] || ''; + }, + isCellEditable: (params) => params.row.type?.value === AdoptionStatus.End + }, + { + field: 'actions', + headerName: 'Actions', + sortable: false, + minWidth: 80, + display: 'flex', + renderCell: (params: GridRenderCellParams) => ( + handleDeleteRow(params.row.objectid)} color="error"> + + + ), + } + ], [adoptionStatusOptions, adoptionResultOptions, centerAnimalsOptions, handleDeleteRow]); + + const isRowValid = useCallback((row: AdoptionData) => { + const { id, date, type, result } = row; + const isTypeEnd = type?.value === AdoptionStatus.End; + + return ( + id !== '' && id !== null && id !== undefined && + date !== null && date !== undefined && + type !== null && type !== undefined && + (!isTypeEnd || (result !== null && result !== undefined)) + ); + }, []); + + const isFormValid = useMemo(() => { + return animals.length > 0 && animals.every(isRowValid); + }, [animals, isRowValid]); + + const getCellClassName = useCallback((params: GridCellParams) => { + const { field, value, row } = params; + + const isRequired = + field === 'id' || + field === 'date' || + field === 'type' || + (field === 'result' && row.type?.value === AdoptionStatus.End); + + if (isRequired && (value === null || value === undefined || value === '')) { + return 'required-field-error'; + } + + return ''; + }, []); + + const handleSubmit = useCallback(() => { + console.log('Submitting form...', animals); + startAdoptionSubmission(animals).then((res) => { + if(res.success){ + // Housing transfer complete + window.location.href = ActionURL.buildURL( + "query", + 'executeQuery', + ActionURL.getContainer(), + {schemaName: "study", queryName: "adoptions"}); + }else{ + // If this happens, the issue is likely related to a faulty submission in the java portion that didn't throw + // an error correctly. Otherwise, it would have gotten caught in the catch below. + setErrorMsg(["Unknown Error Occurred"]); + } + setIsSaving(false); + }).catch(err => { + if(err.errors){ + setErrorMsg(err.errors.map(e => e.msg)); + }else{ + setErrorMsg(err); + } + setIsSaving(false); + }); + }, [animals]); + + return ( + + + {!prevForm && + + + + } + + + row.objectid} + getRowHeight={() => 'auto'} + disableRowSelectionOnClick + autosizeOptions={autoSizeOptions} + columnVisibilityModel={{ + actions: !prevForm, + }} + autosizeOnMount + sx={{ + '& .required-field-error': { + backgroundColor: '#ffebee', // Light red background + '&:hover': { + backgroundColor: '#ffcdd2', + }, + }, + '& .MuiDataGrid-cell': { + display: 'flex', + alignItems: 'center', + padding: '8px', + whiteSpace: 'normal', + wordBreak: 'break-word', + }, + '& .MuiDataGrid-cellContent': { + width: '100%', + display: 'flex', + alignItems: 'center', + }, + '& .MuiInputBase-root': { + height: 'auto', + minHeight: '100%', + }, + '& .MuiOutlinedInput-root': { + height: 'auto', + }, + '& .MuiAutocomplete-root': { + width: '100%', + }, + '& .MuiTextField-root': { + width: '100%', + }, + '& .MuiDateTimePicker': { + height: '100%', + }, + '& .MuiTablePagination-selectLabel, & .MuiTablePagination-displayedRows': { + margin: 0, + }, + }} + /> + + {animals.length > 0 && ( +
+ +
+ )} + {errorMsg.length > 0 && } +
+ ); +}; + diff --git a/CageUI/src/client/entryPoints.js b/CageUI/src/client/entryPoints.js index 59fa2c8f2..6e27e447f 100644 --- a/CageUI/src/client/entryPoints.js +++ b/CageUI/src/client/entryPoints.js @@ -40,6 +40,15 @@ module.exports = { 'org.labkey.api.security.permissions.ReadPermission', ], path: './src/client/pages/updateRackStatus' + }, + { + name: "adoptionDataEntry", + title: "Adoption Form", + permissionClasses: [ + 'org.labkey.api.security.permissions.ReadPermission', + 'org.labkey.cageui.security.permissions.CageUIAdoptionsPermission' + ], + path: './src/client/pages/adoptionDataEntry' } ] }; diff --git a/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx b/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx new file mode 100644 index 000000000..d232d6326 --- /dev/null +++ b/CageUI/src/client/pages/adoptionDataEntry/AdoptionDataEntry.tsx @@ -0,0 +1,85 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { FC, useEffect, useState } from 'react'; +import '../../cageui.scss'; +import { labkeyActionSelectWithPromise } from '../../api/labkeyActions'; +import { AdoptionForm } from '../../components/adoptionDataEntry/AdoptionForm'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import { ActionURL, Filter, Query } from '@labkey/api'; +import { AdoptionData, AdoptionResult, AdoptionStatus } from '../../types/adoptionFormTypes'; +import dayjs from 'dayjs'; + + +export const AdoptionDataEntry: FC = () => { + const prevFormObjId = ActionURL.getParameter('objectid'); + const [prevFormData, setPrevFormData] = useState(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + if(!prevFormObjId) { + setIsLoading(false); + return; + } + const config: Query.SelectRowsOptions = { + schemaName: 'study', + queryName: 'adoptions', + columns: ['Id', 'objectid', 'date', 'dam', 'sire', 'result/value', 'result/title', 'type/value', 'type/title'], + filterArray: [Filter.create('objectid', prevFormObjId, Filter.Types.EQUAL)] + }; + + labkeyActionSelectWithPromise(config).then(result => { + if (result.rowCount === 1) { + const res = result.rows[0]; + const adoptionData: AdoptionData = { + dam: res.dam, + sire: res.sire, + date: dayjs(res.date), + id: res.Id, + objectid: res.objectid, + result: { + label: res['result/title'] as keyof typeof AdoptionResult, + value: parseInt(res['result/value']) + }, + type: { + label: res['type/title'] as keyof typeof AdoptionStatus, + value: parseInt(res['type/value']) + } + }; + setPrevFormData(adoptionData); + setIsLoading(false); + }else{ + setIsLoading(false); + } + }).catch(err => { + console.error('Error fetching alive at center animals', err); + setIsLoading(false); + }); + }, []); + + return( + !isLoading && + +
+ +
+
+ ) +}; \ No newline at end of file diff --git a/CageUI/src/client/pages/adoptionDataEntry/app.tsx b/CageUI/src/client/pages/adoptionDataEntry/app.tsx new file mode 100644 index 000000000..ce896bd1d --- /dev/null +++ b/CageUI/src/client/pages/adoptionDataEntry/app.tsx @@ -0,0 +1,30 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { createRoot } from 'react-dom/client'; +import { AdoptionDataEntry } from './AdoptionDataEntry'; + + +// Need to wait for container element to be available in labkey wrapper before render +window.addEventListener('DOMContentLoaded', (event) => { + + createRoot(document.getElementById('app')).render( + + ); +}); \ No newline at end of file diff --git a/CageUI/src/client/pages/adoptionDataEntry/dev.tsx b/CageUI/src/client/pages/adoptionDataEntry/dev.tsx new file mode 100644 index 000000000..b449ef975 --- /dev/null +++ b/CageUI/src/client/pages/adoptionDataEntry/dev.tsx @@ -0,0 +1,29 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import * as React from 'react'; +import { createRoot } from 'react-dom/client'; +import { AdoptionDataEntry } from './AdoptionDataEntry'; + +const render = () => { + createRoot(document.getElementById('app')).render( + + ); +}; + +render(); \ No newline at end of file diff --git a/CageUI/src/client/types/adoptionFormTypes.ts b/CageUI/src/client/types/adoptionFormTypes.ts new file mode 100644 index 000000000..914311ed6 --- /dev/null +++ b/CageUI/src/client/types/adoptionFormTypes.ts @@ -0,0 +1,41 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +import { Dayjs } from 'dayjs'; + +export interface AdoptionData { + objectid: string; + id: string; + date: Dayjs; + dam: string | null; + sire: string | null; + type: {label: keyof typeof AdoptionStatus, value: AdoptionStatus}; + result?: {label: keyof typeof AdoptionResult, value: AdoptionResult}; +} + +export enum AdoptionStatus { + Start, + End, + Pause, + Resume +} + +export enum AdoptionResult { + Success, + Failure +} \ No newline at end of file diff --git a/CageUI/src/org/labkey/cageui/CageUIController.java b/CageUI/src/org/labkey/cageui/CageUIController.java index 88a1deb5e..97d5ea89a 100644 --- a/CageUI/src/org/labkey/cageui/CageUIController.java +++ b/CageUI/src/org/labkey/cageui/CageUIController.java @@ -54,10 +54,16 @@ import org.labkey.api.view.HtmlView; import org.labkey.api.view.JspView; import org.labkey.api.view.NavTree; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.view.ActionURL; +import org.labkey.api.view.UnauthorizedException; +import org.labkey.cageui.action.AdoptionDataForm; import org.labkey.cageui.action.BundledForms; import org.labkey.cageui.action.CagesForm; import org.labkey.cageui.action.RackTypesForm; import org.labkey.cageui.action.RacksForm; +import org.labkey.cageui.model.AdoptionData; +import org.labkey.cageui.model.AdoptionType; import org.labkey.cageui.model.Cage; import org.labkey.cageui.model.Manufacturer; import org.labkey.cageui.model.ModData; @@ -69,6 +75,8 @@ import org.labkey.cageui.model.RackTypes; import org.labkey.cageui.model.Room; import org.labkey.cageui.model.SessionLog; +import org.labkey.cageui.security.permissions.CageUIAdoptionsPermission; +import org.labkey.cageui.security.permissions.CageUIAnimalEditorPermission; import org.labkey.cageui.security.permissions.CageUILayoutEditorAccessPermission; import org.labkey.cageui.security.permissions.CageUIModificationEditorPermission; import org.labkey.cageui.security.permissions.CageUIRoomCreatorPermission; @@ -80,15 +88,20 @@ import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; public class CageUIController extends SpringActionController { + private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(CageUIController.class); public static final String NAME = "cageui"; @@ -115,6 +128,196 @@ public void addNavTrail(NavTree root) } } + @RequiresPermission(CageUIAdoptionsPermission.class) + public static class SubmitAdoptionFormAction extends MutatingApiAction + { + ArrayList _adoptionData; + + public ArrayList getAdoptionData() + { + return _adoptionData; + } + + public void setAdoptionData(ArrayList adoptionData) + { + _adoptionData = adoptionData; + } + + + @Override + public void validateForm(SimpleApiJsonForm form, Errors errors) + { + JSONObject json = form.getJsonObject(); + if (json == null) + { + errors.reject(ERROR_MSG, "Missing json parameter."); + return; + } + + JSONArray jsonTransferData = json.getJSONArray("adoptionData"); + ObjectMapper mapper = JsonUtil.createDefaultMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + try + { + TypeReference> typeRef = new TypeReference>() + { + }; + ArrayList adoptionDataList = mapper.readValue(jsonTransferData.toString(), typeRef); + setAdoptionData(adoptionDataList); + }catch (JsonProcessingException e) + { + errors.reject(ERROR_MSG, e.getMessage()); + } + + // Validation on each individual row + for (AdoptionData row : getAdoptionData()){ + + if(row.getDam() == null && row.getSire() == null){ + errors.reject(ERROR_MSG, "Animal " + row.getId() + " must have a Sire or Dam."); + } + if(row.getSire() != null && row.getId().equals(row.getSire())){ + errors.reject(ERROR_MSG, "Infant cannot be the Sire."); + } + if(row.getDam() != null && row.getId().equals(row.getDam())) { + errors.reject(ERROR_MSG, "Infant cannot be the Dam."); + } + if(row.getSire() != null && row.getDam() != null && row.getSire().equals(row.getDam())){ + errors.reject(ERROR_MSG, "Sire and Dam cannot be the same."); + } + } + + + + Map> dataById = getAdoptionData().stream() + .collect(Collectors.groupingBy(AdoptionData::getId)); + + // validation cross referencing other rows + for (Map.Entry> entry : dataById.entrySet()) + { + String id = entry.getKey(); + + List newAdoptions = entry.getValue(); + newAdoptions.sort(Comparator.comparing(AdoptionData::getDate)); + + List existingAdoptions = CageUIManager.getAdoptionsForId(id, getUser(), getContainer()); + existingAdoptions.sort(Comparator.comparing(AdoptionDataForm::getDate)); + + AdoptionDataForm lastAdoption = existingAdoptions.isEmpty() ? null : existingAdoptions.get(existingAdoptions.size() - 1); + String expectedDam = lastAdoption != null ? lastAdoption.getDam() : null; + + for (AdoptionData newAdoption : newAdoptions) + { + AdoptionType newType = AdoptionType.fromInt(newAdoption.getType().getValue()); + AdoptionType lastType = lastAdoption != null ? AdoptionType.fromInt(lastAdoption.getType()) : null; + + // Type validation + if (newType == AdoptionType.START) + { + if (lastType != null && lastType != AdoptionType.END) + { + errors.reject(ERROR_MSG, "Animal " + id + " already has an ongoing adoption. Must end previous adoption before starting a new one."); + } + } + else if (newType == AdoptionType.PAUSE) + { + if (lastType != AdoptionType.START && lastType != AdoptionType.RESUME) + { + errors.reject(ERROR_MSG, "Animal " + id + " can only be paused if it is currently started or resumed."); + } + } + else if (newType == AdoptionType.RESUME) + { + if (lastType != AdoptionType.PAUSE) + { + errors.reject(ERROR_MSG, "Animal " + id + " can only be resumed if it is currently paused."); + } + } + else if (newType == AdoptionType.END) + { + if (lastType == AdoptionType.END) + { + errors.reject(ERROR_MSG, "Animal " + id + " adoption has already ended."); + } + } + + // Dam validation + if (expectedDam == null) + { + expectedDam = newAdoption.getDam(); + } + else if (!expectedDam.equals(newAdoption.getDam())) + { + errors.reject(ERROR_MSG, "Dam ID for animal " + id + " must be consistent across adoptions. Expected: " + expectedDam + ", Found: " + newAdoption.getDam()); + } + + // Date validation + if (lastAdoption != null && !newAdoption.getDate().after(lastAdoption.getDate())) + { + errors.reject(ERROR_MSG, "Date for animal " + id + " must be after the previous adoption entry's date."); + } + + // Update last adoption for next iteration + AdoptionDataForm currentAsForm = new AdoptionDataForm(); + currentAsForm.setId(newAdoption.getId()); + currentAsForm.setType(newAdoption.getType().getValue()); + currentAsForm.setDate(newAdoption.getDate()); + currentAsForm.setDam(newAdoption.getDam()); + lastAdoption = currentAsForm; + } + } + } + + @Override + public Object execute(SimpleApiJsonForm form, BindException errors) throws Exception + { + BatchValidationException batchErrors = new BatchValidationException(); + ApiSimpleResponse response = new ApiSimpleResponse(); + UserSchema studySchema = QueryService.get().getUserSchema(getUser(), getContainer(), "study"); + ArrayList finalForm = new ArrayList(); + + for(AdoptionData row : getAdoptionData()){ + AdoptionDataForm finalRow = new AdoptionDataForm(); + finalRow.setId(row.getId()); + finalRow.setDate(row.getDate()); + finalRow.setDam(row.getDam()); + finalRow.setType(row.getType().getValue()); + if(row.getResult() != null){ + finalRow.setResult(row.getResult().getValue()); + } + finalForm.add(finalRow); + } + + TableInfo studyAdoptionsTable = studySchema.getTable("adoptions"); + QueryUpdateService studyAdoptionsQus = studyAdoptionsTable.getUpdateService(); + if (studyAdoptionsQus == null) + { + throw new IllegalStateException(studyAdoptionsTable.getName() + " query update service"); + } + + try (DbScope.Transaction tx = CageUISchema.getInstance().getSchema().getScope().ensureTransaction()) + { + List> adoptionMapList = CageUIManager.get().convertToMapList(finalForm); + + studyAdoptionsQus.insertRows(getUser(), getContainer(), adoptionMapList, batchErrors, null, null); + + if (batchErrors.hasErrors()) + { + response.put("success", false); + response.put("errors", batchErrors); + return response; + } + tx.commit(); + response.put("success", true); + } + catch (QueryUpdateServiceException | BatchValidationException | DuplicateKeyException | RuntimeException | + SQLException e) + { + throw new ValidationException(e.getMessage()); + } + return response; + } + } + @RequiresPermission(CageUIRoomModifierPermission.class) public static class UpdateRackConditionStatusAction extends MutatingApiAction { diff --git a/CageUI/src/org/labkey/cageui/CageUIManager.java b/CageUI/src/org/labkey/cageui/CageUIManager.java index bb4d80cd5..020d59ecd 100644 --- a/CageUI/src/org/labkey/cageui/CageUIManager.java +++ b/CageUI/src/org/labkey/cageui/CageUIManager.java @@ -42,6 +42,7 @@ import org.labkey.api.query.ValidationException; import org.labkey.api.security.User; import org.labkey.api.util.JsonUtil; +import org.labkey.cageui.action.AdoptionDataForm; import org.labkey.cageui.action.AllHistoryForm; import org.labkey.cageui.action.BundledForms; import org.labkey.cageui.action.CageModificationHistoryForm; @@ -510,6 +511,24 @@ public static ArrayList getRacksInRoom(String room) return racksForm; } + public static ArrayList getAdoptionsForId(String id, User user, Container container) + { + + //TableInfo table = getRealTableForDataset(container, "adoptions"); + UserSchema studySchema = QueryService.get().getUserSchema(user, container, "study"); + TableInfo table = studySchema.getTable("adoptions"); + + SimpleFilter filter = new SimpleFilter(); + filter.addCondition(FieldKey.fromString("Id"), id, CompareType.EQUAL); + TableSelector selector = new TableSelector(table, filter, null); + + ObjectMapper mapper = JsonUtil.createDefaultMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + TypeReference> typeRef = new TypeReference>() {}; + ArrayList form = mapper.convertValue(selector.getMapArray(), typeRef); + return form; + } + // ends an all history row public static AllHistoryForm endPreviousAllHistory(String room, Date endDate) diff --git a/CageUI/src/org/labkey/cageui/action/AdoptionDataForm.java b/CageUI/src/org/labkey/cageui/action/AdoptionDataForm.java new file mode 100644 index 000000000..4bce301cd --- /dev/null +++ b/CageUI/src/org/labkey/cageui/action/AdoptionDataForm.java @@ -0,0 +1,85 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.action; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.util.Date; + +public class AdoptionDataForm +{ + @JsonAlias({"id", "Id"}) + private String id; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS") + private Date date; + private String dam; + private Integer type; + private Integer result; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public Date getDate() + { + return date; + } + + public void setDate(Date date) + { + this.date = date; + } + + public String getDam() + { + return dam; + } + + public void setDam(String dam) + { + this.dam = dam; + } + + public Integer getType() + { + return type; + } + + public void setType(Integer type) + { + this.type = type; + } + + public Integer getResult() + { + return result; + } + + public void setResult(Integer result) + { + this.result = result; + } +} diff --git a/CageUI/src/org/labkey/cageui/model/AdoptionData.java b/CageUI/src/org/labkey/cageui/model/AdoptionData.java new file mode 100644 index 000000000..87a92b309 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/model/AdoptionData.java @@ -0,0 +1,105 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.model; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.util.Date; + +public class AdoptionData +{ + private String id; + private String objectid; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + private Date date; + private String dam; + private String sire; + private Option type; + private Option result; + + public String getId() + { + return this.id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getObjectid() + { + return this.objectid; + } + + public void setObjectid(String objectid) + { + this.objectid = objectid; + } + + public Date getDate() + { + return this.date; + } + + public void setDate(Date date) + { + this.date = date; + } + + public String getDam() + { + return this.dam; + } + + public void setDam(String dam) + { + this.dam = dam; + } + + public Option getType() + { + return this.type; + } + + public void setType(Option type) + { + this.type = type; + } + + public Option getResult() + { + return this.result; + } + + public void setResult(Option result) + { + this.result = result; + } + + public String getSire() + { + return sire; + } + + public void setSire(String sire) + { + this.sire = sire; + } +} diff --git a/CageUI/src/org/labkey/cageui/model/AdoptionType.java b/CageUI/src/org/labkey/cageui/model/AdoptionType.java new file mode 100644 index 000000000..9bf4163bf --- /dev/null +++ b/CageUI/src/org/labkey/cageui/model/AdoptionType.java @@ -0,0 +1,51 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.model; + +public enum AdoptionType +{ + START(0), + END(1), + PAUSE(2), + RESUME(3); + + private final int _value; + + AdoptionType(int value) + { + _value = value; + } + + public int getValue() + { + return _value; + } + + public static AdoptionType fromInt(int value) + { + for (AdoptionType type : AdoptionType.values()) + { + if (type.getValue() == value) + { + return type; + } + } + return null; + } +} diff --git a/CageUI/src/org/labkey/cageui/model/Option.java b/CageUI/src/org/labkey/cageui/model/Option.java new file mode 100644 index 000000000..aebc4768a --- /dev/null +++ b/CageUI/src/org/labkey/cageui/model/Option.java @@ -0,0 +1,78 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.model; + +import java.io.Serializable; +import java.util.Objects; + +public class Option implements Serializable { + private String label; + private T value; + + // Constructor + public Option() {} + + public Option(String label, T value) { + this.label = label; + this.value = value; + } + + // Getters and setters + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public T getValue() { + return value; + } + + public void setValue(T value) { + this.value = value; + } + + @Override + public String toString() { + return "Option{" + + "label='" + label + '\'' + + ", value=" + value + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Option that = (Option) o; + + if (!Objects.equals(label, that.label)) return false; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + int result = label != null ? label.hashCode() : 0; + result = 31 * result + (value != null ? value.hashCode() : 0); + return result; + } +} \ No newline at end of file diff --git a/CageUI/src/org/labkey/cageui/security/permissions/CageUIAdoptionsPermission.java b/CageUI/src/org/labkey/cageui/security/permissions/CageUIAdoptionsPermission.java new file mode 100644 index 000000000..e61a826a7 --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/permissions/CageUIAdoptionsPermission.java @@ -0,0 +1,30 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.permissions; + +import org.labkey.api.security.permissions.AbstractPermission; + +public class CageUIAdoptionsPermission extends AbstractPermission +{ + public CageUIAdoptionsPermission() + { + super("Cage UI Adoptions", + "This permission allows the user access to adoptions table and submitted/editing data"); + } +} \ No newline at end of file diff --git a/CageUI/src/org/labkey/cageui/security/roles/CageUIAdoptionsRole.java b/CageUI/src/org/labkey/cageui/security/roles/CageUIAdoptionsRole.java new file mode 100644 index 000000000..1ee1683ca --- /dev/null +++ b/CageUI/src/org/labkey/cageui/security/roles/CageUIAdoptionsRole.java @@ -0,0 +1,41 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.labkey.cageui.security.roles; + +import org.labkey.api.security.permissions.Permission; +import org.labkey.api.security.roles.AbstractRole; +import org.labkey.cageui.CageUIModule; +import org.labkey.cageui.security.permissions.CageUIAdoptionsPermission; + +public class CageUIAdoptionsRole extends AbstractRole +{ + + public CageUIAdoptionsRole() + { + this("Cage UI Adoptions", + "Adoptions role for Cage UI", + CageUIAdoptionsPermission.class + ); + } + + protected CageUIAdoptionsRole(String name, String description, Class... perms) + { + super(name, description, CageUIModule.class, perms); + } +} \ No newline at end of file diff --git a/WNPRC_EHR/resources/queries/study/adoptions.query.xml b/WNPRC_EHR/resources/queries/study/adoptions.query.xml new file mode 100644 index 000000000..2ee43afab --- /dev/null +++ b/WNPRC_EHR/resources/queries/study/adoptions.query.xml @@ -0,0 +1,64 @@ + + + + + + + + /cageui/adoptionDataEntry.view + + /cageui/adoptionDataEntry.view?objectid=${objectid} + + + + Infant Id + + + yyyy-MM-dd HH:mm + Date + + + + ehr_lookups + adoption_status + value + title + + + + + ehr_lookups + adoption_results + value + title + + + + Foster Dam + /ehr/participantView.view?participantId=${dam} + + + Foster Sire + /ehr/participantView.view?participantId=${sire} + + +
+
+
+
\ No newline at end of file diff --git a/WNPRC_EHR/resources/queries/study/adoptions/.qview.xml b/WNPRC_EHR/resources/queries/study/adoptions/.qview.xml new file mode 100644 index 000000000..d17db41a9 --- /dev/null +++ b/WNPRC_EHR/resources/queries/study/adoptions/.qview.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/WNPRC_EHR/resources/queries/study/adoptionsOngoing.sql b/WNPRC_EHR/resources/queries/study/adoptionsOngoing.sql new file mode 100644 index 000000000..de5c5eda5 --- /dev/null +++ b/WNPRC_EHR/resources/queries/study/adoptionsOngoing.sql @@ -0,0 +1,33 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +SELECT + a.Id, + a.date, + a.type, + a.result, + a.dam +FROM study.adoptions a +WHERE (a.type = '0') -- start + AND NOT EXISTS ( + SELECT 1 + FROM study.adoptions a2 + WHERE a.Id = a2.Id + AND a2.date > a.date + AND (a2.type = '1') -- end + ) diff --git a/WNPRC_EHR/resources/queries/study/adoptionsSuccess.sql b/WNPRC_EHR/resources/queries/study/adoptionsSuccess.sql new file mode 100644 index 000000000..1ca424e84 --- /dev/null +++ b/WNPRC_EHR/resources/queries/study/adoptionsSuccess.sql @@ -0,0 +1,40 @@ +/* + * + * * Copyright (c) 2026 Board of Regents of the University of Wisconsin System + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +SELECT + a_start.dam, + a_start.Id, + a_start.date AS start_date, + a_end.date AS end_date, + timestampdiff('SQL_TSI_DAY', a_start.date, a_end.date) AS days_adopted, + (SELECT COUNT(*) FROM study.adoptions a_sub WHERE a_sub.dam = a_start.dam AND a_sub.type = '1' AND a_sub.result = '0' AND a_sub.date <= a_end.date) AS total_adoptions_for_dam +FROM study.adoptions a_start +JOIN study.adoptions a_end ON a_start.Id = a_end.Id AND a_start.dam = a_end.dam +WHERE a_start.type = '0' + AND a_end.type = '1' + AND a_end.result = '0' + AND a_end.date = ( + SELECT MIN(a2.date) + FROM study.adoptions a2 + WHERE a2.Id = a_start.Id + AND a2.dam = a_start.dam + AND a2.type = '1' + AND a2.date > a_start.date + ) + +