Skip to content

Commit

Permalink
Merge pull request #398 from sancsoft/397-add-holiday-time-entries-wh…
Browse files Browse the repository at this point in the history
…en-holiday-is-added-after-holidays-are-automatically-added-for-the-current-week

Refactored Time entry generation and updated support for previously a…
  • Loading branch information
rmaffitsancsoft authored Jan 13, 2025
2 parents ff7090f + 2ba4ebb commit e0dec34
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,25 @@
}
</hq-select-input>
}
@if(clientDetailService.showCurrentOnly$ | async){
<div class="inline-flex items-center">
<div class="text-[12px] mr-[10px] leading-none text-nowrap">
Show current only
</div>
<label class="relative inline-flex cursor-pointer items-center">
<input id="switch" type="checkbox" class="peer sr-only" [formControl]="clientDetailService.currentOnly" checked />
<label for="switch" class="hidden"></label>
<div
class="peer h-[14px] w-[33px] rounded-full bg-slate-200 after:absolute after:left-[4px] after:top-[3px] after:h-[8px] after:w-[8px] after:rounded-full after:bg-gray-500 peer-checked:after:bg-white after:transition-all after:content-[''] peer-checked:bg-steel-blue-600 peer-checked:after:translate-x-full peer-checked:after:right-[12px] peer-checked:after:left-auto">
@if (clientDetailService.showCurrentOnly$ | async) {
<div class="inline-flex items-center">
<div class="text-[12px] mr-[10px] leading-none text-nowrap">
Show current only
</div>
</label>
<div class="text-xs ml-[6px]">On</div>
</div>
}
<label class="relative inline-flex cursor-pointer items-center">
<input
id="switch"
type="checkbox"
class="peer sr-only"
[formControl]="clientDetailService.currentOnly"
checked
/>
<label for="switch" class="hidden"></label>
<div
class="peer h-[14px] w-[33px] rounded-full bg-slate-200 after:absolute after:left-[4px] after:top-[3px] after:h-[8px] after:w-[8px] after:rounded-full after:bg-gray-500 peer-checked:after:bg-white after:transition-all after:content-[''] peer-checked:bg-steel-blue-600 peer-checked:after:translate-x-full peer-checked:after:right-[12px] peer-checked:after:left-auto"
></div>
</label>
<div class="text-xs ml-[6px]">On</div>
</div>
}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@
<div class="flex flex-col xl:flex-row justify-between items-start gap-4">
<hq-client-details-search-filter></hq-client-details-search-filter>
<div class="flex divide-x ml-auto">
<hq-tab routerLink="projects" (click)="clientDetailsService.showCurrentOnly()">Projects</hq-tab>
<hq-tab routerLink="quotes" (click)="clientDetailsService.hideCurrentOnly()">Quotes</hq-tab>
<hq-tab
routerLink="projects"
(click)="clientDetailsService.showCurrentOnly()"
>Projects</hq-tab
>
<hq-tab
routerLink="quotes"
(click)="clientDetailsService.hideCurrentOnly()"
>Quotes</hq-tab
>
<!-- <hq-tab routerLink="services">Services</hq-tab>
<hq-tab routerLink="invoices">Invoices</hq-tab> -->
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,10 @@ export class ClientDetailsService {
this.clientIdSubject.next(clientId);
}
}
showCurrentOnly(){
showCurrentOnly() {
this.showCurrentOnlySubject.next(true);
}
hideCurrentOnly(){
hideCurrentOnly() {
this.showCurrentOnlySubject.next(false);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
shareReplay,
combineLatest,
debounceTime,
switchMap,startWith
switchMap,
startWith,
} from 'rxjs';
import { formControlChanges } from '../../../core/functions/form-control-changes';
import { BaseListService } from '../../../core/services/base-list.service';
Expand Down Expand Up @@ -43,20 +44,22 @@ export class ClientProjectListService extends BaseListService<
this.clientDetailsService.projectStatus,
).pipe(
tap(() => this.goToPage(1)),
tap((status)=> {
if(status != null){
tap((status) => {
if (status != null) {
this.clientDetailsService.currentOnly.setValue(false);
}
}),
}
}),
shareReplay({ bufferSize: 1, refCount: false }),
);
const currentOnly$ = formControlChanges(this.clientDetailsService.currentOnly).pipe(
const currentOnly$ = formControlChanges(
this.clientDetailsService.currentOnly,
).pipe(
startWith(this.clientDetailsService.currentOnly.value),
tap(value => {
if (value){
tap((value) => {
if (value) {
this.clientDetailsService.projectStatus.setValue(null);
}
})
}),
);
const result$ = combineLatest({
search: search$,
Expand All @@ -66,7 +69,7 @@ export class ClientProjectListService extends BaseListService<
sortBy: this.sortOption$,
projectStatus: projectStatus$,
sortDirection: this.sortDirection$,
currentOnly: currentOnly$
currentOnly: currentOnly$,
}).pipe(
debounceTime(500),
tap(() => this.loadingSubject.next(true)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,23 @@ <h1 class="font-rajdhani font-semibold text-3xl">Projects</h1>
}
</hq-select-input>
</div>
@if(listService){}
@if (listService) {}
<div class="inline-flex items-center">
<div class="text-[12px] mr-[10px] leading-none text-nowrap">
Show current only
</div>
<label class="relative inline-flex cursor-pointer items-center">
<input id="switch" type="checkbox" class="peer sr-only" [formControl]="listService.currentOnly" checked />
<input
id="switch"
type="checkbox"
class="peer sr-only"
[formControl]="listService.currentOnly"
checked
/>
<label for="switch" class="hidden"></label>
<div
class="peer h-[14px] w-[33px] rounded-full bg-slate-200 after:absolute after:left-[4px] after:top-[3px] after:h-[8px] after:w-[8px] after:rounded-full after:bg-gray-500 peer-checked:after:bg-white after:transition-all after:content-[''] peer-checked:bg-steel-blue-600 peer-checked:after:translate-x-full peer-checked:after:right-[12px] peer-checked:after:left-auto">
</div>
class="peer h-[14px] w-[33px] rounded-full bg-slate-200 after:absolute after:left-[4px] after:top-[3px] after:h-[8px] after:w-[8px] after:rounded-full after:bg-gray-500 peer-checked:after:bg-white after:transition-all after:content-[''] peer-checked:bg-steel-blue-600 peer-checked:after:translate-x-full peer-checked:after:right-[12px] peer-checked:after:left-auto"
></div>
</label>
<div class="text-xs ml-[6px]">On</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
Observable,
shareReplay,
switchMap,
tap, startWith
tap,
startWith,
} from 'rxjs';
import { SortDirection } from '../../models/common/sort-direction';
import { HQService } from '../../services/hq.service';
Expand Down Expand Up @@ -43,9 +44,9 @@ export class ProjectListService extends BaseListService<
public projectStatus = new FormControl<ProjectStatus | null>(null);
public projectStatus$ = formControlChanges(this.projectStatus).pipe(
tap(() => this.goToPage(1)),
tap((status)=> {
if(status != null){
this.currentOnly.setValue(false);
tap((status) => {
if (status != null) {
this.currentOnly.setValue(false);
}
}),
shareReplay({ bufferSize: 1, refCount: false }),
Expand All @@ -60,13 +61,13 @@ export class ProjectListService extends BaseListService<
projectManagers$: Observable<GetStaffV1Record[]>;
currentOnly = new FormControl<boolean>(true);
public currentOnly$ = formControlChanges(this.currentOnly).pipe(
startWith(this.currentOnly.value),
tap(value => {
if (value){
this.projectStatus.setValue(null);
}
})
);
startWith(this.currentOnly.value),
tap((value) => {
if (value) {
this.projectStatus.setValue(null);
}
}),
);
protected override getResponse(): Observable<GetProjectResponseV1> {
return combineLatest({
search: this.search$,
Expand Down
144 changes: 81 additions & 63 deletions src/dotnet/HQ.Server/Services/HolidayServiceV1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,33 +31,53 @@ public HolidayServiceV1(HQDbContext context, ILogger<ProjectStatusReportServiceV

public async Task<Result<UpsertHolidayV1.Response>> UpsertHolidayV1(UpsertHolidayV1.Request request, CancellationToken ct = default)
{
var validationResult = Result.Merge(
using (var transaction = await _context.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable, ct))
{

try
{
var validationResult = Result.Merge(
Result.FailIf(string.IsNullOrEmpty(request.Name), "Name is required."),
Result.FailIf(await _context.Holidays.AnyAsync(t => t.Id != request.Id && t.Date == request.Date && t.Jurisdiciton == request.Jurisdiciton, ct), "Another holiday already exists on that date.")
);

if (validationResult.IsFailed)
{
return validationResult;
}
if (validationResult.IsFailed)
{
return validationResult;
}

var holiday = await _context.Holidays.FindAsync(request.Id);
if (holiday == null)
{
holiday = new();
_context.Holidays.Add(holiday);
}
var holiday = await _context.Holidays.FindAsync(request.Id);
if (holiday == null)
{
holiday = new();
_context.Holidays.Add(holiday);
}

holiday.Name = request.Name;
holiday.Jurisdiciton = request.Jurisdiciton;
holiday.Date = request.Date;
holiday.Name = request.Name;
holiday.Jurisdiciton = request.Jurisdiciton;
holiday.Date = request.Date;

await _context.SaveChangesAsync(ct);
await _context.SaveChangesAsync(ct);

return new UpsertHolidayV1.Response()
{
Id = holiday.Id
};
var holidayChargeCode = await _context.ChargeCodes.Where(t => t.Project!.Name.ToLower().Contains("holiday")).FirstOrDefaultAsync(ct);
if (holidayChargeCode == null)
{
return Result.Fail<UpsertHolidayV1.Response>("Holiday charge code not found");
}
await GenerateTimeEntriesForHoliday(holiday, holidayChargeCode, ct);
await transaction.CommitAsync(ct);

return new UpsertHolidayV1.Response()
{
Id = holiday.Id
};
}
catch (Exception ex)
{
await transaction.RollbackAsync(ct);
return Result.Fail(new Error("An error occurred while upserting the Chargecode.").CausedBy(ex));
}
}
}

public async Task<Result<DeleteHolidayV1.Response?>> DeleteHolidayV1(DeleteHolidayV1.Request request, CancellationToken ct = default)
Expand Down Expand Up @@ -210,6 +230,44 @@ public HolidayServiceV1(HQDbContext context, ILogger<ProjectStatusReportServiceV
Updated = updated
};
}

private async Task GenerateTimeEntriesForHoliday(Holiday holiday, ChargeCode holidayChargeCode, CancellationToken ct)
{
var jurisdiciton = holiday.Jurisdiciton;
var staff = _context.Staff.
AsNoTracking()
.AsQueryable();

staff = staff.Where(t => t.EndDate == null && t.Jurisdiciton == jurisdiciton);
var times = _context.Times
.AsNoTracking()
.AsQueryable()
.Include(t => t.Staff);
var staffWithHoliday = await times.Where(t => t.Date == holiday.Date && t.ChargeCode.Id == holidayChargeCode.Id && t.Staff.Jurisdiciton == jurisdiciton && t.Staff.EndDate == null).AsNoTracking().Select(t => t.Staff).ToListAsync(ct);

var staffWithHolidayIds = staffWithHoliday.Select(s => s.Id).ToList();
var staffWithoutEnteredHoliday = await staff.Where(t => !staffWithHolidayIds.Contains(t.Id)).ToListAsync(ct);

_logger.LogInformation($"Creating time entries for Holiday {holiday.Name} for staff count {staffWithoutEnteredHoliday.Count()}");
foreach (var staffMember in staffWithoutEnteredHoliday)
{
var timeEntry = new Time
{
ChargeCodeId = holidayChargeCode.Id,
Hours = 8,
HoursApproved = 8,
Notes = holiday.Name,
StaffId = staffMember.Id,
Date = holiday.Date,
Status = TimeStatus.Accepted,
HolidayId = holiday.Id
};
_context.Times.Add(timeEntry);
}
await _context.SaveChangesAsync(ct);
_logger.LogInformation($"Created time entries for Holiday {holiday.Name} for staff count {staffWithoutEnteredHoliday.Count()}");
}

public async Task BackgroundAutoGenerateHolidayTimeEntryV1(CancellationToken ct)
{
var today = DateOnly.FromDateTime(DateTime.UtcNow);
Expand All @@ -219,57 +277,17 @@ public async Task BackgroundAutoGenerateHolidayTimeEntryV1(CancellationToken ct)
var holidays = _context.Holidays
.AsNoTracking()
.AsQueryable();
var jurisdicitons = Enum.GetValues(typeof(Jurisdiciton));
var holidayChargeCode = await _context.ChargeCodes.Where(t => t.Project!.Name.ToLower().Contains("holiday")).FirstOrDefaultAsync(ct);
if (holidayChargeCode == null)
{
return;
}
foreach (Jurisdiciton jurisdiciton in jurisdicitons)
{
var upcomingHolidays = await holidays.Where(t => t.Date >= startDate && t.Date <= endDate && t.Jurisdiciton == jurisdiciton).ToListAsync(ct);
foreach (var upcomingHoliday in upcomingHolidays)
{
var staff = _context.Staff.
AsNoTracking()
.AsQueryable();

var upcomingHolidays = await holidays.Where(t => t.Date >= startDate && t.Date <= endDate).ToListAsync(ct);
foreach (var upcomingHoliday in upcomingHolidays)
{

staff = staff.Where(t => t.EndDate == null && t.Jurisdiciton == jurisdiciton);
var times = _context.Times
.AsNoTracking()
.AsQueryable()
.Include(t => t.Staff);
var staffWithHoliday = await times.Where(t => t.Date == upcomingHoliday.Date && t.ChargeCode.Id == holidayChargeCode.Id && t.Staff.Jurisdiciton == jurisdiciton && t.Staff.EndDate == null).AsNoTracking().Select(t => t.Staff).ToListAsync(ct);

var staffWithHolidayIds = staffWithHoliday.Select(s => s.Id).ToList();
var staffWithoutEnteredHoliday = await staff.Where(t => !staffWithHolidayIds.Contains(t.Id)).ToListAsync(ct);

_logger.LogInformation($"Creating time entries for Holiday {upcomingHoliday.Name} for staff count {staffWithoutEnteredHoliday.Count()}");
foreach (var staffMember in staffWithoutEnteredHoliday)
{

var timeEntry = new Time
{
ChargeCodeId = holidayChargeCode.Id,
Hours = 8,
HoursApproved = 8,
Notes = upcomingHoliday.Name,
StaffId = staffMember.Id,
Date = upcomingHoliday.Date,
Status = TimeStatus.Accepted,
HolidayId = upcomingHoliday.Id
};
_context.Times.Add(timeEntry);
}
await _context.SaveChangesAsync(ct);


_logger.LogInformation($"Created time entries for Holiday {upcomingHoliday.Name} for staff count {staffWithoutEnteredHoliday.Count()}");
}

await GenerateTimeEntriesForHoliday(upcomingHoliday, holidayChargeCode, ct);
}

}

}

0 comments on commit e0dec34

Please sign in to comment.