diff --git a/package-lock.json b/package-lock.json index c30263893b..33c9e6ab9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@ctrl/ngx-emoji-mart": "^9.2.0", "@ngneat/hotkeys": "^4.0.0", "@ngstack/code-editor": "7.3.0", + "@swimlane/ngx-charts": "^20.5.0", "@uirouter/angular": "^13.0", "@uirouter/angular-hybrid": "^17.1.0", "@uirouter/angularjs": "^1.0.30", @@ -5471,6 +5472,39 @@ "dev": true, "license": "MIT" }, + "node_modules/@swimlane/ngx-charts": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@swimlane/ngx-charts/-/ngx-charts-20.5.0.tgz", + "integrity": "sha512-PNBIHdu/R3ceD7jnw1uCBVOj4k3T6IxfdW6xsDsglGkZyoWMEEq4tLoEurjLEKzmDtRv9c35kVNOXy0lkOuXeA==", + "license": "MIT", + "dependencies": { + "d3-array": "^3.1.1", + "d3-brush": "^3.0.0", + "d3-color": "^3.1.0", + "d3-ease": "^3.0.1", + "d3-format": "^3.1.0", + "d3-hierarchy": "^3.1.0", + "d3-interpolate": "^3.0.1", + "d3-sankey": "^0.12.3", + "d3-scale": "^4.0.2", + "d3-selection": "^3.0.0", + "d3-shape": "^3.2.0", + "d3-time-format": "^3.0.0", + "d3-transition": "^3.0.1", + "rfdc": "^1.3.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/animations": ">=12.0.0", + "@angular/cdk": ">=12.0.0", + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0", + "@angular/forms": ">=12.0.0", + "@angular/platform-browser": ">=12.0.0", + "@angular/platform-browser-dynamic": ">=12.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "dev": true, @@ -8788,6 +8822,263 @@ "version": "3.5.17", "license": "BSD-3-Clause" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1 - 2" + } + }, + "node_modules/d3-time-format/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-time-format/node_modules/d3-time": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", + "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "2" + } + }, + "node_modules/d3-time-format/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, "node_modules/dargs": { "version": "7.0.0", "dev": true, @@ -13207,6 +13498,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/interpret": { "version": "1.1.0", "dev": true, @@ -19644,7 +19944,6 @@ }, "node_modules/rfdc": { "version": "1.3.1", - "dev": true, "license": "MIT" }, "node_modules/right-align": { diff --git a/package.json b/package.json index fb458e4b24..f7298995fb 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@ctrl/ngx-emoji-mart": "^9.2.0", "@ngneat/hotkeys": "^4.0.0", "@ngstack/code-editor": "7.3.0", + "@swimlane/ngx-charts": "^20.5.0", "@uirouter/angular": "^13.0", "@uirouter/angular-hybrid": "^17.1.0", "@uirouter/angularjs": "^1.0.30", diff --git a/src/app/api/services/marking-session.service.ts b/src/app/api/services/marking-session.service.ts index f1ce830a02..fb2d79abd3 100644 --- a/src/app/api/services/marking-session.service.ts +++ b/src/app/api/services/marking-session.service.ts @@ -33,7 +33,7 @@ export class MarkingSessionService extends CachedEntityService { ); } - public createInstanceFrom(json: object, other?: Unit): MarkingSession { + public createInstanceFrom(_json: object, other?: Unit): MarkingSession { return new MarkingSession(other); } } diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 318c601b35..4eac28aa79 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -109,7 +109,10 @@ import { } from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; -import {DateFnsAdapter, MAT_DATE_FNS_FORMATS} from '@angular/material-date-fns-adapter'; +import { + DateFnsAdapter, + //MAT_DATE_FNS_FORMATS (unused vars) +} from '@angular/material-date-fns-adapter'; import {enAU} from 'date-fns/locale'; import {CalendarModule, DateAdapter as CalendarDateAdapter} from 'angular-calendar'; import {adapterFactory} from 'angular-calendar/date-adapters/date-fns'; @@ -296,7 +299,9 @@ import {MarkingSessionService} from './api/services/marking-session.service'; import {PortfolioIncludedTasksComponent} from './projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component'; import {OverseerScriptEditorModalComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/overseer-script-editor-modal/overseer-script-editor-modal.component'; import {CodeEditorModule} from '@ngstack/code-editor'; -import {UploadGradesComponent} from './units/states/portfolios/upload-grades/upload-grades.component'; +//import {UploadGradesComponent} from './units/states/portfolios/upload-grades/upload-grades.component'; (unused vars) +import {CommonModule} from '@angular/common'; +import {NgxChartsModule} from '@swimlane/ngx-charts'; // See https://stackoverflow.com/questions/55721254/how-to-change-mat-datepicker-date-format-to-dd-mm-yyyy-in-simplest-way/58189036#58189036 const MY_DATE_FORMAT = { @@ -604,6 +609,8 @@ const MY_DATE_FORMAT = { MatDialogModuleNew, CalendarModule.forRoot({provide: CalendarDateAdapter, useFactory: adapterFactory}), CodeEditorModule.forRoot(), + CommonModule, + NgxChartsModule, ], }) diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html index dbd2f4302a..d8deb54c21 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html @@ -31,11 +31,9 @@

Tutor Times Session Summary

- - - + + +
@@ -46,7 +44,7 @@

Tutor Times Session Summary

[(ngModel)]="tutorTimeSummaryStartDate" (dateChange)="onDateChange($event)" /> - Sessions from this day onward + Sessions from
@@ -59,7 +57,7 @@

Tutor Times Session Summary

[(ngModel)]="tutorTimeSummaryEndDate" (dateChange)="onDateChange($event)" /> - Sessions up to this day + Sessions up until @@ -72,6 +70,36 @@

Tutor Times Session Summary

>Hide sessions during tutorials
+ +
+
+

+ {{ totalMinutes / 60 | number: '1.2-2' }} hrs +

+
Total Time
+
+ +
+

+ {{ totalAssessments }} +

+
Assessements made
+
+ +
+

+ {{ avgMinPerAssessments | number: '1.0-1' }} +

+
Min per Assessment
+
+ +
+

+ {{ totalSubmissionsOpened | number: '1.0-1' }} +

+
Submissions Viewed
+
+
@@ -111,4 +139,32 @@

Tutor Times Session Summary

(beforeViewRender)="beforeViewRender($event)" /> + +
+

Marking Trend

+
+ + + +
+ No marking sessions recorded for this week. +
+
+
diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts index 9b65234bbd..94a403b05f 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts @@ -48,6 +48,13 @@ export class AnalyticsTutorTimesComponent implements OnInit { events: SessionEvent[] = []; filteredEvents = []; + // hold data for new cards and chart + totalMinutes: number = 0; + totalAssessments: number = 0; + avgMinPerAssessments: number = 0; + totalSubmissionsOpened: number = 0; + weekChartData: {name: string; series: {name: string; value: number}[]}[] = []; + tutorTimeSummaryStartDate: Date; tutorTimeSummaryEndDate: Date; daysInWeek: number = 7; @@ -126,10 +133,65 @@ export class AnalyticsTutorTimesComponent implements OnInit { (e) => (this.selectedUserId === null || e.userId === this.selectedUserId) && (!this.hideSessionsDuringTutorials || !e.duringTutorial) && - e.duration >= 1, + e.duration >= 0, ); + + // recalcuate stats when filter sekection changes + this.calculateAnalytics(); + } + + calculateAnalytics() { + this.totalMinutes = 0; + this.totalAssessments = 0; + this.totalSubmissionsOpened = 0; + + const groupedMap = new Map(); + const iterDate = new Date(this.tutorTimeSummaryStartDate); + const endDate = new Date(this.tutorTimeSummaryEndDate); + + // Normalize to midnight + iterDate.setHours(0, 0, 0, 0); + endDate.setHours(0, 0, 0, 0); + + // Loop through the date range to create "buckets" + while (iterDate <= endDate) { + const label = iterDate.toLocaleDateString('en-AU', {weekday: 'short', day: 'numeric'}); + groupedMap.set(label, {time: 0, count: 0}); + iterDate.setDate(iterDate.getDate() + 1); + } + + this.filteredEvents.forEach((event: SessionEvent) => { + const duration = event.duration; + + this.totalMinutes += duration; + this.totalAssessments += event.assessments || 0; + this.totalSubmissionsOpened += event.submissionsOpened || 0; + + const label = event.start.toLocaleDateString('en-AU', {weekday: 'short', day: 'numeric'}); + + if (groupedMap.has(label)) { + const currentVal = groupedMap.get(label)!; + currentVal.time += duration; + currentVal.count += event.assessments || 0; + } + }); + + this.avgMinPerAssessments = + this.totalAssessments > 0 ? this.totalMinutes / this.totalAssessments : 0; + + this.weekChartData = Array.from(groupedMap, ([name, data]) => ({ + name, + series: [ + {name: 'Time (hrs)', value: data.time / 60}, + {name: 'Assessments', value: data.count}, + ], + })); } + public formatDataLabel = (value: number): string => { + return value === 0 ? '' : value.toString(); + }; + onDateChange(_event: MatDatepickerInputEvent) { if (!this.tutorTimeSummaryStartDate || !this.tutorTimeSummaryEndDate) { return; diff --git a/src/app/units/states/analytics/unit-analytics-route.component.ts b/src/app/units/states/analytics/unit-analytics-route.component.ts index 4c8e068722..2e3eb47e3f 100644 --- a/src/app/units/states/analytics/unit-analytics-route.component.ts +++ b/src/app/units/states/analytics/unit-analytics-route.component.ts @@ -1,6 +1,4 @@ -import {Component, Input, OnInit} from '@angular/core'; -import {MatDatepickerInputEvent} from '@angular/material/datepicker'; -import {CalendarEvent} from 'angular-calendar'; +import {Component, Input} from '@angular/core'; import {Observable} from 'rxjs'; import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; import {Unit} from 'src/app/api/models/unit';