Created a period table for date ranges and made the recipes dependent on it instead of having arbitrary date ranges. This will enable easy copying into new months.

This commit is contained in:
2022-07-28 22:16:28 +05:30
parent 492b80e116
commit 3eb715e2de
68 changed files with 996 additions and 353 deletions
@@ -0,0 +1,41 @@
<mat-card>
<mat-card-title-group>
<mat-card-title>Period</mat-card-title>
</mat-card-title-group>
<mat-card-content>
<form [formGroup]="form" fxLayout="column">
<div fxLayout="row">
<mat-form-field fxFlex>
<input
matInput
[matDatepicker]="validFrom"
placeholder="Valid From"
formControlName="validFrom"
autocomplete="off"
/>
<mat-datepicker-toggle matSuffix [for]="validFrom"></mat-datepicker-toggle>
<mat-datepicker #validFrom></mat-datepicker>
</mat-form-field>
</div>
<div fxLayout="row">
<mat-form-field fxFlex>
<input
matInput
[matDatepicker]="validTill"
placeholder="Valid Till"
formControlName="validTill"
autocomplete="off"
/>
<mat-datepicker-toggle matSuffix [for]="validTill"></mat-datepicker-toggle>
<mat-datepicker #validTill></mat-datepicker>
</mat-form-field>
</div>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" (click)="save()">Save</button>
<button mat-raised-button color="warn" (click)="confirmDelete()" *ngIf="!!item.id">
Delete
</button>
</mat-card-actions>
</mat-card>
@@ -0,0 +1,28 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { MatDialogModule } from '@angular/material/dialog';
import { RouterTestingModule } from '@angular/router/testing';
import { PeriodDetailComponent } from './period-detail.component';
describe('PeriodDetailComponent', () => {
let component: PeriodDetailComponent;
let fixture: ComponentFixture<PeriodDetailComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [MatDialogModule, ReactiveFormsModule, RouterTestingModule],
declarations: [PeriodDetailComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PeriodDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,97 @@
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import * as moment from 'moment';
import { ToasterService } from '../../core/toaster.service';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
import { Period } from '../period';
import { PeriodService } from '../period.service';
@Component({
selector: 'app-period-detail',
templateUrl: './period-detail.component.html',
styleUrls: ['./period-detail.component.css'],
})
export class PeriodDetailComponent implements OnInit {
form: FormGroup<{
validFrom: FormControl<Date>;
validTill: FormControl<Date>;
}>;
item: Period = new Period();
constructor(
private route: ActivatedRoute,
private router: Router,
private dialog: MatDialog,
private toaster: ToasterService,
private ser: PeriodService,
) {
this.form = new FormGroup({
validFrom: new FormControl(new Date(), { nonNullable: true }),
validTill: new FormControl(new Date(), { nonNullable: true }),
});
}
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { item: Period };
this.showItem(data.item);
});
}
showItem(item: Period) {
this.item = item;
this.form.setValue({
validFrom: moment(this.item.validFrom, 'DD-MMM-YYYY').toDate(),
validTill: moment(this.item.validTill, 'DD-MMM-YYYY').toDate(),
});
}
save() {
this.ser.saveOrUpdate(this.getItem()).subscribe(
() => {
this.toaster.show('Success', '');
this.router.navigateByUrl('/periods');
},
(error) => {
this.toaster.show('Danger', error);
},
);
}
delete() {
this.ser.delete(this.item.id as string).subscribe(
() => {
this.toaster.show('Success', '');
this.router.navigateByUrl('/periods');
},
(error) => {
this.toaster.show('Danger', error);
},
);
}
confirmDelete(): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '250px',
data: { title: 'Delete Period?', content: 'Are you sure? This cannot be undone.' },
});
dialogRef.afterClosed().subscribe((result: boolean) => {
if (result) {
this.delete();
}
});
}
getItem(): Period {
const formValue = this.form.value;
this.item.validFrom = moment(formValue.validFrom).format('DD-MMM-YYYY');
this.item.validTill = moment(formValue.validTill).format('DD-MMM-YYYY');
return this.item;
}
}
@@ -0,0 +1,17 @@
import { HttpClientModule } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing';
import { PeriodListResolver } from './period-list-resolver.service';
describe('PeriodListResolver', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
providers: [PeriodListResolver],
});
});
it('should be created', inject([PeriodListResolver], (service: PeriodListResolver) => {
expect(service).toBeTruthy();
}));
});
@@ -0,0 +1,17 @@
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable } from 'rxjs/internal/Observable';
import { Period } from './period';
import { PeriodService } from './period.service';
@Injectable({
providedIn: 'root',
})
export class PeriodListResolver implements Resolve<Period[]> {
constructor(private ser: PeriodService) {}
resolve(): Observable<Period[]> {
return this.ser.list();
}
}
@@ -0,0 +1,16 @@
import { DataSource } from '@angular/cdk/collections';
import { Observable, of as observableOf } from 'rxjs';
import { Period } from '../period';
export class PeriodListDataSource extends DataSource<Period> {
constructor(public data: Period[]) {
super();
}
connect(): Observable<Period[]> {
return observableOf(this.data);
}
disconnect() {}
}
@@ -0,0 +1,31 @@
<mat-card>
<mat-card-title-group>
<mat-card-title>Periods</mat-card-title>
<a mat-button [routerLink]="['/periods', 'new']">
<mat-icon>add_box</mat-icon>
Add
</a>
</mat-card-title-group>
<mat-card-content>
<mat-table #table [dataSource]="dataSource" aria-label="Elements">
<!-- Valid From Column -->
<ng-container matColumnDef="validFrom">
<mat-header-cell *matHeaderCellDef>Valid From</mat-header-cell>
<mat-cell *matCellDef="let row">
<a [routerLink]="['/periods', row.id]">
{{ row.validFrom }}
</a>
</mat-cell>
</ng-container>
<!-- Valid Till Column -->
<ng-container matColumnDef="validTill">
<mat-header-cell *matHeaderCellDef>Valid Till</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.validTill }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
</mat-card-content>
</mat-card>
@@ -0,0 +1,25 @@
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { PeriodListComponent } from './period-list.component';
describe('PeriodListComponent', () => {
let component: PeriodListComponent;
let fixture: ComponentFixture<PeriodListComponent>;
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [ReactiveFormsModule, RouterTestingModule],
declarations: [PeriodListComponent],
}).compileComponents();
fixture = TestBed.createComponent(PeriodListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should compile', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,33 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Period } from '../period';
import { PeriodListDataSource } from './period-list-datasource';
@Component({
selector: 'app-period-list',
templateUrl: './period-list.component.html',
styleUrls: ['./period-list.component.css'],
})
export class PeriodListComponent implements OnInit {
dataSource: PeriodListDataSource;
list: Period[] = [];
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['validFrom', 'validTill'];
constructor(private route: ActivatedRoute) {
this.dataSource = new PeriodListDataSource(this.list);
}
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { list: Period[] };
this.list = data.list;
});
this.dataSource = new PeriodListDataSource(this.list);
}
}
@@ -0,0 +1,18 @@
import { HttpClientModule } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { PeriodResolver } from './period-resolver.service';
describe('PeriodResolver', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule, RouterTestingModule],
providers: [PeriodResolver],
});
});
it('should be created', inject([PeriodResolver], (service: PeriodResolver) => {
expect(service).toBeTruthy();
}));
});
@@ -0,0 +1,18 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router';
import { Observable } from 'rxjs/internal/Observable';
import { Period } from './period';
import { PeriodService } from './period.service';
@Injectable({
providedIn: 'root',
})
export class PeriodResolver implements Resolve<Period> {
constructor(private ser: PeriodService, private router: Router) {}
resolve(route: ActivatedRouteSnapshot): Observable<Period> {
const id = route.paramMap.get('id');
return this.ser.get(id);
}
}
@@ -0,0 +1,13 @@
import { PeriodRoutingModule } from './period-routing.module';
describe('PeriodRoutingModule', () => {
let periodRoutingModule: PeriodRoutingModule;
beforeEach(() => {
periodRoutingModule = new PeriodRoutingModule();
});
it('should create an instance', () => {
expect(periodRoutingModule).toBeTruthy();
});
});
@@ -0,0 +1,56 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '../auth/auth-guard.service';
import { CostCentreListResolver } from '../cost-centre/cost-centre-list-resolver.service';
import { PeriodDetailComponent } from './period-detail/period-detail.component';
import { PeriodListResolver } from './period-list-resolver.service';
import { PeriodListComponent } from './period-list/period-list.component';
import { PeriodResolver } from './period-resolver.service';
const periodRoutes: Routes = [
{
path: '',
component: PeriodListComponent,
canActivate: [AuthGuard],
data: {
permission: 'Recipes',
},
resolve: {
list: PeriodListResolver,
},
},
{
path: 'new',
component: PeriodDetailComponent,
canActivate: [AuthGuard],
data: {
permission: 'Recipes',
},
resolve: {
item: PeriodResolver,
costCentres: CostCentreListResolver,
},
},
{
path: ':id',
component: PeriodDetailComponent,
canActivate: [AuthGuard],
data: {
permission: 'Recipes',
},
resolve: {
item: PeriodResolver,
costCentres: CostCentreListResolver,
},
},
];
@NgModule({
imports: [CommonModule, RouterModule.forChild(periodRoutes)],
exports: [RouterModule],
providers: [PeriodListResolver, PeriodResolver],
})
export class PeriodRoutingModule {}
@@ -0,0 +1,13 @@
import { PeriodModule } from './period.module';
describe('PeriodModule', () => {
let periodModule: PeriodModule;
beforeEach(() => {
periodModule = new PeriodModule();
});
it('should create an instance', () => {
expect(periodModule).toBeTruthy();
});
});
+69
View File
@@ -0,0 +1,69 @@
import { CdkTableModule } from '@angular/cdk/table';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MomentDateAdapter } from '@angular/material-moment-adapter';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import {
DateAdapter,
MAT_DATE_FORMATS,
MAT_DATE_LOCALE,
MatOptionModule,
} from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { PeriodDetailComponent } from './period-detail/period-detail.component';
import { PeriodListComponent } from './period-list/period-list.component';
import { PeriodRoutingModule } from './period-routing.module';
export const MY_FORMATS = {
parse: {
dateInput: 'DD-MMM-YYYY',
},
display: {
dateInput: 'DD-MMM-YYYY',
monthYearLabel: 'MMM YYYY',
dateA11yLabel: 'DD-MMM-YYYY',
monthYearA11yLabel: 'MMM YYYY',
},
};
@NgModule({
imports: [
CommonModule,
CdkTableModule,
FlexLayoutModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
MatCardModule,
MatDatepickerModule,
MatDialogModule,
MatProgressSpinnerModule,
MatInputModule,
MatButtonModule,
MatIconModule,
MatOptionModule,
MatSelectModule,
MatCheckboxModule,
ReactiveFormsModule,
PeriodRoutingModule,
],
declarations: [PeriodListComponent, PeriodDetailComponent],
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
{ provide: MAT_DATE_FORMATS, useValue: MY_FORMATS },
],
})
export class PeriodModule {}
@@ -0,0 +1,17 @@
import { HttpClientModule } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing';
import { PeriodService } from './period.service';
describe('PeriodService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
providers: [PeriodService],
});
});
it('should be created', inject([PeriodService], (service: PeriodService) => {
expect(service).toBeTruthy();
}));
});
+54
View File
@@ -0,0 +1,54 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators';
import { ErrorLoggerService } from '../core/error-logger.service';
import { Period } from './period';
const url = '/api/periods';
const serviceName = 'PeriodService';
@Injectable({ providedIn: 'root' })
export class PeriodService {
constructor(private http: HttpClient, private log: ErrorLoggerService) {}
get(id: string | null): Observable<Period> {
const getUrl: string = id === null ? `${url}` : `${url}/${id}`;
return this.http
.get<Period>(getUrl)
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Period>;
}
list(): Observable<Period[]> {
return this.http
.get<Period[]>(`${url}/list`)
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Period[]>;
}
save(period: Period): Observable<Period> {
return this.http
.post<Period>(`${url}`, period)
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<Period>;
}
update(period: Period): Observable<Period> {
return this.http
.put<Period>(`${url}/${period.id}`, period)
.pipe(catchError(this.log.handleError(serviceName, 'update'))) as Observable<Period>;
}
saveOrUpdate(period: Period): Observable<Period> {
if (!period.id) {
return this.save(period);
}
return this.update(period);
}
delete(id: string): Observable<Period> {
return this.http
.delete<Period>(`${url}/${id}`)
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<Period>;
}
}
+11
View File
@@ -0,0 +1,11 @@
export class Period {
id: string | undefined;
validFrom: string;
validTill: string;
public constructor(init?: Partial<Period>) {
this.validFrom = '';
this.validTill = '';
Object.assign(this, init);
}
}