├── .nvmrc ├── src ├── features │ ├── settings │ │ ├── store │ │ │ ├── settings.actions.ts │ │ │ ├── settings.facade.ts │ │ │ ├── settings.reducer.ts │ │ │ ├── settings.actions.spec.ts │ │ │ ├── settings.facade.spec.ts │ │ │ └── settings.reducer.spec.ts │ │ ├── service │ │ │ └── settings.service.ts │ │ ├── pages │ │ │ └── settings │ │ │ │ ├── settings.module.ts │ │ │ │ ├── settings.html │ │ │ │ ├── settings.ts │ │ │ │ └── settings.scss │ │ └── settings.module.ts │ ├── connections │ │ ├── components │ │ │ ├── task-list │ │ │ │ ├── task-list.component.scss │ │ │ │ ├── task-list.component.html │ │ │ │ ├── task-list.component.ts │ │ │ │ └── task-list.component.spec.ts │ │ │ ├── instructions │ │ │ │ ├── instructions.component.scss │ │ │ │ ├── instructions.component.html │ │ │ │ ├── instructions.component.ts │ │ │ │ └── instructions.component.spec.ts │ │ │ ├── project-item │ │ │ │ ├── project-item.component.scss │ │ │ │ ├── project-item.component.html │ │ │ │ ├── project-item.component.ts │ │ │ │ └── project-item.component.spec.ts │ │ │ ├── project-list │ │ │ │ ├── project-list.component.scss │ │ │ │ ├── project-list.component.html │ │ │ │ ├── project-list.component.spec.ts │ │ │ │ └── project-list.component.ts │ │ │ ├── connection-list │ │ │ │ ├── connection-list.component.ts │ │ │ │ ├── connection-list.component.spec.ts │ │ │ │ ├── connection-list.component.html │ │ │ │ └── connection-list.component.scss │ │ │ ├── verify-modal │ │ │ │ ├── verify-modal.component.ts │ │ │ │ ├── verify-modal.component.html │ │ │ │ └── verify-modal.component.scss │ │ │ └── share-scope-link-modal │ │ │ │ ├── share-scope-link-modal.component.html │ │ │ │ ├── share-scope-link-modal.component.ts │ │ │ │ └── share-scope-link-modal.component.scss │ │ ├── pages │ │ │ ├── select-project │ │ │ │ ├── select-project.component.scss │ │ │ │ ├── select-project.component.module.ts │ │ │ │ ├── select-project.component.html │ │ │ │ ├── select-project.component.spec.ts │ │ │ │ └── select-project.component.ts │ │ │ ├── add-connection │ │ │ │ ├── add-connection.component.scss │ │ │ │ ├── add-connection.component.module.ts │ │ │ │ ├── add-connection.component.spec.ts │ │ │ │ └── add-connection.component.html │ │ │ ├── connection-details │ │ │ │ ├── connection-details.component.module.ts │ │ │ │ ├── connection-details.component.scss │ │ │ │ ├── connection-details.component.spec.ts │ │ │ │ ├── connection-details.component.html │ │ │ │ └── connection-details.component.ts │ │ │ └── select-task-list │ │ │ │ ├── select-task-list.component.module.ts │ │ │ │ ├── select-task-list.component.scss │ │ │ │ ├── select-task-list.component.spec.ts │ │ │ │ ├── select-task-list.component.html │ │ │ │ └── select-task-list.component.ts │ │ ├── services │ │ │ └── connection.service.spec.ts │ │ └── connections.module.ts │ ├── scoping │ │ ├── components │ │ │ ├── session-header │ │ │ │ ├── session-header.component.html │ │ │ │ ├── session-header.component.scss │ │ │ │ ├── session-header.component.ts │ │ │ │ └── session-header.component.spec.ts │ │ │ ├── task-card │ │ │ │ ├── task-card.component.scss │ │ │ │ ├── task-card.component.html │ │ │ │ ├── task-card.component.ts │ │ │ │ └── task-card.component.spec.ts │ │ │ ├── result-estimate │ │ │ │ ├── result-estimate.component.ts │ │ │ │ ├── result-estimate.component.html │ │ │ │ ├── result-estimate.component.spec.ts │ │ │ │ └── result-estimate.component.scss │ │ │ ├── vote │ │ │ │ ├── vote.component.html │ │ │ │ ├── vote.component.spec.ts │ │ │ │ ├── vote.component.scss │ │ │ │ └── vote.component.ts │ │ │ ├── counted-votes │ │ │ │ ├── counted-votes.component.html │ │ │ │ ├── counted-votes.component.ts │ │ │ │ ├── counted-votes.component.scss │ │ │ │ └── counted-votes.component.spec.ts │ │ │ ├── session-access │ │ │ │ ├── session-access.component.html │ │ │ │ ├── session-access.component.scss │ │ │ │ ├── session-access.component.spec.ts │ │ │ │ └── session-access.component.ts │ │ │ └── select-result │ │ │ │ ├── select-result.component.spec.ts │ │ │ │ ├── select-result.component.html │ │ │ │ ├── select-result.component.scss │ │ │ │ └── select-result.component.ts │ │ ├── services │ │ │ └── scoping.service.spec.ts │ │ ├── pages │ │ │ ├── session-results │ │ │ │ ├── session-results.component.module.ts │ │ │ │ ├── session-results.component.scss │ │ │ │ ├── session-results.component.spec.ts │ │ │ │ ├── session-results.component.ts │ │ │ │ └── session-results.component.html │ │ │ └── session-scoping │ │ │ │ ├── session-scoping.component.module.ts │ │ │ │ ├── session-scoping.component.spec.ts │ │ │ │ └── session-scoping.component.scss │ │ └── scoping.module.ts │ ├── dashboard │ │ ├── components │ │ │ ├── session-history-list │ │ │ │ ├── session-history-list.component.scss │ │ │ │ ├── session-history-list.component.html │ │ │ │ ├── session-history-list.component.spec.ts │ │ │ │ └── session-history-list.component.ts │ │ │ ├── join-session │ │ │ │ ├── join-session.component.scss │ │ │ │ ├── join-session.component.html │ │ │ │ ├── join-session.component.ts │ │ │ │ └── join-session.component.spec.ts │ │ │ ├── session-history-item │ │ │ │ ├── session-history-item.component.scss │ │ │ │ ├── session-history-item.component.spec.ts │ │ │ │ ├── session-history-item.component.html │ │ │ │ └── session-history-item.component.ts │ │ │ └── session-detail-modal │ │ │ │ ├── session-detail-modal.component.html │ │ │ │ ├── session-detail-modal.component.scss │ │ │ │ └── session-detail-modal.component.ts │ │ ├── pages │ │ │ └── dashboard │ │ │ │ ├── dashboard.component.scss │ │ │ │ ├── dashboard.component.module.ts │ │ │ │ └── dashboard.component.html │ │ ├── dashboard-routing.module.ts │ │ ├── services │ │ │ └── history.service.spec.ts │ │ ├── dashboard.module.ts │ │ └── store │ │ │ └── dashboard.reducer.ts │ └── authentication │ │ ├── pages │ │ └── login │ │ │ ├── login.component.module.ts │ │ │ ├── login.component.scss │ │ │ └── login.component.html │ │ ├── authentication-routing.module.ts │ │ ├── authentication.module.ts │ │ └── store │ │ ├── auth.actions.ts │ │ ├── auth.actions.spec.ts │ │ └── auth.reducer.ts ├── shared │ ├── components │ │ └── info-modal │ │ │ ├── info-modal.scss │ │ │ ├── info-modal.html │ │ │ └── info-modal.ts │ ├── pipes │ │ ├── object-iterators.pipe.spec.ts │ │ └── object-iterators.pipe.ts │ ├── loading.service.ts │ ├── api.interceptor.ts │ └── shared.module.ts ├── app │ ├── app.html │ ├── main.ts │ ├── not-found.component.ts │ ├── app.constants.ts │ ├── app-routing.module.ts │ ├── auth.guard.ts │ ├── un-auth.guard.ts │ └── app.component.ts ├── test │ ├── jest-test.ts │ ├── jest-test.helper.ts │ └── jest-global-mocks.ts ├── models │ ├── task-list.ts │ ├── faq.ts │ ├── project.ts │ ├── user.ts │ ├── task.ts │ ├── connection.ts │ ├── scoping-session.ts │ └── history-item.ts ├── assets │ ├── imgs │ │ └── logo.png │ ├── icon │ │ └── favicon.ico │ └── fonts │ │ └── Avenir-Next │ │ ├── bold.eot │ │ ├── bold.ttf │ │ ├── bold.woff │ │ ├── demi.eot │ │ ├── demi.ttf │ │ ├── demi.woff │ │ ├── thin.eot │ │ ├── thin.ttf │ │ ├── thin.woff │ │ ├── bold.woff2 │ │ ├── demi.woff2 │ │ ├── medium.eot │ │ ├── medium.ttf │ │ ├── medium.woff │ │ ├── regular.eot │ │ ├── regular.ttf │ │ ├── regular.woff │ │ ├── thin.woff2 │ │ ├── ultralight.eot │ │ ├── ultralight.ttf │ │ └── ultralight.woff ├── store │ ├── app.actions.ts │ ├── router.actions.ts │ ├── app.facade.ts │ ├── router.reducer.ts │ ├── router.actions.spec.ts │ ├── router.facade.ts │ ├── app.reducer.ts │ └── router.reducer.spec.ts ├── manifest.json ├── tsconfig.spec.json ├── environments │ ├── environment.ts │ └── environment.prod.ts ├── service-worker.js ├── test.ts └── index.html ├── firestore.indexes.json ├── resources ├── icon.png.md5 ├── splash.png.md5 ├── icon.png ├── splash.png ├── ios │ ├── icon │ │ ├── icon.png │ │ ├── icon-40.png │ │ ├── icon-50.png │ │ ├── icon-60.png │ │ ├── icon-72.png │ │ ├── icon-76.png │ │ ├── icon@2x.png │ │ ├── icon-1024.png │ │ ├── icon-40@2x.png │ │ ├── icon-40@3x.png │ │ ├── icon-50@2x.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-72@2x.png │ │ ├── icon-76@2x.png │ │ ├── icon-small.png │ │ ├── icon-83.5@2x.png │ │ ├── icon-small@2x.png │ │ └── icon-small@3x.png │ └── splash │ │ ├── Default-667h.png │ │ ├── Default-736h.png │ │ ├── Default~iphone.png │ │ ├── Default@2x~iphone.png │ │ ├── Default-Portrait~ipad.png │ │ ├── Default-568h@2x~iphone.png │ │ ├── Default-Landscape-736h.png │ │ ├── Default-Landscape~ipad.png │ │ ├── Default-Landscape@2x~ipad.png │ │ ├── Default-Landscape@~ipadpro.png │ │ ├── Default-Portrait@2x~ipad.png │ │ ├── Default-Portrait@~ipadpro.png │ │ ├── Default@2x~universal~anyany.png │ │ ├── Default@2x~universal~anyany iphonex-landscape.png │ │ └── Default@2x~universal~anyany iphonex-portrait.png ├── android │ ├── icon │ │ ├── drawable-hdpi-icon.png │ │ ├── drawable-ldpi-icon.png │ │ ├── drawable-mdpi-icon.png │ │ ├── drawable-xhdpi-icon.png │ │ ├── drawable-xxhdpi-icon.png │ │ └── drawable-xxxhdpi-icon.png │ └── splash │ │ ├── drawable-land-hdpi-screen.png │ │ ├── drawable-land-ldpi-screen.png │ │ ├── drawable-land-mdpi-screen.png │ │ ├── drawable-land-xhdpi-screen.png │ │ ├── drawable-land-xxhdpi-screen.png │ │ ├── drawable-port-hdpi-screen.png │ │ ├── drawable-port-ldpi-screen.png │ │ ├── drawable-port-mdpi-screen.png │ │ ├── drawable-port-xhdpi-screen.png │ │ ├── drawable-port-xxhdpi-screen.png │ │ ├── drawable-land-xxxhdpi-screen.png │ │ └── drawable-port-xxxhdpi-screen.png └── README.md ├── .npmrc ├── functions ├── src │ ├── middleware │ │ ├── index.ts │ │ └── id-token-auth.ts │ ├── api │ │ ├── connections │ │ │ ├── projects │ │ │ │ ├── sessions │ │ │ │ │ ├── index.ts │ │ │ │ │ └── add-session.ts │ │ │ │ ├── tasklists │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── get-tasks.ts │ │ │ │ │ └── get-tasklists.ts │ │ │ │ ├── index.ts │ │ │ │ └── get-projects.ts │ │ │ ├── tasks │ │ │ │ ├── index.ts │ │ │ │ ├── get-task.ts │ │ │ │ └── put-estimate.ts │ │ │ ├── delete-connection.ts │ │ │ ├── index.ts │ │ │ └── add-connection.ts │ │ ├── estimate │ │ │ ├── index.ts │ │ │ └── set-estimate.ts │ │ ├── index.ts │ │ └── session-links │ │ │ ├── index.ts │ │ │ ├── delete-session.ts │ │ │ ├── refresh-access-code.ts │ │ │ └── decode-session-link.ts │ ├── fw.html │ ├── services │ │ ├── index.ts │ │ └── encryption.service.ts │ └── index.ts ├── tsconfig.json └── package.json ├── juntoscope.jks ├── .runtimeconfig.json ├── .firebaserc ├── ionic.config.json ├── tslint.json ├── apple-app-site-association ├── .editorconfig ├── firebase.json ├── .gitignore ├── tsconfig.json ├── .travis.yml └── firestore.rules /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /src/features/settings/store/settings.actions.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/features/settings/store/settings.facade.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/features/settings/store/settings.reducer.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/shared/components/info-modal/info-modal.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/features/settings/store/settings.actions.spec.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/features/settings/store/settings.facade.spec.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/features/settings/store/settings.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [] 3 | } 4 | -------------------------------------------------------------------------------- /resources/icon.png.md5: -------------------------------------------------------------------------------- 1 | 0036184da03326d2165c8c5255980d96 -------------------------------------------------------------------------------- /resources/splash.png.md5: -------------------------------------------------------------------------------- 1 | 5dba1401df8cd17293ee687404148024 -------------------------------------------------------------------------------- /src/app/app.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" 2 | message="chore(release): %s" 3 | -------------------------------------------------------------------------------- /functions/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from './id-token-auth'; 2 | -------------------------------------------------------------------------------- /src/features/connections/components/task-list/task-list.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/features/connections/components/instructions/instructions.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/features/connections/pages/select-project/select-project.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /juntoscope.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/juntoscope.jks -------------------------------------------------------------------------------- /src/test/jest-test.ts: -------------------------------------------------------------------------------- 1 | import "jest-preset-angular"; 2 | import "jest-global-mocks"; 3 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/icon.png -------------------------------------------------------------------------------- /resources/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/splash.png -------------------------------------------------------------------------------- /src/models/task-list.ts: -------------------------------------------------------------------------------- 1 | export interface TaskList { 2 | id: string; 3 | name: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/imgs/logo.png -------------------------------------------------------------------------------- /resources/ios/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon.png -------------------------------------------------------------------------------- /src/assets/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/icon/favicon.ico -------------------------------------------------------------------------------- /.runtimeconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "encryption": { 3 | "secret": "J*+48Iz{J%m9xr08]Z1DLNTj5f66pE7G" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /resources/ios/icon/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-40.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-50.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-60.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-72.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-76.png -------------------------------------------------------------------------------- /resources/ios/icon/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-1024.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-40@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-40@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-50@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-60@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-60@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-72@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-76@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-small.png -------------------------------------------------------------------------------- /src/features/connections/components/instructions/instructions.component.html: -------------------------------------------------------------------------------- 1 |

2 | instructions works! 3 |

4 | -------------------------------------------------------------------------------- /resources/ios/icon/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-83.5@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-small@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/icon/icon-small@3x.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-667h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-667h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-736h.png -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/bold.eot -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/bold.woff -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/demi.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/demi.eot -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/demi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/demi.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/demi.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/demi.woff -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/thin.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/thin.eot -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/thin.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/thin.woff -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "juntoscope-dev-963e3", 4 | "production": "juntoscope-prod-61b4d" 5 | } 6 | } -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "juntoscope", 3 | "integrations": { 4 | "cordova": {} 5 | }, 6 | "type": "ionic-angular" 7 | } -------------------------------------------------------------------------------- /resources/ios/splash/Default~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default~iphone.png -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/bold.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/demi.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/demi.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/medium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/medium.eot -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/medium.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/medium.woff -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/regular.eot -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/regular.woff -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/thin.woff2 -------------------------------------------------------------------------------- /resources/ios/splash/Default@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default@2x~iphone.png -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/ultralight.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/ultralight.eot -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/ultralight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/ultralight.ttf -------------------------------------------------------------------------------- /src/models/faq.ts: -------------------------------------------------------------------------------- 1 | export interface Faq { 2 | id?: string; 3 | category: string; 4 | question: string; 5 | answer: string; 6 | } 7 | -------------------------------------------------------------------------------- /resources/android/icon/drawable-hdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/icon/drawable-hdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-ldpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/icon/drawable-ldpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-mdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/icon/drawable-mdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/icon/drawable-xhdpi-icon.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-Portrait~ipad.png -------------------------------------------------------------------------------- /src/assets/fonts/Avenir-Next/ultralight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/src/assets/fonts/Avenir-Next/ultralight.woff -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/icon/drawable-xxhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/icon/drawable-xxxhdpi-icon.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-568h@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-568h@2x~iphone.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-Landscape-736h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-Landscape~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-Landscape@2x~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape@~ipadpro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-Landscape@~ipadpro.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-Portrait@2x~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait@~ipadpro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default-Portrait@~ipadpro.png -------------------------------------------------------------------------------- /resources/ios/splash/Default@2x~universal~anyany.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default@2x~universal~anyany.png -------------------------------------------------------------------------------- /src/store/app.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "@ngrx/store"; 2 | 3 | export class NoopAction implements Action { 4 | readonly type = "[App] NO-OP"; 5 | } 6 | -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-land-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-land-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-land-mdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-land-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-land-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-port-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-port-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-port-mdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-port-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-port-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-land-xxxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/android/splash/drawable-port-xxxhdpi-screen.png -------------------------------------------------------------------------------- /src/models/project.ts: -------------------------------------------------------------------------------- 1 | import { TaskList } from "./task-list"; 2 | 3 | export interface Project { 4 | id?: string; 5 | name: string; 6 | taskLists: { [id: string]: TaskList }; 7 | } 8 | -------------------------------------------------------------------------------- /resources/ios/splash/Default@2x~universal~anyany iphonex-landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default@2x~universal~anyany iphonex-landscape.png -------------------------------------------------------------------------------- /resources/ios/splash/Default@2x~universal~anyany iphonex-portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openforge/JuntoScope/HEAD/resources/ios/splash/Default@2x~universal~anyany iphonex-portrait.png -------------------------------------------------------------------------------- /src/app/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app.module'; 4 | 5 | platformBrowserDynamic().bootstrapModule(AppModule); 6 | -------------------------------------------------------------------------------- /src/features/scoping/components/session-header/session-header.component.html: -------------------------------------------------------------------------------- 1 |

{{ session.projectName }}

2 |

Project Task List {{ session.numScopedTasks + 1 }}/{{ session.numTasks }}

