├── .nvmrc ├── __mocks__ ├── fileMock.js └── react-hotkeys-hook.js ├── .env.development ├── src ├── dataEntryApp │ ├── views │ │ ├── subjectDashBoard │ │ │ └── components │ │ │ │ ├── CompletedProgramEncounter.js │ │ │ │ ├── FloatingButton.jsx │ │ │ │ ├── NewVisitLinkButton.jsx │ │ │ │ ├── Button.jsx │ │ │ │ ├── news │ │ │ │ └── NewsDetails.jsx │ │ │ │ └── FindRelativeTable.jsx │ │ ├── viewmodel │ │ │ └── StaticFormElement.js │ │ ├── dashboardNew │ │ │ └── content.jsx │ │ ├── registration │ │ │ └── FormWizardButton.jsx │ │ ├── audio │ │ │ └── Player.jsx │ │ └── GlobalSearch │ │ │ └── CheckBoxSearchComponent.jsx │ ├── index.js │ ├── constants.js │ ├── sagas │ │ ├── selectors.test.js │ │ ├── referenceDataSaga.test.js │ │ ├── programSaga.js │ │ ├── NewsSaga.js │ │ └── generalSubjectDashboardSaga.js │ ├── components │ │ ├── Loading.jsx │ │ ├── Checkbox.jsx │ │ ├── PrimaryButton.jsx │ │ ├── Checkbox.stories.jsx │ │ ├── DeleteButton.jsx │ │ ├── Radio.jsx │ │ ├── ValidationError.jsx │ │ ├── ToolTipContainer.jsx │ │ ├── SingleSelectFormElement.jsx │ │ ├── MultiSelectFormElement.jsx │ │ ├── LandingSubjectFormElement.jsx │ │ ├── CodedConceptFormElement.jsx │ │ ├── MessageDialog.jsx │ │ ├── MediaObservations.jsx │ │ ├── GroupMembershipCardView.jsx │ │ ├── CustomizedBackdrop.jsx │ │ ├── SubjectCardView.stories.jsx │ │ ├── ConfirmDialog.jsx │ │ ├── MediaFormElement.jsx │ │ └── SubjectVoided.jsx │ ├── Colors.js │ ├── test │ │ └── TestKeyValueFactory.js │ ├── services │ │ ├── AddressLevelService.js │ │ ├── ConceptService.js │ │ ├── SubjectService.js │ │ ├── FormElementService.test.js │ │ └── SubjectSearchService.js │ ├── index.css │ ├── reducers │ │ ├── searchFilterReducer.js │ │ ├── loadReducer.js │ │ ├── programReducer.js │ │ ├── generalSubjectDashboardReducer.js │ │ └── searchReducer.js │ ├── utils │ │ └── LocationUtil.js │ └── state │ │ └── Wizard.js ├── logo.png ├── login_image.png ├── reports │ ├── index.js │ ├── Common.js │ ├── export │ │ └── ReportTypes.js │ ├── api.js │ ├── components │ │ └── export │ │ │ ├── ExportRequestBody.jsx │ │ │ ├── RegistrationType.jsx │ │ │ ├── DateOptions.jsx │ │ │ ├── DateSelector.jsx │ │ │ ├── GroupSubjectType.jsx │ │ │ └── ExportOptions.jsx │ └── sagas.js ├── avni-background.jpeg ├── avni-logo-black.png ├── formDesigner │ ├── index.css │ ├── styles │ │ └── images │ │ │ ├── avniLogo.png │ │ │ ├── background.jpg │ │ │ └── Alert_Sub-Dashboard-Tab-Icons.png │ ├── components │ │ ├── DeclarativeRule │ │ │ ├── MiddleText.jsx │ │ │ ├── InputField.jsx │ │ │ └── RuleSummaryComponent.jsx │ │ ├── BorderBox.jsx │ │ ├── ValidationError.jsx │ │ ├── MessageRule │ │ │ └── MessageReducer.js │ │ ├── StorageManagement │ │ │ └── StorageManagementConfigReducer.js │ │ ├── PhoneNumberConcept.jsx │ │ ├── ImageV2Concept.jsx │ │ ├── IconButton.jsx │ │ ├── TemplateOrganisations │ │ │ └── TemplateDialogErrorBoundary.jsx │ │ ├── ColourStyle.jsx │ │ ├── Video │ │ │ └── VideoReducer.js │ │ ├── ConceptActiveSwitch.jsx │ │ └── JsonEditor.jsx │ ├── common │ │ ├── ColorValue.jsx │ │ ├── ShowLabelValue.jsx │ │ ├── CommonSearch.jsx │ │ └── LocationTypeSearch.jsx │ ├── views │ │ └── FormDesignerContext.js │ ├── api │ │ └── ChecklistDetailsApi.js │ └── util │ │ └── KeyValuesUtil.js ├── upload │ ├── index.js │ ├── Types.js │ ├── UploadTypes.js │ ├── sagas.js │ └── LocationHierarchy.jsx ├── rootApp │ ├── index.js │ ├── security │ │ ├── BaseIdp.js │ │ ├── UndecidedIdp.jsx │ │ ├── StubbedLocalStorage.ts │ │ ├── NoAuthSession.js │ │ ├── KeycloakAuthSession.js │ │ ├── BaseAuthSession.js │ │ ├── CognitoAuthSession.js │ │ ├── NullIdp.jsx │ │ ├── IdpDetails.ts │ │ └── IdpFactory.jsx │ ├── LoginPage.css │ ├── SecureApp.css │ ├── utils.js │ ├── SagaErrorReducer.js │ ├── rootSaga.js │ └── rootReducer.js ├── news │ ├── model │ │ └── ContactGroup.js │ ├── whatsapp │ │ └── ReceiverType.js │ ├── components │ │ ├── ActionButton.js │ │ ├── DialogActionButton.jsx │ │ ├── AvniAlertDialog.jsx │ │ ├── PublishBroadcast.jsx │ │ ├── CustomToolbar.jsx │ │ └── CustomDialogTitle.jsx │ ├── News.jsx │ ├── utils │ │ ├── BroadcastPath.js │ │ └── index.jsx │ └── api │ │ └── index.js ├── common │ ├── store │ │ ├── index.js │ │ ├── getters.js │ │ └── commonReduxStoreReducer.js │ ├── service │ │ ├── EntityService.js │ │ ├── index.js │ │ ├── UserService.js │ │ ├── OrganisationService.jsx │ │ └── MessageService.js │ ├── model │ │ ├── OperationalModules.js │ │ ├── reports │ │ │ ├── WebReportCardFactory.js │ │ │ └── WebDashboardSectionCardMapping.js │ │ ├── WebForm.js │ │ ├── WebProgram.js │ │ ├── SubjectTypeFactory.js │ │ ├── WebEncounterType.js │ │ ├── WebSubjectType.test.js │ │ ├── GroupModel.js │ │ ├── WebFormElementGroup.js │ │ ├── WebStandardReportCardType.js │ │ ├── WebFormElement.js │ │ ├── UserInfo.test.js │ │ └── UserInfo.js │ ├── utils │ │ ├── StringUtil.js │ │ ├── keyValues.js │ │ ├── routeUtil.js │ │ ├── LocalStorageLocator.js │ │ ├── ReactSelectHelper.js │ │ ├── files.js │ │ ├── MuiComponentHelper.js │ │ ├── index.js │ │ ├── General.js │ │ ├── S3Client.js │ │ ├── httpClient.test.js │ │ └── CollectionUtil.js │ ├── AvniRouter.jsx │ ├── components │ │ ├── AvniSelectForm.jsx │ │ ├── BooleanStatusInShow.jsx │ │ ├── TextFieldInShow.jsx │ │ ├── TextFormatFieldInShow.jsx │ │ ├── chatbot │ │ │ ├── index.ts │ │ │ └── ChatbotWrapper.tsx │ │ ├── Body.jsx │ │ ├── UserError.jsx │ │ ├── CustomUserMenu.jsx │ │ ├── Footer.jsx │ │ ├── DocumentationContainer.jsx │ │ ├── AvniRadioButtonGroupInput.jsx │ │ ├── AvniFormControl.jsx │ │ ├── CreateComponent.jsx │ │ ├── ActivityIndicatorModal.jsx │ │ ├── ToolTipContainer.jsx │ │ ├── ErrorMessage.jsx │ │ ├── SingleSelectSearch.jsx │ │ ├── AvniFormLabel.jsx │ │ ├── FileDownloadButton.jsx │ │ ├── Icon.jsx │ │ ├── HelpText.jsx │ │ ├── AvniSwitch.jsx │ │ ├── AvniTextField.jsx │ │ ├── ValueTextUnitSelect.jsx │ │ └── SaveComponent.jsx │ ├── stories │ │ └── JSEditor.stories.jsx │ ├── mapper │ │ ├── BaseEncounterMapper.js │ │ ├── EncounterMapper.js │ │ └── ProgramEncounterMapper.js │ ├── hooks │ │ └── useMRTPagination.js │ └── material-table │ │ └── fetch.js ├── adminApp │ ├── OrgManagerContext.js │ ├── domain │ │ └── OrganisationCategory.js │ ├── components │ │ ├── Title.jsx │ │ ├── TitleChip.jsx │ │ ├── AvniSelectInput.jsx │ │ ├── AvniReferenceInput.jsx │ │ ├── AvniTextInput.jsx │ │ ├── AvniFormDataConsumer.jsx │ │ ├── CatchmentSelectInput.jsx │ │ ├── CustomisedExpansionPanelSummary.js │ │ ├── RuleDisplay.jsx │ │ ├── AvniBooleanInput.jsx │ │ ├── AlertModal.jsx │ │ ├── CreateEditFiltersHOC.jsx │ │ └── OpenOrganisation.jsx │ ├── README.md │ ├── react-admin-config │ │ ├── index.js │ │ └── SpringResponse.js │ ├── Util │ │ └── EntityEditUtil.js │ ├── AppDesignerLayout.jsx │ ├── SubjectType │ │ ├── GroupHandlers.js │ │ ├── effects.js │ │ ├── OptionSelect.jsx │ │ └── Types.js │ ├── service │ │ ├── MediaService.jsx │ │ └── ApplicationMenuService.js │ ├── Constant.js │ ├── EncounterType │ │ └── EncounterTypeErrors.jsx │ └── ApplicationMenu │ │ └── AdminMenuItem.js ├── documentation │ ├── context │ │ └── index.js │ ├── DocumentationRoutes.jsx │ ├── hooks │ │ └── index.js │ └── api │ │ └── index.js ├── translations │ ├── index.js │ ├── reducers │ │ └── index.js │ ├── sagas │ │ └── index.js │ └── api │ │ └── index.js ├── i18nTranslations │ ├── api │ │ └── index.js │ ├── TranslationSaga.js │ └── TranslationReducers.js ├── modelTest │ ├── OperationalModulesFactory.js │ └── SubjectTypeFactory.js ├── setupTests.cjs ├── assignment │ ├── util │ │ ├── util.js │ │ ├── DateFilterOptions.js │ │ └── FilterStyles.js │ ├── taskAssignment │ │ ├── FetchTasks.js │ │ └── TableColumns.js │ ├── components │ │ ├── TextFilter.jsx │ │ └── SelectFilter.jsx │ ├── api │ │ └── index.js │ └── subjectAssignment │ │ └── SubjectAssignmentData.js ├── tutorials │ ├── Tutorials.jsx │ └── Body.jsx ├── index.css ├── phoneNumberVerification │ └── api.js ├── index.jsx ├── custom-hooks │ └── useGetData.jsx ├── avni-models.js └── userGroups │ └── UserGroupDetails.jsx ├── cypress.json ├── public ├── favicon.ico ├── icons │ ├── group.png │ ├── person.png │ ├── household.png │ ├── individual.png │ └── ai-chat-icon.png ├── documentation │ └── sideBarDocumentation │ │ ├── Report.md │ │ ├── Card.md │ │ ├── Documentation.md │ │ ├── IdentifierUserAssignment.md │ │ ├── Relationship.md │ │ ├── NewsBroadcast.md │ │ ├── Bundle.md │ │ ├── Checklist.md │ │ ├── SearchFilter.md │ │ ├── Video.md │ │ ├── MyDashboardFilter.md │ │ ├── IdentifierSource.md │ │ ├── SearchResultFields.md │ │ ├── PhoneNumberVerification.md │ │ ├── WorklistUpdationRule.md │ │ ├── Translation.md │ │ ├── ApplicationMenu.md │ │ ├── RelationshipType.md │ │ ├── UserMessagingConfig.md │ │ ├── UserGroup.md │ │ ├── Dashboard.md │ │ ├── Prints.md │ │ ├── Language.md │ │ ├── Program.md │ │ ├── Concept.md │ │ ├── User.md │ │ ├── StorageManagementConfig.md │ │ ├── Catchment.md │ │ ├── Upload.md │ │ ├── Location.md │ │ ├── LocationType.md │ │ ├── View.md │ │ ├── EncounterType.md │ │ └── OrganisationDetail.md ├── bulkuploads │ └── sample │ │ ├── locations.csv │ │ └── usersAndCatchments.csv └── manifest.json ├── .env ├── .env.production ├── Dockerfile ├── .editorconfig ├── cypress ├── fixtures │ └── example.json ├── constants.js ├── support │ ├── commands.js │ └── index.js ├── pages │ └── loginPage.js └── plugins │ └── index.js ├── scripts └── token.js ├── .github ├── ISSUE_TEMPLATE │ └── story.md ├── add_to_project.yml └── workflows │ ├── add_to_project.yml │ └── issue_states.yml ├── .vscode └── settings.json ├── .gitignore ├── tsconfig.json └── index.html /.nvmrc: -------------------------------------------------------------------------------- 1 | v24.4.1 2 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | export default "mocked-file"; 2 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | PORT=6010 2 | VITE_REACT_APP_ENVIRONMENT=development 3 | -------------------------------------------------------------------------------- /src/dataEntryApp/views/subjectDashBoard/components/CompletedProgramEncounter.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avniproject/avni-webapp/HEAD/src/logo.png -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultCommandTimeout": 30000, 3 | "watchForFileChanges": false 4 | } 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avniproject/avni-webapp/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/login_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avniproject/avni-webapp/HEAD/src/login_image.png -------------------------------------------------------------------------------- /src/reports/index.js: -------------------------------------------------------------------------------- 1 | import Export from "./export/Export"; 2 | 3 | export default { Export }; 4 | -------------------------------------------------------------------------------- /public/icons/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avniproject/avni-webapp/HEAD/public/icons/group.png -------------------------------------------------------------------------------- /public/icons/person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avniproject/avni-webapp/HEAD/public/icons/person.png -------------------------------------------------------------------------------- /src/avni-background.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avniproject/avni-webapp/HEAD/src/avni-background.jpeg -------------------------------------------------------------------------------- /src/avni-logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avniproject/avni-webapp/HEAD/src/avni-logo-black.png -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_PATH=src/ 2 | SKIP_PREFLIGHT_CHECK=true 3 | PORT=6010 4 | VITE_REACT_APP_ENVIRONMENT=development 5 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Report.md: -------------------------------------------------------------------------------- 1 | Reports of different type can be generated from here. 2 | -------------------------------------------------------------------------------- /public/icons/household.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avniproject/avni-webapp/HEAD/public/icons/household.png -------------------------------------------------------------------------------- /public/icons/individual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avniproject/avni-webapp/HEAD/public/icons/individual.png -------------------------------------------------------------------------------- /src/formDesigner/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/upload/index.js: -------------------------------------------------------------------------------- 1 | import UploadDashboard from "./UploadDashboard"; 2 | 3 | export { UploadDashboard }; 4 | -------------------------------------------------------------------------------- /public/icons/ai-chat-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avniproject/avni-webapp/HEAD/public/icons/ai-chat-icon.png -------------------------------------------------------------------------------- /src/dataEntryApp/index.js: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import DataEntry from "./DataEntry"; 3 | 4 | export { DataEntry }; 5 | -------------------------------------------------------------------------------- /src/dataEntryApp/constants.js: -------------------------------------------------------------------------------- 1 | export const dateTimeFormat = "dd/MM/yyyy HH:mm"; 2 | export const dateFormat = "dd/MM/yyyy"; 3 | -------------------------------------------------------------------------------- /src/rootApp/index.js: -------------------------------------------------------------------------------- 1 | import App from "./App"; 2 | import SecureApp from "./SecureApp"; 3 | 4 | export { App, SecureApp }; 5 | -------------------------------------------------------------------------------- /src/news/model/ContactGroup.js: -------------------------------------------------------------------------------- 1 | class ContactGroup { 2 | id; 3 | label; 4 | description; 5 | } 6 | 7 | export default ContactGroup; 8 | -------------------------------------------------------------------------------- /src/formDesigner/styles/images/avniLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avniproject/avni-webapp/HEAD/src/formDesigner/styles/images/avniLogo.png -------------------------------------------------------------------------------- /src/formDesigner/styles/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avniproject/avni-webapp/HEAD/src/formDesigner/styles/images/background.jpg -------------------------------------------------------------------------------- /src/common/store/index.js: -------------------------------------------------------------------------------- 1 | import { store, adminHistory, appDesignerHistory } from "./createStore"; 2 | export { store, adminHistory, appDesignerHistory }; 3 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # For reference. Set following variables in 2 | # .env.production.local to avoid accidental commits: 3 | VITE_REACT_APP_ENVIRONMENT=production 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | COPY build/ /build 3 | RUN mkdir -p /opt/openchs/static/ 4 | CMD ["sh", "-c", "cp -r /build/* /opt/openchs/static/ && sleep infinity"] -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Card.md: -------------------------------------------------------------------------------- 1 | Card contains the actual query that gets executed. Right now query should return a list of individual object. 2 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Documentation.md: -------------------------------------------------------------------------------- 1 | You can create documentation for the app. Currently documentation can be attached to a form element. 2 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/IdentifierUserAssignment.md: -------------------------------------------------------------------------------- 1 | [Learn more about setting up identifiers](https://avni.readme.io/docs/creating-identifiers) 2 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Relationship.md: -------------------------------------------------------------------------------- 1 | If you plan to store relationships of individuals between each other, define these relationships here. 2 | -------------------------------------------------------------------------------- /src/adminApp/OrgManagerContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const OrgManagerContext = createContext(); 4 | export default OrgManagerContext; 5 | -------------------------------------------------------------------------------- /src/rootApp/security/BaseIdp.js: -------------------------------------------------------------------------------- 1 | class BaseIdp { 2 | constructor(idpDetails) { 3 | this.idpDetails = idpDetails; 4 | } 5 | } 6 | 7 | export default BaseIdp; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{js,json}] 4 | indent_style = space 5 | indent_size = 2 6 | max_line_length = 140 7 | 8 | [Makefile] 9 | indent_style = tab 10 | -------------------------------------------------------------------------------- /src/documentation/context/index.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const DocumentationContext = createContext(); 4 | 5 | export default DocumentationContext; 6 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/NewsBroadcast.md: -------------------------------------------------------------------------------- 1 | You can create and publish news from here. Once news is published it starts showing up to the field users after sync. 2 | -------------------------------------------------------------------------------- /src/dataEntryApp/sagas/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | 3 | //todo 4 | describe("please fill me in", () => { 5 | it("should have tests", function() {}); 6 | }); 7 | -------------------------------------------------------------------------------- /src/formDesigner/styles/images/Alert_Sub-Dashboard-Tab-Icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avniproject/avni-webapp/HEAD/src/formDesigner/styles/images/Alert_Sub-Dashboard-Tab-Icons.png -------------------------------------------------------------------------------- /src/news/whatsapp/ReceiverType.js: -------------------------------------------------------------------------------- 1 | const ReceiverType = Object.freeze({ 2 | Subject: "Subject", 3 | User: "User", 4 | Group: "Group" 5 | }); 6 | 7 | export default ReceiverType; 8 | -------------------------------------------------------------------------------- /src/translations/index.js: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import Translations from "./Translations"; 3 | import "bootstrap/dist/css/bootstrap.min.css"; 4 | 5 | export default Translations; 6 | -------------------------------------------------------------------------------- /src/translations/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import onLoad from "./onLoadReducer"; 3 | 4 | export default combineReducers({ 5 | onLoad 6 | }); 7 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import ScreenWithAppBar from "../../common/components/ScreenWithAppBar"; 2 | 3 | export default () => ; 4 | -------------------------------------------------------------------------------- /src/adminApp/domain/OrganisationCategory.js: -------------------------------------------------------------------------------- 1 | class OrganisationCategory { 2 | static Production = "Production"; 3 | static UAT = "UAT"; 4 | } 5 | 6 | export default OrganisationCategory; 7 | -------------------------------------------------------------------------------- /src/rootApp/LoginPage.css: -------------------------------------------------------------------------------- 1 | #img2 { 2 | float: right; 3 | } 4 | #img1 { 5 | margin-left: 20px; 6 | margin: 100px 55px; 7 | } 8 | 9 | .custom_li { 10 | list-style: none; 11 | } 12 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/Checkbox.jsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from "@mui/material"; 2 | 3 | export default props => ( 4 | 5 | ); 6 | -------------------------------------------------------------------------------- /src/translations/sagas/index.js: -------------------------------------------------------------------------------- 1 | import onLoadSaga from "./onLoadSaga"; 2 | import { all, fork } from "redux-saga/effects"; 3 | 4 | export default function* rootSaga() { 5 | yield all([onLoadSaga].map(fork)); 6 | } 7 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Bundle.md: -------------------------------------------------------------------------------- 1 | Configuration from an organisation can be exported and imported into another organisation. This helps migrating the implementation of a program from one organisation to another. 2 | -------------------------------------------------------------------------------- /__mocks__/react-hotkeys-hook.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useHotkeys: jest.fn(), 3 | useRecordHotkeys: jest.fn(() => []), 4 | isHotkeyPressed: jest.fn(() => false), 5 | parseHotkey: jest.fn(), 6 | parseKeys: jest.fn() 7 | }; 8 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Checklist.md: -------------------------------------------------------------------------------- 1 | Checklists are typically used to upload time-tables of things to do such as vaccination. 2 | 3 | [Learn about uploading a checklist](https://avni.readme.io/docs/upload-checklist) 4 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/SearchFilter.md: -------------------------------------------------------------------------------- 1 | Filters on the Search tab of the field app can be enhanced by adding filters here. 2 | 3 | [Look up more details](https://avni.readme.io/docs/my-dashboard-and-search-filters) 4 | -------------------------------------------------------------------------------- /public/bulkuploads/sample/locations.csv: -------------------------------------------------------------------------------- 1 | "PHC Area","Subcenter","Village","GPS coordinates" 2 | "PHC A","Sub A","Vil A", 3 | "PHC B","Sub B","Vil B", 4 | "PHC B","Sub B","Vil C", 5 | "PHC B","Sub C","Vil C", 6 | "PHC C","Sub C","Vil C", 7 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Video.md: -------------------------------------------------------------------------------- 1 | You can create video playlist from webApp. These videos will show up in field application. 2 | 3 | **Make sure that videos are already downloaded and exists in the path "OpenCHS/movies"** 4 | -------------------------------------------------------------------------------- /src/i18nTranslations/api/index.js: -------------------------------------------------------------------------------- 1 | import { httpClient } from "common/utils/httpClient"; 2 | export default { 3 | fetchOrganisationConfig: () => 4 | httpClient.fetchJson("/organisationConfig").then(response => response.json) 5 | }; 6 | -------------------------------------------------------------------------------- /src/formDesigner/components/DeclarativeRule/MiddleText.jsx: -------------------------------------------------------------------------------- 1 | const MiddleText = ({ text }) => { 2 | return ( 3 | {text} 4 | ); 5 | }; 6 | 7 | export default MiddleText; 8 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/MyDashboardFilter.md: -------------------------------------------------------------------------------- 1 | MyDashboard in Avni comes with some default filters. Additional filters can be added here. 2 | 3 | [Look up more details](https://avni.readme.io/docs/my-dashboard-and-search-filters) 4 | -------------------------------------------------------------------------------- /src/adminApp/components/Title.jsx: -------------------------------------------------------------------------------- 1 | export const Title = ({ record, title }) => { 2 | return ( 3 | record && ( 4 | 5 | {title}: {record.name ? record.name : record.title} 6 | 7 | ) 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/common/service/EntityService.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | class EntityService { 4 | static findByUuid(collection, uuid) { 5 | return _.find(collection, st => st.uuid === uuid); 6 | } 7 | } 8 | 9 | export default EntityService; 10 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/PrimaryButton.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@mui/material"; 2 | 3 | export default ({ children, ...props }) => ( 4 | 7 | ); 8 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/IdentifierSource.md: -------------------------------------------------------------------------------- 1 | Create autogenerated Ids by configuring Identifier Sources, and creating Identifier user assignments 2 | 3 | [Learn more about setting up identifiers](https://avni.readme.io/docs/creating-identifiers) 4 | -------------------------------------------------------------------------------- /src/rootApp/SecureApp.css: -------------------------------------------------------------------------------- 1 | .centerContainer { 2 | min-height: 100vh; 3 | /* display: flex; */ 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: center; 7 | font-size: calc(10px + 2vmin); 8 | background-color: white; 9 | } 10 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/SearchResultFields.md: -------------------------------------------------------------------------------- 1 | Custom search result fields can be setup for each subject type. User can choose concepts from the 2 | registration form. If no fields are setup for a subject type default fields will show up in the search result. 3 | -------------------------------------------------------------------------------- /src/common/model/OperationalModules.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | class OperationalModules { 4 | static isLoaded(operationalModules) { 5 | return !_.isNil(_.get(operationalModules, "subjectTypes")); 6 | } 7 | } 8 | 9 | export default OperationalModules; 10 | -------------------------------------------------------------------------------- /src/rootApp/security/UndecidedIdp.jsx: -------------------------------------------------------------------------------- 1 | import BaseIdp from "./BaseIdp"; 2 | import IdpDetails from "./IdpDetails"; 3 | 4 | class UndecidedIdp extends BaseIdp { 5 | get idpType() { 6 | return IdpDetails.both; 7 | } 8 | } 9 | 10 | export default UndecidedIdp; 11 | -------------------------------------------------------------------------------- /src/rootApp/security/StubbedLocalStorage.ts: -------------------------------------------------------------------------------- 1 | class StubbedLocalStorage { 2 | constructor(itemMap: Map) { 3 | this.itemMap = itemMap; 4 | } 5 | 6 | getItem(item) { 7 | return this.itemMap.get(item); 8 | } 9 | } 10 | 11 | export default StubbedLocalStorage; 12 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/PhoneNumberVerification.md: -------------------------------------------------------------------------------- 1 | This configuration is used to integrate with Msg91 to perform verification of phone numbers by sending an OTP. 2 | 3 | [Learn more about Phone Number Verification](https://avni.readme.io/docs/phone-number-verification) 4 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/WorklistUpdationRule.md: -------------------------------------------------------------------------------- 1 | Worklist updation rules help stitch together different workflows, such as registration, enrolment and completion of various forms. 2 | 3 | [Learn more about writing rules](https://avni.readme.io/docs/rules-concept-guide) 4 | -------------------------------------------------------------------------------- /src/common/store/getters.js: -------------------------------------------------------------------------------- 1 | // Use to write functions to query store 2 | // outside of React components 3 | 4 | /* 5 | import { store } from '.'; 6 | 7 | export const getUser = () => { 8 | const { app: { user } } = store.getState(); 9 | return user; 10 | }; 11 | */ 12 | -------------------------------------------------------------------------------- /src/rootApp/security/NoAuthSession.js: -------------------------------------------------------------------------------- 1 | class NoAuthSession { 2 | userInfoUpdate(roles, updatedUsername, name) { 3 | this.roles = roles; 4 | this.username = this.username || updatedUsername; 5 | this.name = name; 6 | } 7 | } 8 | 9 | export default NoAuthSession; 10 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Translation.md: -------------------------------------------------------------------------------- 1 | After adding [languages](#/admin/language) you will be able to download/upload the translation file for all the different languages. 2 | 3 | Learn more about [translation management](https://avni.readme.io/docs/translation-management). 4 | -------------------------------------------------------------------------------- /src/common/utils/StringUtil.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | class StringUtil { 4 | static substring(str, length) { 5 | if (_.isEmpty(str)) return str; 6 | return str.length > length ? str.substring(0, length) : str; 7 | } 8 | } 9 | 10 | export default StringUtil; 11 | -------------------------------------------------------------------------------- /src/common/utils/keyValues.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | const valueByKey = (model, key) => { 4 | const valueObject = _.find(model.keyValues, keyValue => keyValue[key] === key); 5 | return _.get(valueObject, "value"); 6 | }; 7 | 8 | export default { 9 | valueByKey 10 | }; 11 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/ApplicationMenu.md: -------------------------------------------------------------------------------- 1 | The customizable "Application menu" feature helps you add a new menu item that will show up on the "More" option of the Android app. 2 | 3 | [Learn about creating an entry in Application Menu.](https://avni.readme.io/docs/application-menu) 4 | -------------------------------------------------------------------------------- /src/rootApp/security/KeycloakAuthSession.js: -------------------------------------------------------------------------------- 1 | import BaseAuthSession from "./BaseAuthSession"; 2 | 3 | class KeycloakAuthSession extends BaseAuthSession { 4 | constructor(authState) { 5 | super(); 6 | this.authState = authState; 7 | } 8 | } 9 | 10 | export default KeycloakAuthSession; 11 | -------------------------------------------------------------------------------- /src/modelTest/OperationalModulesFactory.js: -------------------------------------------------------------------------------- 1 | class OperationalModulesFactory { 2 | static create({ subjectTypes = [] }) { 3 | return { 4 | subjectTypes: subjectTypes, 5 | programs: [], 6 | encounterTypes: [] 7 | }; 8 | } 9 | } 10 | 11 | export default OperationalModulesFactory; 12 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/RelationshipType.md: -------------------------------------------------------------------------------- 1 | Relationship Types define the different kind of relationships, and their reverse relationships. For example, the reverse relationship of a relationship "Son" could be "Mother" or "Father" (or "Guardian", or "Parent" depending on how you model relationships). 2 | -------------------------------------------------------------------------------- /scripts/token.js: -------------------------------------------------------------------------------- 1 | const tokenFn = require("openchs-idi/token"); 2 | 3 | const [serverUrl, user, password] = process.argv.slice(2); 4 | 5 | tokenFn({ serverUrl, user, password }) 6 | .then(token => { 7 | console.log("", token); 8 | }) 9 | .catch(error => { 10 | console.log(error, "+++"); 11 | }); 12 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/UserMessagingConfig.md: -------------------------------------------------------------------------------- 1 | Newly created users will receive a message on their "WhatsApp application registered with their mobile number", based on the configuration here. 2 | [Refer this link to know more about how to configure.](https://avni.readme.io/docs/writing-rules#13-message-rule) 3 | -------------------------------------------------------------------------------- /src/dataEntryApp/views/viewmodel/StaticFormElement.js: -------------------------------------------------------------------------------- 1 | class StaticFormElement { 2 | constructor(name, mandatory, editable) { 3 | this.name = name; 4 | this.mandatory = mandatory; 5 | this.editable = editable; 6 | this.staticElement = true; 7 | } 8 | } 9 | 10 | export default StaticFormElement; 11 | -------------------------------------------------------------------------------- /src/adminApp/README.md: -------------------------------------------------------------------------------- 1 | `orgManager` will encapsulate functionality related to managing 2 | an organisation's data by the organisation administrator. 3 | * Users 4 | * Catchments 5 | * Operational Programs (?) 6 | * Operational Encounter Types (?) etc. 7 | 8 | --- 9 | Plus organisation-wide metadata: 10 | * Concept Answers 11 | -------------------------------------------------------------------------------- /src/common/AvniRouter.jsx: -------------------------------------------------------------------------------- 1 | import CurrentUserService from "./service/CurrentUserService"; 2 | 3 | class AvniRouter { 4 | static getRedirectRouteFromRoot(userInfo) { 5 | return CurrentUserService.isAdminButNotImpersonating(userInfo) 6 | ? "/admin" 7 | : "/home"; 8 | } 9 | } 10 | 11 | export default AvniRouter; 12 | -------------------------------------------------------------------------------- /src/translations/api/index.js: -------------------------------------------------------------------------------- 1 | import { httpClient as http } from "../../common/utils/httpClient"; 2 | 3 | export default { 4 | fetchOrgConfig: () => http.fetchJson("/organisationConfig").then(response => response.json), 5 | fetchDashboardData: params => http.fetchJson(http.withParams("/translation", params)).then(res => res.json) 6 | }; 7 | -------------------------------------------------------------------------------- /src/dataEntryApp/Colors.js: -------------------------------------------------------------------------------- 1 | class Colors { 2 | static ValidationError = "#d0011b"; 3 | static DefaultPrimary = "rgba(0, 0, 0, 1)"; 4 | static DefaultDisabled = "#afafaf"; 5 | static HighlightBackgroundColor = "#F8F9F9"; 6 | static WhiteColor = "#FFF"; 7 | static SuccessColor = "#009688"; 8 | } 9 | 10 | export default Colors; 11 | -------------------------------------------------------------------------------- /src/common/utils/routeUtil.js: -------------------------------------------------------------------------------- 1 | export function getHref(resourcePath, basePath = "") { 2 | return `#${basePath}/${resourcePath}`; 3 | } 4 | 5 | export function getLinkTo(resourcePath) { 6 | return `/${resourcePath}`; 7 | } 8 | 9 | export function getRoutePath(basePath, resourcePath) { 10 | return `${basePath}/${resourcePath}`; 11 | } 12 | -------------------------------------------------------------------------------- /src/formDesigner/components/BorderBox.jsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@mui/material"; 2 | 3 | export default ({ children }) => { 4 | return ( 5 | 13 | {children} 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/story.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Story 3 | about: Template for webapp stories 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ### Motivation 10 | 11 | ### Acceptance Criteria 12 | 13 | ### Visual Design 14 | 15 | ### Tech Approach 16 | 17 | ### Test Scenarios 18 | 19 | - [ ] Including all required translations 20 | -------------------------------------------------------------------------------- /src/common/utils/LocalStorageLocator.js: -------------------------------------------------------------------------------- 1 | class LocalStorageLocator { 2 | constructor() { 3 | this.localStorage = localStorage; 4 | } 5 | 6 | setLocalStorage(x) { 7 | this.localStorage = x; 8 | } 9 | 10 | getLocalStorage() { 11 | return this.localStorage; 12 | } 13 | } 14 | 15 | export default new LocalStorageLocator(); 16 | -------------------------------------------------------------------------------- /src/common/utils/ReactSelectHelper.js: -------------------------------------------------------------------------------- 1 | class ReactSelectHelper { 2 | label; 3 | value; 4 | 5 | static toReactSelectItem(x) { 6 | return { label: x.name, value: x }; 7 | } 8 | 9 | static getCurrentValues(reactSelectItems) { 10 | return reactSelectItems.map(x => x.value); 11 | } 12 | } 13 | 14 | export default ReactSelectHelper; 15 | -------------------------------------------------------------------------------- /src/dataEntryApp/test/TestKeyValueFactory.js: -------------------------------------------------------------------------------- 1 | import { KeyValue } from "openchs-models"; 2 | 3 | class TestKeyValueFactory { 4 | static create({ key, value }) { 5 | const keyValue = new KeyValue(); 6 | keyValue.key = key; 7 | keyValue.value = value; 8 | return keyValue; 9 | } 10 | } 11 | 12 | export default TestKeyValueFactory; 13 | -------------------------------------------------------------------------------- /src/news/components/ActionButton.js: -------------------------------------------------------------------------------- 1 | import { Button } from "@mui/material"; 2 | import { styled } from "@mui/material/styles"; 3 | 4 | export const ActionButton = styled(Button)(({ theme }) => ({ 5 | color: theme.palette.getContrastText("#008b8a"), 6 | backgroundColor: "#008b8a", 7 | "&:hover": { 8 | backgroundColor: "#008b8a" 9 | } 10 | })); -------------------------------------------------------------------------------- /src/formDesigner/common/ColorValue.jsx: -------------------------------------------------------------------------------- 1 | const ColorValue = ({ colour }) => { 2 | return ( 3 |
11 |   12 |
13 | ); 14 | }; 15 | 16 | export default ColorValue; 17 | -------------------------------------------------------------------------------- /src/formDesigner/common/ShowLabelValue.jsx: -------------------------------------------------------------------------------- 1 | import { FormLabel } from "@mui/material"; 2 | 3 | export const ShowLabelValue = ({ label, value }) => { 4 | return ( 5 |
6 | {label} 7 |
8 | {value} 9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/modelTest/SubjectTypeFactory.js: -------------------------------------------------------------------------------- 1 | import { SubjectType } from "openchs-models"; 2 | 3 | class SubjectTypeFactory { 4 | static create({ uuid, name }) { 5 | const subjectType = new SubjectType(); 6 | subjectType.uuid = uuid; 7 | subjectType.name = name; 8 | return subjectType; 9 | } 10 | } 11 | 12 | export default SubjectTypeFactory; 13 | -------------------------------------------------------------------------------- /src/rootApp/security/BaseAuthSession.js: -------------------------------------------------------------------------------- 1 | class BaseAuthSession { 2 | static AuthStates = { 3 | SignedIn: "signedIn" 4 | }; 5 | 6 | userInfoUpdate(roles, updatedUsername, name) { 7 | this.roles = roles; 8 | this.username = this.username || updatedUsername; 9 | this.name = name; 10 | } 11 | } 12 | 13 | export default BaseAuthSession; 14 | -------------------------------------------------------------------------------- /src/setupTests.cjs: -------------------------------------------------------------------------------- 1 | const { TextEncoder, TextDecoder } = require('util'); 2 | 3 | if (typeof global.TextEncoder === 'undefined') { 4 | global.TextEncoder = TextEncoder; 5 | global.TextDecoder = TextDecoder; 6 | } 7 | 8 | if (typeof global.fetch === 'undefined') { 9 | // eslint-disable-next-line no-undef 10 | global.fetch = jest.fn(); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/common/utils/files.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | static download(filename, content) { 3 | const anchorTag = document.createElement("a"); 4 | anchorTag.href = window.URL.createObjectURL(new Blob([content])); 5 | anchorTag.setAttribute("download", filename); 6 | document.body.appendChild(anchorTag); 7 | anchorTag.click(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/documentation/DocumentationRoutes.jsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from "react-router-dom"; 2 | import DocumentationList from "./DocumentationList"; 3 | 4 | function DocumentationRoutes() { 5 | return ( 6 | 7 | } /> 8 | 9 | ); 10 | } 11 | 12 | export default DocumentationRoutes; 13 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/UserGroup.md: -------------------------------------------------------------------------------- 1 | User Groups can be created to finely control user access to functions in the field and data entry apps. 2 | 3 | By default, no configuration is required here as there is already an Everyone group that has all privileges. 4 | 5 | [Learn more about Access Control and User Groups](https://avni.readme.io/docs/access-control) 6 | -------------------------------------------------------------------------------- /src/common/utils/MuiComponentHelper.js: -------------------------------------------------------------------------------- 1 | class MuiComponentHelper { 2 | static getDialogClosingHandler(handleClose) { 3 | return (event, reason) => { 4 | if (reason !== "backdropClick" && reason !== "escapeKeyDown" && handleClose) { 5 | handleClose(event, reason); 6 | } 7 | }; 8 | } 9 | } 10 | 11 | export default MuiComponentHelper; 12 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/Checkbox.stories.jsx: -------------------------------------------------------------------------------- 1 | import Checkbox from "./Checkbox"; 2 | 3 | export default { 4 | title: "DEA/Components/Checkbox", 5 | component: Checkbox 6 | }; 7 | 8 | const Template = args => ; 9 | 10 | export const Primary = Template.bind({}); 11 | Primary.args = { 12 | checked: true, 13 | color: "primary" 14 | }; 15 | -------------------------------------------------------------------------------- /src/reports/Common.js: -------------------------------------------------------------------------------- 1 | import { PlayForWork, Explore } from "@mui/icons-material"; 2 | 3 | export const reportSideBarOptions = [ 4 | { href: "#/export", name: "Longitudinal Export", Icon: PlayForWork }, 5 | { href: "#/newExport", name: "New Longitudinal Export", Icon: PlayForWork }, 6 | { href: "#/selfservicereports", name: "Self-service Reports", Icon: Explore } 7 | ]; 8 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Avni", 3 | "name": "Avni Web Console", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/adminApp/components/TitleChip.jsx: -------------------------------------------------------------------------------- 1 | import { Chip } from "@mui/material"; 2 | import { useRecordContext } from "react-admin"; 3 | 4 | export const TitleChip = ({ source = "name" }) => { 5 | const record = useRecordContext(); 6 | 7 | if (!record) return <>; 8 | 9 | const value = record[source]; 10 | return value ? : <>; 11 | }; 12 | -------------------------------------------------------------------------------- /cypress/constants.js: -------------------------------------------------------------------------------- 1 | export const isDevEnv = false; //process.env.OPENCHS_MODE !== "live"; 2 | export const DEV_BASE_URL = "http://localhost:6010/"; 3 | export const CI_BASE_URL = "https://staging.avniproject.org/"; 4 | export const URL = isDevEnv ? DEV_BASE_URL : CI_BASE_URL; 5 | export const formWizardOrgUsername = "admin@form_scenarios"; 6 | export const formWizardOrgPassword = "password"; 7 | -------------------------------------------------------------------------------- /src/adminApp/components/AvniSelectInput.jsx: -------------------------------------------------------------------------------- 1 | import { SelectInput } from "react-admin"; 2 | import { ToolTipContainer } from "../../common/components/ToolTipContainer"; 3 | 4 | export const AvniSelectInput = ({ toolTipKey, ...props }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/common/components/AvniSelectForm.jsx: -------------------------------------------------------------------------------- 1 | import SelectForm from "../../adminApp/SubjectType/SelectForm"; 2 | 3 | import { ToolTipContainer } from "./ToolTipContainer"; 4 | 5 | export const AvniSelectForm = ({ toolTipKey, ...props }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/common/components/BooleanStatusInShow.jsx: -------------------------------------------------------------------------------- 1 | import { FormLabel } from "@mui/material"; 2 | 3 | export const BooleanStatusInShow = ({ status, label }) => { 4 | return ( 5 | <> 6 |
7 | {label} 8 |
9 | {status ? "Yes" : "No"} 10 |
11 |

12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/DeleteButton.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@mui/material"; 2 | 3 | import { useTranslation } from "react-i18next"; 4 | 5 | export const DeleteButton = ({ onDelete }) => { 6 | const { t } = useTranslation(); 7 | return ( 8 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /src/news/News.jsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from "react-router-dom"; 2 | import NewsList from "./NewsList"; 3 | import NewsDetails from "./NewsDetails"; 4 | 5 | function News() { 6 | return ( 7 | 8 | } /> 9 | } /> 10 | 11 | ); 12 | } 13 | 14 | export default News; 15 | -------------------------------------------------------------------------------- /src/adminApp/components/AvniReferenceInput.jsx: -------------------------------------------------------------------------------- 1 | import { ReferenceInput } from "react-admin"; 2 | import { ToolTipContainer } from "../../common/components/ToolTipContainer"; 3 | 4 | export const AvniReferenceInput = ({ toolTipKey, ...props }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/common/components/TextFieldInShow.jsx: -------------------------------------------------------------------------------- 1 | import { FormLabel } from "@mui/material"; 2 | 3 | export const TextFieldInShow = ({ text, label }) => { 4 | return ( 5 | <> 6 |

7 | {label} 8 |
9 | {text} 10 |
11 |

12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/documentation/hooks/index.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import DocumentationContext from "../context"; 3 | 4 | export const useDocumentationDispatch = () => { 5 | const { dispatch } = useContext(DocumentationContext); 6 | return dispatch; 7 | }; 8 | 9 | export const getDocumentationState = () => { 10 | const { state } = useContext(DocumentationContext); 11 | return state; 12 | }; 13 | -------------------------------------------------------------------------------- /src/rootApp/security/CognitoAuthSession.js: -------------------------------------------------------------------------------- 1 | import BaseAuthSession from "./BaseAuthSession"; 2 | 3 | class CognitoAuthSession extends BaseAuthSession { 4 | constructor(authState, authData) { 5 | super(); 6 | this.authState = authState; 7 | this.username = authData.username; 8 | } 9 | 10 | roles; 11 | name; 12 | authState; 13 | username; 14 | } 15 | 16 | export default CognitoAuthSession; 17 | -------------------------------------------------------------------------------- /src/adminApp/components/AvniTextInput.jsx: -------------------------------------------------------------------------------- 1 | import { ToolTipContainer } from "../../common/components/ToolTipContainer"; 2 | import { StyledTextInput } from "../Util/Styles"; 3 | 4 | export const AvniTextInput = ({ toolTipKey, ...props }) => { 5 | return ( 6 | 7 | 8 | {props.children} 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/common/model/reports/WebReportCardFactory.js: -------------------------------------------------------------------------------- 1 | import WebReportCard from "../WebReportCard"; 2 | 3 | class WebReportCardFactory { 4 | static create({ id, uuid, name }) { 5 | const webReportCard = new WebReportCard(); 6 | webReportCard.id = id; 7 | webReportCard.uuid = uuid; 8 | webReportCard.name = name; 9 | return webReportCard; 10 | } 11 | } 12 | 13 | export default WebReportCardFactory; 14 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | import { CI_BASE_URL, DEV_BASE_URL, isDevEnv } from "../constants"; 2 | 3 | Cypress.Commands.add("login", (username, password) => { 4 | const url = isDevEnv ? `${DEV_BASE_URL}#/home` : CI_BASE_URL; 5 | cy.visit(url); 6 | if (!isDevEnv) { 7 | cy.get("#username").type(username); 8 | cy.get("#password").type(password); 9 | cy.get(".MuiButtonBase-root").click(); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /src/dataEntryApp/views/dashboardNew/content.jsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@mui/material/styles"; 2 | 3 | const StyledContainer = styled("div")({ 4 | width: "100%", 5 | display: "flex", 6 | backgroundColor: "white", 7 | color: "black", 8 | height: "750px" 9 | }); 10 | 11 | export default function Header() { 12 | return ( 13 | 14 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/news/components/DialogActionButton.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@mui/material"; 2 | 3 | export const DialogActionButton = ({ 4 | onClick, 5 | color, 6 | text, 7 | textColor, 8 | ...props 9 | }) => { 10 | return ( 11 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/adminApp/react-admin-config/index.js: -------------------------------------------------------------------------------- 1 | import { authProvider } from "./authProvider"; 2 | import dataProviderFactory from "./dataProvider"; 3 | import LogoutButton from "./LogoutButton"; 4 | 5 | // Create the actual dataProvider by calling the factory with empty string 6 | // since the application uses relative URLs 7 | const dataProvider = dataProviderFactory(""); 8 | 9 | export { authProvider, dataProvider, LogoutButton }; 10 | -------------------------------------------------------------------------------- /src/common/components/TextFormatFieldInShow.jsx: -------------------------------------------------------------------------------- 1 | import { TextFieldInShow } from "./TextFieldInShow"; 2 | 3 | export const TextFormatFieldInShow = ({ format, label }) => { 4 | return ( 5 | <> 6 | 7 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/common/model/WebForm.js: -------------------------------------------------------------------------------- 1 | import { Form } from "openchs-models"; 2 | import WebFormElementGroup from "./WebFormElementGroup"; 3 | 4 | class WebForm extends Form { 5 | get formElementGroups() { 6 | return this.toEntityList("formElementGroups", WebFormElementGroup); 7 | } 8 | 9 | set formElementGroups(x) { 10 | this.that.formElementGroups = this.fromEntityList(x); 11 | } 12 | } 13 | 14 | export default WebForm; 15 | -------------------------------------------------------------------------------- /src/common/components/chatbot/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Chatbot } from "./Chatbot"; 2 | export { default as MessageInput } from "./MessageInput"; 3 | export { default as WelcomeScreen } from "./WelcomeScreen"; 4 | export { default as ChatMessage } from "./ChatMessage"; 5 | export { default as SuggestionCards } from "./SuggestionCards"; 6 | export { useChatbot } from "./useChatbot"; 7 | export * from "./types"; 8 | export * from "./constants"; 9 | -------------------------------------------------------------------------------- /src/common/stories/JSEditor.stories.jsx: -------------------------------------------------------------------------------- 1 | import { JSEditor } from "../components/JSEditor"; 2 | 3 | export default { 4 | component: JSEditor, 5 | title: "common/components/JSEditor" 6 | }; 7 | 8 | export const EmptyCode = args => ; 9 | EmptyCode.args = {}; 10 | 11 | export const WithContent = args => ; 12 | WithContent.args = { 13 | value: '() => {console.log("Hello World")}' 14 | }; 15 | -------------------------------------------------------------------------------- /src/rootApp/security/NullIdp.jsx: -------------------------------------------------------------------------------- 1 | import { devEnvUserName } from "../../common/constants"; 2 | import BaseIdp from "./BaseIdp"; 3 | import IdpDetails from "./IdpDetails"; 4 | 5 | class NullIdp extends BaseIdp { 6 | updateRequestWithSession() {} 7 | 8 | getToken() { 9 | return `user-name=${devEnvUserName}`; 10 | } 11 | 12 | get idpType() { 13 | return IdpDetails.none; 14 | } 15 | } 16 | 17 | export default NullIdp; 18 | -------------------------------------------------------------------------------- /src/assignment/util/util.js: -------------------------------------------------------------------------------- 1 | export const labelValue = (label, value = label) => ({ label, value }); 2 | export const labelValueObject = (label, value, object) => ({ label, value, object }); 3 | 4 | export const refreshTable = ref => { 5 | if (ref.current && typeof ref.current.refresh === "function") { 6 | ref.current.refresh(); 7 | } else { 8 | console.warn("refreshTable: refresh method not found on tableRef"); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/common/components/Body.jsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@mui/material/styles"; 2 | import { Paper } from "@mui/material"; 3 | 4 | const StyledPaper = styled(Paper)(({ theme }) => ({ 5 | padding: theme.spacing(2), 6 | marginRight: theme.spacing(2), 7 | boxSizing: "border-box" 8 | })); 9 | 10 | const Body = ({ ...props }) => { 11 | return {props.children}; 12 | }; 13 | 14 | export default Body; 15 | -------------------------------------------------------------------------------- /src/common/model/WebProgram.js: -------------------------------------------------------------------------------- 1 | import { Program } from "openchs-models"; 2 | 3 | class WebProgram { 4 | static fromResource(resource) { 5 | const program = new Program(); 6 | program.uuid = resource.uuid; 7 | program.name = resource.name; 8 | return program; 9 | } 10 | 11 | static fromResources(resources) { 12 | return resources.map(WebProgram.fromResource); 13 | } 14 | } 15 | 16 | export default WebProgram; 17 | -------------------------------------------------------------------------------- /src/common/components/UserError.jsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@mui/material"; 2 | 3 | import _ from "lodash"; 4 | 5 | export default function({ error }) { 6 | if (_.isNil(error)) return null; 7 | 8 | return ( 9 | error && ( 10 | theme.palette.error.main, mb: 2.5, ml: 2.5 }} 13 | > 14 | {error} 15 | 16 | ) 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Dashboard.md: -------------------------------------------------------------------------------- 1 | `Dashboard` created here will be shown in the field app. 2 | You can add multiple `Sections` to a dashboard. 3 | `Sections` will be shown in the same order as added here from the app-designer. 4 | `Sections` can have multiple `Cards`, either in `Tile` or `List` format. 5 | Within a `Section`, `Cards` will be shown in the same order as added here. 6 | 7 | Collapse all the `Sections` for changing their order. 8 | -------------------------------------------------------------------------------- /cypress/pages/loginPage.js: -------------------------------------------------------------------------------- 1 | import { CI_BASE_URL, DEV_BASE_URL, isDevEnv } from "cypress/integration/constants"; 2 | 3 | export default { 4 | clickNext(username, password) { 5 | const url = isDevEnv ? `${DEV_BASE_URL}#/home` : CI_BASE_URL; 6 | cy.visit(url); 7 | if (!isDevEnv) { 8 | cy.get("#username").type(username); 9 | cy.get("#password").type(password); 10 | cy.get(".MuiButtonBase-root").click(); 11 | } 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/formDesigner/components/ValidationError.jsx: -------------------------------------------------------------------------------- 1 | import { FormLabel } from "@mui/material"; 2 | 3 | import _ from "lodash"; 4 | 5 | export const ValidationError = ({ validationError }) => { 6 | return !_.isEmpty(validationError) ? ( 7 | 14 | {validationError.message} 15 | 16 | ) : null; 17 | }; 18 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Prints.md: -------------------------------------------------------------------------------- 1 | Custom web pages for the prints can be uploaded from here. All the required static files must be zipped and uploaded here. Below information is required while uploading the zip file. 2 | 3 | `Label: Text that will appear on the subject dashboard` 4 | 5 | `File Name: HTML file name that will be served when above label button is pressed` 6 | 7 | Multiple `Label/FileName` can be added by clicking on `Add more extensions` 8 | -------------------------------------------------------------------------------- /src/formDesigner/components/MessageRule/MessageReducer.js: -------------------------------------------------------------------------------- 1 | export const MessageReducer = (state, action) => { 2 | switch (action.type) { 3 | case "setRules": 4 | return { ...state, rules: action.payload }; 5 | case "setTemplates": 6 | return { ...state, templates: action.payload }; 7 | case "setTemplateFetchError": 8 | return { ...state, templateFetchError: action.error }; 9 | default: 10 | return state; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/formDesigner/views/FormDesignerContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | 3 | const FormDesignerContext = createContext(); 4 | 5 | export default FormDesignerContext; 6 | 7 | export const useSetFormState = () => { 8 | const { setState } = useContext(FormDesignerContext); 9 | return setState; 10 | }; 11 | 12 | export const getFormState = () => { 13 | const { state } = useContext(FormDesignerContext); 14 | return state; 15 | }; 16 | -------------------------------------------------------------------------------- /src/common/components/CustomUserMenu.jsx: -------------------------------------------------------------------------------- 1 | import { UserMenu } from "react-admin"; 2 | import LogoutButton from "../../adminApp/react-admin-config/LogoutButton"; 3 | 4 | const CustomUserMenu = ({ username, lastSessionTimeMillis }) => ( 5 | 6 | {}} 9 | lastSessionTimeMillis={lastSessionTimeMillis} 10 | /> 11 | 12 | ); 13 | 14 | export default CustomUserMenu; 15 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/Radio.jsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from "@mui/material"; 2 | import { RadioButtonChecked, RadioButtonUnchecked } from "@mui/icons-material"; 3 | 4 | export default props => ( 5 | } 8 | checkedIcon={ 9 | 13 | } 14 | /> 15 | ); 16 | -------------------------------------------------------------------------------- /.github/add_to_project.yml: -------------------------------------------------------------------------------- 1 | name: Add to Product board 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add all issues created in this repository to the Avni product board 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v0.5.0 14 | with: 15 | project-url: https://github.com/orgs/avniproject/projects/2 16 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 17 | -------------------------------------------------------------------------------- /src/common/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@mui/material"; 2 | import { styled } from "@mui/material/styles"; 3 | 4 | const StyledFooter = styled(Box)(({ theme }) => ({ 5 | backgroundColor: theme.palette.grey[50], 6 | borderTop: `1px solid ${theme.palette.grey[300]}`, 7 | marginTop: theme.spacing(4), 8 | width: "100%", 9 | boxSizing: "border-box" 10 | })); 11 | 12 | const Footer = () => { 13 | return ; 14 | }; 15 | 16 | export default Footer; 17 | -------------------------------------------------------------------------------- /src/common/mapper/BaseEncounterMapper.js: -------------------------------------------------------------------------------- 1 | export function mapBasicEncounter(encounter, planEncounter) { 2 | encounter.encounterType = planEncounter.encounterType; 3 | encounter.encounterDateTime = planEncounter.encounterDateTime; 4 | encounter.earliestVisitDateTime = planEncounter.earliestVisitDateTime; 5 | encounter.maxVisitDateTime = planEncounter.maxVisitDateTime; 6 | encounter.name = planEncounter.name; 7 | encounter.uuid = planEncounter.uuid; 8 | return encounter; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/model/SubjectTypeFactory.js: -------------------------------------------------------------------------------- 1 | import { SubjectType } from "openchs-models"; 2 | 3 | class SubjectTypeFactory { 4 | static create({ uuid, name, type, allowEmptyLocation = false }) { 5 | const subjectType = new SubjectType(); 6 | subjectType.uuid = uuid; 7 | subjectType.name = name; 8 | subjectType.type = type; 9 | subjectType.allowEmptyLocation = allowEmptyLocation; 10 | return subjectType; 11 | } 12 | } 13 | 14 | export default SubjectTypeFactory; 15 | -------------------------------------------------------------------------------- /src/i18nTranslations/TranslationSaga.js: -------------------------------------------------------------------------------- 1 | import { call, put, takeLatest } from "redux-saga/effects"; 2 | import { types, setOrgConfigInfo } from "./TranslationReducers"; 3 | import api from "./api"; 4 | 5 | export function* organisationConfigWatcher() { 6 | yield takeLatest(types.GET_ORG_CONFIG, setOrganisationConfig); 7 | } 8 | 9 | function* setOrganisationConfig() { 10 | const orgConfig = yield call(api.fetchOrganisationConfig); 11 | yield put(setOrgConfigInfo(orgConfig)); 12 | } 13 | -------------------------------------------------------------------------------- /src/tutorials/Tutorials.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | import AppBar from "../common/components/AppBar"; 3 | import Resources from "./Resources"; 4 | 5 | const Tutorials = () => { 6 | return ( 7 | 8 | 9 |

10 | 11 |
12 | 13 | ); 14 | }; 15 | 16 | export default Tutorials; 17 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Language.md: -------------------------------------------------------------------------------- 1 | Choose all the languages you need the app to be available in. 2 | 3 | Users can choose the language of their choice through their settings. You should remember to provide translations for your forms in these languages through the [translations](/#/translations) section. 4 | 5 | When creating users, you can also provide the default language for each user 6 | 7 | [Learn more about translation management](https://avni.readme.io/docs/translation-management) 8 | -------------------------------------------------------------------------------- /src/common/model/WebEncounterType.js: -------------------------------------------------------------------------------- 1 | import { EncounterType } from "openchs-models"; 2 | 3 | class WebEncounterType { 4 | static fromResource(resource) { 5 | const encounterType = new EncounterType(); 6 | encounterType.uuid = resource.uuid; 7 | encounterType.name = resource.name; 8 | return encounterType; 9 | } 10 | 11 | static fromResources(resources) { 12 | return resources.map(WebEncounterType.fromResource); 13 | } 14 | } 15 | 16 | export default WebEncounterType; 17 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Program.md: -------------------------------------------------------------------------------- 1 | A program defines the service, or intervention that you provide to subjects. 2 | 3 | A subject is enrolled into the program using the Enrolment Form. Routine information is collected through Encounters. A subject exits a program by filling in the Exit Form. 4 | 5 | - [Learn more about Avni's domain model](https://avni.readme.io/docs/avnis-domain-model-of-field-based-work) 6 | - [Learn more about writing rules](https://avni.readme.io/docs/rules-concept-guide) 7 | -------------------------------------------------------------------------------- /src/common/utils/index.js: -------------------------------------------------------------------------------- 1 | import { localeChoices } from "../constants"; 2 | import { filter, sortBy } from "lodash"; 3 | 4 | export const getLocales = organisationConfig => { 5 | if (!organisationConfig) return []; 6 | 7 | const settings = organisationConfig.getSettings ? organisationConfig.getSettings() : organisationConfig.settings; 8 | 9 | if (!settings || !settings.languages) return []; 10 | 11 | return sortBy(filter(localeChoices, l => settings.languages.includes(l.id)), "name"); 12 | }; 13 | -------------------------------------------------------------------------------- /src/formDesigner/api/ChecklistDetailsApi.js: -------------------------------------------------------------------------------- 1 | import { httpClient as http } from "common/utils/httpClient"; 2 | import { get } from "lodash"; 3 | 4 | export default { 5 | saveChecklistDetails: checklistDetails => 6 | http 7 | .postJson("/checklistDetail", checklistDetails) 8 | .then(r => [r.status, null]) 9 | .catch(r => [null, `${get(r, "response.data") || get(r, "message") || "unknown error"}`]), 10 | 11 | getChecklistDetails: () => http.fetchJson("/web/checklistDetails") 12 | }; 13 | -------------------------------------------------------------------------------- /src/dataEntryApp/views/registration/FormWizardButton.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@mui/material"; 2 | 3 | const FormWizardButton = ({ 4 | text, 5 | className, 6 | to, 7 | params, 8 | onClick, 9 | disabled, 10 | id 11 | }) => { 12 | return ( 13 | 22 | ); 23 | }; 24 | 25 | export default FormWizardButton; 26 | -------------------------------------------------------------------------------- /src/common/service/index.js: -------------------------------------------------------------------------------- 1 | import { httpClient } from "../utils/httpClient"; 2 | 3 | export default { 4 | fetchOperationalModules: () => httpClient.fetchJson("/web/operationalModules").then(response => response.json), 5 | fetchGenders: () => httpClient.fetchJson("/web/gender").then(response => response.json), 6 | fetchOrganisationConfigs: () => httpClient.fetchJson("/web/organisationConfig").then(response => response.json), 7 | fetchSubjectTypes: () => httpClient.get("/subjectType").then(res => res.data._embedded.subjectType) 8 | }; 9 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Concept.md: -------------------------------------------------------------------------------- 1 | Concepts define the different pieces of information that you collect as part of your service delivery. 2 | 3 | For example, if you collect the blood pressure of a subject in a form, then "Blood Pressure" should be defined as a concept. You would notice that every question in a form requires a concept 4 | 5 | - [More information about concepts](https://avni.readme.io/docs/concepts) 6 | - [Learn more about Avni's domain model](https://avni.readme.io/docs/avnis-domain-model-of-field-based-work) 7 | -------------------------------------------------------------------------------- /src/adminApp/Util/EntityEditUtil.js: -------------------------------------------------------------------------------- 1 | import { httpClient as http } from "../../common/utils/httpClient"; 2 | 3 | class EntityEditUtil { 4 | static onDelete(resource, id, entityDisplayName, onSuccessfulDelete) { 5 | if (window.confirm(`Do you really want to delete ${entityDisplayName}?`)) { 6 | http.delete(`/web/${resource}/${id}`).then(response => { 7 | if (response.status === 200) { 8 | onSuccessfulDelete(); 9 | } 10 | }); 11 | } 12 | } 13 | } 14 | 15 | export default EntityEditUtil; 16 | -------------------------------------------------------------------------------- /src/common/components/DocumentationContainer.jsx: -------------------------------------------------------------------------------- 1 | import { PlatformDocumentation } from "./PlatformDocumentation"; 2 | import { Grid } from "@mui/material"; 3 | 4 | export const DocumentationContainer = ({ filename, ...props }) => { 5 | return ( 6 | 7 | {props.children} 8 | {filename && ( 9 | 10 | 11 | 12 | )} 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/adminApp/components/AvniFormDataConsumer.jsx: -------------------------------------------------------------------------------- 1 | import { useFormContext, useWatch } from "react-hook-form"; 2 | import { ToolTipContainer } from "../../common/components/ToolTipContainer"; 3 | 4 | export const AvniFormDataConsumer = ({ toolTipKey, children, ...props }) => { 5 | const form = useFormContext(); 6 | const formData = useWatch({ control: form.control }); 7 | 8 | return ( 9 | 10 | {children({ formData, ...form, ...props })} 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/dataEntryApp/services/AddressLevelService.js: -------------------------------------------------------------------------------- 1 | import { isEmpty } from "lodash"; 2 | 3 | class AddressLevelService { 4 | constructor() { 5 | this.addressLevels = []; 6 | } 7 | 8 | findByUUID(addressLevelUuid) { 9 | if (!isEmpty(addressLevelUuid)) { 10 | return this.addressLevels.find(x => x.uuid === addressLevelUuid); 11 | } 12 | } 13 | 14 | addAddressLevel(addressLevel) { 15 | this.addressLevels.push(addressLevel); 16 | } 17 | } 18 | 19 | export const addressLevelService = new AddressLevelService(); 20 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/User.md: -------------------------------------------------------------------------------- 1 | Logins for Avni. Users will get their first login details on email and SMS. 2 | 3 | Ensure that you have created catchments for your users before creating them. Users can be created either through the [Users screen](/#/admin/user) or through the [upload screen](/#/admin/upload). 4 | 5 | Users can be created or disabled. Disabled users cannot login. In case of password issues, the field application has the capability to reset passwords. 6 | 7 | [Learn more about Users](https://avni.readme.io/docs/access-control) 8 | -------------------------------------------------------------------------------- /src/common/utils/General.js: -------------------------------------------------------------------------------- 1 | import { format, isValid } from "date-fns"; 2 | 3 | export const formatDate = (aDate) => (aDate && isValid(new Date(aDate)) ? format(new Date(aDate), "dd-MM-yyyy") : "-"); 4 | export const formatDateTime = (aDate) => (aDate && isValid(new Date(aDate)) ? format(new Date(aDate), "dd-MM-yyyy HH:mm") : "-"); 5 | 6 | export function getEnvVar(key, defaultValue = undefined) { 7 | if (typeof process !== "undefined" && process.env) { 8 | return process.env[key] || defaultValue; 9 | } 10 | 11 | return defaultValue; 12 | } 13 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/ValidationError.jsx: -------------------------------------------------------------------------------- 1 | import { FormHelperText } from "@mui/material"; 2 | 3 | import { useTranslation } from "react-i18next"; 4 | 5 | export const ValidationError = ({ validationResult }) => { 6 | const { t } = useTranslation(); 7 | 8 | const renderErrorMessage = () => { 9 | return ( 10 | 11 | {t(validationResult.messageKey, validationResult.extra)} 12 | 13 | ); 14 | }; 15 | 16 | return validationResult ? renderErrorMessage() :
; 17 | }; 18 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/StorageManagementConfig.md: -------------------------------------------------------------------------------- 1 | This feature enables configuration of a custom SQL query which is used to determine which subjects' records should be excluded from syncing to the Avni mobile app. This helps to reduce storage usage on mobile devices and the time taken to sync the records. 2 | 3 | The SQL query should only return the IDs of subjects whose data needs to be excluded from syncing. Refer to [the documentation for this feature](https://avni.readme.io/docs/app-storage-management-and-sync-disable) for additional details. 4 | -------------------------------------------------------------------------------- /.github/workflows/add_to_project.yml: -------------------------------------------------------------------------------- 1 | name: Automatically add items to project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add issue to project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v0.5.0 14 | with: 15 | # You can target a project in a different organization 16 | # to the issue 17 | project-url: https://github.com/orgs/avniproject/projects/2/ 18 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 19 | -------------------------------------------------------------------------------- /.github/workflows/issue_states.yml: -------------------------------------------------------------------------------- 1 | name: "Close cards moved to Done Lane" 2 | 3 | on: 4 | project_card: 5 | types: [created, edited, moved] 6 | 7 | permissions: 8 | repository-projects: read 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | action: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: dessant/issue-states@v3 17 | with: 18 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 19 | open-issue-columns: "" 20 | closed-issue-columns: "Done" 21 | log-output: false 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.tabSize": 2, 4 | "editor.insertSpaces": true, 5 | "editor.autoIndent": "full", 6 | "editor.formatOnSave": true, 7 | "editor.formatOnPaste": true 8 | }, 9 | "[javascriptreact]": { 10 | "editor.tabSize": 2, 11 | "editor.insertSpaces": true, 12 | "editor.autoIndent": "full", 13 | "editor.formatOnSave": true, 14 | "editor.formatOnPaste": true 15 | }, 16 | "editor.detectIndentation": false, 17 | "prettier.tabWidth": 2, 18 | "prettier.useTabs": false 19 | } 20 | -------------------------------------------------------------------------------- /src/adminApp/AppDesignerLayout.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | StyledLayout, 3 | AdminNotification, 4 | } from "../common/components/AdminLayout"; 5 | import AdminAppBar from "../common/components/AdminAppBar"; 6 | import { AppDesignerMenu } from "./AppDesignerMenu"; 7 | 8 | const AppDesignerLayout = ({ children, ...props }) => ( 9 | 15 | {children} 16 | 17 | ); 18 | 19 | export default AppDesignerLayout; 20 | -------------------------------------------------------------------------------- /src/adminApp/components/CatchmentSelectInput.jsx: -------------------------------------------------------------------------------- 1 | import { AutocompleteInput } from "react-admin"; 2 | 3 | export const CatchmentSelectInput = (props) => ( 4 | ({ name: searchText })} 8 | resettable="true" 9 | sx={{ 10 | display: "inline-block", 11 | width: "auto", 12 | "& .MuiInputBase-root": { 13 | backgroundColor: "white", 14 | width: "auto", 15 | minWidth: "120px", 16 | }, 17 | }} 18 | /> 19 | ); 20 | -------------------------------------------------------------------------------- /src/dataEntryApp/services/ConceptService.js: -------------------------------------------------------------------------------- 1 | //@deprecated wrong use of service 2 | class ConceptService { 3 | constructor() { 4 | this.concepts = []; 5 | } 6 | 7 | getConceptByUUID(conceptUuid) { 8 | if (conceptUuid !== null) { 9 | return this.concepts.find(x => x.uuid === conceptUuid); 10 | } 11 | } 12 | 13 | addConcept(concept) { 14 | this.concepts.push(concept); 15 | } 16 | } 17 | 18 | export const conceptService = new ConceptService(); 19 | 20 | export class i18n { 21 | t(concept) { 22 | return concept; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/adminApp/SubjectType/GroupHandlers.js: -------------------------------------------------------------------------------- 1 | import { filter, isEmpty, isNil } from "lodash"; 2 | 3 | export const validateGroup = groupRoles => { 4 | const emptyRoles = filter( 5 | groupRoles, 6 | ({ role, subjectMemberName, minimumNumberOfMembers, maximumNumberOfMembers }) => 7 | isEmpty(role) || 8 | isEmpty(subjectMemberName) || 9 | isNil(minimumNumberOfMembers) || 10 | isNil(maximumNumberOfMembers) || 11 | minimumNumberOfMembers === 0 || 12 | maximumNumberOfMembers === 0 13 | ); 14 | return emptyRoles.length > 0; 15 | }; 16 | -------------------------------------------------------------------------------- /src/reports/export/ReportTypes.js: -------------------------------------------------------------------------------- 1 | import { findKey, map } from "lodash"; 2 | 3 | export const reportTypes = Object.freeze({ 4 | Registration: "Registration", 5 | Enrolment: "Enrolment", 6 | Encounter: "Encounter", 7 | GroupSubject: "Group Subject" 8 | }); 9 | 10 | export default class ReportTypes { 11 | static get names() { 12 | return map(reportTypes, name => ({ name })); 13 | } 14 | 15 | static getName(code) { 16 | return reportTypes[code]; 17 | } 18 | 19 | static getCode(name) { 20 | return findKey(reportTypes, n => name === n); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/upload/Types.js: -------------------------------------------------------------------------------- 1 | import UploadTypes from "./UploadTypes"; 2 | 3 | const typesForStaticDownload = Object.freeze({ 4 | metadataZip: { name: "Metadata Zip" } 5 | }); 6 | 7 | const typesForDynamicDownload = Object.freeze({ 8 | locations: { name: "Locations" }, 9 | usersAndCatchments: { name: "Users & Catchments" } 10 | }); 11 | 12 | let staticTypesWithStaticDownload = new UploadTypes(typesForStaticDownload), 13 | staticTypesWithDynamicDownload = new UploadTypes(typesForDynamicDownload); 14 | 15 | export { staticTypesWithStaticDownload, staticTypesWithDynamicDownload }; 16 | -------------------------------------------------------------------------------- /src/assignment/util/DateFilterOptions.js: -------------------------------------------------------------------------------- 1 | import { startOfDay, addDays } from "date-fns"; 2 | import { labelValue } from "./util"; 3 | 4 | const getDateAfter = days => startOfDay(addDays(new Date(), days)); 5 | 6 | export const dateFilterFieldOptions = [ 7 | labelValue("Any time", new Date("1900-01-01T18:30:00.000Z")), 8 | labelValue("Yesterday", getDateAfter(-1)), 9 | labelValue("Last 7 days", getDateAfter(-7)), 10 | labelValue("Last 30 days", getDateAfter(-30)), 11 | labelValue("Last 60 days", getDateAfter(-60)), 12 | labelValue("Last 180 days", getDateAfter(-180)) 13 | ]; 14 | -------------------------------------------------------------------------------- /src/dataEntryApp/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | background-color: #f8f4f4; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 14 | monospace; 15 | } 16 | 17 | h1,h2,h3,h4,h5,h6 { 18 | font-family: "Lucida Grande", sans-serif; 19 | } 20 | -------------------------------------------------------------------------------- /src/dataEntryApp/reducers/searchFilterReducer.js: -------------------------------------------------------------------------------- 1 | const prefix = "app/dataEntry/reducer/searchFilter/"; 2 | 3 | export const types = { 4 | ADD_SEARCH_REQUEST: `${prefix}ADD_SEARCH_REQUEST` 5 | }; 6 | 7 | const initialState = { request: { includeVoided: false, includeDisplayCount: false } }; 8 | 9 | export default function(state = initialState, action) { 10 | switch (action.type) { 11 | case types.ADD_SEARCH_REQUEST: { 12 | return { 13 | ...state, 14 | request: action.value 15 | }; 16 | } 17 | default: 18 | return state; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/upload/UploadTypes.js: -------------------------------------------------------------------------------- 1 | import { map, findKey, find, get } from "lodash"; 2 | 3 | export default class { 4 | constructor(types = {}) { 5 | this.types = types; 6 | } 7 | 8 | get names() { 9 | return map(this.types, n => ({ name: n.name })); 10 | } 11 | 12 | getName(code) { 13 | return get(this.types[code], "name"); 14 | } 15 | 16 | getCode(name) { 17 | return findKey(this.types, n => name === n.name); 18 | } 19 | 20 | isApprovalEnabled(name) { 21 | return get(find(this.types, n => n.name === name), "approvalEnabled", false); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/common/service/UserService.js: -------------------------------------------------------------------------------- 1 | import { httpClient as http } from "../utils/httpClient"; 2 | 3 | const userEndpoint = "/user", 4 | userSearchEndpoint = "/user/search/find"; 5 | 6 | class UserService { 7 | static searchUsers(name, phoneNumber, email) { 8 | return http 9 | .fetchJson(`${userSearchEndpoint}?name=${name}&phoneNumber=${phoneNumber}&email=${email}&page=${0}&size=${30}`) 10 | .then(response => response.json); 11 | } 12 | static searchUserById(id) { 13 | return http.get(`${userEndpoint}/${id}`); 14 | } 15 | } 16 | 17 | export default UserService; 18 | -------------------------------------------------------------------------------- /src/dataEntryApp/utils/LocationUtil.js: -------------------------------------------------------------------------------- 1 | import { get, isEmpty } from "lodash"; 2 | 3 | export const locationNameRenderer = location => { 4 | const locName = get(location, "name", get(location, "title")); 5 | const locType = get(location, "type", get(location, "typeString")); 6 | if (isEmpty(locName)) { 7 | return ""; 8 | } 9 | let retVal = `${locName} (${locType})`; 10 | let lineageParts = location.titleLineage.split(", "); 11 | if (lineageParts.length > 1) 12 | retVal += ` in ${lineageParts.slice(0, lineageParts.length - 1).join(" > ")}`; 13 | return retVal; 14 | }; 15 | -------------------------------------------------------------------------------- /src/formDesigner/components/StorageManagement/StorageManagementConfigReducer.js: -------------------------------------------------------------------------------- 1 | export const StorageManagementConfigReducer = (state, action) => { 2 | switch (action.type) { 3 | case "sqlQuery": 4 | return { ...state, sqlQuery: action.payload.value }; 5 | case "realmQuery": 6 | return { ...state, realmQuery: action.payload.value }; 7 | case "batchSize": 8 | return { ...state, batchSize: action.payload.value }; 9 | case "storageManagementConfig": 10 | return { ...state, ...action.payload }; 11 | default: 12 | return state; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Catchment.md: -------------------------------------------------------------------------------- 1 | A Catchment is a group of locations. Catchments are used to segregate areas of operation of each user (or group of users). 2 | 3 | The field app will sync only data for the catchment assigned to the logged in user. By dividing into catchments, we ensure that the user has a smaller set of information to work with. 4 | 5 | **Uploading catchments** 6 | Catchments can also be created along with users in bulk using the [upload screen](/#/admin/upload). 7 | 8 | [Learn more about Avni's domain model](https://avni.readme.io/docs/avnis-domain-model-of-field-based-work) 9 | -------------------------------------------------------------------------------- /src/common/components/AvniRadioButtonGroupInput.jsx: -------------------------------------------------------------------------------- 1 | import { ToolTipContainer } from "./ToolTipContainer"; 2 | import { RadioButtonGroupInput } from "react-admin"; 3 | import { Box } from "@mui/material"; 4 | 5 | export const AvniRadioButtonGroupInput = ({ toolTipKey, ...props }) => { 6 | return ( 7 | 8 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/dataEntryApp/services/SubjectService.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | class SubjectService { 4 | constructor() { 5 | this.subjects = new Map(); 6 | } 7 | 8 | findByUUID(subjectUuid) { 9 | if (!_.isNil(subjectUuid)) { 10 | return this.subjects.get(subjectUuid); 11 | } 12 | } 13 | 14 | addSubject(subject) { 15 | this.subjects.set(subject.uuid, subject); 16 | } 17 | 18 | addSubjects(subjects) { 19 | subjects.reduce((acc, subject) => acc.set(subject.uuid, subject), this.subjects); 20 | } 21 | } 22 | 23 | export const subjectService = new SubjectService(); 24 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/Upload.md: -------------------------------------------------------------------------------- 1 | You can upload users, subjects, enrolments and encounters in bulk in this screen. Metadata zip files that have been downloaded from another organisation can also be uploaded in this screen. 2 | 3 | All files except the metadata zip are supposed to be in a [CSV](https://en.wikipedia.org/wiki/Comma-separated_values) format. If you use Microsoft Excel, it has an option to save your spreadsheet in CSV format. 4 | 5 | Use the **Download Sample** option to download a sample file. More details about the sample file are available [here](https://avni.readme.io/docs/upload-data) 6 | -------------------------------------------------------------------------------- /src/common/components/AvniFormControl.jsx: -------------------------------------------------------------------------------- 1 | import { ToolTipContainer } from "./ToolTipContainer"; 2 | import { styled } from "@mui/material/styles"; 3 | import { FormControl as MuiFormControl } from "@mui/material"; 4 | 5 | const StyledFormControl = styled(MuiFormControl)({ 6 | paddingBottom: 10, 7 | width: "100%" 8 | }); 9 | 10 | export const AvniFormControl = ({ toolTipKey, styles, ...props }) => { 11 | return ( 12 | 13 | {props.children} 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/common/components/CreateComponent.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@mui/material"; 2 | import { isEmpty } from "lodash"; 3 | 4 | export const CreateComponent = ({ 5 | onSubmit, 6 | disabledFlag = false, 7 | name = "CREATE", 8 | styles = {}, 9 | fullWidth = false 10 | }) => { 11 | return ( 12 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/dataEntryApp/reducers/loadReducer.js: -------------------------------------------------------------------------------- 1 | const prefix = "app/dataEntry/reducer/load/"; 2 | 3 | export const types = { 4 | SET_LOAD: `${prefix}SET_LOAD` 5 | }; 6 | 7 | export const setLoad = load => ({ 8 | type: types.SET_LOAD, 9 | load 10 | }); 11 | 12 | const initialState = { 13 | load: false 14 | }; 15 | 16 | // reducer 17 | export default function(state = initialState, action) { 18 | switch (action.type) { 19 | case types.SET_LOAD: { 20 | return { 21 | ...state, 22 | load: action.load 23 | }; 24 | } 25 | default: 26 | return state; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/news/utils/BroadcastPath.js: -------------------------------------------------------------------------------- 1 | class BroadcastPath { 2 | static Root = "broadcast"; 3 | static News = "news"; 4 | static WhatsApp = "whatsApp"; 5 | static WhatsAppFullPath = `${this.Root}/${this.WhatsApp}`; 6 | static ContactGroup = "contactGroup"; 7 | static User = "users"; 8 | static Subject = "subjects"; 9 | static ContactGroupFullPath = `${this.Root}/${this.WhatsApp}/${this.ContactGroup}`; 10 | static UserFullPath = `/${this.Root}/${this.WhatsApp}/${this.User}`; 11 | static SubjectFullPath = `/${this.Root}/${this.WhatsApp}/${this.Subject}`; 12 | } 13 | 14 | export default BroadcastPath; 15 | -------------------------------------------------------------------------------- /src/common/model/WebSubjectType.test.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import WebSubjectType from "./WebSubjectType"; 3 | import SubjectTypeFactory from "./SubjectTypeFactory"; 4 | import { SubjectTypeType } from "../../adminApp/SubjectType/Types"; 5 | 6 | it("empty location is recommended for subject type = user", function() { 7 | const subjectType = SubjectTypeFactory.create({ type: SubjectTypeType.Individual }); 8 | assert.equal(false, subjectType.allowEmptyLocation); 9 | WebSubjectType.updateType(subjectType, SubjectTypeType.User); 10 | assert.equal(true, subjectType.allowEmptyLocation); 11 | }); 12 | -------------------------------------------------------------------------------- /src/news/api/index.js: -------------------------------------------------------------------------------- 1 | import { httpClient as http } from "../../common/utils/httpClient"; 2 | 3 | const NEWS_API_ENDPOINT = "/web/news"; 4 | export default { 5 | getNews: () => http.fetchJson(NEWS_API_ENDPOINT).then(response => response.json), 6 | getNewsById: id => http.get(`${NEWS_API_ENDPOINT}/${id}`), 7 | createNews: news => http.post(NEWS_API_ENDPOINT, news), 8 | editNews: news => http.put(`${NEWS_API_ENDPOINT}/${news.id}`, news), 9 | deleteNews: id => http.delete(`${NEWS_API_ENDPOINT}/${id}`), 10 | getPublishedNews: () => http.fetchJson("/web/publishedNews").then(response => response.json) 11 | }; 12 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/ToolTipContainer.jsx: -------------------------------------------------------------------------------- 1 | import { ToolTip } from "./ToolTip"; 2 | 3 | export const ToolTipContainer = ({ 4 | toolTipKey, 5 | styles, 6 | onHover, 7 | position, 8 | ...props 9 | }) => { 10 | return ( 11 |
12 |
{props.children}
13 |
14 | 19 |
20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", 5 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | background-color: #3cc4b0; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | 16 | h1, 17 | h2, 18 | h3, 19 | h4, 20 | h5, 21 | h6 { 22 | font-family: "Lucida Grande", sans-serif; 23 | } 24 | 25 | *:focus { 26 | outline: none !important; 27 | } 28 | -------------------------------------------------------------------------------- /src/adminApp/components/CustomisedExpansionPanelSummary.js: -------------------------------------------------------------------------------- 1 | import { AccordionSummary } from "@mui/material"; 2 | import { styled } from "@mui/material/styles"; 3 | 4 | const CustomisedAccordionSummary = styled(AccordionSummary)({ 5 | backgroundColor: "rgba(0,0,0,0.07)", 6 | borderBottom: "1px solid rgba(0,0,0,.125)", 7 | marginBottom: -1, 8 | minHeight: 40, 9 | "&.Mui-expanded": { 10 | minHeight: 40 11 | }, 12 | "& .MuiAccordionSummary-content.Mui-expanded": { 13 | margin: "12px 0" 14 | } 15 | }); 16 | 17 | CustomisedAccordionSummary.muiName = "AccordionSummary"; 18 | 19 | export { CustomisedAccordionSummary }; 20 | -------------------------------------------------------------------------------- /src/common/components/ActivityIndicatorModal.jsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@mui/material/styles"; 2 | import { CircularProgress, Modal } from "@mui/material"; 3 | import MuiComponentHelper from "../utils/MuiComponentHelper"; 4 | 5 | const StyledProgress = styled(CircularProgress)({ 6 | position: "absolute", 7 | top: "30%", 8 | left: "50%", 9 | zIndex: 1 10 | }); 11 | 12 | const ActivityIndicatorModal = ({ open }) => { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default ActivityIndicatorModal; 21 | -------------------------------------------------------------------------------- /src/common/model/GroupModel.js: -------------------------------------------------------------------------------- 1 | class GroupModel { 2 | name; 3 | 4 | static Everyone = "Everyone"; 5 | static Administrators = "Administrators"; 6 | static MetabaseUsers = "Metabase Users"; 7 | 8 | static nonRemovableGroup(name) { 9 | return [this.Everyone, this.Administrators, this.MetabaseUsers].includes(name); 10 | } 11 | 12 | static isEveryoneWithAllPrivileges(group) { 13 | return group.name === "Everyone" && group.hasAllPrivileges; 14 | } 15 | 16 | static notEveryoneWithoutAllPrivileges(group) { 17 | return group.name !== "Everyone" && !group.hasAllPrivileges; 18 | } 19 | } 20 | 21 | export default GroupModel; 22 | -------------------------------------------------------------------------------- /src/common/model/WebFormElementGroup.js: -------------------------------------------------------------------------------- 1 | import { FormElementGroup } from "openchs-models"; 2 | import WebFormElement from "./WebFormElement"; 3 | import WebForm from "./WebForm"; 4 | 5 | class WebFormElementGroup extends FormElementGroup { 6 | get formElements() { 7 | return this.toEntityList("formElements", WebFormElement); 8 | } 9 | 10 | set formElements(x) { 11 | this.that.formElements = this.fromEntityList(x); 12 | } 13 | 14 | get form() { 15 | return this.toEntity("form", WebForm); 16 | } 17 | 18 | set form(x) { 19 | this.that.form = this.fromObject(x); 20 | } 21 | } 22 | 23 | export default WebFormElementGroup; 24 | -------------------------------------------------------------------------------- /src/formDesigner/components/PhoneNumberConcept.jsx: -------------------------------------------------------------------------------- 1 | import { AvniSwitch } from "../../common/components/AvniSwitch"; 2 | 3 | export const PhoneNumberConcept = ({ onKeyValueChange, checked }) => { 4 | const onChange = event => 5 | onKeyValueChange( 6 | { key: "verifyPhoneNumber", value: event.target.checked }, 7 | 0 8 | ); 9 | return ( 10 |
11 | 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/common/service/OrganisationService.jsx: -------------------------------------------------------------------------------- 1 | import { httpClient as http } from "../utils/httpClient"; 2 | import CurrentUserService from "./CurrentUserService"; 3 | 4 | class OrganisationService { 5 | static getOrganisation(id) { 6 | return http 7 | .fetchJson(`/organisation/${id}`) 8 | .then(response => response.json); 9 | } 10 | 11 | static getApplicableOrganisation(id) { 12 | return CurrentUserService.isOrganisationImpersonated() 13 | ? OrganisationService.getOrganisation(id) 14 | : http.fetchJson("/organisation/current").then(response => response.json); 15 | } 16 | } 17 | 18 | export default OrganisationService; 19 | -------------------------------------------------------------------------------- /src/adminApp/service/MediaService.jsx: -------------------------------------------------------------------------------- 1 | import { httpClient as http } from "../../common/utils/httpClient"; 2 | 3 | class MediaService { 4 | static async getMedia(url) { 5 | try { 6 | return await http 7 | .get(http.withParams(`/media/signedUrl`, { url: url })) 8 | .then(res => res.data); 9 | } catch (exception) { 10 | return "Unable to fetch media"; 11 | } 12 | } 13 | 14 | static async getMultipleMedia(urls) { 15 | if (!Array.isArray(urls) || urls.length === 0) { 16 | return []; 17 | } 18 | return (await http.post(`/media/signedUrls`, urls)).data; 19 | } 20 | } 21 | 22 | export default MediaService; 23 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/SingleSelectFormElement.jsx: -------------------------------------------------------------------------------- 1 | import { get } from "lodash"; 2 | 3 | import { CodedConceptFormElement } from "./CodedConceptFormElement"; 4 | 5 | export default ({ 6 | formElement: fe, 7 | value, 8 | update, 9 | validationResults, 10 | uuid 11 | }) => { 12 | return ( 13 | value === answer.uuid} 15 | onChange={answer => { 16 | update(get(answer, "uuid")); 17 | }} 18 | validationResults={validationResults} 19 | uuid={uuid} 20 | mandatory={fe.mandatory} 21 | > 22 | {fe} 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/formDesigner/components/ImageV2Concept.jsx: -------------------------------------------------------------------------------- 1 | import { AvniSwitch } from "../../common/components/AvniSwitch"; 2 | 3 | export const ImageV2Concept = ({ onKeyValueChange, checked }) => { 4 | const onChange = event => 5 | onKeyValueChange( 6 | { key: "captureLocationInformation", value: event.target.checked }, 7 | 0 8 | ); 9 | return ( 10 |
11 | 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/adminApp/SubjectType/effects.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { httpClient as http } from "../../common/utils/httpClient"; 3 | 4 | export const useFormMappings = cb => 5 | useEffect(() => { 6 | http.get("/web/operationalModules").then(response => { 7 | const formMap = response.data.formMappings; 8 | formMap.map(l => (l["isVoided"] = false)); 9 | cb(formMap, response.data.forms, response.data.subjectTypes); 10 | }); 11 | }, []); 12 | 13 | export const useLocationType = cb => 14 | useEffect(() => { 15 | http.get("/web/addressLevelType").then(response => { 16 | cb(response.data); 17 | }); 18 | }, []); 19 | -------------------------------------------------------------------------------- /src/common/components/ToolTipContainer.jsx: -------------------------------------------------------------------------------- 1 | import { ToolTip } from "./ToolTip"; 2 | 3 | export const ToolTipContainer = ({ 4 | toolTipKey, 5 | styles, 6 | onHover, 7 | position, 8 | ...props 9 | }) => { 10 | return ( 11 |
12 |
{props.children}
13 | {toolTipKey && ( 14 |
15 | 20 |
21 | )} 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/dataEntryApp/views/subjectDashBoard/components/FloatingButton.jsx: -------------------------------------------------------------------------------- 1 | import Button from "./Button"; 2 | 3 | const FloatingButton = ({ 4 | btnLabel, 5 | btnClass, 6 | btnClick, 7 | btnDisabled, 8 | id, 9 | left 10 | }) => { 11 | const floatingStyle = { 12 | margin: 0, 13 | top: "auto", 14 | left: left, 15 | bottom: 20, 16 | position: "fixed" 17 | }; 18 | 19 | return ( 20 | 27 | 28 | 29 | ); 30 | }; 31 | export default MessageDialog; 32 | -------------------------------------------------------------------------------- /src/dataEntryApp/reducers/generalSubjectDashboardReducer.js: -------------------------------------------------------------------------------- 1 | const prefix = "app/dataEntry/reducer/subjectGeneral/"; 2 | 3 | export const types = { 4 | GET_SUBJECT_GENERAL: `${prefix}GET_SUBJECT_GENERAL`, 5 | SET_SUBJECT_GENERAL: `${prefix}SET_SUBJECT_GENERAL` 6 | }; 7 | 8 | export const getSubjectGeneral = subjectGeneralUUID => ({ 9 | type: types.GET_SUBJECT_GENERAL, 10 | subjectGeneralUUID 11 | }); 12 | 13 | export const setSubjectGeneral = subjectGeneral => ({ 14 | type: types.SET_SUBJECT_GENERAL, 15 | subjectGeneral 16 | }); 17 | 18 | export default function(state = {}, action) { 19 | switch (action.type) { 20 | case types.SET_SUBJECT_GENERAL: { 21 | return { 22 | ...state, 23 | subjectGeneral: action.subjectGeneral 24 | }; 25 | } 26 | default: 27 | return state; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/reports/api.js: -------------------------------------------------------------------------------- 1 | import { httpClient } from "../common/utils/httpClient"; 2 | import { get } from "lodash"; 3 | 4 | export default { 5 | fetchOperationalModules: () => httpClient.fetchJson("/web/operationalModules/").then(response => response.json), 6 | startExportJob: body => 7 | httpClient 8 | .postJson("/export", body) 9 | .then(r => [r.text, null]) 10 | .catch(r => [null, `${get(r, "response.data") || get(r, "message") || "unknown error"}`]), 11 | startExportV2Job: body => 12 | httpClient 13 | .postJson("/export/v2", body) 14 | .then(r => [r.text, null]) 15 | .catch(r => [null, `${get(r, "response.data") || get(r, "message") || "unknown error"}`]), 16 | fetchUploadJobStatuses: params => httpClient.fetchJson(httpClient.withParams("/export/status", { size: 10, ...params })).then(r => r.json) 17 | }; 18 | -------------------------------------------------------------------------------- /src/reports/components/export/ExportRequestBody.jsx: -------------------------------------------------------------------------------- 1 | import { isUndefined } from "lodash"; 2 | import { Box, Typography } from "@mui/material"; 3 | import { JsonEditor } from "../../../formDesigner/components/JsonEditor"; 4 | 5 | export const ExportRequestBody = ({ 6 | customRequest, 7 | exportRequest, 8 | dispatch 9 | }) => { 10 | return ( 11 | 16 | 17 | {"Request body"} 18 | 19 | dispatch(event)} 26 | /> 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/common/components/FileDownloadButton.jsx: -------------------------------------------------------------------------------- 1 | import { Link, IconButton } from "@mui/material"; 2 | import { CloudDownload } from "@mui/icons-material"; 3 | import { httpClient as http } from "common/utils/httpClient"; 4 | 5 | const FileDownloadButton = ({ 6 | url, 7 | filename, 8 | disabled, 9 | buttonColor = "primary" 10 | }) => { 11 | const DownloadButton = () => ( 12 | 13 | 14 | 15 | ); 16 | 17 | return disabled ? ( 18 | 19 | ) : ( 20 | { 23 | e.preventDefault(); 24 | http.downloadFile(url, filename); 25 | }} 26 | > 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default FileDownloadButton; 33 | -------------------------------------------------------------------------------- /src/common/model/WebStandardReportCardType.js: -------------------------------------------------------------------------------- 1 | import { StandardReportCardType } from "openchs-models"; 2 | 3 | class WebStandardReportCardType extends StandardReportCardType { 4 | get id() { 5 | return this.that.id; 6 | } 7 | 8 | set id(x) { 9 | this.that.id = x; 10 | } 11 | 12 | static fromResource(resource) { 13 | const webStandardReportCardType = new WebStandardReportCardType(); 14 | webStandardReportCardType.id = resource.id; 15 | webStandardReportCardType.uuid = resource.uuid; 16 | webStandardReportCardType.name = resource.name; 17 | webStandardReportCardType.description = resource.description; 18 | webStandardReportCardType.voided = resource.voided; 19 | webStandardReportCardType.type = resource.type; 20 | return webStandardReportCardType; 21 | } 22 | } 23 | 24 | export default WebStandardReportCardType; 25 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/View.md: -------------------------------------------------------------------------------- 1 | The views on this page are automatically generated or refreshed on click of `Create/Refresh View` button. There is one view corresponding to each form with every form field available as a column. These views help in writing the reports easily without having to understand the generic underlying Avni schema. Once generated, the views are available in Metabase or any other reporting tool you use. 2 | 3 | The column names are based on form fields (concepts) which can be longer than permitted by the database, in such a case the names are shortened and you can see them at the top of view definition in comments. The format is - `Field name >> Shortened column name`. 4 | 5 | Some types of change in form definition can cause these views to become obsolete. In such a case please regenerate these views and check your reports based on these views. 6 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/MediaObservations.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import ReactImageVideoLightbox from "react-image-video-lightbox"; 3 | 4 | const MediaObservations = ({ 5 | mediaDataList, 6 | currentMediaItemIndex, 7 | onClose, 8 | showResourceCount 9 | }) => { 10 | useEffect(() => { 11 | const LightboxContainer = document.querySelector( 12 | "div.mediaObservationContainer" 13 | ); 14 | LightboxContainer.firstChild.style.zIndex = 2; 15 | }); 16 | 17 | return ( 18 |
19 | onClose()} 24 | /> 25 |
26 | ); 27 | }; 28 | 29 | export default MediaObservations; 30 | -------------------------------------------------------------------------------- /src/common/components/Icon.jsx: -------------------------------------------------------------------------------- 1 | import TemplatesIcon from "../icons/templates.svg?react"; 2 | import StartFreshIcon from "../icons/start-fresh.svg?react"; 3 | 4 | const iconMap = { 5 | templates: TemplatesIcon, 6 | startFresh: StartFreshIcon, 7 | }; 8 | 9 | const Icon = ({ 10 | name, 11 | width = 24, 12 | height = 24, 13 | className = "", 14 | color = "currentColor", 15 | ...props 16 | }) => { 17 | const IconComponent = iconMap[name]; 18 | 19 | if (!IconComponent) { 20 | console.warn( 21 | `Icon "${name}" not found. Available icons: ${Object.keys(iconMap).join(", ")}`, 22 | ); 23 | return null; 24 | } 25 | 26 | return ( 27 | 34 | ); 35 | }; 36 | 37 | export default Icon; 38 | -------------------------------------------------------------------------------- /src/common/components/HelpText.jsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@mui/material/styles"; 2 | import { isEmpty } from "lodash"; 3 | import ShowMoreText from "react-show-more-text"; 4 | 5 | const StyledContainer = styled("div")(({ theme }) => ({ 6 | width: "50%", 7 | marginBottom: theme.spacing(2.5), // 20px 8 | color: "rgba(69,69,69,0.54)" 9 | })); 10 | 11 | export const HelpText = ({ text, t }) => { 12 | const renderText = () => ( 13 | 14 | 23 | {t(text)} 24 | 25 | 26 | ); 27 | 28 | return isEmpty(text) ? null : renderText(); 29 | }; 30 | -------------------------------------------------------------------------------- /src/assignment/taskAssignment/FetchTasks.js: -------------------------------------------------------------------------------- 1 | import { httpClient as http } from "common/utils/httpClient"; 2 | import { getFilterPayload } from "../reducers/TaskAssignmentReducer"; 3 | import { isEmpty } from "lodash"; 4 | 5 | export const fetchTasks = (query, filterCriteria) => { 6 | return new Promise(resolve => { 7 | let apiUrl = "/web/task?"; 8 | apiUrl += "size=" + query.pageSize; 9 | apiUrl += "&page=" + query.page; 10 | if (!isEmpty(query.orderBy)) { 11 | apiUrl += `&sort=${query.orderBy.field},${query.orderDirection}`; 12 | } 13 | http 14 | .post(apiUrl, getFilterPayload(filterCriteria)) 15 | .then(response => response.data) 16 | .then(result => { 17 | resolve({ 18 | data: result.content, 19 | page: query.page, 20 | totalCount: result.totalElements 21 | }); 22 | }); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /src/formDesigner/components/TemplateOrganisations/TemplateDialogErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import { ErrorBoundary } from "react-error-boundary"; 2 | import { ErrorFallback } from "../../../dataEntryApp/ErrorFallback"; 3 | 4 | /** 5 | * Error boundary wrapper specifically for Template Application Dialog 6 | * Provides error handling and user feedback for template application failures 7 | */ 8 | export const TemplateDialogErrorBoundary = ({ children }) => { 9 | return ( 10 | ( 12 | 13 | )} 14 | onError={(error, errorInfo) => { 15 | console.error("Template Dialog Error:", error, errorInfo); 16 | // Could add error reporting service here 17 | }} 18 | > 19 | {children} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/formDesigner/components/ColourStyle.jsx: -------------------------------------------------------------------------------- 1 | import { Grid } from "@mui/material"; 2 | import { AvniFormLabel } from "../../common/components/AvniFormLabel"; 3 | import { PopoverColorPicker } from "../../common/components/PopoverColorPicker"; 4 | 5 | export const ColourStyle = ({ label, onChange, colour = "", toolTipKey }) => { 6 | return ( 7 | 16 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/formDesigner/components/Video/VideoReducer.js: -------------------------------------------------------------------------------- 1 | export const VideoReducer = (video, action) => { 2 | switch (action.type) { 3 | case "name": 4 | return { ...video, title: action.payload }; 5 | case "fileName": 6 | return { ...video, fileName: action.payload }; 7 | case "duration": 8 | return { 9 | ...video, 10 | duration: action.payload === "" ? "" : +action.payload.replace(/\D/g, "") 11 | }; 12 | case "description": 13 | return { ...video, description: action.payload }; 14 | case "setData": 15 | console.log("calling setData"); 16 | return { 17 | ...video, 18 | title: action.payload.title, 19 | fileName: action.payload.fileName, 20 | duration: action.payload.duration, 21 | description: action.payload.description 22 | }; 23 | default: 24 | return video; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/EncounterType.md: -------------------------------------------------------------------------------- 1 | Encounter Types (also called Visit Types) are used to determine the kinds of encounters/visits that can be performed. An encounter can be scheduled for a specific encounter type using rules. Here, we define that encounter type and the forms associated with the encounter type. 2 | 3 | An encounter type is associated to a subject type. It need not be associated with programs (you can perform an annual survey of a population using encounter types not associated with programs, and use this information to enrol subjects into a program). 4 | 5 | The encounter eligibility check rule is used to determine eligibility of an encounter type for a subject at any time. 6 | 7 | - [Learn more about Avni's domain model](https://avni.readme.io/docs/avnis-domain-model-of-field-based-work) 8 | - [Learn more about writing rules](https://avni.readme.io/docs/rules-concept-guide) 9 | -------------------------------------------------------------------------------- /src/formDesigner/common/CommonSearch.jsx: -------------------------------------------------------------------------------- 1 | import { deburr, isEmpty } from "lodash"; 2 | import AsyncSelect from "react-select/async"; 3 | 4 | const CommonSearch = ({ 5 | value, 6 | onChange, 7 | isMulti, 8 | placeholder, 9 | defaultOptions = [], 10 | loadOptionsByValue 11 | }) => { 12 | const loadOptions = (value, callback) => { 13 | if (!value) { 14 | callback(defaultOptions); 15 | } 16 | const inputValue = deburr(value.trim()).toLowerCase(); 17 | loadOptionsByValue(encodeURIComponent(inputValue), callback); 18 | }; 19 | 20 | return ( 21 | 30 | ); 31 | }; 32 | 33 | export default CommonSearch; 34 | -------------------------------------------------------------------------------- /src/common/utils/S3Client.js: -------------------------------------------------------------------------------- 1 | import { httpClient as http } from "./httpClient"; 2 | import { get } from "lodash"; 3 | 4 | const uploadMedia = async (existingURL, file, folderName, type) => { 5 | if (file == null) { 6 | return [existingURL]; 7 | } 8 | return http 9 | .uploadFile(http.withParams(`/media/save${type}`, { folderName }), file) 10 | .then((r) => [r.data, null]) 11 | .catch((r) => [null, `${get(r, "response.data") || get(r, "message") || "unknown error"}`]); 12 | }; 13 | 14 | export const uploadImage = async (existingURL, file, folderName) => uploadMedia(existingURL, file, folderName, "Image"); 15 | export const uploadVideo = async (existingURL, file, folderName) => uploadMedia(existingURL, file, folderName, "Video"); 16 | 17 | export const MediaFolder = Object.freeze({ 18 | PROFILE_PICS: "profile-pics", 19 | ICONS: "icons", 20 | NEWS: "news", 21 | METADATA: "metadata", 22 | }); 23 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/GroupMembershipCardView.jsx: -------------------------------------------------------------------------------- 1 | import SubjectCardView from "./SubjectCardView"; 2 | import { Typography } from "@mui/material"; 3 | 4 | const GroupMembershipCardView = ({ 5 | groupMembership: { groupSubject, groupRole } 6 | }) => { 7 | return ( 8 |
9 | 15 | theme.palette.text.secondary, 19 | mb: 1, 20 | textAlign: "center" 21 | }} 22 | > 23 | {groupRole.role} 24 | 25 | 26 |
27 | ); 28 | }; 29 | 30 | export default GroupMembershipCardView; 31 | -------------------------------------------------------------------------------- /src/dataEntryApp/sagas/generalSubjectDashboardSaga.js: -------------------------------------------------------------------------------- 1 | import { all, call, fork, put, takeLatest } from "redux-saga/effects"; 2 | import { types, setSubjectGeneral } from "../reducers/generalSubjectDashboardReducer"; 3 | import api from "../api"; 4 | import { mapGeneral } from "../../common/subjectModelMapper"; 5 | import { setLoad } from "../reducers/loadReducer"; 6 | 7 | export default function*() { 8 | yield all([subjectGeneralFetchWatcher].map(fork)); 9 | } 10 | 11 | export function* subjectGeneralFetchWatcher() { 12 | yield takeLatest(types.GET_SUBJECT_GENERAL, subjectGeneralFetchWorker); 13 | } 14 | 15 | export function* subjectGeneralFetchWorker({ subjectGeneralUUID }) { 16 | yield put.resolve(setLoad(false)); 17 | const subjectGeneral = yield call(api.fetchSubjectGeneral, subjectGeneralUUID); 18 | yield put(setSubjectGeneral(mapGeneral(subjectGeneral))); 19 | yield put.resolve(setLoad(true)); 20 | } 21 | -------------------------------------------------------------------------------- /src/common/components/AvniSwitch.jsx: -------------------------------------------------------------------------------- 1 | import { Grid, Switch } from "@mui/material"; 2 | 3 | import { ToolTipContainer } from "./ToolTipContainer"; 4 | 5 | export const AvniSwitch = ({ toolTipKey, switchFirst, ...props }) => { 6 | return ( 7 | 11 | 19 | {switchFirst && ( 20 | 21 | 22 | 23 | )} 24 | {props.name} 25 | {!switchFirst && ( 26 | 27 | 28 | 29 | )} 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/CustomizedBackdrop.jsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from "react"; 2 | import { styled } from "@mui/material/styles"; 3 | import { Fade, CircularProgress } from "@mui/material"; 4 | 5 | const StyledBackdrop = styled("div")(({ theme }) => ({ 6 | color: "#fff", 7 | zIndex: theme.zIndex.drawer + 1, 8 | position: "fixed", 9 | display: "flex", 10 | alignItems: "center", 11 | justifyContent: "center", 12 | right: 0, 13 | bottom: 0, 14 | top: 0, 15 | left: 0, 16 | backgroundColor: "rgba(0,0,0,0.5)" 17 | })); 18 | 19 | const CustomizedBackdrop = forwardRef(function Backdrop({ load }, ref) { 20 | const open = !load; 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }); 29 | 30 | export default CustomizedBackdrop; 31 | -------------------------------------------------------------------------------- /src/adminApp/react-admin-config/SpringResponse.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | class SpringResponse { 4 | static toReactAdminResourceListResponse(json, resource) { 5 | if (json["content"]) { 6 | return { 7 | data: json["content"], 8 | total: json["totalElements"] 9 | }; 10 | } else if (json["_embedded"]) { 11 | let resources = json["_embedded"][resource]; 12 | let page = json["page"]; 13 | let totalElements = page ? page["totalElements"] : resources.length; 14 | return { 15 | data: resources, 16 | total: totalElements 17 | }; 18 | } else if (_.get(json, ["page", "totalElements"]) === 0) { 19 | return { 20 | data: [], 21 | total: 0 22 | }; 23 | } else { 24 | return { 25 | data: json, 26 | total: json.length 27 | }; 28 | } 29 | } 30 | } 31 | 32 | export default SpringResponse; 33 | -------------------------------------------------------------------------------- /src/common/utils/httpClient.test.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import { httpClient } from "common/utils/httpClient"; 3 | import NoAuthSession from "../../rootApp/security/NoAuthSession"; 4 | import IdpFactory from "../../rootApp/security/IdpFactory"; 5 | import IdpDetails from "../../rootApp/security/IdpDetails"; 6 | 7 | describe("httpClient", () => { 8 | it("set headers", () => { 9 | const noAuthSession = new NoAuthSession(); 10 | noAuthSession.userInfoUpdate([], "abcd", "ABCD"); 11 | httpClient.initAuthSession(noAuthSession); 12 | const params = {}; 13 | httpClient.setIdp(IdpFactory.createIdp(IdpDetails.none, {})); 14 | httpClient.setHeaders(params); 15 | assert.deepEqual( 16 | params.headers, 17 | new Headers({ 18 | accept: "application/json", 19 | "content-type": "application/json", 20 | "user-name": "abcd" 21 | }) 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/common/utils/CollectionUtil.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | class CollectionUtil { 4 | static toObject(collection, keyKey, valueKey) { 5 | return _.reduce( 6 | collection, 7 | (result, x) => { 8 | result[x[keyKey]] = x[valueKey]; 9 | return result; 10 | }, 11 | {} 12 | ); 13 | } 14 | 15 | static switchItemPosition(array, startIndex, endIndex, fieldToUpdate) { 16 | const workingItems = _.sortBy(array, [fieldToUpdate]); 17 | if (_.inRange(startIndex, 0, array.length) && _.inRange(endIndex, 0, array.length)) { 18 | let removedElement = _.pullAt(workingItems, startIndex)[0]; 19 | workingItems.splice(endIndex, 0, removedElement); 20 | } 21 | workingItems.forEach((item, index) => { 22 | item[fieldToUpdate] = index + 1; 23 | }); 24 | array.splice(0, array.length, ...workingItems); 25 | } 26 | } 27 | 28 | export default CollectionUtil; 29 | -------------------------------------------------------------------------------- /src/formDesigner/components/ConceptActiveSwitch.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { httpClient as http } from "../../common/utils/httpClient"; 3 | import { AvniSwitch } from "../../common/components/AvniSwitch"; 4 | import _ from "lodash"; 5 | 6 | export const ConceptActiveSwitch = ({ active, handleActive, conceptUUID }) => { 7 | const [conceptUsage, setConceptUsage] = useState({}); 8 | useEffect(() => { 9 | if (conceptUUID) { 10 | http 11 | .get("/web/concept/usage/" + conceptUUID) 12 | .then(response => setConceptUsage(response.data)); 13 | } 14 | }, [conceptUUID]); 15 | 16 | return ( 17 |
18 | 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/upload/sagas.js: -------------------------------------------------------------------------------- 1 | import { setStatuses, setUploadTypes, types } from "./reducers"; 2 | import { all, call, fork, put, takeLatest } from "redux-saga/effects"; 3 | import api from "./api"; 4 | 5 | export function* getImportJobStatusesWatcher() { 6 | yield takeLatest(types.GET_UPLOAD_JOB_STATUSES, getImportJobStatusesWorker); 7 | } 8 | 9 | export function* getImportJobStatusesWorker({ page }) { 10 | const valueFromApi = yield call(api.fetchUploadJobStatuses, { page }); 11 | yield put(setStatuses(valueFromApi, page)); 12 | } 13 | 14 | export function* getUploadTypesWatcher() { 15 | yield takeLatest(types.GET_UPLOAD_JOB_STATUSES, getUploadTypesWorker); 16 | } 17 | 18 | export function* getUploadTypesWorker() { 19 | const valueFromApi = yield call(api.fetchUploadTypes); 20 | yield put(setUploadTypes(valueFromApi)); 21 | } 22 | 23 | export default function* main() { 24 | yield all([getImportJobStatusesWatcher, getUploadTypesWatcher].map(fork)); 25 | } 26 | -------------------------------------------------------------------------------- /src/formDesigner/common/LocationTypeSearch.jsx: -------------------------------------------------------------------------------- 1 | import { httpClient as http } from "../../common/utils/httpClient"; 2 | import { map } from "lodash"; 3 | import CommonSearch from "./CommonSearch"; 4 | 5 | const LocationTypeSearch = ({ value, onChange, isMulti }) => { 6 | const loadLocation = (value, callback) => { 7 | http.get("/search/locationType?name=" + value).then(response => { 8 | const locationTypes = response.data; 9 | const locationTypeOptions = map(locationTypes, ({ name, uuid }) => ({ 10 | label: name, 11 | value: { name, uuid, toString: () => uuid } 12 | })); 13 | callback(locationTypeOptions); 14 | }); 15 | }; 16 | 17 | return ( 18 | 25 | ); 26 | }; 27 | 28 | export default LocationTypeSearch; 29 | -------------------------------------------------------------------------------- /src/adminApp/EncounterType/EncounterTypeErrors.jsx: -------------------------------------------------------------------------------- 1 | import { FormLabel } from "@mui/material"; 2 | 3 | const EncounterTypeErrors = ({ nameValidation, subjectValidation, error }) => { 4 | return ( 5 |
6 |
7 | {nameValidation && ( 8 | 9 | Empty name is not allowed. 10 | 11 | )} 12 |
13 |
14 | {subjectValidation && ( 15 | 16 | Empty subject type is not allowed. 17 | 18 | )} 19 |
20 |
21 | {error !== "" && ( 22 | 23 | {error} 24 | 25 | )} 26 |
27 |
28 | ); 29 | }; 30 | export default EncounterTypeErrors; 31 | -------------------------------------------------------------------------------- /src/common/mapper/EncounterMapper.js: -------------------------------------------------------------------------------- 1 | import { get, isNil } from "lodash"; 2 | import { Encounter } from "avni-models"; 3 | import { mapBasicEncounter } from "./BaseEncounterMapper"; 4 | 5 | export const getNewEligibleEncounters = (encounterTypes, eligibleEncounters) => { 6 | const scheduledEncounters = get(eligibleEncounters, "scheduledEncounters", []).map(pe => mapBasicEncounter(new Encounter(), pe)); 7 | const unplannedEncounters = get(eligibleEncounters, "eligibleEncounterTypeUUIDs", []) 8 | .map(uuid => { 9 | const result = encounterTypes.find(eT => eT.uuid === uuid); 10 | if (!isNil(result)) { 11 | const encounter = new Encounter(); 12 | encounter.encounterType = result; 13 | encounter.name = encounter.encounterType.operationalEncounterTypeName; 14 | return encounter; 15 | } 16 | return null; 17 | }) 18 | .filter(enc => !isNil(enc)); 19 | return { scheduledEncounters, unplannedEncounters }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/dataEntryApp/views/audio/Player.jsx: -------------------------------------------------------------------------------- 1 | import { replace } from "lodash"; 2 | 3 | //We play the audio this way when it is opened in the tab instead of passing S3 signed URL because there is codec 4 | //issues in Firefox when audio is recorded from android(which doesn't support mp3 natively) and played in in the browser. 5 | 6 | const styles = { 7 | container: { 8 | display: "flex", 9 | alignItems: "center", 10 | justifyContent: "center", 11 | backgroundColor: "#000", 12 | opacity: 0.8, 13 | height: "100vh" 14 | } 15 | }; 16 | 17 | export default function Player({ ...props }) { 18 | const url = replace(props.location.search, "?url=", ""); 19 | return ( 20 | url && ( 21 |
22 |
30 | ) 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/formDesigner/components/JsonEditor.jsx: -------------------------------------------------------------------------------- 1 | import { highlight, languages } from "prismjs/components/prism-core"; 2 | 3 | import Editor from "react-simple-code-editor"; 4 | import { ValidationError } from "./ValidationError"; 5 | 6 | export const JsonEditor = ({ 7 | value, 8 | onChange, 9 | validationError, 10 | readOnly = false 11 | }) => { 12 | return ( 13 | <> 14 | onChange(event)} 18 | highlight={code => highlight(code, languages.js)} 19 | padding={10} 20 | style={{ 21 | fontFamily: '"Fira code", "Fira Mono", monospace', 22 | fontSize: 15, 23 | width: "100%", 24 | height: "auto", 25 | borderStyle: "solid", 26 | borderWidth: "1px" 27 | }} 28 | /> 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/common/components/AvniTextField.jsx: -------------------------------------------------------------------------------- 1 | import { TextField } from "@mui/material"; 2 | import { ToolTipContainer } from "./ToolTipContainer"; 3 | import _ from "lodash"; 4 | 5 | export const AvniTextField = ({ toolTipKey, ...props }) => { 6 | const copy = { ...props }; 7 | copy.value = _.isNil(copy.value) ? "" : copy.value; 8 | return ( 9 | 10 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/adminApp/components/AlertModal.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogContentText, 7 | DialogActions 8 | } from "@mui/material"; 9 | import MuiComponentHelper from "../../common/utils/MuiComponentHelper"; 10 | 11 | export const AlertModal = ({ message, showAlert, setShowAlert, className }) => { 12 | return ( 13 | 16 | setShowAlert(false) 17 | )} 18 | > 19 | {message.title} 20 | 21 | {message.content} 22 | 23 | 24 | 31 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/reports/components/export/RegistrationType.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | import { ExportOptions } from "./ExportOptions"; 3 | import { DateOptions } from "./DateOptions"; 4 | 5 | export const RegistrationType = ({ 6 | subjectTypes, 7 | subjectType, 8 | startDate, 9 | endDate, 10 | dispatch, 11 | setEnableExport 12 | }) => { 13 | const onSubjectTypeChange = st => { 14 | dispatch("subjectType", st); 15 | setEnableExport(true); 16 | }; 17 | 18 | return ( 19 | 20 | onSubjectTypeChange(st)} 25 | /> 26 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/rootApp/rootSaga.js: -------------------------------------------------------------------------------- 1 | import { all, fork } from "redux-saga/effects"; 2 | import dataEntrySaga from "../dataEntryApp/sagas"; 3 | import broadcastSaga from "../news/sagas"; 4 | import translationsSaga from "../translations/sagas"; 5 | import uploadSagas from "../upload/sagas"; 6 | import reportSagas from "../reports/sagas"; 7 | import userGroupsSagas from "../userGroups/sagas"; 8 | import { organisationConfigWatcher } from "../i18nTranslations/TranslationSaga"; 9 | 10 | import { getAdminOrgsWatcher, logoutWatcher, onSetAuthSession, userInfoWatcher } from "./saga"; 11 | 12 | export default function* rootSaga() { 13 | yield all([ 14 | fork(onSetAuthSession), 15 | fork(userInfoWatcher), 16 | fork(getAdminOrgsWatcher), 17 | fork(logoutWatcher), 18 | fork(organisationConfigWatcher), 19 | fork(dataEntrySaga), 20 | fork(broadcastSaga), 21 | fork(translationsSaga), 22 | fork(uploadSagas), 23 | fork(reportSagas), 24 | fork(userGroupsSagas) 25 | ]); 26 | } 27 | -------------------------------------------------------------------------------- /src/assignment/components/TextFilter.jsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@mui/material/styles"; 2 | import { 3 | FormControl, 4 | FormLabel, 5 | TextField as MuiTextField 6 | } from "@mui/material"; 7 | import { Filter } from "../util/FilterStyles"; 8 | 9 | const StyledTextField = styled(MuiTextField)({ 10 | backgroundColor: "#FFF", 11 | "& .MuiInputBase-input": { 12 | padding: 10 13 | } 14 | }); 15 | 16 | const TextFilter = ({ 17 | label, 18 | value, 19 | filterCriteria, 20 | onFilterChange, 21 | isNumeric 22 | }) => { 23 | return ( 24 | 25 | 26 | {label} 27 | onFilterChange(event.target.value)} 31 | type={isNumeric ? "number" : "text"} 32 | /> 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default TextFilter; 39 | -------------------------------------------------------------------------------- /src/rootApp/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import dataEntry from "../dataEntryApp/reducers/dataEntryReducer"; 3 | import broadcast from "../news/reducers/metadataReducer"; 4 | import bulkUpload from "../upload/reducers"; 5 | import translations from "../translations/reducers"; 6 | import reports from "../reports/reducers"; 7 | import userGroups from "../userGroups/reducers"; 8 | import app from "./ducks"; 9 | import translationsReducer from "../i18nTranslations/TranslationReducers"; 10 | import programs from "../dataEntryApp/reducers/programReducer"; 11 | import sagaErrorState from "./SagaErrorReducer"; 12 | 13 | const createRootReducer = routerReducer => 14 | combineReducers({ 15 | app, 16 | dataEntry, 17 | broadcast, 18 | bulkUpload, 19 | translations, 20 | reports, 21 | translationsReducer, 22 | programs, 23 | userGroups, 24 | sagaErrorState, 25 | router: routerReducer 26 | }); 27 | 28 | export default createRootReducer; 29 | -------------------------------------------------------------------------------- /src/common/model/WebFormElement.js: -------------------------------------------------------------------------------- 1 | import { FormElement } from "openchs-models"; 2 | import WebFormElementGroup from "./WebFormElementGroup"; 3 | 4 | class WebFormElement extends FormElement { 5 | get group() { 6 | return this.toEntity("group", WebFormElement); 7 | } 8 | 9 | set group(x) { 10 | this.that.group = this.fromObject(x); 11 | } 12 | 13 | get questionGroupIndex() { 14 | return this.that.questionGroupIndex; 15 | } 16 | 17 | set questionGroupIndex(x) { 18 | this.that.questionGroupIndex = x; 19 | } 20 | 21 | newFormElement() { 22 | return new WebFormElement(); 23 | } 24 | 25 | clone() { 26 | const clone = super.clone(); 27 | clone.group = this.group; 28 | return clone; 29 | } 30 | 31 | get formElementGroup() { 32 | return this.toEntity("formElementGroup", WebFormElementGroup); 33 | } 34 | 35 | set formElementGroup(x) { 36 | this.that.formElementGroup = this.fromObject(x); 37 | } 38 | } 39 | 40 | export default WebFormElement; 41 | -------------------------------------------------------------------------------- /src/assignment/api/index.js: -------------------------------------------------------------------------------- 1 | import { httpClient as http } from "common/utils/httpClient"; 2 | import { get } from "lodash"; 3 | 4 | export default { 5 | getTaskMetadata: () => http.fetchJson("/web/taskMetadata").then(response => response.json), 6 | assignTask: payload => 7 | http 8 | .post("/web/taskAssignment", payload) 9 | .then(r => [null]) 10 | .catch(r => [`${get(r, "response.data") || get(r, "message") || "unknown error"}`]), 11 | getSubjects: payload => http.post("/web/subjectAssignment/search", payload), 12 | getSubjectAssignmentMetadata: () => http.fetchJson("/web/subjectAssignmentMetadata").then(response => response.json), 13 | getAssignmentMetadata: () => http.fetchJson("/web/assignmentMetadata").then(response => response.json), 14 | postUpdateUserAssignmentToSubject: payload => 15 | http 16 | .post("/web/userSubjectAssignment", payload) 17 | .then(r => [null]) 18 | .catch(r => [`${get(r, "response.data") || get(r, "message") || "unknown error"}`]) 19 | }; 20 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/SubjectCardView.stories.jsx: -------------------------------------------------------------------------------- 1 | import SubjectCardView from "./SubjectCardView"; 2 | 3 | export default { 4 | component: SubjectCardView, 5 | title: "DEA/Components/SubjectCardView", 6 | parameters: { 7 | layout: "centered" 8 | } 9 | }; 10 | 11 | const Template = args => ; 12 | 13 | export const Person = Template.bind({}); 14 | Person.args = { 15 | uuid: "personUuid", 16 | name: "FirstName LastName", 17 | gender: "Female", 18 | age: 42, 19 | location: "LocationName" 20 | }; 21 | 22 | export const NonPerson = Template.bind({}); 23 | NonPerson.args = { 24 | ...Person.args, 25 | name: "FirstName", 26 | gender: null, 27 | age: null, 28 | location: "LocationName" 29 | }; 30 | 31 | export const Group = Template.bind({}); 32 | Group.args = { 33 | ...NonPerson.args, 34 | name: "GroupName" 35 | }; 36 | 37 | export const Household = Template.bind({}); 38 | Household.args = { 39 | ...NonPerson.args, 40 | name: "HouseholdName" 41 | }; 42 | -------------------------------------------------------------------------------- /src/adminApp/components/CreateEditFiltersHOC.jsx: -------------------------------------------------------------------------------- 1 | import { useLocation, useNavigate } from "react-router-dom"; 2 | import { isNil } from "lodash"; 3 | import { CreateEditFilters } from "./CreateEditFilters"; 4 | 5 | const CreateEditFiltersHOC = () => { 6 | const location = useLocation(); 7 | const navigate = useNavigate(); 8 | const { 9 | selectedFilter, 10 | title, 11 | filterType, 12 | worklistUpdationRule, 13 | operationalModules, 14 | filename, 15 | settings 16 | } = location.state || {}; 17 | 18 | if (isNil(location.state)) { 19 | navigate("/"); 20 | return
; 21 | } 22 | 23 | return ( 24 | 33 | ); 34 | }; 35 | 36 | export default CreateEditFiltersHOC; 37 | -------------------------------------------------------------------------------- /src/common/model/UserInfo.test.js: -------------------------------------------------------------------------------- 1 | import UserInfo from "./UserInfo"; 2 | import { Privilege } from "openchs-models"; 3 | import { assert } from "chai"; 4 | 5 | it("should check for all privileges", function() { 6 | const { 7 | EditUserGroup, 8 | EditUserConfiguration, 9 | EditLocationType, 10 | EditCatchment 11 | } = Privilege.PrivilegeType; 12 | const userPrivileges = [ 13 | { privilegeType: EditUserGroup }, 14 | { privilegeType: EditUserConfiguration }, 15 | { privilegeType: EditLocationType } 16 | ]; 17 | const userInfo = { privileges: userPrivileges, hasAllPrivileges: false }; 18 | assert.equal( 19 | UserInfo.hasMultiplePrivileges(userInfo, [EditUserGroup, EditUserConfiguration]), 20 | true 21 | ); 22 | assert.equal(UserInfo.hasMultiplePrivileges(userInfo, [EditUserGroup, EditCatchment]), false); 23 | assert.equal(UserInfo.hasMultiplePrivileges(userInfo, [EditUserGroup]), true); 24 | assert.equal(UserInfo.hasMultiplePrivileges(userInfo, [EditCatchment]), false); 25 | }); 26 | -------------------------------------------------------------------------------- /public/documentation/sideBarDocumentation/OrganisationDetail.md: -------------------------------------------------------------------------------- 1 | You can delete all the data that is filled using the field app. Also if you select to delete metadata, it'll delete all the forms and concept that you had created using web application. 2 | 3 | **Please note that DELETE ALL DATA action is irreversible, so choose the options very carefully** 4 | 5 | Several organisation-wide changes can be enabled from here. 6 | 7 | `Approval Workflow`: Enabling this will allow you to approve/reject all the forms filled by the field users. You'll need to create a [custom dashboard](#/appdesigner/dashboard) and also adjust the [privileges](#/admin/userGroups) accordingly. 8 | 9 | `Draft Save`: Enabling this feature will enable saving the registration form automatically on every press of next button. All the drafts are available on the register page. 10 | 11 | `Enable Messaging`: Enabling this will introduce the capability of sending messages through Whatsapp. Remember that there is more configuration required for this integration to work. 12 | -------------------------------------------------------------------------------- /src/assignment/components/SelectFilter.jsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@mui/material/styles"; 2 | import { FormControl, FormLabel } from "@mui/material"; 3 | import Select from "react-select"; 4 | 5 | const StyledFormControl = styled(FormControl)(({ theme }) => ({ 6 | marginBottom: theme.spacing(5) 7 | })); 8 | 9 | const StyledSelect = styled(Select)({ 10 | width: "auto" 11 | }); 12 | 13 | const SelectFilter = ({ 14 | label, 15 | options, 16 | filter, 17 | isMulti = false, 18 | filterCriteria, 19 | onFilterChange, 20 | isClearable = true 21 | }) => { 22 | return ( 23 | 24 | {label} 25 | onFilterChange(filter, event)} 32 | /> 33 | 34 | ); 35 | }; 36 | 37 | export default SelectFilter; 38 | -------------------------------------------------------------------------------- /src/reports/components/export/DateOptions.jsx: -------------------------------------------------------------------------------- 1 | import { Grid } from "@mui/material"; 2 | import { DateSelector } from "./DateSelector"; 3 | import { LocalizationProvider } from "@mui/x-date-pickers"; 4 | import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; 5 | 6 | export const DateOptions = ({ 7 | startDate, 8 | endDate, 9 | dispatch, 10 | startDateLabel, 11 | endDateLabel 12 | }) => { 13 | return ( 14 | 15 | 22 | dispatch("startDate", date)} 26 | /> 27 | dispatch("endDate", date)} 31 | /> 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/adminApp/components/OpenOrganisation.jsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from "react-redux"; 2 | import { Link } from "@mui/material"; 3 | import { getUserInfo } from "../../rootApp/ducks"; 4 | import { useNavigate } from "react-router-dom"; 5 | import { useRecordContext } from "react-admin"; 6 | 7 | const OpenOrganisation = () => { 8 | const dispatch = useDispatch(); 9 | const navigate = useNavigate(); 10 | const record = useRecordContext(); 11 | 12 | const handleClick = (event, organisationUUID) => { 13 | // Prevent the event from bubbling up to the Datagrid row 14 | event.stopPropagation(); 15 | event.preventDefault(); 16 | 17 | localStorage.setItem("ORGANISATION_UUID", organisationUUID); 18 | dispatch(getUserInfo()); 19 | navigate("/home"); 20 | }; 21 | 22 | if (!record) { 23 | return null; 24 | } 25 | 26 | return ( 27 | handleClick(event, record.uuid)}> 28 | Go To Organisation 29 | 30 | ); 31 | }; 32 | 33 | export default OpenOrganisation; 34 | -------------------------------------------------------------------------------- /src/assignment/subjectAssignment/SubjectAssignmentData.js: -------------------------------------------------------------------------------- 1 | import api from "../api"; 2 | import { getFilterPayload } from "../reducers/SubjectAssignmentReducer"; 3 | 4 | export const fetchSubjectData = (query, filterCriteria) => { 5 | const requestBody = getFilterPayload(filterCriteria); 6 | return new Promise(resolve => { 7 | const pageElement = {}; 8 | pageElement.pageNumber = query.page; 9 | pageElement.numberOfRecordPerPage = query.pageSize; 10 | requestBody.pageElement = pageElement; 11 | api 12 | .getSubjects(requestBody) 13 | .then(response => response.data) 14 | .then(result => { 15 | resolve({ 16 | data: result.listOfRecords, 17 | page: query.page, 18 | totalCount: result.totalElements 19 | }); 20 | }); 21 | }); 22 | }; 23 | 24 | export const updateUserAssignmentToSubject = async ({ userId, subjectId, voided }) => { 25 | const payload = { userId, subjectIds: [subjectId], voided }; 26 | return api.postUpdateUserAssignmentToSubject(payload); 27 | }; 28 | -------------------------------------------------------------------------------- /src/common/material-table/fetch.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { httpClient as http } from "../utils/httpClient"; 3 | 4 | const baseUrl = "/web"; 5 | 6 | const fetchData = (resourceName, resourceUrl, params) => query => 7 | new Promise(resolve => { 8 | let searchParams = new URLSearchParams(params); 9 | 10 | let apiUrl = `${baseUrl}${resourceUrl}`; 11 | searchParams.append("size", query.pageSize); 12 | searchParams.append("page", query.page); 13 | if (!_.isEmpty(query.orderBy) && !_.isEmpty(query.orderBy.field)) 14 | searchParams.append("sort", `${query.orderBy.field},${query.orderDirection}`); 15 | apiUrl += "?" + searchParams.toString(); 16 | http 17 | .get(apiUrl) 18 | .then(response => response.data) 19 | .then(result => { 20 | resolve({ 21 | data: result._embedded ? result._embedded[resourceName] : [], 22 | page: result.page.number, 23 | totalCount: result.page.totalElements 24 | }); 25 | }); 26 | }); 27 | 28 | export default fetchData; 29 | -------------------------------------------------------------------------------- /src/reports/components/export/DateSelector.jsx: -------------------------------------------------------------------------------- 1 | import Box from "@mui/material/Box"; 2 | import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; 3 | import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; 4 | 5 | export const DateSelector = ({ label, value, onChange }) => { 6 | return ( 7 | 12 | 13 | onChange(date)} 20 | slotProps={{ 21 | textField: { 22 | label, 23 | margin: "normal", 24 | variant: "outlined" 25 | }, 26 | actionBar: { actions: ["clear"] }, 27 | openPickerButton: { "aria-label": "change date" } 28 | }} 29 | /> 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/reports/sagas.js: -------------------------------------------------------------------------------- 1 | import { all, call, fork, put, takeLatest } from "redux-saga/effects"; 2 | import { setOperationalModules, setUploadStatus, types } from "./reducers"; 3 | import api from "./api"; 4 | import { mapOperationalModules } from "../common/adapters"; 5 | 6 | export function* onLoadWatcher() { 7 | yield takeLatest(types.GET_OPERATIONAL_MODULES, onLoadWorker); 8 | } 9 | 10 | export function* onLoadWorker() { 11 | const operationalModules = yield call(api.fetchOperationalModules); 12 | yield put(setOperationalModules(yield call(mapOperationalModules, operationalModules))); 13 | } 14 | 15 | export function* getExportJobStatusesWatcher() { 16 | yield takeLatest(types.GET_UPLOAD_JOB_STATUSES, getExportJobStatusesWorker); 17 | } 18 | 19 | export function* getExportJobStatusesWorker({ page }) { 20 | const jobStatus = yield call(api.fetchUploadJobStatuses, { page }); 21 | yield put(setUploadStatus(jobStatus)); 22 | } 23 | 24 | export default function* onLoadSaga() { 25 | yield all([onLoadWatcher, getExportJobStatusesWatcher].map(fork)); 26 | } 27 | -------------------------------------------------------------------------------- /src/assignment/util/FilterStyles.js: -------------------------------------------------------------------------------- 1 | import { styled } from "@mui/material/styles"; 2 | 3 | export const Root = styled("div")(({ theme }) => ({ 4 | paddingRight: theme.spacing(5), 5 | paddingLeft: theme.spacing(5), 6 | paddingTop: theme.spacing(3), 7 | paddingBottom: theme.spacing(40), 8 | backgroundColor: "#F5F7F9", 9 | overflow: "auto", 10 | position: "fixed", 11 | height: "100vh" 12 | })); 13 | 14 | export const Filter = styled("div")(({ theme }) => ({ 15 | marginBottom: theme.spacing(1), 16 | width: "100%" 17 | })); 18 | 19 | export const Header = styled("div")(({ theme }) => ({ 20 | marginBottom: theme.spacing(3) 21 | })); 22 | 23 | export const TextField = styled("div")({ 24 | backgroundColor: "#FFF" 25 | }); 26 | 27 | export const ApplyButton = styled("div")(({ theme }) => ({ 28 | position: "absolute", 29 | bottom: 0, 30 | width: "26%", 31 | paddingRight: theme.spacing(5), 32 | paddingLeft: theme.spacing(5), 33 | paddingTop: theme.spacing(3), 34 | paddingBottom: theme.spacing(3), 35 | backgroundColor: "#F5F7F9" 36 | })); 37 | -------------------------------------------------------------------------------- /src/reports/components/export/GroupSubjectType.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | import { filter } from "lodash"; 3 | import { ExportOptions } from "./ExportOptions"; 4 | import { DateOptions } from "./DateOptions"; 5 | 6 | export const GroupSubjectType = ({ 7 | subjectTypes, 8 | subjectType, 9 | startDate, 10 | endDate, 11 | dispatch, 12 | setEnableExport 13 | }) => { 14 | const onSubjectTypeChange = st => { 15 | dispatch("subjectType", st); 16 | setEnableExport(true); 17 | }; 18 | 19 | return ( 20 | 21 | !!group)} 23 | label={"Group Subject Type"} 24 | selectedOption={subjectType} 25 | onChange={st => onSubjectTypeChange(st)} 26 | /> 27 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/common/mapper/ProgramEncounterMapper.js: -------------------------------------------------------------------------------- 1 | import { get, isNil } from "lodash"; 2 | import { ProgramEncounter } from "openchs-models"; 3 | import { mapBasicEncounter } from "./BaseEncounterMapper"; 4 | 5 | export const getNewEligibleProgramEncounters = (encounterTypes, eligibleEncounters) => { 6 | const planEncounterList = get(eligibleEncounters, "scheduledEncounters", []).map(planEncounter => 7 | mapBasicEncounter(new ProgramEncounter(), planEncounter) 8 | ); 9 | 10 | const unplanEncounterList = get(eligibleEncounters, "eligibleEncounterTypeUUIDs", []) 11 | .map(uuid => { 12 | const result = encounterTypes.find(eT => eT.uuid === uuid); 13 | if (!isNil(result)) { 14 | const unplannedVisit = new ProgramEncounter(); 15 | unplannedVisit.encounterType = result; 16 | unplannedVisit.name = unplannedVisit.encounterType.operationalEncounterTypeName; 17 | return unplannedVisit; 18 | } 19 | return null; 20 | }) 21 | .filter(enc => !isNil(enc)); 22 | return { planEncounterList, unplanEncounterList }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/reports/components/export/ExportOptions.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | FormLabel, 4 | FormGroup, 5 | FormControlLabel 6 | } from "@mui/material"; 7 | import { isEmpty } from "lodash"; 8 | import Radio from "../../../dataEntryApp/components/Radio"; 9 | 10 | export const ExportOptions = ({ options, label, selectedOption, onChange }) => { 11 | return isEmpty(options) ? null : ( 12 |
13 | 14 | {label} 15 | 16 | {options.map(option => ( 17 | onChange(option)} 23 | value={option.name} 24 | /> 25 | } 26 | label={option.name} 27 | /> 28 | ))} 29 | 30 | 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/formDesigner/components/DeclarativeRule/RuleSummaryComponent.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | import { isEmpty, map, zip, toUpper } from "lodash"; 3 | import { Typography, Chip } from "@mui/material"; 4 | 5 | const RuleSummaryComponent = ({ summary, ruleNumber, displayRuleCounts }) => { 6 | if (isEmpty(summary)) return null; 7 | 8 | const { actionSummary, ruleSummary, conjunctions } = summary; 9 | return ( 10 | 11 | {displayRuleCounts && ( 12 | 13 | {`Rule ${ruleNumber}`} 14 | 15 | )} 16 |
    17 | {map(actionSummary, as => ( 18 |
  • {as}
  • 19 | ))} 20 | {map(zip(ruleSummary, conjunctions), ([ruleSummary, conjunction]) => ( 21 | 22 |
  • {ruleSummary}
  • 23 | {conjunction && } 24 |
    25 | ))} 26 |
27 |
28 | ); 29 | }; 30 | 31 | export default RuleSummaryComponent; 32 | -------------------------------------------------------------------------------- /src/news/components/PublishBroadcast.jsx: -------------------------------------------------------------------------------- 1 | import { AvniAlertDialog } from "./AvniAlertDialog"; 2 | import { DialogActionButton } from "./DialogActionButton"; 3 | import API from "../api"; 4 | 5 | export const PublishBroadcast = ({ open, setOpen, setRedirect, news }) => { 6 | const publishNews = () => { 7 | API.editNews({ ...news, publishedDate: new Date() }).then(response => { 8 | if (response.status === 200) { 9 | setRedirect(true); 10 | } 11 | }); 12 | }; 13 | 14 | const actions = []; 15 | actions.push( 16 | 23 | ); 24 | 25 | return ( 26 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/common/components/chatbot/ChatbotWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { Chatbot } from "./index"; 4 | 5 | // TODO Remove this component and related functionality in-case we continue using Dify 6 | const ChatbotWrapper: React.FC = () => { 7 | const appInitialised = useSelector((state: any) => state.app?.appInitialised); 8 | const userInfo = useSelector((state: any) => state.app?.userInfo); 9 | const organisation = useSelector((state: any) => state.app?.organisation); 10 | const genericConfig = useSelector( 11 | (state: any) => state.app?.genericConfig || {}, 12 | ); 13 | 14 | const isLoggedIn = appInitialised && userInfo; 15 | const isProductionOrg = 16 | organisation?.organisationCategoryName === "Production"; 17 | const isCopilotEnabled = genericConfig.copilotEnabled; 18 | 19 | const shouldShowChatbot = isLoggedIn && !isProductionOrg && isCopilotEnabled; 20 | 21 | if (!shouldShowChatbot) { 22 | return null; 23 | } 24 | 25 | return ; 26 | }; 27 | 28 | export default ChatbotWrapper; 29 | -------------------------------------------------------------------------------- /src/common/model/UserInfo.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { getPrivilegeType } from "../../adminApp/domain/formMapping"; 3 | 4 | class UserInfo { 5 | privileges; 6 | hasAllPrivileges; 7 | isAdmin; 8 | 9 | static createEmpty() { 10 | return { 11 | privileges: [] 12 | }; 13 | } 14 | 15 | static hasPrivilege(userInfo, privilegeType) { 16 | return ( 17 | userInfo.hasAllPrivileges || 18 | _.some(userInfo.privileges, x => x.privilegeType === privilegeType) 19 | ); 20 | } 21 | 22 | static hasMultiplePrivileges(userInfo, privilegeTypes = []) { 23 | return ( 24 | userInfo.hasAllPrivileges || 25 | _.intersectionBy( 26 | userInfo.privileges, 27 | privilegeTypes.map(x => { 28 | return { privilegeType: x }; 29 | }), 30 | "privilegeType" 31 | ).length === privilegeTypes.length 32 | ); 33 | } 34 | 35 | static hasFormEditPrivilege(userInfo, formType) { 36 | return UserInfo.hasPrivilege(userInfo, getPrivilegeType(formType)); 37 | } 38 | } 39 | 40 | export default UserInfo; 41 | -------------------------------------------------------------------------------- /src/common/components/ValueTextUnitSelect.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | import { TextField, Select, InputLabel } from "@mui/material"; 3 | 4 | export const ValueTextUnitSelect = ({ 5 | label, 6 | value, 7 | unit, 8 | units, 9 | textProps, 10 | selectProps, 11 | errorMsg, 12 | onValueChange, 13 | onUnitChange 14 | }) => { 15 | return ( 16 | 17 |

18 |

19 | {label && {label}} 20 | onValueChange(event)} 25 | {...textProps} 26 | /> 27 | 35 |
36 | {errorMsg && errorMsg} 37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/formDesigner/util/KeyValuesUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ensures keyValues is always returned as an array 3 | * @param {Array} keyValues - The keyValues array which might be undefined 4 | * @returns {Array} - A safe array to work with 5 | */ 6 | export const safeKeyValues = keyValues => (Array.isArray(keyValues) ? keyValues : []); 7 | 8 | /** 9 | * Finds a keyValue object by its key property 10 | * @param {Array} keyValues - Array of key-value objects 11 | * @param {string} key - The key to search for 12 | * @returns {Object|undefined} - The found keyValue object or undefined 13 | */ 14 | export const findKeyValue = (keyValues, key) => { 15 | return safeKeyValues(keyValues).find(item => item && item.key === key); 16 | }; 17 | 18 | /** 19 | * Gets the value for a given key 20 | * @param {Array} keyValues - Array of key-value objects 21 | * @param {string} key - The key to search for 22 | * @returns {*} - The value of the found key or undefined 23 | */ 24 | export const getKeyValue = (keyValues, key) => { 25 | const found = findKeyValue(keyValues, key); 26 | return found ? found.value : undefined; 27 | }; 28 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/ConfirmDialog.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContentText, 6 | DialogTitle, 7 | DialogContent 8 | } from "@mui/material"; 9 | import { useTranslation } from "react-i18next"; 10 | 11 | const ConfirmDialog = ({ title, message, open, setOpen, onConfirm }) => { 12 | const { t } = useTranslation(); 13 | return ( 14 | setOpen(false)}> 15 | {title} 16 | 17 | {message} 18 | 19 | 20 | 23 | 33 | 34 | 35 | ); 36 | }; 37 | export default ConfirmDialog; 38 | -------------------------------------------------------------------------------- /src/dataEntryApp/components/MediaFormElement.jsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import { find, lowerCase } from "lodash"; 3 | import { ValidationError } from "./ValidationError"; 4 | import { MediaUploader } from "./MediaUploader"; 5 | 6 | export default function MediaFormElement({ 7 | formElement, 8 | value, 9 | update, 10 | validationResults, 11 | uuid 12 | }) { 13 | const { t } = useTranslation(); 14 | const { mandatory, name } = formElement; 15 | const validationResult = find( 16 | validationResults, 17 | ({ formIdentifier, questionGroupIndex }) => 18 | formIdentifier === uuid && 19 | questionGroupIndex === formElement.questionGroupIndex 20 | ); 21 | const label = `${t(name)} ${mandatory ? "*" : ""}`; 22 | 23 | return ( 24 |
25 | 32 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/assignment/taskAssignment/TableColumns.js: -------------------------------------------------------------------------------- 1 | import { get, isNil, reject } from "lodash"; 2 | import { TaskMetadata } from "../reducers/TaskAssignmentReducer"; 3 | 4 | export const getTableColumns = taskMetadata => { 5 | const metaSearchColumns = TaskMetadata.getAllSearchFields(taskMetadata).map(([name]) => { 6 | const column = { 7 | title: name, 8 | sorting: false, 9 | render: rowData => get(rowData, ["metadata", name]) 10 | }; 11 | column[name] = name; 12 | return column; 13 | }); 14 | 15 | const columns = [ 16 | { 17 | title: "Name", 18 | field: "name" 19 | }, 20 | ...metaSearchColumns, 21 | { 22 | title: "Assigned To", 23 | field: "assignedTo" 24 | }, 25 | { 26 | title: "Status", 27 | field: "taskStatus" 28 | }, 29 | { 30 | title: "Created", 31 | field: "createdDateTime" 32 | }, 33 | { 34 | title: "Scheduled", 35 | field: "scheduledOn" 36 | }, 37 | { 38 | title: "Completed", 39 | field: "completedOn" 40 | } 41 | ]; 42 | return reject(columns, isNil); 43 | }; 44 | -------------------------------------------------------------------------------- /src/dataEntryApp/views/subjectDashBoard/components/news/NewsDetails.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { styled } from "@mui/material/styles"; 3 | import { useParams, useNavigate } from "react-router-dom"; 4 | import API from "../../../../../news/api"; 5 | import NewsDetailsCard from "../../../../../news/components/NewsDetailsCard"; 6 | import { Paper } from "@mui/material"; 7 | 8 | const StyledPaper = styled(Paper)(({ theme }) => ({ 9 | marginTop: theme.spacing(5), 10 | marginBottom: theme.spacing(5), 11 | marginLeft: theme.spacing(10), 12 | marginRight: theme.spacing(10), 13 | padding: theme.spacing(5), 14 | paddingTop: theme.spacing(3) 15 | })); 16 | 17 | const NewsDetails = () => { 18 | const [news, setNews] = useState({}); 19 | const { id } = useParams(); 20 | 21 | useEffect(() => { 22 | API.getNewsById(id) 23 | .then(res => res.data) 24 | .then(res => setNews(res)); 25 | }, [id]); 26 | 27 | return ( 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default NewsDetails; 35 | -------------------------------------------------------------------------------- /src/adminApp/SubjectType/Types.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | export class SubjectTypeType { 4 | static Person = "Person"; 5 | static Individual = "Individual"; 6 | static Group = "Group"; 7 | static Household = "Household"; 8 | static User = "User"; 9 | 10 | static getAll() { 11 | return [SubjectTypeType.Person, SubjectTypeType.Individual, SubjectTypeType.Group, SubjectTypeType.Household, SubjectTypeType.User]; 12 | } 13 | } 14 | 15 | export default class { 16 | static get groupMemberTypes() { 17 | return _.filter(SubjectTypeType.getAll(), type => _.includes([SubjectTypeType.Person, SubjectTypeType.Individual], type)); 18 | } 19 | 20 | static get householdMemberTypes() { 21 | return _.filter(SubjectTypeType.getAll(), type => type === SubjectTypeType.Person); 22 | } 23 | 24 | static getType(name) { 25 | return _.find(SubjectTypeType.getAll(), x => x === name); 26 | } 27 | 28 | static isGroup(name) { 29 | return _.includes([SubjectTypeType.Group, SubjectTypeType.Household], name); 30 | } 31 | 32 | static isHousehold(name) { 33 | return SubjectTypeType.Household === name; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/dataEntryApp/views/subjectDashBoard/components/FindRelativeTable.jsx: -------------------------------------------------------------------------------- 1 | import { first } from "lodash"; 2 | import { useSelector } from "react-redux"; 3 | import { useTranslation } from "react-i18next"; 4 | import SelectSubject from "../../../../common/components/subject/SelectSubject"; 5 | 6 | const FindRelativeTable = ({ subjectData, errormsg }) => { 7 | const { t } = useTranslation(); 8 | const Relations = useSelector(state => state.dataEntry.relations); 9 | const subjects = useSelector(state => state.dataEntry.search.subjects); 10 | const searchParams = useSelector( 11 | state => state.dataEntry.search.subjectSearchParams 12 | ); 13 | const subjectTypes = useSelector(state => 14 | first(state.dataEntry.metadata.operationalModules.subjectTypes) 15 | ); 16 | 17 | const onSelectedItem = row => { 18 | sessionStorage.setItem("selectedRelative", JSON.stringify(row)); 19 | }; 20 | 21 | return ( 22 | 28 | ); 29 | }; 30 | 31 | export default FindRelativeTable; 32 | -------------------------------------------------------------------------------- /src/dataEntryApp/reducers/searchReducer.js: -------------------------------------------------------------------------------- 1 | const prefix = "app/dataEntry/reducer/search/"; 2 | 3 | export const types = { 4 | SET_SUBJECTS: `${prefix}SET_SUBJECTS`, 5 | SET_SUBJECT_SEARCH_PARAMS: `${prefix}SET_SUBJECT_SEARCH_PARAMS`, 6 | SEARCH_SUBJECTS: `${prefix}SEARCH_SUBJECTS` 7 | }; 8 | 9 | export const setSubjects = subjects => ({ 10 | type: types.SET_SUBJECTS, 11 | subjects 12 | }); 13 | 14 | export const searchSubjects = params => ({ 15 | type: types.SEARCH_SUBJECTS, 16 | params 17 | }); 18 | 19 | const initialState = { 20 | subjects: {}, 21 | subjectSearchParams: {} 22 | }; 23 | 24 | // reducer 25 | export default function(state = initialState, action) { 26 | switch (action.type) { 27 | case types.SET_SUBJECTS: { 28 | return { 29 | ...state, 30 | subjects: action.subjects 31 | }; 32 | } 33 | case types.SET_SUBJECT_SEARCH_PARAMS: { 34 | return { 35 | ...state, 36 | subjectSearchParams: { 37 | ...state.subjectSearchParams, 38 | ...action.params 39 | } 40 | }; 41 | } 42 | default: 43 | return state; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/news/components/CustomToolbar.jsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@mui/material"; 2 | import { ActionButton } from "./ActionButton"; 3 | 4 | export const CustomToolbar = ({ totalNews, setOpenCreate }) => { 5 | return ( 6 |
16 |
17 | {`News broadcast`} 18 | 19 | {`A total of ${totalNews} news broadcast has been listed below`} 20 | 21 |
22 |
23 | setOpenCreate(true)} 25 | variant="contained" 26 | style={{ paddingHorizontal: 10 }} 27 | size="medium" 28 | > 29 | {"Create a New Broadcast"} 30 | 31 |
32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/userGroups/UserGroupDetails.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Grid } from "@mui/material"; 3 | import { useParams, useSearchParams } from "react-router-dom"; 4 | import { TabView } from "./components/TabbedView"; 5 | import Box from "@mui/material/Box"; 6 | import { Title } from "react-admin"; 7 | 8 | const UserGroupDetails = () => { 9 | const { id } = useParams(); 10 | const [searchParams] = useSearchParams(); 11 | const groupName = searchParams.get("groupName"); 12 | const [hasAllPrivileges, setHasAllPrivileges] = useState( 13 | searchParams.get("hasAllPrivileges") === "true" 14 | ); 15 | 16 | return ( 17 | 24 | 25 | <Grid container> 26 | <TabView 27 | groupId={id} 28 | groupName={groupName} 29 | hasAllPrivileges={hasAllPrivileges} 30 | setHasAllPrivileges={setHasAllPrivileges} 31 | /> 32 | </Grid> 33 | </Box> 34 | ); 35 | }; 36 | 37 | export default UserGroupDetails; 38 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | <!-- /avni-webapp/index.html --> 2 | <!DOCTYPE html> 3 | <html lang="en"> 4 | <head> 5 | <meta charset="utf-8" /> 6 | <link rel="shortcut icon" href="/favicon.ico" /> 7 | <meta 8 | name="viewport" 9 | content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" 10 | /> 11 | <meta name="theme-color" content="#000000" /> 12 | <link rel="manifest" href="/manifest.json" /> 13 | <title>Avni Web Console 14 | 15 | 26 | 27 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/bulkuploads/sample/usersAndCatchments.csv: -------------------------------------------------------------------------------- 1 | Location with full hierarchy,Catchment Name,Username,Full Name of User,Email,Phone,Language,Track Location,Is Allowed To Invoke Token Generation API,Date picker mode,Enable Beneficiary mode,Beneficiary ID Prefix 2 | "PHC B, Sub B, Vil B",Org X Cat A,user29@orgx,user29,email@example.com,+919876543210,English,yes,no,calendar,no,U29 3 | "PHC B, Sub B, Vil C",Org X Cat A,user29@orgx,user29,email@example.com,+919876543210,English,yes,no,calendar,no,U29 4 | "PHC B, Sub C, Vil C",Org X Cat A,user29@orgx,user29,email@example.com,+919876543210,English,yes,no,calendar,no,U29 5 | "PHC B, Sub B, Vil C",Org X Cat B,user49@orgx,user49,email@example.com,+919876543210,English,yes,no,,no,U49 6 | "PHC B, Sub C, Vil C",Org X Cat B,user49@orgx,user49,email@example.com,+919876543210,English,yes,no,,no,U49 7 | "PHC C, Sub C, Vil C",Org X Cat B,user49@orgx,user49,email@example.com,+919876543210,English,yes,no,,no,U49 8 | "PHC B, Sub C, Vil C",Org X Cat C,user69@orgx,user69,email@example.com,+919876543210,English,no,yes,spinner,no,U69 9 | "PHC C, Sub C, Vil C",Org X Cat C,user69@orgx,user69,email@example.com,+919876543210,English,no,yes,spinner,no,U69 -------------------------------------------------------------------------------- /src/dataEntryApp/components/SubjectVoided.jsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@mui/material/styles"; 2 | import { Paper, Grid, Button, Typography } from "@mui/material"; 3 | 4 | const StyledPaper = styled(Paper)({ 5 | padding: 20, 6 | marginBottom: 10, 7 | elevation: 2 8 | }); 9 | 10 | const StyledGrid = styled(Grid)({ 11 | alignItems: "flex-start" 12 | }); 13 | 14 | const StyledTypography = styled(Typography)(({ theme }) => ({ 15 | color: theme.palette.error.main, 16 | marginBottom: 8 17 | })); 18 | 19 | const SubjectVoided = ({ onUnVoid, showUnVoid }) => { 20 | return ( 21 | 22 | 23 | 24 | 25 | {"THE SUBJECT HAS BEEN VOIDED"} 26 | 27 | 28 | 29 | {showUnVoid && ( 30 | 33 | )} 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default SubjectVoided; 41 | -------------------------------------------------------------------------------- /src/dataEntryApp/state/Wizard.js: -------------------------------------------------------------------------------- 1 | class Wizard { 2 | constructor(numberOfPages, formStartsAt = 1, currentPage = 1) { 3 | this.numberOfPages = numberOfPages; 4 | this.formStartsAt = formStartsAt; 5 | this.currentPage = currentPage; 6 | } 7 | 8 | clone() { 9 | const wizard = new Wizard(this.numberOfPages, this.formStartsAt, this.currentPage); 10 | return wizard; 11 | } 12 | 13 | moveNext() { 14 | if (this.isLastPage()) { 15 | throw Error("Already on the last page"); 16 | } 17 | this.currentPage = this.currentPage + 1; 18 | } 19 | 20 | movePrevious() { 21 | if (this.currentPage === 1) { 22 | throw Error("Already on the first page"); 23 | } 24 | this.currentPage = this.currentPage - 1; 25 | } 26 | 27 | isFirstFormPage() { 28 | return this.currentPage === this.formStartsAt; 29 | } 30 | 31 | isNonFormPage() { 32 | return this.currentPage < this.formStartsAt; 33 | } 34 | 35 | isLastPage() { 36 | return this.numberOfPages === this.currentPage; 37 | } 38 | 39 | isFirstPage() { 40 | return this.currentPage === 1; 41 | } 42 | } 43 | 44 | export default Wizard; 45 | -------------------------------------------------------------------------------- /src/news/components/CustomDialogTitle.jsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@mui/material/styles"; 2 | import { Typography, DialogTitle, IconButton } from "@mui/material"; 3 | import { Close } from "@mui/icons-material"; 4 | 5 | const StyledDialogTitle = styled(DialogTitle)(({ theme }) => ({ 6 | margin: 0, 7 | padding: theme.spacing(2) 8 | })); 9 | 10 | const StyledCloseButton = styled(IconButton)(({ theme }) => ({ 11 | position: "absolute", 12 | right: theme.spacing(1), 13 | top: theme.spacing(1), 14 | color: theme.palette.grey[500] 15 | })); 16 | 17 | export const CustomDialogTitle = ({ children, onClose, ...other }) => { 18 | return ( 19 | 29 | 30 | {children} 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/adminApp/ApplicationMenu/AdminMenuItem.js: -------------------------------------------------------------------------------- 1 | import { MenuItem } from "openchs-models"; 2 | import { mapAuditFields } from "../components/AuditUtil"; 3 | import _ from "lodash"; 4 | 5 | class AdminMenuItem extends MenuItem { 6 | static VAL_DISPLAY_KEY = "valDisplayKey"; 7 | static VAL_GROUP = "valGroup"; 8 | static VAL_TYPE = "valType"; 9 | 10 | static fromResource(resource) { 11 | const adminMenuItem = MenuItem.assignFields(resource, new AdminMenuItem()); 12 | mapAuditFields(resource, adminMenuItem); 13 | return adminMenuItem; 14 | } 15 | 16 | validate() { 17 | const errors = new Map(); 18 | if (_.isEmpty(this.displayKey)) 19 | errors.set(AdminMenuItem.VAL_DISPLAY_KEY, "Display key is mandatory"); 20 | if (_.isEmpty(this.group)) errors.set(AdminMenuItem.VAL_GROUP, "Group is mandatory"); 21 | if (_.isEmpty(this.type)) errors.set(AdminMenuItem.VAL_TYPE, "Type is mandatory"); 22 | return errors; 23 | } 24 | 25 | clone() { 26 | const clone = MenuItem.assignFields(this, new AdminMenuItem()); 27 | mapAuditFields(this, clone); 28 | return clone; 29 | } 30 | } 31 | 32 | export default AdminMenuItem; 33 | -------------------------------------------------------------------------------- /src/dataEntryApp/views/GlobalSearch/CheckBoxSearchComponent.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | import { styled } from "@mui/material/styles"; 3 | import { FormControl, FormControlLabel, Checkbox, Grid } from "@mui/material"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | const StyledGrid = styled(Grid)({ 7 | marginTop: "1%", 8 | marginBottom: "1%" 9 | }); 10 | 11 | function CheckBoxSearchComponent({ label, checked, onChange }) { 12 | const { t } = useTranslation(); 13 | return ( 14 | 15 | 16 | 17 | 18 | 25 | } 26 | label={t(label)} 27 | labelPlacement="end" 28 | /> 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export default CheckBoxSearchComponent; 37 | -------------------------------------------------------------------------------- /src/rootApp/security/IdpFactory.jsx: -------------------------------------------------------------------------------- 1 | import IdpDetails from "./IdpDetails"; 2 | import NullIdp from "./NullIdp"; 3 | import KeycloakWebClient from "./KeycloakWebClient"; 4 | import CognitoWebClient from "./CognitoWebClient"; 5 | import UndecidedIdp from "./UndecidedIdp"; 6 | 7 | class IdpFactory { 8 | static createIdp(activeIdpType, idpDetails) { 9 | if (activeIdpType === IdpDetails.none) { 10 | return new NullIdp(idpDetails); 11 | } else if ( 12 | activeIdpType === IdpDetails.keycloak || 13 | (activeIdpType === IdpDetails.both && 14 | KeycloakWebClient.isAuthenticatedWithKeycloak()) 15 | ) { 16 | return new KeycloakWebClient(idpDetails); 17 | } else if ( 18 | activeIdpType === IdpDetails.cognito || 19 | (activeIdpType === IdpDetails.both && 20 | CognitoWebClient.isAuthenticatedWithCognito()) 21 | ) { 22 | return new CognitoWebClient(idpDetails); 23 | } else if (activeIdpType === IdpDetails.both) { 24 | return new UndecidedIdp(idpDetails); 25 | } 26 | throw new Error(`IdpType: ${activeIdpType} is not supported`); 27 | } 28 | } 29 | 30 | export default IdpFactory; 31 | -------------------------------------------------------------------------------- /src/upload/LocationHierarchy.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | FormLabel, 4 | RadioGroup, 5 | FormControlLabel, 6 | Radio 7 | } from "@mui/material"; 8 | 9 | export const LocationHierarchy = ({ 10 | hierarchy, 11 | setHierarchy, 12 | configuredHierarchies 13 | }) => { 14 | const handleChange = event => { 15 | setHierarchy(event.target.value); 16 | }; 17 | 18 | return ( 19 | 20 | Select Location Hierarchy 21 | 27 | {configuredHierarchies.map(hierarchicalValue => ( 28 | } 33 | label={hierarchicalValue.label} 34 | /> 35 | ))} 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/common/components/SaveComponent.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Button } from "@mui/material"; 3 | import { isEmpty } from "lodash"; 4 | import { Save } from "@mui/icons-material"; 5 | 6 | export const SaveComponent = ({ 7 | disabledFlag = false, 8 | name = "SAVE", 9 | fullWidth = false, 10 | onSubmit, 11 | ...props 12 | }) => { 13 | const [saveInProgress, setSaveInProgress] = useState(false); 14 | 15 | const enableSaveButton = () => { 16 | setSaveInProgress(false); 17 | }; 18 | const disableSaveButton = () => { 19 | setSaveInProgress(true); 20 | }; 21 | 22 | const onSave = async event => { 23 | disableSaveButton(); 24 | try { 25 | await onSubmit(event); 26 | } finally { 27 | enableSaveButton(); 28 | } 29 | }; 30 | 31 | return ( 32 | 43 | ); 44 | }; 45 | --------------------------------------------------------------------------------