diff --git a/packages/desktop-ui-lib/src/lib/recap/features/recap/recap.component.html b/packages/desktop-ui-lib/src/lib/recap/features/recap/recap.component.html index 9ab9ad1da4e..c8381f4f55b 100644 --- a/packages/desktop-ui-lib/src/lib/recap/features/recap/recap.component.html +++ b/packages/desktop-ui-lib/src/lib/recap/features/recap/recap.component.html @@ -5,8 +5,8 @@
- +
diff --git a/packages/desktop-ui-lib/src/lib/recap/features/recap/recap.component.ts b/packages/desktop-ui-lib/src/lib/recap/features/recap/recap.component.ts index 274a2977f5e..4a7e9baf1cd 100644 --- a/packages/desktop-ui-lib/src/lib/recap/features/recap/recap.component.ts +++ b/packages/desktop-ui-lib/src/lib/recap/features/recap/recap.component.ts @@ -2,6 +2,8 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { UntilDestroy } from '@ngneat/until-destroy'; import { Observable } from 'rxjs'; import { RecapQuery } from '../../+state/recap.query'; +import { RecapStore } from '../../+state/recap.store'; +import { IDateRangePicker } from '../../shared/features/date-range-picker/date-picker.interface'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -13,12 +15,16 @@ import { RecapQuery } from '../../+state/recap.query'; export class RecapComponent { private readonly basePath = ['/', 'time-tracker', 'daily']; - constructor(private readonly recapQuery: RecapQuery) {} + constructor(private readonly recapQuery: RecapQuery, private readonly recapStore: RecapStore) {} public get isLoading$(): Observable { return this.recapQuery.isLoading$; } + public onRangeChange(range: IDateRangePicker) { + this.recapStore.update({ range }); + } + public get segments() { return [ { diff --git a/packages/desktop-ui-lib/src/lib/recap/features/time-tracking-charts/time-tracking-charts.component.html b/packages/desktop-ui-lib/src/lib/recap/features/time-tracking-charts/time-tracking-charts.component.html index 21338713c02..d9e8247aaf5 100644 --- a/packages/desktop-ui-lib/src/lib/recap/features/time-tracking-charts/time-tracking-charts.component.html +++ b/packages/desktop-ui-lib/src/lib/recap/features/time-tracking-charts/time-tracking-charts.component.html @@ -3,7 +3,7 @@ m[m]': {trim: 'both'} }} · - {{dailyActivities$ | async}}% + {{dailyActivities$ | async | percent:'1.0-2' }} state.count.weekDuration)); } - public get dailyActivities$(): Observable { - return this.recapQuery.state$.pipe(map((state) => (state.count.weekActivities || 0).toFixed(2))); + public get dailyActivities$(): Observable { + return this.recapQuery.state$.pipe(map((state) => state.count.weekActivities / 100)); } public get chartData$(): Observable { diff --git a/packages/desktop-ui-lib/src/lib/recap/recap.module.ts b/packages/desktop-ui-lib/src/lib/recap/recap.module.ts index 07532ca784a..55baa500efc 100644 --- a/packages/desktop-ui-lib/src/lib/recap/recap.module.ts +++ b/packages/desktop-ui-lib/src/lib/recap/recap.module.ts @@ -43,6 +43,10 @@ import { AutoRefreshComponent } from './shared/ui/auto-refresh/auto-refresh.comp import { ProgressStatusModule } from './shared/ui/progress-status/progress-status.module'; import { ProjectColumnViewModule } from './shared/ui/project-column-view/project-column-view.module'; import { StatisticComponent } from './shared/ui/statistic/statistic.component'; +import { WeeklyCalendarComponent } from './weekly/features/weekly-calendar/weekly-calendar.component'; +import { WeeklyProgressComponent } from './weekly/features/weekly-progress/weekly-progress.component'; +import { WeeklyRecapComponent } from './weekly/features/weekly-recap/weekly-recap.component'; +import { WeeklyStatisticComponent } from './weekly/features/weekly-statistic/weekly-statistic.component'; @NgModule({ declarations: [ @@ -55,7 +59,11 @@ import { StatisticComponent } from './shared/ui/statistic/statistic.component'; StatisticComponent, AutoRefreshComponent, ActivityReportComponent, - SegmentedControlComponent + SegmentedControlComponent, + WeeklyRecapComponent, + WeeklyCalendarComponent, + WeeklyProgressComponent, + WeeklyStatisticComponent ], imports: [ CommonModule, @@ -95,6 +103,6 @@ import { StatisticComponent } from './shared/ui/statistic/statistic.component'; RequestQuery, RequestStore ], - exports: [RecapComponent] + exports: [RecapComponent, WeeklyRecapComponent] }) export class RecapModule {} diff --git a/packages/desktop-ui-lib/src/lib/recap/shared/features/activity-report/activity-report.component.scss b/packages/desktop-ui-lib/src/lib/recap/shared/features/activity-report/activity-report.component.scss index 22294101eab..b8dfd836e41 100644 --- a/packages/desktop-ui-lib/src/lib/recap/shared/features/activity-report/activity-report.component.scss +++ b/packages/desktop-ui-lib/src/lib/recap/shared/features/activity-report/activity-report.component.scss @@ -92,7 +92,7 @@ } .main-report-wrapper { - height: calc(100vh - 8.5rem); + height: calc(100vh - 8rem); margin-bottom: 0 !important; .main-report-body { diff --git a/packages/desktop-ui-lib/src/lib/recap/shared/features/date-range-picker/date-picker.utils.ts b/packages/desktop-ui-lib/src/lib/recap/shared/features/date-range-picker/date-picker.utils.ts index 71e75907b09..d2a0110cb7c 100644 --- a/packages/desktop-ui-lib/src/lib/recap/shared/features/date-range-picker/date-picker.utils.ts +++ b/packages/desktop-ui-lib/src/lib/recap/shared/features/date-range-picker/date-picker.utils.ts @@ -1,7 +1,9 @@ -import { IDateRangePicker, ISelectedDateRange, ITimeLogFilters, WeekDaysEnum } from '@gauzy/contracts'; -import * as moment from 'moment'; -import { TimePeriod } from './date-picker.interface'; +import { ISelectedDateRange, ITimeLogFilters, WeekDaysEnum } from '@gauzy/contracts'; +import * as momentDefault from 'moment'; +import { extendMoment } from 'moment-range'; +import { IDateRangePicker, TimePeriod } from './date-picker.interface'; +export const moment = extendMoment(momentDefault); /** * We are having issue, when organization not allowed future date * When someone run timer for today, all statistic not displaying correctly @@ -66,3 +68,18 @@ export function dayOfWeekAsString(weekDay: WeekDaysEnum): number { WeekDaysEnum.SATURDAY ].indexOf(weekDay); } + +/** + * Updates the week days based on the specified start and end dates. + * If no dates are provided in the request, it defaults to the current week. + */ +export function updateWeekDays(input: IDateRangePicker) { + const { startDate = moment().startOf('week'), endDate = moment().endOf('week') } = input; + + const start = moment(moment(startDate).format('YYYY-MM-DD')); + const end = moment(moment(endDate).format('YYYY-MM-DD')); + const range = Array.from(moment.range(start, end).by('day')); + const weekDays = range.map((date: moment.Moment) => date.format('YYYY-MM-DD')); + + return { range, weekDays }; +} diff --git a/packages/desktop-ui-lib/src/lib/recap/shared/features/date-range-picker/date-range-picker.component.html b/packages/desktop-ui-lib/src/lib/recap/shared/features/date-range-picker/date-range-picker.component.html index d91dbe25433..523e39cf726 100644 --- a/packages/desktop-ui-lib/src/lib/recap/shared/features/date-range-picker/date-range-picker.component.html +++ b/packages/desktop-ui-lib/src/lib/recap/shared/features/date-range-picker/date-range-picker.component.html @@ -12,7 +12,7 @@ (); + + @Input() + public set dates(range: IDateRangePicker) { + this.dates$.next({ + ...range, + isCustomDate: false + }); + } ngOnInit(): void { this.range$ @@ -188,48 +208,64 @@ export class DateRangePickerComponent implements OnInit, OnDestroy { distinctUntilChange(), debounceTime(500), tap((range: IDateRangePicker) => { - this.recapStore.update({ - range: { - startDate: moment(range.startDate).toISOString(), - endDate: moment(range.endDate).toISOString() - } + this.rangeChanges.emit({ + startDate: moment(range.startDate).toISOString(), + endDate: moment(range.endDate).toISOString() }); }), untilDestroyed(this) ) .subscribe(); - this.selectedDateRange = this.getSelectorDates(); - this.createDateRangeMenus(); - this.setPastStrategy(); - this.setFutureStrategy(); + this.store.selectedOrganization$ + .pipe( + filter((organization: IOrganization) => !!organization), + tap((organization: IOrganization) => { + this.futureDateAllowed = organization.futureDateAllowed; + this.selectedDateRange = this.getSelectorDates(); + this.createDateRangeMenus(); + this.setPastStrategy(); + this.setFutureStrategy(); + }), + untilDestroyed(this) + ) + .subscribe(); } /** * Create Date Range Translated Menus */ - createDateRangeMenus() { - this.ranges = { - [DateRangeKeyEnum.TODAY]: [moment(), moment()], - [DateRangeKeyEnum.YESTERDAY]: [moment().subtract(1, 'days'), moment().subtract(1, 'days')] + private createDateRangeMenus() { + const ranges = { + day: { + [DateRangeKeyEnum.TODAY]: [moment().startOf('day'), moment().endOf('day')], + [DateRangeKeyEnum.YESTERDAY]: [ + moment().subtract(1, 'days').startOf('day'), + moment().subtract(1, 'days').endOf('day') + ] + }, + week: { + [DateRangeKeyEnum.CURRENT_WEEK]: [moment().startOf('isoWeek'), moment().endOf('isoWeek')], + [DateRangeKeyEnum.LAST_WEEK]: [ + moment().subtract(1, 'week').startOf('isoWeek'), + moment().subtract(1, 'week').endOf('isoWeek') + ] + }, + month: { + [DateRangeKeyEnum.CURRENT_MONTH]: [moment().startOf('month'), moment().endOf('month')], + [DateRangeKeyEnum.LAST_MONTH]: [ + moment().subtract(1, 'month').startOf('month'), + moment().subtract(1, 'month').endOf('month') + ] + } }; - // Define the units of time to remove based on conditions - const unitsToRemove = []; - - if (this.isLockDatePicker && this.unitOfTime !== 'day') { - unitsToRemove.push(DateRangeKeyEnum.TODAY, DateRangeKeyEnum.YESTERDAY); - } - if (this.isLockDatePicker && this.unitOfTime !== 'week') { - unitsToRemove.push(DateRangeKeyEnum.CURRENT_WEEK, DateRangeKeyEnum.LAST_WEEK); - } - if (this.isLockDatePicker && this.unitOfTime !== 'month') { - unitsToRemove.push(DateRangeKeyEnum.CURRENT_MONTH, DateRangeKeyEnum.LAST_MONTH); - } - - // Remove date ranges based on unitsToRemove - unitsToRemove.forEach((unit) => { - delete this.ranges[unit]; - }); + this.ranges = this.isLockDatePicker + ? ranges[this.unitOfTime] + : ({ + ...ranges.day, + ...ranges.week, + ...ranges.month + } as any as DateRanges); } /** diff --git a/packages/desktop-ui-lib/src/lib/recap/shared/features/date-range-picker/date-range-picker.module.ts b/packages/desktop-ui-lib/src/lib/recap/shared/features/date-range-picker/date-range-picker.module.ts index f39f85de76a..3954373be38 100644 --- a/packages/desktop-ui-lib/src/lib/recap/shared/features/date-range-picker/date-range-picker.module.ts +++ b/packages/desktop-ui-lib/src/lib/recap/shared/features/date-range-picker/date-range-picker.module.ts @@ -8,6 +8,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { NgxDaterangepickerMd as NgxDateRangePickerMd } from 'ngx-daterangepicker-material'; import { RecapQuery } from '../../../+state/recap.query'; import { RecapStore } from '../../../+state/recap.store'; +import { PipeModule } from '../../../../time-tracker/pipes/pipe.module'; import { dayOfWeekAsString } from './date-picker.utils'; import { DateRangePickerComponent } from './date-range-picker.component'; @@ -22,6 +23,7 @@ import { DateRangePickerComponent } from './date-range-picker.component'; NbInputModule, NgSelectModule, TranslateModule, + PipeModule, NgxDateRangePickerMd.forRoot({ firstDay: dayOfWeekAsString(WeekDaysEnum.MONDAY) }) diff --git a/packages/desktop-ui-lib/src/lib/recap/shared/features/segmented-control/segmented-control.component.scss b/packages/desktop-ui-lib/src/lib/recap/shared/features/segmented-control/segmented-control.component.scss index 715e0a9807a..fada4e0627f 100644 --- a/packages/desktop-ui-lib/src/lib/recap/shared/features/segmented-control/segmented-control.component.scss +++ b/packages/desktop-ui-lib/src/lib/recap/shared/features/segmented-control/segmented-control.component.scss @@ -36,7 +36,7 @@ $text-color: var(--gauzy-text-color-2); // Segment component styles .segment { @include flex-center($gap-small); - padding: $padding-small; + padding: 0 $padding-small; span { display: none; diff --git a/packages/desktop-ui-lib/src/lib/recap/shared/ui/progress-status/progress-status.component.html b/packages/desktop-ui-lib/src/lib/recap/shared/ui/progress-status/progress-status.component.html index 5c45b8b68f2..e68e5494e79 100644 --- a/packages/desktop-ui-lib/src/lib/recap/shared/ui/progress-status/progress-status.component.html +++ b/packages/desktop-ui-lib/src/lib/recap/shared/ui/progress-status/progress-status.component.html @@ -1,8 +1,8 @@
-
{{ percentage }}%
+
{{ (percentage / 100) | percent:'1.0-2' }}
- +
diff --git a/packages/desktop-ui-lib/src/lib/recap/shared/ui/progress-status/progress-status.component.scss b/packages/desktop-ui-lib/src/lib/recap/shared/ui/progress-status/progress-status.component.scss index aaa78629d3c..368fed70bbf 100644 --- a/packages/desktop-ui-lib/src/lib/recap/shared/ui/progress-status/progress-status.component.scss +++ b/packages/desktop-ui-lib/src/lib/recap/shared/ui/progress-status/progress-status.component.scss @@ -3,6 +3,7 @@ .wrapper { display: flex; align-items: center; + gap: 0.5rem } :host { @@ -20,12 +21,6 @@ .progress-container { height: 5px !important; } - - .progress-value { - span { - display: none; - } - } } } } @@ -34,12 +29,11 @@ width: 100%; .percentage-col { - margin-right: 10px; width: 60px; } .progress-col { - width: 75%; + width: 100%; display: flex; align-items: flex-end; } diff --git a/packages/desktop-ui-lib/src/lib/recap/shared/ui/progress-status/progress-status.component.ts b/packages/desktop-ui-lib/src/lib/recap/shared/ui/progress-status/progress-status.component.ts index 530a8070bd2..59798278df7 100644 --- a/packages/desktop-ui-lib/src/lib/recap/shared/ui/progress-status/progress-status.component.ts +++ b/packages/desktop-ui-lib/src/lib/recap/shared/ui/progress-status/progress-status.component.ts @@ -2,6 +2,11 @@ import { Component, Input } from '@angular/core'; import { progressStatus } from '@gauzy/ui-core/common'; import { NbComponentOrCustomStatus } from '@nebular/theme'; +export interface IProgressStatusDisplayValue { + in?: boolean; + out?: boolean; +} + @Component({ selector: 'ngx-progress-status', templateUrl: './progress-status.component.html', @@ -11,7 +16,7 @@ export class ProgressStatusComponent { /* * Getter & Setter for Percentage */ - private _percentage: any; + private _percentage: number = 0; get percentage(): number { return this._percentage; } @@ -19,10 +24,24 @@ export class ProgressStatusComponent { this._percentage = value; } + /* + * Getter & Setter for Percentage + */ + private _displayValue: IProgressStatusDisplayValue = { + in: false, + out: true + }; + get displayValue(): IProgressStatusDisplayValue { + return this._displayValue; + } + @Input() set displayValue(value: IProgressStatusDisplayValue) { + this._displayValue = value; + } + /* * Getter & Setter for NbComponentOrCustomStatus */ - private _defaultStatus: NbComponentOrCustomStatus; + private _defaultStatus: NbComponentOrCustomStatus = 'success'; get defaultStatus(): NbComponentOrCustomStatus { return this._defaultStatus; } diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/+state/weekly.query.ts b/packages/desktop-ui-lib/src/lib/recap/weekly/+state/weekly.query.ts new file mode 100644 index 00000000000..98bd3f58449 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/+state/weekly.query.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { Query } from '@datorama/akita'; +import { Observable } from 'rxjs'; +import { IDateRangePicker } from '../../shared/features/date-range-picker/date-picker.interface'; +import { IWeeklyRecapState, WeeklyRecapStore } from './weekly.store'; + +@Injectable({ providedIn: 'root' }) +export class WeeklyRecapQuery extends Query { + public readonly range$: Observable = this.select((state) => state.range); + public readonly state$: Observable = this.select(); + public readonly isLoading$: Observable = this.selectLoading(); + + constructor(protected store: WeeklyRecapStore) { + super(store); + } +} diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/+state/weekly.service.ts b/packages/desktop-ui-lib/src/lib/recap/weekly/+state/weekly.service.ts new file mode 100644 index 00000000000..c5008270f21 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/+state/weekly.service.ts @@ -0,0 +1,98 @@ +import { Injectable } from '@angular/core'; +import { ICountsStatistics, IGetCountsStatistics, IGetTimeLogInput, ReportDayData } from '@gauzy/contracts'; +import { Observable } from 'rxjs'; +import { RequestQuery } from '../../+state/request/request.query'; +import { Store, ToastrNotificationService } from '../../../services'; +import { TimesheetService, TimesheetStatisticsService } from '../../services/timesheet'; +import { IDateRangePicker } from '../../shared/features/date-range-picker/date-picker.interface'; +import { WeeklyRecapQuery } from './weekly.query'; +import { IWeeklyRecapState, WeeklyRecapStore } from './weekly.store'; + +@Injectable({ + providedIn: 'root' +}) +export class WeeklyRecapService { + constructor( + private readonly timesheetStatisticsService: TimesheetStatisticsService, + private readonly timesheetService: TimesheetService, + private readonly notificationService: ToastrNotificationService, + private readonly weeklyQuery: WeeklyRecapQuery, + private readonly weeklyStore: WeeklyRecapStore, + private readonly requestQuery: RequestQuery, + private readonly store: Store + ) {} + + public update(state: Partial) { + this.weeklyStore.update(state); + } + + public get state$(): Observable { + return this.weeklyQuery.state$; + } + + public get range$(): Observable { + return this.weeklyQuery.range$; + } + + public get range(): IDateRangePicker { + return this.weeklyQuery.getValue().range; + } + + public get count(): ICountsStatistics { + return this.weeklyQuery.getValue().count; + } + + public get weeklyActivities(): ReportDayData[] { + return this.weeklyQuery.getValue().weeklyActivities; + } + + public async getWeeklyActivities(): Promise { + try { + this.weeklyStore.setLoading(true); + const { organizationId, tenantId, user } = this.store; + const employeeIds = [user.employee.id]; + const timeZone = user.timeZone; + const timeFormat = user.timeFormat; + const request: IGetTimeLogInput = { + ...this.requestQuery.request, + ...this.range, + organizationId, + employeeIds, + tenantId, + timeFormat, + timeZone, + unitOfTime: 'week' + }; + const weeklyActivities = await this.timesheetService.getWeeklyReportChart(request); + this.weeklyStore.update({ weeklyActivities }); + } catch (error) { + this.notificationService.error(error.message || 'An error occurred while fetching tasks.'); + this.weeklyStore.setError(error); + } finally { + this.weeklyStore.setLoading(false); + } + } + + public async getCounts(): Promise { + try { + this.weeklyStore.setLoading(true); + const { organizationId, tenantId, user } = this.store; + const employeeIds = [user.employee.id]; + const request: IGetCountsStatistics = { + ...this.requestQuery.request, + ...this.range, + organizationId, + employeeIds, + onlyMe: true, + tenantId + }; + const count = await this.timesheetStatisticsService.getCounts(request); + this.weeklyStore.update({ count: { ...count, reWeeklyLimit: user.employee.reWeeklyLimit } }); + } catch (error) { + this.notificationService.error(error.message || 'An error occurred while fetching tasks.'); + this.weeklyStore.setError(error); + } finally { + this.weeklyStore.setLoading(false); + } + } +} diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/+state/weekly.store.ts b/packages/desktop-ui-lib/src/lib/recap/weekly/+state/weekly.store.ts new file mode 100644 index 00000000000..7cc915dbfd3 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/+state/weekly.store.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; +import { Store, StoreConfig } from '@datorama/akita'; +import { ICountsStatistics, ReportDayData } from '@gauzy/contracts'; +import { TimeTrackerDateManager } from '../../../services'; +import { IDateRangePicker } from '../../shared/features/date-range-picker/date-picker.interface'; + +export interface IWeeklyRecapState { + count: ICountsStatistics & { reWeeklyLimit: number }; + range: IDateRangePicker; + weeklyActivities: ReportDayData[]; +} + +export function createInitialState(): IWeeklyRecapState { + return { + weeklyActivities: [], + range: { + startDate: TimeTrackerDateManager.startCurrentWeek, + endDate: TimeTrackerDateManager.endCurrentWeek + }, + count: { + reWeeklyLimit: 0, + projectsCount: 0, + employeesCount: 0, + weekActivities: 0, + weekDuration: 0, + todayActivities: 0, + todayDuration: 0 + } + }; +} + +@StoreConfig({ name: '_weeklyRecap' }) +@Injectable({ providedIn: 'root' }) +export class WeeklyRecapStore extends Store { + constructor() { + super(createInitialState()); + } +} diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-calendar/weekly-calendar.component.html b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-calendar/weekly-calendar.component.html new file mode 100644 index 00000000000..ab9810097b7 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-calendar/weekly-calendar.component.html @@ -0,0 +1,2 @@ + diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-calendar/weekly-calendar.component.scss b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-calendar/weekly-calendar.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-calendar/weekly-calendar.component.ts b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-calendar/weekly-calendar.component.ts new file mode 100644 index 00000000000..ee77f83dc2c --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-calendar/weekly-calendar.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { map } from 'rxjs'; +import { WeeklyRecapService } from '../../+state/weekly.service'; +import { IDateRangePicker } from '../../../shared/features/date-range-picker/date-picker.interface'; + +@Component({ + selector: 'ngx-weekly-calendar', + templateUrl: './weekly-calendar.component.html', + styleUrls: ['./weekly-calendar.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class WeeklyCalendarComponent { + constructor(private readonly weeklyRecapService: WeeklyRecapService) {} + + public get selectedDateRange$() { + return this.weeklyRecapService.state$.pipe(map((state) => state.range)); + } + + public onRangeChange(range: IDateRangePicker) { + this.weeklyRecapService.update({ range }); + } +} diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-progress/weekly-progress.component.html b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-progress/weekly-progress.component.html new file mode 100644 index 00000000000..494472e4838 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-progress/weekly-progress.component.html @@ -0,0 +1,20 @@ +
+
+
+ {{(range$ | async).startDate | dateTime : 'll'}} - {{(range$ | async).endDate | dateTime : 'll'}} +
+
+ +
+
{{ day | dateTime : 'D dddd' }}
+ +
{{ (weeklyActivities$ | async)?.dates[day]?.sum | durationFormat : 'h[h] m[m] s[s]':{trim:'both'} }} +
+
+
+
+ + + + diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-progress/weekly-progress.component.scss b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-progress/weekly-progress.component.scss new file mode 100644 index 00000000000..1007ce1db87 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-progress/weekly-progress.component.scss @@ -0,0 +1,35 @@ +@import 'report'; + +.activity { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.week-days { + display: flex; + align-items: center; + justify-content: space-between; + border-top: 0.5px solid var(--select-filled-control-disabled-text-color); + padding-top: 1rem; + + .day, + .hours { + text-wrap: nowrap; + } +} + +.week-range { + font-weight: 500; +} + +.week-wrapper { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; + border-radius: var(--border-radius); + background-color: var(--gauzy-card-3); + height: 100%; +} diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-progress/weekly-progress.component.ts b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-progress/weekly-progress.component.ts new file mode 100644 index 00000000000..91441e0a8bc --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-progress/weekly-progress.component.ts @@ -0,0 +1,30 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { map, tap } from 'rxjs'; +import { WeeklyRecapService } from '../../+state/weekly.service'; +import { updateWeekDays } from '../../../shared/features/date-range-picker'; + +@Component({ + selector: 'ngx-weekly-progress', + templateUrl: './weekly-progress.component.html', + styleUrls: ['./weekly-progress.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class WeeklyProgressComponent { + public weekDays: string[] = []; + + constructor(private readonly weeklyRecapService: WeeklyRecapService) {} + + public get weeklyActivities$() { + return this.weeklyRecapService.state$.pipe(map((state) => state.weeklyActivities[0])); + } + + public get range$() { + return this.weeklyRecapService.state$.pipe( + tap((state) => { + const { weekDays } = updateWeekDays(state.range); + this.weekDays = weekDays; + }), + map((state) => state.range) + ); + } +} diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-recap/weekly-recap.component.html b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-recap/weekly-recap.component.html new file mode 100644 index 00000000000..d8674ef95b5 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-recap/weekly-recap.component.html @@ -0,0 +1,15 @@ + + + + + +
+ + +
+
+ + + + +
diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-recap/weekly-recap.component.scss b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-recap/weekly-recap.component.scss new file mode 100644 index 00000000000..2d05816ca92 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-recap/weekly-recap.component.scss @@ -0,0 +1,11 @@ +@import '../../../features/recap/recap.component.scss'; + +.recap-weekly { + display: flex; + flex-direction: column; + gap: 1rem; + background-color: var(--gauzy-card-2); + padding: 1rem; + border-radius: var(--border-radius); + height: 100%; +} diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-recap/weekly-recap.component.ts b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-recap/weekly-recap.component.ts new file mode 100644 index 00000000000..4306520d33d --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-recap/weekly-recap.component.ts @@ -0,0 +1,44 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { BehaviorSubject, combineLatest, concatMap } from 'rxjs'; +import { WeeklyRecapService } from '../../+state/weekly.service'; +import { AutoRefreshService } from '../../../+state/auto-refresh/auto-refresh.service'; +import { RequestQuery } from '../../../+state/request/request.query'; +import { LoggerService } from '../../../../electron/services'; + +@UntilDestroy({ checkProperties: true }) +@Component({ + selector: 'ngx-weekly-recap', + templateUrl: './weekly-recap.component.html', + styleUrls: ['./weekly-recap.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class WeeklyRecapComponent implements OnInit { + public isLoading$ = new BehaviorSubject(false); + constructor( + private readonly weeklyRecapService: WeeklyRecapService, + private readonly requestQuery: RequestQuery, + private readonly autoRefreshService: AutoRefreshService, + private readonly logger: LoggerService + ) {} + + ngOnInit(): void { + combineLatest([this.weeklyRecapService.range$, this.requestQuery.request$, this.autoRefreshService.refresh$]) + .pipe( + concatMap(() => this.load()), + untilDestroyed(this) + ) + .subscribe(); + } + + public async load(): Promise { + try { + this.isLoading$.next(true); + await Promise.allSettled([this.weeklyRecapService.getCounts(), this.weeklyRecapService.getWeeklyActivities()]); + } catch (error) { + this.logger.error(error) + }finally { + this.isLoading$.next(false) + } + } +} diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-statistic/weekly-statistic.component.html b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-statistic/weekly-statistic.component.html new file mode 100644 index 00000000000..c834dae87cc --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-statistic/weekly-statistic.component.html @@ -0,0 +1,38 @@ + + + + +
{{ 'TIMESHEET.WORKED_TODAY' | translate }}
+
+ +
{{ todayDuration$ | async | durationFormat : 'h[h] m[m] s[s]':{trim: 'both'} }}
+
+
+ + +
{{ 'TIMESHEET.TODAY_ACTIVITY' | translate }}
+
+ +
{{ todayActivity$ | async | percent:'1.0-2' }}
+
+
+ + +
{{ 'TIMESHEET.WORKED_FOR_WEEK' | translate }}
+
+ +
{{ weeklyDuration$ | async | durationFormat : 'h[h] m[m] s[s]':{trim: 'both'} }}
+
of {{ weeklyLimit$ | async | durationFormat : 'h[h]':{trim: 'both'} }} +
+
+
+ + +
{{ 'TIMESHEET.ACTIVITY_FOR_WEEK' | translate }}
+
+ +
{{ weeklyActivity$ | async | percent:'1.0-2' }}
+
+
+
+
diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-statistic/weekly-statistic.component.scss b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-statistic/weekly-statistic.component.scss new file mode 100644 index 00000000000..7f65afa2f18 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-statistic/weekly-statistic.component.scss @@ -0,0 +1,36 @@ +nb-card { + margin: 0 !important; + + nb-card-body, + nb-card-header { + padding: 0 !important; + } + + .card-wrapper { + display: flex; + flex-direction: row; + gap: 1rem; + + nb-card { + padding: 1rem; + width: 100%; + } + } +} + +.h1 { + color: var(--gauzy-text-color-1); + font-size: 32px; + font-weight: 400; + line-height: 44px; + letter-spacing: 0; + text-wrap: nowrap; +} + +.h2 { + font-size: 16px; + font-weight: 400; + line-height: 16px; + letter-spacing: -0.009em; + color: var(--gauzy-text-color-2); +} diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-statistic/weekly-statistic.component.ts b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-statistic/weekly-statistic.component.ts new file mode 100644 index 00000000000..cf1e2103b9e --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-statistic/weekly-statistic.component.ts @@ -0,0 +1,33 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { map } from 'rxjs'; +import { WeeklyRecapService } from '../../+state/weekly.service'; + +@Component({ + selector: 'ngx-weekly-statistic', + templateUrl: './weekly-statistic.component.html', + styleUrls: ['./weekly-statistic.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class WeeklyStatisticComponent { + constructor(private readonly weeklyRecapService: WeeklyRecapService) {} + + public get todayDuration$() { + return this.weeklyRecapService.state$.pipe(map((state) => state.count.todayDuration)); + } + + public get weeklyDuration$() { + return this.weeklyRecapService.state$.pipe(map((state) => state.count.weekDuration)); + } + + public get todayActivity$() { + return this.weeklyRecapService.state$.pipe(map((state) => state.count.todayActivities / 100)); + } + + public get weeklyActivity$() { + return this.weeklyRecapService.state$.pipe(map((state) => state.count.weekActivities / 100)); + } + + public get weeklyLimit$() { + return this.weeklyRecapService.state$.pipe(map((state) => state.count.reWeeklyLimit)); + } +} diff --git a/packages/desktop-ui-lib/src/lib/services/time-tracker-date.manager.ts b/packages/desktop-ui-lib/src/lib/services/time-tracker-date.manager.ts index b8571b17cc9..1672b455e0e 100644 --- a/packages/desktop-ui-lib/src/lib/services/time-tracker-date.manager.ts +++ b/packages/desktop-ui-lib/src/lib/services/time-tracker-date.manager.ts @@ -1,5 +1,5 @@ -import * as moment from 'moment'; import { IOrganization, LanguagesEnum } from '@gauzy/contracts'; +import * as moment from 'moment'; export class TimeTrackerDateManager { private static _instance: TimeTrackerDateManager; @@ -26,31 +26,19 @@ export class TimeTrackerDateManager { } public static get startWeek(): string { - return moment() - .startOf('week') - .subtract(this.utcOffset, 'minutes') - .format('YYYY-MM-DD HH:mm:ss'); + return moment().startOf('week').subtract(this.utcOffset, 'minutes').format('YYYY-MM-DD HH:mm:ss'); } public static get endWeek(): string { - return moment() - .endOf('week') - .subtract(this.utcOffset, 'minutes') - .format('YYYY-MM-DD HH:mm:ss'); + return moment().endOf('week').subtract(this.utcOffset, 'minutes').format('YYYY-MM-DD HH:mm:ss'); } public static get startToday(): string { - return moment() - .startOf('day') - .subtract(this.utcOffset, 'minutes') - .format('YYYY-MM-DD HH:mm:ss'); + return moment().startOf('day').subtract(this.utcOffset, 'minutes').format('YYYY-MM-DD HH:mm:ss'); } public static get endToday(): string { - return moment() - .endOf('day') - .subtract(this.utcOffset, 'minutes') - .format('YYYY-MM-DD HH:mm:ss'); + return moment().endOf('day').subtract(this.utcOffset, 'minutes').format('YYYY-MM-DD HH:mm:ss'); } public static get utcOffset(): number { @@ -77,17 +65,15 @@ export class TimeTrackerDateManager { public static get isMidnight(): boolean { const now = moment(); const endOfDay = now.clone().endOf('day'); - return moment(now.format('YYYY-MM-DD HH:mm:ss')).isSame( - endOfDay.format('YYYY-MM-DD HH:mm:ss') - ); + return moment(now.format('YYYY-MM-DD HH:mm:ss')).isSame(endOfDay.format('YYYY-MM-DD HH:mm:ss')); } // Set the start of the week private startWeekDay() { moment.updateLocale(this._language, { week: { - dow: TimeTrackerDateManager._startWeekDayNumber, - }, + dow: TimeTrackerDateManager._startWeekDayNumber + } }); } @@ -103,4 +89,12 @@ export class TimeTrackerDateManager { public static get endCurrentDay(): string { return moment().endOf('day').format('YYYY-MM-DD HH:mm:ss'); } + + public static get startCurrentWeek(): string { + return moment().startOf('week').format('YYYY-MM-DD HH:mm:ss'); + } + + public static get endCurrentWeek(): string { + return moment().endOf('week').format('YYYY-MM-DD HH:mm:ss'); + } } diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/organization-selector/organization-selector.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/organization-selector/organization-selector.component.ts index ae11953e59a..97c3b664910 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/organization-selector/organization-selector.component.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/organization-selector/organization-selector.component.ts @@ -1,24 +1,16 @@ -import { - Component, - OnInit, - Output, - EventEmitter, - AfterViewInit, - NgZone, - Input, -} from '@angular/core'; +import { AfterViewInit, Component, EventEmitter, Input, NgZone, OnInit, Output } from '@angular/core'; import { IOrganization, IUser } from '@gauzy/contracts'; import { uniq } from 'underscore'; -import { UserOrganizationService } from './user-organization.service'; import { ElectronService } from '../../electron/services'; +import { Store } from '../../services'; +import { UserOrganizationService } from './user-organization.service'; @Component({ selector: 'ngx-desktop-timer-organization-selector', templateUrl: './organization-selector.component.html', - styleUrls: ['./organization-selector.component.scss'], + styleUrls: ['./organization-selector.component.scss'] }) export class OrganizationSelectorComponent implements OnInit, AfterViewInit { - public organizations: IOrganization[] = []; public selectedOrganization: IOrganization; @@ -47,13 +39,14 @@ export class OrganizationSelectorComponent implements OnInit, AfterViewInit { constructor( private readonly _userOrganizationService: UserOrganizationService, private readonly _electronService: ElectronService, + private readonly _store: Store, private readonly _ngZone: NgZone ) { this.organizationChange = new EventEmitter(); this._isDisabled = false; } - ngOnInit() { } + ngOnInit() {} /** * Component lifecycle hook for operations after the view initializes. @@ -91,6 +84,7 @@ export class OrganizationSelectorComponent implements OnInit, AfterViewInit { // Emit an event indicating the organization has changed if (this.organizationChange) { + this._store.selectedOrganization = this.selectedOrganization; this.organizationChange.emit(organization); } } @@ -125,9 +119,7 @@ export class OrganizationSelectorComponent implements OnInit, AfterViewInit { if (this.organizations.length > 0) { // Find the default organization - const defaultOrganization = this.organizations.find( - (organization) => organization.isDefault - ); + const defaultOrganization = this.organizations.find((organization) => organization.isDefault); // Select the first organization from the list const [firstOrganization] = this.organizations; @@ -144,6 +136,8 @@ export class OrganizationSelectorComponent implements OnInit, AfterViewInit { // If no specific ID, use default or first organization this.selectedOrganization = defaultOrganization || firstOrganization; } + + this._store.selectedOrganization = this.selectedOrganization; } } catch (error) { console.error('Error loading organizations:', error); // Error handling diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/pipes/dayjs.pipe.ts b/packages/desktop-ui-lib/src/lib/time-tracker/pipes/dayjs.pipe.ts new file mode 100644 index 00000000000..db42da38877 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/pipes/dayjs.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import * as dayjs from 'dayjs'; + +@Pipe({ + name: 'dayjs' +}) +export class DayjsPipe implements PipeTransform { + transform(value: Date): dayjs.Dayjs { + return dayjs(value); + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/pipes/pipe.module.ts b/packages/desktop-ui-lib/src/lib/time-tracker/pipes/pipe.module.ts index 09b4bb419fa..4ca0e903832 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/pipes/pipe.module.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/pipes/pipe.module.ts @@ -1,11 +1,12 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { DateTimePipe } from './date-time.pipe'; +import { DayjsPipe } from './dayjs.pipe'; import { DurationFormatPipe } from './duration-format.pipe'; import { HumanizePipe } from './humanize.pipe'; import { ReplacePipe } from './replace.pipe'; -const pipes = [DateTimePipe, DurationFormatPipe, HumanizePipe, ReplacePipe]; +const pipes = [DateTimePipe, DurationFormatPipe, HumanizePipe, ReplacePipe, DayjsPipe]; @NgModule({ declarations: [...pipes], imports: [CommonModule], diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html index c3c158d9d91..3e557967a4c 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html @@ -4,31 +4,24 @@
-
+ }" class="no-padding full-width timer">
+ [disabled]="start$ | async">
-
+ }" class="timer-container"> {{ timeRun$ | async }}
@@ -40,25 +33,21 @@ {{ 'TIMER_TRACKER.TODAY' | translate }}
-
+ [ngClass]="{ over: (isOver$ | async) }" class="work-duration-container" + nbTooltipIcon="alert-triangle-outline" nbTooltipStatus="danger">
{{ weeklyDuration$ | async }}
{{ 'TIMER_TRACKER.OF_HRS' - | translate - : { - limit: (weeklyLimit$ | async) - } - }} + | translate + : { + limit: (weeklyLimit$ | async) + } + }}
{{ 'TIMESHEET.WEEK' | translate }} @@ -69,37 +58,18 @@
- -
@@ -108,23 +78,16 @@
- + [placeholder]="'TIMER_TRACKER.SELECT_CLIENT' | translate" bindLabel="name" + bindValue="id" nbTooltipStatus="warning"> @@ -133,12 +96,8 @@
- + {{ item?.name }} @@ -150,23 +109,16 @@
- + [placeholder]="'TIMER_TRACKER.SELECT_PROJECT' | translate" bindLabel="name" + bindValue="id" nbTooltipStatus="warning"> @@ -175,12 +127,8 @@
- + {{ item?.name }} @@ -192,21 +140,13 @@
- + [placeholder]="'FORM.LABELS.SELECT_TEAM' | translate | titlecase" bindLabel="name" + bindValue="id" nbTooltipStatus="warning"> @@ -227,32 +167,22 @@
- + [placeholder]="'TIMER_TRACKER.SELECT_TASK' | translate" bindLabel="title" + bindValue="id" nbTooltipStatus="warning"> - + #{{ - item?.taskNumber || item?.prefix.concat('-').concat(item?.number) - | uppercase + item?.taskNumber || item?.prefix.concat('-').concat(item?.number) + | uppercase }} @@ -263,13 +193,11 @@
- + #{{ - item?.taskNumber || - item?.prefix.concat('-').concat(item?.number) | uppercase + item?.taskNumber || + item?.prefix.concat('-').concat(item?.number) | uppercase }} @@ -282,36 +210,26 @@
-
+ class="col-12 form-group" nbTooltipStatus="warning"> - +
-
+
- {{ 'TIMER_TRACKER.WAKATIME_INTEGRATION' | translate }} + {{ 'TIMER_TRACKER.WAKATIME_INTEGRATION' | translate + }}
@@ -320,26 +238,18 @@
-
+
{{ 'TIMER_TRACKER.LAST_CAPTURE_TAKEN' | translate }} {{ (lastScreenCapture$ | async)?.recordedAt | humanize }}
- - + [src]="(lastScreenCapture$ | async)?.thumbUrl" class="screen-capture-img" /> +
@@ -351,15 +261,11 @@
-
@@ -382,8 +288,7 @@
- + [settings]="smartTableSettings" [source]="sourceData$ | async" + style="cursor: pointer">
@@ -450,73 +341,42 @@ + + +
-