├── .bowerrc ├── .dockerignore ├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── release-image.yml │ ├── release-npm.yml │ └── test.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .prettierignore ├── .template-lintrc-ci.js ├── .template-lintrc.js ├── .watchmanconfig ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── abilities │ ├── absence-credit.js │ ├── overtime-credit.js │ ├── page.js │ ├── report.js │ └── user.js ├── adapters │ ├── activity-block.js │ └── application.js ├── analysis │ ├── edit │ │ ├── controller.js │ │ ├── route.js │ │ └── template.hbs │ ├── index │ │ ├── controller.js │ │ ├── route.js │ │ └── template.hbs │ └── route.js ├── app.js ├── application │ ├── route.js │ └── template.hbs ├── breakpoints.js ├── components │ ├── async-list │ │ └── template.hbs │ ├── attendance-slider │ │ ├── component.js │ │ └── template.hbs │ ├── balance-donut │ │ ├── component.js │ │ └── template.hbs │ ├── changed-warning │ │ └── template.hbs │ ├── customer-visible-icon │ │ └── template.hbs │ ├── date-buttons │ │ ├── component.js │ │ └── template.hbs │ ├── date-navigation │ │ ├── component.js │ │ └── template.hbs │ ├── duration-since │ │ ├── component.js │ │ └── template.hbs │ ├── filter-sidebar │ │ ├── component.js │ │ ├── filter │ │ │ └── template.hbs │ │ ├── group │ │ │ ├── component.js │ │ │ ├── styles.scss │ │ │ └── template.hbs │ │ ├── label │ │ │ └── template.hbs │ │ └── template.hbs │ ├── in-viewport │ │ ├── component.js │ │ └── template.hbs │ ├── loading-icon │ │ └── template.hbs │ ├── magic-link-btn │ │ ├── component.js │ │ └── template.hbs │ ├── magic-link-modal │ │ ├── component.js │ │ └── template.hbs │ ├── no-mobile-message │ │ └── template.hbs │ ├── no-permission │ │ └── template.hbs │ ├── not-identical-warning │ │ └── template.hbs │ ├── optimized-power-select │ │ ├── component.js │ │ ├── custom-options │ │ │ ├── customer-option.hbs │ │ │ ├── project-option.hbs │ │ │ ├── task-option.hbs │ │ │ └── user-option.hbs │ │ ├── custom-select │ │ │ ├── task-selection.hbs │ │ │ └── user-selection.hbs │ │ ├── options │ │ │ ├── component.js │ │ │ └── template.hbs │ │ ├── template.hbs │ │ └── trigger │ │ │ ├── component.js │ │ │ └── template.hbs │ ├── page-permission │ │ └── template.hbs │ ├── progress-tooltip │ │ ├── component.js │ │ └── template.hbs │ ├── record-button │ │ ├── component.js │ │ └── template.hbs │ ├── report-review-warning │ │ ├── component.js │ │ └── template.hbs │ ├── report-row │ │ ├── component.js │ │ └── template.hbs │ ├── scroll-container.hbs │ ├── sort-header │ │ ├── component.js │ │ └── template.hbs │ ├── statistic-list │ │ ├── bar │ │ │ ├── component.js │ │ │ └── template.hbs │ │ ├── column │ │ │ └── template.hbs │ │ ├── component.js │ │ └── template.hbs │ ├── sy-calendar │ │ ├── component.js │ │ ├── styles.scss │ │ └── template.hbs │ ├── sy-checkbox │ │ ├── component.js │ │ └── template.hbs │ ├── sy-checkmark │ │ ├── component.js │ │ └── template.hbs │ ├── sy-datepicker-btn │ │ ├── component.js │ │ └── template.hbs │ ├── sy-datepicker │ │ ├── component.js │ │ └── template.hbs │ ├── sy-durationpicker-day │ │ ├── component.js │ │ └── template.hbs │ ├── sy-durationpicker │ │ ├── component.js │ │ └── template.hbs │ ├── sy-modal-target │ │ └── template.hbs │ ├── sy-modal │ │ ├── body │ │ │ ├── styles.scss │ │ │ └── template.hbs │ │ ├── footer │ │ │ └── template.hbs │ │ ├── header │ │ │ └── template.hbs │ │ ├── overlay │ │ │ ├── component.js │ │ │ └── template.hbs │ │ └── template.hbs │ ├── sy-timepicker │ │ ├── component.js │ │ └── template.hbs │ ├── sy-toggle │ │ ├── component.js │ │ └── template.hbs │ ├── sy-topnav │ │ ├── component.js │ │ └── template.hbs │ ├── task-selection │ │ ├── component.js │ │ └── template.hbs │ ├── timed-clock │ │ ├── component.js │ │ └── template.hbs │ ├── tracking-bar │ │ ├── component.js │ │ └── template.hbs │ ├── user-selection │ │ ├── component.js │ │ └── template.hbs │ ├── vertical-collection │ │ └── component.js │ ├── weekly-overview-benchmark │ │ ├── component.js │ │ └── template.hbs │ ├── weekly-overview-day │ │ ├── component.js │ │ └── template.hbs │ ├── weekly-overview │ │ ├── component.js │ │ └── template.hbs │ ├── welcome-modal │ │ └── template.hbs │ └── worktime-balance-chart │ │ ├── component.js │ │ └── template.hbs ├── controllers │ └── qpcontroller.js ├── helpers │ ├── balance-highlight-class.js │ ├── format-duration.js │ ├── humanize-duration.js │ └── parse-django-duration.js ├── index.html ├── index │ ├── activities │ │ ├── controller.js │ │ ├── edit │ │ │ ├── controller.js │ │ │ ├── route.js │ │ │ └── template.hbs │ │ ├── route.js │ │ └── template.hbs │ ├── attendances │ │ ├── controller.js │ │ ├── route.js │ │ └── template.hbs │ ├── controller.js │ ├── reports │ │ ├── controller.js │ │ ├── route.js │ │ └── template.hbs │ ├── route.js │ └── template.hbs ├── initializers │ └── responsive.js ├── login │ ├── route.js │ └── template.hbs ├── models │ ├── absence-balance.js │ ├── absence-credit.js │ ├── absence-type.js │ ├── absence.js │ ├── activity.js │ ├── attendance.js │ ├── billing-type.js │ ├── cost-center.js │ ├── customer-assignee.js │ ├── customer-statistic.js │ ├── customer.js │ ├── employment.js │ ├── location.js │ ├── month-statistic.js │ ├── overtime-credit.js │ ├── project-assignee.js │ ├── project-statistic.js │ ├── project.js │ ├── public-holiday.js │ ├── report-intersection.js │ ├── report.js │ ├── task-assignee.js │ ├── task-statistic.js │ ├── task.js │ ├── user-statistic.js │ ├── user.js │ ├── worktime-balance.js │ └── year-statistic.js ├── no-access │ ├── route.js │ └── template.hbs ├── notfound │ ├── route.js │ └── template.hbs ├── projects │ ├── controller.js │ ├── route.js │ └── template.hbs ├── protected │ ├── controller.js │ ├── route.js │ └── template.hbs ├── router.js ├── serializers │ ├── application.js │ ├── attendance.js │ └── employment.js ├── services │ ├── autostart-tour.js │ ├── fetch.js │ ├── metadata-fetcher.js │ ├── rejected-reports.js │ ├── tour.js │ ├── tracking.js │ └── unverified-reports.js ├── sso-login │ └── route.js ├── statistics │ ├── controller.js │ ├── route.js │ └── template.hbs ├── styles │ ├── activities.scss │ ├── adcssy.scss │ ├── analysis.scss │ ├── app.scss │ ├── attendances.scss │ ├── badge.scss │ ├── components │ │ ├── attendance-slider.scss │ │ ├── balance-donut.scss │ │ ├── date-buttons.scss │ │ ├── date-navigation.scss │ │ ├── filter-sidebar--group.scss │ │ ├── filter-sidebar--label.scss │ │ ├── loading-icon.scss │ │ ├── magic-link-btn.scss │ │ ├── nav-top.scss │ │ ├── progress-tooltip.scss │ │ ├── record-button.scss │ │ ├── scroll-container.scss │ │ ├── sort-header.scss │ │ ├── statistic-list-bar.scss │ │ ├── sy-calendar.scss │ │ ├── sy-checkbox.scss │ │ ├── sy-datepicker.scss │ │ ├── sy-durationpicker-day.scss │ │ ├── sy-modal--footer.scss │ │ ├── sy-modal--overlay.scss │ │ ├── sy-toggle.scss │ │ ├── timed-clock.scss │ │ ├── tracking-bar.scss │ │ ├── weekly-overview-benchmark.scss │ │ ├── weekly-overview-day.scss │ │ ├── weekly-overview.scss │ │ └── welcome-modal.scss │ ├── ember-power-select-custom.scss │ ├── filter-sidebar.scss │ ├── form-list.scss │ ├── loader.scss │ ├── login.scss │ ├── projects.scss │ ├── reports.scss │ ├── statistics.scss │ ├── toolbar.scss │ ├── tour.scss │ ├── users-navigation.scss │ ├── users.scss │ └── variables.scss ├── tours │ ├── index.js │ └── index │ │ ├── activities.js │ │ ├── attendances.js │ │ └── reports.js ├── transforms │ ├── django-date.js │ ├── django-datetime.js │ ├── django-duration.js │ ├── django-time.js │ ├── django-workdays.js │ └── moment.js ├── users │ ├── edit │ │ ├── controller.js │ │ ├── credits │ │ │ ├── absence-credits │ │ │ │ ├── edit │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ └── new │ │ │ │ │ └── route.js │ │ │ ├── index │ │ │ │ ├── controller.js │ │ │ │ ├── route.js │ │ │ │ └── template.hbs │ │ │ ├── overtime-credits │ │ │ │ ├── edit │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── route.js │ │ │ │ │ └── template.hbs │ │ │ │ └── new │ │ │ │ │ └── route.js │ │ │ ├── route.js │ │ │ └── template.hbs │ │ ├── index │ │ │ ├── controller.js │ │ │ ├── route.js │ │ │ └── template.hbs │ │ ├── responsibilities │ │ │ ├── controller.js │ │ │ ├── route.js │ │ │ └── template.hbs │ │ ├── route.js │ │ └── template.hbs │ ├── index │ │ ├── controller.js │ │ ├── route.js │ │ └── template.hbs │ ├── route.js │ └── template.hbs ├── utils │ ├── format-duration.js │ ├── humanize-duration.js │ ├── parse-django-duration.js │ ├── query-params.js │ ├── serialize-moment.js │ └── url.js ├── validations │ ├── absence-credit.js │ ├── absence.js │ ├── activity.js │ ├── attendance.js │ ├── intersection.js │ ├── multiple-absence.js │ ├── overtime-credit.js │ ├── project.js │ ├── report.js │ └── task.js └── validators │ ├── intersection-task.js │ ├── moment.js │ └── null-or-not-blank.js ├── config ├── coverage.js ├── dependency-lint.js ├── deprecation-workflow.js ├── ember-cli-update.json ├── environment.js ├── icons.js ├── optional-features.json └── targets.js ├── contrib └── nginx.conf ├── docker-compose.yml ├── docker-entrypoint.sh ├── ember-cli-build.js ├── mirage ├── config.js ├── factories │ ├── absence-balance.js │ ├── absence-credit.js │ ├── absence-type.js │ ├── absence.js │ ├── activity.js │ ├── attendance.js │ ├── billing-type.js │ ├── cost-center.js │ ├── customer-statistic.js │ ├── customer.js │ ├── employment.js │ ├── location.js │ ├── month-statistic.js │ ├── overtime-credit.js │ ├── project-assignee.js │ ├── project-statistic.js │ ├── project.js │ ├── public-holiday.js │ ├── report-intersection.js │ ├── report.js │ ├── task-statistic.js │ ├── task.js │ ├── user-statistic.js │ ├── user.js │ ├── worktime-balance.js │ └── year-statistic.js ├── fixtures │ └── absence-types.js ├── helpers │ └── duration.js ├── scenarios │ └── default.js └── serializers │ └── application.js ├── package.json ├── pnpm-lock.yaml ├── public ├── assets │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── logo.png │ ├── logo.svg │ └── logo_text.png ├── crossdomain.xml └── robots.txt ├── renovate.json ├── testem.js ├── tests ├── .eslintrc.js ├── acceptance │ ├── analysis-edit-test.js │ ├── analysis-test.js │ ├── auth-test.js │ ├── external-employee-test.js │ ├── index-activities-edit-test.js │ ├── index-activities-test.js │ ├── index-attendances-test.js │ ├── index-reports-test.js │ ├── index-test.js │ ├── magic-link-test.js │ ├── notfound-test.js │ ├── project-test.js │ ├── statistics-test.js │ ├── tour-test.js │ ├── users-edit-credits-absence-credit-test.js │ ├── users-edit-credits-overtime-credit-test.js │ ├── users-edit-credits-test.js │ ├── users-edit-responsibilities-test.js │ ├── users-edit-test.js │ └── users-test.js ├── helpers │ ├── index.js │ ├── responsive.js │ ├── session-mock.js │ ├── task-select.js │ ├── tracking-mock.js │ └── user-select.js ├── index.html ├── integration │ └── components │ │ ├── async-list │ │ └── component-test.js │ │ ├── attendance-slider │ │ └── component-test.js │ │ ├── balance-donut │ │ └── component-test.js │ │ ├── changed-warning │ │ └── component-test.js │ │ ├── customer-visible-icon │ │ └── component-test.js │ │ ├── date-buttons │ │ └── component-test.js │ │ ├── date-navigation │ │ └── component-test.js │ │ ├── duration-since │ │ └── component-test.js │ │ ├── filter-sidebar │ │ ├── component-test.js │ │ ├── filter │ │ │ └── component-test.js │ │ ├── group │ │ │ └── component-test.js │ │ └── label │ │ │ └── component-test.js │ │ ├── in-viewport │ │ └── component-test.js │ │ ├── loading-icon │ │ └── component-test.js │ │ ├── no-mobile-message │ │ └── component-test.js │ │ ├── no-permission │ │ └── component-test.js │ │ ├── not-identical-warning │ │ └── component-test.js │ │ ├── optimized-power-select │ │ └── component-test.js │ │ ├── progress-tooltip │ │ └── component-test.js │ │ ├── record-button │ │ └── component-test.js │ │ ├── report-review-warning │ │ └── component-test.js │ │ ├── report-row │ │ └── component-test.js │ │ ├── sort-header │ │ └── component-test.js │ │ ├── statistic-list │ │ ├── bar │ │ │ └── component-test.js │ │ ├── column │ │ │ └── component-test.js │ │ └── component-test.js │ │ ├── sy-calendar │ │ └── component-test.js │ │ ├── sy-checkbox │ │ └── component-test.js │ │ ├── sy-checkmark │ │ └── component-test.js │ │ ├── sy-datepicker-btn │ │ └── component-test.js │ │ ├── sy-datepicker │ │ └── component-test.js │ │ ├── sy-durationpicker-day │ │ └── component-test.js │ │ ├── sy-durationpicker │ │ └── component-test.js │ │ ├── sy-modal-target │ │ └── component-test.js │ │ ├── sy-modal │ │ ├── body │ │ │ └── component-test.js │ │ ├── component-test.js │ │ ├── footer │ │ │ └── component-test.js │ │ ├── header │ │ │ └── component-test.js │ │ └── overlay │ │ │ └── component-test.js │ │ ├── sy-timepicker │ │ └── component-test.js │ │ ├── sy-toggle │ │ └── component-test.js │ │ ├── sy-topnav │ │ └── component-test.js │ │ ├── task-selection │ │ └── component-test.js │ │ ├── timed-clock │ │ └── component-test.js │ │ ├── tracking-bar │ │ └── component-test.js │ │ ├── user-selection │ │ └── component-test.js │ │ ├── weekly-overview-benchmark │ │ └── component-test.js │ │ ├── weekly-overview-day │ │ └── component-test.js │ │ ├── weekly-overview │ │ └── component-test.js │ │ ├── welcome-modal │ │ └── component-test.js │ │ └── worktime-balance-chart │ │ └── component-test.js ├── test-helper.js └── unit │ ├── abilities │ └── report-test.js │ ├── analysis │ ├── edit │ │ ├── controller-test.js │ │ └── route-test.js │ ├── index │ │ ├── controller-test.js │ │ └── route-test.js │ └── route-test.js │ ├── controllers │ └── qpcontroller │ │ └── controller-test.js │ ├── helpers │ ├── balance-highlight-class-test.js │ ├── format-duration-test.js │ ├── humanize-duration-test.js │ └── parse-django-duration-test.js │ ├── index │ ├── activities │ │ ├── controller-test.js │ │ ├── edit │ │ │ ├── controller-test.js │ │ │ └── route-test.js │ │ └── route-test.js │ ├── attendances │ │ ├── controller-test.js │ │ └── route-test.js │ ├── controller-test.js │ ├── reports │ │ ├── controller-test.js │ │ └── route-test.js │ └── route-test.js │ ├── login │ └── route-test.js │ ├── models │ ├── absence-balance-test.js │ ├── activity-test.js │ ├── attendance-test.js │ ├── billing-type-test.js │ ├── cost-center-test.js │ ├── customer-statistic-test.js │ ├── customer-test.js │ ├── employment-test.js │ ├── location-test.js │ ├── month-statistic-test.js │ ├── overtime-credit-test.js │ ├── project-statistic-test.js │ ├── project-test.js │ ├── public-holiday-test.js │ ├── report-intersection-test.js │ ├── report-test.js │ ├── task-statistic-test.js │ ├── task-test.js │ ├── user-statistic-test.js │ ├── user-test.js │ ├── worktime-balance-test.js │ └── year-statistic-test.js │ ├── no-access │ └── route-test.js │ ├── notfound │ └── route-test.js │ ├── projects │ ├── controller-test.js │ └── route-test.js │ ├── protected │ ├── controller-test.js │ └── route-test.js │ ├── serializers │ ├── attendance-test.js │ └── employment-test.js │ ├── services │ ├── autostart-tour-test.js │ ├── fetch-test.js │ ├── metadata-fetcher-test.js │ ├── rejected-reports-test.js │ ├── tracking-test.js │ └── unverified-reports-test.js │ ├── sso-login │ └── route-test.js │ ├── statistics │ ├── controller-test.js │ └── route-test.js │ ├── transforms │ ├── django-date-test.js │ ├── django-datetime-test.js │ ├── django-duration-test.js │ ├── django-time-test.js │ └── django-workdays-test.js │ ├── users │ ├── edit │ │ ├── controller-test.js │ │ ├── credits │ │ │ ├── absence-credits │ │ │ │ ├── edit │ │ │ │ │ ├── controller-test.js │ │ │ │ │ └── route-test.js │ │ │ │ └── new │ │ │ │ │ └── route-test.js │ │ │ ├── index │ │ │ │ ├── controller-test.js │ │ │ │ └── route-test.js │ │ │ ├── overtime-credits │ │ │ │ ├── edit │ │ │ │ │ ├── controller-test.js │ │ │ │ │ └── route-test.js │ │ │ │ └── new │ │ │ │ │ └── route-test.js │ │ │ └── route-test.js │ │ ├── index │ │ │ ├── controller-test.js │ │ │ └── route-test.js │ │ ├── responsibilities │ │ │ ├── controller-test.js │ │ │ └── route-test.js │ │ └── route-test.js │ ├── index │ │ ├── controller-test.js │ │ └── route-test.js │ └── route-test.js │ ├── utils │ ├── format-duration-test.js │ ├── humanize-duration-test.js │ ├── parse-django-duration-test.js │ ├── query-params-test.js │ └── url-test.js │ └── validators │ ├── moment-test.js │ └── null-or-not-blank-test.js └── vendor └── .gitkeep /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | .git/ 4 | tmp/ 5 | dist/ -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": true, 9 | 10 | /** 11 | Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript 12 | rather than JavaScript by default, when a TypeScript version of a given blueprint is available. 13 | */ 14 | "isTypeScriptProject": false 15 | } 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .*/ 17 | .eslintcache 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /bower.json.ember-try 22 | /npm-shrinkwrap.json.ember-try 23 | /package.json.ember-try 24 | /package-lock.json.ember-try 25 | 26 | # template lint ci config 27 | .template-lintrc-ci.js 28 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | "use-strict"; 2 | 3 | module.exports = { 4 | extends: ["@adfinis/eslint-config/ember-app"], 5 | rules: { 6 | "ember/no-actions-hash": "warn", 7 | "ember/no-component-lifecycle-hooks": "warn", 8 | "ember/no-mixins": "warn", 9 | "ember/no-new-mixins": "warn", 10 | "ember/no-classic-classes": "warn", 11 | "ember/no-classic-components": "warn", 12 | "ember/no-get": "warn", 13 | "ember/no-observers": "warn", 14 | "qunit/no-assert-equal": "warn", 15 | "ember/require-tagless-components": "warn", 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "friday" 8 | time: "12:00" 9 | timezone: "Europe/Zurich" 10 | - package-ecosystem: npm 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | day: "friday" 15 | time: "12:00" 16 | timezone: "Europe/Zurich" 17 | open-pull-requests-limit: 10 18 | versioning-strategy: increase 19 | -------------------------------------------------------------------------------- /.github/workflows/release-npm.yml: -------------------------------------------------------------------------------- 1 | name: Release npm package 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | release: 7 | name: Release 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | persist-credentials: false 14 | 15 | - name: Install pnpm 16 | uses: pnpm/action-setup@v2.4.0 17 | with: 18 | version: 7 19 | 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: "18" 23 | cache: "pnpm" 24 | 25 | - name: Install dependencies 26 | run: pnpm install 27 | 28 | - name: Release on NPM 29 | run: pnpm semantic-release 30 | env: 31 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 32 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /.eslintcache 14 | /connect.lock 15 | /coverage/* 16 | /libpeerconnection.log 17 | npm-debug.log* 18 | testem.log 19 | *.swp 20 | *.orig 21 | 22 | # vscode 23 | jsconfig.json 24 | 25 | /.vscode/ 26 | /.idea/ 27 | 28 | # ember-try 29 | /.node_modules.ember-try/ 30 | /bower.json.ember-try 31 | /npm-shrinkwrap.json.ember-try 32 | /package.json.ember-try 33 | /package-lock.json.ember-try 34 | /yarn.lock.ember-try 35 | 36 | # broccoli-debug 37 | /DEBUG/ 38 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # skip in CI 5 | [ -n "$CI" ] && exit 0 6 | 7 | # lint commit message 8 | pnpm commitlint --edit $1 9 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # skip in CI 5 | [ -n "$CI" ] && exit 0 6 | 7 | # lint staged files 8 | pnpm lint-staged 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # TODO: delete this when updating to pnpm v8 2 | auto-install-peers=false 3 | 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | .lint-todo/ 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /bower.json.ember-try 22 | /npm-shrinkwrap.json.ember-try 23 | /package.json.ember-try 24 | /package-lock.json.ember-try 25 | /yarn.lock.ember-try 26 | -------------------------------------------------------------------------------- /.template-lintrc-ci.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | extends: "recommended", 5 | rules: { 6 | // following rules are for temporary use only, delete when ember 4.0 ready 7 | "no-action": "warn", 8 | "no-curly-component-invocation": "warn", 9 | "no-duplicate-id": "warn", 10 | "no-link-to-positional-params": "warn", 11 | "no-link-to-tagname": "warn", 12 | "no-invalid-interactive": "warn", 13 | "no-implicit-this": "warn", 14 | "no-passed-in-event-handlers": "warn", 15 | "no-positional-data-test-selectors": "warn", 16 | "no-unknown-arguments-for-builtin-components": "warn", 17 | "no-with": "warn", 18 | "no-yield-only": "warn", 19 | "require-input-label": "warn", 20 | "require-has-block-helper": "warn", 21 | "require-presentational-children": "warn", 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | extends: "recommended", 5 | }; 6 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM danlynn/ember-cli:3.28.5 as build 2 | 3 | # pin pnpm to v7 as long as we are on the ember-cli:3.28.5 image 4 | RUN npm install -g pnpm@7 5 | 6 | COPY package.json pnpm-lock.yaml /myapp/ 7 | 8 | RUN pnpm fetch 9 | 10 | COPY . /myapp/ 11 | 12 | RUN pnpm install --frozen-lockfile --offline 13 | 14 | RUN pnpm run build --environment=production 15 | 16 | FROM nginx:alpine 17 | 18 | COPY --from=build /myapp/dist /var/www/html 19 | COPY ./contrib/nginx.conf /etc/nginx/conf.d/default.conf 20 | 21 | WORKDIR /var/www/html 22 | 23 | COPY ./docker-entrypoint.sh / 24 | ENV TIMED_SSO_CLIENT_HOST https://sso.example.com/auth/realms/example/protocol/openid-connect 25 | ENV TIMED_SSO_CLIENT_ID timed 26 | 27 | EXPOSE 80 28 | 29 | ENTRYPOINT ["/docker-entrypoint.sh"] 30 | CMD ["nginx", "-g", "daemon off;"] 31 | -------------------------------------------------------------------------------- /app/abilities/absence-credit.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | import { Ability } from "ember-can"; 3 | 4 | export default class AbsenceCreditAbility extends Ability { 5 | @service session; 6 | 7 | get user() { 8 | return this.session.data.user; 9 | } 10 | get canEdit() { 11 | return this.user.isSuperuser; 12 | } 13 | get canCreate() { 14 | return this.user.isSuperuser; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/abilities/overtime-credit.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | import { Ability } from "ember-can"; 3 | 4 | export default class OvertimeCreditAbility extends Ability { 5 | @service session; 6 | 7 | get user() { 8 | return this.session.data.user; 9 | } 10 | get canEdit() { 11 | return this.user.isSuperuser; 12 | } 13 | get canCreate() { 14 | return this.user.isSuperuser; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/abilities/page.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | import { Ability } from "ember-can"; 3 | 4 | export default class PageAbility extends Ability { 5 | @service session; 6 | 7 | get user() { 8 | return this.session.data.user; 9 | } 10 | get canAccess() { 11 | if (!this.user) { 12 | return false; 13 | } 14 | 15 | return !this.user.activeEmployment?.isExternal || this.user.isReviewer; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/abilities/report.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | import { Ability } from "ember-can"; 3 | 4 | export default class ReportAbility extends Ability { 5 | @service session; 6 | 7 | get user() { 8 | return this.session.data.user; 9 | } 10 | 11 | get canEdit() { 12 | const isEditable = 13 | this.user?.isSuperuser || 14 | (!this.model?.verifiedBy?.get("id") && 15 | // eslint-disable-next-line ember/no-get 16 | (this.model?.user?.get("id") === this.user?.get("id") || 17 | // eslint-disable-next-line ember/no-get 18 | (this.model?.user?.get("supervisors") ?? []) 19 | .mapBy("id") 20 | .includes(this.user?.get("id")))); 21 | const isReviewer = 22 | (this.model?.taskAssignees ?? []) 23 | .concat( 24 | this.model?.projectAssignees ?? [], 25 | this.model?.customerAssignees ?? [] 26 | ) 27 | .mapBy("user.id") 28 | .includes(this.user?.get("id")) && !this.model?.verifiedBy?.get("id"); 29 | return isEditable || isReviewer; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/abilities/user.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | import { Ability } from "ember-can"; 3 | 4 | export default class UserAbility extends Ability { 5 | @service session; 6 | 7 | get user() { 8 | return this.session.data.user; 9 | } 10 | 11 | get canRead() { 12 | return ( 13 | this.user?.isSuperuser || 14 | this.user?.id === this.model.id || 15 | this.model.supervisors.mapBy("id").includes(this.user?.id) 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/adapters/activity-block.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-adapters 4 | * @public 5 | */ 6 | import ApplicationAdapter from "timed/adapters/application"; 7 | 8 | /** 9 | * The activity block adapter 10 | * 11 | * @class ActivityBlockAdapter 12 | * @extends ApplicationAdapter 13 | * @public 14 | */ 15 | export default ApplicationAdapter.extend({ 16 | /** 17 | * Custom url for updating records 18 | * 19 | * This causes a reload of the activity so we don't have to do the reload 20 | * ourselves 21 | * 22 | * @method urlForUpdateRecord 23 | * @return {String} The URL 24 | * @public 25 | */ 26 | urlForUpdateRecord(...args) { 27 | return `${this._super(...args)}?include=activity`; 28 | }, 29 | 30 | /** 31 | * Custom url for creating records 32 | * 33 | * This causes a reload of the activity so we don't have to do the reload 34 | * ourselves 35 | * 36 | * @method urlForCreateRecord 37 | * @return {String} The URL 38 | * @public 39 | */ 40 | urlForCreateRecord(...args) { 41 | return `${this._super(...args)}?include=activity`; 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /app/adapters/application.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-adapters 4 | * @public 5 | */ 6 | import OIDCJSONAPIAdapter from "ember-simple-auth-oidc/adapters/oidc-json-api-adapter"; 7 | 8 | /** 9 | * The application adapter 10 | * 11 | * @class ApplicationAdapter 12 | * @extends OIDCAdapterMixin 13 | * @uses EmberSimpleAuthOIDC.OIDCAdapterMixin 14 | * @public 15 | */ 16 | export default class ApplicationAdapter extends OIDCJSONAPIAdapter { 17 | namespace = "api/v1"; 18 | } 19 | -------------------------------------------------------------------------------- /app/analysis/edit/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { resetQueryParams } from "timed/utils/query-params"; 3 | 4 | export default class AnalysisEditRoute extends Route { 5 | setupController(controller) { 6 | if (controller.id) { 7 | controller.id = controller.id.split(","); 8 | } 9 | controller.intersection.perform(); 10 | } 11 | resetController(controller, isExiting, transition) { 12 | if (isExiting && transition.targetName !== "error") { 13 | resetQueryParams(controller, controller.queryParams); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/analysis/index/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { next } from "@ember/runloop"; 3 | 4 | export default class AnalysisIndexRoute extends Route { 5 | queryParams = { 6 | rejected: { 7 | refreshModel: true, 8 | }, 9 | verified: { 10 | refreshModel: true, 11 | }, 12 | }; 13 | 14 | model() { 15 | /* eslint-disable-next-line ember/no-controller-access-in-routes */ 16 | const controller = this.controllerFor("analysis.index"); 17 | const skipReset = controller.skipResetOnSetup; 18 | next(() => { 19 | if (!skipReset) { 20 | controller._reset(); 21 | } 22 | }); 23 | } 24 | 25 | setupController(controller) { 26 | controller.prefetchData.perform(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/analysis/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class AnalysisRoute extends Route {} 4 | -------------------------------------------------------------------------------- /app/application/route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-routes 4 | * @public 5 | */ 6 | import Route from "@ember/routing/route"; 7 | import { inject as service } from "@ember/service"; 8 | 9 | /** 10 | * The application route 11 | * 12 | * @class ApplicationRoute 13 | * @extends Ember.Route 14 | * @uses EmberSimpleAuth.ApplicationRouteMixin 15 | * @public 16 | */ 17 | export default class ApplicationRoute extends Route { 18 | @service session; 19 | 20 | async beforeModel() { 21 | await this.session.setup(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/application/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | {{outlet}} 7 | -------------------------------------------------------------------------------- /app/breakpoints.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mo: "(max-width: 479px)", 3 | xs: "(min-width: 480px) and (max-width: 767px)", 4 | sm: "(min-width: 768px) and (max-width: 991px)", 5 | md: "(min-width: 992px) and (max-width: 1199px)", 6 | lg: "(min-width: 1200px) and (max-width: 1439px)", 7 | xl: "(min-width: 1440px)", 8 | }; 9 | -------------------------------------------------------------------------------- /app/components/async-list/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if @data.isRunning}} 2 |
3 | 4 |
5 | {{else if @data.isError}} 6 |
7 |
8 | 9 |