-------------------------------------------------------------------------------- /src/features/connections/components/project-item/project-item.component.scss: -------------------------------------------------------------------------------- 1 | app-project-item { 2 | ion-item.item.item-block .item-inner { 3 | border-bottom: 2px solid $primary-orange; 4 | } 5 | } -------------------------------------------------------------------------------- /src/features/scoping/components/session-header/session-header.component.scss: -------------------------------------------------------------------------------- 1 | app-session-header { 2 | p { 3 | text-align: center; 4 | border-bottom: 2px solid $primary-orange; 5 | padding-bottom: 5px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/features/scoping/components/task-card/task-card.component.scss: -------------------------------------------------------------------------------- 1 | app-task-card { 2 | .task-card-container { 3 | p:first-child { 4 | border-bottom: 2px solid; 5 | padding-bottom: 5px; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/models/user.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | uid: string; 3 | displayName: string; 4 | } 5 | 6 | export enum SessionUserType { 7 | MODERATOR = "Session Moderator", 8 | PARTICIPANT = "Session Participant" 9 | } 10 | -------------------------------------------------------------------------------- /src/features/connections/components/task-list/task-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ taskList.name }} 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/features/scoping/components/task-card/task-card.component.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Task: {{task.name}} 4 |

5 |

6 | Description: {{task.description}} 7 |

8 |
9 | -------------------------------------------------------------------------------- /src/models/task.ts: -------------------------------------------------------------------------------- 1 | export interface Task { 2 | name: string; 3 | description: string; 4 | votes?: Votes; 5 | estimate?: number; 6 | id?: number; 7 | } 8 | 9 | export interface Votes { 10 | [userId: string]: number; 11 | } 12 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-duplicate-variable": true, 4 | "no-unused-variable": [ 5 | true 6 | ] 7 | }, 8 | "rulesDirectory": [ 9 | "node_modules/tslint-eslint-rules/dist/rules" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/models/connection.ts: -------------------------------------------------------------------------------- 1 | import { Project } from "./project"; 2 | 3 | export interface Connection { 4 | id?: string; 5 | type: string; 6 | token: string; 7 | externalData?: any; 8 | projects?: { [projectId: string]: Project }; 9 | } 10 | -------------------------------------------------------------------------------- /functions/src/api/connections/projects/sessions/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | import { addSession } from './add-session'; 4 | 5 | export const sessionsRouter = express.Router({ mergeParams: true }); 6 | 7 | sessionsRouter.post('/', addSession); 8 | -------------------------------------------------------------------------------- /apple-app-site-association: -------------------------------------------------------------------------------- 1 | { 2 | "applinks": { 3 | "apps": [], 4 | "details": [ 5 | { 6 | "appID": "FA5H7L6B2W.com.openforge.juntoscope", 7 | "paths": [ "*" ] 8 | } 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /src/shared/pipes/object-iterators.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { ObjectIteratorsPipe } from "./object-iterators.pipe"; 2 | 3 | describe("ObjectIteratorsPipe", () => { 4 | it("create an instance", () => { 5 | const pipe = new ObjectIteratorsPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/features/connections/components/project-item/project-item.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ project.name }} 4 | 5 | 8 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6"], 4 | "module": "commonjs", 5 | "noImplicitReturns": false, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "target": "es6" 9 | }, 10 | "compileOnSave": true, 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /functions/src/api/estimate/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | 3 | import { setEstimate } from "./set-estimate"; 4 | 5 | export const estimateRouter = express.Router({ mergeParams: true }); 6 | 7 | estimateRouter.put( 8 | "/:ownerId/connections/:connectionId/sessions/:sessionId/tasks/:taskId", 9 | setEstimate 10 | ); 11 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JuntoScope", 3 | "short_name": "JuntoScope", 4 | "start_url": "index.html", 5 | "display": "standalone", 6 | "icons": [{ 7 | "src": "assets/imgs/logo.png", 8 | "sizes": "512x512", 9 | "type": "image/png" 10 | }], 11 | "background_color": "#4e8ef7", 12 | "theme_color": "#4e8ef7" 13 | } -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.ng-cli.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": ["jasmine", "node"] 9 | }, 10 | "files": ["test.ts"], 11 | "include": ["**/*.spec.ts", "**/*.d.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /functions/src/fw.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 11 | 12 | -------------------------------------------------------------------------------- /functions/src/api/connections/tasks/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | import { getTask } from './get-task'; 4 | 5 | import { putEstimate } from './put-estimate'; 6 | 7 | export const tasksRouter = express.Router({ mergeParams: true }); 8 | 9 | tasksRouter.get('/:taskId', getTask); 10 | 11 | tasksRouter.put('/:taskId', putEstimate); -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FIREBASE_DEV_CONFIG, 3 | FIREBASE_DEV_FUNCTIONS, 4 | GOOGLE_WEB_CLIENT_ID_DEV 5 | } from "../config/config"; 6 | 7 | export const environment = { 8 | production: false, 9 | apiBaseUrl: FIREBASE_DEV_FUNCTIONS, 10 | firebase: FIREBASE_DEV_CONFIG, 11 | webClientId: GOOGLE_WEB_CLIENT_ID_DEV 12 | }; 13 | -------------------------------------------------------------------------------- /src/features/connections/components/instructions/instructions.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "app-instructions", 5 | templateUrl: "./instructions.component.html" 6 | }) 7 | export class InstructionsComponent implements OnInit { 8 | constructor() {} 9 | 10 | ngOnInit() {} 11 | } 12 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FIREBASE_PROD_FUNCTIONS, 3 | FIREBASE_PROD_CONFIG, 4 | GOOGLE_WEB_CLIENT_ID_PROD 5 | } from "../config/config"; 6 | 7 | export const environment = { 8 | production: true, 9 | apiBaseUrl: FIREBASE_PROD_FUNCTIONS, 10 | firebase: FIREBASE_PROD_CONFIG, 11 | webClientId: GOOGLE_WEB_CLIENT_ID_PROD 12 | }; 13 | -------------------------------------------------------------------------------- /src/features/dashboard/components/session-history-list/session-history-list.component.scss: -------------------------------------------------------------------------------- 1 | app-session-history-list { 2 | .list-headers { 3 | margin: 2rem 1rem; 4 | 5 | ion-list-header.list-header { 6 | border-bottom: 2px solid $primary-orange; 7 | } 8 | } 9 | 10 | ion-list.list { 11 | border: 2px solid $primary-orange; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/features/settings/service/settings.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { AngularFirestore } from "angularfire2/firestore"; 3 | 4 | @Injectable() 5 | export class SettingsService { 6 | constructor(private afs: AngularFirestore) {} 7 | 8 | getFaqs() { 9 | return this.afs.collection(`/faqs`).valueChanges(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "app-not-found", 5 | template: ` 6 |

7 | Uh Oh ... '{{ url$ | async }}' Not Found! 8 |

