├── .dockerignore ├── .env-example ├── .gitignore ├── .semaphore └── semaphore.yml ├── .vscode └── settings.json ├── README.md ├── backend ├── .dockerignore ├── .gitignore ├── .prettierrc ├── Dockerfile ├── Procfile ├── README.md ├── local.env-example ├── nest-cli.json ├── nodemon-debug.json ├── nodemon.json ├── package.json ├── production.env ├── remote.env ├── src │ ├── app.controller.ts │ ├── app.module.ts │ ├── auth │ │ ├── auth.graphql │ │ ├── auth.module.ts │ │ ├── auth.resolvers.ts │ │ ├── auth.service.ts │ │ ├── authType.enum.ts │ │ ├── filterOnRoleOrUser.interceptor.ts │ │ ├── gqlAuth.guard.ts │ │ ├── gqlRole.guard.ts │ │ ├── jwt.config.ts │ │ ├── jwt.strategy.ts │ │ ├── jwtAuth.guard.ts │ │ ├── jwtPayload.interface.ts │ │ ├── loginToken.entity.ts │ │ ├── user.entity.ts │ │ ├── user.param.decorator.ts │ │ ├── user.request.decorator.ts │ │ └── userRole.enum.ts │ ├── autoJob │ │ ├── DTOs │ │ │ └── LoginDto.ts │ │ ├── autojob.controller.ts │ │ ├── autojob.module.ts │ │ └── autojob.services.ts │ ├── clientConfig │ │ ├── clientConfig.graphql │ │ ├── clientConfig.module.ts │ │ ├── clientConfig.resolvers.ts │ │ └── clientConfig.type.ts │ ├── common │ │ ├── defaults.ts │ │ └── scalars │ │ │ └── date.scalar.ts │ ├── config │ │ ├── config.module.ts │ │ └── config.service.ts │ ├── fakeData.service.ts │ ├── gql.ts │ ├── gqlContext.ts │ ├── localization │ │ ├── localization.controller.ts │ │ ├── localization.module.ts │ │ ├── localization.service.ts │ │ └── localizations │ │ │ ├── cs │ │ │ └── translations.json │ │ │ └── en │ │ │ └── translations.json │ ├── main.hmr.ts │ ├── main.ts │ ├── schema.graphql │ └── task │ │ ├── comment │ │ ├── comment.entity.ts │ │ ├── comment.module.ts │ │ ├── comment.resolvers.ts │ │ └── comment.service.ts │ │ ├── subtask │ │ ├── subtask.entity.ts │ │ ├── subtask.module.ts │ │ ├── subtask.resolvers.ts │ │ └── subtask.service.ts │ │ ├── task.entity.ts │ │ ├── task.graphql │ │ ├── task.module.ts │ │ ├── task.resolvers.ts │ │ ├── task.service.ts │ │ └── taskState.enum.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.json ├── tsconfig.spec.json ├── tslint.json ├── webpack.config.js └── yarn.lock ├── dckr ├── env │ ├── .gitignore │ └── mysql-example.env ├── mysql │ └── docker-entrypoint-initdb.d │ │ ├── .gitignore │ │ └── createdb.sql.example ├── nginx │ ├── Dockerfile │ └── default.conf └── scripts │ └── wait-for-mysql.sh ├── docker-compose.prod.yml ├── docker-compose.yml ├── fe ├── .gitignore ├── Procfile ├── README.md ├── codegen.yml ├── components │ ├── AdminSelect │ │ └── AdminSelect.tsx │ ├── Administration │ │ ├── AdminContainer │ │ │ └── AdminContainer.tsx │ │ └── UserList │ │ │ ├── DeleteUser.tsx │ │ │ ├── EditUser.tsx │ │ │ ├── User.tsx │ │ │ ├── UserList.tsx │ │ │ └── UserListContainer.tsx │ ├── Background │ │ └── Background.tsx │ ├── DarkModeSelect │ │ └── DarkModeSelect.tsx │ ├── Dates │ │ ├── DateFormatter.tsx │ │ └── DatePicker.tsx │ ├── Errors │ │ └── Kaomoji.tsx │ ├── Homepage │ │ └── HomePage.tsx │ ├── LanguageSelect │ │ └── LanguageSelect.tsx │ ├── Layouts │ │ ├── HeadComponent.tsx │ │ └── Layout.tsx │ ├── LinkComponents │ │ └── LinkComponent.tsx │ ├── Loading │ │ └── Loading.tsx │ ├── Login │ │ ├── Login.tsx │ │ ├── MicrosoftButtonLogin.tsx │ │ ├── SocialButton.tsx │ │ └── TipBar.tsx │ ├── MainAppBar │ │ ├── Logo │ │ │ └── Logo.tsx │ │ ├── MainAppBar.tsx │ │ ├── UserLogged.tsx │ │ └── logo_new.png │ ├── NewTask │ │ └── NewTaskContainer.tsx │ ├── RoleIcon │ │ └── RoleIcon.tsx │ ├── RoleSelect │ │ └── RoleSelect.tsx │ ├── ScrollTop │ │ └── ScrollTop.tsx │ ├── Sharing │ │ └── SharingHead │ │ │ └── SharingHead.tsx │ ├── Snow │ │ ├── Snow.tsx │ │ └── SnowComponent.tsx │ ├── TaskBoard │ │ ├── AddComment.tsx │ │ ├── BoardContainer.tsx │ │ ├── CommentsContainer.tsx │ │ ├── TaskActions.tsx │ │ ├── TaskBoard.tsx │ │ ├── TaskComment.tsx │ │ └── TaskDetail.tsx │ └── Themes │ │ ├── MainTheme.ts │ │ └── ThemeProvider.tsx ├── generated │ └── schema.json ├── next-env.d.ts ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── _error.tsx │ ├── about.tsx │ ├── admin │ │ ├── index.tsx │ │ └── users.tsx │ ├── board.tsx │ ├── index.tsx │ ├── login.tsx │ ├── new.tsx │ └── settings.tsx ├── public │ └── static │ │ ├── helpdesk_bg_trans.png │ │ ├── images │ │ └── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── maxresdefault.png │ │ │ ├── mstile-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ ├── mstile-310x150.png │ │ │ ├── mstile-310x310.png │ │ │ ├── mstile-70x70.png │ │ │ ├── safari-pinned-tab.svg │ │ │ └── site.webmanifest │ │ ├── logos │ │ ├── christmas_logo.png │ │ └── logo_new.png │ │ └── static.css ├── src │ ├── Global │ │ └── Keys.ts │ ├── Locales │ │ └── LocalizationKeys.ts │ ├── Routes.ts │ ├── auth │ │ └── authWrapper.tsx │ ├── graphql │ │ ├── auth.tsx │ │ ├── client.ts │ │ ├── graphql-global-types.ts │ │ ├── graphql-schema.json │ │ ├── mutations.ts │ │ ├── queries.ts │ │ ├── resolvers.js │ │ ├── typedefs.js │ │ └── types │ │ │ ├── AddComment.ts │ │ │ ├── DeleteComment.ts │ │ │ ├── EditUser.ts │ │ │ ├── EditUserAdmin.ts │ │ │ ├── EditUserLanguage.ts │ │ │ ├── GetTaskComments.ts │ │ │ ├── GetTaskDetail.ts │ │ │ ├── addTask.ts │ │ │ ├── changeTaskBoardState.ts │ │ │ ├── changeTaskState.ts │ │ │ ├── createUserEmail.ts │ │ │ ├── deleteTask.ts │ │ │ ├── getAdmins.ts │ │ │ ├── getSession.ts │ │ │ ├── getTask.ts │ │ │ ├── getTasks.ts │ │ │ ├── getUsers.ts │ │ │ ├── loginEmail.ts │ │ │ ├── loginExternal.ts │ │ │ ├── loginOffice.ts │ │ │ ├── logout.ts │ │ │ └── removeUser.ts │ ├── i18n.ts │ ├── services │ │ └── microsoft.ts │ └── theme.ts ├── tsconfig.json ├── tslint.json ├── types │ └── react-social-login │ │ └── index.d.ts ├── utils │ ├── TextHelper.ts │ ├── cookieHelper.ts │ └── dateHelper.ts └── yarn.lock ├── flutter └── helpdesk_mobile │ ├── .flutter-plugins-dependencies │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── android │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── helpdesk_mobile │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle │ ├── ios │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ └── contents.xcworkspacedata │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ ├── lib │ ├── main.dart │ └── src │ │ ├── app.dart │ │ ├── models │ │ ├── authenticatedUserType.dart │ │ ├── enums.dart │ │ ├── logType.dart │ │ ├── taskType.dart │ │ └── userType.dart │ │ └── ui │ │ ├── components │ │ └── taskList │ │ │ ├── sections.dart │ │ │ └── taskCard.dart │ │ └── pages │ │ ├── createTask.dart │ │ ├── login.dart │ │ ├── taskDetail.dart │ │ └── tasklist.dart │ ├── pubspec.lock │ └── pubspec.yaml └── init.sh /.env-example: -------------------------------------------------------------------------------- 1 | MYSQL_PORT=3306 2 | NODE_PORT=3000 3 | STORYBOOK_PORT=9001 4 | FRONTEND_NODE_PORT=3001 5 | IONIC_PORT=8100 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Mac DS_Store files 2 | .DS_Store 3 | 4 | # IntelliJ project files 5 | *.iml 6 | *.iws 7 | *.ipr 8 | .idea/ 9 | 10 | # NetBeans specific 11 | nbproject/private/ 12 | build/ 13 | nbbuild/ 14 | dist/ 15 | nbdist/ 16 | nbactions.xml 17 | nb-configuration.xml 18 | ### VisualStudioCode template 19 | .vscode/* 20 | !.vscode/settings.json 21 | !.vscode/tasks.json 22 | !.vscode/launch.json 23 | !.vscode/extensions.json 24 | ### JetBrains template 25 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 26 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 27 | 28 | # User-specific stuff 29 | .idea/**/workspace.xml 30 | .idea/**/tasks.xml 31 | .idea/**/usage.statistics.xml 32 | .idea/**/dictionaries 33 | .idea/**/shelf 34 | 35 | # Sensitive or high-churn files 36 | .idea/**/dataSources/ 37 | .idea/**/dataSources.ids 38 | .idea/**/dataSources.local.xml 39 | .idea/**/sqlDataSources.xml 40 | .idea/**/dynamic.xml 41 | .idea/**/uiDesigner.xml 42 | .idea/**/dbnavigator.xml 43 | 44 | # Gradle 45 | .idea/**/gradle.xml 46 | .idea/**/libraries 47 | 48 | # Gradle and Maven with auto-import 49 | # When using Gradle or Maven with auto-import, you should exclude module files, 50 | # since they will be recreated, and may cause churn. Uncomment if using 51 | # auto-import. 52 | # .idea/modules.xml 53 | # .idea/*.iml 54 | # .idea/modules 55 | 56 | # CMake 57 | cmake-build-*/ 58 | 59 | # Mongo Explorer plugin 60 | .idea/**/mongoSettings.xml 61 | 62 | # File-based project format 63 | *.iws 64 | 65 | # IntelliJ 66 | out/ 67 | 68 | # mpeltonen/sbt-idea plugin 69 | .idea_modules/ 70 | 71 | # JIRA plugin 72 | atlassian-ide-plugin.xml 73 | 74 | # Cursive Clojure plugin 75 | .idea/replstate.xml 76 | 77 | # Crashlytics plugin (for Android Studio and IntelliJ) 78 | com_crashlytics_export_strings.xml 79 | crashlytics.properties 80 | crashlytics-build.properties 81 | fabric.properties 82 | 83 | # Editor-based Rest Client 84 | .idea/httpRequests 85 | .env 86 | node_modules 87 | yarn-error.log 88 | flutter/helpdesk_mobile/.flutter-plugins-dependencies 89 | -------------------------------------------------------------------------------- /.semaphore/semaphore.yml: -------------------------------------------------------------------------------- 1 | version: v1.0 2 | name: Node JS 3 | agent: 4 | machine: 5 | type: e1-standard-2 6 | os_image: ubuntu1804 7 | blocks: 8 | - name: Frontend 9 | task: 10 | jobs: 11 | - name: yarn lint 12 | commands: 13 | - yarn lint 14 | - name: yarn typescript 15 | commands: 16 | - yarn typescript 17 | - name: yarn build 18 | commands: 19 | - yarn build 20 | prologue: 21 | commands: 22 | - sem-version node 12.14.1 23 | - checkout 24 | - cd fe 25 | - yarn install 26 | dependencies: [] 27 | - name: Backend 28 | dependencies: [] 29 | task: 30 | jobs: 31 | - name: yarn lint 32 | commands: 33 | - yarn lint 34 | - name: yarn typescript 35 | commands: 36 | - yarn typescript 37 | - name: yarn build 38 | commands: 39 | - yarn build 40 | prologue: 41 | commands: 42 | - sem-version node 12.14.1 43 | - checkout 44 | - cd backend 45 | - yarn install 46 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 4, 3 | "editor.detectIndentation": false 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project for Delta HelpDesk 2 | ## Project structure 3 | 4 | ### Backend - /backend 5 | Written using NestJS with TypeORM and GraphQL. 6 | 7 | ### NextJS frontend - /fe 8 | Written using NextJS. 9 | 10 | ### Mobile application - /flutter/helpdesk_mobile 11 | Writting using Flutter. 12 | 13 | [![Build Status](https://helpdesk.semaphoreci.com/badges/helpdesk/branches/dev.svg)](https://helpdesk.semaphoreci.com/projects/helpdesk) 14 | 15 | ## Quick links 16 | 17 | - [Frontend](https://delta-nextjs.herokuapp.com/) 18 | - [Backend](https://delta-helpdesk.herokuapp.com/) 19 | - [Trello](https://trello.com/b/MXKHJvS8/helpdesk) 20 | 21 | ## Installation/Start 22 | 23 | ### Backend 24 | 25 | 1. `cd backend` 26 | 27 | 2. `yarn install` 28 | 29 | 3. For local MySQL server (docker): `yarn begin` 30 | 31 | 4. 32 | - a) Remote MySQL database: `yarn start:remote` 33 | - b) Local docker MySQL database: `yarn start:local` See: [Docker setup](#Docker) 34 | - c) Remote production MySQL database: `yarn start:prod` 35 | 36 | ### Frontend (Next.js) 37 | 38 | 1. Project directory `cd fe` 39 | 40 | 2. `yarn install` 41 | 42 | 3. `yarn dev` 43 | 44 | ### Docker 45 | 46 | #### Setup 47 | 48 | ##### Windows 10 Pro + NO VirtualBox 49 | 1. Install Docker Desktop. -> Use Linux Containers 50 | 2. Go to settings -> Shared Drives 51 | 3. Select one drive 52 | 53 | ##### Windows 10 or older 54 | 1. Install [Docker Toolbox](https://github.com/docker/toolbox/releases) 55 | 56 | 57 | #### Init 58 | 59 | 2. `bash init.sh` in Git bash **TODO: Without init.sh** 60 | - This will generate required files (.env, ...) 61 | 62 | 63 | 3. `docker-compose up mysql` 64 | - This **should** start MySQL server in docker 65 | 66 | ### Some useful commands 67 | #### `docker-compose up [-d] [services]` 68 | Start all services specified in `docker-compose.yml`. Argument `d` will start containers in detached mode. You can specify which services you want to start. 69 | #### `docker-compose stop` 70 | Stop all running containers. 71 | #### `docker-compose logs [service]` 72 | View logs from containers. -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | local.env 59 | #.env 60 | #*.env 61 | 62 | # next.js build output 63 | .next -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "tabWidth": 4 5 | } -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kkarczmarczyk/node-yarn:latest as build 2 | WORKDIR /app 3 | COPY . . 4 | RUN yarn install 5 | RUN node_modules/.bin/tsc 6 | RUN find src -name '*.graphql' -type f -exec cp {} dist \; 7 | RUN cp -r src/localization/localizations dist/localization/localizations 8 | RUN yarn install --production 9 | 10 | FROM kkarczmarczyk/node-yarn:latest 11 | WORKDIR /app 12 | COPY --from=build /app/dist /app/dist 13 | COPY --from=build /app/node_modules /app/node_modules 14 | CMD ["node", "dist/main.js"] 15 | EXPOSE 3000 16 | -------------------------------------------------------------------------------- /backend/Procfile: -------------------------------------------------------------------------------- 1 | web: yarn start:prod -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Installation 3 | 4 | ```bash 5 | $ yarn install 6 | 7 | 8 | # For local MySQL server 9 | $ yarn begin 10 | ``` 11 | 12 | ## Running the app 13 | 14 | ```bash 15 | # Local docker MySQL database 16 | $ yarn start:local 17 | 18 | # Remote MySQL database 19 | $ yarn start:remote 20 | 21 | # Remote production MySQL database 22 | $ yarn start:prod 23 | ``` 24 | 25 | ### TODO 26 | - [ ] security issue with production.env, use env in hosting? 27 | -------------------------------------------------------------------------------- /backend/local.env-example: -------------------------------------------------------------------------------- 1 | TYPEORM_HOST=192.168.99.100 2 | TYPEORM_USERNAME=root 3 | TYPEORM_PASSWORD=password 4 | TYPEORM_DATABASE=helpdesk 5 | TYPEORM_PORT=3306 6 | TYPEORM_ENTITIES=src/**/**.entity.ts 7 | AUTOJOB_NAME=robot 8 | AUTOJOB_PWD=password -------------------------------------------------------------------------------- /backend/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /backend/nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts", "src/ggl.ts"], 5 | "exec": "node --inspect-brk -r ts-node/register src/main.ts" 6 | } -------------------------------------------------------------------------------- /backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts,graphql", 4 | "ignore": ["src/**/*.spec.ts", "src/gql.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /backend/production.env: -------------------------------------------------------------------------------- 1 | TYPEORM_HOST=eu-cdbr-west-02.cleardb.net 2 | TYPEORM_USERNAME=bf021a8b806e40 3 | TYPEORM_PASSWORD=e8fc9240 4 | TYPEORM_DATABASE=heroku_a7f13a2a84fb687 5 | TYPEORM_PORT=3306 6 | TYPEORM_ENTITIES=dist/src/**/**.entity.js 7 | AUTOJOB_NAME=robot 8 | AUTOJOB_PWD=JlhS5Lx0o9Oxzq54 -------------------------------------------------------------------------------- /backend/remote.env: -------------------------------------------------------------------------------- 1 | TYPEORM_HOST=MYSQL6001.site4now.net 2 | TYPEORM_USERNAME=a42a18_help 3 | TYPEORM_PASSWORD=Heslo123 4 | TYPEORM_DATABASE=db_a42a18_help 5 | TYPEORM_PORT=3306 6 | TYPEORM_ENTITIES=src/**/**.entity.ts 7 | AUTOJOB_NAME=robot 8 | AUTOJOB_PWD=password -------------------------------------------------------------------------------- /backend/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Get, Controller } from '@nestjs/common'; 2 | 3 | @Controller() 4 | export class AppController { 5 | constructor() { } 6 | 7 | @Get() 8 | root(): { status: string } { 9 | return { status: 'ok' }; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { AuthModule } from './auth/auth.module'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Module, Inject } from '@nestjs/common'; 4 | import { AppController } from './app.controller'; 5 | import { GraphQLModule } from '@nestjs/graphql'; 6 | import { LocalizationModule } from './localization/localization.module'; 7 | import { TaskModule } from './task/task.module'; 8 | import { join } from 'path'; 9 | import { gqlContextFunction } from './gqlContext'; 10 | import { FakeDataService } from './fakeData.service'; 11 | import { ConfigService } from './config/config.service'; 12 | import { ConfigModule } from './config/config.module'; 13 | import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'; 14 | import { AutoJobModule } from './autoJob/autojob.module'; 15 | import { User } from './auth/user.entity'; 16 | import { Task } from './task/task.entity'; 17 | import { ClientConfigModule } from './clientConfig/clientConfig.module'; 18 | import { SubTask } from './task/subtask/subtask.entity'; 19 | import { Comment } from './task/comment/comment.entity'; 20 | 21 | @Module({ 22 | imports: [ 23 | GraphQLModule.forRoot({ 24 | context: gqlContextFunction, 25 | typePaths: ['./**/*.graphql'], 26 | definitions: { 27 | path: join(process.cwd(), 'src/gql.ts'), 28 | outputAs: 'class', 29 | }, 30 | debug: true, 31 | introspection: true, 32 | playground: true, 33 | // tracing: true, 34 | }), 35 | TypeOrmModule.forRootAsync({ 36 | imports: [ConfigModule], 37 | inject: [ConfigService], 38 | useFactory: async (config: ConfigService) => { 39 | return { 40 | ...config.getConfig(), 41 | synchronize: true, 42 | logging: true, 43 | type: 'mysql', 44 | } as MysqlConnectionOptions; 45 | }, 46 | }), 47 | AuthModule, 48 | LocalizationModule, 49 | TaskModule, 50 | AutoJobModule, 51 | ClientConfigModule, 52 | TypeOrmModule.forFeature([User, Task, SubTask, Comment]), 53 | ], 54 | controllers: [AppController], 55 | providers: [FakeDataService], 56 | }) 57 | export class AppModule { } 58 | -------------------------------------------------------------------------------- /backend/src/auth/auth.graphql: -------------------------------------------------------------------------------- 1 | enum AuthType { 2 | EMAIL 3 | GOOGLE 4 | FACEBOOK 5 | MICROSOFT 6 | GITHUB 7 | } 8 | 9 | enum UserRole { 10 | DEFAULT 11 | ADMIN 12 | SUPERADMIN 13 | } 14 | 15 | type User { 16 | id: ID! 17 | fullName: String! 18 | email: String! 19 | created_at: Date! 20 | updated_at: Date 21 | role: UserRole 22 | enabled: Boolean! 23 | } 24 | 25 | type AuthenticatedUser { 26 | id: ID! 27 | fullName: String! 28 | email: String! 29 | language: String! 30 | theme: String, 31 | created_at: Date! 32 | updated_at: Date 33 | token: String! 34 | role: UserRole 35 | } 36 | 37 | type Query { 38 | session: AuthenticatedUser 39 | admins: [User] 40 | users(onlyEnabled: Boolean): [User] 41 | } 42 | 43 | type Mutation { 44 | loginOffice(token: String!): AuthenticatedUser 45 | loginExternal(email: String!, name: String!, provider: AuthType!, token: String! ): AuthenticatedUser 46 | loginEmail(email: String!, password: String!): AuthenticatedUser 47 | createUserEmail(email: String!, password: String!, fullName: String!, role: UserRole): User 48 | adminEditUser(userId: ID!, email: String, fullName: String, className: String, role: UserRole): User 49 | editUser(email: String, fullName: String, className: String, language: String, theme: String): User 50 | removeUser(email: String!): Boolean 51 | logout: Boolean 52 | } -------------------------------------------------------------------------------- /backend/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { jwtSecretOrPrivateKey, jwtSignOptions } from './jwt.config'; 2 | import { AuthResolvers } from './auth.resolvers'; 3 | import { Module } from '@nestjs/common'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { User } from './user.entity'; 6 | import { AuthService } from './auth.service'; 7 | import { PassportModule } from '@nestjs/passport'; 8 | import { JwtModule } from '@nestjs/jwt'; 9 | import { JwtStrategy } from './jwt.strategy'; 10 | import { LoginToken } from './loginToken.entity'; 11 | 12 | @Module({ 13 | imports: [ 14 | TypeOrmModule.forFeature([User, LoginToken]), 15 | PassportModule.register({ 16 | defaultStrategy: 'jwt', 17 | property: 'user', 18 | }), 19 | JwtModule.register({ 20 | secretOrPrivateKey: jwtSecretOrPrivateKey, 21 | signOptions: jwtSignOptions, 22 | }), 23 | ], 24 | providers: [ 25 | JwtStrategy, 26 | AuthService, 27 | AuthResolvers, 28 | ], 29 | controllers: [], 30 | }) 31 | export class AuthModule {} -------------------------------------------------------------------------------- /backend/src/auth/authType.enum.ts: -------------------------------------------------------------------------------- 1 | export enum AuthType { 2 | EMAIL = 'EMAIL', 3 | GOOGLE = 'GOOGLE', 4 | FACEBOOK = 'FACEBOOK', 5 | MICROSOFT = 'MICROSOFT', 6 | GITHUB = 'GITHUB', 7 | } -------------------------------------------------------------------------------- /backend/src/auth/filterOnRoleOrUser.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | import { UserRole, checkUserRole } from './userRole.enum'; 5 | import { GqlExecutionContext } from '@nestjs/graphql'; 6 | import { User } from './user.entity'; 7 | import { pickBy } from 'lodash'; 8 | 9 | @Injectable() 10 | export class FilterOnRoleOrUserInterceptor implements NestInterceptor, Partial | Array>> { 11 | constructor(private filteredKeys: string[], private role?: UserRole | undefined, private userIdKey?: keyof T | undefined) { } 12 | intercept( 13 | context: ExecutionContext, 14 | next: CallHandler, 15 | ): Observable | Array>> { 16 | // get graphql context and user from it 17 | const ctx = GqlExecutionContext.create(context); 18 | const user: User = ctx.getContext().req.user; 19 | 20 | // intercept every response in observable and filter it 21 | return next 22 | .handle() 23 | .pipe(map(data => { 24 | // declare filtering function 25 | const doFilter = (dataToFilter: T): Partial | T => { 26 | let shouldFilter = true; 27 | if (!this.role && !this.userIdKey) { 28 | shouldFilter = false; 29 | } else if (this.userIdKey && Number(dataToFilter[this.userIdKey]) !== Number(user.id)) { 30 | shouldFilter = false; 31 | } else if (this.role && checkUserRole((user as User).role, this.role)) { 32 | shouldFilter = false; 33 | } 34 | 35 | if (shouldFilter) { 36 | return pickBy(dataToFilter, (_, key) => !this.filteredKeys.includes(key)); 37 | } else { 38 | return dataToFilter; 39 | } 40 | }; 41 | // filter the object in response or filter array of objects in response 42 | let filteredData: (Partial | T) | Array | T>; 43 | if (Array.isArray(data)) { 44 | filteredData = data.map(doFilter) as T[]; 45 | } else { 46 | filteredData = doFilter(data); 47 | } 48 | return filteredData; 49 | })); 50 | } 51 | } -------------------------------------------------------------------------------- /backend/src/auth/gqlAuth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ExecutionContext } from '@nestjs/common'; 2 | import { GqlExecutionContext } from '@nestjs/graphql'; 3 | import { JwtAuthGuard } from './jwtAuth.guard'; 4 | 5 | @Injectable() 6 | export class GqlAuthGuard extends JwtAuthGuard { 7 | getRequest(context: ExecutionContext) { 8 | const ctx = GqlExecutionContext.create(context); 9 | return ctx.getContext().req; 10 | } 11 | } -------------------------------------------------------------------------------- /backend/src/auth/gqlRole.guard.ts: -------------------------------------------------------------------------------- 1 | import { User } from './user.entity'; 2 | import { Injectable, ExecutionContext } from '@nestjs/common'; 3 | import { UserRole, UserRoleAscendency, checkUserRole } from './userRole.enum'; 4 | import { GqlAuthGuard } from './gqlAuth.guard'; 5 | 6 | @Injectable() 7 | export class GqlRoleGuard extends GqlAuthGuard { 8 | constructor(private role: UserRole) { 9 | super(); 10 | } 11 | canActivate(context: ExecutionContext) { 12 | const { user } = this.getRequest(context); 13 | if (!user) { 14 | return false; 15 | } 16 | return checkUserRole((user as User).role, this.role); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/auth/jwt.config.ts: -------------------------------------------------------------------------------- 1 | import { SignOptions } from 'jsonwebtoken'; 2 | 3 | export const jwtSecretOrPrivateKey = `bi',.;p[p[../42y5u6jugng259/**[;l;'.;'joigytyg215985+39+*-*9+393gtryerttyrweswry8tbd];`; // todo: from ENV 4 | export const jwtSignOptions: SignOptions = { 5 | expiresIn: 60 * 60 * 24 * 2, // 2 days 6 | }; -------------------------------------------------------------------------------- /backend/src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { jwtSecretOrPrivateKey } from './jwt.config'; 2 | import { ExtractJwt, Strategy } from 'passport-jwt'; 3 | import { AuthService } from './auth.service'; 4 | import { PassportStrategy } from '@nestjs/passport'; 5 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 6 | import { JwtPayload } from './jwtPayload.interface'; 7 | 8 | @Injectable() 9 | export class JwtStrategy extends PassportStrategy(Strategy) { 10 | constructor( 11 | private readonly authService: AuthService) { 12 | super({ 13 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 14 | secretOrKey: jwtSecretOrPrivateKey, 15 | passReqToCallback: true, 16 | }); 17 | } 18 | 19 | async validate(req: any, jwtPayload: JwtPayload) { 20 | const token = req.headers.authorization.split(' ')[1]; 21 | const user = await this.authService.validateJwtPayload(token, jwtPayload); 22 | if (!user) { 23 | throw new UnauthorizedException(); 24 | } 25 | return user; 26 | } 27 | } -------------------------------------------------------------------------------- /backend/src/auth/jwtAuth.guard.ts: -------------------------------------------------------------------------------- 1 | import { User as UserEntity } from './user.entity'; 2 | import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common'; 3 | import { AuthGuard } from '@nestjs/passport'; 4 | 5 | @Injectable() 6 | export class JwtAuthGuard extends AuthGuard('jwt') { 7 | canActivate(context: ExecutionContext) { 8 | return super.canActivate(context); 9 | } 10 | 11 | handleRequest(err: any, user: any, info: any): TUser { 12 | if (err || !user) { 13 | throw err || new UnauthorizedException(); 14 | } 15 | return user as TUser; 16 | } 17 | } -------------------------------------------------------------------------------- /backend/src/auth/jwtPayload.interface.ts: -------------------------------------------------------------------------------- 1 | import { AuthType } from './authType.enum'; 2 | 3 | export interface JwtPayload { 4 | userId: number; 5 | authType: AuthType; 6 | externalToken?: string; 7 | issued?: Date; 8 | } -------------------------------------------------------------------------------- /backend/src/auth/loginToken.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, CreateDateColumn, ManyToOne, JoinColumn, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { User } from './user.entity'; 3 | import { AuthType } from './authType.enum'; 4 | 5 | @Entity() 6 | export class LoginToken { 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | @Column({ nullable: false }) 11 | loginProvider: AuthType; 12 | 13 | @Column({ length: 500, nullable: false }) 14 | providerKey: string; 15 | 16 | @ManyToOne(type => User, user => user.assignedTasks, { eager: true }) 17 | owner: User; 18 | 19 | @CreateDateColumn() 20 | created_at: Date; 21 | 22 | @Column({ nullable: true }) 23 | expiration?: Date; 24 | 25 | get expired(): boolean { 26 | return !!this.expiration && this.expiration < new Date(); 27 | } 28 | } -------------------------------------------------------------------------------- /backend/src/auth/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'; 2 | import { UserRole } from './userRole.enum'; 3 | import { LoginToken } from './loginToken.entity'; 4 | import { Task } from '../task/task.entity'; 5 | import { Comment } from '../task/comment/comment.entity'; 6 | 7 | @Entity() 8 | export class User { 9 | @PrimaryGeneratedColumn() 10 | id: number; 11 | 12 | @Column({ length: 120 }) 13 | fullName: string; 14 | 15 | @Column({ default: true }) 16 | enabled: boolean; 17 | 18 | @Column({ length: 120, unique: true }) 19 | email: string; 20 | 21 | @Column({ length: 255, nullable: true }) 22 | password?: string; 23 | 24 | @Column({ length: 10, nullable: true }) 25 | className?: string; 26 | 27 | @Column({ length: 10, default: 'cs_CZ', nullable: false }) 28 | language: string; 29 | 30 | @Column({ length: 10, nullable: true }) 31 | theme?: string; 32 | 33 | token?: string; 34 | 35 | @CreateDateColumn() 36 | created_at: Date; 37 | 38 | @UpdateDateColumn() 39 | updated_at: Date; 40 | 41 | @OneToMany(type => Task, x => x.author) 42 | createdTasks: Task[]; 43 | 44 | @OneToMany(type => Task, x => x.assignee) 45 | assignedTasks: Task[]; 46 | 47 | @OneToMany(type => Comment, x => x.author) 48 | createdComments: Comment[]; 49 | 50 | @OneToMany(type => LoginToken, token => token.owner) 51 | loginTokens: Promise; 52 | 53 | @Column({ default: UserRole.DEFAULT }) 54 | role: UserRole; 55 | } -------------------------------------------------------------------------------- /backend/src/auth/user.param.decorator.ts: -------------------------------------------------------------------------------- 1 | import { User as UserEntity } from './user.entity'; 2 | import { createParamDecorator } from '@nestjs/common'; 3 | import * as util from 'util'; 4 | 5 | export const User = createParamDecorator( 6 | (data, [root, args, ctx, info]): UserEntity | undefined => { 7 | return ctx.req.user; 8 | }, 9 | ); -------------------------------------------------------------------------------- /backend/src/auth/user.request.decorator.ts: -------------------------------------------------------------------------------- 1 | import { User as UserEntity } from './user.entity'; 2 | import { createParamDecorator } from '@nestjs/common'; 3 | 4 | export const User = createParamDecorator((data, req): UserEntity | undefined => { 5 | return req.user; 6 | }); -------------------------------------------------------------------------------- /backend/src/auth/userRole.enum.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'auth/user.entity'; 2 | 3 | export enum UserRole { 4 | DEFAULT = 'DEFAULT', 5 | ADMIN = 'ADMIN', 6 | SUPERADMIN = 'SUPERADMIN', 7 | } 8 | 9 | export const UserRoleAscendency = [ 10 | UserRole.DEFAULT, 11 | UserRole.ADMIN, 12 | UserRole.SUPERADMIN, 13 | ]; 14 | 15 | export function checkUserRole(userRole: UserRole, requiredUserRole: UserRole) { 16 | const requiredRoleIndex = UserRoleAscendency.findIndex(role => role === requiredUserRole); 17 | const userRoleIndex = UserRoleAscendency.findIndex(role => role === userRole); 18 | return userRoleIndex >= requiredRoleIndex; 19 | } -------------------------------------------------------------------------------- /backend/src/autoJob/DTOs/LoginDto.ts: -------------------------------------------------------------------------------- 1 | export interface LoginDto { 2 | username: string; 3 | password: string; 4 | } -------------------------------------------------------------------------------- /backend/src/autoJob/autojob.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Header, Body } from '@nestjs/common'; 2 | import { LoginDto } from './DTOs/LoginDto'; 3 | import { AutoJobService } from './autojob.services'; 4 | 5 | @Controller('autojob') 6 | export class AutoJobController { 7 | constructor( 8 | private readonly autoService: AutoJobService, 9 | ) { } 10 | 11 | @Post('clearexpiredtokens') 12 | @Header('Cache-Control', 'none') 13 | async clearExpiredTokens(@Body() loginDto: LoginDto): Promise { 14 | if (!loginDto) { 15 | return false; 16 | } 17 | const { username, password } = loginDto; 18 | if (!this.autoService.checkLogin(username, password)) { 19 | return false; 20 | } 21 | const res = await this.autoService.clearExpiredTokens(); 22 | return res; 23 | } 24 | } -------------------------------------------------------------------------------- /backend/src/autoJob/autojob.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AutoJobController } from './autojob.controller'; 3 | import { AutoJobService } from './autojob.services'; 4 | import { ConfigModule } from '../config/config.module'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { LoginToken } from '../auth/loginToken.entity'; 7 | 8 | @Module({ 9 | controllers: [AutoJobController], 10 | providers: [ 11 | AutoJobService, 12 | ], 13 | imports: [ 14 | TypeOrmModule.forFeature([LoginToken]), 15 | ConfigModule, 16 | ], 17 | }) 18 | export class AutoJobModule { } -------------------------------------------------------------------------------- /backend/src/autoJob/autojob.services.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '../config/config.service'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Repository } from 'typeorm'; 5 | import { LoginToken } from '../auth/loginToken.entity'; 6 | 7 | @Injectable() 8 | export class AutoJobService { 9 | constructor( 10 | private readonly configService: ConfigService, 11 | @InjectRepository(LoginToken) 12 | private readonly tokenRepository: Repository, 13 | ) { } 14 | 15 | checkLogin(username: string, password: string): boolean { 16 | if (!username || !password) { 17 | return false; 18 | } 19 | const autoName = this.configService.get('AUTOJOB_NAME'); 20 | const autoPwd = this.configService.get('AUTOJOB_PWD'); 21 | if (username !== autoName || password !== autoPwd) { 22 | return false; 23 | } 24 | return true; 25 | } 26 | 27 | async clearExpiredTokens() { 28 | try { 29 | const res = await this.tokenRepository 30 | .createQueryBuilder() 31 | .where('login_token.expiration IS NOT NULL') 32 | .andWhere('expiration < :exp ', { exp: new Date() }) 33 | .delete() 34 | .execute(); 35 | } catch { 36 | return false; 37 | } 38 | return true; 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /backend/src/clientConfig/clientConfig.graphql: -------------------------------------------------------------------------------- 1 | type Validation { 2 | taskSubjectMaxLength: Int! 3 | taskIssueMaxLength: Int! 4 | subtaskMaxLength: Int! 5 | commentMaxLength: Int! 6 | } 7 | 8 | type ClientConfig { 9 | validation: Validation! 10 | preferencies: Preferencies! 11 | } 12 | 13 | type Query { 14 | clientConfig: ClientConfig! 15 | } 16 | 17 | type Preferencies { 18 | language: String! 19 | theme: String! 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/clientConfig/clientConfig.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ClientConfigResolver } from './clientConfig.resolvers'; 3 | 4 | @Module({ 5 | providers: [ClientConfigResolver], 6 | }) 7 | export class ClientConfigModule {} 8 | -------------------------------------------------------------------------------- /backend/src/clientConfig/clientConfig.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query } from '@nestjs/graphql'; 2 | import { ClientConfig } from './clientConfig.type'; 3 | import { Defaults } from '../common/defaults'; 4 | 5 | @Resolver('ClientConfig') 6 | export class ClientConfigResolver { 7 | @Query('clientConfig') 8 | getConfig(): ClientConfig { 9 | return { 10 | // Validation config 11 | validation: Defaults.GetValidation(), 12 | // Default prefs 13 | preferencies: Defaults.GetPreferences(), 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/clientConfig/clientConfig.type.ts: -------------------------------------------------------------------------------- 1 | import { Field, Int, ObjectType } from 'type-graphql'; 2 | 3 | @ObjectType() 4 | export class Validation { 5 | @Field(() => Int) 6 | taskSubjectMaxLength: number; 7 | 8 | @Field(() => Int) 9 | commentMaxLength: number; 10 | 11 | @Field(() => Int) 12 | taskIssueMaxLength: number; 13 | 14 | @Field(() => Int) 15 | subtaskMaxLength: number; 16 | } 17 | 18 | @ObjectType() 19 | export class Preferencies { 20 | @Field(() => String) 21 | language: string; 22 | 23 | @Field(() => String) 24 | theme: string; 25 | } 26 | 27 | @ObjectType() 28 | export class ClientConfig { 29 | @Field(() => Validation) 30 | validation: Validation; 31 | 32 | @Field(() => Preferencies) 33 | preferencies: Preferencies; 34 | } 35 | -------------------------------------------------------------------------------- /backend/src/common/defaults.ts: -------------------------------------------------------------------------------- 1 | import { Validation, Preferencies } from '../clientConfig/clientConfig.type'; 2 | 3 | export class Defaults { 4 | 5 | static taskSubjectMaxLength: number = 20; 6 | static commentMaxLength: number = 120; 7 | static taskIssueMaxLength: number = 120; 8 | static subtaskMaxLength: number = 50; 9 | static language: string = 'cs_CZ'; 10 | static theme: string = 'light'; 11 | 12 | static GetValidation(): Validation { 13 | return { 14 | taskSubjectMaxLength: this.taskSubjectMaxLength, 15 | commentMaxLength: this.commentMaxLength, 16 | taskIssueMaxLength: this.taskIssueMaxLength, 17 | subtaskMaxLength: this.subtaskMaxLength, 18 | }; 19 | } 20 | 21 | static GetPreferences(): Preferencies { 22 | return { 23 | language: this.language, 24 | theme: this.theme, 25 | }; 26 | } 27 | } -------------------------------------------------------------------------------- /backend/src/common/scalars/date.scalar.ts: -------------------------------------------------------------------------------- 1 | import { Scalar } from '@nestjs/graphql'; 2 | import { Kind } from 'graphql'; 3 | 4 | @Scalar('Date') 5 | export class DateScalar { 6 | description = 'Date custom scalar type'; 7 | 8 | parseValue(value: any) { 9 | return new Date(value); // value from the client 10 | } 11 | 12 | serialize(value: any) { 13 | return value.getTime(); // value sent to the client 14 | } 15 | 16 | parseLiteral(ast: any) { 17 | if (ast.kind === Kind.INT) { 18 | return parseInt(ast.value, 10); // ast value is always in string format 19 | } 20 | return null; 21 | } 22 | } -------------------------------------------------------------------------------- /backend/src/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from './config.service'; 3 | 4 | @Module({ 5 | providers: [ 6 | { 7 | provide: ConfigService, 8 | useValue: new ConfigService(`${process.env.NODE_ENV || 'remote'}.env`), 9 | }, 10 | ], 11 | exports: [ConfigService], 12 | }) 13 | export class ConfigModule {} -------------------------------------------------------------------------------- /backend/src/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as dotenv from 'dotenv'; 3 | import { Injectable } from '@nestjs/common'; 4 | 5 | @Injectable() 6 | export class ConfigService { 7 | private readonly envConfig: { [key: string]: string }; 8 | 9 | constructor(filePath: string) { 10 | this.envConfig = dotenv.parse(fs.readFileSync(filePath)); 11 | } 12 | 13 | get(key: string): string { 14 | return this.envConfig[key]; 15 | } 16 | 17 | getConfig(): any { 18 | return { 19 | host: this.get('TYPEORM_HOST'), 20 | database: this.get('TYPEORM_DATABASE'), 21 | username: this.get('TYPEORM_USERNAME'), 22 | password: this.get('TYPEORM_PASSWORD'), 23 | port: this.get('TYPEORM_PORT'), 24 | entities: [this.get('TYPEORM_ENTITIES')], 25 | }; 26 | } 27 | } -------------------------------------------------------------------------------- /backend/src/gqlContext.ts: -------------------------------------------------------------------------------- 1 | import { User as UserEntity } from 'auth/user.entity'; 2 | import { ContextFunction } from 'apollo-server-core'; 3 | 4 | export interface GqlContext { 5 | req: any; 6 | } 7 | 8 | export const gqlContextFunction: ContextFunction = async ({req}) => { 9 | return { req }; 10 | }; -------------------------------------------------------------------------------- /backend/src/localization/localization.controller.ts: -------------------------------------------------------------------------------- 1 | import { LocalizationService } from './localization.service'; 2 | import { Controller, Get, Param, Header } from '@nestjs/common'; 3 | 4 | @Controller('localization') 5 | export class LocalizationController { 6 | constructor(private readonly localizationService: LocalizationService) {} 7 | 8 | @Get(':lang/:ns') 9 | @Header('Access-Control-Allow-Origin', '*') 10 | root(@Param('lang') lang: string, @Param('ns') ns: string): {[s: string]: string} { 11 | return this.localizationService.getLocalization(lang, ns); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/localization/localization.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { LocalizationController } from './localization.controller'; 3 | import { LocalizationService } from './localization.service'; 4 | 5 | @Module({ 6 | controllers: [LocalizationController], 7 | providers: [ 8 | LocalizationService, 9 | ], 10 | }) 11 | export class LocalizationModule { } 12 | -------------------------------------------------------------------------------- /backend/src/localization/localization.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class LocalizationService { 5 | getLocalization(lang: string, ns: string) { 6 | const path = require('path'); 7 | 8 | if (!lang) { 9 | lang = 'cs'; 10 | } 11 | if (!ns) { 12 | ns = 'translations'; 13 | } 14 | if (!(/^[A-Za-z\-]+$/m.test(lang))) { 15 | throw new HttpException('Bad lang string', HttpStatus.BAD_REQUEST); 16 | } 17 | if (!(/^[A-Za-z\-]+$/m.test(ns))) { 18 | throw new HttpException('Bad namespace string', HttpStatus.BAD_REQUEST); 19 | } 20 | let localization: any; 21 | try { 22 | localization = require(path.resolve('./src/localization/localizations', lang, `${ns}.json`)); 23 | } catch (error) { 24 | localization = require('./localizations/cs/translations.json'); 25 | } 26 | return localization; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/main.hmr.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | declare const module: any; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | await app.listen(3000); 9 | 10 | if (module.hot) { 11 | module.hot.accept(); 12 | module.hot.dispose(() => app.close()); 13 | } 14 | } 15 | bootstrap(); 16 | -------------------------------------------------------------------------------- /backend/src/main.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | // app.setGlobalPrefix('api'); 8 | app.enableCors(); 9 | await app.listen(process.env.PORT || 3000); 10 | } 11 | bootstrap(); 12 | -------------------------------------------------------------------------------- /backend/src/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar Date 2 | 3 | schema { 4 | query: Query 5 | mutation: Mutation 6 | } -------------------------------------------------------------------------------- /backend/src/task/comment/comment.entity.ts: -------------------------------------------------------------------------------- 1 | import { Task } from '../task.entity'; 2 | import { CreateDateColumn, ManyToOne, Column, PrimaryGeneratedColumn, Entity, UpdateDateColumn } from 'typeorm'; 3 | import { User } from '../../auth/user.entity'; 4 | 5 | @Entity() 6 | export class Comment { 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | @ManyToOne(type => User, user => user.createdComments, { eager: true, onDelete: 'CASCADE' }) 11 | author: User; 12 | 13 | @Column({ nullable: false }) 14 | message: string; 15 | 16 | @ManyToOne(type => Task, task => task.comments, { onDelete: 'CASCADE' }) 17 | task: Task; 18 | 19 | @UpdateDateColumn() 20 | updated_at: Date; 21 | 22 | @CreateDateColumn() 23 | created_at: Date; 24 | } -------------------------------------------------------------------------------- /backend/src/task/comment/comment.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { AuthModule } from '../../auth/auth.module'; 4 | import { Task } from '../task.entity'; 5 | import { Comment } from './comment.entity'; 6 | import { CommentService } from './comment.service'; 7 | import { CommentResolvers } from './comment.resolvers'; 8 | import { User } from '../../auth/user.entity'; 9 | 10 | @Module({ 11 | imports: [ 12 | AuthModule, 13 | TypeOrmModule.forFeature([Task, User, Comment]), 14 | ], 15 | providers: [ 16 | CommentService, 17 | CommentResolvers, 18 | ], 19 | }) 20 | export class CommentModule { } -------------------------------------------------------------------------------- /backend/src/task/comment/comment.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards, HttpStatus, HttpException } from '@nestjs/common'; 2 | import { Resolver, Mutation, Args } from '@nestjs/graphql'; 3 | import { GqlAuthGuard } from '../../auth/gqlAuth.guard'; 4 | import { GqlRoleGuard } from '../../auth/gqlRole.guard'; 5 | import { User } from '../../auth/user.param.decorator'; 6 | import { UserRole } from '../../auth/userRole.enum'; 7 | import { CommentService } from './comment.service'; 8 | import { Comment } from './comment.entity'; 9 | import { User as UserEntity } from '../../auth/user.entity'; 10 | 11 | @UseGuards(GqlAuthGuard) 12 | @Resolver('Comment') 13 | export class CommentResolvers { 14 | constructor(private readonly commentService: CommentService) { } 15 | 16 | @UseGuards(GqlAuthGuard) 17 | @Mutation('addComment') 18 | async addSubTask( 19 | @Args('taskId') 20 | taskId: number, 21 | @Args('message') 22 | message: string, 23 | @User() 24 | currentUser: UserEntity, 25 | ): Promise { 26 | if (!taskId) { 27 | throw new HttpException('taskId', HttpStatus.BAD_REQUEST); 28 | } 29 | return await this.commentService.addComment(message, currentUser.id, taskId); 30 | } 31 | 32 | @UseGuards(GqlAuthGuard, new GqlRoleGuard(UserRole.ADMIN)) 33 | @Mutation('changeComment') 34 | async changeSubTask( 35 | @Args('commentId') 36 | commentId: number, 37 | @Args('message') 38 | message?: string, 39 | ): Promise { 40 | if (!commentId) { 41 | throw new HttpException('commentId', HttpStatus.BAD_REQUEST); 42 | } 43 | return await this.commentService.changeComment(commentId, message); 44 | } 45 | 46 | @UseGuards(GqlAuthGuard, new GqlRoleGuard(UserRole.ADMIN)) 47 | @Mutation('deleteComment') 48 | async deleteSubTask( 49 | @Args('subTaskId') 50 | commentId: number, 51 | ): Promise { 52 | if (!commentId) { 53 | throw new HttpException('commentId', HttpStatus.BAD_REQUEST); 54 | } 55 | return await this.commentService.deleteComment(commentId); 56 | } 57 | } -------------------------------------------------------------------------------- /backend/src/task/subtask/subtask.entity.ts: -------------------------------------------------------------------------------- 1 | import { Task } from '../task.entity'; 2 | import { CreateDateColumn, ManyToOne, Column, PrimaryGeneratedColumn, Entity } from 'typeorm'; 3 | 4 | @Entity() 5 | export class SubTask { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column({ nullable: false }) 10 | message: string; 11 | 12 | @Column({ nullable: false, default: false }) 13 | completed: boolean; 14 | 15 | @Column({ nullable: true }) 16 | completed_at?: Date; 17 | 18 | @ManyToOne(type => Task, task => task.subtasks, { onDelete: 'CASCADE' }) 19 | task: Task; 20 | 21 | @CreateDateColumn() 22 | created_at: Date; 23 | } -------------------------------------------------------------------------------- /backend/src/task/subtask/subtask.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { AuthModule } from '../../auth/auth.module'; 4 | import { Task } from '../task.entity'; 5 | import { SubTask } from '../subtask/subtask.entity'; 6 | import { SubTaskService } from '../subtask/subtask.service'; 7 | import { SubTaskResolvers } from '../subtask/subtask.resolvers'; 8 | 9 | @Module({ 10 | imports: [ 11 | AuthModule, 12 | TypeOrmModule.forFeature([Task, SubTask]), 13 | ], 14 | providers: [ 15 | SubTaskService, 16 | SubTaskResolvers, 17 | ], 18 | }) 19 | export class SubtaskModule { } -------------------------------------------------------------------------------- /backend/src/task/subtask/subtask.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Mutation, Args } from '@nestjs/graphql'; 2 | import { UseGuards, HttpStatus, HttpException } from '@nestjs/common'; 3 | import { GqlAuthGuard } from '../../auth/gqlAuth.guard'; 4 | import { SubTaskService } from './subtask.service'; 5 | import { SubTask } from './subtask.entity'; 6 | import { GqlRoleGuard } from '../../auth/gqlRole.guard'; 7 | import { UserRole } from '../../auth/userRole.enum'; 8 | 9 | @UseGuards(GqlAuthGuard) 10 | @Resolver('SubTask') 11 | export class SubTaskResolvers { 12 | constructor(private readonly subService: SubTaskService) { } 13 | 14 | @UseGuards(GqlAuthGuard, new GqlRoleGuard(UserRole.ADMIN)) 15 | @Mutation('addSubTask') 16 | async addSubTask( 17 | @Args('taskId') 18 | taskId: number, 19 | @Args('message') 20 | message: string, 21 | ): Promise { 22 | if (!taskId) { 23 | throw new HttpException('taskId', HttpStatus.BAD_REQUEST); 24 | } 25 | return await this.subService.addSubTask(message, taskId); 26 | } 27 | 28 | @UseGuards(GqlAuthGuard, new GqlRoleGuard(UserRole.ADMIN)) 29 | @Mutation('changeSubTask') 30 | async changeSubTask( 31 | @Args('subTaskId') 32 | subTaskId: number, 33 | @Args('message') 34 | message?: string, 35 | @Args('completed') 36 | completed?: boolean, 37 | ): Promise { 38 | if (!subTaskId) { 39 | throw new HttpException('subTaskId', HttpStatus.BAD_REQUEST); 40 | } 41 | return await this.subService.changeSubTaskState(subTaskId, message, completed); 42 | } 43 | 44 | @UseGuards(GqlAuthGuard, new GqlRoleGuard(UserRole.ADMIN)) 45 | @Mutation('deleteSubTask') 46 | async deleteSubTask( 47 | @Args('subTaskId') 48 | subTaskId: number, 49 | ): Promise { 50 | if (!subTaskId) { 51 | throw new HttpException('subTaskId', HttpStatus.BAD_REQUEST); 52 | } 53 | return await this.subService.deleteTask(subTaskId); 54 | } 55 | } -------------------------------------------------------------------------------- /backend/src/task/task.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany, JoinColumn } from 'typeorm'; 2 | import { User } from '../auth/user.entity'; 3 | import { TaskState } from './taskState.enum'; 4 | import { SubTask } from './subtask/subtask.entity'; 5 | import { Comment } from './comment/comment.entity'; 6 | 7 | @Entity() 8 | export class Task { 9 | @PrimaryGeneratedColumn() 10 | id: number; 11 | 12 | @Column('longtext') 13 | issue: string; 14 | 15 | @Column() 16 | subject: string; 17 | 18 | authorId: number; 19 | 20 | @Column({ default: true }) 21 | enabled: boolean; 22 | 23 | @ManyToOne(type => User, user => user.createdTasks, { eager: true, onDelete: 'CASCADE' }) 24 | @JoinColumn({ name: 'authorId' }) 25 | author: User; 26 | 27 | @ManyToOne(type => User, user => user.assignedTasks, { nullable: true, eager: true, onDelete: 'CASCADE' }) 28 | assignee?: User; 29 | 30 | @CreateDateColumn() 31 | created_at: Date; 32 | 33 | @UpdateDateColumn() 34 | updated_at: Date; 35 | 36 | @Column({ default: TaskState.UNRESOLVED }) 37 | state: TaskState; 38 | 39 | @OneToMany(type => SubTask, subtask => subtask.task, { eager: true }) 40 | subtasks: SubTask[]; 41 | 42 | @OneToMany(type => Comment, x => x.task, { eager: true }) 43 | comments: Comment[]; 44 | } 45 | -------------------------------------------------------------------------------- /backend/src/task/task.graphql: -------------------------------------------------------------------------------- 1 | enum State { 2 | UNRESOLVED 3 | SOLVING 4 | SOLVED 5 | RETURNED 6 | } 7 | 8 | type Comment { 9 | id: ID! 10 | author: User! 11 | created_at: Date! 12 | updated_at: Date! 13 | message: String! 14 | } 15 | 16 | type SubTask{ 17 | id: ID! 18 | message: String! 19 | completed: Boolean 20 | completed_at: Date 21 | task: Task! 22 | created_at: Date! 23 | } 24 | 25 | type Task { 26 | id: ID! 27 | subject: String! 28 | issue: String! 29 | author: User! 30 | assignee: User 31 | enabled: Boolean! 32 | created_at: Date! 33 | updated_at: Date 34 | state: State! 35 | subtasks: [SubTask] 36 | comments: [Comment] 37 | } 38 | 39 | type Query{ 40 | tasks(onlyEnabled: Boolean, lastUpdate: Date): [Task] 41 | task(id: ID): Task 42 | } 43 | 44 | type Mutation { 45 | addTask(subject: String!, issue: String!, assigneeId: ID): Task 46 | changeTaskState(taskId: ID, comment: String, state: State, assigneeId: ID, enabled: Boolean): Task 47 | deleteTask(taskId: ID): Boolean 48 | addSubTask(taskId: ID!, message: String!): SubTask 49 | changeSubTask(subTaskId: ID!, message: String, completed: Boolean): SubTask 50 | deleteSubTask(subTaskId: ID!): Boolean 51 | addComment(taskId: ID!, message: String!): Comment 52 | changeComment(commentId: ID!, message: String): Comment 53 | deleteComment(subTaskId: ID!): Boolean 54 | } 55 | 56 | -------------------------------------------------------------------------------- /backend/src/task/task.module.ts: -------------------------------------------------------------------------------- 1 | import { AuthModule } from '../auth/auth.module'; 2 | import { Module } from '@nestjs/common'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { TaskService } from './task.service'; 5 | import { Task } from './task.entity'; 6 | import { TaskResolvers } from './task.resolvers'; 7 | import { User } from '../auth/user.entity'; 8 | import { Comment } from './comment/comment.entity'; 9 | import { SubtaskModule } from './subtask/subtask.module'; 10 | import { CommentModule } from './comment/comment.module'; 11 | 12 | @Module({ 13 | imports: [ 14 | AuthModule, 15 | SubtaskModule, 16 | CommentModule, 17 | TypeOrmModule.forFeature([User, Task, Comment]), 18 | ], 19 | providers: [ 20 | TaskService, 21 | TaskResolvers, 22 | ], 23 | }) 24 | export class TaskModule { } -------------------------------------------------------------------------------- /backend/src/task/taskState.enum.ts: -------------------------------------------------------------------------------- 1 | export enum TaskState { 2 | UNRESOLVED = 'UNRESOLVED', 3 | SOLVING = 'SOLVING', 4 | SOLVED = 'SOLVED', 5 | RETURNED = 'RETURNED', 6 | } -------------------------------------------------------------------------------- /backend/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /backend/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": false, 5 | "noImplicitAny": false, 6 | "strictNullChecks": true, 7 | "removeComments": true, 8 | "noLib": false, 9 | "allowSyntheticDefaultImports": true, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es6", 13 | "sourceMap": true, 14 | "outDir": "./dist", 15 | "baseUrl": "./src", 16 | "lib": [ 17 | "es2016", 18 | "dom", 19 | "esnext.asynciterable" 20 | ] 21 | }, 22 | "includes": [ 23 | "src/**/*" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "**/*.spec.ts" 28 | ] 29 | } -------------------------------------------------------------------------------- /backend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /backend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": { 7 | "no-unused-expression": true 8 | }, 9 | "rules": { 10 | "eofline": false, 11 | "quotemark": [ 12 | true, 13 | "single" 14 | ], 15 | "indent": false, 16 | "member-access": [ 17 | false 18 | ], 19 | "ordered-imports": [ 20 | false 21 | ], 22 | "max-line-length": [ 23 | true, 24 | 150 25 | ], 26 | "member-ordering": [ 27 | false 28 | ], 29 | "curly": false, 30 | "interface-name": [ 31 | false 32 | ], 33 | "array-type": [ 34 | false 35 | ], 36 | "no-empty-interface": false, 37 | "no-empty": false, 38 | "arrow-parens": false, 39 | "object-literal-sort-keys": false, 40 | "no-unused-expression": false, 41 | "max-classes-per-file": [ 42 | false 43 | ], 44 | "variable-name": [ 45 | false 46 | ], 47 | "one-line": [ 48 | false 49 | ], 50 | "one-variable-per-declaration": [ 51 | false 52 | ] 53 | }, 54 | "rulesDirectory": [], 55 | "linterOptions": { 56 | "exclude": [ 57 | "src/gql.ts" 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /backend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = { 6 | entry: ['webpack/hot/poll?1000', './src/main.hmr.ts'], 7 | watch: true, 8 | target: 'node', 9 | externals: [ 10 | nodeExternals({ 11 | whitelist: ['webpack/hot/poll?1000'], 12 | }), 13 | ], 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | use: 'ts-loader', 19 | exclude: /node_modules/, 20 | }, 21 | ], 22 | }, 23 | mode: "development", 24 | resolve: { 25 | extensions: ['.tsx', '.ts', '.js'], 26 | }, 27 | plugins: [ 28 | new webpack.HotModuleReplacementPlugin(), 29 | ], 30 | output: { 31 | path: path.join(__dirname, 'dist'), 32 | filename: 'server.js', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /dckr/env/.gitignore: -------------------------------------------------------------------------------- 1 | mysql.env 2 | node.env 3 | redis.env -------------------------------------------------------------------------------- /dckr/env/mysql-example.env: -------------------------------------------------------------------------------- 1 | MYSQL_ROOT_PASSWORD=password -------------------------------------------------------------------------------- /dckr/mysql/docker-entrypoint-initdb.d/.gitignore: -------------------------------------------------------------------------------- 1 | *.sql -------------------------------------------------------------------------------- /dckr/mysql/docker-entrypoint-initdb.d/createdb.sql.example: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS `helpdesk` COLLATE 'utf8_general_ci' ; 2 | GRANT ALL ON `helpdesk`.* TO 'root'@'%' ; 3 | 4 | FLUSH PRIVILEGES ; -------------------------------------------------------------------------------- /dckr/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | COPY default.conf /etc/nginx/conf.d/default.conf -------------------------------------------------------------------------------- /dckr/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | location ~ ^/(graphql|api)/ { 6 | proxy_pass http://backend:3000; 7 | } 8 | 9 | location / { 10 | proxy_pass http://frontend:3000; 11 | } 12 | } -------------------------------------------------------------------------------- /dckr/scripts/wait-for-mysql.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | node_modules/.bin/wait-on tcp:mysql:3306 && yarn start:dev -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | nginx: 4 | image: nginx 5 | networks: 6 | helpdesk_network: 7 | aliases: 8 | - nginx 9 | ports: 10 | - '80:80' 11 | - '443:443' 12 | volumes: 13 | - '/home/spravce/default.conf:/etc/nginx/conf.d/default.conf' 14 | depends_on: 15 | - frontend 16 | - backend 17 | backend: 18 | image: alpha.deltasystems.cz:5000/helpdesk-backend 19 | environment: 20 | TYPEORM_CONNECTION: mysql 21 | TYPEORM_HOST: mysql 22 | TYPEORM_USERNAME: ${DB_USER} 23 | TYPEORM_PASSWORD: ${DB_PASSWORD} 24 | TYPEORM_DATABASE: ${DB_DATABASE} 25 | TYPEORM_PORT: 3306 26 | TYPEORM_ENTITIES: src/**/**.entity.ts 27 | networks: 28 | helpdesk_network: 29 | aliases: 30 | - backend 31 | networks: 32 | helpdesk_network: 33 | driver: 'bridge' 34 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | backend: 4 | image: kkarczmarczyk/node-yarn 5 | env_file: 6 | - backend/local.env 7 | networks: 8 | backend: 9 | aliases: 10 | - api 11 | expose: 12 | - '3000' 13 | ports: 14 | - '${NODE_PORT}:3000' 15 | volumes: 16 | - './backend:/backend' 17 | - './dckr:/dckr' 18 | working_dir: '/backend' 19 | depends_on: 20 | - mysql 21 | command: ['/dckr/scripts/wait-for-mysql.sh'] 22 | mysql: 23 | image: mysql:5.7 24 | env_file: 25 | - dckr/env/mysql.env 26 | networks: 27 | backend: 28 | aliases: 29 | - mysql 30 | expose: 31 | - '3306' 32 | ports: 33 | - '${MYSQL_PORT}:3306' 34 | volumes: 35 | - './dckr/mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d' 36 | - 'mysql-data:/var/lib/mysql' 37 | networks: 38 | backend: 39 | driver: 'bridge' 40 | volumes: 41 | mysql-data: 42 | driver: 'local' -------------------------------------------------------------------------------- /fe/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | -------------------------------------------------------------------------------- /fe/Procfile: -------------------------------------------------------------------------------- 1 | web: yarn start -------------------------------------------------------------------------------- /fe/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Installation 3 | 4 | ```bash 5 | $ yarn install 6 | 7 | ``` 8 | 9 | ## Running the app 10 | 11 | ```bash 12 | # Remote (hosted) backend 13 | $ yarn dev 14 | 15 | ``` -------------------------------------------------------------------------------- /fe/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "https://delta-helpdesk.herokuapp.com/graphql" 3 | generates: 4 | src/graphql/types.tsx: 5 | plugins: 6 | - "typescript" 7 | - "typescript-operations" 8 | - "typescript-react-apollo" 9 | config: 10 | withHooks: true 11 | -------------------------------------------------------------------------------- /fe/components/AdminSelect/AdminSelect.tsx: -------------------------------------------------------------------------------- 1 | import { FormControl, InputLabel, Select, MenuItem, FormHelperText } from "@material-ui/core"; 2 | import { useState, FunctionComponent, ChangeEvent } from "react"; 3 | import { getAdmins, getAdmins_admins } from "../../src/graphql/types/getAdmins"; 4 | import { getAdminsQuery } from "../../src/graphql/queries"; 5 | import { useQuery } from "react-apollo"; 6 | import Skeleton from "@material-ui/lab/Skeleton"; 7 | 8 | interface IProps { 9 | helperText?: string; 10 | title?: string; 11 | onSelected?: (admin: getAdmins_admins) => void; 12 | selectOneText?: string; 13 | } 14 | 15 | const AdminSelect: FunctionComponent = ({ 16 | helperText = "", 17 | onSelected = null, 18 | title = "Select admin", 19 | selectOneText = "Select one", 20 | }) => { 21 | 22 | const [selected, setSelected] = useState(""); 23 | 24 | const { loading, data, error } = useQuery(getAdminsQuery); 25 | 26 | if (loading) { 27 | return <> 28 | 29 | ; 30 | } 31 | 32 | if (error) { 33 | return <> 34 | {error} 35 | ; 36 | } 37 | 38 | const { admins } = data; 39 | 40 | const handleChange = (event: ChangeEvent<{ value: string }>) => { 41 | 42 | const selectedValue = event.target.value; 43 | setSelected(selectedValue); 44 | 45 | const selectedAdmin = admins.find(({ id }) => id === selectedValue); 46 | 47 | if (onSelected) { 48 | onSelected(selectedAdmin); 49 | } 50 | }; 51 | 52 | return <> 53 | 54 | {title} 55 | 72 | {helperText} 73 | 74 | ; 75 | }; 76 | 77 | export default AdminSelect; 78 | -------------------------------------------------------------------------------- /fe/components/Administration/AdminContainer/AdminContainer.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, ChangeEvent, useState, useContext } from "react"; 2 | import Tabs from "@material-ui/core/Tabs"; 3 | import Tab from "@material-ui/core/Tab"; 4 | import { Grid } from "@material-ui/core"; 5 | import Router from "next/router"; 6 | import GroupIcon from "@material-ui/icons/Group"; 7 | import AllInboxIcon from "@material-ui/icons/AllInbox"; 8 | import { ReactAuthContext, userIsAdmin } from "../../../src/graphql/auth"; 9 | import customRoutes from "../../../src/Routes"; 10 | import { useTranslation } from "react-i18next"; 11 | import locKeys from "../../../src/Locales/LocalizationKeys"; 12 | 13 | interface IProps { 14 | activeTab?: number; 15 | } 16 | 17 | const AdminContaier: FunctionComponent = ({ activeTab = 0, children }) => { 18 | 19 | const [value, setValue] = useState(activeTab); 20 | const { user } = useContext(ReactAuthContext); 21 | 22 | const handleChange = (event: ChangeEvent<{}>, newValue: number) => { 23 | setValue(newValue); 24 | 25 | if (value === newValue) { 26 | return; 27 | } 28 | 29 | setTimeout(() => { 30 | switch (newValue) { 31 | case 0: 32 | Router.push(customRoutes.administration); 33 | break; 34 | case 1: 35 | Router.push(customRoutes.userList); 36 | break; 37 | } 38 | }, 250); 39 | }; 40 | 41 | const { t } = useTranslation(); 42 | 43 | return <> 44 | 45 | 46 | 54 | } /> 55 | {(userIsAdmin(user)) ? 56 | } /> 57 | : <> 58 | } 59 | 60 | 61 | 62 | {children} 63 | 64 | 65 | 66 | ; 67 | }; 68 | 69 | export default AdminContaier; 70 | -------------------------------------------------------------------------------- /fe/components/Administration/UserList/DeleteUser.tsx: -------------------------------------------------------------------------------- 1 | import { useState, FunctionComponent } from "react"; 2 | import { IconButton, Dialog, DialogTitle, DialogContent, Grid, TextField, DialogActions, Button } from "@material-ui/core"; 3 | import DeleteOutlineOutlinedIcon from "@material-ui/icons/DeleteOutlineOutlined"; 4 | import { getUsers_users } from "../../../src/graphql/types/getUsers"; 5 | import { useMutation } from "react-apollo"; 6 | import { removeUser, removeUserVariables } from "../../../src/graphql/types/removeUser"; 7 | import { removeUserMutation } from "../../../src/graphql/mutations"; 8 | import { useSnackbar } from "notistack"; 9 | import { useTranslation } from "react-i18next"; 10 | import locKeys from "../../../src/Locales/LocalizationKeys"; 11 | 12 | interface IProps { 13 | user: getUsers_users; 14 | onRemoved?: () => void; 15 | } 16 | 17 | const RemoveUser: FunctionComponent = ({ user, onRemoved = null }) => { 18 | const [open, setOpen] = useState(false); 19 | 20 | const [remUser] = useMutation(removeUserMutation); 21 | 22 | const { enqueueSnackbar } = useSnackbar(); 23 | 24 | const { email, fullName } = user; 25 | 26 | const handleClose = () => { 27 | setOpen(false); 28 | }; 29 | 30 | const handleOpen = () => { 31 | setOpen(true); 32 | }; 33 | const handleChanges = async () => { 34 | 35 | const variables: removeUserVariables = { 36 | email, 37 | }; 38 | 39 | const res = await remUser({ variables }); 40 | 41 | const { errors, data } = res; 42 | 43 | if (errors) { 44 | console.log(errors); 45 | enqueueSnackbar(errors[0].message, { variant: "error" }); 46 | } 47 | 48 | if (data.removeUser) { 49 | enqueueSnackbar("User removed"); 50 | if (onRemoved) { 51 | onRemoved(); 52 | } 53 | } 54 | 55 | handleClose(); 56 | }; 57 | 58 | const { t } = useTranslation(); 59 | 60 | return <> 61 | 62 | 63 | 64 | 65 | {t(locKeys.users.deleteUser)} 66 | 67 | Opravdu si přejete smazat uživatele {fullName}? 68 | 69 | 70 | 73 | 76 | 77 | 78 | ; 79 | }; 80 | 81 | export default RemoveUser; 82 | -------------------------------------------------------------------------------- /fe/components/Administration/UserList/UserList.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useContext } from "react"; 2 | import { ReactAuthContext, userIsInRole } from "../../../src/graphql/auth"; 3 | import { useQuery } from "react-apollo"; 4 | import UserComponent from "./User"; 5 | import { getUsers } from "../../../src/graphql/types/getUsers"; 6 | import { getUsersQuery } from "../../../src/graphql/queries"; 7 | import { UserRole } from "../../../src/graphql/graphql-global-types"; 8 | 9 | const UserList: FunctionComponent = () => { 10 | const { user: logged } = useContext(ReactAuthContext); 11 | 12 | const allowDelete = userIsInRole(logged, UserRole.SUPERADMIN); 13 | 14 | const { loading, error, data, refetch } = useQuery(getUsersQuery); 15 | 16 | const reload = () => { 17 | refetch(); 18 | }; 19 | 20 | if (loading) { 21 | return <> 22 | { 23 | Array.from(new Array(10)).map((user, index) => 24 | ) 25 | } 26 | ; 27 | } 28 | 29 | if (error) { 30 | return <> 31 | Error! {error.message} 32 | ; 33 | } 34 | 35 | const { users } = data; 36 | return <> 37 | { 38 | users.map((user) => 39 | ) 40 | } 41 | ; 42 | }; 43 | 44 | export default UserList; 45 | -------------------------------------------------------------------------------- /fe/components/Administration/UserList/UserListContainer.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useContext } from "react"; 2 | import { Table, TableHead, TableRow, TableCell, TableBody, Paper, Fade } from "@material-ui/core"; 3 | import { ReactAuthContext, checkUserRole } from "../../../src/graphql/auth"; 4 | import UserList from "./UserList"; 5 | import { UserRole } from "../../../src/graphql/graphql-global-types"; 6 | import { Theme, makeStyles, createStyles } from "@material-ui/core"; 7 | import getTheme from "../../Themes/MainTheme"; 8 | import locKeys from "../../../src/Locales/LocalizationKeys"; 9 | import { useTranslation } from "react-i18next"; 10 | 11 | const useStyles = makeStyles((theme: Theme) => 12 | createStyles({ 13 | userListContainer: { 14 | padding: "1rem", 15 | }, 16 | })); 17 | 18 | const UserListContainer: FunctionComponent = () => { 19 | 20 | const classes = useStyles(getTheme()); 21 | 22 | const { user: logged } = useContext(ReactAuthContext); 23 | 24 | const isAdmin = !!logged && checkUserRole(logged.role, UserRole.ADMIN); 25 | 26 | const { t } = useTranslation(); 27 | 28 | return <> 29 | 30 | 31 | 32 | 33 | 34 | # 35 | {t(locKeys.login.name)} 36 | {t(locKeys.login.email)} 37 | {t(locKeys.common.role)} 38 | {t(locKeys.common.registration)} 39 | {t(locKeys.common.update)} 40 | {isAdmin && {t(locKeys.common.actions)} } 41 | 42 | 43 | 44 | 45 | 46 |
47 |
48 |
49 | ; 50 | }; 51 | 52 | export default UserListContainer; 53 | -------------------------------------------------------------------------------- /fe/components/Background/Background.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import { makeStyles } from "@material-ui/styles"; 3 | import { useMediaQuery } from "@material-ui/core"; 4 | import getTheme from "../Themes/MainTheme"; 5 | 6 | const Background: FunctionComponent = (props) => { 7 | const useStyles = makeStyles(() => ({ 8 | background: { 9 | position: "fixed", 10 | top: "50%", 11 | left: "50%", 12 | transform: "translate(-50%,-50%)", 13 | width: "100%", 14 | zIndex: -5, 15 | }, 16 | 17 | backgroundResponsive: { 18 | position: "fixed", 19 | top: "50%", 20 | left: "50%", 21 | transform: "translate(-50%,-50%)", 22 | zIndex: -5, 23 | }, 24 | }), 25 | ); 26 | 27 | const classes = useStyles(props); 28 | 29 | const theme = getTheme(); 30 | const sm = useMediaQuery(theme.breakpoints.up("md")); 31 | 32 | return <> 33 | 34 | ; 35 | }; 36 | export default Background; 37 | -------------------------------------------------------------------------------- /fe/components/DarkModeSelect/DarkModeSelect.tsx: -------------------------------------------------------------------------------- 1 | import Select from "@material-ui/core/Select"; 2 | import MenuItem from "@material-ui/core/MenuItem"; 3 | import { useState, useEffect, FunctionComponent, ChangeEvent } from "react"; 4 | import Router from "next/router"; 5 | import InputLabel from "@material-ui/core/InputLabel"; 6 | import FormControl from "@material-ui/core/FormControl"; 7 | import { makeStyles, Theme, createStyles } from "@material-ui/core"; 8 | import mainTheme from "../Themes/MainTheme"; 9 | import CookieHelper from "../../utils/cookieHelper"; 10 | import { editUser } from "../../src/graphql/mutations"; 11 | import getTheme from "../Themes/MainTheme"; 12 | import { useMutation } from "react-apollo"; 13 | import locKeys from "../../src/Locales/LocalizationKeys"; 14 | import { useTranslation } from "react-i18next"; 15 | import { EditUser } from "../../src/graphql/types/EditUser"; 16 | 17 | const useStyles = makeStyles((theme: Theme) => 18 | createStyles({ 19 | formControl: { 20 | margin: theme.spacing(1), 21 | minWidth: 120, 22 | }, 23 | selectEmpty: { 24 | marginTop: theme.spacing(2), 25 | }, 26 | }), 27 | ); 28 | 29 | const DarkModeSelect: FunctionComponent = () => { 30 | const cookieHelper = new CookieHelper(); 31 | const [editTheme] = useMutation(editUser); 32 | 33 | const [theme, setTheme] = useState("dark"); 34 | 35 | const classes = useStyles(getTheme()); 36 | 37 | useEffect(() => { 38 | const currentTheme = cookieHelper.getThemeName(); 39 | setTheme(currentTheme); 40 | }, []); 41 | 42 | const handleChange = async (event: ChangeEvent<{ value: unknown }>) => { 43 | const value = event.target.value as string; 44 | const res = await editTheme({ 45 | variables: { 46 | theme: value, 47 | }, 48 | }); 49 | 50 | cookieHelper.setTheme(value); 51 | setTheme(value); 52 | 53 | setTimeout(() => { 54 | Router.reload(); 55 | }, 500); 56 | 57 | }; 58 | 59 | const { t } = useTranslation(); 60 | 61 | return <> 62 | 63 | {t(locKeys.common.themeChoose)} 64 | 72 | 73 | ; 74 | }; 75 | 76 | export default DarkModeSelect; 77 | -------------------------------------------------------------------------------- /fe/components/Dates/DateFormatter.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, FunctionComponent } from "react"; 2 | import moment from "moment"; 3 | 4 | interface IDateProps { 5 | date: Date; 6 | relative?: boolean; 7 | } 8 | 9 | export const getFormattedDate = (date: string, relative = false) => { 10 | return relative 11 | ? moment(date).calendar(null, { 12 | sameDay: "[Dnes]", 13 | lastDay: "[Včera]", 14 | lastWeek: "[Minulý týden]", 15 | sameElse: "DD.MM.YYYY", 16 | }) 17 | : moment(date).format("DD.MM.YYYY, HH:mm"); 18 | }; 19 | 20 | const DateFormatComponent: FunctionComponent = ({ date, relative }) => { 21 | 22 | const [dateMoment, setDateMoment] = useState(""); 23 | 24 | // TODO: use without useEffect 25 | 26 | useEffect(() => { 27 | const dateString: string = relative 28 | ? moment(date).calendar(null, { 29 | sameDay: "[Dnes]", 30 | lastDay: "[Včera]", 31 | lastWeek: "[Minulý] dddd", 32 | sameElse: "DD.MM.YYYY", 33 | }) 34 | : moment(date).format("DD.MM.YYYY, HH:mm"); 35 | setDateMoment(dateString); 36 | }); 37 | 38 | return ( 39 | {dateMoment} 40 | ); 41 | }; 42 | 43 | export default DateFormatComponent; 44 | -------------------------------------------------------------------------------- /fe/components/Dates/DatePicker.tsx: -------------------------------------------------------------------------------- 1 | import { withStyles, Theme, createStyles, WithStyles } from "@material-ui/core/styles"; 2 | import TextField from "@material-ui/core/TextField"; 3 | 4 | const styles = (theme: Theme) => createStyles({ 5 | container: { 6 | display: "flex", 7 | flexWrap: "wrap", 8 | }, 9 | textField: { 10 | // TODO: Fix 11 | // marginLeft: theme.spacing.unit, 12 | // marginRight: theme.spacing.unit, 13 | width: 200, 14 | }, 15 | }); 16 | 17 | interface IDatePickerProps extends WithStyles { 18 | id: string; 19 | label: string; 20 | defaultValue: string; 21 | disabled?: boolean; 22 | onChange: (date: string) => void; 23 | } 24 | 25 | function formatDate(date: string) { 26 | // Change the format if required 27 | // Substring crops the time part from ISO formatted date 28 | // e.g.: 2013-03-10T02:00:00Z => 2013-03-10 29 | return new Date(date).toISOString().substring(0, 10); 30 | } 31 | 32 | function DatePicker(props: IDatePickerProps) { 33 | 34 | const { id, label, defaultValue, classes, disabled = false, onChange } = props; 35 | 36 | return ( 37 | onChange(formatDate((e.target.value)))} 45 | /> 46 | 47 | ); 48 | } 49 | 50 | export default withStyles(styles)(DatePicker); 51 | -------------------------------------------------------------------------------- /fe/components/Errors/Kaomoji.tsx: -------------------------------------------------------------------------------- 1 | import { Fade } from "@material-ui/core"; 2 | import { useEffect, useState, FunctionComponent } from "react"; 3 | 4 | const Kaomoji: FunctionComponent = () => { 5 | 6 | const [kaomoji, setKaomoji] = useState(""); 7 | 8 | const faces: string[] = [ 9 | "(•_•)", 10 | "¯\\_(ツ)_/¯", 11 | "( ´・・)ノ", 12 | "(._.`)", 13 | "(×_×)", 14 | "(>。<)", 15 | "〣( ºΔº )〣", 16 | "\(º □ º l|l)/", 17 | "(・_・;)", 18 | "(・_・ヾ", 19 | "ヽ(°〇°)ノ", 20 | "(°ロ°)", 21 | "╰( ͡° ͜ʖ ͡° )つ", 22 | ]; 23 | 24 | useEffect(() => { 25 | const k = faces[Math.floor(Math.random() * faces.length)]; 26 | setKaomoji(k); 27 | }, []); 28 | 29 | return <> 30 | 31 |
32 |

33 | {kaomoji} 34 |

35 |
36 |
37 | ; 38 | }; 39 | 40 | export default Kaomoji; 41 | -------------------------------------------------------------------------------- /fe/components/Layouts/HeadComponent.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import Head from "next/head"; 3 | import getTheme from "../Themes/MainTheme"; 4 | 5 | const HeadComponent: FunctionComponent = ({ children }) => { 6 | 7 | const theme = getTheme(); 8 | 9 | const primaryColor = theme.palette.primary.main; 10 | 11 | return <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {children} 31 | ; 32 | }; 33 | 34 | export default HeadComponent; 35 | -------------------------------------------------------------------------------- /fe/components/Layouts/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import Head from "next/head"; 3 | import MainAppBar from "../MainAppBar/MainAppBar"; 4 | import { makeStyles, Theme, createStyles } from "@material-ui/core"; 5 | import { fade } from "@material-ui/core/styles"; 6 | import getTheme from "../Themes/MainTheme"; 7 | 8 | const useStyles = makeStyles((theme: Theme) => 9 | createStyles({ 10 | root: { 11 | display: "flex", 12 | }, 13 | toolbar: theme.mixins.toolbar, 14 | content: { 15 | flexGrow: 1, 16 | // backgroundColor: theme.palette.background.default, 17 | padding: theme.spacing(3), 18 | }, 19 | search: { 20 | "position": "relative", 21 | "borderRadius": theme.shape.borderRadius, 22 | "backgroundColor": fade(theme.palette.common.white, 0.15), 23 | "&:hover": { 24 | backgroundColor: fade(theme.palette.common.white, 0.25), 25 | }, 26 | "marginRight": theme.spacing(2), 27 | "marginLeft": 0, 28 | "width": "100%", 29 | [theme.breakpoints.up("sm")]: { 30 | marginLeft: theme.spacing(3), 31 | width: "auto", 32 | }, 33 | }, 34 | searchIcon: { 35 | width: theme.spacing(7), 36 | height: "100%", 37 | position: "absolute", 38 | pointerEvents: "none", 39 | display: "flex", 40 | alignItems: "center", 41 | justifyContent: "center", 42 | }, 43 | inputRoot: { 44 | color: "inherit", 45 | }, 46 | inputInput: { 47 | padding: theme.spacing(1, 1, 1, 7), 48 | transition: theme.transitions.create("width"), 49 | width: "100%", 50 | [theme.breakpoints.up("md")]: { 51 | width: 200, 52 | }, 53 | }, 54 | sectionDesktop: { 55 | display: "none", 56 | [theme.breakpoints.up("md")]: { 57 | display: "flex", 58 | }, 59 | }, 60 | grow: { 61 | flexGrow: 1, 62 | }, 63 | }), 64 | ); 65 | 66 | interface IProps { 67 | title?: string; 68 | } 69 | 70 | const Layout: FunctionComponent = ({ children, title = "Helpdesk" }) => { 71 | const classes = useStyles(getTheme()); 72 | 73 | return
74 | 75 | {title} - Helpdesk 76 | 77 |
78 | 79 |
80 |
81 |
82 | {children} 83 |
84 |
; 85 | }; 86 | 87 | export default Layout; 88 | -------------------------------------------------------------------------------- /fe/components/LinkComponents/LinkComponent.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import Link from "next/link"; 3 | import { Button, PropTypes } from "@material-ui/core"; 4 | 5 | interface IProps { 6 | href: string; 7 | isButton?: boolean; 8 | text: string; 9 | variant?: "text" | "outlined" | "contained"; 10 | color?: PropTypes.Color; 11 | } 12 | 13 | const LinkComponent: FunctionComponent = ({ href, isButton = false, text, variant = "contained", color = "primary" }) => { 14 | 15 | return <> 16 | 17 |
18 | {!isButton && {text}} 19 | {isButton && } 20 |
21 | 22 | ; 23 | }; 24 | 25 | export default LinkComponent; 26 | -------------------------------------------------------------------------------- /fe/components/Loading/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import { withStyles, Theme, createStyles, WithStyles } from "@material-ui/core/styles"; 3 | import CircularProgress from "@material-ui/core/CircularProgress"; 4 | import LinearProgress from "@material-ui/core/LinearProgress"; 5 | 6 | interface IProps { 7 | isLinear?: boolean; 8 | linearWidth?: string; 9 | } 10 | 11 | const Loading: FunctionComponent = ({ isLinear = false, linearWidth = "30rem" }) => { 12 | return ( 13 |
14 |
15 | { 16 | isLinear 17 | ? 18 | : 19 | } 20 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default Loading; 27 | -------------------------------------------------------------------------------- /fe/components/Login/MicrosoftButtonLogin.tsx: -------------------------------------------------------------------------------- 1 | import { SFC } from "react"; 2 | import Button from "@material-ui/core/Button"; 3 | import Icon from "@mdi/react"; 4 | import { mdiWindows } from "@mdi/js"; 5 | 6 | interface IMicrosoftButtonLoginProps { 7 | onClick: () => void; 8 | className?: string; 9 | } 10 | 11 | const MicrosoftButtonLogin: SFC = ({ onClick, className = null }) => { 12 | return ( 13 | 24 | ); 25 | }; 26 | 27 | export default MicrosoftButtonLogin; 28 | -------------------------------------------------------------------------------- /fe/components/Login/SocialButton.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import SocialLogin, { ISocialLoginProps } from "react-social-login"; 3 | import { Link } from "@material-ui/core"; 4 | 5 | class SocialButton extends Component { 6 | render() { 7 | return ( 8 | 9 | {this.props.children} 10 | 11 | ); 12 | } 13 | } 14 | 15 | export default SocialLogin(SocialButton); 16 | -------------------------------------------------------------------------------- /fe/components/Login/TipBar.tsx: -------------------------------------------------------------------------------- 1 | import { Fade, makeStyles, Theme, createStyles } from "@material-ui/core"; 2 | import { useEffect, useState, FunctionComponent } from "react"; 3 | import getTheme from "../Themes/MainTheme"; 4 | import { useTranslation } from "react-i18next"; 5 | import locKeys from "../../src/Locales/LocalizationKeys"; 6 | 7 | const useStyles = makeStyles((theme: Theme) => 8 | createStyles({ 9 | tipBar: { 10 | overflowWrap: "anywhere", 11 | }, 12 | tipBox: { 13 | padding: "1rem", 14 | paddingLeft: "2rem", 15 | borderRadius: "1em", 16 | 17 | margin: "1rem", 18 | width: "26.5em", 19 | 20 | borderLeft: "#40a351 solid 10px", 21 | }, 22 | heading: { 23 | margin: "0px", 24 | marginBottom: "5px", 25 | }, 26 | })); 27 | 28 | const TipBar: FunctionComponent = () => { 29 | 30 | const classes = useStyles(getTheme()); 31 | 32 | const [tip , setTip] = useState(""); 33 | 34 | const { t } = useTranslation(); 35 | 36 | const tips: string[] = [ 37 | // t(locKeys.tips.loginSafety), 38 | "...přihlášení přes sociální sítě je naprosto bezpečné? Přístup k vašim údajům vůbec nedostaneme!", 39 | ]; 40 | 41 | useEffect(() => { 42 | const k = tips[Math.floor(Math.random() * tips.length)]; 43 | setTip(k); 44 | }, []); 45 | 46 | return <> 47 | 48 |
49 |

50 | {t(locKeys.tips.didYouKnow)} 51 |

52 |
53 | {tip} 54 |
55 |
56 |
57 | ; 58 | }; 59 | 60 | export default TipBar; 61 | -------------------------------------------------------------------------------- /fe/components/MainAppBar/Logo/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import DateHelper from "../../../utils/dateHelper"; 3 | import { Link, makeStyles } from "@material-ui/core"; 4 | 5 | const useStyles = makeStyles(() => ({ 6 | menuLogo: { 7 | padding: "5px 0px", 8 | maxHeight: "70px", 9 | }, 10 | })); 11 | 12 | const Logo: FunctionComponent<{}> = () => { 13 | const dateHelper = new DateHelper(); 14 | 15 | const isXmasTime = dateHelper.isChristmasTime(); 16 | 17 | const isPrideMonth = dateHelper.isPrideMonth(); 18 | 19 | const styles = useStyles({}); 20 | 21 | const url = isPrideMonth ? "/static/logos/logo_new.png" : 22 | isXmasTime ? "/static/logos/christmas_logo.png" : 23 | /* Default */ "/static/logos/logo_new.png"; 24 | 25 | return <> 26 | 27 |
28 | 29 |
30 | 31 | ; 32 | }; 33 | 34 | export default Logo; 35 | -------------------------------------------------------------------------------- /fe/components/MainAppBar/logo_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/components/MainAppBar/logo_new.png -------------------------------------------------------------------------------- /fe/components/RoleSelect/RoleSelect.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, ChangeEvent } from "react"; 2 | import { FormControl, InputLabel, Select, MenuItem, FormHelperText } from "@material-ui/core"; 3 | import { useState } from "react"; 4 | import { UserRole, State } from "../../src/graphql/graphql-global-types"; 5 | import { useTranslation } from "react-i18next"; 6 | 7 | interface IProps { 8 | onSelected?: (admin: UserRole) => void; 9 | currentRole: UserRole; 10 | } 11 | 12 | const RoleSelect: FunctionComponent = ({ 13 | onSelected = null, 14 | currentRole, 15 | }) => { 16 | 17 | const [selected, setSelected] = useState(currentRole); 18 | const { t } = useTranslation(); 19 | 20 | const handleChange = (event: ChangeEvent<{ value: UserRole }>) => { 21 | const state = event.target.value as UserRole; 22 | setSelected(state); 23 | if (onSelected) { 24 | onSelected(state); 25 | } 26 | }; 27 | 28 | return <> 29 | 30 | Práva uživatele 31 | 42 | 43 | ; 44 | }; 45 | 46 | export default RoleSelect; 47 | -------------------------------------------------------------------------------- /fe/components/ScrollTop/ScrollTop.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, ReactElement, Fragment } from "react"; 2 | import { makeStyles, Theme, createStyles } from "@material-ui/core/styles"; 3 | import useScrollTrigger from "@material-ui/core/useScrollTrigger"; 4 | import Fab from "@material-ui/core/Fab"; 5 | import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp"; 6 | import Zoom from "@material-ui/core/Zoom"; 7 | import getTheme from "../Themes/MainTheme"; 8 | 9 | interface IProps { 10 | children: ReactElement; 11 | } 12 | 13 | const useStyles = makeStyles((theme: Theme) => 14 | createStyles({ 15 | root: { 16 | position: "fixed", 17 | bottom: theme.spacing(2), 18 | right: theme.spacing(2), 19 | zIndex: 5000, 20 | }, 21 | }), 22 | ); 23 | 24 | const ScrollTop: FunctionComponent = ({ children }) => { 25 | const classes = useStyles(getTheme()); 26 | 27 | const trigger = useScrollTrigger({ 28 | disableHysteresis: true, 29 | threshold: 100, 30 | }); 31 | 32 | const handleClick = () => { 33 | window.scroll({ top: 0, left: 0, behavior: "smooth" }); 34 | }; 35 | 36 | return ( 37 | 38 |
39 | {children} 40 |
41 |
42 | ); 43 | }; 44 | 45 | const ScrollButton = () => { 46 | return ( 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | export default ScrollButton; 58 | -------------------------------------------------------------------------------- /fe/components/Sharing/SharingHead/SharingHead.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import Head from "next/head"; 3 | 4 | interface IProps { 5 | title?: string; 6 | description?: string; 7 | imageUrl?: string; 8 | } 9 | 10 | const SharingHead: FunctionComponent = ({ title = "Delta - Helpdesk", description = "", 11 | imageUrl = "/static/images/favicon/maxresdefault.png" }) => { 12 | 13 | // Must end with '/' 14 | const deployUrl = "https://delta-nextjs.herokuapp.com/"; 15 | 16 | if (imageUrl.startsWith("/")) { 17 | imageUrl = imageUrl.substr(1, imageUrl.length - 1); 18 | } 19 | 20 | return <> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ; 29 | }; 30 | 31 | export default SharingHead; 32 | -------------------------------------------------------------------------------- /fe/components/Snow/Snow.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import S from "react-snow-effect"; 3 | import DateHelper from "../../utils/dateHelper"; 4 | 5 | const Snow: FunctionComponent<{}> = () => { 6 | 7 | const dateHelper = new DateHelper(); 8 | const isChristmasTime = dateHelper.isChristmasTime(); 9 | 10 | if (!isChristmasTime) { 11 | return <>; 12 | } 13 | 14 | return ; 15 | }; 16 | 17 | export default Snow; 18 | -------------------------------------------------------------------------------- /fe/components/Snow/SnowComponent.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | 3 | const SnowComponent = dynamic<{}>(() => import("./Snow"), 4 | { ssr: false }); 5 | 6 | export default SnowComponent; 7 | -------------------------------------------------------------------------------- /fe/components/TaskBoard/BoardContainer.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import { Grid, Paper, useMediaQuery, makeStyles, createStyles, Theme } from "@material-ui/core"; 3 | import TaskBoard from "./TaskBoard"; 4 | import TaskDetail from "./TaskDetail"; 5 | import { useState } from "react"; 6 | import { getTasks_tasks } from "../../src/graphql/types/getTasks"; 7 | import getTheme from "../Themes/MainTheme"; 8 | 9 | const useStyles = makeStyles((theme: Theme) => 10 | createStyles({ 11 | cards: { 12 | paddingTop: "2rem", 13 | paddingBottom: "2rem", 14 | }, 15 | detail: { 16 | padding: "2rem", 17 | }, 18 | })); 19 | 20 | interface IProps { 21 | taskId?: string; 22 | } 23 | 24 | const BoardContainer: FunctionComponent = ({ taskId }) => { 25 | 26 | const [task, setTask] = useState(null); 27 | 28 | const theme = getTheme(); 29 | const classes = useStyles(getTheme()); 30 | 31 | const smallDisplay = useMediaQuery(theme.breakpoints.down("md")); 32 | 33 | const detail = smallDisplay ? 12 : 5; 34 | const tasks = smallDisplay ? 12 : 7; 35 | 36 | const handleScroll = () => { 37 | const anchor = (document).querySelector( 38 | "#task-detail-box", 39 | ); 40 | 41 | if (anchor) { 42 | anchor.scrollIntoView({ behavior: "smooth", block: "center" }); 43 | } 44 | }; 45 | 46 | const handleClickTask = (clicked: getTasks_tasks) => { 47 | setTask(clicked); 48 | handleScroll(); 49 | }; 50 | 51 | return <> 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | ; 70 | }; 71 | 72 | export default BoardContainer; 73 | -------------------------------------------------------------------------------- /fe/components/TaskBoard/CommentsContainer.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import { useQuery } from "react-apollo"; 3 | import { GetTaskDetail, GetTaskDetailVariables } from "../../src/graphql/types/GetTaskDetail"; 4 | import { getTaskDetail, getTaskComments } from "../../src/graphql/queries"; 5 | import { GetTaskComments } from "../../src/graphql/types/GetTaskComments"; 6 | import TaskComment from "./TaskComment"; 7 | import { Grid, Divider } from "@material-ui/core"; 8 | import AddCommentComponent from "./AddComment"; 9 | import { useTranslation } from "react-i18next"; 10 | import locKeys from "../../src/Locales/LocalizationKeys"; 11 | 12 | interface IProps { 13 | taskId: string; 14 | } 15 | 16 | const CommentsContainer: FunctionComponent = ({ taskId }) => { 17 | 18 | const vars: GetTaskDetailVariables = { id: taskId }; 19 | 20 | const { t } = useTranslation(); 21 | 22 | const { loading, error, data, refetch } = useQuery(getTaskComments, { variables: vars }); 23 | 24 | if (loading) { 25 | return <> 26 | { 27 | Array.from(new Array(5)).map((x) => <>) 28 | } 29 | ; 30 | } 31 | 32 | if (error) { 33 | return <> 34 | {t(locKeys.error.undefinedError)} 35 | ; 36 | } 37 | 38 | const { task } = data; 39 | const { comments } = task; 40 | 41 | const refresh = () => { 42 | refetch(); 43 | }; 44 | 45 | if (!comments || comments.length < 1) { 46 | 47 | return <> 48 | 49 | {t(locKeys.common.noResponse)} 50 | 51 | 52 | 53 | 54 | 55 | ; 56 | } 57 | 58 | return <> 59 | 60 | 61 | 62 | { 63 | comments.map((comment, index) => 64 | 67 | ) 68 | } 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | ; 78 | }; 79 | 80 | export default CommentsContainer; 81 | -------------------------------------------------------------------------------- /fe/components/TaskBoard/TaskActions.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import { Typography } from "@material-ui/core"; 3 | import { useTranslation } from "react-i18next"; 4 | import locKeys from "../../src/Locales/LocalizationKeys"; 5 | 6 | interface IProps { 7 | taskId: string; 8 | } 9 | 10 | const TaskActions: FunctionComponent = ({ taskId }) => { 11 | const { t } = useTranslation(); 12 | 13 | return <> 14 | 15 | {t(locKeys.error.WIP)} 16 | 17 | ; 18 | }; 19 | 20 | export default TaskActions; 21 | -------------------------------------------------------------------------------- /fe/components/Themes/MainTheme.ts: -------------------------------------------------------------------------------- 1 | import { createMuiTheme, Theme } from "@material-ui/core"; 2 | import CookieHelper from "../../utils/cookieHelper"; 3 | 4 | const cookieHelper = new CookieHelper(); 5 | 6 | const getTheme = (): Theme => { 7 | 8 | const theme = cookieHelper.getThemeName(); 9 | // console.log(dark); 10 | 11 | const mainTheme = createMuiTheme({ 12 | palette: { 13 | type: theme === "dark" ? "dark" : "light", 14 | primary: { 15 | main: "#40a351", 16 | }, 17 | }, 18 | }); 19 | 20 | return mainTheme; 21 | 22 | }; 23 | 24 | export default getTheme; 25 | -------------------------------------------------------------------------------- /fe/components/Themes/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import { ThemeProvider } from "@material-ui/styles"; 3 | import { createMuiTheme, useMediaQuery, Fade } from "@material-ui/core"; 4 | import { Transition } from "react-transition-group"; 5 | import getTheme from "./MainTheme"; 6 | 7 | const ThemeContainer: FunctionComponent<{}> = ({ children }) => { 8 | 9 | const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); 10 | 11 | return <> 12 | 13 | {children} 14 | 15 | ; 16 | }; 17 | 18 | export default ThemeContainer; 19 | -------------------------------------------------------------------------------- /fe/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /fe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-typescript", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "next", 6 | "build": "next build", 7 | "start": "next start -p $PORT", 8 | "type-check": "tsc", 9 | "lint": "tslint -p tsconfig.json -c tslint.json", 10 | "schema": "apollo client:download-schema --endpoint=https://delta-helpdesk.herokuapp.com/graphql/ ./src/graphql/graphql-schema.json", 11 | "generate": "apollo client:codegen --localSchemaFile=./src/graphql/graphql-schema.json --target=typescript --includes=./src/graphql/**/*.ts --tagName=gql --addTypename --globalTypesFile=./src/graphql/graphql-global-types.ts types", 12 | "typescript": "tsc -p ./tsconfig.json --pretty --noEmit" 13 | }, 14 | "dependencies": { 15 | "@material-ui/core": "^4.5.2", 16 | "@material-ui/icons": "^3.0.1", 17 | "@material-ui/lab": "^4.0.0-alpha.33", 18 | "@material-ui/styles": "^4.5.2", 19 | "@mdi/font": "^3.2.89", 20 | "@mdi/js": "^3.2.89", 21 | "@mdi/react": "^1.1.0", 22 | "@types/node-fetch": "^2.5.2", 23 | "@types/react-router-dom": "^5.1.0", 24 | "apollo": "2.21.0", 25 | "apollo-boost": "^0.4.4", 26 | "graphql": "^14.5.8", 27 | "i18next": "^19.0.2", 28 | "i18next-xhr-backend": "^3.2.2", 29 | "mem": "^6.0.1", 30 | "moment": "^2.24.0", 31 | "msal": "^1.1.3", 32 | "next": "^9.1.4", 33 | "next-i18next": "^4.2.0", 34 | "node-fetch": "^2.6.0", 35 | "notistack": "^0.9.7", 36 | "react": "^16.10.1", 37 | "react-apollo": "^3.1.1", 38 | "react-dom": "^16.10.1", 39 | "react-i18next": "^11.2.7", 40 | "react-localization": "^1.0.15", 41 | "react-router-dom": "^5.1.2", 42 | "react-snow-effect": "^1.2.0", 43 | "react-social-login": "^3.4.6", 44 | "react-trello-for-timeline": "^2.2.6", 45 | "serialize-javascript": "^2.1.2", 46 | "tslint": "^5.20.0", 47 | "universal-cookie": "^4.0.2", 48 | "xhr2": "^0.2.0" 49 | }, 50 | "devDependencies": { 51 | "@graphql-codegen/cli": "^1.8.3", 52 | "@graphql-codegen/typescript": "^1.8.3", 53 | "@graphql-codegen/typescript-operations": "1.3.1", 54 | "@graphql-codegen/typescript-react-apollo": "1.3.1", 55 | "@types/node": "^12.7.8", 56 | "@types/react": "^16.9.3", 57 | "@types/react-dom": "^16.9.1", 58 | "typescript": "3.7.2" 59 | }, 60 | "license": "ISC" 61 | } 62 | -------------------------------------------------------------------------------- /fe/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import App from "next/app"; 2 | import { AuthContext } from "../src/graphql/auth"; 3 | import fetch from "node-fetch"; 4 | import { ApolloProvider } from "react-apollo"; 5 | import client from "../src/graphql/client"; 6 | import HeadComponent from "../components/Layouts/HeadComponent"; 7 | import theme from "../src/theme"; 8 | import { MuiThemeProvider } from "@material-ui/core"; 9 | import ThemeContainer from "../components/Themes/ThemeProvider"; 10 | import ScrollButton from "../components/ScrollTop/ScrollTop"; 11 | import SnowComponent from "../components/Snow/SnowComponent"; 12 | import "../src/i18n"; 13 | import { SnackbarProvider } from "notistack"; 14 | 15 | class HelpDeskApp extends App<{}> { 16 | 17 | componentDidMount() { 18 | // Remove the server-side injected CSS. 19 | const jssStyles = document.querySelector("#jss-server-side"); 20 | if (jssStyles) { 21 | jssStyles.parentElement!.removeChild(jssStyles); 22 | } 23 | } 24 | 25 | render() { 26 | // @ts-ignore 27 | global.fetch = fetch; 28 | 29 | // tslint:disable-next-line: no-string-literal 30 | if (!global["XMLHttpRequest"]) { 31 | // @ts-ignore 32 | global.XMLHttpRequest = require("xhr2"); 33 | } 34 | 35 | const { Component, pageProps } = this.props; 36 | 37 | return <> 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ; 52 | } 53 | 54 | } 55 | 56 | export default HelpDeskApp; 57 | -------------------------------------------------------------------------------- /fe/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Children } from "react"; 2 | import Document, { Head, Main, NextScript } from "next/document"; 3 | import { ServerStyleSheets } from "@material-ui/core/styles"; 4 | 5 | export default class MyDocument extends Document { 6 | render() { 7 | return ( 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | ); 16 | } 17 | } 18 | 19 | MyDocument.getInitialProps = async (ctx) => { 20 | // Resolution order 21 | // 22 | // On the server: 23 | // 1. app.getInitialProps 24 | // 2. page.getInitialProps 25 | // 3. document.getInitialProps 26 | // 4. app.render 27 | // 5. page.render 28 | // 6. document.render 29 | // 30 | // On the server with error: 31 | // 1. document.getInitialProps 32 | // 2. app.render 33 | // 3. page.render 34 | // 4. document.render 35 | // 36 | // On the client 37 | // 1. app.getInitialProps 38 | // 2. page.getInitialProps 39 | // 3. app.render 40 | // 4. page.render 41 | 42 | // Render app and page and get the context of the page with collected side effects. 43 | const sheets = new ServerStyleSheets(); 44 | const originalRenderPage = ctx.renderPage; 45 | 46 | ctx.renderPage = () => 47 | originalRenderPage({ 48 | enhanceApp: (App) => (props) => sheets.collect(), 49 | }); 50 | 51 | const initialProps = await Document.getInitialProps(ctx); 52 | 53 | return { 54 | ...initialProps, 55 | // Styles fragment is rendered after the app and page rendering finish. 56 | styles: [ 57 | ...Children.toArray(initialProps.styles), 58 | sheets.getStyleElement(), 59 | ], 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /fe/pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import Link from "next/link"; 3 | import { Button, Paper, Grid, Typography } from "@material-ui/core"; 4 | import HeadComponent from "../components/Layouts/HeadComponent"; 5 | import Kaomoji from "../components/Errors/Kaomoji"; 6 | 7 | interface IProps { 8 | statusCode?: string; 9 | } 10 | 11 | class Error extends Component { 12 | static getInitialProps({ res, err }) { 13 | const statusCode = res ? res.statusCode : err ? err.statusCode : null; 14 | return { statusCode }; 15 | } 16 | 17 | render() { 18 | return <> 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | { 30 | /* 31 | TODO: Lokalizace 32 | this.props.statusCode 33 | ? localisation.formatString(localisation.error.errorCodeOccured, 34 | this.props.statusCode.toString()) 35 | : localisation.error.errorOnClient*/ 36 | this.props.statusCode 37 | ? "Error" + this.props.statusCode.toString() 38 | : "Error on client" 39 | } 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ; 52 | } 53 | } 54 | 55 | export default Error; 56 | -------------------------------------------------------------------------------- /fe/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import Link from "next/link"; 3 | import Layout from "../components/Layouts/Layout"; 4 | 5 | const AboutPage: FunctionComponent = () => ( 6 | 7 |

About

8 |

This is the about page

9 |

10 | 11 | Go home 12 | 13 |

14 |
15 | ); 16 | 17 | export default AboutPage; 18 | -------------------------------------------------------------------------------- /fe/pages/admin/index.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "../../components/Layouts/Layout"; 2 | import { withAuthSync } from "../../src/auth/authWrapper"; 3 | import AdminContainer from "../../components/Administration/AdminContainer/AdminContainer"; 4 | import BoardContainer from "../../components/TaskBoard/BoardContainer"; 5 | import { NextPageContext } from "next"; 6 | import { Component } from "react"; 7 | 8 | // tslint:disable-next-line:no-empty-interface 9 | interface IProps { 10 | taskId?: string; 11 | } 12 | 13 | // tslint:disable-next-line: interface-over-type-literal 14 | type RequestQuery = { 15 | taskId: string, 16 | }; 17 | class AdminPage extends Component { 18 | 19 | static getInitialProps = async ({ query }: NextPageContext) => { 20 | const taskId: string = query.taskId?.toString(); 21 | return { taskId }; 22 | } 23 | 24 | render() { 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | } 34 | 35 | export default withAuthSync(AdminPage); 36 | -------------------------------------------------------------------------------- /fe/pages/admin/users.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from "next"; 2 | import Layout from "../../components/Layouts/Layout"; 3 | import UserListContainer from "../../components/Administration/UserList/UserListContainer"; 4 | import AdminContaier from "../../components/Administration/AdminContainer/AdminContainer"; 5 | 6 | const UserListPage: NextPage = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default UserListPage; 17 | -------------------------------------------------------------------------------- /fe/pages/board.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import Layout from "../components/Layouts/Layout"; 3 | import TaskBoard from "../components/TaskBoard/TaskBoard"; 4 | import { withAuthSync } from "../src/auth/authWrapper"; 5 | import { UserRole } from "../src/graphql/graphql-global-types"; 6 | import { NextPageContext } from "next"; 7 | 8 | // tslint:disable-next-line:no-empty-interface 9 | interface IProps { 10 | taskId?: string; 11 | } 12 | 13 | // tslint:disable-next-line: interface-over-type-literal 14 | type RequestQuery = { 15 | taskId: string, 16 | }; 17 | 18 | class Board extends Component { 19 | 20 | render() { 21 | return <> 22 | 23 |
24 | 25 |
26 |
27 | ; 28 | } 29 | } 30 | 31 | export default withAuthSync(Board, UserRole.ADMIN); 32 | -------------------------------------------------------------------------------- /fe/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "../components/Layouts/Layout"; 2 | import { NextPage } from "next"; 3 | import HomePage from "../components/Homepage/HomePage"; 4 | import HeadComponent from "../components/Layouts/HeadComponent"; 5 | import { MuiThemeProvider } from "@material-ui/core"; 6 | import SharingHead from "../components/Sharing/SharingHead/SharingHead"; 7 | 8 | const IndexPage: NextPage = () => { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default IndexPage; 18 | -------------------------------------------------------------------------------- /fe/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "../components/Layouts/Layout"; 2 | import { NextPage } from "next"; 3 | import LoginPageComponent from "../components/Login/Login"; 4 | import { useTranslation } from "react-i18next"; 5 | import locKeys from "../src/Locales/LocalizationKeys"; 6 | 7 | const LoginPage: NextPage = () => { 8 | const { t } = useTranslation(); 9 | 10 | return <> 11 | 12 |
13 | 14 |
15 |
16 | ; 17 | }; 18 | 19 | export default LoginPage; 20 | -------------------------------------------------------------------------------- /fe/pages/new.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "../components/Layouts/Layout"; 2 | import { NextPage } from "next"; 3 | import NewTaskContainer from "../components/NewTask/NewTaskContainer"; 4 | import { withAuthSync } from "../src/auth/authWrapper"; 5 | 6 | const NewtaskPage: NextPage = () => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default withAuthSync(NewtaskPage); 15 | -------------------------------------------------------------------------------- /fe/pages/settings.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import HeadComponent from "../components/Layouts/HeadComponent"; 3 | import { Typography, Grid, Divider, Paper } from "@material-ui/core"; 4 | import Layout from "../components/Layouts/Layout"; 5 | import LanguageSelect from "../components/LanguageSelect/LanguageSelect"; 6 | import DarkModeSelect from "../components/DarkModeSelect/DarkModeSelect"; 7 | import { withAuthSync } from "../src/auth/authWrapper"; 8 | import { useTranslation } from "react-i18next"; 9 | import locKeys from "../src/Locales/LocalizationKeys"; 10 | 11 | const SettingsPage: FunctionComponent = () => { 12 | const { t } = useTranslation(); 13 | 14 | return <> 15 | 16 | 17 | 18 | 19 | 20 | 21 | {t(locKeys.common.settings)} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ; 36 | }; 37 | 38 | export default withAuthSync(SettingsPage); 39 | -------------------------------------------------------------------------------- /fe/public/static/helpdesk_bg_trans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/helpdesk_bg_trans.png -------------------------------------------------------------------------------- /fe/public/static/images/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/images/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /fe/public/static/images/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/images/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /fe/public/static/images/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/images/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /fe/public/static/images/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | #40a351 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /fe/public/static/images/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/images/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /fe/public/static/images/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/images/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /fe/public/static/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/images/favicon/favicon.ico -------------------------------------------------------------------------------- /fe/public/static/images/favicon/maxresdefault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/images/favicon/maxresdefault.png -------------------------------------------------------------------------------- /fe/public/static/images/favicon/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/images/favicon/mstile-144x144.png -------------------------------------------------------------------------------- /fe/public/static/images/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/images/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /fe/public/static/images/favicon/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/images/favicon/mstile-310x150.png -------------------------------------------------------------------------------- /fe/public/static/images/favicon/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/images/favicon/mstile-310x310.png -------------------------------------------------------------------------------- /fe/public/static/images/favicon/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/images/favicon/mstile-70x70.png -------------------------------------------------------------------------------- /fe/public/static/images/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 28 | 31 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /fe/public/static/images/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Delta - helpdesk", 3 | "short_name": "Delta - helpdesk", 4 | "icons": [ 5 | { 6 | "src": "/static/images/favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/static/images/favicon/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /fe/public/static/logos/christmas_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/logos/christmas_logo.png -------------------------------------------------------------------------------- /fe/public/static/logos/logo_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/fe/public/static/logos/logo_new.png -------------------------------------------------------------------------------- /fe/public/static/static.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px !important; 3 | } 4 | 5 | .clear-button-chip { 6 | color: inherit; 7 | border: none; 8 | cursor: pointer; 9 | outline: inherit; 10 | } 11 | 12 | .noselect { 13 | -webkit-touch-callout: none; 14 | /* iOS Safari */ 15 | -webkit-user-select: none; 16 | /* Safari */ 17 | -khtml-user-select: none; 18 | /* Konqueror HTML */ 19 | -moz-user-select: none; 20 | /* Firefox */ 21 | -ms-user-select: none; 22 | /* Internet Explorer/Edge */ 23 | user-select: none; 24 | /* Non-prefixed version, currently 25 | supported by Chrome and Opera */ 26 | } 27 | 28 | .animated-transition { 29 | transition: 0.5s; 30 | } 31 | 32 | .admin-card { 33 | width: 15rem; 34 | height: 15rem; 35 | } 36 | 37 | .text-desc { 38 | overflow: hidden; 39 | text-overflow: ellipsis; 40 | display: -webkit-box; 41 | line-height: 16px; 42 | --lines-to-show: 6; 43 | /* fallback */ 44 | max-height: calc(16px*var(--lines-to-show)); 45 | /* fallback */ 46 | -webkit-line-clamp: var(--lines-to-show); 47 | /* number of lines to show */ 48 | -webkit-box-orient: vertical; 49 | } 50 | 51 | .eVDmWl { 52 | background-color: transparent !important; 53 | } 54 | 55 | .scrollbar-custom::-webkit-scrollbar-track { 56 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3) !important; 57 | border-radius: 10px !important; 58 | background-color: transparent !important; 59 | } 60 | 61 | .scrollbar-custom::-webkit-scrollbar { 62 | width: 12px !important; 63 | background-color: transparent !important; 64 | } 65 | 66 | .scrollbar-custom::-webkit-scrollbar-thumb { 67 | border-radius: 10px !important; 68 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3) !important; 69 | background-color: #ff6600 !important; 70 | } 71 | 72 | .smooth-dnd-container.horizontal { 73 | margin: 0 auto; 74 | } 75 | 76 | .smooth-dnd-container.horizontal:last-of-type{ 77 | display: none; 78 | } -------------------------------------------------------------------------------- /fe/src/Global/Keys.ts: -------------------------------------------------------------------------------- 1 | const UserTokenCookieKey: string = "UToken"; 2 | 3 | export {UserTokenCookieKey}; 4 | -------------------------------------------------------------------------------- /fe/src/Routes.ts: -------------------------------------------------------------------------------- 1 | 2 | const customRoutes = { 3 | loginRoute: "/login", 4 | newTask: "/new", 5 | administration: "/admin", 6 | taskBoard: "/board", 7 | userList: "/admin/users", 8 | settings: "/settings", 9 | }; 10 | 11 | export default customRoutes; 12 | -------------------------------------------------------------------------------- /fe/src/auth/authWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { ReactAuthContext, checkUserRole } from "../graphql/auth"; 2 | import Router from "next/router"; 3 | import customRoutes from "../Routes"; 4 | import { useContext, useEffect } from "react"; 5 | import { UserRole } from "../graphql/graphql-global-types"; 6 | 7 | export const withAuthSync = (WrappedComponent: any, minRole: UserRole = UserRole.DEFAULT) => { 8 | 9 | const Wrapper = (props: any) => { 10 | const syncLogout = (event: { key: string; }) => { 11 | if (event.key === "logout") { 12 | console.log("logged out from storage!"); 13 | Router.push(customRoutes.loginRoute); 14 | } 15 | }; 16 | 17 | const showError = (text: string) => { 18 | console.log(text); 19 | }; 20 | 21 | const { isLoggedIn, user } = useContext(ReactAuthContext); 22 | 23 | useEffect(() => { 24 | window.addEventListener("storage", syncLogout); 25 | if (!isLoggedIn) { 26 | showError("Musíte se přihásit"); 27 | Router.push(customRoutes.loginRoute); 28 | return; 29 | } 30 | if (user && !checkUserRole(user.role, minRole)) { 31 | showError("Nemáte dostatečné oprávnění"); 32 | Router.back(); 33 | return; 34 | } 35 | 36 | return () => { 37 | window.removeEventListener("storage", syncLogout); 38 | window.localStorage.removeItem("logout"); 39 | }; 40 | }, [null]); 41 | 42 | return isLoggedIn && ; 43 | }; 44 | 45 | Wrapper.getInitialProps = async (ctx: any) => { 46 | // console.log(ctx); 47 | const token = ""; // ctx.token; 48 | 49 | const componentProps = 50 | WrappedComponent.getInitialProps && 51 | (await WrappedComponent.getInitialProps(ctx)); 52 | 53 | return { ...componentProps, token }; 54 | }; 55 | 56 | return Wrapper; 57 | }; 58 | -------------------------------------------------------------------------------- /fe/src/graphql/client.ts: -------------------------------------------------------------------------------- 1 | import ApolloClient from "apollo-boost"; 2 | import resolvers from "./resolvers.js"; 3 | import typeDefs from "./typedefs.js"; 4 | import { lastToken, isLoggedIn, lastContextValue } from "./auth"; 5 | import { ServerError } from "apollo-link-http-common"; 6 | 7 | const apiUri = "https://delta-helpdesk.herokuapp.com/graphql"; 8 | 9 | const client = new ApolloClient({ 10 | uri: apiUri, 11 | fetch: require("node-fetch"), 12 | request: async (operation) => { 13 | if (isLoggedIn()) { 14 | operation.setContext({ 15 | headers: { 16 | Authorization: `Bearer ${lastToken}`, 17 | }, 18 | }); 19 | } 20 | }, 21 | onError({ networkError, response, forward, operation, ...other }) { 22 | if ((networkError && (networkError as ServerError).statusCode === 401) || 23 | (response && response.errors && (response.errors[0].message as any).statusCode === 401)) { 24 | if (operation.operationName !== "logout") { 25 | lastContextValue.logout(); 26 | } 27 | } 28 | console.error("GraphQL onError handle", { networkError, response, operation, ...other }); 29 | forward(operation); 30 | }, 31 | clientState: { 32 | defaults: {}, 33 | resolvers, 34 | typeDefs, 35 | }, 36 | }); 37 | 38 | export default client; 39 | -------------------------------------------------------------------------------- /fe/src/graphql/graphql-global-types.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | //============================================================== 6 | // START Enums and Input Objects 7 | //============================================================== 8 | 9 | export enum AuthType { 10 | EMAIL = "EMAIL", 11 | FACEBOOK = "FACEBOOK", 12 | GITHUB = "GITHUB", 13 | GOOGLE = "GOOGLE", 14 | MICROSOFT = "MICROSOFT", 15 | } 16 | 17 | export enum State { 18 | RETURNED = "RETURNED", 19 | SOLVED = "SOLVED", 20 | SOLVING = "SOLVING", 21 | UNRESOLVED = "UNRESOLVED", 22 | } 23 | 24 | export enum UserRole { 25 | ADMIN = "ADMIN", 26 | DEFAULT = "DEFAULT", 27 | SUPERADMIN = "SUPERADMIN", 28 | } 29 | 30 | //============================================================== 31 | // END Enums and Input Objects 32 | //============================================================== 33 | -------------------------------------------------------------------------------- /fe/src/graphql/queries.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export const getUsersQuery = gql` 4 | query getUsers{ 5 | users { 6 | id 7 | fullName 8 | email 9 | created_at 10 | updated_at 11 | role 12 | enabled 13 | } 14 | } 15 | `; 16 | 17 | export const getAdminsQuery = gql` 18 | query getAdmins { 19 | admins { 20 | id 21 | fullName 22 | } 23 | } 24 | `; 25 | 26 | export const taskDetailQuery = gql` 27 | query getTask($id: ID!) { 28 | task(id: $id){ 29 | id 30 | subject 31 | issue 32 | state 33 | created_at 34 | author { 35 | id 36 | fullName 37 | } 38 | assignee { 39 | id 40 | fullName 41 | } 42 | } 43 | } 44 | `; 45 | 46 | export const getTaskDetail = gql` 47 | query GetTaskDetail($id: ID!) { 48 | task(id: $id) { 49 | subject 50 | issue 51 | author { 52 | fullName 53 | role 54 | } 55 | created_at 56 | updated_at 57 | state 58 | comments { 59 | author { 60 | fullName 61 | role 62 | } 63 | created_at 64 | updated_at 65 | message 66 | } 67 | } 68 | } 69 | `; 70 | 71 | export const getTaskComments = gql` 72 | query GetTaskComments($id: ID!) { 73 | task(id: $id) { 74 | comments { 75 | id 76 | author { 77 | fullName 78 | role 79 | } 80 | created_at 81 | updated_at 82 | message 83 | } 84 | } 85 | }`; 86 | 87 | export const getTasksQuery = gql` 88 | query getTasks{ 89 | tasks { 90 | id 91 | subject 92 | issue 93 | state 94 | created_at 95 | author { 96 | id 97 | fullName 98 | role 99 | } 100 | assignee { 101 | id 102 | fullName 103 | role 104 | } 105 | } 106 | } 107 | `; 108 | 109 | export const getSessionQuery = gql` 110 | query getSession { 111 | session { 112 | id 113 | fullName 114 | email 115 | role 116 | token 117 | language 118 | } 119 | } 120 | `; 121 | -------------------------------------------------------------------------------- /fe/src/graphql/resolvers.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-boost"; 2 | 3 | let nextTodoId = 0; 4 | 5 | const resolvers = { 6 | Mutation: { 7 | addTodo: (_, { text }, { cache }) => { 8 | const query = gql` 9 | query GetTodos { 10 | todos @client { 11 | id 12 | text 13 | completed 14 | } 15 | } 16 | `; 17 | const previous = cache.readQuery({ query }); 18 | const newTodo = { 19 | id: nextTodoId++, 20 | text, 21 | completed: false, 22 | __typename: "TodoItem" 23 | }; 24 | const data = { 25 | todos: previous.todos.concat([newTodo]) 26 | }; 27 | cache.writeData({ data }); 28 | return newTodo; 29 | }, 30 | toggleTodo: (_, variables, { cache }) => { 31 | const id = `TodoItem:${variables.id}`; 32 | const fragment = gql` 33 | fragment completeTodo on TodoItem { 34 | completed 35 | } 36 | `; 37 | const todo = cache.readFragment({ fragment, id }); 38 | const data = { ...todo, completed: !todo.completed }; 39 | cache.writeData({ id, data }); 40 | return null; 41 | } 42 | } 43 | }; 44 | 45 | export default resolvers; 46 | -------------------------------------------------------------------------------- /fe/src/graphql/typedefs.js: -------------------------------------------------------------------------------- 1 | const typeDefs = ` 2 | type Todo { 3 | id: Int! 4 | text: String! 5 | completed: Boolean! 6 | } 7 | 8 | type Mutation { 9 | addTodo(text: String!): Todo 10 | toggleTodo(id: Int!): Todo 11 | } 12 | 13 | type Query { 14 | todos: [Todo] 15 | } 16 | `; 17 | 18 | export default typeDefs; -------------------------------------------------------------------------------- /fe/src/graphql/types/AddComment.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: AddComment 7 | // ==================================================== 8 | 9 | export interface AddComment_addComment { 10 | __typename: "Comment"; 11 | id: string; 12 | } 13 | 14 | export interface AddComment { 15 | addComment: AddComment_addComment | null; 16 | } 17 | 18 | export interface AddCommentVariables { 19 | taskId: string; 20 | message: string; 21 | } 22 | -------------------------------------------------------------------------------- /fe/src/graphql/types/DeleteComment.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: DeleteComment 7 | // ==================================================== 8 | 9 | export interface DeleteComment { 10 | deleteComment: boolean | null; 11 | } 12 | 13 | export interface DeleteCommentVariables { 14 | id: string; 15 | } 16 | -------------------------------------------------------------------------------- /fe/src/graphql/types/EditUser.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: EditUser 7 | // ==================================================== 8 | 9 | export interface EditUser_editUser { 10 | __typename: "User"; 11 | id: string; 12 | } 13 | 14 | export interface EditUser { 15 | editUser: EditUser_editUser | null; 16 | } 17 | 18 | export interface EditUserVariables { 19 | theme: string; 20 | } 21 | -------------------------------------------------------------------------------- /fe/src/graphql/types/EditUserAdmin.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | import { UserRole } from "./../graphql-global-types"; 6 | 7 | // ==================================================== 8 | // GraphQL mutation operation: EditUserAdmin 9 | // ==================================================== 10 | 11 | export interface EditUserAdmin_adminEditUser { 12 | __typename: "User"; 13 | id: string; 14 | } 15 | 16 | export interface EditUserAdmin { 17 | adminEditUser: EditUserAdmin_adminEditUser | null; 18 | } 19 | 20 | export interface EditUserAdminVariables { 21 | userId: string; 22 | email: string; 23 | fullName: string; 24 | role: UserRole; 25 | } 26 | -------------------------------------------------------------------------------- /fe/src/graphql/types/EditUserLanguage.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: EditUserLanguage 7 | // ==================================================== 8 | 9 | export interface EditUserLanguage_editUser { 10 | __typename: "User"; 11 | id: string; 12 | } 13 | 14 | export interface EditUserLanguage { 15 | editUser: EditUserLanguage_editUser | null; 16 | } 17 | 18 | export interface EditUserLanguageVariables { 19 | language: string; 20 | } 21 | -------------------------------------------------------------------------------- /fe/src/graphql/types/GetTaskComments.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | import { UserRole } from "./../graphql-global-types"; 6 | 7 | // ==================================================== 8 | // GraphQL query operation: GetTaskComments 9 | // ==================================================== 10 | 11 | export interface GetTaskComments_task_comments_author { 12 | __typename: "User"; 13 | fullName: string; 14 | role: UserRole | null; 15 | } 16 | 17 | export interface GetTaskComments_task_comments { 18 | __typename: "Comment"; 19 | id: string; 20 | author: GetTaskComments_task_comments_author; 21 | created_at: any; 22 | updated_at: any; 23 | message: string; 24 | } 25 | 26 | export interface GetTaskComments_task { 27 | __typename: "Task"; 28 | comments: (GetTaskComments_task_comments | null)[] | null; 29 | } 30 | 31 | export interface GetTaskComments { 32 | task: GetTaskComments_task | null; 33 | } 34 | 35 | export interface GetTaskCommentsVariables { 36 | id: string; 37 | } 38 | -------------------------------------------------------------------------------- /fe/src/graphql/types/GetTaskDetail.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | import { UserRole, State } from "./../graphql-global-types"; 6 | 7 | // ==================================================== 8 | // GraphQL query operation: GetTaskDetail 9 | // ==================================================== 10 | 11 | export interface GetTaskDetail_task_author { 12 | __typename: "User"; 13 | fullName: string; 14 | role: UserRole | null; 15 | } 16 | 17 | export interface GetTaskDetail_task_comments_author { 18 | __typename: "User"; 19 | fullName: string; 20 | role: UserRole | null; 21 | } 22 | 23 | export interface GetTaskDetail_task_comments { 24 | __typename: "Comment"; 25 | author: GetTaskDetail_task_comments_author; 26 | created_at: any; 27 | updated_at: any; 28 | message: string; 29 | } 30 | 31 | export interface GetTaskDetail_task { 32 | __typename: "Task"; 33 | subject: string; 34 | issue: string; 35 | author: GetTaskDetail_task_author; 36 | created_at: any; 37 | updated_at: any | null; 38 | state: State; 39 | comments: (GetTaskDetail_task_comments | null)[] | null; 40 | } 41 | 42 | export interface GetTaskDetail { 43 | task: GetTaskDetail_task | null; 44 | } 45 | 46 | export interface GetTaskDetailVariables { 47 | id: string; 48 | } 49 | -------------------------------------------------------------------------------- /fe/src/graphql/types/addTask.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | import { State } from "./../graphql-global-types"; 6 | 7 | // ==================================================== 8 | // GraphQL mutation operation: addTask 9 | // ==================================================== 10 | 11 | export interface addTask_addTask_author { 12 | __typename: "User"; 13 | id: string; 14 | fullName: string; 15 | } 16 | 17 | export interface addTask_addTask_assignee { 18 | __typename: "User"; 19 | id: string; 20 | fullName: string; 21 | } 22 | 23 | export interface addTask_addTask { 24 | __typename: "Task"; 25 | id: string; 26 | issue: string; 27 | state: State; 28 | author: addTask_addTask_author; 29 | assignee: addTask_addTask_assignee | null; 30 | } 31 | 32 | export interface addTask { 33 | addTask: addTask_addTask | null; 34 | } 35 | 36 | export interface addTaskVariables { 37 | subject: string; 38 | issue: string; 39 | assigneeId?: string | null; 40 | } 41 | -------------------------------------------------------------------------------- /fe/src/graphql/types/changeTaskBoardState.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | import { State } from "./../graphql-global-types"; 6 | 7 | // ==================================================== 8 | // GraphQL mutation operation: changeTaskBoardState 9 | // ==================================================== 10 | 11 | export interface changeTaskBoardState_changeTaskState_author { 12 | __typename: "User"; 13 | id: string; 14 | fullName: string; 15 | } 16 | 17 | export interface changeTaskBoardState_changeTaskState_assignee { 18 | __typename: "User"; 19 | id: string; 20 | fullName: string; 21 | } 22 | 23 | export interface changeTaskBoardState_changeTaskState { 24 | __typename: "Task"; 25 | id: string; 26 | subject: string; 27 | issue: string; 28 | author: changeTaskBoardState_changeTaskState_author; 29 | assignee: changeTaskBoardState_changeTaskState_assignee | null; 30 | created_at: any; 31 | updated_at: any | null; 32 | state: State; 33 | } 34 | 35 | export interface changeTaskBoardState { 36 | changeTaskState: changeTaskBoardState_changeTaskState | null; 37 | } 38 | 39 | export interface changeTaskBoardStateVariables { 40 | taskId: string; 41 | comment: string; 42 | state: State; 43 | } 44 | -------------------------------------------------------------------------------- /fe/src/graphql/types/changeTaskState.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | import { State } from "./../graphql-global-types"; 6 | 7 | // ==================================================== 8 | // GraphQL mutation operation: changeTaskState 9 | // ==================================================== 10 | 11 | export interface changeTaskState_changeTaskState { 12 | __typename: "Task"; 13 | id: string; 14 | } 15 | 16 | export interface changeTaskState { 17 | changeTaskState: changeTaskState_changeTaskState | null; 18 | } 19 | 20 | export interface changeTaskStateVariables { 21 | taskId?: string | null; 22 | comment?: string | null; 23 | state?: State | null; 24 | assigneeId?: string | null; 25 | } 26 | -------------------------------------------------------------------------------- /fe/src/graphql/types/createUserEmail.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: createUserEmail 7 | // ==================================================== 8 | 9 | export interface createUserEmail_createUserEmail { 10 | __typename: "User"; 11 | email: string; 12 | } 13 | 14 | export interface createUserEmail { 15 | createUserEmail: createUserEmail_createUserEmail | null; 16 | } 17 | 18 | export interface createUserEmailVariables { 19 | email: string; 20 | password: string; 21 | fullName: string; 22 | } 23 | -------------------------------------------------------------------------------- /fe/src/graphql/types/deleteTask.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: deleteTask 7 | // ==================================================== 8 | 9 | export interface deleteTask { 10 | deleteTask: boolean | null; 11 | } 12 | 13 | export interface deleteTaskVariables { 14 | taskId: string; 15 | } 16 | -------------------------------------------------------------------------------- /fe/src/graphql/types/getAdmins.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL query operation: getAdmins 7 | // ==================================================== 8 | 9 | export interface getAdmins_admins { 10 | __typename: "User"; 11 | id: string; 12 | fullName: string; 13 | } 14 | 15 | export interface getAdmins { 16 | admins: (getAdmins_admins | null)[] | null; 17 | } 18 | -------------------------------------------------------------------------------- /fe/src/graphql/types/getSession.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | import { UserRole } from "./../graphql-global-types"; 6 | 7 | // ==================================================== 8 | // GraphQL query operation: getSession 9 | // ==================================================== 10 | 11 | export interface getSession_session { 12 | __typename: "AuthenticatedUser"; 13 | id: string; 14 | fullName: string; 15 | email: string; 16 | role: UserRole | null; 17 | token: string; 18 | language: string; 19 | } 20 | 21 | export interface getSession { 22 | session: getSession_session | null; 23 | } 24 | -------------------------------------------------------------------------------- /fe/src/graphql/types/getTask.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | import { State } from "./../graphql-global-types"; 6 | 7 | // ==================================================== 8 | // GraphQL query operation: getTask 9 | // ==================================================== 10 | 11 | export interface getTask_task_author { 12 | __typename: "User"; 13 | id: string; 14 | fullName: string; 15 | } 16 | 17 | export interface getTask_task_assignee { 18 | __typename: "User"; 19 | id: string; 20 | fullName: string; 21 | } 22 | 23 | export interface getTask_task { 24 | __typename: "Task"; 25 | id: string; 26 | subject: string; 27 | issue: string; 28 | state: State; 29 | created_at: any; 30 | author: getTask_task_author; 31 | assignee: getTask_task_assignee | null; 32 | } 33 | 34 | export interface getTask { 35 | task: getTask_task | null; 36 | } 37 | 38 | export interface getTaskVariables { 39 | id: string; 40 | } 41 | -------------------------------------------------------------------------------- /fe/src/graphql/types/getTasks.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | import { State, UserRole } from "./../graphql-global-types"; 6 | 7 | // ==================================================== 8 | // GraphQL query operation: getTasks 9 | // ==================================================== 10 | 11 | export interface getTasks_tasks_author { 12 | __typename: "User"; 13 | id: string; 14 | fullName: string; 15 | role: UserRole | null; 16 | } 17 | 18 | export interface getTasks_tasks_assignee { 19 | __typename: "User"; 20 | id: string; 21 | fullName: string; 22 | role: UserRole | null; 23 | } 24 | 25 | export interface getTasks_tasks { 26 | __typename: "Task"; 27 | id: string; 28 | subject: string; 29 | issue: string; 30 | state: State; 31 | created_at: any; 32 | author: getTasks_tasks_author; 33 | assignee: getTasks_tasks_assignee | null; 34 | } 35 | 36 | export interface getTasks { 37 | tasks: (getTasks_tasks | null)[] | null; 38 | } 39 | -------------------------------------------------------------------------------- /fe/src/graphql/types/getUsers.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | import { UserRole } from "./../graphql-global-types"; 6 | 7 | // ==================================================== 8 | // GraphQL query operation: getUsers 9 | // ==================================================== 10 | 11 | export interface getUsers_users { 12 | __typename: "User"; 13 | id: string; 14 | fullName: string; 15 | email: string; 16 | created_at: any; 17 | updated_at: any | null; 18 | role: UserRole | null; 19 | enabled: boolean; 20 | } 21 | 22 | export interface getUsers { 23 | users: (getUsers_users | null)[] | null; 24 | } 25 | -------------------------------------------------------------------------------- /fe/src/graphql/types/loginEmail.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: loginEmail 7 | // ==================================================== 8 | 9 | export interface loginEmail_loginEmail { 10 | __typename: "AuthenticatedUser"; 11 | token: string; 12 | language: string; 13 | theme: string | null; 14 | } 15 | 16 | export interface loginEmail { 17 | loginEmail: loginEmail_loginEmail | null; 18 | } 19 | 20 | export interface loginEmailVariables { 21 | email: string; 22 | password: string; 23 | } 24 | -------------------------------------------------------------------------------- /fe/src/graphql/types/loginExternal.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | import { AuthType } from "./../graphql-global-types"; 6 | 7 | // ==================================================== 8 | // GraphQL mutation operation: loginExternal 9 | // ==================================================== 10 | 11 | export interface loginExternal_loginExternal { 12 | __typename: "AuthenticatedUser"; 13 | token: string; 14 | language: string; 15 | theme: string | null; 16 | } 17 | 18 | export interface loginExternal { 19 | loginExternal: loginExternal_loginExternal | null; 20 | } 21 | 22 | export interface loginExternalVariables { 23 | email: string; 24 | name: string; 25 | provider: AuthType; 26 | token: string; 27 | } 28 | -------------------------------------------------------------------------------- /fe/src/graphql/types/loginOffice.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: loginOffice 7 | // ==================================================== 8 | 9 | export interface loginOffice_loginOffice { 10 | __typename: "AuthenticatedUser"; 11 | token: string; 12 | language: string; 13 | theme: string | null; 14 | } 15 | 16 | export interface loginOffice { 17 | loginOffice: loginOffice_loginOffice | null; 18 | } 19 | 20 | export interface loginOfficeVariables { 21 | token: string; 22 | } 23 | -------------------------------------------------------------------------------- /fe/src/graphql/types/logout.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: logout 7 | // ==================================================== 8 | 9 | export interface logout { 10 | logout: boolean | null; 11 | } 12 | -------------------------------------------------------------------------------- /fe/src/graphql/types/removeUser.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: removeUser 7 | // ==================================================== 8 | 9 | export interface removeUser { 10 | removeUser: boolean | null; 11 | } 12 | 13 | export interface removeUserVariables { 14 | email: string; 15 | } 16 | -------------------------------------------------------------------------------- /fe/src/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | import { initReactI18next } from "react-i18next"; 3 | import Backend from "i18next-xhr-backend"; 4 | 5 | i18n 6 | .use(Backend) 7 | .use(initReactI18next) 8 | .init({ 9 | lng: "cs", 10 | fallbackLng: "cs", 11 | defaultNS: "translations", 12 | debug: true, 13 | interpolation: { 14 | escapeValue: false, // not needed for react as it escapes by default 15 | }, 16 | backend: { 17 | loadPath: `https://delta-helpdesk.herokuapp.com/localization/{{lng}}/{{ns}}`, 18 | crossDomain: true, 19 | }, 20 | react: { 21 | useSuspense: false, 22 | }, 23 | }); 24 | 25 | export default i18n; 26 | -------------------------------------------------------------------------------- /fe/src/services/microsoft.ts: -------------------------------------------------------------------------------- 1 | import * as Msal from "msal"; 2 | // https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-core/README.md 3 | 4 | export default class MicrosoftAuthService { 5 | app: Msal.UserAgentApplication; 6 | applicationConfig: Msal.Configuration; 7 | loginRequest: Msal.AuthenticationParameters; 8 | 9 | constructor() { 10 | this.applicationConfig = { 11 | auth: { 12 | clientId: "12a2525e-d4a2-45fb-9020-0809921a1503", 13 | }, 14 | }; 15 | this.loginRequest = { 16 | scopes: ["user.read", "mail.send"], 17 | }; 18 | 19 | this.app = new Msal.UserAgentApplication(this.applicationConfig); 20 | this.app.handleRedirectCallback((error, response) => { 21 | // this.authCallback(error.message || "Chyba", response.idToken, error, response.tokenType); 22 | }); 23 | } 24 | 25 | authCallback(errorDesc, token, error, tokenType) { 26 | if (token) { 27 | console.log("CALLBACK", token); 28 | } else { 29 | console.log(error + ":" + errorDesc); 30 | } 31 | } 32 | 33 | login = async () => { 34 | try { 35 | const response = await this.app.loginRedirect(this.loginRequest); 36 | const user = this.app.getAccount(); 37 | if (user) { 38 | return user; 39 | } 40 | return null; 41 | } catch (e) { 42 | return null; 43 | } 44 | } 45 | 46 | logout = () => { 47 | this.app.logout(); 48 | } 49 | 50 | getToken = async () => { 51 | try { 52 | const accessToken = await this.app.acquireTokenSilent(this.loginRequest); 53 | return accessToken; 54 | } catch (error) { 55 | try { 56 | const accessToken1 = await this.app 57 | .acquireTokenPopup(this.loginRequest); 58 | return accessToken1; 59 | } catch (err) { 60 | console.error(err); 61 | } 62 | } 63 | } 64 | } 65 | 66 | // export default class GraphService { 67 | // constructor() { 68 | // this.graphUrl = 'https://graph.microsoft.com/v1.0/'; 69 | // } 70 | // getUserInfo = token => { 71 | // const headers = new Headers({ Authorization: `Bearer ${token}` }); 72 | // const options = { 73 | // headers 74 | // }; 75 | // return fetch(`${this.graphUrl}/me`, options) 76 | // .then(response => response.json()) 77 | // .catch(response => { 78 | // throw new Error(response.text()); 79 | // }); 80 | // }; 81 | // } 82 | -------------------------------------------------------------------------------- /fe/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "jsx": "preserve", 6 | "lib": [ 7 | "dom", 8 | "es2017" 9 | ], 10 | "moduleResolution": "node", 11 | "allowJs": true, 12 | "noEmit": true, 13 | "strict": false, 14 | "allowSyntheticDefaultImports": true, 15 | "skipLibCheck": true, 16 | "noUnusedLocals": false, 17 | "noUnusedParameters": false, 18 | "removeComments": false, 19 | "preserveConstEnums": true, 20 | "sourceMap": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "esModuleInterop": true, 23 | "resolveJsonModule": true, 24 | "isolatedModules": true, 25 | "typeRoots": [ 26 | "./node_modules/@types", 27 | "./types" 28 | ] 29 | }, 30 | "exclude": [ 31 | "node_modules" 32 | ], 33 | "include": [ 34 | "**/*.ts", 35 | "**/*.tsx" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /fe/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended" 4 | ], 5 | "linterOptions": { 6 | "exclude": [ 7 | "config/**/*.js", 8 | "node_modules/**/*.ts", 9 | "coverage/lcov-report/*.js", 10 | "src/graphql/types.ts", 11 | "src/graphql/*.js", 12 | "src/graphql/types.tsx" 13 | ] 14 | }, 15 | "rules": { 16 | "member-access": [ 17 | true, 18 | "no-public" 19 | ], 20 | "ordered-imports": [ 21 | false 22 | ], 23 | "object-literal-sort-keys": false, 24 | "no-console": false, 25 | "jsx-no-lambda": false, 26 | "max-line-length": [ 27 | true, 28 | { 29 | "limit": 120, 30 | "ignore-pattern": "^import |^export {(.*?)} |className={* |style={{ *}}", 31 | "check-strings": false, 32 | "check-regex": false 33 | } 34 | ] 35 | }, 36 | "jsRules": { 37 | "object-literal-sort-keys": false, 38 | "no-console": false 39 | } 40 | } -------------------------------------------------------------------------------- /fe/types/react-social-login/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-social-login" { 2 | export type ISocialLoginProvider = "amazon" | "facebook" | "github" | "google" | "instagram" | "linkedin"; 3 | 4 | interface ISocialLoginProps { 5 | appId: string; 6 | autoCleanUri?: boolean; 7 | autoLogin?: boolean; 8 | gatekeeper?: string; 9 | getInstance?: () => React.RefObject; 10 | onLoginFailure: (result: any) => void; 11 | onLoginSuccess: (result: any) => void; 12 | onLogoutFailure?: (result: any) => void; 13 | onLogoutSuccess?: (result: any) => void; 14 | provider: ISocialLoginProvider; 15 | className?: string; 16 | redirect?: string; 17 | scope?: [] | string; 18 | [key: string]: any; 19 | } 20 | 21 | export default function SocialLogin( 22 | WrappedComponent: React.ComponentType, 23 | ): React.ComponentType; 24 | } 25 | -------------------------------------------------------------------------------- /fe/utils/TextHelper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} input 3 | */ 4 | export const GetFirstLetters = (input: string): string => { 5 | if (!input) { 6 | return ""; 7 | } 8 | const letters = input.split(" "); 9 | let output = ""; 10 | letters.map((x) => output += x[0]); 11 | return output; 12 | 13 | }; 14 | 15 | /** 16 | * @param {number} current 17 | * @param {number} max 18 | * @param {string} onExceed 19 | * @param {number} min? 20 | * @param {string} onShort? 21 | */ 22 | export const GetHelperText = ( 23 | current: number, 24 | max: number, 25 | onExceed: string, 26 | min?: number, 27 | onShort?: string, 28 | ): string => { 29 | 30 | if (current > max) { 31 | return `${onExceed} (${max})`; 32 | } 33 | 34 | if (min !== null && onShort !== null) { 35 | if (current > 0 && current < min) { 36 | return `${onShort} (<${min})`; 37 | } 38 | } 39 | 40 | return `${current}/${max}`; 41 | }; 42 | 43 | /** 44 | * @param {number} current 45 | * @param {number} max 46 | * @param {number} min? 47 | */ 48 | export const IsTextfieldError = ( 49 | current: number, 50 | max: number, 51 | min?: number, 52 | ): boolean => { 53 | 54 | if (current > max) { 55 | return true; 56 | } 57 | 58 | if (min !== null) { 59 | if (current > 0 && current < min) { 60 | return true; 61 | } 62 | } 63 | 64 | return false; 65 | }; 66 | -------------------------------------------------------------------------------- /fe/utils/cookieHelper.ts: -------------------------------------------------------------------------------- 1 | import Cookies from "universal-cookie"; 2 | 3 | export default class CookieHelper { 4 | static getDefaultTheme = (): string => "dark"; 5 | 6 | cookies = new Cookies(); 7 | 8 | getThemeName = (): string => { 9 | const theme = this.cookies.get("theme"); 10 | if (!theme) { 11 | const setVal = CookieHelper.getDefaultTheme(); 12 | this.setTheme(setVal); 13 | return setVal; 14 | } 15 | 16 | return theme; 17 | } 18 | 19 | setTheme = (theme: string): void => { 20 | this.cookies.set("theme", theme, { path: "/", maxAge: 60 * 60 * 24 }); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /fe/utils/dateHelper.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | export default class DateHelper { 4 | 5 | isBetween = (main: Date, min: Date, max: Date): boolean => { 6 | return (min <= main && main <= max); 7 | } 8 | 9 | isChristmasTime = (): boolean => { 10 | const today = new Date(); 11 | const year = today.getFullYear(); 12 | 13 | // is between 1.1.year and 6.1.year or 14 | // is between 10.12.year and 31.12.year 15 | return this.isBetween(today, new Date(year, 1 - 1, 1), new Date(year, 1 - 1, 6)) || 16 | this.isBetween(today, new Date(year, 12 - 1, 10), new Date(year, 12 - 1, 31)); 17 | } 18 | 19 | isPrideMonth = (): boolean => { 20 | const today = new Date(); 21 | const year = today.getFullYear(); 22 | 23 | // is between 1.6 and 30.6 24 | return this.isBetween(today, new Date(year, 6 - 1, 1), new Date(year, 6 - 1, 30)); 25 | } 26 | 27 | getFormattedDate = (date: string, relative: "strict" | "relative" | "fromNow"): string => { 28 | switch (relative) { 29 | case "strict": 30 | return moment(date).format("DD.MM.YYYY, hh:mm"); 31 | case "relative": 32 | return moment(date).calendar(null, { 33 | sameDay: "[Dnes]", 34 | lastDay: "[Včera]", 35 | lastWeek: "[Minulý týden]", 36 | sameElse: "DD.MM.YYYY", 37 | }); 38 | case "fromNow": 39 | return moment(date).fromNow(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/.flutter-plugins-dependencies: -------------------------------------------------------------------------------- 1 | {"dependencyGraph":[{"name":"path_provider","dependencies":[]}]} -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .classpath 21 | .project 22 | .settings/ 23 | .vscode/ 24 | 25 | # Flutter repo-specific 26 | /bin/cache/ 27 | /bin/mingit/ 28 | /dev/benchmarks/mega_gallery/ 29 | /dev/bots/.recipe_deps 30 | /dev/bots/android_tools/ 31 | /dev/docs/doc/ 32 | /dev/docs/flutter.docs.zip 33 | /dev/docs/lib/ 34 | /dev/docs/pubspec.yaml 35 | /dev/integration_tests/**/xcuserdata 36 | /dev/integration_tests/**/Pods 37 | /packages/flutter/coverage/ 38 | version 39 | 40 | # packages file containing multi-root paths 41 | .packages.generated 42 | 43 | # Flutter/Dart/Pub related 44 | **/doc/api/ 45 | .dart_tool/ 46 | .flutter-plugins 47 | .flutter-plugins-dependencies 48 | .packages 49 | .pub-cache/ 50 | .pub/ 51 | build/ 52 | flutter_*.png 53 | linked_*.ds 54 | unlinked.ds 55 | unlinked_spec.ds 56 | 57 | # Android related 58 | **/android/**/gradle-wrapper.jar 59 | **/android/.gradle 60 | **/android/captures/ 61 | **/android/gradlew 62 | **/android/gradlew.bat 63 | **/android/local.properties 64 | **/android/**/GeneratedPluginRegistrant.java 65 | **/android/key.properties 66 | *.jks 67 | 68 | # iOS/XCode related 69 | **/ios/**/*.mode1v3 70 | **/ios/**/*.mode2v3 71 | **/ios/**/*.moved-aside 72 | **/ios/**/*.pbxuser 73 | **/ios/**/*.perspectivev3 74 | **/ios/**/*sync/ 75 | **/ios/**/.sconsign.dblite 76 | **/ios/**/.tags* 77 | **/ios/**/.vagrant/ 78 | **/ios/**/DerivedData/ 79 | **/ios/**/Icon? 80 | **/ios/**/Pods/ 81 | **/ios/**/.symlinks/ 82 | **/ios/**/profile 83 | **/ios/**/xcuserdata 84 | **/ios/.generated/ 85 | **/ios/Flutter/App.framework 86 | **/ios/Flutter/Flutter.framework 87 | **/ios/Flutter/Flutter.podspec 88 | **/ios/Flutter/Generated.xcconfig 89 | **/ios/Flutter/app.flx 90 | **/ios/Flutter/app.zip 91 | **/ios/Flutter/flutter_assets/ 92 | **/ios/Flutter/flutter_export_environment.sh 93 | **/ios/ServiceDefinitions.json 94 | **/ios/Runner/GeneratedPluginRegistrant.* 95 | 96 | # Coverage 97 | coverage/ 98 | 99 | # Exceptions to above rules. 100 | !**/ios/**/default.mode1v3 101 | !**/ios/**/default.mode2v3 102 | !**/ios/**/default.pbxuser 103 | !**/ios/**/default.perspectivev3 104 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 105 | !/dev/ci/**/Gemfile.lock -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 2d2a1ffec95cc70a3218872a2cd3f8de4933c42f 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/README.md: -------------------------------------------------------------------------------- 1 | # helpdesk_mobile 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.example.helpdesk_mobile" 42 | minSdkVersion 16 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 47 | } 48 | 49 | buildTypes { 50 | release { 51 | // TODO: Add your own signing config for the release build. 52 | // Signing with the debug keys for now, so `flutter run --release` works. 53 | signingConfig signingConfigs.debug 54 | } 55 | } 56 | } 57 | 58 | flutter { 59 | source '../..' 60 | } 61 | 62 | dependencies { 63 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 64 | testImplementation 'junit:junit:4.12' 65 | androidTestImplementation 'androidx.test:runner:1.1.1' 66 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 67 | } 68 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 13 | 20 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/app/src/main/kotlin/com/example/helpdesk_mobile/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.helpdesk_mobile 2 | 3 | import android.os.Bundle 4 | 5 | import io.flutter.app.FlutterActivity 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity: FlutterActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | GeneratedPluginRegistrant.registerWith(this) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.2.71' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.2.1' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | android.enableR8=true 6 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 7 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeltaHelpDesk/helpdesk/1f7c9bafe33fc5585b8fc944f0e13c287c1c6710/flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | helpdesk_mobile 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'src/app.dart'; 3 | import 'package:graphql_flutter/graphql_flutter.dart'; 4 | 5 | void main() { 6 | final HttpLink httpLink = HttpLink( 7 | uri: "https://delta-helpdesk.herokuapp.com/graphql", 8 | headers: { 9 | 'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjExLCJhdXRoVHlwZSI6IkVNQUlMIiwiaXNzdWVkIjoiMjAxOS0xMi0wNFQxMDo0OTozNC45ODhaIiwiaWF0IjoxNTc1NDU2NTc0LCJleHAiOjE1NzU2MjkzNzR9.UO5K2FLBwL2GXIXD2QvOLQBQ1q_VfgTL5bib1L-hDSs', 10 | }, 11 | ); 12 | final ValueNotifier client = ValueNotifier( 13 | GraphQLClient( 14 | cache: InMemoryCache(), 15 | link: httpLink as Link, 16 | ), 17 | ); 18 | runApp(HelpdeskApp(client)); 19 | } 20 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/lib/src/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'ui/pages/tasklist.dart'; 3 | import 'package:graphql_flutter/graphql_flutter.dart'; 4 | 5 | class HelpdeskApp extends StatelessWidget { 6 | final ValueNotifier client; 7 | HelpdeskApp(ValueNotifier client) : client = client; 8 | @override 9 | Widget build(BuildContext context) { 10 | return GraphQLProvider( 11 | client: client, 12 | child: MaterialApp( 13 | title: 'Helpdesk - Delta', 14 | theme: ThemeData.light(), 15 | home: Scaffold( 16 | body: TaskList(), 17 | ), 18 | ), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/lib/src/models/authenticatedUserType.dart: -------------------------------------------------------------------------------- 1 | import './enums.dart'; 2 | 3 | class AuthenticatedUserType { 4 | String id; 5 | String fullName; 6 | String email; 7 | DateTime created_at; 8 | DateTime updated_at; 9 | String token; 10 | UserRole role; 11 | 12 | AuthenticatedUserType.fromJson(Map json) 13 | : id = json['id'], 14 | fullName = json['fullName'], 15 | email = json['email'], 16 | created_at = (json['created_at'] == null) 17 | ? null 18 | : DateTime.parse(json['created_at']), 19 | updated_at = (json['created_at'] == null) 20 | ? null 21 | : DateTime.parse(json['created_at']), 22 | token = json['token'], 23 | role = json['role'] == null 24 | ? null 25 | : json['role'] == 'ADMIN' 26 | ? UserRole.ADMIN 27 | : json['role'] == 'DEFAULT' 28 | ? UserRole.DEFAULT 29 | : UserRole.SUPERADMIN; 30 | } 31 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/lib/src/models/enums.dart: -------------------------------------------------------------------------------- 1 | enum UserRole { 2 | DEFAULT, 3 | ADMIN, 4 | SUPERADMIN 5 | } 6 | 7 | enum Status { 8 | UNRESOLVED, 9 | SOLVING, 10 | SOLVED, 11 | RETURNED 12 | } 13 | 14 | enum AuthType { 15 | EMAIL, 16 | GOOGLE, 17 | FACEBOOK, 18 | MICROSOFT 19 | } -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/lib/src/models/logType.dart: -------------------------------------------------------------------------------- 1 | import './enums.dart'; 2 | import './userType.dart'; 3 | 4 | class LogType { 5 | String id; 6 | UserType author; 7 | DateTime created_at; 8 | String comment; 9 | Status state; 10 | UserType assignee; 11 | 12 | LogType.fromJson(Map json) 13 | : id = json['id'], 14 | author = UserType.fromJson(json['author']), 15 | created_at = (json['created_at'] == null) 16 | ? null 17 | : DateTime.parse(json['created_at']), 18 | comment = json['comment'], 19 | state = json['state'] == null 20 | ? null 21 | : json['state'] == 'SOLVED' 22 | ? Status.SOLVED 23 | : json['state'] == 'RETURNED' 24 | ? Status.RETURNED 25 | : json['state'] == 'SOLVING' ? Status.SOLVING : Status.UNRESOLVED, 26 | assignee = UserType.fromJson(json['assignee']); 27 | } 28 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/lib/src/models/taskType.dart: -------------------------------------------------------------------------------- 1 | import './userType.dart'; 2 | import './enums.dart'; 3 | import './logType.dart'; 4 | 5 | class TaskType { 6 | String id; 7 | String subject; 8 | String issue; 9 | UserType author; 10 | UserType assignee; 11 | DateTime created_at; 12 | DateTime updated_at; 13 | Status state; 14 | List logs; 15 | 16 | TaskType(); 17 | static TaskType fromJson(Map json) { 18 | var task = new TaskType(); 19 | var logs = new List(); 20 | /* 21 | for (var log in json['logs']) { 22 | logs.add(LogType.fromJson(log)); 23 | } */ 24 | task.id = json['id']; 25 | task.subject = json['subject']; 26 | task.issue = json['issue']; 27 | task.author = json['author'] == null ? null : UserType.fromJson(json['author']); 28 | task.assignee = json['assignee'] == null ? null : UserType.fromJson(json['assignee']); 29 | task.created_at = (json['created_at'] == null) 30 | ? null 31 | : DateTime.parse(json['created_at']); 32 | task.updated_at = (json['updated_at'] == null) 33 | ? null 34 | : DateTime.parse(json['updated_at']); 35 | task.state = json['state'] == null 36 | ? null 37 | : json['state'] == 'SOLVED' 38 | ? Status.SOLVED 39 | : json['state'] == 'RETURNED' 40 | ? Status.RETURNED 41 | : json['state'] == 'SOLVING' ? Status.SOLVING : Status.UNRESOLVED; 42 | task.logs = logs; 43 | 44 | return task; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/lib/src/models/userType.dart: -------------------------------------------------------------------------------- 1 | import './enums.dart'; 2 | 3 | class UserType { 4 | String id; 5 | String fullName; 6 | String email; 7 | DateTime created_at; 8 | DateTime updated_at; 9 | UserRole role; 10 | 11 | UserType.fromJson(Map json) 12 | : id = json['id'] == null ? null : json['id'], 13 | fullName = json['fullName'] == null ? null : json['fullName'], 14 | email = json['email'] == null ? null : json['email'], 15 | created_at = (json['created_at'] == null) 16 | ? null 17 | : DateTime.parse(json['created_at']), 18 | updated_at = (json['updated_at'] == null) 19 | ? null 20 | : DateTime.parse(json['updated_at']), 21 | role = json['role'] == null 22 | ? null 23 | : json['role'] == 'ADMIN' 24 | ? UserRole.ADMIN 25 | : json['role'] == 'DEFAULT' 26 | ? UserRole.DEFAULT 27 | : UserRole.SUPERADMIN; 28 | } 29 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/lib/src/ui/components/taskList/sections.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:helpdesk_mobile/src/models/taskType.dart'; 3 | import 'package:helpdesk_mobile/src/ui/components/taskList/taskCard.dart'; 4 | import 'package:helpdesk_mobile/src/models/enums.dart'; 5 | 6 | class Sections extends StatefulWidget { 7 | final List tasks; 8 | Sections({Key key, this.tasks}) : super(key: key); 9 | 10 | @override 11 | State createState() => _SectionsState(tasks: this.tasks); 12 | } 13 | 14 | class _SectionsState extends State { 15 | PageController pageController; 16 | 17 | final List tasks; 18 | Map> sectionedTasks = Map>(); 19 | 20 | _SectionsState({this.tasks}) { 21 | sectionedTasks['Nezapočato'] = 22 | tasks.where((task) => task.state == Status.UNRESOLVED).toList(); 23 | sectionedTasks['Pracuje se na tom'] = 24 | tasks.where((task) => task.state == Status.SOLVING).toList(); 25 | sectionedTasks['Dokončeno'] = 26 | tasks.where((task) => task.state == Status.SOLVED).toList(); 27 | } 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | pageController = PageController(initialPage: 0, viewportFraction: 0.8); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return PageView.builder( 38 | controller: pageController, 39 | itemCount: this.sectionedTasks.length, 40 | itemBuilder: (context, position) { 41 | return SafeArea( 42 | child: Card( 43 | color: Color.fromRGBO(245, 245, 245, 1), 44 | child: Padding( 45 | padding: EdgeInsets.only(top: 10), 46 | child: ListView( 47 | children: [ 48 | Center( 49 | child: Text( 50 | sectionedTasks.keys.elementAt(position).toUpperCase(), 51 | style: TextStyle( 52 | fontWeight: FontWeight.w500, 53 | ), 54 | ), 55 | ), 56 | Center( 57 | child: Column( 58 | children: List.generate( 59 | sectionedTasks[sectionedTasks.keys.elementAt(position)] 60 | .length, 61 | (index) { 62 | return TaskCard(sectionedTasks[ 63 | sectionedTasks.keys.elementAt(position)][index]); 64 | }, 65 | ), 66 | ), 67 | ), 68 | ], 69 | ), 70 | ), 71 | ), 72 | ); 73 | }, 74 | ); 75 | } 76 | 77 | @override 78 | void dispose() { 79 | pageController.dispose(); 80 | super.dispose(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/lib/src/ui/components/taskList/taskCard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:helpdesk_mobile/src/models/taskType.dart'; 3 | 4 | class TaskCard extends StatelessWidget { 5 | final TaskType task; 6 | TaskCard(this.task); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Padding( 11 | padding: EdgeInsets.fromLTRB(15, 10, 15, 0), 12 | child: Card( 13 | child: Container( 14 | height: 100, 15 | padding: EdgeInsets.all(10), 16 | child: Column( 17 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 18 | crossAxisAlignment: CrossAxisAlignment.start, 19 | children: [ 20 | Text( 21 | task.subject, 22 | textAlign: TextAlign.left, 23 | style: TextStyle(fontWeight: FontWeight.w500), 24 | ), 25 | Row( 26 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 27 | children: [ 28 | Text( 29 | "${task.created_at.day}/${task.created_at.month}", 30 | style: TextStyle(color: Colors.grey), 31 | ), 32 | Text( 33 | task.author.fullName, 34 | style: TextStyle(color: Colors.grey), 35 | ) 36 | ], 37 | ), 38 | ], 39 | ), 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/lib/src/ui/pages/createTask.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CreateTask extends StatelessWidget{ 4 | @override 5 | Widget build(BuildContext context) { 6 | return Text("Create task works"); 7 | } 8 | } -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/lib/src/ui/pages/login.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Login extends StatelessWidget{ 4 | @override 5 | Widget build(BuildContext context) { 6 | return Text("Login works"); 7 | } 8 | } -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/lib/src/ui/pages/taskDetail.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TaskDetail extends StatelessWidget{ 4 | @override 5 | Widget build(BuildContext context) { 6 | return Text("Task detail works"); 7 | } 8 | } -------------------------------------------------------------------------------- /flutter/helpdesk_mobile/lib/src/ui/pages/tasklist.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:graphql_flutter/graphql_flutter.dart'; 3 | import 'package:helpdesk_mobile/src/models/taskType.dart'; 4 | import '../components/taskList/sections.dart'; 5 | 6 | class TaskList extends StatelessWidget { 7 | final String tasks = r''' query getTasks{ 8 | tasks { 9 | id 10 | subject 11 | issue 12 | author { 13 | id 14 | fullName 15 | email 16 | created_at 17 | updated_at 18 | role 19 | } 20 | assignee { 21 | id 22 | fullName 23 | email 24 | created_at 25 | updated_at 26 | role 27 | } 28 | created_at 29 | updated_at 30 | state 31 | logs { 32 | id 33 | author { 34 | id 35 | fullName 36 | email 37 | created_at 38 | updated_at 39 | role 40 | } 41 | created_at 42 | comment 43 | state 44 | assignee { 45 | id 46 | fullName 47 | email 48 | created_at 49 | updated_at 50 | role 51 | } 52 | } 53 | } 54 | } 55 | '''; 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | return Query( 60 | options: QueryOptions( 61 | document: tasks, 62 | ), 63 | builder: (QueryResult result, {VoidCallback refetch}) { 64 | if (result.errors != null) { 65 | print(result.data['tasks']); 66 | return Text(result.errors.toString()); 67 | } 68 | 69 | if (result.loading) { 70 | return Center( 71 | child: CircularProgressIndicator(), 72 | ); 73 | } 74 | 75 | if (result.data == null) { 76 | return Center(child: Text("No Data Found!")); 77 | } 78 | 79 | var tasks = result.data['tasks']; 80 | var parsedTasks = List(); 81 | tasks.forEach((task) => parsedTasks.add(TaskType.fromJson(task))); 82 | return SafeArea( 83 | child: Sections( 84 | tasks: parsedTasks, 85 | ), 86 | ); 87 | }, 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # docker-compose run --no-deps backend yarn install 3 | # docker-compose run --no-deps frontend yarn install 4 | # docker-compose run --no-deps mobile yarn install 5 | 6 | cp -i ./dckr/env/mysql-example.env ./dckr/env/mysql.env 7 | cp -i ./dckr/mysql/docker-entrypoint-initdb.d/createdb.sql.example ./dckr/mysql/docker-entrypoint-initdb.d/createdb.sql 8 | cp -i ./.env-example ./.env 9 | # cp -i ./frontend/env-example ./frontend/.env 10 | cp -i ./backend/env-example ./backend/.env 11 | --------------------------------------------------------------------------------