From 45acd18188817a7b20ec6be875c67a6e92f7fa10 Mon Sep 17 00:00:00 2001 From: Zapper9982 Date: Thu, 25 Sep 2025 15:19:33 +0530 Subject: [PATCH 01/97] feat(linting): commitlint ,Commitzen, Husky configuration --- .github/workflows/commit-lint.yml | 34 + .gitignore | 1 + .husky/commit-msg | 1 + .husky/pre-commit | 1 + README.md | 43 + commitlint.config.js | 36 + package-lock.json | 3433 +++++++++++++++++++++++++++++ package.json | 30 + 8 files changed, 3579 insertions(+) create mode 100644 .github/workflows/commit-lint.yml create mode 100644 .husky/commit-msg create mode 100644 .husky/pre-commit create mode 100644 commitlint.config.js create mode 100644 package-lock.json create mode 100644 package.json 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/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 From d97c0bef4e4342a63d1964697736b53da8900d27 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 7 May 2026 09:48:26 +0530 Subject: [PATCH 02/97] feat: added new register api for stoptb --- .../iemr/flw/controller/StopTBController.java | 97 +++++++++ .../iemr/flw/domain/iemr/BenFlowStatus.java | 82 ++++++++ .../flw/domain/iemr/StopTBRegistration.java | 70 +++++++ .../flw/dto/iemr/StopTBRegistrationDto.java | 34 ++++ .../iemr/flw/repo/iemr/BenFlowStatusRepo.java | 26 +++ .../flw/repo/iemr/StopTBRegistrationRepo.java | 25 +++ .../com/iemr/flw/service/StopTBService.java | 12 ++ .../flw/service/impl/StopTBServiceImpl.java | 190 ++++++++++++++++++ src/main/resources/application.properties | 2 +- 9 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/iemr/flw/controller/StopTBController.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/BenFlowStatus.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java create mode 100644 src/main/java/com/iemr/flw/dto/iemr/StopTBRegistrationDto.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java create mode 100644 src/main/java/com/iemr/flw/service/StopTBService.java create mode 100644 src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java 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..449d8326 --- /dev/null +++ b/src/main/java/com/iemr/flw/controller/StopTBController.java @@ -0,0 +1,97 @@ +package com.iemr.flw.controller; + +import com.iemr.flw.dto.iemr.StopTBRegistrationDto; +import com.iemr.flw.service.StopTBService; +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.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.LinkedHashMap; +import java.util.Map; + +@RestController +@RequestMapping("/stoptb") +public class StopTBController { + + private final Logger logger = LoggerFactory.getLogger(StopTBController.class); + + @Autowired + private StopTBService stopTBService; + + @PostMapping("/registration/save") + @Operation(summary = "Save Stop TB beneficiary registration") + public ResponseEntity> saveRegistration( + @RequestBody String requestBody, + @RequestHeader("Authorization") String authorization) { + Map response = new LinkedHashMap<>(); + try { + Map data = stopTBService.saveRegistration(requestBody, authorization); + response.put("data", data); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in saveRegistration: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error in registration: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } + + @PostMapping("/registration/get") + @Operation(summary = "Get Stop TB beneficiary registration details") + public ResponseEntity> getRegistration(@RequestBody StopTBRegistrationDto dto) { + Map response = new LinkedHashMap<>(); + try { + Map data = stopTBService.getRegistration(dto); + response.put("data", data); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in getRegistration: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error fetching registration: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } + + @PostMapping("/registrar/worklist") + @Operation(summary = "Get registrar worklist for Stop TB") + public ResponseEntity> getRegistrarWorklist(@RequestBody StopTBRegistrationDto dto) { + Map response = new LinkedHashMap<>(); + try { + Map data = stopTBService.getRegistrarWorklist(dto); + response.put("data", data); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in getRegistrarWorklist: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error fetching worklist: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } + + @PostMapping("/nurse/worklist") + @Operation(summary = "Get nurse worklist for Stop TB") + public ResponseEntity> getNurseWorklist(@RequestBody StopTBRegistrationDto dto) { + Map response = new LinkedHashMap<>(); + try { + Map data = stopTBService.getNurseWorklist(dto); + response.put("data", data); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in getNurseWorklist: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error fetching nurse worklist: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } +} 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/StopTBRegistration.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java new file mode 100644 index 00000000..1ca6aff8 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java @@ -0,0 +1,70 @@ +package com.iemr.flw.domain.iemr; + +import lombok.Data; + +import jakarta.persistence.*; +import java.sql.Timestamp; + +@Entity +@Table(name = "tb_stoptb_registration", schema = "db_iemr") +@Data +public class StopTBRegistration { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "benRegID") + private Long benRegID; + + @Column(name = "providerServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "personFrom") + private String personFrom; + + @Column(name = "caseFindingType") + private String caseFindingType; + + @Column(name = "tuId") + private Integer tuId; + + @Column(name = "tuName") + private String tuName; + + @Column(name = "healthFacilityId") + private Integer healthFacilityId; + + @Column(name = "healthFacilityName") + private String healthFacilityName; + + @Column(name = "weight") + private Double weight; + + @Column(name = "height") + private Double height; + + @Column(name = "bmi") + private Double bmi; + + @Column(name = "temperatureValue") + private Double temperatureValue; + + @Column(name = "isMobileAvailable") + private Boolean isMobileAvailable; + + @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) + private Timestamp lastModDate; + + @Column(name = "deleted") + private Boolean deleted; +} diff --git a/src/main/java/com/iemr/flw/dto/iemr/StopTBRegistrationDto.java b/src/main/java/com/iemr/flw/dto/iemr/StopTBRegistrationDto.java new file mode 100644 index 00000000..11ae1291 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/StopTBRegistrationDto.java @@ -0,0 +1,34 @@ +package com.iemr.flw.dto.iemr; + +import com.fasterxml.jackson.annotation.JsonRawValue; +import lombok.Data; + +@Data +public class StopTBRegistrationDto { + + // Standard beneficiary fields — forwarded to Common-API + @JsonRawValue + private String benD; + + // Stop TB specific fields + private Integer providerServiceMapID; + private String personFrom; + private String caseFindingType; + private Integer tuId; + private String tuName; + private Integer healthFacilityId; + private String healthFacilityName; + private Boolean isMobileAvailable; + + // Anthropometry fields + private Double weight; + private Double height; + private Double bmi; + private Double temperatureValue; + + private String createdBy; + + // For get/update + private Long benRegID; + private String modifiedBy; +} 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..c44cbab9 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java @@ -0,0 +1,26 @@ +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.nurseFlag = 1 AND b.visitDate >= :fromDate AND b.visitDate <= :toDate AND b.deleted = false") + List getNurseWorklist( + @Param("psmId") Integer psmId, + @Param("fromDate") Timestamp fromDate, + @Param("toDate") Timestamp toDate + ); + + @Modifying + @Query("UPDATE BenFlowStatus b SET b.nurseFlag = 9, b.pharmacistFlag = 1 WHERE b.benFlowID = :benFlowID") + int updateAfterNurseSubmit(@Param("benFlowID") Long benFlowID); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java new file mode 100644 index 00000000..003369d4 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java @@ -0,0 +1,25 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.StopTBRegistration; +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 StopTBRegistrationRepo extends JpaRepository { + + @Query("SELECT t FROM StopTBRegistration t WHERE t.benRegID = :benRegID AND t.deleted = false") + StopTBRegistration findByBenRegID(@Param("benRegID") Long benRegID); + + @Query("SELECT t FROM StopTBRegistration t WHERE t.providerServiceMapID = :psmId AND t.createdBy = :createdBy AND t.createdDate >= :fromDate AND t.createdDate <= :toDate AND t.deleted = false") + List getRegistrarWorklist( + @Param("psmId") Integer psmId, + @Param("createdBy") String createdBy, + @Param("fromDate") Timestamp fromDate, + @Param("toDate") Timestamp toDate + ); +} 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..8372df1f --- /dev/null +++ b/src/main/java/com/iemr/flw/service/StopTBService.java @@ -0,0 +1,12 @@ +package com.iemr.flw.service; + +import com.iemr.flw.dto.iemr.StopTBRegistrationDto; + +import java.util.Map; + +public interface StopTBService { + Map saveRegistration(String requestBody, String authorization) throws Exception; + Map getRegistration(StopTBRegistrationDto dto) throws Exception; + Map getRegistrarWorklist(StopTBRegistrationDto dto) throws Exception; + Map getNurseWorklist(StopTBRegistrationDto dto) throws Exception; +} 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..c3c3ac14 --- /dev/null +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -0,0 +1,190 @@ +package com.iemr.flw.service.impl; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.iemr.flw.domain.iemr.BenFlowStatus; +import com.iemr.flw.domain.iemr.StopTBRegistration; +import com.iemr.flw.dto.iemr.StopTBRegistrationDto; +import com.iemr.flw.repo.iemr.BenFlowStatusRepo; +import com.iemr.flw.repo.iemr.StopTBRegistrationRepo; +import com.iemr.flw.service.StopTBService; +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.http.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class StopTBServiceImpl implements StopTBService { + + private final Logger logger = LoggerFactory.getLogger(StopTBServiceImpl.class); + + @Autowired + private StopTBRegistrationRepo stopTBRegistrationRepo; + + @Autowired + private BenFlowStatusRepo benFlowStatusRepo; + + @Value("${tm-url}") + private String tmUrl; + + @Override + @Transactional + public Map saveRegistration(String requestBody, String authorization) throws Exception { + Map result = new HashMap<>(); + + // Parse incoming JSON + JsonObject jsonObject = JsonParser.parseString(requestBody).getAsJsonObject(); + + // Extract Stop TB specific fields + String personFrom = getStringField(jsonObject, "personFrom"); + String caseFindingType = getStringField(jsonObject, "caseFindingType"); + Integer tuId = getIntField(jsonObject, "tuId"); + String tuName = getStringField(jsonObject, "tuName"); + Integer healthFacilityId = getIntField(jsonObject, "healthFacilityId"); + String healthFacilityName = getStringField(jsonObject, "healthFacilityName"); + Double weight = getDoubleField(jsonObject, "weight"); + Double height = getDoubleField(jsonObject, "height"); + Double bmi = getDoubleField(jsonObject, "bmi"); + Double temperatureValue = getDoubleField(jsonObject, "temperatureValue"); + String createdBy = getStringField(jsonObject, "createdBy"); + Integer providerServiceMapID = getIntField(jsonObject, "providerServiceMapID"); + + // Remove Stop TB fields — forward clean TM-API request + jsonObject.remove("personFrom"); + jsonObject.remove("caseFindingType"); + jsonObject.remove("tuId"); + jsonObject.remove("tuName"); + jsonObject.remove("healthFacilityId"); + jsonObject.remove("healthFacilityName"); + jsonObject.remove("weight"); + jsonObject.remove("height"); + jsonObject.remove("bmi"); + jsonObject.remove("temperatureValue"); + + // Step 1 — forward to TM-API for standard beneficiary registration + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", authorization); + + HttpEntity request = new HttpEntity<>(jsonObject.toString(), headers); + String tmRegistrationUrl = tmUrl + "/registrar/registrarBeneficaryRegistrationNew"; + + ResponseEntity response = restTemplate.exchange(tmRegistrationUrl, HttpMethod.POST, request, Map.class); + + if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) { + throw new Exception("Beneficiary registration failed in TM-API"); + } + + Map responseBody = response.getBody(); + Map data = (Map) responseBody.get("data"); + Long benRegID = Long.valueOf(data.get("benRegId").toString()); + Long beneficiaryID = Long.valueOf(data.get("benGenId").toString()); + + // Step 2 — save Stop TB specific fields @Transactional — all or nothing + StopTBRegistration registration = new StopTBRegistration(); + registration.setBenRegID(benRegID); + registration.setProviderServiceMapID(providerServiceMapID); + registration.setPersonFrom(personFrom); + registration.setCaseFindingType(caseFindingType); + registration.setTuId(tuId); + registration.setTuName(tuName); + registration.setHealthFacilityId(healthFacilityId); + registration.setHealthFacilityName(healthFacilityName); + registration.setWeight(weight); + registration.setHeight(height); + registration.setBmi(bmi); + registration.setTemperatureValue(temperatureValue); + registration.setCreatedBy(createdBy); + registration.setDeleted(false); + stopTBRegistrationRepo.save(registration); + + // Step 3 — create flow record nurse_flag=1 + BenFlowStatus flowStatus = new BenFlowStatus(); + flowStatus.setBeneficiaryRegID(benRegID); + flowStatus.setBeneficiaryID(beneficiaryID); + flowStatus.setVisitCategory("Stop TB"); + flowStatus.setNurseFlag((short) 1); + flowStatus.setDoctorFlag((short) 0); + flowStatus.setPharmacistFlag((short) 0); + flowStatus.setProviderServiceMapId(providerServiceMapID); + flowStatus.setVanID(null); + flowStatus.setAgentId(createdBy); + flowStatus.setRegistrationDate(new Timestamp(System.currentTimeMillis())); + benFlowStatusRepo.save(flowStatus); + + result.put("beneficiaryRegID", benRegID); + result.put("beneficiaryID", beneficiaryID); + return result; + } + + @Override + public Map getRegistration(StopTBRegistrationDto dto) throws Exception { + Map result = new HashMap<>(); + StopTBRegistration registration = stopTBRegistrationRepo.findByBenRegID(dto.getBenRegID()); + if (registration == null) + throw new Exception("No registration found for benRegID: " + dto.getBenRegID()); + + result.put("benRegID", registration.getBenRegID()); + result.put("personFrom", registration.getPersonFrom()); + result.put("caseFindingType", registration.getCaseFindingType()); + result.put("tuId", registration.getTuId()); + result.put("tuName", registration.getTuName()); + result.put("healthFacilityId", registration.getHealthFacilityId()); + result.put("healthFacilityName", registration.getHealthFacilityName()); + result.put("weight", registration.getWeight()); + result.put("height", registration.getHeight()); + result.put("bmi", registration.getBmi()); + result.put("temperatureValue", registration.getTemperatureValue()); + result.put("createdBy", registration.getCreatedBy()); + result.put("createdDate", registration.getCreatedDate()); + return result; + } + + @Override + public Map getRegistrarWorklist(StopTBRegistrationDto dto) throws Exception { + Timestamp fromDate = Timestamp.valueOf(LocalDate.now().atStartOfDay()); + Timestamp toDate = Timestamp.valueOf(LocalDate.now().atTime(LocalTime.MAX)); + List list = stopTBRegistrationRepo.getRegistrarWorklist( + dto.getProviderServiceMapID(), dto.getCreatedBy(), fromDate, toDate); + Map result = new HashMap<>(); + result.put("worklist", list); + result.put("count", list.size()); + return result; + } + + @Override + public Map getNurseWorklist(StopTBRegistrationDto dto) throws Exception { + Timestamp fromDate = Timestamp.valueOf(LocalDate.now().atStartOfDay()); + Timestamp toDate = Timestamp.valueOf(LocalDate.now().atTime(LocalTime.MAX)); + List list = benFlowStatusRepo.getNurseWorklist( + dto.getProviderServiceMapID(), fromDate, toDate); + Map result = new HashMap<>(); + result.put("worklist", list); + result.put("count", list.size()); + return result; + } + + private String getStringField(JsonObject obj, String field) { + return obj.has(field) && !obj.get(field).isJsonNull() ? obj.get(field).getAsString() : null; + } + + private Integer getIntField(JsonObject obj, String field) { + return obj.has(field) && !obj.get(field).isJsonNull() ? obj.get(field).getAsInt() : null; + } + + private Double getDoubleField(JsonObject obj, String field) { + return obj.has(field) && !obj.get(field).isJsonNull() ? obj.get(field).getAsDouble() : null; + } +} 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 From eb8649e894d956f5db59a338cfd2feda6e47474a Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 7 May 2026 10:18:51 +0530 Subject: [PATCH 03/97] feat: added new register api for stoptb --- .../iemr/flw/controller/StopTBController.java | 4 +- .../flw/service/impl/StopTBServiceImpl.java | 46 +++++++++++++++++-- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/StopTBController.java b/src/main/java/com/iemr/flw/controller/StopTBController.java index 449d8326..54ff6ddd 100644 --- a/src/main/java/com/iemr/flw/controller/StopTBController.java +++ b/src/main/java/com/iemr/flw/controller/StopTBController.java @@ -25,10 +25,10 @@ public class StopTBController { @Operation(summary = "Save Stop TB beneficiary registration") public ResponseEntity> saveRegistration( @RequestBody String requestBody, - @RequestHeader("Authorization") String authorization) { + @RequestHeader("JwtToken") String jwtToken) { Map response = new LinkedHashMap<>(); try { - Map data = stopTBService.saveRegistration(requestBody, authorization); + Map data = stopTBService.saveRegistration(requestBody, jwtToken); response.put("data", data); response.put("statusCode", 200); response.put("status", "Success"); diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index c3c3ac14..5e29b9c1 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -8,6 +8,8 @@ import com.iemr.flw.repo.iemr.BenFlowStatusRepo; import com.iemr.flw.repo.iemr.StopTBRegistrationRepo; import com.iemr.flw.service.StopTBService; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -16,6 +18,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import java.sql.Timestamp; import java.time.LocalDate; @@ -72,11 +76,30 @@ public Map saveRegistration(String requestBody, String authoriza jsonObject.remove("bmi"); jsonObject.remove("temperatureValue"); + // Prevent TM-API from creating its own flow record — we create Stop TB specific one + jsonObject.addProperty("isMobile", true); + // Step 1 — forward to TM-API for standard beneficiary registration + // Extract auth headers from current request — same pattern as FLW-API + String jwtToken = authorization; + String cookieValue = null; + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + HttpServletRequest httpRequest = attributes.getRequest(); + if (httpRequest.getCookies() != null) { + for (Cookie cookie : httpRequest.getCookies()) { + if ("Jwttoken".equals(cookie.getName())) { + cookieValue = cookie.getValue(); + } + } + } + } + RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - headers.set("Authorization", authorization); + headers.set("Authorization", jwtToken); + headers.set("User-Agent", "okhttp"); HttpEntity request = new HttpEntity<>(jsonObject.toString(), headers); String tmRegistrationUrl = tmUrl + "/registrar/registrarBeneficaryRegistrationNew"; @@ -88,9 +111,16 @@ public Map saveRegistration(String requestBody, String authoriza } Map responseBody = response.getBody(); - Map data = (Map) responseBody.get("data"); - Long benRegID = Long.valueOf(data.get("benRegId").toString()); - Long beneficiaryID = Long.valueOf(data.get("benGenId").toString()); + // TM-API returns data as a JSON string — parse it + Object dataObj = responseBody.get("data"); + JsonObject dataJson; + if (dataObj instanceof String) { + dataJson = JsonParser.parseString((String) dataObj).getAsJsonObject(); + } else { + dataJson = JsonParser.parseString(new com.google.gson.Gson().toJson(dataObj)).getAsJsonObject(); + } + Long benRegID = dataJson.get("benRegId").getAsLong(); + Long beneficiaryID = dataJson.get("benGenId").getAsLong(); // Step 2 — save Stop TB specific fields @Transactional — all or nothing StopTBRegistration registration = new StopTBRegistration(); @@ -181,7 +211,13 @@ private String getStringField(JsonObject obj, String field) { } private Integer getIntField(JsonObject obj, String field) { - return obj.has(field) && !obj.get(field).isJsonNull() ? obj.get(field).getAsInt() : null; + if (!obj.has(field) || obj.get(field).isJsonNull()) return null; + try { + return obj.get(field).getAsInt(); + } catch (Exception e) { + // handle string values like "1727" + return Integer.parseInt(obj.get(field).getAsString().trim()); + } } private Double getDoubleField(JsonObject obj, String field) { From 2e50511f45e956c3e88e82079586b2d4c54e9f30 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 7 May 2026 20:07:03 +0530 Subject: [PATCH 04/97] feat: added new api for nurse --- .../iemr/flw/controller/StopTBController.java | 206 +++++++++ .../domain/iemr/StopTBGeneralExamination.java | 85 ++++ .../flw/domain/iemr/StopTBGeneralOpd.java | 62 +++ .../com/iemr/flw/domain/iemr/TBScreening.java | 33 ++ .../flw/repo/identity/BeneficiaryRepo.java | 3 + .../iemr/flw/repo/iemr/BenFlowStatusRepo.java | 18 +- .../iemr/StopTBGeneralExaminationRepo.java | 19 + .../flw/repo/iemr/StopTBGeneralOpdRepo.java | 19 + .../flw/repo/iemr/StopTBRegistrationRepo.java | 9 +- .../iemr/flw/repo/iemr/TBScreeningRepo.java | 6 + .../com/iemr/flw/service/StopTBService.java | 21 + .../flw/service/impl/StopTBServiceImpl.java | 401 +++++++++++++++++- 12 files changed, 857 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralOpd.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java diff --git a/src/main/java/com/iemr/flw/controller/StopTBController.java b/src/main/java/com/iemr/flw/controller/StopTBController.java index 54ff6ddd..273b6847 100644 --- a/src/main/java/com/iemr/flw/controller/StopTBController.java +++ b/src/main/java/com/iemr/flw/controller/StopTBController.java @@ -94,4 +94,210 @@ public ResponseEntity> getNurseWorklist(@RequestBody StopTBR } return ResponseEntity.ok(response); } + + // ── Nurse: General Examination ──────────────────────────────────────────── + + @PostMapping("/nurse/generalExamination/save") + @Operation(summary = "Save general examination for Stop TB beneficiary") + public ResponseEntity> saveGeneralExamination(@RequestBody Map data) { + Map response = new LinkedHashMap<>(); + try { + Map result = stopTBService.saveGeneralExamination(data); + response.put("data", result); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in saveGeneralExamination: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error saving general examination: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } + + @PostMapping("/nurse/generalExamination/get") + @Operation(summary = "Get general examination for a Stop TB beneficiary") + public ResponseEntity> getGeneralExamination(@RequestBody Map body) { + Map response = new LinkedHashMap<>(); + try { + Object raw = body.get("benRegID"); + if (raw == null) throw new Exception("benRegID is required"); + Long benRegID = Long.parseLong(raw.toString()); + Map data = stopTBService.getGeneralExamination(benRegID); + response.put("data", data); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in getGeneralExamination: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error fetching general examination: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } + + @PostMapping("/nurse/generalExamination/getAll") + @Operation(summary = "Get all general examinations at a camp") + public ResponseEntity> getAllGeneralExaminations(@RequestBody Map body) { + Map response = new LinkedHashMap<>(); + try { + Object raw = body.get("providerServiceMapID"); + if (raw == null) throw new Exception("providerServiceMapID is required"); + Integer psmId = Integer.parseInt(raw.toString()); + Map data = stopTBService.getAllGeneralExaminations(psmId); + response.put("data", data); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in getAllGeneralExaminations: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error fetching general examinations: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } + + // ── Nurse: TB Screening ─────────────────────────────────────────────────── + + @PostMapping("/nurse/tbScreening/save") + @Operation(summary = "Save TB screening for Stop TB beneficiary") + public ResponseEntity> saveNurseTBScreening(@RequestBody Map data) { + Map response = new LinkedHashMap<>(); + try { + Map result = stopTBService.saveNurseTBScreening(data); + response.put("data", result); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in saveNurseTBScreening: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error saving TB screening: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } + + @PostMapping("/nurse/tbScreening/get") + @Operation(summary = "Get TB screening for a Stop TB beneficiary") + public ResponseEntity> getNurseTBScreening(@RequestBody Map body) { + Map response = new LinkedHashMap<>(); + try { + Object raw = body.get("benRegID"); + if (raw == null) throw new Exception("benRegID is required"); + Long benRegID = Long.parseLong(raw.toString()); + Map data = stopTBService.getNurseTBScreening(benRegID); + response.put("data", data); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in getNurseTBScreening: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error fetching TB screening: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } + + @PostMapping("/nurse/tbScreening/getAll") + @Operation(summary = "Get all TB screenings at a camp") + public ResponseEntity> getAllNurseTBScreenings(@RequestBody Map body) { + Map response = new LinkedHashMap<>(); + try { + Object raw = body.get("providerServiceMapID"); + if (raw == null) throw new Exception("providerServiceMapID is required"); + Integer psmId = Integer.parseInt(raw.toString()); + Map data = stopTBService.getAllNurseTBScreenings(psmId); + response.put("data", data); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in getAllNurseTBScreenings: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error fetching TB screenings: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } + + // ── Nurse: General OPD ─────────────────────────────────────────────────── + + @PostMapping("/nurse/generalOpd/save") + @Operation(summary = "Save general OPD record for Stop TB beneficiary") + public ResponseEntity> saveGeneralOpd(@RequestBody Map data) { + Map response = new LinkedHashMap<>(); + try { + Map result = stopTBService.saveGeneralOpd(data); + response.put("data", result); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in saveGeneralOpd: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error saving general OPD: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } + + @PostMapping("/nurse/generalOpd/get") + @Operation(summary = "Get general OPD record for a Stop TB beneficiary") + public ResponseEntity> getGeneralOpd(@RequestBody Map body) { + Map response = new LinkedHashMap<>(); + try { + Object raw = body.get("benRegID"); + if (raw == null) throw new Exception("benRegID is required"); + Long benRegID = Long.parseLong(raw.toString()); + Map data = stopTBService.getGeneralOpd(benRegID); + response.put("data", data); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in getGeneralOpd: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error fetching general OPD: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } + + @PostMapping("/nurse/generalOpd/getAll") + @Operation(summary = "Get all general OPD records at a camp") + public ResponseEntity> getAllGeneralOpd(@RequestBody Map body) { + Map response = new LinkedHashMap<>(); + try { + Object raw = body.get("providerServiceMapID"); + if (raw == null) throw new Exception("providerServiceMapID is required"); + Integer psmId = Integer.parseInt(raw.toString()); + Map data = stopTBService.getAllGeneralOpd(psmId); + response.put("data", data); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in getAllGeneralOpd: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error fetching general OPD records: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } + + // ── Nurse: Submit ───────────────────────────────────────────────────────── + + @PostMapping("/nurse/submit") + @Operation(summary = "Submit nurse data — mark nurse done for a Stop TB beneficiary") + public ResponseEntity> submitNurseData(@RequestBody Map data) { + Map response = new LinkedHashMap<>(); + try { + Map result = stopTBService.submitNurseData(data); + response.put("data", result); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in submitNurseData: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error submitting nurse data: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } } 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..892091f6 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java @@ -0,0 +1,85 @@ +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 = "ben_reg_id") + private Long benRegID; + + @Column(name = "provider_service_map_id") + private Integer providerServiceMapID; + + // Vitals + @Column(name = "pulse") + private Integer pulse; + + @Column(name = "systolic_bp") + private Integer systolicBP; + + @Column(name = "diastolic_bp") + private Integer diastolicBP; + + @Column(name = "rbs_value") + private Double rbsValue; + + // Clinical signs + @Column(name = "pallor") + private Boolean pallor; + + @Column(name = "icterus") + private Boolean icterus; + + @Column(name = "lymphadenopathy") + private Boolean lymphadenopathy; + + @Column(name = "edema") + private Boolean edema; + + @Column(name = "cyanosis") + private Boolean cyanosis; + + @Column(name = "clubbing") + private Boolean clubbing; + + // JSON array of selected key population risk factors + @Column(name = "key_population_risk_factors", columnDefinition = "TEXT") + private String keyPopulationRiskFactors; + + // Positive | Reactive | Negative | Unknown + @Column(name = "hiv_status") + private String hivStatus; + + // Server-calculated from vitals thresholds + @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; +} 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..e7af3318 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralOpd.java @@ -0,0 +1,62 @@ +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; +} 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..59581be2 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBScreening.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBScreening.java @@ -79,4 +79,37 @@ 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 = "deleted", insertable = false) + private Boolean deleted; + + // Diagnostics / referrals — Stop TB nurse flow only + @Column(name = "referred_for_digital_chest_xray") + private Boolean referredForDigitalChestXray; + + @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") + private Boolean recommendedForTruenat; + + @Column(name = "recommended_for_liquid_culture") + private Boolean recommendedForLiquidCulture; + + // JSON array of denial reasons when patient refuses a test + @Column(name = "test_denial_reasons", columnDefinition = "TEXT") + private String testDenialReasons; } 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..f74ac5bb 100644 --- a/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java +++ b/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java @@ -96,6 +96,9 @@ Page getBenDataWithinDates( @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/BenFlowStatusRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java index c44cbab9..9a575c49 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java @@ -13,14 +13,20 @@ @Repository public interface BenFlowStatusRepo extends JpaRepository { - @Query("SELECT b FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.nurseFlag = 1 AND b.visitDate >= :fromDate AND b.visitDate <= :toDate AND b.deleted = false") - List getNurseWorklist( - @Param("psmId") Integer psmId, - @Param("fromDate") Timestamp fromDate, - @Param("toDate") Timestamp toDate - ); + @Query("SELECT b FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.nurseFlag = 1 AND b.deleted = false ORDER BY b.registrationDate DESC") + List getNurseWorklist(@Param("psmId") Integer psmId); @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/StopTBGeneralExaminationRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java new file mode 100644 index 00000000..208975f8 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java @@ -0,0 +1,19 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.StopTBGeneralExamination; +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.List; + +@Repository +public interface StopTBGeneralExaminationRepo extends JpaRepository { + + @Query("SELECT e FROM StopTBGeneralExamination e WHERE e.benRegID = :benRegID AND e.deleted = false") + StopTBGeneralExamination findByBenRegID(@Param("benRegID") Long benRegID); + + @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); +} 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..0dd69a9d --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java @@ -0,0 +1,19 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.StopTBGeneralOpd; +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.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); + + @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); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java index 003369d4..e7f9c7ca 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java @@ -15,11 +15,6 @@ public interface StopTBRegistrationRepo extends JpaRepository= :fromDate AND t.createdDate <= :toDate AND t.deleted = false") - List getRegistrarWorklist( - @Param("psmId") Integer psmId, - @Param("createdBy") String createdBy, - @Param("fromDate") Timestamp fromDate, - @Param("toDate") Timestamp toDate - ); + @Query("SELECT t FROM StopTBRegistration t WHERE t.providerServiceMapID = :psmId AND t.deleted = false ORDER BY t.createdDate DESC") + List getRegistrarWorklist(@Param("psmId") Integer psmId); } 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..b1462b94 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBScreeningRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBScreeningRepo.java @@ -17,4 +17,10 @@ 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); + + @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); } diff --git a/src/main/java/com/iemr/flw/service/StopTBService.java b/src/main/java/com/iemr/flw/service/StopTBService.java index 8372df1f..c3189bd4 100644 --- a/src/main/java/com/iemr/flw/service/StopTBService.java +++ b/src/main/java/com/iemr/flw/service/StopTBService.java @@ -5,8 +5,29 @@ import java.util.Map; public interface StopTBService { + // Registrar Map saveRegistration(String requestBody, String authorization) throws Exception; Map getRegistration(StopTBRegistrationDto dto) throws Exception; Map getRegistrarWorklist(StopTBRegistrationDto dto) throws Exception; + + // Nurse — worklist Map getNurseWorklist(StopTBRegistrationDto dto) throws Exception; + + // Nurse — General Examination + Map saveGeneralExamination(Map data) throws Exception; + Map getGeneralExamination(Long benRegID) throws Exception; + Map getAllGeneralExaminations(Integer providerServiceMapID) throws Exception; + + // Nurse — TB Screening + Map saveNurseTBScreening(Map data) throws Exception; + Map getNurseTBScreening(Long benRegID) throws Exception; + Map getAllNurseTBScreenings(Integer providerServiceMapID) throws Exception; + + // Nurse — General OPD + Map saveGeneralOpd(Map data) throws Exception; + Map getGeneralOpd(Long benRegID) throws Exception; + Map getAllGeneralOpd(Integer providerServiceMapID) throws Exception; + + // Nurse — submit (mark nurse done, transition flow) + Map submitNurseData(Map data) throws Exception; } diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 5e29b9c1..f0a14b14 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -2,11 +2,19 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.iemr.flw.domain.identity.RMNCHMBeneficiarydetail; import com.iemr.flw.domain.iemr.BenFlowStatus; +import com.iemr.flw.domain.iemr.StopTBGeneralExamination; import com.iemr.flw.domain.iemr.StopTBRegistration; +import com.iemr.flw.domain.iemr.TBScreening; import com.iemr.flw.dto.iemr.StopTBRegistrationDto; +import com.iemr.flw.repo.identity.BeneficiaryRepo; import com.iemr.flw.repo.iemr.BenFlowStatusRepo; +import com.iemr.flw.domain.iemr.StopTBGeneralOpd; +import com.iemr.flw.repo.iemr.StopTBGeneralExaminationRepo; +import com.iemr.flw.repo.iemr.StopTBGeneralOpdRepo; import com.iemr.flw.repo.iemr.StopTBRegistrationRepo; +import com.iemr.flw.repo.iemr.TBScreeningRepo; import com.iemr.flw.service.StopTBService; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; @@ -22,8 +30,6 @@ import org.springframework.web.context.request.ServletRequestAttributes; import java.sql.Timestamp; -import java.time.LocalDate; -import java.time.LocalTime; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -39,6 +45,18 @@ public class StopTBServiceImpl implements StopTBService { @Autowired private BenFlowStatusRepo benFlowStatusRepo; + @Autowired + private BeneficiaryRepo beneficiaryRepo; + + @Autowired + private StopTBGeneralExaminationRepo generalExaminationRepo; + + @Autowired + private TBScreeningRepo tbScreeningRepo; + + @Autowired + private StopTBGeneralOpdRepo generalOpdRepo; + @Value("${tm-url}") private String tmUrl; @@ -166,6 +184,7 @@ public Map getRegistration(StopTBRegistrationDto dto) throws Exc if (registration == null) throw new Exception("No registration found for benRegID: " + dto.getBenRegID()); + // Stop TB specific fields result.put("benRegID", registration.getBenRegID()); result.put("personFrom", registration.getPersonFrom()); result.put("caseFindingType", registration.getCaseFindingType()); @@ -179,33 +198,391 @@ public Map getRegistration(StopTBRegistrationDto dto) throws Exc result.put("temperatureValue", registration.getTemperatureValue()); result.put("createdBy", registration.getCreatedBy()); result.put("createdDate", registration.getCreatedDate()); + + // Beneficiary profile from db_identity + RMNCHMBeneficiarydetail detail = beneficiaryRepo.getDetailByBenRegID( + java.math.BigInteger.valueOf(dto.getBenRegID())); + if (detail != null) { + result.put("firstName", detail.getFirstName()); + result.put("middleName", detail.getMiddleName()); + result.put("lastName", detail.getLastName()); + result.put("dob", detail.getDob()); + result.put("gender", detail.getGender()); + result.put("fatherName", detail.getFatherName()); + result.put("motherName", detail.getMotherName()); + result.put("community", detail.getCommunity()); + result.put("education", detail.getEducation()); + result.put("maritalStatus", detail.getMaritalstatus()); + } return result; } @Override public Map getRegistrarWorklist(StopTBRegistrationDto dto) throws Exception { - Timestamp fromDate = Timestamp.valueOf(LocalDate.now().atStartOfDay()); - Timestamp toDate = Timestamp.valueOf(LocalDate.now().atTime(LocalTime.MAX)); List list = stopTBRegistrationRepo.getRegistrarWorklist( - dto.getProviderServiceMapID(), dto.getCreatedBy(), fromDate, toDate); + dto.getProviderServiceMapID()); + + List> worklist = new java.util.ArrayList<>(); + for (StopTBRegistration reg : list) { + Map item = new HashMap<>(); + item.put("benRegID", reg.getBenRegID()); + item.put("providerServiceMapID", reg.getProviderServiceMapID()); + item.put("personFrom", reg.getPersonFrom()); + item.put("caseFindingType", reg.getCaseFindingType()); + item.put("tuId", reg.getTuId()); + item.put("tuName", reg.getTuName()); + item.put("healthFacilityId", reg.getHealthFacilityId()); + item.put("healthFacilityName", reg.getHealthFacilityName()); + item.put("weight", reg.getWeight()); + item.put("height", reg.getHeight()); + item.put("bmi", reg.getBmi()); + item.put("temperatureValue", reg.getTemperatureValue()); + item.put("createdBy", reg.getCreatedBy()); + item.put("createdDate", reg.getCreatedDate()); + + RMNCHMBeneficiarydetail detail = beneficiaryRepo.getDetailByBenRegID( + java.math.BigInteger.valueOf(reg.getBenRegID())); + if (detail != null) { + item.put("firstName", detail.getFirstName()); + item.put("middleName", detail.getMiddleName()); + item.put("lastName", detail.getLastName()); + item.put("dob", detail.getDob()); + item.put("gender", detail.getGender()); + item.put("fatherName", detail.getFatherName()); + item.put("motherName", detail.getMotherName()); + } + worklist.add(item); + } + Map result = new HashMap<>(); - result.put("worklist", list); - result.put("count", list.size()); + result.put("worklist", worklist); + result.put("count", worklist.size()); return result; } @Override public Map getNurseWorklist(StopTBRegistrationDto dto) throws Exception { - Timestamp fromDate = Timestamp.valueOf(LocalDate.now().atStartOfDay()); - Timestamp toDate = Timestamp.valueOf(LocalDate.now().atTime(LocalTime.MAX)); List list = benFlowStatusRepo.getNurseWorklist( - dto.getProviderServiceMapID(), fromDate, toDate); + dto.getProviderServiceMapID()); + + List> worklist = new java.util.ArrayList<>(); + for (BenFlowStatus flow : list) { + Map item = new HashMap<>(); + item.put("benFlowID", flow.getBenFlowID()); + item.put("benRegID", flow.getBeneficiaryRegID()); + item.put("beneficiaryID", flow.getBeneficiaryID()); + item.put("visitCategory", flow.getVisitCategory()); + item.put("nurseFlag", flow.getNurseFlag()); + item.put("registrationDate", flow.getRegistrationDate()); + + RMNCHMBeneficiarydetail detail = beneficiaryRepo.getDetailByBenRegID( + java.math.BigInteger.valueOf(flow.getBeneficiaryRegID())); + if (detail != null) { + item.put("firstName", detail.getFirstName()); + item.put("middleName", detail.getMiddleName()); + item.put("lastName", detail.getLastName()); + item.put("dob", detail.getDob()); + item.put("gender", detail.getGender()); + item.put("fatherName", detail.getFatherName()); + item.put("motherName", detail.getMotherName()); + } + worklist.add(item); + } + Map result = new HashMap<>(); - result.put("worklist", list); - result.put("count", list.size()); + result.put("worklist", worklist); + result.put("count", worklist.size()); return result; } + // ── Nurse: General Examination ──────────────────────────────────────────── + + @Override + @Transactional + public Map saveGeneralExamination(Map data) throws Exception { + Long benRegID = getLong(data, "benRegID"); + if (benRegID == null) throw new Exception("benRegID is required"); + + StopTBGeneralExamination exam = generalExaminationRepo.findByBenRegID(benRegID); + if (exam == null) exam = new StopTBGeneralExamination(); + + exam.setBenRegID(benRegID); + exam.setProviderServiceMapID(getInt(data, "providerServiceMapID")); + exam.setPulse(getInt(data, "pulse")); + exam.setSystolicBP(getInt(data, "systolicBP")); + exam.setDiastolicBP(getInt(data, "diastolicBP")); + exam.setRbsValue(getDouble(data, "rbsValue")); + exam.setPallor(getBool(data, "pallor")); + exam.setIcterus(getBool(data, "icterus")); + exam.setLymphadenopathy(getBool(data, "lymphadenopathy")); + exam.setEdema(getBool(data, "edema")); + exam.setCyanosis(getBool(data, "cyanosis")); + exam.setClubbing(getBool(data, "clubbing")); + exam.setKeyPopulationRiskFactors(getString(data, "keyPopulationRiskFactors")); + exam.setHivStatus(getString(data, "hivStatus")); + exam.setCreatedBy(getString(data, "createdBy")); + exam.setDeleted(false); + exam.setReferralToHWCNeeded(calculateReferralNeeded(exam)); + + generalExaminationRepo.save(exam); + + Map result = new HashMap<>(); + result.put("benRegID", benRegID); + result.put("referralToHWCNeeded", exam.getReferralToHWCNeeded()); + return result; + } + + private boolean calculateReferralNeeded(StopTBGeneralExamination e) { + if (e.getPulse() != null && (e.getPulse() < 60 || e.getPulse() > 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.getRbsValue() != null && e.getRbsValue() >= 100) return true; + return false; + } + + @Override + public Map getGeneralExamination(Long benRegID) throws Exception { + StopTBGeneralExamination exam = generalExaminationRepo.findByBenRegID(benRegID); + if (exam == null) throw new Exception("No general examination found for benRegID: " + benRegID); + return examToMap(exam); + } + + @Override + public Map getAllGeneralExaminations(Integer providerServiceMapID) throws Exception { + List list = generalExaminationRepo.findAllByProviderServiceMapID(providerServiceMapID); + List> items = new java.util.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 HashMap<>(); + m.put("id", e.getId()); + m.put("benRegID", e.getBenRegID()); + m.put("providerServiceMapID", e.getProviderServiceMapID()); + m.put("pulse", e.getPulse()); + m.put("systolicBP", e.getSystolicBP()); + m.put("diastolicBP", e.getDiastolicBP()); + m.put("rbsValue", e.getRbsValue()); + m.put("pallor", e.getPallor()); + m.put("icterus", e.getIcterus()); + m.put("lymphadenopathy", e.getLymphadenopathy()); + m.put("edema", e.getEdema()); + m.put("cyanosis", e.getCyanosis()); + m.put("clubbing", e.getClubbing()); + m.put("keyPopulationRiskFactors", e.getKeyPopulationRiskFactors()); + m.put("hivStatus", e.getHivStatus()); + m.put("referralToHWCNeeded", e.getReferralToHWCNeeded()); + m.put("createdBy", e.getCreatedBy()); + m.put("createdDate", e.getCreatedDate()); + return m; + } + + // ── Nurse: TB Screening ─────────────────────────────────────────────────── + + @Override + @Transactional + public Map saveNurseTBScreening(Map data) throws Exception { + Long benRegID = getLong(data, "benRegID"); + if (benRegID == null) throw new Exception("benRegID is required"); + + TBScreening screening = tbScreeningRepo.findByBenRegID(benRegID); + if (screening == null) screening = new TBScreening(); + + screening.setBenRegID(benRegID); + screening.setProviderServiceMapID(getInt(data, "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.setSympotomatic(getString(data, "symptomatic")); + 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.setVisitDate(new Timestamp(System.currentTimeMillis())); + + tbScreeningRepo.save(screening); + + Map result = new HashMap<>(); + result.put("benRegID", benRegID); + return result; + } + + @Override + public Map getNurseTBScreening(Long benRegID) throws Exception { + TBScreening screening = tbScreeningRepo.findByBenRegID(benRegID); + if (screening == null) throw new Exception("No TB screening found for benRegID: " + benRegID); + return screeningToMap(screening); + } + + @Override + public Map getAllNurseTBScreenings(Integer providerServiceMapID) throws Exception { + List list = tbScreeningRepo.findAllByProviderServiceMapID(providerServiceMapID); + List> items = new java.util.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 HashMap<>(); + m.put("id", s.getId()); + m.put("benRegID", 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("symptomatic", s.getSympotomatic()); + 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()); + return m; + } + + // ── Nurse: General OPD ─────────────────────────────────────────────────── + + @Override + @Transactional + public Map saveGeneralOpd(Map data) throws Exception { + Long benRegID = getLong(data, "benRegID"); + if (benRegID == null) throw new Exception("benRegID is required"); + + StopTBGeneralOpd opd = generalOpdRepo.findByBenRegID(benRegID); + if (opd == null) opd = new StopTBGeneralOpd(); + + opd.setBenRegID(benRegID); + opd.setProviderServiceMapID(getInt(data, "providerServiceMapID")); + opd.setChiefComplaint(getString(data, "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.setDeleted(false); + + generalOpdRepo.save(opd); + + Map result = new HashMap<>(); + result.put("benRegID", benRegID); + return result; + } + + @Override + public Map getGeneralOpd(Long benRegID) throws Exception { + StopTBGeneralOpd opd = generalOpdRepo.findByBenRegID(benRegID); + if (opd == null) throw new Exception("No general OPD record found for benRegID: " + benRegID); + return opdToMap(opd); + } + + @Override + public Map getAllGeneralOpd(Integer providerServiceMapID) throws Exception { + List list = generalOpdRepo.findAllByProviderServiceMapID(providerServiceMapID); + List> items = new java.util.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 HashMap<>(); + m.put("id", o.getId()); + m.put("benRegID", 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()); + return m; + } + + // ── Nurse: Submit ───────────────────────────────────────────────────────── + + @Override + @Transactional + public Map submitNurseData(Map data) throws Exception { + Long benFlowID = getLong(data, "benFlowID"); + if (benFlowID == null) throw new Exception("benFlowID is required"); + + int updated = benFlowStatusRepo.updateStopTBAfterNurseSubmit(benFlowID); + if (updated == 0) throw new Exception("Flow record not found for benFlowID: " + benFlowID); + + Map result = new HashMap<>(); + result.put("benFlowID", benFlowID); + result.put("status", "nurse_done"); + return result; + } + + // ── Map helper extractors ───────────────────────────────────────────────── + + 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 String getStringField(JsonObject obj, String field) { return obj.has(field) && !obj.get(field).isJsonNull() ? obj.get(field).getAsString() : null; } From 51fd584c4c2cdd0906dbe6b5c15c42d2a8589843 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 7 May 2026 23:46:40 +0530 Subject: [PATCH 05/97] fix: populate workingDistrictId from blockId in getUserDetail when district is null Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/iemr/flw/repo/iemr/UserServiceRoleRepo.java | 5 +++++ .../java/com/iemr/flw/service/impl/UserServiceImpl.java | 7 +++++++ 2 files changed, 12 insertions(+) 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..90a50f17 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,9 @@ public interface UserServiceRoleRepo extends JpaRepository Date: Fri, 8 May 2026 10:45:24 +0530 Subject: [PATCH 06/97] =?UTF-8?q?feat:=20align=20Stop=20TB=20APIs=20with?= =?UTF-8?q?=20BRD=20=E2=80=94=20village=20scoping=20+=20correct=20field=20?= =?UTF-8?q?names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - StopTBGeneralExamination: rewrite with BRD-correct fields (pallorId+pallor string pairs, pulseRate, randomBloodSugar, oedema, beneficiaryRegID, keyPopulationRiskFactorIds, hivStatusId — no more boolean clinical signs) - StopTBRegistration: add villageId + villageName from i_bendemographics.districtBranchID - StopTBRegistrationDto: add villageId + villageName for worklist requests - StopTBRegistrationRepo: scope registrar worklist by psmId + villageId - BenFlowStatusRepo: scope nurse worklist by psmId + villageId - StopTBGeneralExaminationRepo: rename findByBeneficiaryRegID - StopTBServiceImpl: extract village from demographics on registration, set on both tb_stoptb_registration and i_ben_flow_outreach flow record, fix all BRD field names throughout, add toJsonString for array fields Co-Authored-By: Claude Sonnet 4.6 --- .../domain/iemr/StopTBGeneralExamination.java | 56 +++-- .../flw/domain/iemr/StopTBRegistration.java | 6 + .../flw/dto/iemr/StopTBRegistrationDto.java | 3 + .../iemr/flw/repo/iemr/BenFlowStatusRepo.java | 4 +- .../iemr/StopTBGeneralExaminationRepo.java | 4 +- .../flw/repo/iemr/StopTBRegistrationRepo.java | 4 +- .../flw/service/impl/StopTBServiceImpl.java | 222 ++++++++++-------- 7 files changed, 179 insertions(+), 120 deletions(-) diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java index 892091f6..7a66b9b8 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java @@ -16,15 +16,15 @@ public class StopTBGeneralExamination { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "ben_reg_id") - private Long benRegID; + @Column(name = "beneficiary_reg_id") + private Long beneficiaryRegID; @Column(name = "provider_service_map_id") private Integer providerServiceMapID; // Vitals - @Column(name = "pulse") - private Integer pulse; + @Column(name = "pulse_rate") + private Integer pulseRate; @Column(name = "systolic_bp") private Integer systolicBP; @@ -32,33 +32,57 @@ public class StopTBGeneralExamination { @Column(name = "diastolic_bp") private Integer diastolicBP; - @Column(name = "rbs_value") - private Double rbsValue; + @Column(name = "random_blood_sugar") + private Double randomBloodSugar; + + // Clinical signs — ID + label ("PRESENT" / "ABSENT") + @Column(name = "pallor_id") + private Integer pallorId; - // Clinical signs @Column(name = "pallor") - private Boolean pallor; + private String pallor; + + @Column(name = "icterus_id") + private Integer icterusId; @Column(name = "icterus") - private Boolean icterus; + private String icterus; + + @Column(name = "lymphadenopathy_id") + private Integer lymphadenopathyId; @Column(name = "lymphadenopathy") - private Boolean lymphadenopathy; + private String lymphadenopathy; + + @Column(name = "oedema_id") + private Integer oedemaId; - @Column(name = "edema") - private Boolean edema; + @Column(name = "oedema") + private String oedema; + + @Column(name = "cyanosis_id") + private Integer cyanosisId; @Column(name = "cyanosis") - private Boolean cyanosis; + private String cyanosis; + + @Column(name = "clubbing_id") + private Integer clubbingId; @Column(name = "clubbing") - private Boolean clubbing; + private String clubbing; + + // JSON arrays serialised from mobile + @Column(name = "key_population_risk_factor_ids", columnDefinition = "TEXT") + private String keyPopulationRiskFactorIds; - // JSON array of selected key population risk factors @Column(name = "key_population_risk_factors", columnDefinition = "TEXT") private String keyPopulationRiskFactors; - // Positive | Reactive | Negative | Unknown + @Column(name = "hiv_status_id") + private Integer hivStatusId; + + // "Positive" | "Reactive" | "Negative" | "Unknown" @Column(name = "hiv_status") private String hivStatus; diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java index 1ca6aff8..82422dc4 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java @@ -53,6 +53,12 @@ public class StopTBRegistration { @Column(name = "isMobileAvailable") private Boolean isMobileAvailable; + @Column(name = "villageId") + private Integer villageId; + + @Column(name = "villageName") + private String villageName; + @Column(name = "createdBy") private String createdBy; diff --git a/src/main/java/com/iemr/flw/dto/iemr/StopTBRegistrationDto.java b/src/main/java/com/iemr/flw/dto/iemr/StopTBRegistrationDto.java index 11ae1291..64194ff1 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/StopTBRegistrationDto.java +++ b/src/main/java/com/iemr/flw/dto/iemr/StopTBRegistrationDto.java @@ -28,6 +28,9 @@ public class StopTBRegistrationDto { private String createdBy; + private Integer villageId; + private String villageName; + // For get/update private Long benRegID; private String modifiedBy; diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java index 9a575c49..3a654b25 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java @@ -13,8 +13,8 @@ @Repository public interface BenFlowStatusRepo extends JpaRepository { - @Query("SELECT b FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.nurseFlag = 1 AND b.deleted = false ORDER BY b.registrationDate DESC") - List getNurseWorklist(@Param("psmId") Integer psmId); + @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); @Modifying @Query("UPDATE BenFlowStatus b SET b.nurseFlag = 9, b.pharmacistFlag = 1 WHERE b.benFlowID = :benFlowID") diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java index 208975f8..847e3fd0 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java @@ -11,8 +11,8 @@ @Repository public interface StopTBGeneralExaminationRepo extends JpaRepository { - @Query("SELECT e FROM StopTBGeneralExamination e WHERE e.benRegID = :benRegID AND e.deleted = false") - StopTBGeneralExamination findByBenRegID(@Param("benRegID") Long benRegID); + @Query("SELECT e FROM StopTBGeneralExamination e WHERE e.beneficiaryRegID = :beneficiaryRegID AND e.deleted = false") + StopTBGeneralExamination findByBeneficiaryRegID(@Param("beneficiaryRegID") Long beneficiaryRegID); @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); diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java index e7f9c7ca..93c12168 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java @@ -15,6 +15,6 @@ public interface StopTBRegistrationRepo extends JpaRepository getRegistrarWorklist(@Param("psmId") Integer psmId); + @Query("SELECT t FROM StopTBRegistration t WHERE t.providerServiceMapID = :psmId AND t.villageId = :villageId AND t.deleted = false ORDER BY t.createdDate DESC") + List getRegistrarWorklist(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); } diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index f0a14b14..875c4ad7 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -60,15 +60,16 @@ public class StopTBServiceImpl implements StopTBService { @Value("${tm-url}") private String tmUrl; + // ── Registration ───────────────────────────────────────────────────────── + @Override @Transactional public Map saveRegistration(String requestBody, String authorization) throws Exception { Map result = new HashMap<>(); - // Parse incoming JSON JsonObject jsonObject = JsonParser.parseString(requestBody).getAsJsonObject(); - // Extract Stop TB specific fields + // Stop TB specific fields String personFrom = getStringField(jsonObject, "personFrom"); String caseFindingType = getStringField(jsonObject, "caseFindingType"); Integer tuId = getIntField(jsonObject, "tuId"); @@ -81,8 +82,16 @@ public Map saveRegistration(String requestBody, String authoriza Double temperatureValue = getDoubleField(jsonObject, "temperatureValue"); String createdBy = getStringField(jsonObject, "createdBy"); Integer providerServiceMapID = getIntField(jsonObject, "providerServiceMapID"); + // Village from demographics — same pattern as HWC getBenFlowRecordObj + Integer villageId = null; + String villageName = null; + if (jsonObject.has("i_bendemographics") && !jsonObject.get("i_bendemographics").isJsonNull()) { + JsonObject demo = jsonObject.getAsJsonObject("i_bendemographics"); + villageId = getIntField(demo, "districtBranchID"); + villageName = getStringField(demo, "districtBranchName"); + } - // Remove Stop TB fields — forward clean TM-API request + // Strip Stop TB fields before forwarding to TM-API jsonObject.remove("personFrom"); jsonObject.remove("caseFindingType"); jsonObject.remove("tuId"); @@ -93,44 +102,24 @@ public Map saveRegistration(String requestBody, String authoriza jsonObject.remove("height"); jsonObject.remove("bmi"); jsonObject.remove("temperatureValue"); - - // Prevent TM-API from creating its own flow record — we create Stop TB specific one jsonObject.addProperty("isMobile", true); - // Step 1 — forward to TM-API for standard beneficiary registration - // Extract auth headers from current request — same pattern as FLW-API - String jwtToken = authorization; - String cookieValue = null; - ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attributes != null) { - HttpServletRequest httpRequest = attributes.getRequest(); - if (httpRequest.getCookies() != null) { - for (Cookie cookie : httpRequest.getCookies()) { - if ("Jwttoken".equals(cookie.getName())) { - cookieValue = cookie.getValue(); - } - } - } - } - + // Forward to TM-API for standard beneficiary registration RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - headers.set("Authorization", jwtToken); + headers.set("Authorization", authorization); headers.set("User-Agent", "okhttp"); HttpEntity request = new HttpEntity<>(jsonObject.toString(), headers); - String tmRegistrationUrl = tmUrl + "/registrar/registrarBeneficaryRegistrationNew"; + ResponseEntity response = restTemplate.exchange( + tmUrl + "/registrar/registrarBeneficaryRegistrationNew", + HttpMethod.POST, request, Map.class); - ResponseEntity response = restTemplate.exchange(tmRegistrationUrl, HttpMethod.POST, request, Map.class); - - if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) { + if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) throw new Exception("Beneficiary registration failed in TM-API"); - } - Map responseBody = response.getBody(); - // TM-API returns data as a JSON string — parse it - Object dataObj = responseBody.get("data"); + Object dataObj = response.getBody().get("data"); JsonObject dataJson; if (dataObj instanceof String) { dataJson = JsonParser.parseString((String) dataObj).getAsJsonObject(); @@ -140,7 +129,7 @@ public Map saveRegistration(String requestBody, String authoriza Long benRegID = dataJson.get("benRegId").getAsLong(); Long beneficiaryID = dataJson.get("benGenId").getAsLong(); - // Step 2 — save Stop TB specific fields @Transactional — all or nothing + // Save Stop TB registration StopTBRegistration registration = new StopTBRegistration(); registration.setBenRegID(benRegID); registration.setProviderServiceMapID(providerServiceMapID); @@ -150,6 +139,8 @@ public Map saveRegistration(String requestBody, String authoriza registration.setTuName(tuName); registration.setHealthFacilityId(healthFacilityId); registration.setHealthFacilityName(healthFacilityName); + registration.setVillageId(villageId); + registration.setVillageName(villageName); registration.setWeight(weight); registration.setHeight(height); registration.setBmi(bmi); @@ -158,7 +149,7 @@ public Map saveRegistration(String requestBody, String authoriza registration.setDeleted(false); stopTBRegistrationRepo.save(registration); - // Step 3 — create flow record nurse_flag=1 + // Create flow record — nurse_flag=1, scoped by village (like HWC vanID) BenFlowStatus flowStatus = new BenFlowStatus(); flowStatus.setBeneficiaryRegID(benRegID); flowStatus.setBeneficiaryID(beneficiaryID); @@ -167,7 +158,8 @@ public Map saveRegistration(String requestBody, String authoriza flowStatus.setDoctorFlag((short) 0); flowStatus.setPharmacistFlag((short) 0); flowStatus.setProviderServiceMapId(providerServiceMapID); - flowStatus.setVanID(null); + flowStatus.setVillageID(villageId); + flowStatus.setVillageName(villageName); flowStatus.setAgentId(createdBy); flowStatus.setRegistrationDate(new Timestamp(System.currentTimeMillis())); benFlowStatusRepo.save(flowStatus); @@ -179,27 +171,27 @@ public Map saveRegistration(String requestBody, String authoriza @Override public Map getRegistration(StopTBRegistrationDto dto) throws Exception { - Map result = new HashMap<>(); - StopTBRegistration registration = stopTBRegistrationRepo.findByBenRegID(dto.getBenRegID()); - if (registration == null) + StopTBRegistration reg = stopTBRegistrationRepo.findByBenRegID(dto.getBenRegID()); + if (reg == null) throw new Exception("No registration found for benRegID: " + dto.getBenRegID()); - // Stop TB specific fields - result.put("benRegID", registration.getBenRegID()); - result.put("personFrom", registration.getPersonFrom()); - result.put("caseFindingType", registration.getCaseFindingType()); - result.put("tuId", registration.getTuId()); - result.put("tuName", registration.getTuName()); - result.put("healthFacilityId", registration.getHealthFacilityId()); - result.put("healthFacilityName", registration.getHealthFacilityName()); - result.put("weight", registration.getWeight()); - result.put("height", registration.getHeight()); - result.put("bmi", registration.getBmi()); - result.put("temperatureValue", registration.getTemperatureValue()); - result.put("createdBy", registration.getCreatedBy()); - result.put("createdDate", registration.getCreatedDate()); - - // Beneficiary profile from db_identity + Map result = new HashMap<>(); + result.put("benRegID", reg.getBenRegID()); + result.put("personFrom", reg.getPersonFrom()); + result.put("caseFindingType", reg.getCaseFindingType()); + result.put("tuId", reg.getTuId()); + result.put("tuName", reg.getTuName()); + result.put("healthFacilityId", reg.getHealthFacilityId()); + result.put("healthFacilityName", reg.getHealthFacilityName()); + result.put("villageId", reg.getVillageId()); + result.put("villageName", reg.getVillageName()); + result.put("weight", reg.getWeight()); + result.put("height", reg.getHeight()); + result.put("bmi", reg.getBmi()); + result.put("temperatureValue", reg.getTemperatureValue()); + result.put("createdBy", reg.getCreatedBy()); + result.put("createdDate", reg.getCreatedDate()); + RMNCHMBeneficiarydetail detail = beneficiaryRepo.getDetailByBenRegID( java.math.BigInteger.valueOf(dto.getBenRegID())); if (detail != null) { @@ -217,16 +209,20 @@ public Map getRegistration(StopTBRegistrationDto dto) throws Exc return result; } + // ── Worklists ───────────────────────────────────────────────────────────── + @Override public Map getRegistrarWorklist(StopTBRegistrationDto dto) throws Exception { List list = stopTBRegistrationRepo.getRegistrarWorklist( - dto.getProviderServiceMapID()); + dto.getProviderServiceMapID(), dto.getVillageId()); List> worklist = new java.util.ArrayList<>(); for (StopTBRegistration reg : list) { Map item = new HashMap<>(); item.put("benRegID", reg.getBenRegID()); item.put("providerServiceMapID", reg.getProviderServiceMapID()); + item.put("villageId", reg.getVillageId()); + item.put("villageName", reg.getVillageName()); item.put("personFrom", reg.getPersonFrom()); item.put("caseFindingType", reg.getCaseFindingType()); item.put("tuId", reg.getTuId()); @@ -263,7 +259,7 @@ public Map getRegistrarWorklist(StopTBRegistrationDto dto) throw @Override public Map getNurseWorklist(StopTBRegistrationDto dto) throws Exception { List list = benFlowStatusRepo.getNurseWorklist( - dto.getProviderServiceMapID()); + dto.getProviderServiceMapID(), dto.getVillageId()); List> worklist = new java.util.ArrayList<>(); for (BenFlowStatus flow : list) { @@ -273,6 +269,8 @@ public Map getNurseWorklist(StopTBRegistrationDto dto) throws Ex item.put("beneficiaryID", flow.getBeneficiaryID()); item.put("visitCategory", flow.getVisitCategory()); item.put("nurseFlag", flow.getNurseFlag()); + item.put("villageId", flow.getVillageID()); + item.put("villageName", flow.getVillageName()); item.put("registrationDate", flow.getRegistrationDate()); RMNCHMBeneficiarydetail detail = beneficiaryRepo.getDetailByBenRegID( @@ -300,25 +298,40 @@ public Map getNurseWorklist(StopTBRegistrationDto dto) throws Ex @Override @Transactional public Map saveGeneralExamination(Map data) throws Exception { - Long benRegID = getLong(data, "benRegID"); - if (benRegID == null) throw new Exception("benRegID is required"); + Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); + if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); - StopTBGeneralExamination exam = generalExaminationRepo.findByBenRegID(benRegID); + StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegID(beneficiaryRegID); if (exam == null) exam = new StopTBGeneralExamination(); - exam.setBenRegID(benRegID); + exam.setBeneficiaryRegID(beneficiaryRegID); exam.setProviderServiceMapID(getInt(data, "providerServiceMapID")); - exam.setPulse(getInt(data, "pulse")); + + // Vitals + exam.setPulseRate(getInt(data, "pulseRate")); exam.setSystolicBP(getInt(data, "systolicBP")); exam.setDiastolicBP(getInt(data, "diastolicBP")); - exam.setRbsValue(getDouble(data, "rbsValue")); - exam.setPallor(getBool(data, "pallor")); - exam.setIcterus(getBool(data, "icterus")); - exam.setLymphadenopathy(getBool(data, "lymphadenopathy")); - exam.setEdema(getBool(data, "edema")); - exam.setCyanosis(getBool(data, "cyanosis")); - exam.setClubbing(getBool(data, "clubbing")); - exam.setKeyPopulationRiskFactors(getString(data, "keyPopulationRiskFactors")); + exam.setRandomBloodSugar(getDouble(data, "randomBloodSugar")); + + // Clinical signs — ID + label + 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")); + + // JSON arrays from mobile + 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.setDeleted(false); @@ -327,23 +340,23 @@ public Map saveGeneralExamination(Map data) thro generalExaminationRepo.save(exam); Map result = new HashMap<>(); - result.put("benRegID", benRegID); + result.put("beneficiaryRegID", beneficiaryRegID); result.put("referralToHWCNeeded", exam.getReferralToHWCNeeded()); return result; } private boolean calculateReferralNeeded(StopTBGeneralExamination e) { - if (e.getPulse() != null && (e.getPulse() < 60 || e.getPulse() > 90)) return true; + 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.getRbsValue() != null && e.getRbsValue() >= 100) return true; + if (e.getRandomBloodSugar() != null && e.getRandomBloodSugar() >= 100) return true; return false; } @Override - public Map getGeneralExamination(Long benRegID) throws Exception { - StopTBGeneralExamination exam = generalExaminationRepo.findByBenRegID(benRegID); - if (exam == null) throw new Exception("No general examination found for benRegID: " + benRegID); + public Map getGeneralExamination(Long beneficiaryRegID) throws Exception { + StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegID(beneficiaryRegID); + if (exam == null) throw new Exception("No general examination found for beneficiaryRegID: " + beneficiaryRegID); return examToMap(exam); } @@ -361,19 +374,27 @@ public Map getAllGeneralExaminations(Integer providerServiceMapI private Map examToMap(StopTBGeneralExamination e) { Map m = new HashMap<>(); m.put("id", e.getId()); - m.put("benRegID", e.getBenRegID()); + m.put("beneficiaryRegID", e.getBeneficiaryRegID()); m.put("providerServiceMapID", e.getProviderServiceMapID()); - m.put("pulse", e.getPulse()); + m.put("pulseRate", e.getPulseRate()); m.put("systolicBP", e.getSystolicBP()); m.put("diastolicBP", e.getDiastolicBP()); - m.put("rbsValue", e.getRbsValue()); + 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("edema", e.getEdema()); + 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()); @@ -386,13 +407,13 @@ private Map examToMap(StopTBGeneralExamination e) { @Override @Transactional public Map saveNurseTBScreening(Map data) throws Exception { - Long benRegID = getLong(data, "benRegID"); - if (benRegID == null) throw new Exception("benRegID is required"); + Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); + if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); - TBScreening screening = tbScreeningRepo.findByBenRegID(benRegID); + TBScreening screening = tbScreeningRepo.findByBenRegID(beneficiaryRegID); if (screening == null) screening = new TBScreening(); - screening.setBenRegID(benRegID); + screening.setBenRegID(beneficiaryRegID); screening.setProviderServiceMapID(getInt(data, "providerServiceMapID")); screening.setCoughMoreThan2Weeks(getBool(data, "coughMoreThan2Weeks")); screening.setBloodInSputum(getBool(data, "bloodInSputum")); @@ -418,14 +439,14 @@ public Map saveNurseTBScreening(Map data) throws tbScreeningRepo.save(screening); Map result = new HashMap<>(); - result.put("benRegID", benRegID); + result.put("beneficiaryRegID", beneficiaryRegID); return result; } @Override - public Map getNurseTBScreening(Long benRegID) throws Exception { - TBScreening screening = tbScreeningRepo.findByBenRegID(benRegID); - if (screening == null) throw new Exception("No TB screening found for benRegID: " + benRegID); + public Map getNurseTBScreening(Long beneficiaryRegID) throws Exception { + TBScreening screening = tbScreeningRepo.findByBenRegID(beneficiaryRegID); + if (screening == null) throw new Exception("No TB screening found for beneficiaryRegID: " + beneficiaryRegID); return screeningToMap(screening); } @@ -443,7 +464,7 @@ public Map getAllNurseTBScreenings(Integer providerServiceMapID) private Map screeningToMap(TBScreening s) { Map m = new HashMap<>(); m.put("id", s.getId()); - m.put("benRegID", s.getBenRegID()); + m.put("beneficiaryRegID", s.getBenRegID()); m.put("providerServiceMapID", s.getProviderServiceMapID()); m.put("coughMoreThan2Weeks", s.getCoughMoreThan2Weeks()); m.put("bloodInSputum", s.getBloodInSputum()); @@ -473,13 +494,13 @@ private Map screeningToMap(TBScreening s) { @Override @Transactional public Map saveGeneralOpd(Map data) throws Exception { - Long benRegID = getLong(data, "benRegID"); - if (benRegID == null) throw new Exception("benRegID is required"); + Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); + if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); - StopTBGeneralOpd opd = generalOpdRepo.findByBenRegID(benRegID); + StopTBGeneralOpd opd = generalOpdRepo.findByBenRegID(beneficiaryRegID); if (opd == null) opd = new StopTBGeneralOpd(); - opd.setBenRegID(benRegID); + opd.setBenRegID(beneficiaryRegID); opd.setProviderServiceMapID(getInt(data, "providerServiceMapID")); opd.setChiefComplaint(getString(data, "chiefComplaint")); opd.setMedication(getString(data, "medication")); @@ -493,14 +514,14 @@ public Map saveGeneralOpd(Map data) throws Excep generalOpdRepo.save(opd); Map result = new HashMap<>(); - result.put("benRegID", benRegID); + result.put("beneficiaryRegID", beneficiaryRegID); return result; } @Override - public Map getGeneralOpd(Long benRegID) throws Exception { - StopTBGeneralOpd opd = generalOpdRepo.findByBenRegID(benRegID); - if (opd == null) throw new Exception("No general OPD record found for benRegID: " + benRegID); + public Map getGeneralOpd(Long beneficiaryRegID) throws Exception { + StopTBGeneralOpd opd = generalOpdRepo.findByBenRegID(beneficiaryRegID); + if (opd == null) throw new Exception("No general OPD record found for beneficiaryRegID: " + beneficiaryRegID); return opdToMap(opd); } @@ -518,7 +539,7 @@ public Map getAllGeneralOpd(Integer providerServiceMapID) throws private Map opdToMap(StopTBGeneralOpd o) { Map m = new HashMap<>(); m.put("id", o.getId()); - m.put("benRegID", o.getBenRegID()); + m.put("beneficiaryRegID", o.getBenRegID()); m.put("providerServiceMapID", o.getProviderServiceMapID()); m.put("chiefComplaint", o.getChiefComplaint()); m.put("medication", o.getMedication()); @@ -548,7 +569,13 @@ public Map submitNurseData(Map data) throws Exce return result; } - // ── Map helper extractors ───────────────────────────────────────────────── + // ── 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); @@ -592,8 +619,7 @@ private Integer getIntField(JsonObject obj, String field) { try { return obj.get(field).getAsInt(); } catch (Exception e) { - // handle string values like "1727" - return Integer.parseInt(obj.get(field).getAsString().trim()); + try { return Integer.parseInt(obj.get(field).getAsString().trim()); } catch (Exception ex) { return null; } } } From ef5f091f6358788a6c2e2a72516e05f016448f7f Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 8 May 2026 15:16:18 +0530 Subject: [PATCH 07/97] refactor: replace tb_stoptb_registration with BenFlowStatus for registrar worklist and add beneficiary details endpoint - getRegistrarWorklist now queries i_ben_flow_outreach by villageID + visitCategory instead of tb_stoptb_registration - add getRegistrarWorklist query to BenFlowStatusRepo - add getBeneficiaryDetails endpoint that proxies TM-API quickSearchNew and merges otherFields into flat response object Co-Authored-By: Claude Sonnet 4.6 --- .../iemr/flw/controller/StopTBController.java | 23 +++++ .../iemr/flw/repo/iemr/BenFlowStatusRepo.java | 3 + .../com/iemr/flw/service/StopTBService.java | 1 + .../flw/service/impl/StopTBServiceImpl.java | 94 +++++++++++++------ 4 files changed, 91 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/StopTBController.java b/src/main/java/com/iemr/flw/controller/StopTBController.java index 273b6847..29f6a706 100644 --- a/src/main/java/com/iemr/flw/controller/StopTBController.java +++ b/src/main/java/com/iemr/flw/controller/StopTBController.java @@ -59,6 +59,29 @@ public ResponseEntity> getRegistration(@RequestBody StopTBRe return ResponseEntity.ok(response); } + @PostMapping("/beneficiary/getDetails") + @Operation(summary = "Get full beneficiary details with Stop TB fields as individual objects") + public ResponseEntity> getBeneficiaryDetails( + @RequestBody Map body, + @RequestHeader("Authorization") String authorization) { + Map response = new LinkedHashMap<>(); + try { + Object raw = body.get("beneficiaryRegID"); + if (raw == null) throw new Exception("beneficiaryRegID is required"); + Long beneficiaryRegID = Long.parseLong(raw.toString()); + Map data = stopTBService.getBeneficiaryDetails(beneficiaryRegID, authorization); + response.put("data", data); + response.put("statusCode", 200); + response.put("status", "Success"); + } catch (Exception e) { + logger.error("Error in getBeneficiaryDetails: " + e); + response.put("statusCode", 5000); + response.put("errorMessage", "Error fetching beneficiary details: " + e.getMessage()); + response.put("status", "FAILURE"); + } + return ResponseEntity.ok(response); + } + @PostMapping("/registrar/worklist") @Operation(summary = "Get registrar worklist for Stop TB") public ResponseEntity> getRegistrarWorklist(@RequestBody StopTBRegistrationDto dto) { diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java index 3a654b25..2765f193 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java @@ -16,6 +16,9 @@ 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.visitCategory = 'Stop TB' AND b.deleted = false ORDER BY b.registrationDate DESC") + List getRegistrarWorklist(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); + @Modifying @Query("UPDATE BenFlowStatus b SET b.nurseFlag = 9, b.pharmacistFlag = 1 WHERE b.benFlowID = :benFlowID") int updateAfterNurseSubmit(@Param("benFlowID") Long benFlowID); diff --git a/src/main/java/com/iemr/flw/service/StopTBService.java b/src/main/java/com/iemr/flw/service/StopTBService.java index c3189bd4..87abf3de 100644 --- a/src/main/java/com/iemr/flw/service/StopTBService.java +++ b/src/main/java/com/iemr/flw/service/StopTBService.java @@ -9,6 +9,7 @@ public interface StopTBService { Map saveRegistration(String requestBody, String authorization) throws Exception; Map getRegistration(StopTBRegistrationDto dto) throws Exception; Map getRegistrarWorklist(StopTBRegistrationDto dto) throws Exception; + Map getBeneficiaryDetails(Long beneficiaryRegID, String authorization) throws Exception; // Nurse — worklist Map getNurseWorklist(StopTBRegistrationDto dto) throws Exception; diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 875c4ad7..62809ffb 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -209,44 +209,78 @@ public Map getRegistration(StopTBRegistrationDto dto) throws Exc return result; } + // ── Beneficiary Details ─────────────────────────────────────────────────── + + @Override + public Map getBeneficiaryDetails(Long beneficiaryRegID, String authorization) throws Exception { + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", authorization); + + Map requestBody = new HashMap<>(); + requestBody.put("beneficiaryRegID", beneficiaryRegID); + + HttpEntity> entity = new HttpEntity<>(requestBody, headers); + ResponseEntity response = restTemplate.exchange( + tmUrl + "/registrar/quickSearchNew", + HttpMethod.POST, entity, Map.class); + + if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) + throw new Exception("Failed to fetch beneficiary details from TM-API"); + + Map body = response.getBody(); + Object dataObj = body.get("data"); + if (dataObj == null) + throw new Exception("No data in TM-API response"); + + // quickSearchNew returns a list — take first element + java.util.List dataList = (java.util.List) dataObj; + if (dataList.isEmpty()) + throw new Exception("Beneficiary not found"); + + Map benData = new HashMap<>((Map) dataList.get(0)); + + // Parse otherFields JSON string and merge into response + Object otherFieldsRaw = benData.get("otherFields"); + if (otherFieldsRaw != null) { + try { + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + Map otherFields = mapper.readValue(otherFieldsRaw.toString(), Map.class); + benData.putAll(otherFields); + } catch (Exception e) { + logger.warn("Could not parse otherFields: " + e.getMessage()); + } + } + benData.remove("otherFields"); + return benData; + } + // ── Worklists ───────────────────────────────────────────────────────────── @Override public Map getRegistrarWorklist(StopTBRegistrationDto dto) throws Exception { - List list = stopTBRegistrationRepo.getRegistrarWorklist( + List list = benFlowStatusRepo.getRegistrarWorklist( dto.getProviderServiceMapID(), dto.getVillageId()); List> worklist = new java.util.ArrayList<>(); - for (StopTBRegistration reg : list) { + for (BenFlowStatus flow : list) { Map item = new HashMap<>(); - item.put("benRegID", reg.getBenRegID()); - item.put("providerServiceMapID", reg.getProviderServiceMapID()); - item.put("villageId", reg.getVillageId()); - item.put("villageName", reg.getVillageName()); - item.put("personFrom", reg.getPersonFrom()); - item.put("caseFindingType", reg.getCaseFindingType()); - item.put("tuId", reg.getTuId()); - item.put("tuName", reg.getTuName()); - item.put("healthFacilityId", reg.getHealthFacilityId()); - item.put("healthFacilityName", reg.getHealthFacilityName()); - item.put("weight", reg.getWeight()); - item.put("height", reg.getHeight()); - item.put("bmi", reg.getBmi()); - item.put("temperatureValue", reg.getTemperatureValue()); - item.put("createdBy", reg.getCreatedBy()); - item.put("createdDate", reg.getCreatedDate()); - - RMNCHMBeneficiarydetail detail = beneficiaryRepo.getDetailByBenRegID( - java.math.BigInteger.valueOf(reg.getBenRegID())); - if (detail != null) { - item.put("firstName", detail.getFirstName()); - item.put("middleName", detail.getMiddleName()); - item.put("lastName", detail.getLastName()); - item.put("dob", detail.getDob()); - item.put("gender", detail.getGender()); - item.put("fatherName", detail.getFatherName()); - item.put("motherName", detail.getMotherName()); - } + item.put("benFlowID", flow.getBenFlowID()); + item.put("benRegID", flow.getBeneficiaryRegID()); + item.put("beneficiaryID", flow.getBeneficiaryID()); + item.put("benName", flow.getBenName()); + item.put("dob", flow.getDob()); + item.put("genderID", flow.getGenderID()); + item.put("genderName", flow.getGenderName()); + item.put("phoneNo", flow.getPreferredPhoneNum()); + item.put("villageID", flow.getVillageID()); + item.put("villageName", flow.getVillageName()); + item.put("districtID", flow.getDistrictID()); + item.put("districtName", flow.getDistrictName()); + item.put("registrationDate", flow.getRegistrationDate()); + item.put("nurseFlag", flow.getNurseFlag()); + item.put("doctorFlag", flow.getDoctorFlag()); worklist.add(item); } From 59c549a744645a145be9a0998c072178f76ee37f Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 8 May 2026 16:01:02 +0530 Subject: [PATCH 08/97] fix: worklist chnges --- .../iemr/flw/repo/iemr/BenFlowStatusRepo.java | 2 +- .../flw/service/impl/StopTBServiceImpl.java | 26 ++++++++----------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java index 2765f193..2fb45d7b 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java @@ -16,7 +16,7 @@ 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.visitCategory = 'Stop TB' AND b.deleted = false ORDER BY b.registrationDate DESC") + @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); @Modifying diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 62809ffb..d2fe5f4b 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -301,23 +301,19 @@ public Map getNurseWorklist(StopTBRegistrationDto dto) throws Ex item.put("benFlowID", flow.getBenFlowID()); item.put("benRegID", flow.getBeneficiaryRegID()); item.put("beneficiaryID", flow.getBeneficiaryID()); - item.put("visitCategory", flow.getVisitCategory()); - item.put("nurseFlag", flow.getNurseFlag()); - item.put("villageId", flow.getVillageID()); + item.put("benName", flow.getBenName()); + item.put("dob", flow.getDob()); + item.put("genderID", flow.getGenderID()); + item.put("genderName", flow.getGenderName()); + item.put("phoneNo", flow.getPreferredPhoneNum()); + item.put("districtID", flow.getDistrictID()); + item.put("districtName", flow.getDistrictName()); + item.put("villageID", flow.getVillageID()); item.put("villageName", flow.getVillageName()); + item.put("providerServiceMapID", flow.getProviderServiceMapId()); + item.put("nurseFlag", flow.getNurseFlag()); + item.put("doctorFlag", flow.getDoctorFlag()); item.put("registrationDate", flow.getRegistrationDate()); - - RMNCHMBeneficiarydetail detail = beneficiaryRepo.getDetailByBenRegID( - java.math.BigInteger.valueOf(flow.getBeneficiaryRegID())); - if (detail != null) { - item.put("firstName", detail.getFirstName()); - item.put("middleName", detail.getMiddleName()); - item.put("lastName", detail.getLastName()); - item.put("dob", detail.getDob()); - item.put("gender", detail.getGender()); - item.put("fatherName", detail.getFatherName()); - item.put("motherName", detail.getMotherName()); - } worklist.add(item); } From 2edec41ba1779248d05ba19814247de63a7300a3 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Sat, 9 May 2026 01:05:11 +0530 Subject: [PATCH 09/97] fix: use JwtToken header for getBeneficiaryDetails TM-API call Co-Authored-By: Claude Sonnet 4.6 --- src/main/java/com/iemr/flw/controller/StopTBController.java | 4 ++-- .../java/com/iemr/flw/service/impl/StopTBServiceImpl.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/StopTBController.java b/src/main/java/com/iemr/flw/controller/StopTBController.java index 29f6a706..f130cf27 100644 --- a/src/main/java/com/iemr/flw/controller/StopTBController.java +++ b/src/main/java/com/iemr/flw/controller/StopTBController.java @@ -63,13 +63,13 @@ public ResponseEntity> getRegistration(@RequestBody StopTBRe @Operation(summary = "Get full beneficiary details with Stop TB fields as individual objects") public ResponseEntity> getBeneficiaryDetails( @RequestBody Map body, - @RequestHeader("Authorization") String authorization) { + @RequestHeader("JwtToken") String jwtToken) { Map response = new LinkedHashMap<>(); try { Object raw = body.get("beneficiaryRegID"); if (raw == null) throw new Exception("beneficiaryRegID is required"); Long beneficiaryRegID = Long.parseLong(raw.toString()); - Map data = stopTBService.getBeneficiaryDetails(beneficiaryRegID, authorization); + Map data = stopTBService.getBeneficiaryDetails(beneficiaryRegID, jwtToken); response.put("data", data); response.put("statusCode", 200); response.put("status", "Success"); diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index d2fe5f4b..b86c4675 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -216,7 +216,7 @@ public Map getBeneficiaryDetails(Long beneficiaryRegID, String a RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - headers.set("Authorization", authorization); + headers.set("JwtToken", authorization); Map requestBody = new HashMap<>(); requestBody.put("beneficiaryRegID", beneficiaryRegID); From 27de8a21df08a9aaa3e45c5d3f643ecb956439a8 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Sat, 9 May 2026 01:19:35 +0530 Subject: [PATCH 10/97] fix: use okhttp User-Agent pattern for TM-API call in getBeneficiaryDetails Co-Authored-By: Claude Sonnet 4.6 --- src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index b86c4675..7870b497 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -216,7 +216,8 @@ public Map getBeneficiaryDetails(Long beneficiaryRegID, String a RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - headers.set("JwtToken", authorization); + headers.set("Authorization", authorization); + headers.set("User-Agent", "okhttp"); Map requestBody = new HashMap<>(); requestBody.put("beneficiaryRegID", beneficiaryRegID); From 408b29f104d09176c609dc9e0d36b1fbdf11075d Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Sat, 9 May 2026 01:27:23 +0530 Subject: [PATCH 11/97] fix: look up 12-digit beneficiaryID from BenFlowStatus for TM-API quickSearchNew Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java | 3 +++ .../java/com/iemr/flw/service/impl/StopTBServiceImpl.java | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java index 2fb45d7b..4658f7cc 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/BenFlowStatusRepo.java @@ -19,6 +19,9 @@ public interface BenFlowStatusRepo extends JpaRepository { @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); diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 7870b497..b5c77582 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -219,8 +219,14 @@ public Map getBeneficiaryDetails(Long beneficiaryRegID, String a headers.set("Authorization", authorization); headers.set("User-Agent", "okhttp"); + // TM-API quickSearchNew requires 12-digit beneficiaryID — look it up from BenFlowStatus + List flows = benFlowStatusRepo.findByBeneficiaryRegID(beneficiaryRegID); + if (flows == null || flows.isEmpty()) + throw new Exception("No flow record found for beneficiaryRegID: " + beneficiaryRegID); + Long beneficiaryID = flows.get(0).getBeneficiaryID(); + Map requestBody = new HashMap<>(); - requestBody.put("beneficiaryRegID", beneficiaryRegID); + requestBody.put("beneficiaryID", beneficiaryID); HttpEntity> entity = new HttpEntity<>(requestBody, headers); ResponseEntity response = restTemplate.exchange( From 5e7c2d893fd42a6cb3d36e6986b97c521c283aaa Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Sat, 9 May 2026 18:04:38 +0530 Subject: [PATCH 12/97] refactor: redesign Stop TB worklist to return full beneficiary data in getBeneficiaryData style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - saveRegistration: forward full payload to TM-API without stripping Stop TB/anthropometry fields — they land in ExtraFields via checkExtraFields; enrich BenFlowStatus with benName, dob, gender, phone, district extracted from registration payload - getRegistrarWorklist: returns getBeneficiaryData-style response — BenFlowStatus fields + db_identity detail (community, education, fatherName, motherName) + otherFields parsed for anthropometry and Stop TB details (personFrom, caseFindingType, tuId, weight, height, bmi, temp) - getNurseWorklist: same rich response + generalExamination, tbScreening, generalOpd attached if already saved (null if not yet captured) - Remove getRegistration and getBeneficiaryDetails endpoints — worklist covers both - Add otherFields (ExtraFields column) mapping to RMNCHMBeneficiarydetail entity - BenFlowStatusRepo worklist queries scoped to psmId + villageId only (visitCategory column not guaranteed to exist in i_ben_flow_outreach table) Co-Authored-By: Claude Sonnet 4.6 --- .../iemr/flw/controller/StopTBController.java | 81 +--- .../identity/RMNCHMBeneficiarydetail.java | 3 + .../com/iemr/flw/service/StopTBService.java | 4 +- .../flw/service/impl/StopTBServiceImpl.java | 418 +++++++----------- 4 files changed, 192 insertions(+), 314 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/StopTBController.java b/src/main/java/com/iemr/flw/controller/StopTBController.java index f130cf27..4b102a07 100644 --- a/src/main/java/com/iemr/flw/controller/StopTBController.java +++ b/src/main/java/com/iemr/flw/controller/StopTBController.java @@ -21,14 +21,16 @@ public class StopTBController { @Autowired private StopTBService stopTBService; + // ── Registration ────────────────────────────────────────────────────────── + @PostMapping("/registration/save") @Operation(summary = "Save Stop TB beneficiary registration") public ResponseEntity> saveRegistration( @RequestBody String requestBody, - @RequestHeader("JwtToken") String jwtToken) { + @RequestHeader("Authorization") String authorization) { Map response = new LinkedHashMap<>(); try { - Map data = stopTBService.saveRegistration(requestBody, jwtToken); + Map data = stopTBService.saveRegistration(requestBody, authorization); response.put("data", data); response.put("statusCode", 200); response.put("status", "Success"); @@ -41,54 +43,15 @@ public ResponseEntity> saveRegistration( return ResponseEntity.ok(response); } - @PostMapping("/registration/get") - @Operation(summary = "Get Stop TB beneficiary registration details") - public ResponseEntity> getRegistration(@RequestBody StopTBRegistrationDto dto) { - Map response = new LinkedHashMap<>(); - try { - Map data = stopTBService.getRegistration(dto); - response.put("data", data); - response.put("statusCode", 200); - response.put("status", "Success"); - } catch (Exception e) { - logger.error("Error in getRegistration: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error fetching registration: " + e.getMessage()); - response.put("status", "FAILURE"); - } - return ResponseEntity.ok(response); - } - - @PostMapping("/beneficiary/getDetails") - @Operation(summary = "Get full beneficiary details with Stop TB fields as individual objects") - public ResponseEntity> getBeneficiaryDetails( - @RequestBody Map body, - @RequestHeader("JwtToken") String jwtToken) { - Map response = new LinkedHashMap<>(); - try { - Object raw = body.get("beneficiaryRegID"); - if (raw == null) throw new Exception("beneficiaryRegID is required"); - Long beneficiaryRegID = Long.parseLong(raw.toString()); - Map data = stopTBService.getBeneficiaryDetails(beneficiaryRegID, jwtToken); - response.put("data", data); - response.put("statusCode", 200); - response.put("status", "Success"); - } catch (Exception e) { - logger.error("Error in getBeneficiaryDetails: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error fetching beneficiary details: " + e.getMessage()); - response.put("status", "FAILURE"); - } - return ResponseEntity.ok(response); - } + // ── Worklists ───────────────────────────────────────────────────────────── @PostMapping("/registrar/worklist") - @Operation(summary = "Get registrar worklist for Stop TB") + @Operation(summary = "Get registrar worklist — full beneficiary details for Stop TB") public ResponseEntity> getRegistrarWorklist(@RequestBody StopTBRegistrationDto dto) { Map response = new LinkedHashMap<>(); try { Map data = stopTBService.getRegistrarWorklist(dto); - response.put("data", data); + response.put("data", data.get("data")); response.put("statusCode", 200); response.put("status", "Success"); } catch (Exception e) { @@ -101,12 +64,12 @@ public ResponseEntity> getRegistrarWorklist(@RequestBody Sto } @PostMapping("/nurse/worklist") - @Operation(summary = "Get nurse worklist for Stop TB") + @Operation(summary = "Get nurse worklist — full beneficiary details + nurse module data") public ResponseEntity> getNurseWorklist(@RequestBody StopTBRegistrationDto dto) { Map response = new LinkedHashMap<>(); try { Map data = stopTBService.getNurseWorklist(dto); - response.put("data", data); + response.put("data", data.get("data")); response.put("statusCode", 200); response.put("status", "Success"); } catch (Exception e) { @@ -145,8 +108,7 @@ public ResponseEntity> getGeneralExamination(@RequestBody Ma try { Object raw = body.get("benRegID"); if (raw == null) throw new Exception("benRegID is required"); - Long benRegID = Long.parseLong(raw.toString()); - Map data = stopTBService.getGeneralExamination(benRegID); + Map data = stopTBService.getGeneralExamination(Long.parseLong(raw.toString())); response.put("data", data); response.put("statusCode", 200); response.put("status", "Success"); @@ -160,14 +122,13 @@ public ResponseEntity> getGeneralExamination(@RequestBody Ma } @PostMapping("/nurse/generalExamination/getAll") - @Operation(summary = "Get all general examinations at a camp") + @Operation(summary = "Get all general examinations for a camp") public ResponseEntity> getAllGeneralExaminations(@RequestBody Map body) { Map response = new LinkedHashMap<>(); try { Object raw = body.get("providerServiceMapID"); if (raw == null) throw new Exception("providerServiceMapID is required"); - Integer psmId = Integer.parseInt(raw.toString()); - Map data = stopTBService.getAllGeneralExaminations(psmId); + Map data = stopTBService.getAllGeneralExaminations(Integer.parseInt(raw.toString())); response.put("data", data); response.put("statusCode", 200); response.put("status", "Success"); @@ -207,8 +168,7 @@ public ResponseEntity> getNurseTBScreening(@RequestBody Map< try { Object raw = body.get("benRegID"); if (raw == null) throw new Exception("benRegID is required"); - Long benRegID = Long.parseLong(raw.toString()); - Map data = stopTBService.getNurseTBScreening(benRegID); + Map data = stopTBService.getNurseTBScreening(Long.parseLong(raw.toString())); response.put("data", data); response.put("statusCode", 200); response.put("status", "Success"); @@ -222,14 +182,13 @@ public ResponseEntity> getNurseTBScreening(@RequestBody Map< } @PostMapping("/nurse/tbScreening/getAll") - @Operation(summary = "Get all TB screenings at a camp") + @Operation(summary = "Get all TB screenings for a camp") public ResponseEntity> getAllNurseTBScreenings(@RequestBody Map body) { Map response = new LinkedHashMap<>(); try { Object raw = body.get("providerServiceMapID"); if (raw == null) throw new Exception("providerServiceMapID is required"); - Integer psmId = Integer.parseInt(raw.toString()); - Map data = stopTBService.getAllNurseTBScreenings(psmId); + Map data = stopTBService.getAllNurseTBScreenings(Integer.parseInt(raw.toString())); response.put("data", data); response.put("statusCode", 200); response.put("status", "Success"); @@ -242,7 +201,7 @@ public ResponseEntity> getAllNurseTBScreenings(@RequestBody return ResponseEntity.ok(response); } - // ── Nurse: General OPD ─────────────────────────────────────────────────── + // ── Nurse: General OPD ──────────────────────────────────────────────────── @PostMapping("/nurse/generalOpd/save") @Operation(summary = "Save general OPD record for Stop TB beneficiary") @@ -269,8 +228,7 @@ public ResponseEntity> getGeneralOpd(@RequestBody Map data = stopTBService.getGeneralOpd(benRegID); + Map data = stopTBService.getGeneralOpd(Long.parseLong(raw.toString())); response.put("data", data); response.put("statusCode", 200); response.put("status", "Success"); @@ -284,14 +242,13 @@ public ResponseEntity> getGeneralOpd(@RequestBody Map> getAllGeneralOpd(@RequestBody Map body) { Map response = new LinkedHashMap<>(); try { Object raw = body.get("providerServiceMapID"); if (raw == null) throw new Exception("providerServiceMapID is required"); - Integer psmId = Integer.parseInt(raw.toString()); - Map data = stopTBService.getAllGeneralOpd(psmId); + Map data = stopTBService.getAllGeneralOpd(Integer.parseInt(raw.toString())); response.put("data", data); response.put("statusCode", 200); response.put("status", "Success"); 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..1667fc96 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,7 @@ public class RMNCHMBeneficiarydetail { @Column(name = "familyid") private String familyId; + @Column(name = "ExtraFields") + private String otherFields; + } diff --git a/src/main/java/com/iemr/flw/service/StopTBService.java b/src/main/java/com/iemr/flw/service/StopTBService.java index 87abf3de..2c99e6d1 100644 --- a/src/main/java/com/iemr/flw/service/StopTBService.java +++ b/src/main/java/com/iemr/flw/service/StopTBService.java @@ -7,9 +7,7 @@ public interface StopTBService { // Registrar Map saveRegistration(String requestBody, String authorization) throws Exception; - Map getRegistration(StopTBRegistrationDto dto) throws Exception; Map getRegistrarWorklist(StopTBRegistrationDto dto) throws Exception; - Map getBeneficiaryDetails(Long beneficiaryRegID, String authorization) throws Exception; // Nurse — worklist Map getNurseWorklist(StopTBRegistrationDto dto) throws Exception; @@ -29,6 +27,6 @@ public interface StopTBService { Map getGeneralOpd(Long benRegID) throws Exception; Map getAllGeneralOpd(Integer providerServiceMapID) throws Exception; - // Nurse — submit (mark nurse done, transition flow) + // Nurse — submit Map submitNurseData(Map data) throws Exception; } diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index b5c77582..b74cf12e 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -1,23 +1,20 @@ package com.iemr.flw.service.impl; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.iemr.flw.domain.identity.RMNCHMBeneficiarydetail; import com.iemr.flw.domain.iemr.BenFlowStatus; import com.iemr.flw.domain.iemr.StopTBGeneralExamination; -import com.iemr.flw.domain.iemr.StopTBRegistration; +import com.iemr.flw.domain.iemr.StopTBGeneralOpd; import com.iemr.flw.domain.iemr.TBScreening; import com.iemr.flw.dto.iemr.StopTBRegistrationDto; import com.iemr.flw.repo.identity.BeneficiaryRepo; import com.iemr.flw.repo.iemr.BenFlowStatusRepo; -import com.iemr.flw.domain.iemr.StopTBGeneralOpd; import com.iemr.flw.repo.iemr.StopTBGeneralExaminationRepo; import com.iemr.flw.repo.iemr.StopTBGeneralOpdRepo; -import com.iemr.flw.repo.iemr.StopTBRegistrationRepo; import com.iemr.flw.repo.iemr.TBScreeningRepo; import com.iemr.flw.service.StopTBService; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -26,22 +23,16 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; +import java.math.BigInteger; import java.sql.Timestamp; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; @Service public class StopTBServiceImpl implements StopTBService { private final Logger logger = LoggerFactory.getLogger(StopTBServiceImpl.class); - @Autowired - private StopTBRegistrationRepo stopTBRegistrationRepo; - @Autowired private BenFlowStatusRepo benFlowStatusRepo; @@ -60,51 +51,50 @@ public class StopTBServiceImpl implements StopTBService { @Value("${tm-url}") private String tmUrl; - // ── Registration ───────────────────────────────────────────────────────── + // ── Registration ────────────────────────────────────────────────────────── @Override @Transactional public Map saveRegistration(String requestBody, String authorization) throws Exception { - Map result = new HashMap<>(); - JsonObject jsonObject = JsonParser.parseString(requestBody).getAsJsonObject(); - // Stop TB specific fields - String personFrom = getStringField(jsonObject, "personFrom"); - String caseFindingType = getStringField(jsonObject, "caseFindingType"); - Integer tuId = getIntField(jsonObject, "tuId"); - String tuName = getStringField(jsonObject, "tuName"); - Integer healthFacilityId = getIntField(jsonObject, "healthFacilityId"); - String healthFacilityName = getStringField(jsonObject, "healthFacilityName"); - Double weight = getDoubleField(jsonObject, "weight"); - Double height = getDoubleField(jsonObject, "height"); - Double bmi = getDoubleField(jsonObject, "bmi"); - Double temperatureValue = getDoubleField(jsonObject, "temperatureValue"); - String createdBy = getStringField(jsonObject, "createdBy"); + // Extract fields for BenFlowStatus — do NOT remove from jsonObject + // All fields (including Stop TB extras) forward to TM-API → ExtraFields via checkExtraFields + String firstName = getStringField(jsonObject, "firstName"); + String lastName = getStringField(jsonObject, "lastName"); + String benName = ((firstName != null ? firstName : "") + " " + (lastName != null ? lastName : "")).trim(); + + Integer genderID = getIntField(jsonObject, "genderID"); + String genderName = getStringField(jsonObject, "genderName"); + if (genderName == null) genderName = getStringField(jsonObject, "gender"); + + String createdBy = getStringField(jsonObject, "createdBy"); Integer providerServiceMapID = getIntField(jsonObject, "providerServiceMapID"); - // Village from demographics — same pattern as HWC getBenFlowRecordObj - Integer villageId = null; - String villageName = null; + + Timestamp dob = parseDob(jsonObject); + + String phoneNo = null; + if (jsonObject.has("i_bencontacts") && !jsonObject.get("i_bencontacts").isJsonNull()) { + JsonObject contacts = jsonObject.getAsJsonObject("i_bencontacts"); + phoneNo = getStringField(contacts, "phoneNo1"); + } + if (phoneNo == null) phoneNo = getStringField(jsonObject, "phoneNo1"); + + Integer villageId = null; + String villageName = null; + Integer districtID = null; + String districtName = null; if (jsonObject.has("i_bendemographics") && !jsonObject.get("i_bendemographics").isJsonNull()) { JsonObject demo = jsonObject.getAsJsonObject("i_bendemographics"); - villageId = getIntField(demo, "districtBranchID"); - villageName = getStringField(demo, "districtBranchName"); + villageId = getIntField(demo, "districtBranchID"); + villageName = getStringField(demo, "districtBranchName"); + districtID = getIntField(demo, "districtID"); + districtName = getStringField(demo, "districtName"); } - // Strip Stop TB fields before forwarding to TM-API - jsonObject.remove("personFrom"); - jsonObject.remove("caseFindingType"); - jsonObject.remove("tuId"); - jsonObject.remove("tuName"); - jsonObject.remove("healthFacilityId"); - jsonObject.remove("healthFacilityName"); - jsonObject.remove("weight"); - jsonObject.remove("height"); - jsonObject.remove("bmi"); - jsonObject.remove("temperatureValue"); + // isMobile=true tells TM-API to skip its own BenFlowStatus creation — FLW-API owns it jsonObject.addProperty("isMobile", true); - // Forward to TM-API for standard beneficiary registration RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -120,213 +110,149 @@ public Map saveRegistration(String requestBody, String authoriza throw new Exception("Beneficiary registration failed in TM-API"); Object dataObj = response.getBody().get("data"); - JsonObject dataJson; - if (dataObj instanceof String) { - dataJson = JsonParser.parseString((String) dataObj).getAsJsonObject(); - } else { - dataJson = JsonParser.parseString(new com.google.gson.Gson().toJson(dataObj)).getAsJsonObject(); - } - Long benRegID = dataJson.get("benRegId").getAsLong(); - Long beneficiaryID = dataJson.get("benGenId").getAsLong(); - - // Save Stop TB registration - StopTBRegistration registration = new StopTBRegistration(); - registration.setBenRegID(benRegID); - registration.setProviderServiceMapID(providerServiceMapID); - registration.setPersonFrom(personFrom); - registration.setCaseFindingType(caseFindingType); - registration.setTuId(tuId); - registration.setTuName(tuName); - registration.setHealthFacilityId(healthFacilityId); - registration.setHealthFacilityName(healthFacilityName); - registration.setVillageId(villageId); - registration.setVillageName(villageName); - registration.setWeight(weight); - registration.setHeight(height); - registration.setBmi(bmi); - registration.setTemperatureValue(temperatureValue); - registration.setCreatedBy(createdBy); - registration.setDeleted(false); - stopTBRegistrationRepo.save(registration); - - // Create flow record — nurse_flag=1, scoped by village (like HWC vanID) - BenFlowStatus flowStatus = new BenFlowStatus(); - flowStatus.setBeneficiaryRegID(benRegID); - flowStatus.setBeneficiaryID(beneficiaryID); - flowStatus.setVisitCategory("Stop TB"); - flowStatus.setNurseFlag((short) 1); - flowStatus.setDoctorFlag((short) 0); - flowStatus.setPharmacistFlag((short) 0); - flowStatus.setProviderServiceMapId(providerServiceMapID); - flowStatus.setVillageID(villageId); - flowStatus.setVillageName(villageName); - flowStatus.setAgentId(createdBy); - flowStatus.setRegistrationDate(new Timestamp(System.currentTimeMillis())); - benFlowStatusRepo.save(flowStatus); + JsonObject dataJson = dataObj instanceof String + ? JsonParser.parseString((String) dataObj).getAsJsonObject() + : JsonParser.parseString(new com.google.gson.Gson().toJson(dataObj)).getAsJsonObject(); + + Long benRegID = dataJson.get("benRegId").getAsLong(); + Long beneficiaryID = dataJson.get("benGenId").getAsLong(); + + BenFlowStatus flow = new BenFlowStatus(); + flow.setBeneficiaryRegID(benRegID); + flow.setBeneficiaryID(beneficiaryID); + flow.setBenName(benName.isEmpty() ? null : benName); + flow.setDob(dob); + flow.setGenderID(genderID != null ? genderID.shortValue() : null); + flow.setGenderName(genderName); + flow.setPreferredPhoneNum(phoneNo); + flow.setNurseFlag((short) 1); + flow.setDoctorFlag((short) 0); + flow.setPharmacistFlag((short) 0); + flow.setProviderServiceMapId(providerServiceMapID); + flow.setVillageID(villageId); + flow.setVillageName(villageName); + flow.setDistrictID(districtID); + flow.setDistrictName(districtName); + flow.setAgentId(createdBy); + flow.setRegistrationDate(new Timestamp(System.currentTimeMillis())); + flow.setDeleted(false); + benFlowStatusRepo.save(flow); + Map result = new HashMap<>(); result.put("beneficiaryRegID", benRegID); result.put("beneficiaryID", beneficiaryID); return result; } + // ── Worklists ───────────────────────────────────────────────────────────── + @Override - public Map getRegistration(StopTBRegistrationDto dto) throws Exception { - StopTBRegistration reg = stopTBRegistrationRepo.findByBenRegID(dto.getBenRegID()); - if (reg == null) - throw new Exception("No registration found for benRegID: " + dto.getBenRegID()); + public Map getRegistrarWorklist(StopTBRegistrationDto dto) throws Exception { + List flowList = benFlowStatusRepo.getRegistrarWorklist( + dto.getProviderServiceMapID(), dto.getVillageId()); - Map result = new HashMap<>(); - result.put("benRegID", reg.getBenRegID()); - result.put("personFrom", reg.getPersonFrom()); - result.put("caseFindingType", reg.getCaseFindingType()); - result.put("tuId", reg.getTuId()); - result.put("tuName", reg.getTuName()); - result.put("healthFacilityId", reg.getHealthFacilityId()); - result.put("healthFacilityName", reg.getHealthFacilityName()); - result.put("villageId", reg.getVillageId()); - result.put("villageName", reg.getVillageName()); - result.put("weight", reg.getWeight()); - result.put("height", reg.getHeight()); - result.put("bmi", reg.getBmi()); - result.put("temperatureValue", reg.getTemperatureValue()); - result.put("createdBy", reg.getCreatedBy()); - result.put("createdDate", reg.getCreatedDate()); - - RMNCHMBeneficiarydetail detail = beneficiaryRepo.getDetailByBenRegID( - java.math.BigInteger.valueOf(dto.getBenRegID())); - if (detail != null) { - result.put("firstName", detail.getFirstName()); - result.put("middleName", detail.getMiddleName()); - result.put("lastName", detail.getLastName()); - result.put("dob", detail.getDob()); - result.put("gender", detail.getGender()); - result.put("fatherName", detail.getFatherName()); - result.put("motherName", detail.getMotherName()); - result.put("community", detail.getCommunity()); - result.put("education", detail.getEducation()); - result.put("maritalStatus", detail.getMaritalstatus()); - } - return result; - } + List> worklist = new ArrayList<>(); + for (BenFlowStatus flow : flowList) + worklist.add(buildWorklistItem(flow)); - // ── Beneficiary Details ─────────────────────────────────────────────────── + return wrapWorklist(worklist); + } @Override - public Map getBeneficiaryDetails(Long beneficiaryRegID, String authorization) throws Exception { - RestTemplate restTemplate = new RestTemplate(); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - headers.set("Authorization", authorization); - headers.set("User-Agent", "okhttp"); + public Map getNurseWorklist(StopTBRegistrationDto dto) throws Exception { + List flowList = benFlowStatusRepo.getNurseWorklist( + dto.getProviderServiceMapID(), dto.getVillageId()); - // TM-API quickSearchNew requires 12-digit beneficiaryID — look it up from BenFlowStatus - List flows = benFlowStatusRepo.findByBeneficiaryRegID(beneficiaryRegID); - if (flows == null || flows.isEmpty()) - throw new Exception("No flow record found for beneficiaryRegID: " + beneficiaryRegID); - Long beneficiaryID = flows.get(0).getBeneficiaryID(); + List> worklist = new ArrayList<>(); + for (BenFlowStatus flow : flowList) { + Map item = buildWorklistItem(flow); + Long benRegID = flow.getBeneficiaryRegID(); - Map requestBody = new HashMap<>(); - requestBody.put("beneficiaryID", beneficiaryID); + StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegID(benRegID); + item.put("generalExamination", exam != null ? examToMap(exam) : null); - HttpEntity> entity = new HttpEntity<>(requestBody, headers); - ResponseEntity response = restTemplate.exchange( - tmUrl + "/registrar/quickSearchNew", - HttpMethod.POST, entity, Map.class); + TBScreening screening = tbScreeningRepo.findByBenRegID(benRegID); + item.put("tbScreening", screening != null ? screeningToMap(screening) : null); - if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) - throw new Exception("Failed to fetch beneficiary details from TM-API"); - - Map body = response.getBody(); - Object dataObj = body.get("data"); - if (dataObj == null) - throw new Exception("No data in TM-API response"); - - // quickSearchNew returns a list — take first element - java.util.List dataList = (java.util.List) dataObj; - if (dataList.isEmpty()) - throw new Exception("Beneficiary not found"); - - Map benData = new HashMap<>((Map) dataList.get(0)); - - // Parse otherFields JSON string and merge into response - Object otherFieldsRaw = benData.get("otherFields"); - if (otherFieldsRaw != null) { - try { - com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); - Map otherFields = mapper.readValue(otherFieldsRaw.toString(), Map.class); - benData.putAll(otherFields); - } catch (Exception e) { - logger.warn("Could not parse otherFields: " + e.getMessage()); - } - } - benData.remove("otherFields"); - return benData; - } - - // ── Worklists ───────────────────────────────────────────────────────────── - - @Override - public Map getRegistrarWorklist(StopTBRegistrationDto dto) throws Exception { - List list = benFlowStatusRepo.getRegistrarWorklist( - dto.getProviderServiceMapID(), dto.getVillageId()); + StopTBGeneralOpd opd = generalOpdRepo.findByBenRegID(benRegID); + item.put("generalOpd", opd != null ? opdToMap(opd) : null); - List> worklist = new java.util.ArrayList<>(); - for (BenFlowStatus flow : list) { - Map item = new HashMap<>(); - item.put("benFlowID", flow.getBenFlowID()); - item.put("benRegID", flow.getBeneficiaryRegID()); - item.put("beneficiaryID", flow.getBeneficiaryID()); - item.put("benName", flow.getBenName()); - item.put("dob", flow.getDob()); - item.put("genderID", flow.getGenderID()); - item.put("genderName", flow.getGenderName()); - item.put("phoneNo", flow.getPreferredPhoneNum()); - item.put("villageID", flow.getVillageID()); - item.put("villageName", flow.getVillageName()); - item.put("districtID", flow.getDistrictID()); - item.put("districtName", flow.getDistrictName()); - item.put("registrationDate", flow.getRegistrationDate()); - item.put("nurseFlag", flow.getNurseFlag()); - item.put("doctorFlag", flow.getDoctorFlag()); worklist.add(item); } - Map result = new HashMap<>(); - result.put("worklist", worklist); - result.put("count", worklist.size()); - return result; + return wrapWorklist(worklist); } - @Override - public Map getNurseWorklist(StopTBRegistrationDto dto) throws Exception { - List list = benFlowStatusRepo.getNurseWorklist( - dto.getProviderServiceMapID(), dto.getVillageId()); + // getBeneficiaryData-style item — BenFlowStatus + db_identity detail + ExtraFields + private Map buildWorklistItem(BenFlowStatus flow) { + Map item = new LinkedHashMap<>(); + item.put("benficieryid", flow.getBeneficiaryID()); + item.put("BenRegId", flow.getBeneficiaryRegID()); + item.put("benFlowID", flow.getBenFlowID()); + item.put("nurseFlag", flow.getNurseFlag()); + item.put("doctorFlag", flow.getDoctorFlag()); + + Map benDetails = new LinkedHashMap<>(); + benDetails.put("benName", flow.getBenName()); + benDetails.put("dob", flow.getDob()); + benDetails.put("genderId", flow.getGenderID()); + benDetails.put("gender", flow.getGenderName()); + benDetails.put("contact_number", flow.getPreferredPhoneNum()); + benDetails.put("villageId", flow.getVillageID()); + benDetails.put("villageName", flow.getVillageName()); + benDetails.put("districtid", flow.getDistrictID()); + benDetails.put("districtname", flow.getDistrictName()); + benDetails.put("registrationDate", flow.getRegistrationDate()); - List> worklist = new java.util.ArrayList<>(); - for (BenFlowStatus flow : list) { - Map item = new HashMap<>(); - item.put("benFlowID", flow.getBenFlowID()); - item.put("benRegID", flow.getBeneficiaryRegID()); - item.put("beneficiaryID", flow.getBeneficiaryID()); - item.put("benName", flow.getBenName()); - item.put("dob", flow.getDob()); - item.put("genderID", flow.getGenderID()); - item.put("genderName", flow.getGenderName()); - item.put("phoneNo", flow.getPreferredPhoneNum()); - item.put("districtID", flow.getDistrictID()); - item.put("districtName", flow.getDistrictName()); - item.put("villageID", flow.getVillageID()); - item.put("villageName", flow.getVillageName()); - item.put("providerServiceMapID", flow.getProviderServiceMapId()); - item.put("nurseFlag", flow.getNurseFlag()); - item.put("doctorFlag", flow.getDoctorFlag()); - item.put("registrationDate", flow.getRegistrationDate()); - worklist.add(item); + try { + RMNCHMBeneficiarydetail detail = beneficiaryRepo.getDetailByBenRegID( + BigInteger.valueOf(flow.getBeneficiaryRegID())); + if (detail != null) { + benDetails.put("fatherName", detail.getFatherName()); + benDetails.put("motherName", detail.getMotherName()); + benDetails.put("community", detail.getCommunity()); + benDetails.put("communityId", detail.getCommunityId()); + benDetails.put("education", detail.getEducation()); + benDetails.put("maritalStatus", detail.getMaritalstatus()); + + if (detail.getOtherFields() != null && !detail.getOtherFields().isEmpty()) { + try { + Map extra = new ObjectMapper().readValue(detail.getOtherFields(), Map.class); + + Map anthropometry = new LinkedHashMap<>(); + anthropometry.put("weight", extra.get("weight")); + anthropometry.put("height", extra.get("height")); + anthropometry.put("bmi", extra.get("bmi")); + anthropometry.put("temperatureValue", extra.get("temperatureValue")); + item.put("anthropometry", anthropometry); + + Map stopTBDetails = new LinkedHashMap<>(); + stopTBDetails.put("personFrom", extra.get("personFrom")); + stopTBDetails.put("caseFindingType", extra.get("caseFindingType")); + stopTBDetails.put("tuId", extra.get("tuId")); + stopTBDetails.put("tuName", extra.get("tuName")); + stopTBDetails.put("healthFacilityId", extra.get("healthFacilityId")); + stopTBDetails.put("healthFacilityName", extra.get("healthFacilityName")); + item.put("stopTBDetails", stopTBDetails); + } catch (Exception e) { + logger.warn("Cannot parse otherFields for benRegID: " + flow.getBeneficiaryRegID()); + } + } + } + } catch (Exception e) { + logger.warn("Cannot fetch detail for benRegID: " + flow.getBeneficiaryRegID()); } - Map result = new HashMap<>(); - result.put("worklist", worklist); - result.put("count", worklist.size()); + item.put("beneficiaryDetails", benDetails); + return item; + } + + private Map wrapWorklist(List> worklist) { + Map inner = new LinkedHashMap<>(); + inner.put("totalPage", 1); + inner.put("data", worklist); + Map result = new LinkedHashMap<>(); + result.put("data", inner); return result; } @@ -343,14 +269,10 @@ public Map saveGeneralExamination(Map data) thro exam.setBeneficiaryRegID(beneficiaryRegID); exam.setProviderServiceMapID(getInt(data, "providerServiceMapID")); - - // Vitals exam.setPulseRate(getInt(data, "pulseRate")); exam.setSystolicBP(getInt(data, "systolicBP")); exam.setDiastolicBP(getInt(data, "diastolicBP")); exam.setRandomBloodSugar(getDouble(data, "randomBloodSugar")); - - // Clinical signs — ID + label exam.setPallorId(getInt(data, "pallorId")); exam.setPallor(getString(data, "pallor")); exam.setIcterusId(getInt(data, "icterusId")); @@ -363,11 +285,8 @@ public Map saveGeneralExamination(Map data) thro exam.setCyanosis(getString(data, "cyanosis")); exam.setClubbingId(getInt(data, "clubbingId")); exam.setClubbing(getString(data, "clubbing")); - - // JSON arrays from mobile 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")); @@ -400,7 +319,7 @@ public Map getGeneralExamination(Long beneficiaryRegID) throws E @Override public Map getAllGeneralExaminations(Integer providerServiceMapID) throws Exception { List list = generalExaminationRepo.findAllByProviderServiceMapID(providerServiceMapID); - List> items = new java.util.ArrayList<>(); + List> items = new ArrayList<>(); for (StopTBGeneralExamination e : list) items.add(examToMap(e)); Map result = new HashMap<>(); result.put("data", items); @@ -409,7 +328,7 @@ public Map getAllGeneralExaminations(Integer providerServiceMapI } private Map examToMap(StopTBGeneralExamination e) { - Map m = new HashMap<>(); + Map m = new LinkedHashMap<>(); m.put("id", e.getId()); m.put("beneficiaryRegID", e.getBeneficiaryRegID()); m.put("providerServiceMapID", e.getProviderServiceMapID()); @@ -490,7 +409,7 @@ public Map getNurseTBScreening(Long beneficiaryRegID) throws Exc @Override public Map getAllNurseTBScreenings(Integer providerServiceMapID) throws Exception { List list = tbScreeningRepo.findAllByProviderServiceMapID(providerServiceMapID); - List> items = new java.util.ArrayList<>(); + List> items = new ArrayList<>(); for (TBScreening s : list) items.add(screeningToMap(s)); Map result = new HashMap<>(); result.put("data", items); @@ -499,7 +418,7 @@ public Map getAllNurseTBScreenings(Integer providerServiceMapID) } private Map screeningToMap(TBScreening s) { - Map m = new HashMap<>(); + Map m = new LinkedHashMap<>(); m.put("id", s.getId()); m.put("beneficiaryRegID", s.getBenRegID()); m.put("providerServiceMapID", s.getProviderServiceMapID()); @@ -526,7 +445,7 @@ private Map screeningToMap(TBScreening s) { return m; } - // ── Nurse: General OPD ─────────────────────────────────────────────────── + // ── Nurse: General OPD ──────────────────────────────────────────────────── @Override @Transactional @@ -565,7 +484,7 @@ public Map getGeneralOpd(Long beneficiaryRegID) throws Exception @Override public Map getAllGeneralOpd(Integer providerServiceMapID) throws Exception { List list = generalOpdRepo.findAllByProviderServiceMapID(providerServiceMapID); - List> items = new java.util.ArrayList<>(); + List> items = new ArrayList<>(); for (StopTBGeneralOpd o : list) items.add(opdToMap(o)); Map result = new HashMap<>(); result.put("data", items); @@ -574,7 +493,7 @@ public Map getAllGeneralOpd(Integer providerServiceMapID) throws } private Map opdToMap(StopTBGeneralOpd o) { - Map m = new HashMap<>(); + Map m = new LinkedHashMap<>(); m.put("id", o.getId()); m.put("beneficiaryRegID", o.getBenRegID()); m.put("providerServiceMapID", o.getProviderServiceMapID()); @@ -608,6 +527,13 @@ public Map submitNurseData(Map data) throws Exce // ── Helpers ─────────────────────────────────────────────────────────────── + private Timestamp parseDob(JsonObject obj) { + if (!obj.has("dob") || obj.get("dob").isJsonNull()) return null; + try { return new Timestamp(obj.get("dob").getAsLong()); } catch (Exception ignored) {} + try { return Timestamp.valueOf(obj.get("dob").getAsString().replace("T", " ")); } catch (Exception ignored) {} + return null; + } + private String toJsonString(Object value) { if (value == null) return null; if (value instanceof String) return (String) value; @@ -653,14 +579,8 @@ private String getStringField(JsonObject obj, String field) { private Integer getIntField(JsonObject obj, String field) { if (!obj.has(field) || obj.get(field).isJsonNull()) return null; - try { - return obj.get(field).getAsInt(); - } catch (Exception e) { + try { return obj.get(field).getAsInt(); } catch (Exception e) { try { return Integer.parseInt(obj.get(field).getAsString().trim()); } catch (Exception ex) { return null; } } } - - private Double getDoubleField(JsonObject obj, String field) { - return obj.has(field) && !obj.get(field).isJsonNull() ? obj.get(field).getAsDouble() : null; - } } From aef7d851fe8f3a59d688c0e074dca1de7630cd6f Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Sat, 9 May 2026 18:20:21 +0530 Subject: [PATCH 13/97] fix: use mapping table two-step lookup to fetch beneficiary details in Stop TB worklist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getDetailByBenRegID queries i_beneficiarydetails.beneficiaryRegID which is unpopulated; correct path is i_beneficiarymapping → BenDetailsId → i_beneficiarydetails. Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/iemr/flw/service/impl/StopTBServiceImpl.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index b74cf12e..7835d962 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -4,6 +4,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.iemr.flw.domain.identity.RMNCHMBeneficiarydetail; +import com.iemr.flw.domain.identity.RMNCHMBeneficiarymapping; import com.iemr.flw.domain.iemr.BenFlowStatus; import com.iemr.flw.domain.iemr.StopTBGeneralExamination; import com.iemr.flw.domain.iemr.StopTBGeneralOpd; @@ -205,8 +206,11 @@ private Map buildWorklistItem(BenFlowStatus flow) { benDetails.put("registrationDate", flow.getRegistrationDate()); try { - RMNCHMBeneficiarydetail detail = beneficiaryRepo.getDetailByBenRegID( + RMNCHMBeneficiarymapping mapping = beneficiaryRepo.getById( BigInteger.valueOf(flow.getBeneficiaryRegID())); + RMNCHMBeneficiarydetail detail = (mapping != null && mapping.getBenDetailsId() != null) + ? beneficiaryRepo.getDetailsById(mapping.getBenDetailsId()) + : null; if (detail != null) { benDetails.put("fatherName", detail.getFatherName()); benDetails.put("motherName", detail.getMotherName()); From 47d2cf8a0ff2614a1f20cec5d99b27d7d076ded8 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Sun, 10 May 2026 15:31:03 +0530 Subject: [PATCH 14/97] refactor: use OutputResponse for all StopTBController endpoints to match AMRIT standard format Co-Authored-By: Claude Sonnet 4.6 --- .../iemr/flw/controller/StopTBController.java | 186 +++++++----------- .../flw/service/impl/StopTBServiceImpl.java | 5 + 2 files changed, 72 insertions(+), 119 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/StopTBController.java b/src/main/java/com/iemr/flw/controller/StopTBController.java index 4b102a07..39063401 100644 --- a/src/main/java/com/iemr/flw/controller/StopTBController.java +++ b/src/main/java/com/iemr/flw/controller/StopTBController.java @@ -1,15 +1,15 @@ package com.iemr.flw.controller; +import com.google.gson.Gson; import com.iemr.flw.dto.iemr.StopTBRegistrationDto; 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.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.LinkedHashMap; import java.util.Map; @RestController @@ -25,259 +25,207 @@ public class StopTBController { @PostMapping("/registration/save") @Operation(summary = "Save Stop TB beneficiary registration") - public ResponseEntity> saveRegistration( + public String saveRegistration( @RequestBody String requestBody, @RequestHeader("Authorization") String authorization) { - Map response = new LinkedHashMap<>(); + OutputResponse response = new OutputResponse(); try { Map data = stopTBService.saveRegistration(requestBody, authorization); - response.put("data", data); - response.put("statusCode", 200); - response.put("status", "Success"); + response.setResponse(new Gson().toJson(data)); } catch (Exception e) { logger.error("Error in saveRegistration: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error in registration: " + e.getMessage()); - response.put("status", "FAILURE"); + response.setError(5000, "Error in registration: " + e.getMessage()); } - return ResponseEntity.ok(response); + return response.toString(); } // ── Worklists ───────────────────────────────────────────────────────────── @PostMapping("/registrar/worklist") @Operation(summary = "Get registrar worklist — full beneficiary details for Stop TB") - public ResponseEntity> getRegistrarWorklist(@RequestBody StopTBRegistrationDto dto) { - Map response = new LinkedHashMap<>(); + public String getRegistrarWorklist(@RequestBody StopTBRegistrationDto dto) { + OutputResponse response = new OutputResponse(); try { Map data = stopTBService.getRegistrarWorklist(dto); - response.put("data", data.get("data")); - response.put("statusCode", 200); - response.put("status", "Success"); + response.setResponse(new Gson().toJson(data.get("data"))); } catch (Exception e) { logger.error("Error in getRegistrarWorklist: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error fetching worklist: " + e.getMessage()); - response.put("status", "FAILURE"); + response.setError(5000, "Error fetching worklist: " + e.getMessage()); } - return ResponseEntity.ok(response); + return response.toString(); } @PostMapping("/nurse/worklist") @Operation(summary = "Get nurse worklist — full beneficiary details + nurse module data") - public ResponseEntity> getNurseWorklist(@RequestBody StopTBRegistrationDto dto) { - Map response = new LinkedHashMap<>(); + public String getNurseWorklist(@RequestBody StopTBRegistrationDto dto) { + OutputResponse response = new OutputResponse(); try { Map data = stopTBService.getNurseWorklist(dto); - response.put("data", data.get("data")); - response.put("statusCode", 200); - response.put("status", "Success"); + response.setResponse(new Gson().toJson(data.get("data"))); } catch (Exception e) { logger.error("Error in getNurseWorklist: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error fetching nurse worklist: " + e.getMessage()); - response.put("status", "FAILURE"); + response.setError(5000, "Error fetching nurse worklist: " + e.getMessage()); } - return ResponseEntity.ok(response); + return response.toString(); } // ── Nurse: General Examination ──────────────────────────────────────────── @PostMapping("/nurse/generalExamination/save") @Operation(summary = "Save general examination for Stop TB beneficiary") - public ResponseEntity> saveGeneralExamination(@RequestBody Map data) { - Map response = new LinkedHashMap<>(); + public String saveGeneralExamination(@RequestBody Map data) { + OutputResponse response = new OutputResponse(); try { Map result = stopTBService.saveGeneralExamination(data); - response.put("data", result); - response.put("statusCode", 200); - response.put("status", "Success"); + response.setResponse(new Gson().toJson(result)); } catch (Exception e) { logger.error("Error in saveGeneralExamination: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error saving general examination: " + e.getMessage()); - response.put("status", "FAILURE"); + response.setError(5000, "Error saving general examination: " + e.getMessage()); } - return ResponseEntity.ok(response); + return response.toString(); } @PostMapping("/nurse/generalExamination/get") @Operation(summary = "Get general examination for a Stop TB beneficiary") - public ResponseEntity> getGeneralExamination(@RequestBody Map body) { - Map response = new LinkedHashMap<>(); + public String getGeneralExamination(@RequestBody Map body) { + OutputResponse response = new OutputResponse(); try { Object raw = body.get("benRegID"); if (raw == null) throw new Exception("benRegID is required"); Map data = stopTBService.getGeneralExamination(Long.parseLong(raw.toString())); - response.put("data", data); - response.put("statusCode", 200); - response.put("status", "Success"); + response.setResponse(new Gson().toJson(data)); } catch (Exception e) { logger.error("Error in getGeneralExamination: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error fetching general examination: " + e.getMessage()); - response.put("status", "FAILURE"); + response.setError(5000, "Error fetching general examination: " + e.getMessage()); } - return ResponseEntity.ok(response); + return response.toString(); } @PostMapping("/nurse/generalExamination/getAll") @Operation(summary = "Get all general examinations for a camp") - public ResponseEntity> getAllGeneralExaminations(@RequestBody Map body) { - Map response = new LinkedHashMap<>(); + 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"); Map data = stopTBService.getAllGeneralExaminations(Integer.parseInt(raw.toString())); - response.put("data", data); - response.put("statusCode", 200); - response.put("status", "Success"); + response.setResponse(new Gson().toJson(data)); } catch (Exception e) { logger.error("Error in getAllGeneralExaminations: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error fetching general examinations: " + e.getMessage()); - response.put("status", "FAILURE"); + response.setError(5000, "Error fetching general examinations: " + e.getMessage()); } - return ResponseEntity.ok(response); + return response.toString(); } // ── Nurse: TB Screening ─────────────────────────────────────────────────── @PostMapping("/nurse/tbScreening/save") @Operation(summary = "Save TB screening for Stop TB beneficiary") - public ResponseEntity> saveNurseTBScreening(@RequestBody Map data) { - Map response = new LinkedHashMap<>(); + public String saveNurseTBScreening(@RequestBody Map data) { + OutputResponse response = new OutputResponse(); try { Map result = stopTBService.saveNurseTBScreening(data); - response.put("data", result); - response.put("statusCode", 200); - response.put("status", "Success"); + response.setResponse(new Gson().toJson(result)); } catch (Exception e) { logger.error("Error in saveNurseTBScreening: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error saving TB screening: " + e.getMessage()); - response.put("status", "FAILURE"); + response.setError(5000, "Error saving TB screening: " + e.getMessage()); } - return ResponseEntity.ok(response); + return response.toString(); } @PostMapping("/nurse/tbScreening/get") @Operation(summary = "Get TB screening for a Stop TB beneficiary") - public ResponseEntity> getNurseTBScreening(@RequestBody Map body) { - Map response = new LinkedHashMap<>(); + public String getNurseTBScreening(@RequestBody Map body) { + OutputResponse response = new OutputResponse(); try { Object raw = body.get("benRegID"); if (raw == null) throw new Exception("benRegID is required"); Map data = stopTBService.getNurseTBScreening(Long.parseLong(raw.toString())); - response.put("data", data); - response.put("statusCode", 200); - response.put("status", "Success"); + response.setResponse(new Gson().toJson(data)); } catch (Exception e) { logger.error("Error in getNurseTBScreening: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error fetching TB screening: " + e.getMessage()); - response.put("status", "FAILURE"); + response.setError(5000, "Error fetching TB screening: " + e.getMessage()); } - return ResponseEntity.ok(response); + return response.toString(); } @PostMapping("/nurse/tbScreening/getAll") @Operation(summary = "Get all TB screenings for a camp") - public ResponseEntity> getAllNurseTBScreenings(@RequestBody Map body) { - Map response = new LinkedHashMap<>(); + 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"); Map data = stopTBService.getAllNurseTBScreenings(Integer.parseInt(raw.toString())); - response.put("data", data); - response.put("statusCode", 200); - response.put("status", "Success"); + response.setResponse(new Gson().toJson(data)); } catch (Exception e) { logger.error("Error in getAllNurseTBScreenings: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error fetching TB screenings: " + e.getMessage()); - response.put("status", "FAILURE"); + response.setError(5000, "Error fetching TB screenings: " + e.getMessage()); } - return ResponseEntity.ok(response); + return response.toString(); } // ── Nurse: General OPD ──────────────────────────────────────────────────── @PostMapping("/nurse/generalOpd/save") @Operation(summary = "Save general OPD record for Stop TB beneficiary") - public ResponseEntity> saveGeneralOpd(@RequestBody Map data) { - Map response = new LinkedHashMap<>(); + public String saveGeneralOpd(@RequestBody Map data) { + OutputResponse response = new OutputResponse(); try { Map result = stopTBService.saveGeneralOpd(data); - response.put("data", result); - response.put("statusCode", 200); - response.put("status", "Success"); + response.setResponse(new Gson().toJson(result)); } catch (Exception e) { logger.error("Error in saveGeneralOpd: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error saving general OPD: " + e.getMessage()); - response.put("status", "FAILURE"); + response.setError(5000, "Error saving general OPD: " + e.getMessage()); } - return ResponseEntity.ok(response); + return response.toString(); } @PostMapping("/nurse/generalOpd/get") @Operation(summary = "Get general OPD record for a Stop TB beneficiary") - public ResponseEntity> getGeneralOpd(@RequestBody Map body) { - Map response = new LinkedHashMap<>(); + public String getGeneralOpd(@RequestBody Map body) { + OutputResponse response = new OutputResponse(); try { Object raw = body.get("benRegID"); if (raw == null) throw new Exception("benRegID is required"); Map data = stopTBService.getGeneralOpd(Long.parseLong(raw.toString())); - response.put("data", data); - response.put("statusCode", 200); - response.put("status", "Success"); + response.setResponse(new Gson().toJson(data)); } catch (Exception e) { logger.error("Error in getGeneralOpd: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error fetching general OPD: " + e.getMessage()); - response.put("status", "FAILURE"); + response.setError(5000, "Error fetching general OPD: " + e.getMessage()); } - return ResponseEntity.ok(response); + return response.toString(); } @PostMapping("/nurse/generalOpd/getAll") @Operation(summary = "Get all general OPD records for a camp") - public ResponseEntity> getAllGeneralOpd(@RequestBody Map body) { - Map response = new LinkedHashMap<>(); + 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"); Map data = stopTBService.getAllGeneralOpd(Integer.parseInt(raw.toString())); - response.put("data", data); - response.put("statusCode", 200); - response.put("status", "Success"); + response.setResponse(new Gson().toJson(data)); } catch (Exception e) { logger.error("Error in getAllGeneralOpd: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error fetching general OPD records: " + e.getMessage()); - response.put("status", "FAILURE"); + response.setError(5000, "Error fetching general OPD records: " + e.getMessage()); } - return ResponseEntity.ok(response); + return response.toString(); } // ── Nurse: Submit ───────────────────────────────────────────────────────── @PostMapping("/nurse/submit") @Operation(summary = "Submit nurse data — mark nurse done for a Stop TB beneficiary") - public ResponseEntity> submitNurseData(@RequestBody Map data) { - Map response = new LinkedHashMap<>(); + public String submitNurseData(@RequestBody Map data) { + OutputResponse response = new OutputResponse(); try { Map result = stopTBService.submitNurseData(data); - response.put("data", result); - response.put("statusCode", 200); - response.put("status", "Success"); + response.setResponse(new Gson().toJson(result)); } catch (Exception e) { logger.error("Error in submitNurseData: " + e); - response.put("statusCode", 5000); - response.put("errorMessage", "Error submitting nurse data: " + e.getMessage()); - response.put("status", "FAILURE"); + response.setError(5000, "Error submitting nurse data: " + e.getMessage()); } - return ResponseEntity.ok(response); + return response.toString(); } } diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 7835d962..768a2fe2 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -80,6 +80,11 @@ public Map saveRegistration(String requestBody, String authoriza phoneNo = getStringField(contacts, "phoneNo1"); } if (phoneNo == null) phoneNo = getStringField(jsonObject, "phoneNo1"); + if (phoneNo == null && jsonObject.has("benPhoneMaps") && jsonObject.get("benPhoneMaps").isJsonArray()) { + com.google.gson.JsonArray phoneMaps = jsonObject.getAsJsonArray("benPhoneMaps"); + if (phoneMaps.size() > 0 && !phoneMaps.get(0).isJsonNull()) + phoneNo = getStringField(phoneMaps.get(0).getAsJsonObject(), "phoneNo"); + } Integer villageId = null; String villageName = null; From 6b2ea68663329b33a518948eb03267d8a01dae98 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 11 May 2026 13:53:06 +0530 Subject: [PATCH 15/97] feat: add Stop TB worklist path and anthropometry to getBeneficiaryData - Add providerServiceMapID to GetBenRequestHandler for Stop TB filtering - Add getAddressesByBenRegIds repo query to fetch addresses by benRegId list - Add Stop TB path in getBenData: filter by providerServiceMapID+villageID via BenFlowStatus - Parse i_beneficiarydetails.ExtraFields to include anthropometry (weight/height/bmi/temperatureValue) in response Co-Authored-By: Claude Sonnet 4.6 --- .../dto/identity/GetBenRequestHandler.java | 10 +++ .../flw/repo/identity/BeneficiaryRepo.java | 4 ++ .../service/impl/BeneficiaryServiceImpl.java | 63 +++++++++++++++++-- 3 files changed, 72 insertions(+), 5 deletions(-) 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..e811c2cb 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; 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 f74ac5bb..aae20ce7 100644 --- a/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java +++ b/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java @@ -93,6 +93,10 @@ Page getBenDataWithinDates( @Query("SELECT b FROM RMNCHMBeneficiarymapping b WHERE b.benRegId = :benRegId") RMNCHMBeneficiarymapping findByBenRegIdFromMapping(@Param("benRegId") BigInteger benRegId); + @Query("SELECT a FROM RMNCHMBeneficiaryaddress a WHERE a.id IN " + + "(SELECT m.benAddressId FROM RMNCHMBeneficiarymapping m WHERE m.benRegId IN :benRegIds)") + List getAddressesByBenRegIds(@Param("benRegIds") List benRegIds); + @Query("SELECT d FROM RMNCHMBeneficiarydetail d WHERE d.beneficiaryDetailsId = :beneficiaryDetailsId") RMNCHMBeneficiarydetail findByBeneficiaryDetailsId(@Param("beneficiaryDetailsId") BigInteger beneficiaryDetailsId); 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..65f5dac0 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -21,6 +21,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; @@ -101,20 +102,60 @@ public class BeneficiaryServiceImpl implements BeneficiaryService { @Autowired private IncentiveLogicService incentiveLogicService; + @Autowired + private BenFlowStatusRepo benFlowStatusRepo; + + @Autowired + private UserServiceRoleRepo userRepo; + 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 benRegIds = flows.stream() + .filter(f -> f.getBeneficiaryRegID() != null) + .map(f -> BigInteger.valueOf(f.getBeneficiaryRegID())) + .collect(Collectors.toList()); + + if (benRegIds.isEmpty()) return null; + + List allAddresses = + beneficiaryRepo.getAddressesByBenRegIds(benRegIds); + + if (allAddresses == null || 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 +164,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 +182,6 @@ public String getBenData(GetBenRequestHandler request, String authorisation) thr ); } else { - pageResult = beneficiaryRepo.getBenDataByUser(userName, pageRequest); } @@ -440,6 +478,21 @@ private String getMappingsForAddressIDs(List addressLi ); resultMap.put("BenRegId", m.getBenRegId()); + // anthropometry from ExtraFields + if (benDetailsOBJ != null && benDetailsOBJ.getOtherFields() != null) { + try { + Map extraFields = new Gson().fromJson(benDetailsOBJ.getOtherFields(), Map.class); + Map anthropometry = new HashMap<>(); + 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")); + if (!anthropometry.isEmpty()) resultMap.put("anthropometry", anthropometry); + } catch (Exception ex) { + logger.warn("Could not parse ExtraFields for benDetailsId: " + benDetailsOBJ.getBeneficiaryDetailsId()); + } + } + // adding asha id / created by - user id if (benAddressOBJ.getCreatedBy() != null) { Integer userID = beneficiaryRepo.getUserIDByUserName(benAddressOBJ.getCreatedBy()); From 37b8c0bf8d6ebc1b437228bc9e4c4739b4362e18 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 11 May 2026 14:02:09 +0530 Subject: [PATCH 16/97] fix: use JOIN query for getAddressesByBenRegIds and accept villageId alias - Replace subquery with JOIN to fix Hibernate 6 multi-valued binding error - Add setVillageId setter as alias for villageID in GetBenRequestHandler Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/iemr/flw/dto/identity/GetBenRequestHandler.java | 4 ++++ src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) 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 e811c2cb..94bfed1c 100644 --- a/src/main/java/com/iemr/flw/dto/identity/GetBenRequestHandler.java +++ b/src/main/java/com/iemr/flw/dto/identity/GetBenRequestHandler.java @@ -92,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/repo/identity/BeneficiaryRepo.java b/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java index aae20ce7..aecce391 100644 --- a/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java +++ b/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java @@ -93,8 +93,7 @@ Page getBenDataWithinDates( @Query("SELECT b FROM RMNCHMBeneficiarymapping b WHERE b.benRegId = :benRegId") RMNCHMBeneficiarymapping findByBenRegIdFromMapping(@Param("benRegId") BigInteger benRegId); - @Query("SELECT a FROM RMNCHMBeneficiaryaddress a WHERE a.id IN " + - "(SELECT m.benAddressId FROM RMNCHMBeneficiarymapping m WHERE m.benRegId IN :benRegIds)") + @Query("SELECT a FROM RMNCHMBeneficiaryaddress a JOIN RMNCHMBeneficiarymapping m ON a.id = m.benAddressId WHERE m.benRegId IN :benRegIds") List getAddressesByBenRegIds(@Param("benRegIds") List benRegIds); @Query("SELECT d FROM RMNCHMBeneficiarydetail d WHERE d.beneficiaryDetailsId = :beneficiaryDetailsId") From 929444462d7ee973433510a6444dfa27955612d8 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 11 May 2026 14:07:35 +0530 Subject: [PATCH 17/97] fix: switch getAddressesByBenRegIds to native SQL to avoid Hibernate 6 IN binding issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JPQL IN with collection param throws multi-valued binding error in Hibernate 6 — native query bypasses this. Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/iemr/flw/repo/identity/BeneficiaryRepo.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 aecce391..cf09d35d 100644 --- a/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java +++ b/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java @@ -93,7 +93,10 @@ Page getBenDataWithinDates( @Query("SELECT b FROM RMNCHMBeneficiarymapping b WHERE b.benRegId = :benRegId") RMNCHMBeneficiarymapping findByBenRegIdFromMapping(@Param("benRegId") BigInteger benRegId); - @Query("SELECT a FROM RMNCHMBeneficiaryaddress a JOIN RMNCHMBeneficiarymapping m ON a.id = m.benAddressId WHERE m.benRegId IN :benRegIds") + @Query(nativeQuery = true, value = + "SELECT a.* FROM db_identity.i_beneficiaryaddress a " + + "INNER JOIN db_identity.i_beneficiarymapping m ON a.VanSerialNo = m.BenAddressId " + + "WHERE m.BenRegId IN :benRegIds") List getAddressesByBenRegIds(@Param("benRegIds") List benRegIds); @Query("SELECT d FROM RMNCHMBeneficiarydetail d WHERE d.beneficiaryDetailsId = :beneficiaryDetailsId") From e7b409e7773dd1369a335f77b25cec1995241cb0 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 11 May 2026 14:17:37 +0530 Subject: [PATCH 18/97] fix: use per-row mapping lookup for Stop TB worklist to avoid Hibernate IN binding issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace bulk native query with per-row findByBenRegIdFromMapping + getAddressById calls — same pattern as existing FLW code, avoids Object[] mapping error from native SELECT *. Co-Authored-By: Claude Sonnet 4.6 --- .../flw/repo/identity/BeneficiaryRepo.java | 5 ----- .../service/impl/BeneficiaryServiceImpl.java | 20 +++++++++---------- 2 files changed, 10 insertions(+), 15 deletions(-) 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 cf09d35d..140e67b0 100644 --- a/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java +++ b/src/main/java/com/iemr/flw/repo/identity/BeneficiaryRepo.java @@ -93,11 +93,6 @@ Page getBenDataWithinDates( @Query("SELECT b FROM RMNCHMBeneficiarymapping b WHERE b.benRegId = :benRegId") RMNCHMBeneficiarymapping findByBenRegIdFromMapping(@Param("benRegId") BigInteger benRegId); - @Query(nativeQuery = true, value = - "SELECT a.* FROM db_identity.i_beneficiaryaddress a " + - "INNER JOIN db_identity.i_beneficiarymapping m ON a.VanSerialNo = m.BenAddressId " + - "WHERE m.BenRegId IN :benRegIds") - List getAddressesByBenRegIds(@Param("benRegIds") List benRegIds); @Query("SELECT d FROM RMNCHMBeneficiarydetail d WHERE d.beneficiaryDetailsId = :beneficiaryDetailsId") RMNCHMBeneficiarydetail findByBeneficiaryDetailsId(@Param("beneficiaryDetailsId") BigInteger beneficiaryDetailsId); 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 65f5dac0..b8e9a397 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -131,17 +131,17 @@ public String getBenData(GetBenRequestHandler request, String authorisation) thr if (flows == null || flows.isEmpty()) return null; - List benRegIds = flows.stream() - .filter(f -> f.getBeneficiaryRegID() != null) - .map(f -> BigInteger.valueOf(f.getBeneficiaryRegID())) - .collect(Collectors.toList()); - - if (benRegIds.isEmpty()) return null; - - List allAddresses = - beneficiaryRepo.getAddressesByBenRegIds(benRegIds); + List allAddresses = new ArrayList<>(); + for (BenFlowStatus flow : flows) { + if (flow.getBeneficiaryRegID() == null) continue; + RMNCHMBeneficiarymapping mapping = beneficiaryRepo.findByBenRegIdFromMapping( + BigInteger.valueOf(flow.getBeneficiaryRegID())); + if (mapping == null || mapping.getBenAddressId() == null) continue; + RMNCHMBeneficiaryaddress address = beneficiaryRepo.getAddressById(mapping.getBenAddressId()); + if (address != null) allAddresses.add(address); + } - if (allAddresses == null || allAddresses.isEmpty()) return null; + if (allAddresses.isEmpty()) return null; int totalPage = (int) Math.ceil((double) allAddresses.size() / pageSize); int start = request.getPageNo() * pageSize; From dba4d1c32182751caeac1574b7c0d45c1cfd7a53 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 11 May 2026 14:45:01 +0530 Subject: [PATCH 19/97] feat: add stopTBDetails block (personFrom, caseFindingType, isMobileAvailable, tuId, tuName) to getBeneficiaryData response Co-Authored-By: Claude Sonnet 4.6 --- .../iemr/flw/service/impl/BeneficiaryServiceImpl.java | 10 +++++++++- .../com/iemr/flw/service/impl/StopTBServiceImpl.java | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) 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 b8e9a397..01b06532 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -478,7 +478,7 @@ private String getMappingsForAddressIDs(List addressLi ); resultMap.put("BenRegId", m.getBenRegId()); - // anthropometry from ExtraFields + // anthropometry and Stop TB fields from ExtraFields if (benDetailsOBJ != null && benDetailsOBJ.getOtherFields() != null) { try { Map extraFields = new Gson().fromJson(benDetailsOBJ.getOtherFields(), Map.class); @@ -488,6 +488,14 @@ private String getMappingsForAddressIDs(List addressLi if (extraFields.containsKey("bmi")) anthropometry.put("bmi", extraFields.get("bmi")); if (extraFields.containsKey("temperatureValue")) anthropometry.put("temperatureValue", extraFields.get("temperatureValue")); if (!anthropometry.isEmpty()) resultMap.put("anthropometry", anthropometry); + + Map stopTBDetails = new HashMap<>(); + 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")); + if (!stopTBDetails.isEmpty()) resultMap.put("stopTBDetails", stopTBDetails); } catch (Exception ex) { logger.warn("Could not parse ExtraFields for benDetailsId: " + benDetailsOBJ.getBeneficiaryDetailsId()); } diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 768a2fe2..f8fb07bb 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -249,7 +249,7 @@ private Map buildWorklistItem(BenFlowStatus flow) { } } } catch (Exception e) { - logger.warn("Cannot fetch detail for benRegID: " + flow.getBeneficiaryRegID()); + logger.error("Cannot fetch identity detail for benRegID: " + flow.getBeneficiaryRegID() + " — " + e.getClass().getName() + ": " + e.getMessage()); } item.put("beneficiaryDetails", benDetails); From a8c2a31546e5748950e34416110525d728317cd8 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 11 May 2026 15:28:08 +0530 Subject: [PATCH 20/97] feat: add occupation, economicStatus, residentialArea to stopTBDetails in worklist response - occupation read from i_beneficiarydetails (already stored by Identity-API) - economicStatus and residentialArea promoted to top-level in registration payload so Identity-API saves them in ExtraFields; read back in worklist Co-Authored-By: Claude Sonnet 4.6 --- .../flw/service/impl/BeneficiaryServiceImpl.java | 14 +++++++++++--- .../iemr/flw/service/impl/StopTBServiceImpl.java | 5 +++++ 2 files changed, 16 insertions(+), 3 deletions(-) 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 01b06532..f886ac31 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -478,7 +478,12 @@ private String getMappingsForAddressIDs(List addressLi ); resultMap.put("BenRegId", m.getBenRegId()); - // anthropometry and Stop TB fields from ExtraFields + // anthropometry and Stop TB fields + Map stopTBDetails = new HashMap<>(); + // occupation from i_beneficiarydetails (stored by Identity-API) + if (benDetailsOBJ != null && benDetailsOBJ.getOccupation() != null) + stopTBDetails.put("occupation", benDetailsOBJ.getOccupation()); + if (benDetailsOBJ != null && benDetailsOBJ.getOtherFields() != null) { try { Map extraFields = new Gson().fromJson(benDetailsOBJ.getOtherFields(), Map.class); @@ -489,17 +494,20 @@ private String getMappingsForAddressIDs(List addressLi if (extraFields.containsKey("temperatureValue")) anthropometry.put("temperatureValue", extraFields.get("temperatureValue")); if (!anthropometry.isEmpty()) resultMap.put("anthropometry", anthropometry); - Map stopTBDetails = new HashMap<>(); 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")); - if (!stopTBDetails.isEmpty()) resultMap.put("stopTBDetails", stopTBDetails); + if (extraFields.containsKey("economicStatus")) stopTBDetails.put("economicStatus", extraFields.get("economicStatus")); + if (extraFields.containsKey("economicStatusId")) stopTBDetails.put("economicStatusId", extraFields.get("economicStatusId")); + if (extraFields.containsKey("residentialArea")) stopTBDetails.put("residentialArea", extraFields.get("residentialArea")); + if (extraFields.containsKey("residentialAreaId")) stopTBDetails.put("residentialAreaId", extraFields.get("residentialAreaId")); } 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) { diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index f8fb07bb..29f8b16c 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -96,6 +96,11 @@ public Map saveRegistration(String requestBody, String authoriza villageName = getStringField(demo, "districtBranchName"); districtID = getIntField(demo, "districtID"); districtName = getStringField(demo, "districtName"); + // promote to top-level so Identity-API saves them in ExtraFields + if (demo.has("economicStatus")) jsonObject.add("economicStatus", demo.get("economicStatus")); + if (demo.has("economicStatusId")) jsonObject.add("economicStatusId", demo.get("economicStatusId")); + if (demo.has("residentialArea")) jsonObject.add("residentialArea", demo.get("residentialArea")); + if (demo.has("residentialAreaId")) jsonObject.add("residentialAreaId", demo.get("residentialAreaId")); } // isMobile=true tells TM-API to skip its own BenFlowStatus creation — FLW-API owns it From 19a8122dee0067db4c09ddcc5ce6e4230a9a3d91 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 11 May 2026 15:31:24 +0530 Subject: [PATCH 21/97] feat: add demographics block with address, occupation, economicStatus, residentialArea to worklist response Co-Authored-By: Claude Sonnet 4.6 --- .../service/impl/BeneficiaryServiceImpl.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 f886ac31..0406af07 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -480,9 +480,13 @@ private String getMappingsForAddressIDs(List addressLi // anthropometry and Stop TB fields Map stopTBDetails = new HashMap<>(); - // occupation from i_beneficiarydetails (stored by Identity-API) + // demographics block: address, occupation, economicStatus, residentialArea + Map demographics = new HashMap<>(); + demographics.put("addressLine1", benAddressOBJ.getPermAddrLine1()); + demographics.put("addressLine2", benAddressOBJ.getPermAddrLine2()); + demographics.put("addressLine3", benAddressOBJ.getPermAddrLine3()); if (benDetailsOBJ != null && benDetailsOBJ.getOccupation() != null) - stopTBDetails.put("occupation", benDetailsOBJ.getOccupation()); + demographics.put("occupation", benDetailsOBJ.getOccupation()); if (benDetailsOBJ != null && benDetailsOBJ.getOtherFields() != null) { try { @@ -499,15 +503,17 @@ private String getMappingsForAddressIDs(List addressLi 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")); - if (extraFields.containsKey("economicStatus")) stopTBDetails.put("economicStatus", extraFields.get("economicStatus")); - if (extraFields.containsKey("economicStatusId")) stopTBDetails.put("economicStatusId", extraFields.get("economicStatusId")); - if (extraFields.containsKey("residentialArea")) stopTBDetails.put("residentialArea", extraFields.get("residentialArea")); - if (extraFields.containsKey("residentialAreaId")) stopTBDetails.put("residentialAreaId", extraFields.get("residentialAreaId")); + + if (extraFields.containsKey("economicStatus")) demographics.put("economicStatus", extraFields.get("economicStatus")); + if (extraFields.containsKey("economicStatusId")) demographics.put("economicStatusId", extraFields.get("economicStatusId")); + if (extraFields.containsKey("residentialArea")) demographics.put("residentialArea", extraFields.get("residentialArea")); + if (extraFields.containsKey("residentialAreaId")) demographics.put("residentialAreaId", extraFields.get("residentialAreaId")); } catch (Exception ex) { logger.warn("Could not parse ExtraFields for benDetailsId: " + benDetailsOBJ.getBeneficiaryDetailsId()); } } if (!stopTBDetails.isEmpty()) resultMap.put("stopTBDetails", stopTBDetails); + resultMap.put("demographics", demographics); // adding asha id / created by - user id if (benAddressOBJ.getCreatedBy() != null) { From 32a5338db944015a916ffb99ecae7c4cdabb9dc3 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 11 May 2026 15:41:59 +0530 Subject: [PATCH 22/97] Revert "feat: add demographics block with address, occupation, economicStatus, residentialArea to worklist response" This reverts commit 3695522d5139363e8eba335d6142cb5526af7ec0. --- .../service/impl/BeneficiaryServiceImpl.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) 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 0406af07..f886ac31 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -480,13 +480,9 @@ private String getMappingsForAddressIDs(List addressLi // anthropometry and Stop TB fields Map stopTBDetails = new HashMap<>(); - // demographics block: address, occupation, economicStatus, residentialArea - Map demographics = new HashMap<>(); - demographics.put("addressLine1", benAddressOBJ.getPermAddrLine1()); - demographics.put("addressLine2", benAddressOBJ.getPermAddrLine2()); - demographics.put("addressLine3", benAddressOBJ.getPermAddrLine3()); + // occupation from i_beneficiarydetails (stored by Identity-API) if (benDetailsOBJ != null && benDetailsOBJ.getOccupation() != null) - demographics.put("occupation", benDetailsOBJ.getOccupation()); + stopTBDetails.put("occupation", benDetailsOBJ.getOccupation()); if (benDetailsOBJ != null && benDetailsOBJ.getOtherFields() != null) { try { @@ -503,17 +499,15 @@ private String getMappingsForAddressIDs(List addressLi 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")); - - if (extraFields.containsKey("economicStatus")) demographics.put("economicStatus", extraFields.get("economicStatus")); - if (extraFields.containsKey("economicStatusId")) demographics.put("economicStatusId", extraFields.get("economicStatusId")); - if (extraFields.containsKey("residentialArea")) demographics.put("residentialArea", extraFields.get("residentialArea")); - if (extraFields.containsKey("residentialAreaId")) demographics.put("residentialAreaId", extraFields.get("residentialAreaId")); + if (extraFields.containsKey("economicStatus")) stopTBDetails.put("economicStatus", extraFields.get("economicStatus")); + if (extraFields.containsKey("economicStatusId")) stopTBDetails.put("economicStatusId", extraFields.get("economicStatusId")); + if (extraFields.containsKey("residentialArea")) stopTBDetails.put("residentialArea", extraFields.get("residentialArea")); + if (extraFields.containsKey("residentialAreaId")) stopTBDetails.put("residentialAreaId", extraFields.get("residentialAreaId")); } catch (Exception ex) { logger.warn("Could not parse ExtraFields for benDetailsId: " + benDetailsOBJ.getBeneficiaryDetailsId()); } } if (!stopTBDetails.isEmpty()) resultMap.put("stopTBDetails", stopTBDetails); - resultMap.put("demographics", demographics); // adding asha id / created by - user id if (benAddressOBJ.getCreatedBy() != null) { From 51d796e60497af419b801cef865bc35c05aab119 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 11 May 2026 15:51:20 +0530 Subject: [PATCH 23/97] feat: use tb-prefixed keys for economicStatus and residentialArea to ensure ExtraFields storage Identity-API ignores unknown processing of economicStatus/residentialArea to DB columns silently; using tbEconomicStatus/tbResidentialArea as keys ensures they land in ExtraFields and are returned in worklist Co-Authored-By: Claude Sonnet 4.6 --- .../iemr/flw/service/impl/BeneficiaryServiceImpl.java | 8 ++++---- .../com/iemr/flw/service/impl/StopTBServiceImpl.java | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) 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 f886ac31..541beba1 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -499,10 +499,10 @@ private String getMappingsForAddressIDs(List addressLi 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")); - if (extraFields.containsKey("economicStatus")) stopTBDetails.put("economicStatus", extraFields.get("economicStatus")); - if (extraFields.containsKey("economicStatusId")) stopTBDetails.put("economicStatusId", extraFields.get("economicStatusId")); - if (extraFields.containsKey("residentialArea")) stopTBDetails.put("residentialArea", extraFields.get("residentialArea")); - if (extraFields.containsKey("residentialAreaId")) stopTBDetails.put("residentialAreaId", extraFields.get("residentialAreaId")); + if (extraFields.containsKey("tbEconomicStatus")) stopTBDetails.put("economicStatus", extraFields.get("tbEconomicStatus")); + if (extraFields.containsKey("tbEconomicStatusId")) stopTBDetails.put("economicStatusId", extraFields.get("tbEconomicStatusId")); + if (extraFields.containsKey("tbResidentialArea")) stopTBDetails.put("residentialArea", extraFields.get("tbResidentialArea")); + if (extraFields.containsKey("tbResidentialAreaId")) stopTBDetails.put("residentialAreaId", extraFields.get("tbResidentialAreaId")); } catch (Exception ex) { logger.warn("Could not parse ExtraFields for benDetailsId: " + benDetailsOBJ.getBeneficiaryDetailsId()); } diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 29f8b16c..24de4a84 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -96,11 +96,11 @@ public Map saveRegistration(String requestBody, String authoriza villageName = getStringField(demo, "districtBranchName"); districtID = getIntField(demo, "districtID"); districtName = getStringField(demo, "districtName"); - // promote to top-level so Identity-API saves them in ExtraFields - if (demo.has("economicStatus")) jsonObject.add("economicStatus", demo.get("economicStatus")); - if (demo.has("economicStatusId")) jsonObject.add("economicStatusId", demo.get("economicStatusId")); - if (demo.has("residentialArea")) jsonObject.add("residentialArea", demo.get("residentialArea")); - if (demo.has("residentialAreaId")) jsonObject.add("residentialAreaId", demo.get("residentialAreaId")); + // use tb-prefixed keys so Identity-API treats as unknown and saves in ExtraFields + if (demo.has("economicStatus")) jsonObject.addProperty("tbEconomicStatus", getStringField(demo, "economicStatus")); + if (demo.has("economicStatusId")) jsonObject.addProperty("tbEconomicStatusId", getIntField(demo, "economicStatusId")); + if (demo.has("residentialArea")) jsonObject.addProperty("tbResidentialArea", getStringField(demo, "residentialArea")); + if (demo.has("residentialAreaId")) jsonObject.addProperty("tbResidentialAreaId", getIntField(demo, "residentialAreaId")); } // isMobile=true tells TM-API to skip its own BenFlowStatus creation — FLW-API owns it From 42e50170afa630f35b87876385aa6a7ae9bd63e8 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 11 May 2026 15:54:46 +0530 Subject: [PATCH 24/97] feat: move occupation, economicStatus, residentialArea into beneficiaryDetails object Co-Authored-By: Claude Sonnet 4.6 --- .../RMNCHBeneficiaryDetailsRmnch.java | 27 +++++++++++++++++++ .../service/impl/BeneficiaryServiceImpl.java | 18 +++++++------ 2 files changed, 37 insertions(+), 8 deletions(-) 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..e4b634ff 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java @@ -521,4 +521,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/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index 541beba1..1d1695b1 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -478,12 +478,12 @@ private String getMappingsForAddressIDs(List addressLi ); resultMap.put("BenRegId", m.getBenRegId()); - // anthropometry and Stop TB fields - Map stopTBDetails = new HashMap<>(); - // occupation from i_beneficiarydetails (stored by Identity-API) + // occupation from i_beneficiarydetails if (benDetailsOBJ != null && benDetailsOBJ.getOccupation() != null) - stopTBDetails.put("occupation", benDetailsOBJ.getOccupation()); + benDetailsRMNCH_OBJ.setOccupation(benDetailsOBJ.getOccupation()); + // anthropometry and 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); @@ -499,10 +499,12 @@ private String getMappingsForAddressIDs(List addressLi 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")); - if (extraFields.containsKey("tbEconomicStatus")) stopTBDetails.put("economicStatus", extraFields.get("tbEconomicStatus")); - if (extraFields.containsKey("tbEconomicStatusId")) stopTBDetails.put("economicStatusId", extraFields.get("tbEconomicStatusId")); - if (extraFields.containsKey("tbResidentialArea")) stopTBDetails.put("residentialArea", extraFields.get("tbResidentialArea")); - if (extraFields.containsKey("tbResidentialAreaId")) stopTBDetails.put("residentialAreaId", extraFields.get("tbResidentialAreaId")); + + // economicStatus and residentialArea into beneficiaryDetails + if (extraFields.containsKey("tbEconomicStatus")) benDetailsRMNCH_OBJ.setEconomicStatus((String) extraFields.get("tbEconomicStatus")); + if (extraFields.containsKey("tbEconomicStatusId")) benDetailsRMNCH_OBJ.setEconomicStatusId(((Number) extraFields.get("tbEconomicStatusId")).intValue()); + if (extraFields.containsKey("tbResidentialArea")) benDetailsRMNCH_OBJ.setResidentialArea((String) extraFields.get("tbResidentialArea")); + if (extraFields.containsKey("tbResidentialAreaId")) benDetailsRMNCH_OBJ.setResidentialAreaId(((Number) extraFields.get("tbResidentialAreaId")).intValue()); } catch (Exception ex) { logger.warn("Could not parse ExtraFields for benDetailsId: " + benDetailsOBJ.getBeneficiaryDetailsId()); } From cab18f2c20dfec0af4dda156dbe7f7ace5320bba Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 11 May 2026 16:09:44 +0530 Subject: [PATCH 25/97] fix: read economicStatus and residentialArea directly from ExtraFields (not tb-prefixed keys) Mobile sends these as top-level fields to TM-API; Identity-API saves them in ExtraFields as-is Co-Authored-By: Claude Sonnet 4.6 --- .../iemr/flw/service/impl/BeneficiaryServiceImpl.java | 8 ++++---- .../com/iemr/flw/service/impl/StopTBServiceImpl.java | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) 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 1d1695b1..4beb335a 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -501,10 +501,10 @@ private String getMappingsForAddressIDs(List addressLi if (extraFields.containsKey("tuName")) stopTBDetails.put("tuName", extraFields.get("tuName")); // economicStatus and residentialArea into beneficiaryDetails - if (extraFields.containsKey("tbEconomicStatus")) benDetailsRMNCH_OBJ.setEconomicStatus((String) extraFields.get("tbEconomicStatus")); - if (extraFields.containsKey("tbEconomicStatusId")) benDetailsRMNCH_OBJ.setEconomicStatusId(((Number) extraFields.get("tbEconomicStatusId")).intValue()); - if (extraFields.containsKey("tbResidentialArea")) benDetailsRMNCH_OBJ.setResidentialArea((String) extraFields.get("tbResidentialArea")); - if (extraFields.containsKey("tbResidentialAreaId")) benDetailsRMNCH_OBJ.setResidentialAreaId(((Number) extraFields.get("tbResidentialAreaId")).intValue()); + 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()); } diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 24de4a84..ec57fabd 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -96,11 +96,11 @@ public Map saveRegistration(String requestBody, String authoriza villageName = getStringField(demo, "districtBranchName"); districtID = getIntField(demo, "districtID"); districtName = getStringField(demo, "districtName"); - // use tb-prefixed keys so Identity-API treats as unknown and saves in ExtraFields - if (demo.has("economicStatus")) jsonObject.addProperty("tbEconomicStatus", getStringField(demo, "economicStatus")); - if (demo.has("economicStatusId")) jsonObject.addProperty("tbEconomicStatusId", getIntField(demo, "economicStatusId")); - if (demo.has("residentialArea")) jsonObject.addProperty("tbResidentialArea", getStringField(demo, "residentialArea")); - if (demo.has("residentialAreaId")) jsonObject.addProperty("tbResidentialAreaId", getIntField(demo, "residentialAreaId")); + // promote to top-level so Identity-API saves them in ExtraFields + if (demo.has("economicStatus")) jsonObject.addProperty("economicStatus", getStringField(demo, "economicStatus")); + if (demo.has("economicStatusId")) jsonObject.addProperty("economicStatusId", getIntField(demo, "economicStatusId")); + if (demo.has("residentialArea")) jsonObject.addProperty("residentialArea", getStringField(demo, "residentialArea")); + if (demo.has("residentialAreaId")) jsonObject.addProperty("residentialAreaId", getIntField(demo, "residentialAreaId")); } // isMobile=true tells TM-API to skip its own BenFlowStatus creation — FLW-API owns it From 4b99b2d8fb50d1cc78b2f59b8cd4106b09d02a84 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 11 May 2026 20:17:16 +0530 Subject: [PATCH 26/97] feat: add Diagnostics module and Yes/No ID fields across Stop TB nurse flow - Add StopTBDiagnostics entity, repo, service, and controller (save/get/getAll) - Add village-scoped getAll (villageID optional) to GeneralExamination, TBScreening, GeneralOpd, Diagnostics via BenFlowStatus subquery - Fix TBScreening visitDate to accept user-provided date; preserve on update (not editable once submitted) - Fix referralToHWCNeeded to trust mobile-computed value instead of server recalculating - Fix chiefComplaint to support multi-select (JSON array) - Add Yes/No boolean ID counterpart fields to TBScreening (14 fields), GeneralExamination (referralToHWCNeededId), Registration (isMobileAvailableId), and Diagnostics (6 fields) - Add getTimestamp helper to StopTBServiceImpl Co-Authored-By: Claude Sonnet 4.6 --- .../iemr/flw/controller/StopTBController.java | 58 ++++++++- .../flw/domain/iemr/StopTBDiagnostics.java | 90 +++++++++++++ .../domain/iemr/StopTBGeneralExamination.java | 4 +- .../flw/domain/iemr/StopTBRegistration.java | 3 + .../com/iemr/flw/domain/iemr/TBScreening.java | 43 ++++++- .../flw/repo/iemr/StopTBDiagnosticsRepo.java | 24 ++++ .../iemr/StopTBGeneralExaminationRepo.java | 5 + .../flw/repo/iemr/StopTBGeneralOpdRepo.java | 5 + .../iemr/flw/repo/iemr/TBScreeningRepo.java | 5 + .../com/iemr/flw/service/StopTBService.java | 11 +- .../flw/service/impl/StopTBServiceImpl.java | 118 ++++++++++++++++-- 11 files changed, 349 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/StopTBDiagnosticsRepo.java diff --git a/src/main/java/com/iemr/flw/controller/StopTBController.java b/src/main/java/com/iemr/flw/controller/StopTBController.java index 39063401..dedaaa5b 100644 --- a/src/main/java/com/iemr/flw/controller/StopTBController.java +++ b/src/main/java/com/iemr/flw/controller/StopTBController.java @@ -108,7 +108,8 @@ public String getAllGeneralExaminations(@RequestBody Map body) { try { Object raw = body.get("providerServiceMapID"); if (raw == null) throw new Exception("providerServiceMapID is required"); - Map data = stopTBService.getAllGeneralExaminations(Integer.parseInt(raw.toString())); + Integer villageID = body.get("villageID") != null ? Integer.parseInt(body.get("villageID").toString()) : null; + Map data = stopTBService.getAllGeneralExaminations(Integer.parseInt(raw.toString()), villageID); response.setResponse(new Gson().toJson(data)); } catch (Exception e) { logger.error("Error in getAllGeneralExaminations: " + e); @@ -156,7 +157,8 @@ public String getAllNurseTBScreenings(@RequestBody Map body) { try { Object raw = body.get("providerServiceMapID"); if (raw == null) throw new Exception("providerServiceMapID is required"); - Map data = stopTBService.getAllNurseTBScreenings(Integer.parseInt(raw.toString())); + Integer villageID = body.get("villageID") != null ? Integer.parseInt(body.get("villageID").toString()) : null; + Map data = stopTBService.getAllNurseTBScreenings(Integer.parseInt(raw.toString()), villageID); response.setResponse(new Gson().toJson(data)); } catch (Exception e) { logger.error("Error in getAllNurseTBScreenings: " + e); @@ -204,7 +206,8 @@ public String getAllGeneralOpd(@RequestBody Map body) { try { Object raw = body.get("providerServiceMapID"); if (raw == null) throw new Exception("providerServiceMapID is required"); - Map data = stopTBService.getAllGeneralOpd(Integer.parseInt(raw.toString())); + Integer villageID = body.get("villageID") != null ? Integer.parseInt(body.get("villageID").toString()) : null; + Map data = stopTBService.getAllGeneralOpd(Integer.parseInt(raw.toString()), villageID); response.setResponse(new Gson().toJson(data)); } catch (Exception e) { logger.error("Error in getAllGeneralOpd: " + e); @@ -213,6 +216,55 @@ public String getAllGeneralOpd(@RequestBody Map body) { return response.toString(); } + // ── Nurse: Diagnostics ─────────────────────────────────────────────────── + + @PostMapping("/nurse/diagnostics/save") + @Operation(summary = "Save diagnostics for Stop TB beneficiary") + public String saveDiagnostics(@RequestBody Map data) { + OutputResponse response = new OutputResponse(); + try { + Map result = stopTBService.saveDiagnostics(data); + response.setResponse(new 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/get") + @Operation(summary = "Get diagnostics for a Stop TB beneficiary") + public String getDiagnostics(@RequestBody Map body) { + OutputResponse response = new OutputResponse(); + try { + Object raw = body.get("benRegID"); + if (raw == null) throw new Exception("benRegID is required"); + Map data = stopTBService.getDiagnostics(Long.parseLong(raw.toString())); + response.setResponse(new Gson().toJson(data)); + } catch (Exception e) { + logger.error("Error in getDiagnostics: " + e); + response.setError(5000, "Error fetching 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(new Gson().toJson(data)); + } catch (Exception e) { + logger.error("Error in getAllDiagnostics: " + e); + response.setError(5000, "Error fetching diagnostics records: " + e.getMessage()); + } + return response.toString(); + } + // ── Nurse: Submit ───────────────────────────────────────────────────────── @PostMapping("/nurse/submit") 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..6caa6128 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java @@ -0,0 +1,90 @@ +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 — auto-populated via cross-device sync + @Column(name = "is_digital_chest_xray_conducted_id") + private Integer isDigitalChestXrayConductedId; + + @Column(name = "is_digital_chest_xray_conducted") + private Boolean isDigitalChestXrayConducted; + + // Positive | Negative + @Column(name = "digital_chest_xray_result_id") + private Integer digitalChestXrayResultId; + + @Column(name = "digital_chest_xray_result", length = 20) + private String digitalChestXrayResult; + + // Truenat — enabled if pregnant or Key Population = Pregnancy + @Column(name = "is_truenat_conducted_id") + private Integer isTruenatConductedId; + + @Column(name = "is_truenat_conducted") + private Boolean isTruenatConducted; + + // 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; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java index 7a66b9b8..c6284c7e 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java @@ -86,7 +86,9 @@ public class StopTBGeneralExamination { @Column(name = "hiv_status") private String hivStatus; - // Server-calculated from vitals thresholds + @Column(name = "referral_to_hwc_needed_id") + private Integer referralToHWCNeededId; + @Column(name = "referral_to_hwc_needed") private Boolean referralToHWCNeeded; diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java index 82422dc4..4ce54a0d 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java @@ -50,6 +50,9 @@ public class StopTBRegistration { @Column(name = "temperatureValue") private Double temperatureValue; + @Column(name = "isMobileAvailableId") + private Integer isMobileAvailableId; + @Column(name = "isMobileAvailable") private Boolean isMobileAvailable; 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 59581be2..45a1363f 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBScreening.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBScreening.java @@ -23,33 +23,63 @@ 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; @@ -94,22 +124,33 @@ public class TBScreening { 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; - // JSON array of denial reasons when patient refuses a test @Column(name = "test_denial_reasons", columnDefinition = "TEXT") private String testDenialReasons; } 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..7d764f4a --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBDiagnosticsRepo.java @@ -0,0 +1,24 @@ +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.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +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); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java index 847e3fd0..88128cbc 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java @@ -16,4 +16,9 @@ public interface StopTBGeneralExaminationRepo extends JpaRepository 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); } diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java index 0dd69a9d..f02e4993 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java @@ -16,4 +16,9 @@ public interface StopTBGeneralOpdRepo extends JpaRepository 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); } 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 b1462b94..c30f8991 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBScreeningRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBScreeningRepo.java @@ -23,4 +23,9 @@ public interface TBScreeningRepo extends JpaRepository { @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); } diff --git a/src/main/java/com/iemr/flw/service/StopTBService.java b/src/main/java/com/iemr/flw/service/StopTBService.java index 2c99e6d1..a1a7c139 100644 --- a/src/main/java/com/iemr/flw/service/StopTBService.java +++ b/src/main/java/com/iemr/flw/service/StopTBService.java @@ -15,17 +15,22 @@ public interface StopTBService { // Nurse — General Examination Map saveGeneralExamination(Map data) throws Exception; Map getGeneralExamination(Long benRegID) throws Exception; - Map getAllGeneralExaminations(Integer providerServiceMapID) throws Exception; + Map getAllGeneralExaminations(Integer providerServiceMapID, Integer villageID) throws Exception; // Nurse — TB Screening Map saveNurseTBScreening(Map data) throws Exception; Map getNurseTBScreening(Long benRegID) throws Exception; - Map getAllNurseTBScreenings(Integer providerServiceMapID) throws Exception; + Map getAllNurseTBScreenings(Integer providerServiceMapID, Integer villageID) throws Exception; // Nurse — General OPD Map saveGeneralOpd(Map data) throws Exception; Map getGeneralOpd(Long benRegID) throws Exception; - Map getAllGeneralOpd(Integer providerServiceMapID) throws Exception; + Map getAllGeneralOpd(Integer providerServiceMapID, Integer villageID) throws Exception; + + // Nurse — Diagnostics + Map saveDiagnostics(Map data) throws Exception; + Map getDiagnostics(Long benRegID) throws Exception; + Map getAllDiagnostics(Integer providerServiceMapID, Integer villageID) throws Exception; // Nurse — submit Map submitNurseData(Map data) throws Exception; diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index ec57fabd..b6c1a5ad 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -6,12 +6,14 @@ import com.iemr.flw.domain.identity.RMNCHMBeneficiarydetail; import com.iemr.flw.domain.identity.RMNCHMBeneficiarymapping; import com.iemr.flw.domain.iemr.BenFlowStatus; +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.dto.iemr.StopTBRegistrationDto; import com.iemr.flw.repo.identity.BeneficiaryRepo; import com.iemr.flw.repo.iemr.BenFlowStatusRepo; +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; @@ -49,6 +51,9 @@ public class StopTBServiceImpl implements StopTBService { @Autowired private StopTBGeneralOpdRepo generalOpdRepo; + @Autowired + private StopTBDiagnosticsRepo diagnosticsRepo; + @Value("${tm-url}") private String tmUrl; @@ -305,7 +310,7 @@ public Map saveGeneralExamination(Map data) thro exam.setHivStatus(getString(data, "hivStatus")); exam.setCreatedBy(getString(data, "createdBy")); exam.setDeleted(false); - exam.setReferralToHWCNeeded(calculateReferralNeeded(exam)); + exam.setReferralToHWCNeeded(getBool(data, "referralToHWCNeeded")); generalExaminationRepo.save(exam); @@ -331,8 +336,10 @@ public Map getGeneralExamination(Long beneficiaryRegID) throws E } @Override - public Map getAllGeneralExaminations(Integer providerServiceMapID) throws Exception { - List list = generalExaminationRepo.findAllByProviderServiceMapID(providerServiceMapID); + 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<>(); @@ -404,7 +411,11 @@ public Map saveNurseTBScreening(Map data) throws screening.setRecommendedForLiquidCulture(getBool(data, "recommendedForLiquidCulture")); screening.setTestDenialReasons(getString(data, "testDenialReasons")); screening.setCreatedBy(getString(data, "createdBy")); - screening.setVisitDate(new Timestamp(System.currentTimeMillis())); + // 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())); + } tbScreeningRepo.save(screening); @@ -421,8 +432,10 @@ public Map getNurseTBScreening(Long beneficiaryRegID) throws Exc } @Override - public Map getAllNurseTBScreenings(Integer providerServiceMapID) throws Exception { - List list = tbScreeningRepo.findAllByProviderServiceMapID(providerServiceMapID); + 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<>(); @@ -472,7 +485,7 @@ public Map saveGeneralOpd(Map data) throws Excep opd.setBenRegID(beneficiaryRegID); opd.setProviderServiceMapID(getInt(data, "providerServiceMapID")); - opd.setChiefComplaint(getString(data, "chiefComplaint")); + opd.setChiefComplaint(toJsonString(data.get("chiefComplaint"))); opd.setMedication(getString(data, "medication")); opd.setDosage(getString(data, "dosage")); opd.setFrequency(getString(data, "frequency")); @@ -496,8 +509,10 @@ public Map getGeneralOpd(Long beneficiaryRegID) throws Exception } @Override - public Map getAllGeneralOpd(Integer providerServiceMapID) throws Exception { - List list = generalOpdRepo.findAllByProviderServiceMapID(providerServiceMapID); + 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<>(); @@ -522,6 +537,84 @@ private Map opdToMap(StopTBGeneralOpd o) { return m; } + // ── Nurse: Diagnostics ──────────────────────────────────────────────────── + + @Override + @Transactional + public Map saveDiagnostics(Map data) throws Exception { + 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(); + + 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.setIsDigitalChestXrayConducted(getBool(data, "isDigitalChestXrayConducted")); + diag.setDigitalChestXrayResult(getString(data, "digitalChestXrayResult")); + diag.setIsTruenatConducted(getBool(data, "isTruenatConducted")); + 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.setDeleted(false); + + diagnosticsRepo.save(diag); + + Map result = new HashMap<>(); + result.put("benRegID", benRegID); + return result; + } + + @Override + public Map getDiagnostics(Long benRegID) throws Exception { + StopTBDiagnostics diag = diagnosticsRepo.findByBenRegID(benRegID); + if (diag == null) throw new Exception("No diagnostics record found for benRegID: " + benRegID); + return diagnosticsToMap(diag); + } + + @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("isDigitalChestXrayConducted", d.getIsDigitalChestXrayConducted()); + m.put("digitalChestXrayResult", d.getDigitalChestXrayResult()); + m.put("isTruenatConducted", d.getIsTruenatConducted()); + 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()); + return m; + } + // ── Nurse: Submit ───────────────────────────────────────────────────────── @Override @@ -587,6 +680,13 @@ private Boolean getBool(Map m, String key) { 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; } + } + private String getStringField(JsonObject obj, String field) { return obj.has(field) && !obj.get(field).isJsonNull() ? obj.get(field).getAsString() : null; } From 6ffc50e708043e6c4a7a0b93be6cd3d9a8108b2c Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 12 May 2026 16:50:45 +0530 Subject: [PATCH 27/97] feat: accept array of objects in all 4 Stop TB nurse save APIs generalExamination, tbScreening, generalOpd, and diagnostics save endpoints now take List instead of a single Map, allowing bulk save in one request. Each item is processed within the same transaction. Co-Authored-By: Claude Sonnet 4.6 --- .../iemr/flw/controller/StopTBController.java | 25 +- .../com/iemr/flw/service/StopTBService.java | 9 +- .../flw/service/impl/StopTBServiceImpl.java | 274 +++++++++--------- 3 files changed, 163 insertions(+), 145 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/StopTBController.java b/src/main/java/com/iemr/flw/controller/StopTBController.java index dedaaa5b..8da6ff63 100644 --- a/src/main/java/com/iemr/flw/controller/StopTBController.java +++ b/src/main/java/com/iemr/flw/controller/StopTBController.java @@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; +import java.util.List; import java.util.Map; @RestController @@ -72,11 +73,11 @@ public String getNurseWorklist(@RequestBody StopTBRegistrationDto dto) { // ── Nurse: General Examination ──────────────────────────────────────────── @PostMapping("/nurse/generalExamination/save") - @Operation(summary = "Save general examination for Stop TB beneficiary") - public String saveGeneralExamination(@RequestBody Map data) { + @Operation(summary = "Save general examination for Stop TB beneficiary (array of objects)") + public String saveGeneralExamination(@RequestBody List> dataList) { OutputResponse response = new OutputResponse(); try { - Map result = stopTBService.saveGeneralExamination(data); + List> result = stopTBService.saveGeneralExamination(dataList); response.setResponse(new Gson().toJson(result)); } catch (Exception e) { logger.error("Error in saveGeneralExamination: " + e); @@ -121,11 +122,11 @@ public String getAllGeneralExaminations(@RequestBody Map body) { // ── Nurse: TB Screening ─────────────────────────────────────────────────── @PostMapping("/nurse/tbScreening/save") - @Operation(summary = "Save TB screening for Stop TB beneficiary") - public String saveNurseTBScreening(@RequestBody Map data) { + @Operation(summary = "Save TB screening for Stop TB beneficiary (array of objects)") + public String saveNurseTBScreening(@RequestBody List> dataList) { OutputResponse response = new OutputResponse(); try { - Map result = stopTBService.saveNurseTBScreening(data); + List> result = stopTBService.saveNurseTBScreening(dataList); response.setResponse(new Gson().toJson(result)); } catch (Exception e) { logger.error("Error in saveNurseTBScreening: " + e); @@ -170,11 +171,11 @@ public String getAllNurseTBScreenings(@RequestBody Map body) { // ── Nurse: General OPD ──────────────────────────────────────────────────── @PostMapping("/nurse/generalOpd/save") - @Operation(summary = "Save general OPD record for Stop TB beneficiary") - public String saveGeneralOpd(@RequestBody Map data) { + @Operation(summary = "Save general OPD record for Stop TB beneficiary (array of objects)") + public String saveGeneralOpd(@RequestBody List> dataList) { OutputResponse response = new OutputResponse(); try { - Map result = stopTBService.saveGeneralOpd(data); + List> result = stopTBService.saveGeneralOpd(dataList); response.setResponse(new Gson().toJson(result)); } catch (Exception e) { logger.error("Error in saveGeneralOpd: " + e); @@ -219,11 +220,11 @@ public String getAllGeneralOpd(@RequestBody Map body) { // ── Nurse: Diagnostics ─────────────────────────────────────────────────── @PostMapping("/nurse/diagnostics/save") - @Operation(summary = "Save diagnostics for Stop TB beneficiary") - public String saveDiagnostics(@RequestBody Map data) { + @Operation(summary = "Save diagnostics for Stop TB beneficiary (array of objects)") + public String saveDiagnostics(@RequestBody List> dataList) { OutputResponse response = new OutputResponse(); try { - Map result = stopTBService.saveDiagnostics(data); + List> result = stopTBService.saveDiagnostics(dataList); response.setResponse(new Gson().toJson(result)); } catch (Exception e) { logger.error("Error in saveDiagnostics: " + e); diff --git a/src/main/java/com/iemr/flw/service/StopTBService.java b/src/main/java/com/iemr/flw/service/StopTBService.java index a1a7c139..b59ccd01 100644 --- a/src/main/java/com/iemr/flw/service/StopTBService.java +++ b/src/main/java/com/iemr/flw/service/StopTBService.java @@ -2,6 +2,7 @@ import com.iemr.flw.dto.iemr.StopTBRegistrationDto; +import java.util.List; import java.util.Map; public interface StopTBService { @@ -13,22 +14,22 @@ public interface StopTBService { Map getNurseWorklist(StopTBRegistrationDto dto) throws Exception; // Nurse — General Examination - Map saveGeneralExamination(Map data) throws Exception; + List> saveGeneralExamination(List> dataList) throws Exception; Map getGeneralExamination(Long benRegID) throws Exception; Map getAllGeneralExaminations(Integer providerServiceMapID, Integer villageID) throws Exception; // Nurse — TB Screening - Map saveNurseTBScreening(Map data) throws Exception; + List> saveNurseTBScreening(List> dataList) throws Exception; Map getNurseTBScreening(Long benRegID) throws Exception; Map getAllNurseTBScreenings(Integer providerServiceMapID, Integer villageID) throws Exception; // Nurse — General OPD - Map saveGeneralOpd(Map data) throws Exception; + List> saveGeneralOpd(List> dataList) throws Exception; Map getGeneralOpd(Long benRegID) throws Exception; Map getAllGeneralOpd(Integer providerServiceMapID, Integer villageID) throws Exception; // Nurse — Diagnostics - Map saveDiagnostics(Map data) throws Exception; + List> saveDiagnostics(List> dataList) throws Exception; Map getDiagnostics(Long benRegID) throws Exception; Map getAllDiagnostics(Integer providerServiceMapID, Integer villageID) throws Exception; diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index b6c1a5ad..ec518fde 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -279,45 +279,49 @@ private Map wrapWorklist(List> worklist) { @Override @Transactional - public Map saveGeneralExamination(Map data) throws Exception { - Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); - if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); - - StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegID(beneficiaryRegID); - if (exam == null) exam = new StopTBGeneralExamination(); - - exam.setBeneficiaryRegID(beneficiaryRegID); - exam.setProviderServiceMapID(getInt(data, "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.setDeleted(false); - exam.setReferralToHWCNeeded(getBool(data, "referralToHWCNeeded")); - - generalExaminationRepo.save(exam); - - Map result = new HashMap<>(); - result.put("beneficiaryRegID", beneficiaryRegID); - result.put("referralToHWCNeeded", exam.getReferralToHWCNeeded()); - return result; + public List> saveGeneralExamination(List> dataList) throws Exception { + List> results = new ArrayList<>(); + for (Map data : dataList) { + Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); + if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); + + StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegID(beneficiaryRegID); + if (exam == null) exam = new StopTBGeneralExamination(); + + exam.setBeneficiaryRegID(beneficiaryRegID); + exam.setProviderServiceMapID(getInt(data, "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.setDeleted(false); + exam.setReferralToHWCNeeded(getBool(data, "referralToHWCNeeded")); + + generalExaminationRepo.save(exam); + + Map result = new HashMap<>(); + result.put("beneficiaryRegID", beneficiaryRegID); + result.put("referralToHWCNeeded", exam.getReferralToHWCNeeded()); + results.add(result); + } + return results; } private boolean calculateReferralNeeded(StopTBGeneralExamination e) { @@ -383,45 +387,49 @@ private Map examToMap(StopTBGeneralExamination e) { @Override @Transactional - public Map saveNurseTBScreening(Map data) throws Exception { - Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); - if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); - - TBScreening screening = tbScreeningRepo.findByBenRegID(beneficiaryRegID); - if (screening == null) screening = new TBScreening(); - - screening.setBenRegID(beneficiaryRegID); - screening.setProviderServiceMapID(getInt(data, "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.setSympotomatic(getString(data, "symptomatic")); - 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")); - // 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())); - } + public List> saveNurseTBScreening(List> dataList) throws Exception { + List> results = new ArrayList<>(); + for (Map data : dataList) { + Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); + if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); + + TBScreening screening = tbScreeningRepo.findByBenRegID(beneficiaryRegID); + if (screening == null) screening = new TBScreening(); + + screening.setBenRegID(beneficiaryRegID); + screening.setProviderServiceMapID(getInt(data, "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.setSympotomatic(getString(data, "symptomatic")); + 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")); + // 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())); + } - tbScreeningRepo.save(screening); + tbScreeningRepo.save(screening); - Map result = new HashMap<>(); - result.put("beneficiaryRegID", beneficiaryRegID); - return result; + Map result = new HashMap<>(); + result.put("beneficiaryRegID", beneficiaryRegID); + results.add(result); + } + return results; } @Override @@ -476,29 +484,33 @@ private Map screeningToMap(TBScreening s) { @Override @Transactional - public Map saveGeneralOpd(Map data) throws Exception { - Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); - if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); - - StopTBGeneralOpd opd = generalOpdRepo.findByBenRegID(beneficiaryRegID); - if (opd == null) opd = new StopTBGeneralOpd(); - - opd.setBenRegID(beneficiaryRegID); - opd.setProviderServiceMapID(getInt(data, "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.setDeleted(false); - - generalOpdRepo.save(opd); - - Map result = new HashMap<>(); - result.put("beneficiaryRegID", beneficiaryRegID); - return result; + public List> saveGeneralOpd(List> dataList) throws Exception { + List> results = new ArrayList<>(); + for (Map data : dataList) { + Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); + if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); + + StopTBGeneralOpd opd = generalOpdRepo.findByBenRegID(beneficiaryRegID); + if (opd == null) opd = new StopTBGeneralOpd(); + + opd.setBenRegID(beneficiaryRegID); + opd.setProviderServiceMapID(getInt(data, "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.setDeleted(false); + + generalOpdRepo.save(opd); + + Map result = new HashMap<>(); + result.put("beneficiaryRegID", beneficiaryRegID); + results.add(result); + } + return results; } @Override @@ -541,40 +553,44 @@ private Map opdToMap(StopTBGeneralOpd o) { @Override @Transactional - public Map saveDiagnostics(Map data) throws Exception { - 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(); + public List> saveDiagnostics(List> dataList) throws Exception { + List> results = new ArrayList<>(); + 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(); + + 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.setBenRegID(benRegID); - diag.setProviderServiceMapID(getInt(data, "providerServiceMapID")); + diag.setNikshayId(getString(data, "nikshayId")); + diag.setIsDigitalChestXrayConducted(getBool(data, "isDigitalChestXrayConducted")); + diag.setDigitalChestXrayResult(getString(data, "digitalChestXrayResult")); + diag.setIsTruenatConducted(getBool(data, "isTruenatConducted")); + 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.setDeleted(false); - // 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())); - } + diagnosticsRepo.save(diag); - diag.setNikshayId(getString(data, "nikshayId")); - diag.setIsDigitalChestXrayConducted(getBool(data, "isDigitalChestXrayConducted")); - diag.setDigitalChestXrayResult(getString(data, "digitalChestXrayResult")); - diag.setIsTruenatConducted(getBool(data, "isTruenatConducted")); - 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")); + Map result = new HashMap<>(); + result.put("benRegID", benRegID); + results.add(result); } - diag.setCreatedBy(getString(data, "createdBy")); - diag.setDeleted(false); - - diagnosticsRepo.save(diag); - - Map result = new HashMap<>(); - result.put("benRegID", benRegID); - return result; + return results; } @Override From ace45fa74f408a70fdcc0e2405539dd261d4fe1c Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 13 May 2026 17:32:19 +0530 Subject: [PATCH 28/97] fix supervisor name --- .../domain/identity/RMNCHMBeneficiarydetail.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 1667fc96..525ef0d0 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiarydetail.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiarydetail.java @@ -193,4 +193,19 @@ public class RMNCHMBeneficiarydetail { @Column(name = "ExtraFields") private String otherFields; + @Column(name = "economicStatus") + private String economicStatus; + + @Column(name = "economicStatusId") + private Integer economicStatusId; + + @Column(name = "residentialArea") + private String residentialArea; + + @Column(name = "residentialAreaId") + private Integer residentialAreaId; + + @Column(name = "address") + private String address; + } From 4ff074e543933d9f89463899219fbc8a983d702c Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 13 May 2026 18:03:27 +0530 Subject: [PATCH 29/97] fix supervisor name --- .../iemr/flw/domain/identity/RMNCHMBeneficiarydetail.java | 5 +++++ 1 file changed, 5 insertions(+) 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 525ef0d0..1a4cec51 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiarydetail.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiarydetail.java @@ -193,18 +193,23 @@ public class RMNCHMBeneficiarydetail { @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; From e4d93699be12a8a3ea0221ed098aab7652ae2b59 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 13 May 2026 18:34:28 +0530 Subject: [PATCH 30/97] fix supervisor name --- .../com/iemr/flw/service/impl/BeneficiaryServiceImpl.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 4beb335a..ac5f5b9e 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -476,6 +476,12 @@ 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("BenRegId", m.getBenRegId()); // occupation from i_beneficiarydetails From e1d6fc492b2397b8bfa7a371cc2c0aba1ded8614 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Mon, 18 May 2026 16:22:49 +0530 Subject: [PATCH 31/97] add update date on get ben api --- .../java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ac5f5b9e..a0f0f5d4 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -481,7 +481,7 @@ private String getMappingsForAddressIDs(List addressLi resultMap.put("residentialAreaId",benDetailsOBJ.getResidentialAreaId()); resultMap.put("residentialArea",benDetailsOBJ.getResidentialArea()); resultMap.put("address",benDetailsOBJ.getAddress()); - + resultMap.put("updateDate",benDetailsRMNCH_OBJ.getUpdatedDate()); resultMap.put("BenRegId", m.getBenRegId()); // occupation from i_beneficiarydetails From ada3b789e636ab86f283dafce551443440a900c3 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Mon, 18 May 2026 18:11:09 +0530 Subject: [PATCH 32/97] add update date on get ben api --- .../java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a0f0f5d4..3bcf2729 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -481,7 +481,7 @@ private String getMappingsForAddressIDs(List addressLi resultMap.put("residentialAreaId",benDetailsOBJ.getResidentialAreaId()); resultMap.put("residentialArea",benDetailsOBJ.getResidentialArea()); resultMap.put("address",benDetailsOBJ.getAddress()); - resultMap.put("updateDate",benDetailsRMNCH_OBJ.getUpdatedDate()); + resultMap.put("updateDate",benDetailsOBJ.getLastModDate()); resultMap.put("BenRegId", m.getBenRegId()); // occupation from i_beneficiarydetails From fda8f8a10a6260e41c31ff6d5dfd7813f03c2ef5 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 19 May 2026 14:00:37 +0530 Subject: [PATCH 33/97] feat: return updateDate and updatedBy in all Stop TB getAll APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add lastModDate + modifiedBy fields to TBScreening, TBSuspected, TBConfirmedCase, MalariaFollowUp entities (columns already exist in DB) - Set modifiedBy in all save methods across StopTB, TBSuspected, TBConfirmed, MalariaFollowUp services - Return updateDate/updatedBy in all 8 getAll API responses so mobile app can detect and refresh stale local records - Fix TBConfirmedCase save duplicate bug — now looks up existing record by benId before inserting new entity - Add updateDate to getBeneficiaryData response from i_beneficiarydetails Co-Authored-By: Claude Sonnet 4.6 --- .../iemr/flw/domain/iemr/MalariaFollowUp.java | 9 +++++++++ .../iemr/flw/domain/iemr/TBConfirmedCase.java | 6 ++++++ .../flw/domain/iemr/TBConfirmedCaseDTO.java | 6 +++++- .../com/iemr/flw/domain/iemr/TBScreening.java | 8 ++++++++ .../com/iemr/flw/domain/iemr/TBSuspected.java | 8 +++++++- .../flw/dto/iemr/MalariaFollowListUpDTO.java | 4 ++++ .../com/iemr/flw/dto/iemr/TBSuspectedDTO.java | 2 ++ .../service/impl/BeneficiaryServiceImpl.java | 3 +++ .../impl/MalariaFollowUpServiceImpl.java | 3 +++ .../flw/service/impl/StopTBServiceImpl.java | 12 ++++++++++++ .../impl/TBConfirmedCaseServiceImpl.java | 18 ++++++++++++------ .../service/impl/TBSuspectedServiceImpl.java | 9 ++++++--- 12 files changed, 77 insertions(+), 11 deletions(-) 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/TBConfirmedCase.java b/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCase.java index 8310eb65..66655a4f 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,10 @@ 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; } 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..07903035 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; 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 45a1363f..73f0bb73 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; @@ -120,6 +121,13 @@ public class TBScreening { @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; 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..2542cb26 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,12 @@ 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; } 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/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/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index 3bcf2729..c501810e 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -397,6 +397,9 @@ private String getMappingsForAddressIDs(List addressLi if (benDetailsOBJ.getCreatedBy() != null) benDetailsRMNCH_OBJ.setCreatedBy(benDetailsOBJ.getCreatedBy()); + resultMap.put("updateDate", benDetailsOBJ.getLastModDate()); + resultMap.put("updatedBy", benDetailsOBJ.getModifiedBy()); + // age calculation String ageDetails = ""; int age_val = 0; 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/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index ec518fde..0c49d8f2 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -311,6 +311,7 @@ public List> saveGeneralExamination(List 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")); @@ -380,6 +381,8 @@ private Map examToMap(StopTBGeneralExamination e) { 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; } @@ -417,6 +420,7 @@ public List> saveNurseTBScreening(List> 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"); @@ -477,6 +481,8 @@ private Map screeningToMap(TBScreening s) { 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; } @@ -502,6 +508,7 @@ public List> saveGeneralOpd(List> dataLi opd.setDuration(getString(data, "duration")); opd.setNotes(getString(data, "notes")); opd.setCreatedBy(getString(data, "createdBy")); + opd.setModifiedBy(getString(data, "createdBy")); opd.setDeleted(false); generalOpdRepo.save(opd); @@ -546,6 +553,8 @@ private Map opdToMap(StopTBGeneralOpd o) { 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; } @@ -582,6 +591,7 @@ public List> saveDiagnostics(List> dataL diag.setLiquidCultureResult(getString(data, "liquidCultureResult")); } diag.setCreatedBy(getString(data, "createdBy")); + diag.setModifiedBy(getString(data, "createdBy")); diag.setDeleted(false); diagnosticsRepo.save(diag); @@ -628,6 +638,8 @@ private Map diagnosticsToMap(StopTBDiagnostics d) { 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; } 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..0bac4eb0 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java @@ -46,10 +46,12 @@ public String save(List request, String authorisation) throw if (request != null) { logger.info("Saving TB confirmed case: " + request); - TBConfirmedCase entity = new TBConfirmedCase(); for(TBConfirmedCaseDTO dto:request){ + List existing = repository.findByBenId(dto.getBenId()); + TBConfirmedCase entity = existing.isEmpty() ? new TBConfirmedCase() : existing.get(0); entity.setBenId(dto.getBenId()); entity.setUserId(jwtUtil.extractUserId(authorisation)); + entity.setModifiedBy(jwtUtil.extractUsername(authorisation)); entity.setRegimenType(dto.getRegimenType()); entity.setTreatmentStartDate(dto.getTreatmentStartDate()); entity.setExpectedTreatmentCompletionDate(dto.getExpectedTreatmentCompletionDate()); @@ -144,22 +146,24 @@ 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); + List dtoList = list.stream().map(this::toDTO).collect(Collectors.toList()); + 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 +180,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/TBSuspectedServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java index 2a8c0eeb..6ee3b47c 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -69,8 +69,11 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { 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)); + 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 +83,7 @@ public String getByUserId(GetBenRequestHandler request) { tbSuspected.getUserId() ); } - }); + } TBSuspectedRequestDTO tbSuspectedRequestDTO = new TBSuspectedRequestDTO(); tbSuspectedRequestDTO.setTbSuspectedList(dtos); tbSuspectedRequestDTO.setUserId(request.getAshaId()); From 6d34fabaf5f85346bb66314f25723caea56f377f Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 19 May 2026 14:35:11 +0530 Subject: [PATCH 34/97] =?UTF-8?q?fix:=20resolve=20compile=20errors=20?= =?UTF-8?q?=E2=80=94=20move=20resultMap.put=20after=20initialization,=20ad?= =?UTF-8?q?d=20Map=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../com/iemr/flw/service/impl/BeneficiaryServiceImpl.java | 6 ++---- .../iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) 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 c501810e..7095875e 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -397,9 +397,6 @@ private String getMappingsForAddressIDs(List addressLi if (benDetailsOBJ.getCreatedBy() != null) benDetailsRMNCH_OBJ.setCreatedBy(benDetailsOBJ.getCreatedBy()); - resultMap.put("updateDate", benDetailsOBJ.getLastModDate()); - resultMap.put("updatedBy", benDetailsOBJ.getModifiedBy()); - // age calculation String ageDetails = ""; int age_val = 0; @@ -484,7 +481,8 @@ private String getMappingsForAddressIDs(List addressLi resultMap.put("residentialAreaId",benDetailsOBJ.getResidentialAreaId()); resultMap.put("residentialArea",benDetailsOBJ.getResidentialArea()); resultMap.put("address",benDetailsOBJ.getAddress()); - resultMap.put("updateDate",benDetailsOBJ.getLastModDate()); + resultMap.put("updateDate", benDetailsOBJ.getLastModDate()); + resultMap.put("updatedBy", benDetailsOBJ.getModifiedBy()); resultMap.put("BenRegId", m.getBenRegId()); // occupation from i_beneficiarydetails 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 0bac4eb0..1876f5f5 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java @@ -18,7 +18,9 @@ import java.sql.Timestamp; import java.time.LocalDate; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Service From efab338cc45a5c7859e8055b6b6cb80a47bcd48b Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 28 May 2026 17:32:31 +0530 Subject: [PATCH 35/97] feat: add chest xray, sputum, NAAT reason fields to StopTB diagnostics Added isReferredForDigitalChestXray, reasonForDenialChestXray, reasonNotConductedChestXray, isReferredForSputumCollection, reasonForDenialSputum, sputumSubmittedAt, reasonNotConductedNaat and their *Other variants to domain, save, and get map. Co-Authored-By: Claude Sonnet 4.6 --- .../flw/domain/iemr/StopTBDiagnostics.java | 38 ++++++++++++++++++- .../flw/service/impl/StopTBServiceImpl.java | 26 +++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java index 6caa6128..0437a617 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java @@ -29,13 +29,28 @@ public class StopTBDiagnostics { @Column(name = "nikshay_id", length = 50) private String nikshayId; - // Digital Chest X-ray — auto-populated via cross-device sync + // 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; @@ -43,13 +58,32 @@ public class StopTBDiagnostics { @Column(name = "digital_chest_xray_result", length = 20) private String digitalChestXrayResult; - // Truenat — enabled if pregnant or Key Population = Pregnancy + // 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; diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 0c49d8f2..b0176fe8 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -581,10 +581,25 @@ public List> saveDiagnostics(List> dataL } 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")) { @@ -630,9 +645,20 @@ private Map diagnosticsToMap(StopTBDiagnostics d) { 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()); From 6bb1fb1e2b56641cb36409b4539e59c64155ec2a Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 4 Jun 2026 12:24:37 +0530 Subject: [PATCH 36/97] feat(stoptb): add local-to-central sync fields for Stop TB mobile flow - Add vanID, parkingPlaceID, visitCode, processed, vanSerialNo to all Stop TB entities (TBScreening, GeneralExamination, GeneralOpd, Diagnostics, TBSuspected, TBConfirmedCase) - Add updateVanSerialNo() to all 6 repos for MMU DataSync pattern - Add CampConfigService to read camp:vanID from Redis (set by MMU login) - Stamp vanID + processed='N' on every save in StopTBServiceImpl, TBSuspectedServiceImpl, TBConfirmedCaseServiceImpl Co-Authored-By: Claude Sonnet 4.6 --- .../flw/domain/iemr/StopTBDiagnostics.java | 16 ++++++ .../domain/iemr/StopTBGeneralExamination.java | 22 ++++++++ .../flw/domain/iemr/StopTBGeneralOpd.java | 19 +++++++ .../iemr/flw/domain/iemr/TBConfirmedCase.java | 22 ++++++++ .../com/iemr/flw/domain/iemr/TBScreening.java | 19 +++++++ .../com/iemr/flw/domain/iemr/TBSuspected.java | 22 ++++++++ .../flw/repo/iemr/StopTBDiagnosticsRepo.java | 7 +++ .../iemr/StopTBGeneralExaminationRepo.java | 7 +++ .../flw/repo/iemr/StopTBGeneralOpdRepo.java | 7 +++ .../iemr/TBConfirmedTreatmentRepository.java | 8 +++ .../iemr/flw/repo/iemr/TBScreeningRepo.java | 7 +++ .../iemr/flw/repo/iemr/TBSuspectedRepo.java | 7 +++ .../iemr/flw/service/CampConfigService.java | 51 +++++++++++++++++++ .../flw/service/impl/StopTBServiceImpl.java | 28 ++++++++++ .../impl/TBConfirmedCaseServiceImpl.java | 15 ++++-- .../service/impl/TBSuspectedServiceImpl.java | 14 +++-- 16 files changed, 264 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/iemr/flw/service/CampConfigService.java diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java index 0437a617..f9abde57 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java @@ -121,4 +121,20 @@ public class StopTBDiagnostics { @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 index c6284c7e..9972d916 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java @@ -108,4 +108,26 @@ public class StopTBGeneralExamination { @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 index e7af3318..4cfbb912 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralOpd.java +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralOpd.java @@ -59,4 +59,23 @@ public class StopTBGeneralOpd { @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 66655a4f..864d9506 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCase.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCase.java @@ -77,4 +77,26 @@ public class TBConfirmedCase { @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; } 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 73f0bb73..d9df1dcf 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBScreening.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBScreening.java @@ -161,4 +161,23 @@ public class TBScreening { @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 2542cb26..5a5929a4 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBSuspected.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBSuspected.java @@ -82,4 +82,26 @@ public class TBSuspected { @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; } diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBDiagnosticsRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBDiagnosticsRepo.java index 7d764f4a..de3fa909 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/StopTBDiagnosticsRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBDiagnosticsRepo.java @@ -2,9 +2,11 @@ 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; @@ -21,4 +23,9 @@ public interface StopTBDiagnosticsRepo extends JpaRepository 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 index 88128cbc..b716f2e4 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java @@ -2,9 +2,11 @@ import com.iemr.flw.domain.iemr.StopTBGeneralExamination; 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; @@ -21,4 +23,9 @@ public interface StopTBGeneralExaminationRepo extends JpaRepository 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 index f02e4993..59d87a15 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java @@ -2,9 +2,11 @@ import com.iemr.flw.domain.iemr.StopTBGeneralOpd; 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; @@ -21,4 +23,9 @@ public interface StopTBGeneralOpdRepo extends JpaRepository 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..21c4a158 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; @@ -14,4 +18,8 @@ public interface TBConfirmedTreatmentRepository List findByBenId(Long benId); List findByUserId(Integer benId); + @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 c30f8991..cd875c90 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBScreeningRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBScreeningRepo.java @@ -2,9 +2,11 @@ import com.iemr.flw.domain.iemr.TBScreening; 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; @@ -28,4 +30,9 @@ public interface TBScreeningRepo extends JpaRepository { "(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..aae6d87a 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; @@ -20,4 +22,9 @@ public interface TBSuspectedRepo extends JpaRepository { @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); + + @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/service/CampConfigService.java b/src/main/java/com/iemr/flw/service/CampConfigService.java new file mode 100644 index 00000000..998d388c --- /dev/null +++ b/src/main/java/com/iemr/flw/service/CampConfigService.java @@ -0,0 +1,51 @@ +package com.iemr.flw.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +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; + + public Integer getVanID() { + String val = read(VAN_ID_KEY); + if (val == null || val.isBlank()) { + throw new IllegalStateException( + "Camp not configured: MMU user must log in before Stop TB data sync can proceed."); + } + 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/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index b0176fe8..e3da4ca8 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -17,6 +17,7 @@ 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,6 +37,9 @@ public class StopTBServiceImpl implements StopTBService { private final Logger logger = LoggerFactory.getLogger(StopTBServiceImpl.class); + @Autowired + private CampConfigService campConfigService; + @Autowired private BenFlowStatusRepo benFlowStatusRepo; @@ -281,12 +285,15 @@ private Map wrapWorklist(List> worklist) { @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"); StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegID(beneficiaryRegID); if (exam == null) exam = new StopTBGeneralExamination(); + boolean isNew = exam.getId() == null; exam.setBeneficiaryRegID(beneficiaryRegID); exam.setProviderServiceMapID(getInt(data, "providerServiceMapID")); @@ -314,8 +321,11 @@ public List> saveGeneralExamination(List exam.setModifiedBy(getString(data, "createdBy")); exam.setDeleted(false); exam.setReferralToHWCNeeded(getBool(data, "referralToHWCNeeded")); + if (exam.getVanID() == null) { exam.setVanID(vanID); exam.setParkingPlaceID(parkingPlaceID); } + exam.setProcessed("N"); generalExaminationRepo.save(exam); + if (isNew) generalExaminationRepo.updateVanSerialNo(exam.getId()); Map result = new HashMap<>(); result.put("beneficiaryRegID", beneficiaryRegID); @@ -392,12 +402,15 @@ private Map examToMap(StopTBGeneralExamination e) { @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"); TBScreening screening = tbScreeningRepo.findByBenRegID(beneficiaryRegID); if (screening == null) screening = new TBScreening(); + boolean isNew = screening.getId() == null; screening.setBenRegID(beneficiaryRegID); screening.setProviderServiceMapID(getInt(data, "providerServiceMapID")); @@ -426,8 +439,11 @@ public List> saveNurseTBScreening(List> Timestamp provided = getTimestamp(data, "visitDate"); screening.setVisitDate(provided != null ? provided : new Timestamp(System.currentTimeMillis())); } + if (screening.getVanID() == 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); @@ -492,12 +508,15 @@ private Map screeningToMap(TBScreening s) { @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"); StopTBGeneralOpd opd = generalOpdRepo.findByBenRegID(beneficiaryRegID); if (opd == null) opd = new StopTBGeneralOpd(); + boolean isNew = opd.getId() == null; opd.setBenRegID(beneficiaryRegID); opd.setProviderServiceMapID(getInt(data, "providerServiceMapID")); @@ -510,8 +529,11 @@ public List> saveGeneralOpd(List> dataLi opd.setCreatedBy(getString(data, "createdBy")); opd.setModifiedBy(getString(data, "createdBy")); opd.setDeleted(false); + if (opd.getVanID() == null) { opd.setVanID(vanID); opd.setParkingPlaceID(parkingPlaceID); } + opd.setProcessed("N"); generalOpdRepo.save(opd); + if (isNew) generalOpdRepo.updateVanSerialNo(opd.getId()); Map result = new HashMap<>(); result.put("beneficiaryRegID", beneficiaryRegID); @@ -564,12 +586,15 @@ private Map opdToMap(StopTBGeneralOpd o) { @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")); @@ -608,8 +633,11 @@ public List> saveDiagnostics(List> dataL diag.setCreatedBy(getString(data, "createdBy")); diag.setModifiedBy(getString(data, "createdBy")); diag.setDeleted(false); + if (diag.getVanID() == 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); 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 1876f5f5..487a729a 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java @@ -7,6 +7,7 @@ import com.iemr.flw.dto.iemr.TBConfirmedCasesResponseDTO; import com.iemr.flw.repo.iemr.TBConfirmedTreatmentRepository; import com.iemr.flw.service.IncentiveLogicService; +import com.iemr.flw.service.CampConfigService; import com.iemr.flw.service.TBConfirmedCaseService; import com.iemr.flw.utils.JwtUtil; import com.iemr.flw.utils.LocalDateAdapter; @@ -36,6 +37,9 @@ public class TBConfirmedCaseServiceImpl implements TBConfirmedCaseService { @Autowired private JwtUtil jwtUtil; + + @Autowired + private CampConfigService campConfigService; public TBConfirmedCaseServiceImpl(TBConfirmedTreatmentRepository repository) { this.repository = repository; } @@ -47,10 +51,13 @@ 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(); for(TBConfirmedCaseDTO dto:request){ List existing = repository.findByBenId(dto.getBenId()); - TBConfirmedCase entity = existing.isEmpty() ? new TBConfirmedCase() : existing.get(0); + boolean isNew = existing.isEmpty(); + TBConfirmedCase entity = isNew ? new TBConfirmedCase() : existing.get(0); entity.setBenId(dto.getBenId()); entity.setUserId(jwtUtil.extractUserId(authorisation)); entity.setModifiedBy(jwtUtil.extractUsername(authorisation)); @@ -68,10 +75,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) { 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); - } } 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 6ee3b47c..5bc71f07 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -8,6 +8,7 @@ import com.iemr.flw.dto.iemr.TBSuspectedRequestDTO; import com.iemr.flw.repo.iemr.TBSuspectedRepo; import com.iemr.flw.service.IncentiveLogicService; +import com.iemr.flw.service.CampConfigService; import com.iemr.flw.service.TBSuspectedService; import org.modelmapper.ModelMapper; import org.slf4j.Logger; @@ -25,6 +26,8 @@ public class TBSuspectedServiceImpl implements TBSuspectedService { private final ModelMapper modelMapper = new ModelMapper(); @Autowired private TBSuspectedRepo tbSuspectedRepo; + @Autowired + private CampConfigService campConfigService; @Autowired private IncentiveLogicService incentiveLogicService; @@ -36,8 +39,9 @@ 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()) { TBSuspected tbSuspected = tbSuspectedRepo.getByUserIdAndBenId(tbSuspectedDTO.getBenId(), requestDTO.getUserId()); @@ -52,7 +56,10 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { } tbSuspected.setUserId(requestDTO.getUserId()); + if (tbSuspected.getVanID() == 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,8 +67,7 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { } } - - }); + } return "no of tb suspected items saved:" + requestDTO.getTbSuspectedList().size(); } From 862e8fa6caf25a545ed337fb0ee00c3b476832f4 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 5 Jun 2026 11:48:52 +0530 Subject: [PATCH 37/97] =?UTF-8?q?fix:=20make=20vanID=20stamping=20conditio?= =?UTF-8?q?nal=20=E2=80=94=20skip=20gracefully=20when=20camp=20not=20confi?= =?UTF-8?q?gured?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When FLW-API is used without a Stop TB camp setup (no MMU login, no camp:vanID in Redis), getVanID() now returns null instead of throwing. All save methods guard with vanID != null before stamping, so regular FLW/ASHA usage is completely unaffected. Co-Authored-By: Claude Sonnet 4.6 --- src/main/java/com/iemr/flw/service/CampConfigService.java | 5 +---- .../java/com/iemr/flw/service/impl/StopTBServiceImpl.java | 8 ++++---- .../iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java | 2 +- .../com/iemr/flw/service/impl/TBSuspectedServiceImpl.java | 2 +- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/iemr/flw/service/CampConfigService.java b/src/main/java/com/iemr/flw/service/CampConfigService.java index 998d388c..44f19485 100644 --- a/src/main/java/com/iemr/flw/service/CampConfigService.java +++ b/src/main/java/com/iemr/flw/service/CampConfigService.java @@ -19,10 +19,7 @@ public class CampConfigService { public Integer getVanID() { String val = read(VAN_ID_KEY); - if (val == null || val.isBlank()) { - throw new IllegalStateException( - "Camp not configured: MMU user must log in before Stop TB data sync can proceed."); - } + if (val == null || val.isBlank()) return null; return Integer.parseInt(val); } diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index e3da4ca8..f203ae35 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -321,7 +321,7 @@ public List> saveGeneralExamination(List exam.setModifiedBy(getString(data, "createdBy")); exam.setDeleted(false); exam.setReferralToHWCNeeded(getBool(data, "referralToHWCNeeded")); - if (exam.getVanID() == null) { exam.setVanID(vanID); exam.setParkingPlaceID(parkingPlaceID); } + if (exam.getVanID() == null && vanID != null) { exam.setVanID(vanID); exam.setParkingPlaceID(parkingPlaceID); } exam.setProcessed("N"); generalExaminationRepo.save(exam); @@ -439,7 +439,7 @@ public List> saveNurseTBScreening(List> Timestamp provided = getTimestamp(data, "visitDate"); screening.setVisitDate(provided != null ? provided : new Timestamp(System.currentTimeMillis())); } - if (screening.getVanID() == null) { screening.setVanID(vanID); screening.setParkingPlaceID(parkingPlaceID); } + if (screening.getVanID() == null && vanID != null) { screening.setVanID(vanID); screening.setParkingPlaceID(parkingPlaceID); } screening.setProcessed("N"); tbScreeningRepo.save(screening); @@ -529,7 +529,7 @@ public List> saveGeneralOpd(List> dataLi opd.setCreatedBy(getString(data, "createdBy")); opd.setModifiedBy(getString(data, "createdBy")); opd.setDeleted(false); - if (opd.getVanID() == null) { opd.setVanID(vanID); opd.setParkingPlaceID(parkingPlaceID); } + if (opd.getVanID() == null && vanID != null) { opd.setVanID(vanID); opd.setParkingPlaceID(parkingPlaceID); } opd.setProcessed("N"); generalOpdRepo.save(opd); @@ -633,7 +633,7 @@ public List> saveDiagnostics(List> dataL diag.setCreatedBy(getString(data, "createdBy")); diag.setModifiedBy(getString(data, "createdBy")); diag.setDeleted(false); - if (diag.getVanID() == null) { diag.setVanID(vanID); diag.setParkingPlaceID(parkingPlaceID); } + if (diag.getVanID() == null && vanID != null) { diag.setVanID(vanID); diag.setParkingPlaceID(parkingPlaceID); } diag.setProcessed("N"); diagnosticsRepo.save(diag); 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 487a729a..3a25160e 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java @@ -75,7 +75,7 @@ public String save(List request, String authorisation) throw entity.setPlaceOfDeath(dto.getPlaceOfDeath()); entity.setReasonForDeath(dto.getReasonForDeath()); entity.setReasonForNotCompleting(dto.getReasonForNotCompleting()); - if (entity.getVanID() == null) { entity.setVanID(vanID); entity.setParkingPlaceID(parkingPlaceID); } + if (entity.getVanID() == null && vanID != null) { entity.setVanID(vanID); entity.setParkingPlaceID(parkingPlaceID); } entity.setProcessed("N"); if(entity!=null){ TBConfirmedCase saved = repository.save(entity); 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 5bc71f07..df86f228 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -56,7 +56,7 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { } tbSuspected.setUserId(requestDTO.getUserId()); - if (tbSuspected.getVanID() == null) { tbSuspected.setVanID(vanID); tbSuspected.setParkingPlaceID(parkingPlaceID); } + if (tbSuspected.getVanID() == null && vanID != null) { tbSuspected.setVanID(vanID); tbSuspected.setParkingPlaceID(parkingPlaceID); } tbSuspected.setProcessed("N"); tbSuspectedRepo.save(tbSuspected); tbSuspectedRepo.updateVanSerialNo(tbSuspected.getId()); From 05fe5f15c1ff0ce8347acd4d04570d29247fca1e Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 16 Jun 2026 16:32:05 +0530 Subject: [PATCH 38/97] feat(stoptb): add stoptb.enforce.vanid flag to fail loudly when camp not configured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CampConfigService.getVanID() previously returned null silently whenever camp:vanID was missing from Redis, letting Stop TB records save with vanID=NULL — these then never sync to central since the fetch query filters on vanID IS NOT NULL. When stoptb.enforce.vanid=true, the save now throws immediately so the error surfaces to the user instead of being discovered later as an un-synced record. --- src/main/environment/common_ci.properties | 3 +++ src/main/environment/common_docker.properties | 3 +++ src/main/environment/common_example.properties | 4 ++++ .../com/iemr/flw/service/CampConfigService.java | 13 ++++++++++++- 4 files changed, 22 insertions(+), 1 deletion(-) 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..b458371c 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -29,6 +29,10 @@ jwt.secret=my-32-character-ultra-secure-and-ultra-long-secret # 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/java/com/iemr/flw/service/CampConfigService.java b/src/main/java/com/iemr/flw/service/CampConfigService.java index 44f19485..a8c8ad35 100644 --- a/src/main/java/com/iemr/flw/service/CampConfigService.java +++ b/src/main/java/com/iemr/flw/service/CampConfigService.java @@ -3,6 +3,7 @@ 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; @@ -17,9 +18,19 @@ public class CampConfigService { @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()) return null; + 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); } From 0b969b672010b3ff684459ee5dfcd64edbb29192 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 17 Jun 2026 15:37:12 +0530 Subject: [PATCH 39/97] fix(stoptb): correct symptomatic/asymptomatic save and retrieve mapping Frontend sends a single "symptomatic" Yes/No value, but the DB has two separate columns (symptomatic, asymptomatic) with swapped semantics. Yes now stores asymptomatic=Yes (symptomatic=null), No stores symptomatic=Yes (asymptomatic=null). Retrieval derives the response "symptomatic" value back from whichever column is Yes, so it always echoes the original input. Co-Authored-By: Claude Sonnet 4.6 --- .../flw/service/impl/StopTBServiceImpl.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index f203ae35..b53f2c77 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -425,7 +425,17 @@ public List> saveNurseTBScreening(List> screening.setRiseOfFever(getBool(data, "riseOfFever")); screening.setLossOfAppetite(getBool(data, "lossOfAppetite")); screening.setContactWithTBPatient(getBool(data, "contactWithTBPatient")); - screening.setSympotomatic(getString(data, "symptomatic")); + 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")); @@ -488,7 +498,13 @@ private Map screeningToMap(TBScreening s) { m.put("riseOfFever", s.getRiseOfFever()); m.put("lossOfAppetite", s.getLossOfAppetite()); m.put("contactWithTBPatient", s.getContactWithTBPatient()); - m.put("symptomatic", s.getSympotomatic()); + 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()); From 165feda6d6cb8cd34b7dea11302fc9f42994c8a4 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 17 Jun 2026 17:27:24 +0530 Subject: [PATCH 40/97] fix sync issue --- .../java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java | 1 - 1 file changed, 1 deletion(-) 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 7095875e..cb2eabd3 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -253,7 +253,6 @@ 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()); From 4dc7b4d2a76ac00b0501d8df9823f14c3fbf381a Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Wed, 17 Jun 2026 20:12:13 +0530 Subject: [PATCH 41/97] feat(counselling): implement dynamic form management for TB counselling Introduces dynamic form definition management with versioning and response lifecycle support (draft, submit, complete, and bulk sync) for the TB counselling module. Includes: - Dynamic form and response APIs - QuestionType and ValidationType enums - TbCounsellingFormSeeder for initial form setup - JWT-based audit stamping (createdBy/updatedBy) - SecurityWhitelistConfig for endpoint-level authentication control --- .../flw/controller/DynamicFormController.java | 100 ++++ .../DynamicFormResponseController.java | 99 ++++ .../controller/GlobalExceptionHandler.java | 154 ++++++ .../com/iemr/flw/domain/iemr/DynamicForm.java | 107 +++++ .../iemr/flw/domain/iemr/FormResponse.java | 115 +++++ .../com/iemr/flw/domain/iemr/FormSection.java | 93 ++++ .../com/iemr/flw/domain/iemr/FormVersion.java | 94 ++++ .../iemr/flw/domain/iemr/OptionCondition.java | 80 ++++ .../iemr/flw/domain/iemr/QuestionOption.java | 81 ++++ .../flw/domain/iemr/QuestionResponse.java | 79 +++ .../flw/domain/iemr/QuestionValidation.java | 78 +++ .../iemr/flw/domain/iemr/SectionQuestion.java | 110 +++++ .../iemr/flw/domain/iemr/SectionResponse.java | 74 +++ .../com/iemr/flw/dto/iemr/DynamicFormDTO.java | 65 +++ .../iemr/flw/dto/iemr/FormResponseDTO.java | 57 +++ .../flw/dto/iemr/FormResponseRequest.java | 58 +++ .../com/iemr/flw/dto/iemr/FormSectionDTO.java | 62 +++ .../com/iemr/flw/dto/iemr/FormVersionDTO.java | 41 ++ .../iemr/flw/dto/iemr/OptionConditionDTO.java | 58 +++ .../flw/dto/iemr/QuestionAnswerRequest.java | 63 +++ .../iemr/flw/dto/iemr/QuestionOptionDTO.java | 55 +++ .../flw/dto/iemr/QuestionResponseDTO.java | 47 ++ .../flw/dto/iemr/QuestionValidationDTO.java | 49 ++ .../flw/dto/iemr/SectionAnswerRequest.java | 50 ++ .../iemr/flw/dto/iemr/SectionQuestionDTO.java | 74 +++ .../iemr/flw/dto/iemr/SectionResponseDTO.java | 48 ++ .../iemr/flw/mapper/DynamicFormMapper.java | 105 ++++ .../com/iemr/flw/masterEnum/FormType.java | 34 ++ .../com/iemr/flw/masterEnum/QuestionType.java | 43 ++ .../iemr/flw/masterEnum/ValidationType.java | 42 ++ .../iemr/flw/repo/iemr/DynamicFormRepo.java | 47 ++ .../iemr/flw/repo/iemr/FormResponseRepo.java | 54 +++ .../iemr/flw/repo/iemr/FormSectionRepo.java | 38 ++ .../iemr/flw/repo/iemr/FormVersionRepo.java | 46 ++ .../flw/repo/iemr/OptionConditionRepo.java | 55 +++ .../flw/repo/iemr/QuestionOptionRepo.java | 48 ++ .../flw/repo/iemr/QuestionResponseRepo.java | 46 ++ .../flw/repo/iemr/QuestionValidationRepo.java | 48 ++ .../flw/repo/iemr/SectionQuestionRepo.java | 48 ++ .../flw/repo/iemr/SectionResponseRepo.java | 47 ++ .../flw/seeder/TbCounsellingFormSeeder.java | 319 ++++++++++++ .../service/DynamicFormDefinitionService.java | 59 +++ .../service/DynamicFormResponseService.java | 61 +++ .../DynamicFormDefinitionServiceImpl.java | 416 ++++++++++++++++ .../impl/DynamicFormResponseServiceImpl.java | 453 ++++++++++++++++++ .../FormFollowUpNotificationScheduler.java | 110 +++++ .../service/impl/FormResponseItemSaver.java | 399 +++++++++++++++ 47 files changed, 4509 insertions(+) create mode 100644 src/main/java/com/iemr/flw/controller/DynamicFormController.java create mode 100644 src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java create mode 100644 src/main/java/com/iemr/flw/controller/GlobalExceptionHandler.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/FormResponse.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/FormSection.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/FormVersion.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java create mode 100644 src/main/java/com/iemr/flw/dto/iemr/DynamicFormDTO.java create mode 100644 src/main/java/com/iemr/flw/dto/iemr/FormResponseDTO.java create mode 100644 src/main/java/com/iemr/flw/dto/iemr/FormResponseRequest.java create mode 100644 src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java create mode 100644 src/main/java/com/iemr/flw/dto/iemr/FormVersionDTO.java create mode 100644 src/main/java/com/iemr/flw/dto/iemr/OptionConditionDTO.java create mode 100644 src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java create mode 100644 src/main/java/com/iemr/flw/dto/iemr/QuestionOptionDTO.java create mode 100644 src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java create mode 100644 src/main/java/com/iemr/flw/dto/iemr/QuestionValidationDTO.java create mode 100644 src/main/java/com/iemr/flw/dto/iemr/SectionAnswerRequest.java create mode 100644 src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java create mode 100644 src/main/java/com/iemr/flw/dto/iemr/SectionResponseDTO.java create mode 100644 src/main/java/com/iemr/flw/mapper/DynamicFormMapper.java create mode 100644 src/main/java/com/iemr/flw/masterEnum/FormType.java create mode 100644 src/main/java/com/iemr/flw/masterEnum/QuestionType.java create mode 100644 src/main/java/com/iemr/flw/masterEnum/ValidationType.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/DynamicFormRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/FormSectionRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/FormVersionRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/OptionConditionRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/QuestionOptionRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/QuestionResponseRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/QuestionValidationRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/SectionQuestionRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/SectionResponseRepo.java create mode 100644 src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java create mode 100644 src/main/java/com/iemr/flw/service/DynamicFormDefinitionService.java create mode 100644 src/main/java/com/iemr/flw/service/DynamicFormResponseService.java create mode 100644 src/main/java/com/iemr/flw/service/impl/DynamicFormDefinitionServiceImpl.java create mode 100644 src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java create mode 100644 src/main/java/com/iemr/flw/service/impl/FormFollowUpNotificationScheduler.java create mode 100644 src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java 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..82d5a79e --- /dev/null +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -0,0 +1,99 @@ +/* + * 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.service.DynamicFormResponseService; +import com.iemr.flw.utils.ApiResponse; +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( + @RequestParam Long responseId, + @Valid @RequestBody FormResponseRequest request) { + FormResponseDTO dto = responseService.completeForm(responseId, request); + 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("Authorization") String authorization) { + List saved = responseService.submitBulk(requests, authorization); + 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)); + } +} 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/domain/iemr/DynamicForm.java b/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java new file mode 100644 index 00000000..5ff86faf --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java @@ -0,0 +1,107 @@ +/* + * 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<>(); + + @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..7c0a09e0 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/FormResponse.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.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; + + @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..97f02763 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java @@ -0,0 +1,93 @@ +/* + * 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.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; + + /** 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<>(); +} 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..0e7ec4ae --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java @@ -0,0 +1,94 @@ +/* + * 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<>(); + + @PrePersist + protected void onCreate() { + createdAt = new Timestamp(System.currentTimeMillis()); + } +} 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..ae54b9ac --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java @@ -0,0 +1,80 @@ +/* + * 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; + +/** + * 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; +} 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..f0145523 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java @@ -0,0 +1,81 @@ +/* + * 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.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 = "optionValue", nullable = false, length = 100) + private String optionValue; + + @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<>(); +} 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..23c62ddc --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java @@ -0,0 +1,79 @@ +/* + * 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; + +/** + * 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; +} 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..eb533442 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java @@ -0,0 +1,78 @@ +/* + * 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; + +/** + * 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; +} 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..1e8581e7 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.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.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.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; + + @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<>(); +} 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..09fbae82 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java @@ -0,0 +1,74 @@ +/* + * 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; +} 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..4c537106 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java @@ -0,0 +1,62 @@ +/* + * 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; + + @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/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..bd78c6b4 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/QuestionOptionDTO.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.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; + + @NotBlank(message = "optionValue is required") + private String optionValue; + + @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..e19bbeb0 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java @@ -0,0 +1,74 @@ +/* + * 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; + + @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/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/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..b3c1d22f --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java @@ -0,0 +1,54 @@ +/* + * 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); + + List findByBeneficiaryId(Long beneficiaryId); + + @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/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/seeder/TbCounsellingFormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java new file mode 100644 index 00000000..d3a2c83e --- /dev/null +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java @@ -0,0 +1,319 @@ +/* + * 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 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 +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; + + public TbCounsellingFormSeeder(DynamicFormDefinitionService formService, + DynamicFormRepo formRepo) { + this.formService = formService; + this.formRepo = 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, false)); + qs.add(yesNoRadio("TB_C_Q2", "DOTS free treatment explained", 2, false)); + 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.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, false, 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", "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.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?", + 3, true)); + + // Side effects reported + qs.add(yesNoRadio("TB_F_Q3", + "Has the patient reported side effects to the treating doctor or DOTS centre?", + 4, true)); + + s.setQuestions(qs); + return s; + } + + // ── Builder Helpers ─────────────────────────────────────────────────────────── + + private FormSectionDTO section(String uuid, String name, String phase, + int order, boolean required, boolean hasSubmitButton) { + FormSectionDTO s = new FormSectionDTO(); + s.setSectionUuid(uuid); + s.setSectionName(name); + 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, int order, boolean mandatory) { + SectionQuestionDTO q = new SectionQuestionDTO(); + q.setQuestionUuid(uuid); + q.setQuestionText(text); + 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, int order, + boolean mandatory, int maxLength, boolean visible) { + SectionQuestionDTO q = new SectionQuestionDTO(); + q.setQuestionUuid(uuid); + q.setQuestionText(text); + 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 value, int order, + List conditions) { + QuestionOptionDTO o = new QuestionOptionDTO(); + o.setOptionLabel(label); + o.setOptionValue(value); + 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/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..c056aaec --- /dev/null +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -0,0 +1,61 @@ +/* + * 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 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(Long responseId, FormResponseRequest request); + + /** 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 authorization); + + /** + * 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); +} 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..f91099d8 --- /dev/null +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -0,0 +1,453 @@ +/* + * 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.DynamicFormResponseService; +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 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; + + @Override + @Transactional + public FormResponseDTO submitForm(FormResponseRequest request) { + 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); + } + return processSections(formResponse, request, version); + } + + @Override + @Transactional + public FormResponseDTO completeForm(Long responseId, FormResponseRequest request) { + FormResponse formResponse = formResponseRepo.findById(responseId) + .orElseThrow(() -> new NoSuchElementException("FormResponse not found: " + responseId)); + if (!STATUS_SUBMITTED.equals(formResponse.getStatus())) { + throw new IllegalStateException( + "Cannot complete a response in status: " + formResponse.getStatus()); + } + formResponse.setStatus(STATUS_COMPLETE); + formResponse.setCompletedAt(new Timestamp(System.currentTimeMillis())); + formResponse = formResponseRepo.save(formResponse); + + FormResponse finalFormResponse = formResponse; + FormVersion version = formVersionRepo.findById(formResponse.getVersionId()) + .orElseThrow(() -> new NoSuchElementException( + "FormVersion not found: " + finalFormResponse.getVersionId())); + return processSections(formResponse, request, version); + } + + @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) { + + 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)) + .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) { + + 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()); + + 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); + + questionResponseRepo.saveAll(questionResponses); + + sectionDTOs.add(buildSectionResponseDTO(sectionResponse, questionResponses)); + } + + return buildFormResponseDTO(formResponse, sectionDTOs); + } + + private SectionResponse upsertSectionResponse(Long responseId, Long sectionId) { + Optional existing = + sectionResponseRepo.findByResponseIdAndSectionId(responseId, sectionId); + if (existing.isPresent()) { + SectionResponse sr = existing.get(); + sr.setStatus(SECTION_STATUS_DONE); + sr.setSavedAt(new Timestamp(System.currentTimeMillis())); + return sectionResponseRepo.save(sr); + } + SectionResponse sr = SectionResponse.builder() + .responseId(responseId) + .sectionId(sectionId) + .status(SECTION_STATUS_DONE) + .savedAt(new Timestamp(System.currentTimeMillis())) + // TODO: set audit fields when auth is re-enabled for /dynamicForm/response endpoints + // .createdBy(jwtUtil.extractUsername(authorization)) + // .updatedBy(jwtUtil.extractUsername(authorization)) + .build(); + return sectionResponseRepo.save(sr); + } + + private List processAnswers( + Long sectionResponseId, + List answers, + Map questionMap, + Map> optionsByQuestion) { + + 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); + + // TODO: set .createdBy / .updatedBy on all QuestionResponse builders below + // when auth is re-enabled for /dynamicForm/response endpoints: + // .createdBy(jwtUtil.extractUsername(authorization)) + // .updatedBy(jwtUtil.extractUsername(authorization)) + 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) + .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) + .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) + .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 authorization) { + if (requests == null || requests.isEmpty()) return List.of(); + List results = new ArrayList<>(); + for (FormResponseRequest req : requests) { + try { + results.add(itemSaver.saveForBulk(req, authorization)); + } 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()); + } +} 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..82fff524 --- /dev/null +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -0,0 +1,399 @@ +/* + * 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.utils.JwtUtil; +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; + + /** + * 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 authorization) { + String actor = jwtUtil.extractUsername(authorization); + 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: Delete old answers — children first, then parents + List oldSections = sectionResponseRepo.findByResponseId(responseId); + if (!oldSections.isEmpty()) { + Set oldSrIds = oldSections.stream() + .map(SectionResponse::getSectionResponseId) + .collect(Collectors.toSet()); + questionResponseRepo.deleteBySectionResponseIdIn(oldSrIds); + sectionResponseRepo.deleteByResponseId(responseId); + } + + // Step 4: 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); + } + + // Step 5: Insert fresh sections and answers + return processSections(formResponse, req, version, actor); + } + + /** 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) { + 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) + .build(); + return formResponseRepo.save(formResponse); + } + + private FormResponseDTO processSections(FormResponse formResponse, FormResponseRequest req, FormVersion version, String actor) { + 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() + "'"); + } + + SectionResponse sectionResponse = upsertSectionResponse( + formResponse.getResponseId(), section.getSectionId(), actor); + + 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); + + questionResponseRepo.saveAll(questionResponses); + sectionDTOs.add(buildSectionResponseDTO(sectionResponse, questionResponses)); + } + + return buildFormResponseDTO(formResponse, sectionDTOs); + } + + private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor) { + 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); + 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) + .build(); + return sectionResponseRepo.save(sr); + } + + private List processAnswers( + Long sectionResponseId, + List answers, + Map questionMap, + Map> optionsByQuestion, + String actor) { + + 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) + .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) + .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) + .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(); + } +} From e6fbe834c1258e1b9909d8fa47ef5bf5af8f3ec8 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 18 Jun 2026 01:47:11 +0530 Subject: [PATCH 42/97] feat(stoptb): add per-visit history to General Examination, TB Screening, OPD, Suspected, Confirmed Every Stop TB clinical save previously upserted by beneficiaryRegID/benId alone, so a beneficiary's second camp visit silently overwrote their first - no history was ever kept. Adds a new tb_stoptb_visit table (mirrors MMU's t_benvisitdetail visitNo pattern, scoped entirely to Stop TB's own schema) and a getOrCreateVisitForToday() helper that infers "new visit" purely from the save date, with no mobile change required. - New TBStopVisit entity/repo/service: visitNo counting, role-handoff flags reserved for future Doctor/Counsellor screens, full sync field set (processed/vanSerialNo/SyncedDate/Syncedby/SyncFailureReason) matching the existing AMRIT-wide DataSyncRepository convention. - StopTBGeneralExamination/TBScreening/StopTBGeneralOpd/TBSuspected/ TBConfirmedCase now upsert keyed by (beneficiaryRegID, visitCode) instead of beneficiaryRegID alone. Reused the existing dormant visitCode column on 4 of 6 tables rather than adding a new one. - Fixed 4 read methods (getNurseWorklist, getGeneralExamination, getNurseTBScreening, getGeneralOpd) that would have thrown NonUniqueResultException once a beneficiary had more than one visit row - switched to findLatestBy... + PageRequest.of(0,1). - StopTBDiagnostics deliberately excluded: liquid culture results arrive 40-45 days after the original encounter and must update the same row, which visit-keying would have broken. Requires DB migration (not included): CREATE TABLE tb_stoptb_visit, plus ALTER TABLE tb_suspected/tb_confirmed_cases ADD COLUMN visitCode BIGINT. Co-Authored-By: Claude Sonnet 4.6 --- .../iemr/flw/domain/iemr/TBConfirmedCase.java | 3 + .../com/iemr/flw/domain/iemr/TBStopVisit.java | 77 +++++++++++++++++++ .../com/iemr/flw/domain/iemr/TBSuspected.java | 3 + .../iemr/StopTBGeneralExaminationRepo.java | 13 ++++ .../flw/repo/iemr/StopTBGeneralOpdRepo.java | 11 +++ .../iemr/TBConfirmedTreatmentRepository.java | 1 + .../iemr/flw/repo/iemr/TBScreeningRepo.java | 11 +++ .../iemr/flw/repo/iemr/TBStopVisitRepo.java | 29 +++++++ .../iemr/flw/repo/iemr/TBSuspectedRepo.java | 4 + .../iemr/flw/service/TBStopVisitService.java | 15 ++++ .../flw/service/impl/StopTBServiceImpl.java | 66 +++++++++++----- .../impl/TBConfirmedCaseServiceImpl.java | 19 ++++- .../service/impl/TBStopVisitServiceImpl.java | 47 +++++++++++ .../service/impl/TBSuspectedServiceImpl.java | 14 +++- 14 files changed, 289 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/iemr/flw/domain/iemr/TBStopVisit.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/TBStopVisitRepo.java create mode 100644 src/main/java/com/iemr/flw/service/TBStopVisitService.java create mode 100644 src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java 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 864d9506..c2c51a6f 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCase.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCase.java @@ -99,4 +99,7 @@ public class TBConfirmedCase { @Column(name = "created_by") private String createdBy; + + @Column(name = "visitCode") + private Long visitCode; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/TBStopVisit.java b/src/main/java/com/iemr/flw/domain/iemr/TBStopVisit.java new file mode 100644 index 00000000..8e7f0e20 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/TBStopVisit.java @@ -0,0 +1,77 @@ +package com.iemr.flw.domain.iemr; + +import lombok.Data; +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.sql.Timestamp; + +/** + * One row per real-world Stop TB camp encounter (a beneficiary seen on a given calendar day). + * Mirrors MMU's t_benvisitdetail (visitNo, insert-only history) but scoped entirely to Stop TB's + * own tables — never written to or read from MMU/HWC's shared visit/outreach infrastructure. + */ +@Entity +@Table(name = "tb_stoptb_visit", schema = "db_iemr") +@Data +public class TBStopVisit { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "beneficiary_reg_id") + private Long beneficiaryRegID; + + @Column(name = "visit_no") + private Integer visitNo; + + @CreationTimestamp + @Column(name = "visit_date", updatable = false) + private Timestamp visitDate; + + @Column(name = "provider_service_map_id") + private Integer providerServiceMapID; + + @Column(name = "created_by") + private String createdBy; + + // Role-handoff flags — only nurseCompletedFlag is populated today (Stop TB has no + // Doctor/Counsellor screens yet). Reserved so a future worklist can query against + // this table without a schema change later. + @Column(name = "nurse_completed_flag") + private String nurseCompletedFlag = "N"; + + @Column(name = "doctor_completed_flag") + private String doctorCompletedFlag = "N"; + + @Column(name = "counsellor_completed_flag") + private String counsellorCompletedFlag = "N"; + + // Sync fields — same vanID/vanSerialNo/processed discipline as every other Stop TB table, + // required so two camp laptops can each generate local visit rows without colliding at + // central (de-duplication key is (vanSerialNo, vanID), not the local auto-increment id). + @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; + + // Stamped by the generic local-to-central DataSyncRepository job (MMU/HWC/TM-API convention) + // when this row is actually pushed — required for tb_stoptb_visit to be syncable by that + // same generic mechanism without code changes there. + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby") + private String syncedby; + + @Column(name = "SyncFailureReason") + private String syncFailureReason; +} 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 5a5929a4..0c8d2fc4 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBSuspected.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBSuspected.java @@ -104,4 +104,7 @@ public class TBSuspected { @Column(name = "created_date") private Timestamp createdDate; + + @Column(name = "visitCode") + private Long visitCode; } diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java index b716f2e4..944e99a6 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralExaminationRepo.java @@ -1,6 +1,7 @@ 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; @@ -16,6 +17,18 @@ public interface StopTBGeneralExaminationRepo extends JpaRepository 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); diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java index 59d87a15..3cb8868f 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/StopTBGeneralOpdRepo.java @@ -1,6 +1,7 @@ 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; @@ -16,6 +17,16 @@ public interface StopTBGeneralOpdRepo extends JpaRepository 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); 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 21c4a158..21c941ff 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java @@ -17,6 +17,7 @@ public interface TBConfirmedTreatmentRepository List findByBenId(Long benId); List findByUserId(Integer benId); + List findByBenIdAndVisitCode(Long benId, Long visitCode); @Transactional @Modifying 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 cd875c90..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,6 +1,7 @@ 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; @@ -23,6 +24,16 @@ public interface TBScreeningRepo extends JpaRepository { @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); diff --git a/src/main/java/com/iemr/flw/repo/iemr/TBStopVisitRepo.java b/src/main/java/com/iemr/flw/repo/iemr/TBStopVisitRepo.java new file mode 100644 index 00000000..51791436 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/TBStopVisitRepo.java @@ -0,0 +1,29 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.TBStopVisit; +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; + +@Repository +public interface TBStopVisitRepo extends JpaRepository { + + @Query("SELECT v FROM TBStopVisit v WHERE v.beneficiaryRegID = :beneficiaryRegID " + + "AND v.visitDate BETWEEN :start AND :end") + TBStopVisit findByBeneficiaryRegIDAndVisitDateBetween(@Param("beneficiaryRegID") Long beneficiaryRegID, + @Param("start") Timestamp start, + @Param("end") Timestamp end); + + @Query("SELECT COUNT(v) FROM TBStopVisit v WHERE v.beneficiaryRegID = :beneficiaryRegID") + Integer getVisitCountForBeneficiary(@Param("beneficiaryRegID") Long beneficiaryRegID); + + @Transactional + @Modifying + @Query("UPDATE TBStopVisit 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 aae6d87a..ef3a2222 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java @@ -20,6 +20,10 @@ 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); 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..ec1d40e3 --- /dev/null +++ b/src/main/java/com/iemr/flw/service/TBStopVisitService.java @@ -0,0 +1,15 @@ +package com.iemr.flw.service; + +import com.iemr.flw.domain.iemr.TBStopVisit; + +public interface TBStopVisitService { + + /** + * Returns today's visit row for this beneficiary, creating one (with visitNo = previous + * count + 1) if none exists yet for the current calendar day. No mobile change required — + * "new visit" is inferred purely from the save timestamp, same pattern MMU uses + * (count existing visits, increment, insert-only — never update an existing visit row). + */ + TBStopVisit getOrCreateVisitForToday(Long beneficiaryRegID, Integer providerServiceMapID, String createdBy, + Integer vanID, Integer parkingPlaceID); +} diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index b53f2c77..37faa4b5 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -10,6 +10,7 @@ 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.domain.iemr.TBStopVisit; import com.iemr.flw.dto.iemr.StopTBRegistrationDto; import com.iemr.flw.repo.identity.BeneficiaryRepo; import com.iemr.flw.repo.iemr.BenFlowStatusRepo; @@ -19,10 +20,12 @@ 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.beans.factory.annotation.Value; +import org.springframework.data.domain.PageRequest; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -58,6 +61,9 @@ public class StopTBServiceImpl implements StopTBService { @Autowired private StopTBDiagnosticsRepo diagnosticsRepo; + @Autowired + private TBStopVisitService tbStopVisitService; + @Value("${tm-url}") private String tmUrl; @@ -188,14 +194,14 @@ public Map getNurseWorklist(StopTBRegistrationDto dto) throws Ex Map item = buildWorklistItem(flow); Long benRegID = flow.getBeneficiaryRegID(); - StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegID(benRegID); - item.put("generalExamination", exam != null ? examToMap(exam) : null); + List examList = generalExaminationRepo.findLatestByBeneficiaryRegID(benRegID, PageRequest.of(0, 1)); + item.put("generalExamination", !examList.isEmpty() ? examToMap(examList.get(0)) : null); - TBScreening screening = tbScreeningRepo.findByBenRegID(benRegID); - item.put("tbScreening", screening != null ? screeningToMap(screening) : null); + List screeningList = tbScreeningRepo.findLatestByBenRegID(benRegID, PageRequest.of(0, 1)); + item.put("tbScreening", !screeningList.isEmpty() ? screeningToMap(screeningList.get(0)) : null); - StopTBGeneralOpd opd = generalOpdRepo.findByBenRegID(benRegID); - item.put("generalOpd", opd != null ? opdToMap(opd) : null); + List opdList = generalOpdRepo.findLatestByBenRegID(benRegID, PageRequest.of(0, 1)); + item.put("generalOpd", !opdList.isEmpty() ? opdToMap(opdList.get(0)) : null); worklist.add(item); } @@ -291,12 +297,18 @@ public List> saveGeneralExamination(List Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); - StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegID(beneficiaryRegID); + Integer providerServiceMapID = getInt(data, "providerServiceMapID"); + String createdBy = getString(data, "createdBy"); + TBStopVisit visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, + createdBy, vanID, parkingPlaceID); + + StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegIDAndVisitCode(beneficiaryRegID, visit.getId()); if (exam == null) exam = new StopTBGeneralExamination(); boolean isNew = exam.getId() == null; exam.setBeneficiaryRegID(beneficiaryRegID); - exam.setProviderServiceMapID(getInt(data, "providerServiceMapID")); + exam.setVisitCode(visit.getId()); + exam.setProviderServiceMapID(providerServiceMapID); exam.setPulseRate(getInt(data, "pulseRate")); exam.setSystolicBP(getInt(data, "systolicBP")); exam.setDiastolicBP(getInt(data, "diastolicBP")); @@ -345,9 +357,9 @@ private boolean calculateReferralNeeded(StopTBGeneralExamination e) { @Override public Map getGeneralExamination(Long beneficiaryRegID) throws Exception { - StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegID(beneficiaryRegID); - if (exam == null) throw new Exception("No general examination found for beneficiaryRegID: " + beneficiaryRegID); - return examToMap(exam); + List examList = generalExaminationRepo.findLatestByBeneficiaryRegID(beneficiaryRegID, PageRequest.of(0, 1)); + if (examList.isEmpty()) throw new Exception("No general examination found for beneficiaryRegID: " + beneficiaryRegID); + return examToMap(examList.get(0)); } @Override @@ -408,12 +420,18 @@ public List> saveNurseTBScreening(List> Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); - TBScreening screening = tbScreeningRepo.findByBenRegID(beneficiaryRegID); + Integer providerServiceMapID = getInt(data, "providerServiceMapID"); + String createdBy = getString(data, "createdBy"); + TBStopVisit visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, + createdBy, vanID, parkingPlaceID); + + TBScreening screening = tbScreeningRepo.findByBenRegIDAndVisitCode(beneficiaryRegID, visit.getId()); if (screening == null) screening = new TBScreening(); boolean isNew = screening.getId() == null; screening.setBenRegID(beneficiaryRegID); - screening.setProviderServiceMapID(getInt(data, "providerServiceMapID")); + screening.setVisitCode(visit.getId()); + screening.setProviderServiceMapID(providerServiceMapID); screening.setCoughMoreThan2Weeks(getBool(data, "coughMoreThan2Weeks")); screening.setBloodInSputum(getBool(data, "bloodInSputum")); screening.setFeverMoreThan2Weeks(getBool(data, "feverMoreThan2Weeks")); @@ -464,9 +482,9 @@ public List> saveNurseTBScreening(List> @Override public Map getNurseTBScreening(Long beneficiaryRegID) throws Exception { - TBScreening screening = tbScreeningRepo.findByBenRegID(beneficiaryRegID); - if (screening == null) throw new Exception("No TB screening found for beneficiaryRegID: " + beneficiaryRegID); - return screeningToMap(screening); + List screeningList = tbScreeningRepo.findLatestByBenRegID(beneficiaryRegID, PageRequest.of(0, 1)); + if (screeningList.isEmpty()) throw new Exception("No TB screening found for beneficiaryRegID: " + beneficiaryRegID); + return screeningToMap(screeningList.get(0)); } @Override @@ -530,12 +548,18 @@ public List> saveGeneralOpd(List> dataLi Long beneficiaryRegID = getLong(data, "beneficiaryRegID"); if (beneficiaryRegID == null) throw new Exception("beneficiaryRegID is required"); - StopTBGeneralOpd opd = generalOpdRepo.findByBenRegID(beneficiaryRegID); + Integer providerServiceMapID = getInt(data, "providerServiceMapID"); + String createdBy = getString(data, "createdBy"); + TBStopVisit visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, + createdBy, vanID, parkingPlaceID); + + StopTBGeneralOpd opd = generalOpdRepo.findByBenRegIDAndVisitCode(beneficiaryRegID, visit.getId()); if (opd == null) opd = new StopTBGeneralOpd(); boolean isNew = opd.getId() == null; opd.setBenRegID(beneficiaryRegID); - opd.setProviderServiceMapID(getInt(data, "providerServiceMapID")); + opd.setVisitCode(visit.getId()); + opd.setProviderServiceMapID(providerServiceMapID); opd.setChiefComplaint(toJsonString(data.get("chiefComplaint"))); opd.setMedication(getString(data, "medication")); opd.setDosage(getString(data, "dosage")); @@ -560,9 +584,9 @@ public List> saveGeneralOpd(List> dataLi @Override public Map getGeneralOpd(Long beneficiaryRegID) throws Exception { - StopTBGeneralOpd opd = generalOpdRepo.findByBenRegID(beneficiaryRegID); - if (opd == null) throw new Exception("No general OPD record found for beneficiaryRegID: " + beneficiaryRegID); - return opdToMap(opd); + List opdList = generalOpdRepo.findLatestByBenRegID(beneficiaryRegID, PageRequest.of(0, 1)); + if (opdList.isEmpty()) throw new Exception("No general OPD record found for beneficiaryRegID: " + beneficiaryRegID); + return opdToMap(opdList.get(0)); } @Override 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 3a25160e..928ac5f0 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java @@ -4,11 +4,14 @@ import com.google.gson.GsonBuilder; import com.iemr.flw.domain.iemr.TBConfirmedCaseDTO; import com.iemr.flw.domain.iemr.TBConfirmedCase; +import com.iemr.flw.domain.iemr.TBStopVisit; import com.iemr.flw.dto.iemr.TBConfirmedCasesResponseDTO; +import com.iemr.flw.repo.identity.BeneficiaryRepo; import com.iemr.flw.repo.iemr.TBConfirmedTreatmentRepository; 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; @@ -40,6 +43,12 @@ public class TBConfirmedCaseServiceImpl implements TBConfirmedCaseService { @Autowired private CampConfigService campConfigService; + + @Autowired + private TBStopVisitService tbStopVisitService; + + @Autowired + private BeneficiaryRepo beneficiaryRepo; public TBConfirmedCaseServiceImpl(TBConfirmedTreatmentRepository repository) { this.repository = repository; } @@ -55,12 +64,18 @@ public String save(List request, String authorisation) throw Integer parkingPlaceID = campConfigService.getParkingPlaceID(); for(TBConfirmedCaseDTO dto:request){ - List existing = repository.findByBenId(dto.getBenId()); + String modifiedBy = jwtUtil.extractUsername(authorisation); + Long beneficiaryRegID = beneficiaryRepo.getRegIDFromBenId(dto.getBenId()); + TBStopVisit visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, null, + modifiedBy, vanID, parkingPlaceID); + + List existing = repository.findByBenIdAndVisitCode(dto.getBenId(), visit.getId()); boolean isNew = existing.isEmpty(); TBConfirmedCase entity = isNew ? new TBConfirmedCase() : existing.get(0); entity.setBenId(dto.getBenId()); + entity.setVisitCode(visit.getId()); entity.setUserId(jwtUtil.extractUserId(authorisation)); - entity.setModifiedBy(jwtUtil.extractUsername(authorisation)); + entity.setModifiedBy(modifiedBy); entity.setRegimenType(dto.getRegimenType()); entity.setTreatmentStartDate(dto.getTreatmentStartDate()); entity.setExpectedTreatmentCompletionDate(dto.getExpectedTreatmentCompletionDate()); 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..5907b678 --- /dev/null +++ b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java @@ -0,0 +1,47 @@ +package com.iemr.flw.service.impl; + +import com.iemr.flw.domain.iemr.TBStopVisit; +import com.iemr.flw.repo.iemr.TBStopVisitRepo; +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 TBStopVisitRepo tbStopVisitRepo; + + @Override + @Transactional + public TBStopVisit getOrCreateVisitForToday(Long beneficiaryRegID, Integer providerServiceMapID, String createdBy, + Integer vanID, Integer parkingPlaceID) { + LocalDate today = LocalDate.now(); + Timestamp todayStart = Timestamp.valueOf(today.atStartOfDay()); + Timestamp todayEnd = Timestamp.valueOf(today.plusDays(1).atStartOfDay()); + + TBStopVisit visit = tbStopVisitRepo.findByBeneficiaryRegIDAndVisitDateBetween(beneficiaryRegID, todayStart, todayEnd); + if (visit != null) { + return visit; + } + + Integer count = tbStopVisitRepo.getVisitCountForBeneficiary(beneficiaryRegID); + visit = new TBStopVisit(); + visit.setBeneficiaryRegID(beneficiaryRegID); + visit.setVisitNo(count != null ? count + 1 : 1); + visit.setProviderServiceMapID(providerServiceMapID); + visit.setCreatedBy(createdBy); + if (vanID != null) { + visit.setVanID(vanID); + visit.setParkingPlaceID(parkingPlaceID); + } + visit.setProcessed("N"); + visit = tbStopVisitRepo.save(visit); + tbStopVisitRepo.updateVanSerialNo(visit.getId()); + return visit; + } +} 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 df86f228..010651f4 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -2,13 +2,16 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.iemr.flw.domain.iemr.TBStopVisit; 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; @@ -28,6 +31,10 @@ public class TBSuspectedServiceImpl implements TBSuspectedService { private TBSuspectedRepo tbSuspectedRepo; @Autowired private CampConfigService campConfigService; + @Autowired + private TBStopVisitService tbStopVisitService; + @Autowired + private BeneficiaryRepo beneficiaryRepo; @Autowired private IncentiveLogicService incentiveLogicService; @@ -42,8 +49,12 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { Integer vanID = campConfigService.getVanID(); Integer parkingPlaceID = campConfigService.getParkingPlaceID(); for (TBSuspectedDTO tbSuspectedDTO : requestDTO.getTbSuspectedList()) { + Long beneficiaryRegID = beneficiaryRepo.getRegIDFromBenId(tbSuspectedDTO.getBenId()); + TBStopVisit 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.getId()); if (tbSuspected == null) { tbSuspected = new TBSuspected(); @@ -56,6 +67,7 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { } tbSuspected.setUserId(requestDTO.getUserId()); + tbSuspected.setVisitCode(visit.getId()); if (tbSuspected.getVanID() == null && vanID != null) { tbSuspected.setVanID(vanID); tbSuspected.setParkingPlaceID(parkingPlaceID); } tbSuspected.setProcessed("N"); tbSuspectedRepo.save(tbSuspected); From b5b8c68d9e6b12f6b0188971f03bb82f5b8f74b6 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 18 Jun 2026 01:52:45 +0530 Subject: [PATCH 43/97] Merge remote-tracking branch 'origin/vb/stoptb' into vb/stoptb --- .../java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java | 1 - 1 file changed, 1 deletion(-) 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 cb2eabd3..9a32fac3 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -256,7 +256,6 @@ private String getMappingsForAddressIDs(List addressLi } benBotnBirthRMNCH_ROBJ = beneficiaryRepo.getBornBirthByRegID((m.getBenRegId()).longValue()); - if (benDetailsRMNCH_OBJ != null && benDetailsRMNCH_OBJ.getHouseoldId() != null) if(!houseHoldRepo .getByHouseHoldID(benDetailsRMNCH_OBJ.getHouseoldId()).isEmpty()){ From 8e3f106481f0463775b3a23e3d73a2b48a4ef6fb Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 18 Jun 2026 10:41:16 +0530 Subject: [PATCH 44/97] chore(local-env): update local datasource/redis/jwt config and add dynamic form schema SQL Adds manual DDL/seed script for the dynamic form tables since ddl-auto wasn't creating them in local runs, and syncs local properties with the values needed to run submitBulk locally. Co-Authored-By: Claude Sonnet 4.6 --- dynamic_form_schema.sql | 173 ++++++++++++++++++ .../environment/common_example.properties | 4 +- 2 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 dynamic_form_schema.sql diff --git a/dynamic_form_schema.sql b/dynamic_form_schema.sql new file mode 100644 index 00000000..99e24b3b --- /dev/null +++ b/dynamic_form_schema.sql @@ -0,0 +1,173 @@ +-- Dynamic Form module schema for db_iemr +-- Run against your local MySQL: mysql -u root -p db_iemr < dynamic_form_schema.sql + +USE db_iemr; + +CREATE TABLE IF NOT EXISTS t_dynamic_form ( + formId BIGINT NOT NULL AUTO_INCREMENT, + formUuid VARCHAR(100) NOT NULL, + formName VARCHAR(255) NOT NULL, + formType VARCHAR(50) NOT NULL, + isActive BIT NOT NULL DEFAULT 1, + follow_up_delay_days INT NULL, + created_by VARCHAR(100) NULL, + updated_by VARCHAR(100) NULL, + createdAt DATETIME NOT NULL, + updatedAt DATETIME NOT NULL, + PRIMARY KEY (formId), + UNIQUE KEY uk_dynamic_form_uuid (formUuid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS t_form_version ( + versionId BIGINT NOT NULL AUTO_INCREMENT, + formId BIGINT NOT NULL, + versionNumber INT NOT NULL, + isLatest BIT NOT NULL DEFAULT 1, + createdAt DATETIME NOT NULL, + createdBy VARCHAR(100) NULL, + updated_by VARCHAR(100) NULL, + notes TEXT NULL, + PRIMARY KEY (versionId), + KEY idx_form_version_formId (formId), + CONSTRAINT fk_form_version_form FOREIGN KEY (formId) REFERENCES t_dynamic_form (formId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS t_form_section ( + sectionId BIGINT NOT NULL AUTO_INCREMENT, + version_id BIGINT NOT NULL, + sectionUuid VARCHAR(100) NOT NULL, + sectionName VARCHAR(255) NOT NULL, + sectionPhase VARCHAR(20) NOT NULL, + isRequired BIT NOT NULL DEFAULT 1, + displayOrder INT NOT NULL, + hasSubmitButton BIT NOT NULL DEFAULT 0, + created_by VARCHAR(100) NULL, + updated_by VARCHAR(100) NULL, + PRIMARY KEY (sectionId), + KEY idx_form_section_version (version_id), + CONSTRAINT fk_form_section_version FOREIGN KEY (version_id) REFERENCES t_form_version (versionId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS t_section_question ( + questionId BIGINT NOT NULL AUTO_INCREMENT, + sectionId BIGINT NOT NULL, + questionUuid VARCHAR(100) NOT NULL, + questionText TEXT NOT NULL, + questionType VARCHAR(20) NOT NULL, + isMandatory BIT NOT NULL DEFAULT 1, + displayOrder INT NOT NULL, + maxLength INT NULL, + defaultValue VARCHAR(500) NULL, + containsPii BIT NOT NULL DEFAULT 0, + created_by VARCHAR(100) NULL, + updated_by VARCHAR(100) NULL, + PRIMARY KEY (questionId), + KEY idx_section_question_sectionId (sectionId), + CONSTRAINT fk_section_question_section FOREIGN KEY (sectionId) REFERENCES t_form_section (sectionId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS t_question_option ( + optionId BIGINT NOT NULL AUTO_INCREMENT, + questionId BIGINT NOT NULL, + optionLabel VARCHAR(255) NOT NULL, + optionValue VARCHAR(100) NOT NULL, + displayOrder INT NOT NULL, + created_by VARCHAR(100) NULL, + updated_by VARCHAR(100) NULL, + PRIMARY KEY (optionId), + KEY idx_question_option_questionId (questionId), + CONSTRAINT fk_question_option_question FOREIGN KEY (questionId) REFERENCES t_section_question (questionId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS t_question_validation ( + validationId BIGINT NOT NULL AUTO_INCREMENT, + questionId BIGINT NOT NULL, + validationType VARCHAR(30) NOT NULL, + validationParam VARCHAR(255) NULL, + errorMessage VARCHAR(500) NOT NULL, + created_by VARCHAR(100) NULL, + updated_by VARCHAR(100) NULL, + PRIMARY KEY (validationId), + KEY idx_question_validation_questionId (questionId), + CONSTRAINT fk_question_validation_question FOREIGN KEY (questionId) REFERENCES t_section_question (questionId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS t_option_condition ( + conditionId BIGINT NOT NULL AUTO_INCREMENT, + optionId BIGINT NOT NULL, + actionType VARCHAR(40) NOT NULL, + targetQuestionId BIGINT NULL, + targetSectionId BIGINT NULL, + created_by VARCHAR(100) NULL, + updated_by VARCHAR(100) NULL, + PRIMARY KEY (conditionId), + KEY idx_option_condition_optionId (optionId), + CONSTRAINT fk_option_condition_option FOREIGN KEY (optionId) REFERENCES t_question_option (optionId), + CONSTRAINT fk_option_condition_target_question FOREIGN KEY (targetQuestionId) REFERENCES t_section_question (questionId), + CONSTRAINT fk_option_condition_target_section FOREIGN KEY (targetSectionId) REFERENCES t_form_section (sectionId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS t_form_response ( + responseId BIGINT NOT NULL AUTO_INCREMENT, + beneficiaryId BIGINT NOT NULL, + formId BIGINT NOT NULL, + versionId BIGINT NOT NULL, + officerId BIGINT NOT NULL, + status VARCHAR(20) NOT NULL, + submittedAt DATETIME NULL, + completedAt DATETIME NULL, + last_follow_up_at DATETIME NULL, + created_by VARCHAR(100) NULL, + updated_by VARCHAR(100) NULL, + createdAt DATETIME NOT NULL, + updatedAt DATETIME NOT NULL, + PRIMARY KEY (responseId), + KEY idx_form_response_ben_form (beneficiaryId, formId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS t_section_response ( + sectionResponseId BIGINT NOT NULL AUTO_INCREMENT, + responseId BIGINT NOT NULL, + sectionId BIGINT NOT NULL, + status VARCHAR(20) NOT NULL, + savedAt DATETIME NULL, + created_by VARCHAR(100) NULL, + updated_by VARCHAR(100) NULL, + PRIMARY KEY (sectionResponseId), + KEY idx_section_response_responseId (responseId), + CONSTRAINT fk_section_response_response FOREIGN KEY (responseId) REFERENCES t_form_response (responseId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS t_question_response ( + questionResponseId BIGINT NOT NULL AUTO_INCREMENT, + sectionResponseId BIGINT NOT NULL, + questionId BIGINT NOT NULL, + optionId BIGINT NULL, + answerText TEXT NULL, + created_by VARCHAR(100) NULL, + updated_by VARCHAR(100) NULL, + PRIMARY KEY (questionResponseId), + KEY idx_question_response_sectionResponseId (sectionResponseId), + CONSTRAINT fk_question_response_section_response FOREIGN KEY (sectionResponseId) REFERENCES t_section_response (sectionResponseId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ───────────────────────────────────────────────────────────── +-- Sample seed data: one form, one version, one section, two questions +-- ───────────────────────────────────────────────────────────── + +INSERT INTO t_dynamic_form (formUuid, formName, formType, isActive, created_by, updated_by, createdAt, updatedAt) +VALUES ('11111111-1111-1111-1111-111111111111', 'TB Counselling Form', 'COUNSELLING', 1, 'system', 'system', NOW(), NOW()); + +INSERT INTO t_form_version (formId, versionNumber, isLatest, createdAt, createdBy, updated_by, notes) +VALUES (LAST_INSERT_ID(), 1, 1, NOW(), 'system', 'system', 'initial version'); + +INSERT INTO t_form_section (version_id, sectionUuid, sectionName, sectionPhase, isRequired, displayOrder, hasSubmitButton, created_by, updated_by) +VALUES (LAST_INSERT_ID(), '22222222-2222-2222-2222-222222222222', 'Disease Awareness', 'PRE_SUBMIT', 1, 1, 1, 'system', 'system'); + +INSERT INTO t_section_question (sectionId, questionUuid, questionText, questionType, isMandatory, displayOrder, containsPii, created_by, updated_by) +VALUES (LAST_INSERT_ID(), '33333333-3333-3333-3333-333333333333', 'Has the beneficiary heard of TB before?', 'RADIO', 1, 1, 0, 'system', 'system'); + +INSERT INTO t_question_option (questionId, optionLabel, optionValue, displayOrder, created_by, updated_by) +VALUES +(LAST_INSERT_ID(), 'Yes', 'YES', 1, 'system', 'system'), +(LAST_INSERT_ID(), 'No', 'NO', 2, 'system', 'system'); diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index b458371c..7e8d43b0 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -24,7 +24,9 @@ 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 From ee2cf2313b652ff9b57f021b2fbcb9c1c28bb716 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 18 Jun 2026 10:44:38 +0530 Subject: [PATCH 45/97] chore(local-env): keep backup copy of common_local.properties Co-Authored-By: Claude Sonnet 4.6 --- .../environment/common_local.properties.bkp | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/main/environment/common_local.properties.bkp 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 From 2d483cb0c16427c6f2300149fda90efece6befd0 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 18 Jun 2026 14:31:47 +0530 Subject: [PATCH 46/97] token chnges --- .../iemr/flw/controller/DynamicFormResponseController.java | 4 ++-- .../com/iemr/flw/service/DynamicFormResponseService.java | 2 +- .../flw/service/impl/DynamicFormResponseServiceImpl.java | 4 ++-- .../com/iemr/flw/service/impl/FormResponseItemSaver.java | 5 +++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java index 82d5a79e..636c6bcf 100644 --- a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -74,8 +74,8 @@ public ResponseEntity completeForm( @RequestMapping(value = "/submitBulk", method = RequestMethod.POST) public ResponseEntity submitBulk( @Valid @RequestBody List requests, - @RequestHeader("Authorization") String authorization) { - List saved = responseService.submitBulk(requests, authorization); + @RequestHeader("JwtToken") String jwtToken) { + List saved = responseService.submitBulk(requests, jwtToken); return ResponseEntity.ok( new ApiResponse(true, "Bulk submit complete: " + saved.size() + " saved", saved)); } diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java index c056aaec..9e92f98e 100644 --- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -50,7 +50,7 @@ public interface DynamicFormResponseService { * 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 authorization); + List submitBulk(List requests, String jwtToken); /** * Returns SUBMITTED responses for the given form IDs whose {@code lastFollowUpAt} diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index f91099d8..c3649990 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -422,12 +422,12 @@ private SectionResponseDTO buildSectionResponseDTO( } @Override - public List submitBulk(List requests, String authorization) { + 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, authorization)); + results.add(itemSaver.saveForBulk(req, jwtToken)); } catch (Exception ex) { log.warn("submitBulk: item skipped for beneficiaryId={} — {}", req.getBeneficiaryId(), ex.getMessage()); diff --git a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java index 82fff524..08cb34ae 100644 --- a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -43,6 +43,7 @@ import com.iemr.flw.repo.iemr.SectionResponseRepo; import com.iemr.flw.masterEnum.QuestionType; 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; @@ -89,8 +90,8 @@ public class FormResponseItemSaver { * restoring the deleted rows automatically. */ @Transactional(propagation = Propagation.REQUIRES_NEW) - public FormResponseDTO saveForBulk(FormResponseRequest req, String authorization) { - String actor = jwtUtil.extractUsername(authorization); + public FormResponseDTO saveForBulk(FormResponseRequest req, String jwtToken) throws IEMRException { + String actor = jwtUtil.extractUserId(jwtToken).toString(); FormVersion version = resolveLatestVersion(req.getFormUuid()); Timestamp now = new Timestamp(System.currentTimeMillis()); Long formId = version.getDynamicForm().getFormId(); From 6cb34ef8f2f796d40dc723890abb37454c622e51 Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:09:23 +0530 Subject: [PATCH 47/97] Vb/counselling (#254) * Enforced mandatory validation for "Reason for Refusal in section e in file TbCounsellingFormSeeder * Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. --------- Co-authored-by: Sehjot Singh Pannu --- .../DynamicFormResponseController.java | 6 ++- .../flw/domain/iemr/TBConfirmedCaseDTO.java | 1 + .../iemr/flw/repo/iemr/FormResponseRepo.java | 8 ++++ .../flw/seeder/TbCounsellingFormSeeder.java | 2 +- .../service/DynamicFormResponseService.java | 3 +- .../impl/DynamicFormResponseServiceImpl.java | 40 ++++++++++++------- .../service/impl/FormResponseItemSaver.java | 20 +++++----- .../impl/TBConfirmedCaseServiceImpl.java | 30 +++++++++++++- 8 files changed, 78 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java index 636c6bcf..78a3dd83 100644 --- a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -25,6 +25,7 @@ import com.iemr.flw.dto.iemr.FormResponseRequest; 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; @@ -64,8 +65,9 @@ public ResponseEntity submitForm( @RequestMapping(value = "/complete", method = RequestMethod.POST) public ResponseEntity completeForm( @RequestParam Long responseId, - @Valid @RequestBody FormResponseRequest request) { - FormResponseDTO dto = responseService.completeForm(responseId, request); + @Valid @RequestBody FormResponseRequest request, + @RequestHeader("JwtToken") String jwtToken) throws IEMRException { + FormResponseDTO dto = responseService.completeForm(responseId, request, jwtToken); return ResponseEntity.ok(new ApiResponse(true, "Form completed successfully", dto)); } 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 07903035..7a521177 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCaseDTO.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCaseDTO.java @@ -29,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/repo/iemr/FormResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java index b3c1d22f..830cad08 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java @@ -42,6 +42,14 @@ public interface FormResponseRepo extends JpaRepository { List findByBeneficiaryId(Long beneficiaryId); + @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' " diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java index d3a2c83e..6c0a248c 100644 --- a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java @@ -185,7 +185,7 @@ private FormSectionDTO buildSectionE() { qs.add(statusQ); // Reason for refusal — hidden by default, max 300 chars - qs.add(textQuestion("TB_E_REFUSAL", "Reason for refusal", 2, false, 300, false)); + 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)); diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java index 9e92f98e..7a07f23f 100644 --- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -23,6 +23,7 @@ import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; +import com.iemr.flw.utils.exception.IEMRException; import java.util.List; @@ -37,7 +38,7 @@ public interface DynamicFormResponseService { FormResponseDTO submitForm(FormResponseRequest request); /** Save POST_SUBMIT section answers and advance status to COMPLETE. */ - FormResponseDTO completeForm(Long responseId, FormResponseRequest request); + FormResponseDTO completeForm(Long responseId, FormResponseRequest request, String jwtToken) throws IEMRException; /** All responses for a beneficiary filtered by form UUID. */ List getResponsesByBeneficiary(Long beneficiaryId, String formUuid); diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index c3649990..4d1df037 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -43,6 +43,8 @@ import com.iemr.flw.repo.iemr.SectionResponseRepo; import com.iemr.flw.masterEnum.QuestionType; 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; @@ -83,6 +85,7 @@ public class DynamicFormResponseServiceImpl implements DynamicFormResponseServic private final SectionQuestionRepo sectionQuestionRepo; private final QuestionOptionRepo questionOptionRepo; private final FormResponseItemSaver itemSaver; + private final JwtUtil jwtUtil; @Override @Transactional @@ -107,18 +110,20 @@ public FormResponseDTO submitForm(FormResponseRequest request) { } else { formResponse = createFormResponse(request, version, STATUS_SUBMITTED, now); } - return processSections(formResponse, request, version); + return processSections(formResponse, request, version, null); } @Override @Transactional - public FormResponseDTO completeForm(Long responseId, FormResponseRequest request) { + public FormResponseDTO completeForm(Long responseId, FormResponseRequest request, String jwtToken) throws IEMRException { + String actor = jwtUtil.extractUserId(jwtToken).toString(); FormResponse formResponse = formResponseRepo.findById(responseId) .orElseThrow(() -> new NoSuchElementException("FormResponse not found: " + responseId)); if (!STATUS_SUBMITTED.equals(formResponse.getStatus())) { throw new IllegalStateException( "Cannot complete a response in status: " + formResponse.getStatus()); } + formResponse.setUpdatedBy(actor); formResponse.setStatus(STATUS_COMPLETE); formResponse.setCompletedAt(new Timestamp(System.currentTimeMillis())); formResponse = formResponseRepo.save(formResponse); @@ -127,7 +132,7 @@ public FormResponseDTO completeForm(Long responseId, FormResponseRequest request FormVersion version = formVersionRepo.findById(formResponse.getVersionId()) .orElseThrow(() -> new NoSuchElementException( "FormVersion not found: " + finalFormResponse.getVersionId())); - return processSections(formResponse, request, version); + return processSections(formResponse, request, version, actor); } @Override @@ -187,7 +192,8 @@ private FormResponse createFormResponse( private FormResponseDTO processSections( FormResponse formResponse, FormResponseRequest request, - FormVersion version) { + FormVersion version, + String actor) { List allSections = formSectionRepo.findByFormVersion_VersionIdOrderByDisplayOrderAsc(version.getVersionId()); @@ -231,7 +237,7 @@ private FormResponseDTO processSections( } SectionResponse sectionResponse = upsertSectionResponse( - formResponse.getResponseId(), section.getSectionId()); + formResponse.getResponseId(), section.getSectionId(), actor); if ("POST_SUBMIT".equals(section.getSectionPhase())) { formResponse.setLastFollowUpAt(sectionResponse.getSavedAt()); @@ -245,7 +251,8 @@ private FormResponseDTO processSections( sectionResponse.getSectionResponseId(), sectionReq.getAnswers(), questionMap, - optionsByQuestion); + optionsByQuestion, + actor); questionResponseRepo.saveAll(questionResponses); @@ -255,13 +262,14 @@ private FormResponseDTO processSections( return buildFormResponseDTO(formResponse, sectionDTOs); } - private SectionResponse upsertSectionResponse(Long responseId, Long sectionId) { + private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor) { 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); return sectionResponseRepo.save(sr); } SectionResponse sr = SectionResponse.builder() @@ -269,9 +277,8 @@ private SectionResponse upsertSectionResponse(Long responseId, Long sectionId) { .sectionId(sectionId) .status(SECTION_STATUS_DONE) .savedAt(new Timestamp(System.currentTimeMillis())) - // TODO: set audit fields when auth is re-enabled for /dynamicForm/response endpoints - // .createdBy(jwtUtil.extractUsername(authorization)) - // .updatedBy(jwtUtil.extractUsername(authorization)) + .createdBy(actor) + .updatedBy(actor) .build(); return sectionResponseRepo.save(sr); } @@ -280,7 +287,8 @@ private List processAnswers( Long sectionResponseId, List answers, Map questionMap, - Map> optionsByQuestion) { + Map> optionsByQuestion, + String actor) { List results = new ArrayList<>(); @@ -305,10 +313,6 @@ private List processAnswers( // Delete any existing answers for this question in this section (handles re-saves) questionResponseRepo.deleteByQuestionIdAndSectionResponseId(questionId, sectionResponseId); - // TODO: set .createdBy / .updatedBy on all QuestionResponse builders below - // when auth is re-enabled for /dynamicForm/response endpoints: - // .createdBy(jwtUtil.extractUsername(authorization)) - // .updatedBy(jwtUtil.extractUsername(authorization)) if (type == QuestionType.RADIO) { if (answer.getOptionValue() != null) { QuestionOption opt = resolveOption( @@ -317,6 +321,8 @@ private List processAnswers( .sectionResponseId(sectionResponseId) .questionId(questionId) .optionId(opt != null ? opt.getOptionId() : null) + .createdBy(actor) + .updatedBy(actor) .build()); } } else if (type == QuestionType.MCQ) { @@ -328,6 +334,8 @@ private List processAnswers( .sectionResponseId(sectionResponseId) .questionId(questionId) .optionId(opt != null ? opt.getOptionId() : null) + .createdBy(actor) + .updatedBy(actor) .build()); } } @@ -340,6 +348,8 @@ private List processAnswers( .sectionResponseId(sectionResponseId) .questionId(questionId) .answerText(value) + .createdBy(actor) + .updatedBy(actor) .build()); } } diff --git a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java index 08cb34ae..0d844eb7 100644 --- a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -108,17 +108,7 @@ public FormResponseDTO saveForBulk(FormResponseRequest req, String jwtToken) thr formResponse = existing.get(0); Long responseId = formResponse.getResponseId(); - // Step 3: Delete old answers — children first, then parents - List oldSections = sectionResponseRepo.findByResponseId(responseId); - if (!oldSections.isEmpty()) { - Set oldSrIds = oldSections.stream() - .map(SectionResponse::getSectionResponseId) - .collect(Collectors.toSet()); - questionResponseRepo.deleteBySectionResponseIdIn(oldSrIds); - sectionResponseRepo.deleteByResponseId(responseId); - } - - // Step 4: Update FormResponse header + // Step 3: Update FormResponse header formResponse.setStatus(STATUS_SUBMITTED); formResponse.setSubmittedAt(now); formResponse.setUpdatedBy(actor); @@ -234,6 +224,14 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR "' 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); 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 928ac5f0..d749b44c 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,16 @@ 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.TBStopVisit; 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; @@ -22,9 +26,12 @@ import java.sql.Timestamp; import java.time.LocalDate; -import java.util.HashMap; +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 @@ -33,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; @@ -49,8 +58,13 @@ public class TBConfirmedCaseServiceImpl implements TBConfirmedCaseService { @Autowired private BeneficiaryRepo beneficiaryRepo; - public TBConfirmedCaseServiceImpl(TBConfirmedTreatmentRepository repository) { + + public TBConfirmedCaseServiceImpl(TBConfirmedTreatmentRepository repository, + FormResponseRepo formResponseRepo, + DynamicFormRepo dynamicFormRepo) { this.repository = repository; + this.formResponseRepo = formResponseRepo; + this.dynamicFormRepo = dynamicFormRepo; } @Override @@ -175,6 +189,18 @@ public String getByUserId(String authorisation) throws Exception { Integer userId = jwtUtil.extractUserId(authorisation); List list = repository.findByUserId(userId); 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); From 3a16e0de04370af9c14440c1c44723f5553dc022 Mon Sep 17 00:00:00 2001 From: sehjotsinghunthinkable Date: Tue, 23 Jun 2026 11:31:47 +0530 Subject: [PATCH 48/97] Vb/counselling (#255) * Enforced mandatory validation for "Reason for Refusal in section e in file TbCounsellingFormSeeder * Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. * Align `/complete` and `/submitBulk` form response editing behavior * Updated `/complete` to resolve `FormResponse` using `beneficiaryId` and `formUuid` from the request body, removing the dependency on `responseId` as a query parameter. * Updated `/submitBulk` section editing to match `/complete` behavior by performing in-place `SectionResponse` upserts and per-question delete-and-reinsert operations, instead of deleting and recreating the entire section. --- .../DynamicFormResponseController.java | 3 +-- .../service/DynamicFormResponseService.java | 2 +- .../impl/DynamicFormResponseServiceImpl.java | 24 ++++++++++--------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java index 78a3dd83..1261dcf2 100644 --- a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -64,10 +64,9 @@ public ResponseEntity submitForm( @Operation(summary = "Save POST_SUBMIT section answers, status → COMPLETE") @RequestMapping(value = "/complete", method = RequestMethod.POST) public ResponseEntity completeForm( - @RequestParam Long responseId, @Valid @RequestBody FormResponseRequest request, @RequestHeader("JwtToken") String jwtToken) throws IEMRException { - FormResponseDTO dto = responseService.completeForm(responseId, request, jwtToken); + FormResponseDTO dto = responseService.completeForm(request, jwtToken); return ResponseEntity.ok(new ApiResponse(true, "Form completed successfully", dto)); } diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java index 7a07f23f..6b27ade6 100644 --- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -38,7 +38,7 @@ public interface DynamicFormResponseService { FormResponseDTO submitForm(FormResponseRequest request); /** Save POST_SUBMIT section answers and advance status to COMPLETE. */ - FormResponseDTO completeForm(Long responseId, FormResponseRequest request, String jwtToken) throws IEMRException; + FormResponseDTO completeForm(FormResponseRequest request, String jwtToken) throws IEMRException; /** All responses for a beneficiary filtered by form UUID. */ List getResponsesByBeneficiary(Long beneficiaryId, String formUuid); diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index 4d1df037..a4d13079 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -115,23 +115,25 @@ public FormResponseDTO submitForm(FormResponseRequest request) { @Override @Transactional - public FormResponseDTO completeForm(Long responseId, FormResponseRequest request, String jwtToken) throws IEMRException { + public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken) throws IEMRException { String actor = jwtUtil.extractUserId(jwtToken).toString(); - FormResponse formResponse = formResponseRepo.findById(responseId) - .orElseThrow(() -> new NoSuchElementException("FormResponse not found: " + responseId)); - if (!STATUS_SUBMITTED.equals(formResponse.getStatus())) { - throw new IllegalStateException( - "Cannot complete a response in status: " + formResponse.getStatus()); + + FormVersion version = resolveLatestVersion(request.getFormUuid()); + Long formId = version.getDynamicForm().getFormId(); + + List existing = + formResponseRepo.findByBeneficiaryIdAndFormId(request.getBeneficiaryId(), formId); + if (existing.isEmpty()) { + throw new NoSuchElementException( + "FormResponse not found for beneficiaryId=" + request.getBeneficiaryId() + + " formUuid=" + request.getFormUuid()); } + FormResponse formResponse = existing.get(0); + formResponse.setUpdatedBy(actor); formResponse.setStatus(STATUS_COMPLETE); formResponse.setCompletedAt(new Timestamp(System.currentTimeMillis())); formResponse = formResponseRepo.save(formResponse); - - FormResponse finalFormResponse = formResponse; - FormVersion version = formVersionRepo.findById(formResponse.getVersionId()) - .orElseThrow(() -> new NoSuchElementException( - "FormVersion not found: " + finalFormResponse.getVersionId())); return processSections(formResponse, request, version, actor); } From 26599aa7468eed0a596524480b8120f130b67c69 Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:11:10 +0530 Subject: [PATCH 49/97] Vb/counselling (#256) * Enforced mandatory validation for "Reason for Refusal in section e in file TbCounsellingFormSeeder * Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. * Align `/complete` and `/submitBulk` form response editing behavior * Updated `/complete` to resolve `FormResponse` using `beneficiaryId` and `formUuid` from the request body, removing the dependency on `responseId` as a query parameter. * Updated `/submitBulk` section editing to match `/complete` behavior by performing in-place `SectionResponse` upserts and per-question delete-and-reinsert operations, instead of deleting and recreating the entire section. * Add van/sync tracking fields (vanID, parkingPlaceID, vanSerialNo, processed, syncedDate, syncedBy, syncFailureReason) to domain entities for offline mobile data collection support --------- Co-authored-by: Sehjot Singh Pannu --- .../com/iemr/flw/domain/iemr/DynamicForm.java | 21 +++++++++++++++++ .../iemr/flw/domain/iemr/FormResponse.java | 21 +++++++++++++++++ .../com/iemr/flw/domain/iemr/FormSection.java | 22 ++++++++++++++++++ .../com/iemr/flw/domain/iemr/FormVersion.java | 21 +++++++++++++++++ .../iemr/flw/domain/iemr/OptionCondition.java | 23 +++++++++++++++++++ .../iemr/flw/domain/iemr/QuestionOption.java | 22 ++++++++++++++++++ .../flw/domain/iemr/QuestionResponse.java | 23 +++++++++++++++++++ .../flw/domain/iemr/QuestionValidation.java | 23 +++++++++++++++++++ .../iemr/flw/domain/iemr/SectionQuestion.java | 22 ++++++++++++++++++ .../iemr/flw/domain/iemr/SectionResponse.java | 21 +++++++++++++++++ 10 files changed, 219 insertions(+) diff --git a/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java b/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java index 5ff86faf..9d969b7d 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java +++ b/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java @@ -94,6 +94,27 @@ public class DynamicForm { @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", length = 255) + private String syncFailureReason; + @PrePersist protected void onCreate() { createdAt = 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 index 7c0a09e0..14e0b891 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java @@ -99,6 +99,27 @@ public class FormResponse { @Column(name = "updatedAt", nullable = false) private Timestamp updatedAt; + @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", length = 255) + private String syncFailureReason; + @PrePersist protected void onCreate() { createdAt = 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 index 97f02763..d7078c06 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java @@ -37,6 +37,7 @@ import lombok.NoArgsConstructor; import lombok.ToString; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; @@ -90,4 +91,25 @@ public class FormSection { @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", length = 255) + 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 index 0e7ec4ae..437a29bc 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java @@ -87,6 +87,27 @@ public class FormVersion { @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", length = 255) + private String syncFailureReason; + @PrePersist protected void onCreate() { createdAt = new Timestamp(System.currentTimeMillis()); diff --git a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java index ae54b9ac..281a87d0 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java +++ b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java @@ -35,6 +35,8 @@ 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 @@ -77,4 +79,25 @@ public class OptionCondition { @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", length = 255) + private String syncFailureReason; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java b/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java index f0145523..4d6cbe19 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java @@ -37,6 +37,7 @@ import lombok.NoArgsConstructor; import lombok.ToString; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; @@ -78,4 +79,25 @@ public class QuestionOption { @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", length = 255) + 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 index 23c62ddc..871e31b5 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java @@ -33,6 +33,8 @@ 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). @@ -76,4 +78,25 @@ public class QuestionResponse { @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", length = 255) + 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 index eb533442..b3253526 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java @@ -38,6 +38,8 @@ 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 @@ -75,4 +77,25 @@ public class QuestionValidation { @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", length = 255) + 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 index 1e8581e7..0b571ea2 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java @@ -41,6 +41,7 @@ import lombok.NoArgsConstructor; import lombok.ToString; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; @@ -107,4 +108,25 @@ public class SectionQuestion { @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", length = 255) + 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 index 09fbae82..1fded4c8 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java @@ -71,4 +71,25 @@ public class SectionResponse { @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", length = 255) + private String syncFailureReason; } From c795640a34180c27fedf4ebc10f08c8c51bfa1b3 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 24 Jun 2026 17:51:34 +0530 Subject: [PATCH 50/97] fix(user-service): handle empty result from getDistrictByBlockId fallback Native query result must be mapped to List, not a bare Object[]; the previous signature caused getUserDetail() to silently skip the workingDistrictId fallback for block-only users. --- .../com/iemr/flw/repo/iemr/UserServiceRoleRepo.java | 2 +- .../com/iemr/flw/service/impl/UserServiceImpl.java | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) 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 90a50f17..f0960249 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/UserServiceRoleRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/UserServiceRoleRepo.java @@ -39,6 +39,6 @@ public interface UserServiceRoleRepo extends JpaRepository getDistrictByBlockId(@Param("blockId") Integer blockId); } 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 627df62a..e0e5b76b 100644 --- a/src/main/java/com/iemr/flw/service/impl/UserServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/UserServiceImpl.java @@ -24,10 +24,13 @@ public UserServiceRoleDTO getUserDetail(Integer userId) { UserServiceRoleDTO userRole = userServiceRoleRepo.getUserRole(userId).get(0); userRole.setFacilityData(facilityDataService.buildFacilityData(userId, userRole.getRoleName())); if (userRole.getWorkingDistrictId() == null && userRole.getBlockId() != null) { - Object[] district = userServiceRoleRepo.getDistrictByBlockId(userRole.getBlockId()); - if (district != null && district.length == 2) { - userRole.setWorkingDistrictId(((Number) district[0]).intValue()); - userRole.setWorkingDistrictName((String) district[1]); + 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]); + } } } return userRole; From bf2ea79d4e420f8e6c25341c10b0878786fc5ed7 Mon Sep 17 00:00:00 2001 From: sehjotsinghunthinkable Date: Thu, 25 Jun 2026 15:42:39 +0530 Subject: [PATCH 51/97] Vb/counselling (#257) * Enforced mandatory validation for "Reason for Refusal in section e in file TbCounsellingFormSeeder * Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. * Align `/complete` and `/submitBulk` form response editing behavior * Updated `/complete` to resolve `FormResponse` using `beneficiaryId` and `formUuid` from the request body, removing the dependency on `responseId` as a query parameter. * Updated `/submitBulk` section editing to match `/complete` behavior by performing in-place `SectionResponse` upserts and per-question delete-and-reinsert operations, instead of deleting and recreating the entire section. * Add van/sync tracking fields (vanID, parkingPlaceID, vanSerialNo, processed, syncedDate, syncedBy, syncFailureReason) to domain entities for offline mobile data collection support * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. --------- Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> --- .../DynamicFormResponseController.java | 7 ++ .../com/iemr/flw/domain/iemr/DynamicForm.java | 2 +- .../iemr/flw/domain/iemr/FormResponse.java | 2 +- .../com/iemr/flw/domain/iemr/FormSection.java | 5 +- .../com/iemr/flw/domain/iemr/FormVersion.java | 2 +- .../iemr/flw/domain/iemr/QuestionOption.java | 8 +- .../flw/domain/iemr/QuestionResponse.java | 2 +- .../flw/domain/iemr/QuestionValidation.java | 2 +- .../iemr/flw/domain/iemr/SectionQuestion.java | 5 +- .../iemr/flw/domain/iemr/SectionResponse.java | 2 +- .../com/iemr/flw/dto/iemr/FormSectionDTO.java | 2 + .../iemr/flw/dto/iemr/QuestionOptionDTO.java | 4 + .../iemr/flw/dto/iemr/SectionQuestionDTO.java | 2 + .../iemr/flw/repo/iemr/FormResponseRepo.java | 3 +- .../flw/seeder/TbCounsellingFormSeeder.java | 94 ++++++++++--------- .../service/DynamicFormResponseService.java | 4 + .../impl/DynamicFormResponseServiceImpl.java | 13 +++ 17 files changed, 105 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java index 1261dcf2..65dd7036 100644 --- a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -23,6 +23,7 @@ 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; @@ -97,4 +98,10 @@ 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") + @RequestMapping(value = "/getCompletedBeneficiaries", method = RequestMethod.GET) + public ResponseEntity getCompletedBeneficiaries(@RequestParam FormType formType) { + List beneficiaryIds = responseService.getCompletedBeneficiaries(formType); + return ResponseEntity.ok(new ApiResponse(true, "Completed beneficiaries fetched successfully", beneficiaryIds)); + } } diff --git a/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java b/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java index 9d969b7d..af183d66 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java +++ b/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java @@ -112,7 +112,7 @@ public class DynamicForm { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; @PrePersist diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java b/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java index 14e0b891..7462a561 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java @@ -117,7 +117,7 @@ public class FormResponse { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; @PrePersist diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java index d7078c06..b8454716 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java @@ -68,6 +68,9 @@ public class FormSection { @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; @@ -110,6 +113,6 @@ public class FormSection { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @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 index 437a29bc..5902aa21 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java @@ -105,7 +105,7 @@ public class FormVersion { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; @PrePersist diff --git a/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java b/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java index 4d6cbe19..2bc02cbd 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java @@ -64,9 +64,15 @@ public class QuestionOption { @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; @@ -98,6 +104,6 @@ public class QuestionOption { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @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 index 871e31b5..c01fc715 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java @@ -97,6 +97,6 @@ public class QuestionResponse { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @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 index b3253526..b7d41dc1 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java @@ -96,6 +96,6 @@ public class QuestionValidation { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @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 index 0b571ea2..1bd7991b 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java @@ -73,6 +73,9 @@ public class SectionQuestion { @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; @@ -127,6 +130,6 @@ public class SectionQuestion { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @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 index 1fded4c8..36996028 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java @@ -90,6 +90,6 @@ public class SectionResponse { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; } diff --git a/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java b/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java index 4c537106..2db1f8c7 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java @@ -47,6 +47,8 @@ public class FormSectionDTO { @NotBlank(message = "sectionName is required") private String sectionName; + private String sectionNameHindi; + @NotBlank(message = "sectionPhase is required (PRE_SUBMIT or POST_SUBMIT)") private String sectionPhase; diff --git a/src/main/java/com/iemr/flw/dto/iemr/QuestionOptionDTO.java b/src/main/java/com/iemr/flw/dto/iemr/QuestionOptionDTO.java index bd78c6b4..0328263c 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/QuestionOptionDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/QuestionOptionDTO.java @@ -44,9 +44,13 @@ public class QuestionOptionDTO { @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; diff --git a/src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java b/src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java index e19bbeb0..11a81430 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java @@ -49,6 +49,8 @@ public class SectionQuestionDTO { @NotBlank(message = "questionText is required") private String questionText; + private String questionTextHindi; + @NotNull(message = "questionType is required") private QuestionType questionType; diff --git a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java index 830cad08..02077c9f 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java @@ -40,7 +40,8 @@ public interface FormResponseRepo extends JpaRepository { List findByBeneficiaryIdAndFormId(Long beneficiaryId, Long formId); - List findByBeneficiaryId(Long beneficiaryId); + @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.beneficiaryId IN :benIds " + diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java index 6c0a248c..edb26b0f 100644 --- a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java @@ -33,6 +33,7 @@ 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; @@ -47,6 +48,7 @@ * @author Piramal Swasthya */ @Component +@RequiredArgsConstructor public class TbCounsellingFormSeeder { private static final Logger log = LoggerFactory.getLogger(TbCounsellingFormSeeder.class); @@ -56,12 +58,6 @@ public class TbCounsellingFormSeeder { private final DynamicFormDefinitionService formService; private final DynamicFormRepo formRepo; - public TbCounsellingFormSeeder(DynamicFormDefinitionService formService, - DynamicFormRepo formRepo) { - this.formService = formService; - this.formRepo = formRepo; - } - @PostConstruct public void seed() { if (formRepo.findByFormUuid(FORM_UUID).isPresent()) { @@ -97,13 +93,13 @@ private DynamicFormDTO buildFormDto() { // ── Section A: Disease Awareness ───────────────────────────────────────────── private FormSectionDTO buildSectionA() { - FormSectionDTO s = section("TB_SEC_A", "Disease Awareness", "PRE_SUBMIT", 1, true, false); + 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)); + 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; } @@ -111,15 +107,15 @@ private FormSectionDTO buildSectionA() { // ── 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); + 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)); + 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; } @@ -127,11 +123,11 @@ private FormSectionDTO buildSectionB() { // ── Section C: Government Schemes ──────────────────────────────────────────── private FormSectionDTO buildSectionC() { - FormSectionDTO s = section("TB_SEC_C", "Government Schemes", "PRE_SUBMIT", 3, false, false); + 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, false)); - qs.add(yesNoRadio("TB_C_Q2", "DOTS free treatment explained", 2, false)); - qs.add(textQuestion("TB_C_REMARKS", "Schemes notes", 3, false, 300, true)); + qs.add(yesNoRadio("TB_C_Q1", "Nikshay Poshan Yojana (NPY) eligibility explained", "निक्षय पोषण योजना (एनपीवाई) पात्रता के बारे में बताया गया", 1, false)); + qs.add(yesNoRadio("TB_C_Q2", "DOTS free treatment explained", "DOTS मुफ़्त इलाज के बारे में जानकारी", 2, false)); + qs.add(textQuestion("TB_C_REMARKS", "Schemes notes", "योजनाओं के नोट्स", 3, false, 300, true)); s.setQuestions(qs); return s; } @@ -139,13 +135,13 @@ private FormSectionDTO buildSectionC() { // ── Section D: Treatment Regimen ───────────────────────────────────────────── private FormSectionDTO buildSectionD() { - FormSectionDTO s = section("TB_SEC_D", "Treatment Regimen", "PRE_SUBMIT", 4, true, false); + 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)); + 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; } @@ -153,20 +149,21 @@ private FormSectionDTO buildSectionD() { // ── Section E: Counselling Completion ──────────────────────────────────────── private FormSectionDTO buildSectionE() { - FormSectionDTO s = section("TB_SEC_E", "Counselling Completion", "PRE_SUBMIT", 5, true, true); + 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()); + QuestionOptionDTO completeOpt = option("Complete", "सम्पूर्ण","COMPLETE", "सम्पूर्ण", 1, List.of()); // "Refused" option — 5 conditions: disable sections A-D + show refusal text box List refusedConditions = new ArrayList<>(); @@ -175,7 +172,7 @@ private FormSectionDTO buildSectionE() { 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); + QuestionOptionDTO refusedOpt = option("Refused", "अस्वीकार करना", "REFUSED", "अस्वीकार करना", 2, refusedConditions); List statusOptions = new ArrayList<>(); statusOptions.add(completeOpt); @@ -185,10 +182,10 @@ private FormSectionDTO buildSectionE() { 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)); + 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)); + qs.add(textQuestion("TB_E_REMARKS", "Counsellor remarks", "काउंसलर की टिप्पणी", 3, false, 500, true)); s.setQuestions(qs); return s; @@ -197,23 +194,24 @@ private FormSectionDTO buildSectionE() { // ── Section F: Follow Up to TU ─────────────────────────────────────────────── private FormSectionDTO buildSectionF() { - FormSectionDTO s = section("TB_SEC_F", "Follow Up to TU", "POST_SUBMIT", 6, true, true); + 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()); + 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); + QuestionOptionDTO noOpt = option("No", "नहीं", "NO", "नहीं", 2, noConditions); List startedOptions = new ArrayList<>(); startedOptions.add(yesOpt); @@ -225,16 +223,19 @@ private FormSectionDTO buildSectionF() { // 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); @@ -243,11 +244,12 @@ private FormSectionDTO buildSectionF() { // ── Builder Helpers ─────────────────────────────────────────────────────────── - private FormSectionDTO section(String uuid, String name, String phase, + 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); @@ -256,28 +258,30 @@ private FormSectionDTO section(String uuid, String name, String phase, return s; } - private SectionQuestionDTO yesNoRadio(String uuid, String text, int order, boolean mandatory) { + 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())); + 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, int order, + 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); @@ -293,11 +297,13 @@ private SectionQuestionDTO textQuestion(String uuid, String text, int order, return q; } - private QuestionOptionDTO option(String label, String value, int order, + 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; diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java index 6b27ade6..436cae75 100644 --- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -23,6 +23,7 @@ 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; @@ -59,4 +60,7 @@ public interface DynamicFormResponseService { * Used by the follow-up notification scheduler. */ List findPendingFollowUps(List formIds, int delayDays); + + /** Returns beneficiary IDs with COMPLETE status for the given form type. */ + List getCompletedBeneficiaries(FormType formType); } diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index a4d13079..8c8cc6ea 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -34,6 +34,9 @@ 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; @@ -77,6 +80,7 @@ public class DynamicFormResponseServiceImpl implements DynamicFormResponseServic 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; @@ -462,4 +466,13 @@ public List findPendingFollowUps(List formIds, int delayD .map(r -> buildFormResponseDTO(r, List.of())) .collect(Collectors.toList()); } + + @Override + @Transactional(readOnly = true) + public List getCompletedBeneficiaries(FormType formType) { + DynamicForm form = dynamicFormRepo.findByFormTypeAndIsActive(formType, true) + .orElseThrow(() -> new NoSuchElementException( + "No active form found for type: " + formType)); + return formResponseRepo.findBeneficiaryIdsByFormIdAndStatus(form.getFormId(), STATUS_COMPLETE); + } } From e0aae0a6a6f18154e8bdbf1551e48f6794707a52 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 25 Jun 2026 18:44:20 +0530 Subject: [PATCH 52/97] fix(dynamic-form): default processed to N via @Builder.Default Lombok @Builder ignores field initializers, so FormResponse, QuestionResponse, and SectionResponse were inserting NULL into the processed column instead of the intended "N" default, causing "Column 'processed' cannot be null" on submitBulk. Co-Authored-By: Claude Sonnet 4.6 --- src/main/java/com/iemr/flw/domain/iemr/FormResponse.java | 1 + src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java | 1 + src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java | 1 + 3 files changed, 3 insertions(+) diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java b/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java index 7462a561..8b338ee5 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java @@ -106,6 +106,7 @@ public class FormResponse { private Integer parkingPlaceID; @Column(name = "processed") + @Builder.Default private String processed = "N"; @Column(name = "vanSerialNo") diff --git a/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java b/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java index c01fc715..aabfc877 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java @@ -86,6 +86,7 @@ public class QuestionResponse { private Integer parkingPlaceID; @Column(name = "processed") + @Builder.Default private String processed = "N"; @Column(name = "vanSerialNo") diff --git a/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java b/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java index 36996028..d8877a50 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java @@ -79,6 +79,7 @@ public class SectionResponse { private Integer parkingPlaceID; @Column(name = "processed") + @Builder.Default private String processed = "N"; @Column(name = "vanSerialNo") From 7af425cdae8a8a767ed255c985b8f5ba39d3d0a8 Mon Sep 17 00:00:00 2001 From: sehjotsinghunthinkable Date: Fri, 26 Jun 2026 13:56:38 +0530 Subject: [PATCH 53/97] Vb/counselling (#258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enforced mandatory validation for "Reason for Refusal in section e in file TbCounsellingFormSeeder * Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. * Align `/complete` and `/submitBulk` form response editing behavior * Updated `/complete` to resolve `FormResponse` using `beneficiaryId` and `formUuid` from the request body, removing the dependency on `responseId` as a query parameter. * Updated `/submitBulk` section editing to match `/complete` behavior by performing in-place `SectionResponse` upserts and per-question delete-and-reinsert operations, instead of deleting and recreating the entire section. * Add van/sync tracking fields (vanID, parkingPlaceID, vanSerialNo, processed, syncedDate, syncedBy, syncFailureReason) to domain entities for offline mobile data collection support * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(beneficiary): expose GPS coordinates in getBeneficiaryData response Add gpsLatitude, gpsLongitude, digipin, gpsTimestamp, and isGpsUnavailable fields to RMNCHBeneficiaryDetailsRmnch and RMNCHHouseHoldDetails entities, mapping to the columns written by Identity-API's /syncDataToAmrit endpoint. Enable serializeNulls on the Gson builder in BeneficiaryServiceImpl so GPS keys are always present in householdDetails and beneficiaryDetails — null for web-registered records, real values for mobile-registered ones. Co-Authored-By: Claude Sonnet 4.6 * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Revert "Committed 82b00ccf with only the three files:" This reverts commit 3040a08077d82d27dfd86f29f0fbdd4dc094ec56. * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields --------- Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- .../RMNCHBeneficiaryDetailsRmnch.java | 18 +++++++++++++++++ .../identity/RMNCHHouseHoldDetails.java | 8 ++++++++ .../identity/RMNCHMBeneficiaryaddress.java | 15 ++++++++++++++ .../iemr/flw/domain/iemr/OptionCondition.java | 2 +- .../service/impl/BeneficiaryServiceImpl.java | 20 +++++++++++++++++++ 5 files changed, 62 insertions(+), 1 deletion(-) 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 e4b634ff..84827388 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,24 @@ 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 = "menstrualBFDId") private Integer menstrualBFDId; 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..ea58143d 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,19 @@ 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; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java index 281a87d0..50af2b38 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java +++ b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java @@ -98,6 +98,6 @@ public class OptionCondition { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; } 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 9a32fac3..09f2a0bb 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; @@ -331,6 +332,25 @@ private String getMappingsForAddressIDs(List addressLi if (benAddressOBJ.getPermAddrLine3() != null) benDetailsRMNCH_OBJ.setAddressLine3(benAddressOBJ.getPermAddrLine3()); + // 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()); + + // 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())); + // ----------------------------------------------------------------------------- // related benids From b8083bbf3990368ff58578dc69aafea10ce0c001 Mon Sep 17 00:00:00 2001 From: sehjotsinghunthinkable Date: Fri, 26 Jun 2026 17:15:23 +0530 Subject: [PATCH 54/97] Vb/counselling (#259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enforced mandatory validation for "Reason for Refusal in section e in file TbCounsellingFormSeeder * Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. * Align `/complete` and `/submitBulk` form response editing behavior * Updated `/complete` to resolve `FormResponse` using `beneficiaryId` and `formUuid` from the request body, removing the dependency on `responseId` as a query parameter. * Updated `/submitBulk` section editing to match `/complete` behavior by performing in-place `SectionResponse` upserts and per-question delete-and-reinsert operations, instead of deleting and recreating the entire section. * Add van/sync tracking fields (vanID, parkingPlaceID, vanSerialNo, processed, syncedDate, syncedBy, syncFailureReason) to domain entities for offline mobile data collection support * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(beneficiary): expose GPS coordinates in getBeneficiaryData response Add gpsLatitude, gpsLongitude, digipin, gpsTimestamp, and isGpsUnavailable fields to RMNCHBeneficiaryDetailsRmnch and RMNCHHouseHoldDetails entities, mapping to the columns written by Identity-API's /syncDataToAmrit endpoint. Enable serializeNulls on the Gson builder in BeneficiaryServiceImpl so GPS keys are always present in householdDetails and beneficiaryDetails — null for web-registered records, real values for mobile-registered ones. Co-Authored-By: Claude Sonnet 4.6 * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Revert "Committed 82b00ccf with only the three files:" This reverts commit 3040a08077d82d27dfd86f29f0fbdd4dc094ec56. * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields # Conflicts: # src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java --------- Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- .../flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java | 4 ++++ .../iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java | 3 +++ .../com/iemr/flw/service/impl/BeneficiaryServiceImpl.java | 2 ++ 3 files changed, 9 insertions(+) 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 84827388..dca4f5ee 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java @@ -120,6 +120,10 @@ public class RMNCHBeneficiaryDetailsRmnch { @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; 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 ea58143d..a589ec7a 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java @@ -262,4 +262,7 @@ public class RMNCHMBeneficiaryaddress implements Serializable { @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/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index 09f2a0bb..b594a3b4 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -344,6 +344,8 @@ private String getMappingsForAddressIDs(List addressLi 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) From 454f7bf88144408aef90b29c329ec5655849f86f Mon Sep 17 00:00:00 2001 From: sehjotsinghunthinkable Date: Tue, 30 Jun 2026 12:16:55 +0530 Subject: [PATCH 55/97] Vb/counselling (#261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enforced mandatory validation for "Reason for Refusal in section e in file TbCounsellingFormSeeder * Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. * Align `/complete` and `/submitBulk` form response editing behavior * Updated `/complete` to resolve `FormResponse` using `beneficiaryId` and `formUuid` from the request body, removing the dependency on `responseId` as a query parameter. * Updated `/submitBulk` section editing to match `/complete` behavior by performing in-place `SectionResponse` upserts and per-question delete-and-reinsert operations, instead of deleting and recreating the entire section. * Add van/sync tracking fields (vanID, parkingPlaceID, vanSerialNo, processed, syncedDate, syncedBy, syncFailureReason) to domain entities for offline mobile data collection support * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(beneficiary): expose GPS coordinates in getBeneficiaryData response Add gpsLatitude, gpsLongitude, digipin, gpsTimestamp, and isGpsUnavailable fields to RMNCHBeneficiaryDetailsRmnch and RMNCHHouseHoldDetails entities, mapping to the columns written by Identity-API's /syncDataToAmrit endpoint. Enable serializeNulls on the Gson builder in BeneficiaryServiceImpl so GPS keys are always present in householdDetails and beneficiaryDetails — null for web-registered records, real values for mobile-registered ones. Co-Authored-By: Claude Sonnet 4.6 * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Revert "Committed 82b00ccf with only the three files:" This reverts commit 3040a08077d82d27dfd86f29f0fbdd4dc094ec56. * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields # Conflicts: # src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java * Updated isMandatory field in Section C, so changed in TbCounsellingFormSeeder --------- Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- .../java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java index edb26b0f..0e956c9c 100644 --- a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java @@ -125,8 +125,8 @@ private FormSectionDTO buildSectionB() { 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, false)); - qs.add(yesNoRadio("TB_C_Q2", "DOTS free treatment explained", "DOTS मुफ़्त इलाज के बारे में जानकारी", 2, false)); + 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; From 5ca325a4ef33c6fcf7bec9e2eacc6018f350623e Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 30 Jun 2026 13:37:45 +0530 Subject: [PATCH 56/97] feat(stoptb): dual-write clinical data to AMRIT standard tables via t_benvisitdetail - Replace tb_stoptb_visit with t_benvisitdetail (MMU-compatible visitCode formula) - Dual-write exam data to t_phy_anthropometry, t_phy_vitals, t_Phy_GeneralExam, t_benreferdetails - Dual-write OPD data to t_benchiefcomplaint, t_prescription, t_prescribeddrug - Add 6 new JPA entities and repos for standard tables (db_iemr schema) - Update TBSuspectedServiceImpl and TBConfirmedCaseServiceImpl to use BenVisitDetail - Fix vanID/parkingPlaceID not being stored in t_form_response, t_section_response, t_question_response - Delete orphaned TBStopVisit entity and TBStopVisitRepo Co-Authored-By: Claude Sonnet 4.6 --- StopTB_Standard_Table_Integration.md | 294 ++++++++++++++++++ .../domain/iemr/BenAnthropometryDetail.java | 70 +++++ .../flw/domain/iemr/BenChiefComplaint.java | 73 +++++ .../domain/iemr/BenPhysicalVitalDetail.java | 76 +++++ .../domain/iemr/PhyGeneralExamination.java | 79 +++++ .../flw/domain/iemr/PrescribedDrugDetail.java | 82 +++++ .../flw/domain/iemr/PrescriptionDetail.java | 70 +++++ .../com/iemr/flw/domain/iemr/TBStopVisit.java | 77 ----- .../flw/repo/iemr/BenAnthropometryRepo.java | 9 + .../flw/repo/iemr/BenChiefComplaintRepo.java | 9 + .../flw/repo/iemr/BenPhysicalVitalRepo.java | 9 + .../flw/repo/iemr/BenVisitDetailsRepo.java | 18 +- .../repo/iemr/PhyGeneralExaminationRepo.java | 9 + .../repo/iemr/PrescribedDrugDetailRepo.java | 9 + .../flw/repo/iemr/PrescriptionDetailRepo.java | 9 + .../iemr/flw/repo/iemr/TBStopVisitRepo.java | 29 -- .../iemr/flw/service/TBStopVisitService.java | 12 +- .../impl/DynamicFormResponseServiceImpl.java | 44 ++- .../service/impl/FormResponseItemSaver.java | 38 ++- .../flw/service/impl/StopTBServiceImpl.java | 250 ++++++++++++++- .../impl/TBConfirmedCaseServiceImpl.java | 8 +- .../service/impl/TBStopVisitServiceImpl.java | 59 ++-- .../service/impl/TBSuspectedServiceImpl.java | 8 +- 23 files changed, 1164 insertions(+), 177 deletions(-) create mode 100644 StopTB_Standard_Table_Integration.md create mode 100644 src/main/java/com/iemr/flw/domain/iemr/BenAnthropometryDetail.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/BenChiefComplaint.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/BenPhysicalVitalDetail.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/PhyGeneralExamination.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/PrescribedDrugDetail.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/PrescriptionDetail.java delete mode 100644 src/main/java/com/iemr/flw/domain/iemr/TBStopVisit.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/BenChiefComplaintRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/PhyGeneralExaminationRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/PrescribedDrugDetailRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/PrescriptionDetailRepo.java delete mode 100644 src/main/java/com/iemr/flw/repo/iemr/TBStopVisitRepo.java diff --git a/StopTB_Standard_Table_Integration.md b/StopTB_Standard_Table_Integration.md new file mode 100644 index 00000000..9df8b37d --- /dev/null +++ b/StopTB_Standard_Table_Integration.md @@ -0,0 +1,294 @@ +# Stop TB — Standard Table Integration + +**Branch:** `vb/counselling` +**Date:** 2026-06-30 +**Author:** vishwab1 + +--- + +## 1. Why this was done + +Stop TB's clinical data was siloed entirely inside custom `tb_stoptb_*` tables. No other AMRIT +service (HWC, MMU) could see a Stop TB beneficiary's record — referrals, vitals, prescriptions +were invisible across service lines. + +This integration dual-writes Stop TB clinical data into the same standard tables that MMU/HWC +already use, making beneficiary records portable across services without any change to the +mobile app. + +--- + +## 2. What was implemented + +### 2.1 Visit layer — `t_benvisitdetail` + +**Problem:** Stop TB used its own `tb_stoptb_visit` mirror table. `benVisitID` was null in all +clinical tables. HWC/MMU had no visit anchor to join on. + +**Fix:** `TBStopVisitServiceImpl` was completely rewritten to create rows in `t_benvisitdetail` +instead of `tb_stoptb_visit`. The old entity and repo were deleted. + +**Key behaviour (get-or-create-for-today):** +- First save of the day → creates a new `t_benvisitdetail` row +- Subsequent saves same day → finds and reuses the same row +- Next camp day → creates a new row (separate visit, history preserved) + +**VisitCode formula (same as MMU):** +``` +visitCode = "1" + vanID(zero-padded 5 digits) + benVisitId(zero-padded 8 digits) +Example: vanID=1, benVisitId=42 → 10000100000042 +``` + +**Files changed:** +- `TBStopVisitService.java` — interface return type changed to `BenVisitDetail` +- `TBStopVisitServiceImpl.java` — full rewrite, uses `BenVisitDetailsRepo` +- `BenVisitDetailsRepo.java` — added `findStopTBVisitForToday()` and `countStopTBVisits()` +- `TBStopVisit.java` — **DELETED** (orphaned) +- `TBStopVisitRepo.java` — **DELETED** (orphaned) + +--- + +### 2.2 Standard table dual-writes + +All dual-writes happen inside `StopTBServiceImpl`. Custom `tb_stoptb_*` tables continue to +be written unchanged. Standard table writes are **additive and insert-only** (no edit +capability exists in the mobile). + +Each write is wrapped in its own try/catch — a standard-table failure never breaks the +primary Stop TB save. + +#### `saveGeneralExamination()` writes to: + +| Standard Table | Data source | Fields written | +|---|---|---| +| `t_phy_anthropometry` | `i_beneficiarydetails.otherFields` (registration) | `Height_cm`, `Weight_Kg`, `BMI` | +| `t_phy_vitals` | otherFields + request | `Temperature` (from otherFields), `PulseRate`, `SystolicBP_1stReading`, `DiastolicBP_1stReading`, `BloodGlucose_Random` | +| `t_Phy_GeneralExam` | request | `Pallor`, `Jaundice` (←icterus), `Cyanosis`, `Clubbing`, `Lymphadenopathy`, `Edema` (←oedema) | +| `t_benreferdetails` | request | written only when `referralToHWCNeeded = true` | + +> **Note:** Height/weight/bmi/temperature come from registration `otherFields` because the +> mobile's `GeneralExaminationSaveRequest` does not include these fields. The mobile hardcodes +> them null in `VitalScreenFragment.saveVitals()`. They are captured at registration and synced +> to Identity-API via TM-API. + +#### `saveGeneralOpd()` writes to: + +| Standard Table | Fields written | +|---|---| +| `t_benchiefcomplaint` | `ChiefComplaint` (free text from request) | +| `t_prescription` | `Instruction` (from `notes` field) | +| `t_prescribeddrug` | `GenericDrugName` (medication), `Dose`, `Frequency`, `Duration`, linked to `PrescriptionID` | + +All 7 standard tables share the same `benVisitID` and `VisitCode` as the join key. + +--- + +### 2.3 New entities and repos created + +All in package `com.iemr.flw.domain.iemr` / `com.iemr.flw.repo.iemr`: + +| Entity | Table | Repo | +|---|---|---| +| `BenAnthropometryDetail` | `t_phy_anthropometry` | `BenAnthropometryRepo` | +| `BenPhysicalVitalDetail` | `t_phy_vitals` | `BenPhysicalVitalRepo` | +| `PhyGeneralExamination` | `t_Phy_GeneralExam` | `PhyGeneralExaminationRepo` | +| `BenChiefComplaint` | `t_benchiefcomplaint` | `BenChiefComplaintRepo` | +| `PrescriptionDetail` | `t_prescription` | `PrescriptionDetailRepo` | +| `PrescribedDrugDetail` | `t_prescribeddrug` | `PrescribedDrugDetailRepo` | + +All 6 standard tables already existed in `db_iemr`. No CREATE TABLE needed for these. +`BenReferDetails` and `BenReferDetailsRepo` already existed in FLW-API — just wired up. + +--- + +### 2.4 Service files updated + +All Stop TB service files that previously used `TBStopVisit` and `visit.getId()` were updated +to use `BenVisitDetail` and `visit.getVisitCode()`: + +| File | Change | +|---|---| +| `StopTBServiceImpl` | `saveGeneralExamination`, `saveNurseTBScreening`, `saveGeneralOpd` — all updated | +| `TBSuspectedServiceImpl` | `save()` — updated | +| `TBConfirmedCaseServiceImpl` | `save()` — updated | + +The `visitCode` column on `tb_stoptb_suspected` and `tb_stoptb_confirmed_cases` is still used +for per-visit keying. The value written is now the 14-digit standard visitCode instead of the +old `tb_stoptb_visit` PK. + +--- + +## 3. DB migration required + +Run these two statements on the server. The standard tables (`t_phy_anthropometry` etc.) +already exist in `db_iemr` — no CREATE needed for them. + +```sql +-- Required: visitCode column added to these two tables when per-visit history was introduced +ALTER TABLE tb_stoptb_suspected ADD COLUMN visitCode BIGINT; +ALTER TABLE tb_stoptb_confirmed_cases ADD COLUMN visitCode BIGINT; + +-- DO NOT run: tb_stoptb_visit is replaced by t_benvisitdetail +-- CREATE TABLE tb_stoptb_visit ← SKIP THIS +``` + +--- + +## 4. BenFlowStatus (`i_ben_flow_outreach`) — current state + +| Flag | Set when | Value | +|---|---|---| +| `nurseFlag` | Registration | `1` (pending for nurse) | +| `nurseFlag` | `submitNurseData()` | `9` (nurse done) | +| `doctorFlag` | `routeToStopTBCounsellor()` | `1` (pending for counsellor) | + +**Known gap:** Mobile does not call `flw-api/stoptb/nurse/submit` (app is live, cannot change). +So `nurseFlag` stays at `1` forever after registration. + +**Why this is not breaking:** The mobile also does not call the nurse worklist or registrar +worklist endpoints — both are commented out in `AmritApiService.kt`. The mobile uses +`flw-api/beneficiary/getBeneficiaryData` instead and manages its own local state. + +**Counsellor:** Finds confirmed TB cases via direct `tb_confirmed_cases` query, not via +`doctorFlag`. This works correctly today. + +**Future action (when web dashboard is built):** Wire up `nurseFlag` advancement by checking +all 3 mandatory nurse saves (generalExamination + tbScreening + generalOpd) exist for the +same `visitCode`, then auto-advance `nurseFlag = 9`. The query +`updateStopTBAfterNurseSubmit()` in `BenFlowStatusRepo` already exists for this. + +--- + +## 5. Complete mobile API list (live, as of 2026-06-30) + +### Auth +| Endpoint | +|---| +| `POST common-api/user/userAuthenticate` | +| `POST common-api/user/refreshToken` | +| `GET flw-api/user/getUserDetail` | +| `POST common-api/firebaseNotification/userToken` | + +### Beneficiary Registration +| Endpoint | +|---| +| `POST tm-api/registrar/registrarBeneficaryRegistrationNew` | +| `POST identity-api/rmnch/syncDataToAmrit` | +| `POST flw-api/beneficiary/getBeneficiaryData` | + +### Consent / OTP +| Endpoint | +|---| +| `POST common-api/beneficiaryConsent/sendConsent` | +| `POST common-api/beneficiaryConsent/resendConsent` | +| `POST common-api/beneficiaryConsent/validateConsent` | + +### Stop TB — Nurse +| Endpoint | +|---| +| `POST flw-api/stoptb/nurse/generalExamination/save` | +| `POST flw-api/stoptb/nurse/generalExamination/getAll` | +| `POST flw-api/stoptb/nurse/tbScreening/save` | +| `POST flw-api/stoptb/nurse/tbScreening/getAll` | +| `POST flw-api/stoptb/nurse/generalOpd/save` | +| `POST flw-api/stoptb/nurse/generalOpd/getAll` | +| `POST flw-api/stoptb/nurse/diagnostics/save` | +| `POST flw-api/stoptb/nurse/diagnostics/getAll` | + +### Stop TB — Suspected / Confirmed +| Endpoint | +|---| +| `POST flw-api/tb/suspected/saveAll` | +| `POST flw-api/tb/suspected/getAll` | +| `POST flw-api/tb/confirmed/save` | +| `GET flw-api/tb/confirmed/getAll` | + +### Counselling (Dynamic Form) +| Endpoint | +|---| +| `GET flw-api/dynamicForm/getAllForms` | +| `GET common-api/dynamicForm/form/{formId}/fields` | +| `POST flw-api/counselling/save` | +| `POST flw-api/dynamicForm/response/submitBulk` | +| `POST flw-api/dynamicForm/response/complete` | +| `GET flw-api/dynamicForm/response/getByBeneficiary` | +| `GET flw-api/dynamicForm/response/getCompletedBeneficiaries` | + +### CBAC / NCD +| Endpoint | +|---| +| `POST hwc-api/NCD/getByUserCbacDetails` | +| `POST hwc-api/NCD/save/nurseData` | +| `POST hwc-api/NCD/save/referDetails` | + +### ABHA / Health ID +| Endpoint | +|---| +| `POST fhir-api/healthIDWithUID/createHealthIDWithUID` | +| `POST fhir-api/healthID/getBenhealthID` | +| `POST fhir-api/healthIDRecord/mapHealthIDToBeneficiary` | +| `POST fhir-api/healthIDRecord/addHealthIdRecord` | +| `POST fhir-api/healthIDCard/generateOTP` | +| `POST fhir-api/healthIDCard/verifyOTPAndGenerateHealthCard` | + +### Other Disease Screens +| Endpoint | +|---| +| `POST flw-api/disease/kalaAzar/saveAll` | +| `POST flw-api/disease/malaria/saveAll` | +| `POST flw-api/disease/leprosy/saveAll` | +| `POST flw-api/disease/leprosy/followUp/saveAll` | +| `POST flw-api/disease/leprosy/followUp/getAll` | +| `POST flw-api/disease/aesJe/saveAll` | +| `POST flw-api/disease/filaria/saveAll` | +| `POST flw-api/disease/getAllDisease` | +| `POST flw-api/follow-up/save` | +| `POST flw-api/follow-up/get` | +| `POST flw-api/child-care/hbncVisit/saveAll` | +| `POST flw-api/disease/cdtfVisit/saveAll` | +| `POST flw-api/disease/cdtfVisit/getAll` | + +### Commented out / never called (documented for future reference) +| Endpoint | Reason | +|---|---| +| `POST flw-api/stoptb/nurse/worklist` | Mobile uses getBeneficiaryData instead | +| `POST flw-api/stoptb/registrar/worklist` | Mobile uses getBeneficiaryData instead | +| `POST flw-api/stoptb/nurse/submit` | Mobile is live — cannot add new call; nurseFlag stays 1 (acceptable, see Section 4) | +| `POST hwc-api/common/getBenReferDetailsByCreatedBy` | Referral pull not needed in Stop TB | +| `POST hwc-api/sync/generalOPDNurseFormDataToServer` | Abandoned earlier attempt at standard-table integration | +| `POST identity-api/id/getByBenId` | getBenHealthID used instead | + +--- + +## 6. What HWC can now see + +Once the DB migration SQL is run and FLW-API is deployed, any service reading from `db_iemr` +standard tables will find Stop TB beneficiary data joined by `benVisitID` / `VisitCode`: + +| Table | Content | +|---|---| +| `t_benvisitdetail` | One row per camp day, `visitCategory = 'Stop TB'` | +| `t_phy_anthropometry` | Height, weight, BMI (from registration) | +| `t_phy_vitals` | Temperature (from registration), pulse, BP, RBS (from nurse exam) | +| `t_Phy_GeneralExam` | Pallor, jaundice, cyanosis, clubbing, lymphadenopathy, edema | +| `t_benchiefcomplaint` | Chief complaint text | +| `t_prescription` | Notes/instructions | +| `t_prescribeddrug` | Medication, dose, frequency, duration | +| `t_benreferdetails` | HWC referral (only when `referralToHWCNeeded = true`) | + +> **Note:** HWC's referral-receiving worklist endpoints (`getBenPreviousReferralHistoryDetails`, +> `getBenReferDetailsByCreatedBy`) have zero callers on HWC-UI and HWC-Mobile-App as of today. +> Writing `t_benreferdetails` is correct for audit/reporting. Surfacing it as a live HWC +> worklist is a separate, future piece of work. + +--- + +## 7. What is NOT done (future scope) + +| Item | Detail | +|---|---| +| `nurseFlag` auto-advancement | Needs web dashboard to be useful. Logic: check all 3 saves (exam + screening + OPD) exist for same `visitCode` → set `nurseFlag=9`. Query already exists in `BenFlowStatusRepo.updateStopTBAfterNurseSubmit()` | +| HWC worklist for Stop TB referrals | HWC-API and HWC-UI changes needed to surface `t_benreferdetails` rows | +| Duplicate beneficiary detection | Requires new mobile screen — no server-side gap | +| Beneficiary edit/update | Requires new mobile screen — no edit endpoint exists anywhere in Stop TB | +| `height`/`weight`/`bmi`/`respiratoryRate`/`spo2` in nurse exam | Mobile captures these locally (`VitalCache`) but `GeneralExaminationSaveRequest.from()` hardcodes them null. Would require mobile change to send live | 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/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/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/TBStopVisit.java b/src/main/java/com/iemr/flw/domain/iemr/TBStopVisit.java deleted file mode 100644 index 8e7f0e20..00000000 --- a/src/main/java/com/iemr/flw/domain/iemr/TBStopVisit.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.iemr.flw.domain.iemr; - -import lombok.Data; -import jakarta.persistence.*; -import org.hibernate.annotations.CreationTimestamp; - -import java.sql.Timestamp; - -/** - * One row per real-world Stop TB camp encounter (a beneficiary seen on a given calendar day). - * Mirrors MMU's t_benvisitdetail (visitNo, insert-only history) but scoped entirely to Stop TB's - * own tables — never written to or read from MMU/HWC's shared visit/outreach infrastructure. - */ -@Entity -@Table(name = "tb_stoptb_visit", schema = "db_iemr") -@Data -public class TBStopVisit { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "beneficiary_reg_id") - private Long beneficiaryRegID; - - @Column(name = "visit_no") - private Integer visitNo; - - @CreationTimestamp - @Column(name = "visit_date", updatable = false) - private Timestamp visitDate; - - @Column(name = "provider_service_map_id") - private Integer providerServiceMapID; - - @Column(name = "created_by") - private String createdBy; - - // Role-handoff flags — only nurseCompletedFlag is populated today (Stop TB has no - // Doctor/Counsellor screens yet). Reserved so a future worklist can query against - // this table without a schema change later. - @Column(name = "nurse_completed_flag") - private String nurseCompletedFlag = "N"; - - @Column(name = "doctor_completed_flag") - private String doctorCompletedFlag = "N"; - - @Column(name = "counsellor_completed_flag") - private String counsellorCompletedFlag = "N"; - - // Sync fields — same vanID/vanSerialNo/processed discipline as every other Stop TB table, - // required so two camp laptops can each generate local visit rows without colliding at - // central (de-duplication key is (vanSerialNo, vanID), not the local auto-increment id). - @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; - - // Stamped by the generic local-to-central DataSyncRepository job (MMU/HWC/TM-API convention) - // when this row is actually pushed — required for tb_stoptb_visit to be syncable by that - // same generic mechanism without code changes there. - @Column(name = "SyncedDate") - private Timestamp syncedDate; - - @Column(name = "Syncedby") - private String syncedby; - - @Column(name = "SyncFailureReason") - private String syncFailureReason; -} 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..b7c9efd3 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java @@ -0,0 +1,9 @@ +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; + +@Repository +public interface BenAnthropometryRepo extends JpaRepository { +} 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/BenPhysicalVitalRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java new file mode 100644 index 00000000..cf1ed35b --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java @@ -0,0 +1,9 @@ +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; + +@Repository +public interface BenPhysicalVitalRepo extends JpaRepository { +} 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/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/TBStopVisitRepo.java b/src/main/java/com/iemr/flw/repo/iemr/TBStopVisitRepo.java deleted file mode 100644 index 51791436..00000000 --- a/src/main/java/com/iemr/flw/repo/iemr/TBStopVisitRepo.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.iemr.flw.repo.iemr; - -import com.iemr.flw.domain.iemr.TBStopVisit; -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; - -@Repository -public interface TBStopVisitRepo extends JpaRepository { - - @Query("SELECT v FROM TBStopVisit v WHERE v.beneficiaryRegID = :beneficiaryRegID " + - "AND v.visitDate BETWEEN :start AND :end") - TBStopVisit findByBeneficiaryRegIDAndVisitDateBetween(@Param("beneficiaryRegID") Long beneficiaryRegID, - @Param("start") Timestamp start, - @Param("end") Timestamp end); - - @Query("SELECT COUNT(v) FROM TBStopVisit v WHERE v.beneficiaryRegID = :beneficiaryRegID") - Integer getVisitCountForBeneficiary(@Param("beneficiaryRegID") Long beneficiaryRegID); - - @Transactional - @Modifying - @Query("UPDATE TBStopVisit 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/service/TBStopVisitService.java b/src/main/java/com/iemr/flw/service/TBStopVisitService.java index ec1d40e3..0e15fd9b 100644 --- a/src/main/java/com/iemr/flw/service/TBStopVisitService.java +++ b/src/main/java/com/iemr/flw/service/TBStopVisitService.java @@ -1,15 +1,9 @@ package com.iemr.flw.service; -import com.iemr.flw.domain.iemr.TBStopVisit; +import com.iemr.flw.domain.iemr.BenVisitDetail; public interface TBStopVisitService { - /** - * Returns today's visit row for this beneficiary, creating one (with visitNo = previous - * count + 1) if none exists yet for the current calendar day. No mobile change required — - * "new visit" is inferred purely from the save timestamp, same pattern MMU uses - * (count existing visits, increment, insert-only — never update an existing visit row). - */ - TBStopVisit getOrCreateVisitForToday(Long beneficiaryRegID, Integer providerServiceMapID, String createdBy, - Integer vanID, Integer parkingPlaceID); + BenVisitDetail getOrCreateVisitForToday(Long beneficiaryRegID, Integer providerServiceMapID, + String createdBy, Integer vanID, Integer parkingPlaceID); } diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index 8c8cc6ea..a920cf07 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -45,6 +45,7 @@ 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; @@ -90,10 +91,13 @@ public class DynamicFormResponseServiceImpl implements DynamicFormResponseServic 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; @@ -112,15 +116,17 @@ public FormResponseDTO submitForm(FormResponseRequest request) { formResponse.setLastFollowUpAt(now); formResponse = formResponseRepo.save(formResponse); } else { - formResponse = createFormResponse(request, version, STATUS_SUBMITTED, now); + formResponse = createFormResponse(request, version, STATUS_SUBMITTED, now, vanID, parkingPlaceID); } - return processSections(formResponse, request, version, null); + 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(); @@ -138,7 +144,7 @@ public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken formResponse.setStatus(STATUS_COMPLETE); formResponse.setCompletedAt(new Timestamp(System.currentTimeMillis())); formResponse = formResponseRepo.save(formResponse); - return processSections(formResponse, request, version, actor); + return processSections(formResponse, request, version, actor, vanID, parkingPlaceID); } @Override @@ -175,7 +181,9 @@ private FormResponse createFormResponse( FormResponseRequest request, FormVersion version, String status, - Timestamp submittedAt) { + Timestamp submittedAt, + Integer vanID, + Integer parkingPlaceID) { FormResponse formResponse = FormResponse.builder() .beneficiaryId(request.getBeneficiaryId()) @@ -188,6 +196,8 @@ private FormResponse createFormResponse( // 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); } @@ -199,7 +209,9 @@ private FormResponseDTO processSections( FormResponse formResponse, FormResponseRequest request, FormVersion version, - String actor) { + String actor, + Integer vanID, + Integer parkingPlaceID) { List allSections = formSectionRepo.findByFormVersion_VersionIdOrderByDisplayOrderAsc(version.getVersionId()); @@ -243,7 +255,7 @@ private FormResponseDTO processSections( } SectionResponse sectionResponse = upsertSectionResponse( - formResponse.getResponseId(), section.getSectionId(), actor); + formResponse.getResponseId(), section.getSectionId(), actor, vanID, parkingPlaceID); if ("POST_SUBMIT".equals(section.getSectionPhase())) { formResponse.setLastFollowUpAt(sectionResponse.getSavedAt()); @@ -258,7 +270,9 @@ private FormResponseDTO processSections( sectionReq.getAnswers(), questionMap, optionsByQuestion, - actor); + actor, + vanID, + parkingPlaceID); questionResponseRepo.saveAll(questionResponses); @@ -268,7 +282,8 @@ private FormResponseDTO processSections( return buildFormResponseDTO(formResponse, sectionDTOs); } - private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor) { + private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor, + Integer vanID, Integer parkingPlaceID) { Optional existing = sectionResponseRepo.findByResponseIdAndSectionId(responseId, sectionId); if (existing.isPresent()) { @@ -276,6 +291,7 @@ private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, S 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() @@ -285,6 +301,8 @@ private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, S .savedAt(new Timestamp(System.currentTimeMillis())) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build(); return sectionResponseRepo.save(sr); } @@ -294,7 +312,9 @@ private List processAnswers( List answers, Map questionMap, Map> optionsByQuestion, - String actor) { + String actor, + Integer vanID, + Integer parkingPlaceID) { List results = new ArrayList<>(); @@ -329,6 +349,8 @@ private List processAnswers( .optionId(opt != null ? opt.getOptionId() : null) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build()); } } else if (type == QuestionType.MCQ) { @@ -342,6 +364,8 @@ private List processAnswers( .optionId(opt != null ? opt.getOptionId() : null) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build()); } } @@ -356,6 +380,8 @@ private List processAnswers( .answerText(value) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build()); } } diff --git a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java index 0d844eb7..6a8fb863 100644 --- a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -42,6 +42,7 @@ 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; @@ -83,6 +84,7 @@ public class FormResponseItemSaver { 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. @@ -92,6 +94,8 @@ public class FormResponseItemSaver { @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(); @@ -117,11 +121,11 @@ public FormResponseDTO saveForBulk(FormResponseRequest req, String jwtToken) thr log.info("saveForBulk: overwriting responseId={} for beneficiaryId={}", formResponse.getResponseId(), req.getBeneficiaryId()); } else { - formResponse = createFormResponse(req, version, now, actor); + formResponse = createFormResponse(req, version, now, actor, vanID, parkingPlaceID); } // Step 5: Insert fresh sections and answers - return processSections(formResponse, req, version, actor); + return processSections(formResponse, req, version, actor, vanID, parkingPlaceID); } /** Pre-flight: validates every sectionUuid and questionUuid in the request exist in the form. */ @@ -169,7 +173,8 @@ private FormVersion resolveLatestVersion(String formUuid) { "No active FormVersion found for form: " + formUuid)); } - private FormResponse createFormResponse(FormResponseRequest req, FormVersion version, Timestamp now, String actor) { + 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()) @@ -180,11 +185,14 @@ private FormResponse createFormResponse(FormResponseRequest req, FormVersion ver .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) { + 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() @@ -233,7 +241,7 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR }); SectionResponse sectionResponse = upsertSectionResponse( - formResponse.getResponseId(), section.getSectionId(), actor); + formResponse.getResponseId(), section.getSectionId(), actor, vanID, parkingPlaceID); if ("POST_SUBMIT".equals(section.getSectionPhase())) { formResponse.setLastFollowUpAt(sectionResponse.getSavedAt()); @@ -248,7 +256,9 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR sectionReq.getAnswers(), questionMap, optionsByQuestion, - actor); + actor, + vanID, + parkingPlaceID); questionResponseRepo.saveAll(questionResponses); sectionDTOs.add(buildSectionResponseDTO(sectionResponse, questionResponses)); @@ -257,7 +267,8 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR return buildFormResponseDTO(formResponse, sectionDTOs); } - private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor) { + private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor, + Integer vanID, Integer parkingPlaceID) { Optional existing = sectionResponseRepo.findByResponseIdAndSectionId(responseId, sectionId); if (existing.isPresent()) { @@ -265,6 +276,7 @@ private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, S 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() @@ -274,6 +286,8 @@ private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, S .savedAt(new Timestamp(System.currentTimeMillis())) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build(); return sectionResponseRepo.save(sr); } @@ -283,7 +297,9 @@ private List processAnswers( List answers, Map questionMap, Map> optionsByQuestion, - String actor) { + String actor, + Integer vanID, + Integer parkingPlaceID) { List results = new ArrayList<>(); for (QuestionAnswerRequest answer : answers) { @@ -315,6 +331,8 @@ private List processAnswers( .optionId(opt != null ? opt.getOptionId() : null) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build()); } } else if (type == QuestionType.MCQ) { @@ -328,6 +346,8 @@ private List processAnswers( .optionId(opt != null ? opt.getOptionId() : null) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build()); } } @@ -342,6 +362,8 @@ private List processAnswers( .answerText(value) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build()); } } diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 37faa4b5..c0148100 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -5,15 +5,29 @@ import com.google.gson.JsonParser; 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.domain.iemr.TBStopVisit; import com.iemr.flw.dto.iemr.StopTBRegistrationDto; 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; @@ -64,6 +78,27 @@ public class StopTBServiceImpl implements StopTBService { @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; + @Value("${tm-url}") private String tmUrl; @@ -299,15 +334,16 @@ public List> saveGeneralExamination(List Integer providerServiceMapID = getInt(data, "providerServiceMapID"); String createdBy = getString(data, "createdBy"); - TBStopVisit visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, createdBy, vanID, parkingPlaceID); - StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegIDAndVisitCode(beneficiaryRegID, visit.getId()); + StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegIDAndVisitCode(beneficiaryRegID, visit.getVisitCode()); if (exam == null) exam = new StopTBGeneralExamination(); boolean isNew = exam.getId() == null; exam.setBeneficiaryRegID(beneficiaryRegID); - exam.setVisitCode(visit.getId()); + exam.setVisitCode(visit.getVisitCode()); + exam.setBenVisitID(visit.getBenVisitId()); exam.setProviderServiceMapID(providerServiceMapID); exam.setPulseRate(getInt(data, "pulseRate")); exam.setSystolicBP(getInt(data, "systolicBP")); @@ -337,7 +373,10 @@ public List> saveGeneralExamination(List exam.setProcessed("N"); generalExaminationRepo.save(exam); - if (isNew) generalExaminationRepo.updateVanSerialNo(exam.getId()); + if (isNew) { + generalExaminationRepo.updateVanSerialNo(exam.getId()); + dualWriteExamToStandardTables(exam, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + } Map result = new HashMap<>(); result.put("beneficiaryRegID", beneficiaryRegID); @@ -422,15 +461,15 @@ public List> saveNurseTBScreening(List> Integer providerServiceMapID = getInt(data, "providerServiceMapID"); String createdBy = getString(data, "createdBy"); - TBStopVisit visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, createdBy, vanID, parkingPlaceID); - TBScreening screening = tbScreeningRepo.findByBenRegIDAndVisitCode(beneficiaryRegID, visit.getId()); + TBScreening screening = tbScreeningRepo.findByBenRegIDAndVisitCode(beneficiaryRegID, visit.getVisitCode()); if (screening == null) screening = new TBScreening(); boolean isNew = screening.getId() == null; screening.setBenRegID(beneficiaryRegID); - screening.setVisitCode(visit.getId()); + screening.setVisitCode(visit.getVisitCode()); screening.setProviderServiceMapID(providerServiceMapID); screening.setCoughMoreThan2Weeks(getBool(data, "coughMoreThan2Weeks")); screening.setBloodInSputum(getBool(data, "bloodInSputum")); @@ -550,15 +589,16 @@ public List> saveGeneralOpd(List> dataLi Integer providerServiceMapID = getInt(data, "providerServiceMapID"); String createdBy = getString(data, "createdBy"); - TBStopVisit visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, createdBy, vanID, parkingPlaceID); - StopTBGeneralOpd opd = generalOpdRepo.findByBenRegIDAndVisitCode(beneficiaryRegID, visit.getId()); + StopTBGeneralOpd opd = generalOpdRepo.findByBenRegIDAndVisitCode(beneficiaryRegID, visit.getVisitCode()); if (opd == null) opd = new StopTBGeneralOpd(); boolean isNew = opd.getId() == null; opd.setBenRegID(beneficiaryRegID); - opd.setVisitCode(visit.getId()); + opd.setVisitCode(visit.getVisitCode()); + opd.setBenVisitID(visit.getBenVisitId()); opd.setProviderServiceMapID(providerServiceMapID); opd.setChiefComplaint(toJsonString(data.get("chiefComplaint"))); opd.setMedication(getString(data, "medication")); @@ -573,7 +613,10 @@ public List> saveGeneralOpd(List> dataLi opd.setProcessed("N"); generalOpdRepo.save(opd); - if (isNew) generalOpdRepo.updateVanSerialNo(opd.getId()); + if (isNew) { + generalOpdRepo.updateVanSerialNo(opd.getId()); + dualWriteOpdToStandardTables(opd, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + } Map result = new HashMap<>(); result.put("beneficiaryRegID", beneficiaryRegID); @@ -754,6 +797,189 @@ public Map submitNurseData(Map data) throws Exce return result; } + // ── 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 Timestamp parseDob(JsonObject obj) { 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 d749b44c..c5643c6f 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java @@ -5,7 +5,7 @@ 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.TBStopVisit; +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; @@ -80,14 +80,14 @@ public String save(List request, String authorisation) throw for(TBConfirmedCaseDTO dto:request){ String modifiedBy = jwtUtil.extractUsername(authorisation); Long beneficiaryRegID = beneficiaryRepo.getRegIDFromBenId(dto.getBenId()); - TBStopVisit visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, null, + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, null, modifiedBy, vanID, parkingPlaceID); - List existing = repository.findByBenIdAndVisitCode(dto.getBenId(), visit.getId()); + 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.getId()); + entity.setVisitCode(visit.getVisitCode()); entity.setUserId(jwtUtil.extractUserId(authorisation)); entity.setModifiedBy(modifiedBy); entity.setRegimenType(dto.getRegimenType()); diff --git a/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java index 5907b678..a54ebe4f 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java @@ -1,7 +1,7 @@ package com.iemr.flw.service.impl; -import com.iemr.flw.domain.iemr.TBStopVisit; -import com.iemr.flw.repo.iemr.TBStopVisitRepo; +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; @@ -14,34 +14,45 @@ public class TBStopVisitServiceImpl implements TBStopVisitService { @Autowired - private TBStopVisitRepo tbStopVisitRepo; + private BenVisitDetailsRepo benVisitDetailsRepo; @Override @Transactional - public TBStopVisit getOrCreateVisitForToday(Long beneficiaryRegID, Integer providerServiceMapID, String createdBy, - Integer vanID, Integer parkingPlaceID) { + public BenVisitDetail getOrCreateVisitForToday(Long beneficiaryRegID, Integer providerServiceMapID, + String createdBy, Integer vanID, Integer parkingPlaceID) { LocalDate today = LocalDate.now(); - Timestamp todayStart = Timestamp.valueOf(today.atStartOfDay()); - Timestamp todayEnd = Timestamp.valueOf(today.plusDays(1).atStartOfDay()); - - TBStopVisit visit = tbStopVisitRepo.findByBeneficiaryRegIDAndVisitDateBetween(beneficiaryRegID, todayStart, todayEnd); - if (visit != null) { - return visit; - } - - Integer count = tbStopVisitRepo.getVisitCountForBeneficiary(beneficiaryRegID); - visit = new TBStopVisit(); - visit.setBeneficiaryRegID(beneficiaryRegID); - visit.setVisitNo(count != null ? count + 1 : 1); + 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); - if (vanID != null) { - visit.setVanID(vanID); - visit.setParkingPlaceID(parkingPlaceID); - } + visit.setVanId(vanID); + visit.setParkingPlaceId(parkingPlaceID); + visit.setDeleted(false); visit.setProcessed("N"); - visit = tbStopVisitRepo.save(visit); - tbStopVisitRepo.updateVanSerialNo(visit.getId()); - return visit; + 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); + return benVisitDetailsRepo.save(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 010651f4..81390d64 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -2,7 +2,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.iemr.flw.domain.iemr.TBStopVisit; +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; @@ -50,11 +50,11 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { Integer parkingPlaceID = campConfigService.getParkingPlaceID(); for (TBSuspectedDTO tbSuspectedDTO : requestDTO.getTbSuspectedList()) { Long beneficiaryRegID = beneficiaryRepo.getRegIDFromBenId(tbSuspectedDTO.getBenId()); - TBStopVisit visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, null, + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, null, requestDTO.getUserId() != null ? requestDTO.getUserId().toString() : null, vanID, parkingPlaceID); TBSuspected tbSuspected = - tbSuspectedRepo.getByUserIdAndBenIdAndVisitCode(tbSuspectedDTO.getBenId(), requestDTO.getUserId(), visit.getId()); + tbSuspectedRepo.getByUserIdAndBenIdAndVisitCode(tbSuspectedDTO.getBenId(), requestDTO.getUserId(), visit.getVisitCode()); if (tbSuspected == null) { tbSuspected = new TBSuspected(); @@ -67,7 +67,7 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { } tbSuspected.setUserId(requestDTO.getUserId()); - tbSuspected.setVisitCode(visit.getId()); + tbSuspected.setVisitCode(visit.getVisitCode()); if (tbSuspected.getVanID() == null && vanID != null) { tbSuspected.setVanID(vanID); tbSuspected.setParkingPlaceID(parkingPlaceID); } tbSuspected.setProcessed("N"); tbSuspectedRepo.save(tbSuspected); From 2d9ca9bbe7c4366cda2e975700d0769239870235 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 30 Jun 2026 14:23:03 +0530 Subject: [PATCH 57/97] fix(stoptb): getBeneficiaryData reads anthropometry/vitals from standard tables - getBeneficiaryData now reads height/weight/bmi from t_phy_anthropometry and temperature/pulse/BP/RBS from t_phy_vitals (latest row per beneficiary) - Fallback to otherFields if exam not yet saved for the beneficiary - Stop TB specific fields (personFrom, caseFindingType etc.) still from otherFields - Add findByBeneficiaryRegIDOrderByCreatedDateDesc and findByBenVisitID to BenAnthropometryRepo and BenPhysicalVitalRepo - Add StopTB_Future_Roadmap.md for future HWC integration and edit support Co-Authored-By: Claude Sonnet 4.6 --- StopTB_Future_Roadmap.md | 246 ++++++++++++++++++ .../flw/repo/iemr/BenAnthropometryRepo.java | 6 + .../flw/repo/iemr/BenPhysicalVitalRepo.java | 6 + .../service/impl/BeneficiaryServiceImpl.java | 58 ++++- 4 files changed, 309 insertions(+), 7 deletions(-) create mode 100644 StopTB_Future_Roadmap.md diff --git a/StopTB_Future_Roadmap.md b/StopTB_Future_Roadmap.md new file mode 100644 index 00000000..d6fea62b --- /dev/null +++ b/StopTB_Future_Roadmap.md @@ -0,0 +1,246 @@ +# Stop TB — Future Roadmap + +**Date:** 2026-06-30 +**Author:** vishwab1 +**Prerequisite:** Read `StopTB_Standard_Table_Integration.md` first for current implementation context. + +--- + +## Why this document exists + +The current dual-write implementation (FLW-API) writes Stop TB clinical data to both custom +`tb_stoptb_*` tables and standard AMRIT tables (`t_phy_vitals`, `t_Phy_GeneralExam` etc.). + +This document captures what still needs to be done in future phases — HWC integration, +edit support, worklist, and BenFlowStatus advancement. + +**None of these future changes require a mobile app change.** + +--- + +## 1. HWC-API — Read common fields from standard tables + +**Trigger:** When HWC doctor needs to see Stop TB beneficiary's vitals/examination history. + +**Problem:** HWC-API currently queries standard tables with a `providerServiceMapID` filter: +```sql +SELECT * FROM t_phy_vitals +WHERE beneficiaryRegID = ? AND providerServiceMapID = ? -- HWC's own PSMID +``` +Stop TB data was written with Stop TB's PSMID → HWC query misses it. + +**Fix — HWC-API only, remove PSMID filter:** +```sql +SELECT * FROM t_phy_vitals +WHERE beneficiaryRegID = ? -- no PSMID filter, shows all service lines +ORDER BY CreatedDate DESC +``` + +**Affected HWC-API endpoints:** +- `getBenPhysicalVitalDetail` +- `getBenAnthropometryDetail` +- `getBenPhyGeneralExamination` +- `getBenChiefComplaint` +- `getBenPrescriptionDetail` + +**Effort:** Small — query change only in HWC-API. No schema change. No FLW-API change. + +--- + +## 2. HWC-API — Read TB specific data from custom tables directly + +**Trigger:** When HWC doctor needs full TB clinical picture — screening questions, diagnostics, +Nikshay ID, treatment tracking. + +Standard tables only hold common clinical fields. TB specific data lives in custom tables. +HWC-API needs to query these directly by `benRegID`. + +**New queries needed in HWC-API:** + +```java +// TB Screening +tbScreeningRepo.findByBenRegIDOrderByCreatedDateDesc(benRegID); + +// Diagnostics (Nikshay ID, X-ray, Truenat, Sputum, Liquid Culture) +tbDiagnosticsRepo.findByBenRegIDOrderByCreatedDateDesc(benRegID); + +// Suspected cases +tbSuspectedRepo.findByBenIdOrderByCreatedDateDesc(benId); + +// Confirmed cases + treatment +tbConfirmedRepo.findByBenIdOrderByCreatedDateDesc(benId); +``` + +**Tables queried:** +| Table | Key data | +|---|---| +| `tb_screening` | Cough, fever, night sweats, weight loss, family history | +| `tb_stoptb_diagnostics` | Nikshay ID, X-ray result, Truenat result, Liquid Culture result | +| `tb_suspected` | Sputum result, chest X-ray, referral, TB/DRTB confirmed flag | +| `tb_confirmed_cases` | Regimen, treatment dates, adherence, outcome | + +**Effort:** Small — new repo methods + new HWC-API service calls. No schema change. + +--- + +## 3. FLW-API — Edit support for standard table dual-write + +**Trigger:** When mobile app adds edit/update capability for nurse data. + +**Current behaviour:** Dual-write only happens on first save (`isNew = true`). On re-save, +only `tb_stoptb_*` custom tables are updated. Standard tables keep the original values. + +**What breaks:** If mobile edits vitals → `tb_stoptb_general_examination` gets new values +but `t_phy_vitals` keeps old values → HWC sees stale data. + +### Changes in StopTBServiceImpl + +```java +// Change from: +if (isNew) { + generalExaminationRepo.updateVanSerialNo(exam.getId()); + dualWriteExamToStandardTables(exam, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); +} + +// Change to: +if (isNew) generalExaminationRepo.updateVanSerialNo(exam.getId()); +dualWriteExamToStandardTables(exam, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); +``` + +Same change for `saveGeneralOpd()`: +```java +// Remove isNew guard around dualWriteOpdToStandardTables +if (isNew) generalOpdRepo.updateVanSerialNo(opd.getId()); +dualWriteOpdToStandardTables(opd, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); +``` + +### Changes in dual-write helpers — upsert instead of insert + +Each helper must find existing row first, update if found, insert if not: + +```java +private void writeVitals(StopTBGeneralExamination exam, ...) { + try { + // Find existing row for this visit + BenPhysicalVitalDetail existing = + benPhysicalVitalRepo.findByBenVisitID(visit.getBenVisitId()); + BenPhysicalVitalDetail v = existing != null ? existing : new BenPhysicalVitalDetail(); + + v.setBeneficiaryRegID(beneficiaryRegID); + v.setBenVisitID(visit.getBenVisitId()); + v.setVisitCode(visit.getVisitCode()); + // ... set all fields + benPhysicalVitalRepo.save(v); + } catch (Exception e) { + logger.error("writeVitals failed", e); + } +} +``` + +Same upsert pattern for: `writeAnthropometry()`, `writePhyGeneralExam()`, +`writeChiefComplaint()`, `writePrescription()`. + +### New repo methods needed (FLW-API) + +```java +// BenPhysicalVitalRepo +BenPhysicalVitalDetail findByBenVisitID(Long benVisitID); + +// BenAnthropometryRepo +BenAnthropometryDetail findByBenVisitID(Long benVisitID); + +// PhyGeneralExaminationRepo +PhyGeneralExamination findByBenVisitID(Long benVisitID); + +// BenChiefComplaintRepo +BenChiefComplaint findByBenVisitID(Long benVisitID); +``` + +> **Note:** `t_prescription` and `t_prescribeddrug` have a parent-child FK relationship. +> For prescription edit: find existing `PrescriptionDetail` by `benVisitID`, delete old +> `PrescribedDrugDetail` rows, re-insert fresh drug rows. + +**Effort:** Small — remove isNew guard, add findByBenVisitID to 4 repos, convert helpers to upsert. + +--- + +## 4. FLW-API — nurseFlag advancement + +**Trigger:** When a web dashboard / supervisor screen is built for Stop TB. + +**Current state:** `nurseFlag` in `i_ben_flow_outreach` is set to `1` at registration and +never advances because mobile never calls `flw-api/stoptb/nurse/submit`. + +**What to implement:** + +After each of the 3 mandatory nurse saves (generalExamination, tbScreening, generalOpd), +check if all 3 are complete for the same `visitCode`. If yes, auto-advance `nurseFlag = 9`. + +```java +private void tryAdvanceNurseFlag(Long benRegID, Long visitCode, Integer psmID) { + boolean examDone = generalExaminationRepo + .findByBeneficiaryRegIDAndVisitCode(benRegID, visitCode) != null; + boolean screeningDone = tbScreeningRepo + .findByBenRegIDAndVisitCode(benRegID, visitCode) != null; + boolean opdDone = generalOpdRepo + .findByBenRegIDAndVisitCode(benRegID, visitCode) != null; + + if (examDone && screeningDone && opdDone) { + benFlowStatusRepo.updateStopTBAfterNurseSubmit(benRegID, psmID); + // updateStopTBAfterNurseSubmit() already exists in BenFlowStatusRepo + } +} +``` + +Call `tryAdvanceNurseFlag()` at the end of each of the 3 save methods in `StopTBServiceImpl`. + +**Effort:** Small — new private method in StopTBServiceImpl, reuses existing repo query. + +--- + +## 5. HWC-API + HWC-UI — Stop TB referral worklist + +**Trigger:** When HWC wants to track Stop TB beneficiaries referred to it. + +**Current state:** When Stop TB nurse marks `referralToHWCNeeded = true`, FLW-API writes a +row to `t_benreferdetails` with `referredToInstituteName = 'HWC'` and +`referralReason = 'Stop TB Referral'`. HWC-API has no endpoint that surfaces these rows. + +**HWC-API change needed:** + +New query in `BenReferDetailsRepo`: +```java +@Query("SELECT r FROM BenReferDetails r WHERE r.beneficiaryRegID = :benRegID " + + "AND r.referralReason = 'Stop TB Referral' AND r.deleted = false " + + "ORDER BY r.createdDate DESC") +List findStopTBReferrals(@Param("benRegID") Long benRegID); +``` + +Or for worklist (all referred beneficiaries for a provider): +```java +@Query("SELECT r FROM BenReferDetails r WHERE r.referredToInstituteName = 'HWC' " + + "AND r.referralReason = 'Stop TB Referral' AND r.deleted = false") +List findAllStopTBReferrals(); +``` + +**HWC-UI change needed:** +- New worklist tab or filter: "Stop TB Referrals" +- Shows beneficiary name, referral date, FLW name + +**Effort:** Medium — HWC-API new endpoint + HWC-UI new screen. + +--- + +## Summary table + +| # | Change | Service | Effort | Blocks on | +|---|---|---|---|---| +| 1 | Read standard tables without PSMID filter | HWC-API | Small | HWC team decision | +| 2 | Read TB specific custom tables | HWC-API | Small | HWC team decision | +| 3 | Edit support — upsert in dual-write helpers | FLW-API | Small | Mobile adding edit | +| 4 | nurseFlag auto-advancement | FLW-API | Small | Web dashboard being built | +| 5 | Stop TB referral worklist | HWC-API + HWC-UI | Medium | HWC team + product decision | + +**No mobile change required for any of these.** +**No schema change required for items 1, 2, 3, 4.** +**Item 5 may need a column in `t_benreferdetails` for status tracking (pending/accepted/rejected) — TBD by HWC team.** diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java index b7c9efd3..1856eb06 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java @@ -4,6 +4,12 @@ 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/BenPhysicalVitalRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java index cf1ed35b..7e474935 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java @@ -4,6 +4,12 @@ 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/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index b594a3b4..e311a0e5 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -56,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; @@ -109,6 +113,12 @@ public class BeneficiaryServiceImpl implements BeneficiaryService { @Autowired private UserServiceRoleRepo userRepo; + @Autowired + private BenAnthropometryRepo benAnthropometryRepo; + + @Autowired + private BenPhysicalVitalRepo benPhysicalVitalRepo; + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy"); @@ -509,17 +519,51 @@ private String getMappingsForAddressIDs(List addressLi if (benDetailsOBJ != null && benDetailsOBJ.getOccupation() != null) benDetailsRMNCH_OBJ.setOccupation(benDetailsOBJ.getOccupation()); - // anthropometry and Stop TB fields from ExtraFields + // 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 a = anthroList.get(0); + if (a.getHeightCm() != null) anthropometry.put("height", a.getHeightCm()); + if (a.getWeightKg() != null) anthropometry.put("weight", a.getWeightKg()); + if (a.getBmi() != null) anthropometry.put("bmi", a.getBmi()); + } + if (!vitalList.isEmpty()) { + BenPhysicalVitalDetail v = vitalList.get(0); + if (v.getTemperature() != null) anthropometry.put("temperatureValue", v.getTemperature()); + if (v.getPulseRate() != null) anthropometry.put("pulseRate", v.getPulseRate()); + if (v.getSystolicBP() != null) anthropometry.put("systolicBP", v.getSystolicBP()); + if (v.getDiastolicBP() != null) anthropometry.put("diastolicBP", v.getDiastolicBP()); + if (v.getBloodGlucoseRandom() != null) anthropometry.put("bloodGlucoseRandom", v.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); - Map anthropometry = new HashMap<>(); - 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")); - if (!anthropometry.isEmpty()) resultMap.put("anthropometry", anthropometry); if (extraFields.containsKey("personFrom")) stopTBDetails.put("personFrom", extraFields.get("personFrom")); if (extraFields.containsKey("caseFindingType")) stopTBDetails.put("caseFindingType", extraFields.get("caseFindingType")); From 9f950207e5396831e061ddaaf1ce5de01d737a23 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 30 Jun 2026 15:29:23 +0530 Subject: [PATCH 58/97] fix(compile): rename variable 'a' to 'anthro' to avoid conflict with outer loop variable Co-Authored-By: Claude Sonnet 4.6 --- .../service/impl/BeneficiaryServiceImpl.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 e311a0e5..b876c42a 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -530,18 +530,18 @@ private String getMappingsForAddressIDs(List addressLi Map anthropometry = new HashMap<>(); if (!anthroList.isEmpty()) { - BenAnthropometryDetail a = anthroList.get(0); - if (a.getHeightCm() != null) anthropometry.put("height", a.getHeightCm()); - if (a.getWeightKg() != null) anthropometry.put("weight", a.getWeightKg()); - if (a.getBmi() != null) anthropometry.put("bmi", a.getBmi()); + 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 v = vitalList.get(0); - if (v.getTemperature() != null) anthropometry.put("temperatureValue", v.getTemperature()); - if (v.getPulseRate() != null) anthropometry.put("pulseRate", v.getPulseRate()); - if (v.getSystolicBP() != null) anthropometry.put("systolicBP", v.getSystolicBP()); - if (v.getDiastolicBP() != null) anthropometry.put("diastolicBP", v.getDiastolicBP()); - if (v.getBloodGlucoseRandom() != null) anthropometry.put("bloodGlucoseRandom", v.getBloodGlucoseRandom()); + 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 From 41f3c4de24e02856d81726603b3ba57d693199e4 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 30 Jun 2026 19:15:03 +0530 Subject: [PATCH 59/97] fix(stoptb): set CreatedDate on t_benvisitdetail insert to prevent NOT NULL constraint violation Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java index a54ebe4f..7a34bd34 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java @@ -38,6 +38,7 @@ public BenVisitDetail getOrCreateVisitForToday(Long beneficiaryRegID, Integer pr 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); From e4f5ecd6d94c677aad5cc3d0e7d14423ec1a51e4 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 30 Jun 2026 20:00:56 +0530 Subject: [PATCH 60/97] fix(anc-pnc): guard CreatedDate on BenVisitDetail insert for ANC and PNC flows If mobile does not send createdDate, ModelMapper leaves it null and MySQL rejects the insert with NOT NULL constraint. Set server timestamp as fallback. Co-Authored-By: Claude Sonnet 4.6 --- .../com/iemr/flw/service/impl/MaternalHealthServiceImpl.java | 4 ++++ 1 file changed, 4 insertions(+) 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 From b9b61549a6b73ead52ae0a506c32d6c1453aaabb Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 1 Jul 2026 09:17:53 +0530 Subject: [PATCH 61/97] fix(docker): set TZ=Asia/Kolkata to prevent UTC/IST date mismatch Without explicit timezone, Docker defaults to UTC. This causes LocalDate.now() in getOrCreateVisitForToday() to use UTC date, so evening IST registrations appear as next-day July 1 in DB and visit lookups fail after UTC midnight. Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) 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 From af98e6685f71fee45d16212419148edbb1a28269 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 1 Jul 2026 17:35:47 +0530 Subject: [PATCH 62/97] fix(stoptb): handle ISO 8601 Z suffix in parseDob to fix beneficiary age display Timestamp.valueOf() throws on "yyyy-MM-dd HH:mm:ss.SSSZ" format because the trailing Z is not a valid nanosecond value. Strip Z before parsing so mobile DOB like "1987-07-01T00:00:00.000Z" is correctly stored in i_ben_flow_outreach.ben_dob. Co-Authored-By: Claude Sonnet 4.6 --- src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index c0148100..38c3acc2 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -985,7 +985,7 @@ private void writePrescription(StopTBGeneralOpd opd, Long beneficiaryRegID, BenV private Timestamp parseDob(JsonObject obj) { if (!obj.has("dob") || obj.get("dob").isJsonNull()) return null; try { return new Timestamp(obj.get("dob").getAsLong()); } catch (Exception ignored) {} - try { return Timestamp.valueOf(obj.get("dob").getAsString().replace("T", " ")); } catch (Exception ignored) {} + try { return Timestamp.valueOf(obj.get("dob").getAsString().replace("T", " ").replace("Z", "").trim()); } catch (Exception ignored) {} return null; } From d848cc10a11ad3746306e0c367b790e07c108932 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 1 Jul 2026 17:56:06 +0530 Subject: [PATCH 63/97] fix(stoptb): fallback to i_ben_flow_outreach.ben_dob when i_beneficiarydetails.dob is null Identity-API mapper (@Mapping source=dto.dob) is commented out, causing dob to be null for all mobile registrations. Fall back to ben_dob from i_ben_flow_outreach so getBeneficiaryData returns correct DOB and mobile shows correct age. Co-Authored-By: Claude Sonnet 4.6 --- .../iemr/flw/service/impl/BeneficiaryServiceImpl.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 b876c42a..dea3e436 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -387,8 +387,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) From 40a3a29dc53720691343b2805e30e24deac08b18 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 1 Jul 2026 18:51:19 +0530 Subject: [PATCH 64/97] fix: use saveAndFlush to prevent FK violation in t_benchiefcomplaint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit save() leaves VisitCode UPDATE pending in JPA session — t_benchiefcomplaint INSERT runs before the UPDATE reaches DB, causing FK constraint failure and full transaction rollback. saveAndFlush forces the UPDATE to DB immediately. Co-Authored-By: Claude Sonnet 4.6 --- .../com/iemr/flw/service/impl/TBStopVisitServiceImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java index 7a34bd34..4f36b8b4 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java @@ -48,7 +48,9 @@ public BenVisitDetail getOrCreateVisitForToday(Long beneficiaryRegID, Integer pr // VisitCode: sessionID(1) + vanID(5-digit) + benVisitId(8-digit) — MMU formula long visitCode = generateVisitCode(visit.getBenVisitId(), vanID != null ? vanID : 1); visit.setVisitCode(visitCode); - return benVisitDetailsRepo.save(visit); + // 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) { From a3d7e46bb931d93097b68f713eec63124831f3d4 Mon Sep 17 00:00:00 2001 From: sehjotsinghunthinkable Date: Thu, 2 Jul 2026 19:23:18 +0530 Subject: [PATCH 65/97] Vb/counselling (#262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enforced mandatory validation for "Reason for Refusal in section e in file TbCounsellingFormSeeder * Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. * Align `/complete` and `/submitBulk` form response editing behavior * Updated `/complete` to resolve `FormResponse` using `beneficiaryId` and `formUuid` from the request body, removing the dependency on `responseId` as a query parameter. * Updated `/submitBulk` section editing to match `/complete` behavior by performing in-place `SectionResponse` upserts and per-question delete-and-reinsert operations, instead of deleting and recreating the entire section. * Add van/sync tracking fields (vanID, parkingPlaceID, vanSerialNo, processed, syncedDate, syncedBy, syncFailureReason) to domain entities for offline mobile data collection support * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(beneficiary): expose GPS coordinates in getBeneficiaryData response Add gpsLatitude, gpsLongitude, digipin, gpsTimestamp, and isGpsUnavailable fields to RMNCHBeneficiaryDetailsRmnch and RMNCHHouseHoldDetails entities, mapping to the columns written by Identity-API's /syncDataToAmrit endpoint. Enable serializeNulls on the Gson builder in BeneficiaryServiceImpl so GPS keys are always present in householdDetails and beneficiaryDetails — null for web-registered records, real values for mobile-registered ones. Co-Authored-By: Claude Sonnet 4.6 * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Revert "Committed 82b00ccf with only the three files:" This reverts commit 3040a08077d82d27dfd86f29f0fbdd4dc094ec56. * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields # Conflicts: # src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java * Updated isMandatory field in Section C, so changed in TbCounsellingFormSeeder * Add optional villageId and providerServiceMapId filters to getCompletedBeneficiaries --------- Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- .../flw/controller/DynamicFormResponseController.java | 9 ++++++--- .../java/com/iemr/flw/repo/iemr/FormResponseRepo.java | 9 +++++++++ .../com/iemr/flw/service/DynamicFormResponseService.java | 4 ++-- .../flw/service/impl/DynamicFormResponseServiceImpl.java | 6 ++++-- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java index 65dd7036..49d8e547 100644 --- a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -98,10 +98,13 @@ 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") + @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) { - List beneficiaryIds = responseService.getCompletedBeneficiaries(formType); + 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/repo/iemr/FormResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java index 02077c9f..66672c16 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java @@ -43,6 +43,15 @@ public interface FormResponseRepo extends JpaRepository { @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 " + diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java index 436cae75..cca29e2e 100644 --- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -61,6 +61,6 @@ public interface DynamicFormResponseService { */ List findPendingFollowUps(List formIds, int delayDays); - /** Returns beneficiary IDs with COMPLETE status for the given form type. */ - List getCompletedBeneficiaries(FormType formType); + /** 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/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index a920cf07..fb303083 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -495,10 +495,12 @@ public List findPendingFollowUps(List formIds, int delayD @Override @Transactional(readOnly = true) - public List getCompletedBeneficiaries(FormType formType) { + 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 formResponseRepo.findBeneficiaryIdsByFormIdAndStatus(form.getFormId(), STATUS_COMPLETE); + return (villageId != null || providerServiceMapId != null) + ? formResponseRepo.findBeneficiaryIdsByFormIdAndStatusFiltered(form.getFormId(), STATUS_COMPLETE, villageId, providerServiceMapId) + : formResponseRepo.findBeneficiaryIdsByFormIdAndStatus(form.getFormId(), STATUS_COMPLETE); } } From 2d5cf2b9b5857a5562074031f5a0450c936d0e0e Mon Sep 17 00:00:00 2001 From: sehjotsinghunthinkable Date: Thu, 2 Jul 2026 19:41:16 +0530 Subject: [PATCH 66/97] Vb/counselling (#263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enforced mandatory validation for "Reason for Refusal in section e in file TbCounsellingFormSeeder * Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. * Align `/complete` and `/submitBulk` form response editing behavior * Updated `/complete` to resolve `FormResponse` using `beneficiaryId` and `formUuid` from the request body, removing the dependency on `responseId` as a query parameter. * Updated `/submitBulk` section editing to match `/complete` behavior by performing in-place `SectionResponse` upserts and per-question delete-and-reinsert operations, instead of deleting and recreating the entire section. * Add van/sync tracking fields (vanID, parkingPlaceID, vanSerialNo, processed, syncedDate, syncedBy, syncFailureReason) to domain entities for offline mobile data collection support * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(beneficiary): expose GPS coordinates in getBeneficiaryData response Add gpsLatitude, gpsLongitude, digipin, gpsTimestamp, and isGpsUnavailable fields to RMNCHBeneficiaryDetailsRmnch and RMNCHHouseHoldDetails entities, mapping to the columns written by Identity-API's /syncDataToAmrit endpoint. Enable serializeNulls on the Gson builder in BeneficiaryServiceImpl so GPS keys are always present in householdDetails and beneficiaryDetails — null for web-registered records, real values for mobile-registered ones. Co-Authored-By: Claude Sonnet 4.6 * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Revert "Committed 82b00ccf with only the three files:" This reverts commit 3040a08077d82d27dfd86f29f0fbdd4dc094ec56. * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields # Conflicts: # src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java * Updated isMandatory field in Section C, so changed in TbCounsellingFormSeeder * Add optional villageId and providerServiceMapId filters to getCompletedBeneficiaries * Removing section f from seeder --------- Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java index 0e956c9c..4fdd3a2d 100644 --- a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java @@ -85,7 +85,7 @@ private DynamicFormDTO buildFormDto() { sections.add(buildSectionC()); sections.add(buildSectionD()); sections.add(buildSectionE()); - sections.add(buildSectionF()); +// sections.add(buildSectionF()); dto.setSections(sections); return dto; } From ead22fd4172ae9be8b92664fcb7f6667e114097c Mon Sep 17 00:00:00 2001 From: sehjotsinghunthinkable Date: Fri, 3 Jul 2026 10:10:47 +0530 Subject: [PATCH 67/97] Vb/counselling (#264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enforced mandatory validation for "Reason for Refusal in section e in file TbCounsellingFormSeeder * Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. * Align `/complete` and `/submitBulk` form response editing behavior * Updated `/complete` to resolve `FormResponse` using `beneficiaryId` and `formUuid` from the request body, removing the dependency on `responseId` as a query parameter. * Updated `/submitBulk` section editing to match `/complete` behavior by performing in-place `SectionResponse` upserts and per-question delete-and-reinsert operations, instead of deleting and recreating the entire section. * Add van/sync tracking fields (vanID, parkingPlaceID, vanSerialNo, processed, syncedDate, syncedBy, syncFailureReason) to domain entities for offline mobile data collection support * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(beneficiary): expose GPS coordinates in getBeneficiaryData response Add gpsLatitude, gpsLongitude, digipin, gpsTimestamp, and isGpsUnavailable fields to RMNCHBeneficiaryDetailsRmnch and RMNCHHouseHoldDetails entities, mapping to the columns written by Identity-API's /syncDataToAmrit endpoint. Enable serializeNulls on the Gson builder in BeneficiaryServiceImpl so GPS keys are always present in householdDetails and beneficiaryDetails — null for web-registered records, real values for mobile-registered ones. Co-Authored-By: Claude Sonnet 4.6 * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Revert "Committed 82b00ccf with only the three files:" This reverts commit 3040a08077d82d27dfd86f29f0fbdd4dc094ec56. * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields # Conflicts: # src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java * Updated isMandatory field in Section C, so changed in TbCounsellingFormSeeder * Add optional villageId and providerServiceMapId filters to getCompletedBeneficiaries * Removing section f from seeder * Removing already existing check in /complete api --------- Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- .../service/impl/DynamicFormResponseServiceImpl.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index fb303083..4f21b8fe 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -130,19 +130,17 @@ public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken FormVersion version = resolveLatestVersion(request.getFormUuid()); Long formId = version.getDynamicForm().getFormId(); + Timestamp now = new Timestamp(System.currentTimeMillis()); List existing = formResponseRepo.findByBeneficiaryIdAndFormId(request.getBeneficiaryId(), formId); - if (existing.isEmpty()) { - throw new NoSuchElementException( - "FormResponse not found for beneficiaryId=" + request.getBeneficiaryId() - + " formUuid=" + request.getFormUuid()); - } - FormResponse formResponse = existing.get(0); + FormResponse formResponse = existing.isEmpty() + ? createFormResponse(request, version, STATUS_SUBMITTED, now) + : existing.get(0); formResponse.setUpdatedBy(actor); formResponse.setStatus(STATUS_COMPLETE); - formResponse.setCompletedAt(new Timestamp(System.currentTimeMillis())); + formResponse.setCompletedAt(now); formResponse = formResponseRepo.save(formResponse); return processSections(formResponse, request, version, actor, vanID, parkingPlaceID); } From 2002dcf93e74a08139eb27f35e8797318363f9b7 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 3 Jul 2026 10:16:06 +0530 Subject: [PATCH 68/97] fix(stoptb): compute age from benDetailsRMNCH_OBJ.dob instead of benDetailsOBJ.dob benDetailsOBJ.dob (i_beneficiarydetails) is null for Stop TB mobile registrations, which is why c22b3fd2 added a fallback that fills benDetailsRMNCH_OBJ.dob from i_ben_flow_outreach. The age calculation block still read the original, still-null benDetailsOBJ.dob, so dob came back correct but age/ageFull stayed 0/"". Point it at the already-corrected benDetailsRMNCH_OBJ.dob instead. Co-Authored-By: Claude Sonnet 5 --- .../com/iemr/flw/service/impl/BeneficiaryServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 dea3e436..ed762790 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -438,9 +438,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); From b66b99178682dd485a2e67524ac26dda11a3f215 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 3 Jul 2026 10:20:59 +0530 Subject: [PATCH 69/97] field missing --- .../iemr/flw/service/impl/DynamicFormResponseServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index 4f21b8fe..28d89fce 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -135,7 +135,7 @@ public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken List existing = formResponseRepo.findByBeneficiaryIdAndFormId(request.getBeneficiaryId(), formId); FormResponse formResponse = existing.isEmpty() - ? createFormResponse(request, version, STATUS_SUBMITTED, now) + ? createFormResponse(request, version, STATUS_SUBMITTED, now, vanID, parkingPlaceID) : existing.get(0); formResponse.setUpdatedBy(actor); From 21e7a85329baa6191281e724a7d101333b43fa78 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 3 Jul 2026 14:50:42 +0530 Subject: [PATCH 70/97] fix(stoptb): scope Suspected/Confirmed getAll by facility+village instead of userId Nurse and Counsellor are different logins hitting the same /tb/suspected/getAll and /tb/confirmed/getAll endpoints, but both queries filtered by the calling user's own userId. So a Counsellor pulling data after a Nurse creates a Suspected/Confirmed record saw none of it - only rows tied to their own login. Adds an optional providerServiceMapID(+villageID)-scoped query path on both endpoints, joined through the already-populated BenFlowStatus table (same pattern as StopTBDiagnosticsRepo), so any role at the same facility/village sees the same worklist. The old userId-based path is kept as the fallback when the new fields aren't sent, so already-deployed app builds are unaffected. --- .../java/com/iemr/flw/controller/TBController.java | 9 ++++++--- .../flw/repo/iemr/TBConfirmedTreatmentRepository.java | 4 ++++ .../java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java | 6 ++++++ .../com/iemr/flw/service/TBConfirmedCaseService.java | 2 ++ .../flw/service/impl/TBConfirmedCaseServiceImpl.java | 10 ++++++++++ .../iemr/flw/service/impl/TBSuspectedServiceImpl.java | 5 ++++- 6 files changed, 32 insertions(+), 4 deletions(-) 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/repo/iemr/TBConfirmedTreatmentRepository.java b/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java index 21c941ff..d333a913 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java @@ -19,6 +19,10 @@ public interface TBConfirmedTreatmentRepository List findByUserId(Integer benId); List findByBenIdAndVisitCode(Long benId, Long visitCode); + @Query("SELECT tc FROM TBConfirmedCase tc WHERE tc.benId IN " + + "(SELECT b.beneficiaryRegID 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") 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 ef3a2222..c7ca3066 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java @@ -27,6 +27,12 @@ TBSuspected getByUserIdAndBenIdAndVisitCode(@Param("benId") Long benId, @Param(" @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.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId) " + + "AND tbs.visitDate >= :fromDate and tbs.visitDate <= :toDate") + List getByProviderServiceMapIdAndVillageId(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId, + @Param("fromDate") Timestamp fromDate, @Param("toDate") Timestamp toDate); + @Transactional @Modifying @Query("UPDATE TBSuspected t SET t.vanSerialNo = t.id WHERE t.id = :id") 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/impl/TBConfirmedCaseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java index c5643c6f..f9240c5c 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java @@ -188,6 +188,16 @@ public String getByBenId(Long benId, String authorisation) throws Exception { public String getByUserId(String authorisation) throws Exception { Integer userId = jwtUtil.extractUserId(authorisation); List list = repository.findByUserId(userId); + 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()); 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 81390d64..7a1646cb 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -86,7 +86,10 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { @Override public String getByUserId(GetBenRequestHandler request) { List dtos = new ArrayList<>(); - List tbSuspectedList = tbSuspectedRepo.getByUserId(request.getAshaId(), request.getFromDate(), request.getToDate()); + List tbSuspectedList = request.getProviderServiceMapID() != null + ? tbSuspectedRepo.getByProviderServiceMapIdAndVillageId(request.getProviderServiceMapID(), request.getVillageID(), + request.getFromDate(), request.getToDate()) + : tbSuspectedRepo.getByUserId(request.getAshaId(), request.getFromDate(), request.getToDate()); for (TBSuspected tbSuspected : tbSuspectedList) { TBSuspectedDTO dto = modelMapper.map(tbSuspected, TBSuspectedDTO.class); dto.setUpdateDate(tbSuspected.getLastModDate()); From 69e48e3435b8b91ac2aee226a6d667cc31b1d600 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 3 Jul 2026 15:07:04 +0530 Subject: [PATCH 71/97] fix(stoptb): drop date filter from facility-scoped Suspected query visitDate isn't the right dimension for a facility worklist - an unresolved Suspected case from months ago is still actionable, and filtering it out by date would hide it from the Counsellor, working against the visibility fix in the previous commit. Also matches StopTBDiagnosticsRepo's facility-scoped query, which never date-filters. Old ashaId-based fallback path is untouched and still date-filters as before. --- src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java | 6 ++---- .../com/iemr/flw/service/impl/TBSuspectedServiceImpl.java | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) 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 c7ca3066..c7b3ab35 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java @@ -28,10 +28,8 @@ TBSuspected getByUserIdAndBenIdAndVisitCode(@Param("benId") Long benId, @Param(" 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.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId) " + - "AND tbs.visitDate >= :fromDate and tbs.visitDate <= :toDate") - List getByProviderServiceMapIdAndVillageId(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId, - @Param("fromDate") Timestamp fromDate, @Param("toDate") Timestamp toDate); + "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId)") + List getByProviderServiceMapIdAndVillageId(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); @Transactional @Modifying 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 7a1646cb..cc0abdd0 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -87,8 +87,7 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { public String getByUserId(GetBenRequestHandler request) { List dtos = new ArrayList<>(); List tbSuspectedList = request.getProviderServiceMapID() != null - ? tbSuspectedRepo.getByProviderServiceMapIdAndVillageId(request.getProviderServiceMapID(), request.getVillageID(), - request.getFromDate(), request.getToDate()) + ? 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); From e86b109beadb8710c0007a52246f5fc89fbf07cb Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 3 Jul 2026 16:14:11 +0530 Subject: [PATCH 72/97] fix(stoptb): join facility-scoped query on beneficiaryID, not beneficiaryRegID TBSuspected.benId / TBConfirmedCase.benId store the person-level beneficiary_id (e.g. 907797933607), not beneficiaryRegID (e.g. 35137) - confirmed by querying i_ben_flow_outreach directly. The facility-scoped query added in the previous two commits joined against BenFlowStatus.beneficiaryRegID, so it never matched anything and always returned empty. Switched the join to BenFlowStatus.beneficiaryID, which is the correct corresponding column. --- .../com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java | 2 +- src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 d333a913..122890e3 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java @@ -20,7 +20,7 @@ public interface TBConfirmedTreatmentRepository List findByBenIdAndVisitCode(Long benId, Long visitCode); @Query("SELECT tc FROM TBConfirmedCase tc WHERE tc.benId IN " + - "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId)") + "(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 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 c7b3ab35..e559bdfb 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java @@ -28,7 +28,7 @@ TBSuspected getByUserIdAndBenIdAndVisitCode(@Param("benId") Long benId, @Param(" 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.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId)") + "(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 From 251e64c062fae626ca35f61cb1d51985ca63a874 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 7 Jul 2026 12:55:33 +0530 Subject: [PATCH 73/97] Add debug logging, RMNCH pinCode field, and GPS fallback pinCode mapping Co-Authored-By: Claude Sonnet 5 --- .../java/com/iemr/flw/controller/BeneficiaryController.java | 2 ++ .../iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java | 3 +++ .../java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java | 2 ++ 3 files changed, 7 insertions(+) diff --git a/src/main/java/com/iemr/flw/controller/BeneficiaryController.java b/src/main/java/com/iemr/flw/controller/BeneficiaryController.java index d439fbf5..1419bfe4 100644 --- a/src/main/java/com/iemr/flw/controller/BeneficiaryController.java +++ b/src/main/java/com/iemr/flw/controller/BeneficiaryController.java @@ -54,7 +54,9 @@ public String getBeneficiaryDataByAsha(@RequestBody GetBenRequestHandler request if (requestDTO != null) { logger.info("request object with timestamp : " + new Timestamp(System.currentTimeMillis()) + " " + requestDTO); + System.out.println("getBeneficiaryData request : " + requestDTO); String s = beneficiaryService.getBenData(requestDTO, authorization); + System.out.println("getBeneficiaryData response : " + s); if (s != null) response.setResponse(s); else 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 dca4f5ee..e227776c 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java @@ -434,6 +434,9 @@ public class RMNCHBeneficiaryDetailsRmnch { @Expose @Transient private String addressLine3; + @Expose + @Transient + private String pinCode; // ---------------------------------------------- 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 ed762790..ebe4c6d9 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -341,6 +341,8 @@ 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) From fa6885605928b744eb297a051c8b0b94b1ba15d4 Mon Sep 17 00:00:00 2001 From: sehjotsinghunthinkable Date: Tue, 7 Jul 2026 13:21:27 +0530 Subject: [PATCH 74/97] Vb/counselling (#269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enforced mandatory validation for "Reason for Refusal in section e in file TbCounsellingFormSeeder * Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. * Align `/complete` and `/submitBulk` form response editing behavior * Updated `/complete` to resolve `FormResponse` using `beneficiaryId` and `formUuid` from the request body, removing the dependency on `responseId` as a query parameter. * Updated `/submitBulk` section editing to match `/complete` behavior by performing in-place `SectionResponse` upserts and per-question delete-and-reinsert operations, instead of deleting and recreating the entire section. * Add van/sync tracking fields (vanID, parkingPlaceID, vanSerialNo, processed, syncedDate, syncedBy, syncFailureReason) to domain entities for offline mobile data collection support * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(beneficiary): expose GPS coordinates in getBeneficiaryData response Add gpsLatitude, gpsLongitude, digipin, gpsTimestamp, and isGpsUnavailable fields to RMNCHBeneficiaryDetailsRmnch and RMNCHHouseHoldDetails entities, mapping to the columns written by Identity-API's /syncDataToAmrit endpoint. Enable serializeNulls on the Gson builder in BeneficiaryServiceImpl so GPS keys are always present in householdDetails and beneficiaryDetails — null for web-registered records, real values for mobile-registered ones. Co-Authored-By: Claude Sonnet 4.6 * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Revert "Committed 82b00ccf with only the three files:" This reverts commit 3040a08077d82d27dfd86f29f0fbdd4dc094ec56. * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Updated isMandatory field in Section C, so changed in TbCounsellingFormSeeder * Add optional villageId and providerServiceMapId filters to getCompletedBeneficiaries * Removing section f from seeder * Removing already existing check in /complete api * Add a CHECKBOX question type and a TB_COUNSELLING_V2 seeder with a GENERAL_INFO consent gate ahead of the counselling sections. * Seed single-checkbox questions for Sections A-D and point the Counselled lookup at TB_COUNSELLING_V2 instead of the retired V1 form * Mark form/section status REFUSED when TB2_GI_Q1=NO on /complete, split getCompletedBeneficiaries into completed/refused lists, and extract FormResponseStatus enum * Resolved conflicts * fix(stoptb): getBeneficiaryData reads anthropometry/vitals from standard tables - getBeneficiaryData now reads height/weight/bmi from t_phy_anthropometry and temperature/pulse/BP/RBS from t_phy_vitals (latest row per beneficiary) - Fallback to otherFields if exam not yet saved for the beneficiary - Stop TB specific fields (personFrom, caseFindingType etc.) still from otherFields - Add findByBeneficiaryRegIDOrderByCreatedDateDesc and findByBenVisitID to BenAnthropometryRepo and BenPhysicalVitalRepo - Add StopTB_Future_Roadmap.md for future HWC integration and edit support Co-Authored-By: Claude Sonnet 4.6 * fix(compile): rename variable 'a' to 'anthro' to avoid conflict with outer loop variable Co-Authored-By: Claude Sonnet 4.6 * fix(stoptb): set CreatedDate on t_benvisitdetail insert to prevent NOT NULL constraint violation Co-Authored-By: Claude Sonnet 4.6 * fix(anc-pnc): guard CreatedDate on BenVisitDetail insert for ANC and PNC flows If mobile does not send createdDate, ModelMapper leaves it null and MySQL rejects the insert with NOT NULL constraint. Set server timestamp as fallback. Co-Authored-By: Claude Sonnet 4.6 * fix(docker): set TZ=Asia/Kolkata to prevent UTC/IST date mismatch Without explicit timezone, Docker defaults to UTC. This causes LocalDate.now() in getOrCreateVisitForToday() to use UTC date, so evening IST registrations appear as next-day July 1 in DB and visit lookups fail after UTC midnight. Co-Authored-By: Claude Sonnet 4.6 * fix(stoptb): handle ISO 8601 Z suffix in parseDob to fix beneficiary age display Timestamp.valueOf() throws on "yyyy-MM-dd HH:mm:ss.SSSZ" format because the trailing Z is not a valid nanosecond value. Strip Z before parsing so mobile DOB like "1987-07-01T00:00:00.000Z" is correctly stored in i_ben_flow_outreach.ben_dob. Co-Authored-By: Claude Sonnet 4.6 * fix(stoptb): fallback to i_ben_flow_outreach.ben_dob when i_beneficiarydetails.dob is null Identity-API mapper (@Mapping source=dto.dob) is commented out, causing dob to be null for all mobile registrations. Fall back to ben_dob from i_ben_flow_outreach so getBeneficiaryData returns correct DOB and mobile shows correct age. Co-Authored-By: Claude Sonnet 4.6 * fix: use saveAndFlush to prevent FK violation in t_benchiefcomplaint save() leaves VisitCode UPDATE pending in JPA session — t_benchiefcomplaint INSERT runs before the UPDATE reaches DB, causing FK constraint failure and full transaction rollback. saveAndFlush forces the UPDATE to DB immediately. Co-Authored-By: Claude Sonnet 4.6 * fix(stoptb): compute age from benDetailsRMNCH_OBJ.dob instead of benDetailsOBJ.dob benDetailsOBJ.dob (i_beneficiarydetails) is null for Stop TB mobile registrations, which is why c22b3fd2 added a fallback that fills benDetailsRMNCH_OBJ.dob from i_ben_flow_outreach. The age calculation block still read the original, still-null benDetailsOBJ.dob, so dob came back correct but age/ageFull stayed 0/"". Point it at the already-corrected benDetailsRMNCH_OBJ.dob instead. Co-Authored-By: Claude Sonnet 5 * fix(stoptb): scope Suspected/Confirmed getAll by facility+village instead of userId Nurse and Counsellor are different logins hitting the same /tb/suspected/getAll and /tb/confirmed/getAll endpoints, but both queries filtered by the calling user's own userId. So a Counsellor pulling data after a Nurse creates a Suspected/Confirmed record saw none of it - only rows tied to their own login. Adds an optional providerServiceMapID(+villageID)-scoped query path on both endpoints, joined through the already-populated BenFlowStatus table (same pattern as StopTBDiagnosticsRepo), so any role at the same facility/village sees the same worklist. The old userId-based path is kept as the fallback when the new fields aren't sent, so already-deployed app builds are unaffected. * fix(stoptb): drop date filter from facility-scoped Suspected query visitDate isn't the right dimension for a facility worklist - an unresolved Suspected case from months ago is still actionable, and filtering it out by date would hide it from the Counsellor, working against the visibility fix in the previous commit. Also matches StopTBDiagnosticsRepo's facility-scoped query, which never date-filters. Old ashaId-based fallback path is untouched and still date-filters as before. * fix(stoptb): join facility-scoped query on beneficiaryID, not beneficiaryRegID TBSuspected.benId / TBConfirmedCase.benId store the person-level beneficiary_id (e.g. 907797933607), not beneficiaryRegID (e.g. 35137) - confirmed by querying i_ben_flow_outreach directly. The facility-scoped query added in the previous two commits joined against BenFlowStatus.beneficiaryRegID, so it never matched anything and always returned empty. Switched the join to BenFlowStatus.beneficiaryID, which is the correct corresponding column. * Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. * Revert "Committed 82b00ccf with only the three files:" This reverts commit 3040a08077d82d27dfd86f29f0fbdd4dc094ec56. * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields --------- Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 Co-authored-by: vishwab1 --- .../DynamicFormResponseController.java | 10 +- .../com/iemr/flw/domain/iemr/FormSection.java | 11 +- .../iemr/flw/domain/iemr/SectionQuestion.java | 2 +- .../dto/iemr/CompletedBeneficiariesDTO.java | 44 +++ .../com/iemr/flw/dto/iemr/FormSectionDTO.java | 7 +- .../iemr/flw/dto/iemr/SectionResponseDTO.java | 2 + .../flw/masterEnum/FormResponseStatus.java | 36 +++ .../com/iemr/flw/masterEnum/FormType.java | 3 +- .../com/iemr/flw/masterEnum/QuestionType.java | 3 + .../com/iemr/flw/masterEnum/SectionPhase.java | 37 +++ .../iemr/flw/repo/iemr/FormResponseRepo.java | 8 + .../flw/seeder/TbCounsellingFormSeeder.java | 15 +- .../flw/seeder/TbCounsellingV2FormSeeder.java | 287 ++++++++++++++++++ .../service/DynamicFormResponseService.java | 10 +- .../impl/DynamicFormResponseServiceImpl.java | 124 ++++++-- .../service/impl/FormResponseItemSaver.java | 36 ++- 16 files changed, 574 insertions(+), 61 deletions(-) create mode 100644 src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java create mode 100644 src/main/java/com/iemr/flw/masterEnum/FormResponseStatus.java create mode 100644 src/main/java/com/iemr/flw/masterEnum/SectionPhase.java create mode 100644 src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java index 49d8e547..33ac46b2 100644 --- a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -21,6 +21,7 @@ */ package com.iemr.flw.controller; +import com.iemr.flw.dto.iemr.CompletedBeneficiariesDTO; import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; import com.iemr.flw.masterEnum.FormType; @@ -53,7 +54,7 @@ public class DynamicFormResponseController { private final DynamicFormResponseService responseService; - @Operation(summary = "Submit PRE_SUBMIT section answers, status → SUBMITTED. " + + @Operation(summary = "Submit GENERAL_INFO and 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( @@ -98,13 +99,14 @@ 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") + @Operation(summary = "Get beneficiary IDs that have a COMPLETE or REFUSED 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)); + CompletedBeneficiariesDTO result = + responseService.getCompletedBeneficiaries(formType, villageId, providerServiceMapId); + return ResponseEntity.ok(new ApiResponse(true, "Completed beneficiaries fetched successfully", result)); } } diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java index b8454716..28f50859 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java @@ -21,9 +21,12 @@ */ package com.iemr.flw.domain.iemr; +import com.iemr.flw.masterEnum.SectionPhase; 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; @@ -71,9 +74,9 @@ public class FormSection { @Column(name = "sectionName_hindi", length = 255) private String sectionNameHindi; - /** PRE_SUBMIT sections are shown before the form's main submit. POST_SUBMIT after. */ + @Enumerated(EnumType.STRING) @Column(name = "sectionPhase", nullable = false, length = 20) - private String sectionPhase; + private SectionPhase sectionPhase; @Column(name = "isRequired", nullable = false) private Boolean isRequired = true; @@ -81,10 +84,12 @@ public class FormSection { @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 = "isEditable", nullable = false) + private Boolean isEditable = false; + @Column(name = "created_by", length = 100) private String createdBy; diff --git a/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java index 1bd7991b..91cc3c43 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java @@ -47,7 +47,7 @@ /** * A question within a form section. - * questionType: RADIO | MCQ | TEXT | DATE | DISPLAY | AUTO_FILL + * questionType: RADIO | MCQ | CHECKBOX | TEXT | DATE | DISPLAY | AUTO_FILL */ @Entity @Table(name = "t_section_question", schema = "db_iemr") diff --git a/src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java b/src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java new file mode 100644 index 00000000..b391cff0 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java @@ -0,0 +1,44 @@ +/* + * 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.util.List; + +/** + * Beneficiary IDs for a form type, split by outcome: those who completed the form and those + * who refused (TB2_GI_Q1 = NO on the General Info consent question). + * + * @author Piramal Swasthya + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CompletedBeneficiariesDTO { + private List completed; + private List refused; +} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java b/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java index 2db1f8c7..6bb10795 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java @@ -21,6 +21,7 @@ */ package com.iemr.flw.dto.iemr; +import com.iemr.flw.masterEnum.SectionPhase; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -49,8 +50,8 @@ public class FormSectionDTO { private String sectionNameHindi; - @NotBlank(message = "sectionPhase is required (PRE_SUBMIT or POST_SUBMIT)") - private String sectionPhase; + @NotNull(message = "sectionPhase is required (GENERAL_INFO, PRE_SUBMIT, or POST_SUBMIT)") + private SectionPhase sectionPhase; private Boolean isRequired = true; @@ -59,6 +60,8 @@ public class FormSectionDTO { private Boolean hasSubmitButton = false; + private Boolean isEditable = false; + @Valid private List questions = 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 index dac289a7..78c918fe 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/SectionResponseDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/SectionResponseDTO.java @@ -42,6 +42,8 @@ public class SectionResponseDTO { private Long sectionResponseId; private Long sectionId; + private String sectionUuid; + private Boolean isEditable; private String status; private Timestamp savedAt; private List answers; diff --git a/src/main/java/com/iemr/flw/masterEnum/FormResponseStatus.java b/src/main/java/com/iemr/flw/masterEnum/FormResponseStatus.java new file mode 100644 index 00000000..62fffd00 --- /dev/null +++ b/src/main/java/com/iemr/flw/masterEnum/FormResponseStatus.java @@ -0,0 +1,36 @@ +/* + * 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; + +/** + * Lifecycle status stored in FormResponse.status. + * + * @author Piramal Swasthya + */ +public enum FormResponseStatus { + /** Saved via /submit. */ + SUBMITTED, + /** Saved via /complete, consent question answered YES (or not applicable). */ + COMPLETE, + /** Saved via /complete, consent question TB2_GI_Q1 answered NO. */ + REFUSED +} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/masterEnum/FormType.java b/src/main/java/com/iemr/flw/masterEnum/FormType.java index 779fce19..ccb0cfe2 100644 --- a/src/main/java/com/iemr/flw/masterEnum/FormType.java +++ b/src/main/java/com/iemr/flw/masterEnum/FormType.java @@ -30,5 +30,6 @@ public enum FormType { COUNSELLING_MODULE, SCREENING, COUNSELLING, - TB_COUNSELLING + TB_COUNSELLING, + TB_COUNSELLING_V2 } diff --git a/src/main/java/com/iemr/flw/masterEnum/QuestionType.java b/src/main/java/com/iemr/flw/masterEnum/QuestionType.java index ae4b91e1..0af07aa1 100644 --- a/src/main/java/com/iemr/flw/masterEnum/QuestionType.java +++ b/src/main/java/com/iemr/flw/masterEnum/QuestionType.java @@ -32,6 +32,9 @@ public enum QuestionType { RADIO, /** Multi-select from a predefined list of options. */ MCQ, + /** Single boolean checkbox — checked or unchecked. May carry options for display + * purposes; answers are still stored as free text. */ + CHECKBOX, /** Free-text input. */ TEXT, /** Date picker input. */ diff --git a/src/main/java/com/iemr/flw/masterEnum/SectionPhase.java b/src/main/java/com/iemr/flw/masterEnum/SectionPhase.java new file mode 100644 index 00000000..13faa60d --- /dev/null +++ b/src/main/java/com/iemr/flw/masterEnum/SectionPhase.java @@ -0,0 +1,37 @@ +/* + * 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 phases a form section can belong to. + * No other values may be stored in t_form_section.sectionPhase. + * + * @author Piramal Swasthya + */ +public enum SectionPhase { + /** Shown first, before PRE_SUBMIT sections. */ + GENERAL_INFO, + /** Shown before the form's main submit. Saved via /submit. */ + PRE_SUBMIT, + /** Shown after the main submit. Saved via /complete. */ + POST_SUBMIT +} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java index 66672c16..e902ee94 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java @@ -60,6 +60,14 @@ List findCounselledBenIds(@Param("benIds") List benIds, @Param("formId") Long formId, @Param("status") String status); + @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' " diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java index 4fdd3a2d..ecddba20 100644 --- a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java @@ -29,6 +29,7 @@ import com.iemr.flw.dto.iemr.SectionQuestionDTO; import com.iemr.flw.masterEnum.FormType; import com.iemr.flw.masterEnum.QuestionType; +import com.iemr.flw.masterEnum.SectionPhase; import com.iemr.flw.masterEnum.ValidationType; import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.service.DynamicFormDefinitionService; @@ -93,7 +94,7 @@ private DynamicFormDTO buildFormDto() { // ── Section A: Disease Awareness ───────────────────────────────────────────── private FormSectionDTO buildSectionA() { - FormSectionDTO s = section("TB_SEC_A", "Disease Awareness", "बीमारी के बारे में जागरूकता", "PRE_SUBMIT", 1, true, false); + FormSectionDTO s = section("TB_SEC_A", "Disease Awareness", "बीमारी के बारे में जागरूकता", SectionPhase.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)); @@ -107,7 +108,7 @@ private FormSectionDTO buildSectionA() { // ── 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); + FormSectionDTO s = section("TB_SEC_B", "Do's and Don'ts", "करो और ना करो", SectionPhase.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)); @@ -123,7 +124,7 @@ private FormSectionDTO buildSectionB() { // ── Section C: Government Schemes ──────────────────────────────────────────── private FormSectionDTO buildSectionC() { - FormSectionDTO s = section("TB_SEC_C", "Government Schemes", "सरकारी योजनाएं", "PRE_SUBMIT", 3, false, false); + FormSectionDTO s = section("TB_SEC_C", "Government Schemes", "सरकारी योजनाएं", SectionPhase.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)); @@ -135,7 +136,7 @@ private FormSectionDTO buildSectionC() { // ── Section D: Treatment Regimen ───────────────────────────────────────────── private FormSectionDTO buildSectionD() { - FormSectionDTO s = section("TB_SEC_D", "Treatment Regimen", "इलाज का तरीका", "PRE_SUBMIT", 4, true, false); + FormSectionDTO s = section("TB_SEC_D", "Treatment Regimen", "इलाज का तरीका", SectionPhase.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)); @@ -149,7 +150,7 @@ private FormSectionDTO buildSectionD() { // ── Section E: Counselling Completion ──────────────────────────────────────── private FormSectionDTO buildSectionE() { - FormSectionDTO s = section("TB_SEC_E", "Counselling Completion", "काउंसलिंग पूरी होना", "PRE_SUBMIT", 5, true, true); + FormSectionDTO s = section("TB_SEC_E", "Counselling Completion", "काउंसलिंग पूरी होना", SectionPhase.PRE_SUBMIT, 5, true, true); List qs = new ArrayList<>(); // Counselling completion status — RADIO: Complete | Refused @@ -194,7 +195,7 @@ private FormSectionDTO buildSectionE() { // ── 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); + FormSectionDTO s = section("TB_SEC_F", "Follow Up to TU", "TU के बाद की कार्रवाई", SectionPhase.POST_SUBMIT, 6, true, true); List qs = new ArrayList<>(); // "Has the patient started TB treatment?" — RADIO: Yes | No @@ -244,7 +245,7 @@ private FormSectionDTO buildSectionF() { // ── Builder Helpers ─────────────────────────────────────────────────────────── - private FormSectionDTO section(String uuid, String name, String nameHindi, String phase, + private FormSectionDTO section(String uuid, String name, String nameHindi, SectionPhase phase, int order, boolean required, boolean hasSubmitButton) { FormSectionDTO s = new FormSectionDTO(); s.setSectionUuid(uuid); diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java new file mode 100644 index 00000000..0173db59 --- /dev/null +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java @@ -0,0 +1,287 @@ +/* + * 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.SectionPhase; +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 (v2) form definition on application startup if it does not already exist. + * Idempotent: skips creation if the form UUID is already present in the database. + * + * Adds an initial GENERAL_INFO consent gate ("Has the beneficiary agreed for counselling?") ahead + * of the PRE_SUBMIT sections, and uses CHECKBOX questions in place of Yes/No RADIO questions. + * + * @author Piramal Swasthya + */ +@Component +@RequiredArgsConstructor +public class TbCounsellingV2FormSeeder { + + private static final Logger log = LoggerFactory.getLogger(TbCounsellingV2FormSeeder.class); + + public static final String FORM_UUID = "TB_COUNSELLING_V2"; + + private final DynamicFormDefinitionService formService; + private final DynamicFormRepo formRepo; + + @PostConstruct + public void seed() { + if (formRepo.findByFormUuid(FORM_UUID).isPresent()) { + log.info("TbCounsellingV2FormSeeder: form '{}' already exists — skipping seed.", FORM_UUID); + return; + } + log.info("TbCounsellingV2FormSeeder: seeding TB Counselling (v2) form..."); + formService.createForm(buildFormDto()); + log.info("TbCounsellingV2FormSeeder: seed complete."); + } + + // ── Form Definition ────────────────────────────────────────────────────────── + + private DynamicFormDTO buildFormDto() { + DynamicFormDTO dto = new DynamicFormDTO(); + dto.setFormUuid(FORM_UUID); + dto.setFormName("TB Counselling"); + dto.setFormType(FormType.TB_COUNSELLING_V2); + dto.setIsActive(true); + dto.setFollowUpDelayDays(15); + + List sections = new ArrayList<>(); + sections.add(buildGeneralInfoSection()); + sections.add(buildSectionA()); + sections.add(buildSectionB()); + sections.add(buildSectionC()); + sections.add(buildSectionD()); + sections.add(buildSectionE()); + dto.setSections(sections); + return dto; + } + + // ── General Info: Counselling Consent ──────────────────────────────────────── + + private FormSectionDTO buildGeneralInfoSection() { + FormSectionDTO s = section("TB2_SEC_GENERAL_INFO", "General Information", "सामान्य जानकारी", + SectionPhase.GENERAL_INFO, 1, true, false, true); + List qs = new ArrayList<>(); + + // "Has the beneficiary agreed for counselling?" — RADIO: Yes | No + SectionQuestionDTO consentQ = new SectionQuestionDTO(); + consentQ.setQuestionUuid("TB2_GI_Q1"); + consentQ.setQuestionText("Has the beneficiary agreed for counselling?"); + consentQ.setQuestionTextHindi("क्या लाभार्थी काउंसलिंग के लिए सहमत हुआ है?"); + consentQ.setQuestionType(QuestionType.RADIO); + consentQ.setIsMandatory(true); + consentQ.setDisplayOrder(1); + consentQ.setVisibleByDefault(true); + + // "Yes" option — no conditions, sections A-E remain enabled by default + QuestionOptionDTO yesOpt = option("Yes", "हाँ", "YES", "हाँ", 1, List.of()); + + // "No" option — disable validation for sections A-E + show refusal reason field + List noConditions = new ArrayList<>(); + noConditions.add(disableSectionValidation("TB2_SEC_A")); + noConditions.add(disableSectionValidation("TB2_SEC_B")); + noConditions.add(disableSectionValidation("TB2_SEC_C")); + noConditions.add(disableSectionValidation("TB2_SEC_D")); + noConditions.add(disableSectionValidation("TB2_SEC_E")); + noConditions.add(showQuestion("TB2_GI_REFUSAL")); + QuestionOptionDTO noOpt = option("No", "नहीं", "NO", "नहीं", 2, noConditions); + + List consentOptions = new ArrayList<>(); + consentOptions.add(yesOpt); + consentOptions.add(noOpt); + consentQ.setOptions(consentOptions); + consentQ.setValidations(List.of()); + qs.add(consentQ); + + // Reason for refusal — hidden by default, mandatory when shown, max 300 chars + qs.add(textQuestion("TB2_GI_REFUSAL", "Reason for refusal", "इनकार का कारण", 2, true, 300, false)); + + s.setQuestions(qs); + return s; + } + + // ── Section A: Disease Awareness ───────────────────────────────────────────── + + private FormSectionDTO buildSectionA() { + FormSectionDTO s = section("TB2_SEC_A", "Disease Awareness", "बीमारी के बारे में जागरूकता", SectionPhase.PRE_SUBMIT, 2, true, false, false); + List qs = new ArrayList<>(); + qs.add(checkboxQuestion("TB2_A_Q1", "TB disease explained to patient", "मरीज़ को टीबी बीमारी के बारे में समझाया गया।", 1, true)); + qs.add(checkboxQuestion("TB2_A_Q2", "Transmission route explained", "ट्रांसमिशन के तरीके के बारे में जानकारी", 2, true)); + qs.add(checkboxQuestion("TB2_A_Q3", "Symptoms explained", "लक्षणों की जानकारी", 3, true)); + qs.add(checkboxQuestion("TB2_A_Q4", "Treatment duration explained", "इलाज की अवधि के बारे में जानकारी", 4, true)); + qs.add(textQuestion("TB2_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("TB2_SEC_B", "Do's and Don'ts", "करो और ना करो", SectionPhase.PRE_SUBMIT, 3, true, false, false); + List qs = new ArrayList<>(); + qs.add(checkboxQuestion("TB2_B_Q1", "Cover mouth while coughing — advised", "खांसते समय मुंह ढकने की सलाह दी जाती है।", 1, true)); + qs.add(checkboxQuestion("TB2_B_Q2", "Complete full treatment course — advised", "इलाज का पूरा कोर्स पूरा करने की सलाह दी जाती है।", 2, true)); + qs.add(checkboxQuestion("TB2_B_Q3", "Regular follow-up attendance — advised", "इलाज का पूरा कोर्स पूरा करने की सलाह दी जाती है।", 3, true)); + qs.add(checkboxQuestion("TB2_B_Q4", "Nutritional guidance provided", "पोषण संबंधी मार्गदर्शन प्रदान किया गया", 4, true)); + qs.add(checkboxQuestion("TB2_B_Q5", "No smoking / alcohol — advised", "धूम्रपान / शराब न लेने की सलाह दी जाती है।", 5, true)); + qs.add(checkboxQuestion("TB2_B_Q6", "Isolation precautions explained", "अलगाव सावधानियों के बारे में बताया गया", 6, true)); + qs.add(textQuestion("TB2_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("TB2_SEC_C", "Government Schemes", "सरकारी योजनाएं", SectionPhase.PRE_SUBMIT, 4, true, false, false); + List qs = new ArrayList<>(); + qs.add(checkboxQuestion("TB2_C_Q1", "Nikshay Poshan Yojana (NPY) eligibility explained", "निक्षय पोषण योजना (एनपीवाई) पात्रता के बारे में बताया गया", 1, true)); + qs.add(checkboxQuestion("TB2_C_Q2", "DOTS free treatment explained", "DOTS मुफ़्त इलाज के बारे में जानकारी", 2, true)); + qs.add(textQuestion("TB2_C_REMARKS", "Schemes notes", "योजनाओं के नोट्स", 3, false, 300, true)); + s.setQuestions(qs); + return s; + } + + // ── Section D: Treatment Regimen ───────────────────────────────────────────── + + private FormSectionDTO buildSectionD() { + FormSectionDTO s = section("TB2_SEC_D", "Treatment Regimen", "इलाज का तरीका", SectionPhase.PRE_SUBMIT, 5, true, false, false); + List qs = new ArrayList<>(); + qs.add(checkboxQuestion("TB2_D_Q1", "Regimen explained to patient", "मरीज़ को इलाज का तरीका समझाया गया।", 1, true)); + qs.add(checkboxQuestion("TB2_D_Q2", "Medication names explained", "दवाओं के नामों की जानकारी", 2, true)); + qs.add(checkboxQuestion("TB2_D_Q3", "Side effects explained", "साइड इफ़ेक्ट्स के बारे में जानकारी", 3, true)); + qs.add(checkboxQuestion("TB2_D_Q4", "Importance of adherence explained", "निर्देशों का पालन करने का महत्व समझाया गया", 4, true)); + qs.add(textQuestion("TB2_D_REMARKS", "Treatment regimen notes", "इलाज के तरीके से जुड़े नोट्स", 5, false, 300, true)); + s.setQuestions(qs); + return s; + } + + // ── Section E: Impact Assessment ───────────────────────────────────────────── + + private FormSectionDTO buildSectionE() { + FormSectionDTO s = section("TB2_SEC_E", "Impact Assessment", "प्रभाव का आकलन", SectionPhase.PRE_SUBMIT, 6, true, true, true); + List qs = new ArrayList<>(); + qs.add(textQuestion("TB2_E_Q1", + "How has TB disease impacted the life of this beneficiary", + "टीबी बीमारी ने इस लाभार्थी के जीवन को कैसे प्रभावित किया है", + 1, false, 400, true)); + s.setQuestions(qs); + return s; + } + + // ── Builder Helpers ─────────────────────────────────────────────────────────── + + private FormSectionDTO section(String uuid, String name, String nameHindi, SectionPhase phase, + int order, boolean required, boolean hasSubmitButton, boolean isEditable) { + 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.setIsEditable(isEditable); + s.setQuestions(new ArrayList<>()); + return s; + } + + private SectionQuestionDTO checkboxQuestion(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.CHECKBOX); + q.setIsMandatory(mandatory); + q.setDisplayOrder(order); + q.setVisibleByDefault(true); + q.setOptions(List.of(option("Checked", "चेक किया गया", "CHECKED", "चेक किया गया", 1, List.of()))); + 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java index cca29e2e..61a3bce1 100644 --- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -21,6 +21,7 @@ */ package com.iemr.flw.service; +import com.iemr.flw.dto.iemr.CompletedBeneficiariesDTO; import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; import com.iemr.flw.masterEnum.FormType; @@ -35,7 +36,7 @@ */ public interface DynamicFormResponseService { - /** Save PRE_SUBMIT section answers and advance status to SUBMITTED. */ + /** Save GENERAL_INFO and PRE_SUBMIT section answers and advance status to SUBMITTED. */ FormResponseDTO submitForm(FormResponseRequest request); /** Save POST_SUBMIT section answers and advance status to COMPLETE. */ @@ -61,6 +62,9 @@ public interface DynamicFormResponseService { */ 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); + /** + * Returns beneficiary IDs for the given form type split by outcome — COMPLETE and REFUSED — + * optionally filtered by village and/or provider service map. + */ + CompletedBeneficiariesDTO getCompletedBeneficiaries(FormType formType, Integer villageId, Integer providerServiceMapId); } diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index 28d89fce..f6b26d4a 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -28,6 +28,7 @@ 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.CompletedBeneficiariesDTO; import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; import com.iemr.flw.dto.iemr.QuestionAnswerRequest; @@ -35,6 +36,8 @@ 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.FormResponseStatus; +import com.iemr.flw.masterEnum.FormResponseStatus; import com.iemr.flw.masterEnum.FormType; import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.repo.iemr.FormResponseRepo; @@ -45,6 +48,7 @@ import com.iemr.flw.repo.iemr.SectionQuestionRepo; import com.iemr.flw.repo.iemr.SectionResponseRepo; import com.iemr.flw.masterEnum.QuestionType; +import com.iemr.flw.masterEnum.SectionPhase; import com.iemr.flw.service.CampConfigService; import com.iemr.flw.service.DynamicFormResponseService; import com.iemr.flw.utils.JwtUtil; @@ -76,10 +80,11 @@ @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 static final String SECTION_STATUS_REFUSED = "REFUSED"; + private static final String SECTION_UUID_GENERAL_INFO = "TB2_SEC_GENERAL_INFO"; + private static final String QUESTION_UUID_CONSENT = "TB2_GI_Q1"; + private static final String OPTION_VALUE_CONSENT_REFUSED = "NO"; private final DynamicFormRepo dynamicFormRepo; private final FormResponseRepo formResponseRepo; @@ -106,19 +111,19 @@ public FormResponseDTO submitForm(FormResponseRequest request) { formResponse = formResponseRepo.findById(request.getResponseId()) .orElseThrow(() -> new NoSuchElementException( "FormResponse not found: " + request.getResponseId())); - if (STATUS_COMPLETE.equals(formResponse.getStatus())) { + if (FormResponseStatus.COMPLETE.name().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.setStatus(FormResponseStatus.SUBMITTED.name()); formResponse.setSubmittedAt(now); formResponse.setLastFollowUpAt(now); formResponse = formResponseRepo.save(formResponse); } else { - formResponse = createFormResponse(request, version, STATUS_SUBMITTED, now, vanID, parkingPlaceID); + formResponse = createFormResponse(request, version, FormResponseStatus.SUBMITTED.name(), now, vanID, parkingPlaceID); } - return processSections(formResponse, request, version, null, vanID, parkingPlaceID); + return processSections(formResponse, request, version, null, false, vanID, parkingPlaceID); } @Override @@ -135,14 +140,16 @@ public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken List existing = formResponseRepo.findByBeneficiaryIdAndFormId(request.getBeneficiaryId(), formId); FormResponse formResponse = existing.isEmpty() - ? createFormResponse(request, version, STATUS_SUBMITTED, now, vanID, parkingPlaceID) + ? createFormResponse(request, version, FormResponseStatus.SUBMITTED.name(), now, vanID, parkingPlaceID) : existing.get(0); formResponse.setUpdatedBy(actor); - formResponse.setStatus(STATUS_COMPLETE); + formResponse.setStatus(isConsentRefused(request) + ? FormResponseStatus.REFUSED.name() + : FormResponseStatus.COMPLETE.name()); formResponse.setCompletedAt(now); formResponse = formResponseRepo.save(formResponse); - return processSections(formResponse, request, version, actor, vanID, parkingPlaceID); + return processSections(formResponse, request, version, actor, true, vanID, parkingPlaceID); } @Override @@ -208,6 +215,7 @@ private FormResponseDTO processSections( FormResponseRequest request, FormVersion version, String actor, + boolean applyRefusalRule, Integer vanID, Integer parkingPlaceID) { @@ -252,10 +260,17 @@ private FormResponseDTO processSections( "' not found in form '" + request.getFormUuid() + "'"); } - SectionResponse sectionResponse = upsertSectionResponse( - formResponse.getResponseId(), section.getSectionId(), actor, vanID, parkingPlaceID); + String desiredStatus = applyRefusalRule + ? determineSectionStatus(section, sectionReq.getAnswers()) + : SECTION_STATUS_DONE; + Optional sectionResponseOpt = upsertSectionResponse( + formResponse.getResponseId(), section, actor, desiredStatus, vanID, parkingPlaceID); + if (sectionResponseOpt.isEmpty()) { + continue; + } + SectionResponse sectionResponse = sectionResponseOpt.get(); - if ("POST_SUBMIT".equals(section.getSectionPhase())) { + if (section.getSectionPhase() == SectionPhase.POST_SUBMIT) { formResponse.setLastFollowUpAt(sectionResponse.getSavedAt()); formResponseRepo.save(formResponse); } @@ -274,35 +289,72 @@ private FormResponseDTO processSections( questionResponseRepo.saveAll(questionResponses); - sectionDTOs.add(buildSectionResponseDTO(sectionResponse, questionResponses)); + sectionDTOs.add(buildSectionResponseDTO(sectionResponse, section, questionResponses)); } return buildFormResponseDTO(formResponse, sectionDTOs); } - private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor, - Integer vanID, Integer parkingPlaceID) { + private Optional upsertSectionResponse( + Long responseId, FormSection section, String actor, String status, Integer vanID, Integer parkingPlaceID) { Optional existing = - sectionResponseRepo.findByResponseIdAndSectionId(responseId, sectionId); + sectionResponseRepo.findByResponseIdAndSectionId(responseId, section.getSectionId()); if (existing.isPresent()) { + if (Boolean.FALSE.equals(section.getIsEditable())) { + log.info("Section '{}' is not editable — skipping update for responseId={}", + section.getSectionUuid(), responseId); + return Optional.empty(); + } SectionResponse sr = existing.get(); - sr.setStatus(SECTION_STATUS_DONE); + sr.setStatus(status); sr.setSavedAt(new Timestamp(System.currentTimeMillis())); sr.setUpdatedBy(actor); if (sr.getVanID() == null) { sr.setVanID(vanID); sr.setParkingPlaceID(parkingPlaceID); } - return sectionResponseRepo.save(sr); + return Optional.of(sectionResponseRepo.save(sr)); } SectionResponse sr = SectionResponse.builder() .responseId(responseId) - .sectionId(sectionId) - .status(SECTION_STATUS_DONE) + .sectionId(section.getSectionId()) + .status(status) .savedAt(new Timestamp(System.currentTimeMillis())) .createdBy(actor) .updatedBy(actor) .vanID(vanID) .parkingPlaceID(parkingPlaceID) .build(); - return sectionResponseRepo.save(sr); + return Optional.of(sectionResponseRepo.save(sr)); + } + + // TB2_SEC_GENERAL_INFO's consent question (TB2_GI_Q1) gates the rest of the TB Counselling + // form; a "NO" answer means the beneficiary refused counselling. Absence-tolerant by design: + // any other section, a missing answers list, or no answer for the consent question at all + // just falls through to the normal DONE status. + private String determineSectionStatus(FormSection section, List answers) { + if (!SECTION_UUID_GENERAL_INFO.equals(section.getSectionUuid()) || answers == null) { + return SECTION_STATUS_DONE; + } + boolean refused = answers.stream().anyMatch(this::isRefusalAnswer); + return refused ? SECTION_STATUS_REFUSED : SECTION_STATUS_DONE; + } + + // Same TB2_GI_Q1="NO" signal as determineSectionStatus, but scanned directly off the raw + // request (before any FormSection is resolved) so completeForm() can decide the top-level + // FormResponse status. Absence-tolerant: no TB2_SEC_GENERAL_INFO section, no answers, or no + // TB2_GI_Q1 answer at all just means "not refused". + private boolean isConsentRefused(FormResponseRequest request) { + if (request.getSections() == null) { + return false; + } + return request.getSections().stream() + .filter(s -> SECTION_UUID_GENERAL_INFO.equals(s.getSectionUuid())) + .filter(s -> s.getAnswers() != null) + .flatMap(s -> s.getAnswers().stream()) + .anyMatch(this::isRefusalAnswer); + } + + private boolean isRefusalAnswer(QuestionAnswerRequest answer) { + return QUESTION_UUID_CONSENT.equals(answer.getQuestionUuid()) + && OPTION_VALUE_CONSENT_REFUSED.equals(answer.getOptionValue()); } private List processAnswers( @@ -368,7 +420,7 @@ private List processAnswers( } } } else { - // TEXT, DATE, AUTO_FILL — prefer answerText, then answerDate, then optionValue (legacy) + // TEXT, DATE, AUTO_FILL, CHECKBOX — prefer answerText, then answerDate, then optionValue (legacy) String value = answer.getAnswerText() != null ? answer.getAnswerText() : answer.getAnswerDate() != null ? answer.getAnswerDate() : answer.getOptionValue(); @@ -416,9 +468,16 @@ private List loadSectionResponseDTOs(Long responseId) { Map> answersBySectionResponse = allAnswers.stream() .collect(Collectors.groupingBy(QuestionResponse::getSectionResponseId)); + Set sectionIds = sections.stream() + .map(SectionResponse::getSectionId) + .collect(Collectors.toSet()); + Map sectionById = formSectionRepo.findAllById(sectionIds).stream() + .collect(Collectors.toMap(FormSection::getSectionId, Function.identity())); + return sections.stream() .map(sr -> buildSectionResponseDTO( sr, + sectionById.get(sr.getSectionId()), answersBySectionResponse.getOrDefault(sr.getSectionResponseId(), List.of()))) .collect(Collectors.toList()); } @@ -443,7 +502,7 @@ private FormResponseDTO buildFormResponseDTO(FormResponse r, List answers) { + SectionResponse sr, FormSection section, List answers) { List answerDTOs = answers.stream() .map(a -> QuestionResponseDTO.builder() .questionResponseId(a.getQuestionResponseId()) @@ -455,6 +514,8 @@ private SectionResponseDTO buildSectionResponseDTO( return SectionResponseDTO.builder() .sectionResponseId(sr.getSectionResponseId()) .sectionId(sr.getSectionId()) + .sectionUuid(section != null ? section.getSectionUuid() : null) + .isEditable(section != null ? section.getIsEditable() : null) .status(sr.getStatus()) .savedAt(sr.getSavedAt()) .answers(answerDTOs) @@ -493,12 +554,21 @@ public List findPendingFollowUps(List formIds, int delayD @Override @Transactional(readOnly = true) - public List getCompletedBeneficiaries(FormType formType, Integer villageId, Integer providerServiceMapId) { + public CompletedBeneficiariesDTO getCompletedBeneficiaries( + FormType formType, Integer villageId, Integer providerServiceMapId) { DynamicForm form = dynamicFormRepo.findByFormTypeAndIsActive(formType, true) .orElseThrow(() -> new NoSuchElementException( "No active form found for type: " + formType)); + return CompletedBeneficiariesDTO.builder() + .completed(findBeneficiaryIdsByStatus(form.getFormId(), FormResponseStatus.COMPLETE.name(), villageId, providerServiceMapId)) + .refused(findBeneficiaryIdsByStatus(form.getFormId(), FormResponseStatus.REFUSED.name(), villageId, providerServiceMapId)) + .build(); + } + + private List findBeneficiaryIdsByStatus( + Long formId, String status, Integer villageId, Integer providerServiceMapId) { return (villageId != null || providerServiceMapId != null) - ? formResponseRepo.findBeneficiaryIdsByFormIdAndStatusFiltered(form.getFormId(), STATUS_COMPLETE, villageId, providerServiceMapId) - : formResponseRepo.findBeneficiaryIdsByFormIdAndStatus(form.getFormId(), STATUS_COMPLETE); + ? formResponseRepo.findBeneficiaryIdsByFormIdAndStatusFiltered(formId, status, villageId, providerServiceMapId) + : formResponseRepo.findBeneficiaryIdsByFormIdAndStatus(formId, status); } } diff --git a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java index 6a8fb863..4767f505 100644 --- a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -41,7 +41,9 @@ 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.FormResponseStatus; import com.iemr.flw.masterEnum.QuestionType; +import com.iemr.flw.masterEnum.SectionPhase; import com.iemr.flw.service.CampConfigService; import com.iemr.flw.utils.JwtUtil; import com.iemr.flw.utils.exception.IEMRException; @@ -73,7 +75,6 @@ @Component public class FormResponseItemSaver { - private static final String STATUS_SUBMITTED = "SUBMITTED"; private static final String SECTION_STATUS_DONE = "DONE"; private final FormResponseRepo formResponseRepo; @@ -113,7 +114,7 @@ public FormResponseDTO saveForBulk(FormResponseRequest req, String jwtToken) thr Long responseId = formResponse.getResponseId(); // Step 3: Update FormResponse header - formResponse.setStatus(STATUS_SUBMITTED); + formResponse.setStatus(FormResponseStatus.SUBMITTED.name()); formResponse.setSubmittedAt(now); formResponse.setUpdatedBy(actor); formResponse.setLastFollowUpAt(now); @@ -180,7 +181,7 @@ private FormResponse createFormResponse(FormResponseRequest req, FormVersion ver .formId(version.getDynamicForm().getFormId()) .versionId(version.getVersionId()) .officerId(req.getOfficerId()) - .status(STATUS_SUBMITTED) + .status(FormResponseStatus.SUBMITTED.name()) .createdBy(actor) .updatedBy(actor) .submittedAt(now) @@ -232,18 +233,24 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR "' not found in form '" + req.getFormUuid() + "'"); } + Optional existingSr = sectionResponseRepo.findByResponseIdAndSectionId( + formResponse.getResponseId(), section.getSectionId()); + if (existingSr.isPresent() && Boolean.FALSE.equals(section.getIsEditable())) { + log.info("saveForBulk: section '{}' is not editable — skipping update for responseId={}", + section.getSectionUuid(), formResponse.getResponseId()); + continue; + } + // 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()); - }); + existingSr.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())) { + if (section.getSectionPhase() == SectionPhase.POST_SUBMIT) { formResponse.setLastFollowUpAt(sectionResponse.getSavedAt()); formResponseRepo.save(formResponse); } @@ -261,7 +268,7 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR parkingPlaceID); questionResponseRepo.saveAll(questionResponses); - sectionDTOs.add(buildSectionResponseDTO(sectionResponse, questionResponses)); + sectionDTOs.add(buildSectionResponseDTO(sectionResponse, section, questionResponses)); } return buildFormResponseDTO(formResponse, sectionDTOs); @@ -352,7 +359,7 @@ private List processAnswers( } } } else { - // TEXT, DATE, AUTO_FILL — prefer answerText, then answerDate, then optionValue (legacy) + // TEXT, DATE, AUTO_FILL, CHECKBOX — prefer answerText, then answerDate, then optionValue (legacy) String value = answer.getAnswerText() != null ? answer.getAnswerText() : answer.getAnswerDate() != null ? answer.getAnswerDate() : answer.getOptionValue(); @@ -400,7 +407,8 @@ private FormResponseDTO buildFormResponseDTO(FormResponse r, List answers) { + private SectionResponseDTO buildSectionResponseDTO( + SectionResponse sr, FormSection section, List answers) { List answerDTOs = answers.stream() .map(a -> QuestionResponseDTO.builder() .questionResponseId(a.getQuestionResponseId()) @@ -412,6 +420,8 @@ private SectionResponseDTO buildSectionResponseDTO(SectionResponse sr, List Date: Tue, 7 Jul 2026 14:00:29 +0530 Subject: [PATCH 75/97] fix(counselling): remove duplicate findCounselledBenIds method and import left over from PR #269 merge The duplicate interface method was a compile error that cascaded into hundreds of unrelated Lombok symbol errors across the module. Co-Authored-By: Claude Sonnet 5 --- .../java/com/iemr/flw/repo/iemr/FormResponseRepo.java | 8 -------- .../flw/service/impl/DynamicFormResponseServiceImpl.java | 1 - 2 files changed, 9 deletions(-) diff --git a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java index e902ee94..66672c16 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java @@ -60,14 +60,6 @@ List findCounselledBenIds(@Param("benIds") List benIds, @Param("formId") Long formId, @Param("status") String status); - @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' " diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index f6b26d4a..64f405b9 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -37,7 +37,6 @@ import com.iemr.flw.dto.iemr.SectionResponseDTO; import com.iemr.flw.domain.iemr.DynamicForm; import com.iemr.flw.masterEnum.FormResponseStatus; -import com.iemr.flw.masterEnum.FormResponseStatus; import com.iemr.flw.masterEnum.FormType; import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.repo.iemr.FormResponseRepo; From ed387f5c2bb2ab95c76ac81471cfecb82763d4b0 Mon Sep 17 00:00:00 2001 From: sehjotsinghunthinkable Date: Tue, 7 Jul 2026 16:26:35 +0530 Subject: [PATCH 76/97] Vb/counselling (#270) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enforced mandatory validation for "Reason for Refusal in section e in file TbCounsellingFormSeeder * Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. * Align `/complete` and `/submitBulk` form response editing behavior * Updated `/complete` to resolve `FormResponse` using `beneficiaryId` and `formUuid` from the request body, removing the dependency on `responseId` as a query parameter. * Updated `/submitBulk` section editing to match `/complete` behavior by performing in-place `SectionResponse` upserts and per-question delete-and-reinsert operations, instead of deleting and recreating the entire section. * Add van/sync tracking fields (vanID, parkingPlaceID, vanSerialNo, processed, syncedDate, syncedBy, syncFailureReason) to domain entities for offline mobile data collection support * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. * feat(beneficiary): expose GPS coordinates in getBeneficiaryData response Add gpsLatitude, gpsLongitude, digipin, gpsTimestamp, and isGpsUnavailable fields to RMNCHBeneficiaryDetailsRmnch and RMNCHHouseHoldDetails entities, mapping to the columns written by Identity-API's /syncDataToAmrit endpoint. Enable serializeNulls on the Gson builder in BeneficiaryServiceImpl so GPS keys are always present in householdDetails and beneficiaryDetails — null for web-registered records, real values for mobile-registered ones. Co-Authored-By: Claude Sonnet 4.6 * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Revert "Committed 82b00ccf with only the three files:" This reverts commit 3040a08077d82d27dfd86f29f0fbdd4dc094ec56. * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields # Conflicts: # src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java * Updated isMandatory field in Section C, so changed in TbCounsellingFormSeeder * Add optional villageId and providerServiceMapId filters to getCompletedBeneficiaries * Removing section f from seeder * Removing already existing check in /complete api * Add a CHECKBOX question type and a TB_COUNSELLING_V2 seeder with a GENERAL_INFO consent gate ahead of the counselling sections. * Seed single-checkbox questions for Sections A-D and point the Counselled lookup at TB_COUNSELLING_V2 instead of the retired V1 form * Mark form/section status REFUSED when TB2_GI_Q1=NO on /complete, split getCompletedBeneficiaries into completed/refused lists, and extract FormResponseStatus enum * Resolved conflicts * fix(stoptb): getBeneficiaryData reads anthropometry/vitals from standard tables - getBeneficiaryData now reads height/weight/bmi from t_phy_anthropometry and temperature/pulse/BP/RBS from t_phy_vitals (latest row per beneficiary) - Fallback to otherFields if exam not yet saved for the beneficiary - Stop TB specific fields (personFrom, caseFindingType etc.) still from otherFields - Add findByBeneficiaryRegIDOrderByCreatedDateDesc and findByBenVisitID to BenAnthropometryRepo and BenPhysicalVitalRepo - Add StopTB_Future_Roadmap.md for future HWC integration and edit support Co-Authored-By: Claude Sonnet 4.6 * fix(compile): rename variable 'a' to 'anthro' to avoid conflict with outer loop variable Co-Authored-By: Claude Sonnet 4.6 * fix(stoptb): set CreatedDate on t_benvisitdetail insert to prevent NOT NULL constraint violation Co-Authored-By: Claude Sonnet 4.6 * fix(anc-pnc): guard CreatedDate on BenVisitDetail insert for ANC and PNC flows If mobile does not send createdDate, ModelMapper leaves it null and MySQL rejects the insert with NOT NULL constraint. Set server timestamp as fallback. Co-Authored-By: Claude Sonnet 4.6 * fix(docker): set TZ=Asia/Kolkata to prevent UTC/IST date mismatch Without explicit timezone, Docker defaults to UTC. This causes LocalDate.now() in getOrCreateVisitForToday() to use UTC date, so evening IST registrations appear as next-day July 1 in DB and visit lookups fail after UTC midnight. Co-Authored-By: Claude Sonnet 4.6 * fix(stoptb): handle ISO 8601 Z suffix in parseDob to fix beneficiary age display Timestamp.valueOf() throws on "yyyy-MM-dd HH:mm:ss.SSSZ" format because the trailing Z is not a valid nanosecond value. Strip Z before parsing so mobile DOB like "1987-07-01T00:00:00.000Z" is correctly stored in i_ben_flow_outreach.ben_dob. Co-Authored-By: Claude Sonnet 4.6 * fix(stoptb): fallback to i_ben_flow_outreach.ben_dob when i_beneficiarydetails.dob is null Identity-API mapper (@Mapping source=dto.dob) is commented out, causing dob to be null for all mobile registrations. Fall back to ben_dob from i_ben_flow_outreach so getBeneficiaryData returns correct DOB and mobile shows correct age. Co-Authored-By: Claude Sonnet 4.6 * fix: use saveAndFlush to prevent FK violation in t_benchiefcomplaint save() leaves VisitCode UPDATE pending in JPA session — t_benchiefcomplaint INSERT runs before the UPDATE reaches DB, causing FK constraint failure and full transaction rollback. saveAndFlush forces the UPDATE to DB immediately. Co-Authored-By: Claude Sonnet 4.6 * fix(stoptb): compute age from benDetailsRMNCH_OBJ.dob instead of benDetailsOBJ.dob benDetailsOBJ.dob (i_beneficiarydetails) is null for Stop TB mobile registrations, which is why c22b3fd2 added a fallback that fills benDetailsRMNCH_OBJ.dob from i_ben_flow_outreach. The age calculation block still read the original, still-null benDetailsOBJ.dob, so dob came back correct but age/ageFull stayed 0/"". Point it at the already-corrected benDetailsRMNCH_OBJ.dob instead. Co-Authored-By: Claude Sonnet 5 * fix(stoptb): scope Suspected/Confirmed getAll by facility+village instead of userId Nurse and Counsellor are different logins hitting the same /tb/suspected/getAll and /tb/confirmed/getAll endpoints, but both queries filtered by the calling user's own userId. So a Counsellor pulling data after a Nurse creates a Suspected/Confirmed record saw none of it - only rows tied to their own login. Adds an optional providerServiceMapID(+villageID)-scoped query path on both endpoints, joined through the already-populated BenFlowStatus table (same pattern as StopTBDiagnosticsRepo), so any role at the same facility/village sees the same worklist. The old userId-based path is kept as the fallback when the new fields aren't sent, so already-deployed app builds are unaffected. * fix(stoptb): drop date filter from facility-scoped Suspected query visitDate isn't the right dimension for a facility worklist - an unresolved Suspected case from months ago is still actionable, and filtering it out by date would hide it from the Counsellor, working against the visibility fix in the previous commit. Also matches StopTBDiagnosticsRepo's facility-scoped query, which never date-filters. Old ashaId-based fallback path is untouched and still date-filters as before. * fix(stoptb): join facility-scoped query on beneficiaryID, not beneficiaryRegID TBSuspected.benId / TBConfirmedCase.benId store the person-level beneficiary_id (e.g. 907797933607), not beneficiaryRegID (e.g. 35137) - confirmed by querying i_ben_flow_outreach directly. The facility-scoped query added in the previous two commits joined against BenFlowStatus.beneficiaryRegID, so it never matched anything and always returned empty. Switched the join to BenFlowStatus.beneficiaryID, which is the correct corresponding column. * Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. * Revert "Committed 82b00ccf with only the three files:" This reverts commit 3040a08077d82d27dfd86f29f0fbdd4dc094ec56. * Committed 82b00ccf with only the three files: - BeneficiaryServiceImpl.java — serializeNulls() + GPS fallback from address - RMNCHBeneficiaryDetailsRmnch.java — gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable fields - RMNCHHouseHoldDetails.java — gpsLatitude, gpsLongitude fields * feat(beneficiary): expose GPS coordinates in getBeneficiaryData response Add gpsLatitude, gpsLongitude, digipin, gpsTimestamp, and isGpsUnavailable fields to RMNCHBeneficiaryDetailsRmnch and RMNCHHouseHoldDetails entities, mapping to the columns written by Identity-API's /syncDataToAmrit endpoint. Enable serializeNulls on the Gson builder in BeneficiaryServiceImpl so GPS keys are always present in householdDetails and beneficiaryDetails — null for web-registered records, real values for mobile-registered ones. Co-Authored-By: Claude Sonnet 4.6 * Changing working of checkbox as of radio button --------- Co-authored-by: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 Co-authored-by: vishwab1 --- .../flw/dto/iemr/QuestionAnswerRequest.java | 1 + .../flw/dto/iemr/QuestionResponseDTO.java | 2 +- .../com/iemr/flw/masterEnum/QuestionType.java | 4 ++-- .../service/impl/BeneficiaryServiceImpl.java | 19 +++++++++++++++++++ .../impl/DynamicFormResponseServiceImpl.java | 3 ++- .../service/impl/FormResponseItemSaver.java | 2 +- 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java b/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java index 8ee08c45..ad314ba5 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java +++ b/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java @@ -33,6 +33,7 @@ * 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) + * CHECKBOX → optionValue (single string, e.g. "CHECKED") * MCQ → optionValues (list of strings, one row saved per element) * TEXT/AUTO_FILL → answerText * DATE → answerDate (ISO date string) or answerText diff --git a/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java b/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java index f5f76d63..98557c10 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java @@ -40,7 +40,7 @@ public class QuestionResponseDTO { private Long questionResponseId; private Long questionId; - /** Populated for RADIO and MCQ answers. */ + /** Populated for RADIO, MCQ, and CHECKBOX answers. */ private Long optionId; /** Populated for TEXT, DATE, AUTO_FILL answers. */ private String answerText; diff --git a/src/main/java/com/iemr/flw/masterEnum/QuestionType.java b/src/main/java/com/iemr/flw/masterEnum/QuestionType.java index 0af07aa1..82308544 100644 --- a/src/main/java/com/iemr/flw/masterEnum/QuestionType.java +++ b/src/main/java/com/iemr/flw/masterEnum/QuestionType.java @@ -32,8 +32,8 @@ public enum QuestionType { RADIO, /** Multi-select from a predefined list of options. */ MCQ, - /** Single boolean checkbox — checked or unchecked. May carry options for display - * purposes; answers are still stored as free text. */ + /** Single boolean checkbox — checked or unchecked. Resolved like RADIO: submit + * optionValue to select it, omit to leave unanswered. */ CHECKBOX, /** Free-text input. */ TEXT, 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 ebe4c6d9..1d332bc2 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -365,6 +365,25 @@ private String getMappingsForAddressIDs(List addressLi if (benDetailsRMNCH_OBJ.getGpsLongitude() != null) benDetailsRMNCH_OBJ.setLongitude(BigDecimal.valueOf(benDetailsRMNCH_OBJ.getGpsLongitude())); + // 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()); + + // 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())); + // ----------------------------------------------------------------------------- // related benids diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index 64f405b9..a36710a5 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -37,6 +37,7 @@ import com.iemr.flw.dto.iemr.SectionResponseDTO; import com.iemr.flw.domain.iemr.DynamicForm; import com.iemr.flw.masterEnum.FormResponseStatus; +import com.iemr.flw.masterEnum.FormResponseStatus; import com.iemr.flw.masterEnum.FormType; import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.repo.iemr.FormResponseRepo; @@ -388,7 +389,7 @@ private List processAnswers( // Delete any existing answers for this question in this section (handles re-saves) questionResponseRepo.deleteByQuestionIdAndSectionResponseId(questionId, sectionResponseId); - if (type == QuestionType.RADIO) { + if (type == QuestionType.RADIO || type == QuestionType.CHECKBOX) { if (answer.getOptionValue() != null) { QuestionOption opt = resolveOption( optionsByQuestion, questionId, answer.getOptionValue(), answer.getQuestionUuid()); diff --git a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java index 4767f505..4ff8ca07 100644 --- a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -328,7 +328,7 @@ private List processAnswers( questionResponseRepo.deleteByQuestionIdAndSectionResponseId(questionId, sectionResponseId); - if (type == QuestionType.RADIO) { + if (type == QuestionType.RADIO || type == QuestionType.CHECKBOX) { if (answer.getOptionValue() != null) { QuestionOption opt = resolveOption(optionsByQuestion, questionId, answer.getOptionValue(), answer.getQuestionUuid()); From fe2c0fe6424d4bc265db75d70215a5e7a6e9fe82 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 8 Jul 2026 13:22:08 +0530 Subject: [PATCH 77/97] Move Risk Factors and HIV Status from General Examination to TB Screening Adds a mandatory Risk Factors section (Key Population/Risk Factors multi-select + HIV Status) to nurse TB Screening, matching the new form requirement, and removes it from General Examination where it previously lived. Co-Authored-By: Claude Sonnet 5 --- ...r_tb_screening_risk_factors_hiv_status.sql | 19 +++++++++++++++++++ .../domain/iemr/StopTBGeneralExamination.java | 14 -------------- .../com/iemr/flw/domain/iemr/TBScreening.java | 14 ++++++++++++++ .../flw/service/impl/StopTBServiceImpl.java | 16 ++++++++-------- 4 files changed, 41 insertions(+), 22 deletions(-) create mode 100644 alter_tb_screening_risk_factors_hiv_status.sql diff --git a/alter_tb_screening_risk_factors_hiv_status.sql b/alter_tb_screening_risk_factors_hiv_status.sql new file mode 100644 index 00000000..fd6ea509 --- /dev/null +++ b/alter_tb_screening_risk_factors_hiv_status.sql @@ -0,0 +1,19 @@ +-- Adds the "Risk Factors" section (Key Population / Risk Factors + HIV Status) +-- to tb_screening, moved here from tb_stoptb_general_examination. + +ALTER TABLE db_iemr.tb_screening + ADD COLUMN key_population_risk_factor_ids TEXT NULL, + ADD COLUMN key_population_risk_factors TEXT NULL, + ADD COLUMN hiv_status_id INT NULL, + ADD COLUMN hiv_status VARCHAR(20) NULL; + +-- NOTE: columns key_population_risk_factor_ids, key_population_risk_factors, +-- hiv_status_id, hiv_status are left in place on db_iemr.tb_stoptb_general_examination +-- for historical data already captured there. Drop them separately once confirmed +-- they are no longer needed: +-- +-- ALTER TABLE db_iemr.tb_stoptb_general_examination +-- DROP COLUMN key_population_risk_factor_ids, +-- DROP COLUMN key_population_risk_factors, +-- DROP COLUMN hiv_status_id, +-- DROP COLUMN hiv_status; diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java index 9972d916..6fdd4ced 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java @@ -72,20 +72,6 @@ public class StopTBGeneralExamination { @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; 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 d9df1dcf..06e36309 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBScreening.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBScreening.java @@ -84,6 +84,20 @@ public class TBScreening { @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; diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 38c3acc2..cfc9551e 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -361,10 +361,6 @@ public List> saveGeneralExamination(List 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); @@ -435,10 +431,6 @@ private Map examToMap(StopTBGeneralExamination e) { 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()); @@ -482,6 +474,10 @@ public List> saveNurseTBScreening(List> 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); @@ -555,6 +551,10 @@ private Map screeningToMap(TBScreening s) { 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())) { From 597b842f6f9a0327914b4625820ed461a300e857 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 8 Jul 2026 14:38:11 +0530 Subject: [PATCH 78/97] Keep Risk Factors and HIV Status on General Examination too Per updated requirement, these fields should be captured on both General Examination and TB Screening, not moved exclusively to TB Screening. Restores keyPopulationRiskFactorIds/Factors and hivStatusId/hivStatus on StopTBGeneralExamination (save + response map), on top of the earlier commit that added them to TBScreening. Co-Authored-By: Claude Sonnet 5 --- .../flw/domain/iemr/StopTBGeneralExamination.java | 14 ++++++++++++++ .../iemr/flw/service/impl/StopTBServiceImpl.java | 8 ++++++++ 2 files changed, 22 insertions(+) diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java index 6fdd4ced..9972d916 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java @@ -72,6 +72,20 @@ public class StopTBGeneralExamination { @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; diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index cfc9551e..0ed9e377 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -361,6 +361,10 @@ public List> saveGeneralExamination(List 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); @@ -431,6 +435,10 @@ private Map examToMap(StopTBGeneralExamination e) { 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()); From f935ce03a39d40d0ce45b15c0899becc4255a5f0 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 8 Jul 2026 16:27:07 +0530 Subject: [PATCH 79/97] Stop TB API responses were silently dropping null fields new Gson() excludes any key whose value is null by default, so fields like hivStatus/keyPopulationRiskFactor* that were saved as NULL never appeared in GET/GETALL responses at all. Switch to a shared Gson instance built with serializeNulls() across all 16 StopTBController response sites. Co-Authored-By: Claude Sonnet 5 --- .../iemr/flw/controller/StopTBController.java | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/StopTBController.java b/src/main/java/com/iemr/flw/controller/StopTBController.java index 8da6ff63..386ed597 100644 --- a/src/main/java/com/iemr/flw/controller/StopTBController.java +++ b/src/main/java/com/iemr/flw/controller/StopTBController.java @@ -1,6 +1,7 @@ package com.iemr.flw.controller; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.iemr.flw.dto.iemr.StopTBRegistrationDto; import com.iemr.flw.service.StopTBService; import com.iemr.flw.utils.response.OutputResponse; @@ -19,6 +20,10 @@ 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; @@ -32,7 +37,7 @@ public String saveRegistration( OutputResponse response = new OutputResponse(); try { Map data = stopTBService.saveRegistration(requestBody, authorization); - response.setResponse(new Gson().toJson(data)); + response.setResponse(gson.toJson(data)); } catch (Exception e) { logger.error("Error in saveRegistration: " + e); response.setError(5000, "Error in registration: " + e.getMessage()); @@ -48,7 +53,7 @@ public String getRegistrarWorklist(@RequestBody StopTBRegistrationDto dto) { OutputResponse response = new OutputResponse(); try { Map data = stopTBService.getRegistrarWorklist(dto); - response.setResponse(new Gson().toJson(data.get("data"))); + response.setResponse(gson.toJson(data.get("data"))); } catch (Exception e) { logger.error("Error in getRegistrarWorklist: " + e); response.setError(5000, "Error fetching worklist: " + e.getMessage()); @@ -62,7 +67,7 @@ public String getNurseWorklist(@RequestBody StopTBRegistrationDto dto) { OutputResponse response = new OutputResponse(); try { Map data = stopTBService.getNurseWorklist(dto); - response.setResponse(new Gson().toJson(data.get("data"))); + response.setResponse(gson.toJson(data.get("data"))); } catch (Exception e) { logger.error("Error in getNurseWorklist: " + e); response.setError(5000, "Error fetching nurse worklist: " + e.getMessage()); @@ -78,7 +83,7 @@ public String saveGeneralExamination(@RequestBody List> data OutputResponse response = new OutputResponse(); try { List> result = stopTBService.saveGeneralExamination(dataList); - response.setResponse(new Gson().toJson(result)); + response.setResponse(gson.toJson(result)); } catch (Exception e) { logger.error("Error in saveGeneralExamination: " + e); response.setError(5000, "Error saving general examination: " + e.getMessage()); @@ -94,7 +99,7 @@ public String getGeneralExamination(@RequestBody Map body) { Object raw = body.get("benRegID"); if (raw == null) throw new Exception("benRegID is required"); Map data = stopTBService.getGeneralExamination(Long.parseLong(raw.toString())); - response.setResponse(new Gson().toJson(data)); + response.setResponse(gson.toJson(data)); } catch (Exception e) { logger.error("Error in getGeneralExamination: " + e); response.setError(5000, "Error fetching general examination: " + e.getMessage()); @@ -111,7 +116,7 @@ public String getAllGeneralExaminations(@RequestBody Map body) { 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(new Gson().toJson(data)); + response.setResponse(gson.toJson(data)); } catch (Exception e) { logger.error("Error in getAllGeneralExaminations: " + e); response.setError(5000, "Error fetching general examinations: " + e.getMessage()); @@ -127,7 +132,7 @@ public String saveNurseTBScreening(@RequestBody List> dataLi OutputResponse response = new OutputResponse(); try { List> result = stopTBService.saveNurseTBScreening(dataList); - response.setResponse(new Gson().toJson(result)); + response.setResponse(gson.toJson(result)); } catch (Exception e) { logger.error("Error in saveNurseTBScreening: " + e); response.setError(5000, "Error saving TB screening: " + e.getMessage()); @@ -143,7 +148,7 @@ public String getNurseTBScreening(@RequestBody Map body) { Object raw = body.get("benRegID"); if (raw == null) throw new Exception("benRegID is required"); Map data = stopTBService.getNurseTBScreening(Long.parseLong(raw.toString())); - response.setResponse(new Gson().toJson(data)); + response.setResponse(gson.toJson(data)); } catch (Exception e) { logger.error("Error in getNurseTBScreening: " + e); response.setError(5000, "Error fetching TB screening: " + e.getMessage()); @@ -160,7 +165,7 @@ public String getAllNurseTBScreenings(@RequestBody Map body) { 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(new Gson().toJson(data)); + response.setResponse(gson.toJson(data)); } catch (Exception e) { logger.error("Error in getAllNurseTBScreenings: " + e); response.setError(5000, "Error fetching TB screenings: " + e.getMessage()); @@ -176,7 +181,7 @@ public String saveGeneralOpd(@RequestBody List> dataList) { OutputResponse response = new OutputResponse(); try { List> result = stopTBService.saveGeneralOpd(dataList); - response.setResponse(new Gson().toJson(result)); + response.setResponse(gson.toJson(result)); } catch (Exception e) { logger.error("Error in saveGeneralOpd: " + e); response.setError(5000, "Error saving general OPD: " + e.getMessage()); @@ -192,7 +197,7 @@ public String getGeneralOpd(@RequestBody Map body) { Object raw = body.get("benRegID"); if (raw == null) throw new Exception("benRegID is required"); Map data = stopTBService.getGeneralOpd(Long.parseLong(raw.toString())); - response.setResponse(new Gson().toJson(data)); + response.setResponse(gson.toJson(data)); } catch (Exception e) { logger.error("Error in getGeneralOpd: " + e); response.setError(5000, "Error fetching general OPD: " + e.getMessage()); @@ -209,7 +214,7 @@ public String getAllGeneralOpd(@RequestBody Map body) { 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(new Gson().toJson(data)); + response.setResponse(gson.toJson(data)); } catch (Exception e) { logger.error("Error in getAllGeneralOpd: " + e); response.setError(5000, "Error fetching general OPD records: " + e.getMessage()); @@ -225,7 +230,7 @@ public String saveDiagnostics(@RequestBody List> dataList) { OutputResponse response = new OutputResponse(); try { List> result = stopTBService.saveDiagnostics(dataList); - response.setResponse(new Gson().toJson(result)); + response.setResponse(gson.toJson(result)); } catch (Exception e) { logger.error("Error in saveDiagnostics: " + e); response.setError(5000, "Error saving diagnostics: " + e.getMessage()); @@ -241,7 +246,7 @@ public String getDiagnostics(@RequestBody Map body) { Object raw = body.get("benRegID"); if (raw == null) throw new Exception("benRegID is required"); Map data = stopTBService.getDiagnostics(Long.parseLong(raw.toString())); - response.setResponse(new Gson().toJson(data)); + response.setResponse(gson.toJson(data)); } catch (Exception e) { logger.error("Error in getDiagnostics: " + e); response.setError(5000, "Error fetching diagnostics: " + e.getMessage()); @@ -258,7 +263,7 @@ public String getAllDiagnostics(@RequestBody Map body) { 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(new Gson().toJson(data)); + response.setResponse(gson.toJson(data)); } catch (Exception e) { logger.error("Error in getAllDiagnostics: " + e); response.setError(5000, "Error fetching diagnostics records: " + e.getMessage()); @@ -274,7 +279,7 @@ public String submitNurseData(@RequestBody Map data) { OutputResponse response = new OutputResponse(); try { Map result = stopTBService.submitNurseData(data); - response.setResponse(new Gson().toJson(result)); + response.setResponse(gson.toJson(result)); } catch (Exception e) { logger.error("Error in submitNurseData: " + e); response.setError(5000, "Error submitting nurse data: " + e.getMessage()); From 07defaa6263f4138da51fd33ceb88f9e426fff4f Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 13 Jul 2026 10:42:37 +0530 Subject: [PATCH 80/97] chore: remove stale StopTB planning docs Co-Authored-By: Claude Sonnet 5 --- StopTB_Future_Roadmap.md | 246 ---------------------- StopTB_Standard_Table_Integration.md | 294 --------------------------- 2 files changed, 540 deletions(-) delete mode 100644 StopTB_Future_Roadmap.md delete mode 100644 StopTB_Standard_Table_Integration.md diff --git a/StopTB_Future_Roadmap.md b/StopTB_Future_Roadmap.md deleted file mode 100644 index d6fea62b..00000000 --- a/StopTB_Future_Roadmap.md +++ /dev/null @@ -1,246 +0,0 @@ -# Stop TB — Future Roadmap - -**Date:** 2026-06-30 -**Author:** vishwab1 -**Prerequisite:** Read `StopTB_Standard_Table_Integration.md` first for current implementation context. - ---- - -## Why this document exists - -The current dual-write implementation (FLW-API) writes Stop TB clinical data to both custom -`tb_stoptb_*` tables and standard AMRIT tables (`t_phy_vitals`, `t_Phy_GeneralExam` etc.). - -This document captures what still needs to be done in future phases — HWC integration, -edit support, worklist, and BenFlowStatus advancement. - -**None of these future changes require a mobile app change.** - ---- - -## 1. HWC-API — Read common fields from standard tables - -**Trigger:** When HWC doctor needs to see Stop TB beneficiary's vitals/examination history. - -**Problem:** HWC-API currently queries standard tables with a `providerServiceMapID` filter: -```sql -SELECT * FROM t_phy_vitals -WHERE beneficiaryRegID = ? AND providerServiceMapID = ? -- HWC's own PSMID -``` -Stop TB data was written with Stop TB's PSMID → HWC query misses it. - -**Fix — HWC-API only, remove PSMID filter:** -```sql -SELECT * FROM t_phy_vitals -WHERE beneficiaryRegID = ? -- no PSMID filter, shows all service lines -ORDER BY CreatedDate DESC -``` - -**Affected HWC-API endpoints:** -- `getBenPhysicalVitalDetail` -- `getBenAnthropometryDetail` -- `getBenPhyGeneralExamination` -- `getBenChiefComplaint` -- `getBenPrescriptionDetail` - -**Effort:** Small — query change only in HWC-API. No schema change. No FLW-API change. - ---- - -## 2. HWC-API — Read TB specific data from custom tables directly - -**Trigger:** When HWC doctor needs full TB clinical picture — screening questions, diagnostics, -Nikshay ID, treatment tracking. - -Standard tables only hold common clinical fields. TB specific data lives in custom tables. -HWC-API needs to query these directly by `benRegID`. - -**New queries needed in HWC-API:** - -```java -// TB Screening -tbScreeningRepo.findByBenRegIDOrderByCreatedDateDesc(benRegID); - -// Diagnostics (Nikshay ID, X-ray, Truenat, Sputum, Liquid Culture) -tbDiagnosticsRepo.findByBenRegIDOrderByCreatedDateDesc(benRegID); - -// Suspected cases -tbSuspectedRepo.findByBenIdOrderByCreatedDateDesc(benId); - -// Confirmed cases + treatment -tbConfirmedRepo.findByBenIdOrderByCreatedDateDesc(benId); -``` - -**Tables queried:** -| Table | Key data | -|---|---| -| `tb_screening` | Cough, fever, night sweats, weight loss, family history | -| `tb_stoptb_diagnostics` | Nikshay ID, X-ray result, Truenat result, Liquid Culture result | -| `tb_suspected` | Sputum result, chest X-ray, referral, TB/DRTB confirmed flag | -| `tb_confirmed_cases` | Regimen, treatment dates, adherence, outcome | - -**Effort:** Small — new repo methods + new HWC-API service calls. No schema change. - ---- - -## 3. FLW-API — Edit support for standard table dual-write - -**Trigger:** When mobile app adds edit/update capability for nurse data. - -**Current behaviour:** Dual-write only happens on first save (`isNew = true`). On re-save, -only `tb_stoptb_*` custom tables are updated. Standard tables keep the original values. - -**What breaks:** If mobile edits vitals → `tb_stoptb_general_examination` gets new values -but `t_phy_vitals` keeps old values → HWC sees stale data. - -### Changes in StopTBServiceImpl - -```java -// Change from: -if (isNew) { - generalExaminationRepo.updateVanSerialNo(exam.getId()); - dualWriteExamToStandardTables(exam, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); -} - -// Change to: -if (isNew) generalExaminationRepo.updateVanSerialNo(exam.getId()); -dualWriteExamToStandardTables(exam, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); -``` - -Same change for `saveGeneralOpd()`: -```java -// Remove isNew guard around dualWriteOpdToStandardTables -if (isNew) generalOpdRepo.updateVanSerialNo(opd.getId()); -dualWriteOpdToStandardTables(opd, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); -``` - -### Changes in dual-write helpers — upsert instead of insert - -Each helper must find existing row first, update if found, insert if not: - -```java -private void writeVitals(StopTBGeneralExamination exam, ...) { - try { - // Find existing row for this visit - BenPhysicalVitalDetail existing = - benPhysicalVitalRepo.findByBenVisitID(visit.getBenVisitId()); - BenPhysicalVitalDetail v = existing != null ? existing : new BenPhysicalVitalDetail(); - - v.setBeneficiaryRegID(beneficiaryRegID); - v.setBenVisitID(visit.getBenVisitId()); - v.setVisitCode(visit.getVisitCode()); - // ... set all fields - benPhysicalVitalRepo.save(v); - } catch (Exception e) { - logger.error("writeVitals failed", e); - } -} -``` - -Same upsert pattern for: `writeAnthropometry()`, `writePhyGeneralExam()`, -`writeChiefComplaint()`, `writePrescription()`. - -### New repo methods needed (FLW-API) - -```java -// BenPhysicalVitalRepo -BenPhysicalVitalDetail findByBenVisitID(Long benVisitID); - -// BenAnthropometryRepo -BenAnthropometryDetail findByBenVisitID(Long benVisitID); - -// PhyGeneralExaminationRepo -PhyGeneralExamination findByBenVisitID(Long benVisitID); - -// BenChiefComplaintRepo -BenChiefComplaint findByBenVisitID(Long benVisitID); -``` - -> **Note:** `t_prescription` and `t_prescribeddrug` have a parent-child FK relationship. -> For prescription edit: find existing `PrescriptionDetail` by `benVisitID`, delete old -> `PrescribedDrugDetail` rows, re-insert fresh drug rows. - -**Effort:** Small — remove isNew guard, add findByBenVisitID to 4 repos, convert helpers to upsert. - ---- - -## 4. FLW-API — nurseFlag advancement - -**Trigger:** When a web dashboard / supervisor screen is built for Stop TB. - -**Current state:** `nurseFlag` in `i_ben_flow_outreach` is set to `1` at registration and -never advances because mobile never calls `flw-api/stoptb/nurse/submit`. - -**What to implement:** - -After each of the 3 mandatory nurse saves (generalExamination, tbScreening, generalOpd), -check if all 3 are complete for the same `visitCode`. If yes, auto-advance `nurseFlag = 9`. - -```java -private void tryAdvanceNurseFlag(Long benRegID, Long visitCode, Integer psmID) { - boolean examDone = generalExaminationRepo - .findByBeneficiaryRegIDAndVisitCode(benRegID, visitCode) != null; - boolean screeningDone = tbScreeningRepo - .findByBenRegIDAndVisitCode(benRegID, visitCode) != null; - boolean opdDone = generalOpdRepo - .findByBenRegIDAndVisitCode(benRegID, visitCode) != null; - - if (examDone && screeningDone && opdDone) { - benFlowStatusRepo.updateStopTBAfterNurseSubmit(benRegID, psmID); - // updateStopTBAfterNurseSubmit() already exists in BenFlowStatusRepo - } -} -``` - -Call `tryAdvanceNurseFlag()` at the end of each of the 3 save methods in `StopTBServiceImpl`. - -**Effort:** Small — new private method in StopTBServiceImpl, reuses existing repo query. - ---- - -## 5. HWC-API + HWC-UI — Stop TB referral worklist - -**Trigger:** When HWC wants to track Stop TB beneficiaries referred to it. - -**Current state:** When Stop TB nurse marks `referralToHWCNeeded = true`, FLW-API writes a -row to `t_benreferdetails` with `referredToInstituteName = 'HWC'` and -`referralReason = 'Stop TB Referral'`. HWC-API has no endpoint that surfaces these rows. - -**HWC-API change needed:** - -New query in `BenReferDetailsRepo`: -```java -@Query("SELECT r FROM BenReferDetails r WHERE r.beneficiaryRegID = :benRegID " + - "AND r.referralReason = 'Stop TB Referral' AND r.deleted = false " + - "ORDER BY r.createdDate DESC") -List findStopTBReferrals(@Param("benRegID") Long benRegID); -``` - -Or for worklist (all referred beneficiaries for a provider): -```java -@Query("SELECT r FROM BenReferDetails r WHERE r.referredToInstituteName = 'HWC' " + - "AND r.referralReason = 'Stop TB Referral' AND r.deleted = false") -List findAllStopTBReferrals(); -``` - -**HWC-UI change needed:** -- New worklist tab or filter: "Stop TB Referrals" -- Shows beneficiary name, referral date, FLW name - -**Effort:** Medium — HWC-API new endpoint + HWC-UI new screen. - ---- - -## Summary table - -| # | Change | Service | Effort | Blocks on | -|---|---|---|---|---| -| 1 | Read standard tables without PSMID filter | HWC-API | Small | HWC team decision | -| 2 | Read TB specific custom tables | HWC-API | Small | HWC team decision | -| 3 | Edit support — upsert in dual-write helpers | FLW-API | Small | Mobile adding edit | -| 4 | nurseFlag auto-advancement | FLW-API | Small | Web dashboard being built | -| 5 | Stop TB referral worklist | HWC-API + HWC-UI | Medium | HWC team + product decision | - -**No mobile change required for any of these.** -**No schema change required for items 1, 2, 3, 4.** -**Item 5 may need a column in `t_benreferdetails` for status tracking (pending/accepted/rejected) — TBD by HWC team.** diff --git a/StopTB_Standard_Table_Integration.md b/StopTB_Standard_Table_Integration.md deleted file mode 100644 index 9df8b37d..00000000 --- a/StopTB_Standard_Table_Integration.md +++ /dev/null @@ -1,294 +0,0 @@ -# Stop TB — Standard Table Integration - -**Branch:** `vb/counselling` -**Date:** 2026-06-30 -**Author:** vishwab1 - ---- - -## 1. Why this was done - -Stop TB's clinical data was siloed entirely inside custom `tb_stoptb_*` tables. No other AMRIT -service (HWC, MMU) could see a Stop TB beneficiary's record — referrals, vitals, prescriptions -were invisible across service lines. - -This integration dual-writes Stop TB clinical data into the same standard tables that MMU/HWC -already use, making beneficiary records portable across services without any change to the -mobile app. - ---- - -## 2. What was implemented - -### 2.1 Visit layer — `t_benvisitdetail` - -**Problem:** Stop TB used its own `tb_stoptb_visit` mirror table. `benVisitID` was null in all -clinical tables. HWC/MMU had no visit anchor to join on. - -**Fix:** `TBStopVisitServiceImpl` was completely rewritten to create rows in `t_benvisitdetail` -instead of `tb_stoptb_visit`. The old entity and repo were deleted. - -**Key behaviour (get-or-create-for-today):** -- First save of the day → creates a new `t_benvisitdetail` row -- Subsequent saves same day → finds and reuses the same row -- Next camp day → creates a new row (separate visit, history preserved) - -**VisitCode formula (same as MMU):** -``` -visitCode = "1" + vanID(zero-padded 5 digits) + benVisitId(zero-padded 8 digits) -Example: vanID=1, benVisitId=42 → 10000100000042 -``` - -**Files changed:** -- `TBStopVisitService.java` — interface return type changed to `BenVisitDetail` -- `TBStopVisitServiceImpl.java` — full rewrite, uses `BenVisitDetailsRepo` -- `BenVisitDetailsRepo.java` — added `findStopTBVisitForToday()` and `countStopTBVisits()` -- `TBStopVisit.java` — **DELETED** (orphaned) -- `TBStopVisitRepo.java` — **DELETED** (orphaned) - ---- - -### 2.2 Standard table dual-writes - -All dual-writes happen inside `StopTBServiceImpl`. Custom `tb_stoptb_*` tables continue to -be written unchanged. Standard table writes are **additive and insert-only** (no edit -capability exists in the mobile). - -Each write is wrapped in its own try/catch — a standard-table failure never breaks the -primary Stop TB save. - -#### `saveGeneralExamination()` writes to: - -| Standard Table | Data source | Fields written | -|---|---|---| -| `t_phy_anthropometry` | `i_beneficiarydetails.otherFields` (registration) | `Height_cm`, `Weight_Kg`, `BMI` | -| `t_phy_vitals` | otherFields + request | `Temperature` (from otherFields), `PulseRate`, `SystolicBP_1stReading`, `DiastolicBP_1stReading`, `BloodGlucose_Random` | -| `t_Phy_GeneralExam` | request | `Pallor`, `Jaundice` (←icterus), `Cyanosis`, `Clubbing`, `Lymphadenopathy`, `Edema` (←oedema) | -| `t_benreferdetails` | request | written only when `referralToHWCNeeded = true` | - -> **Note:** Height/weight/bmi/temperature come from registration `otherFields` because the -> mobile's `GeneralExaminationSaveRequest` does not include these fields. The mobile hardcodes -> them null in `VitalScreenFragment.saveVitals()`. They are captured at registration and synced -> to Identity-API via TM-API. - -#### `saveGeneralOpd()` writes to: - -| Standard Table | Fields written | -|---|---| -| `t_benchiefcomplaint` | `ChiefComplaint` (free text from request) | -| `t_prescription` | `Instruction` (from `notes` field) | -| `t_prescribeddrug` | `GenericDrugName` (medication), `Dose`, `Frequency`, `Duration`, linked to `PrescriptionID` | - -All 7 standard tables share the same `benVisitID` and `VisitCode` as the join key. - ---- - -### 2.3 New entities and repos created - -All in package `com.iemr.flw.domain.iemr` / `com.iemr.flw.repo.iemr`: - -| Entity | Table | Repo | -|---|---|---| -| `BenAnthropometryDetail` | `t_phy_anthropometry` | `BenAnthropometryRepo` | -| `BenPhysicalVitalDetail` | `t_phy_vitals` | `BenPhysicalVitalRepo` | -| `PhyGeneralExamination` | `t_Phy_GeneralExam` | `PhyGeneralExaminationRepo` | -| `BenChiefComplaint` | `t_benchiefcomplaint` | `BenChiefComplaintRepo` | -| `PrescriptionDetail` | `t_prescription` | `PrescriptionDetailRepo` | -| `PrescribedDrugDetail` | `t_prescribeddrug` | `PrescribedDrugDetailRepo` | - -All 6 standard tables already existed in `db_iemr`. No CREATE TABLE needed for these. -`BenReferDetails` and `BenReferDetailsRepo` already existed in FLW-API — just wired up. - ---- - -### 2.4 Service files updated - -All Stop TB service files that previously used `TBStopVisit` and `visit.getId()` were updated -to use `BenVisitDetail` and `visit.getVisitCode()`: - -| File | Change | -|---|---| -| `StopTBServiceImpl` | `saveGeneralExamination`, `saveNurseTBScreening`, `saveGeneralOpd` — all updated | -| `TBSuspectedServiceImpl` | `save()` — updated | -| `TBConfirmedCaseServiceImpl` | `save()` — updated | - -The `visitCode` column on `tb_stoptb_suspected` and `tb_stoptb_confirmed_cases` is still used -for per-visit keying. The value written is now the 14-digit standard visitCode instead of the -old `tb_stoptb_visit` PK. - ---- - -## 3. DB migration required - -Run these two statements on the server. The standard tables (`t_phy_anthropometry` etc.) -already exist in `db_iemr` — no CREATE needed for them. - -```sql --- Required: visitCode column added to these two tables when per-visit history was introduced -ALTER TABLE tb_stoptb_suspected ADD COLUMN visitCode BIGINT; -ALTER TABLE tb_stoptb_confirmed_cases ADD COLUMN visitCode BIGINT; - --- DO NOT run: tb_stoptb_visit is replaced by t_benvisitdetail --- CREATE TABLE tb_stoptb_visit ← SKIP THIS -``` - ---- - -## 4. BenFlowStatus (`i_ben_flow_outreach`) — current state - -| Flag | Set when | Value | -|---|---|---| -| `nurseFlag` | Registration | `1` (pending for nurse) | -| `nurseFlag` | `submitNurseData()` | `9` (nurse done) | -| `doctorFlag` | `routeToStopTBCounsellor()` | `1` (pending for counsellor) | - -**Known gap:** Mobile does not call `flw-api/stoptb/nurse/submit` (app is live, cannot change). -So `nurseFlag` stays at `1` forever after registration. - -**Why this is not breaking:** The mobile also does not call the nurse worklist or registrar -worklist endpoints — both are commented out in `AmritApiService.kt`. The mobile uses -`flw-api/beneficiary/getBeneficiaryData` instead and manages its own local state. - -**Counsellor:** Finds confirmed TB cases via direct `tb_confirmed_cases` query, not via -`doctorFlag`. This works correctly today. - -**Future action (when web dashboard is built):** Wire up `nurseFlag` advancement by checking -all 3 mandatory nurse saves (generalExamination + tbScreening + generalOpd) exist for the -same `visitCode`, then auto-advance `nurseFlag = 9`. The query -`updateStopTBAfterNurseSubmit()` in `BenFlowStatusRepo` already exists for this. - ---- - -## 5. Complete mobile API list (live, as of 2026-06-30) - -### Auth -| Endpoint | -|---| -| `POST common-api/user/userAuthenticate` | -| `POST common-api/user/refreshToken` | -| `GET flw-api/user/getUserDetail` | -| `POST common-api/firebaseNotification/userToken` | - -### Beneficiary Registration -| Endpoint | -|---| -| `POST tm-api/registrar/registrarBeneficaryRegistrationNew` | -| `POST identity-api/rmnch/syncDataToAmrit` | -| `POST flw-api/beneficiary/getBeneficiaryData` | - -### Consent / OTP -| Endpoint | -|---| -| `POST common-api/beneficiaryConsent/sendConsent` | -| `POST common-api/beneficiaryConsent/resendConsent` | -| `POST common-api/beneficiaryConsent/validateConsent` | - -### Stop TB — Nurse -| Endpoint | -|---| -| `POST flw-api/stoptb/nurse/generalExamination/save` | -| `POST flw-api/stoptb/nurse/generalExamination/getAll` | -| `POST flw-api/stoptb/nurse/tbScreening/save` | -| `POST flw-api/stoptb/nurse/tbScreening/getAll` | -| `POST flw-api/stoptb/nurse/generalOpd/save` | -| `POST flw-api/stoptb/nurse/generalOpd/getAll` | -| `POST flw-api/stoptb/nurse/diagnostics/save` | -| `POST flw-api/stoptb/nurse/diagnostics/getAll` | - -### Stop TB — Suspected / Confirmed -| Endpoint | -|---| -| `POST flw-api/tb/suspected/saveAll` | -| `POST flw-api/tb/suspected/getAll` | -| `POST flw-api/tb/confirmed/save` | -| `GET flw-api/tb/confirmed/getAll` | - -### Counselling (Dynamic Form) -| Endpoint | -|---| -| `GET flw-api/dynamicForm/getAllForms` | -| `GET common-api/dynamicForm/form/{formId}/fields` | -| `POST flw-api/counselling/save` | -| `POST flw-api/dynamicForm/response/submitBulk` | -| `POST flw-api/dynamicForm/response/complete` | -| `GET flw-api/dynamicForm/response/getByBeneficiary` | -| `GET flw-api/dynamicForm/response/getCompletedBeneficiaries` | - -### CBAC / NCD -| Endpoint | -|---| -| `POST hwc-api/NCD/getByUserCbacDetails` | -| `POST hwc-api/NCD/save/nurseData` | -| `POST hwc-api/NCD/save/referDetails` | - -### ABHA / Health ID -| Endpoint | -|---| -| `POST fhir-api/healthIDWithUID/createHealthIDWithUID` | -| `POST fhir-api/healthID/getBenhealthID` | -| `POST fhir-api/healthIDRecord/mapHealthIDToBeneficiary` | -| `POST fhir-api/healthIDRecord/addHealthIdRecord` | -| `POST fhir-api/healthIDCard/generateOTP` | -| `POST fhir-api/healthIDCard/verifyOTPAndGenerateHealthCard` | - -### Other Disease Screens -| Endpoint | -|---| -| `POST flw-api/disease/kalaAzar/saveAll` | -| `POST flw-api/disease/malaria/saveAll` | -| `POST flw-api/disease/leprosy/saveAll` | -| `POST flw-api/disease/leprosy/followUp/saveAll` | -| `POST flw-api/disease/leprosy/followUp/getAll` | -| `POST flw-api/disease/aesJe/saveAll` | -| `POST flw-api/disease/filaria/saveAll` | -| `POST flw-api/disease/getAllDisease` | -| `POST flw-api/follow-up/save` | -| `POST flw-api/follow-up/get` | -| `POST flw-api/child-care/hbncVisit/saveAll` | -| `POST flw-api/disease/cdtfVisit/saveAll` | -| `POST flw-api/disease/cdtfVisit/getAll` | - -### Commented out / never called (documented for future reference) -| Endpoint | Reason | -|---|---| -| `POST flw-api/stoptb/nurse/worklist` | Mobile uses getBeneficiaryData instead | -| `POST flw-api/stoptb/registrar/worklist` | Mobile uses getBeneficiaryData instead | -| `POST flw-api/stoptb/nurse/submit` | Mobile is live — cannot add new call; nurseFlag stays 1 (acceptable, see Section 4) | -| `POST hwc-api/common/getBenReferDetailsByCreatedBy` | Referral pull not needed in Stop TB | -| `POST hwc-api/sync/generalOPDNurseFormDataToServer` | Abandoned earlier attempt at standard-table integration | -| `POST identity-api/id/getByBenId` | getBenHealthID used instead | - ---- - -## 6. What HWC can now see - -Once the DB migration SQL is run and FLW-API is deployed, any service reading from `db_iemr` -standard tables will find Stop TB beneficiary data joined by `benVisitID` / `VisitCode`: - -| Table | Content | -|---|---| -| `t_benvisitdetail` | One row per camp day, `visitCategory = 'Stop TB'` | -| `t_phy_anthropometry` | Height, weight, BMI (from registration) | -| `t_phy_vitals` | Temperature (from registration), pulse, BP, RBS (from nurse exam) | -| `t_Phy_GeneralExam` | Pallor, jaundice, cyanosis, clubbing, lymphadenopathy, edema | -| `t_benchiefcomplaint` | Chief complaint text | -| `t_prescription` | Notes/instructions | -| `t_prescribeddrug` | Medication, dose, frequency, duration | -| `t_benreferdetails` | HWC referral (only when `referralToHWCNeeded = true`) | - -> **Note:** HWC's referral-receiving worklist endpoints (`getBenPreviousReferralHistoryDetails`, -> `getBenReferDetailsByCreatedBy`) have zero callers on HWC-UI and HWC-Mobile-App as of today. -> Writing `t_benreferdetails` is correct for audit/reporting. Surfacing it as a live HWC -> worklist is a separate, future piece of work. - ---- - -## 7. What is NOT done (future scope) - -| Item | Detail | -|---|---| -| `nurseFlag` auto-advancement | Needs web dashboard to be useful. Logic: check all 3 saves (exam + screening + OPD) exist for same `visitCode` → set `nurseFlag=9`. Query already exists in `BenFlowStatusRepo.updateStopTBAfterNurseSubmit()` | -| HWC worklist for Stop TB referrals | HWC-API and HWC-UI changes needed to surface `t_benreferdetails` rows | -| Duplicate beneficiary detection | Requires new mobile screen — no server-side gap | -| Beneficiary edit/update | Requires new mobile screen — no edit endpoint exists anywhere in Stop TB | -| `height`/`weight`/`bmi`/`respiratoryRate`/`spo2` in nurse exam | Mobile captures these locally (`VitalCache`) but `GeneralExaminationSaveRequest.from()` hardcodes them null. Would require mobile change to send live | From 46536d30923a1d6ea7f3ac00edf4114c9ea6d9a1 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 13 Jul 2026 17:44:45 +0530 Subject: [PATCH 81/97] Revert "Vb/counselling (#270)" This reverts commit 0861661e490855109f03b3a9d8d71c9da983eb92. --- .../flw/dto/iemr/QuestionAnswerRequest.java | 1 - .../flw/dto/iemr/QuestionResponseDTO.java | 2 +- .../com/iemr/flw/masterEnum/QuestionType.java | 4 ++-- .../service/impl/BeneficiaryServiceImpl.java | 19 ------------------- .../impl/DynamicFormResponseServiceImpl.java | 3 +-- .../service/impl/FormResponseItemSaver.java | 2 +- 6 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java b/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java index ad314ba5..8ee08c45 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java +++ b/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java @@ -33,7 +33,6 @@ * 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) - * CHECKBOX → optionValue (single string, e.g. "CHECKED") * MCQ → optionValues (list of strings, one row saved per element) * TEXT/AUTO_FILL → answerText * DATE → answerDate (ISO date string) or answerText diff --git a/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java b/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java index 98557c10..f5f76d63 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java @@ -40,7 +40,7 @@ public class QuestionResponseDTO { private Long questionResponseId; private Long questionId; - /** Populated for RADIO, MCQ, and CHECKBOX answers. */ + /** 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/masterEnum/QuestionType.java b/src/main/java/com/iemr/flw/masterEnum/QuestionType.java index 82308544..0af07aa1 100644 --- a/src/main/java/com/iemr/flw/masterEnum/QuestionType.java +++ b/src/main/java/com/iemr/flw/masterEnum/QuestionType.java @@ -32,8 +32,8 @@ public enum QuestionType { RADIO, /** Multi-select from a predefined list of options. */ MCQ, - /** Single boolean checkbox — checked or unchecked. Resolved like RADIO: submit - * optionValue to select it, omit to leave unanswered. */ + /** Single boolean checkbox — checked or unchecked. May carry options for display + * purposes; answers are still stored as free text. */ CHECKBOX, /** Free-text input. */ TEXT, 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 1d332bc2..ebe4c6d9 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -365,25 +365,6 @@ private String getMappingsForAddressIDs(List addressLi if (benDetailsRMNCH_OBJ.getGpsLongitude() != null) benDetailsRMNCH_OBJ.setLongitude(BigDecimal.valueOf(benDetailsRMNCH_OBJ.getGpsLongitude())); - // 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()); - - // 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())); - // ----------------------------------------------------------------------------- // related benids diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index a36710a5..64f405b9 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -37,7 +37,6 @@ import com.iemr.flw.dto.iemr.SectionResponseDTO; import com.iemr.flw.domain.iemr.DynamicForm; import com.iemr.flw.masterEnum.FormResponseStatus; -import com.iemr.flw.masterEnum.FormResponseStatus; import com.iemr.flw.masterEnum.FormType; import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.repo.iemr.FormResponseRepo; @@ -389,7 +388,7 @@ private List processAnswers( // Delete any existing answers for this question in this section (handles re-saves) questionResponseRepo.deleteByQuestionIdAndSectionResponseId(questionId, sectionResponseId); - if (type == QuestionType.RADIO || type == QuestionType.CHECKBOX) { + if (type == QuestionType.RADIO) { if (answer.getOptionValue() != null) { QuestionOption opt = resolveOption( optionsByQuestion, questionId, answer.getOptionValue(), answer.getQuestionUuid()); diff --git a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java index 4ff8ca07..4767f505 100644 --- a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -328,7 +328,7 @@ private List processAnswers( questionResponseRepo.deleteByQuestionIdAndSectionResponseId(questionId, sectionResponseId); - if (type == QuestionType.RADIO || type == QuestionType.CHECKBOX) { + if (type == QuestionType.RADIO) { if (answer.getOptionValue() != null) { QuestionOption opt = resolveOption(optionsByQuestion, questionId, answer.getOptionValue(), answer.getQuestionUuid()); From 03dfd29785c124728defa98769cf33bc8c8f65f7 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 13 Jul 2026 17:46:51 +0530 Subject: [PATCH 82/97] Revert "Vb/counselling (#269)" This reverts commit 8bdc9dee65c9f4d46c20545caba217a2a66b1469. --- .../DynamicFormResponseController.java | 10 +- .../com/iemr/flw/domain/iemr/FormSection.java | 11 +- .../iemr/flw/domain/iemr/SectionQuestion.java | 2 +- .../dto/iemr/CompletedBeneficiariesDTO.java | 44 --- .../com/iemr/flw/dto/iemr/FormSectionDTO.java | 7 +- .../iemr/flw/dto/iemr/SectionResponseDTO.java | 2 - .../flw/masterEnum/FormResponseStatus.java | 36 --- .../com/iemr/flw/masterEnum/FormType.java | 3 +- .../com/iemr/flw/masterEnum/QuestionType.java | 3 - .../com/iemr/flw/masterEnum/SectionPhase.java | 37 --- .../flw/seeder/TbCounsellingFormSeeder.java | 15 +- .../flw/seeder/TbCounsellingV2FormSeeder.java | 287 ------------------ .../service/DynamicFormResponseService.java | 10 +- .../impl/DynamicFormResponseServiceImpl.java | 123 ++------ .../service/impl/FormResponseItemSaver.java | 36 +-- 15 files changed, 61 insertions(+), 565 deletions(-) delete mode 100644 src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java delete mode 100644 src/main/java/com/iemr/flw/masterEnum/FormResponseStatus.java delete mode 100644 src/main/java/com/iemr/flw/masterEnum/SectionPhase.java delete mode 100644 src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java index 33ac46b2..49d8e547 100644 --- a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -21,7 +21,6 @@ */ package com.iemr.flw.controller; -import com.iemr.flw.dto.iemr.CompletedBeneficiariesDTO; import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; import com.iemr.flw.masterEnum.FormType; @@ -54,7 +53,7 @@ public class DynamicFormResponseController { private final DynamicFormResponseService responseService; - @Operation(summary = "Submit GENERAL_INFO and PRE_SUBMIT section answers, status → SUBMITTED. " + + @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( @@ -99,14 +98,13 @@ 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 or REFUSED form response for the given form type, optionally filtered by village and/or provider service map") + @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) { - CompletedBeneficiariesDTO result = - responseService.getCompletedBeneficiaries(formType, villageId, providerServiceMapId); - return ResponseEntity.ok(new ApiResponse(true, "Completed beneficiaries fetched successfully", result)); + 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/domain/iemr/FormSection.java b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java index 28f50859..b8454716 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java @@ -21,12 +21,9 @@ */ package com.iemr.flw.domain.iemr; -import com.iemr.flw.masterEnum.SectionPhase; 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; @@ -74,9 +71,9 @@ public class FormSection { @Column(name = "sectionName_hindi", length = 255) private String sectionNameHindi; - @Enumerated(EnumType.STRING) + /** PRE_SUBMIT sections are shown before the form's main submit. POST_SUBMIT after. */ @Column(name = "sectionPhase", nullable = false, length = 20) - private SectionPhase sectionPhase; + private String sectionPhase; @Column(name = "isRequired", nullable = false) private Boolean isRequired = true; @@ -84,12 +81,10 @@ public class FormSection { @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 = "isEditable", nullable = false) - private Boolean isEditable = false; - @Column(name = "created_by", length = 100) private String createdBy; diff --git a/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java index 91cc3c43..1bd7991b 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java @@ -47,7 +47,7 @@ /** * A question within a form section. - * questionType: RADIO | MCQ | CHECKBOX | TEXT | DATE | DISPLAY | AUTO_FILL + * questionType: RADIO | MCQ | TEXT | DATE | DISPLAY | AUTO_FILL */ @Entity @Table(name = "t_section_question", schema = "db_iemr") diff --git a/src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java b/src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java deleted file mode 100644 index b391cff0..00000000 --- a/src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.util.List; - -/** - * Beneficiary IDs for a form type, split by outcome: those who completed the form and those - * who refused (TB2_GI_Q1 = NO on the General Info consent question). - * - * @author Piramal Swasthya - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class CompletedBeneficiariesDTO { - private List completed; - private List refused; -} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java b/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java index 6bb10795..2db1f8c7 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java @@ -21,7 +21,6 @@ */ package com.iemr.flw.dto.iemr; -import com.iemr.flw.masterEnum.SectionPhase; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -50,8 +49,8 @@ public class FormSectionDTO { private String sectionNameHindi; - @NotNull(message = "sectionPhase is required (GENERAL_INFO, PRE_SUBMIT, or POST_SUBMIT)") - private SectionPhase sectionPhase; + @NotBlank(message = "sectionPhase is required (PRE_SUBMIT or POST_SUBMIT)") + private String sectionPhase; private Boolean isRequired = true; @@ -60,8 +59,6 @@ public class FormSectionDTO { private Boolean hasSubmitButton = false; - private Boolean isEditable = false; - @Valid private List questions = 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 index 78c918fe..dac289a7 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/SectionResponseDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/SectionResponseDTO.java @@ -42,8 +42,6 @@ public class SectionResponseDTO { private Long sectionResponseId; private Long sectionId; - private String sectionUuid; - private Boolean isEditable; private String status; private Timestamp savedAt; private List answers; diff --git a/src/main/java/com/iemr/flw/masterEnum/FormResponseStatus.java b/src/main/java/com/iemr/flw/masterEnum/FormResponseStatus.java deleted file mode 100644 index 62fffd00..00000000 --- a/src/main/java/com/iemr/flw/masterEnum/FormResponseStatus.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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; - -/** - * Lifecycle status stored in FormResponse.status. - * - * @author Piramal Swasthya - */ -public enum FormResponseStatus { - /** Saved via /submit. */ - SUBMITTED, - /** Saved via /complete, consent question answered YES (or not applicable). */ - COMPLETE, - /** Saved via /complete, consent question TB2_GI_Q1 answered NO. */ - REFUSED -} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/masterEnum/FormType.java b/src/main/java/com/iemr/flw/masterEnum/FormType.java index ccb0cfe2..779fce19 100644 --- a/src/main/java/com/iemr/flw/masterEnum/FormType.java +++ b/src/main/java/com/iemr/flw/masterEnum/FormType.java @@ -30,6 +30,5 @@ public enum FormType { COUNSELLING_MODULE, SCREENING, COUNSELLING, - TB_COUNSELLING, - TB_COUNSELLING_V2 + TB_COUNSELLING } diff --git a/src/main/java/com/iemr/flw/masterEnum/QuestionType.java b/src/main/java/com/iemr/flw/masterEnum/QuestionType.java index 0af07aa1..ae4b91e1 100644 --- a/src/main/java/com/iemr/flw/masterEnum/QuestionType.java +++ b/src/main/java/com/iemr/flw/masterEnum/QuestionType.java @@ -32,9 +32,6 @@ public enum QuestionType { RADIO, /** Multi-select from a predefined list of options. */ MCQ, - /** Single boolean checkbox — checked or unchecked. May carry options for display - * purposes; answers are still stored as free text. */ - CHECKBOX, /** Free-text input. */ TEXT, /** Date picker input. */ diff --git a/src/main/java/com/iemr/flw/masterEnum/SectionPhase.java b/src/main/java/com/iemr/flw/masterEnum/SectionPhase.java deleted file mode 100644 index 13faa60d..00000000 --- a/src/main/java/com/iemr/flw/masterEnum/SectionPhase.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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 phases a form section can belong to. - * No other values may be stored in t_form_section.sectionPhase. - * - * @author Piramal Swasthya - */ -public enum SectionPhase { - /** Shown first, before PRE_SUBMIT sections. */ - GENERAL_INFO, - /** Shown before the form's main submit. Saved via /submit. */ - PRE_SUBMIT, - /** Shown after the main submit. Saved via /complete. */ - POST_SUBMIT -} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java index ecddba20..4fdd3a2d 100644 --- a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java @@ -29,7 +29,6 @@ import com.iemr.flw.dto.iemr.SectionQuestionDTO; import com.iemr.flw.masterEnum.FormType; import com.iemr.flw.masterEnum.QuestionType; -import com.iemr.flw.masterEnum.SectionPhase; import com.iemr.flw.masterEnum.ValidationType; import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.service.DynamicFormDefinitionService; @@ -94,7 +93,7 @@ private DynamicFormDTO buildFormDto() { // ── Section A: Disease Awareness ───────────────────────────────────────────── private FormSectionDTO buildSectionA() { - FormSectionDTO s = section("TB_SEC_A", "Disease Awareness", "बीमारी के बारे में जागरूकता", SectionPhase.PRE_SUBMIT, 1, true, false); + 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)); @@ -108,7 +107,7 @@ private FormSectionDTO buildSectionA() { // ── Section B: Do's and Don'ts ─────────────────────────────────────────────── private FormSectionDTO buildSectionB() { - FormSectionDTO s = section("TB_SEC_B", "Do's and Don'ts", "करो और ना करो", SectionPhase.PRE_SUBMIT, 2, true, false); + 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)); @@ -124,7 +123,7 @@ private FormSectionDTO buildSectionB() { // ── Section C: Government Schemes ──────────────────────────────────────────── private FormSectionDTO buildSectionC() { - FormSectionDTO s = section("TB_SEC_C", "Government Schemes", "सरकारी योजनाएं", SectionPhase.PRE_SUBMIT, 3, false, false); + 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)); @@ -136,7 +135,7 @@ private FormSectionDTO buildSectionC() { // ── Section D: Treatment Regimen ───────────────────────────────────────────── private FormSectionDTO buildSectionD() { - FormSectionDTO s = section("TB_SEC_D", "Treatment Regimen", "इलाज का तरीका", SectionPhase.PRE_SUBMIT, 4, true, false); + 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)); @@ -150,7 +149,7 @@ private FormSectionDTO buildSectionD() { // ── Section E: Counselling Completion ──────────────────────────────────────── private FormSectionDTO buildSectionE() { - FormSectionDTO s = section("TB_SEC_E", "Counselling Completion", "काउंसलिंग पूरी होना", SectionPhase.PRE_SUBMIT, 5, true, true); + FormSectionDTO s = section("TB_SEC_E", "Counselling Completion", "काउंसलिंग पूरी होना", "PRE_SUBMIT", 5, true, true); List qs = new ArrayList<>(); // Counselling completion status — RADIO: Complete | Refused @@ -195,7 +194,7 @@ private FormSectionDTO buildSectionE() { // ── Section F: Follow Up to TU ─────────────────────────────────────────────── private FormSectionDTO buildSectionF() { - FormSectionDTO s = section("TB_SEC_F", "Follow Up to TU", "TU के बाद की कार्रवाई", SectionPhase.POST_SUBMIT, 6, true, true); + 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 @@ -245,7 +244,7 @@ private FormSectionDTO buildSectionF() { // ── Builder Helpers ─────────────────────────────────────────────────────────── - private FormSectionDTO section(String uuid, String name, String nameHindi, SectionPhase phase, + private FormSectionDTO section(String uuid, String name, String nameHindi, String phase, int order, boolean required, boolean hasSubmitButton) { FormSectionDTO s = new FormSectionDTO(); s.setSectionUuid(uuid); diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java deleted file mode 100644 index 0173db59..00000000 --- a/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * 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.SectionPhase; -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 (v2) form definition on application startup if it does not already exist. - * Idempotent: skips creation if the form UUID is already present in the database. - * - * Adds an initial GENERAL_INFO consent gate ("Has the beneficiary agreed for counselling?") ahead - * of the PRE_SUBMIT sections, and uses CHECKBOX questions in place of Yes/No RADIO questions. - * - * @author Piramal Swasthya - */ -@Component -@RequiredArgsConstructor -public class TbCounsellingV2FormSeeder { - - private static final Logger log = LoggerFactory.getLogger(TbCounsellingV2FormSeeder.class); - - public static final String FORM_UUID = "TB_COUNSELLING_V2"; - - private final DynamicFormDefinitionService formService; - private final DynamicFormRepo formRepo; - - @PostConstruct - public void seed() { - if (formRepo.findByFormUuid(FORM_UUID).isPresent()) { - log.info("TbCounsellingV2FormSeeder: form '{}' already exists — skipping seed.", FORM_UUID); - return; - } - log.info("TbCounsellingV2FormSeeder: seeding TB Counselling (v2) form..."); - formService.createForm(buildFormDto()); - log.info("TbCounsellingV2FormSeeder: seed complete."); - } - - // ── Form Definition ────────────────────────────────────────────────────────── - - private DynamicFormDTO buildFormDto() { - DynamicFormDTO dto = new DynamicFormDTO(); - dto.setFormUuid(FORM_UUID); - dto.setFormName("TB Counselling"); - dto.setFormType(FormType.TB_COUNSELLING_V2); - dto.setIsActive(true); - dto.setFollowUpDelayDays(15); - - List sections = new ArrayList<>(); - sections.add(buildGeneralInfoSection()); - sections.add(buildSectionA()); - sections.add(buildSectionB()); - sections.add(buildSectionC()); - sections.add(buildSectionD()); - sections.add(buildSectionE()); - dto.setSections(sections); - return dto; - } - - // ── General Info: Counselling Consent ──────────────────────────────────────── - - private FormSectionDTO buildGeneralInfoSection() { - FormSectionDTO s = section("TB2_SEC_GENERAL_INFO", "General Information", "सामान्य जानकारी", - SectionPhase.GENERAL_INFO, 1, true, false, true); - List qs = new ArrayList<>(); - - // "Has the beneficiary agreed for counselling?" — RADIO: Yes | No - SectionQuestionDTO consentQ = new SectionQuestionDTO(); - consentQ.setQuestionUuid("TB2_GI_Q1"); - consentQ.setQuestionText("Has the beneficiary agreed for counselling?"); - consentQ.setQuestionTextHindi("क्या लाभार्थी काउंसलिंग के लिए सहमत हुआ है?"); - consentQ.setQuestionType(QuestionType.RADIO); - consentQ.setIsMandatory(true); - consentQ.setDisplayOrder(1); - consentQ.setVisibleByDefault(true); - - // "Yes" option — no conditions, sections A-E remain enabled by default - QuestionOptionDTO yesOpt = option("Yes", "हाँ", "YES", "हाँ", 1, List.of()); - - // "No" option — disable validation for sections A-E + show refusal reason field - List noConditions = new ArrayList<>(); - noConditions.add(disableSectionValidation("TB2_SEC_A")); - noConditions.add(disableSectionValidation("TB2_SEC_B")); - noConditions.add(disableSectionValidation("TB2_SEC_C")); - noConditions.add(disableSectionValidation("TB2_SEC_D")); - noConditions.add(disableSectionValidation("TB2_SEC_E")); - noConditions.add(showQuestion("TB2_GI_REFUSAL")); - QuestionOptionDTO noOpt = option("No", "नहीं", "NO", "नहीं", 2, noConditions); - - List consentOptions = new ArrayList<>(); - consentOptions.add(yesOpt); - consentOptions.add(noOpt); - consentQ.setOptions(consentOptions); - consentQ.setValidations(List.of()); - qs.add(consentQ); - - // Reason for refusal — hidden by default, mandatory when shown, max 300 chars - qs.add(textQuestion("TB2_GI_REFUSAL", "Reason for refusal", "इनकार का कारण", 2, true, 300, false)); - - s.setQuestions(qs); - return s; - } - - // ── Section A: Disease Awareness ───────────────────────────────────────────── - - private FormSectionDTO buildSectionA() { - FormSectionDTO s = section("TB2_SEC_A", "Disease Awareness", "बीमारी के बारे में जागरूकता", SectionPhase.PRE_SUBMIT, 2, true, false, false); - List qs = new ArrayList<>(); - qs.add(checkboxQuestion("TB2_A_Q1", "TB disease explained to patient", "मरीज़ को टीबी बीमारी के बारे में समझाया गया।", 1, true)); - qs.add(checkboxQuestion("TB2_A_Q2", "Transmission route explained", "ट्रांसमिशन के तरीके के बारे में जानकारी", 2, true)); - qs.add(checkboxQuestion("TB2_A_Q3", "Symptoms explained", "लक्षणों की जानकारी", 3, true)); - qs.add(checkboxQuestion("TB2_A_Q4", "Treatment duration explained", "इलाज की अवधि के बारे में जानकारी", 4, true)); - qs.add(textQuestion("TB2_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("TB2_SEC_B", "Do's and Don'ts", "करो और ना करो", SectionPhase.PRE_SUBMIT, 3, true, false, false); - List qs = new ArrayList<>(); - qs.add(checkboxQuestion("TB2_B_Q1", "Cover mouth while coughing — advised", "खांसते समय मुंह ढकने की सलाह दी जाती है।", 1, true)); - qs.add(checkboxQuestion("TB2_B_Q2", "Complete full treatment course — advised", "इलाज का पूरा कोर्स पूरा करने की सलाह दी जाती है।", 2, true)); - qs.add(checkboxQuestion("TB2_B_Q3", "Regular follow-up attendance — advised", "इलाज का पूरा कोर्स पूरा करने की सलाह दी जाती है।", 3, true)); - qs.add(checkboxQuestion("TB2_B_Q4", "Nutritional guidance provided", "पोषण संबंधी मार्गदर्शन प्रदान किया गया", 4, true)); - qs.add(checkboxQuestion("TB2_B_Q5", "No smoking / alcohol — advised", "धूम्रपान / शराब न लेने की सलाह दी जाती है।", 5, true)); - qs.add(checkboxQuestion("TB2_B_Q6", "Isolation precautions explained", "अलगाव सावधानियों के बारे में बताया गया", 6, true)); - qs.add(textQuestion("TB2_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("TB2_SEC_C", "Government Schemes", "सरकारी योजनाएं", SectionPhase.PRE_SUBMIT, 4, true, false, false); - List qs = new ArrayList<>(); - qs.add(checkboxQuestion("TB2_C_Q1", "Nikshay Poshan Yojana (NPY) eligibility explained", "निक्षय पोषण योजना (एनपीवाई) पात्रता के बारे में बताया गया", 1, true)); - qs.add(checkboxQuestion("TB2_C_Q2", "DOTS free treatment explained", "DOTS मुफ़्त इलाज के बारे में जानकारी", 2, true)); - qs.add(textQuestion("TB2_C_REMARKS", "Schemes notes", "योजनाओं के नोट्स", 3, false, 300, true)); - s.setQuestions(qs); - return s; - } - - // ── Section D: Treatment Regimen ───────────────────────────────────────────── - - private FormSectionDTO buildSectionD() { - FormSectionDTO s = section("TB2_SEC_D", "Treatment Regimen", "इलाज का तरीका", SectionPhase.PRE_SUBMIT, 5, true, false, false); - List qs = new ArrayList<>(); - qs.add(checkboxQuestion("TB2_D_Q1", "Regimen explained to patient", "मरीज़ को इलाज का तरीका समझाया गया।", 1, true)); - qs.add(checkboxQuestion("TB2_D_Q2", "Medication names explained", "दवाओं के नामों की जानकारी", 2, true)); - qs.add(checkboxQuestion("TB2_D_Q3", "Side effects explained", "साइड इफ़ेक्ट्स के बारे में जानकारी", 3, true)); - qs.add(checkboxQuestion("TB2_D_Q4", "Importance of adherence explained", "निर्देशों का पालन करने का महत्व समझाया गया", 4, true)); - qs.add(textQuestion("TB2_D_REMARKS", "Treatment regimen notes", "इलाज के तरीके से जुड़े नोट्स", 5, false, 300, true)); - s.setQuestions(qs); - return s; - } - - // ── Section E: Impact Assessment ───────────────────────────────────────────── - - private FormSectionDTO buildSectionE() { - FormSectionDTO s = section("TB2_SEC_E", "Impact Assessment", "प्रभाव का आकलन", SectionPhase.PRE_SUBMIT, 6, true, true, true); - List qs = new ArrayList<>(); - qs.add(textQuestion("TB2_E_Q1", - "How has TB disease impacted the life of this beneficiary", - "टीबी बीमारी ने इस लाभार्थी के जीवन को कैसे प्रभावित किया है", - 1, false, 400, true)); - s.setQuestions(qs); - return s; - } - - // ── Builder Helpers ─────────────────────────────────────────────────────────── - - private FormSectionDTO section(String uuid, String name, String nameHindi, SectionPhase phase, - int order, boolean required, boolean hasSubmitButton, boolean isEditable) { - 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.setIsEditable(isEditable); - s.setQuestions(new ArrayList<>()); - return s; - } - - private SectionQuestionDTO checkboxQuestion(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.CHECKBOX); - q.setIsMandatory(mandatory); - q.setDisplayOrder(order); - q.setVisibleByDefault(true); - q.setOptions(List.of(option("Checked", "चेक किया गया", "CHECKED", "चेक किया गया", 1, List.of()))); - 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; - } -} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java index 61a3bce1..cca29e2e 100644 --- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -21,7 +21,6 @@ */ package com.iemr.flw.service; -import com.iemr.flw.dto.iemr.CompletedBeneficiariesDTO; import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; import com.iemr.flw.masterEnum.FormType; @@ -36,7 +35,7 @@ */ public interface DynamicFormResponseService { - /** Save GENERAL_INFO and PRE_SUBMIT section answers and advance status to SUBMITTED. */ + /** Save PRE_SUBMIT section answers and advance status to SUBMITTED. */ FormResponseDTO submitForm(FormResponseRequest request); /** Save POST_SUBMIT section answers and advance status to COMPLETE. */ @@ -62,9 +61,6 @@ public interface DynamicFormResponseService { */ List findPendingFollowUps(List formIds, int delayDays); - /** - * Returns beneficiary IDs for the given form type split by outcome — COMPLETE and REFUSED — - * optionally filtered by village and/or provider service map. - */ - CompletedBeneficiariesDTO getCompletedBeneficiaries(FormType formType, Integer villageId, Integer providerServiceMapId); + /** 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/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index 64f405b9..28d89fce 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -28,7 +28,6 @@ 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.CompletedBeneficiariesDTO; import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; import com.iemr.flw.dto.iemr.QuestionAnswerRequest; @@ -36,7 +35,6 @@ 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.FormResponseStatus; import com.iemr.flw.masterEnum.FormType; import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.repo.iemr.FormResponseRepo; @@ -47,7 +45,6 @@ import com.iemr.flw.repo.iemr.SectionQuestionRepo; import com.iemr.flw.repo.iemr.SectionResponseRepo; import com.iemr.flw.masterEnum.QuestionType; -import com.iemr.flw.masterEnum.SectionPhase; import com.iemr.flw.service.CampConfigService; import com.iemr.flw.service.DynamicFormResponseService; import com.iemr.flw.utils.JwtUtil; @@ -79,11 +76,10 @@ @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 static final String SECTION_STATUS_REFUSED = "REFUSED"; - private static final String SECTION_UUID_GENERAL_INFO = "TB2_SEC_GENERAL_INFO"; - private static final String QUESTION_UUID_CONSENT = "TB2_GI_Q1"; - private static final String OPTION_VALUE_CONSENT_REFUSED = "NO"; private final DynamicFormRepo dynamicFormRepo; private final FormResponseRepo formResponseRepo; @@ -110,19 +106,19 @@ public FormResponseDTO submitForm(FormResponseRequest request) { formResponse = formResponseRepo.findById(request.getResponseId()) .orElseThrow(() -> new NoSuchElementException( "FormResponse not found: " + request.getResponseId())); - if (FormResponseStatus.COMPLETE.name().equals(formResponse.getStatus())) { + 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(FormResponseStatus.SUBMITTED.name()); + formResponse.setStatus(STATUS_SUBMITTED); formResponse.setSubmittedAt(now); formResponse.setLastFollowUpAt(now); formResponse = formResponseRepo.save(formResponse); } else { - formResponse = createFormResponse(request, version, FormResponseStatus.SUBMITTED.name(), now, vanID, parkingPlaceID); + formResponse = createFormResponse(request, version, STATUS_SUBMITTED, now, vanID, parkingPlaceID); } - return processSections(formResponse, request, version, null, false, vanID, parkingPlaceID); + return processSections(formResponse, request, version, null, vanID, parkingPlaceID); } @Override @@ -139,16 +135,14 @@ public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken List existing = formResponseRepo.findByBeneficiaryIdAndFormId(request.getBeneficiaryId(), formId); FormResponse formResponse = existing.isEmpty() - ? createFormResponse(request, version, FormResponseStatus.SUBMITTED.name(), now, vanID, parkingPlaceID) + ? createFormResponse(request, version, STATUS_SUBMITTED, now, vanID, parkingPlaceID) : existing.get(0); formResponse.setUpdatedBy(actor); - formResponse.setStatus(isConsentRefused(request) - ? FormResponseStatus.REFUSED.name() - : FormResponseStatus.COMPLETE.name()); + formResponse.setStatus(STATUS_COMPLETE); formResponse.setCompletedAt(now); formResponse = formResponseRepo.save(formResponse); - return processSections(formResponse, request, version, actor, true, vanID, parkingPlaceID); + return processSections(formResponse, request, version, actor, vanID, parkingPlaceID); } @Override @@ -214,7 +208,6 @@ private FormResponseDTO processSections( FormResponseRequest request, FormVersion version, String actor, - boolean applyRefusalRule, Integer vanID, Integer parkingPlaceID) { @@ -259,17 +252,10 @@ private FormResponseDTO processSections( "' not found in form '" + request.getFormUuid() + "'"); } - String desiredStatus = applyRefusalRule - ? determineSectionStatus(section, sectionReq.getAnswers()) - : SECTION_STATUS_DONE; - Optional sectionResponseOpt = upsertSectionResponse( - formResponse.getResponseId(), section, actor, desiredStatus, vanID, parkingPlaceID); - if (sectionResponseOpt.isEmpty()) { - continue; - } - SectionResponse sectionResponse = sectionResponseOpt.get(); + SectionResponse sectionResponse = upsertSectionResponse( + formResponse.getResponseId(), section.getSectionId(), actor, vanID, parkingPlaceID); - if (section.getSectionPhase() == SectionPhase.POST_SUBMIT) { + if ("POST_SUBMIT".equals(section.getSectionPhase())) { formResponse.setLastFollowUpAt(sectionResponse.getSavedAt()); formResponseRepo.save(formResponse); } @@ -288,72 +274,35 @@ private FormResponseDTO processSections( questionResponseRepo.saveAll(questionResponses); - sectionDTOs.add(buildSectionResponseDTO(sectionResponse, section, questionResponses)); + sectionDTOs.add(buildSectionResponseDTO(sectionResponse, questionResponses)); } return buildFormResponseDTO(formResponse, sectionDTOs); } - private Optional upsertSectionResponse( - Long responseId, FormSection section, String actor, String status, Integer vanID, Integer parkingPlaceID) { + private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor, + Integer vanID, Integer parkingPlaceID) { Optional existing = - sectionResponseRepo.findByResponseIdAndSectionId(responseId, section.getSectionId()); + sectionResponseRepo.findByResponseIdAndSectionId(responseId, sectionId); if (existing.isPresent()) { - if (Boolean.FALSE.equals(section.getIsEditable())) { - log.info("Section '{}' is not editable — skipping update for responseId={}", - section.getSectionUuid(), responseId); - return Optional.empty(); - } SectionResponse sr = existing.get(); - sr.setStatus(status); + 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 Optional.of(sectionResponseRepo.save(sr)); + return sectionResponseRepo.save(sr); } SectionResponse sr = SectionResponse.builder() .responseId(responseId) - .sectionId(section.getSectionId()) - .status(status) + .sectionId(sectionId) + .status(SECTION_STATUS_DONE) .savedAt(new Timestamp(System.currentTimeMillis())) .createdBy(actor) .updatedBy(actor) .vanID(vanID) .parkingPlaceID(parkingPlaceID) .build(); - return Optional.of(sectionResponseRepo.save(sr)); - } - - // TB2_SEC_GENERAL_INFO's consent question (TB2_GI_Q1) gates the rest of the TB Counselling - // form; a "NO" answer means the beneficiary refused counselling. Absence-tolerant by design: - // any other section, a missing answers list, or no answer for the consent question at all - // just falls through to the normal DONE status. - private String determineSectionStatus(FormSection section, List answers) { - if (!SECTION_UUID_GENERAL_INFO.equals(section.getSectionUuid()) || answers == null) { - return SECTION_STATUS_DONE; - } - boolean refused = answers.stream().anyMatch(this::isRefusalAnswer); - return refused ? SECTION_STATUS_REFUSED : SECTION_STATUS_DONE; - } - - // Same TB2_GI_Q1="NO" signal as determineSectionStatus, but scanned directly off the raw - // request (before any FormSection is resolved) so completeForm() can decide the top-level - // FormResponse status. Absence-tolerant: no TB2_SEC_GENERAL_INFO section, no answers, or no - // TB2_GI_Q1 answer at all just means "not refused". - private boolean isConsentRefused(FormResponseRequest request) { - if (request.getSections() == null) { - return false; - } - return request.getSections().stream() - .filter(s -> SECTION_UUID_GENERAL_INFO.equals(s.getSectionUuid())) - .filter(s -> s.getAnswers() != null) - .flatMap(s -> s.getAnswers().stream()) - .anyMatch(this::isRefusalAnswer); - } - - private boolean isRefusalAnswer(QuestionAnswerRequest answer) { - return QUESTION_UUID_CONSENT.equals(answer.getQuestionUuid()) - && OPTION_VALUE_CONSENT_REFUSED.equals(answer.getOptionValue()); + return sectionResponseRepo.save(sr); } private List processAnswers( @@ -419,7 +368,7 @@ private List processAnswers( } } } else { - // TEXT, DATE, AUTO_FILL, CHECKBOX — prefer answerText, then answerDate, then optionValue (legacy) + // 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(); @@ -467,16 +416,9 @@ private List loadSectionResponseDTOs(Long responseId) { Map> answersBySectionResponse = allAnswers.stream() .collect(Collectors.groupingBy(QuestionResponse::getSectionResponseId)); - Set sectionIds = sections.stream() - .map(SectionResponse::getSectionId) - .collect(Collectors.toSet()); - Map sectionById = formSectionRepo.findAllById(sectionIds).stream() - .collect(Collectors.toMap(FormSection::getSectionId, Function.identity())); - return sections.stream() .map(sr -> buildSectionResponseDTO( sr, - sectionById.get(sr.getSectionId()), answersBySectionResponse.getOrDefault(sr.getSectionResponseId(), List.of()))) .collect(Collectors.toList()); } @@ -501,7 +443,7 @@ private FormResponseDTO buildFormResponseDTO(FormResponse r, List answers) { + SectionResponse sr, List answers) { List answerDTOs = answers.stream() .map(a -> QuestionResponseDTO.builder() .questionResponseId(a.getQuestionResponseId()) @@ -513,8 +455,6 @@ private SectionResponseDTO buildSectionResponseDTO( return SectionResponseDTO.builder() .sectionResponseId(sr.getSectionResponseId()) .sectionId(sr.getSectionId()) - .sectionUuid(section != null ? section.getSectionUuid() : null) - .isEditable(section != null ? section.getIsEditable() : null) .status(sr.getStatus()) .savedAt(sr.getSavedAt()) .answers(answerDTOs) @@ -553,21 +493,12 @@ public List findPendingFollowUps(List formIds, int delayD @Override @Transactional(readOnly = true) - public CompletedBeneficiariesDTO getCompletedBeneficiaries( - FormType formType, Integer villageId, Integer providerServiceMapId) { + 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 CompletedBeneficiariesDTO.builder() - .completed(findBeneficiaryIdsByStatus(form.getFormId(), FormResponseStatus.COMPLETE.name(), villageId, providerServiceMapId)) - .refused(findBeneficiaryIdsByStatus(form.getFormId(), FormResponseStatus.REFUSED.name(), villageId, providerServiceMapId)) - .build(); - } - - private List findBeneficiaryIdsByStatus( - Long formId, String status, Integer villageId, Integer providerServiceMapId) { return (villageId != null || providerServiceMapId != null) - ? formResponseRepo.findBeneficiaryIdsByFormIdAndStatusFiltered(formId, status, villageId, providerServiceMapId) - : formResponseRepo.findBeneficiaryIdsByFormIdAndStatus(formId, status); + ? 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/FormResponseItemSaver.java b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java index 4767f505..6a8fb863 100644 --- a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -41,9 +41,7 @@ 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.FormResponseStatus; import com.iemr.flw.masterEnum.QuestionType; -import com.iemr.flw.masterEnum.SectionPhase; import com.iemr.flw.service.CampConfigService; import com.iemr.flw.utils.JwtUtil; import com.iemr.flw.utils.exception.IEMRException; @@ -75,6 +73,7 @@ @Component public class FormResponseItemSaver { + private static final String STATUS_SUBMITTED = "SUBMITTED"; private static final String SECTION_STATUS_DONE = "DONE"; private final FormResponseRepo formResponseRepo; @@ -114,7 +113,7 @@ public FormResponseDTO saveForBulk(FormResponseRequest req, String jwtToken) thr Long responseId = formResponse.getResponseId(); // Step 3: Update FormResponse header - formResponse.setStatus(FormResponseStatus.SUBMITTED.name()); + formResponse.setStatus(STATUS_SUBMITTED); formResponse.setSubmittedAt(now); formResponse.setUpdatedBy(actor); formResponse.setLastFollowUpAt(now); @@ -181,7 +180,7 @@ private FormResponse createFormResponse(FormResponseRequest req, FormVersion ver .formId(version.getDynamicForm().getFormId()) .versionId(version.getVersionId()) .officerId(req.getOfficerId()) - .status(FormResponseStatus.SUBMITTED.name()) + .status(STATUS_SUBMITTED) .createdBy(actor) .updatedBy(actor) .submittedAt(now) @@ -233,24 +232,18 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR "' not found in form '" + req.getFormUuid() + "'"); } - Optional existingSr = sectionResponseRepo.findByResponseIdAndSectionId( - formResponse.getResponseId(), section.getSectionId()); - if (existingSr.isPresent() && Boolean.FALSE.equals(section.getIsEditable())) { - log.info("saveForBulk: section '{}' is not editable — skipping update for responseId={}", - section.getSectionUuid(), formResponse.getResponseId()); - continue; - } - // Delete existing SectionResponse (and its QuestionResponses) for this section only - existingSr.ifPresent(sr -> { - questionResponseRepo.deleteBySectionResponseIdIn(Set.of(sr.getSectionResponseId())); - sectionResponseRepo.deleteById(sr.getSectionResponseId()); - }); + 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 (section.getSectionPhase() == SectionPhase.POST_SUBMIT) { + if ("POST_SUBMIT".equals(section.getSectionPhase())) { formResponse.setLastFollowUpAt(sectionResponse.getSavedAt()); formResponseRepo.save(formResponse); } @@ -268,7 +261,7 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR parkingPlaceID); questionResponseRepo.saveAll(questionResponses); - sectionDTOs.add(buildSectionResponseDTO(sectionResponse, section, questionResponses)); + sectionDTOs.add(buildSectionResponseDTO(sectionResponse, questionResponses)); } return buildFormResponseDTO(formResponse, sectionDTOs); @@ -359,7 +352,7 @@ private List processAnswers( } } } else { - // TEXT, DATE, AUTO_FILL, CHECKBOX — prefer answerText, then answerDate, then optionValue (legacy) + // 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(); @@ -407,8 +400,7 @@ private FormResponseDTO buildFormResponseDTO(FormResponse r, List answers) { + private SectionResponseDTO buildSectionResponseDTO(SectionResponse sr, List answers) { List answerDTOs = answers.stream() .map(a -> QuestionResponseDTO.builder() .questionResponseId(a.getQuestionResponseId()) @@ -420,8 +412,6 @@ private SectionResponseDTO buildSectionResponseDTO( return SectionResponseDTO.builder() .sectionResponseId(sr.getSectionResponseId()) .sectionId(sr.getSectionId()) - .sectionUuid(section != null ? section.getSectionUuid() : null) - .isEditable(section != null ? section.getIsEditable() : null) .status(sr.getStatus()) .savedAt(sr.getSavedAt()) .answers(answerDTOs) From 7b6838780af6e72a6563bf61ed5376cb56e3c673 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 13 Jul 2026 17:50:59 +0530 Subject: [PATCH 83/97] Revert "Vb/counselling (#264)" This reverts commit 82286d26. --- .../service/impl/DynamicFormResponseServiceImpl.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index 28d89fce..fb303083 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -130,17 +130,19 @@ public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken 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_SUBMITTED, now, vanID, parkingPlaceID) - : existing.get(0); + if (existing.isEmpty()) { + throw new NoSuchElementException( + "FormResponse not found for beneficiaryId=" + request.getBeneficiaryId() + + " formUuid=" + request.getFormUuid()); + } + FormResponse formResponse = existing.get(0); formResponse.setUpdatedBy(actor); formResponse.setStatus(STATUS_COMPLETE); - formResponse.setCompletedAt(now); + formResponse.setCompletedAt(new Timestamp(System.currentTimeMillis())); formResponse = formResponseRepo.save(formResponse); return processSections(formResponse, request, version, actor, vanID, parkingPlaceID); } From e7ad7c6359826a9e9046564ed37ce709f654c79a Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 8 Jul 2026 13:22:08 +0530 Subject: [PATCH 84/97] Move Risk Factors and HIV Status from General Examination to TB Screening Adds a mandatory Risk Factors section (Key Population/Risk Factors multi-select + HIV Status) to nurse TB Screening, matching the new form requirement, and removes it from General Examination where it previously lived. Co-Authored-By: Claude Sonnet 5 --- .../flw/domain/iemr/StopTBGeneralExamination.java | 14 -------------- .../iemr/flw/service/impl/StopTBServiceImpl.java | 8 -------- 2 files changed, 22 deletions(-) diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java index 9972d916..6fdd4ced 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java @@ -72,20 +72,6 @@ public class StopTBGeneralExamination { @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; diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 0ed9e377..cfc9551e 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -361,10 +361,6 @@ public List> saveGeneralExamination(List 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); @@ -435,10 +431,6 @@ private Map examToMap(StopTBGeneralExamination e) { 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()); From 8db7386efdabc692d46e811966036d4a0fac8b63 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 8 Jul 2026 14:38:11 +0530 Subject: [PATCH 85/97] Keep Risk Factors and HIV Status on General Examination too Per updated requirement, these fields should be captured on both General Examination and TB Screening, not moved exclusively to TB Screening. Restores keyPopulationRiskFactorIds/Factors and hivStatusId/hivStatus on StopTBGeneralExamination (save + response map), on top of the earlier commit that added them to TBScreening. Co-Authored-By: Claude Sonnet 5 --- .../flw/domain/iemr/StopTBGeneralExamination.java | 14 ++++++++++++++ .../iemr/flw/service/impl/StopTBServiceImpl.java | 8 ++++++++ 2 files changed, 22 insertions(+) diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java index 6fdd4ced..9972d916 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBGeneralExamination.java @@ -72,6 +72,20 @@ public class StopTBGeneralExamination { @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; diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index cfc9551e..0ed9e377 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -361,6 +361,10 @@ public List> saveGeneralExamination(List 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); @@ -431,6 +435,10 @@ private Map examToMap(StopTBGeneralExamination e) { 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()); From e42ceb05cdc3b8de61cab70eb842f68507675dc9 Mon Sep 17 00:00:00 2001 From: sehjotsinghunthinkable Date: Tue, 14 Jul 2026 10:46:17 +0530 Subject: [PATCH 86/97] Changing in complete api accoridng to release 1.1 (#279) --- .../service/impl/DynamicFormResponseServiceImpl.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index fb303083..6e3b29c2 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -130,19 +130,17 @@ public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken FormVersion version = resolveLatestVersion(request.getFormUuid()); Long formId = version.getDynamicForm().getFormId(); + Timestamp now = new Timestamp(System.currentTimeMillis()); List existing = formResponseRepo.findByBeneficiaryIdAndFormId(request.getBeneficiaryId(), formId); - if (existing.isEmpty()) { - throw new NoSuchElementException( - "FormResponse not found for beneficiaryId=" + request.getBeneficiaryId() - + " formUuid=" + request.getFormUuid()); - } - FormResponse formResponse = existing.get(0); + FormResponse formResponse = existing.isEmpty() + ? createFormResponse(request, version, STATUS_COMPLETE, now, vanID, parkingPlaceID) + : existing.get(0); formResponse.setUpdatedBy(actor); formResponse.setStatus(STATUS_COMPLETE); - formResponse.setCompletedAt(new Timestamp(System.currentTimeMillis())); + formResponse.setCompletedAt(now); formResponse = formResponseRepo.save(formResponse); return processSections(formResponse, request, version, actor, vanID, parkingPlaceID); } From 2cdfae641660023dee2647a92545ab03c970253f Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 14 Jul 2026 13:06:24 +0530 Subject: [PATCH 87/97] fix: tolerate duplicate i_beneficiarymapping rows in Stop TB worklist benRegId has no unique constraint on i_beneficiarymapping, so findByBenRegIdFromMapping's single-result query threw IncorrectResultSizeDataAccessException whenever a beneficiary had more than one mapping row, crashing the Stop TB registrar worklist refresh. Return the list ordered by benMapId DESC and use the latest mapping instead of assuming uniqueness. --- .../java/com/iemr/flw/repo/identity/BeneficiaryRepo.java | 6 ++++-- .../com/iemr/flw/service/impl/BeneficiaryServiceImpl.java | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) 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 140e67b0..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,8 +90,10 @@ 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") 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 ebe4c6d9..b70ce80e 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -145,9 +145,11 @@ public String getBenData(GetBenRequestHandler request, String authorisation) thr List allAddresses = new ArrayList<>(); for (BenFlowStatus flow : flows) { if (flow.getBeneficiaryRegID() == null) continue; - RMNCHMBeneficiarymapping mapping = beneficiaryRepo.findByBenRegIdFromMapping( + List mappings = beneficiaryRepo.findByBenRegIdFromMapping( BigInteger.valueOf(flow.getBeneficiaryRegID())); - if (mapping == null || mapping.getBenAddressId() == null) continue; + 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); } From 8152fd43bef3a0391c96b6775c33bab1c69d85f7 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 21 Jul 2026 23:35:18 +0530 Subject: [PATCH 88/97] fix: remove PII-dumping debug println statements from BeneficiaryController getBeneficiaryData request/response System.out.println calls logged full beneficiary details (name, address, health ID) to stdout unconditionally. --- .../java/com/iemr/flw/controller/BeneficiaryController.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/BeneficiaryController.java b/src/main/java/com/iemr/flw/controller/BeneficiaryController.java index 1419bfe4..d439fbf5 100644 --- a/src/main/java/com/iemr/flw/controller/BeneficiaryController.java +++ b/src/main/java/com/iemr/flw/controller/BeneficiaryController.java @@ -54,9 +54,7 @@ public String getBeneficiaryDataByAsha(@RequestBody GetBenRequestHandler request if (requestDTO != null) { logger.info("request object with timestamp : " + new Timestamp(System.currentTimeMillis()) + " " + requestDTO); - System.out.println("getBeneficiaryData request : " + requestDTO); String s = beneficiaryService.getBenData(requestDTO, authorization); - System.out.println("getBeneficiaryData response : " + s); if (s != null) response.setResponse(s); else From b30e4102aaa8cf59098c63ca54c2a784ef7c08b0 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 21 Jul 2026 23:37:56 +0530 Subject: [PATCH 89/97] fix: add try/catch to VillageLevelFormController save methods Without local exception handling, an unhandled error in saveVhndForm/ saveVhncForm/savePhcForm/saveAhdForm/saveDewormingForm now falls through to GlobalExceptionHandler's catch-all, which returns a different response shape (ApiResponse) than this controller's existing status/statusCode/ message convention, breaking existing clients. Wrap each in the same try/catch pattern already used by getVillageLevelFormData. --- .../VillageLevelFormController.java | 160 ++++++++++-------- 1 file changed, 92 insertions(+), 68 deletions(-) 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); } - } From 988c9407c0e53c83e3af2f3bb14f667124b70595 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 21 Jul 2026 23:55:21 +0530 Subject: [PATCH 90/97] chore: remove confirmed-dead StopTB endpoints and backing code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified against the actual mobile app source (STOP-TB-App release-1.1 AmritApiService.kt) and ADMIN-UI web client — none of these are called: - /stoptb/registration/save — mobile registers directly against TM-API's registrarBeneficaryRegistrationNew; TM-API creates BenFlowStatus itself via its own isMobile-flag branch when the request doesn't carry isMobile=true (confirmed in TM-API's RegistrarServiceImpl) - /stoptb/registrar/worklist, /stoptb/nurse/worklist — explicitly commented out in the app's network layer - /stoptb/nurse/submit — no reference at all - /stoptb/nurse/{generalExamination,tbScreening,generalOpd,diagnostics}/get (singular) — only the /getAll variants are used Also removes the now-fully-orphaned StopTBRegistration entity/DTO/repo (the registration table itself was already replaced by BenFlowStatus in an earlier commit) and StopTBServiceImpl helper methods that only existed to support the removed saveRegistration path. --- .../iemr/flw/controller/StopTBController.java | 128 -------- .../flw/domain/iemr/StopTBRegistration.java | 79 ----- .../flw/dto/iemr/StopTBRegistrationDto.java | 37 --- .../flw/repo/iemr/StopTBRegistrationRepo.java | 20 -- .../com/iemr/flw/service/StopTBService.java | 16 - .../flw/service/impl/StopTBServiceImpl.java | 289 ------------------ 6 files changed, 569 deletions(-) delete mode 100644 src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java delete mode 100644 src/main/java/com/iemr/flw/dto/iemr/StopTBRegistrationDto.java delete mode 100644 src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java diff --git a/src/main/java/com/iemr/flw/controller/StopTBController.java b/src/main/java/com/iemr/flw/controller/StopTBController.java index 386ed597..7f73d29e 100644 --- a/src/main/java/com/iemr/flw/controller/StopTBController.java +++ b/src/main/java/com/iemr/flw/controller/StopTBController.java @@ -2,7 +2,6 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.iemr.flw.dto.iemr.StopTBRegistrationDto; import com.iemr.flw.service.StopTBService; import com.iemr.flw.utils.response.OutputResponse; import io.swagger.v3.oas.annotations.Operation; @@ -27,54 +26,6 @@ public class StopTBController { @Autowired private StopTBService stopTBService; - // ── Registration ────────────────────────────────────────────────────────── - - @PostMapping("/registration/save") - @Operation(summary = "Save Stop TB beneficiary registration") - public String saveRegistration( - @RequestBody String requestBody, - @RequestHeader("Authorization") String authorization) { - OutputResponse response = new OutputResponse(); - try { - Map data = stopTBService.saveRegistration(requestBody, authorization); - response.setResponse(gson.toJson(data)); - } catch (Exception e) { - logger.error("Error in saveRegistration: " + e); - response.setError(5000, "Error in registration: " + e.getMessage()); - } - return response.toString(); - } - - // ── Worklists ───────────────────────────────────────────────────────────── - - @PostMapping("/registrar/worklist") - @Operation(summary = "Get registrar worklist — full beneficiary details for Stop TB") - public String getRegistrarWorklist(@RequestBody StopTBRegistrationDto dto) { - OutputResponse response = new OutputResponse(); - try { - Map data = stopTBService.getRegistrarWorklist(dto); - response.setResponse(gson.toJson(data.get("data"))); - } catch (Exception e) { - logger.error("Error in getRegistrarWorklist: " + e); - response.setError(5000, "Error fetching worklist: " + e.getMessage()); - } - return response.toString(); - } - - @PostMapping("/nurse/worklist") - @Operation(summary = "Get nurse worklist — full beneficiary details + nurse module data") - public String getNurseWorklist(@RequestBody StopTBRegistrationDto dto) { - OutputResponse response = new OutputResponse(); - try { - Map data = stopTBService.getNurseWorklist(dto); - response.setResponse(gson.toJson(data.get("data"))); - } catch (Exception e) { - logger.error("Error in getNurseWorklist: " + e); - response.setError(5000, "Error fetching nurse worklist: " + e.getMessage()); - } - return response.toString(); - } - // ── Nurse: General Examination ──────────────────────────────────────────── @PostMapping("/nurse/generalExamination/save") @@ -91,22 +42,6 @@ public String saveGeneralExamination(@RequestBody List> data return response.toString(); } - @PostMapping("/nurse/generalExamination/get") - @Operation(summary = "Get general examination for a Stop TB beneficiary") - public String getGeneralExamination(@RequestBody Map body) { - OutputResponse response = new OutputResponse(); - try { - Object raw = body.get("benRegID"); - if (raw == null) throw new Exception("benRegID is required"); - Map data = stopTBService.getGeneralExamination(Long.parseLong(raw.toString())); - response.setResponse(gson.toJson(data)); - } catch (Exception e) { - logger.error("Error in getGeneralExamination: " + e); - response.setError(5000, "Error fetching 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) { @@ -140,22 +75,6 @@ public String saveNurseTBScreening(@RequestBody List> dataLi return response.toString(); } - @PostMapping("/nurse/tbScreening/get") - @Operation(summary = "Get TB screening for a Stop TB beneficiary") - public String getNurseTBScreening(@RequestBody Map body) { - OutputResponse response = new OutputResponse(); - try { - Object raw = body.get("benRegID"); - if (raw == null) throw new Exception("benRegID is required"); - Map data = stopTBService.getNurseTBScreening(Long.parseLong(raw.toString())); - response.setResponse(gson.toJson(data)); - } catch (Exception e) { - logger.error("Error in getNurseTBScreening: " + e); - response.setError(5000, "Error fetching 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) { @@ -189,22 +108,6 @@ public String saveGeneralOpd(@RequestBody List> dataList) { return response.toString(); } - @PostMapping("/nurse/generalOpd/get") - @Operation(summary = "Get general OPD record for a Stop TB beneficiary") - public String getGeneralOpd(@RequestBody Map body) { - OutputResponse response = new OutputResponse(); - try { - Object raw = body.get("benRegID"); - if (raw == null) throw new Exception("benRegID is required"); - Map data = stopTBService.getGeneralOpd(Long.parseLong(raw.toString())); - response.setResponse(gson.toJson(data)); - } catch (Exception e) { - logger.error("Error in getGeneralOpd: " + e); - response.setError(5000, "Error fetching 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) { @@ -238,22 +141,6 @@ public String saveDiagnostics(@RequestBody List> dataList) { return response.toString(); } - @PostMapping("/nurse/diagnostics/get") - @Operation(summary = "Get diagnostics for a Stop TB beneficiary") - public String getDiagnostics(@RequestBody Map body) { - OutputResponse response = new OutputResponse(); - try { - Object raw = body.get("benRegID"); - if (raw == null) throw new Exception("benRegID is required"); - Map data = stopTBService.getDiagnostics(Long.parseLong(raw.toString())); - response.setResponse(gson.toJson(data)); - } catch (Exception e) { - logger.error("Error in getDiagnostics: " + e); - response.setError(5000, "Error fetching 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) { @@ -271,19 +158,4 @@ public String getAllDiagnostics(@RequestBody Map body) { return response.toString(); } - // ── Nurse: Submit ───────────────────────────────────────────────────────── - - @PostMapping("/nurse/submit") - @Operation(summary = "Submit nurse data — mark nurse done for a Stop TB beneficiary") - public String submitNurseData(@RequestBody Map data) { - OutputResponse response = new OutputResponse(); - try { - Map result = stopTBService.submitNurseData(data); - response.setResponse(gson.toJson(result)); - } catch (Exception e) { - logger.error("Error in submitNurseData: " + e); - response.setError(5000, "Error submitting nurse data: " + e.getMessage()); - } - return response.toString(); - } } diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java deleted file mode 100644 index 4ce54a0d..00000000 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBRegistration.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.iemr.flw.domain.iemr; - -import lombok.Data; - -import jakarta.persistence.*; -import java.sql.Timestamp; - -@Entity -@Table(name = "tb_stoptb_registration", schema = "db_iemr") -@Data -public class StopTBRegistration { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "benRegID") - private Long benRegID; - - @Column(name = "providerServiceMapID") - private Integer providerServiceMapID; - - @Column(name = "personFrom") - private String personFrom; - - @Column(name = "caseFindingType") - private String caseFindingType; - - @Column(name = "tuId") - private Integer tuId; - - @Column(name = "tuName") - private String tuName; - - @Column(name = "healthFacilityId") - private Integer healthFacilityId; - - @Column(name = "healthFacilityName") - private String healthFacilityName; - - @Column(name = "weight") - private Double weight; - - @Column(name = "height") - private Double height; - - @Column(name = "bmi") - private Double bmi; - - @Column(name = "temperatureValue") - private Double temperatureValue; - - @Column(name = "isMobileAvailableId") - private Integer isMobileAvailableId; - - @Column(name = "isMobileAvailable") - private Boolean isMobileAvailable; - - @Column(name = "villageId") - private Integer villageId; - - @Column(name = "villageName") - private String villageName; - - @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) - private Timestamp lastModDate; - - @Column(name = "deleted") - private Boolean deleted; -} diff --git a/src/main/java/com/iemr/flw/dto/iemr/StopTBRegistrationDto.java b/src/main/java/com/iemr/flw/dto/iemr/StopTBRegistrationDto.java deleted file mode 100644 index 64194ff1..00000000 --- a/src/main/java/com/iemr/flw/dto/iemr/StopTBRegistrationDto.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.iemr.flw.dto.iemr; - -import com.fasterxml.jackson.annotation.JsonRawValue; -import lombok.Data; - -@Data -public class StopTBRegistrationDto { - - // Standard beneficiary fields — forwarded to Common-API - @JsonRawValue - private String benD; - - // Stop TB specific fields - private Integer providerServiceMapID; - private String personFrom; - private String caseFindingType; - private Integer tuId; - private String tuName; - private Integer healthFacilityId; - private String healthFacilityName; - private Boolean isMobileAvailable; - - // Anthropometry fields - private Double weight; - private Double height; - private Double bmi; - private Double temperatureValue; - - private String createdBy; - - private Integer villageId; - private String villageName; - - // For get/update - private Long benRegID; - private String modifiedBy; -} diff --git a/src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java b/src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java deleted file mode 100644 index 93c12168..00000000 --- a/src/main/java/com/iemr/flw/repo/iemr/StopTBRegistrationRepo.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.iemr.flw.repo.iemr; - -import com.iemr.flw.domain.iemr.StopTBRegistration; -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 StopTBRegistrationRepo extends JpaRepository { - - @Query("SELECT t FROM StopTBRegistration t WHERE t.benRegID = :benRegID AND t.deleted = false") - StopTBRegistration findByBenRegID(@Param("benRegID") Long benRegID); - - @Query("SELECT t FROM StopTBRegistration t WHERE t.providerServiceMapID = :psmId AND t.villageId = :villageId AND t.deleted = false ORDER BY t.createdDate DESC") - List getRegistrarWorklist(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); -} diff --git a/src/main/java/com/iemr/flw/service/StopTBService.java b/src/main/java/com/iemr/flw/service/StopTBService.java index b59ccd01..e549daa6 100644 --- a/src/main/java/com/iemr/flw/service/StopTBService.java +++ b/src/main/java/com/iemr/flw/service/StopTBService.java @@ -1,38 +1,22 @@ package com.iemr.flw.service; -import com.iemr.flw.dto.iemr.StopTBRegistrationDto; - import java.util.List; import java.util.Map; public interface StopTBService { - // Registrar - Map saveRegistration(String requestBody, String authorization) throws Exception; - Map getRegistrarWorklist(StopTBRegistrationDto dto) throws Exception; - - // Nurse — worklist - Map getNurseWorklist(StopTBRegistrationDto dto) throws Exception; - // Nurse — General Examination List> saveGeneralExamination(List> dataList) throws Exception; - Map getGeneralExamination(Long benRegID) throws Exception; Map getAllGeneralExaminations(Integer providerServiceMapID, Integer villageID) throws Exception; // Nurse — TB Screening List> saveNurseTBScreening(List> dataList) throws Exception; - Map getNurseTBScreening(Long benRegID) throws Exception; Map getAllNurseTBScreenings(Integer providerServiceMapID, Integer villageID) throws Exception; // Nurse — General OPD List> saveGeneralOpd(List> dataList) throws Exception; - Map getGeneralOpd(Long benRegID) throws Exception; Map getAllGeneralOpd(Integer providerServiceMapID, Integer villageID) throws Exception; // Nurse — Diagnostics List> saveDiagnostics(List> dataList) throws Exception; - Map getDiagnostics(Long benRegID) throws Exception; Map getAllDiagnostics(Integer providerServiceMapID, Integer villageID) throws Exception; - - // Nurse — submit - Map submitNurseData(Map data) throws Exception; } diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 0ed9e377..10016c4a 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -1,8 +1,6 @@ package com.iemr.flw.service.impl; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import com.iemr.flw.domain.identity.RMNCHMBeneficiarydetail; import com.iemr.flw.domain.identity.RMNCHMBeneficiarymapping; import com.iemr.flw.domain.iemr.BenAnthropometryDetail; @@ -18,7 +16,6 @@ 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.dto.iemr.StopTBRegistrationDto; import com.iemr.flw.repo.identity.BeneficiaryRepo; import com.iemr.flw.repo.iemr.BenAnthropometryRepo; import com.iemr.flw.repo.iemr.BenChiefComplaintRepo; @@ -38,12 +35,9 @@ 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.domain.PageRequest; -import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.client.RestTemplate; import java.math.BigInteger; import java.sql.Timestamp; @@ -99,227 +93,6 @@ public class StopTBServiceImpl implements StopTBService { @Autowired private BenReferDetailsRepo benReferDetailsRepo; - @Value("${tm-url}") - private String tmUrl; - - // ── Registration ────────────────────────────────────────────────────────── - - @Override - @Transactional - public Map saveRegistration(String requestBody, String authorization) throws Exception { - JsonObject jsonObject = JsonParser.parseString(requestBody).getAsJsonObject(); - - // Extract fields for BenFlowStatus — do NOT remove from jsonObject - // All fields (including Stop TB extras) forward to TM-API → ExtraFields via checkExtraFields - String firstName = getStringField(jsonObject, "firstName"); - String lastName = getStringField(jsonObject, "lastName"); - String benName = ((firstName != null ? firstName : "") + " " + (lastName != null ? lastName : "")).trim(); - - Integer genderID = getIntField(jsonObject, "genderID"); - String genderName = getStringField(jsonObject, "genderName"); - if (genderName == null) genderName = getStringField(jsonObject, "gender"); - - String createdBy = getStringField(jsonObject, "createdBy"); - Integer providerServiceMapID = getIntField(jsonObject, "providerServiceMapID"); - - Timestamp dob = parseDob(jsonObject); - - String phoneNo = null; - if (jsonObject.has("i_bencontacts") && !jsonObject.get("i_bencontacts").isJsonNull()) { - JsonObject contacts = jsonObject.getAsJsonObject("i_bencontacts"); - phoneNo = getStringField(contacts, "phoneNo1"); - } - if (phoneNo == null) phoneNo = getStringField(jsonObject, "phoneNo1"); - if (phoneNo == null && jsonObject.has("benPhoneMaps") && jsonObject.get("benPhoneMaps").isJsonArray()) { - com.google.gson.JsonArray phoneMaps = jsonObject.getAsJsonArray("benPhoneMaps"); - if (phoneMaps.size() > 0 && !phoneMaps.get(0).isJsonNull()) - phoneNo = getStringField(phoneMaps.get(0).getAsJsonObject(), "phoneNo"); - } - - Integer villageId = null; - String villageName = null; - Integer districtID = null; - String districtName = null; - if (jsonObject.has("i_bendemographics") && !jsonObject.get("i_bendemographics").isJsonNull()) { - JsonObject demo = jsonObject.getAsJsonObject("i_bendemographics"); - villageId = getIntField(demo, "districtBranchID"); - villageName = getStringField(demo, "districtBranchName"); - districtID = getIntField(demo, "districtID"); - districtName = getStringField(demo, "districtName"); - // promote to top-level so Identity-API saves them in ExtraFields - if (demo.has("economicStatus")) jsonObject.addProperty("economicStatus", getStringField(demo, "economicStatus")); - if (demo.has("economicStatusId")) jsonObject.addProperty("economicStatusId", getIntField(demo, "economicStatusId")); - if (demo.has("residentialArea")) jsonObject.addProperty("residentialArea", getStringField(demo, "residentialArea")); - if (demo.has("residentialAreaId")) jsonObject.addProperty("residentialAreaId", getIntField(demo, "residentialAreaId")); - } - - // isMobile=true tells TM-API to skip its own BenFlowStatus creation — FLW-API owns it - jsonObject.addProperty("isMobile", true); - - RestTemplate restTemplate = new RestTemplate(); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - headers.set("Authorization", authorization); - headers.set("User-Agent", "okhttp"); - - HttpEntity request = new HttpEntity<>(jsonObject.toString(), headers); - ResponseEntity response = restTemplate.exchange( - tmUrl + "/registrar/registrarBeneficaryRegistrationNew", - HttpMethod.POST, request, Map.class); - - if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) - throw new Exception("Beneficiary registration failed in TM-API"); - - Object dataObj = response.getBody().get("data"); - JsonObject dataJson = dataObj instanceof String - ? JsonParser.parseString((String) dataObj).getAsJsonObject() - : JsonParser.parseString(new com.google.gson.Gson().toJson(dataObj)).getAsJsonObject(); - - Long benRegID = dataJson.get("benRegId").getAsLong(); - Long beneficiaryID = dataJson.get("benGenId").getAsLong(); - - BenFlowStatus flow = new BenFlowStatus(); - flow.setBeneficiaryRegID(benRegID); - flow.setBeneficiaryID(beneficiaryID); - flow.setBenName(benName.isEmpty() ? null : benName); - flow.setDob(dob); - flow.setGenderID(genderID != null ? genderID.shortValue() : null); - flow.setGenderName(genderName); - flow.setPreferredPhoneNum(phoneNo); - flow.setNurseFlag((short) 1); - flow.setDoctorFlag((short) 0); - flow.setPharmacistFlag((short) 0); - flow.setProviderServiceMapId(providerServiceMapID); - flow.setVillageID(villageId); - flow.setVillageName(villageName); - flow.setDistrictID(districtID); - flow.setDistrictName(districtName); - flow.setAgentId(createdBy); - flow.setRegistrationDate(new Timestamp(System.currentTimeMillis())); - flow.setDeleted(false); - benFlowStatusRepo.save(flow); - - Map result = new HashMap<>(); - result.put("beneficiaryRegID", benRegID); - result.put("beneficiaryID", beneficiaryID); - return result; - } - - // ── Worklists ───────────────────────────────────────────────────────────── - - @Override - public Map getRegistrarWorklist(StopTBRegistrationDto dto) throws Exception { - List flowList = benFlowStatusRepo.getRegistrarWorklist( - dto.getProviderServiceMapID(), dto.getVillageId()); - - List> worklist = new ArrayList<>(); - for (BenFlowStatus flow : flowList) - worklist.add(buildWorklistItem(flow)); - - return wrapWorklist(worklist); - } - - @Override - public Map getNurseWorklist(StopTBRegistrationDto dto) throws Exception { - List flowList = benFlowStatusRepo.getNurseWorklist( - dto.getProviderServiceMapID(), dto.getVillageId()); - - List> worklist = new ArrayList<>(); - for (BenFlowStatus flow : flowList) { - Map item = buildWorklistItem(flow); - Long benRegID = flow.getBeneficiaryRegID(); - - List examList = generalExaminationRepo.findLatestByBeneficiaryRegID(benRegID, PageRequest.of(0, 1)); - item.put("generalExamination", !examList.isEmpty() ? examToMap(examList.get(0)) : null); - - List screeningList = tbScreeningRepo.findLatestByBenRegID(benRegID, PageRequest.of(0, 1)); - item.put("tbScreening", !screeningList.isEmpty() ? screeningToMap(screeningList.get(0)) : null); - - List opdList = generalOpdRepo.findLatestByBenRegID(benRegID, PageRequest.of(0, 1)); - item.put("generalOpd", !opdList.isEmpty() ? opdToMap(opdList.get(0)) : null); - - worklist.add(item); - } - - return wrapWorklist(worklist); - } - - // getBeneficiaryData-style item — BenFlowStatus + db_identity detail + ExtraFields - private Map buildWorklistItem(BenFlowStatus flow) { - Map item = new LinkedHashMap<>(); - item.put("benficieryid", flow.getBeneficiaryID()); - item.put("BenRegId", flow.getBeneficiaryRegID()); - item.put("benFlowID", flow.getBenFlowID()); - item.put("nurseFlag", flow.getNurseFlag()); - item.put("doctorFlag", flow.getDoctorFlag()); - - Map benDetails = new LinkedHashMap<>(); - benDetails.put("benName", flow.getBenName()); - benDetails.put("dob", flow.getDob()); - benDetails.put("genderId", flow.getGenderID()); - benDetails.put("gender", flow.getGenderName()); - benDetails.put("contact_number", flow.getPreferredPhoneNum()); - benDetails.put("villageId", flow.getVillageID()); - benDetails.put("villageName", flow.getVillageName()); - benDetails.put("districtid", flow.getDistrictID()); - benDetails.put("districtname", flow.getDistrictName()); - benDetails.put("registrationDate", flow.getRegistrationDate()); - - try { - RMNCHMBeneficiarymapping mapping = beneficiaryRepo.getById( - BigInteger.valueOf(flow.getBeneficiaryRegID())); - RMNCHMBeneficiarydetail detail = (mapping != null && mapping.getBenDetailsId() != null) - ? beneficiaryRepo.getDetailsById(mapping.getBenDetailsId()) - : null; - if (detail != null) { - benDetails.put("fatherName", detail.getFatherName()); - benDetails.put("motherName", detail.getMotherName()); - benDetails.put("community", detail.getCommunity()); - benDetails.put("communityId", detail.getCommunityId()); - benDetails.put("education", detail.getEducation()); - benDetails.put("maritalStatus", detail.getMaritalstatus()); - - if (detail.getOtherFields() != null && !detail.getOtherFields().isEmpty()) { - try { - Map extra = new ObjectMapper().readValue(detail.getOtherFields(), Map.class); - - Map anthropometry = new LinkedHashMap<>(); - anthropometry.put("weight", extra.get("weight")); - anthropometry.put("height", extra.get("height")); - anthropometry.put("bmi", extra.get("bmi")); - anthropometry.put("temperatureValue", extra.get("temperatureValue")); - item.put("anthropometry", anthropometry); - - Map stopTBDetails = new LinkedHashMap<>(); - stopTBDetails.put("personFrom", extra.get("personFrom")); - stopTBDetails.put("caseFindingType", extra.get("caseFindingType")); - stopTBDetails.put("tuId", extra.get("tuId")); - stopTBDetails.put("tuName", extra.get("tuName")); - stopTBDetails.put("healthFacilityId", extra.get("healthFacilityId")); - stopTBDetails.put("healthFacilityName", extra.get("healthFacilityName")); - item.put("stopTBDetails", stopTBDetails); - } catch (Exception e) { - logger.warn("Cannot parse otherFields for benRegID: " + flow.getBeneficiaryRegID()); - } - } - } - } catch (Exception e) { - logger.error("Cannot fetch identity detail for benRegID: " + flow.getBeneficiaryRegID() + " — " + e.getClass().getName() + ": " + e.getMessage()); - } - - item.put("beneficiaryDetails", benDetails); - return item; - } - - private Map wrapWorklist(List> worklist) { - Map inner = new LinkedHashMap<>(); - inner.put("totalPage", 1); - inner.put("data", worklist); - Map result = new LinkedHashMap<>(); - result.put("data", inner); - return result; - } - // ── Nurse: General Examination ──────────────────────────────────────────── @Override @@ -394,13 +167,6 @@ private boolean calculateReferralNeeded(StopTBGeneralExamination e) { return false; } - @Override - public Map getGeneralExamination(Long beneficiaryRegID) throws Exception { - List examList = generalExaminationRepo.findLatestByBeneficiaryRegID(beneficiaryRegID, PageRequest.of(0, 1)); - if (examList.isEmpty()) throw new Exception("No general examination found for beneficiaryRegID: " + beneficiaryRegID); - return examToMap(examList.get(0)); - } - @Override public Map getAllGeneralExaminations(Integer providerServiceMapID, Integer villageID) throws Exception { List list = (villageID != null) @@ -523,13 +289,6 @@ public List> saveNurseTBScreening(List> return results; } - @Override - public Map getNurseTBScreening(Long beneficiaryRegID) throws Exception { - List screeningList = tbScreeningRepo.findLatestByBenRegID(beneficiaryRegID, PageRequest.of(0, 1)); - if (screeningList.isEmpty()) throw new Exception("No TB screening found for beneficiaryRegID: " + beneficiaryRegID); - return screeningToMap(screeningList.get(0)); - } - @Override public Map getAllNurseTBScreenings(Integer providerServiceMapID, Integer villageID) throws Exception { List list = (villageID != null) @@ -633,13 +392,6 @@ public List> saveGeneralOpd(List> dataLi return results; } - @Override - public Map getGeneralOpd(Long beneficiaryRegID) throws Exception { - List opdList = generalOpdRepo.findLatestByBenRegID(beneficiaryRegID, PageRequest.of(0, 1)); - if (opdList.isEmpty()) throw new Exception("No general OPD record found for beneficiaryRegID: " + beneficiaryRegID); - return opdToMap(opdList.get(0)); - } - @Override public Map getAllGeneralOpd(Integer providerServiceMapID, Integer villageID) throws Exception { List list = (villageID != null) @@ -737,13 +489,6 @@ public List> saveDiagnostics(List> dataL return results; } - @Override - public Map getDiagnostics(Long benRegID) throws Exception { - StopTBDiagnostics diag = diagnosticsRepo.findByBenRegID(benRegID); - if (diag == null) throw new Exception("No diagnostics record found for benRegID: " + benRegID); - return diagnosticsToMap(diag); - } - @Override public Map getAllDiagnostics(Integer providerServiceMapID, Integer villageID) throws Exception { List list = (villageID != null) @@ -788,23 +533,6 @@ private Map diagnosticsToMap(StopTBDiagnostics d) { return m; } - // ── Nurse: Submit ───────────────────────────────────────────────────────── - - @Override - @Transactional - public Map submitNurseData(Map data) throws Exception { - Long benFlowID = getLong(data, "benFlowID"); - if (benFlowID == null) throw new Exception("benFlowID is required"); - - int updated = benFlowStatusRepo.updateStopTBAfterNurseSubmit(benFlowID); - if (updated == 0) throw new Exception("Flow record not found for benFlowID: " + benFlowID); - - Map result = new HashMap<>(); - result.put("benFlowID", benFlowID); - result.put("status", "nurse_done"); - return result; - } - // ── Standard table dual-writes ──────────────────────────────────────────── private Map getRegistrationExtras(Long beneficiaryRegID) { @@ -990,13 +718,6 @@ private void writePrescription(StopTBGeneralOpd opd, Long beneficiaryRegID, BenV // ── Helpers ─────────────────────────────────────────────────────────────── - private Timestamp parseDob(JsonObject obj) { - if (!obj.has("dob") || obj.get("dob").isJsonNull()) return null; - try { return new Timestamp(obj.get("dob").getAsLong()); } catch (Exception ignored) {} - try { return Timestamp.valueOf(obj.get("dob").getAsString().replace("T", " ").replace("Z", "").trim()); } catch (Exception ignored) {} - return null; - } - private String toJsonString(Object value) { if (value == null) return null; if (value instanceof String) return (String) value; @@ -1043,14 +764,4 @@ private Timestamp getTimestamp(Map m, String key) { try { return new Timestamp(Long.parseLong(v.toString())); } catch (Exception e) { return null; } } - private String getStringField(JsonObject obj, String field) { - return obj.has(field) && !obj.get(field).isJsonNull() ? obj.get(field).getAsString() : null; - } - - private Integer getIntField(JsonObject obj, String field) { - if (!obj.has(field) || obj.get(field).isJsonNull()) return null; - try { return obj.get(field).getAsInt(); } catch (Exception e) { - try { return Integer.parseInt(obj.get(field).getAsString().trim()); } catch (Exception ex) { return null; } - } - } } From 6a49e7133faed0c25e391e857ac9e9394937863a Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 22 Jul 2026 00:01:21 +0530 Subject: [PATCH 91/97] fix: handle List<> return type change in AbhaBeneficiaryServiceImpl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit findByBenRegIdFromMapping's return type was changed from a single object to List by an earlier Stop TB commit (fixing an IncorrectResultSizeDataAccessException when a beneficiary has more than one mapping row), but this caller was never updated to match — a pre-existing oversight in the original commit, surfaced by the build check. --- .../iemr/flw/service/impl/AbhaBeneficiaryServiceImpl.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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){ From d57b042a64b1b886527185e2114f12eeec3d4227 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 22 Jul 2026 10:06:12 +0530 Subject: [PATCH 92/97] chore: remove local-dev SQL scripts from repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dynamic_form_schema.sql and alter_tb_screening_risk_factors_hiv_status.sql are dropped from the codebase — the underlying DB schema changes they describe are still required (documented in stoptb_schema_changes.sql), they will just be run directly against the DB instead of being tracked as files in this repo. --- ...r_tb_screening_risk_factors_hiv_status.sql | 19 -- dynamic_form_schema.sql | 173 ------------------ 2 files changed, 192 deletions(-) delete mode 100644 alter_tb_screening_risk_factors_hiv_status.sql delete mode 100644 dynamic_form_schema.sql diff --git a/alter_tb_screening_risk_factors_hiv_status.sql b/alter_tb_screening_risk_factors_hiv_status.sql deleted file mode 100644 index fd6ea509..00000000 --- a/alter_tb_screening_risk_factors_hiv_status.sql +++ /dev/null @@ -1,19 +0,0 @@ --- Adds the "Risk Factors" section (Key Population / Risk Factors + HIV Status) --- to tb_screening, moved here from tb_stoptb_general_examination. - -ALTER TABLE db_iemr.tb_screening - ADD COLUMN key_population_risk_factor_ids TEXT NULL, - ADD COLUMN key_population_risk_factors TEXT NULL, - ADD COLUMN hiv_status_id INT NULL, - ADD COLUMN hiv_status VARCHAR(20) NULL; - --- NOTE: columns key_population_risk_factor_ids, key_population_risk_factors, --- hiv_status_id, hiv_status are left in place on db_iemr.tb_stoptb_general_examination --- for historical data already captured there. Drop them separately once confirmed --- they are no longer needed: --- --- ALTER TABLE db_iemr.tb_stoptb_general_examination --- DROP COLUMN key_population_risk_factor_ids, --- DROP COLUMN key_population_risk_factors, --- DROP COLUMN hiv_status_id, --- DROP COLUMN hiv_status; diff --git a/dynamic_form_schema.sql b/dynamic_form_schema.sql deleted file mode 100644 index 99e24b3b..00000000 --- a/dynamic_form_schema.sql +++ /dev/null @@ -1,173 +0,0 @@ --- Dynamic Form module schema for db_iemr --- Run against your local MySQL: mysql -u root -p db_iemr < dynamic_form_schema.sql - -USE db_iemr; - -CREATE TABLE IF NOT EXISTS t_dynamic_form ( - formId BIGINT NOT NULL AUTO_INCREMENT, - formUuid VARCHAR(100) NOT NULL, - formName VARCHAR(255) NOT NULL, - formType VARCHAR(50) NOT NULL, - isActive BIT NOT NULL DEFAULT 1, - follow_up_delay_days INT NULL, - created_by VARCHAR(100) NULL, - updated_by VARCHAR(100) NULL, - createdAt DATETIME NOT NULL, - updatedAt DATETIME NOT NULL, - PRIMARY KEY (formId), - UNIQUE KEY uk_dynamic_form_uuid (formUuid) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS t_form_version ( - versionId BIGINT NOT NULL AUTO_INCREMENT, - formId BIGINT NOT NULL, - versionNumber INT NOT NULL, - isLatest BIT NOT NULL DEFAULT 1, - createdAt DATETIME NOT NULL, - createdBy VARCHAR(100) NULL, - updated_by VARCHAR(100) NULL, - notes TEXT NULL, - PRIMARY KEY (versionId), - KEY idx_form_version_formId (formId), - CONSTRAINT fk_form_version_form FOREIGN KEY (formId) REFERENCES t_dynamic_form (formId) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS t_form_section ( - sectionId BIGINT NOT NULL AUTO_INCREMENT, - version_id BIGINT NOT NULL, - sectionUuid VARCHAR(100) NOT NULL, - sectionName VARCHAR(255) NOT NULL, - sectionPhase VARCHAR(20) NOT NULL, - isRequired BIT NOT NULL DEFAULT 1, - displayOrder INT NOT NULL, - hasSubmitButton BIT NOT NULL DEFAULT 0, - created_by VARCHAR(100) NULL, - updated_by VARCHAR(100) NULL, - PRIMARY KEY (sectionId), - KEY idx_form_section_version (version_id), - CONSTRAINT fk_form_section_version FOREIGN KEY (version_id) REFERENCES t_form_version (versionId) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS t_section_question ( - questionId BIGINT NOT NULL AUTO_INCREMENT, - sectionId BIGINT NOT NULL, - questionUuid VARCHAR(100) NOT NULL, - questionText TEXT NOT NULL, - questionType VARCHAR(20) NOT NULL, - isMandatory BIT NOT NULL DEFAULT 1, - displayOrder INT NOT NULL, - maxLength INT NULL, - defaultValue VARCHAR(500) NULL, - containsPii BIT NOT NULL DEFAULT 0, - created_by VARCHAR(100) NULL, - updated_by VARCHAR(100) NULL, - PRIMARY KEY (questionId), - KEY idx_section_question_sectionId (sectionId), - CONSTRAINT fk_section_question_section FOREIGN KEY (sectionId) REFERENCES t_form_section (sectionId) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS t_question_option ( - optionId BIGINT NOT NULL AUTO_INCREMENT, - questionId BIGINT NOT NULL, - optionLabel VARCHAR(255) NOT NULL, - optionValue VARCHAR(100) NOT NULL, - displayOrder INT NOT NULL, - created_by VARCHAR(100) NULL, - updated_by VARCHAR(100) NULL, - PRIMARY KEY (optionId), - KEY idx_question_option_questionId (questionId), - CONSTRAINT fk_question_option_question FOREIGN KEY (questionId) REFERENCES t_section_question (questionId) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS t_question_validation ( - validationId BIGINT NOT NULL AUTO_INCREMENT, - questionId BIGINT NOT NULL, - validationType VARCHAR(30) NOT NULL, - validationParam VARCHAR(255) NULL, - errorMessage VARCHAR(500) NOT NULL, - created_by VARCHAR(100) NULL, - updated_by VARCHAR(100) NULL, - PRIMARY KEY (validationId), - KEY idx_question_validation_questionId (questionId), - CONSTRAINT fk_question_validation_question FOREIGN KEY (questionId) REFERENCES t_section_question (questionId) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS t_option_condition ( - conditionId BIGINT NOT NULL AUTO_INCREMENT, - optionId BIGINT NOT NULL, - actionType VARCHAR(40) NOT NULL, - targetQuestionId BIGINT NULL, - targetSectionId BIGINT NULL, - created_by VARCHAR(100) NULL, - updated_by VARCHAR(100) NULL, - PRIMARY KEY (conditionId), - KEY idx_option_condition_optionId (optionId), - CONSTRAINT fk_option_condition_option FOREIGN KEY (optionId) REFERENCES t_question_option (optionId), - CONSTRAINT fk_option_condition_target_question FOREIGN KEY (targetQuestionId) REFERENCES t_section_question (questionId), - CONSTRAINT fk_option_condition_target_section FOREIGN KEY (targetSectionId) REFERENCES t_form_section (sectionId) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS t_form_response ( - responseId BIGINT NOT NULL AUTO_INCREMENT, - beneficiaryId BIGINT NOT NULL, - formId BIGINT NOT NULL, - versionId BIGINT NOT NULL, - officerId BIGINT NOT NULL, - status VARCHAR(20) NOT NULL, - submittedAt DATETIME NULL, - completedAt DATETIME NULL, - last_follow_up_at DATETIME NULL, - created_by VARCHAR(100) NULL, - updated_by VARCHAR(100) NULL, - createdAt DATETIME NOT NULL, - updatedAt DATETIME NOT NULL, - PRIMARY KEY (responseId), - KEY idx_form_response_ben_form (beneficiaryId, formId) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS t_section_response ( - sectionResponseId BIGINT NOT NULL AUTO_INCREMENT, - responseId BIGINT NOT NULL, - sectionId BIGINT NOT NULL, - status VARCHAR(20) NOT NULL, - savedAt DATETIME NULL, - created_by VARCHAR(100) NULL, - updated_by VARCHAR(100) NULL, - PRIMARY KEY (sectionResponseId), - KEY idx_section_response_responseId (responseId), - CONSTRAINT fk_section_response_response FOREIGN KEY (responseId) REFERENCES t_form_response (responseId) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -CREATE TABLE IF NOT EXISTS t_question_response ( - questionResponseId BIGINT NOT NULL AUTO_INCREMENT, - sectionResponseId BIGINT NOT NULL, - questionId BIGINT NOT NULL, - optionId BIGINT NULL, - answerText TEXT NULL, - created_by VARCHAR(100) NULL, - updated_by VARCHAR(100) NULL, - PRIMARY KEY (questionResponseId), - KEY idx_question_response_sectionResponseId (sectionResponseId), - CONSTRAINT fk_question_response_section_response FOREIGN KEY (sectionResponseId) REFERENCES t_section_response (sectionResponseId) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- ───────────────────────────────────────────────────────────── --- Sample seed data: one form, one version, one section, two questions --- ───────────────────────────────────────────────────────────── - -INSERT INTO t_dynamic_form (formUuid, formName, formType, isActive, created_by, updated_by, createdAt, updatedAt) -VALUES ('11111111-1111-1111-1111-111111111111', 'TB Counselling Form', 'COUNSELLING', 1, 'system', 'system', NOW(), NOW()); - -INSERT INTO t_form_version (formId, versionNumber, isLatest, createdAt, createdBy, updated_by, notes) -VALUES (LAST_INSERT_ID(), 1, 1, NOW(), 'system', 'system', 'initial version'); - -INSERT INTO t_form_section (version_id, sectionUuid, sectionName, sectionPhase, isRequired, displayOrder, hasSubmitButton, created_by, updated_by) -VALUES (LAST_INSERT_ID(), '22222222-2222-2222-2222-222222222222', 'Disease Awareness', 'PRE_SUBMIT', 1, 1, 1, 'system', 'system'); - -INSERT INTO t_section_question (sectionId, questionUuid, questionText, questionType, isMandatory, displayOrder, containsPii, created_by, updated_by) -VALUES (LAST_INSERT_ID(), '33333333-3333-3333-3333-333333333333', 'Has the beneficiary heard of TB before?', 'RADIO', 1, 1, 0, 'system', 'system'); - -INSERT INTO t_question_option (questionId, optionLabel, optionValue, displayOrder, created_by, updated_by) -VALUES -(LAST_INSERT_ID(), 'Yes', 'YES', 1, 'system', 'system'), -(LAST_INSERT_ID(), 'No', 'NO', 2, 'system', 'system'); From 487d04dc3662be1c05569e97719bb3b643c3a0f6 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 10 Jul 2026 13:43:24 +0530 Subject: [PATCH 93/97] Add Nikshay TU/Facility/Village support for Stop TB - getUserDetail: additive Nikshay TU/Facility fields via a new base-table query, bypassing the shared v_userservicerolemapping view entirely so other service lines' reads are provably unaffected. - New NikshayLocationDerivationService: auto-derives a beneficiary's Nikshay TU/Facility/Village from their existing village (i_ben_flow_outreach), no new input needed from the mobile app. - TBSuspected/StopTBDiagnostics: snapshot nikshayTUID/FacilityID/VillageID at save time, wired into both save paths. Co-Authored-By: Claude Sonnet 5 --- .../iemr/NikshayVillageFacilityMapping.java | 33 ++++++++++ .../flw/domain/iemr/StopTBDiagnostics.java | 12 ++++ .../com/iemr/flw/domain/iemr/TBSuspected.java | 12 ++++ .../iemr/flw/dto/iemr/UserServiceRoleDTO.java | 8 +++ .../NikshayVillageFacilityMappingRepo.java | 16 +++++ .../flw/repo/iemr/UserServiceRoleRepo.java | 18 +++++ .../NikshayLocationDerivationService.java | 65 +++++++++++++++++++ .../flw/service/impl/StopTBServiceImpl.java | 14 ++++ .../service/impl/TBSuspectedServiceImpl.java | 16 +++++ .../flw/service/impl/UserServiceImpl.java | 15 +++++ 10 files changed, 209 insertions(+) create mode 100644 src/main/java/com/iemr/flw/domain/iemr/NikshayVillageFacilityMapping.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/NikshayVillageFacilityMappingRepo.java create mode 100644 src/main/java/com/iemr/flw/service/impl/NikshayLocationDerivationService.java diff --git a/src/main/java/com/iemr/flw/domain/iemr/NikshayVillageFacilityMapping.java b/src/main/java/com/iemr/flw/domain/iemr/NikshayVillageFacilityMapping.java new file mode 100644 index 00000000..6827ee00 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/NikshayVillageFacilityMapping.java @@ -0,0 +1,33 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.*; + +/** + * FLW-API's own read copy of Admin-API's m_nikshay_village_facility_mapping — + * links an existing AMRIT village to the Nikshay Facility/TU it falls under. + * Read-only from this side; the table itself is owned/populated by the + * Nikshay location import (see Admin-API). + */ +@Entity +@Table(name = "m_nikshay_village_facility_mapping", schema = "db_iemr") +public class NikshayVillageFacilityMapping { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "MappingID") + private Integer mappingID; + + @Column(name = "AmritVillageID") + private Integer amritVillageID; + + @Column(name = "NikshayFacilityID") + private Integer nikshayFacilityID; + + @Column(name = "NikshayTUID") + private Integer nikshayTUID; + + public Integer getMappingID() { return mappingID; } + public Integer getAmritVillageID() { return amritVillageID; } + public Integer getNikshayFacilityID() { return nikshayFacilityID; } + public Integer getNikshayTUID() { return nikshayTUID; } +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java index f9abde57..4ba6dd79 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java @@ -29,6 +29,18 @@ public class StopTBDiagnostics { @Column(name = "nikshay_id", length = 50) private String nikshayId; + // Snapshot of this case's Nikshay location, auto-derived from the beneficiary's + // village at the time of save (not re-derived later, so it stays fixed even if + // the beneficiary's own village record changes afterwards) + @Column(name = "nikshay_tu_id") + private Integer nikshayTUID; + + @Column(name = "nikshay_facility_id") + private Integer nikshayFacilityID; + + @Column(name = "nikshay_village_id") + private Integer nikshayVillageID; + // Digital Chest X-ray @Column(name = "is_referred_for_digital_chest_xray") private Boolean isReferredForDigitalChestXray; 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 0c8d2fc4..3f42cb86 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBSuspected.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBSuspected.java @@ -33,6 +33,18 @@ public class TBSuspected { @Column(name = "nikshay_id") private String nikshayId; + // Snapshot of this case's Nikshay location, auto-derived from the beneficiary's + // village at the time of save (not re-derived later, so it stays fixed even if + // the beneficiary's own village record changes afterwards) + @Column(name = "nikshay_tu_id") + private Integer nikshayTUID; + + @Column(name = "nikshay_facility_id") + private Integer nikshayFacilityID; + + @Column(name = "nikshay_village_id") + private Integer nikshayVillageID; + @Column(name = "sputum_test_result") private String sputumTestResult; 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/repo/iemr/NikshayVillageFacilityMappingRepo.java b/src/main/java/com/iemr/flw/repo/iemr/NikshayVillageFacilityMappingRepo.java new file mode 100644 index 00000000..9d271abc --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/NikshayVillageFacilityMappingRepo.java @@ -0,0 +1,16 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.NikshayVillageFacilityMapping; +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.List; + +@Repository +public interface NikshayVillageFacilityMappingRepo extends JpaRepository { + + @Query("SELECT m FROM NikshayVillageFacilityMapping m WHERE m.amritVillageID = :villageId") + List findByAmritVillageID(@Param("villageId") Integer villageId); +} 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 f0960249..5086c3a4 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/UserServiceRoleRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/UserServiceRoleRepo.java @@ -41,4 +41,22 @@ 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). + @Query(value = "SELECT " + + "GROUP_CONCAT(DISTINCT usrm.NikshayTUID ORDER BY usrm.NikshayTUID), " + + "GROUP_CONCAT(DISTINCT nt.TUName ORDER BY usrm.NikshayTUID), " + + "GROUP_CONCAT(DISTINCT usrm.NikshayFacilityID ORDER BY usrm.NikshayFacilityID), " + + "GROUP_CONCAT(DISTINCT nf.FacilityName ORDER BY usrm.NikshayFacilityID) " + + "FROM m_userservicerolemapping usrm " + + "LEFT JOIN m_nikshay_tu nt ON nt.NikshayTUID = usrm.NikshayTUID " + + "LEFT JOIN m_nikshay_facility nf ON nf.NikshayFacilityID = usrm.NikshayFacilityID " + + "WHERE usrm.UserID = :userId AND usrm.ProviderServiceMapID = :providerServiceMapId " + + "AND usrm.Deleted = false AND usrm.NikshayTUID IS NOT NULL", + nativeQuery = true) + List getNikshayLocationScope(@Param("userId") Integer userId, + @Param("providerServiceMapId") Integer providerServiceMapId); + } diff --git a/src/main/java/com/iemr/flw/service/impl/NikshayLocationDerivationService.java b/src/main/java/com/iemr/flw/service/impl/NikshayLocationDerivationService.java new file mode 100644 index 00000000..f6c54e40 --- /dev/null +++ b/src/main/java/com/iemr/flw/service/impl/NikshayLocationDerivationService.java @@ -0,0 +1,65 @@ + +package com.iemr.flw.service.impl; + +import com.iemr.flw.domain.iemr.BenFlowStatus; +import com.iemr.flw.domain.iemr.NikshayVillageFacilityMapping; +import com.iemr.flw.repo.iemr.BenFlowStatusRepo; +import com.iemr.flw.repo.iemr.NikshayVillageFacilityMappingRepo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * Stop TB / Nikshay — auto-derives a beneficiary's Nikshay TU/Facility/Village + * from their existing village (already known from registration), so a field + * worker never needs to select this themselves when saving a case. Called + * once, at the moment a TB Suspected/Diagnostics case is saved, so the result + * is a snapshot of what was true then — not re-derived later if the + * beneficiary's own village record changes afterwards. + */ +@Service +public class NikshayLocationDerivationService { + + @Autowired + private BenFlowStatusRepo benFlowStatusRepo; + + @Autowired + private NikshayVillageFacilityMappingRepo nikshayVillageFacilityMappingRepo; + + public static class NikshayLocationScope { + public Integer villageID; + public Integer facilityID; + public Integer tuID; + } + + /** + * Returns null if the beneficiary has no known village, or their village + * has no confirmed match in the Nikshay location master yet (both are + * flagged gaps, not blocking — the case still saves, just without a + * Nikshay tag until the gap is resolved). + */ + public NikshayLocationScope deriveForBeneficiary(Long beneficiaryRegID) { + if (beneficiaryRegID == null) { + return null; + } + List flows = benFlowStatusRepo.findByBeneficiaryRegID(beneficiaryRegID); + if (flows == null || flows.isEmpty()) { + return null; + } + Integer villageID = flows.get(0).getVillageID(); + if (villageID == null) { + return null; + } + List mappings = nikshayVillageFacilityMappingRepo.findByAmritVillageID(villageID); + if (mappings == null || mappings.isEmpty()) { + return null; + } + NikshayVillageFacilityMapping mapping = mappings.get(0); + NikshayLocationScope scope = new NikshayLocationScope(); + scope.villageID = villageID; + scope.facilityID = mapping.getNikshayFacilityID(); + scope.tuID = mapping.getNikshayTUID(); + return scope; + } +} diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 10016c4a..e7b93f64 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -69,6 +69,9 @@ public class StopTBServiceImpl implements StopTBService { @Autowired private StopTBDiagnosticsRepo diagnosticsRepo; + @Autowired + private NikshayLocationDerivationService nikshayLocationDerivationService; + @Autowired private TBStopVisitService tbStopVisitService; @@ -479,6 +482,17 @@ public List> saveDiagnostics(List> dataL if (diag.getVanID() == null && vanID != null) { diag.setVanID(vanID); diag.setParkingPlaceID(parkingPlaceID); } diag.setProcessed("N"); + // Stop TB / Nikshay — auto-derive from the beneficiary's existing village. + // Additive only: if no match is found, the case still saves normally, + // just without a Nikshay tag until that gap is resolved. + NikshayLocationDerivationService.NikshayLocationScope nikshayScope = + nikshayLocationDerivationService.deriveForBeneficiary(benRegID); + if (nikshayScope != null) { + diag.setNikshayVillageID(nikshayScope.villageID); + diag.setNikshayFacilityID(nikshayScope.facilityID); + diag.setNikshayTUID(nikshayScope.tuID); + } + diagnosticsRepo.save(diag); if (isNew) diagnosticsRepo.updateVanSerialNo(diag.getId()); 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 cc0abdd0..9ec4a906 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -39,6 +39,9 @@ public class TBSuspectedServiceImpl implements TBSuspectedService { @Autowired private IncentiveLogicService incentiveLogicService; + @Autowired + private NikshayLocationDerivationService nikshayLocationDerivationService; + @Override public String getByBenId(Long benId, String authorisation) throws Exception { return null; @@ -70,6 +73,19 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { tbSuspected.setVisitCode(visit.getVisitCode()); if (tbSuspected.getVanID() == null && vanID != null) { tbSuspected.setVanID(vanID); tbSuspected.setParkingPlaceID(parkingPlaceID); } tbSuspected.setProcessed("N"); + + // Stop TB / Nikshay — auto-derive from the beneficiary's existing village. + // Additive only: if no match is found (village not yet linked, or + // beneficiary has no village on file), the case still saves normally, + // just without a Nikshay tag until that gap is resolved. + NikshayLocationDerivationService.NikshayLocationScope nikshayScope = + nikshayLocationDerivationService.deriveForBeneficiary(tbSuspected.getBenId()); + if (nikshayScope != null) { + tbSuspected.setNikshayVillageID(nikshayScope.villageID); + tbSuspected.setNikshayFacilityID(nikshayScope.facilityID); + tbSuspected.setNikshayTUID(nikshayScope.tuID); + } + tbSuspectedRepo.save(tbSuspected); tbSuspectedRepo.updateVanSerialNo(tbSuspected.getId()); if(tbSuspected!=null){ 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 e0e5b76b..99ae4acf 100644 --- a/src/main/java/com/iemr/flw/service/impl/UserServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/UserServiceImpl.java @@ -33,6 +33,21 @@ public UserServiceRoleDTO getUserDetail(Integer userId) { } } } + + // 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, so no explicit service-name check is needed here. + java.util.List nikshayResults = userServiceRoleRepo.getNikshayLocationScope(userId, userRole.getProviderServiceMapId()); + if (nikshayResults != null && !nikshayResults.isEmpty()) { + Object[] nikshay = nikshayResults.get(0); + if (nikshay != null && nikshay.length == 4 && nikshay[0] != null) { + userRole.setTuId((String) nikshay[0]); + userRole.setTuName((String) nikshay[1]); + userRole.setHealthFacilityId((String) nikshay[2]); + userRole.setHealthFacilityName((String) nikshay[3]); + } + } + return userRole; } } From 08e6b82d1d8288434fa1208b49d77b4753c9c32d Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 14 Jul 2026 17:39:06 +0530 Subject: [PATCH 94/97] refactor(stoptb): drop per-record Nikshay TU/Facility/Village snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep the Nikshay TU/Facility lookup in getUserDetail (user-level, both old and new users handled gracefully) but remove the per-case snapshot on TBSuspected/StopTBDiagnostics — not needed there. Deletes the now- orphaned NikshayLocationDerivationService and NikshayVillageFacilityMapping (entity + repo), which existed only to serve that snapshot. --- .../iemr/NikshayVillageFacilityMapping.java | 33 ---------- .../flw/domain/iemr/StopTBDiagnostics.java | 12 ---- .../com/iemr/flw/domain/iemr/TBSuspected.java | 12 ---- .../NikshayVillageFacilityMappingRepo.java | 16 ----- .../NikshayLocationDerivationService.java | 65 ------------------- .../flw/service/impl/StopTBServiceImpl.java | 14 ---- .../service/impl/TBSuspectedServiceImpl.java | 15 ----- 7 files changed, 167 deletions(-) delete mode 100644 src/main/java/com/iemr/flw/domain/iemr/NikshayVillageFacilityMapping.java delete mode 100644 src/main/java/com/iemr/flw/repo/iemr/NikshayVillageFacilityMappingRepo.java delete mode 100644 src/main/java/com/iemr/flw/service/impl/NikshayLocationDerivationService.java diff --git a/src/main/java/com/iemr/flw/domain/iemr/NikshayVillageFacilityMapping.java b/src/main/java/com/iemr/flw/domain/iemr/NikshayVillageFacilityMapping.java deleted file mode 100644 index 6827ee00..00000000 --- a/src/main/java/com/iemr/flw/domain/iemr/NikshayVillageFacilityMapping.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.iemr.flw.domain.iemr; - -import jakarta.persistence.*; - -/** - * FLW-API's own read copy of Admin-API's m_nikshay_village_facility_mapping — - * links an existing AMRIT village to the Nikshay Facility/TU it falls under. - * Read-only from this side; the table itself is owned/populated by the - * Nikshay location import (see Admin-API). - */ -@Entity -@Table(name = "m_nikshay_village_facility_mapping", schema = "db_iemr") -public class NikshayVillageFacilityMapping { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "MappingID") - private Integer mappingID; - - @Column(name = "AmritVillageID") - private Integer amritVillageID; - - @Column(name = "NikshayFacilityID") - private Integer nikshayFacilityID; - - @Column(name = "NikshayTUID") - private Integer nikshayTUID; - - public Integer getMappingID() { return mappingID; } - public Integer getAmritVillageID() { return amritVillageID; } - public Integer getNikshayFacilityID() { return nikshayFacilityID; } - public Integer getNikshayTUID() { return nikshayTUID; } -} diff --git a/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java b/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java index 4ba6dd79..f9abde57 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java +++ b/src/main/java/com/iemr/flw/domain/iemr/StopTBDiagnostics.java @@ -29,18 +29,6 @@ public class StopTBDiagnostics { @Column(name = "nikshay_id", length = 50) private String nikshayId; - // Snapshot of this case's Nikshay location, auto-derived from the beneficiary's - // village at the time of save (not re-derived later, so it stays fixed even if - // the beneficiary's own village record changes afterwards) - @Column(name = "nikshay_tu_id") - private Integer nikshayTUID; - - @Column(name = "nikshay_facility_id") - private Integer nikshayFacilityID; - - @Column(name = "nikshay_village_id") - private Integer nikshayVillageID; - // Digital Chest X-ray @Column(name = "is_referred_for_digital_chest_xray") private Boolean isReferredForDigitalChestXray; 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 3f42cb86..0c8d2fc4 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBSuspected.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBSuspected.java @@ -33,18 +33,6 @@ public class TBSuspected { @Column(name = "nikshay_id") private String nikshayId; - // Snapshot of this case's Nikshay location, auto-derived from the beneficiary's - // village at the time of save (not re-derived later, so it stays fixed even if - // the beneficiary's own village record changes afterwards) - @Column(name = "nikshay_tu_id") - private Integer nikshayTUID; - - @Column(name = "nikshay_facility_id") - private Integer nikshayFacilityID; - - @Column(name = "nikshay_village_id") - private Integer nikshayVillageID; - @Column(name = "sputum_test_result") private String sputumTestResult; diff --git a/src/main/java/com/iemr/flw/repo/iemr/NikshayVillageFacilityMappingRepo.java b/src/main/java/com/iemr/flw/repo/iemr/NikshayVillageFacilityMappingRepo.java deleted file mode 100644 index 9d271abc..00000000 --- a/src/main/java/com/iemr/flw/repo/iemr/NikshayVillageFacilityMappingRepo.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.iemr.flw.repo.iemr; - -import com.iemr.flw.domain.iemr.NikshayVillageFacilityMapping; -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.List; - -@Repository -public interface NikshayVillageFacilityMappingRepo extends JpaRepository { - - @Query("SELECT m FROM NikshayVillageFacilityMapping m WHERE m.amritVillageID = :villageId") - List findByAmritVillageID(@Param("villageId") Integer villageId); -} diff --git a/src/main/java/com/iemr/flw/service/impl/NikshayLocationDerivationService.java b/src/main/java/com/iemr/flw/service/impl/NikshayLocationDerivationService.java deleted file mode 100644 index f6c54e40..00000000 --- a/src/main/java/com/iemr/flw/service/impl/NikshayLocationDerivationService.java +++ /dev/null @@ -1,65 +0,0 @@ - -package com.iemr.flw.service.impl; - -import com.iemr.flw.domain.iemr.BenFlowStatus; -import com.iemr.flw.domain.iemr.NikshayVillageFacilityMapping; -import com.iemr.flw.repo.iemr.BenFlowStatusRepo; -import com.iemr.flw.repo.iemr.NikshayVillageFacilityMappingRepo; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.List; - -/** - * Stop TB / Nikshay — auto-derives a beneficiary's Nikshay TU/Facility/Village - * from their existing village (already known from registration), so a field - * worker never needs to select this themselves when saving a case. Called - * once, at the moment a TB Suspected/Diagnostics case is saved, so the result - * is a snapshot of what was true then — not re-derived later if the - * beneficiary's own village record changes afterwards. - */ -@Service -public class NikshayLocationDerivationService { - - @Autowired - private BenFlowStatusRepo benFlowStatusRepo; - - @Autowired - private NikshayVillageFacilityMappingRepo nikshayVillageFacilityMappingRepo; - - public static class NikshayLocationScope { - public Integer villageID; - public Integer facilityID; - public Integer tuID; - } - - /** - * Returns null if the beneficiary has no known village, or their village - * has no confirmed match in the Nikshay location master yet (both are - * flagged gaps, not blocking — the case still saves, just without a - * Nikshay tag until the gap is resolved). - */ - public NikshayLocationScope deriveForBeneficiary(Long beneficiaryRegID) { - if (beneficiaryRegID == null) { - return null; - } - List flows = benFlowStatusRepo.findByBeneficiaryRegID(beneficiaryRegID); - if (flows == null || flows.isEmpty()) { - return null; - } - Integer villageID = flows.get(0).getVillageID(); - if (villageID == null) { - return null; - } - List mappings = nikshayVillageFacilityMappingRepo.findByAmritVillageID(villageID); - if (mappings == null || mappings.isEmpty()) { - return null; - } - NikshayVillageFacilityMapping mapping = mappings.get(0); - NikshayLocationScope scope = new NikshayLocationScope(); - scope.villageID = villageID; - scope.facilityID = mapping.getNikshayFacilityID(); - scope.tuID = mapping.getNikshayTUID(); - return scope; - } -} diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index e7b93f64..10016c4a 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -69,9 +69,6 @@ public class StopTBServiceImpl implements StopTBService { @Autowired private StopTBDiagnosticsRepo diagnosticsRepo; - @Autowired - private NikshayLocationDerivationService nikshayLocationDerivationService; - @Autowired private TBStopVisitService tbStopVisitService; @@ -482,17 +479,6 @@ public List> saveDiagnostics(List> dataL if (diag.getVanID() == null && vanID != null) { diag.setVanID(vanID); diag.setParkingPlaceID(parkingPlaceID); } diag.setProcessed("N"); - // Stop TB / Nikshay — auto-derive from the beneficiary's existing village. - // Additive only: if no match is found, the case still saves normally, - // just without a Nikshay tag until that gap is resolved. - NikshayLocationDerivationService.NikshayLocationScope nikshayScope = - nikshayLocationDerivationService.deriveForBeneficiary(benRegID); - if (nikshayScope != null) { - diag.setNikshayVillageID(nikshayScope.villageID); - diag.setNikshayFacilityID(nikshayScope.facilityID); - diag.setNikshayTUID(nikshayScope.tuID); - } - diagnosticsRepo.save(diag); if (isNew) diagnosticsRepo.updateVanSerialNo(diag.getId()); 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 9ec4a906..9b4c12fa 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -39,9 +39,6 @@ public class TBSuspectedServiceImpl implements TBSuspectedService { @Autowired private IncentiveLogicService incentiveLogicService; - @Autowired - private NikshayLocationDerivationService nikshayLocationDerivationService; - @Override public String getByBenId(Long benId, String authorisation) throws Exception { return null; @@ -74,18 +71,6 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { if (tbSuspected.getVanID() == null && vanID != null) { tbSuspected.setVanID(vanID); tbSuspected.setParkingPlaceID(parkingPlaceID); } tbSuspected.setProcessed("N"); - // Stop TB / Nikshay — auto-derive from the beneficiary's existing village. - // Additive only: if no match is found (village not yet linked, or - // beneficiary has no village on file), the case still saves normally, - // just without a Nikshay tag until that gap is resolved. - NikshayLocationDerivationService.NikshayLocationScope nikshayScope = - nikshayLocationDerivationService.deriveForBeneficiary(tbSuspected.getBenId()); - if (nikshayScope != null) { - tbSuspected.setNikshayVillageID(nikshayScope.villageID); - tbSuspected.setNikshayFacilityID(nikshayScope.facilityID); - tbSuspected.setNikshayTUID(nikshayScope.tuID); - } - tbSuspectedRepo.save(tbSuspected); tbSuspectedRepo.updateVanSerialNo(tbSuspected.getId()); if(tbSuspected!=null){ From e8ef9021ed2e1de9aec84607cb1b9925bd21fa12 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 15 Jul 2026 10:21:32 +0530 Subject: [PATCH 95/97] fix(nikshay): join Nikshay TU/Facility masters with FIND_IN_SET NikshayTUID/NikshayFacilityID on m_userservicerolemapping are now TEXT columns holding a comma-joined list of IDs per row. The old equality join (nt.NikshayTUID = usrm.NikshayTUID) silently matched only the first ID in the list, since MySQL casts "12,45" to 12 for numeric comparison, and dropped the rest without erroring. Switch to FIND_IN_SET so every ID in the list resolves to its name. Co-Authored-By: Claude Sonnet 5 --- .../com/iemr/flw/repo/iemr/UserServiceRoleRepo.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 5086c3a4..6c32d707 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/UserServiceRoleRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/UserServiceRoleRepo.java @@ -45,14 +45,19 @@ public interface UserServiceRoleRepo extends JpaRepository 0 " + + "LEFT JOIN m_nikshay_facility nf ON FIND_IN_SET(nf.NikshayFacilityID, usrm.NikshayFacilityID) > 0 " + "WHERE usrm.UserID = :userId AND usrm.ProviderServiceMapID = :providerServiceMapId " + "AND usrm.Deleted = false AND usrm.NikshayTUID IS NOT NULL", nativeQuery = true) From 93f02f7e04e7f1d6e8d90acf874f0b2fc83dd74f Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 15 Jul 2026 15:16:27 +0530 Subject: [PATCH 96/97] fix(nikshay): stop patching District via AMRIT block lookup for Stop TB getDistrictByBlockId(userRole.getBlockId()) joins the raw BlockId against m_districtblock/m_district - correct for other servicelines, where BlockId genuinely is an AMRIT block ID. Stop TB's BlockId holds the first selected Nikshay TU's ID instead, so this join resolves to whatever AMRIT district happens to share that same numeric ID by coincidence, not real data - feeding a wrong district into the mobile app's login/session response for Stop TB workers. Fetch the Nikshay scope first so Stop TB users can be detected and skip the district-by-block patch entirely for them. Co-Authored-By: Claude Sonnet 5 --- .../flw/service/impl/UserServiceImpl.java | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) 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 99ae4acf..4d20cba5 100644 --- a/src/main/java/com/iemr/flw/service/impl/UserServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/UserServiceImpl.java @@ -23,7 +23,22 @@ 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())); - if (userRole.getWorkingDistrictId() == null && userRole.getBlockId() != null) { + + // 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 == 4 + && 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); @@ -34,18 +49,12 @@ public UserServiceRoleDTO getUserDetail(Integer userId) { } } - // 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, so no explicit service-name check is needed here. - java.util.List nikshayResults = userServiceRoleRepo.getNikshayLocationScope(userId, userRole.getProviderServiceMapId()); - if (nikshayResults != null && !nikshayResults.isEmpty()) { + if (isStopTBUser) { Object[] nikshay = nikshayResults.get(0); - if (nikshay != null && nikshay.length == 4 && nikshay[0] != null) { - userRole.setTuId((String) nikshay[0]); - userRole.setTuName((String) nikshay[1]); - userRole.setHealthFacilityId((String) nikshay[2]); - userRole.setHealthFacilityName((String) nikshay[3]); - } + userRole.setTuId((String) nikshay[0]); + userRole.setTuName((String) nikshay[1]); + userRole.setHealthFacilityId((String) nikshay[2]); + userRole.setHealthFacilityName((String) nikshay[3]); } return userRole; From f02f6c024750a363ba7c37093082d8f80ec54eec Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Thu, 16 Jul 2026 13:58:44 +0530 Subject: [PATCH 97/97] fix(nikshay): resolve Stop TB's real district instead of leaving it null Stop TB never sets WorkingLocationID, so workingDistrictId/Name (derived from it, used by every other serviceline) always come back null - and the earlier fallback (resolving district via BlockId) was correctly disabled for Stop TB since BlockId holds a Nikshay TU ID, not a real AMRIT block, and would resolve to a coincidentally-matching wrong district. DistrictID already sits directly on the mapping row for Stop TB (no work-location indirection layer to go through), so getNikshayLocationScope now also returns it, resolved via a join against Nikshay's own district table (safe - same ID space, unlike AMRIT's m_district). getUserDetail uses this to populate workingDistrictId/Name for Stop TB users directly, instead of leaving them null. This is what the mobile app's own village selection screen falls back to Block's name for, when District comes back null - so this fixes that screen showing the wrong value too. Co-Authored-By: Claude Sonnet 5 --- .../com/iemr/flw/repo/iemr/UserServiceRoleRepo.java | 13 +++++++++++-- .../com/iemr/flw/service/impl/UserServiceImpl.java | 13 ++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) 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 6c32d707..f9e10e65 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/UserServiceRoleRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/UserServiceRoleRepo.java @@ -50,16 +50,25 @@ public interface UserServiceRoleRepo extends JpaRepository 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", + "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/service/impl/UserServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/UserServiceImpl.java index 4d20cba5..bd2d3e7c 100644 --- a/src/main/java/com/iemr/flw/service/impl/UserServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/UserServiceImpl.java @@ -30,7 +30,7 @@ public UserServiceRoleDTO getUserDetail(Integer userId) { // 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 == 4 + && 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 - @@ -55,6 +55,17 @@ public UserServiceRoleDTO getUserDetail(Integer userId) { 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;