From 19515e13936ee11023abee95b084eb990b22b150 Mon Sep 17 00:00:00 2001 From: Ryan Maffit Date: Fri, 21 Jun 2024 21:36:44 +0000 Subject: [PATCH 01/25] WIP staff dashboard --- src/angular/hq/src/app/layout.component.html | 4 +- .../staff-dashboard/get-dashboard-time-v1.ts | 11 ++- .../staff-dashboard.component.html | 98 ++++++++++++++++++- .../staff-dashboard.component.ts | 6 +- .../staff-dashboard.service.ts | 22 ++++- .../HQ.Abstractions/DateOnlyExtensions.cs | 22 +++++ .../Times/GetDashboardTimeV1.cs | 13 ++- .../HQ.Server/Services/TimeEntryServiceV1.cs | 61 ++++++++---- 8 files changed, 203 insertions(+), 34 deletions(-) diff --git a/src/angular/hq/src/app/layout.component.html b/src/angular/hq/src/app/layout.component.html index 2d2c54da..0302ea66 100644 --- a/src/angular/hq/src/app/layout.component.html +++ b/src/angular/hq/src/app/layout.component.html @@ -85,9 +85,9 @@
My Dashboard
diff --git a/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts b/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts index e7e47452..2ea7f384 100644 --- a/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts +++ b/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts @@ -3,11 +3,17 @@ import { Period } from '../../projects/project-create/project-create.component'; export interface GetDashboardTimeV1Request { staffId: string; period: Period; - fromDate?: string | null; - toDate?: string | null; + date?: string; + search?: string | null; } export interface GetDashboardTimeV1Response { + totalHours: number; + billableHours: number; + startDate: string; + endDate: string; + previousDate: string; + nextDate: string; dates: GetDashboardTimeV1TimeForDate[]; chargeCodes: GetDashboardTimeV1ChargeCode[]; clients: GetDashboardTimeV1Client[]; @@ -20,6 +26,7 @@ export interface GetDashboardTimeV1TimeForDate { } export interface GetDashboardTimeV1TimeForDateTimes { + id: string; date: string; hours: number; notes: string | null; diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html index b6200847..b528137e 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html @@ -1,4 +1,94 @@ -

staff-dashboard works!

-
-    {{ staffDashboardService.time$ | async | json }}
-
+ + + + +@if (staffDashboardService.time$ | async; as dashboard) { + + + +
Total Hours: {{ dashboard.totalHours }}
+
Billable Hours: {{ dashboard.billableHours }}
+ @for (date of dashboard.dates; track date.date) { +

{{ date.date }}

+ + + + + + + + + + + + + + @for (time of date.times; track time.id) { + + + + + + + + + + } + +
+ Hrs + + Date + + Chrg Code + + Client + + Project + + Activity / Task + + Description +
{{ time.hours }}{{ time.date }} + {{ time.chargeCodeId }} + {{ time.clientId }} + {{ time.projectId }} + + {{ time.activityId ?? "" + time.task ?? "" }} + {{ time.notes }}
+ } +} diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts index 88f27724..ac202e23 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts @@ -1,14 +1,18 @@ import { Component } from '@angular/core'; import { StaffDashboardService } from './staff-dashboard.service'; import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; +import { Period } from '../models/times/get-time-v1'; @Component({ selector: 'hq-staff-dashboard', standalone: true, - imports: [CommonModule], + imports: [CommonModule, ReactiveFormsModule], providers: [StaffDashboardService], templateUrl: './staff-dashboard.component.html', }) export class StaffDashboardComponent { constructor(public staffDashboardService: StaffDashboardService) {} + + Period = Period; } diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts index 2af39fde..457388fe 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts @@ -3,7 +3,15 @@ import { FormControl } from '@angular/forms'; import { Period } from '../projects/project-create/project-create.component'; import { HQService } from '../services/hq.service'; import { OidcSecurityService } from 'angular-auth-oidc-client'; -import { Observable, combineLatest, map, startWith, switchMap } from 'rxjs'; +import { + Observable, + combineLatest, + debounceTime, + map, + startWith, + switchMap, + tap, +} from 'rxjs'; import { GetDashboardTimeV1Response } from '../models/staff-dashboard/get-dashboard-time-v1'; @Injectable({ @@ -12,7 +20,9 @@ import { GetDashboardTimeV1Response } from '../models/staff-dashboard/get-dashbo export class StaffDashboardService { search = new FormControl(null); period = new FormControl(Period.Week, { nonNullable: true }); - startDate = new FormControl(null); + date = new FormControl(new Date().toISOString().split('T')[0], { + nonNullable: true, + }); time$: Observable; @@ -25,15 +35,23 @@ export class StaffDashboardService { map((t) => t.staff_id as string), ); + const search$ = this.search.valueChanges.pipe(startWith(this.search.value)); const period$ = this.period.valueChanges.pipe(startWith(this.period.value)); + const date$ = this.date.valueChanges.pipe(startWith(this.date.value)); const request$ = combineLatest({ staffId: staffId$, period: period$, + search: search$, + date: date$, }); this.time$ = request$.pipe( + debounceTime(250), switchMap((request) => this.hqService.getDashboardTimeV1(request)), + tap((response) => + this.date.setValue(response.startDate, { emitEvent: false }), + ), ); } } diff --git a/src/dotnet/HQ.Abstractions/DateOnlyExtensions.cs b/src/dotnet/HQ.Abstractions/DateOnlyExtensions.cs index df664a20..5844ca43 100644 --- a/src/dotnet/HQ.Abstractions/DateOnlyExtensions.cs +++ b/src/dotnet/HQ.Abstractions/DateOnlyExtensions.cs @@ -12,6 +12,28 @@ namespace HQ.Abstractions { public static class DateOnlyExtension { + public static DateOnly AddPeriod(this DateOnly forDate, Period period, int value) + { + switch (period) + { + case Period.Year: + return forDate.AddYears(value); + case Period.Quarter: + return forDate.AddMonths(value * 3); + case Period.Month: + return forDate.AddMonths(value); + case Period.Today: + return forDate.AddDays(value); + case Period.LastWeek: + return forDate.AddDays(-7 + value * 7); + case Period.LastMonth: + return forDate.AddMonths(-1 + value); + case Period.Week: + default: + return forDate.AddDays(value * 7); + } + } + public static DateOnly GetPeriodStartDate(this DateOnly forDate, Period period) { switch (period) diff --git a/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs b/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs index 1539b705..750954bb 100644 --- a/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs +++ b/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs @@ -7,13 +7,19 @@ public class GetDashboardTimeV1 public class Request { public Guid StaffId { get; set; } + public DateOnly Date { get; set; } public Period Period { get; set; } - public DateOnly? FromDate { get; set; } - public DateOnly? ToDate { get; set; } + public string? Search { get; set; } } public class Response { + public decimal TotalHours { get; set; } + public decimal BillableHours { get; set; } + public DateOnly StartDate { get; set; } + public DateOnly EndDate { get; set; } + public DateOnly PreviousDate { get; set; } + public DateOnly NextDate { get; set; } public List Dates { get; set; } = new(); public List ChargeCodes { get; set; } = new(); public List Clients { get; set; } = new(); @@ -39,12 +45,12 @@ public class Project public Guid Id { get; set; } public Guid ClientId { get; set; } public string Name { get; set; } = null!; + public List Activities { get; set; } = new(); } public class Activities { public Guid Id { get; set; } - public Guid ProjectId { get; set; } public string Name { get; set; } = null!; } @@ -56,6 +62,7 @@ public class TimeForDate public class TimeEntry { + public Guid Id { get; set; } public DateOnly Date { get; set; } public decimal Hours { get; set; } public string? Notes { get; set; } diff --git a/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs b/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs index c2a7aa71..aafc688f 100644 --- a/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs +++ b/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs @@ -451,25 +451,32 @@ public TimeEntryServiceV1(HQDbContext context) public async Task> GetDashboardTimeV1(GetDashboardTimeV1.Request request, CancellationToken ct = default) { - if (request.Period == Period.Custom) - { - if (!request.FromDate.HasValue || !request.ToDate.HasValue || request.FromDate.Value > request.ToDate.Value) - { - return Result.Fail("Invalid date range."); - } - } - else + var startDate = request.Date.GetPeriodStartDate(request.Period); + var endDate = request.Date.GetPeriodEndDate(request.Period); + var previousDate = startDate.AddPeriod(request.Period, -1); + var nextDate = startDate.AddPeriod(request.Period, 1); + + var timesQuery = _context.Times + .AsNoTracking() + .Where(t => t.StaffId == request.StaffId && t.Date >= startDate && t.Date <= endDate) + .AsQueryable(); + + if (!String.IsNullOrEmpty(request.Search)) { - request.FromDate = DateOnly.FromDateTime(DateTime.Today).GetPeriodStartDate(request.Period); - request.ToDate = DateOnly.FromDateTime(DateTime.Today).GetPeriodEndDate(request.Period); + timesQuery = timesQuery.Where(t => + t.ChargeCode.Code.ToLower().Contains(request.Search.ToLower()) || + t.ChargeCode.Project!.Client.Name.ToLower().Contains(request.Search.ToLower()) || + t.ChargeCode.Project!.Name.ToLower().Contains(request.Search.ToLower()) || + t.Activity!.Name.ToLower().Contains(request.Search.ToLower()) || + t.Task!.ToLower().Contains(request.Search.ToLower()) || + t.Notes!.ToLower().Contains(request.Search.ToLower()) + ); } - - var times = await _context.Times - .Where(t => t.StaffId == request.StaffId && t.Date >= request.FromDate && t.Date <= request.ToDate) - .OrderByDescending(t => t.CreatedAt) + var times = await timesQuery.OrderByDescending(t => t.CreatedAt) .Select(t => new GetDashboardTimeV1.TimeEntry() { + Id = t.Id, Date = t.Date, ChargeCodeId = t.ChargeCodeId, ActivityId = t.ActivityId, @@ -482,8 +489,8 @@ public TimeEntryServiceV1(HQDbContext context) .GroupBy(t => t.Date) .ToDictionaryAsync(t => t.Key, t => t.ToList()); - var chargeCodes = await _context.ChargeCodes + .AsNoTracking() .OrderBy(t => t.Code) .Select(t => new GetDashboardTimeV1.ChargeCode() { @@ -496,6 +503,7 @@ public TimeEntryServiceV1(HQDbContext context) var clients = await _context.Clients + .AsNoTracking() .OrderBy(t => t.Name) .Select(t => new GetDashboardTimeV1.Client() { @@ -506,12 +514,19 @@ public TimeEntryServiceV1(HQDbContext context) var projects = await _context.Projects + .AsNoTracking() .OrderBy(t => t.Name) + .Include(t => t.Activities) .Select(t => new GetDashboardTimeV1.Project() { Id = t.Id, Name = t.Name, - ClientId = t.ClientId + ClientId = t.ClientId, + Activities = t.Activities.Select(x => new Abstractions.Times.GetDashboardTimeV1.Activities() + { + Id = x.Id, + Name = x.Name + }).ToList() }) .ToListAsync(ct); @@ -520,8 +535,14 @@ public TimeEntryServiceV1(HQDbContext context) response.ChargeCodes = chargeCodes; response.Clients = clients; response.Projects = projects; - - DateOnly date = request.ToDate.Value; + response.StartDate = startDate; + response.EndDate = endDate; + response.TotalHours = await timesQuery.SumAsync(t => t.Hours); + response.BillableHours = await timesQuery.Where(t => t.ChargeCode.Billable).SumAsync(t => t.Hours); + response.NextDate = nextDate; + response.PreviousDate = previousDate; + + DateOnly date = endDate; do { var timeForDate = new GetDashboardTimeV1.TimeForDate(); @@ -531,14 +552,14 @@ public TimeEntryServiceV1(HQDbContext context) timeForDate.Times = times[date]; } - response.Dates.Add(timeForDate); date = date.AddDays(-1); } - while (date >= request.FromDate.Value); + while (date >= startDate); return response; } + public GetTimesV1.Request MapToGetTimesV1Request(ExportTimesV1.Request exportRequest) { return new GetTimesV1.Request From 75ec1a73fc29162712801261b96ce4ff1c4ea5d3 Mon Sep 17 00:00:00 2001 From: Ryan Maffit Date: Mon, 24 Jun 2024 17:38:05 +0000 Subject: [PATCH 02/25] Got events working with component --- src/angular/hq/.eslintrc.json | 2 +- .../common/functions/round-to-next-quarter.ts | 3 + .../staff-dashboard/get-dashboard-time-v1.ts | 12 +- .../psrtime-list/psrtime-list.component.ts | 6 +- .../staff-dashboard-time-entry.component.html | 99 +++++++++ .../staff-dashboard-time-entry.component.ts | 190 ++++++++++++++++++ .../staff-dashboard.component.html | 61 ++++-- .../staff-dashboard.component.ts | 18 +- .../staff-dashboard.service.ts | 22 +- .../times/time-edit/time-edit.component.ts | 6 +- .../Times/GetDashboardTimeV1.cs | 6 +- .../HQ.Server/Services/TimeEntryServiceV1.cs | 29 ++- 12 files changed, 397 insertions(+), 57 deletions(-) create mode 100644 src/angular/hq/src/app/common/functions/round-to-next-quarter.ts create mode 100644 src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html create mode 100644 src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts diff --git a/src/angular/hq/.eslintrc.json b/src/angular/hq/.eslintrc.json index 9a597f81..22ff546a 100644 --- a/src/angular/hq/.eslintrc.json +++ b/src/angular/hq/.eslintrc.json @@ -22,7 +22,7 @@ "@angular-eslint/component-selector": [ "error", { - "type": "element", + "type": ["element", "attribute"], "prefix": "hq", "style": "kebab-case" } diff --git a/src/angular/hq/src/app/common/functions/round-to-next-quarter.ts b/src/angular/hq/src/app/common/functions/round-to-next-quarter.ts new file mode 100644 index 00000000..334ef77f --- /dev/null +++ b/src/angular/hq/src/app/common/functions/round-to-next-quarter.ts @@ -0,0 +1,3 @@ +export function roundToNextQuarter(value: number | string) { + return Math.ceil(Number(value) * 4) / 4; +} diff --git a/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts b/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts index 2ea7f384..1c630b6a 100644 --- a/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts +++ b/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts @@ -17,7 +17,6 @@ export interface GetDashboardTimeV1Response { dates: GetDashboardTimeV1TimeForDate[]; chargeCodes: GetDashboardTimeV1ChargeCode[]; clients: GetDashboardTimeV1Client[]; - projects: GetDashboardTimeV1Project[]; } export interface GetDashboardTimeV1TimeForDate { @@ -32,6 +31,7 @@ export interface GetDashboardTimeV1TimeForDateTimes { notes: string | null; task: string | null; chargeCodeId: string | null; + chargeCode: string; clientId: string | null; projectId: string | null; activityId: string | null; @@ -47,10 +47,18 @@ export interface GetDashboardTimeV1ChargeCode { export interface GetDashboardTimeV1Client { id: string; name: string; + projects: GetDashboardTimeV1Project[]; } export interface GetDashboardTimeV1Project { id: string; - clientId: string; + chargeCodeId: string | null; + chargeCode: string | null; + name: string; + activities: GetDashboardTimeV1ProjectActivity[]; +} + +export interface GetDashboardTimeV1ProjectActivity { + id: string; name: string; } diff --git a/src/angular/hq/src/app/psr/psrtime-list/psrtime-list.component.ts b/src/angular/hq/src/app/psr/psrtime-list/psrtime-list.component.ts index 5b8f0d6a..b556ef67 100644 --- a/src/angular/hq/src/app/psr/psrtime-list/psrtime-list.component.ts +++ b/src/angular/hq/src/app/psr/psrtime-list/psrtime-list.component.ts @@ -54,6 +54,7 @@ import { ToastService } from '../../services/toast.service'; import { InRolePipe } from '../../pipes/in-role.pipe'; import { HQRole } from '../../enums/hqrole'; import { OidcSecurityService } from 'angular-auth-oidc-client'; +import { roundToNextQuarter } from '../../common/functions/round-to-next-quarter'; export interface ChargeCodeViewModel { id: string; @@ -548,7 +549,7 @@ export class PSRTimeListComponent implements OnInit { async updateBillableHours(timeId: string, event: Event) { const billableHours = (event.target as HTMLInputElement).value; - const roundedBillableHours = this.roundToNextQuarter(billableHours); + const roundedBillableHours = roundToNextQuarter(billableHours); const psrId = await firstValueFrom(this.psrId$); const time = await firstValueFrom( this.time$.pipe(map((times) => times.find((x) => x.id == timeId))), @@ -639,7 +640,4 @@ export class PSRTimeListComponent implements OnInit { this.sortDirection$.next(SortDirection.Asc); } } - roundToNextQuarter(num: string | number) { - return Math.ceil(Number(num) * 4) / 4; - } } diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html new file mode 100644 index 00000000..92292bf5 --- /dev/null +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html @@ -0,0 +1,99 @@ + + + + + + + + + {{ form.controls.chargeCode.value }} + + +
+ + + + +
+ + +
+ + + + +
+ + + @if (activities$ | async; as activities) { + @if (activities.length > 0) { +
+ + + + +
+ } @else { + + } + } + + + + +
diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts new file mode 100644 index 00000000..bfd6783f --- /dev/null +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts @@ -0,0 +1,190 @@ +import { + Component, + EventEmitter, + HostBinding, + Input, + OnChanges, + OnDestroy, + Output, + SimpleChanges, +} from '@angular/core'; +import { + GetDashboardTimeV1Project, + GetDashboardTimeV1ProjectActivity, + GetDashboardTimeV1TimeForDateTimes, +} from '../../models/staff-dashboard/get-dashboard-time-v1'; +import { StaffDashboardService } from '../staff-dashboard.service'; +import { + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { + Observable, + Subject, + combineLatest, + debounceTime, + distinctUntilChanged, + filter, + map, + pairwise, + shareReplay, + skip, + startWith, + takeUntil, + tap, +} from 'rxjs'; +import { roundToNextQuarter } from '../../common/functions/round-to-next-quarter'; + +export interface HQTimeChangeEvent { + id?: string | null; + date?: string | null; + hours?: number | null; + notes?: string | null; + task?: string | null; + chargeCode?: string | null; + chargeCodeId?: string | null; + clientId?: string | null; + projectId?: string | null; + activityId?: string | null; +} + +interface Form { + id: FormControl; + date: FormControl; + hours: FormControl; + notes: FormControl; + task: FormControl; + chargeCode: FormControl; + chargeCodeId: FormControl; + clientId: FormControl; + projectId: FormControl; + activityId: FormControl; +} + +@Component({ + selector: 'tr[hq-staff-dashboard-time-entry]', + standalone: true, + imports: [CommonModule, ReactiveFormsModule], + templateUrl: './staff-dashboard-time-entry.component.html', +}) +export class StaffDashboardTimeEntryComponent implements OnChanges, OnDestroy { + @Input() + time?: Partial; + + @Output() + hqTimeChange = new EventEmitter(); + + @HostBinding('class') + class = 'even:bg-gray-850 odd:bg-black-alt'; + + private destroyed$ = new Subject(); + + form = new FormGroup
({ + id: new FormControl(null), + date: new FormControl(null, [Validators.required]), + hours: new FormControl(null, [Validators.required]), + notes: new FormControl(null, [Validators.required]), + task: new FormControl(null), + chargeCode: new FormControl(null), + chargeCodeId: new FormControl(null, [Validators.required]), + clientId: new FormControl(null), + projectId: new FormControl(null), + activityId: new FormControl(null), + }); + + projects$: Observable; + activities$: Observable; + + constructor(public staffDashboardService: StaffDashboardService) { + const form$ = this.form.valueChanges.pipe(shareReplay(1)); + + const clientId$ = form$.pipe( + map((t) => t.clientId), + distinctUntilChanged(), + ); + + const projectId$ = form$.pipe( + map((t) => t.projectId), + distinctUntilChanged(), + ); + + const hours$ = form$.pipe( + map((t) => t.hours), + distinctUntilChanged(), + ); + + const client$ = combineLatest({ + clientId: clientId$, + clients: staffDashboardService.clients$, + }).pipe(map((t) => t.clients.find((x) => x.id == t.clientId))); + + this.projects$ = client$.pipe(map((t) => t?.projects ?? [])); + + const project$ = combineLatest({ + projectId: projectId$, + client: client$, + }).pipe(map((t) => t.client?.projects?.find((x) => x.id == t.projectId))); + + this.activities$ = project$.pipe( + map((t) => t?.activities ?? []), + startWith([]), + ); + + clientId$ + .pipe(pairwise(), takeUntil(this.destroyed$)) + .subscribe(([previousClientId, currentClientId]) => { + if (currentClientId != previousClientId) { + this.form.patchValue({ + chargeCodeId: null, + chargeCode: null, + projectId: null, + }); + } + }); + + project$.pipe(takeUntil(this.destroyed$)).subscribe((project) => { + if (project) { + this.form.patchValue({ + chargeCodeId: project.chargeCodeId, + chargeCode: project.chargeCode, + }); + } + }); + + hours$ + .pipe(debounceTime(500), takeUntil(this.destroyed$)) + .subscribe((hours) => { + if (hours != null) { + this.form.patchValue({ hours: roundToNextQuarter(hours) }); + } + }); + + form$ + .pipe(takeUntil(this.destroyed$), debounceTime(750)) + .subscribe((time) => { + if (this.form.touched) { + this.class = this.form.invalid + ? 'bg-red-850' + : 'even:bg-gray-850 odd:bg-black-alt'; + + if (this.form.valid) { + this.hqTimeChange.emit(time); + } + } + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['time'].currentValue) { + this.form.patchValue(changes['time'].currentValue); + } + } + + ngOnDestroy() { + this.destroyed$.next(); + this.destroyed$.complete(); + } +} diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html index b528137e..b6e5924f 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html @@ -43,26 +43,48 @@
Total Hours: {{ dashboard.totalHours }}
Billable Hours: {{ dashboard.billableHours }}
@for (date of dashboard.dates; track date.date) { -

{{ date.date }}

+
+ {{ date.date | date: "EEEE" }} | {{ date.date }} +
- - - - - - + @for (time of date.times; track time.id) { - - - - - - - - - + }
+ Hrs + Date + Chrg Code + Client + Project + Activity / Task @@ -71,22 +93,17 @@

{{ date.date }}

{{ time.hours }}{{ time.date }} - {{ time.chargeCodeId }} - {{ time.clientId }} - {{ time.projectId }} - - {{ time.activityId ?? "" + time.task ?? "" }} - {{ time.notes }}
diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts index ac202e23..74ecbe58 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts @@ -3,11 +3,19 @@ import { StaffDashboardService } from './staff-dashboard.service'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { Period } from '../models/times/get-time-v1'; +import { + HQTimeChangeEvent, + StaffDashboardTimeEntryComponent, +} from './staff-dashboard-time-entry/staff-dashboard-time-entry.component'; @Component({ selector: 'hq-staff-dashboard', standalone: true, - imports: [CommonModule, ReactiveFormsModule], + imports: [ + CommonModule, + ReactiveFormsModule, + StaffDashboardTimeEntryComponent, + ], providers: [StaffDashboardService], templateUrl: './staff-dashboard.component.html', }) @@ -15,4 +23,12 @@ export class StaffDashboardComponent { constructor(public staffDashboardService: StaffDashboardService) {} Period = Period; + + updateTime(time: HQTimeChangeEvent) { + console.log(time); + } + + createTime(time: HQTimeChangeEvent) { + console.log(time); + } } diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts index 457388fe..f129b3c3 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts @@ -8,11 +8,16 @@ import { combineLatest, debounceTime, map, + shareReplay, startWith, switchMap, tap, } from 'rxjs'; -import { GetDashboardTimeV1Response } from '../models/staff-dashboard/get-dashboard-time-v1'; +import { + GetDashboardTimeV1ChargeCode, + GetDashboardTimeV1Client, + GetDashboardTimeV1Response, +} from '../models/staff-dashboard/get-dashboard-time-v1'; @Injectable({ providedIn: 'root', @@ -20,11 +25,16 @@ import { GetDashboardTimeV1Response } from '../models/staff-dashboard/get-dashbo export class StaffDashboardService { search = new FormControl(null); period = new FormControl(Period.Week, { nonNullable: true }); - date = new FormControl(new Date().toISOString().split('T')[0], { - nonNullable: true, - }); + date = new FormControl( + /*new Date().toISOString().split('T')[0]*/ '2024-06-21', + { + nonNullable: true, + }, + ); time$: Observable; + chargeCodes$: Observable; + clients$: Observable; constructor( private hqService: HQService, @@ -52,6 +62,10 @@ export class StaffDashboardService { tap((response) => this.date.setValue(response.startDate, { emitEvent: false }), ), + shareReplay(1), ); + + this.chargeCodes$ = this.time$.pipe(map((t) => t.chargeCodes)); + this.clients$ = this.time$.pipe(map((t) => t.clients)); } } diff --git a/src/angular/hq/src/app/times/time-edit/time-edit.component.ts b/src/angular/hq/src/app/times/time-edit/time-edit.component.ts index bb3f02f9..f9e87e64 100644 --- a/src/angular/hq/src/app/times/time-edit/time-edit.component.ts +++ b/src/angular/hq/src/app/times/time-edit/time-edit.component.ts @@ -29,6 +29,7 @@ import { GetTimeRecordProjectsV1, } from '../../models/times/get-time-v1'; import { OidcSecurityService } from 'angular-auth-oidc-client'; +import { roundToNextQuarter } from '../../common/functions/round-to-next-quarter'; interface Form { ProjectId: FormControl; @@ -160,7 +161,7 @@ export class TimeEditComponent implements OnInit { } updateHours(event: Event) { const hours = (event.target as HTMLInputElement).value; - const roundedHours = this.roundToNextQuarter(hours); + const roundedHours = roundToNextQuarter(hours); this.form.get('Hours')?.setValue(roundedHours); } private async getTime() { @@ -186,7 +187,4 @@ export class TimeEditComponent implements OnInit { } } } - roundToNextQuarter(num: string | number) { - return Math.ceil(Number(num) * 4) / 4; - } } diff --git a/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs b/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs index 750954bb..f5e0b0da 100644 --- a/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs +++ b/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs @@ -23,7 +23,6 @@ public class Response public List Dates { get; set; } = new(); public List ChargeCodes { get; set; } = new(); public List Clients { get; set; } = new(); - public List Projects { get; set; } = new(); } public class ChargeCode @@ -38,14 +37,16 @@ public class Client { public Guid Id { get; set; } public string Name { get; set; } = null!; + public List Projects { get; set; } = null!; } public class Project { public Guid Id { get; set; } - public Guid ClientId { get; set; } public string Name { get; set; } = null!; public List Activities { get; set; } = new(); + public Guid? ChargeCodeId { get; set; } + public string? ChargeCode { get; set; } } public class Activities @@ -71,5 +72,6 @@ public class TimeEntry public Guid? ClientId { get; set; } public Guid? ProjectId { get; set; } public Guid? ActivityId { get; set; } + public string ChargeCode { get; set; } = null!; } } \ No newline at end of file diff --git a/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs b/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs index aafc688f..58779ea2 100644 --- a/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs +++ b/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs @@ -479,6 +479,7 @@ public TimeEntryServiceV1(HQDbContext context) Id = t.Id, Date = t.Date, ChargeCodeId = t.ChargeCodeId, + ChargeCode = t.ChargeCode.Code, ActivityId = t.ActivityId, Hours = t.Hours, Notes = t.Notes, @@ -505,36 +506,30 @@ public TimeEntryServiceV1(HQDbContext context) var clients = await _context.Clients .AsNoTracking() .OrderBy(t => t.Name) + .Include(t => t.Projects) + .ThenInclude(t => t.Activities) .Select(t => new GetDashboardTimeV1.Client() - { - Id = t.Id, - Name = t.Name - }) - .ToListAsync(ct); - - - var projects = await _context.Projects - .AsNoTracking() - .OrderBy(t => t.Name) - .Include(t => t.Activities) - .Select(t => new GetDashboardTimeV1.Project() { Id = t.Id, Name = t.Name, - ClientId = t.ClientId, - Activities = t.Activities.Select(x => new Abstractions.Times.GetDashboardTimeV1.Activities() + Projects = t.Projects.Select(x => new Abstractions.Times.GetDashboardTimeV1.Project() { Id = x.Id, - Name = x.Name + ChargeCodeId = x.ChargeCode != null ? x.ChargeCode.Id : null, + ChargeCode = x.ChargeCode != null ? x.ChargeCode.Code : null, + Name = x.Name, + Activities = x.Activities.Select(y => new Abstractions.Times.GetDashboardTimeV1.Activities() + { + Id = y.Id, + Name = y.Name + }).ToList() }).ToList() }) .ToListAsync(ct); - var response = new GetDashboardTimeV1.Response(); response.ChargeCodes = chargeCodes; response.Clients = clients; - response.Projects = projects; response.StartDate = startDate; response.EndDate = endDate; response.TotalHours = await timesQuery.SumAsync(t => t.Hours); From 0dce9f36f88c56853abeb433f7ac62eb451d6a62 Mon Sep 17 00:00:00 2001 From: Ryan Maffit Date: Mon, 24 Jun 2024 17:43:25 +0000 Subject: [PATCH 03/25] Switched CLI BillableHours to Hours --- src/dotnet/HQ.Abstractions/Times/UpsertTimeV1.cs | 1 - .../HQ.CLI/Commands/TimeEntries/CreateTimeEntryCommand.cs | 4 ++-- src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/dotnet/HQ.Abstractions/Times/UpsertTimeV1.cs b/src/dotnet/HQ.Abstractions/Times/UpsertTimeV1.cs index 1c5b5f4d..ea5a7fab 100644 --- a/src/dotnet/HQ.Abstractions/Times/UpsertTimeV1.cs +++ b/src/dotnet/HQ.Abstractions/Times/UpsertTimeV1.cs @@ -11,7 +11,6 @@ public class Request { public Guid? Id { get; set; } public DateOnly Date { get; set; } - public decimal? BillableHours { get; set; } public decimal? Hours { get; set; } public string? Task { get; set; } public Guid? ActivityId { get; set; } diff --git a/src/dotnet/HQ.CLI/Commands/TimeEntries/CreateTimeEntryCommand.cs b/src/dotnet/HQ.CLI/Commands/TimeEntries/CreateTimeEntryCommand.cs index 2fe76e91..7e57751f 100644 --- a/src/dotnet/HQ.CLI/Commands/TimeEntries/CreateTimeEntryCommand.cs +++ b/src/dotnet/HQ.CLI/Commands/TimeEntries/CreateTimeEntryCommand.cs @@ -16,7 +16,7 @@ internal class CreateTimeEntrySettings : HQCommandSettings public required string ChargeCode { get; set; } [CommandArgument(1, "")] - public decimal BillableHours { get; set; } + public decimal Hours { get; set; } [CommandArgument(2, "")] public required string Notes { get; set; } @@ -50,7 +50,7 @@ public override async Task ExecuteAsync(CommandContext context, CreateTimeE Notes = settings.Notes, Task = settings.Task, Date = settings.Date ?? DateOnly.FromDateTime(DateTime.Now), - BillableHours = settings.BillableHours + Hours = settings.Hours }; var result = await _hqService.UpsertTimeEntryV1(timeEntryRequest); diff --git a/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs b/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs index 58779ea2..21254ee4 100644 --- a/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs +++ b/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs @@ -31,7 +31,7 @@ public TimeEntryServiceV1(HQDbContext context) var validationResult = Result.Merge( Result.FailIf(string.IsNullOrEmpty(request.Notes), "Notes are required."), Result.FailIf(!request.Id.HasValue && request.StaffId == null, "Staff is required."), - Result.FailIf(request.BillableHours <= 0, "Billable Hours must be greater than 0.") + Result.FailIf(request.Hours <= 0, "Hours must be greater than 0.") ); From 83262951fcc703c0c571c8657bf4a0bf2d63301f Mon Sep 17 00:00:00 2001 From: Ryan Maffit Date: Mon, 24 Jun 2024 19:06:10 +0000 Subject: [PATCH 04/25] Integrated create/update/delete events with API --- .../common/functions/charge-code-to-color.ts | 32 ++++++++ .../hq/src/app/models/times/delete-time-v1.ts | 5 ++ .../hq/src/app/models/times/update-time-v1.ts | 10 ++- src/angular/hq/src/app/services/hq.service.ts | 21 +++++ .../staff-dashboard-time-entry.component.html | 41 +++++++--- .../staff-dashboard-time-entry.component.ts | 26 +++++++ .../staff-dashboard.component.html | 11 ++- .../staff-dashboard.component.ts | 76 +++++++++++++++++-- .../staff-dashboard.service.ts | 17 ++++- .../Times/GetDashboardTimeV1.cs | 1 + .../HQ.Server/Services/TimeEntryServiceV1.cs | 5 +- 11 files changed, 219 insertions(+), 26 deletions(-) create mode 100644 src/angular/hq/src/app/common/functions/charge-code-to-color.ts create mode 100644 src/angular/hq/src/app/models/times/delete-time-v1.ts diff --git a/src/angular/hq/src/app/common/functions/charge-code-to-color.ts b/src/angular/hq/src/app/common/functions/charge-code-to-color.ts new file mode 100644 index 00000000..30cb25a8 --- /dev/null +++ b/src/angular/hq/src/app/common/functions/charge-code-to-color.ts @@ -0,0 +1,32 @@ +const colors = [ + '#7FB5B5', + '#646B63', + '#317F43', + '#E7EBDA', + '#A5A5A5', + '#82898F', + '#E4A010', + '#E1CC4F', +]; + +function hexToRgb(hex: string) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } + : null; +} + +export function chargeCodeToColor(code: string, opacity = 0.4) { + const value = code.charCodeAt(0) * 10000 + parseInt(code.substring(1)); + const hexColor = colors[value % colors.length]; + const rgb = hexToRgb(hexColor); + if (!rgb) { + return colors[0]; + } + + return `rgba(${rgb.r},${rgb.g},${rgb.b},${opacity})`; +} diff --git a/src/angular/hq/src/app/models/times/delete-time-v1.ts b/src/angular/hq/src/app/models/times/delete-time-v1.ts new file mode 100644 index 00000000..e9b34a0c --- /dev/null +++ b/src/angular/hq/src/app/models/times/delete-time-v1.ts @@ -0,0 +1,5 @@ +export interface DeleteTimeV1Request { + id: string; +} + +export interface DeleteTimeV1Response {} diff --git a/src/angular/hq/src/app/models/times/update-time-v1.ts b/src/angular/hq/src/app/models/times/update-time-v1.ts index 49fa0f3f..4f5860ed 100644 --- a/src/angular/hq/src/app/models/times/update-time-v1.ts +++ b/src/angular/hq/src/app/models/times/update-time-v1.ts @@ -1,12 +1,14 @@ export interface updateTimeRequestV1 { - id: string; + id?: string | null; + date: string; activityId?: string | null; task?: string | null; - notes: string; - hours: number; + notes?: string | null; + chargeCodeId?: string | null; + hours?: number | null; chargeCode: string; } export interface UpdateTimeResponseV1 { - message: string; + id: string; } diff --git a/src/angular/hq/src/app/services/hq.service.ts b/src/angular/hq/src/app/services/hq.service.ts index 55d9742a..be31b70e 100644 --- a/src/angular/hq/src/app/services/hq.service.ts +++ b/src/angular/hq/src/app/services/hq.service.ts @@ -114,6 +114,10 @@ import { GetDashboardTimeV1Request, GetDashboardTimeV1Response, } from '../models/staff-dashboard/get-dashboard-time-v1'; +import { + DeleteTimeV1Request, + DeleteTimeV1Response, +} from '../models/times/delete-time-v1'; @Injectable({ providedIn: 'root', @@ -442,6 +446,23 @@ export class HQService { }), ); } + deleteTimeV1(request: Partial) { + return this.appSettings.apiUrl$.pipe( + switchMap((apiUrl) => + this.http.post( + `${apiUrl}/v1/TimeEntries/DeleteTimeV1`, + request, + ), + ), + catchError((err: HttpErrorResponse) => { + if (err.status == 400) { + return throwError(() => new APIError(err.error)); + } + + throw err; + }), + ); + } getClientInvoiceSummaryV1( request: Partial, diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html index 92292bf5..88a229b1 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html @@ -1,25 +1,35 @@ - - {{ form.controls.chargeCode.value }} + + @if (form.controls.chargeCode.value) { + + {{ form.controls.chargeCode.value }} + + }
@@ -66,7 +76,7 @@ @if (activities.length > 0) {
} } - + + + @if (form.controls.id.value) { + + } + diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts index bfd6783f..39931868 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts @@ -37,6 +37,7 @@ import { tap, } from 'rxjs'; import { roundToNextQuarter } from '../../common/functions/round-to-next-quarter'; +import { chargeCodeToColor } from '../../common/functions/charge-code-to-color'; export interface HQTimeChangeEvent { id?: string | null; @@ -51,6 +52,10 @@ export interface HQTimeChangeEvent { activityId?: string | null; } +export interface HQTimeDeleteEvent { + id: string; +} + interface Form { id: FormControl; date: FormControl; @@ -77,11 +82,16 @@ export class StaffDashboardTimeEntryComponent implements OnChanges, OnDestroy { @Output() hqTimeChange = new EventEmitter(); + @Output() + hqTimeDelete = new EventEmitter(); + @HostBinding('class') class = 'even:bg-gray-850 odd:bg-black-alt'; private destroyed$ = new Subject(); + chargeCodeToColor = chargeCodeToColor; + form = new FormGroup({ id: new FormControl(null), date: new FormControl(null, [Validators.required]), @@ -175,6 +185,15 @@ export class StaffDashboardTimeEntryComponent implements OnChanges, OnDestroy { } } }); + + this.staffDashboardService.refresh$.subscribe(() => { + if (!this.time?.id) { + this.form.reset(); + this.form.patchValue({ + date: this.time?.date, + }); + } + }); } ngOnChanges(changes: SimpleChanges) { @@ -187,4 +206,11 @@ export class StaffDashboardTimeEntryComponent implements OnChanges, OnDestroy { this.destroyed$.next(); this.destroyed$.complete(); } + + deleteTime() { + const id = this.form.controls.id.value; + if (id) { + this.hqTimeDelete.emit({ id }); + } + } } diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html index b6e5924f..19ffef12 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html @@ -83,25 +83,30 @@ Activity / Task Description + @for (time of date.times; track time.id) { } diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts index 74ecbe58..0a54db89 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts @@ -5,8 +5,15 @@ import { ReactiveFormsModule } from '@angular/forms'; import { Period } from '../models/times/get-time-v1'; import { HQTimeChangeEvent, + HQTimeDeleteEvent, StaffDashboardTimeEntryComponent, } from './staff-dashboard-time-entry/staff-dashboard-time-entry.component'; +import { updateTimeRequestV1 } from '../models/times/update-time-v1'; +import { firstValueFrom } from 'rxjs'; +import { HQService } from '../services/hq.service'; +import { APIError } from '../errors/apierror'; +import { ToastService } from '../services/toast.service'; +import { ModalService } from '../services/modal.service'; @Component({ selector: 'hq-staff-dashboard', @@ -20,15 +27,74 @@ import { templateUrl: './staff-dashboard.component.html', }) export class StaffDashboardComponent { - constructor(public staffDashboardService: StaffDashboardService) {} + constructor( + public staffDashboardService: StaffDashboardService, + private hqService: HQService, + private toastService: ToastService, + private modalService: ModalService, + ) {} Period = Period; - updateTime(time: HQTimeChangeEvent) { - console.log(time); + async deleteTime(event: HQTimeDeleteEvent) { + const request = { + id: event.id, + }; + + const confirm = await firstValueFrom( + this.modalService.confirm( + 'Delete', + 'Are you sure you want to delete this time entry?', + ), + ); + + if (!confirm) { + return; + } + + try { + await firstValueFrom(this.hqService.deleteTimeV1(request)); + this.toastService.show('Success', 'Time entry successfully deleted.'); + this.staffDashboardService.refresh(); + } catch (err) { + if (err instanceof APIError) { + this.toastService.show('Error', err.errors.join('\n')); + } else { + this.toastService.show('Error', 'An unexpected error has occurred.'); + } + } } - createTime(time: HQTimeChangeEvent) { - console.log(time); + async upsertTime(event: HQTimeChangeEvent) { + if (!event.date) { + return; + } + + const request: Partial = { + id: event.id, + hours: event.hours, + chargeCodeId: event.chargeCodeId, + task: event.task, + activityId: event.activityId, + notes: event.notes, + date: event.date, + }; + + try { + await firstValueFrom(this.hqService.upsertTimeV1(request)); + if (event.id) { + this.toastService.show('Success', 'Time entry successfully updated.'); + } else { + this.toastService.show('Success', 'Time entry successfully created.'); + this.staffDashboardService.search.reset(); + this.staffDashboardService.refresh(); + } + } catch (err) { + if (err instanceof APIError) { + this.toastService.show('Error', err.errors.join('\n')); + } else { + this.toastService.show('Error', 'An unexpected error has occurred.'); + } + } } } diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts index f129b3c3..b7f98157 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts @@ -5,9 +5,11 @@ import { HQService } from '../services/hq.service'; import { OidcSecurityService } from 'angular-auth-oidc-client'; import { Observable, + Subject, combineLatest, debounceTime, map, + merge, shareReplay, startWith, switchMap, @@ -36,6 +38,8 @@ export class StaffDashboardService { chargeCodes$: Observable; clients$: Observable; + refresh$ = new Subject(); + constructor( private hqService: HQService, private oidcSecurityService: OidcSecurityService, @@ -56,16 +60,25 @@ export class StaffDashboardService { date: date$, }); - this.time$ = request$.pipe( - debounceTime(250), + const time$ = request$.pipe( switchMap((request) => this.hqService.getDashboardTimeV1(request)), tap((response) => this.date.setValue(response.startDate, { emitEvent: false }), ), + ); + + const refreshTime$ = this.refresh$.pipe(switchMap(() => time$)); + + this.time$ = merge(time$, refreshTime$).pipe( + debounceTime(250), shareReplay(1), ); this.chargeCodes$ = this.time$.pipe(map((t) => t.chargeCodes)); this.clients$ = this.time$.pipe(map((t) => t.clients)); } + + refresh() { + this.refresh$.next(); + } } diff --git a/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs b/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs index f5e0b0da..daee4f85 100644 --- a/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs +++ b/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs @@ -64,6 +64,7 @@ public class TimeForDate public class TimeEntry { public Guid Id { get; set; } + public DateTime CreatedAt { get; set; } public DateOnly Date { get; set; } public decimal Hours { get; set; } public string? Notes { get; set; } diff --git a/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs b/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs index 21254ee4..1b3cc5ba 100644 --- a/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs +++ b/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs @@ -473,10 +473,11 @@ public TimeEntryServiceV1(HQDbContext context) ); } - var times = await timesQuery.OrderByDescending(t => t.CreatedAt) + var times = await timesQuery .Select(t => new GetDashboardTimeV1.TimeEntry() { Id = t.Id, + CreatedAt = t.CreatedAt, Date = t.Date, ChargeCodeId = t.ChargeCodeId, ChargeCode = t.ChargeCode.Code, @@ -488,7 +489,7 @@ public TimeEntryServiceV1(HQDbContext context) ClientId = t.ChargeCode.Project != null ? t.ChargeCode.Project.ClientId : null }) .GroupBy(t => t.Date) - .ToDictionaryAsync(t => t.Key, t => t.ToList()); + .ToDictionaryAsync(t => t.Key, t => t.OrderByDescending(x => x.CreatedAt).ToList()); var chargeCodes = await _context.ChargeCodes .AsNoTracking() From 0364eb13359f0796e49a45f20a322c8fce5843a8 Mon Sep 17 00:00:00 2001 From: amahdysancsoft Date: Mon, 24 Jun 2024 16:15:31 -0400 Subject: [PATCH 05/25] Created a separate search filter component for staff dashboard --- .../{ => service}/staff-dashboard.service.ts | 7 +- ...aff-dashboard-search-filter.component.html | 99 +++++++++++++++++++ ...staff-dashboard-search-filter.component.ts | 15 +++ .../staff-dashboard-time-entry.component.ts | 2 +- .../staff-dashboard.component.html | 20 ++-- .../staff-dashboard.component.ts | 4 +- 6 files changed, 131 insertions(+), 16 deletions(-) rename src/angular/hq/src/app/staff-dashboard/{ => service}/staff-dashboard.service.ts (90%) create mode 100644 src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.html create mode 100644 src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.ts diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts b/src/angular/hq/src/app/staff-dashboard/service/staff-dashboard.service.ts similarity index 90% rename from src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts rename to src/angular/hq/src/app/staff-dashboard/service/staff-dashboard.service.ts index b7f98157..b3cba015 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.service.ts +++ b/src/angular/hq/src/app/staff-dashboard/service/staff-dashboard.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { FormControl } from '@angular/forms'; -import { Period } from '../projects/project-create/project-create.component'; -import { HQService } from '../services/hq.service'; +import { Period } from '../../projects/project-create/project-create.component'; +import { HQService } from '../../services/hq.service'; import { OidcSecurityService } from 'angular-auth-oidc-client'; import { Observable, @@ -19,7 +19,7 @@ import { GetDashboardTimeV1ChargeCode, GetDashboardTimeV1Client, GetDashboardTimeV1Response, -} from '../models/staff-dashboard/get-dashboard-time-v1'; +} from '../../models/staff-dashboard/get-dashboard-time-v1'; @Injectable({ providedIn: 'root', @@ -34,6 +34,7 @@ export class StaffDashboardService { }, ); + Period = Period; time$: Observable; chargeCodes$: Observable; clients$: Observable; diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.html new file mode 100644 index 00000000..933765d4 --- /dev/null +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.html @@ -0,0 +1,99 @@ +
+
+ + +
+ + + + +
+
+
+ +
+ + + + +
+
+ +
+ +
+
+ + + +
+ + + + +
+
+ +
-
+
+ + + +
+ + + + +
+
+
+
+
diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.ts b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.ts new file mode 100644 index 00000000..08479bdc --- /dev/null +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.ts @@ -0,0 +1,15 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input, input } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { StaffDashboardService } from '../service/staff-dashboard.service'; + +@Component({ + selector: 'hq-staff-dashboard-search-filter', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, FormsModule], + templateUrl: './staff-dashboard-search-filter.component.html', +}) +export class StaffDashboardSearchFilterComponent { + @Input() endDate?: string; + constructor(public staffDashboardService: StaffDashboardService) {} +} diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts index 39931868..941d3ec0 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts @@ -13,7 +13,7 @@ import { GetDashboardTimeV1ProjectActivity, GetDashboardTimeV1TimeForDateTimes, } from '../../models/staff-dashboard/get-dashboard-time-v1'; -import { StaffDashboardService } from '../staff-dashboard.service'; +import { StaffDashboardService } from '../service/staff-dashboard.service'; import { FormControl, FormGroup, diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html index 19ffef12..ffbcf86b 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html @@ -1,14 +1,12 @@ - - +
+
+
+@if (staffDashboardService.time$ | async; as dashboard) { + +} +
+
+
} - @if (date.times.length > 0) { - - - - - - - - - - - - - - - - @for (time of date.times; track time.id) { - @switch (time.timeStatus) { - @case (timeStatus.Submitted) { - - } - @default { - - } +
- Hrs - - Date - - Chrg Code - - Client - - Project - - Activity / Task - - Description -
+ + + + + + + + + + + + + + + @for (time of date.times; track time.id) { + @switch (time.timeStatus) { + @case (timeStatus.Submitted) { + + } + @default { + } } - -
+ Hrs + + Date + + Chrg Code + + Client + + Project + + Activity / Task + + Description +
- } @else { -
-

No records found for this date.

- -
- } + } + + } } From dee9691289e277aaa7d13313a001d52b9e01f0b7 Mon Sep 17 00:00:00 2001 From: amrmahdyy Date: Tue, 25 Jun 2024 16:53:16 -0400 Subject: [PATCH 12/25] applied formatting --- .../staff-dashboard-date-range.component.ts | 6 ++---- .../staff-dashboard-search-filter.component.html | 1 - .../src/app/staff-dashboard/staff-dashboard.component.html | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.ts b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.ts index a8450e60..d81a0d8f 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.ts +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.ts @@ -7,10 +7,8 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; selector: 'hq-staff-dashboard-date-range', standalone: true, imports: [CommonModule, ReactiveFormsModule, FormsModule], - templateUrl: './staff-dashboard-date-range.component.html' + templateUrl: './staff-dashboard-date-range.component.html', }) export class StaffDashboardDateRangeComponent { -constructor(public staffDashboardService: StaffDashboardService) { - -} + constructor(public staffDashboardService: StaffDashboardService) {} } diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.html index 483f5c03..4fa0cb84 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.html @@ -44,4 +44,3 @@
- diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html index 0b799a54..2ed8bd77 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html @@ -95,7 +95,7 @@ @for (time of date.times; track time.id) { @switch (time.timeStatus) { From e23f4fa335e969dddbc46a70c561516ad8cb568a Mon Sep 17 00:00:00 2001 From: amrmahdyy Date: Tue, 25 Jun 2024 17:02:42 -0400 Subject: [PATCH 13/25] Fix angular linter errors --- .../staff-dashboard-date-range.component.html | 2 +- .../app/staff-dashboard/staff-dashboard.component.html | 8 ++++++-- .../src/app/staff-dashboard/staff-dashboard.component.ts | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html index a6a8560c..e4b1d2cb 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html @@ -31,7 +31,7 @@ @if ( - staffDashboardService.period.value != + staffDashboardService.period.value !== staffDashboardService.Period.Today ) {
diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html index 2ed8bd77..b85ea37b 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html @@ -3,14 +3,18 @@
- +
{{ (staffDashboardService.time$ | async)?.totalHours?.toFixed(2) }}
- +
{{ (staffDashboardService.time$ | async)?.billableHours?.toFixed(2) }}
diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts index 1e3792a1..358ad8a5 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts @@ -105,12 +105,12 @@ export class StaffDashboardComponent { } async submitTimes() { try { - var timesIds = await firstValueFrom( + const timesIds = await firstValueFrom( this.staffDashboardService.time$.pipe( map((t) => t.dates.flatMap((d) => d.times.map((time) => time.id))), ), ); - let submitTimesRequest = { ids: timesIds }; + const submitTimesRequest = { ids: timesIds }; await firstValueFrom(this.hqService.submitTimesV1(submitTimesRequest)); this.toastService.show('Success', 'Time entries successfully submitted.'); this.staffDashboardService.refresh(); From b67facdbc1c242d5c42329b7cd5bb8489c4b6bf6 Mon Sep 17 00:00:00 2001 From: Ryan Maffit Date: Tue, 25 Jun 2024 21:38:19 +0000 Subject: [PATCH 14/25] Fixed width on hours column --- .../staff-dashboard-time-entry.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html index 1786c8e0..0ac781c9 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html @@ -2,9 +2,9 @@ @switch (time?.timeStatus) { @case (timeStatus.Pending) { - + - + {{ time?.hours }} From 84135d4a2ebd8ad6a006526c71b68fdad10516a0 Mon Sep 17 00:00:00 2001 From: amrmahdyy Date: Tue, 25 Jun 2024 19:05:14 -0400 Subject: [PATCH 15/25] Added Staff work hours calculations and worked on fixing after the feedback --- .../src/app/models/times/submit-times-v1.ts | 1 + .../service/staff-dashboard.service.ts | 9 ++--- .../staff-dashboard-date-range.component.html | 4 +-- .../staff-dashboard-time-entry.component.html | 9 +++-- .../staff-dashboard.component.html | 4 +-- .../staff-dashboard.component.ts | 33 ++++++++++++++++--- .../Times/GetDashboardTimeV1.cs | 5 +++ .../HQ.Abstractions/Times/SubmitTimes.cs | 14 ++++++++ .../HQ.Abstractions/Times/UpsertTimeV1.cs | 12 ------- .../TimeEntryAuthorizationHandler.cs | 2 ++ .../Authorization/TimeEntryOperation.cs | 2 ++ .../Controllers/TimeEntriesControllerV1.cs | 13 ++++++++ .../HQ.Server/Services/TimeEntryServiceV1.cs | 17 ++++++++++ 13 files changed, 96 insertions(+), 29 deletions(-) create mode 100644 src/dotnet/HQ.Abstractions/Times/SubmitTimes.cs diff --git a/src/angular/hq/src/app/models/times/submit-times-v1.ts b/src/angular/hq/src/app/models/times/submit-times-v1.ts index 480340bd..cee2e769 100644 --- a/src/angular/hq/src/app/models/times/submit-times-v1.ts +++ b/src/angular/hq/src/app/models/times/submit-times-v1.ts @@ -1,5 +1,6 @@ export interface SubmitTimesRequestV1 { ids: string[] | null; + staffId: string; } export interface SubmitTimeResponseV1 {} diff --git a/src/angular/hq/src/app/staff-dashboard/service/staff-dashboard.service.ts b/src/angular/hq/src/app/staff-dashboard/service/staff-dashboard.service.ts index 3b242244..2a3de1cd 100644 --- a/src/angular/hq/src/app/staff-dashboard/service/staff-dashboard.service.ts +++ b/src/angular/hq/src/app/staff-dashboard/service/staff-dashboard.service.ts @@ -61,7 +61,8 @@ export class StaffDashboardService { date: date$, }); - const time$ = request$.pipe( + this.time$ = request$.pipe( + debounceTime(250), switchMap((request) => this.hqService.getDashboardTimeV1(request)), tap((response) => this.date.setValue(response.startDate, { emitEvent: false }), @@ -69,15 +70,9 @@ export class StaffDashboardService { shareReplay(1), ); - this.time$ = merge(time$, this.refresh$.pipe(switchMap(() => time$))).pipe( - debounceTime(250), - shareReplay(1), - ); - this.chargeCodes$ = this.time$.pipe(map((t) => t.chargeCodes)); this.clients$ = this.time$.pipe(map((t) => t.clients)); } - refresh() { this.refresh$.next(); } diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html index e4b1d2cb..5ee4c554 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html @@ -21,7 +21,7 @@ [formControl]="staffDashboardService.date" type="date" id="startDate" - class="w-full block text-[14px] px-2 py-1 border row-start-1 col-start-1 self-center justify-self-end border-black appearance-none focus:outline-none placeholder:text-gray-100 bg-blue-900 rounded h-[36px] uppercase" + class="w-full block text-[14px] px-4 py-1 border row-start-1 col-start-1 self-center justify-self-end border-black appearance-none focus:outline-none placeholder:text-gray-100 bg-blue-900 rounded h-[36px] uppercase" /> - + {{ time?.chargeCode }} @@ -145,7 +148,9 @@ {{ time?.activityName || time?.task }} - {{ time?.notes }} + @if (time?.notes; as notes) { + {{ notes.length > 70 ? notes.substring(0, 70) + "..." : notes }} + } } diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html index b85ea37b..ef6d5919 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html @@ -6,7 +6,7 @@ -
+
{{ (staffDashboardService.time$ | async)?.totalHours?.toFixed(2) }}
@@ -15,7 +15,7 @@ -
+
{{ (staffDashboardService.time$ | async)?.billableHours?.toFixed(2) }}
diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts index 358ad8a5..345aa629 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.ts @@ -17,6 +17,7 @@ import { ModalService } from '../services/modal.service'; import { StaffDashboardSearchFilterComponent } from './staff-dashboard-search-filter/staff-dashboard-search-filter.component'; import { StaffDashboardDateRangeComponent } from './staff-dashboard-date-range/staff-dashboard-date-range.component'; import { TimeStatus } from '../models/common/time-status'; +import { OidcSecurityService } from 'angular-auth-oidc-client'; @Component({ selector: 'hq-staff-dashboard', @@ -37,6 +38,7 @@ export class StaffDashboardComponent { private hqService: HQService, private toastService: ToastService, private modalService: ModalService, + private oidcSecurityService: OidcSecurityService, ) {} Period = Period; @@ -104,16 +106,39 @@ export class StaffDashboardComponent { } } async submitTimes() { + const confirm = await firstValueFrom( + this.modalService.confirm( + 'Submit', + 'Are you sure you want to submit time entries?', + ), + ); + + if (!confirm) { + return; + } try { const timesIds = await firstValueFrom( this.staffDashboardService.time$.pipe( map((t) => t.dates.flatMap((d) => d.times.map((time) => time.id))), ), ); - const submitTimesRequest = { ids: timesIds }; - await firstValueFrom(this.hqService.submitTimesV1(submitTimesRequest)); - this.toastService.show('Success', 'Time entries successfully submitted.'); - this.staffDashboardService.refresh(); + const staffId = await firstValueFrom( + this.oidcSecurityService.userData$.pipe( + map((t) => t.userData?.staff_id), + ), + ); + if (staffId) { + const submitTimesRequest = { ids: timesIds, staffId: staffId }; + await firstValueFrom(this.hqService.submitTimesV1(submitTimesRequest)); + this.toastService.show( + 'Success', + 'Time entries successfully submitted.', + ); + this.staffDashboardService.search.reset(); + this.staffDashboardService.refresh(); + } else { + console.log('ERROR: Could not find staff'); + } } catch (err) { if (err instanceof APIError) { this.toastService.show('Error', err.errors.join('\n')); diff --git a/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs b/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs index 282528c4..8a9ee050 100644 --- a/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs +++ b/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs @@ -23,6 +23,11 @@ public class Response public List Dates { get; set; } = new(); public List ChargeCodes { get; set; } = new(); public List Clients { get; set; } = new(); + public decimal HoursThisWeek { get; set; } + public decimal HoursThisMonth { get; set; } + public decimal HoursLastWeek { get; set; } + public int Vacation { get; set; } + } public class ChargeCode diff --git a/src/dotnet/HQ.Abstractions/Times/SubmitTimes.cs b/src/dotnet/HQ.Abstractions/Times/SubmitTimes.cs new file mode 100644 index 00000000..79e7bd6a --- /dev/null +++ b/src/dotnet/HQ.Abstractions/Times/SubmitTimes.cs @@ -0,0 +1,14 @@ +public class SubmitTimesV1 +{ + public class Request + { + public required List Ids { get; set; } + public Guid? StaffId { get; set; } + + } + + public class Response + { + + } +} \ No newline at end of file diff --git a/src/dotnet/HQ.Abstractions/Times/UpsertTimeV1.cs b/src/dotnet/HQ.Abstractions/Times/UpsertTimeV1.cs index c0519a86..95dd2f75 100644 --- a/src/dotnet/HQ.Abstractions/Times/UpsertTimeV1.cs +++ b/src/dotnet/HQ.Abstractions/Times/UpsertTimeV1.cs @@ -120,18 +120,6 @@ public class Response } } - public class SubmitTimesV1 - { - public class Request - { - public required List Ids { get; set; } - - } - public class Response - { - - } - } } \ No newline at end of file diff --git a/src/dotnet/HQ.Server/Authorization/TimeEntryAuthorizationHandler.cs b/src/dotnet/HQ.Server/Authorization/TimeEntryAuthorizationHandler.cs index c3180fe7..1320fa13 100644 --- a/src/dotnet/HQ.Server/Authorization/TimeEntryAuthorizationHandler.cs +++ b/src/dotnet/HQ.Server/Authorization/TimeEntryAuthorizationHandler.cs @@ -32,6 +32,8 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte case nameof(TimeEntryOperation.GetTimes): case nameof(TimeEntryOperation.DeleteTime): case nameof(TimeEntryOperation.UpsertTime): + case nameof(TimeEntryOperation.SubmitTimes): + { if ((isStaff && staffId.HasValue && staffId.Value == resource.StaffId) || isPartner || isExecutive || isAdmin) { diff --git a/src/dotnet/HQ.Server/Authorization/TimeEntryOperation.cs b/src/dotnet/HQ.Server/Authorization/TimeEntryOperation.cs index 50f5173e..8fd0592f 100644 --- a/src/dotnet/HQ.Server/Authorization/TimeEntryOperation.cs +++ b/src/dotnet/HQ.Server/Authorization/TimeEntryOperation.cs @@ -7,5 +7,7 @@ public class TimeEntryOperation public static OperationAuthorizationRequirement UpsertTime = new OperationAuthorizationRequirement { Name = nameof(UpsertTime) }; public static OperationAuthorizationRequirement GetTimes = new OperationAuthorizationRequirement { Name = nameof(GetTimes) }; public static OperationAuthorizationRequirement DeleteTime = new OperationAuthorizationRequirement { Name = nameof(DeleteTime) }; + public static OperationAuthorizationRequirement SubmitTimes = new OperationAuthorizationRequirement { Name = nameof(SubmitTimes) }; + } \ No newline at end of file diff --git a/src/dotnet/HQ.Server/Controllers/TimeEntriesControllerV1.cs b/src/dotnet/HQ.Server/Controllers/TimeEntriesControllerV1.cs index 715cb5d0..f4aef2fa 100644 --- a/src/dotnet/HQ.Server/Controllers/TimeEntriesControllerV1.cs +++ b/src/dotnet/HQ.Server/Controllers/TimeEntriesControllerV1.cs @@ -224,6 +224,19 @@ public async Task GetDashboardTimeV1([FromBody] GetDashboardTimeV1 [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task SubmitTimesV1([FromBody] SubmitTimesV1.Request request, CancellationToken ct = default) { + var times = _context.Times.Where(t => request.Ids.Contains(t.Id)); + if (times == null) + { + return NotFound(); + } + + var authorizationResult = await _authorizationService + .AuthorizeAsync(User, times.FirstOrDefault(), TimeEntryOperation.SubmitTimes); + + if (!authorizationResult.Succeeded) + { + return Forbid(); + } return await _TimeEntryServiceV1.SubmitTimesV1(request, ct) .ToActionResult(new HQResultEndpointProfile()); } diff --git a/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs b/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs index c7d4c034..af1f2a5c 100644 --- a/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs +++ b/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs @@ -477,6 +477,19 @@ public TimeEntryServiceV1(HQDbContext context) .AsNoTracking() .Where(t => t.StaffId == request.StaffId && t.Date >= startDate && t.Date <= endDate) .AsQueryable(); + var hrsThisWeekQuery = _context.Times + .AsNoTracking() + .Where(t => t.StaffId == request.StaffId && t.Date >= request.Date.GetPeriodStartDate(Period.Week) && t.Date <= request.Date.GetPeriodEndDate(Period.Week)) + .AsQueryable(); + var hrsLastWeekQuery = _context.Times + .AsNoTracking() + .Where(t => t.StaffId == request.StaffId && t.Date >= request.Date.GetPeriodStartDate(Period.LastWeek) && t.Date <= request.Date.GetPeriodEndDate(Period.LastWeek)) + .AsQueryable(); + var hrsThisMonthQuery = _context.Times + .AsNoTracking() + .Where(t => t.StaffId == request.StaffId && t.Date >= request.Date.GetPeriodStartDate(Period.Month) && t.Date <= request.Date.GetPeriodEndDate(Period.Month)) + .AsQueryable(); + var vacationHours = (await _context.Staff.FirstOrDefaultAsync(t => t.Id == request.StaffId))?.VacationHours; if (!String.IsNullOrEmpty(request.Search)) { @@ -556,6 +569,10 @@ public TimeEntryServiceV1(HQDbContext context) response.EndDate = endDate; response.TotalHours = await timesQuery.SumAsync(t => t.Hours); response.BillableHours = await timesQuery.Where(t => t.ChargeCode.Billable).SumAsync(t => t.Hours); + response.HoursThisWeek = await hrsThisWeekQuery.SumAsync(t => t.Hours); + response.HoursLastWeek = await hrsLastWeekQuery.SumAsync(t => t.Hours); + response.HoursThisMonth = await hrsThisMonthQuery.SumAsync(t => t.Hours); + response.Vacation = vacationHours ?? 0; response.NextDate = nextDate; response.PreviousDate = previousDate; From cb1a153651347feaa04bdb623775cf801a238017 Mon Sep 17 00:00:00 2001 From: amrmahdyy Date: Tue, 25 Jun 2024 20:37:30 -0400 Subject: [PATCH 16/25] Added staff name and hours computation UI --- .../staff-dashboard/get-dashboard-time-v1.ts | 5 +++ .../staff-dashboard-time-entry.component.html | 2 +- .../staff-dashboard.component.html | 35 +++++++++++++++++++ .../Times/GetDashboardTimeV1.cs | 1 + .../HQ.Server/Services/TimeEntryServiceV1.cs | 1 + 5 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts b/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts index 09c94bea..df8464be 100644 --- a/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts +++ b/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts @@ -11,6 +11,11 @@ export interface GetDashboardTimeV1Request { export interface GetDashboardTimeV1Response { totalHours: number; billableHours: number; + hoursThisWeek: number; + hoursThisMonth: number; + hoursLastWeek: number; + staffName: string; + vacation: number; startDate: string; endDate: string; previousDate: string; diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html index 299177fb..9648d385 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html @@ -124,7 +124,7 @@ } @case (timeStatus.Submitted) { - + {{ time?.hours }} diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html index ef6d5919..6ea3e75e 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html @@ -1,3 +1,38 @@ +
+
+
+ {{ (staffDashboardService.time$ | async)?.staffName }} +
+
+
+

HRS THIS WEEK

+

+ {{ (staffDashboardService.time$ | async)?.hoursThisWeek?.toFixed(2) }} +

+
+
+

HRS LAST WEEK

+

+ {{ (staffDashboardService.time$ | async)?.hoursLastWeek?.toFixed(2) }} +

+
+
+

HRS THIS MONTH

+

+ {{ + (staffDashboardService.time$ | async)?.hoursThisMonth?.toFixed(2) + }} +

+
+
+

VACATION

+

+ {{ (staffDashboardService.time$ | async)?.vacation?.toFixed(2) }} +

+
+
+
+
diff --git a/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs b/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs index 8a9ee050..8884e495 100644 --- a/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs +++ b/src/dotnet/HQ.Abstractions/Times/GetDashboardTimeV1.cs @@ -26,6 +26,7 @@ public class Response public decimal HoursThisWeek { get; set; } public decimal HoursThisMonth { get; set; } public decimal HoursLastWeek { get; set; } + public string? StaffName { get; set; } public int Vacation { get; set; } } diff --git a/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs b/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs index af1f2a5c..51f3430d 100644 --- a/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs +++ b/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs @@ -575,6 +575,7 @@ public TimeEntryServiceV1(HQDbContext context) response.Vacation = vacationHours ?? 0; response.NextDate = nextDate; response.PreviousDate = previousDate; + response.StaffName = (await _context.Staff.FirstOrDefaultAsync(t => t.Id == request.StaffId))?.Name; DateOnly date = endDate; do From 1b0618881c0c3e8c7f5ec57e5f1d4369be039c27 Mon Sep 17 00:00:00 2001 From: amrmahdyy Date: Tue, 25 Jun 2024 21:26:09 -0400 Subject: [PATCH 17/25] Fixed date issue with firefox --- .../staff-dashboard-date-range.component.html | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html index 5ee4c554..76026450 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html @@ -21,11 +21,14 @@ [formControl]="staffDashboardService.date" type="date" id="startDate" - class="w-full block text-[14px] px-4 py-1 border row-start-1 col-start-1 self-center justify-self-end border-black appearance-none focus:outline-none placeholder:text-gray-100 bg-blue-900 rounded h-[36px] uppercase" + class="w-full block text-[14px] px-4 py-1 border row-start-1 col-start-1 self-center justify-self-end border-black appearance-none focus:outline-none bg-blue-900 rounded h-[36px] uppercase pr-[36px]" /> +
@@ -51,11 +54,14 @@ [value]="(staffDashboardService.time$ | async)?.endDate" type="date" id="endDate" - class="w-full block text-[14px] px-4 py-1 border row-start-1 col-start-1 self-center justify-self-end border-black appearance-none focus:outline-none placeholder:text-gray-100 bg-blue-900 rounded h-[36px] uppercase" + class="w-full block text-[14px] px-4 py-1 border row-start-1 col-start-1 self-center justify-self-end border-black appearance-none focus:outline-none bg-blue-900 rounded h-[36px] uppercase pr-[36px]" /> +
From f100ece6745d0c83e785cf11d526790afe03f641 Mon Sep 17 00:00:00 2001 From: Ryan Maffit Date: Wed, 26 Jun 2024 03:14:48 +0000 Subject: [PATCH 18/25] Added cancellation tokens --- src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs b/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs index 51f3430d..37cb1143 100644 --- a/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs +++ b/src/dotnet/HQ.Server/Services/TimeEntryServiceV1.cs @@ -567,15 +567,15 @@ public TimeEntryServiceV1(HQDbContext context) response.Clients = clients; response.StartDate = startDate; response.EndDate = endDate; - response.TotalHours = await timesQuery.SumAsync(t => t.Hours); - response.BillableHours = await timesQuery.Where(t => t.ChargeCode.Billable).SumAsync(t => t.Hours); - response.HoursThisWeek = await hrsThisWeekQuery.SumAsync(t => t.Hours); - response.HoursLastWeek = await hrsLastWeekQuery.SumAsync(t => t.Hours); - response.HoursThisMonth = await hrsThisMonthQuery.SumAsync(t => t.Hours); + response.TotalHours = await timesQuery.SumAsync(t => t.Hours, ct); + response.BillableHours = await timesQuery.Where(t => t.ChargeCode.Billable).SumAsync(t => t.Hours, ct); + response.HoursThisWeek = await hrsThisWeekQuery.SumAsync(t => t.Hours, ct); + response.HoursLastWeek = await hrsLastWeekQuery.SumAsync(t => t.Hours, ct); + response.HoursThisMonth = await hrsThisMonthQuery.SumAsync(t => t.Hours, ct); response.Vacation = vacationHours ?? 0; response.NextDate = nextDate; response.PreviousDate = previousDate; - response.StaffName = (await _context.Staff.FirstOrDefaultAsync(t => t.Id == request.StaffId))?.Name; + response.StaffName = (await _context.Staff.FirstOrDefaultAsync(t => t.Id == request.StaffId, ct))?.Name; DateOnly date = endDate; do From d5eb0c18421bb61b4889b0791686abbad09200d0 Mon Sep 17 00:00:00 2001 From: Ryan Maffit Date: Wed, 26 Jun 2024 03:21:07 +0000 Subject: [PATCH 19/25] Fixed linter errors --- .../staff-dashboard-date-range.component.html | 10 ++++---- .../staff-dashboard-time-entry.component.html | 2 +- .../staff-dashboard.component.html | 24 ++++++++++++------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html index 76026450..3d2ab3cb 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html @@ -3,12 +3,13 @@
-
-
+
diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html index 9648d385..c9e0f5e7 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html @@ -132,7 +132,7 @@ {{ time?.chargeCode }} diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html index 6ea3e75e..9da488fe 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html @@ -5,28 +5,36 @@
-

HRS THIS WEEK

-

+

+ HRS THIS WEEK +
+

{{ (staffDashboardService.time$ | async)?.hoursThisWeek?.toFixed(2) }}

-

HRS LAST WEEK

-

+

+ HRS LAST WEEK +
+

{{ (staffDashboardService.time$ | async)?.hoursLastWeek?.toFixed(2) }}

-

HRS THIS MONTH

-

+

+ HRS THIS MONTH +
+

{{ (staffDashboardService.time$ | async)?.hoursThisMonth?.toFixed(2) }}

-

VACATION

-

+

+ VACATION +
+

{{ (staffDashboardService.time$ | async)?.vacation?.toFixed(2) }}

From 7b2a456206f912824049987cbeffed451e2fee93 Mon Sep 17 00:00:00 2001 From: Ryan Maffit Date: Wed, 26 Jun 2024 03:25:18 +0000 Subject: [PATCH 20/25] Default to today --- .../app/staff-dashboard/service/staff-dashboard.service.ts | 6 ++++-- .../staff-dashboard-search-filter.component.html | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/angular/hq/src/app/staff-dashboard/service/staff-dashboard.service.ts b/src/angular/hq/src/app/staff-dashboard/service/staff-dashboard.service.ts index 2a3de1cd..02870ed0 100644 --- a/src/angular/hq/src/app/staff-dashboard/service/staff-dashboard.service.ts +++ b/src/angular/hq/src/app/staff-dashboard/service/staff-dashboard.service.ts @@ -26,9 +26,11 @@ import { }) export class StaffDashboardService { search = new FormControl(null); - period = new FormControl(Period.Week, { nonNullable: true }); + period = new FormControl(Period.Today, { nonNullable: true }); date = new FormControl( - /*new Date().toISOString().split('T')[0]*/ '2024-06-21', + new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000) + .toISOString() + .split('T')[0], { nonNullable: true, }, diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.html index 4fa0cb84..2c7eaed9 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-search-filter/staff-dashboard-search-filter.component.html @@ -33,7 +33,7 @@ [formControl]="staffDashboardService.period" class="w-full pl-2 pr-[43px] appearance-none focus:outline-none hover:cursor-pointer font-medium row-start-1 col-start-1 border border-steel-blue-600 text-gray-100 bg-blue-900 rounded h-[36px]" > - + Date: Wed, 26 Jun 2024 03:51:12 +0000 Subject: [PATCH 21/25] Fixed extra update events after component is disposed --- .../staff-dashboard-time-entry.component.ts | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts index 3b1c2f66..34ce7ea1 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.ts @@ -182,7 +182,7 @@ export class StaffDashboardTimeEntryComponent implements OnChanges, OnDestroy { }); form$ - .pipe(takeUntil(this.destroyed$), debounceTime(750)) + .pipe(debounceTime(750), takeUntil(this.destroyed$)) .subscribe((time) => { if (this.form.touched) { this.class = this.form.invalid @@ -195,14 +195,16 @@ export class StaffDashboardTimeEntryComponent implements OnChanges, OnDestroy { } }); - this.staffDashboardService.refresh$.subscribe(() => { - if (!this.time?.id) { - this.form.reset(); - this.form.patchValue({ - date: this.time?.date, - }); - } - }); + this.staffDashboardService.refresh$ + .pipe(takeUntil(this.destroyed$)) + .subscribe(() => { + if (!this.time?.id) { + this.form.reset(); + this.form.patchValue({ + date: this.time?.date, + }); + } + }); } ngOnChanges(changes: SimpleChanges) { @@ -212,6 +214,7 @@ export class StaffDashboardTimeEntryComponent implements OnChanges, OnDestroy { } ngOnDestroy() { + console.log('destroying'); this.destroyed$.next(); this.destroyed$.complete(); } @@ -222,4 +225,13 @@ export class StaffDashboardTimeEntryComponent implements OnChanges, OnDestroy { this.hqTimeDelete.emit({ id }); } } + + blurInput(target: EventTarget | null) { + if ( + target instanceof HTMLInputElement || + target instanceof HTMLSelectElement + ) { + target.blur(); + } + } } From 8684347ee78b7765929228bcd7fece69f53fe1df Mon Sep 17 00:00:00 2001 From: Ryan Maffit Date: Wed, 26 Jun 2024 03:51:25 +0000 Subject: [PATCH 22/25] Blur input on enter --- .../staff-dashboard-time-entry.component.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html index c9e0f5e7..6ce307ee 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-time-entry/staff-dashboard-time-entry.component.html @@ -8,6 +8,7 @@ type="number" formControlName="hours" step="0.25" + (keydown.enter)="blurInput($event.target)" /> @@ -99,6 +100,7 @@ class="w-[100%] block text-[14px] px-2 py-1 border border-black appearance-none focus:outline-none focus:border-white placeholder:text-gray-100 bg-blue-900 rounded h-[30px]" type="text" formControlName="task" + (keydown.enter)="blurInput($event.target)" /> } } @@ -108,6 +110,7 @@ class="w-[100%] block text-[14px] px-2 py-1 border border-black appearance-none focus:outline-none focus:border-white placeholder:text-gray-100 bg-blue-900 rounded h-[30px]" type="text" formControlName="notes" + (keydown.enter)="blurInput($event.target)" /> From fb32637ea59946a220ba3fa6dc03f286170e1fa2 Mon Sep 17 00:00:00 2001 From: Ryan Maffit Date: Wed, 26 Jun 2024 04:16:51 +0000 Subject: [PATCH 23/25] QoL updates to dashboard --- CHANGELOG.md | 15 +++++ src/angular/hq/src/app/app.routes.ts | 2 +- .../staff-dashboard/get-dashboard-time-v1.ts | 1 + .../staff-dashboard-date-range.component.html | 17 +----- .../staff-dashboard.component.html | 59 ++++++++++++------- .../staff-dashboard.component.ts | 2 +- .../Times/GetDashboardTimeV1.cs | 1 + .../HQ.Server/Services/TimeEntryServiceV1.cs | 1 + 8 files changed, 60 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b906dfa1..e4a23067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.3.5] - 2024-06-20 + +### Fixed +- CLI null reference exception if notes are empty +- Newlines in CLI output messages +- Client feedback + +### Added +- StaffId to CLI state object for querying time +- Labels to all form inputs, search fields +- `AcceptedBy` staff to time entries to track who accepted the time entry +- Authorization checks to Angular project + - Staff can view mostly everything and executive/admin users can create/edit everything +- No records found on client table lists + ## [0.3.4] - 2024-06-17 ### Fixed diff --git a/src/angular/hq/src/app/app.routes.ts b/src/angular/hq/src/app/app.routes.ts index 393e79a6..4a3a990e 100644 --- a/src/angular/hq/src/app/app.routes.ts +++ b/src/angular/hq/src/app/app.routes.ts @@ -7,7 +7,7 @@ import { HQRole } from './enums/hqrole'; export const routes: Routes = [ { path: '', - redirectTo: 'psr', + redirectTo: 'dashboard', pathMatch: 'full', }, { diff --git a/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts b/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts index df8464be..b4bb5a09 100644 --- a/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts +++ b/src/angular/hq/src/app/models/staff-dashboard/get-dashboard-time-v1.ts @@ -28,6 +28,7 @@ export interface GetDashboardTimeV1Response { export interface GetDashboardTimeV1TimeForDate { date: string; times: GetDashboardTimeV1TimeForDateTimes[]; + totalHours: number; } export interface GetDashboardTimeV1TimeForDateTimes { diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html index 3d2ab3cb..d87e1515 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard-date-range/staff-dashboard-date-range.component.html @@ -5,17 +5,12 @@
-
+
- -
diff --git a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html index 9da488fe..74ebd395 100644 --- a/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html +++ b/src/angular/hq/src/app/staff-dashboard/staff-dashboard.component.html @@ -1,7 +1,7 @@
- {{ (staffDashboardService.time$ | async)?.staffName }} + {{ (staffDashboardService.time$ | async)?.staffName || "-" }}
@@ -9,7 +9,10 @@ HRS THIS WEEK

- {{ (staffDashboardService.time$ | async)?.hoursThisWeek?.toFixed(2) }} + {{ + (staffDashboardService.time$ | async)?.hoursThisWeek?.toFixed(2) || + "-" + }}

@@ -17,7 +20,10 @@ HRS LAST WEEK

- {{ (staffDashboardService.time$ | async)?.hoursLastWeek?.toFixed(2) }} + {{ + (staffDashboardService.time$ | async)?.hoursLastWeek?.toFixed(2) || + "-" + }}

@@ -26,7 +32,8 @@

{{ - (staffDashboardService.time$ | async)?.hoursThisMonth?.toFixed(2) + (staffDashboardService.time$ | async)?.hoursThisMonth?.toFixed(2) || + "-" }}

@@ -35,13 +42,15 @@ VACATION

- {{ (staffDashboardService.time$ | async)?.vacation?.toFixed(2) }} + {{ + (staffDashboardService.time$ | async)?.vacation?.toFixed(2) || "-" + }}

-
+
@@ -49,8 +58,10 @@ -
- {{ (staffDashboardService.time$ | async)?.totalHours?.toFixed(2) }} +
+ {{ + (staffDashboardService.time$ | async)?.totalHours?.toFixed(2) || "-" + }}
@@ -58,16 +69,19 @@ -
- {{ (staffDashboardService.time$ | async)?.billableHours?.toFixed(2) }} +
+ {{ + (staffDashboardService.time$ | async)?.billableHours?.toFixed(2) || + "-" + }}
-
-
+
+