9 | `, 10 | styles: [] 11 | }) 12 | export class NotFoundComponent { 13 | // url$ = this.routerFacade.url$; 14 | 15 | constructor() {} 16 | } 17 | -------------------------------------------------------------------------------- /functions/src/api/connections/projects/tasklists/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | import { getTaskLists } from './get-tasklists'; 4 | 5 | import { getTasks } from './get-tasks'; 6 | 7 | export const tasklistsRouter = express.Router({ mergeParams: true }); 8 | 9 | tasklistsRouter.get('/', getTaskLists); 10 | 11 | tasklistsRouter.get('/:tasklistId', getTasks); 12 | -------------------------------------------------------------------------------- /src/features/settings/pages/settings/settings.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { IonicPageModule } from "ionic-angular"; 3 | import { SettingsPage } from "./settings"; 4 | 5 | @NgModule({ 6 | declarations: [SettingsPage], 7 | imports: [IonicPageModule.forChild(SettingsPage)], 8 | exports: [SettingsPage] 9 | }) 10 | export class SettingsPageModule {} 11 | -------------------------------------------------------------------------------- /src/features/dashboard/pages/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | app-dashboard { 2 | ion-infinite-scroll ion-infinite-scroll-content.dashboard-spinner { 3 | .infinite-loading .infinite-loading-spinner { 4 | ion-spinner.spinner { 5 | position: relative; 6 | display: inline-block; 7 | width: 28px; 8 | height: 28px; 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/features/dashboard/components/join-session/join-session.component.scss: -------------------------------------------------------------------------------- 1 | app-join-session { 2 | .container { 3 | margin-top: 2rem; 4 | 5 | .list-headers { 6 | margin: 2rem 1rem; 7 | 8 | ion-list-header.list-header { 9 | border-bottom: 2px solid $primary-orange; 10 | } 11 | } 12 | 13 | .join { 14 | height: 44px; 15 | padding: 0 2rem; 16 | margin: 0; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "functions": { 7 | "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build" 8 | }, 9 | "hosting": { 10 | "public": "www", 11 | "rewrites": [ 12 | { 13 | "source": "**", 14 | "destination": "/index.html" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/features/connections/pages/add-connection/add-connection.component.scss: -------------------------------------------------------------------------------- 1 | app-add-connection { 2 | .container { 3 | align-items: center; 4 | display: flex; 5 | flex-direction: column; 6 | height: 80%; 7 | justify-content: center; 8 | width: 100%; 9 | 10 | p { 11 | padding: 0 2rem; 12 | } 13 | 14 | button.button { 15 | width: 60%; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/shared/components/info-modal/info-modal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

{{ title }}

10 |

{{ text }}

11 | 12 |
-------------------------------------------------------------------------------- /src/features/connections/components/project-list/project-list.component.scss: -------------------------------------------------------------------------------- 1 | app-project-list { 2 | ion-list.list { 3 | margin: 0; 4 | width: 100%; 5 | 6 | ion-list-header.list-header { 7 | 8 | .icon, h2 { 9 | display: inline-block; 10 | font-weight: bolder; 11 | text-transform: none; 12 | vertical-align: middle; 13 | } 14 | 15 | ion-label.label { 16 | margin-top: 3rem; 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /functions/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | 3 | import { connectionsRouter } from "./connections"; 4 | import { sessionLinksRouter } from "./session-links"; 5 | import { estimateRouter } from "./estimate"; 6 | 7 | export const apiRouter = express.Router(); 8 | 9 | apiRouter.use("/connections", connectionsRouter); 10 | apiRouter.use("/session-links", sessionLinksRouter); 11 | apiRouter.use("/estimate", estimateRouter); 12 | -------------------------------------------------------------------------------- /src/features/scoping/components/result-estimate/result-estimate.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "app-result-estimate", 5 | templateUrl: "./result-estimate.component.html" 6 | }) 7 | export class ResultEstimateComponent implements OnInit { 8 | @Input() estimate: number; 9 | @Input() finalResults: boolean; 10 | 11 | constructor() {} 12 | 13 | ngOnInit() {} 14 | } 15 | -------------------------------------------------------------------------------- /resources/README.md: -------------------------------------------------------------------------------- 1 | These are Cordova resources. You can replace icon.png and splash.png and run 2 | `ionic cordova resources` to generate custom icons and splash screens for your 3 | app. See `ionic cordova resources --help` for details. 4 | 5 | Cordova reference documentation: 6 | 7 | - Icons: https://cordova.apache.org/docs/en/latest/config_ref/images.html 8 | - Splash Screens: https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-splashscreen/ 9 | -------------------------------------------------------------------------------- /src/features/authentication/pages/login/login.component.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { IonicPageModule } from "ionic-angular"; 3 | import { LoginPage } from "./login.component"; 4 | import { SharedModule } from "../../../../shared/shared.module"; 5 | 6 | @NgModule({ 7 | declarations: [LoginPage], 8 | imports: [IonicPageModule.forChild(LoginPage), SharedModule], 9 | exports: [LoginPage] 10 | }) 11 | export class LoginPageModule {} 12 | -------------------------------------------------------------------------------- /src/features/scoping/components/task-card/task-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from "@angular/core"; 2 | import { Task } from "../../../../models/task"; 3 | 4 | @Component({ 5 | selector: "app-task-card", 6 | templateUrl: "./task-card.component.html" 7 | }) 8 | export class TaskCardComponent implements OnInit { 9 | @Input() task: Task; 10 | 11 | @Input() onlyTitle: boolean; 12 | 13 | constructor() {} 14 | 15 | ngOnInit() {} 16 | } 17 | -------------------------------------------------------------------------------- /src/features/scoping/components/session-header/session-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from "@angular/core"; 2 | import { ScopingSession } from "../../../../models/scoping-session"; 3 | 4 | @Component({ 5 | selector: "app-session-header", 6 | templateUrl: "./session-header.component.html" 7 | }) 8 | export class SessionHeaderComponent implements OnInit { 9 | @Input() session: ScopingSession; 10 | 11 | constructor() {} 12 | 13 | ngOnInit() {} 14 | } 15 | -------------------------------------------------------------------------------- /src/features/dashboard/dashboard-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { Routes, RouterModule } from "@angular/router"; 3 | import { DashboardPage } from "./pages/dashboard/dashboard.component"; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: "", 8 | component: DashboardPage 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class DashboardRoutingModule {} 17 | -------------------------------------------------------------------------------- /src/features/connections/components/project-list/project-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

{{ connection.externalData.company }} - {{connection.type | titlecase }}

6 |
7 |
8 | 9 | project 10 |
11 | -------------------------------------------------------------------------------- /functions/src/api/connections/projects/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | import { getProjects } from './get-projects'; 4 | import { sessionsRouter } from './sessions'; 5 | import { tasklistsRouter } from './tasklists'; 6 | 7 | export const projectsRouter = express.Router({ mergeParams: true }); 8 | 9 | projectsRouter.get('/', getProjects); 10 | 11 | projectsRouter.use('/:projectId/tasklists', tasklistsRouter); 12 | 13 | projectsRouter.use('/:projectId/sessions', sessionsRouter); 14 | -------------------------------------------------------------------------------- /src/features/scoping/components/vote/vote.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 | {{ error }} 7 |
8 | 9 | 10 |
11 |
-------------------------------------------------------------------------------- /src/features/scoping/services/scoping.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from "@angular/core/testing"; 2 | import { ScopingService } from "./scoping.service"; 3 | 4 | describe("ScopingService", () => { 5 | beforeEach(() => { 6 | TestBed.configureTestingModule({ 7 | providers: [ScopingService] 8 | }); 9 | }); 10 | 11 | it("should be created", inject( 12 | [ScopingService], 13 | (service: ScopingService) => { 14 | expect(service).toBeTruthy(); 15 | } 16 | )); 17 | }); 18 | -------------------------------------------------------------------------------- /src/features/dashboard/services/history.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from "@angular/core/testing"; 2 | 3 | import { HistoryService } from "./history.service"; 4 | 5 | describe("HistoryService", () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [HistoryService] 9 | }); 10 | }); 11 | 12 | it("should be created", inject( 13 | [HistoryService], 14 | (service: HistoryService) => { 15 | expect(service).toBeTruthy(); 16 | } 17 | )); 18 | }); 19 | -------------------------------------------------------------------------------- /src/features/dashboard/components/join-session/join-session.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |

Join Session

7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | -------------------------------------------------------------------------------- /src/features/connections/pages/add-connection/add-connection.component.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { IonicPageModule } from "ionic-angular"; 3 | import { SharedModule } from "../../../../shared/shared.module"; 4 | import { AddConnectionPage } from "./add-connection.component"; 5 | 6 | @NgModule({ 7 | declarations: [AddConnectionPage], 8 | imports: [IonicPageModule.forChild(AddConnectionPage), SharedModule], 9 | exports: [AddConnectionPage] 10 | }) 11 | export class AddConnectionPageModule {} 12 | -------------------------------------------------------------------------------- /src/features/connections/components/connection-list/connection-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from "@angular/core"; 2 | 3 | import { Connection } from "../../../../models/connection"; 4 | 5 | @Component({ 6 | selector: "app-connection-list", 7 | templateUrl: "./connection-list.component.html" 8 | }) 9 | export class ConnectionListComponent { 10 | @Input() connections: Connection[]; 11 | @Output() select = new EventEmitter(); 12 | @Output() add = new EventEmitter(); 13 | } 14 | -------------------------------------------------------------------------------- /functions/src/api/session-links/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | import { decodeSessionLink } from './decode-session-link'; 4 | import { refreshAccessCode } from './refresh-access-code'; 5 | import { deleteSession } from './delete-session'; 6 | 7 | export const sessionLinksRouter = express.Router({ mergeParams: true }); 8 | 9 | sessionLinksRouter.get('/:sessionLink', decodeSessionLink); 10 | sessionLinksRouter.get('/:sessionLink/refresh', refreshAccessCode); 11 | sessionLinksRouter.delete('/:sessionLink', deleteSession); 12 | -------------------------------------------------------------------------------- /src/features/connections/services/connection.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from "@angular/core/testing"; 2 | 3 | import { ConnectionService } from "./connection.service"; 4 | 5 | describe("ConnectionService", () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [ConnectionService] 9 | }); 10 | }); 11 | 12 | it("should be created", inject( 13 | [ConnectionService], 14 | (service: ConnectionService) => { 15 | expect(service).toBeTruthy(); 16 | } 17 | )); 18 | }); 19 | -------------------------------------------------------------------------------- /src/features/scoping/components/counted-votes/counted-votes.component.html: -------------------------------------------------------------------------------- 1 |
2 |

COUNTED VOTES

3 | 4 | {{ vote === 0 ? '?' : (vote === -1 ? 'N/A' : vote) }} 5 | 6 |
7 | 8 |
9 |

COUNTED VOTES

10 | 11 | - 12 | 13 |
14 | 15 | -------------------------------------------------------------------------------- /src/features/connections/components/task-list/task-list.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | Output, 5 | ChangeDetectionStrategy, 6 | EventEmitter 7 | } from "@angular/core"; 8 | import { TaskList } from "../../../../models/task-list"; 9 | 10 | @Component({ 11 | selector: "app-task-list", 12 | templateUrl: "./task-list.component.html", 13 | changeDetection: ChangeDetectionStrategy.OnPush 14 | }) 15 | export class TaskListComponent { 16 | @Input() taskList: TaskList; 17 | @Output() toggle = new EventEmitter(); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/app.constants.ts: -------------------------------------------------------------------------------- 1 | import { InAppBrowserOptions } from "@ionic-native/in-app-browser"; 2 | 3 | export const MORE_INFO_NEEDED = 0; 4 | export const NOT_APPLICABLE = -1; 5 | 6 | // Time for seconds to show task result view before auto navigating to next task 7 | export const TIMER_FOR_NEXT_TASK = 5000; 8 | 9 | export const IAB_OPTIONS: InAppBrowserOptions = { 10 | location: "no", 11 | 12 | // Android Only 13 | zoom: "no", 14 | hardwareback: "no", 15 | shouldPauseOnSuspend: "yes", 16 | 17 | // iOS Only 18 | closebuttoncaption: "Close" 19 | }; 20 | -------------------------------------------------------------------------------- /src/features/connections/pages/connection-details/connection-details.component.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { IonicPageModule } from "ionic-angular"; 3 | import { SharedModule } from "../../../../shared/shared.module"; 4 | import { ConnectionDetailsPage } from "./connection-details.component"; 5 | 6 | @NgModule({ 7 | declarations: [ConnectionDetailsPage], 8 | imports: [IonicPageModule.forChild(ConnectionDetailsPage), SharedModule], 9 | exports: [ConnectionDetailsPage] 10 | }) 11 | export class ConnectionDetailsPageModule {} 12 | -------------------------------------------------------------------------------- /src/features/connections/components/project-item/project-item.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | Output, 5 | EventEmitter, 6 | ChangeDetectionStrategy 7 | } from "@angular/core"; 8 | import { Project } from "../../../../models/project"; 9 | 10 | @Component({ 11 | selector: "app-project-item", 12 | templateUrl: "./project-item.component.html", 13 | changeDetection: ChangeDetectionStrategy.OnPush 14 | }) 15 | export class ProjectItemComponent { 16 | @Input() project: Project; 17 | @Output() select = new EventEmitter(); 18 | } 19 | -------------------------------------------------------------------------------- /src/features/settings/settings.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from "@angular/core"; 2 | import { SharedModule } from "../../shared/shared.module"; 3 | import { SettingsService } from "./service/settings.service"; 4 | 5 | @NgModule({ 6 | imports: [SharedModule], 7 | declarations: [], 8 | exports: [], 9 | entryComponents: [], 10 | providers: [] 11 | }) 12 | export class SettingsModule { 13 | static forRoot(): ModuleWithProviders { 14 | return { 15 | ngModule: SettingsModule, 16 | providers: [SettingsService] 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/features/scoping/components/counted-votes/counted-votes.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from "@angular/core"; 2 | import { Votes } from "../../../../models/task"; 3 | import * as _ from "lodash"; 4 | 5 | @Component({ 6 | selector: "app-counted-votes", 7 | templateUrl: "./counted-votes.component.html" 8 | }) 9 | export class CountedVotesComponent implements OnInit { 10 | @Input() votes: Array; 11 | 12 | @Input() participantCount: number; 13 | 14 | constructor() {} 15 | 16 | ngOnInit() {} 17 | 18 | getRangeForEmptyVotes(num) { 19 | return _.range(num); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/features/scoping/components/result-estimate/result-estimate.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Total Estimate

3 |
4 | {{ estimate }} 5 |
6 |

Hours

7 |
8 | 9 |
10 | 11 |
12 |

Total Estimate

13 |
14 | {{ estimate }} 15 |
16 |

Hours

17 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | *.log 7 | *.tmp 8 | *.tmp.* 9 | log.txt 10 | *.sublime-project 11 | *.sublime-workspace 12 | .vscode/ 13 | npm-debug.log* 14 | 15 | .idea/ 16 | .sourcemaps/ 17 | .sass-cache/ 18 | .tmp/ 19 | .versions/ 20 | coverage/ 21 | dist/ 22 | node_modules/ 23 | tmp/ 24 | temp/ 25 | hooks/ 26 | src/config/ 27 | platforms/ 28 | plugins/ 29 | plugins/android.json 30 | plugins/ios.json 31 | www/ 32 | functions/lib 33 | $RECYCLE.BIN/ 34 | 35 | .DS_Store 36 | Thumbs.db 37 | UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /functions/src/api/connections/delete-connection.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import { firestore } from "../../services"; 3 | 4 | export async function deleteConnection( 5 | req: express.Request, 6 | res: express.Response 7 | ) { 8 | const uid = res.locals.user.uid; 9 | const connectionId = req.params.connectionId; 10 | await firestore 11 | .doc(`users/${uid}/connections/${connectionId}`) 12 | .delete() 13 | .then( 14 | () => { 15 | return res.status(201).send(); 16 | }, 17 | err => { 18 | return res.status(400).json({ message: err }); 19 | } 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /functions/src/api/connections/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | 3 | import { addConnection } from "./add-connection"; 4 | import { projectsRouter } from "./projects"; 5 | import { tasksRouter } from "./tasks"; 6 | import { deleteConnection } from "./delete-connection"; 7 | 8 | export const connectionsRouter = express.Router({ mergeParams: true }); 9 | 10 | connectionsRouter.post("/", addConnection); 11 | 12 | connectionsRouter.delete("/:connectionId", deleteConnection); 13 | 14 | connectionsRouter.use("/:connectionId/projects", projectsRouter); 15 | 16 | connectionsRouter.use("/:connectionId/tasks", tasksRouter); 17 | -------------------------------------------------------------------------------- /src/features/connections/components/verify-modal/verify-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { NavParams, ViewController } from "ionic-angular"; 3 | 4 | @Component({ 5 | selector: "app-verify-modal", 6 | templateUrl: "./verify-modal.component.html" 7 | }) 8 | export class VerifyModalComponent implements OnInit { 9 | connectionData; 10 | 11 | constructor(private params: NavParams, private viewCtrl: ViewController) {} 12 | 13 | ngOnInit() { 14 | this.connectionData = this.params.data.connectionData; 15 | } 16 | 17 | dismiss() { 18 | this.viewCtrl.dismiss(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/models/scoping-session.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "./task"; 2 | 3 | export interface ScopingSession { 4 | id: string; 5 | sessionId: string; 6 | projectName: string; 7 | ownerId: string; 8 | connectionId: string; 9 | currentTaskId: string; 10 | numTasks: number; 11 | numScopedTasks: number; 12 | tasks: { [taskId: string]: Task }; 13 | participants: { [userId: string]: number }; 14 | } 15 | 16 | export enum SessionStatus { 17 | COMPLETE = "Session Completed", 18 | INCOMPLETE = "Session Incomplete" 19 | } 20 | 21 | export interface SessionValidation { 22 | sessionLink: string; 23 | accessCode: string; 24 | } 25 | -------------------------------------------------------------------------------- /src/models/history-item.ts: -------------------------------------------------------------------------------- 1 | import { ScopingSession, SessionStatus } from "./scoping-session"; 2 | import { SessionUserType } from "./user"; 3 | 4 | export interface HistoryItem extends Partial { 5 | id?: string; 6 | ownerId: string; 7 | connectionId: string; 8 | sessionId: string; 9 | sessionCode: string; 10 | projectId?: string; 11 | participants: { [uid: string]: number }; 12 | } 13 | 14 | export interface HistoryItemOptionEvent { 15 | userType: SessionUserType; 16 | item: HistoryItem; 17 | } 18 | 19 | export interface HistoryItemDetailEvent { 20 | status: SessionStatus; 21 | item: HistoryItem; 22 | } 23 | -------------------------------------------------------------------------------- /src/features/dashboard/pages/dashboard/dashboard.component.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { IonicPageModule } from "ionic-angular"; 3 | 4 | import { DashboardPage } from "./dashboard.component"; 5 | import { DashboardModule } from "../../dashboard.module"; 6 | import { ConnectionsModule } from "../../../connections/connections.module"; 7 | 8 | @NgModule({ 9 | declarations: [DashboardPage], 10 | imports: [ 11 | IonicPageModule.forChild(DashboardPage), 12 | DashboardModule, 13 | ConnectionsModule 14 | ], 15 | exports: [], 16 | entryComponents: [] 17 | }) 18 | export class DashboardPageModule {} 19 | -------------------------------------------------------------------------------- /src/features/connections/pages/select-project/select-project.component.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { IonicPageModule } from "ionic-angular"; 3 | import { SharedModule } from "../../../../shared/shared.module"; 4 | import { SelectProjectPage } from "./select-project.component"; 5 | import { ConnectionsModule } from "../../connections.module"; 6 | 7 | @NgModule({ 8 | declarations: [SelectProjectPage], 9 | imports: [ 10 | IonicPageModule.forChild(SelectProjectPage), 11 | SharedModule, 12 | ConnectionsModule 13 | ], 14 | exports: [SelectProjectPage] 15 | }) 16 | export class SelectProjectPageModule {} 17 | -------------------------------------------------------------------------------- /src/features/scoping/pages/session-results/session-results.component.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { IonicPageModule } from "ionic-angular"; 3 | import { SessionResultsPage } from "./session-results.component"; 4 | import { ScopingModule } from "../../scoping.module"; 5 | import { SharedModule } from "../../../../shared/shared.module"; 6 | 7 | @NgModule({ 8 | declarations: [SessionResultsPage], 9 | imports: [ 10 | IonicPageModule.forChild(SessionResultsPage), 11 | ScopingModule, 12 | SharedModule 13 | ], 14 | exports: [], 15 | entryComponents: [] 16 | }) 17 | export class SessionResultsPageModule {} 18 | -------------------------------------------------------------------------------- /src/features/scoping/pages/session-scoping/session-scoping.component.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { IonicPageModule } from "ionic-angular"; 3 | import { SessionScopingPage } from "./session-scoping.component"; 4 | import { ScopingModule } from "../../scoping.module"; 5 | import { SharedModule } from "../../../../shared/shared.module"; 6 | 7 | @NgModule({ 8 | declarations: [SessionScopingPage], 9 | imports: [ 10 | IonicPageModule.forChild(SessionScopingPage), 11 | ScopingModule, 12 | SharedModule 13 | ], 14 | exports: [], 15 | entryComponents: [] 16 | }) 17 | export class SessionScopingPageModule {} 18 | -------------------------------------------------------------------------------- /src/features/connections/pages/select-task-list/select-task-list.component.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { IonicPageModule } from "ionic-angular"; 3 | import { SharedModule } from "../../../../shared/shared.module"; 4 | import { SelectTaskListPage } from "./select-task-list.component"; 5 | import { ConnectionsModule } from "../../connections.module"; 6 | 7 | @NgModule({ 8 | declarations: [SelectTaskListPage], 9 | imports: [ 10 | IonicPageModule.forChild(SelectTaskListPage), 11 | SharedModule, 12 | ConnectionsModule 13 | ], 14 | exports: [SelectTaskListPage] 15 | }) 16 | export class SelectTaskListPageModule {} 17 | -------------------------------------------------------------------------------- /functions/src/api/session-links/delete-session.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | 3 | import { sessionService } from "../../services"; 4 | 5 | export async function deleteSession( 6 | req: express.Request, 7 | res: express.Response 8 | ) { 9 | const uid = res.locals.user.uid; 10 | const sessionLink = req.params.sessionLink; 11 | 12 | if (!sessionLink) { 13 | return res.status(400).json({ message: "Session Link is required." }); 14 | } 15 | 16 | try { 17 | await sessionService.deleteSession(uid, sessionLink); 18 | } catch (error) { 19 | return res.status(400).json({ message: error }); 20 | } 21 | 22 | return res.sendStatus(204); 23 | } 24 | -------------------------------------------------------------------------------- /src/features/connections/components/verify-modal/verify-modal.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |

TOKEN SUCCESS!

6 |
7 | 8 | {{ connectionData.externalData.company }} 9 | Connection Type: {{ connectionData.type }} 10 | Account Admin Name: {{ connectionData.externalData.name }} 11 | 12 | 13 | 14 | CONFIRM 15 | 16 | 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /src/features/connections/pages/select-task-list/select-task-list.component.scss: -------------------------------------------------------------------------------- 1 | app-select-task-list { 2 | 3 | 4 | ion-list.list { 5 | margin: 0; 6 | width: 100%; 7 | 8 | ion-list-header.list-header { 9 | 10 | .icon, h2 { 11 | display: inline-block; 12 | font-weight: bolder; 13 | text-transform: none; 14 | vertical-align: middle; 15 | } 16 | 17 | ion-label.label { 18 | margin-top: 3rem; 19 | } 20 | } 21 | 22 | ion-item.item.item-block .item-inner { 23 | border-bottom: 2px solid $primary-orange; 24 | } 25 | } 26 | 27 | .start-btn { 28 | margin-top: 2rem; 29 | 30 | button { 31 | width: 60%; 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /functions/src/services/index.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions'; 2 | import * as admin from 'firebase-admin'; 3 | 4 | import { EncryptionService } from './encryption.service'; 5 | import { TeamworkService } from './teamwork.service'; 6 | import { SessionService } from './session.service'; 7 | 8 | export const config = functions.config(); 9 | 10 | admin.initializeApp(config.firebase); 11 | 12 | export const auth = admin.auth(); 13 | export const firestore = admin.firestore(); 14 | 15 | export const encryptionService = new EncryptionService(); 16 | export const teamworkService = new TeamworkService(); 17 | export const sessionService = new SessionService(firestore); 18 | -------------------------------------------------------------------------------- /functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as functions from "firebase-functions"; 2 | import * as express from "express"; 3 | import * as cors from "cors"; 4 | 5 | import { idTokenAuth } from "./middleware"; 6 | import { apiRouter } from "./api"; 7 | 8 | const app = express(); 9 | app.disable("X-Powered-By"); 10 | 11 | app.use(cors({ origin: true })); 12 | 13 | app.use(idTokenAuth); 14 | 15 | app.use(apiRouter); 16 | app.get("/*", (req: express.Request, res: express.Response) => { 17 | res.status(404).send(); 18 | }); 19 | 20 | app.get("/fw", (req: express.Request, res: express.Response) => { 21 | res.sendfile("/fw.html"); 22 | }); 23 | 24 | exports.api = functions.https.onRequest(app); 25 | -------------------------------------------------------------------------------- /functions/src/api/session-links/refresh-access-code.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | import { sessionService } from './../../services'; 4 | 5 | export async function refreshAccessCode( 6 | req: express.Request, 7 | res: express.Response 8 | ) { 9 | const uid = res.locals.user.uid; 10 | const sessionLink = req.params.sessionLink; 11 | 12 | if (!sessionLink) { 13 | return res.status(400).json({ message: 'Session Link is required.' }); 14 | } 15 | 16 | try { 17 | await sessionService.refreshAccessCode(sessionLink, uid); 18 | } catch (error) { 19 | return res.status(400).json({ message: error }); 20 | } 21 | 22 | return res.sendStatus(204); 23 | } 24 | -------------------------------------------------------------------------------- /src/features/scoping/components/session-access/session-access.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Enter Access Code

3 | 4 | 5 | 6 | 7 | 8 | 9 |

10 | An access code is required in order to enter a scoping session. Consult the moderator for credentials. 11 |

12 |
13 | 14 | 15 |
-------------------------------------------------------------------------------- /src/features/connections/pages/select-project/select-project.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Select Project 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

You have no projects for this specific connection. Please add a project for this connection in Teamwork in order to continue.

17 |
18 |
19 | -------------------------------------------------------------------------------- /src/features/dashboard/components/session-history-item/session-history-item.component.scss: -------------------------------------------------------------------------------- 1 | app-session-history-item { 2 | ion-card-header.card-header { 3 | padding: 0; 4 | 5 | h2 { 6 | font-weight: bold; 7 | color: $primary-orange; 8 | margin-left: 0.5rem; 9 | padding: 1.5rem 0; 10 | } 11 | 12 | button.fab { 13 | float: right; 14 | } 15 | } 16 | 17 | ion-card.card { 18 | border-bottom: 2px solid $primary-orange; 19 | box-shadow: none; 20 | 21 | p { 22 | color: $primary-orange; 23 | } 24 | } 25 | 26 | ion-item { 27 | p.cta-text { 28 | text-decoration: underline; 29 | font-weight: bold; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/features/authentication/authentication-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { Routes, RouterModule } from "@angular/router"; 3 | 4 | import { UnAuthGuard } from "../../app/un-auth.guard"; 5 | import { LoginPage } from "./pages/login/login.component"; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: "", 10 | pathMatch: "full", 11 | redirectTo: "/login" 12 | }, 13 | { 14 | path: "login", 15 | component: LoginPage, 16 | canActivate: [UnAuthGuard] 17 | } 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [RouterModule.forChild(routes)], 22 | exports: [RouterModule], 23 | providers: [UnAuthGuard] 24 | }) 25 | export class AuthenticationRoutingModule {} 26 | -------------------------------------------------------------------------------- /src/features/connections/pages/connection-details/connection-details.component.scss: -------------------------------------------------------------------------------- 1 | app-connection-details { 2 | 3 | ion-list.list { 4 | margin: 0; 5 | width: 100%; 6 | 7 | ion-list-header.list-header { 8 | 9 | .icon, h2 { 10 | display: inline-block; 11 | font-weight: bolder; 12 | text-transform: none; 13 | vertical-align: middle; 14 | } 15 | 16 | ion-label.label { 17 | margin-top: 3rem; 18 | } 19 | } 20 | 21 | ion-item.item.item-block .item-inner { 22 | border-bottom: 2px solid $primary-orange; 23 | margin-right: 3rem; 24 | } 25 | } 26 | 27 | .delete-btn { 28 | margin-top: 2rem; 29 | 30 | button { 31 | width: 60%; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/features/scoping/components/counted-votes/counted-votes.component.scss: -------------------------------------------------------------------------------- 1 | app-counted-votes { 2 | .vote-group { 3 | align-items: center; 4 | display: flex; 5 | flex-wrap: wrap; 6 | justify-content: center; 7 | margin: 25px; 8 | text-align: center; 9 | 10 | h2 { 11 | flex-direction: column; 12 | flex-wrap: nowrap; 13 | padding: 0; 14 | } 15 | 16 | .vote { 17 | background: white; 18 | border: 1px solid $primary-orange ; 19 | border-radius: 4px; 20 | flex-direction: row; 21 | font-size: 34px; 22 | font-weight: 500; 23 | margin: 20px 5px 5px 5px; 24 | padding: 10px 25px; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/jest-test.helper.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from "@angular/core/testing"; 2 | 3 | type CompilerOptions = Partial<{ 4 | providers: any[]; 5 | useJit: boolean; 6 | preserveWhitespaces: boolean; 7 | }>; 8 | export type ConfigureFn = (testBed: typeof TestBed) => void; 9 | 10 | export const configureTests = ( 11 | configure: ConfigureFn, 12 | compilerOptions: CompilerOptions = {} 13 | ) => { 14 | const compilerConfig: CompilerOptions = { 15 | preserveWhitespaces: false, 16 | ...compilerOptions 17 | }; 18 | 19 | const configuredTestBed = TestBed.configureCompiler(compilerConfig); 20 | 21 | configure(configuredTestBed); 22 | 23 | return configuredTestBed.compileComponents().then(() => configuredTestBed); 24 | }; 25 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from "@angular/core"; 2 | import { RouterModule } from "@angular/router"; 3 | import { RouterStateSerializer } from "@ngrx/router-store"; 4 | import { CustomSerializer } from "../store/router.reducer"; 5 | import { RouterFacade } from "../store/router.facade"; 6 | 7 | @NgModule({ 8 | imports: [], 9 | exports: [RouterModule] 10 | }) 11 | export class AppRoutingModule { 12 | static forRoot(): ModuleWithProviders { 13 | return { 14 | ngModule: AppRoutingModule, 15 | providers: [ 16 | { 17 | provide: RouterStateSerializer, 18 | useClass: CustomSerializer 19 | }, 20 | RouterFacade 21 | ] 22 | }; 23 | } 24 | } -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "build": "tsc", 5 | "serve": "npm run build && firebase serve --only functions", 6 | "shell": "npm run build && firebase functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "main": "lib/index.js", 12 | "dependencies": { 13 | "cors": "^2.8.4", 14 | "express": "^4.16.3", 15 | "firebase-admin": "~5.12.1", 16 | "firebase-functions": "^1.1.0", 17 | "lodash": "^4.17.10", 18 | "request": "^2.85.0", 19 | "request-promise-native": "^1.0.5" 20 | }, 21 | "devDependencies": { 22 | "typescript": "^2.5.3" 23 | }, 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /src/features/scoping/components/vote/vote.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { VoteComponent } from "./vote.component"; 4 | 5 | describe("VoteComponent", () => { 6 | let component: VoteComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [VoteComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(VoteComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/dashboard/components/join-session/join-session.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ChangeDetectionStrategy, 4 | Output, 5 | EventEmitter 6 | } from "@angular/core"; 7 | 8 | @Component({ 9 | selector: "app-join-session", 10 | templateUrl: "./join-session.component.html", 11 | changeDetection: ChangeDetectionStrategy.OnPush 12 | }) 13 | export class JoinSessionComponent { 14 | 15 | @Output() join = new EventEmitter(); 16 | 17 | onJoin(sessionCode: string) { 18 | if (sessionCode.includes("juntoscope.com")) { 19 | sessionCode = sessionCode.substring(sessionCode.lastIndexOf("/") + 1); 20 | } 21 | if (sessionCode && sessionCode.length > 3) { 22 | this.join.emit(sessionCode); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/features/scoping/components/session-access/session-access.component.scss: -------------------------------------------------------------------------------- 1 | app-session-access { 2 | form { 3 | align-items: center; 4 | display: flex; 5 | flex-direction: column; 6 | height: 100%; 7 | justify-content: center; 8 | width: 100%; 9 | 10 | ion-item.item-block.item[no-lines] { 11 | border: 2px solid $primary-orange; 12 | border-radius: 10px; 13 | height: 8%; 14 | margin-bottom: 1rem; 15 | width: 90%; 16 | 17 | ion-input.input { 18 | color: #000; 19 | font-weight: 400; 20 | } 21 | } 22 | 23 | p { 24 | font-style: italic; 25 | width: 80%; 26 | text-align: center; 27 | } 28 | } 29 | 30 | button.button { 31 | align-items: center; 32 | width: 60%; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/features/scoping/components/task-card/task-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { TaskCardComponent } from "./task-card.component"; 4 | 5 | describe("TaskCardComponent", () => { 6 | let component: TaskCardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [TaskCardComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(TaskCardComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/connections/components/task-list/task-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { TaskListComponent } from "./task-list.component"; 4 | 5 | describe("TaskListComponent", () => { 6 | let component: TaskListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [TaskListComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(TaskListComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/dashboard/components/session-history-list/session-history-list.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

History

4 |
5 |
6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |

You don't have any history yet. Join or start a session to add it to your history

15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/features/connections/components/project-item/project-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { ProjectItemComponent } from "./project-item.component"; 4 | 5 | describe("ProjectItemComponent", () => { 6 | let component: ProjectItemComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ProjectItemComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ProjectItemComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/connections/components/project-list/project-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { ProjectListComponent } from "./project-list.component"; 4 | 5 | describe("ProjectListComponent", () => { 6 | let component: ProjectListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ProjectListComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ProjectListComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/dashboard/components/join-session/join-session.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { JoinSessionComponent } from "./join-session.component"; 4 | 5 | describe("JoinSessionComponent", () => { 6 | let component: JoinSessionComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [JoinSessionComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(JoinSessionComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/scoping/pages/session-results/session-results.component.scss: -------------------------------------------------------------------------------- 1 | app-session-results { 2 | .session-results-top { 3 | text-align: center; 4 | h1 { 5 | text-transform: uppercase; 6 | } 7 | } 8 | 9 | .tasks-header { 10 | display: grid; 11 | color: white; 12 | grid-template-columns: 0.25fr 1fr; 13 | margin: 10px; 14 | background: #f1652d; 15 | border-radius: 5px; 16 | ion-label.label { 17 | margin: 0; 18 | padding: 4px; 19 | } 20 | 21 | .first { 22 | text-align: center; 23 | } 24 | } 25 | 26 | .tasks-item { 27 | display: grid; 28 | color: #c94313; 29 | grid-template-columns: 0.25fr 1fr; 30 | ion-label.label { 31 | margin: 0; 32 | } 33 | 34 | .first { 35 | text-align: center; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/features/connections/components/instructions/instructions.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { InstructionsComponent } from "./instructions.component"; 4 | 5 | describe("InstructionsComponent", () => { 6 | let component: InstructionsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [InstructionsComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(InstructionsComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/scoping/components/counted-votes/counted-votes.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { CountedVotesComponent } from "./counted-votes.component"; 4 | 5 | describe("CountedVotesComponent", () => { 6 | let component: CountedVotesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [CountedVotesComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(CountedVotesComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/scoping/components/select-result/select-result.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { SelectResultComponent } from "./select-result.component"; 4 | 5 | describe("SelectResultComponent", () => { 6 | let component: SelectResultComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [SelectResultComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SelectResultComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "dom", 9 | "es2015" 10 | ], 11 | "module": "es2015", 12 | "moduleResolution": "node", 13 | "sourceMap": true, 14 | "target": "es5" 15 | }, 16 | "include": [ 17 | "src/**/*.ts" 18 | ], 19 | "exclude": [ 20 | "node_modules", 21 | "src/**/*.spec.ts", 22 | "src/**/__tests__/*.ts" 23 | ], 24 | "baseUrl": "./src/", 25 | "paths": { 26 | "@app/*": ["app/*"], 27 | "@env/*": ["environments/*"], 28 | "@models/*": ["models/*"], 29 | "@test/*": ["test/*"] 30 | }, 31 | "compileOnSave": false, 32 | "atom": { 33 | "rewriteTsconfig": false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/features/connections/pages/add-connection/add-connection.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { AddConnectionComponent } from "./add-connection.component"; 4 | 5 | describe("AddConnectionComponent", () => { 6 | let component: AddConnectionComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [AddConnectionComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(AddConnectionComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/connections/pages/select-project/select-project.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { SelectProjectComponent } from "./select-project.component"; 4 | 5 | describe("SelectProjectComponent", () => { 6 | let component: SelectProjectComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [SelectProjectComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SelectProjectComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/scoping/components/session-access/session-access.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { SessionAccessComponent } from "./session-access.component"; 4 | 5 | describe("SessionAccessComponent", () => { 6 | let component: SessionAccessComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [SessionAccessComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SessionAccessComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/scoping/components/session-header/session-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { SessionHeaderComponent } from "./session-header.component"; 4 | 5 | describe("SessionHeaderComponent", () => { 6 | let component: SessionHeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [SessionHeaderComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SessionHeaderComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/shared/components/info-modal/info-modal.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { NavParams, ViewController } from "ionic-angular"; 3 | 4 | @Component({ 5 | selector: "app-info-modal", 6 | templateUrl: "./info-modal.html" 7 | }) 8 | export class InfoModalComponent implements OnInit { 9 | title; 10 | text; 11 | label; 12 | callback; 13 | 14 | constructor(private viewCtrl: ViewController, private params: NavParams) {} 15 | 16 | ngOnInit() { 17 | this.title = this.params.data.title; 18 | this.text = this.params.data.text; 19 | this.label = this.params.data.label; 20 | this.callback = this.params.data.callback; 21 | } 22 | 23 | closeModal() { 24 | this.viewCtrl.dismiss(); 25 | } 26 | 27 | dismiss() { 28 | this.viewCtrl.dismiss(); 29 | this.callback(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/features/scoping/pages/session-results/session-results.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { SessionResultsComponent } from "./session-results.component"; 4 | 5 | describe("SessionResultsComponent", () => { 6 | let component: SessionResultsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [SessionResultsComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SessionResultsComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/scoping/pages/session-scoping/session-scoping.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { SessionScopingComponent } from "./session-scoping.component"; 4 | 5 | describe("SessionScopingComponent", () => { 6 | let component: SessionScopingComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [SessionScopingComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SessionScopingComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/connections/pages/select-task-list/select-task-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { SelectTaskListComponent } from "./select-task-list.component"; 4 | 5 | describe("SelectTaskListComponent", () => { 6 | let component: SelectTaskListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [SelectTaskListComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SelectTaskListComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/scoping/components/result-estimate/result-estimate.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { ResultEstimateComponent } from "./result-estimate.component"; 4 | 5 | describe("ResultEstimateComponent", () => { 6 | let component: ResultEstimateComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ResultEstimateComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ResultEstimateComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/store/router.actions.ts: -------------------------------------------------------------------------------- 1 | import { NavigationExtras, Params } from "@angular/router"; 2 | 3 | import { Action } from "@ngrx/store"; 4 | 5 | export interface NavigationOptions { 6 | path: any[]; 7 | query?: Params; 8 | extras?: NavigationExtras; 9 | } 10 | 11 | export enum RouterActionTypes { 12 | GO = "[Router] Go", 13 | BACK = "[Router] Back", 14 | FORWARD = "[Router] Forward" 15 | } 16 | 17 | export class GoAction implements Action { 18 | readonly type = RouterActionTypes.GO; 19 | constructor(public payload: NavigationOptions) {} 20 | } 21 | 22 | export class BackAction implements Action { 23 | readonly type = RouterActionTypes.BACK; 24 | } 25 | 26 | export class ForwardAction implements Action { 27 | readonly type = RouterActionTypes.FORWARD; 28 | } 29 | 30 | export type RouterActions = GoAction | BackAction | ForwardAction; 31 | -------------------------------------------------------------------------------- /functions/src/api/session-links/decode-session-link.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | 3 | import { sessionService } from "../../services"; 4 | 5 | export async function decodeSessionLink( 6 | req: express.Request, 7 | res: express.Response 8 | ) { 9 | const uid = res.locals.user.uid; 10 | const sessionLink = req.params.sessionLink; 11 | const accessCode = req.query.accessCode; 12 | 13 | if (!sessionLink) { 14 | return res.status(400).json({ message: "Session Link is required." }); 15 | } 16 | 17 | if (!accessCode) { 18 | return res.status(400).json({ message: "Access Code is required." }); 19 | } 20 | 21 | try { 22 | await sessionService.validateSession(sessionLink, accessCode, uid); 23 | } catch (error) { 24 | return res.status(400).json({ message: error }); 25 | } 26 | 27 | return res.send(204); 28 | } 29 | -------------------------------------------------------------------------------- /src/features/connections/components/connection-list/connection-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { ConnectionListComponent } from "./connection-list.component"; 4 | 5 | describe("ConnectionListComponent", () => { 6 | let component: ConnectionListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ConnectionListComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ConnectionListComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check out https://googlechromelabs.github.io/sw-toolbox/ for 3 | * more info on how to use sw-toolbox to custom configure your service worker. 4 | */ 5 | 6 | 'use strict'; 7 | importScripts('./build/sw-toolbox.js'); 8 | 9 | self.toolbox.options.cache = { 10 | name: 'ionic-cache', 11 | }; 12 | 13 | // pre-cache our key assets 14 | self.toolbox.precache([ 15 | './build/main.js', 16 | './build/vendor.js', 17 | './build/main.css', 18 | './build/polyfills.js', 19 | 'index.html', 20 | 'manifest.json', 21 | ]); 22 | 23 | // dynamically cache any other local assets 24 | self.toolbox.router.any('/*', self.toolbox.fastest); 25 | 26 | // for any other requests go to the network, cache, 27 | // and then only use that cached resource if your user goes offline 28 | self.toolbox.router.default = self.toolbox.networkFirst; 29 | -------------------------------------------------------------------------------- /src/features/connections/pages/connection-details/connection-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { ConnectionDetailsComponent } from "./connection-details.component"; 4 | 5 | describe("ConnectionDetailsComponent", () => { 6 | let component: ConnectionDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ConnectionDetailsComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(ConnectionDetailsComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/scoping/components/vote/vote.component.scss: -------------------------------------------------------------------------------- 1 | app-vote { 2 | .vote-form-container { 3 | display: flex; 4 | flex-direction: row; 5 | justify-content: center; 6 | margin: 15px; 7 | 8 | form { 9 | display: flex; 10 | flex-direction: row; 11 | 12 | ion-input { 13 | border-radius: 6px; 14 | -webkit-box-shadow: inset 5px 5px 13px -8px rgba(0, 0, 0, 0.75); 15 | -moz-box-shadow: inset 5px 5px 13px -8px rgba(0, 0, 0, 0.75); 16 | box-shadow: inset 5px 5px 13px -8px rgba(0, 0, 0, 0.75); 17 | background: #f2f2f2; 18 | margin-right: 10px; 19 | 20 | input { 21 | font-size: 16px; 22 | font-weight: 500; 23 | margin-top: 17px; 24 | } 25 | 26 | input::placeholder { 27 | color: #999999; 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/features/dashboard/components/session-history-item/session-history-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { SessionHistoryItemComponent } from "./session-history-item.component"; 4 | 5 | describe("SessionHistoryItemComponent", () => { 6 | let component: SessionHistoryItemComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [SessionHistoryItemComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SessionHistoryItemComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/dashboard/components/session-history-list/session-history-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from "@angular/core/testing"; 2 | 3 | import { SessionHistoryListComponent } from "./session-history-list.component"; 4 | 5 | describe("SessionHistoryListComponent", () => { 6 | let component: SessionHistoryListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [SessionHistoryListComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(SessionHistoryListComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it("should create", () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/features/authentication/authentication.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from "@angular/core"; 2 | 3 | import { StoreModule } from "@ngrx/store"; 4 | import { EffectsModule } from "@ngrx/effects"; 5 | 6 | import { SharedModule } from "../../shared/shared.module"; 7 | import { authReducer } from "./store/auth.reducer"; 8 | import { AuthService } from "./services/auth.service"; 9 | import { AuthFacade } from "./store/auth.facade"; 10 | 11 | @NgModule({ 12 | imports: [ 13 | SharedModule, 14 | StoreModule.forFeature("auth", authReducer), 15 | EffectsModule.forFeature([AuthFacade]) 16 | ], 17 | declarations: [], 18 | entryComponents: [] 19 | }) 20 | export class AuthenticationModule { 21 | static forRoot(): ModuleWithProviders { 22 | return { 23 | ngModule: AuthenticationModule, 24 | providers: [AuthService, AuthFacade] 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/features/connections/components/project-list/project-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter, OnInit } from "@angular/core"; 2 | import { Project } from "../../../../models/project"; 3 | import { Connection } from "../../../../models/connection"; 4 | import { ConnectionFacade } from "../../store/connection.facade"; 5 | 6 | @Component({ 7 | selector: "app-project-list", 8 | templateUrl: "./project-list.component.html" 9 | }) 10 | export class ProjectListComponent implements OnInit { 11 | @Input() projects: Project[]; 12 | @Output() select = new EventEmitter(); 13 | 14 | connection: Connection; 15 | 16 | constructor(private connectionFacade: ConnectionFacade) {} 17 | 18 | ngOnInit() { 19 | this.connectionFacade.selectedConnection$ 20 | .subscribe(con => { 21 | this.connection = con; 22 | }) 23 | .unsubscribe(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/features/dashboard/components/session-history-item/session-history-item.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

{{ item.projectName }}

7 |
8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 | 16 |

- {{ item.tasks && item.tasks[item.currentTaskId].name }}

17 |
18 | 19 | 20 |
21 |

{{ time | date: 'MM/dd/yyyy' }}

22 |
23 |
24 |

{{ ctaText }}

25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/features/authentication/pages/login/login.component.scss: -------------------------------------------------------------------------------- 1 | app-login { 2 | .container { 3 | align-items: center; 4 | display: flex; 5 | flex-direction: column; 6 | height: 100%; 7 | justify-content: center; 8 | width: 100%; 9 | 10 | p { 11 | width: 70%; 12 | font-weight: 600; 13 | } 14 | 15 | button.button { 16 | font-weight: 600; 17 | margin: 0.75rem; 18 | width: 60%; 19 | 20 | &:disabled { 21 | background-color: white; 22 | border: 2px solid $primary-orange; 23 | color: $primary-orange; 24 | } 25 | } 26 | 27 | ion-item.item.item-block .item-inner { 28 | border: none; 29 | } 30 | 31 | .item.item-md .checkbox-md { 32 | margin: 0px 10px 0px 0px; 33 | } 34 | 35 | } 36 | 37 | .terms { 38 | flex-direction: row; 39 | 40 | a { 41 | font-weight: 600; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/store/app.facade.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | 3 | import { Store, select } from "@ngrx/store"; 4 | 5 | import { AppState, AppQuery } from "./app.reducer"; 6 | import { filter } from "rxjs/operators"; 7 | 8 | @Injectable() 9 | export class AppFacade { 10 | /* 11 | * Observable Store Queries 12 | */ 13 | 14 | authRedirect$ = this.store.pipe(select(AppQuery.selectAuthRedirect)); 15 | 16 | uid$ = this.store.pipe( 17 | select(AppQuery.selectUid), 18 | filter(exists => !!exists) 19 | ); 20 | 21 | uidDocPath$ = this.store.pipe( 22 | select(AppQuery.selectUidDocPath), 23 | filter(exists => !!exists) 24 | ); 25 | 26 | connectionsClnPath$ = this.store.pipe( 27 | select(AppQuery.selectConnectionsClnPath), 28 | filter(exists => !!exists) 29 | ); 30 | 31 | /* 32 | * Module-level Effects 33 | */ 34 | 35 | constructor(private store: Store) {} 36 | 37 | /* 38 | * Action Creators 39 | */ 40 | } 41 | -------------------------------------------------------------------------------- /src/features/dashboard/components/session-detail-modal/session-detail-modal.component.html: -------------------------------------------------------------------------------- 1 | 2 | 18 |
19 | 20 | 21 |
22 |
-------------------------------------------------------------------------------- /src/features/scoping/components/select-result/select-result.component.html: -------------------------------------------------------------------------------- 1 |

2 | Results: JuntoScope has determined the average and maximum scope per all the 3 | votes submitted in this session. Please select one of the following values, 4 | or enter in a custom value to be recorded as the final scope for this task. 5 |

6 | 7 |
8 |
9 | 10 |

Average

11 |
12 |
13 | 14 |

Max

15 |
16 |
17 | 18 |

Custom

19 |
20 |
21 | -------------------------------------------------------------------------------- /src/features/scoping/components/select-result/select-result.component.scss: -------------------------------------------------------------------------------- 1 | app-select-result { 2 | .container { 3 | display: flex; 4 | flex-direction: row; 5 | justify-content: center; 6 | text-align: center; 7 | border-bottom: 2px solid; 8 | .result { 9 | margin: 25px 25px 15px 25px; 10 | 11 | button { 12 | height: 60px; 13 | width: 70px; 14 | margin: 0px; 15 | border-radius: 4px; 16 | .button-inner { 17 | font-size: 35px; 18 | font-weight: bolder; 19 | } 20 | } 21 | 22 | ion-input.input { 23 | border: 1px solid; 24 | border-radius: 4px; 25 | height: 60px; 26 | width: 70px; 27 | input { 28 | height: calc(100% - 8px - 16px); 29 | font-size: 35px; 30 | font-weight: bolder; 31 | } 32 | } 33 | 34 | .selected { 35 | background-color: $primary-orange; 36 | color: white; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/features/scoping/components/vote/vote.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Output, EventEmitter } from "@angular/core"; 2 | import { 3 | FormGroup, 4 | FormBuilder, 5 | Validators, 6 | FormControl 7 | } from "@angular/forms"; 8 | 9 | @Component({ 10 | selector: "app-vote", 11 | templateUrl: "./vote.component.html" 12 | }) 13 | export class VoteComponent implements OnInit { 14 | @Output() vote = new EventEmitter(); 15 | 16 | voteForm: FormGroup; 17 | error: string; 18 | 19 | constructor(private fb: FormBuilder) {} 20 | 21 | ngOnInit() { 22 | this.createForm(); 23 | } 24 | 25 | createForm() { 26 | this.voteForm = this.fb.group({ 27 | estimate: ["", [Validators.required, this.isValid]] 28 | }); 29 | } 30 | 31 | sendVote() { 32 | this.vote.emit(this.voteForm.value.estimate); 33 | } 34 | 35 | isValid(control: FormControl): any { 36 | if (control.value < 0.5) { 37 | return { invalid_quantity: true }; 38 | } 39 | 40 | return null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/store/router.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Params, RouterStateSnapshot } from "@angular/router"; 2 | import { RouterStateSerializer, RouterReducerState } from "@ngrx/router-store"; 3 | 4 | interface SerializedRouterState { 5 | url: string; 6 | params: Params; 7 | queryParams: Params; 8 | } 9 | 10 | export class CustomSerializer 11 | implements RouterStateSerializer { 12 | serialize(snapshot: RouterStateSnapshot): SerializedRouterState { 13 | const { url, root: { queryParams } } = snapshot; 14 | let { root: route } = snapshot; 15 | 16 | while (route.firstChild) { 17 | route = route.firstChild; 18 | } 19 | 20 | const { params } = route; 21 | 22 | return { url, params, queryParams }; 23 | } 24 | } 25 | 26 | export type RouterState = RouterReducerState; 27 | 28 | export const initialRouterState: RouterState = { 29 | state: { url: null, params: null, queryParams: null }, 30 | navigationId: 0 31 | }; 32 | 33 | export { routerReducer } from "@ngrx/router-store"; 34 | -------------------------------------------------------------------------------- /src/features/dashboard/pages/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | JuntoScope 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/store/router.actions.spec.ts: -------------------------------------------------------------------------------- 1 | import * as RouterActions from "./router.actions"; 2 | 3 | describe("Router Actions", () => { 4 | describe("Go Action", () => { 5 | it("should create action", () => { 6 | const payload: RouterActions.NavigationOptions = { path: ["/login"] }; 7 | const action = new RouterActions.GoAction(payload); 8 | 9 | expect(action).toEqual({ 10 | type: RouterActions.RouterActionTypes.GO, 11 | payload 12 | }); 13 | }); 14 | }); 15 | 16 | describe("Back Action", () => { 17 | it("should create action", () => { 18 | const action = new RouterActions.BackAction(); 19 | 20 | expect(action).toEqual({ 21 | type: RouterActions.RouterActionTypes.BACK 22 | }); 23 | }); 24 | }); 25 | 26 | describe("Forward Action", () => { 27 | it("should create action", () => { 28 | const action = new RouterActions.ForwardAction(); 29 | 30 | expect(action).toEqual({ 31 | type: RouterActions.RouterActionTypes.FORWARD 32 | }); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/features/connections/components/share-scope-link-modal/share-scope-link-modal.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | Share Scope Link 9 | 10 | 11 | 12 | 13 |

{{connectionName}}

14 |

{{projectName}}

15 | 16 | 29 | 30 | 33 |
34 | -------------------------------------------------------------------------------- /src/features/scoping/pages/session-scoping/session-scoping.component.scss: -------------------------------------------------------------------------------- 1 | app-session-scoping { 2 | 3 | .loading { 4 | display: flex; 5 | justify-content: center; 6 | top: 400px; 7 | } 8 | 9 | .session-info-container { 10 | margin: 30px; 11 | } 12 | 13 | .session-votes-counted { 14 | display: flex; 15 | justify-content: center; 16 | background: $primary-orange; 17 | p { 18 | color: white; 19 | } 20 | } 21 | 22 | ion-list.list { 23 | ion-item.item { 24 | border-bottom: 1px solid $primary-orange; 25 | .item-inner { 26 | border-bottom: none; 27 | } 28 | &:last-child { 29 | border-bottom: none; 30 | } 31 | } 32 | } 33 | 34 | ion-footer.footer { 35 | position: initial; 36 | ion-toolbar.toolbar { 37 | .toolbar-background { 38 | background: white; 39 | } 40 | .toolbar-content { 41 | display: flex; 42 | justify-content: center; 43 | } 44 | button.button { 45 | width: 50%; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/features/authentication/pages/login/login.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | First, we need a way of identifying your participation. 5 | Please sign in with: 6 |

7 | 8 | 11 | 12 | 15 | 16 | 19 | 20 |
21 | 22 | 23 | I Agree 24 | 25 |
26 | 27 |
28 | Terms of Service 29 | and 30 | Privacy Policy 31 |
32 |
33 | 34 |
35 | -------------------------------------------------------------------------------- /functions/src/middleware/id-token-auth.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | import { auth } from './../services'; 4 | 5 | /** 6 | * Middleware function that will intercept the id token 7 | * from Authorization Headers of all incoming requests. 8 | * And will validate the token assigning the user object 9 | * to the locals response object. 10 | */ 11 | export function idTokenAuth(req: express.Request, res: express.Response, next: express.NextFunction) { 12 | const authHeader = req.headers && req.headers.authorization; 13 | const headerValue = authHeader 14 | ? Array.isArray(authHeader) ? authHeader[0] : authHeader 15 | : ''; 16 | 17 | if (!headerValue || !headerValue.startsWith('Bearer ')) { 18 | res.status(403).send('Unauthorized'); 19 | return; 20 | } 21 | 22 | const idToken = headerValue.split('Bearer ')[1]; 23 | 24 | auth.verifyIdToken(idToken) 25 | .then(decodedIdToken => { 26 | res.locals.user = decodedIdToken; 27 | return next(); 28 | }) 29 | .catch(error => { 30 | res.status(403).send('Unauthorized'); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/test/jest-global-mocks.ts: -------------------------------------------------------------------------------- 1 | global["CSS"] = null; 2 | 3 | const mock = () => { 4 | let storage = {}; 5 | return { 6 | getItem: key => (key in storage ? storage[key] : null), 7 | setItem: (key, value) => (storage[key] = value || ""), 8 | removeItem: key => delete storage[key], 9 | clear: () => (storage = {}) 10 | }; 11 | }; 12 | 13 | Object.defineProperty(window, "localStorage", { value: mock() }); 14 | Object.defineProperty(window, "sessionStorage", { value: mock() }); 15 | Object.defineProperty(document, "doctype", { 16 | value: "" 17 | }); 18 | Object.defineProperty(window, "getComputedStyle", { 19 | value: () => { 20 | return { 21 | display: "none", 22 | appearance: ["-webkit-appearance"] 23 | }; 24 | } 25 | }); 26 | /** 27 | * ISSUE: https://github.com/angular/material2/issues/7101 28 | * Workaround for JSDOM missing transform property 29 | */ 30 | Object.defineProperty(document.body.style, "transform", { 31 | value: () => { 32 | return { 33 | enumerable: true, 34 | configurable: true 35 | }; 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /src/features/connections/pages/add-connection/add-connection.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | New Connection 4 | 5 | 6 | 7 | 8 | 9 |
10 |

Connect to Teamwork

11 | 12 |

13 | NOTICE 14 |
15 | This is made specifically for Teamwork. 16 |
17 | If you are not using Teamwork, we recommend trying it! 18 |

19 | 20 |

21 | Connections should only be made by Teamwork users with Administrator privileges in order to successfully deploy 22 | scoping sessions and assign scoped times to tasks. 23 |

24 | 25 |

26 | Register here and use the code "juntoscope" for a 10% discount! 27 |

28 | 29 | 30 |
31 |
32 | -------------------------------------------------------------------------------- /src/features/scoping/components/result-estimate/result-estimate.component.scss: -------------------------------------------------------------------------------- 1 | app-result-estimate { 2 | .result-estimate-container { 3 | align-items: center; 4 | display: flex; 5 | flex-direction: column; 6 | 7 | h2, 8 | h3 { 9 | text-transform: uppercase; 10 | margin-bottom: 25px; 11 | } 12 | 13 | p { 14 | font-size: 16px; 15 | text-transform: capitalize; 16 | margin: 10px 0 20px 0; 17 | } 18 | 19 | 20 | .box-estimate { 21 | background: white; 22 | border: 1px solid $primary-orange; 23 | border-radius: 4px; 24 | font-size: 34px; 25 | font-weight: bold; 26 | margin: 0; 27 | padding: 10px 25px; 28 | position: initial; 29 | } 30 | 31 | 32 | .final-estimate-container { 33 | margin: 10px; 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | text-align: center; 38 | background: rgba(255, 200, 148, 0.5); 39 | width: 85%; 40 | border-radius: 6px; 41 | } 42 | } 43 | 44 | .border { 45 | border-bottom: 2px solid $primary-orange; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/features/connections/pages/select-task-list/select-task-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Select Task List 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

{{ projectName }}

18 |
19 |
20 | 21 | 22 |
23 | 24 |
25 | 26 |
27 |
28 | 29 | 30 |

You have no task lists for this specific project!

31 |
32 | 33 |
34 | -------------------------------------------------------------------------------- /src/app/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { 3 | CanActivate, 4 | ActivatedRouteSnapshot, 5 | RouterStateSnapshot 6 | } from "@angular/router"; 7 | 8 | import { map, tap, filter } from "rxjs/operators"; 9 | 10 | import { RouterFacade } from "../store/router.facade"; 11 | import { AuthFacade } from "../features/authentication/store/auth.facade"; 12 | import { AuthUiState } from "../features/authentication/store/auth.reducer"; 13 | 14 | @Injectable() 15 | export class AuthGuard implements CanActivate { 16 | constructor( 17 | private routerFacade: RouterFacade, 18 | private authFacade: AuthFacade 19 | ) {} 20 | 21 | canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 22 | return this.authFacade.uiState$.pipe( 23 | filter(uiState => uiState !== AuthUiState.LOADING), 24 | map(uiState => uiState === AuthUiState.AUTHENTICATED), 25 | tap(isAuth => { 26 | if (!isAuth) { 27 | this.routerFacade.navigate({ 28 | path: ["/login"], 29 | query: { returnUrl: state.url } 30 | }); 31 | } 32 | }) 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/shared/loading.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { Loading, LoadingController } from "ionic-angular"; 3 | 4 | @Injectable() 5 | export class LoadingService { 6 | loading: Loading; 7 | isLoading = false; 8 | 9 | constructor(private loadingCtrl: LoadingController) {} 10 | 11 | // hide loading object and create to show again in view 12 | hide() { 13 | if (this.loading) { 14 | this.loading.dismiss().then(() => { 15 | this.loading = this.loadingCtrl.create({ 16 | spinner: "crescent", 17 | cssClass: "custom-loading" 18 | }); 19 | }); 20 | } 21 | } 22 | 23 | // dismiss loading object completely from view 24 | dismiss() { 25 | if (this.isLoading) { 26 | this.loading.dismiss().catch(() => {}); 27 | this.isLoading = false; 28 | } 29 | } 30 | 31 | // show loading object in view 32 | present() { 33 | this.isLoading = true; 34 | 35 | this.loading = this.loadingCtrl.create({ 36 | spinner: "crescent", 37 | cssClass: "custom-loading" 38 | }); 39 | 40 | if (!this.isLoading) { 41 | this.loading.present(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/features/dashboard/components/session-history-list/session-history-list.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ChangeDetectionStrategy, 4 | Input, 5 | Output, 6 | EventEmitter 7 | } from "@angular/core"; 8 | import { 9 | HistoryItem, 10 | HistoryItemOptionEvent, 11 | HistoryItemDetailEvent 12 | } from "../../../../models/history-item"; 13 | import { SessionUserType } from "../../../../models/user"; 14 | import { SessionStatus } from "../../../../models/scoping-session"; 15 | 16 | @Component({ 17 | selector: "app-session-history-list", 18 | templateUrl: "./session-history-list.component.html", 19 | changeDetection: ChangeDetectionStrategy.OnPush 20 | }) 21 | export class SessionHistoryListComponent { 22 | @Input() items: HistoryItem[]; 23 | @Input() uid: string; 24 | 25 | @Output() options = new EventEmitter(); 26 | @Output() detail = new EventEmitter(); 27 | 28 | handleOptionClick(userType: SessionUserType, item: HistoryItem) { 29 | this.options.emit({ userType, item }); 30 | } 31 | 32 | handleDetailClick(status: SessionStatus, item: HistoryItem) { 33 | this.detail.emit({ status, item }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/features/connections/pages/connection-details/connection-details.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Connection Detail 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

{{ connection.externalData.company }} - {{ connection.type | titlecase }}

16 |
17 |
18 | 19 | 20 | Account: {{ connection.type | titlecase }} 21 | 22 | 23 | Name: {{ connection.externalData.name }} 24 | 25 | 26 | Company: {{ connection.externalData.company }} 27 | 28 |
29 | 30 |
31 | 32 |
33 | 34 |
35 | 36 |
-------------------------------------------------------------------------------- /src/shared/pipes/object-iterators.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from "@angular/core"; 2 | 3 | @Pipe({ name: "objKeys" }) 4 | export class ObjectKeysPipe implements PipeTransform { 5 | transform(obj: Object): Array { 6 | const transformed = []; 7 | 8 | for (const key in obj) { 9 | if (obj.hasOwnProperty(key)) { 10 | transformed.push(key); 11 | } 12 | } 13 | 14 | return transformed; 15 | } 16 | } 17 | 18 | @Pipe({ name: "objValues" }) 19 | export class ObjectValuesPipe implements PipeTransform { 20 | transform(obj: Object): Array { 21 | const transformed = []; 22 | 23 | for (const key in obj) { 24 | if (obj.hasOwnProperty(key)) { 25 | transformed.push(obj[key]); 26 | } 27 | } 28 | 29 | return transformed; 30 | } 31 | } 32 | 33 | @Pipe({ name: "objKeyValue" }) 34 | export class ObjectKeyValuePipe implements PipeTransform { 35 | transform(obj: Object): Array<{ key: string; value: any }> { 36 | const transformed = []; 37 | 38 | for (const key in obj) { 39 | if (obj.hasOwnProperty(key)) { 40 | transformed.push({ key, value: obj[key] }); 41 | } 42 | } 43 | 44 | return transformed; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /functions/src/api/connections/projects/get-projects.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { firestore, teamworkService, encryptionService } from './../../../services'; 3 | 4 | export async function getProjects(req: express.Request, res: express.Response) { 5 | const uid = res.locals.user.uid; 6 | const connectionId = req.params.connectionId; 7 | 8 | let connectionRef; 9 | try { 10 | connectionRef = await firestore.collection(`/users/${uid}/connections`).doc(connectionId).get(); 11 | } catch(error) { 12 | return res.status(400).json({ message: 'Error getting connection' }); 13 | } 14 | 15 | const connection = connectionRef.data(); 16 | 17 | switch (connection.type.toLowerCase()) { 18 | case 'teamwork': { 19 | 20 | let teamworkResponse; 21 | try { 22 | teamworkResponse = await teamworkService.getProjects(encryptionService.decrypt(connection.token), connection.externalData.baseUrl); 23 | } catch(error) { 24 | return res.status(400).json({ message: error.message }); 25 | } 26 | 27 | return res.json(teamworkResponse); 28 | } 29 | 30 | default: { 31 | return res.status(400).json({ message: 'Unknown Connection Type' }); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import "zone.js/dist/long-stack-trace-zone"; 4 | import "zone.js/dist/proxy.js"; 5 | import "zone.js/dist/sync-test"; 6 | import "zone.js/dist/jasmine-patch"; 7 | import "zone.js/dist/async-test"; 8 | import "zone.js/dist/fake-async-test"; 9 | 10 | import { getTestBed } from "@angular/core/testing"; 11 | import { 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting 14 | } from "@angular/platform-browser-dynamic/testing"; 15 | 16 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 17 | declare var __karma__: any; 18 | declare var require: any; 19 | 20 | // Prevent Karma from running prematurely. 21 | __karma__.loaded = function(): void { 22 | // noop 23 | }; 24 | 25 | // First, initialize the Angular testing environment. 26 | getTestBed().initTestEnvironment( 27 | BrowserDynamicTestingModule, 28 | platformBrowserDynamicTesting() 29 | ); 30 | // Then we find all the tests. 31 | const context: any = require.context("./", true, /\.spec\.ts$/); 32 | // And load the modules. 33 | context.keys().map(context); 34 | // Finally, start Karma to run the tests. 35 | __karma__.start(); 36 | -------------------------------------------------------------------------------- /functions/src/api/connections/tasks/get-task.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { firestore, teamworkService, encryptionService } from './../../../services'; 3 | 4 | export async function getTask(req: express.Request, res: express.Response) { 5 | const uid = res.locals.user.uid; 6 | const connectionId = req.params.connectionId; 7 | const taskId = req.params.taskId; 8 | 9 | let connectionRef; 10 | try { 11 | connectionRef = await firestore.collection(`/users/${uid}/connections`).doc(connectionId).get(); 12 | } catch(error) { 13 | return res.status(400).json({ message: 'Error getting connection' }); 14 | } 15 | 16 | const connection = connectionRef.data(); 17 | 18 | switch (connection.type.toLowerCase()) { 19 | case 'teamwork': { 20 | 21 | let teamworkResponse; 22 | try { 23 | teamworkResponse = await teamworkService.getTask(encryptionService.decrypt(connection.token), connection.externalData.baseUrl, taskId); 24 | } catch(error) { 25 | return res.status(400).json({ message: error.message }); 26 | } 27 | 28 | return res.json(teamworkResponse); 29 | } 30 | 31 | default: { 32 | return res.status(400).json({ message: 'Unknown Connection Type' }); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/features/connections/components/verify-modal/verify-modal.component.scss: -------------------------------------------------------------------------------- 1 | app-verify-modal { 2 | background: rgba(0, 0, 0, 0.5); 3 | width: 100%; 4 | height: 50%; 5 | ion-content.content { 6 | transform: translateY(50%); 7 | border-radius: 2rem; 8 | height: 230px; 9 | width: calc(100% - 7rem); 10 | margin: 3.5rem; 11 | 12 | display: flex; 13 | justify-content: center; 14 | 15 | ion-list.list { 16 | flex-direction: column; 17 | margin-top: 8px; 18 | margin-bottom: 8px; 19 | 20 | ion-list-header.list-header { 21 | margin: 0; 22 | border-radius: 2rem; 23 | h2 { 24 | font-weight: bolder; 25 | } 26 | } 27 | 28 | ion-label.label { 29 | margin: 10px 0 10px 0; 30 | &.title { 31 | font-weight: bold; 32 | font-size: 2rem; 33 | } 34 | } 35 | 36 | ion-item.item { 37 | border-bottom-left-radius: 2rem; 38 | border-bottom-right-radius: 2rem; 39 | border-top: 2px solid $primary-orange; 40 | text-align: center; 41 | margin-top: 3rem; 42 | ion-label.label { 43 | font-weight: bold; 44 | font-size: 2rem; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /functions/src/services/encryption.service.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | const functions = require("firebase-functions"); 3 | 4 | export class EncryptionService { 5 | private algorithm = "aes-256-cbc"; 6 | // Must be 32 characters long 7 | 8 | private SECRET_KEY = functions.config().encryption.secret; 9 | 10 | encrypt(text: string) { 11 | const iv = crypto.randomBytes(16); // Must be 16 bytes long for AES encryption 12 | const cipher = crypto.createCipheriv( 13 | this.algorithm, 14 | new Buffer(this.SECRET_KEY), 15 | iv 16 | ); 17 | 18 | let encrypted = cipher.update(text, "utf8"); 19 | encrypted = Buffer.concat([encrypted, cipher.final()]); 20 | 21 | return `${iv.toString("hex")}:${encrypted.toString("hex")}`; 22 | } 23 | 24 | decrypt(text: string) { 25 | const parts = text.split(":"); 26 | const iv = new Buffer(parts.shift(), "hex"); 27 | const encryptedText = new Buffer(parts.join(":"), "hex"); 28 | const decipher = crypto.createDecipheriv( 29 | this.algorithm, 30 | new Buffer(this.SECRET_KEY), 31 | iv 32 | ); 33 | 34 | let decrypted = decipher.update(encryptedText); 35 | decrypted = Buffer.concat([decrypted, decipher.final()]); 36 | 37 | return decrypted.toString(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /functions/src/api/connections/projects/tasklists/get-tasks.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { firestore, teamworkService, encryptionService } from './../../../../services'; 3 | 4 | export async function getTasks(req: express.Request, res: express.Response) { 5 | const uid = res.locals.user.uid; 6 | const connectionId = req.params.connectionId; 7 | const tasklistId = req.params.tasklistId; 8 | 9 | let connectionRef; 10 | try { 11 | connectionRef = await firestore.collection(`/users/${uid}/connections`).doc(connectionId).get(); 12 | } catch(error) { 13 | return res.status(400).json({ message: 'Error getting connection' }); 14 | } 15 | 16 | const connection = connectionRef.data(); 17 | 18 | switch (connection.type.toLowerCase()) { 19 | case 'teamwork': { 20 | 21 | let teamworkResponse; 22 | try { 23 | teamworkResponse = await teamworkService.getTasks(encryptionService.decrypt(connection.token), connection.externalData.baseUrl, tasklistId); 24 | } catch(error) { 25 | return res.status(400).json({ message: error.message }); 26 | } 27 | 28 | return res.json(teamworkResponse); 29 | } 30 | 31 | default: { 32 | return res.status(400).json({ message: 'Unknown Connection Type' }); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/un-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { 3 | CanActivate, 4 | ActivatedRouteSnapshot, 5 | RouterStateSnapshot 6 | } from "@angular/router"; 7 | 8 | import { map, tap, filter, switchMap } from "rxjs/operators"; 9 | 10 | import { RouterFacade } from "../store/router.facade"; 11 | import { AuthFacade } from "../features/authentication/store/auth.facade"; 12 | import { AuthUiState } from "../features/authentication/store/auth.reducer"; 13 | import { AppFacade } from "../store/app.facade"; 14 | 15 | @Injectable() 16 | export class UnAuthGuard implements CanActivate { 17 | constructor( 18 | private appFacade: AppFacade, 19 | private routerFacade: RouterFacade, 20 | private authFacade: AuthFacade 21 | ) {} 22 | 23 | canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 24 | return this.appFacade.authRedirect$.pipe( 25 | filter(navOptions => !!navOptions), 26 | switchMap(navOptions => 27 | this.authFacade.uiState$.pipe( 28 | filter(uiState => uiState !== AuthUiState.LOADING), 29 | map(uiState => uiState === AuthUiState.NOT_AUTHENTICATED), 30 | tap(unAuth => { 31 | if (!unAuth) { 32 | this.routerFacade.navigate(navOptions); 33 | } 34 | }) 35 | ) 36 | ) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /functions/src/api/connections/projects/tasklists/get-tasklists.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { firestore, teamworkService, encryptionService } from './../../../../services'; 3 | 4 | export async function getTaskLists(req: express.Request, res: express.Response) { 5 | const uid = res.locals.user.uid; 6 | const connectionId = req.params.connectionId; 7 | const projectId = req.params.projectId; 8 | let page = 1; 9 | if (req.query.page) { 10 | page = req.query.page; 11 | } 12 | 13 | let connectionRef; 14 | try { 15 | connectionRef = await firestore.collection(`/users/${uid}/connections`).doc(connectionId).get(); 16 | } catch(error) { 17 | return res.status(400).json({ message: 'Error getting connection' }); 18 | } 19 | 20 | const connection = connectionRef.data(); 21 | 22 | switch (connection.type.toLowerCase()) { 23 | case 'teamwork': { 24 | 25 | let teamworkResponse; 26 | try { 27 | teamworkResponse = await teamworkService.getTaskLists(encryptionService.decrypt(connection.token), connection.externalData.baseUrl, projectId, page); 28 | } catch(error) { 29 | return res.status(400).json({ message: error.message }); 30 | } 31 | 32 | return res.json(teamworkResponse); 33 | } 34 | 35 | default: { 36 | return res.status(400).json({ message: 'Unknown Connection Type' }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/features/connections/components/connection-list/connection-list.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |

My Connections

7 |
8 |
9 | 10 |
11 | Team 12 | Connection 13 |
14 | 15 | 16 | 17 | 18 |
19 | {{ connection.externalData.company }} 20 | {{ connection.type | titlecase }} 21 |
22 |
23 | 26 |
27 | 28 |
29 | 30 | 31 | 32 | 33 | Add New Connection 34 | 35 | 38 | 39 |
40 | -------------------------------------------------------------------------------- /src/features/connections/components/share-scope-link-modal/share-scope-link-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { NavParams, NavController, ViewController } from "ionic-angular"; 3 | 4 | import { LoadingService } from "../../../../shared/loading.service"; 5 | 6 | @Component({ 7 | selector: "app-share-scope-link", 8 | templateUrl: "./share-scope-link-modal.component.html" 9 | }) 10 | export class ShareScopeLinkModalComponent implements OnInit { 11 | connectionName; 12 | projectName; 13 | sessionUrl; 14 | accessCode; 15 | accessCodeLetters; 16 | 17 | constructor( 18 | private params: NavParams, 19 | private viewCtrl: ViewController, 20 | private navCtrl: NavController, 21 | private loadingSrv: LoadingService 22 | ) {} 23 | 24 | ngOnInit() { 25 | this.connectionName = this.params.data.connectionName; 26 | this.projectName = this.params.data.projectName; 27 | this.sessionUrl = this.params.data.sessionUrl; 28 | this.accessCode = this.params.data.accessCode; 29 | this.accessCodeLetters = this.accessCode.split(""); 30 | } 31 | 32 | startScoping() { 33 | this.viewCtrl.dismiss(); 34 | this.loadingSrv.dismiss(); 35 | this.navCtrl.push("SessionScopingPage", { sessionUrl: this.sessionUrl }); 36 | } 37 | 38 | goDashboard() { 39 | this.viewCtrl.dismiss(); 40 | this.navCtrl.setRoot("DashboardPage"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/shared/api.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { 3 | HttpRequest, 4 | HttpHandler, 5 | HttpEvent, 6 | HttpInterceptor, 7 | HttpErrorResponse 8 | } from "@angular/common/http"; 9 | 10 | import { Observable, throwError } from "rxjs"; 11 | import { switchMap, take, catchError } from "rxjs/operators"; 12 | 13 | import { AngularFireAuth } from "angularfire2/auth"; 14 | 15 | import { environment } from "../environments/environment"; 16 | 17 | @Injectable() 18 | export class ApiInterceptor implements HttpInterceptor { 19 | constructor(private afAuth: AngularFireAuth) {} 20 | 21 | intercept( 22 | req: HttpRequest, 23 | next: HttpHandler 24 | ): Observable> { 25 | if (!req.url.includes(environment.apiBaseUrl)) { 26 | return next.handle(req); 27 | } 28 | 29 | return this.afAuth.authState.pipe( 30 | take(1), 31 | switchMap(user => user.getIdToken()), 32 | switchMap(idToken => { 33 | const headers = req.headers.set("Authorization", `Bearer ${idToken}`); 34 | const authReq = req.clone({ headers }); 35 | 36 | return next.handle(authReq).pipe( 37 | catchError(response => { 38 | if (response instanceof HttpErrorResponse) { 39 | return throwError(response.error); 40 | } 41 | 42 | return throwError(response); 43 | }) 44 | ); 45 | }) 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/features/scoping/components/select-result/select-result.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core"; 2 | import { FormGroup, FormBuilder, Validators } from "@angular/forms"; 3 | 4 | enum SELECTED_RESULT { 5 | AVG_RESULT = "avg", 6 | MAX_RESULT = "max", 7 | CUSTOM_RESULT = "custom" 8 | } 9 | 10 | @Component({ 11 | selector: "app-select-result", 12 | templateUrl: "./select-result.component.html" 13 | }) 14 | export class SelectResultComponent implements OnInit { 15 | SELECTED_RESULT = SELECTED_RESULT; 16 | selectionForm: FormGroup; 17 | 18 | @Input() avg: number; 19 | 20 | @Input() max: number; 21 | 22 | selectedResult: SELECTED_RESULT; 23 | 24 | @Output() estimate = new EventEmitter(); 25 | 26 | constructor(private fb: FormBuilder) {} 27 | 28 | ngOnInit() { 29 | this.createForm(); 30 | } 31 | 32 | createForm() { 33 | this.selectionForm = this.fb.group({ 34 | custom: ["", Validators.required] 35 | }); 36 | } 37 | 38 | selectAvg() { 39 | this.selectedResult = SELECTED_RESULT.AVG_RESULT; 40 | this.estimate.emit(this.avg); 41 | } 42 | 43 | selectMax() { 44 | this.selectedResult = SELECTED_RESULT.MAX_RESULT; 45 | this.estimate.emit(this.max); 46 | } 47 | 48 | onChangeValue(ev) { 49 | if (parseInt(ev.value) < 99) { 50 | this.selectedResult = SELECTED_RESULT.CUSTOM_RESULT; 51 | this.estimate.emit(parseInt(ev.value, 10)); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/features/scoping/pages/session-results/session-results.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | import { ScopingFacade } from "../../store/scoping.facade"; 3 | import { NavController, IonicPage } from "ionic-angular"; 4 | import { ScopingSession } from "../../../../models/scoping-session"; 5 | import { Subscription } from "rxjs"; 6 | 7 | @IonicPage({ 8 | segment: "SessionResultsPage", 9 | priority: "high" 10 | }) 11 | @Component({ 12 | selector: "app-session-results", 13 | templateUrl: "./session-results.component.html" 14 | }) 15 | export class SessionResultsPage { 16 | session: ScopingSession; 17 | sessionCode: string; 18 | sessionSub: Subscription; 19 | 20 | constructor( 21 | private scopingFacade: ScopingFacade, 22 | private navCtrl: NavController 23 | ) { 24 | this.sessionSub = this.scopingFacade.session$.subscribe(session => { 25 | this.session = session; 26 | }); 27 | } 28 | 29 | ionViewWillLeave() { 30 | if (this.sessionSub) { 31 | this.sessionSub.unsubscribe(); 32 | } 33 | this.scopingFacade.clearSession(); 34 | } 35 | 36 | goDashboard() { 37 | this.navCtrl.push("DashboardPage"); 38 | } 39 | 40 | goSettings() { 41 | this.navCtrl.push("SettingsPage"); 42 | } 43 | 44 | getTotalEstimate(tasks) { 45 | let totalEstimate = 0; 46 | Object.keys(tasks).map(function(key, index) { 47 | totalEstimate += tasks[key].estimate; 48 | }); 49 | return totalEstimate; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /functions/src/api/connections/tasks/put-estimate.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import { 3 | firestore, 4 | teamworkService, 5 | encryptionService 6 | } from "./../../../services"; 7 | 8 | export async function putEstimate(req: express.Request, res: express.Response) { 9 | const uid = res.locals.user.uid; 10 | const connectionId = req.params.connectionId; 11 | const taskId = req.params.taskId; 12 | 13 | const { hours } = req.body as { hours: number }; 14 | 15 | if (!hours) { 16 | return res.status(400).json({ message: "Estimated hours are required." }); 17 | } 18 | 19 | let connectionRef; 20 | try { 21 | connectionRef = await firestore 22 | .collection(`/users/${uid}/connections`) 23 | .doc(connectionId) 24 | .get(); 25 | } catch (error) { 26 | return res.status(400).json({ message: "Error getting connection" }); 27 | } 28 | 29 | const connection = connectionRef.data(); 30 | 31 | switch (connection.type.toLowerCase()) { 32 | case "teamwork": { 33 | let teamworkResponse; 34 | try { 35 | teamworkResponse = await teamworkService.putEstimate( 36 | encryptionService.decrypt(connection.token), 37 | connection.externalData.baseUrl, 38 | taskId, 39 | hours 40 | ); 41 | } catch (error) { 42 | return res.status(200).json({ message: error.message }); 43 | } 44 | 45 | return res.json(teamworkResponse); 46 | } 47 | 48 | default: { 49 | return res.status(400).json({ message: "Unknown Connection Type" }); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from "@angular/core"; 2 | import { IonicModule } from "ionic-angular"; 3 | import { ReactiveFormsModule, FormsModule } from "@angular/forms"; 4 | import { HTTP_INTERCEPTORS } from "@angular/common/http"; 5 | import { CommonModule } from "@angular/common"; 6 | 7 | import { ApiInterceptor } from "./api.interceptor"; 8 | import { PopupService } from "./popup.service"; 9 | import { LoadingService } from "./loading.service"; 10 | import { 11 | ObjectKeysPipe, 12 | ObjectValuesPipe, 13 | ObjectKeyValuePipe 14 | } from "./pipes/object-iterators.pipe"; 15 | import { InfoModalComponent } from "./components/info-modal/info-modal"; 16 | 17 | @NgModule({ 18 | imports: [IonicModule, FormsModule, ReactiveFormsModule, CommonModule], 19 | declarations: [ 20 | ObjectKeysPipe, 21 | ObjectValuesPipe, 22 | ObjectKeyValuePipe, 23 | InfoModalComponent 24 | ], 25 | entryComponents: [InfoModalComponent], 26 | exports: [ 27 | ObjectKeysPipe, 28 | ObjectValuesPipe, 29 | ObjectKeyValuePipe, 30 | InfoModalComponent, 31 | IonicModule, 32 | FormsModule, 33 | ReactiveFormsModule, 34 | CommonModule 35 | ] 36 | }) 37 | export class SharedModule { 38 | static forRoot(): ModuleWithProviders { 39 | return { 40 | ngModule: SharedModule, 41 | providers: [ 42 | { 43 | provide: HTTP_INTERCEPTORS, 44 | useClass: ApiInterceptor, 45 | multi: true 46 | }, 47 | PopupService, 48 | LoadingService 49 | ] 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/features/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from "@angular/core"; 2 | 3 | import { StoreModule } from "@ngrx/store"; 4 | import { EffectsModule } from "@ngrx/effects"; 5 | 6 | import { HistoryService } from "./services/history.service"; 7 | import { dashboardReducer } from "./store/dashboard.reducer"; 8 | import { DashboardFacade } from "./store/dashboard.facade"; 9 | import { SharedModule } from "../../shared/shared.module"; 10 | import { SessionDetailModalComponent } from "./components/session-detail-modal/session-detail-modal.component"; 11 | import { JoinSessionComponent } from "./components/join-session/join-session.component"; 12 | import { SessionHistoryItemComponent } from "./components/session-history-item/session-history-item.component"; 13 | import { SessionHistoryListComponent } from "./components/session-history-list/session-history-list.component"; 14 | 15 | @NgModule({ 16 | imports: [ 17 | SharedModule, 18 | StoreModule.forFeature("dashboard", dashboardReducer), 19 | EffectsModule.forFeature([DashboardFacade]) 20 | ], 21 | declarations: [ 22 | JoinSessionComponent, 23 | SessionDetailModalComponent, 24 | SessionHistoryItemComponent, 25 | SessionHistoryListComponent 26 | ], 27 | entryComponents: [SessionDetailModalComponent], 28 | exports: [JoinSessionComponent, SessionHistoryListComponent] 29 | }) 30 | export class DashboardModule { 31 | static forRoot(): ModuleWithProviders { 32 | return { 33 | ngModule: DashboardModule, 34 | providers: [DashboardFacade, HistoryService] 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8.11' 4 | branches: 5 | only: 6 | - develop 7 | script: 8 | - npm run build 9 | - npm run firebase:deploy 10 | notifications: 11 | slack: 12 | rooms: 13 | secure: lxH1zn6wXA4+WuqDRUFTePBCBIzkFAS99EgPYeaJKJ6ignwwHoGqn3ciKAbDD5VqmQU+N8I8f2aAO/Ti3zMcL9Gh0zJaVYK3bXBAmUk+/OKOypWNP/oyOh7FJFmaaLcmJawPRvnJ8iGK5VyVB4o/n8tFg4AqxTphy0eDeDLTIAFoI3v5r3zViAbKCeiw1M5JGUhaCzj79icPM1zcoNoP8CuLzGRdvEqQuIXxopta1tmggMKcjpKyGqZgsVdCNUVfxpOE481ZO6GUfw33cIgJDDyvCo17ijuUB06JTqFpEQ566VadKTz22VdzhvqQIE6A5BnyNNFMT4mMISN22pikOnqHN6UWPIW0p1loZOKRT7bZ81n2889LVRWlQxETO8ooAkvBRjv0Uk3mutMGyAeLCZK3tgyF3zMNWt5pXRQjCNcz10emzLT1NYA+jVnrEPGM6jLgUu33UicEfXD78/PpIh3M70JaP+qFjhB3VSSD+jvdquaXqlWVBnzqzvZDxEbXnbDqYqFQOkPo9tHUw/xzIdYVa30daxVmF+pzqHGCyolILByjrj+r/oaPUQLuI/QQxaUnYLNh5y3EIaXYucUXPm4x/gAH8cl9tzqF5kBCwXYO/2m2z93afLI26RkDnQqEYfyZRBCUKc7zC86ZjRwVAKjMN5WWf8GByoyHpTCL9bg= 14 | env: 15 | global: 16 | - secure: AYkLA5NELZRYFHJiEfxtumdAFme2/a+LlyBWG+92WFBl9+J24L3sTBRTI7P68959iUhdkfMGfO1U48ldvFRd8JD2qMqBV4xnQ95Oai1Dr+uJlNICuTFIqO87dlv7GeO/vyEtg8BeCLEpjn992mqDwDa1k3jfK+76IHLV2FX7uQb7oVnjVzg7bJpKXM6u9cEeeLcdXdItwcl4OgcEYBgSXvpjlc1HgwUhTq1M3MymH3JaOodp5S2EFuw5H6LHzPA6Hnd0uAUQgpPEOBJbbKrvi6O8KCPl5l8Hlqb2EuPLGlOgqselyW3bHzE/waPy/sBXarm3K8yUshwMr2hntcrgxrJFiJDT+MjSg3/cHymtyZCeX2jpQvOIfClo8kfUafqVr+5J8UVBx0mfh7hGwG47yh5qLorZ9BO/Qn3HIfVgxIHNstuQRXhqcdndLuU9mzSUsKR3K8PcTd06w/6BoZ2BubY8hEvhYsQjes5jogAvq10eDEt4lAwngADB+WFBSIiOQKPkR8pLPZxqS/XhP+H7w7UIOFWsxHL4wbKHSJxBQYIoxSZOW48GqdOs+acPbPtjXqo1jZSSWEN5vlUH+XWNmQ57eiFowm8jAK10B6h6zAqDidSS1vn37dkZ6Es3C9JxDd8VsAjB3TXxkspLQX6b3nZIt0LRXn/dU264c+U/dBQ= 17 | -------------------------------------------------------------------------------- /functions/src/api/estimate/set-estimate.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | 3 | import { sessionService } from "../../services"; 4 | 5 | export async function setEstimate(req: express.Request, res: express.Response) { 6 | const uid = res.locals.user.uid; 7 | const ownerId = req.params.ownerId; 8 | const connectionId = req.params.connectionId; 9 | const sessionId = req.params.sessionId; 10 | const taskId = req.params.taskId; 11 | const estimate = req.body.estimate; 12 | 13 | if (!uid) { 14 | return res.status(401).json({ message: "Unauthorized." }); 15 | } 16 | if (!ownerId) { 17 | return res.status(400).json({ message: "Owner ID is required." }); 18 | } 19 | // The user must be the moderator of this session 20 | if (ownerId !== uid) { 21 | return res.status(401).json({ message: "Permission denied." }); 22 | } 23 | if (!connectionId) { 24 | return res.status(400).json({ message: "Connection ID is required." }); 25 | } 26 | if (!sessionId) { 27 | return res.status(400).json({ message: "Session ID is required." }); 28 | } 29 | if (!taskId) { 30 | return res.status(400).json({ message: "Task ID is required." }); 31 | } 32 | if (!estimate) { 33 | return res.status(400).json({ message: "Estimate is required." }); 34 | } 35 | 36 | try { 37 | await sessionService.setTaskEstimate( 38 | uid, 39 | ownerId, 40 | connectionId, 41 | sessionId, 42 | taskId, 43 | estimate 44 | ); 45 | } catch (error) { 46 | return res.status(400).json({ message: error }); 47 | } 48 | 49 | return res.sendStatus(204); 50 | } 51 | -------------------------------------------------------------------------------- /src/features/authentication/store/auth.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "@ngrx/store"; 2 | 3 | import { User } from "../../../models/user"; 4 | 5 | export enum AuthActionTypes { 6 | GET_USER = "[Auth] Get User", 7 | AUTHENTICATED = "[Auth] User Authenticated", 8 | NOT_AUTHENTICATED = "[Auth] User not Authenticated", 9 | LOGIN = "[Auth] Login Attempt", 10 | AUTH_ERROR = "[Auth] Auth Error", 11 | LOGOUT = "[Auth] Logout", 12 | CLEAR_ERROR = "[Auth] Clear Error" 13 | } 14 | 15 | export class GetUserAction implements Action { 16 | readonly type = AuthActionTypes.GET_USER; 17 | } 18 | 19 | export class AuthenticatedAction implements Action { 20 | readonly type = AuthActionTypes.AUTHENTICATED; 21 | constructor(public payload: User) {} 22 | } 23 | 24 | export class NotAuthenticatedAction implements Action { 25 | readonly type = AuthActionTypes.NOT_AUTHENTICATED; 26 | } 27 | 28 | export class LoginAction implements Action { 29 | readonly type = AuthActionTypes.LOGIN; 30 | constructor(public payload: { provider: string }) {} 31 | } 32 | 33 | export class AuthErrorAction implements Action { 34 | readonly type = AuthActionTypes.AUTH_ERROR; 35 | constructor(public payload: { message: string }) {} 36 | } 37 | 38 | export class LogoutAction implements Action { 39 | readonly type = AuthActionTypes.LOGOUT; 40 | } 41 | 42 | export class ClearErrorAction implements Action { 43 | readonly type = AuthActionTypes.CLEAR_ERROR; 44 | } 45 | 46 | export type AuthActions = 47 | | GetUserAction 48 | | AuthenticatedAction 49 | | NotAuthenticatedAction 50 | | LoginAction 51 | | AuthErrorAction 52 | | LogoutAction 53 | | ClearErrorAction; 54 | -------------------------------------------------------------------------------- /src/features/dashboard/components/session-detail-modal/session-detail-modal.component.scss: -------------------------------------------------------------------------------- 1 | app-session-detail-modal { 2 | background: rgba(0, 0, 0, 0.5); 3 | width: 100%; 4 | height: 50%; 5 | 6 | ion-content.content { 7 | border-radius: 2rem; 8 | height: calc(100% - 28rem); 9 | width: calc(100% - 7rem); 10 | margin: 3.5rem; 11 | display: flex; 12 | justify-content: center; 13 | } 14 | 15 | .share-box { 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | padding: 30px; 20 | h4 { 21 | text-transform: uppercase; 22 | } 23 | .share-url-section { 24 | display: flex; 25 | margin-bottom: 20px; 26 | .session-url { 27 | border-bottom: 1px solid; 28 | margin-top: 0; 29 | margin-bottom: 0; 30 | margin-right: 15px; 31 | } 32 | } 33 | .access-code-letters { 34 | display: flex; 35 | justify-content: center; 36 | 37 | .single-letter { 38 | color: black; 39 | border: 1px solid $primary-orange; 40 | padding: 5px; 41 | border-radius: 5px; 42 | margin-right: 10px; 43 | margin-left: 10px; 44 | } 45 | } 46 | button.button { 47 | width: 50%; 48 | text-transform: capitalize; 49 | } 50 | } 51 | 52 | .delete-box { 53 | border-top: 2px solid $primary-orange; 54 | padding-top: 20px; 55 | display: flex; 56 | flex-direction: row; 57 | justify-content: center; 58 | button.button { 59 | margin-left: 10px; 60 | width: 35%; 61 | margin-right: 10px; 62 | text-transform: capitalize; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/features/scoping/pages/session-results/session-results.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | Session Results 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |

RESULTS

24 |

{{ session.projectName }}

25 |

{{ session.numScopedTasks }} Tasks Scoped

26 |
27 | 28 | 29 | 30 |
31 | Hours 32 | Task 33 |
34 | 35 | 36 | 37 | 38 |
39 | 40 | {{ (task.estimate == -1) ? "N/A" : task.estimate }} 41 | 42 | 43 | {{ task.name }} 44 | 45 |
46 |
47 |
48 |
49 | 50 |
51 |
52 | -------------------------------------------------------------------------------- /functions/src/api/connections/add-connection.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import { firestore, teamworkService } from "./../../services"; 3 | 4 | export async function addConnection( 5 | req: express.Request, 6 | res: express.Response 7 | ) { 8 | const { type, token } = req.body as { type: string; token: string }; 9 | const uid = res.locals.user.uid; 10 | 11 | if (!type) { 12 | return res.status(400).json({ message: "Connection Type is required." }); 13 | } 14 | 15 | if (!token) { 16 | return res.status(400).json({ message: "Connection Token is required." }); 17 | } 18 | 19 | switch (type.toLowerCase()) { 20 | case "teamwork": { 21 | let teamworkResponse; 22 | try { 23 | teamworkResponse = await teamworkService.validateToken(token); 24 | } catch (error) { 25 | return res.status(400).json({ message: error.message }); 26 | } 27 | 28 | const snapshot = await firestore 29 | .collection(`/users/${uid}/connections`) 30 | .where("type", "==", type.toLowerCase()) 31 | .where("externalData.id", "==", teamworkResponse.id) 32 | .get(); 33 | 34 | if (snapshot.size > 0) { 35 | return res.status(422).json({ message: "Connection already exists!" }); 36 | } 37 | 38 | await firestore.collection(`/users/${uid}/connections`).add({ 39 | type: "teamwork", 40 | token: teamworkResponse.accessToken, 41 | externalData: teamworkResponse 42 | }); 43 | 44 | return res.status(201).send({ 45 | type: "teamwork", 46 | externalData: teamworkResponse 47 | }); 48 | } 49 | 50 | default: { 51 | return res.status(400).json({ message: "Unknown Connection Type" }); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/features/dashboard/components/session-history-item/session-history-item.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ChangeDetectionStrategy, 4 | Input, 5 | Output, 6 | EventEmitter, 7 | OnInit 8 | } from "@angular/core"; 9 | 10 | import { HistoryItem } from "../../../../models/history-item"; 11 | import { SessionUserType } from "../../../../models/user"; 12 | import { SessionStatus } from "../../../../models/scoping-session"; 13 | 14 | @Component({ 15 | selector: "app-session-history-item", 16 | templateUrl: "./session-history-item.component.html", 17 | changeDetection: ChangeDetectionStrategy.OnPush 18 | }) 19 | export class SessionHistoryItemComponent implements OnInit { 20 | @Input() item: HistoryItem; 21 | @Input() uid: string; 22 | @Input() time: number; 23 | 24 | @Output() options = new EventEmitter(); 25 | @Output() detail = new EventEmitter(); 26 | 27 | abbreviation; 28 | 29 | ngOnInit() { 30 | this.abbreviation = this.item.ownerId === this.uid ? "M" : "P"; 31 | } 32 | 33 | get sessionStatus(): string { 34 | return this.isComplete() ? "Complete" : "Incomplete"; 35 | } 36 | 37 | get ctaText(): string { 38 | return this.isComplete() ? "View Results" : "Continue Scoping"; 39 | } 40 | 41 | handleOptionsClick() { 42 | const userType = 43 | this.item.ownerId === this.uid 44 | ? SessionUserType.MODERATOR 45 | : SessionUserType.PARTICIPANT; 46 | 47 | this.options.emit(userType); 48 | } 49 | 50 | handleDetailClick() { 51 | this.detail.emit( 52 | this.isComplete() ? SessionStatus.COMPLETE : SessionStatus.INCOMPLETE 53 | ); 54 | } 55 | 56 | private isComplete() { 57 | return this.item.numScopedTasks === this.item.numTasks; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JuntoScope 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/store/router.facade.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { Location } from "@angular/common"; 3 | import { ViewChild } from "@angular/core"; 4 | import { Nav } from "ionic-angular"; 5 | import { Store } from "@ngrx/store"; 6 | import { Effect, Actions, ofType } from "@ngrx/effects"; 7 | import { map, tap } from "rxjs/operators"; 8 | import { AppState } from "./app.reducer"; 9 | import { 10 | GoAction, 11 | BackAction, 12 | ForwardAction, 13 | RouterActionTypes, 14 | NavigationOptions 15 | } from "./router.actions"; 16 | 17 | @Injectable() 18 | export class RouterFacade { 19 | /* 20 | * Observable Store Queries 21 | */ 22 | 23 | @ViewChild(Nav) nav: Nav; 24 | 25 | /* 26 | * Module-level Effects 27 | */ 28 | 29 | @Effect({ dispatch: false }) 30 | go$ = this.actions$.pipe( 31 | ofType(RouterActionTypes.GO), 32 | map(action => action.payload), 33 | tap(({ path, query: queryParams, extras }) => { 34 | this.nav.push(path[0]); 35 | }) 36 | ); 37 | 38 | @Effect({ dispatch: false }) 39 | back$ = this.actions$.pipe( 40 | ofType(RouterActionTypes.BACK), 41 | tap(() => this.location.back()) 42 | ); 43 | 44 | @Effect({ dispatch: false }) 45 | forward$ = this.actions$.pipe( 46 | ofType(RouterActionTypes.FORWARD), 47 | tap(() => this.location.forward()) 48 | ); 49 | 50 | constructor( 51 | private store: Store, 52 | private actions$: Actions, 53 | private location: Location 54 | ) {} 55 | 56 | /* 57 | * Action Creators 58 | */ 59 | 60 | navigate(options: NavigationOptions) { 61 | this.store.dispatch(new GoAction(options)); 62 | } 63 | 64 | back() { 65 | this.store.dispatch(new BackAction()); 66 | } 67 | 68 | forward() { 69 | this.store.dispatch(new ForwardAction()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/features/connections/pages/select-project/select-project.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { map, filter, find } from "rxjs/operators"; 3 | import { ConnectionFacade } from "../../store/connection.facade"; 4 | import { Project } from "../../../../models/project"; 5 | import { Connection } from "../../../../models/connection"; 6 | import { NavController, NavParams, IonicPage } from "ionic-angular"; 7 | 8 | @IonicPage({ 9 | segment: "SelectProjectPage", 10 | priority: "high" 11 | }) 12 | @Component({ 13 | selector: "app-select-project", 14 | templateUrl: "./select-project.component.html" 15 | }) 16 | export class SelectProjectPage implements OnInit { 17 | connectionId: string; 18 | loaded: boolean = false; 19 | projects$ = this.connectionFacade.selectedConnection$.pipe( 20 | filter((connection: Connection) => !!connection && !!connection.projects), 21 | map((connection: Connection) => { 22 | this.loaded = true; 23 | 24 | return Object.keys(connection.projects).map( 25 | keys => connection.projects[keys] 26 | ); 27 | }) 28 | ); 29 | 30 | connection$ = this.connectionFacade.selectConnection$.pipe( 31 | find( 32 | (connection: any) => connection.id == this.navParams.get("connectionId") 33 | ) 34 | ); 35 | 36 | constructor( 37 | private connectionFacade: ConnectionFacade, 38 | private navCtrl: NavController, 39 | private navParams: NavParams 40 | ) {} 41 | 42 | ngOnInit(): void { 43 | this.connectionId = this.navParams.get("connectionId"); 44 | this.connectionFacade.selectConnection(this.connectionId); 45 | } 46 | 47 | handleProjectSelect(project: Project) { 48 | this.navCtrl.push("SelectTaskListPage", { 49 | connectionId: this.connectionId, 50 | projectId: project.id, 51 | projectName: project.name 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/features/scoping/components/session-access/session-access.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | Output, 5 | Input, 6 | EventEmitter, 7 | OnDestroy 8 | } from "@angular/core"; 9 | import { FormGroup, FormBuilder, Validators } from "@angular/forms"; 10 | import { ScopingFacade } from "../../store/scoping.facade"; 11 | import { SessionValidation } from "../../../../models/scoping-session"; 12 | import { PopupService } from "../../../../shared/popup.service"; 13 | import { Subscription } from "rxjs"; 14 | 15 | @Component({ 16 | selector: "app-session-access", 17 | templateUrl: "./session-access.component.html" 18 | }) 19 | export class SessionAccessComponent implements OnInit, OnDestroy { 20 | accessForm: FormGroup; 21 | error$ = this.scopingFacade.error$; 22 | errorSubscription: Subscription; 23 | 24 | @Input() sessionLink: string; 25 | @Output() access = new EventEmitter(); 26 | 27 | constructor( 28 | private fb: FormBuilder, 29 | private scopingFacade: ScopingFacade, 30 | private popupSvc: PopupService 31 | ) {} 32 | 33 | ngOnInit() { 34 | this.createForm(); 35 | this.errorSubscription = this.error$.subscribe(error => { 36 | if (error) { 37 | this.popupSvc.simpleAlert("Uh Oh!", error, "OK"); 38 | this.scopingFacade.clearError(); 39 | } 40 | }); 41 | } 42 | 43 | ngOnDestroy() { 44 | this.errorSubscription.unsubscribe(); 45 | } 46 | 47 | continue() { 48 | if (this.accessForm.valid) { 49 | const sessionValidation = { 50 | sessionLink: this.sessionLink, 51 | accessCode: this.accessForm.get("code").value 52 | }; 53 | this.access.emit(sessionValidation); 54 | } else { 55 | this.accessForm.get("code").markAsDirty(); 56 | } 57 | } 58 | 59 | createForm() { 60 | this.accessForm = this.fb.group({ 61 | code: ["", Validators.required] 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/features/connections/connections.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from "@angular/core"; 2 | import { StoreModule } from "@ngrx/store"; 3 | import { EffectsModule } from "@ngrx/effects"; 4 | import { SharedModule } from "../../shared/shared.module"; 5 | import { connectionReducer } from "./store/connection.reducer"; 6 | import { ConnectionFacade } from "./store/connection.facade"; 7 | import { ConnectionService } from "./services/connection.service"; 8 | import { VerifyModalComponent } from "./components/verify-modal/verify-modal.component"; 9 | import { ProjectListComponent } from "./components/project-list/project-list.component"; 10 | import { TaskListComponent } from "./components/task-list/task-list.component"; 11 | import { ProjectItemComponent } from "./components/project-item/project-item.component"; 12 | import { InstructionsComponent } from "./components/instructions/instructions.component"; 13 | import { ConnectionListComponent } from "./components/connection-list/connection-list.component"; 14 | import { ShareScopeLinkModalComponent } from "./components/share-scope-link-modal/share-scope-link-modal.component"; 15 | 16 | @NgModule({ 17 | imports: [ 18 | SharedModule, 19 | StoreModule.forFeature("connection", connectionReducer), 20 | EffectsModule.forFeature([ConnectionFacade]) 21 | ], 22 | declarations: [ 23 | ConnectionListComponent, 24 | InstructionsComponent, 25 | ProjectItemComponent, 26 | ProjectListComponent, 27 | TaskListComponent, 28 | VerifyModalComponent, 29 | ShareScopeLinkModalComponent 30 | ], 31 | entryComponents: [VerifyModalComponent, ShareScopeLinkModalComponent], 32 | exports: [TaskListComponent, ProjectListComponent, ConnectionListComponent], 33 | providers: [] 34 | }) 35 | export class ConnectionsModule { 36 | static forRoot(): ModuleWithProviders { 37 | return { 38 | ngModule: ConnectionsModule, 39 | providers: [ConnectionFacade, ConnectionService] 40 | }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/features/scoping/scoping.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from "@angular/core"; 2 | 3 | import { EffectsModule } from "@ngrx/effects"; 4 | import { SharedModule } from "../../shared/shared.module"; 5 | import { SessionAccessComponent } from "./components/session-access/session-access.component"; 6 | import { CountedVotesComponent } from "./components/counted-votes/counted-votes.component"; 7 | import { TaskCardComponent } from "./components/task-card/task-card.component"; 8 | import { SessionHeaderComponent } from "./components/session-header/session-header.component"; 9 | import { VoteComponent } from "./components/vote/vote.component"; 10 | import { ResultEstimateComponent } from "./components/result-estimate/result-estimate.component"; 11 | import { SelectResultComponent } from "./components/select-result/select-result.component"; 12 | import { ScopingFacade } from "./store/scoping.facade"; 13 | import { StoreModule } from "@ngrx/store"; 14 | import { scopingReducer } from "./store/scoping.reducer"; 15 | import { ScopingService } from "./services/scoping.service"; 16 | 17 | @NgModule({ 18 | imports: [ 19 | SharedModule, 20 | StoreModule.forFeature("scoping", scopingReducer), 21 | EffectsModule.forFeature([ScopingFacade]) 22 | ], 23 | declarations: [ 24 | CountedVotesComponent, 25 | ResultEstimateComponent, 26 | SelectResultComponent, 27 | SessionAccessComponent, 28 | SessionHeaderComponent, 29 | TaskCardComponent, 30 | VoteComponent 31 | ], 32 | entryComponents: [], 33 | exports: [ 34 | ResultEstimateComponent, 35 | SessionHeaderComponent, 36 | SessionAccessComponent, 37 | TaskCardComponent, 38 | VoteComponent, 39 | SelectResultComponent, 40 | CountedVotesComponent 41 | ] 42 | }) 43 | export class ScopingModule { 44 | static forRoot(): ModuleWithProviders { 45 | return { 46 | ngModule: ScopingModule, 47 | providers: [ScopingFacade, ScopingService] 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/features/connections/components/connection-list/connection-list.component.scss: -------------------------------------------------------------------------------- 1 | app-connection-list { 2 | ion-list.list { 3 | background-color: $primary-orange; 4 | border-bottom: 1px solid white !important; 5 | margin: 0; 6 | width: 100%; 7 | 8 | ion-list-header.list-header { 9 | background-color: $primary-orange; 10 | padding: 1rem 1rem 0rem 1rem; 11 | 12 | .icon, 13 | h2 { 14 | color: white; 15 | display: inline-block; 16 | font-size: 24px; 17 | font-weight: 600; 18 | text-transform: none; 19 | vertical-align: middle; 20 | } 21 | 22 | .icon { 23 | font-size: 36px; 24 | } 25 | 26 | ion-label.label { 27 | padding: 1rem; 28 | margin: 1rem; 29 | } 30 | } 31 | 32 | // existing projects 33 | ion-item.item-block { 34 | background-color: $primary-orange; 35 | border-bottom: 1px solid $primary-orange; 36 | color: white; 37 | } 38 | } 39 | 40 | ion-item.item.item-block.new-connection { 41 | margin-top: 20px; 42 | border: 2px solid #c94313; 43 | } 44 | 45 | .connections-header { 46 | display: grid; 47 | color: white; 48 | grid-template-columns: 0.75fr 1fr; 49 | margin: 10px; 50 | background: rgba(241, 101, 45, 0.65); 51 | border-radius: 5px; 52 | 53 | ion-label.label { 54 | margin: 0; 55 | padding: 4px; 56 | } 57 | } 58 | 59 | .connection-item { 60 | display: grid; 61 | color: white; 62 | grid-template-columns: 0.75fr 0.75fr; 63 | 64 | ion-label.label { 65 | margin: 0; 66 | } 67 | } 68 | 69 | // custom buttons for android and windows 70 | .button-md, 71 | .button-wp { 72 | border-radius: 11px; 73 | &.existing-button span { 74 | padding-left: 8px; 75 | padding-right: 6px; 76 | } 77 | span { 78 | padding-left: 3px; 79 | padding-right: 2px; 80 | } 81 | } 82 | 83 | // custom buttons for ios 84 | .button-ios { 85 | border: none; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/features/connections/components/share-scope-link-modal/share-scope-link-modal.component.scss: -------------------------------------------------------------------------------- 1 | app-share-scope-link { 2 | ion-header .toolbar { 3 | // Background color 4 | .toolbar-background { 5 | background: $primary-orange; 6 | border: none; 7 | } // Title 8 | .toolbar-title { 9 | // overflow: visible; 10 | color: white; 11 | font-size: 16px; 12 | text-transform: capitalize; 13 | } // Buttons 14 | .bar-button-default { 15 | color: white; 16 | } 17 | ion-buttons.bar-buttons { 18 | order: 1; 19 | } 20 | } 21 | ion-content.container { 22 | .scroll-content { 23 | display: flex; 24 | flex-direction: column; 25 | align-items: center; 26 | .share-box { 27 | display: flex; 28 | flex-direction: column; 29 | align-items: center; 30 | border: 1px solid; 31 | border-radius: 5px; 32 | padding: 30px; 33 | -webkit-box-shadow: inset 4px 4px 13px -8px rgba(0, 0, 0, 0.75); 34 | -moz-box-shadow: inset 4px 4px 13px -8px rgba(0, 0, 0, 0.75); 35 | box-shadow: inset 4px 4px 13px -8px rgba(0, 0, 0, 0.75); 36 | h4 { 37 | text-transform: uppercase; 38 | } 39 | .share-url-section { 40 | display: flex; 41 | margin-bottom: 20px; 42 | .session-url { 43 | border-bottom: 1px solid; 44 | margin-top: 0; 45 | margin-bottom: 0; 46 | margin-right: 15px; 47 | } 48 | } 49 | .access-code-letters { 50 | display: flex; 51 | justify-content: center; 52 | .single-letter { 53 | color: black; 54 | border: 1px solid $primary-orange; 55 | padding: 5px; 56 | border-radius: 5px; 57 | margin-right: 10px; 58 | margin-left: 10px; 59 | } 60 | } 61 | } 62 | button.button { 63 | width: 50%; 64 | margin-top: 20px; 65 | text-transform: capitalize; 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/features/settings/pages/settings/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Settings 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 |

My Connections

15 |
16 |
17 | 18 |
19 | Team 20 | Connection 21 |
22 | 23 | 24 | 25 | 26 |
27 | {{ connection.externalData.company }} 28 | {{ connection.type | titlecase }} 29 |
30 |
31 | 34 |
35 |
36 | 37 | 38 | 39 | 40 | Add New Connection 41 | 42 | 45 | 46 |
47 | 48 |
49 | 50 |

FAQ

51 |
52 | 53 | 54 | 55 |

{{ faq.question}}

56 |

{{ faq.answer}}

57 |
58 |
59 |
60 | 61 |
62 | 63 |
64 | 65 |
66 | -------------------------------------------------------------------------------- /src/store/app.reducer.ts: -------------------------------------------------------------------------------- 1 | import { MetaReducer, ActionReducerMap, createSelector } from "@ngrx/store"; 2 | import { storeFreeze } from "ngrx-store-freeze"; 3 | import { environment } from "../environments/environment"; 4 | import { NavigationOptions } from "./router.actions"; 5 | import { 6 | AuthState, 7 | AuthQuery, 8 | AuthUiState 9 | } from "../features/authentication/store/auth.reducer"; 10 | import { 11 | ConnectionState, 12 | connectionReducer, 13 | initialConnectionState 14 | } from "../features/connections/store/connection.reducer"; 15 | import { 16 | DashboardState, 17 | dashboardReducer, 18 | initialDashboardState 19 | } from "../features/dashboard/store/dashboard.reducer"; 20 | 21 | interface FullAppState { 22 | auth: AuthState; 23 | connection: ConnectionState; 24 | dashboard: DashboardState; 25 | } 26 | 27 | export type AppState = Partial; 28 | 29 | export const reducers: ActionReducerMap = { 30 | connection: connectionReducer, 31 | dashboard: dashboardReducer 32 | }; 33 | 34 | export const metaReducers: MetaReducer[] = environment.production 35 | ? [] 36 | : [storeFreeze]; 37 | 38 | export const initialState: AppState = { 39 | connection: initialConnectionState, 40 | dashboard: initialDashboardState 41 | }; 42 | 43 | export namespace AppQuery { 44 | export const selectUid = createSelector( 45 | AuthQuery.selectUser, 46 | user => (user ? user.uid : null) 47 | ); 48 | 49 | export const selectAuthRedirect = createSelector( 50 | AuthQuery.selectUiState, 51 | (authUiState): NavigationOptions => { 52 | if (authUiState === AuthUiState.NOT_AUTHENTICATED) { 53 | return { path: ["LoginPage"] }; 54 | } 55 | 56 | return { path: ["DashboardPage"] }; 57 | } 58 | ); 59 | 60 | export const selectUidDocPath = createSelector( 61 | AuthQuery.selectUser, 62 | user => user && `users/${user.uid}` 63 | ); 64 | export const selectConnectionsClnPath = createSelector( 65 | selectUidDocPath, 66 | uidPath => uidPath && `${uidPath}/connections` 67 | ); 68 | export const selectPublicSessionsClnPath = "public/data/sessions"; 69 | } 70 | -------------------------------------------------------------------------------- /src/features/authentication/store/auth.actions.spec.ts: -------------------------------------------------------------------------------- 1 | import * as AuthActions from "./auth.actions"; 2 | import { User } from "../../../models/user"; 3 | 4 | describe("Authentication Actions", () => { 5 | describe("GetUser Action", () => { 6 | it("should create action", () => { 7 | const action = new AuthActions.GetUserAction(); 8 | 9 | expect(action).toEqual({ type: AuthActions.AuthActionTypes.GET_USER }); 10 | }); 11 | }); 12 | 13 | describe("AuthenticatedAction Action", () => { 14 | it("should create action", () => { 15 | const payload: User = { uid: "testuid", displayName: "testUser" }; 16 | const action = new AuthActions.AuthenticatedAction(payload); 17 | 18 | expect(action).toEqual({ 19 | type: AuthActions.AuthActionTypes.AUTHENTICATED, 20 | payload 21 | }); 22 | }); 23 | }); 24 | 25 | describe("NotAuthenticatedAction Action", () => { 26 | it("should create action", () => { 27 | const action = new AuthActions.NotAuthenticatedAction(); 28 | 29 | expect(action).toEqual({ 30 | type: AuthActions.AuthActionTypes.NOT_AUTHENTICATED 31 | }); 32 | }); 33 | }); 34 | 35 | describe("LoginAction Action", () => { 36 | it("should create action", () => { 37 | const payload = { provider: "testProvider" }; 38 | const action = new AuthActions.LoginAction(payload); 39 | 40 | expect(action).toEqual({ 41 | type: AuthActions.AuthActionTypes.LOGIN, 42 | payload 43 | }); 44 | }); 45 | }); 46 | 47 | describe("AuthErrorAction Action", () => { 48 | it("should create action", () => { 49 | const payload = { message: "test error message" }; 50 | const action = new AuthActions.AuthErrorAction(payload); 51 | 52 | expect(action).toEqual({ 53 | type: AuthActions.AuthActionTypes.AUTH_ERROR, 54 | payload 55 | }); 56 | }); 57 | }); 58 | 59 | describe("LogoutAction Action", () => { 60 | it("should create action", () => { 61 | const action = new AuthActions.LogoutAction(); 62 | 63 | expect(action).toEqual({ type: AuthActions.AuthActionTypes.LOGOUT }); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/features/connections/pages/connection-details/connection-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from "@angular/core"; 2 | import { NavParams, IonicPage, NavController } from "ionic-angular"; 3 | 4 | import { Subscription } from "rxjs"; 5 | import { TakeUntilDestroy } from "ngx-take-until-destroy"; 6 | 7 | import { Connection } from "../../../../models/connection"; 8 | import { ConnectionFacade } from "../../store/connection.facade"; 9 | import { PopupService } from "../../../../shared/popup.service"; 10 | 11 | @TakeUntilDestroy() 12 | @IonicPage({ 13 | segment: "ConnectionDetailsPage", 14 | priority: "high" 15 | }) 16 | @Component({ 17 | selector: "app-connection-details", 18 | templateUrl: "./connection-details.component.html" 19 | }) 20 | export class ConnectionDetailsPage implements OnInit, OnDestroy { 21 | connectionId = this.navParams.get("connectionId"); 22 | connection: Connection; 23 | connectionSub: Subscription; 24 | 25 | constructor( 26 | private connectionFacade: ConnectionFacade, 27 | private navParams: NavParams, 28 | private navCtrl: NavController, 29 | private popupSvc: PopupService 30 | ) {} 31 | 32 | ngOnInit() { 33 | this.connectionFacade.getConnections(); 34 | 35 | this.connectionSub = this.connectionFacade.connections$.subscribe( 36 | (connections: Connection[]) => { 37 | if (connections) { 38 | this.connection = connections.filter( 39 | c => c.id === this.connectionId 40 | )[0]; 41 | } 42 | } 43 | ); 44 | } 45 | 46 | ngOnDestroy() { 47 | this.connectionSub.unsubscribe(); 48 | } 49 | 50 | deleteConnection(connectionId) { 51 | this.popupSvc.customButtonsAlert( 52 | "Delete Connection", 53 | "Are you sure you would like to delete this connection? Please confirm below.", 54 | [ 55 | { 56 | text: "Cancel", 57 | role: "cancel" 58 | }, 59 | { 60 | text: "Confirm", 61 | handler: () => { 62 | this.connectionFacade.removeConnection(connectionId); 63 | this.navCtrl.pop(); 64 | } 65 | } 66 | ] 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/store/router.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { CustomSerializer, RouterQuery } from "./router.reducer"; 2 | import { AppState } from "./app.reducer"; 3 | 4 | const testParams = { one: "1", two: "2" }; 5 | 6 | describe("CustomSerializer", () => { 7 | it("should serialize router snapshot", () => { 8 | const serializer = new CustomSerializer(); 9 | const testSnapshot = { 10 | url: "/test-url", 11 | root: { 12 | queryParams: testParams, 13 | firstChild: { 14 | firstChild: { 15 | firstChild: { params: testParams } 16 | } 17 | } 18 | } 19 | }; 20 | const expected = { 21 | url: "/test-url", 22 | params: testParams, 23 | queryParams: testParams 24 | }; 25 | 26 | const serialized = serializer.serialize(testSnapshot); 27 | 28 | expect(serialized).toEqual(expected); 29 | }); 30 | }); 31 | 32 | describe("Router Queries", () => { 33 | const testState: AppState = { 34 | router: { 35 | state: { url: "/login", params: testParams, queryParams: testParams }, 36 | navigationId: 1 37 | } 38 | }; 39 | 40 | describe("getState", () => { 41 | it("should select router state from slice", () => { 42 | const expected = testState.router.state; 43 | 44 | const actual = RouterQuery.getState(testState); 45 | 46 | expect(actual).toEqual(expected); 47 | }); 48 | }); 49 | 50 | describe("getUrl", () => { 51 | it("should select route url", () => { 52 | const expected = testState.router.state.url; 53 | 54 | const actual = RouterQuery.getUrl(testState); 55 | 56 | expect(actual).toEqual(expected); 57 | }); 58 | }); 59 | 60 | describe("getParams", () => { 61 | it("should select route params", () => { 62 | const expected = testState.router.state.params; 63 | 64 | const actual = RouterQuery.getParams(testState); 65 | 66 | expect(actual).toEqual(expected); 67 | }); 68 | }); 69 | 70 | describe("getQueryParams", () => { 71 | it("should select route queryParams", () => { 72 | const expected = testState.router.state.queryParams; 73 | 74 | const actual = RouterQuery.getQueryParams(testState); 75 | 76 | expect(actual).toEqual(expected); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/features/dashboard/components/session-detail-modal/session-detail-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { NavParams, ViewController } from "ionic-angular"; 3 | import { PopupService } from "../../../../shared/popup.service"; 4 | import { InfoModalComponent } from "../../../../shared/components/info-modal/info-modal"; 5 | import { Store } from "@ngrx/store"; 6 | import { AppState } from "../../../../store/app.reducer"; 7 | import { 8 | DeleteSessionAction, 9 | RefreshAccessCodeAction 10 | } from "../../store/dashboard.actions"; 11 | import * as moment from "moment"; 12 | 13 | @Component({ 14 | selector: "app-session-detail-modal", 15 | templateUrl: "./session-detail-modal.component.html" 16 | }) 17 | export class SessionDetailModalComponent implements OnInit { 18 | accountData; 19 | isModerator: boolean; 20 | isExpired: boolean; 21 | expirationDate; 22 | accessCodeLetters; 23 | 24 | constructor( 25 | private popupSvc: PopupService, 26 | private viewCtrl: ViewController, 27 | private params: NavParams, 28 | private store: Store 29 | ) {} 30 | 31 | ngOnInit() { 32 | this.accountData = this.params.data.accountData; 33 | this.isModerator = 34 | this.params.data.accountData.userType === "Session Moderator"; 35 | const now = moment(); 36 | this.isExpired = now.isAfter(this.accountData.item.expirationDate); 37 | this.expirationDate = now.to(this.accountData.item.expirationDate); 38 | this.accessCodeLetters = this.accountData.item.accessCode.split(""); 39 | } 40 | 41 | closeModal() { 42 | this.viewCtrl.dismiss(); 43 | } 44 | 45 | refreshCode() { 46 | this.viewCtrl.dismiss(); 47 | this.store.dispatch( 48 | new RefreshAccessCodeAction(this.accountData.item.sessionCode) 49 | ); 50 | } 51 | 52 | deleteSession() { 53 | this.viewCtrl.dismiss(); 54 | this.popupSvc.openModal({ 55 | component: InfoModalComponent, 56 | componentProps: { 57 | title: "Are you sure?", 58 | text: "If you delete the session there is no come back.", 59 | label: "Delete", 60 | callback: () => 61 | this.store.dispatch( 62 | new DeleteSessionAction(this.accountData.item.sessionCode) 63 | ) 64 | } 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/features/authentication/store/auth.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, createFeatureSelector } from "@ngrx/store"; 2 | 3 | import { AuthActions, AuthActionTypes } from "./auth.actions"; 4 | import { User } from "../../../models/user"; 5 | 6 | export enum AuthUiState { 7 | UNKNOWN = "Unknown", 8 | LOADING = "Loading", 9 | AUTHENTICATED = "Authenticated", 10 | NOT_AUTHENTICATED = "Not Authenticated" 11 | } 12 | 13 | export interface AuthState { 14 | user: User; 15 | uiState: AuthUiState; 16 | error: any; 17 | } 18 | 19 | export const initialAuthState: AuthState = { 20 | user: null, 21 | uiState: AuthUiState.UNKNOWN, 22 | error: null 23 | }; 24 | 25 | export function authReducer( 26 | state = initialAuthState, 27 | action: AuthActions 28 | ): AuthState { 29 | switch (action.type) { 30 | case AuthActionTypes.GET_USER: 31 | case AuthActionTypes.LOGIN: 32 | case AuthActionTypes.LOGOUT: 33 | return { ...state, uiState: AuthUiState.LOADING }; 34 | 35 | case AuthActionTypes.AUTHENTICATED: 36 | return { 37 | user: action.payload, 38 | uiState: AuthUiState.AUTHENTICATED, 39 | error: null 40 | }; 41 | 42 | case AuthActionTypes.LOGOUT: 43 | return { 44 | user: null, 45 | uiState: AuthUiState.NOT_AUTHENTICATED, 46 | error: null 47 | }; 48 | 49 | case AuthActionTypes.NOT_AUTHENTICATED: 50 | return { 51 | user: null, 52 | uiState: AuthUiState.NOT_AUTHENTICATED, 53 | error: null 54 | }; 55 | 56 | case AuthActionTypes.AUTH_ERROR: 57 | return { 58 | user: null, 59 | uiState: AuthUiState.NOT_AUTHENTICATED, 60 | error: action.payload.message 61 | }; 62 | 63 | case AuthActionTypes.CLEAR_ERROR: 64 | return { 65 | ...state, 66 | error: null 67 | }; 68 | 69 | default: 70 | return state; 71 | } 72 | } 73 | 74 | export namespace AuthQuery { 75 | const selectSlice = createFeatureSelector("auth"); 76 | export const selectUser = createSelector( 77 | selectSlice, 78 | state => (state.user ? state.user : null) 79 | ); 80 | export const selectUiState = createSelector( 81 | selectSlice, 82 | state => state.uiState 83 | ); 84 | export const selectError = createSelector(selectSlice, state => state.error); 85 | } 86 | -------------------------------------------------------------------------------- /src/features/connections/pages/select-task-list/select-task-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { map, filter } from "rxjs/operators"; 3 | import * as _ from "lodash"; 4 | import { ConnectionFacade } from "../../store/connection.facade"; 5 | import { TaskList } from "../../../../models/task-list"; 6 | import { Connection } from "../../../../models/connection"; 7 | import { NavParams, IonicPage } from "ionic-angular"; 8 | import { LoadingService } from "../../../../shared/loading.service"; 9 | 10 | @IonicPage({ 11 | segment: "SelectTaskListPage", 12 | priority: "high" 13 | }) 14 | @Component({ 15 | selector: "app-select-task-list", 16 | templateUrl: "./select-task-list.component.html" 17 | }) 18 | export class SelectTaskListPage implements OnInit { 19 | connectionId: string; 20 | projectId: string; 21 | projectName: string; 22 | loaded: boolean = false; 23 | 24 | taskLists$ = this.connectionFacade.selectedConnection$.pipe( 25 | filter(connection => { 26 | return _.has(connection, `projects.${this.projectId}.taskLists`); 27 | }), 28 | map((connection: Connection) => { 29 | this.loaded = true; 30 | 31 | return Object.keys(connection.projects[this.projectId].taskLists).map( 32 | keys => connection.projects[this.projectId].taskLists[keys] 33 | ); 34 | }) 35 | ); 36 | 37 | selectedLists: { [taskListId: string]: boolean } = {}; 38 | 39 | get taskListIds() { 40 | return _.invertBy(this.selectedLists)["true"]; 41 | } 42 | 43 | constructor( 44 | private connectionFacade: ConnectionFacade, 45 | private loadingSrv: LoadingService, 46 | private navParams: NavParams 47 | ) {} 48 | 49 | ngOnInit(): void { 50 | this.connectionId = this.navParams.get("connectionId"); 51 | this.projectId = this.navParams.get("projectId"); 52 | this.projectName = this.navParams.get("projectName"); 53 | this.connectionFacade.selectProject(this.connectionId, this.projectId); 54 | } 55 | 56 | handleToggle(checked: boolean, taskList: TaskList) { 57 | this.selectedLists[taskList.id] = checked; 58 | } 59 | 60 | startSession() { 61 | this.loadingSrv.present(); 62 | this.connectionFacade.createSession( 63 | this.connectionId, 64 | this.projectId, 65 | this.taskListIds, 66 | this.projectName 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | import { Platform } from "ionic-angular"; 3 | import { StatusBar } from "@ionic-native/status-bar"; 4 | import { ScreenOrientation } from "@ionic-native/screen-orientation"; 5 | import { SplashScreen } from "@ionic-native/splash-screen"; 6 | import { AuthFacade } from "../features/authentication/store/auth.facade"; 7 | import { Deeplinks } from "@ionic-native/deeplinks"; 8 | import { DashboardPage } from "../features/dashboard/pages/dashboard/dashboard.component"; 9 | import { ConnectionFacade } from "../features/connections/store/connection.facade"; 10 | // import { LoadingService } from "../shared/loading.service"; 11 | 12 | @Component({ 13 | templateUrl: "app.html" 14 | }) 15 | export class JuntoScopeComponent { 16 | rootPage: any = "LoginPage"; 17 | 18 | constructor( 19 | platform: Platform, 20 | statusBar: StatusBar, 21 | splashScreen: SplashScreen, 22 | screenOrientation: ScreenOrientation, 23 | authFacade: AuthFacade, 24 | deeplinks: Deeplinks, 25 | connectionFacade: ConnectionFacade 26 | ) { 27 | platform.ready().then(() => { 28 | // Okay, so the platform is ready and our plugins are available. 29 | // Here you can do any higher level native things you might need. 30 | statusBar.styleDefault(); 31 | splashScreen.hide(); 32 | screenOrientation 33 | .lock(screenOrientation.ORIENTATIONS.PORTRAIT) 34 | .catch(error => console.log(error)); 35 | 36 | authFacade.checkAuth(); 37 | 38 | deeplinks 39 | .route({ 40 | "/": DashboardPage 41 | }) 42 | .subscribe( 43 | match => { 44 | // match.$route - the route we matched, which is the matched entry from the arguments to route() 45 | // match.$args - the args passed in the link 46 | // match.$link - the full link data 47 | console.log("Successfully matched route", match); 48 | const code = match.$args.code; 49 | 50 | const connection = { 51 | token: code, 52 | type: "teamwork" 53 | }; 54 | 55 | connectionFacade.addConnection(connection); 56 | }, 57 | nomatch => { 58 | // nomatch.$link - the full link data 59 | console.error("Got a deeplink that didn't match", nomatch); 60 | } 61 | ); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/features/settings/pages/settings/settings.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from "@angular/core"; 2 | import { IonicPage, NavController } from "ionic-angular"; 3 | import { TakeUntilDestroy, untilDestroyed } from "ngx-take-until-destroy"; 4 | import { filter, take } from "rxjs/operators"; 5 | import { AuthFacade } from "../../../authentication/store/auth.facade"; 6 | import { AuthUiState } from "../../../authentication/store/auth.reducer"; 7 | import { ConnectionFacade } from "../../../connections/store/connection.facade"; 8 | import { LoginPage } from "../../../authentication/pages/login/login.component"; 9 | import { Subscription } from "rxjs"; 10 | import { SettingsService } from "../../service/settings.service"; 11 | 12 | @TakeUntilDestroy() 13 | @IonicPage({ 14 | segment: "SettingsPage", 15 | priority: "high" 16 | }) 17 | @Component({ 18 | selector: "app-settings", 19 | templateUrl: "./settings.html" 20 | }) 21 | export class SettingsPage implements OnInit, OnDestroy { 22 | connections$ = this.connectionFacade.connections$; 23 | logOutSub: Subscription; 24 | faqsSub: Subscription; 25 | 26 | private logoutRedirect$ = this.authFacade.uiState$.pipe( 27 | untilDestroyed(this), 28 | filter(authState => authState === AuthUiState.NOT_AUTHENTICATED) 29 | ); 30 | faqs; 31 | 32 | constructor( 33 | private navCtrl: NavController, 34 | private connectionFacade: ConnectionFacade, 35 | private authFacade: AuthFacade, 36 | private settingsSvc: SettingsService 37 | ) {} 38 | 39 | ngOnDestroy() { 40 | this.unsub(); 41 | } 42 | 43 | ngOnInit() { 44 | this.connectionFacade.getConnections(); 45 | this.faqsSub = this.settingsSvc.getFaqs().subscribe(faqs => { 46 | if (faqs) { 47 | this.faqs = faqs; 48 | } 49 | }); 50 | } 51 | 52 | viewConnectionDetails(connectionId) { 53 | this.navCtrl.push("ConnectionDetailsPage", { 54 | connectionId: connectionId 55 | }); 56 | } 57 | 58 | addConnection() { 59 | this.navCtrl.push("AddConnectionPage"); 60 | } 61 | 62 | logout() { 63 | this.unsub(); 64 | this.authFacade.logout(); 65 | 66 | this.logOutSub = this.logoutRedirect$.pipe(take(1)).subscribe(() => { 67 | this.navCtrl.push(LoginPage); 68 | }); 69 | } 70 | 71 | unsub() { 72 | if (this.logOutSub) { 73 | this.logOutSub.unsubscribe(); 74 | } 75 | if (this.faqsSub) { 76 | this.faqsSub.unsubscribe(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /functions/src/api/connections/projects/sessions/add-session.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import { 3 | firestore, 4 | encryptionService, 5 | sessionService, 6 | teamworkService 7 | } from "./../../../../services"; 8 | 9 | export async function addSession(req: express.Request, res: express.Response) { 10 | const uid = res.locals.user.uid; 11 | const { connectionId, projectId } = req.params; 12 | const { taskListIds, projectName } = req.body; 13 | 14 | if (!connectionId) { 15 | return res 16 | .status(400) 17 | .json({ message: "Connection id is a required parameter." }); 18 | } 19 | 20 | if (!projectId) { 21 | return res 22 | .status(400) 23 | .json({ message: "Project id is a required parameter." }); 24 | } 25 | 26 | if (!Array.isArray(taskListIds) || taskListIds.length === 0) { 27 | return res.status(400).json({ message: "Task List Ids is required" }); 28 | } 29 | 30 | const connection = await firestore 31 | .doc(`/users/${uid}/connections/${connectionId}`) 32 | .get() 33 | .then(snapshot => snapshot.data()); 34 | 35 | if (!connection) { 36 | return res.status(400).json({ message: "Invalid connection id." }); 37 | } 38 | 39 | let sessionTasks; 40 | let newSession; 41 | try { 42 | switch (connection.type) { 43 | case "teamwork": { 44 | const token = encryptionService.decrypt(connection.token); 45 | const baseUrl = connection.externalData.baseUrl; 46 | 47 | sessionTasks = await Promise.all( 48 | taskListIds.map(id => teamworkService.getTasks(token, baseUrl, id)) 49 | ).then(responses => { 50 | return responses 51 | .map(response => response.tasks) 52 | .reduce((allTasks, taskListTasks) => { 53 | for (const taskId in taskListTasks) { 54 | if (taskListTasks.hasOwnProperty(taskId)) { 55 | allTasks[taskId] = taskListTasks[taskId]; 56 | } 57 | } 58 | return allTasks; 59 | }, {}); 60 | }); 61 | 62 | break; 63 | } 64 | 65 | default: { 66 | return res.status(400).json({ message: "Unknown Connection Type" }); 67 | } 68 | } 69 | 70 | newSession = await sessionService.createSession( 71 | uid, 72 | connectionId, 73 | projectId, 74 | sessionTasks, 75 | projectName 76 | ); 77 | } catch (error) { 78 | return res.status(400).json({ message: error }); 79 | } 80 | 81 | return res.status(201).json(newSession); 82 | } 83 | -------------------------------------------------------------------------------- /src/features/settings/pages/settings/settings.scss: -------------------------------------------------------------------------------- 1 | app-settings { 2 | .connections { 3 | ion-list.list { 4 | background-color: $primary-orange; 5 | border-bottom: 1px solid white !important; 6 | margin: 0; 7 | width: 100%; 8 | 9 | ion-list-header.list-header { 10 | background-color: $primary-orange; 11 | padding: 1rem 1rem 0rem 1rem; 12 | 13 | .icon, 14 | h2 { 15 | color: white; 16 | display: inline-block; 17 | font-size: 24px; 18 | font-weight: 600; 19 | text-transform: none; 20 | vertical-align: middle; 21 | } 22 | 23 | .icon { 24 | font-size: 36px; 25 | } 26 | 27 | ion-label.label { 28 | padding: 1rem; 29 | margin: 1rem; 30 | } 31 | } 32 | 33 | // existing projects 34 | ion-item.item-block { 35 | background-color: $primary-orange; 36 | border-bottom: 1px solid $primary-orange; 37 | color: white; 38 | } 39 | } 40 | } 41 | 42 | ion-item.item.item-block.new-connection { 43 | margin-top: 20px; 44 | border: 2px solid #c94313; 45 | } 46 | 47 | .connections-header { 48 | display: grid; 49 | color: white; 50 | grid-template-columns: 0.75fr 1fr; 51 | margin: 10px; 52 | background: rgba(241, 101, 45, 0.65); 53 | border-radius: 5px; 54 | 55 | ion-label.label { 56 | margin: 0; 57 | padding: 4px; 58 | } 59 | } 60 | 61 | .connection-item { 62 | display: grid; 63 | color: white; 64 | grid-template-columns: 0.75fr 0.75fr; 65 | 66 | ion-label.label { 67 | margin: 0; 68 | } 69 | } 70 | 71 | // custom buttons for android and windows 72 | .button-md, 73 | .button-wp { 74 | border-radius: 11px; 75 | &.existing-button span { 76 | padding-left: 8px; 77 | padding-right: 6px; 78 | } 79 | span { 80 | padding-left: 3px; 81 | padding-right: 2px; 82 | } 83 | } 84 | 85 | // custom buttons for ios 86 | .button-ios { 87 | border: none; 88 | } 89 | 90 | .faq { 91 | margin: 2rem 1rem; 92 | 93 | ion-list-header.list-header { 94 | border-bottom: 2px solid $primary-orange; 95 | } 96 | 97 | p, 98 | h2 { 99 | color: $primary-orange; 100 | } 101 | 102 | ion-item.item { 103 | h2 { 104 | font-weight: bold; 105 | } 106 | p { 107 | color: $primary-orange; 108 | white-space: initial; 109 | font-size: 13px; 110 | } 111 | } 112 | } 113 | 114 | .logout { 115 | margin: 2rem 0; 116 | 117 | button { 118 | width: 60%; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/features/dashboard/store/dashboard.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, createFeatureSelector } from "@ngrx/store"; 2 | import { EntityState, EntityAdapter, createEntityAdapter } from "@ngrx/entity"; 3 | 4 | import { DashboardActions, DashboardActionTypes } from "./dashboard.actions"; 5 | import { HistoryItem } from "../../../models/history-item"; 6 | 7 | export enum DashboardUiState { 8 | LOADING = "Loading", 9 | LOADED = "Loaded", 10 | NOT_LOADED = "Not Loaded" 11 | } 12 | 13 | export interface DashboardState extends EntityState { 14 | uiState: DashboardUiState; 15 | error: string; 16 | } 17 | 18 | export const adapter: EntityAdapter = createEntityAdapter< 19 | HistoryItem 20 | >(); 21 | 22 | export const initialDashboardState: DashboardState = adapter.getInitialState({ 23 | uiState: DashboardUiState.NOT_LOADED, 24 | error: null 25 | }); 26 | 27 | export function dashboardReducer( 28 | state = initialDashboardState, 29 | action: DashboardActions 30 | ): DashboardState { 31 | switch (action.type) { 32 | case DashboardActionTypes.LOAD_HISTORY: { 33 | return adapter.removeAll({ ...state, uiState: DashboardUiState.LOADING }); 34 | } 35 | 36 | case DashboardActionTypes.LOAD_MORE_HISTORY: { 37 | return { 38 | ...state, 39 | uiState: DashboardUiState.LOADING 40 | }; 41 | } 42 | 43 | case DashboardActionTypes.NO_HISTORY: { 44 | return { 45 | ...state, 46 | uiState: DashboardUiState.LOADED 47 | }; 48 | } 49 | 50 | case DashboardActionTypes.ADDED: { 51 | return adapter.upsertOne(action.payload.historyItem, { 52 | ...state, 53 | uiState: DashboardUiState.LOADED 54 | }); 55 | } 56 | 57 | case DashboardActionTypes.MODIFIED: { 58 | return adapter.updateOne(action.payload.update, state); 59 | } 60 | 61 | case DashboardActionTypes.REMOVED: { 62 | return adapter.removeOne(action.payload.historyItemId, { 63 | ...state, 64 | uiState: DashboardUiState.LOADED 65 | }); 66 | } 67 | 68 | case DashboardActionTypes.DELETE_SESSION: { 69 | return { 70 | ...state, 71 | uiState: DashboardUiState.LOADING 72 | }; 73 | } 74 | 75 | case DashboardActionTypes.CLEAR_ERROR: { 76 | return { 77 | ...state, 78 | error: null 79 | }; 80 | } 81 | 82 | default: { 83 | return state; 84 | } 85 | } 86 | } 87 | 88 | export namespace DashboardQuery { 89 | const selectSlice = createFeatureSelector("dashboard"); 90 | export const { selectIds, selectEntities, selectAll } = adapter.getSelectors( 91 | selectSlice 92 | ); 93 | export const selectUiState = createSelector( 94 | selectSlice, 95 | state => state.uiState 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | match /databases/{database}/documents { 3 | 4 | match /public/data/sessions/{sessionUrl} { 5 | function isParticipant() { 6 | return request.auth.uid in resource.data.participants; 7 | } 8 | 9 | // Allow authorized users to fetch session links 10 | allow read: if isParticipant(); 11 | allow list: if isParticipant() && request.query.limit <= 10; 12 | 13 | // Writing happens through the Cloud API when creating sessions 14 | // for Access Code & Session URL generation 15 | // allow write: if never --; 16 | } 17 | 18 | match /users/{userId} { 19 | // Allow users to read and write to their own profile 20 | allow read: if request.auth.uid == userId; 21 | 22 | // TODO: Should any user profile data be written from the client side? 23 | // allow write: if ??; 24 | 25 | match /connections/{connectionId} { 26 | // Allow users to read their own connections 27 | allow read: if request.auth.uid == userId; 28 | 29 | // Writing happens through the Cloud API for token validation purposes 30 | // allow write: if never --; 31 | 32 | match /sessions/{sessionId} { 33 | // Helper function to access session in child rules 34 | function getSessionData() { 35 | return get(/databases/$(database)/documents/users/$(userId)/connections/$(connectionId)/sessions/$(sessionId)).data; 36 | } 37 | 38 | // Helper function to access session participants 39 | function getSessionParticipants() { 40 | return get(/databases/$(database)/documents/public/data/sessions/$(getSessionData().sessionCode)).data.participants; 41 | } 42 | 43 | // Allow session participants to view session data 44 | allow read: if request.auth.uid in getSessionParticipants(); 45 | 46 | // TODO: Should any session data be written from the client side? 47 | // allow write: if ?? 48 | 49 | match /tasks/{taskId} { 50 | // Helper function to validate vote hours 51 | function isVoteValid() { 52 | // -1 = 'not applicable', 0 = 'More info needed', 'value > 0' is estimate 53 | return !math.isNaN(request.resource.data.votes[request.auth.uid]); 54 | } 55 | 56 | // Allow session participants to view session tasks 57 | allow read: if request.auth.uid in getSessionParticipants(); 58 | 59 | // Allow users to register ONLY their OWN votes on tasks 60 | allow update: if request.writeFields.size() == 1 && isVoteValid(); 61 | } 62 | 63 | } 64 | 65 | } 66 | 67 | } 68 | 69 | match /faqs/{faqId} { 70 | allow read: if request.auth.uid != null; 71 | } 72 | 73 | } 74 | 75 | } 76 | --------------------------------------------------------------------------------