diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 2c02f77..5034d51 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -16,6 +16,8 @@ import { LoginComponent } from './login/login.component'; import { ExportResolver } from './resolvers/export.resolver'; import { FormResolver } from './resolvers/form.resolver'; import { SubAreaResolver } from './resolvers/sub-area.resolver'; +import { LockRecordsComponent } from './lock-records/lock-records.component'; +import { LockRecordsResolver } from './resolvers/lock-records.resolver'; const routes: Routes = [ { @@ -126,6 +128,16 @@ const routes: Routes = [ }, resolve: [ExportResolver], }, + { + path: 'lock-records', + component: LockRecordsComponent, + canActivate: [AuthGuard], + data: { + label: 'Lock Records', + breadcrumb: 'Lock Records', + }, + resolve: [LockRecordsResolver], + }, { path: 'unauthorized', pathMatch: 'full', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d6903f7..515ed49 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -28,6 +28,7 @@ import { InfiniteLoadingBarModule } from './shared/components/infinite-loading-b import { ToastrModule } from 'ngx-toastr'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { LoadingService } from './services/loading.service'; +import { LockRecordsModule } from './lock-records/lock-records.module'; export function initConfig( configService: ConfigService, @@ -57,6 +58,7 @@ export function initConfig( BreadcrumbModule, ExportReportsModule, EnterDataModule, + LockRecordsModule, HeaderModule, FooterModule, HomeModule, diff --git a/src/app/enter-data/accordion-manager/backcountry-cabins-accordion/backcountry-cabins-accordion.component.html b/src/app/enter-data/accordion-manager/backcountry-cabins-accordion/backcountry-cabins-accordion.component.html index 0452c53..3692034 100644 --- a/src/app/enter-data/accordion-manager/backcountry-cabins-accordion/backcountry-cabins-accordion.component.html +++ b/src/app/enter-data/accordion-manager/backcountry-cabins-accordion/backcountry-cabins-accordion.component.html @@ -6,5 +6,6 @@ [notes]="data?.notes" [summaries]="summaries" [editLink]="'backcountry-cabins'" + [recordLock]="data?.isLocked" > diff --git a/src/app/enter-data/accordion-manager/backcountry-camping-accordion/backcountry-camping-accordion.component.html b/src/app/enter-data/accordion-manager/backcountry-camping-accordion/backcountry-camping-accordion.component.html index 855c468..08b5497 100644 --- a/src/app/enter-data/accordion-manager/backcountry-camping-accordion/backcountry-camping-accordion.component.html +++ b/src/app/enter-data/accordion-manager/backcountry-camping-accordion/backcountry-camping-accordion.component.html @@ -6,5 +6,6 @@ [notes]="data?.notes" [summaries]="summaries" [editLink]="'backcountry-camping'" + [recordLock]="data?.isLocked" > diff --git a/src/app/enter-data/accordion-manager/boating-accordion/boating-accordion.component.html b/src/app/enter-data/accordion-manager/boating-accordion/boating-accordion.component.html index bbf7d62..2ba6b18 100644 --- a/src/app/enter-data/accordion-manager/boating-accordion/boating-accordion.component.html +++ b/src/app/enter-data/accordion-manager/boating-accordion/boating-accordion.component.html @@ -6,5 +6,6 @@ [notes]="data?.notes" [summaries]="summaries" [editLink]="'boating'" + [recordLock]="data?.isLocked" > diff --git a/src/app/enter-data/accordion-manager/day-use-accordion/day-use-accordion.component.html b/src/app/enter-data/accordion-manager/day-use-accordion/day-use-accordion.component.html index fb98dbb..154ba2b 100644 --- a/src/app/enter-data/accordion-manager/day-use-accordion/day-use-accordion.component.html +++ b/src/app/enter-data/accordion-manager/day-use-accordion/day-use-accordion.component.html @@ -6,5 +6,6 @@ [notes]="data?.notes" [summaries]="summaries" [editLink]="'day-use'" + [recordLock]="data?.isLocked" > diff --git a/src/app/enter-data/accordion-manager/frontcountry-cabins-accordion/frontcountry-cabins-accordion.component.html b/src/app/enter-data/accordion-manager/frontcountry-cabins-accordion/frontcountry-cabins-accordion.component.html index 315b4ad..39d2ec8 100644 --- a/src/app/enter-data/accordion-manager/frontcountry-cabins-accordion/frontcountry-cabins-accordion.component.html +++ b/src/app/enter-data/accordion-manager/frontcountry-cabins-accordion/frontcountry-cabins-accordion.component.html @@ -6,5 +6,6 @@ [notes]="data?.notes" [summaries]="summaries" [editLink]="'frontcountry-cabins'" + [recordLock]="data?.isLocked" > diff --git a/src/app/enter-data/accordion-manager/frontcountry-camping-accordion/frontcountry-camping-accordion.component.html b/src/app/enter-data/accordion-manager/frontcountry-camping-accordion/frontcountry-camping-accordion.component.html index a0421ab..9409945 100644 --- a/src/app/enter-data/accordion-manager/frontcountry-camping-accordion/frontcountry-camping-accordion.component.html +++ b/src/app/enter-data/accordion-manager/frontcountry-camping-accordion/frontcountry-camping-accordion.component.html @@ -6,5 +6,6 @@ [notes]="data?.notes" [summaries]="summaries" [editLink]="'frontcountry-camping'" + [recordLock]="data?.isLocked" > diff --git a/src/app/enter-data/accordion-manager/group-camping-accordion/group-camping-accordion.component.html b/src/app/enter-data/accordion-manager/group-camping-accordion/group-camping-accordion.component.html index f6d9050..68ee433 100644 --- a/src/app/enter-data/accordion-manager/group-camping-accordion/group-camping-accordion.component.html +++ b/src/app/enter-data/accordion-manager/group-camping-accordion/group-camping-accordion.component.html @@ -6,5 +6,6 @@ [notes]="data?.notes" [summaries]="summaries" [editLink]="'group-camping'" + [recordLock]="data?.isLocked" > diff --git a/src/app/guards/auth.guard.ts b/src/app/guards/auth.guard.ts index 1df877d..ac35e5c 100644 --- a/src/app/guards/auth.guard.ts +++ b/src/app/guards/auth.guard.ts @@ -63,6 +63,10 @@ export class AuthGuard implements CanActivate { return this.router.parseUrl('/'); } + if (!this.keycloakService.isAllowed('lock-records') && state.url === '/lock-records') { + return this.router.parseUrl('/'); + } + // Show the requested page. return true; } diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts index 457af70..ae41903 100644 --- a/src/app/header/header.component.ts +++ b/src/app/header/header.component.ts @@ -30,7 +30,10 @@ export class HeaderComponent implements OnDestroy { this.routes = router.config.filter(function (obj) { if (obj.path === 'export-reports') { return keycloakService.isAllowed('export-reports'); - } else { + } else if (obj.path === 'lock-records') { + return keycloakService.isAllowed('lock-records') + } + { return obj.path !== '**' && obj.path !== 'unauthorized'; } }); diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 9ac3ed8..c47dffe 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -4,8 +4,8 @@

BC Parks - Attendance and Revenue

-
-
+
+
Locked records + diff --git a/src/app/lock-records/fiscal-year-lock-table/fiscal-year-lock-table.component.scss b/src/app/lock-records/fiscal-year-lock-table/fiscal-year-lock-table.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/lock-records/fiscal-year-lock-table/fiscal-year-lock-table.component.spec.ts b/src/app/lock-records/fiscal-year-lock-table/fiscal-year-lock-table.component.spec.ts new file mode 100644 index 0000000..e94bf09 --- /dev/null +++ b/src/app/lock-records/fiscal-year-lock-table/fiscal-year-lock-table.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FiscalYearLockTableComponent } from './fiscal-year-lock-table.component'; + +describe('FiscalYearLockTableComponent', () => { + let component: FiscalYearLockTableComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FiscalYearLockTableComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FiscalYearLockTableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/lock-records/fiscal-year-lock-table/fiscal-year-lock-table.component.ts b/src/app/lock-records/fiscal-year-lock-table/fiscal-year-lock-table.component.ts new file mode 100644 index 0000000..dfd7b36 --- /dev/null +++ b/src/app/lock-records/fiscal-year-lock-table/fiscal-year-lock-table.component.ts @@ -0,0 +1,82 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { DataService } from 'src/app/services/data.service'; +import { columnSchema } from 'src/app/shared/components/table/table.component'; +import { Constants } from 'src/app/shared/utils/constants'; +import { FiscalYearUnlockerComponent } from './fiscal-year-unlocker/fiscal-year-unlocker.component'; + +@Component({ + selector: 'app-fiscal-year-lock-table', + templateUrl: './fiscal-year-lock-table.component.html', + styleUrls: ['./fiscal-year-lock-table.component.scss'], +}) +export class FiscalYearLockTableComponent implements OnInit { + @Input() data: any[]; + + private subscriptions = new Subscription(); + public columnSchema: columnSchema[] = []; + public tableRows: any[] = []; + + constructor(protected dataService: DataService) { + this.subscriptions.add( + dataService + .getItemValue(Constants.dataIds.LOCK_RECORDS_FISCAL_YEARS_DATA) + .subscribe((res) => { + if (res && res.length) { + this.tableRows = this.filterLockedYears(res); + } + }) + ); + } + + ngOnInit(): void { + this.createColumnSchema(); + } + + filterLockedYears(data) { + let lockedYears: any[] = []; + for (const year of data) { + if (year.isLocked) { + lockedYears.push(year); + } + } + return lockedYears; + } + + // fiscalYearEndObject schema + // pk: fiscalYearEnd + // sk: 2022 + // isLocked: true + createColumnSchema() { + this.columnSchema = [ + { + id: 'year', + displayHeader: 'Year', + columnClasses: 'ps-3 pe-5', + mapValue: (row) => row.sk, + }, + { + id: 'parkName', + displayHeader: 'Park', + width: '70%', + columnClasses: 'px-5', + mapValue: () => 'All Parks', + }, + { + id: 'lockedStatus', + displayHeader: 'Unlock', + width: '10%', + columnClasses: 'ps-5 pe-3', + mapValue: (row) => row.isLocked, + cellTemplate: (row) => { + return { + component: FiscalYearUnlockerComponent, + data: { + data: row, + }, + }; + }, + }, + ]; + } +} diff --git a/src/app/lock-records/fiscal-year-lock-table/fiscal-year-unlocker/fiscal-year-unlocker.component.html b/src/app/lock-records/fiscal-year-lock-table/fiscal-year-unlocker/fiscal-year-unlocker.component.html new file mode 100644 index 0000000..bea839c --- /dev/null +++ b/src/app/lock-records/fiscal-year-lock-table/fiscal-year-unlocker/fiscal-year-unlocker.component.html @@ -0,0 +1,3 @@ + diff --git a/src/app/lock-records/fiscal-year-lock-table/fiscal-year-unlocker/fiscal-year-unlocker.component.scss b/src/app/lock-records/fiscal-year-lock-table/fiscal-year-unlocker/fiscal-year-unlocker.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/lock-records/fiscal-year-lock-table/fiscal-year-unlocker/fiscal-year-unlocker.component.spec.ts b/src/app/lock-records/fiscal-year-lock-table/fiscal-year-unlocker/fiscal-year-unlocker.component.spec.ts new file mode 100644 index 0000000..fbdaf6d --- /dev/null +++ b/src/app/lock-records/fiscal-year-lock-table/fiscal-year-unlocker/fiscal-year-unlocker.component.spec.ts @@ -0,0 +1,28 @@ +import { HttpClientModule } from '@angular/common/http'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ConfigService } from 'src/app/services/config.service'; + +import { FiscalYearUnlockerComponent } from './fiscal-year-unlocker.component'; + +describe('FiscalYearUnlockerComponent', () => { + let component: FiscalYearUnlockerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HttpClientModule], + declarations: [FiscalYearUnlockerComponent], + providers: [ConfigService], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FiscalYearUnlockerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/lock-records/fiscal-year-lock-table/fiscal-year-unlocker/fiscal-year-unlocker.component.ts b/src/app/lock-records/fiscal-year-lock-table/fiscal-year-unlocker/fiscal-year-unlocker.component.ts new file mode 100644 index 0000000..b169b0d --- /dev/null +++ b/src/app/lock-records/fiscal-year-lock-table/fiscal-year-unlocker/fiscal-year-unlocker.component.ts @@ -0,0 +1,20 @@ +import { Component, Input } from '@angular/core'; +import { FiscalYearLockService } from 'src/app/services/fiscal-year-lock.service'; + +@Component({ + selector: 'app-fiscal-year-unlocker', + templateUrl: './fiscal-year-unlocker.component.html', + styleUrls: ['./fiscal-year-unlocker.component.scss'], +}) +export class FiscalYearUnlockerComponent { + @Input() data: any; + + constructor(private fiscalYearLockService: FiscalYearLockService) {} + + unlockFiscalYear() { + this.fiscalYearLockService.lockUnlockFiscalYear( + this.data.year.value, + false + ); + } +} diff --git a/src/app/lock-records/lock-records.component.html b/src/app/lock-records/lock-records.component.html new file mode 100644 index 0000000..cc11d6c --- /dev/null +++ b/src/app/lock-records/lock-records.component.html @@ -0,0 +1,51 @@ +
+

Lock or Unlock Records

+

+ Select a date rage below to lock or unlock all records for all parks for the + selected dates. +

+
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+
+ +
diff --git a/src/app/lock-records/lock-records.component.scss b/src/app/lock-records/lock-records.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/lock-records/lock-records.component.spec.ts b/src/app/lock-records/lock-records.component.spec.ts new file mode 100644 index 0000000..5cb1196 --- /dev/null +++ b/src/app/lock-records/lock-records.component.spec.ts @@ -0,0 +1,34 @@ +import { HttpClientModule } from '@angular/common/http'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; +import { ConfigService } from '../services/config.service'; +import { DatePickerModule } from '../shared/components/date-picker/date-picker.module'; + +import { LockRecordsComponent } from './lock-records.component'; + +describe('LockRecordsComponent', () => { + let component: LockRecordsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + HttpClientModule, + DatePickerModule, + BsDatepickerModule.forRoot(), + ], + declarations: [LockRecordsComponent], + providers: [ConfigService], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LockRecordsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/lock-records/lock-records.component.ts b/src/app/lock-records/lock-records.component.ts new file mode 100644 index 0000000..6a970e8 --- /dev/null +++ b/src/app/lock-records/lock-records.component.ts @@ -0,0 +1,56 @@ +import { Component, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { DataService } from '../services/data.service'; +import { FiscalYearLockService } from '../services/fiscal-year-lock.service'; +import { Constants } from '../shared/utils/constants'; + +@Component({ + selector: 'app-lock-records', + templateUrl: './lock-records.component.html', + styleUrls: ['./lock-records.component.scss'], +}) +export class LockRecordsComponent implements OnInit { + + private subscriptions = new Subscription(); + public fiscalYearStartMonth = 'April'; + public fiscalYearEndMonth = 'March'; + public loading = true; + public fiscalYearsList: any[] = []; + public modelDate = NaN; + public maxDate = new Date(); + public fiscalYearRangeString = 'Select a fiscal year'; + + constructor( + protected dataService: DataService, + protected fiscalYearLockService: FiscalYearLockService + ) { + this.subscriptions.add( + dataService + .getItemValue(Constants.dataIds.LOCK_RECORDS_FISCAL_YEARS_DATA) + .subscribe((res) => { + this.fiscalYearsList = res; + }) + ); + } + + ngOnInit(): void { + this.fiscalYearLockService.fetchFiscalYear(); + } + + onOpenCalendar(container) { + container.setViewMode('year'); + } + + datePickerOutput(event) { + const selectedYear = new Date(event).getFullYear(); + this.modelDate = selectedYear; + const startDate = this.fiscalYearStartMonth + ' ' + (selectedYear - 1); + const endDate = this.fiscalYearEndMonth + ' ' + selectedYear; + const displayRange = `${startDate} - ${endDate}`; + this.fiscalYearRangeString = displayRange; + } + + submit() { + this.fiscalYearLockService.lockUnlockFiscalYear(this.modelDate, true); + } +} diff --git a/src/app/lock-records/lock-records.module.ts b/src/app/lock-records/lock-records.module.ts new file mode 100644 index 0000000..096726d --- /dev/null +++ b/src/app/lock-records/lock-records.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DatePickerModule } from '../shared/components/date-picker/date-picker.module'; +import { LockRecordsComponent } from './lock-records.component'; +import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; +import { RouterModule } from '@angular/router'; +import { TextToLoadingSpinnerModule } from '../shared/components/text-to-loading-spinner/text-to-loading-spinner.module'; +import { FiscalYearLockTableComponent } from './fiscal-year-lock-table/fiscal-year-lock-table.component'; +import { TableModule } from '../shared/components/table/table.module'; +import { FiscalYearUnlockerComponent } from './fiscal-year-lock-table/fiscal-year-unlocker/fiscal-year-unlocker.component'; + +@NgModule({ + declarations: [ + LockRecordsComponent, + FiscalYearLockTableComponent, + FiscalYearUnlockerComponent, + ], + imports: [ + CommonModule, + DatePickerModule, + BsDatepickerModule.forRoot(), + RouterModule, + TextToLoadingSpinnerModule, + TableModule, + ], + exports: [LockRecordsComponent], +}) +export class LockRecordsModule {} diff --git a/src/app/resolvers/lock-records.resolver.spec.ts b/src/app/resolvers/lock-records.resolver.spec.ts new file mode 100644 index 0000000..68c6a48 --- /dev/null +++ b/src/app/resolvers/lock-records.resolver.spec.ts @@ -0,0 +1,21 @@ +import { HttpClientModule } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { ConfigService } from '../services/config.service'; + +import { LockRecordsResolver } from './lock-records.resolver'; + +describe('LockRecordsResolver', () => { + let resolver: LockRecordsResolver; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientModule], + providers: [ConfigService] + }); + resolver = TestBed.inject(LockRecordsResolver); + }); + + it('should be created', () => { + expect(resolver).toBeTruthy(); + }); +}); diff --git a/src/app/resolvers/lock-records.resolver.ts b/src/app/resolvers/lock-records.resolver.ts new file mode 100644 index 0000000..0352ba2 --- /dev/null +++ b/src/app/resolvers/lock-records.resolver.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { + Resolve, +} from '@angular/router'; +import { FiscalYearLockService } from '../services/fiscal-year-lock.service'; + +@Injectable({ + providedIn: 'root' +}) +export class LockRecordsResolver implements Resolve { + constructor(private fiscalYearLockService: FiscalYearLockService) {} + resolve() { + this.fiscalYearLockService.fetchFiscalYear(); + } +} + diff --git a/src/app/services/fiscal-year-lock.service.spec.ts b/src/app/services/fiscal-year-lock.service.spec.ts new file mode 100644 index 0000000..f824e84 --- /dev/null +++ b/src/app/services/fiscal-year-lock.service.spec.ts @@ -0,0 +1,21 @@ +import { HttpClientModule } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { ConfigService } from './config.service'; + +import { FiscalYearLockService } from './fiscal-year-lock.service'; + +describe('FiscalYearLockService', () => { + let service: FiscalYearLockService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientModule], + providers: [ConfigService], + }); + service = TestBed.inject(FiscalYearLockService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/fiscal-year-lock.service.ts b/src/app/services/fiscal-year-lock.service.ts new file mode 100644 index 0000000..7077f8e --- /dev/null +++ b/src/app/services/fiscal-year-lock.service.ts @@ -0,0 +1,114 @@ +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { Constants } from '../shared/utils/constants'; +import { ApiService } from './api.service'; +import { DataService } from './data.service'; +import { EventKeywords, EventObject, EventService } from './event.service'; +import { LoadingService } from './loading.service'; +import { ToastService, ToastTypes } from './toast.service'; + +@Injectable({ + providedIn: 'root', +}) +export class FiscalYearLockService { + constructor( + private dataService: DataService, + private apiService: ApiService, + private toastService: ToastService, + private loadingService: LoadingService, + private eventService: EventService + ) {} + + // get/check fiscal years + // passing year = null returns all fiscal year objects + async fetchFiscalYear(year?) { + this.loadingService.addToFetchList( + Constants.dataIds.LOCK_RECORDS_FISCAL_YEARS_DATA + ); + let res; + let errorSubject = ''; + try { + errorSubject = 'lock-records-fiscal-years-data'; + if (year) { + res = await firstValueFrom( + this.apiService.get('fiscalYearEnd', { fiscalYearEnd: year }) + ); + } else { + res = await firstValueFrom(this.apiService.get('fiscalYearEnd')); + } + this.dataService.setItemValue( + Constants.dataIds.LOCK_RECORDS_FISCAL_YEARS_DATA, + res + ); + } catch (e) { + this.toastService.addMessage( + 'Please refresh the page.', + `Error getting ${errorSubject}`, + ToastTypes.ERROR + ); + this.eventService.setError( + new EventObject( + EventKeywords.ERROR, + String(e), + 'Fiscal Year Lock Service' + ) + ); + this.dataService.setItemValue( + Constants.dataIds.LOCK_RECORDS_FISCAL_YEARS_DATA, + 'error' + ); + } + this.loadingService.removeToFetchList( + Constants.dataIds.LOCK_RECORDS_FISCAL_YEARS_DATA + ); + return res; + } + + // lock = true locks, lock = false unlocks + // must provide year. + async lockUnlockFiscalYear(year, lock: boolean) { + this.loadingService.addToFetchList( + Constants.dataIds.LOCK_RECORDS_FISCAL_YEARS_DATA + ); + let res; + let errorSubject = ''; + let subPath = 'lock'; + let ptString = 'locked'; + if (!lock) { + subPath = 'unlock'; + ptString = 'unlocked'; + } + try { + errorSubject = `lock-records-${subPath}-fiscal-year`; + res = await firstValueFrom( + this.apiService.post(`fiscalYearEnd/${subPath}`, null, { + fiscalYearEnd: year, + }) + ); + const prevYear = year - 1; + // trigger refresh of cached list of fetched fiscal year locks + this.fetchFiscalYear(); + this.toastService.addMessage( + `Fiscal year from April ${prevYear} to March ${year} successfully ${ptString}`, + `Fiscal year ${ptString}`, + ToastTypes.SUCCESS + ); + } catch (e) { + this.toastService.addMessage( + `Something went wrong during fiscal year ${subPath}`, + `Error: ${errorSubject}`, + ToastTypes.ERROR + ); + this.eventService.setError( + new EventObject( + EventKeywords.ERROR, + String(e), + 'Fiscal Year Lock Service' + ) + ); + } + this.loadingService.removeToFetchList( + Constants.dataIds.LOCK_RECORDS_FISCAL_YEARS_DATA + ); + } +} diff --git a/src/app/services/keycloak.service.ts b/src/app/services/keycloak.service.ts index fd55579..76dcb8a 100644 --- a/src/app/services/keycloak.service.ts +++ b/src/app/services/keycloak.service.ts @@ -153,7 +153,7 @@ export class KeycloakService { * @memberof KeycloakService */ isAllowed(service): boolean { - if (service !== 'export-reports') { + if (service !== 'export-reports' && service !== 'lock-records') { return true; } const token = this.getToken(); diff --git a/src/app/shared/components/accordion/accordion.component.html b/src/app/shared/components/accordion/accordion.component.html index 7f0958b..1667f6d 100644 --- a/src/app/shared/components/accordion/accordion.component.html +++ b/src/app/shared/components/accordion/accordion.component.html @@ -7,10 +7,16 @@
@@ -56,10 +62,16 @@
diff --git a/src/app/shared/components/accordion/accordion.component.spec.ts b/src/app/shared/components/accordion/accordion.component.spec.ts index 18f58e3..7001436 100644 --- a/src/app/shared/components/accordion/accordion.component.spec.ts +++ b/src/app/shared/components/accordion/accordion.component.spec.ts @@ -1,5 +1,7 @@ +import { HttpClientModule } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; +import { ConfigService } from 'src/app/services/config.service'; import { AccordionComponent } from './accordion.component'; @@ -9,9 +11,9 @@ describe('AccordionComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [RouterTestingModule], + imports: [RouterTestingModule, HttpClientModule], declarations: [AccordionComponent], - providers: [], + providers: [ConfigService], }).compileComponents(); }); diff --git a/src/app/shared/components/accordion/accordion.component.ts b/src/app/shared/components/accordion/accordion.component.ts index a676f92..b4d9ea7 100644 --- a/src/app/shared/components/accordion/accordion.component.ts +++ b/src/app/shared/components/accordion/accordion.component.ts @@ -1,7 +1,9 @@ -import { Component, Input, OnDestroy } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnDestroy } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { Utils } from '../../utils/utils'; import { Subscription } from 'rxjs'; import { DataService } from 'src/app/services/data.service'; +import { FiscalYearLockService } from 'src/app/services/fiscal-year-lock.service'; import { Constants } from '../../utils/constants'; import { summarySection } from './summary-section/summary-section.component'; @@ -18,17 +20,36 @@ export class AccordionComponent implements OnDestroy { @Input() notes: string = ''; @Input() summaries: Array = []; @Input() editLink: string = ''; + @Input() set recordLock(value: boolean) { + if (value !== null){ + this._recordLock = value; + } else { + this._recordLock = true; + } + this.lockRecords(); + this.changeDetectorRef.detectChanges(); + }; - private subscriptions = new Subscription(); + get recordLock(): boolean { + return this._recordLock + } - private formParams; + public _recordLock = true; + + public FISCAL_YEAR_FINAL_MONTH = 3; + private subscriptions = new Subscription(); + private formParams; + private utils = new Utils(); + public isLocked = true; public readonly iconSize = 50; // icon size in px constructor( private router: Router, private activatedRoute: ActivatedRoute, - protected dataService: DataService + private fiscalYearLockService: FiscalYearLockService, + protected dataService: DataService, + private changeDetectorRef: ChangeDetectorRef ) { this.subscriptions.add( dataService @@ -41,12 +62,25 @@ export class AccordionComponent implements OnDestroy { ); } + async lockRecords() { + // extract year from form params + if (this.formParams?.date) { + const year = this.utils.getFiscalYearFromYYYYMM(this.formParams.date); + const lock = await this.fiscalYearLockService.fetchFiscalYear(year); + if (!this._recordLock) { + this.isLocked = lock.isLocked; + } else { + this.isLocked = this._recordLock; + } + } + } + edit() { this.router.navigate([this.editLink], { relativeTo: this.activatedRoute, queryParams: this.formParams, }); - window.scrollTo(0,0); + window.scrollTo(0, 0); } ngOnDestroy() { diff --git a/src/app/shared/components/sidebar/sidebar.component.ts b/src/app/shared/components/sidebar/sidebar.component.ts index efc7b95..385000f 100644 --- a/src/app/shared/components/sidebar/sidebar.component.ts +++ b/src/app/shared/components/sidebar/sidebar.component.ts @@ -26,10 +26,11 @@ export class SidebarComponent implements OnDestroy { protected subAreaService: SubAreaService, protected keyCloakService: KeycloakService ) { - this.routes = router.config.filter(function (obj) { if (obj.path === 'export-reports') { return keyCloakService.isAllowed('export-reports'); + } else if (obj.path === 'lock-records') { + return keyCloakService.isAllowed('lock-records'); } else { return obj.path !== '**' && obj.path !== 'unauthorized'; } diff --git a/src/app/shared/components/table/table-row/table-row.component.html b/src/app/shared/components/table/table-row/table-row.component.html new file mode 100644 index 0000000..804f118 --- /dev/null +++ b/src/app/shared/components/table/table-row/table-row.component.html @@ -0,0 +1,14 @@ + +
+ + + + + {{ rowData[column.id].value }} + +
+ diff --git a/src/app/shared/components/table/table-row/table-row.component.scss b/src/app/shared/components/table/table-row/table-row.component.scss new file mode 100644 index 0000000..a60f17f --- /dev/null +++ b/src/app/shared/components/table/table-row/table-row.component.scss @@ -0,0 +1,3 @@ +.table-cell { + vertical-align: middle; +} \ No newline at end of file diff --git a/src/app/shared/components/table/table-row/table-row.component.spec.ts b/src/app/shared/components/table/table-row/table-row.component.spec.ts new file mode 100644 index 0000000..c7a6035 --- /dev/null +++ b/src/app/shared/components/table/table-row/table-row.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TableRowComponent } from './table-row.component'; + +describe('TableRowComponent', () => { + let component: TableRowComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TableRowComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TableRowComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/table/table-row/table-row.component.ts b/src/app/shared/components/table/table-row/table-row.component.ts new file mode 100644 index 0000000..9094747 --- /dev/null +++ b/src/app/shared/components/table/table-row/table-row.component.ts @@ -0,0 +1,57 @@ +import { + AfterViewChecked, + Component, + Input, + QueryList, + ViewChildren, + ViewContainerRef, +} from '@angular/core'; +import { columnSchema } from '../table.component'; + +@Component({ + // Throws the following linting error: https://angular.io/guide/styleguide#style-05-03 + // This component is given an [attribute] selector because it augments + // the HTML element . We do this so that the child elements of a + // can be custom components while remaining aligned with the parent
component. + // eslint-disable-next-line + selector: '[app-table-row]', + templateUrl: './table-row.component.html', + styleUrls: ['./table-row.component.scss'], +}) +export class TableRowComponent implements AfterViewChecked { + @Input() columnSchema: columnSchema[]; + @Input() rowData: any; + + @ViewChildren('cellTemplateComponent', { read: ViewContainerRef }) + cellTemplateComponents: QueryList; + + ngAfterViewChecked(): void { + this.loadComponents(); + } + + getComponentIdList() { + // gather list of components in the row + const keys = Object.keys(this.rowData); + return keys.filter((e) => this.rowData[e].cellTemplate !== undefined); + } + + // Load components and map them to their respective cells in the row + loadComponents() { + if (this.cellTemplateComponents) { + this.cellTemplateComponents.map( + (vcr: ViewContainerRef, index: number) => { + vcr.clear(); + const componentIdList = this.getComponentIdList(); + const id = componentIdList.filter( + (id) => componentIdList.indexOf(id) === index + )[0]; + const template = this.rowData[id].cellTemplate; + const cellTemplateComponent = vcr.createComponent< + typeof template.component + >(template.component); + cellTemplateComponent.instance.data = this.rowData; + } + ); + } + } +} diff --git a/src/app/shared/components/table/table.component.html b/src/app/shared/components/table/table.component.html new file mode 100644 index 0000000..10222ca --- /dev/null +++ b/src/app/shared/components/table/table.component.html @@ -0,0 +1,29 @@ +
+ + + + + + + + +
+ {{ column.displayHeader }} +
+
+
+
+ {{ emptyTableMsg }} +
+
+
+
diff --git a/src/app/shared/components/table/table.component.scss b/src/app/shared/components/table/table.component.scss new file mode 100644 index 0000000..fa774d2 --- /dev/null +++ b/src/app/shared/components/table/table.component.scss @@ -0,0 +1,5 @@ +@import "src/assets/themes/variables"; + +.header{ + background-color: $form-grey; +} \ No newline at end of file diff --git a/src/app/shared/components/table/table.component.spec.ts b/src/app/shared/components/table/table.component.spec.ts new file mode 100644 index 0000000..257eb1c --- /dev/null +++ b/src/app/shared/components/table/table.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TableComponent } from './table.component'; + +describe('TableComponent', () => { + let component: TableComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TableComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/table/table.component.ts b/src/app/shared/components/table/table.component.ts new file mode 100644 index 0000000..4f4471d --- /dev/null +++ b/src/app/shared/components/table/table.component.ts @@ -0,0 +1,52 @@ +import { Component, Input, OnChanges } from '@angular/core'; + +export interface columnSchema { + id: string; + displayHeader: string; + mapValue: Function; + cellTemplate?: Function; + width?: string; + columnClasses?: string; +} + +@Component({ + selector: 'app-table', + templateUrl: './table.component.html', + styleUrls: ['./table.component.scss'], +}) +export class TableComponent implements OnChanges { + @Input() columnSchema: columnSchema[]; + @Input() data: any[]; + @Input() emptyTableMsg = 'This table is empty.'; + + public columns; + public rows: any = []; + + constructor() {} + + ngOnChanges() { + this.parseData(); + } + + async parseData() { + this.columns = []; + this.rows = []; + this.columns = this.columnSchema.map((id) => id.displayHeader); + if (this.data && this.data.length > 0) { + for (const item of this.data) { + let row: any = {}; + this.columnSchema.map(async (col) => { + // we pass the whole row to column functions + row[col.id] = { + value: col.mapValue(item), + }; + if (col.cellTemplate) { + row[col.id].cellTemplate = col.cellTemplate(item); + } + row['raw'] = item; + }); + this.rows.push(row); + } + } + } +} diff --git a/src/app/shared/components/table/table.module.ts b/src/app/shared/components/table/table.module.ts new file mode 100644 index 0000000..ce6e625 --- /dev/null +++ b/src/app/shared/components/table/table.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TableComponent } from './table.component'; +import { TableRowComponent } from './table-row/table-row.component'; + +@NgModule({ + declarations: [TableComponent, TableRowComponent], + imports: [CommonModule], + exports: [TableComponent], +}) +export class TableModule {} diff --git a/src/app/shared/utils/constants.ts b/src/app/shared/utils/constants.ts index 5599e43..be232c8 100644 --- a/src/app/shared/utils/constants.ts +++ b/src/app/shared/utils/constants.ts @@ -11,12 +11,16 @@ export class Constants { ACCORDION_BACKCOUNTRY_CABINS: 'accordion-Backcountry Cabins', ENTER_DATA_URL_PARAMS: 'enter-data-url-params', EXPORT_ALL_POLLING_DATA: 'export-all-polling-data', + LOCK_RECORDS_FISCAL_YEARS_DATA: 'lock-records-fiscal-years-data', }; public static readonly ApplicationRoles: any = { ADMIN: 'sysadmin', }; + // March + public static readonly FiscalYearFinalMonth: number = 3; + public static readonly iconUrls = { frontcountryCamping: '../../assets/images/walk-in-camping.svg', frontcountryCabins: '../../assets/images/shelter.svg', diff --git a/src/app/shared/utils/utils.ts b/src/app/shared/utils/utils.ts index 6b45535..55ff23f 100644 --- a/src/app/shared/utils/utils.ts +++ b/src/app/shared/utils/utils.ts @@ -1,4 +1,5 @@ import * as moment from 'moment'; +import { Constants } from './constants'; export class Utils { public convertArrayIntoObjForTypeAhead( @@ -45,6 +46,15 @@ export class Utils { }; } + public getFiscalYearFromYYYYMM(date) { + let year = Number(date.substring(0, 4)); + const month = Number(date.slice(-2)); + if (month > Constants.FiscalYearFinalMonth) { + year += 1; + } + return year; + } + public convertJSDateToYYYYMM(date: Date) { return moment(date).format('YYYYMM'); }