Oops... Something went wrong

10 |

11 | Have you tried turning it off and on again? 12 |
13 | Please try refreshing the page. 14 |

15 |
16 |
17 | {{else if (not @data.value.length)}} 18 |
19 | {{yield "empty" @data.value}} 20 |
21 | {{else}} 22 | {{yield "body" @data.value}} 23 | {{/if}} 24 | -------------------------------------------------------------------------------- /app/components/attendance-slider/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 14 | 15 |
16 | {{#each this.labels as |label|}} 17 |
21 |
22 | {{label.value}} 23 |
24 |
25 | {{/each}} 26 |
27 | 28 |
29 | {{this.duration}} 30 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /app/components/changed-warning/template.hbs: -------------------------------------------------------------------------------- 1 | 2 |   3 | -------------------------------------------------------------------------------- /app/components/customer-visible-icon/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /app/components/date-buttons/template.hbs: -------------------------------------------------------------------------------- 1 | {{#each this.choices as |choice index|}} 2 | 8 | {{/each}} 9 | -------------------------------------------------------------------------------- /app/components/date-navigation/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 15 | 24 | 33 |
34 | 35 | 36 |
37 | -------------------------------------------------------------------------------- /app/components/duration-since/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{format-duration this.duration}} 3 | -------------------------------------------------------------------------------- /app/components/filter-sidebar/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { tracked } from "@glimmer/tracking"; 3 | 4 | export default class FilterSidebar extends Component { 5 | @tracked visible = false; 6 | @tracked destination; 7 | 8 | constructor(...args) { 9 | super(...args); 10 | this.destination = document.getElementById("filter-sidebar-target"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/components/filter-sidebar/group/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { tracked } from "@glimmer/tracking"; 3 | 4 | export default class Group extends Component { 5 | @tracked expanded = false; 6 | } 7 | -------------------------------------------------------------------------------- /app/components/filter-sidebar/group/styles.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/timed-frontend/a91a9595042c81bd618d23bcf4303bd2ea80195d/app/components/filter-sidebar/group/styles.scss -------------------------------------------------------------------------------- /app/components/filter-sidebar/group/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 6 | {{@label}} 7 | 12 | 13 |
14 |
15 | {{yield}} 16 |
17 |
18 |
-------------------------------------------------------------------------------- /app/components/filter-sidebar/label/template.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/in-viewport/component.js: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import Component from "@glimmer/component"; 3 | import { tracked } from "@glimmer/tracking"; 4 | 5 | export default class InViewport extends Component { 6 | @tracked rootSelector = "body"; 7 | @tracked rootMargin = 0; 8 | _observer = null; 9 | 10 | @action 11 | registerObserver(element) { 12 | const observer = new IntersectionObserver( 13 | ([{ isIntersecting }]) => { 14 | if (isIntersecting) { 15 | return (this.args["on-enter-viewport"] ?? (() => {}))(); 16 | } 17 | 18 | return (this.args["on-exit-viewport"] ?? (() => {}))(); 19 | }, 20 | { 21 | root: document.querySelector(this.rootSelector), 22 | rootMargin: `${this.rootMargin}px`, 23 | } 24 | ); 25 | 26 | this._observer = observer; 27 | 28 | // eslint-disable-next-line ember/no-observers 29 | observer.observe(element); 30 | } 31 | 32 | willDestroy(...args) { 33 | super.willDestroy(...args); 34 | this._observer?.disconnect(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/components/in-viewport/template.hbs: -------------------------------------------------------------------------------- 1 |
{{yield}}
-------------------------------------------------------------------------------- /app/components/loading-icon/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
-------------------------------------------------------------------------------- /app/components/magic-link-btn/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { tracked } from "@glimmer/tracking"; 3 | 4 | export default class MagicLinkBtn extends Component { 5 | @tracked isModalVisible = false; 6 | } 7 | -------------------------------------------------------------------------------- /app/components/magic-link-btn/template.hbs: -------------------------------------------------------------------------------- 1 | 10 | 11 | {{#if this.isModalVisible}} 12 | 13 | {{/if}} 14 | -------------------------------------------------------------------------------- /app/components/no-mobile-message/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Sorry, this page doesn't work on mobile!

4 |

5 | The data on this page is taking up too much space for your currently used 6 | device. 7 |

8 |

9 | Try opening this page on a larger device, or go back to the 10 | home page! 11 |

12 |
13 | -------------------------------------------------------------------------------- /app/components/no-permission/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Halt!

4 |

5 | You are not supposed to be here...
6 | Please leave and do not talk about it, 7 | ever! 8 |

9 |
10 | -------------------------------------------------------------------------------- /app/components/not-identical-warning/template.hbs: -------------------------------------------------------------------------------- 1 | 2 |   3 | 4 | -------------------------------------------------------------------------------- /app/components/optimized-power-select/component.js: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import Component from "@glimmer/component"; 3 | 4 | export default class OptimizedPowerSelectComponent extends Component { 5 | get extra() { 6 | return this.args.extra ?? {}; 7 | } 8 | 9 | @action 10 | onFocus({ actions, isOpen }) { 11 | if (!isOpen) { 12 | actions.open(); 13 | } 14 | } 15 | 16 | @action 17 | onKeydown(select, e) { 18 | // this implementation is heavily inspired by the enter key handling of EPS 19 | // https://github.com/cibernox/ember-power-select/blob/6e3d5781a105515b915d407d571698c57290f674/addon/components/power-select.ts#L519 20 | if (e.keyCode === 9 && select.isOpen && select.highlighted !== undefined) { 21 | select.actions.choose(select.highlighted, e); 22 | e.stopImmediatePropagation(); 23 | return false; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/components/optimized-power-select/custom-options/customer-option.hbs: -------------------------------------------------------------------------------- 1 |
8 | {{#if @option.isTask}} 9 | 10 | 11 | 12 | {{@option.project.customer.name}} 13 | > 14 | {{@option.project.name}} 15 | {{@option.name}} 16 | 17 | 18 | {{else}} 19 | {{@option.name}} 20 | {{/if}} 21 | {{#if @option.archived}} 22 | 23 | {{/if}} 24 |
25 | -------------------------------------------------------------------------------- /app/components/optimized-power-select/custom-options/project-option.hbs: -------------------------------------------------------------------------------- 1 |
6 | {{#if (and @current (or (media "isMd") (media "isLg") (media "isXl")))}} 7 | 12 | {{/if}} 13 | {{@option.name}} 14 | {{#if @option.archived}} 15 | 16 | {{/if}} 17 |
18 | -------------------------------------------------------------------------------- /app/components/optimized-power-select/custom-options/task-option.hbs: -------------------------------------------------------------------------------- 1 |
6 | {{#if (and @current (or (media "isMd") (media "isLg") (media "isXl")))}} 7 | 12 | {{/if}} 13 | {{@option.name}} 14 | {{#if @option.archived}} 15 | 16 | {{/if}} 17 |
18 | -------------------------------------------------------------------------------- /app/components/optimized-power-select/custom-options/user-option.hbs: -------------------------------------------------------------------------------- 1 |
5 | {{@option.longName}} 6 | {{#unless @option.isActive}} 7 | 8 | {{/unless}} 9 |
10 | -------------------------------------------------------------------------------- /app/components/optimized-power-select/custom-select/task-selection.hbs: -------------------------------------------------------------------------------- 1 | {{@selected.name}} 2 | -------------------------------------------------------------------------------- /app/components/optimized-power-select/custom-select/user-selection.hbs: -------------------------------------------------------------------------------- 1 | {{@selected.longName}} 2 | -------------------------------------------------------------------------------- /app/components/optimized-power-select/template.hbs: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /app/components/optimized-power-select/trigger/component.js: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import Component from "@glimmer/component"; 3 | 4 | export default class OptimizedPowerSelectTriggerComponent extends Component { 5 | @action 6 | clear(e) { 7 | e.stopPropagation(); 8 | this.args.select.actions.select(null); 9 | if (e.type === "touchstart") { 10 | return false; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/components/page-permission/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if (cannot "access page")}} 2 |
3 |

Access forbidden

4 |

You do not have the permission to access this page

5 |
6 | {{else}} 7 | {{yield}} 8 | {{/if}} -------------------------------------------------------------------------------- /app/components/record-button/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | export default class RecordButton extends Component { 4 | get active() { 5 | return this.args.recording && this.args.activity?.id; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/components/record-button/template.hbs: -------------------------------------------------------------------------------- 1 |
5 | {{#if this.active}} 6 | 14 |
15 | 16 |
17 | {{else}} 18 | 26 | {{/if}} 27 |
28 | -------------------------------------------------------------------------------- /app/components/report-review-warning/component.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | import Component from "@glimmer/component"; 3 | 4 | export default class ReportReviewWarning extends Component { 5 | @service session; 6 | 7 | @service unverifiedReports; 8 | 9 | @service rejectedReports; 10 | } 11 | -------------------------------------------------------------------------------- /app/components/scroll-container.hbs: -------------------------------------------------------------------------------- 1 |
{{yield}}
7 | -------------------------------------------------------------------------------- /app/components/sort-header/component.js: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import Component from "@glimmer/component"; 3 | 4 | export default class SortHeader extends Component { 5 | get direction() { 6 | return this.args.current?.startsWith("-") ? "down" : "up"; 7 | } 8 | 9 | get getColname() { 10 | return this.args.current?.startsWith("-") 11 | ? this.args.current.substring(1) 12 | : this.args.current; 13 | } 14 | 15 | get active() { 16 | return this.getColname === this.args.by; 17 | } 18 | 19 | @action 20 | click() { 21 | const by = this.args.by; 22 | const sort = this.active && this.direction === "down" ? by : `-${by}`; 23 | 24 | this.args.update(sort); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/components/sort-header/template.hbs: -------------------------------------------------------------------------------- 1 | {{! template-lint-disable}} 2 | 3 | {{yield}} 4 | {{#if this.active}} 5 | 6 | {{else}} 7 | 8 | {{/if}} 9 | 10 | -------------------------------------------------------------------------------- /app/components/statistic-list/bar/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | export default class StatisticListBar extends Component { 4 | get didFinishEffortsInBudget() { 5 | return ( 6 | this.args.remaining === 0 && 7 | !this.didFinishEffortsOverBudget && 8 | this.args.archived 9 | ); 10 | } 11 | 12 | get didFinishEffortsOverBudget() { 13 | return this.args.value > this.args.goal; 14 | } 15 | 16 | get spentEffortsBarColor() { 17 | if (this.didFinishEffortsInBudget) { 18 | return "strong-success"; 19 | } 20 | if (this.didFinishEffortsOverBudget) { 21 | return "strong-danger"; 22 | } 23 | return ""; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/components/statistic-list/bar/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
8 | {{#if (and @remaining (lte @remaining 1))}} 9 |
15 | {{/if}} 16 | {{#if @goal}} 17 |
22 | {{/if}} 23 |
24 | -------------------------------------------------------------------------------- /app/components/statistic-list/column/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#if (eq @layout "DURATION")}} 3 | {{humanize-duration @value false}} 4 | {{else if (eq @layout "MONTH")}} 5 | {{moment-format (moment @value "M") "MMMM"}} 6 | {{else}} 7 | {{@value}} 8 | {{/if}} 9 | 10 | -------------------------------------------------------------------------------- /app/components/sy-calendar/component.js: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import PowerCalendarComponent from "ember-power-calendar/components/power-calendar"; 3 | import moment from "moment"; 4 | 5 | const CURRENT_YEAR = moment().year(); 6 | 7 | const YEARS_IN_FUTURE = 5; 8 | 9 | export default class SyCalendar extends PowerCalendarComponent { 10 | months = moment.months(); 11 | 12 | years = [...new Array(40).keys()].map( 13 | (i) => `${CURRENT_YEAR + YEARS_IN_FUTURE - i}` 14 | ); 15 | 16 | @action 17 | changeCenter(unit, calendar, e) { 18 | const newCenter = moment(calendar.center)[unit](e.target.value); 19 | calendar.actions.changeCenter(newCenter); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/components/sy-calendar/styles.scss: -------------------------------------------------------------------------------- 1 | & { 2 | @include ember-power-calendar($cell-size: 35px); 3 | 4 | .ember-power-calendar-nav-control { 5 | color: $color-primary; 6 | cursor: pointer; 7 | 8 | } 9 | 10 | .nav-select-month, 11 | .nav-select-year { 12 | position: relative; 13 | 14 | select { 15 | position: absolute; 16 | top: 0; left: 0; right: 0; bottom: 0; 17 | opacity: 0; 18 | } 19 | } 20 | 21 | .ember-power-calendar-day { 22 | cursor: pointer; 23 | transition: background-color 300ms ease, color 300ms ease; 24 | 25 | &--focused { 26 | box-shadow: inset 0 -2px 0 0 $color-primary; 27 | } 28 | 29 | &--selected { 30 | color: rgb(255,255,255); 31 | background-color: lighten($color-primary, 20%); 32 | 33 | &:hover { 34 | color: rgb(255,255,255); 35 | background-color: lighten($color-primary, 30%); 36 | } 37 | } 38 | } 39 | } 40 | 41 | &.sy-datepicker { 42 | border: 1px solid $color-border; 43 | padding: 0.5rem; 44 | box-shadow: 2px 2px 10px rgba(0,0,0,0.2); 45 | } 46 | -------------------------------------------------------------------------------- /app/components/sy-checkbox/component.js: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import { guidFor } from "@ember/object/internals"; 3 | import Component from "@glimmer/component"; 4 | 5 | /** 6 | * Component for an adcssy styled checkbox 7 | * 8 | * @class SyCheckboxComponent 9 | * @extends Ember.Component 10 | * @public 11 | */ 12 | export default class SyCheckbox extends Component { 13 | constructor(...args) { 14 | super(...args); 15 | 16 | this._checkboxElementId = guidFor(this); 17 | } 18 | 19 | get checkboxElementId() { 20 | return this._checkboxElementId; 21 | } 22 | 23 | @action 24 | handleCheckBox(element) { 25 | if (this.args.checked === null) { 26 | element.indeterminate = true; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/components/sy-checkbox/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 10 | 21 |
22 | -------------------------------------------------------------------------------- /app/components/sy-checkmark/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | export default class SyCheckmark extends Component { 4 | get icon() { 5 | return this.args.checked ? "check-square" : "square"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/components/sy-checkmark/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/components/sy-datepicker-btn/component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-components 4 | * @public 5 | */ 6 | 7 | import { action } from "@ember/object"; 8 | import SyDatepickerComponent from "timed/components/sy-datepicker/component"; 9 | import { localCopy } from "tracked-toolbox"; 10 | 11 | /** 12 | * The sy datepicker btn component 13 | * 14 | * @class SyDatepickerBtnComponent 15 | * @extends SyDatepickerComponent 16 | * @public 17 | */ 18 | export default class SyDatepickerBtnComponent extends SyDatepickerComponent { 19 | @localCopy("args.current") center; 20 | 21 | @action 22 | updateCenter({ moment }) { 23 | this.center = moment; 24 | } 25 | 26 | @action 27 | updateSelection({ moment }) { 28 | this.args.onChange(moment); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/components/sy-datepicker-btn/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/components/sy-durationpicker-day/component.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import SyDurationpickerComponent from "timed/components/sy-durationpicker/component"; 3 | 4 | export default class SyDurationpickerDayComponent extends SyDurationpickerComponent { 5 | maxlength = 5; 6 | 7 | max = moment.duration({ h: 24, m: 0 }); 8 | 9 | min = moment.duration({ h: 0, m: 0 }); 10 | 11 | sanitize(value) { 12 | return value.replace(/[^\d:]/, ""); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/components/sy-durationpicker-day/template.hbs: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /app/components/sy-modal-target/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /app/components/sy-modal/body/styles.scss: -------------------------------------------------------------------------------- 1 | & { 2 | overflow-x: hidden; 3 | 4 | > .form-group:last-child { 5 | margin-bottom: 0; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/components/sy-modal/body/template.hbs: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/components/sy-modal/footer/template.hbs: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/components/sy-modal/header/template.hbs: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/components/sy-modal/overlay/component.js: -------------------------------------------------------------------------------- 1 | import { registerDestructor } from "@ember/destroyable"; 2 | import { action } from "@ember/object"; 3 | import Component from "@glimmer/component"; 4 | 5 | export default class SyModalOverlay extends Component { 6 | @action 7 | setupClickHandler(element) { 8 | this.element = element; 9 | 10 | element.addEventListener("click", this.handleClick); 11 | 12 | registerDestructor(this, () => { 13 | element.removeEventListener("click", this.handleClick); 14 | }); 15 | } 16 | 17 | @action 18 | handleClick(e) { 19 | if (e.target === this.element) { 20 | this.args.onClose(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/components/sy-modal/overlay/template.hbs: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/components/sy-modal/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if @visible}} 2 | 3 | 4 | 14 | 15 | 16 | {{/if}} 17 | -------------------------------------------------------------------------------- /app/components/sy-timepicker/template.hbs: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /app/components/sy-toggle/component.js: -------------------------------------------------------------------------------- 1 | import { assert } from "@ember/debug"; 2 | import { action } from "@ember/object"; 3 | import Component from "@glimmer/component"; 4 | 5 | export default class SyToggle extends Component { 6 | constructor(...args) { 7 | super(...args); 8 | 9 | assert("You must pass a onToggle callback.", this.args.onToggle); 10 | } 11 | 12 | @action 13 | handleKeyUp(event) { 14 | // only trigger on "Space" key 15 | if (event.keyCode === 32 && !this.args.disabled) { 16 | this.args.onToggle(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/components/sy-toggle/template.hbs: -------------------------------------------------------------------------------- 1 |
10 | {{#if (has-block)}} 11 | {{yield}} 12 | {{else}} 13 | 14 | {{/if}} 15 |
16 | -------------------------------------------------------------------------------- /app/components/sy-topnav/component.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | import Component from "@glimmer/component"; 3 | import { tracked } from "@glimmer/tracking"; 4 | 5 | export default class SyTopnav extends Component { 6 | @service session; 7 | 8 | @service media; 9 | 10 | @tracked expand = false; 11 | 12 | get navMobile() { 13 | return this.media.isMo || this.media.isXs || this.media.isSm; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/components/timed-clock/component.js: -------------------------------------------------------------------------------- 1 | import { setProperties } from "@ember/object"; 2 | import { isTesting, macroCondition } from "@embroider/macros"; 3 | import Component from "@glimmer/component"; 4 | import { tracked } from "@glimmer/tracking"; 5 | import { task, timeout } from "ember-concurrency"; 6 | import moment from "moment"; 7 | 8 | export default class TimedClock extends Component { 9 | @tracked hour = 0; 10 | @tracked minute = 0; 11 | @tracked second = 0; 12 | 13 | _update() { 14 | const now = moment(); 15 | 16 | const second = now.seconds() * 6; 17 | const minute = now.minutes() * 6 + second / 60; 18 | const hour = ((now.hours() % 12) / 12) * 360 + minute / 12; 19 | 20 | setProperties(this, { second, minute, hour }); 21 | } 22 | 23 | @task 24 | *timer() { 25 | for (;;) { 26 | this._update(); 27 | 28 | /* istanbul ignore else */ 29 | if (macroCondition(isTesting())) { 30 | return; 31 | } 32 | 33 | /* istanbul ignore next */ 34 | yield timeout(1000); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/components/timed-clock/template.hbs: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 30 | 40 | 41 | -------------------------------------------------------------------------------- /app/components/tracking-bar/component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-components 4 | * @public 5 | */ 6 | import { inject as service } from "@ember/service"; 7 | import Component from "@glimmer/component"; 8 | 9 | /** 10 | * The tracking bar component 11 | * 12 | * @class TrackingBarComponent 13 | * @extends Ember.Component 14 | * @public 15 | */ 16 | export default class TrackingBar extends Component { 17 | @service tracking; 18 | } 19 | -------------------------------------------------------------------------------- /app/components/user-selection/template.hbs: -------------------------------------------------------------------------------- 1 | {{yield 2 | (hash 3 | user=(component 4 | (ensure-safe-component "optimized-power-select") 5 | options=this.users 6 | disabled=@disabled 7 | selected=@user 8 | placeholder="Select user..." 9 | searchField="longName" 10 | tagName="div" 11 | class="user-select" 12 | allowClear=true 13 | onChange=@onChange 14 | extra=(hash 15 | lazy=true 16 | selectedTemplate=this.selectedTemplate 17 | optionTemplate=this.optionTemplate 18 | ) 19 | ) 20 | ) 21 | }} 22 | -------------------------------------------------------------------------------- /app/components/vertical-collection/component.js: -------------------------------------------------------------------------------- 1 | import { isTesting, macroCondition } from "@embroider/macros"; 2 | import VerticalCollectionComponent from "@html-next/vertical-collection/components/vertical-collection/component"; 3 | import classic from "ember-classic-decorator"; 4 | 5 | @classic 6 | export default class VerticalCollection extends VerticalCollectionComponent { 7 | init(...args) { 8 | super.init(...args); 9 | 10 | if (macroCondition(isTesting())) { 11 | this.renderAll = true; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/components/weekly-overview-benchmark/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | export default class WeeklyOverviewBenchmark extends Component { 4 | /** 5 | * Maximum worktime 6 | * 7 | * This is 'only' 20h since noone works 24h a day.. 8 | * 9 | * @property {Number} max 10 | * @public 11 | */ 12 | get max() { 13 | return this.args.max || 20; 14 | } 15 | 16 | /** 17 | * The offset to the bottom 18 | * 19 | * @property {String} style 20 | * @public 21 | */ 22 | get style() { 23 | return { bottom: `calc(100% / ${this.max} * ${this.args.hours})` }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/components/weekly-overview-benchmark/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if @showLabel}} 3 | {{@hours}}h 4 | {{/if}} 5 |
6 |
-------------------------------------------------------------------------------- /app/components/weekly-overview-day/component.js: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import Component from "@glimmer/component"; 3 | import { tracked } from "@glimmer/tracking"; 4 | export default class WeeklyOverviewDay extends Component { 5 | /** 6 | * Maximum worktime in hours 7 | * 8 | * @property {Number} max 9 | * @public 10 | */ 11 | @tracked max = 20; 12 | 13 | get title() { 14 | const pre = this.args.prefix?.length ? `${this.args.prefix}, ` : ""; 15 | 16 | let title = `${this.args.worktime.hours()}h`; 17 | 18 | if (this.args.worktime.minutes()) { 19 | title += ` ${this.args.worktime.minutes()}m`; 20 | } 21 | return `${pre}${title}`; 22 | } 23 | 24 | get style() { 25 | const height = Math.min( 26 | (this.args.worktime.asHours() / this.max) * 100, 27 | 100 28 | ); 29 | return { height: `${height}%` }; 30 | } 31 | 32 | @action 33 | click(event) { 34 | const action = this.args.onClick; 35 | 36 | if (action) { 37 | event.preventDefault(); 38 | 39 | this.args.onClick(this.args.day); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/components/weekly-overview-day/template.hbs: -------------------------------------------------------------------------------- 1 |
13 |
14 |
15 | {{moment-format @day 'DD'}} 16 | {{moment-format @day 'dd'}} 17 |
18 |
19 | -------------------------------------------------------------------------------- /app/components/weekly-overview/component.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { tracked } from "@glimmer/tracking"; 3 | 4 | export default class WeeklyOverview extends Component { 5 | @tracked height = 150; 6 | 7 | get hours() { 8 | return this.args.expected.asHours(); 9 | } 10 | 11 | get style() { 12 | return { height: `${this.height}px` }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/components/weekly-overview/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {{yield}} 16 |
17 |
18 | -------------------------------------------------------------------------------- /app/components/welcome-modal/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Welcome to Timed!

5 | 6 |

Would you like to take a tour?

7 |
8 |
9 | 10 | 11 | 19 | 20 | 28 | 29 | 37 | 38 |
39 | -------------------------------------------------------------------------------- /app/components/worktime-balance-chart/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/controllers/qpcontroller.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { next } from "@ember/runloop"; 3 | 4 | export default class ControllersQPControllerController extends Controller { 5 | #defaults = {}; 6 | 7 | constructor(...args) { 8 | super(...args); 9 | 10 | // defer until the extending controller has set it's query params 11 | next(() => this.storeQPDefaults()); 12 | } 13 | 14 | storeQPDefaults() { 15 | this.queryParams.forEach((qp) => { 16 | this.#defaults[qp] = this[qp]; 17 | }); 18 | } 19 | 20 | resetQueryParams(options = { except: [] }) { 21 | this.queryParams.forEach((qp) => { 22 | if (!options.except.includes(qp)) { 23 | this[qp] = this.#defaults[qp]; 24 | } 25 | }); 26 | } 27 | 28 | get allQueryParams() { 29 | return this.queryParams.reduce( 30 | (acc, key) => 31 | Object.defineProperty(acc, key, { 32 | value: this[key], 33 | enumerable: true, 34 | }), 35 | {} 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/helpers/balance-highlight-class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-helpers 4 | * @public 5 | */ 6 | import { helper } from "@ember/component/helper"; 7 | import moment from "moment"; 8 | 9 | /** 10 | * Helper to determine the color of a balance 11 | * 12 | * > 0: red-ish 13 | * < 0: green-ish 14 | * 15 | * @function balanceHighlightClass 16 | * @param {Array} options The options delivered to the helper 17 | * @return {String} The CSS class to apply the color 18 | * @public 19 | */ 20 | export function balanceHighlightClass([balance]) { 21 | const minutes = moment.isDuration(balance) ? balance.asMinutes() : 0; 22 | 23 | if (minutes > 0) { 24 | return "color-success"; 25 | } else if (minutes < 0) { 26 | return "color-danger"; 27 | } 28 | 29 | return ""; 30 | } 31 | 32 | export default helper(balanceHighlightClass); 33 | -------------------------------------------------------------------------------- /app/helpers/format-duration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-helpers 4 | * @public 5 | */ 6 | import { helper } from "@ember/component/helper"; 7 | import formatDuration from "timed/utils/format-duration"; 8 | 9 | /** 10 | * The format duration helper 11 | * 12 | * @function formatDurationFn 13 | * @param {Array} args The arguments delivered to the helper 14 | * @return {String} The formatted duration 15 | * @public 16 | */ 17 | export const formatDurationFn = (args) => formatDuration(...args); 18 | 19 | export default helper(formatDurationFn); 20 | -------------------------------------------------------------------------------- /app/helpers/humanize-duration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-helpers 4 | * @public 5 | */ 6 | import { helper } from "@ember/component/helper"; 7 | import humanizeDuration from "timed/utils/humanize-duration"; 8 | 9 | /** 10 | * The humanize duration helper 11 | * 12 | * @function humanizeDurationFn 13 | * @param {Array} args The arguments delivered to the helper 14 | * @return {String} The humanized duration 15 | * @public 16 | */ 17 | export const humanizeDurationFn = (args) => humanizeDuration(...args); 18 | 19 | export default helper(humanizeDurationFn); 20 | -------------------------------------------------------------------------------- /app/helpers/parse-django-duration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-helpers 4 | * @public 5 | */ 6 | import { helper } from "@ember/component/helper"; 7 | import parseDjangoDuration from "timed/utils/parse-django-duration"; 8 | 9 | /** 10 | * The parse django duration helper 11 | * 12 | * @function parseDjangoDurationFn 13 | * @param {Array} args The arguments delivered to the helper 14 | * @return {moment.duration} The moment duration 15 | * @public 16 | */ 17 | export const parseDjangoDurationFn = (args) => parseDjangoDuration(...args); 18 | 19 | export default helper(parseDjangoDurationFn); 20 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Timed 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{content-for "head"}} 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | 19 | 20 | {{content-for "body"}} 21 | 22 | 23 | 24 | 25 | {{content-for "body-footer"}} 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/index/activities/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class IndexActivitiesRoute extends Route { 4 | model() { 5 | return this.modelFor("index"); 6 | } 7 | 8 | setupController(controller, ...args) { 9 | super.setupController(controller, ...args); 10 | 11 | controller.set("user", this.modelFor("protected")); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/index/attendances/route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-routes 4 | * @public 5 | */ 6 | import Route from "@ember/routing/route"; 7 | 8 | /** 9 | * The index attendances route 10 | * 11 | * @class IndexAttendancesRoute 12 | * @extends Ember.Route 13 | * @public 14 | */ 15 | export default class AttendaceIndexRoute extends Route { 16 | /** 17 | * Setup controller hook, set the current user 18 | * 19 | * @method setupContrller 20 | * @param {Ember.Controller} controller The controller 21 | * @public 22 | */ 23 | setupController(controller, ...args) { 24 | super.setupController(controller, ...args); 25 | 26 | controller.set("user", this.modelFor("protected")); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/initializers/responsive.js: -------------------------------------------------------------------------------- 1 | import { initialize } from "ember-responsive/initializers/responsive"; 2 | 3 | /** 4 | * Ember responsive initializer 5 | * 6 | * Supports auto injecting media service app-wide. 7 | * 8 | * Generated by the ember-responsive addon. Customize initialize to change 9 | * injection. 10 | */ 11 | 12 | export default { 13 | name: "responsive", 14 | initialize, 15 | }; 16 | -------------------------------------------------------------------------------- /app/login/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | /** 3 | * The login route 4 | * 5 | * @class LoginRoute 6 | * @extends Ember.Route 7 | * @public 8 | */ 9 | export default class LoginRoute extends Route {} 10 | -------------------------------------------------------------------------------- /app/login/template.hbs: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /app/models/absence-balance.js: -------------------------------------------------------------------------------- 1 | import Model, { attr, belongsTo, hasMany } from "@ember-data/model"; 2 | 3 | export default class AbsenceBalance extends Model { 4 | @attr("number") credit; 5 | @attr("number") usedDays; 6 | @attr("django-duration") usedDuration; 7 | @attr("number") balance; 8 | @belongsTo("user") user; 9 | @belongsTo("absence-type") absenceType; 10 | @hasMany("absence-credit") absenceCredits; 11 | } 12 | -------------------------------------------------------------------------------- /app/models/absence-credit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-models 4 | * @public 5 | */ 6 | import Model, { attr, belongsTo } from "@ember-data/model"; 7 | 8 | /** 9 | * The absence credit model 10 | * 11 | * @class AbsenceCredit 12 | * @extends DS.Model 13 | * @public 14 | */ 15 | export default class AbsenceCredit extends Model { 16 | /** 17 | * The days 18 | * 19 | * @property {Number} days 20 | * @public 21 | */ 22 | @attr("number") days; 23 | 24 | /** 25 | * The date 26 | * 27 | * @property {moment} date 28 | * @public 29 | */ 30 | @attr("django-date") date; 31 | 32 | /** 33 | * The comment 34 | * 35 | * @property {String} comment 36 | * @public 37 | */ 38 | @attr("string", { defaultValue: "" }) comment; 39 | 40 | /** 41 | * The absence type for which this credit counts 42 | * 43 | * @property {AbsenceType} absenceType 44 | * @public 45 | */ 46 | @belongsTo("absence-type") absenceType; 47 | 48 | /** 49 | * The user to which this credit belongs to 50 | * 51 | * @property {User} user 52 | * @public 53 | */ 54 | @belongsTo("user") user; 55 | } 56 | -------------------------------------------------------------------------------- /app/models/absence-type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-models 4 | * @public 5 | */ 6 | import Model, { attr, hasMany } from "@ember-data/model"; 7 | 8 | /** 9 | * The absence type model 10 | * 11 | * @class AbsenceType 12 | * @extends DS.Model 13 | * @public 14 | */ 15 | export default class AbsenceType extends Model { 16 | /** 17 | * The name of the absence type 18 | * 19 | * E.g Military, Holiday or Sickness 20 | * 21 | * @property {String} name 22 | * @public 23 | */ 24 | @attr("string") name; 25 | 26 | /** 27 | * Whether the absence type only fills the worktime 28 | * 29 | * @property {Boolean} fillWorktime 30 | * @public 31 | */ 32 | @attr("boolean") fillWorktime; 33 | 34 | /** 35 | * The balances for this type 36 | * 37 | * @property {AbsenceBalance[]} absenceBalances 38 | * @public 39 | */ 40 | @hasMany("absence-balance") absenceBalances; 41 | } 42 | -------------------------------------------------------------------------------- /app/models/billing-type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-models 4 | * @public 5 | */ 6 | import Model, { attr } from "@ember-data/model"; 7 | 8 | /** 9 | * The billing type model 10 | * 11 | * @class BillingType 12 | * @extends DS.Model 13 | * @public 14 | */ 15 | export default class BillingType extends Model { 16 | /** 17 | * The name 18 | * 19 | * @property {String} name 20 | * @public 21 | */ 22 | @attr("string") name; 23 | } 24 | -------------------------------------------------------------------------------- /app/models/cost-center.js: -------------------------------------------------------------------------------- 1 | import Model, { attr } from "@ember-data/model"; 2 | 3 | export default class CostCenter extends Model { 4 | @attr("string") name; 5 | @attr("string") reference; 6 | } 7 | -------------------------------------------------------------------------------- /app/models/customer-statistic.js: -------------------------------------------------------------------------------- 1 | import Model, { attr } from "@ember-data/model"; 2 | 3 | export default class CustomerStatistics extends Model { 4 | @attr("django-duration") duration; 5 | @attr name; 6 | } 7 | -------------------------------------------------------------------------------- /app/models/location.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-models 4 | * @public 5 | */ 6 | import Model, { attr } from "@ember-data/model"; 7 | 8 | /** 9 | * The location model 10 | * 11 | * @class Location 12 | * @extends DS.Model 13 | * @public 14 | */ 15 | export default class Location extends Model { 16 | /** 17 | * The name 18 | * 19 | * @property {String} name 20 | * @public 21 | */ 22 | @attr("string") name; 23 | 24 | /** 25 | * The days on which users in this location need to work 26 | * 27 | * @property {Number[]} workdays 28 | * @public 29 | */ 30 | @attr("django-workdays") workdays; 31 | } 32 | -------------------------------------------------------------------------------- /app/models/month-statistic.js: -------------------------------------------------------------------------------- 1 | import Model, { attr } from "@ember-data/model"; 2 | 3 | export default class MonthStatistic extends Model { 4 | @attr("number") year; 5 | @attr("number") month; 6 | @attr("django-duration") duration; 7 | } 8 | -------------------------------------------------------------------------------- /app/models/overtime-credit.js: -------------------------------------------------------------------------------- 1 | import Model, { attr, belongsTo } from "@ember-data/model"; 2 | 3 | export default class OvertimeCredit extends Model { 4 | @attr("django-date") date; 5 | @attr("django-duration") duration; 6 | @attr("string", { defaultValue: "" }) comment; 7 | @belongsTo("user") user; 8 | } 9 | -------------------------------------------------------------------------------- /app/models/project-statistic.js: -------------------------------------------------------------------------------- 1 | import Model, { attr, belongsTo } from "@ember-data/model"; 2 | 3 | export default class ProjectStatistics extends Model { 4 | @attr name; 5 | @attr("django-duration") estimatedTime; 6 | @attr("django-duration") duration; 7 | @attr("django-duration") totalRemainingEffort; 8 | @belongsTo("customer") customer; 9 | } 10 | -------------------------------------------------------------------------------- /app/models/public-holiday.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-models 4 | * @public 5 | */ 6 | import Model, { attr, belongsTo } from "@ember-data/model"; 7 | 8 | /** 9 | * The public holiday model 10 | * 11 | * @class PublicHoliday 12 | * @extends DS.Model 13 | * @public 14 | */ 15 | export default class PublicHoliday extends Model { 16 | /** 17 | * The name 18 | * 19 | * @property {String} name 20 | * @public 21 | */ 22 | @attr("string") name; 23 | 24 | /** 25 | * The date 26 | * 27 | * @property {moment} date 28 | * @public 29 | */ 30 | @attr("django-date") date; 31 | 32 | /** 33 | * The location 34 | * 35 | * @property {Location} location 36 | * @public 37 | */ 38 | @belongsTo("location") location; 39 | } 40 | -------------------------------------------------------------------------------- /app/models/report-intersection.js: -------------------------------------------------------------------------------- 1 | import Model, { attr, belongsTo } from "@ember-data/model"; 2 | 3 | export default class ReportIntersection extends Model { 4 | @attr("string") comment; 5 | @attr("boolean", { allowNull: true, defaultValue: null }) notBillable; 6 | @attr("boolean", { allowNull: true, defaultValue: false }) rejected; 7 | @attr("boolean", { allowNull: true, defaultValue: null }) review; 8 | @attr("boolean", { allowNull: true, defaultValue: null }) billed; 9 | @attr("boolean", { allowNull: true, defaultValue: null }) verified; 10 | 11 | @belongsTo("customer") customer; 12 | @belongsTo("project") project; 13 | @belongsTo("task") task; 14 | @belongsTo("user") user; 15 | } 16 | -------------------------------------------------------------------------------- /app/models/task-statistic.js: -------------------------------------------------------------------------------- 1 | import Model, { attr, belongsTo } from "@ember-data/model"; 2 | 3 | export default class TaskStatistics extends Model { 4 | @attr name; 5 | @attr("django-duration") duration; 6 | @attr("django-duration") estimatedTime; 7 | @attr("django-duration") mostRecentRemainingEffort; 8 | @belongsTo("project") project; 9 | } 10 | -------------------------------------------------------------------------------- /app/models/task.js: -------------------------------------------------------------------------------- 1 | import Model, { attr, belongsTo, hasMany } from "@ember-data/model"; 2 | 3 | export default class Task extends Model { 4 | @attr("string", { defaultValue: "" }) name; 5 | @attr("django-duration") estimatedTime; 6 | @attr("django-duration") mostRecentRemainingEffort; 7 | @attr("boolean", { defaultValue: false }) archived; 8 | @attr("string", { defaultValue: "" }) reference; 9 | 10 | @belongsTo("project") project; 11 | @hasMany("task-assignee") assignees; 12 | 13 | /** 14 | * Flag saying that this is a task. 15 | * Used in /app/customer-suggestion/template.hbs 16 | * We're using this as a workaround for the fact that one 17 | * can't seem to use helpers like "(eq" in inline templates 18 | * 19 | * @property project 20 | * @type {Project} 21 | * @public 22 | */ 23 | isTask = true; 24 | 25 | get longName() { 26 | const taskName = this.name; 27 | const projectName = this.project.get("name"); 28 | const customerName = this.project.get("customer.name"); 29 | 30 | return `${customerName} > ${projectName} > ${taskName}`; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/models/user-statistic.js: -------------------------------------------------------------------------------- 1 | import Model, { attr, belongsTo } from "@ember-data/model"; 2 | 3 | export default class UserStatistic extends Model { 4 | @attr("django-duration") duration; 5 | 6 | @belongsTo("user") user; 7 | } 8 | -------------------------------------------------------------------------------- /app/models/worktime-balance.js: -------------------------------------------------------------------------------- 1 | import Model, { attr, belongsTo } from "@ember-data/model"; 2 | 3 | export default class WorktimeBalance extends Model { 4 | @attr("django-date") date; 5 | @attr("django-duration") balance; 6 | @belongsTo("user") user; 7 | } 8 | -------------------------------------------------------------------------------- /app/models/year-statistic.js: -------------------------------------------------------------------------------- 1 | import Model, { attr } from "@ember-data/model"; 2 | 3 | export default class YearStatistic extends Model { 4 | @attr("number") year; 5 | @attr("django-duration") duration; 6 | } 7 | -------------------------------------------------------------------------------- /app/no-access/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class NoAccessRoute extends Route {} 4 | -------------------------------------------------------------------------------- /app/no-access/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

You do not have permission to access Timed.

5 |
6 |
7 | -------------------------------------------------------------------------------- /app/notfound/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class NotFoundRoute extends Route {} 4 | -------------------------------------------------------------------------------- /app/notfound/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |

404

3 |

The site you requested does not exist

4 |
5 | -------------------------------------------------------------------------------- /app/projects/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class ProjectsRoute extends Route { 4 | setupController(controller, ...args) { 5 | super.setupController(controller, ...args); 6 | 7 | controller.fetchProjectsByUser.perform(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/protected/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if this.loading}}{{/if}} 3 |
4 | 5 | 6 | {{outlet}} 7 |
8 |
9 | 10 | 16 | -------------------------------------------------------------------------------- /app/serializers/application.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-serializers 4 | * @public 5 | */ 6 | import JSONAPISerializer from "@ember-data/serializer/json-api"; 7 | 8 | /** 9 | * The application serializer 10 | * 11 | * @class ApplicationSerializer 12 | * @extends DS.JSONAPISerializer 13 | * @public 14 | */ 15 | export default class ApplicationSerializer extends JSONAPISerializer {} 16 | -------------------------------------------------------------------------------- /app/serializers/attendance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-serializers 4 | * @public 5 | */ 6 | import ApplicationSerializer from "timed/serializers/application"; 7 | 8 | /** 9 | * The attendance serializer 10 | * 11 | * @class AttendanceSerializer 12 | * @extends ApplicationSerializer 13 | * @public 14 | */ 15 | export default ApplicationSerializer.extend({ 16 | /** 17 | * The attribute mapping 18 | * 19 | * This mapps some properties of the response to another 20 | * property name of the model 21 | * 22 | * @property {Object} attrs 23 | * @property {String} from 24 | * @property {String} to 25 | * @public 26 | */ 27 | attrs: { 28 | from: "from-time", 29 | to: "to-time", 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /app/serializers/employment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-serializers 4 | * @public 5 | */ 6 | import ApplicationSerializer from "timed/serializers/application"; 7 | 8 | /** 9 | * The employment block serializer 10 | * 11 | * @class EmploymentBlockSerializer 12 | * @extends ApplicationSerializer 13 | * @public 14 | */ 15 | export default ApplicationSerializer.extend({ 16 | /** 17 | * The attribute mapping 18 | * 19 | * This mapps some properties of the response to another 20 | * property name of the model 21 | * 22 | * @property {Object} attrs 23 | * @property {String} start 24 | * @property {String} end 25 | * @public 26 | */ 27 | attrs: { 28 | start: "start-date", 29 | end: "end-date", 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /app/services/autostart-tour.js: -------------------------------------------------------------------------------- 1 | import Service from "@ember/service"; 2 | import { tracked } from "@glimmer/tracking"; 3 | import TOURS from "timed/tours"; 4 | 5 | /** 6 | * Autostart tour service 7 | * 8 | * This service helps connecting the tours to the localstorage 9 | * 10 | * @class AutostartTourService 11 | * @extends Ember.Service 12 | * @public 13 | */ 14 | export default class AutostartTourService extends Service { 15 | tours = Object.keys(TOURS); 16 | /** 17 | * The item key to use in the localstorage 18 | * 19 | * @property {String} doneKey 20 | * @public 21 | */ 22 | @tracked doneKey = "timed-tour"; 23 | 24 | get done() { 25 | return Array.from(JSON.parse(localStorage.getItem(this.doneKey)) || []); 26 | } 27 | 28 | set done(value = []) { 29 | localStorage.setItem(this.doneKey, JSON.stringify(value)); 30 | } 31 | 32 | get undoneTours() { 33 | return this.tours.filter((tour) => !this.done.includes(tour)); 34 | } 35 | 36 | get allDone() { 37 | return this.undoneTours.length === 0; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/sso-login/route.js: -------------------------------------------------------------------------------- 1 | import OIDCAuthenticationRoute from "ember-simple-auth-oidc/routes/oidc-authentication"; 2 | 3 | export default class SsoLoginRoute extends OIDCAuthenticationRoute {} 4 | -------------------------------------------------------------------------------- /app/statistics/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class StatisticsRoute extends Route { 4 | setupController(controller) { 5 | controller.data.perform(); 6 | controller.prefetchData.perform(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/styles/attendances.scss: -------------------------------------------------------------------------------- 1 | .table--attendances > tbody > tr > td { 2 | &:nth-child(1), 3 | &:nth-child(2) { 4 | width: auto; 5 | } 6 | 7 | &:nth-child(3) { 8 | width: 105px; 9 | text-align: right; 10 | 11 | .btn { 12 | display: inline-flex; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/styles/badge.scss: -------------------------------------------------------------------------------- 1 | .badge { 2 | display: inline-block; 3 | min-width: 10px; 4 | padding: 0.2rem 0.4rem; 5 | margin-left: 0.2rem; 6 | font-size: $font-size-base * 0.8; 7 | line-height: 1; 8 | color: rgb(255,255,255); 9 | text-align: center; 10 | white-space: nowrap; 11 | vertical-align: middle; 12 | background-color: $color-secondary; 13 | border-radius: 10px; 14 | } 15 | 16 | a.active .badge, 17 | .badge--primary { 18 | background-color: $color-primary; 19 | } 20 | 21 | .badge--success { 22 | background-color: $color-success; 23 | } 24 | 25 | .badge--info { 26 | background-color: $color-info; 27 | } 28 | 29 | .badge--warning { 30 | background-color: $color-warning; 31 | } 32 | 33 | .badge--danger { 34 | background-color: $color-danger; 35 | } 36 | -------------------------------------------------------------------------------- /app/styles/components/date-buttons.scss: -------------------------------------------------------------------------------- 1 | .date-button { 2 | width: 32.5%; 3 | } 4 | -------------------------------------------------------------------------------- /app/styles/components/date-navigation.scss: -------------------------------------------------------------------------------- 1 | .date-navigation { 2 | display: flex; 3 | flex-grow: 1; 4 | justify-content: space-between; 5 | padding: 1rem 0; 6 | } 7 | 8 | .btn-group { 9 | margin-right: 10px; 10 | } 11 | 12 | .date-navigation-container { 13 | flex-grow: 1; 14 | display: flex; 15 | } 16 | 17 | @media #{$sm-viewport} { 18 | .date-navigation { 19 | justify-content: flex-end; 20 | padding: 0; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/styles/components/filter-sidebar--label.scss: -------------------------------------------------------------------------------- 1 | .filter-sidebar-label { 2 | display: block; 3 | font-size: 0.75rem; 4 | font-weight: 500; 5 | padding: 0.5rem 0; 6 | } 7 | 8 | .filter-sidebar-label > * { 9 | font-weight: 300; 10 | margin-top: 0.3rem; 11 | } 12 | -------------------------------------------------------------------------------- /app/styles/components/magic-link-btn.scss: -------------------------------------------------------------------------------- 1 | .ember-basic-dropdown-content { 2 | z-index: 1002; 3 | } 4 | 5 | .magic-link-modal { 6 | min-width: 500px; 7 | } 8 | -------------------------------------------------------------------------------- /app/styles/components/nav-top.scss: -------------------------------------------------------------------------------- 1 | nav { 2 | .nav-top-header-title { 3 | display: none; 4 | font-weight: 500; 5 | } 6 | 7 | .timed-clock { 8 | padding: 0.3rem 0; 9 | margin-right: 0.3rem; 10 | } 11 | 12 | .nav-top-toggle { 13 | display: block; 14 | } 15 | 16 | @media #{$nav-top-mobile-width} { 17 | .nav-top-header-title { 18 | display: block; 19 | } 20 | 21 | .nav-top-header-title-version { 22 | color: rgb(150, 150, 150); 23 | font-size: 0.5rem; 24 | font-family: $font-family-mono; 25 | } 26 | 27 | .nav-top-list-item a.active { 28 | background-color: $color-primary; 29 | color: #fff; 30 | } 31 | 32 | .nav-top-list-item a:hover { 33 | background-color: lighten($color-primary, 20%); 34 | color: #fff; 35 | } 36 | 37 | .nav-top-list-item a { 38 | padding: 0.6rem 0.8rem; 39 | border-radius: 0; 40 | } 41 | 42 | .nav-top-toggle { 43 | display: none; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/styles/components/scroll-container.scss: -------------------------------------------------------------------------------- 1 | .scroll-container { 2 | max-height: 100%; 3 | min-height: 20px; 4 | overflow-y: auto; 5 | 6 | &::after, 7 | &::before { 8 | content: ""; 9 | position: absolute; 10 | z-index: 1; 11 | left: 0; 12 | right: 0; 13 | height: 7px; 14 | background: linear-gradient( 15 | to bottom, 16 | rgba(0, 0, 0, 0.03), 17 | rgba(0, 0, 0, 0) 18 | ); 19 | } 20 | 21 | &::before { 22 | top: 0; 23 | } 24 | 25 | &::after { 26 | bottom: 0; 27 | transform: rotate(180deg); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/styles/components/sort-header.scss: -------------------------------------------------------------------------------- 1 | .sort-header { 2 | white-space: nowrap; 3 | cursor: pointer; 4 | } 5 | -------------------------------------------------------------------------------- /app/styles/components/sy-calendar.scss: -------------------------------------------------------------------------------- 1 | .sy-calendar { 2 | @include ember-power-calendar($cell-size: 35px); 3 | 4 | .ember-power-calendar-nav-control { 5 | color: $color-primary; 6 | cursor: pointer; 7 | } 8 | 9 | .nav-select-month, 10 | .nav-select-year { 11 | position: relative; 12 | 13 | select { 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | right: 0; 18 | bottom: 0; 19 | opacity: 0; 20 | } 21 | } 22 | 23 | .ember-power-calendar-day { 24 | cursor: pointer; 25 | transition: background-color 300ms ease, color 300ms ease; 26 | 27 | &--focused { 28 | box-shadow: inset 0 -2px 0 0 $color-primary; 29 | } 30 | 31 | &--selected { 32 | color: rgb(255, 255, 255); 33 | background-color: lighten($color-primary, 20%); 34 | 35 | &:hover { 36 | color: rgb(255, 255, 255); 37 | background-color: lighten($color-primary, 30%); 38 | } 39 | } 40 | } 41 | } 42 | 43 | .sy-calendar.sy-datepicker { 44 | border: 1px solid $color-border; 45 | padding: 0.5rem; 46 | box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.2); 47 | } 48 | -------------------------------------------------------------------------------- /app/styles/components/sy-checkbox.scss: -------------------------------------------------------------------------------- 1 | .sy-checkbox > input[type="checkbox"]:indeterminate + label:after { 2 | content: "\2012"; 3 | opacity: 1; 4 | transform: scale(1); 5 | left: 0.23rem; 6 | } 7 | -------------------------------------------------------------------------------- /app/styles/components/sy-datepicker.scss: -------------------------------------------------------------------------------- 1 | .sy-datepicker-trigger.ember-basic-dropdown-trigger { 2 | position: relative; 3 | 4 | span.clear { 5 | cursor: pointer; 6 | color: #555; 7 | line-height: 1.5; 8 | font-size: 0.9rem; 9 | background-color: #fff; 10 | padding: 0 2px; 11 | right: 0.75rem; 12 | top: 50%; 13 | transform: translateY(-50%); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/styles/components/sy-durationpicker-day.scss: -------------------------------------------------------------------------------- 1 | .extendend-durationpicker-day { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | 6 | * { 7 | flex: 0 0 1.25rem; 8 | } 9 | 10 | input { 11 | width: 100%; 12 | flex: 1 0 70%; 13 | } 14 | 15 | :nth-child(2) { 16 | margin-left: 0.25rem; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/styles/components/sy-modal--footer.scss: -------------------------------------------------------------------------------- 1 | .modal-footer { 2 | .btn:not(:last-of-type) { 3 | margin-right: 0.7rem; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/styles/components/sy-modal--overlay.scss: -------------------------------------------------------------------------------- 1 | .modal-overlay { 2 | transition: none; 3 | } 4 | -------------------------------------------------------------------------------- /app/styles/components/sy-toggle.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | 3 | .sy-toggle { 4 | display: flex; 5 | align-items: center; 6 | cursor: pointer; 7 | margin: auto; 8 | 9 | &.active { 10 | color: color.adjust($color-primary, $lightness: -10%); 11 | } 12 | 13 | &.inactive { 14 | color: $color-secondary; 15 | } 16 | 17 | &.form-control { 18 | background-color: unset; 19 | border: unset; 20 | box-shadow: unset; 21 | } 22 | } 23 | 24 | .form-list-cell > .margin-small-right { 25 | margin-right: 0.5rem; 26 | } 27 | -------------------------------------------------------------------------------- /app/styles/components/timed-clock.scss: -------------------------------------------------------------------------------- 1 | .timed-clock { 2 | --clock-size: 50px; 3 | --clock-color: rgb(87, 87, 87); 4 | --clock-color-secondary: rgb(217, 83, 79); 5 | 6 | width: var(--clock-size); 7 | height: var(--clock-size); 8 | 9 | .circle { 10 | fill: transparent; 11 | stroke: var(--clock-color); 12 | } 13 | 14 | .hour { 15 | stroke: var(--clock-color); 16 | } 17 | 18 | .minute { 19 | stroke: var(--clock-color); 20 | } 21 | 22 | .second { 23 | stroke: var(--clock-color-secondary); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/styles/components/tracking-bar.scss: -------------------------------------------------------------------------------- 1 | .tracking-bar { 2 | background: white; 3 | border-bottom: 1px solid rgb(220, 220, 220); 4 | padding-bottom: 1.5rem; 5 | margin-bottom: 1.5rem; 6 | 7 | @media #{$lg-viewport} { 8 | #task-form { 9 | display: flex; 10 | flex-direction: row; 11 | 12 | .form-group { 13 | margin-bottom: 0; 14 | } 15 | 16 | .form-group:not(:last-child) { 17 | margin-right: 0.5rem; 18 | } 19 | 20 | .form-group:not(:last-child) { 21 | flex-grow: 2; 22 | } 23 | 24 | .form-group:nth-last-child(2) { 25 | flex-grow: 3; 26 | } 27 | } 28 | 29 | .form-control { 30 | width: 100%; 31 | } 32 | } 33 | 34 | @media #{$xs-viewport} { 35 | .form-control { 36 | padding-right: 35px; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/styles/components/weekly-overview-benchmark.scss: -------------------------------------------------------------------------------- 1 | .weekly-overview-benchmark { 2 | display: flex; 3 | align-items: center; 4 | position: absolute; 5 | left: 0; 6 | bottom: 0; 7 | top: 0; 8 | right: 0; 9 | 10 | &.expected { 11 | hr { 12 | background-color: $color-primary; 13 | } 14 | 15 | span { 16 | color: $color-primary; 17 | } 18 | } 19 | 20 | hr { 21 | margin: 0; 22 | border: none; 23 | height: 1px; 24 | width: 100%; 25 | background-color: transparentize($color-primary, 0.7); 26 | position: absolute; 27 | left: 0; 28 | right: 0; 29 | } 30 | 31 | span { 32 | width: 30px; 33 | font-family: $font-family-mono; 34 | font-size: 12px; 35 | text-align: right; 36 | color: transparentize($color-primary, 0.7); 37 | transform: translateY(50%); 38 | position: absolute; 39 | left: -35px; 40 | right: 0; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/styles/components/weekly-overview.scss: -------------------------------------------------------------------------------- 1 | .weekly-overview { 2 | width: 100%; 3 | padding: 20px 0 50px 50px; 4 | overflow: hidden; 5 | display: none; 6 | display: flex; 7 | 8 | .weekly-overview-children { 9 | flex-grow: 1; 10 | position: relative; 11 | display: flex; 12 | justify-content: space-around; 13 | align-items: flex-end; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/styles/components/welcome-modal.scss: -------------------------------------------------------------------------------- 1 | .welcome-modal-body { 2 | .logo { 3 | max-width: 50%; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/styles/loader.scss: -------------------------------------------------------------------------------- 1 | .loader { 2 | height: 2px; 3 | width: 100%; 4 | position: absolute; 5 | top: 0; 6 | right: 0; 7 | left: 0; 8 | z-index: 9999; 9 | overflow: hidden; 10 | background-color: #ddd; 11 | } 12 | .loader:before { 13 | display: block; 14 | position: absolute; 15 | content: ""; 16 | left: -200px; 17 | width: 200px; 18 | height: 4px; 19 | background-color: $color-primary; 20 | animation: loading 2s linear infinite; 21 | } 22 | 23 | @keyframes loading { 24 | from { 25 | left: -200px; 26 | width: 30%; 27 | } 28 | 50% { 29 | width: 30%; 30 | } 31 | 70% { 32 | width: 70%; 33 | } 34 | 80% { 35 | left: 50%; 36 | } 37 | 95% { 38 | left: 120%; 39 | } 40 | to { 41 | left: 100%; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/styles/login.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | text-align: center; 3 | 4 | h1 { 5 | margin-top: 0.8rem; 6 | } 7 | 8 | .timed-clock { 9 | margin: 0 auto 0.5rem; 10 | } 11 | 12 | @media only screen and (max-width: 768px) { 13 | .btn-primary { 14 | margin-left: 0.5rem; 15 | margin-right: 0.5rem; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/styles/statistics.scss: -------------------------------------------------------------------------------- 1 | .table--statistics td:last-child, 2 | .table--statistics th:last-child { 3 | width: 50%; 4 | } -------------------------------------------------------------------------------- /app/styles/toolbar.scss: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | display: flex; 3 | flex-direction: row; 4 | } 5 | 6 | .toolbar-content { 7 | display: flex; 8 | flex-grow: 1; 9 | justify-content: flex-start; 10 | margin-bottom: 1rem; 11 | } 12 | 13 | .toolbar-content--right { 14 | justify-content: flex-end; 15 | } 16 | -------------------------------------------------------------------------------- /app/styles/users-navigation.scss: -------------------------------------------------------------------------------- 1 | .user-navigation { 2 | margin: 0 ($page-padding-h * -1); 3 | border-bottom: 1px solid $color-border; 4 | background-color: rgb(255,255,255); 5 | box-shadow: 0 2px 6px rgba(0,0,0,0.1); 6 | } 7 | 8 | .user-navigation > ul { 9 | list-style: none; 10 | display: flex; 11 | width: 100%; 12 | justify-content: center; 13 | } 14 | 15 | .user-navigation > ul > li { 16 | flex: 1 0 auto; 17 | display: flex; 18 | } 19 | 20 | .user-navigation > ul > li > a { 21 | flex-grow: 1; 22 | text-align: center; 23 | padding: 0.9rem 2rem; 24 | color: rgb(180,180,180); 25 | font-size: 1.1rem; 26 | } 27 | 28 | .user-navigation > ul > li > a.active { 29 | color: $color-primary; 30 | box-shadow: inset 0 -2px 0 $color-primary; 31 | } 32 | 33 | @media #{$md-viewport} { 34 | .user-navigation > ul > li { 35 | flex-grow: 0; 36 | } 37 | } 38 | 39 | @media #{$lg-viewport} { 40 | .user-navigation { 41 | margin: 0 ($page-padding-h * 1.25 * -1); 42 | } 43 | } 44 | 45 | @media #{$xl-viewport} { 46 | .user-navigation{ 47 | margin: 0 ($page-padding-h * 1.5 * -1); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/tours/index.js: -------------------------------------------------------------------------------- 1 | import IndexActivities from "./index/activities"; 2 | import IndexAttendances from "./index/attendances"; 3 | import IndexReports from "./index/reports"; 4 | 5 | export default { 6 | "index.activities": IndexActivities, 7 | "index.attendances": IndexAttendances, 8 | "index.reports": IndexReports, 9 | }; 10 | -------------------------------------------------------------------------------- /app/tours/index/attendances.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: "addAttendance", 4 | target: ".btn-toolbar .btn-success", 5 | placement: "left", 6 | title: "Add attendance", 7 | content: ` 8 |

9 | Attendances represent time blocks in which you were at the workplace. 10 | They don't count as worktime but are a help for you to roughly guess the 11 | worktime you should have. 12 |

13 |

14 | To add a new attendance just click here. 15 |

16 | `, 17 | }, 18 | { 19 | id: "editAttendance", 20 | target: ".visible-md", 21 | placement: "top", 22 | title: "Edit attendance", 23 | content: ` 24 |

25 | Now you can just adjust the time block by grabing and moving it or 26 | grabing it on one of the ends and adjusting the start or end time. The 27 | attendance saves automatically after every change. 28 |

29 |

30 | You can add as many attendances per day as you want. 31 |

32 | `, 33 | }, 34 | ]; 35 | -------------------------------------------------------------------------------- /app/transforms/django-date.js: -------------------------------------------------------------------------------- 1 | import MomentTransform from "timed/transforms/moment"; 2 | 3 | /** 4 | * The django date transform 5 | * 6 | * This transforms a django date into a moment date 7 | * 8 | * @class DjangoDateTransform 9 | * @extends MomentTransform 10 | * @public 11 | */ 12 | export default class DjangoDateTransform extends MomentTransform { 13 | /** 14 | * The date format 15 | * 16 | * @property {String} format 17 | * @public 18 | */ 19 | format = "YYYY-MM-DD"; 20 | } 21 | -------------------------------------------------------------------------------- /app/transforms/django-datetime.js: -------------------------------------------------------------------------------- 1 | import MomentTransform from "timed/transforms/moment"; 2 | 3 | /** 4 | * The django datetime transform 5 | * 6 | * This transforms a django datetime into a moment datetime 7 | * 8 | * @class DjangoDatetimeTransform 9 | * @extends MomentTransform 10 | * @public 11 | */ 12 | export default class DjangoDatetimeTransform extends MomentTransform { 13 | /** 14 | * The date format 15 | * 16 | * @property {String} format 17 | * @public 18 | */ 19 | format = "YYYY-MM-DDTHH:mm:ss.SSSSZ"; 20 | } 21 | -------------------------------------------------------------------------------- /app/transforms/django-time.js: -------------------------------------------------------------------------------- 1 | import MomentTransform from "timed/transforms/moment"; 2 | 3 | /** 4 | * The django time transform 5 | * 6 | * This transforms a django time into a moment object 7 | * 8 | * @class DjangoTimeTransform 9 | * @extends MomentTransform 10 | * @public 11 | */ 12 | export default class DjangoTimeTransform extends MomentTransform { 13 | /** 14 | * The time format 15 | * 16 | * @property {String} format 17 | * @public 18 | */ 19 | format = "HH:mm:ss"; 20 | } 21 | -------------------------------------------------------------------------------- /app/transforms/django-workdays.js: -------------------------------------------------------------------------------- 1 | import Transform from "@ember-data/serializer/transform"; 2 | 3 | /** 4 | * Django worktime transform 5 | * 6 | * This transforms a string like '1,2,3' into an array of numbers 7 | * 8 | * @class DjangoWorktimeTransform 9 | * @extends DS.Transform 10 | * @public 11 | */ 12 | export default class DjangoWorkdaysTransform extends Transform { 13 | /** 14 | * Deserialize the string separated by comma into an array of numbers 15 | * 16 | * @method deserialize 17 | * @param {String} serialized The string 18 | * @return {Number[]} The deserialized array 19 | * @public 20 | */ 21 | deserialize(serialized) { 22 | return serialized.map(Number); 23 | } 24 | 25 | /** 26 | * Serialize the array of numbers into a string separated by comma 27 | * 28 | * @method serialize 29 | * @param {Number[]} deserialized The number array 30 | * @return {String} The serialized string 31 | * @public 32 | */ 33 | serialize(deserialized) { 34 | return deserialized.map(String); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/users/edit/credits/absence-credits/edit/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class UsersEditCreditsAbsenceCreditsEditRoute extends Route { 4 | model = ({ absence_credit_id: id }) => id; 5 | 6 | setupController(controller, ...args) { 7 | super.setupController(controller, ...args); 8 | 9 | controller.set("user", this.modelFor("users.edit")); 10 | controller.absenceTypes.perform(); 11 | controller.credit.perform(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/users/edit/credits/absence-credits/new/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | const EDIT_PATH = "users.edit.credits.absence-credits.edit"; 4 | 5 | export default class UsersEditCreditsAbsenceCreditNewRoute extends Route { 6 | controllerName = EDIT_PATH; 7 | 8 | templateName = EDIT_PATH; 9 | 10 | model = () => null; 11 | 12 | setupController(controller, ...args) { 13 | super.setupController(controller, ...args); 14 | 15 | controller.set("user", this.modelFor("users.edit")); 16 | controller.absenceTypes.perform(); 17 | controller.credit.perform(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/users/edit/credits/index/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class UsersEditCreditsIndexRoute extends Route { 4 | model() { 5 | return this.modelFor("users/edit"); 6 | } 7 | setupController(controller, model, ...args) { 8 | super.setupController(controller, model, ...args); 9 | controller.years.perform(); 10 | controller.absenceCredits.perform(); 11 | controller.overtimeCredits.perform(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/users/edit/credits/overtime-credits/edit/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class UsersEditCreditsOvertimeCreditsRoute extends Route { 4 | model = ({ overtime_credit_id: id }) => id; 5 | 6 | setupController(controller, ...args) { 7 | super.setupController(controller, ...args); 8 | 9 | controller.set("user", this.modelFor("users.edit")); 10 | controller.credit.perform(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/users/edit/credits/overtime-credits/new/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | const EDIT_PATH = "users.edit.credits.overtime-credits.edit"; 4 | 5 | export default class UsersEditCreditsOvertimeCreditsNewRoute extends Route { 6 | controllerName = EDIT_PATH; 7 | 8 | templateName = EDIT_PATH; 9 | 10 | model = () => null; 11 | 12 | setupController(controller, ...args) { 13 | super.setupController(controller, ...args); 14 | 15 | controller.set("user", this.modelFor("users.edit")); 16 | controller.credit.perform(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/users/edit/credits/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class UsersEditCreditsRoute extends Route {} 4 | -------------------------------------------------------------------------------- /app/users/edit/credits/template.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} -------------------------------------------------------------------------------- /app/users/edit/index/controller.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { inject as service } from "@ember/service"; 3 | import { task } from "ember-concurrency"; 4 | import moment from "moment"; 5 | 6 | export default class EditUser extends Controller { 7 | @service store; 8 | 9 | @task 10 | *absences() { 11 | return yield this.store.query("absence", { 12 | user: this.user.id, 13 | ordering: "-date", 14 | // eslint-disable-next-line camelcase 15 | from_date: moment({ 16 | day: 1, 17 | month: 0, 18 | year: this.year, 19 | }).format("YYYY-MM-DD"), 20 | include: "absence_type", 21 | }); 22 | } 23 | 24 | @task 25 | *employments() { 26 | return yield this.store.query("employment", { 27 | user: this.user.id, 28 | ordering: "-start_date", 29 | include: "location", 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/users/edit/index/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class UsersEditsIndexRoute extends Route { 4 | model() { 5 | return this.modelFor("users/edit"); 6 | } 7 | 8 | setupController(controller, model, ...args) { 9 | super.setupController(controller, model, ...args); 10 | 11 | controller.set("user", model); 12 | controller.absences.perform(); 13 | controller.employments.perform(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/users/edit/responsibilities/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class UsersEditResponsitibilitesRoute extends Route { 4 | model() { 5 | return this.modelFor("users/edit"); 6 | } 7 | 8 | setupController(controller, model, ...args) { 9 | super.setupController(controller, model, ...args); 10 | 11 | controller.set("user", model); 12 | controller.projects.perform(); 13 | controller.supervisees.perform(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/users/edit/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { inject as service } from "@ember/service"; 3 | 4 | export default class EditUserRoute extends Route { 5 | @service store; 6 | model({ user_id: id }) { 7 | return this.store.findRecord("user", id, { include: "supervisors" }); 8 | } 9 | 10 | setupController(controller, model, ...args) { 11 | super.setupController(controller, model, ...args); 12 | controller.data.perform(model.id); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/users/index/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class UsersIndexRoute extends Route {} 4 | -------------------------------------------------------------------------------- /app/users/route.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class UsersRoute extends Route {} 4 | -------------------------------------------------------------------------------- /app/users/template.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} -------------------------------------------------------------------------------- /app/utils/humanize-duration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-utils 4 | * @public 5 | */ 6 | 7 | const { abs, floor } = Math; 8 | 9 | /** 10 | * Converts a moment duration into a string with hours minutes and optionally 11 | * seconds 12 | * 13 | * @function humanizeDuration 14 | * @param {moment.duration} duration The duration to format 15 | * @param {Boolean} seconds Whether to show seconds 16 | * @return {String} The formatted duration 17 | * @public 18 | */ 19 | export default function humanizeDuration(duration, seconds = false) { 20 | if (!duration || duration.milliseconds() < 0) { 21 | return seconds ? "0h 0m 0s" : "0h 0m"; 22 | } 23 | 24 | const prefix = duration < 0 ? "-" : ""; 25 | 26 | // TODO: The locale should be defined by the browser 27 | const h = floor(abs(duration.asHours())).toLocaleString("de-CH"); 28 | const m = abs(duration.minutes()); 29 | 30 | if (seconds) { 31 | const s = abs(duration.seconds()); 32 | 33 | return `${prefix}${h}h ${m}m ${s}s`; 34 | } 35 | 36 | return `${prefix}${h}h ${m}m`; 37 | } 38 | -------------------------------------------------------------------------------- /app/utils/parse-django-duration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-utils 4 | * @public 5 | */ 6 | import moment from "moment"; 7 | 8 | /** 9 | * Converts a django duration string to a moment duration 10 | * 11 | * @function parseDjangoDuration 12 | * @param {String} str The django duration string representation 13 | * @return {moment.duration} The parsed duration 14 | * @public 15 | */ 16 | export default function parseDjangoDuration(str) { 17 | if (!str) { 18 | return null; 19 | } 20 | 21 | const re = new RegExp(/^(-?\d+)?\s?(\d{2}):(\d{2}):(\d{2})(\.\d{6})?$/); 22 | 23 | const [, days, hours, minutes, seconds, microseconds] = str 24 | .match(re) 25 | .map((m) => Number(m) || 0); 26 | 27 | return moment.duration({ 28 | days, 29 | hours, 30 | minutes, 31 | seconds, 32 | milliseconds: microseconds * 1000, 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /app/utils/serialize-moment.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | export const DATE_FORMAT = "YYYY-MM-DD"; 4 | 5 | export function serializeMoment(momentObject) { 6 | if (momentObject) { 7 | momentObject = moment(momentObject); 8 | } 9 | return (momentObject && momentObject.format(DATE_FORMAT)) || null; 10 | } 11 | export function deserializeMoment(momentString) { 12 | return (momentString && moment(momentString, DATE_FORMAT)) || null; 13 | } 14 | -------------------------------------------------------------------------------- /app/utils/url.js: -------------------------------------------------------------------------------- 1 | const notNullOrUndefined = (value) => value !== null && value !== undefined; 2 | 3 | export const cleanParams = (params) => 4 | Object.keys(params) 5 | .filter((key) => notNullOrUndefined(params[key])) 6 | .reduce((cleaned, key) => ({ ...cleaned, [key]: params[key] }), {}); 7 | 8 | export const toQueryString = (params) => 9 | Object.keys(params) 10 | .map((key) => `${key}=${params[key]}`) 11 | .join("&"); 12 | -------------------------------------------------------------------------------- /app/validations/absence-credit.js: -------------------------------------------------------------------------------- 1 | import { 2 | validatePresence, 3 | validateNumber, 4 | } from "ember-changeset-validations/validators"; 5 | 6 | export default { 7 | user: validatePresence(true), 8 | date: validatePresence(true), 9 | absenceType: validatePresence(true), 10 | days: [validatePresence(true), validateNumber({ integer: true })], 11 | }; 12 | -------------------------------------------------------------------------------- /app/validations/absence.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-validations 4 | * @public 5 | */ 6 | import { validatePresence } from "ember-changeset-validations/validators"; 7 | 8 | /** 9 | * Validations for absences 10 | * 11 | * @class AbsenceValidations 12 | * @public 13 | */ 14 | export default { 15 | /** 16 | * Absence type validator, check if an absence type is existent 17 | * 18 | * @property {Function} absenceType 19 | * @public 20 | */ 21 | absenceType: validatePresence(true), 22 | }; 23 | -------------------------------------------------------------------------------- /app/validations/activity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-validations 4 | * @public 5 | */ 6 | import validateMoment from "timed/validators/moment"; 7 | 8 | /** 9 | * Validations for activities 10 | * 11 | * @class ActivityValidations 12 | * @public 13 | */ 14 | export default { 15 | from: validateMoment({ lt: "to" }), 16 | to: validateMoment({ gt: "from" }), 17 | }; 18 | -------------------------------------------------------------------------------- /app/validations/attendance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-validations 4 | * @public 5 | */ 6 | import { validatePresence } from "ember-changeset-validations/validators"; 7 | import validateMoment from "timed/validators/moment"; 8 | 9 | /** 10 | * Validations for attendances 11 | * 12 | * @class AttendanceValidations 13 | * @public 14 | */ 15 | export default { 16 | date: validatePresence(true), 17 | from: [validatePresence(true), validateMoment({ lt: "to" })], 18 | to: [validatePresence(true), validateMoment({ gt: "from" })], 19 | }; 20 | -------------------------------------------------------------------------------- /app/validations/intersection.js: -------------------------------------------------------------------------------- 1 | import validateIntersectionTask from "timed/validators/intersection-task"; 2 | import validateNullOrNotBlank from "timed/validators/null-or-not-blank"; 3 | 4 | export default { 5 | task: validateIntersectionTask(), 6 | comment: validateNullOrNotBlank(), 7 | }; 8 | -------------------------------------------------------------------------------- /app/validations/multiple-absence.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-validations 4 | * @public 5 | */ 6 | import { 7 | validateLength, 8 | validatePresence, 9 | } from "ember-changeset-validations/validators"; 10 | 11 | /** 12 | * Validations for multiple absences 13 | * 14 | * @class MultipleAbsenceValidations 15 | * @public 16 | */ 17 | export default { 18 | /** 19 | * Absence type validator, check if an absence type is existent 20 | * 21 | * @property {Function} absenceTtype 22 | * @public 23 | */ 24 | absenceType: validatePresence(true), 25 | 26 | /** 27 | * Date validation, ensure at least one date is selected 28 | * 29 | * @property {Function} dates 30 | * @public 31 | */ 32 | dates: validateLength({ 33 | min: 1, 34 | message: "At least one date must be selected", 35 | }), 36 | }; 37 | -------------------------------------------------------------------------------- /app/validations/overtime-credit.js: -------------------------------------------------------------------------------- 1 | import { validatePresence } from "ember-changeset-validations/validators"; 2 | 3 | export default { 4 | user: validatePresence(true), 5 | date: validatePresence(true), 6 | duration: validatePresence(true), 7 | }; 8 | -------------------------------------------------------------------------------- /app/validations/project.js: -------------------------------------------------------------------------------- 1 | import { validatePresence } from "ember-changeset-validations/validators"; 2 | 3 | export default { 4 | name: validatePresence(true), 5 | billingType: validatePresence(true), 6 | }; 7 | -------------------------------------------------------------------------------- /app/validations/report.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timed 3 | * @submodule timed-validations 4 | * @public 5 | */ 6 | import { validatePresence } from "ember-changeset-validations/validators"; 7 | 8 | /** 9 | * Validations for reports 10 | * 11 | * @class ReportValidations 12 | * @public 13 | */ 14 | export default { 15 | /** 16 | * Task validator, check if a task is existent 17 | * 18 | * @property {Function} task 19 | * @public 20 | */ 21 | task: validatePresence(true), 22 | 23 | /** 24 | * Duration validator, check if a duration is existent 25 | * 26 | * @property {Function} duration 27 | * @public 28 | */ 29 | duration: validatePresence(true), 30 | }; 31 | -------------------------------------------------------------------------------- /app/validations/task.js: -------------------------------------------------------------------------------- 1 | import { validatePresence } from "ember-changeset-validations/validators"; 2 | 3 | export default { 4 | name: validatePresence(true), 5 | }; 6 | -------------------------------------------------------------------------------- /app/validators/intersection-task.js: -------------------------------------------------------------------------------- 1 | export default function validateIntersectionTask() { 2 | return (key, newValue, oldValue, changes, content) => { 3 | const customerChanged = 4 | Object.keys(changes).includes("customer") && 5 | (changes.customer?.get("id") || null) !== 6 | (content.customer?.get("id") || null); 7 | 8 | const projectChanged = 9 | Object.keys(changes).includes("project") && 10 | (changes.project?.get("id") || null) !== 11 | (content.project?.get("id") || null); 12 | 13 | const hasTask = !!(newValue && newValue.id); 14 | 15 | return ( 16 | hasTask || 17 | (!hasTask && !customerChanged && !projectChanged) || 18 | "Task must not be empty" 19 | ); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /app/validators/null-or-not-blank.js: -------------------------------------------------------------------------------- 1 | import { capitalize } from "@ember/string"; 2 | 3 | export default function validateNullOrNotBlank() { 4 | return (key, newValue) => { 5 | return ( 6 | !!(newValue === null || (newValue && newValue.length > 0)) || 7 | `${capitalize(key)} must be null or not blank` 8 | ); 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /config/coverage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useBabelInstrumenter: true, 3 | babelPlugins: [ 4 | "babel-plugin-transform-async-to-generator", 5 | "babel-plugin-transform-decorators-legacy", 6 | "babel-plugin-transform-object-rest-spread", 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /config/dependency-lint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // only lint deps manually 3 | generateTests: false, 4 | }; 5 | -------------------------------------------------------------------------------- /config/deprecation-workflow.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | window.deprecationWorkflow = window.deprecationWorkflow || {}; 3 | self.deprecationWorkflow.config = { 4 | workflow: [ 5 | { handler: "silence", matchId: "ensure-safe-component.string" }, // optimized-power-select 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "4.4.1", 7 | "blueprints": [ 8 | { 9 | "name": "app", 10 | "outputRepo": "https://github.com/ember-cli/ember-new-output", 11 | "codemodsSource": "ember-app-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": ["--yarn", "--no-welcome"] 14 | } 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /config/icons.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return { 3 | "free-regular-svg-icons": [ 4 | "calendar", 5 | "calendar-plus", 6 | "calendar-xmark", 7 | "chart-bar", 8 | "clock", 9 | "eye", 10 | "floppy-disk", 11 | "folder-open", 12 | "hand", 13 | "square", 14 | "square-check", 15 | "circle-xmark", 16 | "trash-can", 17 | "user", 18 | ], 19 | "free-solid-svg-icons": [ 20 | "angle-right", 21 | "angle-left", 22 | "arrow-left", 23 | "arrow-right", 24 | "ban", 25 | "bolt", 26 | "briefcase", 27 | "chart-line", 28 | "chevron-left", 29 | "dollar-sign", 30 | "download", 31 | "exclamation-triangle", 32 | "info-circle", 33 | "magnifying-glass", 34 | "mobile-screen-button", 35 | "power-off", 36 | "slash", 37 | "sliders", 38 | "sort", 39 | "sort-down", 40 | "sort-up", 41 | "stop", 42 | "square", 43 | "play", 44 | "plus", 45 | "users", 46 | "question", 47 | "magic", 48 | ], 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /config/targets.js: -------------------------------------------------------------------------------- 1 | "use-strict"; 2 | 3 | const browsers = [ 4 | "last 1 Chrome versions", 5 | "last 1 Firefox versions", 6 | "last 1 Safari versions", 7 | ]; 8 | 9 | module.exports = { 10 | browsers, 11 | }; 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | db: 5 | image: postgres:9.4 6 | ports: 7 | - 5432:5432 8 | volumes: 9 | - dbdata:/var/lib/postgresql/data 10 | environment: 11 | - POSTGRES_USER=timed 12 | - POSTGRES_PASSWORD=timed 13 | 14 | frontend: 15 | build: 16 | context: . 17 | ports: 18 | - 4200:80 19 | 20 | backend: 21 | image: ghcr.io/adfinis/timed-backend:latest 22 | ports: 23 | - 8000:80 24 | depends_on: 25 | - db 26 | - mailhog 27 | environment: 28 | - DJANGO_DATABASE_HOST=db 29 | - DJANGO_DATABASE_PORT=5432 30 | - ENV=docker 31 | - STATIC_ROOT=/var/www/static 32 | - EMAIL_URL=smtp://mailhog:1025 33 | command: /bin/sh -c "wait-for-it.sh -t 60 db:5432 -- ./manage.py migrate && ./manage.py loaddata timed/fixtures/test_data.json && uwsgi" 34 | 35 | mailhog: 36 | image: mailhog/mailhog 37 | ports: 38 | - 8025:8025 39 | environment: 40 | - MH_UI_WEB_PATH=mailhog 41 | 42 | volumes: 43 | dbdata: 44 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eu 4 | 5 | urlencode() { 6 | # urlencode 7 | # blatantly pinched from https://gist.github.com/cdown/1163649 8 | 9 | local length="${#1}" 10 | for i in $(seq 0 $((length-1))); do 11 | local c="${1:i:1}" 12 | case $c in 13 | [a-zA-Z0-9.~_-]) printf "$c" ;; 14 | *) printf '%%%02X' "'$c" ;; 15 | esac 16 | done 17 | } 18 | 19 | sed -i \ 20 | -e "s/sso-client-id/$(urlencode ${TIMED_SSO_CLIENT_ID})/g" \ 21 | -e "s/sso-client-host/$(urlencode ${TIMED_SSO_CLIENT_HOST})/g" \ 22 | /var/www/html/index.html 23 | 24 | exec "$@" 25 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // eslint-disable-next-line n/no-missing-require 4 | const Funnel = require("broccoli-funnel"); 5 | const EmberApp = require("ember-cli/lib/broccoli/ember-app"); 6 | 7 | module.exports = function (defaults) { 8 | const app = new EmberApp(defaults, { 9 | sassOptions: { 10 | onlyIncluded: true, 11 | }, 12 | "ember-fetch": { 13 | preferNative: true, 14 | }, 15 | "ember-simple-auth": { 16 | useSessionSetupMethod: true, 17 | }, 18 | "ember-validated-form": { 19 | theme: "bootstrap", 20 | }, 21 | }); 22 | 23 | app.import("node_modules/@fontsource/source-sans-pro/index.css"); 24 | 25 | app.import("node_modules/simplebar/dist/simplebar.css"); 26 | 27 | app.import("node_modules/downloadjs/download.min.js", { 28 | using: [{ transformation: "amd", as: "downloadjs" }], 29 | }); 30 | 31 | const fonts = new Funnel("node_modules/@fontsource/source-sans-pro/files", { 32 | include: ["*.woff", "*.woff2"], 33 | destDir: "/assets/files/", 34 | }); 35 | 36 | return app.toTree([fonts]); 37 | }; 38 | -------------------------------------------------------------------------------- /mirage/factories/absence-balance.js: -------------------------------------------------------------------------------- 1 | import { Factory, trait } from "ember-cli-mirage"; 2 | import faker from "faker"; 3 | import moment from "moment"; 4 | 5 | import { randomDuration } from "../helpers/duration"; 6 | 7 | export default Factory.extend({ 8 | date: () => moment(), 9 | balance: () => randomDuration(), 10 | 11 | days: trait({ 12 | credit: () => faker.random.number({ min: 10, max: 20 }), 13 | usedDays: () => faker.random.number({ min: 5, max: 25 }), 14 | }), 15 | 16 | duration: trait({ 17 | usedDuration: () => randomDuration(), 18 | }), 19 | }); 20 | -------------------------------------------------------------------------------- /mirage/factories/absence-credit.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | import faker from "faker"; 3 | import moment from "moment"; 4 | 5 | export default Factory.extend({ 6 | date: () => moment().format("YYYY-MM-DD"), 7 | days: () => faker.random.number({ min: 1, max: 25 }), 8 | comment: () => faker.lorem.sentence(), 9 | 10 | afterCreate(absenceCredit, server) { 11 | absenceCredit.update({ absenceTypeId: server.db.absenceTypes[0].id }); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /mirage/factories/absence-type.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | import faker from "faker"; 3 | 4 | export default Factory.extend({ 5 | name: () => faker.lorem.word(), 6 | fillWorktime: false, 7 | }); 8 | -------------------------------------------------------------------------------- /mirage/factories/absence.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | import faker from "faker"; 3 | import moment from "moment"; 4 | 5 | export default Factory.extend({ 6 | comment: () => faker.lorem.sentence(), 7 | date: () => moment().format("YYYY-MM-DD"), 8 | duration: () => "08:30:00", 9 | 10 | afterCreate(absence, server) { 11 | absence.update({ 12 | absenceTypeId: server.schema.absenceTypes.all().models[0].id, 13 | }); 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /mirage/factories/attendance.js: -------------------------------------------------------------------------------- 1 | import { Factory, trait } from "ember-cli-mirage"; 2 | import moment from "moment"; 3 | 4 | export default Factory.extend({ 5 | date: moment().format("YYYY-MM-DD"), 6 | 7 | morning: trait({ 8 | fromTime: "08:00:00", 9 | toTime: "11:30:00", 10 | }), 11 | 12 | afternoon: trait({ 13 | fromTime: "12:00:00", 14 | toTime: "17:00:00", 15 | }), 16 | }); 17 | -------------------------------------------------------------------------------- /mirage/factories/billing-type.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | import faker from "faker"; 3 | 4 | export default Factory.extend({ 5 | name: () => faker.lorem.word(), 6 | }); 7 | -------------------------------------------------------------------------------- /mirage/factories/cost-center.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | import faker from "faker"; 3 | 4 | export default Factory.extend({ 5 | name: () => faker.finance.accountName(), 6 | reference: () => faker.finance.account(), 7 | }); 8 | -------------------------------------------------------------------------------- /mirage/factories/customer-statistic.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | 3 | import { randomDuration } from "../helpers/duration"; 4 | 5 | export default Factory.extend({ 6 | duration: () => randomDuration(15, false, 20), 7 | 8 | afterCreate(customerStatistic, server) { 9 | customerStatistic.update({ customerId: server.create("customer").id }); 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /mirage/factories/customer.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | import faker from "faker"; 3 | 4 | export default Factory.extend({ 5 | name: () => faker.company.companyName(), 6 | }); 7 | -------------------------------------------------------------------------------- /mirage/factories/employment.js: -------------------------------------------------------------------------------- 1 | import { Factory, trait } from "ember-cli-mirage"; 2 | import faker from "faker"; 3 | import moment from "moment"; 4 | import DjangoDurationTransform from "timed/transforms/django-duration"; 5 | 6 | export default Factory.extend({ 7 | percentage: faker.random.arrayElement([50, 60, 80, 100]), 8 | // location: association(), 9 | // user: association(), 10 | 11 | isExternal: false, 12 | 13 | worktimePerDay() { 14 | const worktime = moment.duration( 15 | (moment.duration({ h: 8, m: 30 }) / 100) * this.percentage 16 | ); 17 | 18 | return DjangoDurationTransform.create().serialize(worktime); 19 | }, 20 | 21 | start: () => faker.date.past(4), 22 | end: () => faker.date.past(1), 23 | 24 | active: trait({ 25 | start: () => faker.date.recent(), 26 | end: null, 27 | }), 28 | 29 | afterCreate(employment, server) { 30 | employment.update({ locationId: server.create("location").id }); 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /mirage/factories/location.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | import faker from "faker"; 3 | 4 | export default Factory.extend({ 5 | name: () => faker.address.city(), 6 | workdays: () => ["1", "2", "3", "4", "5"], 7 | }); 8 | -------------------------------------------------------------------------------- /mirage/factories/month-statistic.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | 3 | import { randomDuration } from "../helpers/duration"; 4 | 5 | export default Factory.extend({ 6 | year: (i) => 2010 + Math.ceil((i + 1) / 12), 7 | month: (i) => (i % 12) + 1, 8 | duration: () => randomDuration(15, false, 20), 9 | }); 10 | -------------------------------------------------------------------------------- /mirage/factories/overtime-credit.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | import faker from "faker"; 3 | import moment from "moment"; 4 | 5 | import { randomDuration } from "../helpers/duration"; 6 | 7 | export default Factory.extend({ 8 | date: () => moment().format("YYYY-MM-DD"), 9 | duration: () => randomDuration(), 10 | comment: () => faker.lorem.sentence(), 11 | }); 12 | -------------------------------------------------------------------------------- /mirage/factories/project-assignee.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | 3 | export default Factory.extend({ 4 | isReviewer: true, 5 | 6 | afterCreate(projectAssignee, server) { 7 | const project = server.create("project"); 8 | const user = server.create("user"); 9 | projectAssignee.update({ project }); 10 | projectAssignee.update({ user }); 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /mirage/factories/project-statistic.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | 3 | import { randomDuration } from "../helpers/duration"; 4 | 5 | export default Factory.extend({ 6 | duration: () => randomDuration(15, false, 20), 7 | 8 | afterCreate(projectStatistic, server) { 9 | projectStatistic.update({ projectId: server.create("project").id }); 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /mirage/factories/project.js: -------------------------------------------------------------------------------- 1 | import { Factory, association, trait } from "ember-cli-mirage"; 2 | import faker from "faker"; 3 | 4 | import { randomDuration } from "../helpers/duration"; 5 | 6 | export default Factory.extend({ 7 | name: () => faker.commerce.productName(), 8 | estimatedTime: () => randomDuration(), 9 | 10 | afterCreate(project, server) { 11 | project.update({ customerId: server.create("customer").id }); 12 | }, 13 | 14 | withBillingType: trait({ 15 | billingType: association(), 16 | }), 17 | }); 18 | -------------------------------------------------------------------------------- /mirage/factories/public-holiday.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | import faker from "faker"; 3 | import moment from "moment"; 4 | 5 | export default Factory.extend({ 6 | name: () => faker.lorem.word(), 7 | // location: association(), 8 | 9 | date() { 10 | const random = faker.date.between( 11 | moment.startOf("year").format("YYYY-MM-DD"), 12 | moment.endOf("year").format("YYYY-MM-DD") 13 | ); 14 | 15 | return moment(random).startOf("day"); 16 | }, 17 | 18 | afterCreate(publicHoliday, server) { 19 | publicHoliday.update({ locationId: server.create("location").id }); 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /mirage/factories/report-intersection.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | import faker from "faker"; 3 | 4 | export default Factory.extend({ 5 | comment: () => faker.lorem.sentence(), 6 | notBillable: () => faker.random.boolean(), 7 | review: () => faker.random.boolean(), 8 | verified: () => faker.random.boolean(), 9 | 10 | afterCreate(intersection, server) { 11 | const task = server.create("task"); 12 | 13 | intersection.update({ 14 | customerId: task.project.customer.id, 15 | projectId: task.project.id, 16 | taskId: task.id, 17 | }); 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /mirage/factories/report.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | import faker from "faker"; 3 | import moment from "moment"; 4 | 5 | import { randomDuration } from "../helpers/duration"; 6 | 7 | export default Factory.extend({ 8 | comment: () => faker.lorem.sentence(), 9 | date: () => moment().format("YYYY-MM-DD"), 10 | duration: () => randomDuration(), 11 | review: () => faker.random.boolean(), 12 | notBillable: () => faker.random.boolean(), 13 | verifiedBy: null, 14 | 15 | afterCreate(report, server) { 16 | report.update({ taskId: server.create("task").id }); 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /mirage/factories/task-statistic.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | 3 | import { randomDuration } from "../helpers/duration"; 4 | 5 | export default Factory.extend({ 6 | duration: () => randomDuration(15, false, 20), 7 | 8 | afterCreate(taskStatistic, server) { 9 | taskStatistic.update({ taskId: server.create("task").id }); 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /mirage/factories/task.js: -------------------------------------------------------------------------------- 1 | import { capitalize } from "@ember/string"; 2 | import { Factory } from "ember-cli-mirage"; 3 | import faker from "faker"; 4 | 5 | import { randomDuration } from "../helpers/duration"; 6 | 7 | export default Factory.extend({ 8 | name: () => capitalize(faker.hacker.ingverb()), 9 | estimatedTime: () => randomDuration(), 10 | 11 | afterCreate(task, server) { 12 | task.update({ projectId: server.create("project").id }); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /mirage/factories/user-statistic.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | 3 | import { randomDuration } from "../helpers/duration"; 4 | 5 | export default Factory.extend({ 6 | duration: () => randomDuration(15, false, 20), 7 | 8 | afterCreate(userStatistic, server) { 9 | userStatistic.update({ userId: server.create("user").id }); 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /mirage/factories/worktime-balance.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | import moment from "moment"; 3 | 4 | import { randomDuration } from "../helpers/duration"; 5 | 6 | export default Factory.extend({ 7 | date: () => moment(), 8 | balance: () => randomDuration(), 9 | }); 10 | -------------------------------------------------------------------------------- /mirage/factories/year-statistic.js: -------------------------------------------------------------------------------- 1 | import { Factory } from "ember-cli-mirage"; 2 | 3 | import { randomDuration } from "../helpers/duration"; 4 | 5 | export default Factory.extend({ 6 | year: (i) => 2010 + i, 7 | duration: () => randomDuration(15, false, 20), 8 | }); 9 | -------------------------------------------------------------------------------- /mirage/fixtures/absence-types.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { id: 1, name: "Ferien" }, 3 | { id: 2, name: "EO" }, 4 | { id: 3, name: "Krankheit" }, 5 | ]; 6 | -------------------------------------------------------------------------------- /mirage/helpers/duration.js: -------------------------------------------------------------------------------- 1 | import faker from "faker"; 2 | import moment from "moment"; 3 | import DjangoDurationTransform from "timed/transforms/django-duration"; 4 | 5 | export function randomDuration(precision = 15, seconds = false, maxHours = 2) { 6 | const h = faker.random.number({ max: maxHours }); 7 | const m = Math.abs( 8 | Math.ceil(faker.random.number({ min: 0, max: 60 }) / precision) * precision 9 | ); 10 | const s = Math.abs(seconds ? faker.random.number({ max: 59, min: 0 }) : 0); 11 | 12 | return DjangoDurationTransform.create().serialize( 13 | moment.duration({ h, m, s }) 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /mirage/serializers/application.js: -------------------------------------------------------------------------------- 1 | import { JSONAPISerializer } from "ember-cli-mirage"; 2 | 3 | export default JSONAPISerializer.extend({ 4 | alwaysIncludeLinkageData: true, 5 | }); 6 | -------------------------------------------------------------------------------- /public/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/timed-frontend/a91a9595042c81bd618d23bcf4303bd2ea80195d/public/assets/favicon-16x16.png -------------------------------------------------------------------------------- /public/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/timed-frontend/a91a9595042c81bd618d23bcf4303bd2ea80195d/public/assets/favicon-32x32.png -------------------------------------------------------------------------------- /public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/timed-frontend/a91a9595042c81bd618d23bcf4303bd2ea80195d/public/assets/favicon.ico -------------------------------------------------------------------------------- /public/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/timed-frontend/a91a9595042c81bd618d23bcf4303bd2ea80195d/public/assets/logo.png -------------------------------------------------------------------------------- /public/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/assets/logo_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/timed-frontend/a91a9595042c81bd618d23bcf4303bd2ea80195d/public/assets/logo_text.png -------------------------------------------------------------------------------- /public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: / 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base", ":rebaseStalePrs"], 4 | "automerge": true, 5 | "automergeSchedule": ["after 12am on monday"], 6 | "automergeType": "branch", 7 | "major": { 8 | "automerge": false 9 | }, 10 | "schedule": ["before 2am on monday"], 11 | "stabilityDays": 3 12 | } 13 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | "use-strict"; 2 | 3 | module.exports = { 4 | test_page: "tests/index.html?hidepassed", 5 | disable_watching: true, 6 | parallel: -1, 7 | launch_in_dev: [], 8 | launch_in_ci: ["chrome"], 9 | browser_start_timeout: 120, 10 | browser_args: { 11 | Chrome: { 12 | ci: [ 13 | // --no-sandbox is needed when running Chrome inside a container 14 | process.env.CI ? "--no-sandbox" : null, 15 | "--headless", 16 | "--disable-gpu", 17 | "--disable-dev-shm-usage", 18 | "--disable-software-rasterizer", 19 | "--mute-audio", 20 | "--remote-debugging-port=9222", 21 | "--window-size=1440,900", 22 | ].filter(Boolean), 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | env: { 5 | embertest: true, 6 | node: true, 7 | }, 8 | globals: { 9 | server: true, 10 | taskSelect: true, 11 | userSelect: true, 12 | setBreakpoint: true, 13 | selectChoose: true, 14 | selectSearch: true, 15 | removeMultipleOption: true, 16 | clearSelected: true, 17 | }, 18 | rules: { 19 | "no-magic-numbers": "off", 20 | "require-jsdoc": "off", 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /tests/acceptance/notfound-test.js: -------------------------------------------------------------------------------- 1 | import { visit } from "@ember/test-helpers"; 2 | import { setupMirage } from "ember-cli-mirage/test-support"; 3 | import { setupApplicationTest } from "ember-qunit"; 4 | import { authenticateSession } from "ember-simple-auth/test-support"; 5 | import { module, test } from "qunit"; 6 | 7 | module("Acceptance | notfound", function (hooks) { 8 | setupApplicationTest(hooks); 9 | setupMirage(hooks); 10 | 11 | test("displays a 404 page for undefined routes if logged in", async function (assert) { 12 | const user = this.server.create("user"); 13 | 14 | // eslint-disable-next-line camelcase 15 | await authenticateSession({ user_id: user.id }); 16 | 17 | await visit("/thiswillneverbeavalidrouteurl"); 18 | 19 | assert.dom("[data-test-notfound]").exists({ count: 1 }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/helpers/session-mock.js: -------------------------------------------------------------------------------- 1 | import Service from "@ember/service"; 2 | 3 | class SessionMock extends Service { 4 | isAuthenticated = true; 5 | headers = { 6 | authorization: "Bearer TEST1234", 7 | }; 8 | } 9 | 10 | export default function setupSession(hooks) { 11 | hooks.beforeEach(async function () { 12 | this.owner.register("service:session", SessionMock); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /tests/helpers/task-select.js: -------------------------------------------------------------------------------- 1 | import { selectChoose } from "ember-power-select/test-support"; 2 | 3 | export default async function ( 4 | selector = "", 5 | options = { fromHistory: false } 6 | ) { 7 | if (options.fromHistory) { 8 | await selectChoose( 9 | `${selector} .customer-select`, 10 | ".ember-power-select-option", 11 | 0 12 | ); 13 | 14 | return; 15 | } 16 | 17 | await selectChoose( 18 | `${selector} .customer-select`, 19 | ".ember-power-select-option", 20 | 1 21 | ); 22 | await selectChoose( 23 | `${selector} .project-select`, 24 | ".ember-power-select-option", 25 | 0 26 | ); 27 | await selectChoose( 28 | `${selector} .task-select`, 29 | ".ember-power-select-option", 30 | 0 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /tests/helpers/tracking-mock.js: -------------------------------------------------------------------------------- 1 | import Service from "@ember/service"; 2 | 3 | const LOCAL_OVERRIDES = { 4 | activity: { 5 | comment: "", 6 | }, 7 | customers: [], 8 | recentTasks: [], 9 | }; 10 | 11 | class TrackingServiceStub extends Service { 12 | get activity() { 13 | return LOCAL_OVERRIDES.activity; 14 | } 15 | 16 | get customers() { 17 | return LOCAL_OVERRIDES.customers; 18 | } 19 | 20 | get recentTasks() { 21 | return LOCAL_OVERRIDES.recentTasks; 22 | } 23 | } 24 | 25 | export function setup(context, overrides) { 26 | context.owner.register("service:tracking", TrackingServiceStub); 27 | 28 | const service = context.owner.lookup("service:tracking"); 29 | 30 | Object.keys(overrides).forEach((key) => { 31 | if (LOCAL_OVERRIDES[key]) { 32 | LOCAL_OVERRIDES[key] = overrides[key]; 33 | } else { 34 | service[key] = overrides[key]; 35 | } 36 | }); 37 | } 38 | 39 | export function teardown(context) { 40 | context.owner.unregister("service:tracking"); 41 | } 42 | -------------------------------------------------------------------------------- /tests/helpers/user-select.js: -------------------------------------------------------------------------------- 1 | import { selectChoose } from "ember-power-select/test-support"; 2 | 3 | export default async function (selector = "") { 4 | await selectChoose( 5 | `${selector} .user-select`, 6 | ".ember-power-select-option", 7 | 0 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /tests/integration/components/changed-warning/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | changed warning", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs`{{changed-warning}}`); 11 | 12 | assert.dom(".fa-triangle-exclamation").exists(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/integration/components/customer-visible-icon/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | customer visible icon", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs`{{customer-visible-icon}}`); 11 | 12 | assert.dom(".fa-eye").exists(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/integration/components/filter-sidebar/label/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | filter sidebar/label", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs` 11 | {{#filter-sidebar/label}} 12 | Some label 13 | {{/filter-sidebar/label}} 14 | `); 15 | 16 | assert.dom("label").exists(); 17 | assert.dom("label").hasText("Some label"); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/integration/components/in-viewport/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | in viewport", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs` 11 |
12 |
13 | test 14 |
15 |
16 | `); 17 | 18 | assert.dom(".child").includesText("test"); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/integration/components/loading-icon/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | loading icon", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs``); 11 | 12 | assert.dom(".loading-dot").exists({ count: 9 }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/integration/components/no-mobile-message/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupMirage } from "ember-cli-mirage/test-support"; 4 | import { setupRenderingTest } from "ember-qunit"; 5 | import { module, test } from "qunit"; 6 | 7 | module("Integration | Component | no mobile message", function (hooks) { 8 | setupRenderingTest(hooks); 9 | setupMirage(hooks); 10 | 11 | test("renders", async function (assert) { 12 | await render(hbs``); 13 | assert.ok(this.element); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/integration/components/no-permission/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | no permission", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs``); 11 | 12 | assert.dom(".empty").exists(); 13 | assert.dom(".empty").includesText("Halt"); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/integration/components/not-identical-warning/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | not identical warning", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs``); 11 | 12 | assert.dom(".fa-circle-info").exists(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/integration/components/report-review-warning/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | report review warning", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs`{{report-review-warning}}`); 11 | assert.ok(this.element); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/integration/components/sort-header/component-test.js: -------------------------------------------------------------------------------- 1 | import { click, render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | sort header", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs``); 11 | assert.dom(".fa-sort").exists({ count: 1 }); 12 | }); 13 | 14 | test("renders active state", async function (assert) { 15 | this.set("current", "-test"); 16 | this.set("update", (sort) => { 17 | this.set("current", sort); 18 | }); 19 | 20 | await render( 21 | hbs`` 22 | ); 23 | assert.dom(".fa-sort-down").exists({ count: 1 }); 24 | 25 | await click(".sort-header"); 26 | assert.dom(".fa-sort-up").exists({ count: 1 }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/integration/components/sy-checkmark/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | sy checkmark", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("works unchecked", async function (assert) { 10 | await render(hbs``); 11 | assert.dom(".fa-square").exists({ count: 1 }); 12 | }); 13 | 14 | test("works checked", async function (assert) { 15 | await render(hbs``); 16 | assert.dom(".fa-square-check").exists({ count: 1 }); 17 | }); 18 | 19 | test("works highlight", async function (assert) { 20 | await render(hbs``); 21 | assert.dom(".fa-square-check").exists({ count: 1 }); 22 | assert.dom(".highlight").exists({ count: 1 }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/integration/components/sy-durationpicker-day/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | sy durationpicker day", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs`{{sy-durationpicker-day}}`); 11 | assert.ok(this.element); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/integration/components/sy-modal-target/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | sy modal target", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs``); 11 | assert.dom("#sy-modals").exists({ count: 1 }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/integration/components/sy-modal/body/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | sy modal/body", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs`Test`); 11 | 12 | assert.dom(this.element).hasText("Test"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/integration/components/sy-modal/footer/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | SyModal::Footer", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs`Test`); 11 | 12 | assert.dom(this.element).hasText("Test"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/integration/components/sy-modal/header/component-test.js: -------------------------------------------------------------------------------- 1 | import { click, render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | SyModal::Header", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | this.set("visible", true); 11 | 12 | await render(hbs` 13 | 16 | Test 17 | 18 | `); 19 | 20 | assert.dom(this.element).hasText("Test ×"); 21 | }); 22 | 23 | test("closes on click of the close icon", async function (assert) { 24 | this.set("visible", true); 25 | 26 | await render(hbs` 27 | 30 | Test 31 | 32 | `); 33 | 34 | assert.ok(this.visible); 35 | 36 | await click("button"); 37 | 38 | assert.notOk(this.visible); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/integration/components/sy-topnav/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupMirage } from "ember-cli-mirage/test-support"; 4 | import { setupRenderingTest } from "ember-qunit"; 5 | import { module, test } from "qunit"; 6 | 7 | module("Integration | Component | sy topnav", function (hooks) { 8 | setupRenderingTest(hooks); 9 | setupMirage(hooks); 10 | 11 | test("renders", async function (assert) { 12 | await render(hbs``); 13 | assert.ok(this.element); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/integration/components/timed-clock/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | timed clock", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs`{{timed-clock}}`); 11 | assert.ok(this.element); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/integration/components/tracking-bar/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | import { setup as setupTrackingService } from "timed/tests/helpers/tracking-mock"; 6 | 7 | module("Integration | Component | tracking bar", function (hooks) { 8 | setupRenderingTest(hooks); 9 | 10 | hooks.beforeEach(function () { 11 | setupTrackingService(this, { 12 | activity: { comment: "asdf" }, 13 | fetchRecentTasks: { last: Promise.resolve() }, 14 | fetchCustomers: { 15 | perform: () => {}, 16 | last: Promise.resolve(), 17 | }, 18 | }); 19 | }); 20 | 21 | test("renders", async function (assert) { 22 | await render(hbs``); 23 | 24 | assert.dom("input[type=text]").hasValue("asdf"); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/integration/components/user-selection/component-test.js: -------------------------------------------------------------------------------- 1 | import { find, render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupMirage } from "ember-cli-mirage/test-support"; 4 | import { setupRenderingTest } from "ember-qunit"; 5 | import { module, test } from "qunit"; 6 | 7 | module("Integration | Component | user selection", function (hooks) { 8 | setupRenderingTest(hooks); 9 | setupMirage(hooks); 10 | 11 | test("renders", async function (assert) { 12 | assert.expect(1); 13 | const user = this.server.create("user"); 14 | this.set("user", user); 15 | 16 | await render(hbs` 17 | 18 | {{u.user}} 19 | 20 | `); 21 | 22 | assert.strictEqual( 23 | find(".user-select .ember-power-select-selected-item").textContent.trim(), 24 | user.longName 25 | ); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/integration/components/weekly-overview-benchmark/component-test.js: -------------------------------------------------------------------------------- 1 | import { find, render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | weekly overview benchmark", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs`{{weekly-overview-benchmark hours=20}}`); 11 | 12 | assert.ok(this.element); 13 | }); 14 | 15 | test("computes the position correctly", async function (assert) { 16 | await render(hbs`{{weekly-overview-benchmark hours=10 max=10}}`); 17 | 18 | assert.strictEqual(find("hr").getAttribute("style"), "bottom: calc(100%);"); 19 | }); 20 | 21 | test("shows labels only when permitted", async function (assert) { 22 | await render(hbs`{{weekly-overview-benchmark showLabel=true hours=8.5}}`); 23 | 24 | assert.strictEqual(find("span").textContent, "8.5h"); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/integration/components/welcome-modal/component-test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | welcome modal", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("renders", async function (assert) { 10 | await render(hbs` 11 | 12 | 13 | `); 14 | assert.ok(this.element); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import { setApplication } from "@ember/test-helpers"; 2 | import { start } from "ember-qunit"; 3 | import setupSinon from "ember-sinon-qunit"; 4 | import * as QUnit from "qunit"; 5 | import { setup } from "qunit-dom"; 6 | 7 | import Application from "../app"; 8 | import config from "../config/environment"; 9 | 10 | setApplication(Application.create(config.APP)); 11 | 12 | setup(QUnit.assert); 13 | 14 | setupSinon(); 15 | 16 | start(); 17 | -------------------------------------------------------------------------------- /tests/unit/analysis/edit/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | analysis/edit", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup("controller:analysis/index"); 9 | assert.ok(controller); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/analysis/edit/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | analysis/edit", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:analysis/edit"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/analysis/index/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | analysis/index", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup("controller:analysis/index"); 9 | assert.ok(controller); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/analysis/index/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | analysis/index", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:analysis/index"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/analysis/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | analysis", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:analysis"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/controllers/qpcontroller/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | controllers/qpcontroller", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("get all query params", function (assert) { 8 | const controller = this.owner.lookup("controller:qpcontroller"); 9 | controller.queryParams = ["qp1", "qp2"]; 10 | controller.qp1 = "baz"; 11 | controller.qp2 = "foo"; 12 | 13 | assert.strictEqual(controller.allQueryParams.qp1, "baz"); 14 | assert.strictEqual(controller.allQueryParams.qp2, "foo"); 15 | }); 16 | 17 | test("reset query params", function (assert) { 18 | const controller = this.owner.lookup("controller:qpcontroller"); 19 | controller.queryParams = ["qp1", "qp2"]; 20 | controller.qp1 = "baz"; 21 | controller.qp2 = "foo"; 22 | 23 | controller.resetQueryParams(); 24 | 25 | assert.strictEqual(controller.allQueryParams.qp1, undefined); 26 | assert.strictEqual(controller.allQueryParams.qp2, undefined); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/unit/helpers/format-duration-test.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import { module, test } from "qunit"; 3 | import { formatDurationFn } from "timed/helpers/format-duration"; 4 | 5 | module("Unit | Helper | format duration", function () { 6 | test("works", function (assert) { 7 | const duration = moment.duration({ 8 | hours: 3, 9 | minutes: 56, 10 | seconds: 59, 11 | }); 12 | 13 | const result = formatDurationFn([duration]); 14 | 15 | assert.strictEqual(result, "03:56:59"); 16 | }); 17 | 18 | test("works without seconds", function (assert) { 19 | const duration = moment.duration({ 20 | hours: 3, 21 | minutes: 56, 22 | seconds: 59, 23 | }); 24 | 25 | const result = formatDurationFn([duration, false]); 26 | 27 | assert.strictEqual(result, "03:56"); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/unit/helpers/humanize-duration-test.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import { module, test } from "qunit"; 3 | import { humanizeDurationFn } from "timed/helpers/humanize-duration"; 4 | 5 | module("Unit | Helper | humanize duration", function () { 6 | test("works", function (assert) { 7 | const duration = moment.duration({ 8 | hours: 3, 9 | minutes: 56, 10 | seconds: 59, 11 | }); 12 | 13 | const result = humanizeDurationFn([duration]); 14 | 15 | assert.strictEqual(result, "3h 56m"); 16 | }); 17 | 18 | test("works with seconds", function (assert) { 19 | const duration = moment.duration({ 20 | hours: 3, 21 | minutes: 56, 22 | seconds: 59, 23 | }); 24 | 25 | const result = humanizeDurationFn([duration, true]); 26 | 27 | assert.strictEqual(result, "3h 56m 59s"); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/unit/helpers/parse-django-duration-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { parseDjangoDurationFn } from "timed/helpers/parse-django-duration"; 3 | 4 | module("Unit | Helper | parse django duration", function () { 5 | test("works", function (assert) { 6 | const result = parseDjangoDurationFn(["11:30:00"]); 7 | 8 | assert.strictEqual(result.asHours(), 11.5); 9 | }); 10 | 11 | test("works with a negative duration", function (assert) { 12 | const result = parseDjangoDurationFn(["-1 11:30:00"]); 13 | 14 | assert.strictEqual(result.asHours(), -12.5); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/unit/index/activities/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | index/activities", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup("controller:index/activities"); 9 | assert.ok(controller); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/index/activities/edit/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | index/activities/edit", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup("controller:index/activities/edit"); 9 | assert.ok(controller); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/index/activities/edit/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | index/activities/edit", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:index/activities/edit"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/index/activities/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | index/activities", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:index/activities"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/index/attendances/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | index/attendances", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup("controller:index/attendances"); 9 | assert.ok(controller); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/index/attendances/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | index/attendances", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:index/attendances"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/index/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | index", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup("controller:index"); 9 | assert.ok(controller); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/index/reports/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | index/reports", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup("controller:index/reports"); 9 | assert.ok(controller); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/index/reports/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | index/reports", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:index/reports"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/index/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | index", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:index"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/login/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | login", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:login"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/models/absence-balance-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | absence balance", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner 9 | .lookup("service:store") 10 | .modelFor("absence-balance"); 11 | 12 | assert.ok(model); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/unit/models/activity-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | activity", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner.lookup("service:store").createRecord("activity"); 9 | 10 | assert.ok(model); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/attendance-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import moment from "moment"; 3 | import { module, test } from "qunit"; 4 | 5 | module("Unit | Model | attendance", function (hooks) { 6 | setupTest(hooks); 7 | 8 | test("exists", function (assert) { 9 | const model = this.owner.lookup("service:store").modelFor("attendance"); 10 | 11 | assert.ok(model); 12 | }); 13 | 14 | test("calculates the duration", function (assert) { 15 | const model = this.owner 16 | .lookup("service:store") 17 | .createRecord("attendance", { 18 | from: moment({ h: 8, m: 0, s: 0 }), 19 | to: moment({ h: 17, m: 0, s: 0 }), 20 | }); 21 | 22 | assert.strictEqual(model.get("duration").asHours(), 9); 23 | }); 24 | 25 | test("calculates the duration when the end time is 00:00", function (assert) { 26 | const model = this.owner 27 | .lookup("service:store") 28 | .createRecord("attendance", { 29 | from: moment({ h: 0, m: 0, s: 0 }), 30 | to: moment({ h: 0, m: 0, s: 0 }), 31 | }); 32 | 33 | assert.strictEqual(model.get("duration").asHours(), 24); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/unit/models/billing-type-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | billing type", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner.lookup("service:store").modelFor("billing-type"); 9 | 10 | assert.ok(model); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/cost-center-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | cost center", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner.lookup("service:store").modelFor("cost-center"); 9 | 10 | assert.ok(model); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/customer-statistic-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | customer statistic", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner 9 | .lookup("service:store") 10 | .modelFor("customer-statistic"); 11 | 12 | assert.ok(model); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/unit/models/customer-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | customer", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner.lookup("service:store").modelFor("customer"); 9 | 10 | assert.ok(model); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/employment-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | employment", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner.lookup("service:store").modelFor("employment"); 9 | 10 | assert.ok(model); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/location-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | location", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner.lookup("service:store").modelFor("location"); 9 | 10 | assert.ok(model); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/month-statistic-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | month statistic", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner 9 | .lookup("service:store") 10 | .modelFor("month-statistic"); 11 | 12 | assert.ok(model); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/unit/models/overtime-credit-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | overtime credit", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner 9 | .lookup("service:store") 10 | .modelFor("overtime-credit"); 11 | 12 | assert.ok(model); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/unit/models/project-statistic-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | project statistic", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner 9 | .lookup("service:store") 10 | .modelFor("project-statistic"); 11 | 12 | assert.ok(model); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/unit/models/project-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | project", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner.lookup("service:store").modelFor("project"); 9 | 10 | assert.ok(model); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/public-holiday-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | public holiday", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner.lookup("service:store").modelFor("public-holiday"); 9 | 10 | assert.ok(model); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/report-intersection-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | report intersection", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner 9 | .lookup("service:store") 10 | .modelFor("report-intersection"); 11 | 12 | assert.ok(model); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/unit/models/report-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | report", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner.lookup("service:store").modelFor("report"); 9 | 10 | assert.ok(model); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/task-statistic-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | task statistic", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner.lookup("service:store").modelFor("task-statistic"); 9 | 10 | assert.ok(model); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/task-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | task", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner.lookup("service:store").modelFor("task"); 9 | 10 | assert.ok(model); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/user-statistic-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | user statistic", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner.lookup("service:store").modelFor("user-statistic"); 9 | 10 | assert.ok(model); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/worktime-balance-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | worktime balance", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner 9 | .lookup("service:store") 10 | .modelFor("worktime-balance"); 11 | 12 | assert.ok(model); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/unit/models/year-statistic-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Model | year statistic", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const model = this.owner.lookup("service:store").modelFor("year-statistic"); 9 | 10 | assert.ok(model); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/no-access/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | no-access", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("it exists", function (assert) { 8 | const route = this.owner.lookup("route:no-access"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/notfound/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | notfound", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:notfound"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/projects/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | projects", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup("controller:projects"); 9 | assert.ok(controller); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/projects/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | projects", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:projects"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/protected/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | protected", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup("controller:protected"); 9 | assert.ok(controller); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/protected/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | protected", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:protected"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/serializers/attendance-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Serializer | attendance", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("serializes records", function (assert) { 8 | const record = this.owner 9 | .lookup("service:store") 10 | .createRecord("attendance"); 11 | 12 | const serializedRecord = record.serialize(); 13 | 14 | assert.ok(serializedRecord); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/unit/serializers/employment-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Serializer | employment", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("serializes records", function (assert) { 8 | const record = this.owner 9 | .lookup("service:store") 10 | .createRecord("employment"); 11 | 12 | const serializedRecord = record.serialize(); 13 | 14 | assert.ok(serializedRecord); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/unit/services/fetch-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | import setupSession from "timed/tests/helpers/session-mock"; 4 | 5 | module("Unit | Service | fetch", function (hooks) { 6 | setupTest(hooks); 7 | setupSession(hooks); 8 | 9 | test("exists", function (assert) { 10 | const service = this.owner.lookup("service:fetch"); 11 | assert.ok(service); 12 | }); 13 | 14 | test("adds the auth token to the headers", function (assert) { 15 | const service = this.owner.lookup("service:fetch"); 16 | const session = this.owner.lookup("service:session"); 17 | 18 | assert.strictEqual( 19 | service.get("headers.authorization"), 20 | session.headers.authorization 21 | ); 22 | }); 23 | 24 | test("does not add the auth token to the headers if no token is given", function (assert) { 25 | const service = this.owner.lookup("service:fetch"); 26 | const session = this.owner.lookup("service:session"); 27 | 28 | delete session.headers.authorization; 29 | 30 | assert.notOk(service.get("headers.authorization")); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/unit/services/metadata-fetcher-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Service | metadata fetcher", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const service = this.owner.lookup("service:metadata-fetcher"); 9 | assert.ok(service); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/services/rejected-reports-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Service | rejectedReports", function (hooks) { 5 | setupTest(hooks); 6 | 7 | // TODO: Replace this with your real tests. 8 | test("it exists", function (assert) { 9 | const service = this.owner.lookup("service:rejected-reports"); 10 | assert.ok(service); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/services/tracking-test.js: -------------------------------------------------------------------------------- 1 | import { settled } from "@ember/test-helpers"; 2 | import { setupMirage } from "ember-cli-mirage/test-support"; 3 | import { setupTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Unit | Service | tracking", function (hooks) { 7 | setupTest(hooks); 8 | setupMirage(hooks); 9 | 10 | test("exists", async function (assert) { 11 | const service = this.owner.lookup("service:tracking"); 12 | await settled(); 13 | assert.ok(service); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/unit/services/unverified-reports-test.js: -------------------------------------------------------------------------------- 1 | import { settled } from "@ember/test-helpers"; 2 | import { setupMirage } from "ember-cli-mirage/test-support"; 3 | import { setupTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Unit | Service | unverified reports", function (hooks) { 7 | setupTest(hooks); 8 | setupMirage(hooks); 9 | 10 | test("exists", async function (assert) { 11 | const service = this.owner.lookup("service:unverified-reports"); 12 | await settled(); 13 | assert.ok(service); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/unit/sso-login/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | sso-login", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("it exists", function (assert) { 8 | const route = this.owner.lookup("route:sso-login"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/statistics/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | statistics", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup("controller:statistics"); 9 | assert.ok(controller); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/statistics/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | statistics", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:statistics"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/transforms/django-date-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import moment from "moment"; 3 | import { module, test } from "qunit"; 4 | 5 | module("Unit | Transform | django date", function (hooks) { 6 | setupTest(hooks); 7 | 8 | test("serializes", function (assert) { 9 | const transform = this.owner.lookup("transform:django-date"); 10 | 11 | const result = transform.serialize( 12 | moment({ 13 | y: 2017, 14 | M: 2, // moments months are zerobased 15 | d: 11, 16 | }) 17 | ); 18 | 19 | assert.strictEqual(result, "2017-03-11"); 20 | }); 21 | 22 | test("deserializes", function (assert) { 23 | const transform = this.owner.lookup("transform:django-date"); 24 | 25 | assert.notOk(transform.deserialize("")); 26 | assert.notOk(transform.deserialize(null)); 27 | 28 | const result = transform.deserialize("2017-03-11"); 29 | 30 | assert.strictEqual(result.year(), 2017); 31 | assert.strictEqual(result.month(), 2); // moments months are zerobased 32 | assert.strictEqual(result.date(), 11); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/unit/transforms/django-workdays-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Transform | django workdays", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("serializes", function (assert) { 8 | const transform = this.owner.lookup("transform:django-workdays"); 9 | 10 | const result = transform.serialize([1, 2, 3, 4, 5]); 11 | 12 | assert.deepEqual(result, ["1", "2", "3", "4", "5"]); 13 | }); 14 | 15 | test("deserializes", function (assert) { 16 | const transform = this.owner.lookup("transform:django-workdays"); 17 | 18 | const result = transform.deserialize(["1", "2", "3", "4", "5"]); 19 | 20 | assert.deepEqual(result, [1, 2, 3, 4, 5]); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/unit/users/edit/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | users/edit", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup("controller:users/edit"); 9 | assert.ok(controller); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/users/edit/credits/absence-credits/edit/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module( 5 | "Unit | Controller | users/edit/credits/absence credits/edit", 6 | function (hooks) { 7 | setupTest(hooks); 8 | 9 | test("exists", function (assert) { 10 | const controller = this.owner.lookup( 11 | "controller:users/edit/credits/absence-credits/edit" 12 | ); 13 | assert.ok(controller); 14 | }); 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /tests/unit/users/edit/credits/absence-credits/edit/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module( 5 | "Unit | Route | users/edit/credits/absence credits/edit", 6 | function (hooks) { 7 | setupTest(hooks); 8 | 9 | test("exists", function (assert) { 10 | const route = this.owner.lookup( 11 | "route:users/edit/credits/absence-credits/edit" 12 | ); 13 | assert.ok(route); 14 | }); 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /tests/unit/users/edit/credits/absence-credits/new/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module( 5 | "Unit | Route | users/edit/credits/absence credits/new", 6 | function (hooks) { 7 | setupTest(hooks); 8 | 9 | test("exists", function (assert) { 10 | const route = this.owner.lookup( 11 | "route:users/edit/credits/absence-credits/new" 12 | ); 13 | assert.ok(route); 14 | }); 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /tests/unit/users/edit/credits/index/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | users/edit/credits/index", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup("controller:users/edit/credits/index"); 9 | assert.ok(controller); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/users/edit/credits/index/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | users/edit/credits/index", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:users/edit/credits/index"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/users/edit/credits/overtime-credits/edit/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module( 5 | "Unit | Controller | users/edit/credits/overtime credits/edit", 6 | function (hooks) { 7 | setupTest(hooks); 8 | 9 | test("exists", function (assert) { 10 | const controller = this.owner.lookup( 11 | "controller:users/edit/credits/overtime-credits/edit" 12 | ); 13 | assert.ok(controller); 14 | }); 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /tests/unit/users/edit/credits/overtime-credits/edit/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module( 5 | "Unit | Route | users/edit/credits/overtime credits/edit", 6 | function (hooks) { 7 | setupTest(hooks); 8 | 9 | test("exists", function (assert) { 10 | const route = this.owner.lookup( 11 | "route:users/edit/credits/overtime-credits/edit" 12 | ); 13 | assert.ok(route); 14 | }); 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /tests/unit/users/edit/credits/overtime-credits/new/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module( 5 | "Unit | Route | users/edit/credits/overtime credits/new", 6 | function (hooks) { 7 | setupTest(hooks); 8 | 9 | test("exists", function (assert) { 10 | const route = this.owner.lookup( 11 | "route:users/edit/credits/overtime-credits/edit" 12 | ); 13 | assert.ok(route); 14 | }); 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /tests/unit/users/edit/credits/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | users/edit/credits", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:users/edit/credits"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/users/edit/index/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | users/edit/index", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup("controller:users/edit/index"); 9 | assert.ok(controller); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/users/edit/index/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | users/edit/index", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:users/edit/index"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/users/edit/responsibilities/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | users/edit/responsibilities", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup( 9 | "controller:users/edit/responsibilities" 10 | ); 11 | assert.ok(controller); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/unit/users/edit/responsibilities/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | users/edit/responsibilities", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:users/edit/responsibilities"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/users/edit/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | users/edit", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:users/edit"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/users/index/controller-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Controller | users/index", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const controller = this.owner.lookup("controller:users/index"); 9 | assert.ok(controller); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/users/index/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | users/index", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:users/index"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/users/route-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Route | users", function (hooks) { 5 | setupTest(hooks); 6 | 7 | test("exists", function (assert) { 8 | const route = this.owner.lookup("route:users"); 9 | assert.ok(route); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/utils/query-params-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { 3 | serializeQueryParams, 4 | underscoreQueryParams, 5 | filterQueryParams, 6 | } from "timed/utils/query-params"; 7 | 8 | module("Unit | Utility | query params", function () { 9 | test("can serialize query params", function (assert) { 10 | const params = { foo: 10 }; 11 | const qp = { 12 | foo: { 13 | serialize: (val) => val * 10, 14 | }, 15 | }; 16 | 17 | const result = serializeQueryParams(params, qp); 18 | 19 | assert.strictEqual(result.foo, 100); 20 | }); 21 | 22 | test("can underline query params", function (assert) { 23 | const params = { fooBar: 10, "baz-x": 10 }; 24 | 25 | const result = underscoreQueryParams(params); 26 | 27 | assert.deepEqual(Object.keys(result), ["foo_bar", "baz_x"]); 28 | }); 29 | 30 | test("can filter params", function (assert) { 31 | const params = { foo: 10, bar: 10, baz: 10 }; 32 | const result = filterQueryParams(params, "foo", "bar"); 33 | 34 | assert.deepEqual(result, { baz: 10 }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/unit/utils/url-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { cleanParams, toQueryString } from "timed/utils/url"; 3 | 4 | module("Unit | Utility | url", function () { 5 | test("can clean params", function (assert) { 6 | const params = { 7 | 1: "", 8 | 2: null, 9 | 3: undefined, 10 | 4: 0, 11 | 5: "test", 12 | }; 13 | 14 | const result = cleanParams(params); 15 | 16 | assert.deepEqual(result, { 17 | 1: "", 18 | 4: 0, 19 | 5: "test", 20 | }); 21 | }); 22 | 23 | test("can convert params to a query string", function (assert) { 24 | const params = { 25 | foo: "", 26 | bar: 0, 27 | baz: "test", 28 | }; 29 | 30 | const result = toQueryString(params); 31 | 32 | assert.strictEqual(result, "foo=&bar=0&baz=test"); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/unit/validators/null-or-not-blank-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import validateNullOrNotBlank from "timed/validators/null-or-not-blank"; 3 | 4 | module("Unit | Validator | null or not blank", function () { 5 | test("works", function (assert) { 6 | assert.true(validateNullOrNotBlank()("key", "test")); 7 | assert.true(validateNullOrNotBlank()("key", null)); 8 | assert.strictEqual(typeof validateNullOrNotBlank()("key", ""), "string"); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/timed-frontend/a91a9595042c81bd618d23bcf4303bd2ea80195d/vendor/.gitkeep --------------------------------------------------------------------------------