diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml new file mode 100644 index 00000000..f0ed48e4 --- /dev/null +++ b/.github/workflows/commit-lint.yml @@ -0,0 +1,34 @@ +name: Commit Compliance + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + commitlint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code with submodule + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + submodules: true + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install dependencies + run: | + npm ci --legacy-peer-deps + npm i --legacy-peer-deps + + - name: Run commitlint on PR + run: | + npx commitlint --from ${{ github.event.pull_request.base.sha }} \ + --to ${{ github.event.pull_request.head.sha }} \ + --verbose diff --git a/.gitignore b/.gitignore index a4786a90..5a1e6d24 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ mvnw.cmd .mvn /logs/ src/main/environment/common_local.properties +node_modules diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 00000000..34eed8b2 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +npx --no -- commitlint --edit $1 \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..d0a77842 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f99697fa..417c00e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,8 @@ FROM eclipse-temurin:17-jre WORKDIR /app +ENV TZ=Asia/Kolkata + # Copy the built WAR file from the build stage COPY --from=build /app/target/*.war app.war diff --git a/README.md b/README.md index 560f69a7..5c992214 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,49 @@ This repository comprises of all APIs related to FLW Mobile App. Flw-API is a microservice which is used for the creation and management of beneficiaries related data +## Setting Up Commit Hooks + +This project uses Git hooks to enforce consistent code quality and commit message standards. Even though this is a Java project, the hooks are powered by Node.js. Follow these steps to set up the hooks locally: + +### Prerequisites +- Node.js (v14 or later) +- npm (comes with Node.js) + +### Setup Steps + +1. **Install Node.js and npm** + - Download and install from [nodejs.org](https://nodejs.org/) + - Verify installation with: + ``` + node --version + npm --version + ``` +2. **Install dependencies** + - From the project root directory, run: + ``` + npm ci + ``` + - This will install all required dependencies including Husky and commitlint +3. **Verify hooks installation** + - The hooks should be automatically installed by Husky + - You can verify by checking if the `.husky` directory contains executable hooks +### Commit Message Convention +This project follows a specific commit message format: +- Format: `type(scope): subject` +- Example: `feat(login): add remember me functionality` +Types include: +- `feat`: A new feature +- `fix`: A bug fix +- `docs`: Documentation changes +- `style`: Code style changes (formatting, etc.) +- `refactor`: Code changes that neither fix bugs nor add features +- `perf`: Performance improvements +- `test`: Adding or fixing tests +- `build`: Changes to build process or tools +- `ci`: Changes to CI configuration +- `chore`: Other changes (e.g., maintenance tasks, dependencies) +Your commit messages will be automatically validated when you commit, ensuring project consistency. + ## Filing Issues If you encounter any issues, bugs, or have feature requests, please file them in the [main AMRIT repository](https://github.com/PSMRI/AMRIT/issues). Centralizing all feedback helps us streamline improvements and address concerns efficiently. diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 00000000..e65d7e68 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,36 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'body-leading-blank': [1, 'always'], + 'body-max-line-length': [2, 'always', 100], + 'footer-leading-blank': [1, 'always'], + 'footer-max-line-length': [2, 'always', 100], + 'header-max-length': [2, 'always', 100], + 'subject-case': [ + 2, + 'never', + ['sentence-case', 'start-case', 'pascal-case', 'upper-case'], + ], + 'subject-empty': [2, 'never'], + 'subject-full-stop': [2, 'never', '.'], + 'type-case': [2, 'always', 'lower-case'], + 'type-empty': [2, 'never'], + 'type-enum': [ + 2, + 'always', + [ + 'build', + 'chore', + 'ci', + 'docs', + 'feat', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test', + ], + ], + } +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..d11ee7ec --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3433 @@ +{ + "name": "flw-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "flw-api", + "version": "1.0.0", + "devDependencies": { + "@commitlint/cli": "^19.8.0", + "@commitlint/config-conventional": "^19.8.0", + "commitizen": "^4.3.1", + "cz-conventional-changelog": "^3.3.0", + "husky": "^9.1.7", + "lint-staged": "^15.5.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@commitlint/cli": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.8.1.tgz", + "integrity": "sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/format": "^19.8.1", + "@commitlint/lint": "^19.8.1", + "@commitlint/load": "^19.8.1", + "@commitlint/read": "^19.8.1", + "@commitlint/types": "^19.8.1", + "tinyexec": "^1.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-19.8.1.tgz", + "integrity": "sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "conventional-changelog-conventionalcommits": "^7.0.2" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-19.8.1.tgz", + "integrity": "sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/ensure": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-19.8.1.tgz", + "integrity": "sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-19.8.1.tgz", + "integrity": "sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/format": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-19.8.1.tgz", + "integrity": "sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-19.8.1.tgz", + "integrity": "sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/lint": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-19.8.1.tgz", + "integrity": "sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/is-ignored": "^19.8.1", + "@commitlint/parse": "^19.8.1", + "@commitlint/rules": "^19.8.1", + "@commitlint/types": "^19.8.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/load": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-19.8.1.tgz", + "integrity": "sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^19.8.1", + "@commitlint/execute-rule": "^19.8.1", + "@commitlint/resolve-extends": "^19.8.1", + "@commitlint/types": "^19.8.1", + "chalk": "^5.3.0", + "cosmiconfig": "^9.0.0", + "cosmiconfig-typescript-loader": "^6.1.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/message": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-19.8.1.tgz", + "integrity": "sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/parse": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-19.8.1.tgz", + "integrity": "sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "conventional-changelog-angular": "^7.0.0", + "conventional-commits-parser": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/read": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-19.8.1.tgz", + "integrity": "sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/top-level": "^19.8.1", + "@commitlint/types": "^19.8.1", + "git-raw-commits": "^4.0.0", + "minimist": "^1.2.8", + "tinyexec": "^1.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-19.8.1.tgz", + "integrity": "sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^19.8.1", + "@commitlint/types": "^19.8.1", + "global-directory": "^4.0.1", + "import-meta-resolve": "^4.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/rules": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-19.8.1.tgz", + "integrity": "sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/ensure": "^19.8.1", + "@commitlint/message": "^19.8.1", + "@commitlint/to-lines": "^19.8.1", + "@commitlint/types": "^19.8.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-19.8.1.tgz", + "integrity": "sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-19.8.1.tgz", + "integrity": "sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^7.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/types": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-19.8.1.tgz", + "integrity": "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/conventional-commits-parser": "^5.0.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz", + "integrity": "sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/cachedir": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", + "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commitizen": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.3.1.tgz", + "integrity": "sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cachedir": "2.3.0", + "cz-conventional-changelog": "3.3.0", + "dedent": "0.7.0", + "detect-indent": "6.1.0", + "find-node-modules": "^2.1.2", + "find-root": "1.1.0", + "fs-extra": "9.1.0", + "glob": "7.2.3", + "inquirer": "8.2.5", + "is-utf8": "^0.2.1", + "lodash": "4.17.21", + "minimist": "1.2.7", + "strip-bom": "4.0.0", + "strip-json-comments": "3.1.1" + }, + "bin": { + "commitizen": "bin/commitizen", + "cz": "bin/git-cz", + "git-cz": "bin/git-cz" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/commitizen/node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-commit-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-3.0.0.tgz", + "integrity": "sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==", + "dev": true, + "license": "ISC" + }, + "node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.1.0.tgz", + "integrity": "sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jiti": "^2.4.1" + }, + "engines": { + "node": ">=v18" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=9", + "typescript": ">=5" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cz-conventional-changelog": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz", + "integrity": "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.4.1", + "commitizen": "^4.0.3", + "conventional-commit-types": "^3.0.0", + "lodash.map": "^4.5.1", + "longest": "^2.0.1", + "word-wrap": "^1.0.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@commitlint/load": ">6.1.1" + } + }, + "node_modules/cz-conventional-changelog/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cz-conventional-changelog/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dargs": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", + "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true, + "license": "MIT" + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-node-modules": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/find-node-modules/-/find-node-modules-2.1.3.tgz", + "integrity": "sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "findup-sync": "^4.0.0", + "merge": "^2.1.1" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true, + "license": "MIT" + }, + "node_modules/find-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/findup-sync": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", + "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^4.0.2", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-raw-commits": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", + "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dargs": "^8.0.0", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/inquirer": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-text-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "text-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", + "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lint-staged": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", + "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", + "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/longest": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz", + "integrity": "sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz", + "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/text-extensions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..bf8bff9d --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "flw-api", + "version": "1.0.0", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "prepare": "husky", + "commit": "cz" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + }, + "lint-staged": { + "src/**/*.{ts,js}": [ + "eslint --fix" + ], + "*.xml": [ + "xmllint --noout {{files}}" + ] + }, + "devDependencies": { + "@commitlint/cli": "^19.8.0", + "@commitlint/config-conventional": "^19.8.0", + "commitizen": "^4.3.1", + "cz-conventional-changelog": "^3.3.0", + "husky": "^9.1.7", + "lint-staged": "^15.5.1" + } +} \ No newline at end of file diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index 238d5f92..8d24ebe9 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -27,6 +27,9 @@ jwt.secret=@env.JWT_SECRET_KEY@ # Redis Config spring.redis.host=@env.REDIS_HOST@ +# Stop TB: when true, saves fail with an error if camp (vanID) is not configured +stoptb.enforce.vanid=@env.STOPTB_ENFORCE_VANID@ + cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@ #### SMS Configuration diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index 07309133..c021fc52 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -27,6 +27,9 @@ jwt.secret=${JWT_SECRET_KEY} # Redis Config spring.redis.host=${REDIS_HOST} +# Stop TB: when true, saves fail with an error if camp (vanID) is not configured +stoptb.enforce.vanid=${STOPTB_ENFORCE_VANID} + #### SMS Configuration send-sms=${SEND_SMS} source-address=${SMS_CONSENT_SOURCE_ADDRESS} diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index 3c679d09..7e8d43b0 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -24,11 +24,17 @@ springdoc.swagger-ui.enabled=true logging.path=logs/ logging.file.name=logs/flw-api.log -jwt.secret=my-32-character-ultra-secure-and-ultra-long-secret + +jwt.secret=eefa40497bb9b2a8491b85069aff62e7596521c6816e471c464181b52eac7670ff91394a + # Redis Config spring.redis.host=localhost +# Stop TB: when true, saves fail with an error if camp (vanID) is not configured +# instead of silently storing vanID=NULL (which then never gets synced to central) +stoptb.enforce.vanid=false + cors.allowed-origins=http://localhost:* source-address= diff --git a/src/main/environment/common_local.properties.bkp b/src/main/environment/common_local.properties.bkp new file mode 100644 index 00000000..b6463a03 --- /dev/null +++ b/src/main/environment/common_local.properties.bkp @@ -0,0 +1,49 @@ +spring.main.banner-mode=off +spring.data.jpa.repositories.enabled=true +#spring.jpa.hibernate.ddl-auto=none + +# Naming strategies +spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl +spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + +# The SQL dialect makes Hibernate generate better SQL for the chosen database +spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQLDialect + +#Below lines are added for security reasons + +spring.session.store-type=redis +spring.redis.password= +spring.redis.port=6379 + +## Below values are needed for extending the expiry time and extend expiry time. +iemr.extend.expiry.time=true +iemr.session.expiry.time=7200 +enableIPValidation=false + +#logging.pattern.level=DEBUG +logging.level.root=INFO +logging.level.org.springframework.web=INFO +logging.level.org.hibernate=INFO +logging.level.com.iemr=DEBUG +logging.level.org.springframework=INFO + +spring.datasource.tomcat.initial-size=5 +spring.datasource.tomcat.max-idle=15 +spring.datasource.tomcat.max-active=30 +spring.datasource.tomcat.min-idle=5 +spring.datasource.tomcat.min-evictable-idle-time-millis=15000 +spring.datasource.tomcat.remove-abandoned=true +spring.datasource.tomcat.logAbandoned=true +spring.datasource.continue-on-error=true +spring.datasource.tomcat.remove-abandoned-timeout=600 +door-to-door-page-size=10 + +#Get-HRP-Status +get-HRP-Status=ANC/getHRPStatus + +#Get Beneficiary ABHA +getHealthID=healthID/getBenhealthID + +# TM API URL +tm-url=https://amritdemo.piramalswasthya.org/tm-api +jwt.secret=eefa40497bb9b2a8491b85069aff62e7596521c6816e471c464181b52eac7670ff91394a diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormController.java b/src/main/java/com/iemr/flw/controller/DynamicFormController.java new file mode 100644 index 00000000..59295cbf --- /dev/null +++ b/src/main/java/com/iemr/flw/controller/DynamicFormController.java @@ -0,0 +1,100 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.controller; + +import com.iemr.flw.dto.iemr.DynamicFormDTO; +import com.iemr.flw.service.DynamicFormDefinitionService; +import com.iemr.flw.utils.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST controller for dynamic form definition management. + * All write operations use POST; all read operations use GET. + * Exceptions propagate to GlobalExceptionHandler — no local try-catch. + * + * @author Piramal Swasthya + */ +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(value = "/dynamicForm", headers = "Authorization") +public class DynamicFormController { + + private final DynamicFormDefinitionService formService; + + @Operation(summary = "Create a full dynamic form definition with sections, questions, options and validations") + @RequestMapping(value = "/createForm", method = RequestMethod.POST) + public ResponseEntity createForm(@RequestBody @Valid DynamicFormDTO dto) { + return ResponseEntity.ok(new ApiResponse(true, null, formService.createForm(dto))); + } + + @Operation(summary = "Replace form structure and bump version (unchanged sections reused)") + @RequestMapping(value = "/updateForm", method = RequestMethod.POST) + public ResponseEntity updateForm( + @RequestParam Long formId, + @RequestBody @Valid DynamicFormDTO dto) { + return ResponseEntity.ok(new ApiResponse(true, null, formService.updateForm(formId, dto))); + } + + @Operation(summary = "Get form definition — latest version or specific version if ?version= provided") + @RequestMapping(value = "/getDefinition", method = RequestMethod.GET) + public ResponseEntity getFormDefinition( + @RequestParam Long formId, + @RequestParam(required = false) Integer version) { + Object result = version != null + ? formService.getFormDefinitionByVersion(formId, version) + : formService.getFormDefinition(formId); + return ResponseEntity.ok(new ApiResponse(true, null, result)); + } + + @Operation(summary = "Get metadata list of all active forms") + @RequestMapping(value = "/getAllForms", method = RequestMethod.GET) + public ResponseEntity getAllForms() { + return ResponseEntity.ok(new ApiResponse(true, null, formService.getAllForms())); + } + + @Operation(summary = "Activate a form (isActive = true)") + @RequestMapping(value = "/activateForm", method = RequestMethod.POST) + public ResponseEntity activateForm( + @RequestParam Long formId) { + formService.activateForm(formId); + return ResponseEntity.ok(new ApiResponse(true, "Form activated", null)); + } + + @Operation(summary = "Deactivate a form (isActive = false)") + @RequestMapping(value = "/deactivateForm", method = RequestMethod.POST) + public ResponseEntity deactivateForm( + @RequestParam Long formId) { + formService.deactivateForm(formId); + return ResponseEntity.ok(new ApiResponse(true, "Form deactivated", null)); + } + +} diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java new file mode 100644 index 00000000..49d8e547 --- /dev/null +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -0,0 +1,110 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.controller; + +import com.iemr.flw.dto.iemr.FormResponseDTO; +import com.iemr.flw.dto.iemr.FormResponseRequest; +import com.iemr.flw.masterEnum.FormType; +import com.iemr.flw.service.DynamicFormResponseService; +import com.iemr.flw.utils.ApiResponse; +import com.iemr.flw.utils.exception.IEMRException; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * REST controller for saving and retrieving dynamic form responses (beneficiary answers). + * All exceptions propagate to GlobalExceptionHandler — no local try-catch. + * + * @author Piramal Swasthya + */ +@RequiredArgsConstructor +@RestController +@RequestMapping(value = "/dynamicForm/response", headers = "Authorization") +public class DynamicFormResponseController { + + private final DynamicFormResponseService responseService; + + @Operation(summary = "Submit PRE_SUBMIT section answers, status → SUBMITTED. " + + "Pass responseId in body to re-submit an existing SUBMITTED response.") + @RequestMapping(value = "/submit", method = RequestMethod.POST) + public ResponseEntity submitForm( + @Valid @RequestBody FormResponseRequest request) { + FormResponseDTO dto = responseService.submitForm(request); + return ResponseEntity.ok(new ApiResponse(true, "Form submitted successfully", dto)); + } + + @Operation(summary = "Save POST_SUBMIT section answers, status → COMPLETE") + @RequestMapping(value = "/complete", method = RequestMethod.POST) + public ResponseEntity completeForm( + @Valid @RequestBody FormResponseRequest request, + @RequestHeader("JwtToken") String jwtToken) throws IEMRException { + FormResponseDTO dto = responseService.completeForm(request, jwtToken); + return ResponseEntity.ok(new ApiResponse(true, "Form completed successfully", dto)); + } + + @Operation(summary = "Submit multiple form responses in one bulk transaction. " + + "All-or-nothing: any failure rolls back all. responseId is populated for every saved item.") + @RequestMapping(value = "/submitBulk", method = RequestMethod.POST) + public ResponseEntity submitBulk( + @Valid @RequestBody List requests, + @RequestHeader("JwtToken") String jwtToken) { + List saved = responseService.submitBulk(requests, jwtToken); + return ResponseEntity.ok( + new ApiResponse(true, "Bulk submit complete: " + saved.size() + " saved", saved)); + } + + @Operation(summary = "Get all form responses for a beneficiary filtered by form UUID") + @RequestMapping(value = "/getByBeneficiary", method = RequestMethod.GET) + public ResponseEntity getByBeneficiary( + @RequestParam Long beneficiaryId, + @RequestParam String formUuid) { + List dtos = + responseService.getResponsesByBeneficiary(beneficiaryId, formUuid); + return ResponseEntity.ok(new ApiResponse(true, "Responses fetched successfully", dtos)); + } + + @Operation(summary = "Get a single form response with all nested answers") + @RequestMapping(value = "/getById", method = RequestMethod.GET) + public ResponseEntity getById(@RequestParam Long responseId) { + FormResponseDTO dto = responseService.getResponseById(responseId); + return ResponseEntity.ok(new ApiResponse(true, "Response fetched successfully", dto)); + } + @Operation(summary = "Get beneficiary IDs that have a COMPLETE form response for the given form type, optionally filtered by village and/or provider service map") + @RequestMapping(value = "/getCompletedBeneficiaries", method = RequestMethod.GET) + public ResponseEntity getCompletedBeneficiaries( + @RequestParam FormType formType, + @RequestParam(required = false) Integer villageId, + @RequestParam(required = false) Integer providerServiceMapId) { + List beneficiaryIds = responseService.getCompletedBeneficiaries(formType, villageId, providerServiceMapId); + return ResponseEntity.ok(new ApiResponse(true, "Completed beneficiaries fetched successfully", beneficiaryIds)); + } +} diff --git a/src/main/java/com/iemr/flw/controller/GlobalExceptionHandler.java b/src/main/java/com/iemr/flw/controller/GlobalExceptionHandler.java new file mode 100644 index 00000000..8470b97f --- /dev/null +++ b/src/main/java/com/iemr/flw/controller/GlobalExceptionHandler.java @@ -0,0 +1,154 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.controller; + +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; +import com.iemr.flw.utils.ApiResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.util.NoSuchElementException; +import java.util.stream.Collectors; + +/** + * Converts all Spring MVC and application exceptions into a consistent ApiResponse envelope. + * + * @author Piramal Swasthya + */ +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** @Valid / @Validated failures — collect every field error into one readable message. */ + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + + String message = ex.getBindingResult().getFieldErrors().stream() + .map(fe -> fe.getField() + ": " + fe.getDefaultMessage()) + .collect(Collectors.joining(", ")); + + log.warn("Validation failed: {}", message); + return ResponseEntity.badRequest() + .body(new ApiResponse(false, "Validation failed — " + message, null)); + } + + /** Malformed JSON body, including unrecognized field names. */ + @Override + protected ResponseEntity handleHttpMessageNotReadable( + HttpMessageNotReadableException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + + String message = "Malformed JSON request body"; + if (ex.getCause() instanceof UnrecognizedPropertyException upe) { + message = "Unrecognized field '" + upe.getPropertyName() + + "' — use camelCase field names (e.g. sectionUuid, questionUuid)"; + } + log.warn("Unreadable request body: {}", ex.getMessage()); + return ResponseEntity.badRequest().body(new ApiResponse(false, message, null)); + } + + /** Invalid argument (e.g. null UUID field when wrong field name was sent). */ + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgument(IllegalArgumentException ex) { + log.warn("Illegal argument: {}", ex.getMessage()); + return ResponseEntity.badRequest() + .body(new ApiResponse(false, ex.getMessage(), null)); + } + + /** Required @RequestParam missing from the request. */ + @Override + protected ResponseEntity handleMissingServletRequestParameter( + MissingServletRequestParameterException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + + log.warn("Missing request parameter: {}", ex.getParameterName()); + return ResponseEntity.badRequest() + .body(new ApiResponse(false, + "Missing required parameter: '" + ex.getParameterName() + "'", null)); + } + + /** Required @RequestHeader missing from the request. */ + @ExceptionHandler(MissingRequestHeaderException.class) + public ResponseEntity handleMissingHeader(MissingRequestHeaderException ex) { + log.warn("Missing required header: {}", ex.getHeaderName()); + return ResponseEntity.badRequest() + .body(new ApiResponse(false, + "Missing required header: '" + ex.getHeaderName() + "'", null)); + } + + /** Wrong type for a @RequestParam or @PathVariable (e.g. "abc" passed for a Long). */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handleTypeMismatch(MethodArgumentTypeMismatchException ex) { + String expected = ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "unknown"; + String message = "Invalid value '" + ex.getValue() + + "' for parameter '" + ex.getName() + + "' — expected type: " + expected; + log.warn("Type mismatch: {}", message); + return ResponseEntity.badRequest().body(new ApiResponse(false, message, null)); + } + + /** Entity not found (thrown by service layer orElseThrow). */ + @ExceptionHandler(NoSuchElementException.class) + public ResponseEntity handleNotFound(NoSuchElementException ex) { + log.warn("Resource not found: {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ApiResponse(false, ex.getMessage(), null)); + } + + /** Invalid lifecycle transition (e.g. completing a non-SUBMITTED response). */ + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity handleIllegalState(IllegalStateException ex) { + log.warn("Illegal state: {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(new ApiResponse(false, ex.getMessage(), null)); + } + + /** Catch-all for anything not handled above. */ + @ExceptionHandler(Exception.class) + public ResponseEntity handleGeneric(Exception ex) { + log.error("Unhandled exception: {}", ex.getMessage(), ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ApiResponse(false, ex.getMessage(), null)); + } +} diff --git a/src/main/java/com/iemr/flw/controller/StopTBController.java b/src/main/java/com/iemr/flw/controller/StopTBController.java new file mode 100644 index 00000000..7f73d29e --- /dev/null +++ b/src/main/java/com/iemr/flw/controller/StopTBController.java @@ -0,0 +1,161 @@ +package com.iemr.flw.controller; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.iemr.flw.service.StopTBService; +import com.iemr.flw.utils.response.OutputResponse; +import io.swagger.v3.oas.annotations.Operation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/stoptb") +public class StopTBController { + + private final Logger logger = LoggerFactory.getLogger(StopTBController.class); + + // serializeNulls() so fields with no value (e.g. hivStatus, keyPopulationRiskFactor*) + // still appear in the response as null instead of being silently dropped + private final Gson gson = new GsonBuilder().serializeNulls().create(); + + @Autowired + private StopTBService stopTBService; + + // ── Nurse: General Examination ──────────────────────────────────────────── + + @PostMapping("/nurse/generalExamination/save") + @Operation(summary = "Save general examination for Stop TB beneficiary (array of objects)") + public String saveGeneralExamination(@RequestBody List> dataList) { + OutputResponse response = new OutputResponse(); + try { + List> result = stopTBService.saveGeneralExamination(dataList); + response.setResponse(gson.toJson(result)); + } catch (Exception e) { + logger.error("Error in saveGeneralExamination: " + e); + response.setError(5000, "Error saving general examination: " + e.getMessage()); + } + return response.toString(); + } + + @PostMapping("/nurse/generalExamination/getAll") + @Operation(summary = "Get all general examinations for a camp") + public String getAllGeneralExaminations(@RequestBody Map body) { + OutputResponse response = new OutputResponse(); + try { + Object raw = body.get("providerServiceMapID"); + if (raw == null) throw new Exception("providerServiceMapID is required"); + Integer villageID = body.get("villageID") != null ? Integer.parseInt(body.get("villageID").toString()) : null; + Map data = stopTBService.getAllGeneralExaminations(Integer.parseInt(raw.toString()), villageID); + response.setResponse(gson.toJson(data)); + } catch (Exception e) { + logger.error("Error in getAllGeneralExaminations: " + e); + response.setError(5000, "Error fetching general examinations: " + e.getMessage()); + } + return response.toString(); + } + + // ── Nurse: TB Screening ─────────────────────────────────────────────────── + + @PostMapping("/nurse/tbScreening/save") + @Operation(summary = "Save TB screening for Stop TB beneficiary (array of objects)") + public String saveNurseTBScreening(@RequestBody List> dataList) { + OutputResponse response = new OutputResponse(); + try { + List> result = stopTBService.saveNurseTBScreening(dataList); + response.setResponse(gson.toJson(result)); + } catch (Exception e) { + logger.error("Error in saveNurseTBScreening: " + e); + response.setError(5000, "Error saving TB screening: " + e.getMessage()); + } + return response.toString(); + } + + @PostMapping("/nurse/tbScreening/getAll") + @Operation(summary = "Get all TB screenings for a camp") + public String getAllNurseTBScreenings(@RequestBody Map body) { + OutputResponse response = new OutputResponse(); + try { + Object raw = body.get("providerServiceMapID"); + if (raw == null) throw new Exception("providerServiceMapID is required"); + Integer villageID = body.get("villageID") != null ? Integer.parseInt(body.get("villageID").toString()) : null; + Map data = stopTBService.getAllNurseTBScreenings(Integer.parseInt(raw.toString()), villageID); + response.setResponse(gson.toJson(data)); + } catch (Exception e) { + logger.error("Error in getAllNurseTBScreenings: " + e); + response.setError(5000, "Error fetching TB screenings: " + e.getMessage()); + } + return response.toString(); + } + + // ── Nurse: General OPD ──────────────────────────────────────────────────── + + @PostMapping("/nurse/generalOpd/save") + @Operation(summary = "Save general OPD record for Stop TB beneficiary (array of objects)") + public String saveGeneralOpd(@RequestBody List> dataList) { + OutputResponse response = new OutputResponse(); + try { + List> result = stopTBService.saveGeneralOpd(dataList); + response.setResponse(gson.toJson(result)); + } catch (Exception e) { + logger.error("Error in saveGeneralOpd: " + e); + response.setError(5000, "Error saving general OPD: " + e.getMessage()); + } + return response.toString(); + } + + @PostMapping("/nurse/generalOpd/getAll") + @Operation(summary = "Get all general OPD records for a camp") + public String getAllGeneralOpd(@RequestBody Map body) { + OutputResponse response = new OutputResponse(); + try { + Object raw = body.get("providerServiceMapID"); + if (raw == null) throw new Exception("providerServiceMapID is required"); + Integer villageID = body.get("villageID") != null ? Integer.parseInt(body.get("villageID").toString()) : null; + Map data = stopTBService.getAllGeneralOpd(Integer.parseInt(raw.toString()), villageID); + response.setResponse(gson.toJson(data)); + } catch (Exception e) { + logger.error("Error in getAllGeneralOpd: " + e); + response.setError(5000, "Error fetching general OPD records: " + e.getMessage()); + } + return response.toString(); + } + + // ── Nurse: Diagnostics ─────────────────────────────────────────────────── + + @PostMapping("/nurse/diagnostics/save") + @Operation(summary = "Save diagnostics for Stop TB beneficiary (array of objects)") + public String saveDiagnostics(@RequestBody List> dataList) { + OutputResponse response = new OutputResponse(); + try { + List> result = stopTBService.saveDiagnostics(dataList); + response.setResponse(gson.toJson(result)); + } catch (Exception e) { + logger.error("Error in saveDiagnostics: " + e); + response.setError(5000, "Error saving diagnostics: " + e.getMessage()); + } + return response.toString(); + } + + @PostMapping("/nurse/diagnostics/getAll") + @Operation(summary = "Get all diagnostics records for a camp") + public String getAllDiagnostics(@RequestBody Map body) { + OutputResponse response = new OutputResponse(); + try { + Object raw = body.get("providerServiceMapID"); + if (raw == null) throw new Exception("providerServiceMapID is required"); + Integer villageID = body.get("villageID") != null ? Integer.parseInt(body.get("villageID").toString()) : null; + Map data = stopTBService.getAllDiagnostics(Integer.parseInt(raw.toString()), villageID); + response.setResponse(gson.toJson(data)); + } catch (Exception e) { + logger.error("Error in getAllDiagnostics: " + e); + response.setError(5000, "Error fetching diagnostics records: " + e.getMessage()); + } + return response.toString(); + } + +} diff --git a/src/main/java/com/iemr/flw/controller/TBController.java b/src/main/java/com/iemr/flw/controller/TBController.java index e95c3ab4..becc387a 100644 --- a/src/main/java/com/iemr/flw/controller/TBController.java +++ b/src/main/java/com/iemr/flw/controller/TBController.java @@ -168,13 +168,16 @@ public String saveConfirmedCase( @Operation(summary = "get tb confirmed case by beneficiary id") @RequestMapping(value = { "/confirmed/getAll" }, method = { RequestMethod.GET }) public String getConfirmedByBenId( - @RequestHeader(value = "jwtToken") String token) { + @RequestHeader(value = "jwtToken") String token, + @RequestParam(required = false) Integer providerServiceMapID, + @RequestParam(required = false) Integer villageID) { OutputResponse response = new OutputResponse(); try { - String result = - tbConfirmedCaseService.getByUserId(token); + String result = providerServiceMapID != null + ? tbConfirmedCaseService.getByProviderServiceMapId(providerServiceMapID, villageID) + : tbConfirmedCaseService.getByUserId(token); if (result != null) response.setResponse(result); diff --git a/src/main/java/com/iemr/flw/controller/VillageLevelFormController.java b/src/main/java/com/iemr/flw/controller/VillageLevelFormController.java index 884f794d..8eb5eb41 100644 --- a/src/main/java/com/iemr/flw/controller/VillageLevelFormController.java +++ b/src/main/java/com/iemr/flw/controller/VillageLevelFormController.java @@ -45,108 +45,132 @@ public class VillageLevelFormController { @RequestMapping(value = "vhnd/saveAll",method = RequestMethod.POST) public ResponseEntity> saveVhndForm(@RequestBody VhndDto dto) { Map response = new HashMap<>(); - if (!dto.getEntries().isEmpty()) { - Boolean isSaved = villageLevelFormService.saveForm(dto); - boolean ok = isSaved; - response.put("status", ok ? "Success" : "Fail"); - response.put("statusCode", ok ? 200 : 500); - response.put("message", "Save Successfully"); - return new ResponseEntity<>(response, ok ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR); - - } else { + try { + if (!dto.getEntries().isEmpty()) { + Boolean isSaved = villageLevelFormService.saveForm(dto); + boolean ok = isSaved; + response.put("status", ok ? "Success" : "Fail"); + response.put("statusCode", ok ? 200 : 500); + response.put("message", "Save Successfully"); + return new ResponseEntity<>(response, ok ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR); + + } else { + response.put("status", "Fail"); + response.put("statusCode", 400); + response.put("message", "Invalid Request Object"); + return ResponseEntity.ok(response); + + } + } catch (Exception e) { response.put("status", "Fail"); - response.put("statusCode", 400); - response.put("message", "Invalid Request Object"); - return ResponseEntity.ok(response); - + response.put("statusCode", 500); + response.put("errorMessage", e.getMessage()); + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); } - - } @RequestMapping(value = "vhnc/saveAll", method = RequestMethod.POST) public ResponseEntity> saveVhncForm(@RequestBody VhncDto dto) { Map response = new HashMap<>(); - if (!dto.getEntries().isEmpty()) { - Boolean isSaved = villageLevelFormService.saveVhncForm(dto); - boolean ok = isSaved; - response.put("status", ok ? "Success" : "Fail"); - response.put("statusCode", ok ? 200 : 500); - response.put("message", "Save Successfully"); - return new ResponseEntity<>(response, ok ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR); - } else { + try { + if (!dto.getEntries().isEmpty()) { + Boolean isSaved = villageLevelFormService.saveVhncForm(dto); + boolean ok = isSaved; + response.put("status", ok ? "Success" : "Fail"); + response.put("statusCode", ok ? 200 : 500); + response.put("message", "Save Successfully"); + return new ResponseEntity<>(response, ok ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR); + } else { + response.put("status", "Fail"); + response.put("statusCode", 400); + response.put("message", "Invalid Request Object"); + return ResponseEntity.ok(response); + } + } catch (Exception e) { response.put("status", "Fail"); - response.put("statusCode", 400); - response.put("message", "Invalid Request Object"); - return ResponseEntity.ok(response); - - + response.put("statusCode", 500); + response.put("errorMessage", e.getMessage()); + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); } - - } @RequestMapping(value = "phc/saveAll", method = RequestMethod.POST) public ResponseEntity> savePhcForm(@RequestBody PhcReviewMeetingDTO dto) { Map response = new HashMap<>(); - if (!dto.getEntries().isEmpty()) { - Boolean isSaved = villageLevelFormService.savePhcForm(dto); - boolean ok = isSaved; - response.put("status", ok ? "Success" : "Fail"); - response.put("statusCode", ok ? 200 : 500); - response.put("message", "Save Successfully"); - return new ResponseEntity<>(response, ok ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR); - } else { + try { + if (!dto.getEntries().isEmpty()) { + Boolean isSaved = villageLevelFormService.savePhcForm(dto); + boolean ok = isSaved; + response.put("status", ok ? "Success" : "Fail"); + response.put("statusCode", ok ? 200 : 500); + response.put("message", "Save Successfully"); + return new ResponseEntity<>(response, ok ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR); + } else { + response.put("status", "Fail"); + response.put("statusCode", 400); + response.put("message", "Invalid Request Object"); + return ResponseEntity.ok(response); + } + } catch (Exception e) { response.put("status", "Fail"); - response.put("statusCode", 400); - response.put("message", "Invalid Request Object"); - return ResponseEntity.ok(response); - + response.put("statusCode", 500); + response.put("errorMessage", e.getMessage()); + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); } - - } @RequestMapping(value = "ahd/saveAll", method = RequestMethod.POST) public ResponseEntity> saveAhdForm(@RequestBody AhdMeetingDto dto) { Map response = new HashMap<>(); - if(!dto.getEntries().isEmpty()){ - Boolean isSaved = villageLevelFormService.saveAhdForm(dto); - boolean ok = isSaved; - response.put("status", ok ? "Success" : "Fail"); - response.put("statusCode", ok ? 200 : 500); - response.put("message", "Save Successfully"); - return new ResponseEntity<>(response, ok ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR); - }else { + try { + if(!dto.getEntries().isEmpty()){ + Boolean isSaved = villageLevelFormService.saveAhdForm(dto); + boolean ok = isSaved; + response.put("status", ok ? "Success" : "Fail"); + response.put("statusCode", ok ? 200 : 500); + response.put("message", "Save Successfully"); + return new ResponseEntity<>(response, ok ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR); + }else { + response.put("status", "Fail"); + response.put("statusCode", 400); + response.put("message", "Invalid Request Object"); + return ResponseEntity.ok(response); + } + } catch (Exception e) { response.put("status", "Fail"); - response.put("statusCode", 400); - response.put("message", "Invalid Request Object"); - return ResponseEntity.ok(response); + response.put("statusCode", 500); + response.put("errorMessage", e.getMessage()); + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); } - } @RequestMapping(value = "deworming/saveAll", method = RequestMethod.POST) public ResponseEntity> saveDewormingForm(@RequestBody DewormingDto dto) { Map response = new HashMap<>(); - if(!dto.getEntries().isEmpty()){ - Boolean isSaved = villageLevelFormService.saveDewormingForm(dto); - boolean ok = isSaved; - response.put("status", ok ? "Success" : "Fail"); - response.put("statusCode", ok ? 200 : 500); - response.put("message", "Save Successfully"); - return new ResponseEntity<>(response, ok ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR); - }else { + try { + if(!dto.getEntries().isEmpty()){ + Boolean isSaved = villageLevelFormService.saveDewormingForm(dto); + boolean ok = isSaved; + response.put("status", ok ? "Success" : "Fail"); + response.put("statusCode", ok ? 200 : 500); + response.put("message", "Save Successfully"); + return new ResponseEntity<>(response, ok ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR); + }else { + response.put("status", "Fail"); + response.put("statusCode", 400); + response.put("message", "Invalid Request Object"); + return ResponseEntity.ok(response); + } + } catch (Exception e) { response.put("status", "Fail"); - response.put("statusCode", 400); - response.put("message", "Invalid Request Object"); - return ResponseEntity.ok(response); + response.put("statusCode", 500); + response.put("errorMessage", e.getMessage()); + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); } - } diff --git a/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java b/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java index 297694b7..e227776c 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java @@ -102,6 +102,28 @@ public class RMNCHBeneficiaryDetailsRmnch { @Column(name = "longitude") private BigDecimal longitude; + @Column(name = "gpsLatitude") + private Double gpsLatitude; + + @Column(name = "gpsLongitude") + private Double gpsLongitude; + + @Expose + @Column(name = "digipin") + private String digipin; + + @Expose + @Column(name = "gpsTimestamp") + private Timestamp gpsTimestamp; + + @Expose + @Column(name = "isGpsUnavailable", nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0") + private Boolean isGpsUnavailable = false; + + @Expose + @Column(name = "gpsUnavailableReason") + private String gpsUnavailableReason; + @Expose @Column(name = "menstrualBFDId") private Integer menstrualBFDId; @@ -412,6 +434,9 @@ public class RMNCHBeneficiaryDetailsRmnch { @Expose @Transient private String addressLine3; + @Expose + @Transient + private String pinCode; // ---------------------------------------------- @@ -521,4 +546,31 @@ public class RMNCHBeneficiaryDetailsRmnch { @Column(name = "noOfDaysForDelivery") private Integer noOfDaysForDelivery; + @Expose + @Transient + private String occupation; + @Expose + @Transient + private String economicStatus; + @Expose + @Transient + private Integer economicStatusId; + @Expose + @Transient + private String residentialArea; + @Expose + @Transient + private Integer residentialAreaId; + + public String getOccupation() { return occupation; } + public void setOccupation(String occupation) { this.occupation = occupation; } + public String getEconomicStatus() { return economicStatus; } + public void setEconomicStatus(String economicStatus) { this.economicStatus = economicStatus; } + public Integer getEconomicStatusId() { return economicStatusId; } + public void setEconomicStatusId(Integer economicStatusId) { this.economicStatusId = economicStatusId; } + public String getResidentialArea() { return residentialArea; } + public void setResidentialArea(String residentialArea) { this.residentialArea = residentialArea; } + public Integer getResidentialAreaId() { return residentialAreaId; } + public void setResidentialAreaId(Integer residentialAreaId) { this.residentialAreaId = residentialAreaId; } + } diff --git a/src/main/java/com/iemr/flw/domain/identity/RMNCHHouseHoldDetails.java b/src/main/java/com/iemr/flw/domain/identity/RMNCHHouseHoldDetails.java index 3a3ccec8..a0a0adf1 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHHouseHoldDetails.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHHouseHoldDetails.java @@ -329,4 +329,12 @@ public class RMNCHHouseHoldDetails { @Column(name = "isDeactivate") private Boolean isDeactivate; + @Expose + @Column(name = "gpsLatitude") + private Double gpsLatitude; + + @Expose + @Column(name = "gpsLongitude") + private Double gpsLongitude; + } diff --git a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java index c9468688..a589ec7a 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java @@ -247,4 +247,22 @@ public class RMNCHMBeneficiaryaddress implements Serializable { @Expose @Column(name = "SyncedDate") private Timestamp SyncedDate; + + @Column(name = "gpsLatitude") + private Double gpsLatitude; + + @Column(name = "gpsLongitude") + private Double gpsLongitude; + + @Column(name = "digipin") + private String digipin; + + @Column(name = "gpsTimestamp") + private Timestamp gpsTimestamp; + + @Column(name = "isGpsUnavailable", nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0") + private Boolean isGpsUnavailable = false; + + @Column(name = "gpsUnavailableReason") + private String gpsUnavailableReason; } diff --git a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiarydetail.java b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiarydetail.java index 779a09da..1a4cec51 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiarydetail.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiarydetail.java @@ -190,4 +190,27 @@ public class RMNCHMBeneficiarydetail { @Column(name = "familyid") private String familyId; + @Column(name = "ExtraFields") + private String otherFields; + + @Expose + @Column(name = "economicStatus") + private String economicStatus; + + @Expose + @Column(name = "economicStatusId") + private Integer economicStatusId; + + @Expose + @Column(name = "residentialArea") + private String residentialArea; + + @Expose + @Column(name = "residentialAreaId") + private Integer residentialAreaId; + + @Expose + @Column(name = "address") + private String address; + } diff --git a/src/main/java/com/iemr/flw/domain/iemr/BenAnthropometryDetail.java b/src/main/java/com/iemr/flw/domain/iemr/BenAnthropometryDetail.java new file mode 100644 index 00000000..777e8011 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/BenAnthropometryDetail.java @@ -0,0 +1,70 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.*; +import lombok.Data; +import java.sql.Timestamp; + +@Entity +@Table(name = "t_phy_anthropometry", schema = "db_iemr") +@Data +public class BenAnthropometryDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ID") + private Long id; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "Weight_Kg") + private Double weightKg; + + @Column(name = "Height_cm") + private Double heightCm; + + @Column(name = "BMI") + private Double bmi; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "VanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/BenChiefComplaint.java b/src/main/java/com/iemr/flw/domain/iemr/BenChiefComplaint.java new file mode 100644 index 00000000..3b016c5f --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/BenChiefComplaint.java @@ -0,0 +1,73 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.*; +import lombok.Data; +import java.sql.Timestamp; + +@Entity +@Table(name = "t_benchiefcomplaint", schema = "db_iemr") +@Data +public class BenChiefComplaint { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ID") + private Long id; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "ChiefComplaint") + private String chiefComplaint; + + @Column(name = "Duration") + private Integer duration; + + @Column(name = "UnitOfDuration") + private String unitOfDuration; + + @Column(name = "Description") + private String description; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "VanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/BenFlowStatus.java b/src/main/java/com/iemr/flw/domain/iemr/BenFlowStatus.java new file mode 100644 index 00000000..b4600e26 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/BenFlowStatus.java @@ -0,0 +1,82 @@ +package com.iemr.flw.domain.iemr; + +import lombok.Data; +import jakarta.persistence.*; +import java.sql.Timestamp; + +@Entity +@Table(name = "i_ben_flow_outreach") +@Data +public class BenFlowStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ben_flow_id") + private Long benFlowID; + + @Column(name = "beneficiary_reg_id") + private Long beneficiaryRegID; + + @Column(name = "beneficiary_id") + private Long beneficiaryID; + + @Column(name = "visit_category") + private String visitCategory; + + @Column(name = "nurse_flag") + private Short nurseFlag; + + @Column(name = "doctor_flag") + private Short doctorFlag; + + @Column(name = "pharmacist_flag") + private Short pharmacistFlag; + + @Column(name = "providerServiceMapID") + private Integer providerServiceMapId; + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "ben_name") + private String benName; + + @Column(name = "ben_age_val") + private Integer benAgeVal; + + @Column(name = "ben_dob") + private Timestamp dob; + + @Column(name = "ben_gender_val") + private Short genderID; + + @Column(name = "ben_gender") + private String genderName; + + @Column(name = "ben_phone_no") + private String preferredPhoneNum; + + @Column(name = "districtID") + private Integer districtID; + + @Column(name = "district") + private String districtName; + + @Column(name = "villageID") + private Integer villageID; + + @Column(name = "village") + private String villageName; + + @Column(name = "registrationDate") + private Timestamp registrationDate; + + @Column(name = "created_by") + private String agentId; + + @Column(name = "created_date", insertable = false, updatable = false) + private Timestamp visitDate; + + @Column(name = "deleted", insertable = false) + private Boolean deleted; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/BenPhysicalVitalDetail.java b/src/main/java/com/iemr/flw/domain/iemr/BenPhysicalVitalDetail.java new file mode 100644 index 00000000..15b03aee --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/BenPhysicalVitalDetail.java @@ -0,0 +1,76 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.*; +import lombok.Data; +import java.sql.Timestamp; + +@Entity +@Table(name = "t_phy_vitals", schema = "db_iemr") +@Data +public class BenPhysicalVitalDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ID") + private Long id; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "Temperature") + private Double temperature; + + @Column(name = "PulseRate") + private Short pulseRate; + + @Column(name = "SystolicBP_1stReading") + private Short systolicBP; + + @Column(name = "DiastolicBP_1stReading") + private Short diastolicBP; + + @Column(name = "BloodGlucose_Random") + private Short bloodGlucoseRandom; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "VanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java b/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java new file mode 100644 index 00000000..af183d66 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java @@ -0,0 +1,128 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.domain.iemr; + +import com.iemr.flw.masterEnum.FormType; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +/** + * Root entity for a dynamic form definition (e.g. TB Counselling Form). + */ +@Entity +@Table(name = "t_dynamic_form", schema = "db_iemr") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DynamicForm { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "formId") + private Long formId; + + @Column(name = "formUuid", unique = true, nullable = false, length = 100) + private String formUuid; + + @Column(name = "formName", nullable = false, length = 255) + private String formName; + + @Enumerated(EnumType.STRING) + @Column(name = "formType", nullable = false, length = 50) + private FormType formType; + + @Column(name = "isActive", nullable = false) + private Boolean isActive = true; + + /** Days after the most-recently-completed section before the next follow-up notification fires. + * Null means no follow-up notifications for this form. */ + @Column(name = "follow_up_delay_days") + private Integer followUpDelayDays; + + @Column(name = "created_by", length = 100) + private String createdBy; + + @Column(name = "updated_by", length = 100) + private String updatedBy; + + @Column(name = "createdAt", nullable = false, updatable = false) + private Timestamp createdAt; + + @Column(name = "updatedAt", nullable = false) + private Timestamp updatedAt; + + @OneToMany(mappedBy = "dynamicForm", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @ToString.Exclude + private List versions = new ArrayList<>(); + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason") + private String syncFailureReason; + + @PrePersist + protected void onCreate() { + createdAt = new Timestamp(System.currentTimeMillis()); + updatedAt = new Timestamp(System.currentTimeMillis()); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = new Timestamp(System.currentTimeMillis()); + } +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java b/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java new file mode 100644 index 00000000..8b338ee5 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java @@ -0,0 +1,137 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +/** + * Root entity for a beneficiary's response to a dynamic form. + * Tracks the exact form version used and the status lifecycle (DRAFT → SUBMITTED → COMPLETE). + * + * @author Piramal Swasthya + */ +@Entity +@Table(name = "t_form_response", schema = "db_iemr") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class FormResponse { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "responseId") + private Long responseId; + + @Column(name = "beneficiaryId", nullable = false) + private Long beneficiaryId; + + /** Denormalized from versionId for fast form-level queries. */ + @Column(name = "formId", nullable = false) + private Long formId; + + /** Exact FormVersion active at submission time — resolved server-side from isLatest=true. */ + @Column(name = "versionId", nullable = false) + private Long versionId; + + /** Logged-in ASHA/officer ID from the request body. */ + @Column(name = "officerId", nullable = false) + private Long officerId; + + /** DRAFT | SUBMITTED | COMPLETE */ + @Column(name = "status", nullable = false, length = 20) + private String status; + + /** Set when PRE_SUBMIT sections are saved via /submit. */ + @Column(name = "submittedAt") + private Timestamp submittedAt; + + /** Set when POST_SUBMIT sections are saved via /complete. */ + @Column(name = "completedAt") + private Timestamp completedAt; + + /** Updated to now() each time the most-recently-completed section is saved (PRE_SUBMIT or any + * POST_SUBMIT section). The follow-up notification scheduler uses this as its reference date. */ + @Column(name = "last_follow_up_at") + private Timestamp lastFollowUpAt; + + @Column(name = "created_by", length = 100) + private String createdBy; + + @Column(name = "updated_by", length = 100) + private String updatedBy; + + @Column(name = "createdAt", nullable = false, updatable = false) + private Timestamp createdAt; + + @Column(name = "updatedAt", nullable = false) + private Timestamp updatedAt; + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + @Builder.Default + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason") + private String syncFailureReason; + + @PrePersist + protected void onCreate() { + createdAt = new Timestamp(System.currentTimeMillis()); + updatedAt = new Timestamp(System.currentTimeMillis()); + if (status == null) { + status = "DRAFT"; + } + } + + @PreUpdate + protected void onUpdate() { + updatedAt = new Timestamp(System.currentTimeMillis()); + } +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java new file mode 100644 index 00000000..b8454716 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java @@ -0,0 +1,118 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +/** + * A section within a dynamic form version (e.g. "Section A: Disease Awareness"). + * Each section belongs to exactly one FormVersion via the version_id FK. + */ +@Entity +@Table(name = "t_form_section", schema = "db_iemr") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FormSection { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "sectionId") + private Long sectionId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "version_id", nullable = false) + @ToString.Exclude + private FormVersion formVersion; + + @Column(name = "sectionUuid", nullable = false, length = 100) + private String sectionUuid; + + @Column(name = "sectionName", nullable = false, length = 255) + private String sectionName; + + @Column(name = "sectionName_hindi", length = 255) + private String sectionNameHindi; + + /** PRE_SUBMIT sections are shown before the form's main submit. POST_SUBMIT after. */ + @Column(name = "sectionPhase", nullable = false, length = 20) + private String sectionPhase; + + @Column(name = "isRequired", nullable = false) + private Boolean isRequired = true; + + @Column(name = "displayOrder", nullable = false) + private Integer displayOrder; + + /** True only on the last PRE_SUBMIT section — renders the Submit button. */ + @Column(name = "hasSubmitButton", nullable = false) + private Boolean hasSubmitButton = false; + + @Column(name = "created_by", length = 100) + private String createdBy; + + @Column(name = "updated_by", length = 100) + private String updatedBy; + + @OneToMany(mappedBy = "formSection", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @ToString.Exclude + private List questions = new ArrayList<>(); + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason") + private String syncFailureReason; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java b/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java new file mode 100644 index 00000000..5902aa21 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java @@ -0,0 +1,115 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +/** + * A versioned snapshot of a dynamic form's section structure. + * Every structural change creates a new FormVersion; only one version has isLatest=true per form. + * + * @author Piramal Swasthya + */ +@Entity +@Table(name = "t_form_version", schema = "db_iemr") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FormVersion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "versionId") + private Long versionId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "formId", nullable = false) + @ToString.Exclude + private DynamicForm dynamicForm; + + @Column(name = "versionNumber", nullable = false) + private Integer versionNumber; + + @Column(name = "isLatest", nullable = false) + private Boolean isLatest = true; + + @Column(name = "createdAt", nullable = false, updatable = false) + private Timestamp createdAt; + + @Column(name = "createdBy", length = 100) + private String createdBy; + + @Column(name = "updated_by", length = 100) + private String updatedBy; + + @Column(name = "notes", columnDefinition = "TEXT") + private String notes; + + @OneToMany(mappedBy = "formVersion", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @ToString.Exclude + private List sections = new ArrayList<>(); + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason") + private String syncFailureReason; + + @PrePersist + protected void onCreate() { + createdAt = new Timestamp(System.currentTimeMillis()); + } +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/MalariaFollowUp.java b/src/main/java/com/iemr/flw/domain/iemr/MalariaFollowUp.java index cd7384e1..bc30e7dd 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/MalariaFollowUp.java +++ b/src/main/java/com/iemr/flw/domain/iemr/MalariaFollowUp.java @@ -2,7 +2,9 @@ import jakarta.persistence.*; import lombok.Data; +import org.hibernate.annotations.UpdateTimestamp; +import java.sql.Timestamp; import java.util.Date; @Entity @@ -47,4 +49,11 @@ public class MalariaFollowUp { @Column(name = "referral_date") @Temporal(TemporalType.DATE) private Date referralDate; + + @Column(name = "modified_by") + private String modifiedBy; + + @UpdateTimestamp + @Column(name = "last_mod_date") + private Timestamp lastModDate; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java new file mode 100644 index 00000000..50af2b38 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java @@ -0,0 +1,103 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.sql.Timestamp; + +/** + * Conditional action triggered when a specific option is selected. + * actionType: SHOW_QUESTION | DISABLE_SECTION_VALIDATION | LOCK_FORM + * Exactly one of targetQuestion / targetSection is non-null per row. + */ +@Entity +@Table(name = "t_option_condition", schema = "db_iemr") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OptionCondition { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "conditionId") + private Long conditionId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "optionId", nullable = false) + @ToString.Exclude + private QuestionOption questionOption; + + @Column(name = "actionType", nullable = false, length = 40) + private String actionType; + + /** Set when actionType = SHOW_QUESTION. */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "targetQuestionId", nullable = true) + @ToString.Exclude + private SectionQuestion targetQuestion; + + /** Set when actionType = DISABLE_SECTION_VALIDATION. */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "targetSectionId", nullable = true) + @ToString.Exclude + private FormSection targetSection; + + @Column(name = "created_by", length = 100) + private String createdBy; + + @Column(name = "updated_by", length = 100) + private String updatedBy; + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason") + private String syncFailureReason; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/PhyGeneralExamination.java b/src/main/java/com/iemr/flw/domain/iemr/PhyGeneralExamination.java new file mode 100644 index 00000000..b99fd804 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/PhyGeneralExamination.java @@ -0,0 +1,79 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.*; +import lombok.Data; +import java.sql.Timestamp; + +@Entity +@Table(name = "t_Phy_GeneralExam", schema = "db_iemr") +@Data +public class PhyGeneralExamination { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ID") + private Long id; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "Pallor") + private String pallor; + + @Column(name = "Jaundice") + private String jaundice; + + @Column(name = "Cyanosis") + private String cyanosis; + + @Column(name = "Clubbing") + private String clubbing; + + @Column(name = "Lymphadenopathy") + private String lymphadenopathy; + + @Column(name = "Edema") + private String edema; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/PrescribedDrugDetail.java b/src/main/java/com/iemr/flw/domain/iemr/PrescribedDrugDetail.java new file mode 100644 index 00000000..9a0a1502 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/PrescribedDrugDetail.java @@ -0,0 +1,82 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.*; +import lombok.Data; +import java.sql.Timestamp; + +@Entity +@Table(name = "t_prescribeddrug", schema = "db_iemr") +@Data +public class PrescribedDrugDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "PrescribedDrugID", insertable = false, updatable = false) + private Long id; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "PrescriptionID") + private Long prescriptionID; + + @Column(name = "GenericDrugName") + private String drugName; + + @Column(name = "Dose") + private String dose; + + @Column(name = "Frequency") + private String frequency; + + @Column(name = "Duration") + private String duration; + + @Column(name = "DuartionUnit") + private String durationUnit; + + @Column(name = "SpecialInstruction") + private String specialInstruction; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "VanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/PrescriptionDetail.java b/src/main/java/com/iemr/flw/domain/iemr/PrescriptionDetail.java new file mode 100644 index 00000000..5bc5295d --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/PrescriptionDetail.java @@ -0,0 +1,70 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.*; +import lombok.Data; +import java.sql.Timestamp; + +@Entity +@Table(name = "t_prescription", schema = "db_iemr") +@Data +public class PrescriptionDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "PrescriptionID", insertable = false, updatable = false) + private Long prescriptionID; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "DiagnosisProvided") + private String diagnosisProvided; + + @Column(name = "Instruction") + private String instruction; + + @Column(name = "Remarks") + private String remarks; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "VanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java b/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java new file mode 100644 index 00000000..2bc02cbd --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java @@ -0,0 +1,109 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +/** + * A selectable option for a RADIO or MCQ question. + */ +@Entity +@Table(name = "t_question_option", schema = "db_iemr") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class QuestionOption { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "optionId") + private Long optionId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "questionId", nullable = false) + @ToString.Exclude + private SectionQuestion sectionQuestion; + + @Column(name = "optionLabel", nullable = false, length = 255) + private String optionLabel; + + @Column(name = "optionLabel_hindi", length = 100) + private String optionLabelHindi; + + @Column(name = "optionValue", nullable = false, length = 100) + private String optionValue; + + @Column(name = "optionValue_hindi", length = 100) + private String optionValueHindi; + + @Column(name = "displayOrder", nullable = false) + private Integer displayOrder; + + @Column(name = "created_by", length = 100) + private String createdBy; + + @Column(name = "updated_by", length = 100) + private String updatedBy; + + @OneToMany(mappedBy = "questionOption", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @ToString.Exclude + private List conditions = new ArrayList<>(); + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason") + private String syncFailureReason; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java b/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java new file mode 100644 index 00000000..aabfc877 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java @@ -0,0 +1,103 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +/** + * Stores one answer to a single question within a section response. + * MCQ questions produce multiple rows (one per selected option) — no unique constraint on (sectionResponseId, questionId). + * RADIO: optionId set, answerText null. + * MCQ: optionId set per selection, answerText null. + * TEXT/DATE/AUTO_FILL: answerText set, optionId null. + * DISPLAY: no row stored (read-only question type). + * + * @author Piramal Swasthya + */ +@Entity +@Table(name = "t_question_response", schema = "db_iemr") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class QuestionResponse { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "questionResponseId") + private Long questionResponseId; + + @Column(name = "sectionResponseId", nullable = false) + private Long sectionResponseId; + + @Column(name = "questionId", nullable = false) + private Long questionId; + + /** Set for RADIO (one row) and MCQ (one row per selected option). */ + @Column(name = "optionId") + private Long optionId; + + /** Set for TEXT, DATE, AUTO_FILL answers. */ + @Lob + @Column(name = "answerText") + private String answerText; + + @Column(name = "created_by", length = 100) + private String createdBy; + + @Column(name = "updated_by", length = 100) + private String updatedBy; + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + @Builder.Default + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason") + private String syncFailureReason; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java b/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java new file mode 100644 index 00000000..b7d41dc1 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java @@ -0,0 +1,101 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.domain.iemr; + +import com.iemr.flw.masterEnum.ValidationType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.sql.Timestamp; + +/** + * Server-side validation rule for a question. + * validationType: MAX_LENGTH | MIN_DATE | MAX_DATE | REGEX | MANDATORY_IF + */ +@Entity +@Table(name = "t_question_validation", schema = "db_iemr") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class QuestionValidation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "validationId") + private Long validationId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "questionId", nullable = false) + @ToString.Exclude + private SectionQuestion sectionQuestion; + + @Enumerated(EnumType.STRING) + @Column(name = "validationType", nullable = false, length = 30) + private ValidationType validationType; + + /** e.g. "500" for MAX_LENGTH, "TODAY" for MAX_DATE, a regex pattern for REGEX. */ + @Column(name = "validationParam", length = 255) + private String validationParam; + + @Column(name = "errorMessage", nullable = false, length = 500) + private String errorMessage; + + @Column(name = "created_by", length = 100) + private String createdBy; + + @Column(name = "updated_by", length = 100) + private String updatedBy; + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason") + private String syncFailureReason; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java new file mode 100644 index 00000000..1bd7991b --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java @@ -0,0 +1,135 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.domain.iemr; + +import com.iemr.flw.masterEnum.QuestionType; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +/** + * A question within a form section. + * questionType: RADIO | MCQ | TEXT | DATE | DISPLAY | AUTO_FILL + */ +@Entity +@Table(name = "t_section_question", schema = "db_iemr") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SectionQuestion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "questionId") + private Long questionId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sectionId", nullable = false) + @ToString.Exclude + private FormSection formSection; + + @Column(name = "questionUuid", nullable = false, length = 100) + private String questionUuid; + + @Lob + @Column(name = "questionText", nullable = false) + private String questionText; + + @Column(name = "questionText_hindi", columnDefinition = "TEXT") + private String questionTextHindi; + + @Enumerated(EnumType.STRING) + @Column(name = "questionType", nullable = false, length = 20) + private QuestionType questionType; + + @Column(name = "isMandatory", nullable = false) + private Boolean isMandatory = true; + + @Column(name = "displayOrder", nullable = false) + private Integer displayOrder; + + /** Convenience shortcut — enforced by FormValidationEngine for TEXT questions. */ + @Column(name = "maxLength") + private Integer maxLength; + + /** e.g. "TODAY" for date fields with a default. */ + @Column(name = "defaultValue", length = 500) + private String defaultValue; + + /** If true, answerText is encrypted via CryptoUtil before persistence. */ + @Column(name = "containsPii", nullable = false) + private Boolean containsPii = false; + + @Column(name = "created_by", length = 100) + private String createdBy; + + @Column(name = "updated_by", length = 100) + private String updatedBy; + + @OneToMany(mappedBy = "sectionQuestion", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @ToString.Exclude + private List options = new ArrayList<>(); + + @OneToMany(mappedBy = "sectionQuestion", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @ToString.Exclude + private List validations = new ArrayList<>(); + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason") + private String syncFailureReason; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java b/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java new file mode 100644 index 00000000..d8877a50 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java @@ -0,0 +1,96 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +/** + * Tracks completion state of one section within a form response. + * sectionId FK to t_form_section already implies the form version via its version_id FK. + * + * @author Piramal Swasthya + */ +@Entity +@Table(name = "t_section_response", schema = "db_iemr") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SectionResponse { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "sectionResponseId") + private Long sectionResponseId; + + @Column(name = "responseId", nullable = false) + private Long responseId; + + @Column(name = "sectionId", nullable = false) + private Long sectionId; + + /** PENDING | IN_PROGRESS | DONE | SKIPPED */ + @Column(name = "status", nullable = false, length = 20) + private String status; + + @Column(name = "savedAt") + private Timestamp savedAt; + + @Column(name = "created_by", length = 100) + private String createdBy; + + @Column(name = "updated_by", length = 100) + private String updatedBy; + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + @Builder.Default + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason") + private String syncFailureReason; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java new file mode 100644 index 00000000..f9abde57 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java @@ -0,0 +1,140 @@ +package com.iemr.flw.domain.iemr; + +import lombok.Data; +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.sql.Timestamp; + +@Entity +@Table(name = "tb_stoptb_diagnostics", schema = "db_iemr") +@Data +public class StopTBDiagnostics { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "ben_reg_id") + private Long benRegID; + + @Column(name = "provider_service_map_id") + private Integer providerServiceMapID; + + // PRD: user-provided date, not editable once submitted + @Column(name = "visit_date") + private Timestamp visitDate; + + @Column(name = "nikshay_id", length = 50) + private String nikshayId; + + // Digital Chest X-ray + @Column(name = "is_referred_for_digital_chest_xray") + private Boolean isReferredForDigitalChestXray; + + @Column(name = "reason_for_denial_chest_xray", length = 255) + private String reasonForDenialChestXray; + + @Column(name = "reason_for_denial_chest_xray_other", length = 255) + private String reasonForDenialChestXrayOther; + + @Column(name = "is_digital_chest_xray_conducted_id") + private Integer isDigitalChestXrayConductedId; + + @Column(name = "is_digital_chest_xray_conducted") + private Boolean isDigitalChestXrayConducted; + + @Column(name = "reason_not_conducted_chest_xray", length = 255) + private String reasonNotConductedChestXray; + + @Column(name = "reason_not_conducted_chest_xray_other", length = 255) + private String reasonNotConductedChestXrayOther; + + // Positive | Negative + @Column(name = "digital_chest_xray_result_id") + private Integer digitalChestXrayResultId; + + @Column(name = "digital_chest_xray_result", length = 20) + private String digitalChestXrayResult; + + // Sputum Collection + @Column(name = "is_referred_for_sputum_collection") + private Boolean isReferredForSputumCollection; + + @Column(name = "reason_for_denial_sputum", length = 500) + private String reasonForDenialSputum; + + @Column(name = "reason_for_denial_sputum_other", length = 255) + private String reasonForDenialSputumOther; + + @Column(name = "sputum_submitted_at", length = 255) + private String sputumSubmittedAt; + + // Truenat / NAAT + @Column(name = "is_truenat_conducted_id") + private Integer isTruenatConductedId; + + @Column(name = "is_truenat_conducted") + private Boolean isTruenatConducted; + + @Column(name = "reason_not_conducted_naat", length = 255) + private String reasonNotConductedNaat; + + @Column(name = "reason_not_conducted_naat_other", length = 255) + private String reasonNotConductedNaatOther; + + // Positive | Negative + @Column(name = "truenat_result_id") + private Integer truenatResultId; + + @Column(name = "truenat_result", length = 20) + private String truenatResult; + + // Liquid Culture — enabled if History of TB = Yes AND Anti-TB drugs = Yes + @Column(name = "recommended_for_liquid_culture_id") + private Integer recommendedForLiquidCultureId; + + @Column(name = "recommended_for_liquid_culture") + private Boolean recommendedForLiquidCulture; + + // Positive | Negative — editable after submission (results come after 40-45 days) + @Column(name = "liquid_culture_result_id") + private Integer liquidCultureResultId; + + @Column(name = "liquid_culture_result", length = 20) + private String liquidCultureResult; + + @Column(name = "created_by") + private String createdBy; + + @CreationTimestamp + @Column(name = "created_date", updatable = false) + private Timestamp createdDate; + + @Column(name = "modified_by") + private String modifiedBy; + + @UpdateTimestamp + @Column(name = "last_mod_date") + private Timestamp lastModDate; + + @Column(name = "deleted") + private Boolean deleted = false; + + // Sync fields + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "visitCode") + private Long visitCode; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java new file mode 100644 index 00000000..9972d916 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java @@ -0,0 +1,133 @@ +package com.iemr.flw.domain.iemr; + +import lombok.Data; +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.sql.Timestamp; + +@Entity +@Table(name = "tb_stoptb_general_examination", schema = "db_iemr") +@Data +public class StopTBGeneralExamination { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "beneficiary_reg_id") + private Long beneficiaryRegID; + + @Column(name = "provider_service_map_id") + private Integer providerServiceMapID; + + // Vitals + @Column(name = "pulse_rate") + private Integer pulseRate; + + @Column(name = "systolic_bp") + private Integer systolicBP; + + @Column(name = "diastolic_bp") + private Integer diastolicBP; + + @Column(name = "random_blood_sugar") + private Double randomBloodSugar; + + // Clinical signs — ID + label ("PRESENT" / "ABSENT") + @Column(name = "pallor_id") + private Integer pallorId; + + @Column(name = "pallor") + private String pallor; + + @Column(name = "icterus_id") + private Integer icterusId; + + @Column(name = "icterus") + private String icterus; + + @Column(name = "lymphadenopathy_id") + private Integer lymphadenopathyId; + + @Column(name = "lymphadenopathy") + private String lymphadenopathy; + + @Column(name = "oedema_id") + private Integer oedemaId; + + @Column(name = "oedema") + private String oedema; + + @Column(name = "cyanosis_id") + private Integer cyanosisId; + + @Column(name = "cyanosis") + private String cyanosis; + + @Column(name = "clubbing_id") + private Integer clubbingId; + + @Column(name = "clubbing") + private String clubbing; + + // JSON arrays serialised from mobile + @Column(name = "key_population_risk_factor_ids", columnDefinition = "TEXT") + private String keyPopulationRiskFactorIds; + + @Column(name = "key_population_risk_factors", columnDefinition = "TEXT") + private String keyPopulationRiskFactors; + + @Column(name = "hiv_status_id") + private Integer hivStatusId; + + // "Positive" | "Reactive" | "Negative" | "Unknown" + @Column(name = "hiv_status") + private String hivStatus; + + @Column(name = "referral_to_hwc_needed_id") + private Integer referralToHWCNeededId; + + @Column(name = "referral_to_hwc_needed") + private Boolean referralToHWCNeeded; + + @Column(name = "created_by") + private String createdBy; + + @CreationTimestamp + @Column(name = "created_date", updatable = false) + private Timestamp createdDate; + + @Column(name = "modified_by") + private String modifiedBy; + + @UpdateTimestamp + @Column(name = "last_mod_date") + private Timestamp lastModDate; + + @Column(name = "deleted") + private Boolean deleted = false; + + // Sync fields + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "visitCode") + private Long visitCode; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "benVisitID") + private Long benVisitID; + + @Column(name = "ben_id") + private Long benId; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralOpd.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralOpd.java new file mode 100644 index 00000000..4cfbb912 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralOpd.java @@ -0,0 +1,81 @@ +package com.iemr.flw.domain.iemr; + +import lombok.Data; +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.sql.Timestamp; + +@Entity +@Table(name = "tb_stoptb_general_opd", schema = "db_iemr") +@Data +public class StopTBGeneralOpd { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "ben_reg_id") + private Long benRegID; + + @Column(name = "provider_service_map_id") + private Integer providerServiceMapID; + + @Column(name = "chief_complaint", columnDefinition = "TEXT") + private String chiefComplaint; + + // JSON array of selected drug names + @Column(name = "medication", columnDefinition = "TEXT") + private String medication; + + @Column(name = "dosage") + private String dosage; + + // Once daily | Twice daily | Thrice daily | SoS + @Column(name = "frequency") + private String frequency; + + // 1 day | 2 days | 3 days | 5 days | 7 days + @Column(name = "duration") + private String duration; + + @Column(name = "notes", columnDefinition = "TEXT") + private String notes; + + @Column(name = "created_by") + private String createdBy; + + @CreationTimestamp + @Column(name = "created_date", updatable = false) + private Timestamp createdDate; + + @Column(name = "modified_by") + private String modifiedBy; + + @UpdateTimestamp + @Column(name = "last_mod_date") + private Timestamp lastModDate; + + @Column(name = "deleted") + private Boolean deleted = false; + + // Sync fields + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "visitCode") + private Long visitCode; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "benVisitID") + private Long benVisitID; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCase.java b/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCase.java index 8310eb65..c2c51a6f 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCase.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCase.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.Data; +import org.hibernate.annotations.UpdateTimestamp; import java.sql.Timestamp; import java.time.LocalDate; @@ -70,5 +71,35 @@ public class TBConfirmedCase { @Column(name = "updated_at") private LocalDate updatedAt; + @Column(name = "modified_by") + private String modifiedBy; + @UpdateTimestamp + @Column(name = "last_mod_date") + private Timestamp lastModDate; + + // Sync fields + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "benRegID") + private Long benRegID; + + @Column(name = "providerServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "visitCode") + private Long visitCode; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCaseDTO.java b/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCaseDTO.java index 180c3bbe..7a521177 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCaseDTO.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCaseDTO.java @@ -3,12 +3,16 @@ import jakarta.persistence.*; import lombok.Data; +import java.sql.Timestamp; import java.time.LocalDate; @Data public class TBConfirmedCaseDTO { - + + private Integer id; private Long benId; + private Timestamp updateDate; + private String updatedBy; private Integer userId; private Integer suspectedTbId; private String regimenType; @@ -25,5 +29,6 @@ public class TBConfirmedCaseDTO { private String placeOfDeath; private String reasonForDeath ; private String reasonForNotCompleting; + private Boolean counselled; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/TBScreening.java b/src/main/java/com/iemr/flw/domain/iemr/TBScreening.java index 6789f9bf..06e36309 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBScreening.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBScreening.java @@ -1,6 +1,7 @@ package com.iemr.flw.domain.iemr; import lombok.Data; +import org.hibernate.annotations.UpdateTimestamp; import jakarta.persistence.*; import java.sql.Timestamp; @@ -23,36 +24,80 @@ public class TBScreening { @Column(name = "visit_date") private Timestamp visitDate; + @Column(name = "cough_check_id") + private Integer coughMoreThan2WeeksId; + @Column(name = "cough_check") private Boolean coughMoreThan2Weeks; + @Column(name = "blood_check_id") + private Integer bloodInSputumId; + @Column(name = "blood_check") private Boolean bloodInSputum; + @Column(name = "fever_check_id") + private Integer feverMoreThan2WeeksId; + @Column(name = "fever_check") private Boolean feverMoreThan2Weeks; + @Column(name = "weight_check_id") + private Integer lossOfWeightId; + @Column(name = "weight_check") private Boolean lossOfWeight; + @Column(name = "sweats_check_id") + private Integer nightSweatsId; + @Column(name = "sweats_check") private Boolean nightSweats; + @Column(name = "history_check_id") + private Integer historyOfTbId; + @Column(name = "history_check") private Boolean historyOfTb; + @Column(name = "drugs_check_id") + private Integer takingAntiTBDrugsId; + @Column(name = "drugs_check") private Boolean takingAntiTBDrugs; + @Column(name = "family_check_id") + private Integer familySufferingFromTBId; + @Column(name = "family_check") private Boolean familySufferingFromTB; + @Column(name = "rise_of_fever_id") + private Integer riseOfFeverId; + @Column(name = "rise_of_fever") private Boolean riseOfFever; + @Column(name = "loss_of_appetite_id") + private Integer lossOfAppetiteId; + @Column(name = "loss_of_appetite") private Boolean lossOfAppetite; + // Risk Factors section — JSON arrays serialised from mobile + @Column(name = "key_population_risk_factor_ids", columnDefinition = "TEXT") + private String keyPopulationRiskFactorIds; + + @Column(name = "key_population_risk_factors", columnDefinition = "TEXT") + private String keyPopulationRiskFactors; + + @Column(name = "hiv_status_id") + private Integer hivStatusId; + + // "Positive" | "Reactive" | "Negative" | "Unknown" + @Column(name = "hiv_status") + private String hivStatus; + @Column(name = "age") private Boolean age; @@ -79,4 +124,74 @@ public class TBScreening { @Column(name = "recommandate_test") private String recommandateTest; + + // Added for Stop TB nurse flow — nullable, not used by ASHA flow + @Column(name = "ben_reg_id") + private Long benRegID; + + @Column(name = "provider_service_map_id") + private Integer providerServiceMapID; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "modified_by") + private String modifiedBy; + + @UpdateTimestamp + @Column(name = "last_mod_date") + private Timestamp lastModDate; + + @Column(name = "deleted", insertable = false) + private Boolean deleted; + + // Diagnostics / referrals — Stop TB nurse flow only + @Column(name = "referred_for_digital_chest_xray_id") + private Integer referredForDigitalChestXrayId; + + @Column(name = "referred_for_digital_chest_xray") + private Boolean referredForDigitalChestXray; + + @Column(name = "referred_for_sputum_collection_id") + private Integer referredForSputumCollectionId; + + @Column(name = "referred_for_sputum_collection") + private Boolean referredForSputumCollection; + + @Column(name = "sputum_sample_submitted_at", length = 50) + private String sputumSampleSubmittedAt; + + @Column(name = "recommended_for_truenat_id") + private Integer recommendedForTruenatId; + + @Column(name = "recommended_for_truenat") + private Boolean recommendedForTruenat; + + @Column(name = "recommended_for_liquid_culture_id") + private Integer recommendedForLiquidCultureId; + + @Column(name = "recommended_for_liquid_culture") + private Boolean recommendedForLiquidCulture; + + @Column(name = "test_denial_reasons", columnDefinition = "TEXT") + private String testDenialReasons; + + // Sync fields — stamped by local laptop on save, used by MMU DataSync for local→central push + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "visitCode") + private Long visitCode; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "createdDate") + private java.sql.Timestamp createdDate; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/TBSuspected.java b/src/main/java/com/iemr/flw/domain/iemr/TBSuspected.java index 2de2baf8..0c8d2fc4 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBSuspected.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBSuspected.java @@ -1,6 +1,7 @@ package com.iemr.flw.domain.iemr; import lombok.Data; +import org.hibernate.annotations.UpdateTimestamp; import jakarta.persistence.*; import java.sql.Timestamp; @@ -73,7 +74,37 @@ public class TBSuspected { private Integer providerServiceMapId; @Column(name = "is_confirmed") - private Boolean isConfirmed; + private Boolean isConfirmed; + @Column(name = "modified_by") + private String modifiedBy; + @UpdateTimestamp + @Column(name = "last_mod_date") + private Timestamp lastModDate; + + // Sync fields + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "benRegID") + private Long benRegID; + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "created_date") + private Timestamp createdDate; + + @Column(name = "visitCode") + private Long visitCode; } diff --git a/src/main/java/com/iemr/flw/dto/identity/GetBenRequestHandler.java b/src/main/java/com/iemr/flw/dto/identity/GetBenRequestHandler.java index d1a654ec..94bfed1c 100644 --- a/src/main/java/com/iemr/flw/dto/identity/GetBenRequestHandler.java +++ b/src/main/java/com/iemr/flw/dto/identity/GetBenRequestHandler.java @@ -12,6 +12,16 @@ public class GetBenRequestHandler { private Integer ashaId; + private Integer providerServiceMapID; + + public Integer getProviderServiceMapID() { + return providerServiceMapID; + } + + public void setProviderServiceMapID(Integer providerServiceMapID) { + this.providerServiceMapID = providerServiceMapID; + } + private Long activityId; private Integer month; @@ -82,6 +92,10 @@ public void setVillageID(Integer villageID) { this.villageID = villageID; } + public void setVillageId(Integer villageId) { + this.villageID = villageId; + } + public Timestamp getFromDate() { return fromDate; } diff --git a/src/main/java/com/iemr/flw/dto/iemr/DynamicFormDTO.java b/src/main/java/com/iemr/flw/dto/iemr/DynamicFormDTO.java new file mode 100644 index 00000000..c9ecb63c --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/DynamicFormDTO.java @@ -0,0 +1,65 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.dto.iemr; + +import com.iemr.flw.masterEnum.FormType; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Transfer object for a full dynamic form definition. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DynamicFormDTO { + + private Long formId; + + @NotBlank(message = "formUuid is required") + private String formUuid; + + @NotBlank(message = "formName is required") + private String formName; + + @NotNull(message = "formType is required") + private FormType formType; + + private Boolean isActive = true; + + /** Days after the most-recently-completed section before the next follow-up notification fires. + * Null means no follow-up notifications. */ + private Integer followUpDelayDays; + + /** Current version number — populated by service on read, ignored on write. */ + private Integer versionNumber; + + @Valid + private List sections = new ArrayList<>(); +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/FormResponseDTO.java b/src/main/java/com/iemr/flw/dto/iemr/FormResponseDTO.java new file mode 100644 index 00000000..d53a5823 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/FormResponseDTO.java @@ -0,0 +1,57 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.dto.iemr; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; +import java.util.List; + +/** + * Outbound representation of a saved form response including all section and question answers. + * + * @author Piramal Swasthya + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class FormResponseDTO { + + private Long responseId; + private Long beneficiaryId; + private Long formId; + private Long versionId; + private Long officerId; + private String status; + private String createdBy; + private String updatedBy; + private Timestamp submittedAt; + private Timestamp completedAt; + private Timestamp lastFollowUpAt; + private Timestamp createdAt; + private Timestamp updatedAt; + private List sections; +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/FormResponseRequest.java b/src/main/java/com/iemr/flw/dto/iemr/FormResponseRequest.java new file mode 100644 index 00000000..a90cb84b --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/FormResponseRequest.java @@ -0,0 +1,58 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.dto.iemr; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Inbound request to save or submit a dynamic form response. + * + * @author Piramal Swasthya + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FormResponseRequest { + + /** Present on re-submit: update an existing SUBMITTED response's sections instead of creating new. */ + private Long responseId; + + @NotBlank(message = "formUuid is required") + private String formUuid; + + @NotNull(message = "beneficiaryId is required") + private Long beneficiaryId; + + @NotNull(message = "officerId is required") + private Long officerId; + + @Valid + private List sections = new ArrayList<>(); +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java b/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java new file mode 100644 index 00000000..2db1f8c7 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java @@ -0,0 +1,64 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.dto.iemr; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Transfer object for a form section definition. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FormSectionDTO { + + private Long sectionId; + + @NotBlank(message = "sectionUuid is required") + private String sectionUuid; + + @NotBlank(message = "sectionName is required") + private String sectionName; + + private String sectionNameHindi; + + @NotBlank(message = "sectionPhase is required (PRE_SUBMIT or POST_SUBMIT)") + private String sectionPhase; + + private Boolean isRequired = true; + + @NotNull(message = "displayOrder is required") + private Integer displayOrder; + + private Boolean hasSubmitButton = false; + + @Valid + private List questions = new ArrayList<>(); +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/FormVersionDTO.java b/src/main/java/com/iemr/flw/dto/iemr/FormVersionDTO.java new file mode 100644 index 00000000..742b0f7b --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/FormVersionDTO.java @@ -0,0 +1,41 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.dto.iemr; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Transfer object for form version metadata. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FormVersionDTO { + + private Long versionId; + private Integer versionNumber; + private Boolean isLatest; + private String createdBy; + private String notes; +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/MalariaFollowListUpDTO.java b/src/main/java/com/iemr/flw/dto/iemr/MalariaFollowListUpDTO.java index 1a8b8e6e..56dff196 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/MalariaFollowListUpDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/MalariaFollowListUpDTO.java @@ -25,11 +25,15 @@ package com.iemr.flw.dto.iemr; import lombok.Data; + +import java.sql.Timestamp; import java.util.Date; @Data public class MalariaFollowListUpDTO { private Long id; + private Timestamp updateDate; + private String updatedBy; private Long benId; private Long houseHoldDetailsId; private Integer userId ; diff --git a/src/main/java/com/iemr/flw/dto/iemr/OptionConditionDTO.java b/src/main/java/com/iemr/flw/dto/iemr/OptionConditionDTO.java new file mode 100644 index 00000000..d43167a7 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/OptionConditionDTO.java @@ -0,0 +1,58 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.dto.iemr; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Transfer object for a conditional action triggered when an option is selected. + * Exactly one of targetQuestionId / targetSectionId should be non-null. + * + * On create/update: use targetQuestionUuid or targetSectionUuid to reference target. + * On read: targetQuestionId + targetQuestionUuid (or targetSectionId + targetSectionUuid) are + * returned as flat references — the full target object appears in the form's questions/sections list. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OptionConditionDTO { + + private Long conditionId; + + @NotBlank(message = "actionType is required") + private String actionType; + + /** DB ID of the target question. */ + private Long targetQuestionId; + + /** DB ID of the target section. */ + private Long targetSectionId; + + /** UUID of the target question. */ + private String targetQuestionUuid; + + /** UUID of the target section. */ + private String targetSectionUuid; +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java b/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java new file mode 100644 index 00000000..8ee08c45 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java @@ -0,0 +1,63 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.dto.iemr; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * A single question's answer within a section answer request. + * Exactly one of optionValue, optionValues, answerText, or answerDate should be set per question type: + * RADIO → optionValue (single string) + * MCQ → optionValues (list of strings, one row saved per element) + * TEXT/AUTO_FILL → answerText + * DATE → answerDate (ISO date string) or answerText + * DISPLAY → omit entirely (ignored by service) + * + * @author Piramal Swasthya + */ +@JsonIgnoreProperties(ignoreUnknown = false) +@Data +@NoArgsConstructor +@AllArgsConstructor +public class QuestionAnswerRequest { + + @NotBlank(message = "questionUuid is required") + private String questionUuid; + + /** RADIO — single selected option value. */ + private String optionValue; + + /** MCQ — list of selected option values. */ + private List optionValues; + + /** TEXT / AUTO_FILL — free-text answer. */ + private String answerText; + + /** DATE — ISO date string answer. */ + private String answerDate; +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/QuestionOptionDTO.java b/src/main/java/com/iemr/flw/dto/iemr/QuestionOptionDTO.java new file mode 100644 index 00000000..0328263c --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/QuestionOptionDTO.java @@ -0,0 +1,59 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.dto.iemr; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Transfer object for a selectable option on a RADIO or MCQ question. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class QuestionOptionDTO { + + private Long optionId; + + @NotBlank(message = "optionLabel is required") + private String optionLabel; + + private String optionLabelHindi; + + @NotBlank(message = "optionValue is required") + private String optionValue; + + private String optionValueHindi; + + @NotNull(message = "displayOrder is required") + private Integer displayOrder; + + @Valid + private List conditions = new ArrayList<>(); +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java b/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java new file mode 100644 index 00000000..f5f76d63 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java @@ -0,0 +1,47 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.dto.iemr; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Outbound representation of a single question's stored answer. + * For MCQ questions, multiple QuestionResponseDTO entries share the same questionId. + * + * @author Piramal Swasthya + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class QuestionResponseDTO { + + private Long questionResponseId; + private Long questionId; + /** Populated for RADIO and MCQ answers. */ + private Long optionId; + /** Populated for TEXT, DATE, AUTO_FILL answers. */ + private String answerText; +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/QuestionValidationDTO.java b/src/main/java/com/iemr/flw/dto/iemr/QuestionValidationDTO.java new file mode 100644 index 00000000..72c144c5 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/QuestionValidationDTO.java @@ -0,0 +1,49 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.dto.iemr; + +import com.iemr.flw.masterEnum.ValidationType; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Transfer object for a server-side validation rule on a question. + * validationType: MAX_LENGTH | MIN_DATE | MAX_DATE | REGEX | MANDATORY_IF + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class QuestionValidationDTO { + + private Long validationId; + + @NotNull(message = "validationType is required") + private ValidationType validationType; + + private String validationParam; + + @NotBlank(message = "errorMessage is required") + private String errorMessage; +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/SectionAnswerRequest.java b/src/main/java/com/iemr/flw/dto/iemr/SectionAnswerRequest.java new file mode 100644 index 00000000..b63d7a0c --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/SectionAnswerRequest.java @@ -0,0 +1,50 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.dto.iemr; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Answers for one section within a form response request. + * + * @author Piramal Swasthya + */ +@JsonIgnoreProperties(ignoreUnknown = false) +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SectionAnswerRequest { + + @NotBlank(message = "sectionUuid is required") + private String sectionUuid; + + @Valid + private List answers = new ArrayList<>(); +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java b/src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java new file mode 100644 index 00000000..11a81430 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java @@ -0,0 +1,76 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.dto.iemr; + +import com.iemr.flw.masterEnum.QuestionType; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Transfer object for a question within a section. + * visibleByDefault is computed by the service — not stored in DB. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SectionQuestionDTO { + + private Long questionId; + + @NotBlank(message = "questionUuid is required") + private String questionUuid; + + @NotBlank(message = "questionText is required") + private String questionText; + + private String questionTextHindi; + + @NotNull(message = "questionType is required") + private QuestionType questionType; + + private Boolean isMandatory = true; + + @NotNull(message = "displayOrder is required") + private Integer displayOrder; + + private Integer maxLength; + + private String defaultValue; + + private Boolean containsPii = false; + + /** Computed at read time: false if this question is a target of any OptionCondition. */ + private Boolean visibleByDefault = true; + + @Valid + private List options = new ArrayList<>(); + + @Valid + private List validations = new ArrayList<>(); +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/SectionResponseDTO.java b/src/main/java/com/iemr/flw/dto/iemr/SectionResponseDTO.java new file mode 100644 index 00000000..dac289a7 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/SectionResponseDTO.java @@ -0,0 +1,48 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.dto.iemr; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; +import java.util.List; + +/** + * Outbound representation of one section's response data. + * + * @author Piramal Swasthya + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SectionResponseDTO { + + private Long sectionResponseId; + private Long sectionId; + private String status; + private Timestamp savedAt; + private List answers; +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/TBSuspectedDTO.java b/src/main/java/com/iemr/flw/dto/iemr/TBSuspectedDTO.java index 29fb25f6..72b731d0 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/TBSuspectedDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/TBSuspectedDTO.java @@ -8,6 +8,8 @@ public class TBSuspectedDTO { private Long id; + private Timestamp updateDate; + private String updatedBy; private Long benId; private String visitLabel; diff --git a/src/main/java/com/iemr/flw/dto/iemr/UserServiceRoleDTO.java b/src/main/java/com/iemr/flw/dto/iemr/UserServiceRoleDTO.java index ead3ddc0..c336e1c9 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/UserServiceRoleDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/UserServiceRoleDTO.java @@ -27,6 +27,14 @@ public class UserServiceRoleDTO { private String villageName; private Map facilityData; + // Stop TB / Nikshay location scope — populated only when serviceName = "Stop TB". + // Set via setters after construction (additive, not part of the base constructor + // so every other service line's DTO construction stays exactly as it is today). + private String tuId; + private String tuName; + private String healthFacilityId; + private String healthFacilityName; + public UserServiceRoleDTO(Integer userId, String name, String userName, Integer stateId, String stateName, Integer workingDistrictId, String workingDistrictName, Short serviceProviderId, Integer roleId, String roleName, Integer providerServiceMapId, Integer blockId, String blockName, String villageId, String villageName) { this.userId = userId; this.name = name; diff --git a/src/main/java/com/iemr/flw/mapper/DynamicFormMapper.java b/src/main/java/com/iemr/flw/mapper/DynamicFormMapper.java new file mode 100644 index 00000000..551e93a6 --- /dev/null +++ b/src/main/java/com/iemr/flw/mapper/DynamicFormMapper.java @@ -0,0 +1,105 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.mapper; + +import com.iemr.flw.domain.iemr.DynamicForm; +import com.iemr.flw.domain.iemr.FormSection; +import com.iemr.flw.domain.iemr.FormVersion; +import com.iemr.flw.domain.iemr.OptionCondition; +import com.iemr.flw.domain.iemr.QuestionOption; +import com.iemr.flw.domain.iemr.QuestionValidation; +import com.iemr.flw.domain.iemr.SectionQuestion; +import com.iemr.flw.dto.iemr.DynamicFormDTO; +import com.iemr.flw.dto.iemr.FormSectionDTO; +import com.iemr.flw.dto.iemr.FormVersionDTO; +import com.iemr.flw.dto.iemr.OptionConditionDTO; +import com.iemr.flw.dto.iemr.QuestionOptionDTO; +import com.iemr.flw.dto.iemr.QuestionValidationDTO; +import com.iemr.flw.dto.iemr.SectionQuestionDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +/** + * MapStruct mapper for dynamic form definition — entity ↔ DTO conversion. + */ +@Mapper(componentModel = "spring") +public interface DynamicFormMapper { + + // ── Entity → DTO ────────────────────────────────────────────────────────── + + @Mapping(target = "sections", ignore = true) + @Mapping(target = "versionNumber", ignore = true) + DynamicFormDTO toDto(DynamicForm entity); + + FormVersionDTO toDto(FormVersion entity); + + @Mapping(target = "questions", ignore = true) + FormSectionDTO toDto(FormSection entity); + + @Mapping(target = "visibleByDefault", ignore = true) + @Mapping(target = "options", ignore = true) + @Mapping(target = "validations", ignore = true) + SectionQuestionDTO toDto(SectionQuestion entity); + + @Mapping(target = "conditions", ignore = true) + QuestionOptionDTO toDto(QuestionOption entity); + + @Mapping(source = "targetQuestion.questionId", target = "targetQuestionId") + @Mapping(source = "targetQuestion.questionUuid", target = "targetQuestionUuid") + @Mapping(source = "targetSection.sectionId", target = "targetSectionId") + @Mapping(source = "targetSection.sectionUuid", target = "targetSectionUuid") + OptionConditionDTO toDto(OptionCondition entity); + + QuestionValidationDTO toDto(QuestionValidation entity); + + // ── DTO → Entity ────────────────────────────────────────────────────────── + + @Mapping(target = "versions", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + DynamicForm toEntity(DynamicFormDTO dto); + + @Mapping(target = "dynamicForm", ignore = true) + @Mapping(target = "sections", ignore = true) + @Mapping(target = "createdAt", ignore = true) + FormVersion toEntity(FormVersionDTO dto); + + @Mapping(target = "questions", ignore = true) + FormSection toEntity(FormSectionDTO dto); + + @Mapping(target = "formSection", ignore = true) + @Mapping(target = "options", ignore = true) + @Mapping(target = "validations", ignore = true) + SectionQuestion toEntity(SectionQuestionDTO dto); + + @Mapping(target = "sectionQuestion", ignore = true) + @Mapping(target = "conditions", ignore = true) + QuestionOption toEntity(QuestionOptionDTO dto); + + @Mapping(target = "questionOption", ignore = true) + @Mapping(target = "targetQuestion", ignore = true) + @Mapping(target = "targetSection", ignore = true) + OptionCondition toEntity(OptionConditionDTO dto); + + @Mapping(target = "sectionQuestion", ignore = true) + QuestionValidation toEntity(QuestionValidationDTO dto); +} diff --git a/src/main/java/com/iemr/flw/masterEnum/FormType.java b/src/main/java/com/iemr/flw/masterEnum/FormType.java new file mode 100644 index 00000000..779fce19 --- /dev/null +++ b/src/main/java/com/iemr/flw/masterEnum/FormType.java @@ -0,0 +1,34 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.masterEnum; + +/** + * Allowed types for a dynamic form definition. + * + * @author Piramal Swasthya + */ +public enum FormType { + COUNSELLING_MODULE, + SCREENING, + COUNSELLING, + TB_COUNSELLING +} diff --git a/src/main/java/com/iemr/flw/masterEnum/QuestionType.java b/src/main/java/com/iemr/flw/masterEnum/QuestionType.java new file mode 100644 index 00000000..ae4b91e1 --- /dev/null +++ b/src/main/java/com/iemr/flw/masterEnum/QuestionType.java @@ -0,0 +1,43 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.masterEnum; + +/** + * Exhaustive set of question input types for dynamic form questions. + * No other values may be stored in t_section_question.questionType. + * + * @author Piramal Swasthya + */ +public enum QuestionType { + /** Single-select from a predefined list of options. */ + RADIO, + /** Multi-select from a predefined list of options. */ + MCQ, + /** Free-text input. */ + TEXT, + /** Date picker input. */ + DATE, + /** Read-only display text — carries no answer data. */ + DISPLAY, + /** Value auto-filled from context (e.g. ASHA worker ID). */ + AUTO_FILL +} diff --git a/src/main/java/com/iemr/flw/masterEnum/ValidationType.java b/src/main/java/com/iemr/flw/masterEnum/ValidationType.java new file mode 100644 index 00000000..931e2469 --- /dev/null +++ b/src/main/java/com/iemr/flw/masterEnum/ValidationType.java @@ -0,0 +1,42 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.masterEnum; + +/** + * Exhaustive set of validation rule types for dynamic form questions. + * No other values may be stored in t_question_validation.validationType. + * + * @author Piramal Swasthya + */ +public enum ValidationType { + /** Maximum character length for text inputs. validationParam = integer string e.g. "100". */ + MAX_LENGTH, + /** Minimum allowed date. validationParam = ISO date string or "TODAY". */ + MIN_DATE, + /** Maximum allowed date. validationParam = ISO date string or "TODAY". */ + MAX_DATE, + /** Java-compatible regular expression the answer must match. validationParam = regex pattern. */ + REGEX, + /** Field becomes mandatory when another field equals a specific value. + * validationParam format: "QUESTION_UUID=OPTION_VALUE" e.g. "Q-GENDER=FEMALE". */ + MANDATORY_IF +} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java b/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java index e4ce22f0..c9d00c72 100644 --- a/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java +++ b/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java @@ -90,12 +90,18 @@ Page getBenDataWithinDates( @Query(nativeQuery = true, value = " SELECT HealthID,HealthIdNumber,isNewAbha FROM db_iemr.t_healthid WHERE HealthIdNumber = :healthIdNumber ") ArrayList getBenHealthDetails(@Param("healthIdNumber") String healthIdNumber); - @Query("SELECT b FROM RMNCHMBeneficiarymapping b WHERE b.benRegId = :benRegId") - RMNCHMBeneficiarymapping findByBenRegIdFromMapping(@Param("benRegId") BigInteger benRegId); + // benRegId has no unique constraint on i_beneficiarymapping — a beneficiary can have + // more than one mapping row, so this must return a list, not assume a single result. + @Query("SELECT b FROM RMNCHMBeneficiarymapping b WHERE b.benRegId = :benRegId ORDER BY b.benMapId DESC") + List findByBenRegIdFromMapping(@Param("benRegId") BigInteger benRegId); + @Query("SELECT d FROM RMNCHMBeneficiarydetail d WHERE d.beneficiaryDetailsId = :beneficiaryDetailsId") RMNCHMBeneficiarydetail findByBeneficiaryDetailsId(@Param("beneficiaryDetailsId") BigInteger beneficiaryDetailsId); + @Query("SELECT d FROM RMNCHMBeneficiarydetail d WHERE d.BenRegId = :benRegID") + RMNCHMBeneficiarydetail getDetailByBenRegID(@Param("benRegID") BigInteger benRegID); + // BeneficiaryRepo — replaces 3 separate queries per beneficiary @Query(value = """ diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java new file mode 100644 index 00000000..1856eb06 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java @@ -0,0 +1,15 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.BenAnthropometryDetail; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface BenAnthropometryRepo extends JpaRepository { + + List findByBeneficiaryRegIDOrderByCreatedDateDesc(Long beneficiaryRegID); + + BenAnthropometryDetail findByBenVisitID(Long benVisitID); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenChiefComplaintRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenChiefComplaintRepo.java new file mode 100644 index 00000000..47a73f98 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/BenChiefComplaintRepo.java @@ -0,0 +1,9 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.BenChiefComplaint; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface BenChiefComplaintRepo extends JpaRepository { +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java new file mode 100644 index 00000000..4658f7cc --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java @@ -0,0 +1,38 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.BenFlowStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.sql.Timestamp; +import java.util.List; + +@Repository +public interface BenFlowStatusRepo extends JpaRepository { + + @Query("SELECT b FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId AND b.nurseFlag = 1 AND b.deleted = false ORDER BY b.registrationDate DESC") + List getNurseWorklist(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); + + @Query("SELECT b FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId AND b.deleted = false ORDER BY b.registrationDate DESC") + List getRegistrarWorklist(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); + + @Query("SELECT b FROM BenFlowStatus b WHERE b.beneficiaryRegID = :benRegID AND b.deleted = false ORDER BY b.registrationDate DESC") + List findByBeneficiaryRegID(@Param("benRegID") Long benRegID); + + @Modifying + @Query("UPDATE BenFlowStatus b SET b.nurseFlag = 9, b.pharmacistFlag = 1 WHERE b.benFlowID = :benFlowID") + int updateAfterNurseSubmit(@Param("benFlowID") Long benFlowID); + + // Stop TB: nurse done, counsellor pending when diagnostic is positive (set by device push) + @Modifying + @Query("UPDATE BenFlowStatus b SET b.nurseFlag = 9 WHERE b.benFlowID = :benFlowID") + int updateStopTBAfterNurseSubmit(@Param("benFlowID") Long benFlowID); + + // Stop TB: device operator pushes positive diagnostic → route to counsellor + @Modifying + @Query("UPDATE BenFlowStatus b SET b.doctorFlag = 1 WHERE b.beneficiaryRegID = :benRegID AND b.visitCategory = 'Stop TB' AND b.deleted = false") + int routeToStopTBCounsellor(@Param("benRegID") Long benRegID); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java new file mode 100644 index 00000000..7e474935 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java @@ -0,0 +1,15 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.BenPhysicalVitalDetail; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface BenPhysicalVitalRepo extends JpaRepository { + + List findByBeneficiaryRegIDOrderByCreatedDateDesc(Long beneficiaryRegID); + + BenPhysicalVitalDetail findByBenVisitID(Long benVisitID); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenVisitDetailsRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenVisitDetailsRepo.java index dfd605ef..47aacb2b 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/BenVisitDetailsRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/BenVisitDetailsRepo.java @@ -2,10 +2,26 @@ import com.iemr.flw.domain.iemr.BenVisitDetail; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.sql.Timestamp; import java.util.List; @Repository public interface BenVisitDetailsRepo extends JpaRepository { - public List findByBeneficiaryRegId(Long benRegID);} + + List findByBeneficiaryRegId(Long benRegID); + + @Query("SELECT v FROM BenVisitDetail v WHERE v.beneficiaryRegId = :benRegID " + + "AND v.visitCategory = 'Stop TB' " + + "AND v.visitDateTime >= :dayStart AND v.visitDateTime < :dayEnd") + BenVisitDetail findStopTBVisitForToday(@Param("benRegID") Long benRegID, + @Param("dayStart") Timestamp dayStart, + @Param("dayEnd") Timestamp dayEnd); + + @Query("SELECT COUNT(v) FROM BenVisitDetail v WHERE v.beneficiaryRegId = :benRegID " + + "AND v.visitCategory = 'Stop TB'") + Integer countStopTBVisits(@Param("benRegID") Long benRegID); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/DynamicFormRepo.java b/src/main/java/com/iemr/flw/repo/iemr/DynamicFormRepo.java new file mode 100644 index 00000000..69260ea1 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/DynamicFormRepo.java @@ -0,0 +1,47 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.DynamicForm; +import com.iemr.flw.masterEnum.FormType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * Repository for dynamic form definitions. + */ +@Repository +public interface DynamicFormRepo extends JpaRepository { + + Optional findByFormUuid(String formUuid); + + Optional findByFormUuidAndIsActive(String formUuid, Boolean isActive); + + List findByIsActiveOrderByFormIdAsc(Boolean isActive); + + Optional findByFormTypeAndIsActive(FormType formType, Boolean isActive); + + List findByIsActiveTrueAndFollowUpDelayDaysIsNotNull(); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java new file mode 100644 index 00000000..66672c16 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java @@ -0,0 +1,72 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.FormResponse; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.sql.Timestamp; +import java.util.List; + +/** + * Repository for form-level responses. + * + * @author Piramal Swasthya + */ +@Repository +public interface FormResponseRepo extends JpaRepository { + + List findByBeneficiaryIdAndFormId(Long beneficiaryId, Long formId); + + @Query("SELECT r.beneficiaryId FROM FormResponse r WHERE r.formId = :formId AND r.status = :status") + List findBeneficiaryIdsByFormIdAndStatus(@Param("formId") Long formId, @Param("status") String status); + + @Query("SELECT r.beneficiaryId FROM FormResponse r WHERE r.formId = :formId AND r.status = :status " + + "AND r.beneficiaryId IN (SELECT b.beneficiaryID FROM BenFlowStatus b WHERE b.deleted = false " + + "AND (:villageId IS NULL OR b.villageID = :villageId) " + + "AND (:providerServiceMapId IS NULL OR b.providerServiceMapId = :providerServiceMapId))") + List findBeneficiaryIdsByFormIdAndStatusFiltered(@Param("formId") Long formId, + @Param("status") String status, + @Param("villageId") Integer villageId, + @Param("providerServiceMapId") Integer providerServiceMapId); + + @Query("SELECT r.beneficiaryId FROM FormResponse r " + + "WHERE r.beneficiaryId IN :benIds " + + "AND r.formId = :formId " + + "AND r.status = :status") + List findCounselledBenIds(@Param("benIds") List benIds, + @Param("formId") Long formId, + @Param("status") String status); + + @Query("SELECT r FROM FormResponse r " + + "WHERE r.formId IN :formIds " + + "AND r.status = 'SUBMITTED' " + + "AND r.lastFollowUpAt >= :windowStart " + + "AND r.lastFollowUpAt < :windowEnd") + List findPendingFollowUps( + @Param("formIds") List formIds, + @Param("windowStart") Timestamp windowStart, + @Param("windowEnd") Timestamp windowEnd); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/FormSectionRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormSectionRepo.java new file mode 100644 index 00000000..b4064395 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/FormSectionRepo.java @@ -0,0 +1,38 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.FormSection; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * Repository for form sections. + * Each section belongs to one FormVersion via the version_id FK. + */ +@Repository +public interface FormSectionRepo extends JpaRepository { + + List findByFormVersion_VersionIdOrderByDisplayOrderAsc(Long versionId); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/FormVersionRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormVersionRepo.java new file mode 100644 index 00000000..1d8583ae --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/FormVersionRepo.java @@ -0,0 +1,46 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.FormVersion; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * Repository for form versions. + */ +@Repository +public interface FormVersionRepo extends JpaRepository { + + Optional findByDynamicForm_FormIdAndIsLatest(Long formId, Boolean isLatest); + + Optional findByDynamicForm_FormIdAndVersionNumber(Long formId, Integer versionNumber); + + Optional findTopByDynamicForm_FormIdOrderByVersionNumberDesc(Long formId); + + List findByDynamicForm_FormIdOrderByVersionNumberAsc(Long formId); + + Optional findByDynamicForm_FormUuidAndIsLatest(String formUuid, boolean b); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/OptionConditionRepo.java b/src/main/java/com/iemr/flw/repo/iemr/OptionConditionRepo.java new file mode 100644 index 00000000..63341889 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/OptionConditionRepo.java @@ -0,0 +1,55 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.OptionCondition; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; + +/** + * Repository for option conditions. + */ +@Repository +public interface OptionConditionRepo extends JpaRepository { + + List findByQuestionOption_OptionId(Long optionId); + + @Query("SELECT oc.targetQuestion.questionId FROM OptionCondition oc " + + "WHERE oc.questionOption.sectionQuestion.formSection.formVersion.versionId = :versionId " + + "AND oc.targetQuestion IS NOT NULL") + List findTargetQuestionIdsByVersionId(@Param("versionId") Long versionId); + + /** + * Loads all conditions for a set of options in one query. + * JOIN FETCH ensures questionOption, targetQuestion, and targetSection are hydrated + * so callers can group and resolve references without extra queries. + */ + @Query("SELECT c FROM OptionCondition c JOIN FETCH c.questionOption " + + "LEFT JOIN FETCH c.targetQuestion LEFT JOIN FETCH c.targetSection " + + "WHERE c.questionOption.optionId IN :optionIds") + List findByOptionIds(@Param("optionIds") Collection optionIds); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/PhyGeneralExaminationRepo.java b/src/main/java/com/iemr/flw/repo/iemr/PhyGeneralExaminationRepo.java new file mode 100644 index 00000000..abd73b0b --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/PhyGeneralExaminationRepo.java @@ -0,0 +1,9 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.PhyGeneralExamination; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PhyGeneralExaminationRepo extends JpaRepository { +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/PrescribedDrugDetailRepo.java b/src/main/java/com/iemr/flw/repo/iemr/PrescribedDrugDetailRepo.java new file mode 100644 index 00000000..bcdd8ab5 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/PrescribedDrugDetailRepo.java @@ -0,0 +1,9 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.PrescribedDrugDetail; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PrescribedDrugDetailRepo extends JpaRepository { +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/PrescriptionDetailRepo.java b/src/main/java/com/iemr/flw/repo/iemr/PrescriptionDetailRepo.java new file mode 100644 index 00000000..c0d425ec --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/PrescriptionDetailRepo.java @@ -0,0 +1,9 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.PrescriptionDetail; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PrescriptionDetailRepo extends JpaRepository { +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/QuestionOptionRepo.java b/src/main/java/com/iemr/flw/repo/iemr/QuestionOptionRepo.java new file mode 100644 index 00000000..f7512055 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/QuestionOptionRepo.java @@ -0,0 +1,48 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.QuestionOption; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; + +/** + * Repository for question options. + */ +@Repository +public interface QuestionOptionRepo extends JpaRepository { + + List findBySectionQuestion_QuestionIdOrderByDisplayOrderAsc(Long questionId); + + /** + * Loads all options for a set of questions in one query. + * JOIN FETCH ensures sectionQuestion is hydrated so callers can group by questionId without extra queries. + */ + @Query("SELECT o FROM QuestionOption o JOIN FETCH o.sectionQuestion " + + "WHERE o.sectionQuestion.questionId IN :questionIds ORDER BY o.displayOrder ASC") + List findByQuestionIdsOrderByDisplayOrderAsc(@Param("questionIds") Collection questionIds); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/QuestionResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/QuestionResponseRepo.java new file mode 100644 index 00000000..7dee9795 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/QuestionResponseRepo.java @@ -0,0 +1,46 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.QuestionResponse; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; + +/** + * Repository for question-level answers. + * + * @author Piramal Swasthya + */ +@Repository +public interface QuestionResponseRepo extends JpaRepository { + + List findBySectionResponseId(Long sectionResponseId); + + List findBySectionResponseIdIn(Collection sectionResponseIds); + + void deleteByQuestionIdAndSectionResponseId(Long questionId, Long sectionResponseId); + + void deleteBySectionResponseIdIn(Collection sectionResponseIds); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/QuestionValidationRepo.java b/src/main/java/com/iemr/flw/repo/iemr/QuestionValidationRepo.java new file mode 100644 index 00000000..d1cf4187 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/QuestionValidationRepo.java @@ -0,0 +1,48 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.QuestionValidation; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; + +/** + * Repository for question validation rules. + */ +@Repository +public interface QuestionValidationRepo extends JpaRepository { + + List findBySectionQuestion_QuestionId(Long questionId); + + /** + * Loads all validations for a set of questions in one query. + * JOIN FETCH ensures sectionQuestion is hydrated so callers can group by questionId without extra queries. + */ + @Query("SELECT v FROM QuestionValidation v JOIN FETCH v.sectionQuestion " + + "WHERE v.sectionQuestion.questionId IN :questionIds") + List findByQuestionIds(@Param("questionIds") Collection questionIds); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/SectionQuestionRepo.java b/src/main/java/com/iemr/flw/repo/iemr/SectionQuestionRepo.java new file mode 100644 index 00000000..6a117e96 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/SectionQuestionRepo.java @@ -0,0 +1,48 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.SectionQuestion; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; + +/** + * Repository for section questions. + */ +@Repository +public interface SectionQuestionRepo extends JpaRepository { + + List findByFormSection_SectionIdOrderByDisplayOrderAsc(Long sectionId); + + /** + * Loads all questions for a set of sections in one query. + * JOIN FETCH ensures formSection is hydrated so callers can group by sectionId without extra queries. + */ + @Query("SELECT q FROM SectionQuestion q JOIN FETCH q.formSection " + + "WHERE q.formSection.sectionId IN :sectionIds ORDER BY q.displayOrder ASC") + List findBySectionIdsOrderByDisplayOrderAsc(@Param("sectionIds") Collection sectionIds); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/SectionResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/SectionResponseRepo.java new file mode 100644 index 00000000..b83ad8e9 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/SectionResponseRepo.java @@ -0,0 +1,47 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.SectionResponse; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/** + * Repository for section-level responses. + * + * @author Piramal Swasthya + */ +@Repository +public interface SectionResponseRepo extends JpaRepository { + + List findByResponseId(Long responseId); + + Optional findByResponseIdAndSectionId(Long responseId, Long sectionId); + + List findByResponseIdIn(Collection responseIds); + + void deleteByResponseId(Long responseId); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBDiagnosticsRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBDiagnosticsRepo.java new file mode 100644 index 00000000..de3fa909 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBDiagnosticsRepo.java @@ -0,0 +1,31 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.StopTBDiagnostics; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Repository +public interface StopTBDiagnosticsRepo extends JpaRepository { + + @Query("SELECT d FROM StopTBDiagnostics d WHERE d.benRegID = :benRegID AND d.deleted = false") + StopTBDiagnostics findByBenRegID(@Param("benRegID") Long benRegID); + + @Query("SELECT d FROM StopTBDiagnostics d WHERE d.providerServiceMapID = :psmId AND d.deleted = false ORDER BY d.visitDate DESC") + List findAllByProviderServiceMapID(@Param("psmId") Integer psmId); + + @Query("SELECT d FROM StopTBDiagnostics d WHERE d.benRegID IN " + + "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId) " + + "AND d.deleted = false ORDER BY d.visitDate DESC") + List findAllByProviderServiceMapIDAndVillageID(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); + + @Transactional + @Modifying + @Query("UPDATE StopTBDiagnostics t SET t.vanSerialNo = t.id WHERE t.id = :id") + void updateVanSerialNo(@Param("id") Long id); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java new file mode 100644 index 00000000..944e99a6 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java @@ -0,0 +1,44 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.StopTBGeneralExamination; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Repository +public interface StopTBGeneralExaminationRepo extends JpaRepository { + + @Query("SELECT e FROM StopTBGeneralExamination e WHERE e.beneficiaryRegID = :beneficiaryRegID AND e.deleted = false") + StopTBGeneralExamination findByBeneficiaryRegID(@Param("beneficiaryRegID") Long beneficiaryRegID); + + // Read-side fix: a beneficiary can now have one row per visit, so the old single-result + // finder above throws NonUniqueResultException once a 2nd visit exists. Used by read/worklist + // endpoints that want "current" status — pass PageRequest.of(0, 1) to get just the latest. + @Query("SELECT e FROM StopTBGeneralExamination e WHERE e.beneficiaryRegID = :beneficiaryRegID " + + "AND e.deleted = false ORDER BY e.createdDate DESC") + List findLatestByBeneficiaryRegID(@Param("beneficiaryRegID") Long beneficiaryRegID, Pageable pageable); + + @Query("SELECT e FROM StopTBGeneralExamination e WHERE e.beneficiaryRegID = :beneficiaryRegID " + + "AND e.visitCode = :visitCode AND e.deleted = false") + StopTBGeneralExamination findByBeneficiaryRegIDAndVisitCode(@Param("beneficiaryRegID") Long beneficiaryRegID, + @Param("visitCode") Long visitCode); + + @Query("SELECT e FROM StopTBGeneralExamination e WHERE e.providerServiceMapID = :psmId AND e.deleted = false ORDER BY e.createdDate DESC") + List findAllByProviderServiceMapID(@Param("psmId") Integer psmId); + + @Query("SELECT e FROM StopTBGeneralExamination e WHERE e.beneficiaryRegID IN " + + "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId) " + + "AND e.deleted = false ORDER BY e.createdDate DESC") + List findAllByProviderServiceMapIDAndVillageID(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); + + @Transactional + @Modifying + @Query("UPDATE StopTBGeneralExamination t SET t.vanSerialNo = t.id WHERE t.id = :id") + void updateVanSerialNo(@Param("id") Long id); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java new file mode 100644 index 00000000..3cb8868f --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java @@ -0,0 +1,42 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.StopTBGeneralOpd; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Repository +public interface StopTBGeneralOpdRepo extends JpaRepository { + + @Query("SELECT o FROM StopTBGeneralOpd o WHERE o.benRegID = :benRegID AND o.deleted = false") + StopTBGeneralOpd findByBenRegID(@Param("benRegID") Long benRegID); + + // Read-side fix: same NonUniqueResultException risk once a beneficiary has more than one + // visit. Pass PageRequest.of(0, 1) for the latest only. + @Query("SELECT o FROM StopTBGeneralOpd o WHERE o.benRegID = :benRegID " + + "AND o.deleted = false ORDER BY o.createdDate DESC") + List findLatestByBenRegID(@Param("benRegID") Long benRegID, Pageable pageable); + + @Query("SELECT o FROM StopTBGeneralOpd o WHERE o.benRegID = :benRegID " + + "AND o.visitCode = :visitCode AND o.deleted = false") + StopTBGeneralOpd findByBenRegIDAndVisitCode(@Param("benRegID") Long benRegID, @Param("visitCode") Long visitCode); + + @Query("SELECT o FROM StopTBGeneralOpd o WHERE o.providerServiceMapID = :psmId AND o.deleted = false ORDER BY o.createdDate DESC") + List findAllByProviderServiceMapID(@Param("psmId") Integer psmId); + + @Query("SELECT o FROM StopTBGeneralOpd o WHERE o.benRegID IN " + + "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId) " + + "AND o.deleted = false ORDER BY o.createdDate DESC") + List findAllByProviderServiceMapIDAndVillageID(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); + + @Transactional + @Modifying + @Query("UPDATE StopTBGeneralOpd t SET t.vanSerialNo = t.id WHERE t.id = :id") + void updateVanSerialNo(@Param("id") Long id); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java b/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java index 31345dd4..122890e3 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java @@ -2,7 +2,11 @@ import com.iemr.flw.domain.iemr.TBConfirmedCase; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @@ -13,5 +17,14 @@ public interface TBConfirmedTreatmentRepository List findByBenId(Long benId); List findByUserId(Integer benId); + List findByBenIdAndVisitCode(Long benId, Long visitCode); + @Query("SELECT tc FROM TBConfirmedCase tc WHERE tc.benId IN " + + "(SELECT b.beneficiaryID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId)") + List getByProviderServiceMapIdAndVillageId(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); + + @Transactional + @Modifying + @Query("UPDATE TBConfirmedCase t SET t.vanSerialNo = t.id WHERE t.id = :id") + void updateVanSerialNo(@Param("id") Integer id); } diff --git a/src/main/java/com/iemr/flw/repo/iemr/TBScreeningRepo.java b/src/main/java/com/iemr/flw/repo/iemr/TBScreeningRepo.java index 61be2a57..f656dc91 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBScreeningRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBScreeningRepo.java @@ -1,10 +1,13 @@ package com.iemr.flw.repo.iemr; import com.iemr.flw.domain.iemr.TBScreening; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import java.sql.Timestamp; import java.util.List; @@ -17,4 +20,30 @@ public interface TBScreeningRepo extends JpaRepository { @Query(value = "SELECT tbs FROM TBScreening tbs WHERE tbs.userId = :userId") List getByUserId(@Param("userId") Integer userId); + + @Query("SELECT tbs FROM TBScreening tbs WHERE tbs.benRegID = :benRegID AND tbs.deleted = false") + TBScreening findByBenRegID(@Param("benRegID") Long benRegID); + + // Read-side fix: same NonUniqueResultException risk as StopTBGeneralExaminationRepo once a + // beneficiary has more than one visit. Pass PageRequest.of(0, 1) for the latest only. + @Query("SELECT tbs FROM TBScreening tbs WHERE tbs.benRegID = :benRegID " + + "AND tbs.deleted = false ORDER BY tbs.visitDate DESC") + List findLatestByBenRegID(@Param("benRegID") Long benRegID, Pageable pageable); + + @Query("SELECT tbs FROM TBScreening tbs WHERE tbs.benRegID = :benRegID " + + "AND tbs.visitCode = :visitCode AND tbs.deleted = false") + TBScreening findByBenRegIDAndVisitCode(@Param("benRegID") Long benRegID, @Param("visitCode") Long visitCode); + + @Query("SELECT tbs FROM TBScreening tbs WHERE tbs.providerServiceMapID = :psmId AND tbs.deleted = false ORDER BY tbs.visitDate DESC") + List findAllByProviderServiceMapID(@Param("psmId") Integer psmId); + + @Query("SELECT tbs FROM TBScreening tbs WHERE tbs.benRegID IN " + + "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId) " + + "AND tbs.deleted = false ORDER BY tbs.visitDate DESC") + List findAllByProviderServiceMapIDAndVillageID(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); + + @Transactional + @Modifying + @Query("UPDATE TBScreening t SET t.vanSerialNo = t.id WHERE t.id = :id") + void updateVanSerialNo(@Param("id") Long id); } diff --git a/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java b/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java index 995ca760..e559bdfb 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java @@ -2,9 +2,11 @@ import com.iemr.flw.domain.iemr.TBSuspected; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import java.sql.Timestamp; import java.util.List; @@ -18,6 +20,19 @@ public interface TBSuspectedRepo extends JpaRepository { @Query("SELECT tbs FROM TBSuspected tbs WHERE tbs.benId = :benId and tbs.userId = :userId") TBSuspected getByUserIdAndBenId(@Param("benId") Long benId, @Param("userId") Integer userId); + @Query("SELECT tbs FROM TBSuspected tbs WHERE tbs.benId = :benId and tbs.userId = :userId and tbs.visitCode = :visitCode") + TBSuspected getByUserIdAndBenIdAndVisitCode(@Param("benId") Long benId, @Param("userId") Integer userId, + @Param("visitCode") Long visitCode); + @Query("SELECT tbs FROM TBSuspected tbs WHERE tbs.userId = :userId and tbs.visitDate >= :fromDate and tbs.visitDate <= :toDate") List getByUserId(@Param("userId") Integer userId, @Param("fromDate") Timestamp fromDate, @Param("toDate") Timestamp toDate); + + @Query("SELECT tbs FROM TBSuspected tbs WHERE tbs.benId IN " + + "(SELECT b.beneficiaryID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId)") + List getByProviderServiceMapIdAndVillageId(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); + + @Transactional + @Modifying + @Query("UPDATE TBSuspected t SET t.vanSerialNo = t.id WHERE t.id = :id") + void updateVanSerialNo(@Param("id") Long id); } diff --git a/src/main/java/com/iemr/flw/repo/iemr/UserServiceRoleRepo.java b/src/main/java/com/iemr/flw/repo/iemr/UserServiceRoleRepo.java index 2b78b68f..f9e10e65 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/UserServiceRoleRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/UserServiceRoleRepo.java @@ -36,4 +36,41 @@ public interface UserServiceRoleRepo extends JpaRepository getDistrictByBlockId(@Param("blockId") Integer blockId); + + // Stop TB / Nikshay — additive only. Reads m_userservicerolemapping directly + // (NOT the shared v_userservicerolemapping view) so the view stays untouched + // and no other service line's query is affected. Returns nothing for any + // user whose rows don't have NikshayTUID set (i.e. every non-Stop-TB user). + // NikshayTUID/NikshayFacilityID are TEXT columns holding a comma-joined + // list of IDs per row (e.g. "12,45,78"), not a single ID, so the TU/Facility + // master tables are joined with FIND_IN_SET rather than plain equality — + // an equality join here would silently match only the first ID in the list + // (MySQL casts "12,45" to 12 for numeric comparison) and drop the rest. + // + // DistrictID on this row is a Nikshay district ID, not an AMRIT one (see + // setWorkLocationObject in Admin-UI) - resolving its name against + // m_nikshay_district (the same ID space) is safe, unlike joining it + // against AMRIT's own m_district, which would silently match whatever + // AMRIT district happens to share that same numeric ID by coincidence. + @Query(value = "SELECT " + + "GROUP_CONCAT(DISTINCT usrm.NikshayTUID ORDER BY usrm.NikshayTUID), " + + "GROUP_CONCAT(DISTINCT nt.TUName ORDER BY nt.NikshayTUID), " + + "GROUP_CONCAT(DISTINCT usrm.NikshayFacilityID ORDER BY usrm.NikshayFacilityID), " + + "GROUP_CONCAT(DISTINCT nf.FacilityName ORDER BY nf.NikshayFacilityID), " + + "usrm.DistrictID, nd.DistrictName " + + "FROM m_userservicerolemapping usrm " + + "LEFT JOIN m_nikshay_tu nt ON FIND_IN_SET(nt.NikshayTUID, usrm.NikshayTUID) > 0 " + + "LEFT JOIN m_nikshay_facility nf ON FIND_IN_SET(nf.NikshayFacilityID, usrm.NikshayFacilityID) > 0 " + + "LEFT JOIN m_nikshay_district nd ON nd.NikshayDistrictID = usrm.DistrictID " + + "WHERE usrm.UserID = :userId AND usrm.ProviderServiceMapID = :providerServiceMapId " + + "AND usrm.Deleted = false AND usrm.NikshayTUID IS NOT NULL " + + "GROUP BY usrm.DistrictID, nd.DistrictName", + nativeQuery = true) + List getNikshayLocationScope(@Param("userId") Integer userId, + @Param("providerServiceMapId") Integer providerServiceMapId); + } diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java new file mode 100644 index 00000000..4fdd3a2d --- /dev/null +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java @@ -0,0 +1,325 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.seeder; + +import com.iemr.flw.dto.iemr.DynamicFormDTO; +import com.iemr.flw.dto.iemr.FormSectionDTO; +import com.iemr.flw.dto.iemr.OptionConditionDTO; +import com.iemr.flw.dto.iemr.QuestionOptionDTO; +import com.iemr.flw.dto.iemr.QuestionValidationDTO; +import com.iemr.flw.dto.iemr.SectionQuestionDTO; +import com.iemr.flw.masterEnum.FormType; +import com.iemr.flw.masterEnum.QuestionType; +import com.iemr.flw.masterEnum.ValidationType; +import com.iemr.flw.repo.iemr.DynamicFormRepo; +import com.iemr.flw.service.DynamicFormDefinitionService; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * Seeds the TB Counselling form definition on application startup if it does not already exist. + * Idempotent: skips creation if the form UUID is already present in the database. + * + * @author Piramal Swasthya + */ +@Component +@RequiredArgsConstructor +public class TbCounsellingFormSeeder { + + private static final Logger log = LoggerFactory.getLogger(TbCounsellingFormSeeder.class); + + public static final String FORM_UUID = "TB_COUNSELLING"; + + private final DynamicFormDefinitionService formService; + private final DynamicFormRepo formRepo; + + @PostConstruct + public void seed() { + if (formRepo.findByFormUuid(FORM_UUID).isPresent()) { + log.info("TbCounsellingFormSeeder: form '{}' already exists — skipping seed.", FORM_UUID); + return; + } + log.info("TbCounsellingFormSeeder: seeding TB Counselling form..."); + formService.createForm(buildFormDto()); + log.info("TbCounsellingFormSeeder: seed complete."); + } + + // ── Form Definition ────────────────────────────────────────────────────────── + + private DynamicFormDTO buildFormDto() { + DynamicFormDTO dto = new DynamicFormDTO(); + dto.setFormUuid(FORM_UUID); + dto.setFormName("TB Counselling"); + dto.setFormType(FormType.TB_COUNSELLING); + dto.setIsActive(true); + dto.setFollowUpDelayDays(15); + + List sections = new ArrayList<>(); + sections.add(buildSectionA()); + sections.add(buildSectionB()); + sections.add(buildSectionC()); + sections.add(buildSectionD()); + sections.add(buildSectionE()); +// sections.add(buildSectionF()); + dto.setSections(sections); + return dto; + } + + // ── Section A: Disease Awareness ───────────────────────────────────────────── + + private FormSectionDTO buildSectionA() { + FormSectionDTO s = section("TB_SEC_A", "Disease Awareness", "बीमारी के बारे में जागरूकता", "PRE_SUBMIT", 1, true, false); + List qs = new ArrayList<>(); + qs.add(yesNoRadio("TB_A_Q1", "TB disease explained to patient", "मरीज़ को टीबी बीमारी के बारे में समझाया गया।", 1, true)); + qs.add(yesNoRadio("TB_A_Q2", "Transmission route explained", "ट्रांसमिशन के तरीके के बारे में जानकारी", 2, true)); + qs.add(yesNoRadio("TB_A_Q3", "Symptoms explained", "लक्षणों की जानकारी", 3, true)); + qs.add(yesNoRadio("TB_A_Q4", "Treatment duration explained", "इलाज की अवधि के बारे में जानकारी", 4, true)); + qs.add(textQuestion("TB_A_REMARKS", "Disease awareness notes", "बीमारी के बारे में जानकारी देने वाले नोट्स", 5, false, 500, true)); + s.setQuestions(qs); + return s; + } + + // ── Section B: Do's and Don'ts ─────────────────────────────────────────────── + + private FormSectionDTO buildSectionB() { + FormSectionDTO s = section("TB_SEC_B", "Do's and Don'ts", "करो और ना करो", "PRE_SUBMIT", 2, true, false); + List qs = new ArrayList<>(); + qs.add(yesNoRadio("TB_B_Q1", "Cover mouth while coughing — advised", "खांसते समय मुंह ढकने की सलाह दी जाती है।", 1, true)); + qs.add(yesNoRadio("TB_B_Q2", "Complete full treatment course — advised", "इलाज का पूरा कोर्स पूरा करने की सलाह दी जाती है।", 2, true)); + qs.add(yesNoRadio("TB_B_Q3", "Regular follow-up attendance — advised", "इलाज का पूरा कोर्स पूरा करने की सलाह दी जाती है।", 3, true)); + qs.add(yesNoRadio("TB_B_Q4", "Nutritional guidance provided", "पोषण संबंधी मार्गदर्शन प्रदान किया गया", 4, true)); + qs.add(yesNoRadio("TB_B_Q5", "No smoking / alcohol — advised", "धूम्रपान / शराब न लेने की सलाह दी जाती है।", 5, true)); + qs.add(yesNoRadio("TB_B_Q6", "Isolation precautions explained", "अलगाव सावधानियों के बारे में बताया गया", 6, true)); + qs.add(textQuestion("TB_B_REMARKS", "Do's & Don'ts notes", "क्या करें और क्या न करें - नोट्स", 7, false, 500, true)); + s.setQuestions(qs); + return s; + } + + // ── Section C: Government Schemes ──────────────────────────────────────────── + + private FormSectionDTO buildSectionC() { + FormSectionDTO s = section("TB_SEC_C", "Government Schemes", "सरकारी योजनाएं", "PRE_SUBMIT", 3, false, false); + List qs = new ArrayList<>(); + qs.add(yesNoRadio("TB_C_Q1", "Nikshay Poshan Yojana (NPY) eligibility explained", "निक्षय पोषण योजना (एनपीवाई) पात्रता के बारे में बताया गया", 1, true)); + qs.add(yesNoRadio("TB_C_Q2", "DOTS free treatment explained", "DOTS मुफ़्त इलाज के बारे में जानकारी", 2, true)); + qs.add(textQuestion("TB_C_REMARKS", "Schemes notes", "योजनाओं के नोट्स", 3, false, 300, true)); + s.setQuestions(qs); + return s; + } + + // ── Section D: Treatment Regimen ───────────────────────────────────────────── + + private FormSectionDTO buildSectionD() { + FormSectionDTO s = section("TB_SEC_D", "Treatment Regimen", "इलाज का तरीका", "PRE_SUBMIT", 4, true, false); + List qs = new ArrayList<>(); + qs.add(yesNoRadio("TB_D_Q1", "Regimen explained to patient", "मरीज़ को इलाज का तरीका समझाया गया।", 1, true)); + qs.add(yesNoRadio("TB_D_Q2", "Medication names explained", "दवाओं के नामों की जानकारी", 2, true)); + qs.add(yesNoRadio("TB_D_Q3", "Side effects explained", "साइड इफ़ेक्ट्स के बारे में जानकारी", 3, true)); + qs.add(yesNoRadio("TB_D_Q4", "Importance of adherence explained", "निर्देशों का पालन करने का महत्व समझाया गया", 4, true)); + qs.add(textQuestion("TB_D_REMARKS", "Treatment regimen notes", "इलाज के तरीके से जुड़े नोट्स", 5, false, 300, true)); + s.setQuestions(qs); + return s; + } + + // ── Section E: Counselling Completion ──────────────────────────────────────── + + private FormSectionDTO buildSectionE() { + FormSectionDTO s = section("TB_SEC_E", "Counselling Completion", "काउंसलिंग पूरी होना", "PRE_SUBMIT", 5, true, true); + List qs = new ArrayList<>(); + + // Counselling completion status — RADIO: Complete | Refused + SectionQuestionDTO statusQ = new SectionQuestionDTO(); + statusQ.setQuestionUuid("TB_E_Q1"); + statusQ.setQuestionText("Counselling completion status"); + statusQ.setQuestionTextHindi("काउंसलिंग पूरी होने की स्थिति"); + statusQ.setQuestionType(QuestionType.RADIO); + statusQ.setIsMandatory(true); + statusQ.setDisplayOrder(1); + statusQ.setVisibleByDefault(true); + + // "Complete" option — no conditions + QuestionOptionDTO completeOpt = option("Complete", "सम्पूर्ण","COMPLETE", "सम्पूर्ण", 1, List.of()); + + // "Refused" option — 5 conditions: disable sections A-D + show refusal text box + List refusedConditions = new ArrayList<>(); + refusedConditions.add(disableSectionValidation("TB_SEC_A")); + refusedConditions.add(disableSectionValidation("TB_SEC_B")); + refusedConditions.add(disableSectionValidation("TB_SEC_C")); + refusedConditions.add(disableSectionValidation("TB_SEC_D")); + refusedConditions.add(showQuestion("TB_E_REFUSAL")); + QuestionOptionDTO refusedOpt = option("Refused", "अस्वीकार करना", "REFUSED", "अस्वीकार करना", 2, refusedConditions); + + List statusOptions = new ArrayList<>(); + statusOptions.add(completeOpt); + statusOptions.add(refusedOpt); + statusQ.setOptions(statusOptions); + statusQ.setValidations(List.of()); + qs.add(statusQ); + + // Reason for refusal — hidden by default, max 300 chars + qs.add(textQuestion("TB_E_REFUSAL", "Reason for refusal", "इनकार का कारण",2, true, 300, false)); + + // Counsellor remarks — optional, max 500 chars + qs.add(textQuestion("TB_E_REMARKS", "Counsellor remarks", "काउंसलर की टिप्पणी", 3, false, 500, true)); + + s.setQuestions(qs); + return s; + } + + // ── Section F: Follow Up to TU ─────────────────────────────────────────────── + + private FormSectionDTO buildSectionF() { + FormSectionDTO s = section("TB_SEC_F", "Follow Up to TU", "TU के बाद की कार्रवाई", "POST_SUBMIT", 6, true, true); + List qs = new ArrayList<>(); + + // "Has the patient started TB treatment?" — RADIO: Yes | No + SectionQuestionDTO startedQ = new SectionQuestionDTO(); + startedQ.setQuestionUuid("TB_F_Q1"); + startedQ.setQuestionText("Has the patient started the prescribed TB treatment regimen?"); + startedQ.setQuestionTextHindi("क्या मरीज़ ने टीबी के लिए बताया गया इलाज शुरू कर दिया है?"); + startedQ.setQuestionType(QuestionType.RADIO); + startedQ.setIsMandatory(true); + startedQ.setDisplayOrder(1); + startedQ.setVisibleByDefault(true); + + QuestionOptionDTO yesOpt = option("Yes", "हाँ", "YES", "हाँ", 1, List.of()); + + List noConditions = new ArrayList<>(); + noConditions.add(showQuestion("TB_F_NO_REASON")); + QuestionOptionDTO noOpt = option("No", "नहीं", "NO", "नहीं", 2, noConditions); + + List startedOptions = new ArrayList<>(); + startedOptions.add(yesOpt); + startedOptions.add(noOpt); + startedQ.setOptions(startedOptions); + startedQ.setValidations(List.of()); + qs.add(startedQ); + + // Reason for not starting — hidden by default, max 500 chars + qs.add(textQuestion("TB_F_NO_REASON", + "Reason for not starting the prescribed TB treatment regimen", + "टीबी के लिए तय इलाज का तरीका शुरू न करने का कारण", + 2, false, 500, false)); + + // DOTS centre visit + qs.add(yesNoRadio("TB_F_Q2", + "Has the patient visited the DOTS centre / referred health facility for treatment collection?", + "क्या मरीज़ इलाज लेने के लिए DOTS सेंटर या रेफर किए गए स्वास्थ्य केंद्र गया है?", + 3, true)); + + // Side effects reported + qs.add(yesNoRadio("TB_F_Q3", + "Has the patient reported side effects to the treating doctor or DOTS centre?", + "क्या मरीज़ ने इलाज करने वाले डॉक्टर या DOTS सेंटर को साइड इफ़ेक्ट्स के बारे में बताया है?", + 4, true)); + + s.setQuestions(qs); + return s; + } + + // ── Builder Helpers ─────────────────────────────────────────────────────────── + + private FormSectionDTO section(String uuid, String name, String nameHindi, String phase, + int order, boolean required, boolean hasSubmitButton) { + FormSectionDTO s = new FormSectionDTO(); + s.setSectionUuid(uuid); + s.setSectionName(name); + s.setSectionNameHindi(nameHindi); + s.setSectionPhase(phase); + s.setDisplayOrder(order); + s.setIsRequired(required); + s.setHasSubmitButton(hasSubmitButton); + s.setQuestions(new ArrayList<>()); + return s; + } + + private SectionQuestionDTO yesNoRadio(String uuid, String text, String textHindi, int order, boolean mandatory) { + SectionQuestionDTO q = new SectionQuestionDTO(); + q.setQuestionUuid(uuid); + q.setQuestionText(text); + q.setQuestionTextHindi(textHindi); + q.setQuestionType(QuestionType.RADIO); + q.setIsMandatory(mandatory); + q.setDisplayOrder(order); + q.setVisibleByDefault(true); + + List opts = new ArrayList<>(); + opts.add(option("Yes", "हाँ","YES","हाँ", 1, List.of())); + opts.add(option("No", "नहीं","NO", "नहीं", 2, List.of())); + q.setOptions(opts); + q.setValidations(List.of()); + return q; + } + + private SectionQuestionDTO textQuestion(String uuid, String text, String textHindi, int order, + boolean mandatory, int maxLength, boolean visible) { + SectionQuestionDTO q = new SectionQuestionDTO(); + q.setQuestionUuid(uuid); + q.setQuestionText(text); + q.setQuestionTextHindi(textHindi); + q.setQuestionType(QuestionType.TEXT); + q.setIsMandatory(mandatory); + q.setDisplayOrder(order); + q.setMaxLength(maxLength); + q.setVisibleByDefault(visible); + q.setOptions(List.of()); + + QuestionValidationDTO v = new QuestionValidationDTO(); + v.setValidationType(ValidationType.MAX_LENGTH); + v.setValidationParam(String.valueOf(maxLength)); + v.setErrorMessage("Must be " + maxLength + " characters or fewer"); + q.setValidations(List.of(v)); + return q; + } + + private QuestionOptionDTO option(String label, String labelHindi, String value, String valueHindi, int order, + List conditions) { + QuestionOptionDTO o = new QuestionOptionDTO(); + o.setOptionLabel(label); + o.setOptionLabelHindi(labelHindi); + o.setOptionValue(value); + o.setOptionValueHindi(valueHindi); + o.setDisplayOrder(order); + o.setConditions(conditions); + return o; + } + + private OptionConditionDTO showQuestion(String targetQuestionUuid) { + OptionConditionDTO c = new OptionConditionDTO(); + c.setActionType("SHOW_QUESTION"); + c.setTargetQuestionUuid(targetQuestionUuid); + return c; + } + + private OptionConditionDTO disableSectionValidation(String targetSectionUuid) { + OptionConditionDTO c = new OptionConditionDTO(); + c.setActionType("DISABLE_SECTION_VALIDATION"); + c.setTargetSectionUuid(targetSectionUuid); + return c; + } +} diff --git a/src/main/java/com/iemr/flw/service/CampConfigService.java b/src/main/java/com/iemr/flw/service/CampConfigService.java new file mode 100644 index 00000000..a8c8ad35 --- /dev/null +++ b/src/main/java/com/iemr/flw/service/CampConfigService.java @@ -0,0 +1,59 @@ +package com.iemr.flw.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.stereotype.Service; + +@Service +public class CampConfigService { + + private static final Logger logger = LoggerFactory.getLogger(CampConfigService.class); + private static final String VAN_ID_KEY = "camp:vanID"; + private static final String PARKING_PLACE_ID_KEY = "camp:parkingPlaceID"; + + @Autowired + private LettuceConnectionFactory connectionFactory; + + // When true, saves fail loudly if camp is not configured instead of silently storing vanID=NULL + @Value("${stoptb.enforce.vanid:false}") + private boolean enforceVanID; + + public Integer getVanID() { + String val = read(VAN_ID_KEY); + if (val == null || val.isBlank()) { + if (enforceVanID) { + throw new IllegalStateException( + "Camp not configured: vanID missing. Please select van/service point in MMU before saving Stop TB data."); + } + return null; + } + return Integer.parseInt(val); + } + + public Integer getParkingPlaceID() { + String val = read(PARKING_PLACE_ID_KEY); + if (val == null || val.isBlank()) return 0; + return Integer.parseInt(val); + } + + public boolean isCampConfigured() { + String val = read(VAN_ID_KEY); + return val != null && !val.isBlank(); + } + + private String read(String key) { + try { + RedisConnection conn = connectionFactory.getConnection(); + byte[] data = conn.get(key.getBytes()); + conn.close(); + return data != null ? new String(data) : null; + } catch (Exception e) { + logger.error("Redis read error for key {}: {}", key, e.getMessage()); + return null; + } + } +} diff --git a/src/main/java/com/iemr/flw/service/DynamicFormDefinitionService.java b/src/main/java/com/iemr/flw/service/DynamicFormDefinitionService.java new file mode 100644 index 00000000..c7aae24d --- /dev/null +++ b/src/main/java/com/iemr/flw/service/DynamicFormDefinitionService.java @@ -0,0 +1,59 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.service; + +import com.iemr.flw.dto.iemr.DynamicFormDTO; +import com.iemr.flw.dto.iemr.FormSectionDTO; +import com.iemr.flw.dto.iemr.OptionConditionDTO; +import com.iemr.flw.dto.iemr.QuestionOptionDTO; +import com.iemr.flw.dto.iemr.QuestionValidationDTO; +import com.iemr.flw.dto.iemr.SectionQuestionDTO; + +import java.util.List; + +/** + * Service for managing dynamic form definitions (structure only — not responses). + */ +public interface DynamicFormDefinitionService { + + /** Creates a full form definition (form + v1 + sections + questions + options + validations). */ + DynamicFormDTO createForm(DynamicFormDTO formDTO); + + /** Replaces the form structure with a new version, bumping versionNumber. */ + DynamicFormDTO updateForm(Long formId, DynamicFormDTO formDTO); + + /** Returns the latest version's full definition tree, Redis-cached by formId. */ + DynamicFormDTO getFormDefinition(Long formId); + + /** Returns a specific version's full definition tree (no cache). */ + DynamicFormDTO getFormDefinitionByVersion(Long formId, Integer versionNumber); + + /** Returns metadata for all active forms (without full tree). */ + List getAllForms(); + + /** Sets the form's isActive flag to true. */ + void activateForm(Long formId); + + /** Sets the form's isActive flag to false and invalidates cache. */ + void deactivateForm(Long formId); + +} diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java new file mode 100644 index 00000000..cca29e2e --- /dev/null +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -0,0 +1,66 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.service; + +import com.iemr.flw.dto.iemr.FormResponseDTO; +import com.iemr.flw.dto.iemr.FormResponseRequest; +import com.iemr.flw.masterEnum.FormType; +import com.iemr.flw.utils.exception.IEMRException; + +import java.util.List; + +/** + * Contract for saving and retrieving dynamic form responses. + * + * @author Piramal Swasthya + */ +public interface DynamicFormResponseService { + + /** Save PRE_SUBMIT section answers and advance status to SUBMITTED. */ + FormResponseDTO submitForm(FormResponseRequest request); + + /** Save POST_SUBMIT section answers and advance status to COMPLETE. */ + FormResponseDTO completeForm(FormResponseRequest request, String jwtToken) throws IEMRException; + + /** All responses for a beneficiary filtered by form UUID. */ + List getResponsesByBeneficiary(Long beneficiaryId, String formUuid); + + /** Single response with all nested section and question answers. */ + FormResponseDTO getResponseById(Long responseId); + + /** + * Submit multiple form responses in one bulk transaction. + * All FormResponse rows are batch-inserted via saveAll() so responseIds are available + * before section/question processing begins. All-or-nothing: any failure rolls back all. + */ + List submitBulk(List requests, String jwtToken); + + /** + * Returns SUBMITTED responses for the given form IDs whose {@code lastFollowUpAt} + * falls within the 24-hour window that started exactly {@code delayDays} days ago. + * Used by the follow-up notification scheduler. + */ + List findPendingFollowUps(List formIds, int delayDays); + + /** Returns beneficiary IDs with COMPLETE status for the given form type, optionally filtered by village and/or provider service map. */ + List getCompletedBeneficiaries(FormType formType, Integer villageId, Integer providerServiceMapId); +} diff --git a/src/main/java/com/iemr/flw/service/StopTBService.java b/src/main/java/com/iemr/flw/service/StopTBService.java new file mode 100644 index 00000000..e549daa6 --- /dev/null +++ b/src/main/java/com/iemr/flw/service/StopTBService.java @@ -0,0 +1,22 @@ +package com.iemr.flw.service; + +import java.util.List; +import java.util.Map; + +public interface StopTBService { + // Nurse — General Examination + List> saveGeneralExamination(List> dataList) throws Exception; + Map getAllGeneralExaminations(Integer providerServiceMapID, Integer villageID) throws Exception; + + // Nurse — TB Screening + List> saveNurseTBScreening(List> dataList) throws Exception; + Map getAllNurseTBScreenings(Integer providerServiceMapID, Integer villageID) throws Exception; + + // Nurse — General OPD + List> saveGeneralOpd(List> dataList) throws Exception; + Map getAllGeneralOpd(Integer providerServiceMapID, Integer villageID) throws Exception; + + // Nurse — Diagnostics + List> saveDiagnostics(List> dataList) throws Exception; + Map getAllDiagnostics(Integer providerServiceMapID, Integer villageID) throws Exception; +} diff --git a/src/main/java/com/iemr/flw/service/TBConfirmedCaseService.java b/src/main/java/com/iemr/flw/service/TBConfirmedCaseService.java index 64505b79..6471f799 100644 --- a/src/main/java/com/iemr/flw/service/TBConfirmedCaseService.java +++ b/src/main/java/com/iemr/flw/service/TBConfirmedCaseService.java @@ -13,4 +13,6 @@ public interface TBConfirmedCaseService { String getByBenId(Long benId, String authorisation) throws Exception; String getByUserId(String authorisation) throws Exception; + + String getByProviderServiceMapId(Integer providerServiceMapID, Integer villageID) throws Exception; } diff --git a/src/main/java/com/iemr/flw/service/TBStopVisitService.java b/src/main/java/com/iemr/flw/service/TBStopVisitService.java new file mode 100644 index 00000000..0e15fd9b --- /dev/null +++ b/src/main/java/com/iemr/flw/service/TBStopVisitService.java @@ -0,0 +1,9 @@ +package com.iemr.flw.service; + +import com.iemr.flw.domain.iemr.BenVisitDetail; + +public interface TBStopVisitService { + + BenVisitDetail getOrCreateVisitForToday(Long beneficiaryRegID, Integer providerServiceMapID, + String createdBy, Integer vanID, Integer parkingPlaceID); +} diff --git a/src/main/java/com/iemr/flw/service/impl/AbhaBeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/AbhaBeneficiaryServiceImpl.java index 15ca0e7c..f9098c39 100644 --- a/src/main/java/com/iemr/flw/service/impl/AbhaBeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/AbhaBeneficiaryServiceImpl.java @@ -3,6 +3,7 @@ import com.google.gson.Gson; import com.iemr.flw.controller.AbhaBeneficiaryController; import com.iemr.flw.domain.iemr.AbhaApiResponse; +import com.iemr.flw.domain.identity.RMNCHMBeneficiarymapping; import com.iemr.flw.dto.abhaBeneficiary.AbhaBeneficiaryDTO; import com.iemr.flw.dto.iemr.AbhaRequestDTO; import com.iemr.flw.repo.identity.BeneficiaryRepo; @@ -55,7 +56,10 @@ public Object getBeneficiaryByAbha(AbhaRequestDTO request) { if(request.getHouseHoldId()!=null){ benRedId = beneficiaryRepo.findByHouseoldId(request.getHouseHoldId()).get(0).getBenRegId(); if(benRedId!=null){ - benDetailsdId = beneficiaryRepo.findByBenRegIdFromMapping(BigInteger.valueOf(benRedId)).getBenDetailsId(); + List mappings = beneficiaryRepo.findByBenRegIdFromMapping(BigInteger.valueOf(benRedId)); + if (!mappings.isEmpty()) { + benDetailsdId = mappings.get(0).getBenDetailsId(); + } } if(benDetailsdId!=null){ diff --git a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index 23c4bd60..b70ce80e 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -1,5 +1,6 @@ package com.iemr.flw.service.impl; +import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Date; import java.sql.Timestamp; @@ -21,6 +22,7 @@ import com.iemr.flw.dto.iemr.EyeCheckupListDTO; import com.iemr.flw.dto.iemr.EyeCheckupRequestDTO; import com.iemr.flw.masterEnum.GroupName; +import com.iemr.flw.domain.iemr.BenFlowStatus; import com.iemr.flw.repo.iemr.*; import com.iemr.flw.service.IncentiveLogicService; import com.iemr.flw.service.UserService; @@ -54,8 +56,12 @@ import com.iemr.flw.domain.identity.RMNCHMBeneficiarymapping; import com.iemr.flw.dto.identity.GetBenRequestHandler; import com.iemr.flw.mapper.InputMapper; +import com.iemr.flw.domain.iemr.BenAnthropometryDetail; +import com.iemr.flw.domain.iemr.BenPhysicalVitalDetail; import com.iemr.flw.repo.identity.BeneficiaryRepo; import com.iemr.flw.repo.identity.HouseHoldRepo; +import com.iemr.flw.repo.iemr.BenAnthropometryRepo; +import com.iemr.flw.repo.iemr.BenPhysicalVitalRepo; import com.iemr.flw.service.BeneficiaryService; import com.iemr.flw.utils.config.ConfigProperties; import com.iemr.flw.utils.http.HttpUtils; @@ -101,20 +107,68 @@ public class BeneficiaryServiceImpl implements BeneficiaryService { @Autowired private IncentiveLogicService incentiveLogicService; + @Autowired + private BenFlowStatusRepo benFlowStatusRepo; + + @Autowired + private UserServiceRoleRepo userRepo; + + @Autowired + private BenAnthropometryRepo benAnthropometryRepo; + + @Autowired + private BenPhysicalVitalRepo benPhysicalVitalRepo; + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy"); @Override public String getBenData(GetBenRequestHandler request, String authorisation) throws Exception { - if (request == null || request.getAshaId() == null) { - throw new Exception("Invalid/missing asha details"); + if (request == null) { + throw new Exception("Invalid request"); } if (request.getPageNo() == null || request.getPageNo() < 0) { throw new Exception("Invalid page number"); } + int pageSize = Integer.parseInt(door_to_door_page_size); + + // Stop TB path: filter by providerServiceMapID + villageID + if (request.getProviderServiceMapID() != null && request.getVillageID() != null) { + List flows = benFlowStatusRepo.getRegistrarWorklist( + request.getProviderServiceMapID(), request.getVillageID()); + + if (flows == null || flows.isEmpty()) return null; + + List allAddresses = new ArrayList<>(); + for (BenFlowStatus flow : flows) { + if (flow.getBeneficiaryRegID() == null) continue; + List mappings = beneficiaryRepo.findByBenRegIdFromMapping( + BigInteger.valueOf(flow.getBeneficiaryRegID())); + if (mappings.isEmpty()) continue; + RMNCHMBeneficiarymapping mapping = mappings.get(0); + if (mapping.getBenAddressId() == null) continue; + RMNCHMBeneficiaryaddress address = beneficiaryRepo.getAddressById(mapping.getBenAddressId()); + if (address != null) allAddresses.add(address); + } + + if (allAddresses.isEmpty()) return null; + + int totalPage = (int) Math.ceil((double) allAddresses.size() / pageSize); + int start = request.getPageNo() * pageSize; + int end = Math.min(start + pageSize, allAddresses.size()); + if (start >= allAddresses.size()) return null; + + return getMappingsForAddressIDs(allAddresses.subList(start, end), totalPage, authorisation); + } + + // Normal FLW/ASHA path + if (request.getAshaId() == null) { + throw new Exception("Invalid/missing asha details"); + } + String userName = beneficiaryRepo.getUserName(request.getAshaId()); if (userName == null || userName.isEmpty()) { @@ -123,12 +177,10 @@ public String getBenData(GetBenRequestHandler request, String authorisation) thr request.setUserName(userName); - int pageSize = Integer.parseInt(door_to_door_page_size); PageRequest pageRequest = PageRequest.of(request.getPageNo(), pageSize); Page pageResult; - // ✅ Date Filter Handling if (request.getFromDate() != null && request.getToDate() != null) { if (request.getFromDate().after(request.getToDate())) { @@ -143,7 +195,6 @@ public String getBenData(GetBenRequestHandler request, String authorisation) thr ); } else { - pageResult = beneficiaryRepo.getBenDataByUser(userName, pageRequest); } @@ -215,11 +266,9 @@ private String getMappingsForAddressIDs(List addressLi .getDetailsByRegID((m.getBenRegId()).longValue()).isEmpty()){ benDetailsRMNCH_OBJ = beneficiaryRepo .getDetailsByRegID((m.getBenRegId()).longValue()).get(0); - } benBotnBirthRMNCH_ROBJ = beneficiaryRepo.getBornBirthByRegID((m.getBenRegId()).longValue()); - if (benDetailsRMNCH_OBJ != null && benDetailsRMNCH_OBJ.getHouseoldId() != null) if(!houseHoldRepo .getByHouseHoldID(benDetailsRMNCH_OBJ.getHouseoldId()).isEmpty()){ @@ -294,6 +343,29 @@ private String getMappingsForAddressIDs(List addressLi benDetailsRMNCH_OBJ.setAddressLine2(benAddressOBJ.getPermAddrLine2()); if (benAddressOBJ.getPermAddrLine3() != null) benDetailsRMNCH_OBJ.setAddressLine3(benAddressOBJ.getPermAddrLine3()); + if (benAddressOBJ.getPermPinCode() != null) + benDetailsRMNCH_OBJ.setPinCode(benAddressOBJ.getPermPinCode()); + + // GPS fallback: if not in RMNCH details (syncdatatoamrti not yet called), + // pull from i_beneficiaryaddress (saved during TM-API registration) + if (benDetailsRMNCH_OBJ.getGpsLatitude() == null && benAddressOBJ.getGpsLatitude() != null) + benDetailsRMNCH_OBJ.setGpsLatitude(benAddressOBJ.getGpsLatitude()); + if (benDetailsRMNCH_OBJ.getGpsLongitude() == null && benAddressOBJ.getGpsLongitude() != null) + benDetailsRMNCH_OBJ.setGpsLongitude(benAddressOBJ.getGpsLongitude()); + if (benDetailsRMNCH_OBJ.getDigipin() == null && benAddressOBJ.getDigipin() != null) + benDetailsRMNCH_OBJ.setDigipin(benAddressOBJ.getDigipin()); + if (benDetailsRMNCH_OBJ.getGpsTimestamp() == null && benAddressOBJ.getGpsTimestamp() != null) + benDetailsRMNCH_OBJ.setGpsTimestamp(benAddressOBJ.getGpsTimestamp()); + if (benDetailsRMNCH_OBJ.getIsGpsUnavailable() == null && benAddressOBJ.getIsGpsUnavailable() != null) + benDetailsRMNCH_OBJ.setIsGpsUnavailable(benAddressOBJ.getIsGpsUnavailable()); + if (benDetailsRMNCH_OBJ.getGpsUnavailableReason() == null && benAddressOBJ.getGpsUnavailableReason() != null) + benDetailsRMNCH_OBJ.setGpsUnavailableReason(benAddressOBJ.getGpsUnavailableReason()); + + // Map GPS double fields to the exposed latitude/longitude BigDecimal fields for response + if (benDetailsRMNCH_OBJ.getGpsLatitude() != null) + benDetailsRMNCH_OBJ.setLatitude(BigDecimal.valueOf(benDetailsRMNCH_OBJ.getGpsLatitude())); + if (benDetailsRMNCH_OBJ.getGpsLongitude() != null) + benDetailsRMNCH_OBJ.setLongitude(BigDecimal.valueOf(benDetailsRMNCH_OBJ.getGpsLongitude())); // ----------------------------------------------------------------------------- @@ -319,8 +391,15 @@ private String getMappingsForAddressIDs(List addressLi if (benContactOBJ.getPreferredPhoneNum() != null) benDetailsRMNCH_OBJ.setContact_number(benContactOBJ.getPreferredPhoneNum()); - if (benDetailsOBJ.getDob() != null) + if (benDetailsOBJ.getDob() != null) { benDetailsRMNCH_OBJ.setDob(benDetailsOBJ.getDob()); + } else { + // i_beneficiarydetails.dob is null for Stop TB mobile registrations + // (Identity-API mapper commented out) — fall back to i_ben_flow_outreach.ben_dob + List flows = benFlowStatusRepo.findByBeneficiaryRegID(m.getBenRegId().longValue()); + if (!flows.isEmpty() && flows.get(0).getDob() != null) + benDetailsRMNCH_OBJ.setDob(flows.get(0).getDob()); + } if (benDetailsOBJ.getFatherName() != null) benDetailsRMNCH_OBJ.setFatherName(benDetailsOBJ.getFatherName()); if (benDetailsOBJ.getFirstName() != null) @@ -363,9 +442,9 @@ private String getMappingsForAddressIDs(List addressLi String ageDetails = ""; int age_val = 0; String ageUnit = null; - if (benDetailsOBJ.getDob() != null) { + if (benDetailsRMNCH_OBJ.getDob() != null) { - Date date = new Date(benDetailsOBJ.getDob().getTime()); + Date date = new Date(benDetailsRMNCH_OBJ.getDob().getTime()); Calendar cal = Calendar.getInstance(); cal.setTime(date); @@ -438,8 +517,82 @@ private String getMappingsForAddressIDs(List addressLi ? benDetailsRMNCH_OBJ.getIsDeactivate() : false ); + resultMap.put("economicStatus",benDetailsOBJ.getEconomicStatus()); + resultMap.put("economicStatusId",benDetailsOBJ.getEconomicStatusId()); + resultMap.put("residentialAreaId",benDetailsOBJ.getResidentialAreaId()); + resultMap.put("residentialArea",benDetailsOBJ.getResidentialArea()); + resultMap.put("address",benDetailsOBJ.getAddress()); + resultMap.put("updateDate", benDetailsOBJ.getLastModDate()); + resultMap.put("updatedBy", benDetailsOBJ.getModifiedBy()); resultMap.put("BenRegId", m.getBenRegId()); + // occupation from i_beneficiarydetails + if (benDetailsOBJ != null && benDetailsOBJ.getOccupation() != null) + benDetailsRMNCH_OBJ.setOccupation(benDetailsOBJ.getOccupation()); + + // anthropometry from t_phy_anthropometry, vitals from t_phy_vitals + // fallback to otherFields if exam not yet saved for this beneficiary + Long benRegIdLong = m.getBenRegId() != null ? m.getBenRegId().longValue() : null; + if (benRegIdLong != null) { + List anthroList = + benAnthropometryRepo.findByBeneficiaryRegIDOrderByCreatedDateDesc(benRegIdLong); + List vitalList = + benPhysicalVitalRepo.findByBeneficiaryRegIDOrderByCreatedDateDesc(benRegIdLong); + + Map anthropometry = new HashMap<>(); + if (!anthroList.isEmpty()) { + BenAnthropometryDetail anthro = anthroList.get(0); + if (anthro.getHeightCm() != null) anthropometry.put("height", anthro.getHeightCm()); + if (anthro.getWeightKg() != null) anthropometry.put("weight", anthro.getWeightKg()); + if (anthro.getBmi() != null) anthropometry.put("bmi", anthro.getBmi()); + } + if (!vitalList.isEmpty()) { + BenPhysicalVitalDetail vital = vitalList.get(0); + if (vital.getTemperature() != null) anthropometry.put("temperatureValue", vital.getTemperature()); + if (vital.getPulseRate() != null) anthropometry.put("pulseRate", vital.getPulseRate()); + if (vital.getSystolicBP() != null) anthropometry.put("systolicBP", vital.getSystolicBP()); + if (vital.getDiastolicBP() != null) anthropometry.put("diastolicBP", vital.getDiastolicBP()); + if (vital.getBloodGlucoseRandom() != null) anthropometry.put("bloodGlucoseRandom", vital.getBloodGlucoseRandom()); + } + + // fallback: if no exam saved yet, read from registration otherFields + if (anthropometry.isEmpty() && benDetailsOBJ != null && benDetailsOBJ.getOtherFields() != null) { + try { + Map extraFields = new Gson().fromJson(benDetailsOBJ.getOtherFields(), Map.class); + if (extraFields.containsKey("weight")) anthropometry.put("weight", extraFields.get("weight")); + if (extraFields.containsKey("height")) anthropometry.put("height", extraFields.get("height")); + if (extraFields.containsKey("bmi")) anthropometry.put("bmi", extraFields.get("bmi")); + if (extraFields.containsKey("temperatureValue")) anthropometry.put("temperatureValue", extraFields.get("temperatureValue")); + } catch (Exception ex) { + logger.warn("Could not parse otherFields for fallback anthropometry: " + benDetailsOBJ.getBeneficiaryDetailsId()); + } + } + if (!anthropometry.isEmpty()) resultMap.put("anthropometry", anthropometry); + } + + // Stop TB fields from ExtraFields + Map stopTBDetails = new HashMap<>(); + if (benDetailsOBJ != null && benDetailsOBJ.getOtherFields() != null) { + try { + Map extraFields = new Gson().fromJson(benDetailsOBJ.getOtherFields(), Map.class); + + if (extraFields.containsKey("personFrom")) stopTBDetails.put("personFrom", extraFields.get("personFrom")); + if (extraFields.containsKey("caseFindingType")) stopTBDetails.put("caseFindingType", extraFields.get("caseFindingType")); + if (extraFields.containsKey("isMobileAvailable")) stopTBDetails.put("isMobileAvailable", extraFields.get("isMobileAvailable")); + if (extraFields.containsKey("tuId")) stopTBDetails.put("tuId", extraFields.get("tuId")); + if (extraFields.containsKey("tuName")) stopTBDetails.put("tuName", extraFields.get("tuName")); + + // economicStatus and residentialArea into beneficiaryDetails + if (extraFields.containsKey("economicStatus")) benDetailsRMNCH_OBJ.setEconomicStatus((String) extraFields.get("economicStatus")); + if (extraFields.containsKey("economicStatusId")) benDetailsRMNCH_OBJ.setEconomicStatusId(((Number) extraFields.get("economicStatusId")).intValue()); + if (extraFields.containsKey("residentialArea")) benDetailsRMNCH_OBJ.setResidentialArea((String) extraFields.get("residentialArea")); + if (extraFields.containsKey("residentialAreaId")) benDetailsRMNCH_OBJ.setResidentialAreaId(((Number) extraFields.get("residentialAreaId")).intValue()); + } catch (Exception ex) { + logger.warn("Could not parse ExtraFields for benDetailsId: " + benDetailsOBJ.getBeneficiaryDetailsId()); + } + } + if (!stopTBDetails.isEmpty()) resultMap.put("stopTBDetails", stopTBDetails); + // adding asha id / created by - user id if (benAddressOBJ.getCreatedBy() != null) { Integer userID = beneficiaryRepo.getUserIDByUserName(benAddressOBJ.getCreatedBy()); diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormDefinitionServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormDefinitionServiceImpl.java new file mode 100644 index 00000000..c40b65d7 --- /dev/null +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormDefinitionServiceImpl.java @@ -0,0 +1,416 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.service.impl; + +import com.iemr.flw.domain.iemr.DynamicForm; +import com.iemr.flw.domain.iemr.FormSection; +import com.iemr.flw.domain.iemr.FormVersion; +import com.iemr.flw.domain.iemr.OptionCondition; +import com.iemr.flw.domain.iemr.QuestionOption; +import com.iemr.flw.domain.iemr.QuestionValidation; +import com.iemr.flw.domain.iemr.SectionQuestion; +import com.iemr.flw.dto.iemr.DynamicFormDTO; +import com.iemr.flw.dto.iemr.FormSectionDTO; +import com.iemr.flw.dto.iemr.OptionConditionDTO; +import com.iemr.flw.dto.iemr.QuestionOptionDTO; +import com.iemr.flw.dto.iemr.QuestionValidationDTO; +import com.iemr.flw.dto.iemr.SectionQuestionDTO; +import com.iemr.flw.mapper.DynamicFormMapper; +import com.iemr.flw.repo.iemr.DynamicFormRepo; +import com.iemr.flw.repo.iemr.FormSectionRepo; +import com.iemr.flw.repo.iemr.FormVersionRepo; +import com.iemr.flw.repo.iemr.OptionConditionRepo; +import com.iemr.flw.repo.iemr.QuestionOptionRepo; +import com.iemr.flw.repo.iemr.QuestionValidationRepo; +import com.iemr.flw.repo.iemr.SectionQuestionRepo; +import com.iemr.flw.service.DynamicFormDefinitionService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Implementation of DynamicFormDefinitionService with version-aware writes. + * Every structural change clones all sections fresh into a new FormVersion. + * Sections have a direct FK to FormVersion (no join table). + * + * @author Piramal Swasthya + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class DynamicFormDefinitionServiceImpl implements DynamicFormDefinitionService { + + private final DynamicFormRepo formRepo; + private final FormSectionRepo sectionRepo; + private final SectionQuestionRepo questionRepo; + private final QuestionOptionRepo optionRepo; + private final OptionConditionRepo conditionRepo; + private final QuestionValidationRepo validationRepo; + private final FormVersionRepo versionRepo; + private final DynamicFormMapper mapper; + + // ── PUBLIC WRITE METHODS ────────────────────────────────────────────────── + + @Override + @Transactional + public DynamicFormDTO createForm(DynamicFormDTO dto) { + formRepo.findByFormTypeAndIsActive(dto.getFormType(), true).ifPresent(existing -> { + throw new IllegalStateException( + "An active form of type " + dto.getFormType() + " already exists (formId=" + + existing.getFormId() + "). Deactivate it before creating a new one."); + }); + DynamicForm form = mapper.toEntity(dto); + // TODO: set audit fields when auth is re-enabled for /dynamicForm endpoints + // form.setCreatedBy(jwtUtil.extractUsername(authorization)); + // form.setUpdatedBy(jwtUtil.extractUsername(authorization)); + DynamicForm savedForm = formRepo.save(form); + FormVersion version = createNewVersion(savedForm, 1, null, null); + buildSectionsFromDto(dto.getSections(), version); + log.info("Form created: {} (v1)", savedForm.getFormUuid()); + return loadLatestFromDb(savedForm.getFormId()); + } + + @Override + @Transactional + public DynamicFormDTO updateForm(Long formId, DynamicFormDTO dto) { + DynamicForm form = requireForm(formId); + FormVersion current = requireLatestVersion(formId); + int nextVersion = current.getVersionNumber() + 1; + FormVersion newVersion = createNewVersion(form, nextVersion, null, null); + buildSectionsFromDto(dto.getSections(), newVersion); + current.setIsLatest(false); + versionRepo.save(current); + log.info("Form updated: {} -> v{}", form.getFormUuid(), nextVersion); + return loadLatestFromDb(formId); + } + + @Override + @Transactional + public void activateForm(Long formId) { + DynamicForm form = requireForm(formId); + form.setIsActive(true); + formRepo.save(form); + log.info("Form activated: {}", form.getFormUuid()); + } + + @Override + @Transactional + public void deactivateForm(Long formId) { + DynamicForm form = requireForm(formId); + form.setIsActive(false); + formRepo.save(form); + log.info("Form deactivated: {}", form.getFormUuid()); + } + + @Override + public DynamicFormDTO getFormDefinition(Long formId) { + return loadLatestFromDb(formId); + } + + @Override + public DynamicFormDTO getFormDefinitionByVersion(Long formId, Integer versionNumber) { + FormVersion version = versionRepo.findByDynamicForm_FormIdAndVersionNumber(formId, versionNumber) + .orElseThrow(() -> new RuntimeException( + "Version " + versionNumber + " not found for formId: " + formId)); + return buildFormDto(version.getDynamicForm(), version); + } + + @Override + public List getAllForms() { + return formRepo.findByIsActiveOrderByFormIdAsc(true) + .stream() + .map(form -> { + try { + return loadLatestFromDb(form.getFormId()); + } catch (RuntimeException e) { + log.warn("Skipping form {} — no active version: {}", form.getFormId(), e.getMessage()); + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + // ── VERSION MANAGEMENT ──────────────────────────────────────────────────── + + private FormVersion createNewVersion(DynamicForm form, int versionNumber, + String createdBy, String notes) { + FormVersion version = new FormVersion(); + version.setDynamicForm(form); + version.setVersionNumber(versionNumber); + version.setIsLatest(true); + version.setCreatedBy(createdBy); + // TODO: set audit fields when auth is re-enabled for /dynamicForm endpoints + // version.setCreatedBy(jwtUtil.extractUsername(authorization)); + // version.setUpdatedBy(jwtUtil.extractUsername(authorization)); + version.setNotes(notes); + return versionRepo.save(version); + } + + // ── BUILD HELPERS (DTO → Entity) ────────────────────────────────────────── + + /** + * Two-pass build: + * Pass 1 — persist sections, questions, options, validations; collect uuid→entity maps. + * Pass 2 — wire conditions using targetQuestionUuid/targetSectionUuid from those maps. + */ + private void buildSectionsFromDto(List dtos, FormVersion version) { + if (dtos == null) return; + + Map sectionByUuid = new LinkedHashMap<>(); + Map questionByUuid = new LinkedHashMap<>(); + Map> pendingConditions = new LinkedHashMap<>(); + + // Pass 1: sections → questions → options + validations + for (FormSectionDTO sDto : dtos) { + FormSection section = mapper.toEntity(sDto); + section.setFormVersion(version); + section.setQuestions(new ArrayList<>()); + FormSection savedSection = sectionRepo.save(section); + sectionByUuid.put(savedSection.getSectionUuid(), savedSection); + + if (sDto.getQuestions() == null) continue; + for (SectionQuestionDTO qDto : sDto.getQuestions()) { + SectionQuestion q = mapper.toEntity(qDto); + q.setFormSection(savedSection); + q.setOptions(new ArrayList<>()); + q.setValidations(new ArrayList<>()); + SectionQuestion savedQ = questionRepo.save(q); + questionByUuid.put(savedQ.getQuestionUuid(), savedQ); + + if (qDto.getOptions() != null) { + for (QuestionOptionDTO oDto : qDto.getOptions()) { + QuestionOption opt = mapper.toEntity(oDto); + opt.setSectionQuestion(savedQ); + opt.setConditions(new ArrayList<>()); + QuestionOption savedOpt = optionRepo.save(opt); + if (oDto.getConditions() != null && !oDto.getConditions().isEmpty()) { + pendingConditions.put(savedOpt, oDto.getConditions()); + } + } + } + if (qDto.getValidations() != null) { + for (QuestionValidationDTO vDto : qDto.getValidations()) { + buildValidationEntity(vDto, savedQ); + } + } + } + } + + // Pass 2: conditions — resolve targets by UUID (create path) or ID (addCondition path) + for (Map.Entry> entry : pendingConditions.entrySet()) { + QuestionOption opt = entry.getKey(); + for (OptionConditionDTO cDto : entry.getValue()) { + OptionCondition cond = new OptionCondition(); + cond.setActionType(cDto.getActionType()); + cond.setQuestionOption(opt); + + if (cDto.getTargetQuestionUuid() != null) { + SectionQuestion target = questionByUuid.get(cDto.getTargetQuestionUuid()); + if (target == null) throw new RuntimeException( + "targetQuestionUuid not found in this form: " + cDto.getTargetQuestionUuid()); + cond.setTargetQuestion(target); + } else if (cDto.getTargetQuestionId() != null) { + cond.setTargetQuestion(requireQuestion(cDto.getTargetQuestionId())); + } + + if (cDto.getTargetSectionUuid() != null) { + FormSection target = sectionByUuid.get(cDto.getTargetSectionUuid()); + if (target == null) throw new RuntimeException( + "targetSectionUuid not found in this form: " + cDto.getTargetSectionUuid()); + cond.setTargetSection(target); + } else if (cDto.getTargetSectionId() != null) { + cond.setTargetSection(requireSection(cDto.getTargetSectionId())); + } + + conditionRepo.save(cond); + } + } + } + + private QuestionValidation buildValidationEntity(QuestionValidationDTO dto, + SectionQuestion question) { + QuestionValidation v = mapper.toEntity(dto); + v.setSectionQuestion(question); + return validationRepo.save(v); + } + + // ── LOAD / READ HELPERS ─────────────────────────────────────────────────── + + private DynamicFormDTO loadLatestFromDb(Long formId) { + FormVersion version = versionRepo.findByDynamicForm_FormIdAndIsLatest(formId, true) + .orElseThrow(() -> new RuntimeException("No active version for formId: " + formId)); + return buildFormDto(version.getDynamicForm(), version); + } + + /** + * Builds the full form DTO using exactly 5 queries (constant regardless of form size). + * All grouping, visibleByDefault computation, and embedded question/section population + * happen in memory — zero extra queries beyond the initial 5. + */ + private DynamicFormDTO buildFormDto(DynamicForm form, FormVersion version) { + // Query 1: sections + List sections = sectionRepo + .findByFormVersion_VersionIdOrderByDisplayOrderAsc(version.getVersionId()); + if (sections.isEmpty()) { + DynamicFormDTO dto = mapper.toDto(form); + dto.setVersionNumber(version.getVersionNumber()); + dto.setSections(Collections.emptyList()); + return dto; + } + + List sectionIds = sections.stream().map(FormSection::getSectionId) + .collect(Collectors.toList()); + + // Query 2: questions + List allQuestions = questionRepo + .findBySectionIdsOrderByDisplayOrderAsc(sectionIds); + + List questionIds = allQuestions.stream().map(SectionQuestion::getQuestionId) + .collect(Collectors.toList()); + + List allOptions; + List allValidations; + List allConditions; + + if (questionIds.isEmpty()) { + allOptions = Collections.emptyList(); + allValidations = Collections.emptyList(); + allConditions = Collections.emptyList(); + } else { + // Query 3: options + allOptions = optionRepo.findByQuestionIdsOrderByDisplayOrderAsc(questionIds); + // Query 4: validations + allValidations = validationRepo.findByQuestionIds(questionIds); + + List optionIds = allOptions.stream().map(QuestionOption::getOptionId) + .collect(Collectors.toList()); + // Query 5: conditions (LEFT JOIN FETCH targets) + allConditions = optionIds.isEmpty() ? Collections.emptyList() + : conditionRepo.findByOptionIds(optionIds); + } + + // Compute hidden question IDs (those targeted by any condition) + Set hiddenIds = allConditions.stream() + .filter(c -> c.getTargetQuestion() != null) + .map(c -> c.getTargetQuestion().getQuestionId()) + .collect(Collectors.toSet()); + + // Build index maps — no extra queries; FKs already in memory via JOIN FETCH + Map> questionsBySection = allQuestions.stream() + .collect(Collectors.groupingBy(q -> q.getFormSection().getSectionId())); + Map> optionsByQuestion = allOptions.stream() + .collect(Collectors.groupingBy(o -> o.getSectionQuestion().getQuestionId())); + Map> validationsByQuestion = allValidations.stream() + .collect(Collectors.groupingBy(v -> v.getSectionQuestion().getQuestionId())); + Map> conditionsByOption = allConditions.stream() + .collect(Collectors.groupingBy(c -> c.getQuestionOption().getOptionId())); + + DynamicFormDTO dto = mapper.toDto(form); + dto.setVersionNumber(version.getVersionNumber()); + dto.setSections(assembleSectionDtos(sections, questionsBySection, optionsByQuestion, + validationsByQuestion, conditionsByOption, hiddenIds)); + return dto; + } + + private List assembleSectionDtos( + List sections, + Map> questionsBySection, + Map> optionsByQuestion, + Map> validationsByQuestion, + Map> conditionsByOption, + Set hiddenIds) { + return sections.stream().map(section -> { + FormSectionDTO sDto = mapper.toDto(section); + List qs = questionsBySection + .getOrDefault(section.getSectionId(), Collections.emptyList()); + sDto.setQuestions(assembleQuestionDtos(qs, optionsByQuestion, + validationsByQuestion, conditionsByOption, hiddenIds)); + return sDto; + }).collect(Collectors.toList()); + } + + private List assembleQuestionDtos( + List questions, + Map> optionsByQuestion, + Map> validationsByQuestion, + Map> conditionsByOption, + Set hiddenIds) { + return questions.stream().map(q -> { + SectionQuestionDTO qDto = mapper.toDto(q); + qDto.setVisibleByDefault(!hiddenIds.contains(q.getQuestionId())); + List opts = optionsByQuestion + .getOrDefault(q.getQuestionId(), Collections.emptyList()); + qDto.setOptions(assembleOptionDtos(opts, conditionsByOption)); + qDto.setValidations(validationsByQuestion + .getOrDefault(q.getQuestionId(), Collections.emptyList()) + .stream().map(mapper::toDto).collect(Collectors.toList())); + return qDto; + }).collect(Collectors.toList()); + } + + private List assembleOptionDtos( + List options, + Map> conditionsByOption) { + return options.stream().map(opt -> { + QuestionOptionDTO oDto = mapper.toDto(opt); + oDto.setConditions(conditionsByOption + .getOrDefault(opt.getOptionId(), Collections.emptyList()) + .stream().map(mapper::toDto).collect(Collectors.toList())); + return oDto; + }).collect(Collectors.toList()); + } + + // ── REQUIRE HELPERS ─────────────────────────────────────────────────────── + + private DynamicForm requireForm(Long formId) { + return formRepo.findById(formId) + .orElseThrow(() -> new RuntimeException("Form not found: " + formId)); + } + + private FormVersion requireLatestVersion(Long formId) { + return versionRepo.findByDynamicForm_FormIdAndIsLatest(formId, true) + .orElseThrow(() -> new RuntimeException( + "No latest version found for formId: " + formId)); + } + + private FormSection requireSection(Long sectionId) { + return sectionRepo.findById(sectionId) + .orElseThrow(() -> new RuntimeException("Section not found: " + sectionId)); + } + + private SectionQuestion requireQuestion(Long questionId) { + return questionRepo.findById(questionId) + .orElseThrow(() -> new RuntimeException("Question not found: " + questionId)); + } + +} diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java new file mode 100644 index 00000000..6e3b29c2 --- /dev/null +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -0,0 +1,504 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.service.impl; + +import com.iemr.flw.domain.iemr.FormResponse; +import com.iemr.flw.domain.iemr.FormSection; +import com.iemr.flw.domain.iemr.FormVersion; +import com.iemr.flw.domain.iemr.QuestionOption; +import com.iemr.flw.domain.iemr.QuestionResponse; +import com.iemr.flw.domain.iemr.SectionQuestion; +import com.iemr.flw.domain.iemr.SectionResponse; +import com.iemr.flw.dto.iemr.FormResponseDTO; +import com.iemr.flw.dto.iemr.FormResponseRequest; +import com.iemr.flw.dto.iemr.QuestionAnswerRequest; +import com.iemr.flw.dto.iemr.QuestionResponseDTO; +import com.iemr.flw.dto.iemr.SectionAnswerRequest; +import com.iemr.flw.dto.iemr.SectionResponseDTO; +import com.iemr.flw.domain.iemr.DynamicForm; +import com.iemr.flw.masterEnum.FormType; +import com.iemr.flw.repo.iemr.DynamicFormRepo; +import com.iemr.flw.repo.iemr.FormResponseRepo; +import com.iemr.flw.repo.iemr.FormSectionRepo; +import com.iemr.flw.repo.iemr.FormVersionRepo; +import com.iemr.flw.repo.iemr.QuestionOptionRepo; +import com.iemr.flw.repo.iemr.QuestionResponseRepo; +import com.iemr.flw.repo.iemr.SectionQuestionRepo; +import com.iemr.flw.repo.iemr.SectionResponseRepo; +import com.iemr.flw.masterEnum.QuestionType; +import com.iemr.flw.service.CampConfigService; +import com.iemr.flw.service.DynamicFormResponseService; +import com.iemr.flw.utils.JwtUtil; +import com.iemr.flw.utils.exception.IEMRException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.sql.Timestamp; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Handles save, submit, complete, and fetch of dynamic form responses. + * Resolves the latest FormVersion server-side so the client only needs to send formUuid. + * + * @author Piramal Swasthya + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class DynamicFormResponseServiceImpl implements DynamicFormResponseService { + + private static final String STATUS_DRAFT = "DRAFT"; + private static final String STATUS_SUBMITTED = "SUBMITTED"; + private static final String STATUS_COMPLETE = "COMPLETE"; + private static final String SECTION_STATUS_DONE = "DONE"; + + private final DynamicFormRepo dynamicFormRepo; + private final FormResponseRepo formResponseRepo; + private final SectionResponseRepo sectionResponseRepo; + private final QuestionResponseRepo questionResponseRepo; + private final FormVersionRepo formVersionRepo; + private final FormSectionRepo formSectionRepo; + private final SectionQuestionRepo sectionQuestionRepo; + private final QuestionOptionRepo questionOptionRepo; + private final FormResponseItemSaver itemSaver; + private final JwtUtil jwtUtil; + private final CampConfigService campConfigService; + + @Override + @Transactional + public FormResponseDTO submitForm(FormResponseRequest request) { + Integer vanID = campConfigService.getVanID(); + Integer parkingPlaceID = campConfigService.getParkingPlaceID(); + FormVersion version = resolveLatestVersion(request.getFormUuid()); + Timestamp now = new Timestamp(System.currentTimeMillis()); + FormResponse formResponse; + if (request.getResponseId() != null) { + // Re-submit: update existing SUBMITTED response's sections + formResponse = formResponseRepo.findById(request.getResponseId()) + .orElseThrow(() -> new NoSuchElementException( + "FormResponse not found: " + request.getResponseId())); + if (STATUS_COMPLETE.equals(formResponse.getStatus())) { + throw new IllegalStateException( + "Cannot update a COMPLETE response (responseId=" + request.getResponseId() + ")"); + } + // Bump submittedAt to reflect the re-submission time + formResponse.setStatus(STATUS_SUBMITTED); + formResponse.setSubmittedAt(now); + formResponse.setLastFollowUpAt(now); + formResponse = formResponseRepo.save(formResponse); + } else { + formResponse = createFormResponse(request, version, STATUS_SUBMITTED, now, vanID, parkingPlaceID); + } + return processSections(formResponse, request, version, null, vanID, parkingPlaceID); + } + + @Override + @Transactional + public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken) throws IEMRException { + String actor = jwtUtil.extractUserId(jwtToken).toString(); + Integer vanID = campConfigService.getVanID(); + Integer parkingPlaceID = campConfigService.getParkingPlaceID(); + + FormVersion version = resolveLatestVersion(request.getFormUuid()); + Long formId = version.getDynamicForm().getFormId(); + Timestamp now = new Timestamp(System.currentTimeMillis()); + + List existing = + formResponseRepo.findByBeneficiaryIdAndFormId(request.getBeneficiaryId(), formId); + FormResponse formResponse = existing.isEmpty() + ? createFormResponse(request, version, STATUS_COMPLETE, now, vanID, parkingPlaceID) + : existing.get(0); + + formResponse.setUpdatedBy(actor); + formResponse.setStatus(STATUS_COMPLETE); + formResponse.setCompletedAt(now); + formResponse = formResponseRepo.save(formResponse); + return processSections(formResponse, request, version, actor, vanID, parkingPlaceID); + } + + @Override + @Transactional(readOnly = true) + public List getResponsesByBeneficiary(Long beneficiaryId, String formUuid) { + FormVersion version = resolveLatestVersion(formUuid); + Long formId = version.getDynamicForm().getFormId(); + + List responses = + formResponseRepo.findByBeneficiaryIdAndFormId(beneficiaryId, formId); + return responses.stream() + .map(r -> buildFormResponseDTO(r, loadSectionResponseDTOs(r.getResponseId()))) + .collect(Collectors.toList()); + } + + @Override + @Transactional(readOnly = true) + public FormResponseDTO getResponseById(Long responseId) { + FormResponse formResponse = formResponseRepo.findById(responseId) + .orElseThrow(() -> new NoSuchElementException("FormResponse not found: " + responseId)); + return buildFormResponseDTO(formResponse, loadSectionResponseDTOs(responseId)); + } + + // ---- private helpers ---- + + private FormVersion resolveLatestVersion(String formUuid) { + return formVersionRepo.findByDynamicForm_FormUuidAndIsLatest(formUuid, true) + .orElseThrow(() -> new NoSuchElementException( + "No active FormVersion found for form code: " + formUuid)); + } + + /** Creates a new FormResponse row. */ + private FormResponse createFormResponse( + FormResponseRequest request, + FormVersion version, + String status, + Timestamp submittedAt, + Integer vanID, + Integer parkingPlaceID) { + + FormResponse formResponse = FormResponse.builder() + .beneficiaryId(request.getBeneficiaryId()) + .formId(version.getDynamicForm().getFormId()) + .versionId(version.getVersionId()) + .officerId(request.getOfficerId()) + .status(status) + .submittedAt(submittedAt) + .lastFollowUpAt(submittedAt) + // TODO: set audit fields when auth is re-enabled for /dynamicForm/response endpoints + // .createdBy(jwtUtil.extractUsername(authorization)) + // .updatedBy(jwtUtil.extractUsername(authorization)) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) + .build(); + return formResponseRepo.save(formResponse); + } + + /** + * Batch-loads all sections and questions for the version, then processes each answered section. + */ + private FormResponseDTO processSections( + FormResponse formResponse, + FormResponseRequest request, + FormVersion version, + String actor, + Integer vanID, + Integer parkingPlaceID) { + + List allSections = + formSectionRepo.findByFormVersion_VersionIdOrderByDisplayOrderAsc(version.getVersionId()); + Map sectionByUuid = allSections.stream() + .collect(Collectors.toMap(FormSection::getSectionUuid, Function.identity())); + + Set sectionIds = allSections.stream() + .map(FormSection::getSectionId) + .collect(Collectors.toSet()); + + List allQuestions = + sectionQuestionRepo.findBySectionIdsOrderByDisplayOrderAsc(sectionIds); + Map> questionsBySection = allQuestions.stream() + .collect(Collectors.groupingBy( + q -> q.getFormSection().getSectionId(), + Collectors.toMap(SectionQuestion::getQuestionUuid, Function.identity()))); + + Set questionIds = allQuestions.stream() + .map(SectionQuestion::getQuestionId) + .collect(Collectors.toSet()); + List allOptions = + questionOptionRepo.findByQuestionIdsOrderByDisplayOrderAsc(questionIds); + + // Map: questionId → (optionValue → QuestionOption) + Map> optionsByQuestion = allOptions.stream() + .collect(Collectors.groupingBy( + o -> o.getSectionQuestion().getQuestionId(), + Collectors.toMap(QuestionOption::getOptionValue, Function.identity()))); + + List sectionDTOs = new ArrayList<>(); + for (SectionAnswerRequest sectionReq : request.getSections()) { + if (sectionReq.getSectionUuid() == null) { + throw new IllegalArgumentException( + "sectionUuid is null — did you send sectionCode instead?"); + } + FormSection section = sectionByUuid.get(sectionReq.getSectionUuid()); + if (section == null) { + throw new NoSuchElementException( + "Section '" + sectionReq.getSectionUuid() + + "' not found in form '" + request.getFormUuid() + "'"); + } + + SectionResponse sectionResponse = upsertSectionResponse( + formResponse.getResponseId(), section.getSectionId(), actor, vanID, parkingPlaceID); + + if ("POST_SUBMIT".equals(section.getSectionPhase())) { + formResponse.setLastFollowUpAt(sectionResponse.getSavedAt()); + formResponseRepo.save(formResponse); + } + + Map questionMap = + questionsBySection.getOrDefault(section.getSectionId(), Map.of()); + + List questionResponses = processAnswers( + sectionResponse.getSectionResponseId(), + sectionReq.getAnswers(), + questionMap, + optionsByQuestion, + actor, + vanID, + parkingPlaceID); + + questionResponseRepo.saveAll(questionResponses); + + sectionDTOs.add(buildSectionResponseDTO(sectionResponse, questionResponses)); + } + + return buildFormResponseDTO(formResponse, sectionDTOs); + } + + private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor, + Integer vanID, Integer parkingPlaceID) { + Optional existing = + sectionResponseRepo.findByResponseIdAndSectionId(responseId, sectionId); + if (existing.isPresent()) { + SectionResponse sr = existing.get(); + sr.setStatus(SECTION_STATUS_DONE); + sr.setSavedAt(new Timestamp(System.currentTimeMillis())); + sr.setUpdatedBy(actor); + if (sr.getVanID() == null) { sr.setVanID(vanID); sr.setParkingPlaceID(parkingPlaceID); } + return sectionResponseRepo.save(sr); + } + SectionResponse sr = SectionResponse.builder() + .responseId(responseId) + .sectionId(sectionId) + .status(SECTION_STATUS_DONE) + .savedAt(new Timestamp(System.currentTimeMillis())) + .createdBy(actor) + .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) + .build(); + return sectionResponseRepo.save(sr); + } + + private List processAnswers( + Long sectionResponseId, + List answers, + Map questionMap, + Map> optionsByQuestion, + String actor, + Integer vanID, + Integer parkingPlaceID) { + + List results = new ArrayList<>(); + + for (QuestionAnswerRequest answer : answers) { + if (answer.getQuestionUuid() == null) { + throw new IllegalArgumentException( + "questionUuid is null — did you send questionCode instead?"); + } + SectionQuestion question = questionMap.get(answer.getQuestionUuid()); + if (question == null) { + throw new NoSuchElementException( + "Question '" + answer.getQuestionUuid() + "' not found in section"); + } + QuestionType type = question.getQuestionType(); + Long questionId = question.getQuestionId(); + + if (type == QuestionType.DISPLAY) { + // DISPLAY questions carry no answer data + continue; + } + + // Delete any existing answers for this question in this section (handles re-saves) + questionResponseRepo.deleteByQuestionIdAndSectionResponseId(questionId, sectionResponseId); + + if (type == QuestionType.RADIO) { + if (answer.getOptionValue() != null) { + QuestionOption opt = resolveOption( + optionsByQuestion, questionId, answer.getOptionValue(), answer.getQuestionUuid()); + results.add(QuestionResponse.builder() + .sectionResponseId(sectionResponseId) + .questionId(questionId) + .optionId(opt != null ? opt.getOptionId() : null) + .createdBy(actor) + .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) + .build()); + } + } else if (type == QuestionType.MCQ) { + if (answer.getOptionValues() != null) { + for (String val : answer.getOptionValues()) { + QuestionOption opt = resolveOption( + optionsByQuestion, questionId, val, answer.getQuestionUuid()); + results.add(QuestionResponse.builder() + .sectionResponseId(sectionResponseId) + .questionId(questionId) + .optionId(opt != null ? opt.getOptionId() : null) + .createdBy(actor) + .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) + .build()); + } + } + } else { + // TEXT, DATE, AUTO_FILL — prefer answerText, then answerDate, then optionValue (legacy) + String value = answer.getAnswerText() != null ? answer.getAnswerText() + : answer.getAnswerDate() != null ? answer.getAnswerDate() + : answer.getOptionValue(); + results.add(QuestionResponse.builder() + .sectionResponseId(sectionResponseId) + .questionId(questionId) + .answerText(value) + .createdBy(actor) + .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) + .build()); + } + } + return results; + } + + private QuestionOption resolveOption( + Map> optionsByQuestion, + Long questionId, + String optionValue, + String questionUuid) { + + Map opts = optionsByQuestion.get(questionId); + if (opts == null || !opts.containsKey(optionValue)) { + log.warn("Option value '{}' not found for question '{}', storing null optionId", + optionValue, questionUuid); + return null; + } + return opts.get(optionValue); + } + + // ---- read helpers ---- + + private List loadSectionResponseDTOs(Long responseId) { + List sections = sectionResponseRepo.findByResponseId(responseId); + if (sections.isEmpty()) { + return List.of(); + } + Set srIds = sections.stream() + .map(SectionResponse::getSectionResponseId) + .collect(Collectors.toSet()); + List allAnswers = + questionResponseRepo.findBySectionResponseIdIn(srIds); + Map> answersBySectionResponse = allAnswers.stream() + .collect(Collectors.groupingBy(QuestionResponse::getSectionResponseId)); + + return sections.stream() + .map(sr -> buildSectionResponseDTO( + sr, + answersBySectionResponse.getOrDefault(sr.getSectionResponseId(), List.of()))) + .collect(Collectors.toList()); + } + + private FormResponseDTO buildFormResponseDTO(FormResponse r, List sections) { + return FormResponseDTO.builder() + .responseId(r.getResponseId()) + .beneficiaryId(r.getBeneficiaryId()) + .formId(r.getFormId()) + .versionId(r.getVersionId()) + .officerId(r.getOfficerId()) + .status(r.getStatus()) + .createdBy(r.getCreatedBy()) + .updatedBy(r.getUpdatedBy()) + .submittedAt(r.getSubmittedAt()) + .completedAt(r.getCompletedAt()) + .lastFollowUpAt(r.getLastFollowUpAt()) + .createdAt(r.getCreatedAt()) + .updatedAt(r.getUpdatedAt()) + .sections(sections) + .build(); + } + + private SectionResponseDTO buildSectionResponseDTO( + SectionResponse sr, List answers) { + List answerDTOs = answers.stream() + .map(a -> QuestionResponseDTO.builder() + .questionResponseId(a.getQuestionResponseId()) + .questionId(a.getQuestionId()) + .optionId(a.getOptionId()) + .answerText(a.getAnswerText()) + .build()) + .collect(Collectors.toList()); + return SectionResponseDTO.builder() + .sectionResponseId(sr.getSectionResponseId()) + .sectionId(sr.getSectionId()) + .status(sr.getStatus()) + .savedAt(sr.getSavedAt()) + .answers(answerDTOs) + .build(); + } + + @Override + public List submitBulk(List requests, String jwtToken) { + if (requests == null || requests.isEmpty()) return List.of(); + List results = new ArrayList<>(); + for (FormResponseRequest req : requests) { + try { + results.add(itemSaver.saveForBulk(req, jwtToken)); + } catch (Exception ex) { + log.warn("submitBulk: item skipped for beneficiaryId={} — {}", + req.getBeneficiaryId(), ex.getMessage()); + } + } + return results; + } + + @Override + @Transactional(readOnly = true) + public List findPendingFollowUps(List formIds, int delayDays) { + if (formIds == null || formIds.isEmpty()) { + return List.of(); + } + LocalDate target = LocalDate.now().minusDays(delayDays); + Timestamp windowStart = Timestamp.valueOf(target.atStartOfDay()); + Timestamp windowEnd = Timestamp.valueOf(target.plusDays(1).atStartOfDay()); + return formResponseRepo.findPendingFollowUps(formIds, windowStart, windowEnd) + .stream() + .map(r -> buildFormResponseDTO(r, List.of())) + .collect(Collectors.toList()); + } + + @Override + @Transactional(readOnly = true) + public List getCompletedBeneficiaries(FormType formType, Integer villageId, Integer providerServiceMapId) { + DynamicForm form = dynamicFormRepo.findByFormTypeAndIsActive(formType, true) + .orElseThrow(() -> new NoSuchElementException( + "No active form found for type: " + formType)); + return (villageId != null || providerServiceMapId != null) + ? formResponseRepo.findBeneficiaryIdsByFormIdAndStatusFiltered(form.getFormId(), STATUS_COMPLETE, villageId, providerServiceMapId) + : formResponseRepo.findBeneficiaryIdsByFormIdAndStatus(form.getFormId(), STATUS_COMPLETE); + } +} diff --git a/src/main/java/com/iemr/flw/service/impl/FormFollowUpNotificationScheduler.java b/src/main/java/com/iemr/flw/service/impl/FormFollowUpNotificationScheduler.java new file mode 100644 index 00000000..5062c9fb --- /dev/null +++ b/src/main/java/com/iemr/flw/service/impl/FormFollowUpNotificationScheduler.java @@ -0,0 +1,110 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.service.impl; + +import com.iemr.flw.domain.iemr.DynamicForm; +import com.iemr.flw.dto.iemr.FormResponseDTO; +import com.iemr.flw.repo.iemr.DynamicFormRepo; +import com.iemr.flw.service.DynamicFormResponseService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Generic daily scheduler that sends follow-up push notifications for any active + * dynamic form with a configured {@code followUpDelayDays}. The notification fires + * exactly N days after the most recently completed section (PRE_SUBMIT submission or + * any subsequent POST_SUBMIT section completion). + * + * @author Piramal Swasthya + */ +@Service +public class FormFollowUpNotificationScheduler { + + private static final Logger log = + LoggerFactory.getLogger(FormFollowUpNotificationScheduler.class); + + private final DynamicFormRepo dynamicFormRepo; + private final DynamicFormResponseService responseService; + private final NotificationService notificationService; + + public FormFollowUpNotificationScheduler( + DynamicFormRepo dynamicFormRepo, + DynamicFormResponseService responseService, + NotificationService notificationService) { + this.dynamicFormRepo = dynamicFormRepo; + this.responseService = responseService; + this.notificationService = notificationService; + } + + @Scheduled(cron = "0 0 9 * * *") + public void sendFollowUpReminders() { + log.info("FormFollowUpNotificationScheduler: starting daily follow-up scan"); + + List formsWithDelay = + dynamicFormRepo.findByIsActiveTrueAndFollowUpDelayDaysIsNotNull(); + + if (formsWithDelay.isEmpty()) { + log.debug("No active forms with followUpDelayDays configured — skipping"); + return; + } + + // Group by delay value so we issue one batch DB query per distinct delay + Map> byDelay = formsWithDelay.stream() + .collect(Collectors.groupingBy( + DynamicForm::getFollowUpDelayDays, + Collectors.mapping(DynamicForm::getFormId, Collectors.toList()))); + + for (Map.Entry> entry : byDelay.entrySet()) { + int delayDays = entry.getKey(); + List formIds = entry.getValue(); + + try { + List pending = + responseService.findPendingFollowUps(formIds, delayDays); + + log.info("delayDays={}: found {} pending follow-up(s)", delayDays, pending.size()); + + for (FormResponseDTO r : pending) { + try { + //Notification sending logic + log.info("Sent follow-up notification: responseId={}, officerId={}", + r.getResponseId(), r.getOfficerId()); + } catch (Exception e) { + log.error("Failed to send notification for responseId={}: {}", + r.getResponseId(), e.getMessage()); + } + } + } catch (Exception e) { + log.error("Follow-up scheduler failed for delayDays={}: {}", + delayDays, e.getMessage(), e); + } + } + + log.info("FormFollowUpNotificationScheduler: scan complete"); + } +} diff --git a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java new file mode 100644 index 00000000..6a8fb863 --- /dev/null +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -0,0 +1,420 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.service.impl; + +import com.iemr.flw.domain.iemr.FormResponse; +import com.iemr.flw.domain.iemr.FormSection; +import com.iemr.flw.domain.iemr.FormVersion; +import com.iemr.flw.domain.iemr.QuestionOption; +import com.iemr.flw.domain.iemr.QuestionResponse; +import com.iemr.flw.domain.iemr.SectionQuestion; +import com.iemr.flw.domain.iemr.SectionResponse; +import com.iemr.flw.dto.iemr.FormResponseDTO; +import com.iemr.flw.dto.iemr.FormResponseRequest; +import com.iemr.flw.dto.iemr.QuestionAnswerRequest; +import com.iemr.flw.dto.iemr.QuestionResponseDTO; +import com.iemr.flw.dto.iemr.SectionAnswerRequest; +import com.iemr.flw.dto.iemr.SectionResponseDTO; +import com.iemr.flw.repo.iemr.FormResponseRepo; +import com.iemr.flw.repo.iemr.FormSectionRepo; +import com.iemr.flw.repo.iemr.FormVersionRepo; +import com.iemr.flw.repo.iemr.QuestionOptionRepo; +import com.iemr.flw.repo.iemr.QuestionResponseRepo; +import com.iemr.flw.repo.iemr.SectionQuestionRepo; +import com.iemr.flw.repo.iemr.SectionResponseRepo; +import com.iemr.flw.masterEnum.QuestionType; +import com.iemr.flw.service.CampConfigService; +import com.iemr.flw.utils.JwtUtil; +import com.iemr.flw.utils.exception.IEMRException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Saves a single dynamic form response in its own REQUIRES_NEW transaction. + * Used by submitBulk so that each item in the batch is independently committed or rolled back — + * a failed item does not affect sibling items in the same bulk request. + * + * @author Piramal Swasthya + */ +@Slf4j +@RequiredArgsConstructor +@Component +public class FormResponseItemSaver { + + private static final String STATUS_SUBMITTED = "SUBMITTED"; + private static final String SECTION_STATUS_DONE = "DONE"; + + private final FormResponseRepo formResponseRepo; + private final SectionResponseRepo sectionResponseRepo; + private final QuestionResponseRepo questionResponseRepo; + private final FormVersionRepo formVersionRepo; + private final FormSectionRepo formSectionRepo; + private final SectionQuestionRepo sectionQuestionRepo; + private final QuestionOptionRepo questionOptionRepo; + private final JwtUtil jwtUtil; + private final CampConfigService campConfigService; + + /** + * Saves one form response in its own independent REQUIRES_NEW transaction. + * Order: validate → delete old → save new. If save fails the transaction rolls back, + * restoring the deleted rows automatically. + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public FormResponseDTO saveForBulk(FormResponseRequest req, String jwtToken) throws IEMRException { + String actor = jwtUtil.extractUserId(jwtToken).toString(); + Integer vanID = campConfigService.getVanID(); + Integer parkingPlaceID = campConfigService.getParkingPlaceID(); + FormVersion version = resolveLatestVersion(req.getFormUuid()); + Timestamp now = new Timestamp(System.currentTimeMillis()); + Long formId = version.getDynamicForm().getFormId(); + + // Step 1: Validate all section + question UUIDs BEFORE any writes or deletes + validateRequest(req, version); + + // Step 2: Check for an existing response for this beneficiary+form + List existing = + formResponseRepo.findByBeneficiaryIdAndFormId(req.getBeneficiaryId(), formId); + + FormResponse formResponse; + if (!existing.isEmpty()) { + formResponse = existing.get(0); + Long responseId = formResponse.getResponseId(); + + // Step 3: Update FormResponse header + formResponse.setStatus(STATUS_SUBMITTED); + formResponse.setSubmittedAt(now); + formResponse.setUpdatedBy(actor); + formResponse.setLastFollowUpAt(now); + formResponse = formResponseRepo.save(formResponse); + log.info("saveForBulk: overwriting responseId={} for beneficiaryId={}", + formResponse.getResponseId(), req.getBeneficiaryId()); + } else { + formResponse = createFormResponse(req, version, now, actor, vanID, parkingPlaceID); + } + + // Step 5: Insert fresh sections and answers + return processSections(formResponse, req, version, actor, vanID, parkingPlaceID); + } + + /** Pre-flight: validates every sectionUuid and questionUuid in the request exist in the form. */ + private void validateRequest(FormResponseRequest req, FormVersion version) { + List allSections = + formSectionRepo.findByFormVersion_VersionIdOrderByDisplayOrderAsc(version.getVersionId()); + Map sectionByUuid = allSections.stream() + .collect(Collectors.toMap(FormSection::getSectionUuid, Function.identity())); + + Set sectionIds = allSections.stream() + .map(FormSection::getSectionId) + .collect(Collectors.toSet()); + List allQuestions = + sectionQuestionRepo.findBySectionIdsOrderByDisplayOrderAsc(sectionIds); + Map> questionUuidsBySectionId = allQuestions.stream() + .collect(Collectors.groupingBy( + q -> q.getFormSection().getSectionId(), + Collectors.mapping(SectionQuestion::getQuestionUuid, Collectors.toSet()))); + + for (SectionAnswerRequest sectionReq : req.getSections()) { + FormSection section = sectionByUuid.get(sectionReq.getSectionUuid()); + if (section == null) { + throw new NoSuchElementException( + "Section '" + sectionReq.getSectionUuid() + + "' not found in form '" + req.getFormUuid() + "'"); + } + Set validQuestions = + questionUuidsBySectionId.getOrDefault(section.getSectionId(), Set.of()); + for (QuestionAnswerRequest answer : sectionReq.getAnswers()) { + if (answer.getQuestionUuid() != null + && !validQuestions.contains(answer.getQuestionUuid())) { + throw new NoSuchElementException( + "Question '" + answer.getQuestionUuid() + + "' not found in section '" + sectionReq.getSectionUuid() + "'"); + } + } + } + } + + // ---- private helpers ---- + + private FormVersion resolveLatestVersion(String formUuid) { + return formVersionRepo.findByDynamicForm_FormUuidAndIsLatest(formUuid, true) + .orElseThrow(() -> new NoSuchElementException( + "No active FormVersion found for form: " + formUuid)); + } + + private FormResponse createFormResponse(FormResponseRequest req, FormVersion version, Timestamp now, String actor, + Integer vanID, Integer parkingPlaceID) { + FormResponse formResponse = FormResponse.builder() + .beneficiaryId(req.getBeneficiaryId()) + .formId(version.getDynamicForm().getFormId()) + .versionId(version.getVersionId()) + .officerId(req.getOfficerId()) + .status(STATUS_SUBMITTED) + .createdBy(actor) + .updatedBy(actor) + .submittedAt(now) + .lastFollowUpAt(now) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) + .build(); + return formResponseRepo.save(formResponse); + } + + private FormResponseDTO processSections(FormResponse formResponse, FormResponseRequest req, FormVersion version, String actor, + Integer vanID, Integer parkingPlaceID) { + List allSections = + formSectionRepo.findByFormVersion_VersionIdOrderByDisplayOrderAsc(version.getVersionId()); + Map sectionByUuid = allSections.stream() + .collect(Collectors.toMap(FormSection::getSectionUuid, Function.identity())); + + Set sectionIds = allSections.stream() + .map(FormSection::getSectionId) + .collect(Collectors.toSet()); + + List allQuestions = + sectionQuestionRepo.findBySectionIdsOrderByDisplayOrderAsc(sectionIds); + Map> questionsBySection = allQuestions.stream() + .collect(Collectors.groupingBy( + q -> q.getFormSection().getSectionId(), + Collectors.toMap(SectionQuestion::getQuestionUuid, Function.identity()))); + + Set questionIds = allQuestions.stream() + .map(SectionQuestion::getQuestionId) + .collect(Collectors.toSet()); + List allOptions = + questionOptionRepo.findByQuestionIdsOrderByDisplayOrderAsc(questionIds); + Map> optionsByQuestion = allOptions.stream() + .collect(Collectors.groupingBy( + o -> o.getSectionQuestion().getQuestionId(), + Collectors.toMap(QuestionOption::getOptionValue, Function.identity()))); + + List sectionDTOs = new ArrayList<>(); + for (SectionAnswerRequest sectionReq : req.getSections()) { + if (sectionReq.getSectionUuid() == null) { + throw new IllegalArgumentException( + "sectionUuid is null — did you send sectionCode instead?"); + } + FormSection section = sectionByUuid.get(sectionReq.getSectionUuid()); + if (section == null) { + throw new NoSuchElementException( + "Section '" + sectionReq.getSectionUuid() + + "' not found in form '" + req.getFormUuid() + "'"); + } + + // Delete existing SectionResponse (and its QuestionResponses) for this section only + sectionResponseRepo.findByResponseIdAndSectionId( + formResponse.getResponseId(), section.getSectionId()) + .ifPresent(sr -> { + questionResponseRepo.deleteBySectionResponseIdIn(Set.of(sr.getSectionResponseId())); + sectionResponseRepo.deleteById(sr.getSectionResponseId()); + }); + + SectionResponse sectionResponse = upsertSectionResponse( + formResponse.getResponseId(), section.getSectionId(), actor, vanID, parkingPlaceID); + + if ("POST_SUBMIT".equals(section.getSectionPhase())) { + formResponse.setLastFollowUpAt(sectionResponse.getSavedAt()); + formResponseRepo.save(formResponse); + } + + Map questionMap = + questionsBySection.getOrDefault(section.getSectionId(), Map.of()); + + List questionResponses = processAnswers( + sectionResponse.getSectionResponseId(), + sectionReq.getAnswers(), + questionMap, + optionsByQuestion, + actor, + vanID, + parkingPlaceID); + + questionResponseRepo.saveAll(questionResponses); + sectionDTOs.add(buildSectionResponseDTO(sectionResponse, questionResponses)); + } + + return buildFormResponseDTO(formResponse, sectionDTOs); + } + + private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor, + Integer vanID, Integer parkingPlaceID) { + Optional existing = + sectionResponseRepo.findByResponseIdAndSectionId(responseId, sectionId); + if (existing.isPresent()) { + SectionResponse sr = existing.get(); + sr.setStatus(SECTION_STATUS_DONE); + sr.setSavedAt(new Timestamp(System.currentTimeMillis())); + sr.setUpdatedBy(actor); + if (sr.getVanID() == null) { sr.setVanID(vanID); sr.setParkingPlaceID(parkingPlaceID); } + return sectionResponseRepo.save(sr); + } + SectionResponse sr = SectionResponse.builder() + .responseId(responseId) + .sectionId(sectionId) + .status(SECTION_STATUS_DONE) + .savedAt(new Timestamp(System.currentTimeMillis())) + .createdBy(actor) + .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) + .build(); + return sectionResponseRepo.save(sr); + } + + private List processAnswers( + Long sectionResponseId, + List answers, + Map questionMap, + Map> optionsByQuestion, + String actor, + Integer vanID, + Integer parkingPlaceID) { + + List results = new ArrayList<>(); + for (QuestionAnswerRequest answer : answers) { + if (answer.getQuestionUuid() == null) { + throw new IllegalArgumentException( + "questionUuid is null — did you send questionCode instead?"); + } + SectionQuestion question = questionMap.get(answer.getQuestionUuid()); + if (question == null) { + throw new NoSuchElementException( + "Question '" + answer.getQuestionUuid() + "' not found in section"); + } + QuestionType type = question.getQuestionType(); + Long questionId = question.getQuestionId(); + + if (type == QuestionType.DISPLAY) { + continue; + } + + questionResponseRepo.deleteByQuestionIdAndSectionResponseId(questionId, sectionResponseId); + + if (type == QuestionType.RADIO) { + if (answer.getOptionValue() != null) { + QuestionOption opt = resolveOption(optionsByQuestion, questionId, + answer.getOptionValue(), answer.getQuestionUuid()); + results.add(QuestionResponse.builder() + .sectionResponseId(sectionResponseId) + .questionId(questionId) + .optionId(opt != null ? opt.getOptionId() : null) + .createdBy(actor) + .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) + .build()); + } + } else if (type == QuestionType.MCQ) { + if (answer.getOptionValues() != null) { + for (String val : answer.getOptionValues()) { + QuestionOption opt = resolveOption(optionsByQuestion, questionId, val, + answer.getQuestionUuid()); + results.add(QuestionResponse.builder() + .sectionResponseId(sectionResponseId) + .questionId(questionId) + .optionId(opt != null ? opt.getOptionId() : null) + .createdBy(actor) + .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) + .build()); + } + } + } else { + // TEXT, DATE, AUTO_FILL — prefer answerText, then answerDate, then optionValue (legacy) + String value = answer.getAnswerText() != null ? answer.getAnswerText() + : answer.getAnswerDate() != null ? answer.getAnswerDate() + : answer.getOptionValue(); + results.add(QuestionResponse.builder() + .sectionResponseId(sectionResponseId) + .questionId(questionId) + .answerText(value) + .createdBy(actor) + .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) + .build()); + } + } + return results; + } + + private QuestionOption resolveOption(Map> optionsByQuestion, + Long questionId, String optionValue, String questionUuid) { + Map opts = optionsByQuestion.get(questionId); + if (opts == null || !opts.containsKey(optionValue)) { + log.warn("Option value '{}' not found for question '{}', storing null optionId", + optionValue, questionUuid); + return null; + } + return opts.get(optionValue); + } + + private FormResponseDTO buildFormResponseDTO(FormResponse r, List sections) { + return FormResponseDTO.builder() + .responseId(r.getResponseId()) + .beneficiaryId(r.getBeneficiaryId()) + .formId(r.getFormId()) + .versionId(r.getVersionId()) + .officerId(r.getOfficerId()) + .status(r.getStatus()) + .createdBy(r.getCreatedBy()) + .updatedBy(r.getUpdatedBy()) + .submittedAt(r.getSubmittedAt()) + .completedAt(r.getCompletedAt()) + .lastFollowUpAt(r.getLastFollowUpAt()) + .createdAt(r.getCreatedAt()) + .updatedAt(r.getUpdatedAt()) + .sections(sections) + .build(); + } + + private SectionResponseDTO buildSectionResponseDTO(SectionResponse sr, List answers) { + List answerDTOs = answers.stream() + .map(a -> QuestionResponseDTO.builder() + .questionResponseId(a.getQuestionResponseId()) + .questionId(a.getQuestionId()) + .optionId(a.getOptionId()) + .answerText(a.getAnswerText()) + .build()) + .collect(Collectors.toList()); + return SectionResponseDTO.builder() + .sectionResponseId(sr.getSectionResponseId()) + .sectionId(sr.getSectionId()) + .status(sr.getStatus()) + .savedAt(sr.getSavedAt()) + .answers(answerDTOs) + .build(); + } +} diff --git a/src/main/java/com/iemr/flw/service/impl/MalariaFollowUpServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/MalariaFollowUpServiceImpl.java index f5ef4a09..ac4f7437 100644 --- a/src/main/java/com/iemr/flw/service/impl/MalariaFollowUpServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/MalariaFollowUpServiceImpl.java @@ -51,6 +51,7 @@ public Boolean saveFollowUp(MalariaFollowUpDTO malariaFollowUpDTO,String token) dto.setUserId(jwtUtil.extractUserId(token)); MalariaFollowUp entity = new MalariaFollowUp(); BeanUtils.copyProperties(dto, entity); + entity.setModifiedBy(jwtUtil.extractUsername(token)); repository.save(entity); if(entity!=null){ incentiveLogicService.incentiveForMalariaFollowUp( @@ -76,6 +77,8 @@ public List getByUserId(Integer userId) { return repository.findByUserId(userId).stream().map(entity -> { MalariaFollowListUpDTO dto = new MalariaFollowListUpDTO(); BeanUtils.copyProperties(entity, dto); + dto.setUpdateDate(entity.getLastModDate()); + dto.setUpdatedBy(entity.getModifiedBy()); return dto; }).collect(Collectors.toList()); } diff --git a/src/main/java/com/iemr/flw/service/impl/MaternalHealthServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/MaternalHealthServiceImpl.java index 41849ee3..23973862 100644 --- a/src/main/java/com/iemr/flw/service/impl/MaternalHealthServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/MaternalHealthServiceImpl.java @@ -216,6 +216,8 @@ public String saveANCVisit(List ancVisitDTOs, Integer userId) { benVisitDetail.setModifiedBy(it.getUpdatedBy()); benVisitDetail.setLastModDate(it.getUpdatedDate()); benVisitDetail.setProviderServiceMapID(it.getProviderServiceMapID()); + if (benVisitDetail.getCreatedDate() == null) + benVisitDetail.setCreatedDate(new java.sql.Timestamp(System.currentTimeMillis())); logger.info("Saving BenVisitDetail"); @@ -418,6 +420,8 @@ public String savePNCVisit(List pncVisitDTOs) { benVisitDetail.setProcessed("N"); benVisitDetail.setModifiedBy(it.getUpdatedBy()); benVisitDetail.setLastModDate(it.getUpdatedDate()); + if (benVisitDetail.getCreatedDate() == null) + benVisitDetail.setCreatedDate(new java.sql.Timestamp(System.currentTimeMillis())); benVisitDetail = benVisitDetailsRepo.save(benVisitDetail); // Saving Data in AncCare table diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java new file mode 100644 index 00000000..10016c4a --- /dev/null +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -0,0 +1,767 @@ +package com.iemr.flw.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iemr.flw.domain.identity.RMNCHMBeneficiarydetail; +import com.iemr.flw.domain.identity.RMNCHMBeneficiarymapping; +import com.iemr.flw.domain.iemr.BenAnthropometryDetail; +import com.iemr.flw.domain.iemr.BenChiefComplaint; +import com.iemr.flw.domain.iemr.BenFlowStatus; +import com.iemr.flw.domain.iemr.BenPhysicalVitalDetail; +import com.iemr.flw.domain.iemr.BenReferDetails; +import com.iemr.flw.domain.iemr.BenVisitDetail; +import com.iemr.flw.domain.iemr.PhyGeneralExamination; +import com.iemr.flw.domain.iemr.PrescribedDrugDetail; +import com.iemr.flw.domain.iemr.PrescriptionDetail; +import com.iemr.flw.domain.iemr.StopTBDiagnostics; +import com.iemr.flw.domain.iemr.StopTBGeneralExamination; +import com.iemr.flw.domain.iemr.StopTBGeneralOpd; +import com.iemr.flw.domain.iemr.TBScreening; +import com.iemr.flw.repo.identity.BeneficiaryRepo; +import com.iemr.flw.repo.iemr.BenAnthropometryRepo; +import com.iemr.flw.repo.iemr.BenChiefComplaintRepo; +import com.iemr.flw.repo.iemr.BenFlowStatusRepo; +import com.iemr.flw.repo.iemr.BenPhysicalVitalRepo; +import com.iemr.flw.repo.iemr.BenReferDetailsRepo; +import com.iemr.flw.repo.iemr.PhyGeneralExaminationRepo; +import com.iemr.flw.repo.iemr.PrescribedDrugDetailRepo; +import com.iemr.flw.repo.iemr.PrescriptionDetailRepo; +import com.iemr.flw.repo.iemr.StopTBDiagnosticsRepo; +import com.iemr.flw.repo.iemr.StopTBGeneralExaminationRepo; +import com.iemr.flw.repo.iemr.StopTBGeneralOpdRepo; +import com.iemr.flw.repo.iemr.TBScreeningRepo; +import com.iemr.flw.service.CampConfigService; +import com.iemr.flw.service.StopTBService; +import com.iemr.flw.service.TBStopVisitService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigInteger; +import java.sql.Timestamp; +import java.util.*; + +@Service +public class StopTBServiceImpl implements StopTBService { + + private final Logger logger = LoggerFactory.getLogger(StopTBServiceImpl.class); + + @Autowired + private CampConfigService campConfigService; + + @Autowired + private BenFlowStatusRepo benFlowStatusRepo; + + @Autowired + private BeneficiaryRepo beneficiaryRepo; + + @Autowired + private StopTBGeneralExaminationRepo generalExaminationRepo; + + @Autowired + private TBScreeningRepo tbScreeningRepo; + + @Autowired + private StopTBGeneralOpdRepo generalOpdRepo; + + @Autowired + private StopTBDiagnosticsRepo diagnosticsRepo; + + @Autowired + private TBStopVisitService tbStopVisitService; + + @Autowired + private BenAnthropometryRepo benAnthropometryRepo; + + @Autowired + private BenPhysicalVitalRepo benPhysicalVitalRepo; + + @Autowired + private PhyGeneralExaminationRepo phyGeneralExaminationRepo; + + @Autowired + private BenChiefComplaintRepo benChiefComplaintRepo; + + @Autowired + private PrescriptionDetailRepo prescriptionDetailRepo; + + @Autowired + private PrescribedDrugDetailRepo prescribedDrugDetailRepo; + + @Autowired + private BenReferDetailsRepo benReferDetailsRepo; + + // ── Nurse: General Examination ──────────────────────────────────────────── + + @Override + @Transactional + public List> saveGeneralExamination(List> dataList) throws Exception { + List> results = new ArrayList<>(); + Integer vanID = campConfigService.getVanID(); + Integer parkingPlaceID = campConfigService.getParkingPlaceID(); + for (Map data : dataList) { + Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); + if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); + + Integer providerServiceMapID = getInt(data, "providerServiceMapID"); + String createdBy = getString(data, "createdBy"); + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, + createdBy, vanID, parkingPlaceID); + + StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegIDAndVisitCode(beneficiaryRegID, visit.getVisitCode()); + if (exam == null) exam = new StopTBGeneralExamination(); + boolean isNew = exam.getId() == null; + + exam.setBeneficiaryRegID(beneficiaryRegID); + exam.setVisitCode(visit.getVisitCode()); + exam.setBenVisitID(visit.getBenVisitId()); + exam.setProviderServiceMapID(providerServiceMapID); + exam.setPulseRate(getInt(data, "pulseRate")); + exam.setSystolicBP(getInt(data, "systolicBP")); + exam.setDiastolicBP(getInt(data, "diastolicBP")); + exam.setRandomBloodSugar(getDouble(data, "randomBloodSugar")); + exam.setPallorId(getInt(data, "pallorId")); + exam.setPallor(getString(data, "pallor")); + exam.setIcterusId(getInt(data, "icterusId")); + exam.setIcterus(getString(data, "icterus")); + exam.setLymphadenopathyId(getInt(data, "lymphadenopathyId")); + exam.setLymphadenopathy(getString(data, "lymphadenopathy")); + exam.setOedemaId(getInt(data, "oedemaId")); + exam.setOedema(getString(data, "oedema")); + exam.setCyanosisId(getInt(data, "cyanosisId")); + exam.setCyanosis(getString(data, "cyanosis")); + exam.setClubbingId(getInt(data, "clubbingId")); + exam.setClubbing(getString(data, "clubbing")); + exam.setKeyPopulationRiskFactorIds(toJsonString(data.get("keyPopulationRiskFactorIds"))); + exam.setKeyPopulationRiskFactors(toJsonString(data.get("keyPopulationRiskFactors"))); + exam.setHivStatusId(getInt(data, "hivStatusId")); + exam.setHivStatus(getString(data, "hivStatus")); + exam.setCreatedBy(getString(data, "createdBy")); + exam.setModifiedBy(getString(data, "createdBy")); + exam.setDeleted(false); + exam.setReferralToHWCNeeded(getBool(data, "referralToHWCNeeded")); + if (exam.getVanID() == null && vanID != null) { exam.setVanID(vanID); exam.setParkingPlaceID(parkingPlaceID); } + exam.setProcessed("N"); + + generalExaminationRepo.save(exam); + if (isNew) { + generalExaminationRepo.updateVanSerialNo(exam.getId()); + dualWriteExamToStandardTables(exam, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + } + + Map result = new HashMap<>(); + result.put("beneficiaryRegID", beneficiaryRegID); + result.put("referralToHWCNeeded", exam.getReferralToHWCNeeded()); + results.add(result); + } + return results; + } + + private boolean calculateReferralNeeded(StopTBGeneralExamination e) { + if (e.getPulseRate() != null && (e.getPulseRate() < 60 || e.getPulseRate() > 90)) return true; + if (e.getSystolicBP() != null && (e.getSystolicBP() >= 140 || e.getSystolicBP() < 90)) return true; + if (e.getDiastolicBP() != null && (e.getDiastolicBP() >= 90 || e.getDiastolicBP() < 60)) return true; + if (e.getRandomBloodSugar() != null && e.getRandomBloodSugar() >= 100) return true; + return false; + } + + @Override + public Map getAllGeneralExaminations(Integer providerServiceMapID, Integer villageID) throws Exception { + List list = (villageID != null) + ? generalExaminationRepo.findAllByProviderServiceMapIDAndVillageID(providerServiceMapID, villageID) + : generalExaminationRepo.findAllByProviderServiceMapID(providerServiceMapID); + List> items = new ArrayList<>(); + for (StopTBGeneralExamination e : list) items.add(examToMap(e)); + Map result = new HashMap<>(); + result.put("data", items); + result.put("count", items.size()); + return result; + } + + private Map examToMap(StopTBGeneralExamination e) { + Map m = new LinkedHashMap<>(); + m.put("id", e.getId()); + m.put("beneficiaryRegID", e.getBeneficiaryRegID()); + m.put("providerServiceMapID", e.getProviderServiceMapID()); + m.put("pulseRate", e.getPulseRate()); + m.put("systolicBP", e.getSystolicBP()); + m.put("diastolicBP", e.getDiastolicBP()); + m.put("randomBloodSugar", e.getRandomBloodSugar()); + m.put("pallorId", e.getPallorId()); + m.put("pallor", e.getPallor()); + m.put("icterusId", e.getIcterusId()); + m.put("icterus", e.getIcterus()); + m.put("lymphadenopathyId", e.getLymphadenopathyId()); + m.put("lymphadenopathy", e.getLymphadenopathy()); + m.put("oedemaId", e.getOedemaId()); + m.put("oedema", e.getOedema()); + m.put("cyanosisId", e.getCyanosisId()); + m.put("cyanosis", e.getCyanosis()); + m.put("clubbingId", e.getClubbingId()); + m.put("clubbing", e.getClubbing()); + m.put("keyPopulationRiskFactorIds", e.getKeyPopulationRiskFactorIds()); + m.put("keyPopulationRiskFactors", e.getKeyPopulationRiskFactors()); + m.put("hivStatusId", e.getHivStatusId()); + m.put("hivStatus", e.getHivStatus()); + m.put("referralToHWCNeeded", e.getReferralToHWCNeeded()); + m.put("createdBy", e.getCreatedBy()); + m.put("createdDate", e.getCreatedDate()); + m.put("updateDate", e.getLastModDate()); + m.put("updatedBy", e.getModifiedBy()); + return m; + } + + // ── Nurse: TB Screening ─────────────────────────────────────────────────── + + @Override + @Transactional + public List> saveNurseTBScreening(List> dataList) throws Exception { + List> results = new ArrayList<>(); + Integer vanID = campConfigService.getVanID(); + Integer parkingPlaceID = campConfigService.getParkingPlaceID(); + for (Map data : dataList) { + Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); + if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); + + Integer providerServiceMapID = getInt(data, "providerServiceMapID"); + String createdBy = getString(data, "createdBy"); + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, + createdBy, vanID, parkingPlaceID); + + TBScreening screening = tbScreeningRepo.findByBenRegIDAndVisitCode(beneficiaryRegID, visit.getVisitCode()); + if (screening == null) screening = new TBScreening(); + boolean isNew = screening.getId() == null; + + screening.setBenRegID(beneficiaryRegID); + screening.setVisitCode(visit.getVisitCode()); + screening.setProviderServiceMapID(providerServiceMapID); + screening.setCoughMoreThan2Weeks(getBool(data, "coughMoreThan2Weeks")); + screening.setBloodInSputum(getBool(data, "bloodInSputum")); + screening.setFeverMoreThan2Weeks(getBool(data, "feverMoreThan2Weeks")); + screening.setLossOfWeight(getBool(data, "lossOfWeight")); + screening.setNightSweats(getBool(data, "nightSweats")); + screening.setHistoryOfTb(getBool(data, "historyOfTb")); + screening.setTakingAntiTBDrugs(getBool(data, "takingAntiTBDrugs")); + screening.setFamilySufferingFromTB(getBool(data, "familySufferingFromTB")); + screening.setRiseOfFever(getBool(data, "riseOfFever")); + screening.setLossOfAppetite(getBool(data, "lossOfAppetite")); + screening.setContactWithTBPatient(getBool(data, "contactWithTBPatient")); + screening.setKeyPopulationRiskFactorIds(toJsonString(data.get("keyPopulationRiskFactorIds"))); + screening.setKeyPopulationRiskFactors(toJsonString(data.get("keyPopulationRiskFactors"))); + screening.setHivStatusId(getInt(data, "hivStatusId")); + screening.setHivStatus(getString(data, "hivStatus")); + String symptomaticInput = getString(data, "symptomatic"); + if ("Yes".equalsIgnoreCase(symptomaticInput)) { + screening.setSympotomatic(null); + screening.setAsymptomatic("Yes"); + } else if ("No".equalsIgnoreCase(symptomaticInput)) { + screening.setSympotomatic("Yes"); + screening.setAsymptomatic(null); + } else { + screening.setSympotomatic(null); + screening.setAsymptomatic(null); + } + screening.setReferredForDigitalChestXray(getBool(data, "referredForDigitalChestXray")); + screening.setReferredForSputumCollection(getBool(data, "referredForSputumCollection")); + screening.setSputumSampleSubmittedAt(getString(data, "sputumSampleSubmittedAt")); + screening.setRecommendedForTruenat(getBool(data, "recommendedForTruenat")); + screening.setRecommendedForLiquidCulture(getBool(data, "recommendedForLiquidCulture")); + screening.setTestDenialReasons(getString(data, "testDenialReasons")); + screening.setCreatedBy(getString(data, "createdBy")); + screening.setModifiedBy(getString(data, "createdBy")); + // PRD: date is user-provided, mandatory, not editable once submitted + if (screening.getVisitDate() == null) { + Timestamp provided = getTimestamp(data, "visitDate"); + screening.setVisitDate(provided != null ? provided : new Timestamp(System.currentTimeMillis())); + } + if (screening.getVanID() == null && vanID != null) { screening.setVanID(vanID); screening.setParkingPlaceID(parkingPlaceID); } + screening.setProcessed("N"); + + tbScreeningRepo.save(screening); + if (isNew) tbScreeningRepo.updateVanSerialNo(screening.getId()); + + Map result = new HashMap<>(); + result.put("beneficiaryRegID", beneficiaryRegID); + results.add(result); + } + return results; + } + + @Override + public Map getAllNurseTBScreenings(Integer providerServiceMapID, Integer villageID) throws Exception { + List list = (villageID != null) + ? tbScreeningRepo.findAllByProviderServiceMapIDAndVillageID(providerServiceMapID, villageID) + : tbScreeningRepo.findAllByProviderServiceMapID(providerServiceMapID); + List> items = new ArrayList<>(); + for (TBScreening s : list) items.add(screeningToMap(s)); + Map result = new HashMap<>(); + result.put("data", items); + result.put("count", items.size()); + return result; + } + + private Map screeningToMap(TBScreening s) { + Map m = new LinkedHashMap<>(); + m.put("id", s.getId()); + m.put("beneficiaryRegID", s.getBenRegID()); + m.put("providerServiceMapID", s.getProviderServiceMapID()); + m.put("coughMoreThan2Weeks", s.getCoughMoreThan2Weeks()); + m.put("bloodInSputum", s.getBloodInSputum()); + m.put("feverMoreThan2Weeks", s.getFeverMoreThan2Weeks()); + m.put("lossOfWeight", s.getLossOfWeight()); + m.put("nightSweats", s.getNightSweats()); + m.put("historyOfTb", s.getHistoryOfTb()); + m.put("takingAntiTBDrugs", s.getTakingAntiTBDrugs()); + m.put("familySufferingFromTB", s.getFamilySufferingFromTB()); + m.put("riseOfFever", s.getRiseOfFever()); + m.put("lossOfAppetite", s.getLossOfAppetite()); + m.put("contactWithTBPatient", s.getContactWithTBPatient()); + m.put("keyPopulationRiskFactorIds", s.getKeyPopulationRiskFactorIds()); + m.put("keyPopulationRiskFactors", s.getKeyPopulationRiskFactors()); + m.put("hivStatusId", s.getHivStatusId()); + m.put("hivStatus", s.getHivStatus()); + if ("Yes".equalsIgnoreCase(s.getAsymptomatic())) { + m.put("symptomatic", "Yes"); + } else if ("Yes".equalsIgnoreCase(s.getSympotomatic())) { + m.put("symptomatic", "No"); + } else { + m.put("symptomatic", null); + } + m.put("referredForDigitalChestXray", s.getReferredForDigitalChestXray()); + m.put("referredForSputumCollection", s.getReferredForSputumCollection()); + m.put("sputumSampleSubmittedAt", s.getSputumSampleSubmittedAt()); + m.put("recommendedForTruenat", s.getRecommendedForTruenat()); + m.put("recommendedForLiquidCulture", s.getRecommendedForLiquidCulture()); + m.put("testDenialReasons", s.getTestDenialReasons()); + m.put("createdBy", s.getCreatedBy()); + m.put("visitDate", s.getVisitDate()); + m.put("updateDate", s.getLastModDate()); + m.put("updatedBy", s.getModifiedBy()); + return m; + } + + // ── Nurse: General OPD ──────────────────────────────────────────────────── + + @Override + @Transactional + public List> saveGeneralOpd(List> dataList) throws Exception { + List> results = new ArrayList<>(); + Integer vanID = campConfigService.getVanID(); + Integer parkingPlaceID = campConfigService.getParkingPlaceID(); + for (Map data : dataList) { + Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); + if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); + + Integer providerServiceMapID = getInt(data, "providerServiceMapID"); + String createdBy = getString(data, "createdBy"); + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, + createdBy, vanID, parkingPlaceID); + + StopTBGeneralOpd opd = generalOpdRepo.findByBenRegIDAndVisitCode(beneficiaryRegID, visit.getVisitCode()); + if (opd == null) opd = new StopTBGeneralOpd(); + boolean isNew = opd.getId() == null; + + opd.setBenRegID(beneficiaryRegID); + opd.setVisitCode(visit.getVisitCode()); + opd.setBenVisitID(visit.getBenVisitId()); + opd.setProviderServiceMapID(providerServiceMapID); + opd.setChiefComplaint(toJsonString(data.get("chiefComplaint"))); + opd.setMedication(getString(data, "medication")); + opd.setDosage(getString(data, "dosage")); + opd.setFrequency(getString(data, "frequency")); + opd.setDuration(getString(data, "duration")); + opd.setNotes(getString(data, "notes")); + opd.setCreatedBy(getString(data, "createdBy")); + opd.setModifiedBy(getString(data, "createdBy")); + opd.setDeleted(false); + if (opd.getVanID() == null && vanID != null) { opd.setVanID(vanID); opd.setParkingPlaceID(parkingPlaceID); } + opd.setProcessed("N"); + + generalOpdRepo.save(opd); + if (isNew) { + generalOpdRepo.updateVanSerialNo(opd.getId()); + dualWriteOpdToStandardTables(opd, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + } + + Map result = new HashMap<>(); + result.put("beneficiaryRegID", beneficiaryRegID); + results.add(result); + } + return results; + } + + @Override + public Map getAllGeneralOpd(Integer providerServiceMapID, Integer villageID) throws Exception { + List list = (villageID != null) + ? generalOpdRepo.findAllByProviderServiceMapIDAndVillageID(providerServiceMapID, villageID) + : generalOpdRepo.findAllByProviderServiceMapID(providerServiceMapID); + List> items = new ArrayList<>(); + for (StopTBGeneralOpd o : list) items.add(opdToMap(o)); + Map result = new HashMap<>(); + result.put("data", items); + result.put("count", items.size()); + return result; + } + + private Map opdToMap(StopTBGeneralOpd o) { + Map m = new LinkedHashMap<>(); + m.put("id", o.getId()); + m.put("beneficiaryRegID", o.getBenRegID()); + m.put("providerServiceMapID", o.getProviderServiceMapID()); + m.put("chiefComplaint", o.getChiefComplaint()); + m.put("medication", o.getMedication()); + m.put("dosage", o.getDosage()); + m.put("frequency", o.getFrequency()); + m.put("duration", o.getDuration()); + m.put("notes", o.getNotes()); + m.put("createdBy", o.getCreatedBy()); + m.put("createdDate", o.getCreatedDate()); + m.put("updateDate", o.getLastModDate()); + m.put("updatedBy", o.getModifiedBy()); + return m; + } + + // ── Nurse: Diagnostics ──────────────────────────────────────────────────── + + @Override + @Transactional + public List> saveDiagnostics(List> dataList) throws Exception { + List> results = new ArrayList<>(); + Integer vanID = campConfigService.getVanID(); + Integer parkingPlaceID = campConfigService.getParkingPlaceID(); + for (Map data : dataList) { + Long benRegID = getLong(data, "benRegID"); + if (benRegID == null) throw new Exception("benRegID is required"); + + StopTBDiagnostics diag = diagnosticsRepo.findByBenRegID(benRegID); + if (diag == null) diag = new StopTBDiagnostics(); + boolean isNew = diag.getId() == null; + + diag.setBenRegID(benRegID); + diag.setProviderServiceMapID(getInt(data, "providerServiceMapID")); + + // PRD: date is user-provided, not editable once submitted + if (diag.getVisitDate() == null) { + Timestamp provided = getTimestamp(data, "visitDate"); + diag.setVisitDate(provided != null ? provided : new Timestamp(System.currentTimeMillis())); + } + + diag.setNikshayId(getString(data, "nikshayId")); + + diag.setIsReferredForDigitalChestXray(getBool(data, "isReferredForDigitalChestXray")); + diag.setReasonForDenialChestXray(getString(data, "reasonForDenialChestXray")); + diag.setReasonForDenialChestXrayOther(getString(data, "reasonForDenialChestXrayOther")); + diag.setIsDigitalChestXrayConducted(getBool(data, "isDigitalChestXrayConducted")); + diag.setReasonNotConductedChestXray(getString(data, "reasonNotConductedChestXray")); + diag.setReasonNotConductedChestXrayOther(getString(data, "reasonNotConductedChestXrayOther")); + diag.setDigitalChestXrayResult(getString(data, "digitalChestXrayResult")); + + diag.setIsReferredForSputumCollection(getBool(data, "isReferredForSputumCollection")); + diag.setReasonForDenialSputum(getString(data, "reasonForDenialSputum")); + diag.setReasonForDenialSputumOther(getString(data, "reasonForDenialSputumOther")); + diag.setSputumSubmittedAt(getString(data, "sputumSubmittedAt")); + + diag.setIsTruenatConducted(getBool(data, "isTruenatConducted")); + diag.setReasonNotConductedNaat(getString(data, "reasonNotConductedNaat")); + diag.setReasonNotConductedNaatOther(getString(data, "reasonNotConductedNaatOther")); + diag.setTruenatResult(getString(data, "truenatResult")); + + diag.setRecommendedForLiquidCulture(getBool(data, "recommendedForLiquidCulture")); + // PRD: liquid culture result can be updated after submission (results come after 40-45 days) + if (data.containsKey("liquidCultureResult")) { + diag.setLiquidCultureResult(getString(data, "liquidCultureResult")); + } + diag.setCreatedBy(getString(data, "createdBy")); + diag.setModifiedBy(getString(data, "createdBy")); + diag.setDeleted(false); + if (diag.getVanID() == null && vanID != null) { diag.setVanID(vanID); diag.setParkingPlaceID(parkingPlaceID); } + diag.setProcessed("N"); + + diagnosticsRepo.save(diag); + if (isNew) diagnosticsRepo.updateVanSerialNo(diag.getId()); + + Map result = new HashMap<>(); + result.put("benRegID", benRegID); + results.add(result); + } + return results; + } + + @Override + public Map getAllDiagnostics(Integer providerServiceMapID, Integer villageID) throws Exception { + List list = (villageID != null) + ? diagnosticsRepo.findAllByProviderServiceMapIDAndVillageID(providerServiceMapID, villageID) + : diagnosticsRepo.findAllByProviderServiceMapID(providerServiceMapID); + List> items = new ArrayList<>(); + for (StopTBDiagnostics d : list) items.add(diagnosticsToMap(d)); + Map result = new HashMap<>(); + result.put("data", items); + result.put("count", items.size()); + return result; + } + + private Map diagnosticsToMap(StopTBDiagnostics d) { + Map m = new LinkedHashMap<>(); + m.put("id", d.getId()); + m.put("benRegID", d.getBenRegID()); + m.put("providerServiceMapID", d.getProviderServiceMapID()); + m.put("visitDate", d.getVisitDate()); + m.put("nikshayId", d.getNikshayId()); + m.put("isReferredForDigitalChestXray", d.getIsReferredForDigitalChestXray()); + m.put("reasonForDenialChestXray", d.getReasonForDenialChestXray()); + m.put("reasonForDenialChestXrayOther", d.getReasonForDenialChestXrayOther()); + m.put("isDigitalChestXrayConducted", d.getIsDigitalChestXrayConducted()); + m.put("reasonNotConductedChestXray", d.getReasonNotConductedChestXray()); + m.put("reasonNotConductedChestXrayOther", d.getReasonNotConductedChestXrayOther()); + m.put("digitalChestXrayResult", d.getDigitalChestXrayResult()); + m.put("isReferredForSputumCollection", d.getIsReferredForSputumCollection()); + m.put("reasonForDenialSputum", d.getReasonForDenialSputum()); + m.put("reasonForDenialSputumOther", d.getReasonForDenialSputumOther()); + m.put("sputumSubmittedAt", d.getSputumSubmittedAt()); + m.put("isTruenatConducted", d.getIsTruenatConducted()); + m.put("reasonNotConductedNaat", d.getReasonNotConductedNaat()); + m.put("reasonNotConductedNaatOther", d.getReasonNotConductedNaatOther()); + m.put("truenatResult", d.getTruenatResult()); + m.put("recommendedForLiquidCulture", d.getRecommendedForLiquidCulture()); + m.put("liquidCultureResult", d.getLiquidCultureResult()); + m.put("createdBy", d.getCreatedBy()); + m.put("createdDate", d.getCreatedDate()); + m.put("updateDate", d.getLastModDate()); + m.put("updatedBy", d.getModifiedBy()); + return m; + } + + // ── Standard table dual-writes ──────────────────────────────────────────── + + private Map getRegistrationExtras(Long beneficiaryRegID) { + try { + RMNCHMBeneficiarymapping mapping = beneficiaryRepo.getById(BigInteger.valueOf(beneficiaryRegID)); + RMNCHMBeneficiarydetail detail = (mapping != null && mapping.getBenDetailsId() != null) + ? beneficiaryRepo.getDetailsById(mapping.getBenDetailsId()) + : null; + if (detail != null && detail.getOtherFields() != null && !detail.getOtherFields().isEmpty()) { + return new ObjectMapper().readValue(detail.getOtherFields(), Map.class); + } + } catch (Exception e) { + logger.warn("Cannot read otherFields for benRegID: " + beneficiaryRegID); + } + return Collections.emptyMap(); + } + + private void dualWriteExamToStandardTables(StopTBGeneralExamination exam, Long beneficiaryRegID, + BenVisitDetail visit, String createdBy, Integer vanID, Integer parkingPlaceID) { + try { + Map extras = getRegistrationExtras(beneficiaryRegID); + writeAnthropometry(extras, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + writeVitals(exam, extras, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + writePhyGeneralExam(exam, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + if (Boolean.TRUE.equals(exam.getReferralToHWCNeeded())) { + writeReferral(beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + } + } catch (Exception e) { + logger.error("Dual-write exam to standard tables failed for benRegID: " + beneficiaryRegID, e); + } + } + + private void writeAnthropometry(Map extras, Long beneficiaryRegID, + BenVisitDetail visit, String createdBy, Integer vanID, Integer parkingPlaceID) { + try { + BenAnthropometryDetail a = new BenAnthropometryDetail(); + a.setBeneficiaryRegID(beneficiaryRegID); + a.setBenVisitID(visit.getBenVisitId()); + a.setVisitCode(visit.getVisitCode()); + a.setProviderServiceMapID(visit.getProviderServiceMapID()); + a.setCreatedBy(createdBy); + a.setVanID(vanID); + a.setParkingPlaceID(parkingPlaceID); + Object h = extras.get("height"); + Object w = extras.get("weight"); + Object b = extras.get("bmi"); + if (h instanceof Number) a.setHeightCm(((Number) h).doubleValue()); + if (w instanceof Number) a.setWeightKg(((Number) w).doubleValue()); + if (b instanceof Number) a.setBmi(((Number) b).doubleValue()); + benAnthropometryRepo.save(a); + } catch (Exception e) { + logger.error("writeAnthropometry failed for benRegID: " + beneficiaryRegID, e); + } + } + + private void writeVitals(StopTBGeneralExamination exam, Map extras, + Long beneficiaryRegID, BenVisitDetail visit, String createdBy, Integer vanID, Integer parkingPlaceID) { + try { + BenPhysicalVitalDetail v = new BenPhysicalVitalDetail(); + v.setBeneficiaryRegID(beneficiaryRegID); + v.setBenVisitID(visit.getBenVisitId()); + v.setVisitCode(visit.getVisitCode()); + v.setProviderServiceMapID(visit.getProviderServiceMapID()); + v.setCreatedBy(createdBy); + v.setVanID(vanID); + v.setParkingPlaceID(parkingPlaceID); + Object t = extras.get("temperatureValue"); + if (t instanceof Number) v.setTemperature(((Number) t).doubleValue()); + if (exam.getPulseRate() != null) v.setPulseRate(exam.getPulseRate().shortValue()); + if (exam.getSystolicBP() != null) v.setSystolicBP(exam.getSystolicBP().shortValue()); + if (exam.getDiastolicBP() != null) v.setDiastolicBP(exam.getDiastolicBP().shortValue()); + if (exam.getRandomBloodSugar() != null) v.setBloodGlucoseRandom(exam.getRandomBloodSugar().shortValue()); + benPhysicalVitalRepo.save(v); + } catch (Exception e) { + logger.error("writeVitals failed for benRegID: " + beneficiaryRegID, e); + } + } + + private void writePhyGeneralExam(StopTBGeneralExamination exam, Long beneficiaryRegID, + BenVisitDetail visit, String createdBy, Integer vanID, Integer parkingPlaceID) { + try { + PhyGeneralExamination g = new PhyGeneralExamination(); + g.setBeneficiaryRegID(beneficiaryRegID); + g.setBenVisitID(visit.getBenVisitId()); + g.setVisitCode(visit.getVisitCode()); + g.setProviderServiceMapID(visit.getProviderServiceMapID()); + g.setCreatedBy(createdBy); + g.setVanID(vanID); + g.setParkingPlaceID(parkingPlaceID); + g.setPallor(exam.getPallor()); + g.setJaundice(exam.getIcterus()); + g.setCyanosis(exam.getCyanosis()); + g.setClubbing(exam.getClubbing()); + g.setLymphadenopathy(exam.getLymphadenopathy()); + g.setEdema(exam.getOedema()); + phyGeneralExaminationRepo.save(g); + } catch (Exception e) { + logger.error("writePhyGeneralExam failed for benRegID: " + beneficiaryRegID, e); + } + } + + private void writeReferral(Long beneficiaryRegID, BenVisitDetail visit, + String createdBy, Integer vanID, Integer parkingPlaceID) { + try { + BenReferDetails r = new BenReferDetails(); + r.setBeneficiaryRegID(beneficiaryRegID); + r.setBenVisitID(visit.getBenVisitId()); + r.setVisitCode(visit.getVisitCode()); + r.setProviderServiceMapID(visit.getProviderServiceMapID()); + r.setCreatedBy(createdBy); + r.setVanID(vanID); + r.setParkingPlaceID(parkingPlaceID); + r.setReferredToInstituteName("HWC"); + r.setReferralReason("Stop TB Referral"); + benReferDetailsRepo.save(r); + } catch (Exception e) { + logger.error("writeReferral failed for benRegID: " + beneficiaryRegID, e); + } + } + + private void dualWriteOpdToStandardTables(StopTBGeneralOpd opd, Long beneficiaryRegID, + BenVisitDetail visit, String createdBy, Integer vanID, Integer parkingPlaceID) { + try { + writeChiefComplaint(opd.getChiefComplaint(), beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + writePrescription(opd, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + } catch (Exception e) { + logger.error("Dual-write OPD to standard tables failed for benRegID: " + beneficiaryRegID, e); + } + } + + private void writeChiefComplaint(String complaint, Long beneficiaryRegID, BenVisitDetail visit, + String createdBy, Integer vanID, Integer parkingPlaceID) { + if (complaint == null || complaint.isBlank()) return; + try { + BenChiefComplaint c = new BenChiefComplaint(); + c.setBeneficiaryRegID(beneficiaryRegID); + c.setBenVisitID(visit.getBenVisitId()); + c.setVisitCode(visit.getVisitCode()); + c.setProviderServiceMapID(visit.getProviderServiceMapID()); + c.setCreatedBy(createdBy); + c.setVanID(vanID); + c.setParkingPlaceID(parkingPlaceID); + c.setChiefComplaint(complaint); + benChiefComplaintRepo.save(c); + } catch (Exception e) { + logger.error("writeChiefComplaint failed for benRegID: " + beneficiaryRegID, e); + } + } + + private void writePrescription(StopTBGeneralOpd opd, Long beneficiaryRegID, BenVisitDetail visit, + String createdBy, Integer vanID, Integer parkingPlaceID) { + if (opd.getMedication() == null || opd.getMedication().isBlank()) return; + try { + PrescriptionDetail p = new PrescriptionDetail(); + p.setBeneficiaryRegID(beneficiaryRegID); + p.setBenVisitID(visit.getBenVisitId()); + p.setVisitCode(visit.getVisitCode()); + p.setProviderServiceMapID(visit.getProviderServiceMapID()); + p.setCreatedBy(createdBy); + p.setVanID(vanID); + p.setParkingPlaceID(parkingPlaceID); + p.setInstruction(opd.getNotes()); + p = prescriptionDetailRepo.save(p); + + PrescribedDrugDetail d = new PrescribedDrugDetail(); + d.setBeneficiaryRegID(beneficiaryRegID); + d.setBenVisitID(visit.getBenVisitId()); + d.setVisitCode(visit.getVisitCode()); + d.setProviderServiceMapID(visit.getProviderServiceMapID()); + d.setCreatedBy(createdBy); + d.setVanID(vanID); + d.setParkingPlaceID(parkingPlaceID); + d.setPrescriptionID(p.getPrescriptionID()); + d.setDrugName(opd.getMedication()); + d.setDose(opd.getDosage()); + d.setFrequency(opd.getFrequency()); + d.setDuration(opd.getDuration()); + prescribedDrugDetailRepo.save(d); + } catch (Exception e) { + logger.error("writePrescription failed for benRegID: " + beneficiaryRegID, e); + } + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private String toJsonString(Object value) { + if (value == null) return null; + if (value instanceof String) return (String) value; + return new com.google.gson.Gson().toJson(value); + } + + private String getString(Map m, String key) { + Object v = m.get(key); + return v != null ? v.toString() : null; + } + + private Long getLong(Map m, String key) { + Object v = m.get(key); + if (v == null) return null; + if (v instanceof Number) return ((Number) v).longValue(); + try { return Long.parseLong(v.toString()); } catch (Exception e) { return null; } + } + + private Integer getInt(Map m, String key) { + Object v = m.get(key); + if (v == null) return null; + if (v instanceof Number) return ((Number) v).intValue(); + try { return Integer.parseInt(v.toString()); } catch (Exception e) { return null; } + } + + private Double getDouble(Map m, String key) { + Object v = m.get(key); + if (v == null) return null; + if (v instanceof Number) return ((Number) v).doubleValue(); + try { return Double.parseDouble(v.toString()); } catch (Exception e) { return null; } + } + + private Boolean getBool(Map m, String key) { + Object v = m.get(key); + if (v == null) return null; + if (v instanceof Boolean) return (Boolean) v; + return Boolean.parseBoolean(v.toString()); + } + + private Timestamp getTimestamp(Map m, String key) { + Object v = m.get(key); + if (v == null) return null; + if (v instanceof Number) return new Timestamp(((Number) v).longValue()); + try { return new Timestamp(Long.parseLong(v.toString())); } catch (Exception e) { return null; } + } + +} diff --git a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java index e89928de..f9240c5c 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java @@ -2,12 +2,20 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.iemr.flw.domain.iemr.DynamicForm; import com.iemr.flw.domain.iemr.TBConfirmedCaseDTO; import com.iemr.flw.domain.iemr.TBConfirmedCase; +import com.iemr.flw.domain.iemr.BenVisitDetail; import com.iemr.flw.dto.iemr.TBConfirmedCasesResponseDTO; +import com.iemr.flw.repo.identity.BeneficiaryRepo; +import com.iemr.flw.repo.iemr.DynamicFormRepo; +import com.iemr.flw.repo.iemr.FormResponseRepo; import com.iemr.flw.repo.iemr.TBConfirmedTreatmentRepository; +import com.iemr.flw.seeder.TbCounsellingFormSeeder; import com.iemr.flw.service.IncentiveLogicService; +import com.iemr.flw.service.CampConfigService; import com.iemr.flw.service.TBConfirmedCaseService; +import com.iemr.flw.service.TBStopVisitService; import com.iemr.flw.utils.JwtUtil; import com.iemr.flw.utils.LocalDateAdapter; import com.iemr.flw.utils.response.OutputResponse; @@ -18,7 +26,12 @@ import java.sql.Timestamp; import java.time.LocalDate; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; @Service @@ -27,6 +40,8 @@ public class TBConfirmedCaseServiceImpl implements TBConfirmedCaseService { private static final Logger logger = LoggerFactory.getLogger(TBConfirmedCaseServiceImpl.class); private final TBConfirmedTreatmentRepository repository; + private final FormResponseRepo formResponseRepo; + private final DynamicFormRepo dynamicFormRepo; @Autowired private IncentiveLogicService incentiveLogicService; @@ -34,8 +49,22 @@ public class TBConfirmedCaseServiceImpl implements TBConfirmedCaseService { @Autowired private JwtUtil jwtUtil; - public TBConfirmedCaseServiceImpl(TBConfirmedTreatmentRepository repository) { + + @Autowired + private CampConfigService campConfigService; + + @Autowired + private TBStopVisitService tbStopVisitService; + + @Autowired + private BeneficiaryRepo beneficiaryRepo; + + public TBConfirmedCaseServiceImpl(TBConfirmedTreatmentRepository repository, + FormResponseRepo formResponseRepo, + DynamicFormRepo dynamicFormRepo) { this.repository = repository; + this.formResponseRepo = formResponseRepo; + this.dynamicFormRepo = dynamicFormRepo; } @Override @@ -45,11 +74,22 @@ public String save(List request, String authorisation) throw try { if (request != null) { logger.info("Saving TB confirmed case: " + request); + Integer vanID = campConfigService.getVanID(); + Integer parkingPlaceID = campConfigService.getParkingPlaceID(); - TBConfirmedCase entity = new TBConfirmedCase(); for(TBConfirmedCaseDTO dto:request){ + String modifiedBy = jwtUtil.extractUsername(authorisation); + Long beneficiaryRegID = beneficiaryRepo.getRegIDFromBenId(dto.getBenId()); + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, null, + modifiedBy, vanID, parkingPlaceID); + + List existing = repository.findByBenIdAndVisitCode(dto.getBenId(), visit.getVisitCode()); + boolean isNew = existing.isEmpty(); + TBConfirmedCase entity = isNew ? new TBConfirmedCase() : existing.get(0); entity.setBenId(dto.getBenId()); + entity.setVisitCode(visit.getVisitCode()); entity.setUserId(jwtUtil.extractUserId(authorisation)); + entity.setModifiedBy(modifiedBy); entity.setRegimenType(dto.getRegimenType()); entity.setTreatmentStartDate(dto.getTreatmentStartDate()); entity.setExpectedTreatmentCompletionDate(dto.getExpectedTreatmentCompletionDate()); @@ -64,10 +104,12 @@ public String save(List request, String authorisation) throw entity.setPlaceOfDeath(dto.getPlaceOfDeath()); entity.setReasonForDeath(dto.getReasonForDeath()); entity.setReasonForNotCompleting(dto.getReasonForNotCompleting()); + if (entity.getVanID() == null && vanID != null) { entity.setVanID(vanID); entity.setParkingPlaceID(parkingPlaceID); } + entity.setProcessed("N"); if(entity!=null){ - repository.save(entity); + TBConfirmedCase saved = repository.save(entity); + if (isNew) repository.updateVanSerialNo(saved.getId()); checkIncentive(entity); - } } @@ -144,22 +186,46 @@ public String getByBenId(Long benId, String authorisation) throws Exception { @Override public String getByUserId(String authorisation) throws Exception { - TBConfirmedCasesResponseDTO tbConfirmedCasesResponseDTO = new TBConfirmedCasesResponseDTO(); Integer userId = jwtUtil.extractUserId(authorisation); List list = repository.findByUserId(userId); - tbConfirmedCasesResponseDTO.setUserId(userId); - tbConfirmedCasesResponseDTO.setTbConfirmedCases(list); + return buildTbConfirmedCasesResponse(userId, list); + } + + @Override + public String getByProviderServiceMapId(Integer providerServiceMapID, Integer villageID) throws Exception { + List list = repository.getByProviderServiceMapIdAndVillageId(providerServiceMapID, villageID); + return buildTbConfirmedCasesResponse(null, list); + } + + private String buildTbConfirmedCasesResponse(Integer userId, List list) { + List dtoList = list.stream().map(this::toDTO).collect(Collectors.toList()); + + List benIds = dtoList.stream().map(TBConfirmedCaseDTO::getBenId).collect(Collectors.toList()); + Set counselledBenIds = Collections.emptySet(); + if (!benIds.isEmpty()) { + Optional counsellingForm = dynamicFormRepo.findByFormUuid(TbCounsellingFormSeeder.FORM_UUID); + if (counsellingForm.isPresent()) { + counselledBenIds = new HashSet<>(formResponseRepo.findCounselledBenIds(benIds, counsellingForm.get().getFormId(), "COMPLETE")); + } + } + final Set counselled = counselledBenIds; + dtoList.forEach(dto -> dto.setCounselled(counselled.contains(dto.getBenId()))); + + Map response = new java.util.HashMap<>(); + response.put("userId", userId); + response.put("tbConfirmedCases", dtoList); Gson gson = new GsonBuilder() .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) + .setDateFormat("MMM dd, yyyy h:mm:ss a") .create(); - - return gson.toJson(tbConfirmedCasesResponseDTO); + return gson.toJson(response); } // Utility: convert entity -> DTO private TBConfirmedCaseDTO toDTO(TBConfirmedCase entity) { TBConfirmedCaseDTO dto = new TBConfirmedCaseDTO(); + dto.setId(entity.getId()); dto.setBenId(entity.getBenId()); dto.setUserId(entity.getUserId()); dto.setRegimenType(entity.getRegimenType()); @@ -176,6 +242,8 @@ private TBConfirmedCaseDTO toDTO(TBConfirmedCase entity) { dto.setPlaceOfDeath(entity.getPlaceOfDeath()); dto.setReasonForDeath(entity.getReasonForDeath()); dto.setReasonForNotCompleting(entity.getReasonForNotCompleting()); + dto.setUpdateDate(entity.getLastModDate()); + dto.setUpdatedBy(entity.getModifiedBy()); return dto; } diff --git a/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java new file mode 100644 index 00000000..4f36b8b4 --- /dev/null +++ b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java @@ -0,0 +1,61 @@ +package com.iemr.flw.service.impl; + +import com.iemr.flw.domain.iemr.BenVisitDetail; +import com.iemr.flw.repo.iemr.BenVisitDetailsRepo; +import com.iemr.flw.service.TBStopVisitService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.sql.Timestamp; +import java.time.LocalDate; + +@Service +public class TBStopVisitServiceImpl implements TBStopVisitService { + + @Autowired + private BenVisitDetailsRepo benVisitDetailsRepo; + + @Override + @Transactional + public BenVisitDetail getOrCreateVisitForToday(Long beneficiaryRegID, Integer providerServiceMapID, + String createdBy, Integer vanID, Integer parkingPlaceID) { + LocalDate today = LocalDate.now(); + Timestamp dayStart = Timestamp.valueOf(today.atStartOfDay()); + Timestamp dayEnd = Timestamp.valueOf(today.plusDays(1).atStartOfDay()); + + BenVisitDetail existing = benVisitDetailsRepo.findStopTBVisitForToday(beneficiaryRegID, dayStart, dayEnd); + if (existing != null) return existing; + + Integer visitCount = benVisitDetailsRepo.countStopTBVisits(beneficiaryRegID); + short visitNo = (short) ((visitCount != null ? visitCount : 0) + 1); + + BenVisitDetail visit = new BenVisitDetail(); + visit.setBeneficiaryRegId(beneficiaryRegID); + visit.setVisitNo(visitNo); + visit.setVisitCategory("Stop TB"); + visit.setVisitReason("Screening"); + visit.setVisitDateTime(new Timestamp(System.currentTimeMillis())); + visit.setProviderServiceMapID(providerServiceMapID); + visit.setCreatedBy(createdBy); + visit.setCreatedDate(new Timestamp(System.currentTimeMillis())); + visit.setVanId(vanID); + visit.setParkingPlaceId(parkingPlaceID); + visit.setDeleted(false); + visit.setProcessed("N"); + visit = benVisitDetailsRepo.save(visit); + + // VisitCode: sessionID(1) + vanID(5-digit) + benVisitId(8-digit) — MMU formula + long visitCode = generateVisitCode(visit.getBenVisitId(), vanID != null ? vanID : 1); + visit.setVisitCode(visitCode); + // saveAndFlush ensures the VisitCode UPDATE reaches DB before child tables (e.g. t_benchiefcomplaint) + // insert with FK on VisitCode — same transaction, lazy flush otherwise causes FK violation + return benVisitDetailsRepo.saveAndFlush(visit); + } + + private long generateVisitCode(long visitId, int vanID) { + String vanStr = String.format("%05d", vanID); + String visitStr = String.format("%08d", visitId); + return Long.parseLong("1" + vanStr + visitStr); + } +} diff --git a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java index 2a8c0eeb..9b4c12fa 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -2,12 +2,16 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.iemr.flw.domain.iemr.BenVisitDetail; import com.iemr.flw.domain.iemr.TBSuspected; import com.iemr.flw.dto.identity.GetBenRequestHandler; import com.iemr.flw.dto.iemr.TBSuspectedDTO; import com.iemr.flw.dto.iemr.TBSuspectedRequestDTO; +import com.iemr.flw.repo.identity.BeneficiaryRepo; import com.iemr.flw.repo.iemr.TBSuspectedRepo; import com.iemr.flw.service.IncentiveLogicService; +import com.iemr.flw.service.CampConfigService; +import com.iemr.flw.service.TBStopVisitService; import com.iemr.flw.service.TBSuspectedService; import org.modelmapper.ModelMapper; import org.slf4j.Logger; @@ -25,6 +29,12 @@ public class TBSuspectedServiceImpl implements TBSuspectedService { private final ModelMapper modelMapper = new ModelMapper(); @Autowired private TBSuspectedRepo tbSuspectedRepo; + @Autowired + private CampConfigService campConfigService; + @Autowired + private TBStopVisitService tbStopVisitService; + @Autowired + private BeneficiaryRepo beneficiaryRepo; @Autowired private IncentiveLogicService incentiveLogicService; @@ -36,10 +46,15 @@ public String getByBenId(Long benId, String authorisation) throws Exception { @Override public String save(TBSuspectedRequestDTO requestDTO) throws Exception { - requestDTO.getTbSuspectedList().forEach(tbSuspectedDTO -> - { + Integer vanID = campConfigService.getVanID(); + Integer parkingPlaceID = campConfigService.getParkingPlaceID(); + for (TBSuspectedDTO tbSuspectedDTO : requestDTO.getTbSuspectedList()) { + Long beneficiaryRegID = beneficiaryRepo.getRegIDFromBenId(tbSuspectedDTO.getBenId()); + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, null, + requestDTO.getUserId() != null ? requestDTO.getUserId().toString() : null, vanID, parkingPlaceID); + TBSuspected tbSuspected = - tbSuspectedRepo.getByUserIdAndBenId(tbSuspectedDTO.getBenId(), requestDTO.getUserId()); + tbSuspectedRepo.getByUserIdAndBenIdAndVisitCode(tbSuspectedDTO.getBenId(), requestDTO.getUserId(), visit.getVisitCode()); if (tbSuspected == null) { tbSuspected = new TBSuspected(); @@ -52,7 +67,12 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { } tbSuspected.setUserId(requestDTO.getUserId()); + tbSuspected.setVisitCode(visit.getVisitCode()); + if (tbSuspected.getVanID() == null && vanID != null) { tbSuspected.setVanID(vanID); tbSuspected.setParkingPlaceID(parkingPlaceID); } + tbSuspected.setProcessed("N"); + tbSuspectedRepo.save(tbSuspected); + tbSuspectedRepo.updateVanSerialNo(tbSuspected.getId()); if(tbSuspected!=null){ if(tbSuspected.getIsConfirmed()){ incentiveLogicService.incentiveForTbSuspected(tbSuspected.getBenId(),tbSuspected.getVisitDate(),tbSuspected.getVisitDate(),tbSuspected.getUserId()); @@ -60,17 +80,21 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { } } - - }); + } return "no of tb suspected items saved:" + requestDTO.getTbSuspectedList().size(); } @Override public String getByUserId(GetBenRequestHandler request) { List dtos = new ArrayList<>(); - List tbSuspectedList = tbSuspectedRepo.getByUserId(request.getAshaId(), request.getFromDate(), request.getToDate()); - tbSuspectedList.forEach(tbSuspected -> { - dtos.add(modelMapper.map(tbSuspected, TBSuspectedDTO.class)); + List tbSuspectedList = request.getProviderServiceMapID() != null + ? tbSuspectedRepo.getByProviderServiceMapIdAndVillageId(request.getProviderServiceMapID(), request.getVillageID()) + : tbSuspectedRepo.getByUserId(request.getAshaId(), request.getFromDate(), request.getToDate()); + for (TBSuspected tbSuspected : tbSuspectedList) { + TBSuspectedDTO dto = modelMapper.map(tbSuspected, TBSuspectedDTO.class); + dto.setUpdateDate(tbSuspected.getLastModDate()); + dto.setUpdatedBy(tbSuspected.getModifiedBy()); + dtos.add(dto); if (tbSuspected != null && Boolean.TRUE.equals(tbSuspected.getIsConfirmed())) { incentiveLogicService.incentiveForTbSuspected( @@ -80,7 +104,7 @@ public String getByUserId(GetBenRequestHandler request) { tbSuspected.getUserId() ); } - }); + } TBSuspectedRequestDTO tbSuspectedRequestDTO = new TBSuspectedRequestDTO(); tbSuspectedRequestDTO.setTbSuspectedList(dtos); tbSuspectedRequestDTO.setUserId(request.getAshaId()); diff --git a/src/main/java/com/iemr/flw/service/impl/UserServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/UserServiceImpl.java index 60ae973e..bd2d3e7c 100644 --- a/src/main/java/com/iemr/flw/service/impl/UserServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/UserServiceImpl.java @@ -23,6 +23,51 @@ public UserServiceRoleDTO getUserDetail(Integer userId) { logger.info("calling getUserRole for userId: " + userId); UserServiceRoleDTO userRole = userServiceRoleRepo.getUserRole(userId).get(0); userRole.setFacilityData(facilityDataService.buildFacilityData(userId, userRole.getRoleName())); + + // Stop TB / Nikshay — additive only. This naturally returns nothing for + // any user whose rows don't have NikshayTUID set, i.e. every non-Stop-TB + // user. Fetched first so the district-by-block patch below can tell + // Stop TB users apart and skip them. + java.util.List nikshayResults = userServiceRoleRepo.getNikshayLocationScope(userId, userRole.getProviderServiceMapId()); + boolean isStopTBUser = nikshayResults != null && !nikshayResults.isEmpty() + && nikshayResults.get(0) != null && nikshayResults.get(0).length == 6 + && nikshayResults.get(0)[0] != null; + + // Stop TB's BlockId holds a Nikshay TU ID, not an AMRIT BlockID - + // joining it against m_districtblock/m_district (what + // getDistrictByBlockId does) would resolve to whatever AMRIT + // district happens to share that same numeric ID by coincidence, + // not real data. Skip this patch for Stop TB users. + if (!isStopTBUser && userRole.getWorkingDistrictId() == null && userRole.getBlockId() != null) { + java.util.List districtResults = userServiceRoleRepo.getDistrictByBlockId(userRole.getBlockId()); + if (districtResults != null && !districtResults.isEmpty()) { + Object[] district = districtResults.get(0); + if (district != null && district.length == 2) { + userRole.setWorkingDistrictId(((Number) district[0]).intValue()); + userRole.setWorkingDistrictName((String) district[1]); + } + } + } + + if (isStopTBUser) { + Object[] nikshay = nikshayResults.get(0); + userRole.setTuId((String) nikshay[0]); + userRole.setTuName((String) nikshay[1]); + userRole.setHealthFacilityId((String) nikshay[2]); + userRole.setHealthFacilityName((String) nikshay[3]); + // Stop TB never sets WorkingLocationID, so workingDistrictId/Name + // (derived from it) are always null. DistrictID sits directly on + // the mapping row instead - Stop TB has no "work location" + // indirection layer the way other servicelines do, so read it + // straight from there rather than trying to derive it. + if (nikshay[4] != null) { + userRole.setWorkingDistrictId(((Number) nikshay[4]).intValue()); + } + if (nikshay[5] != null) { + userRole.setWorkingDistrictName((String) nikshay[5]); + } + } + return userRole; } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 034fbcc4..84cca481 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -62,7 +62,7 @@ spring.jpa.defer-datasource-initialization=true fhir-url= # TM Config -tm-url= +tm-url=https://amritdemo.piramalswasthya.org/tm-api springdoc.api-docs.enabled=true springdoc.swagger-ui.enabled=true