Feature: Added a Mozimo Daily Register which shows products from Menu Items category

This commit is contained in:
2024-08-19 17:01:04 +05:30
parent 860c7d39ef
commit 18524a5f47
19 changed files with 915 additions and 21 deletions

View File

@ -89,6 +89,10 @@ export const routes: Routes = [
path: 'ledger',
loadChildren: () => import('./ledger/ledger.routes').then((mod) => mod.routes),
},
{
path: 'mozimo-daily-register',
loadChildren: () => import('./mozimo-daily-register/mozimo-daily-register.routes').then((mod) => mod.routes),
},
{
path: 'mozimo-product-register',
loadChildren: () => import('./mozimo-product-register/mozimo-product-register.routes').then((mod) => mod.routes),

View File

@ -36,6 +36,7 @@
<a mat-menu-item routerLink="/batch-integrity-report">Batch Integrity</a>
<a mat-menu-item routerLink="/non-contract-purchase">Non Contract Purchases</a>
<a mat-menu-item routerLink="/mozimo-product-register">Mozimo Product Register</a>
<a mat-menu-item routerLink="/mozimo-daily-register">Mozimo Daily Register</a>
</mat-menu>
<button mat-button [matMenuTriggerFor]="productReportMenu">Product Reports</button>

View File

@ -0,0 +1,63 @@
import { DataSource } from '@angular/cdk/collections';
import { EventEmitter } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { merge, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { MozimoDailyRegisterItem } from './mozimo-daily-register-item';
export class MozimoDailyRegisterDataSource extends DataSource<MozimoDailyRegisterItem> {
public data: MozimoDailyRegisterItem[] = [];
public paginator?: MatPaginator;
constructor(public dataObs: Observable<MozimoDailyRegisterItem[]>) {
super();
}
connect(): Observable<MozimoDailyRegisterItem[]> {
const dataMutations: EventEmitter<PageEvent>[] = [];
const d = this.dataObs.pipe(
tap((x) => {
this.data = x;
}),
);
if (this.paginator) {
dataMutations.push((this.paginator as MatPaginator).page);
}
return merge(d, ...dataMutations).pipe(
map(() => this.calculate([...this.data])),
tap(() => {
if (this.paginator) {
this.paginator.length = this.data.length;
}
}),
map((x: MozimoDailyRegisterItem[]) => this.getPagedData(x)),
);
}
disconnect() {}
private calculate(data: MozimoDailyRegisterItem[]): MozimoDailyRegisterItem[] {
if (data.length === 0) {
return data;
}
data.forEach((item) => {
if (item.ageing !== null || item.display !== null) {
item.closing = (item.ageing ?? 0) + (item.display ?? 0);
item.variance = item.opening + item.received - item.sale - item.nc - item.closing;
} else {
item.closing = item.opening + item.received - item.sale - item.nc;
item.variance = 0;
}
});
return data;
}
private getPagedData(data: MozimoDailyRegisterItem[]) {
if (this.paginator === undefined) {
return data;
}
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
return data.splice(startIndex, this.paginator.pageSize);
}
}

View File

@ -0,0 +1,30 @@
import { Product } from '../core/product';
export class MozimoDailyRegisterItem {
id: string | null;
product: Product;
opening: number;
received: number;
sale: number;
nc: number;
display: number | null;
ageing: number | null;
variance: number | null;
closing: number;
lastEditDate: string | null;
public constructor(init?: Partial<MozimoDailyRegisterItem>) {
this.id = null;
this.product = new Product();
this.opening = 0;
this.received = 0;
this.sale = 0;
this.nc = 0;
this.display = null;
this.ageing = null;
this.variance = null;
this.closing = 0;
this.lastEditDate = null;
Object.assign(this, init);
}
}

View File

@ -0,0 +1,17 @@
.right {
display: flex;
justify-content: flex-end;
}
.first {
margin-right: 4px;
}
.middle {
margin-left: 4px;
margin-right: 4px;
}
.last {
margin-left: 4px;
}

View File

@ -0,0 +1,136 @@
<mat-card>
<mat-card-header>
<mat-card-title-group>
<mat-card-title>Mozimo Product Register</mat-card-title>
@if (dataSource.data.length) {
<button mat-icon-button (click)="exportCsv()">
<mat-icon>save_alt</mat-icon>
</button>
}
</mat-card-title-group>
</mat-card-header>
<mat-card-content>
<form [formGroup]="form" class="flex flex-col">
<div class="flex flex-row justify-around content-start items-start sm:max-lg:flex-col">
<mat-form-field class="flex-auto basis-4/5 mr-5">
<mat-label>Date</mat-label>
<input
matInput
#dateElement
[matDatepicker]="date"
(focus)="dateElement.select()"
formControlName="date"
autocomplete="off"
/>
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
<mat-datepicker #date></mat-datepicker>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1/5" color="primary" (click)="show()">Show</button>
</div>
<mat-table #table [dataSource]="dataSource" aria-label="Elements" formArrayName="items">
<!-- Product Column -->
<ng-container matColumnDef="product">
<mat-header-cell *matHeaderCellDef class="center first">Product</mat-header-cell>
<mat-cell *matCellDef="let row" class="center first">
<span
matBadge="1"
matBadgeSize="small"
[matBadgeHidden]="!row.lastEditDate"
matTooltip="{{ row.lastEditDate | localTime }}"
[matTooltipDisabled]="!row.lastEditDate"
>{{ row.product.name }}</span
>
</mat-cell>
</ng-container>
<!-- Opening Column -->
<ng-container matColumnDef="opening">
<mat-header-cell *matHeaderCellDef class="right middle">Opening</mat-header-cell>
<mat-cell *matCellDef="let row" class="right middle">{{ row.opening | number: '0.2-2' }}</mat-cell>
</ng-container>
<!-- Received Column -->
<ng-container matColumnDef="received">
<mat-header-cell *matHeaderCellDef class="middle">Received</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Received</mat-label>
<input matInput type="number" formControlName="received" (change)="updateReceived($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Sale Column -->
<ng-container matColumnDef="sale">
<mat-header-cell *matHeaderCellDef class="middle">Sale</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Sale</mat-label>
<input matInput type="number" formControlName="sale" (change)="updateSale($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Nc Column -->
<ng-container matColumnDef="nc">
<mat-header-cell *matHeaderCellDef class="middle">Nc</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Nc</mat-label>
<input matInput type="number" formControlName="nc" (change)="updateNc($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Display Column -->
<ng-container matColumnDef="display">
<mat-header-cell *matHeaderCellDef class="middle">Display</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Display</mat-label>
<input matInput type="number" formControlName="display" (change)="updateDisplay($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Ageing Column -->
<ng-container matColumnDef="ageing">
<mat-header-cell *matHeaderCellDef class="middle">Ageing</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
<mat-form-field class="flex-auto">
<mat-label>Ageing</mat-label>
<input matInput type="number" formControlName="ageing" (change)="updateAgeing($event, row)" />
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Variance Column -->
<ng-container matColumnDef="variance">
<mat-header-cell *matHeaderCellDef class="right middle">Variance</mat-header-cell>
<mat-cell *matCellDef="let row" class="right middle">{{ row.variance | number: '0.2-2' }}</mat-cell>
</ng-container>
<!-- Closing Column -->
<ng-container matColumnDef="closing">
<mat-header-cell *matHeaderCellDef class="right last">Closing</mat-header-cell>
<mat-cell *matCellDef="let row" class="right last">{{ row.closing | number: '0.2-2' }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
<mat-paginator
#paginator
[length]="dataSource.data.length"
[pageIndex]="0"
[pageSize]="50"
[pageSizeOptions]="[25, 50, 100, 250, 300, 5000]"
>
</mat-paginator>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" (click)="save()" [disabled]="form.pristine">Save</button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MozimoDailyRegisterComponent } from './mozimo-daily-register.component';
describe('MozimoProductRegisterComponent', () => {
let component: MozimoDailyRegisterComponent;
let fixture: ComponentFixture<MozimoDailyRegisterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MozimoDailyRegisterComponent],
}).compileComponents();
fixture = TestBed.createComponent(MozimoDailyRegisterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,223 @@
import { DecimalPipe, CurrencyPipe, AsyncPipe } from '@angular/common';
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { FormGroup, FormArray, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatBadgeModule } from '@angular/material/badge';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import moment from 'moment';
import { AuthService } from '../auth/auth.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ToCsvService } from '../shared/to-csv.service';
import { MozimoDailyRegister } from './mozimo-daily-register';
import { MozimoDailyRegisterDataSource } from './mozimo-daily-register-datasource';
import { MozimoDailyRegisterItem } from './mozimo-daily-register-item';
import { MozimoDailyRegisterService } from './mozimo-daily-register.service';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { BehaviorSubject } from 'rxjs';
import { LocalTimePipe } from '../shared/local-time.pipe';
import { MatTooltipModule } from '@angular/material/tooltip';
@Component({
selector: 'app-mozimo-daily-register',
templateUrl: './mozimo-daily-register.component.html',
styleUrls: ['./mozimo-daily-register.component.css'],
standalone: true,
imports: [
AsyncPipe,
CurrencyPipe,
DecimalPipe,
LocalTimePipe,
MatAutocompleteModule,
MatBadgeModule,
MatButtonModule,
MatCardModule,
MatDatepickerModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatPaginatorModule,
MatTableModule,
MatTooltipModule,
ReactiveFormsModule,
],
providers: [LocalTimePipe],
})
export class MozimoDailyRegisterComponent implements OnInit {
@ViewChild('dateElement', { static: false }) dateElement!: ElementRef<HTMLInputElement>;
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
info: MozimoDailyRegister = new MozimoDailyRegister();
body = new BehaviorSubject<MozimoDailyRegisterItem[]>([]);
dataSource: MozimoDailyRegisterDataSource = new MozimoDailyRegisterDataSource(this.body);
form: FormGroup<{
date: FormControl<Date>;
items: FormArray<
FormGroup<{
received: FormControl<number>;
sale: FormControl<number>;
nc: FormControl<number>;
display: FormControl<number | null>;
ageing: FormControl<number | null>;
}>
>;
}>;
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['product', 'opening', 'received', 'sale', 'nc', 'display', 'ageing', 'variance', 'closing'];
constructor(
private route: ActivatedRoute,
private router: Router,
private toCsv: ToCsvService,
private localTimePipe: LocalTimePipe,
private snackBar: MatSnackBar,
public auth: AuthService,
private ser: MozimoDailyRegisterService,
) {
this.form = new FormGroup({
date: new FormControl(new Date(), { nonNullable: true }),
items: new FormArray<
FormGroup<{
received: FormControl<number>;
sale: FormControl<number>;
nc: FormControl<number>;
display: FormControl<number | null>;
ageing: FormControl<number | null>;
}>
>([]),
});
}
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { info: MozimoDailyRegister };
this.info = data.info;
this.form.patchValue({
date: moment(this.info.date, 'DD-MMM-YYYY').toDate(),
});
this.form.controls.items.clear();
this.info.body.forEach((x) =>
this.form.controls.items.push(
new FormGroup({
received: new FormControl(x.received, { nonNullable: true, validators: [Validators.min(0)] }),
sale: new FormControl(x.sale, { nonNullable: true, validators: [Validators.min(0)] }),
nc: new FormControl(x.nc, { nonNullable: true, validators: [Validators.min(0)] }),
display: new FormControl(x.display, { nonNullable: false, validators: [Validators.min(0)] }),
ageing: new FormControl(x.ageing, { nonNullable: false, validators: [Validators.min(0)] }),
}),
),
);
if (!this.dataSource.paginator) {
this.dataSource.paginator = this.paginator;
}
this.body.next(this.info.body);
});
}
show() {
const date = moment(this.form.value.date).format('DD-MMM-YYYY');
this.router.navigate(['/mozimo-daily-register', date]);
}
save() {
this.ser.save(this.getMozimoDailyRegister()).subscribe({
next: () => {
this.snackBar.open('', 'Success');
},
error: (error) => {
this.snackBar.open(error, 'Danger');
},
});
}
getMozimoDailyRegister(): MozimoDailyRegister {
const formModel = this.form.value;
this.info.date = moment(formModel.date).format('DD-MMM-YYYY');
const array = this.form.controls.items;
this.info.body.forEach((item, index) => {
item.received = +(array.controls[index].value.received ?? 0);
item.sale = +(array.controls[index].value.sale ?? 0);
item.nc = +(array.controls[index].value.nc ?? 0);
const display = array.controls[index].value.display ?? null;
const ageing = array.controls[index].value.ageing ?? null;
item.display = display == null ? null : +display;
item.ageing = ageing == null ? null : +ageing;
});
return this.info;
}
exportCsv() {
const headers = {
Product: 'product',
Opening: 'opening',
Received: 'received',
Sale: 'sale',
Nc: 'nc',
Display: 'display',
Ageing: 'ageing',
Variance: 'variance',
Closing: 'closing',
'Last Edit Date': 'lastEditDate',
};
const d = JSON.parse(JSON.stringify(this.dataSource.data)).map((x: MozimoDailyRegisterItem) => ({
id: x.id,
product: x.product.name,
opening: x.opening,
received: x.received,
sale: x.sale,
nc: x.nc,
display: x.display,
ageing: x.ageing,
variance: x.variance,
closing: x.closing,
lastEditDate: x.lastEditDate ? this.localTimePipe.transform(x.lastEditDate ?? '') : 'Unsaved',
}));
const csvData = new Blob([this.toCsv.toCsv(headers, d)], {
type: 'text/csv;charset=utf-8;',
});
const link = document.createElement('a');
link.href = window.URL.createObjectURL(csvData);
link.setAttribute('download', 'mozimo-product-register.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
updateReceived($event: Event, row: MozimoDailyRegisterItem) {
row.received = +($event.target as HTMLInputElement).value;
this.body.next(this.info.body);
}
updateSale($event: Event, row: MozimoDailyRegisterItem) {
row.sale = +($event.target as HTMLInputElement).value;
this.body.next(this.info.body);
}
updateNc($event: Event, row: MozimoDailyRegisterItem) {
row.nc = +($event.target as HTMLInputElement).value;
this.body.next(this.info.body);
}
updateDisplay($event: Event, row: MozimoDailyRegisterItem) {
const val = ($event.target as HTMLInputElement).value;
row.display = val === '' ? null : +val;
this.body.next(this.info.body);
}
updateAgeing($event: Event, row: MozimoDailyRegisterItem) {
const val = ($event.target as HTMLInputElement).value;
row.ageing = val === '' ? null : +val;
this.body.next(this.info.body);
}
}

View File

@ -0,0 +1,18 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { MozimoDailyRegister } from './mozimo-daily-register';
import { mozimoDailyRegisterResolver } from './mozimo-daily-register.resolver';
describe('mozimoProductRegisterResolver', () => {
const executeResolver: ResolveFn<MozimoDailyRegister> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => mozimoDailyRegisterResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});

View File

@ -0,0 +1,10 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { MozimoDailyRegister } from './mozimo-daily-register';
import { MozimoDailyRegisterService } from './mozimo-daily-register.service';
export const mozimoDailyRegisterResolver: ResolveFn<MozimoDailyRegister> = (route) => {
const date = route.paramMap.get('date');
return inject(MozimoDailyRegisterService).list(date);
};

View File

@ -0,0 +1,33 @@
import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service';
import { MozimoDailyRegisterComponent } from './mozimo-daily-register.component';
import { mozimoDailyRegisterResolver } from './mozimo-daily-register.resolver';
export const routes: Routes = [
{
path: '',
component: MozimoDailyRegisterComponent,
canActivate: [authGuard],
data: {
permission: 'Product Ledger',
},
resolve: {
info: mozimoDailyRegisterResolver,
},
runGuardsAndResolvers: 'always',
},
{
path: ':date',
component: MozimoDailyRegisterComponent,
canActivate: [authGuard],
data: {
permission: 'Product Ledger',
},
resolve: {
info: mozimoDailyRegisterResolver,
},
runGuardsAndResolvers: 'always',
},
];

View File

@ -0,0 +1,17 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing';
import { MozimoDailyRegisterService } from './mozimo-daily-register.service';
describe('MozimoProductRegisterService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [MozimoDailyRegisterService, provideHttpClient(withInterceptorsFromDi())],
});
});
it('should be created', inject([MozimoDailyRegisterService], (service: MozimoDailyRegisterService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,34 @@
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 { MozimoDailyRegister } from './mozimo-daily-register';
const url = '/api/mozimo-daily-register';
const serviceName = 'MozimoDailyRegisterService';
@Injectable({
providedIn: 'root',
})
export class MozimoDailyRegisterService {
constructor(
private http: HttpClient,
private log: ErrorLoggerService,
) {}
list(date: string | null): Observable<MozimoDailyRegister> {
const listUrl: string = date === null ? url : `${url}/${date}`;
return this.http
.get<MozimoDailyRegister>(listUrl)
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<MozimoDailyRegister>;
}
save(mozimoProductRegister: MozimoDailyRegister): Observable<MozimoDailyRegister> {
return this.http
.post<MozimoDailyRegister>(`${url}/${mozimoProductRegister.date}`, mozimoProductRegister)
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<MozimoDailyRegister>;
}
}

View File

@ -0,0 +1,12 @@
import { MozimoDailyRegisterItem } from './mozimo-daily-register-item';
export class MozimoDailyRegister {
date: string;
body: MozimoDailyRegisterItem[];
public constructor(init?: Partial<MozimoDailyRegister>) {
this.date = '';
this.body = [];
Object.assign(this, init);
}
}

View File

@ -11,7 +11,7 @@ export const routes: Routes = [
component: MozimoProductRegisterComponent,
canActivate: [authGuard],
data: {
permission: 'Ledger',
permission: 'Product Ledger',
},
resolve: {
info: mozimoProductRegisterResolver,
@ -23,8 +23,7 @@ export const routes: Routes = [
component: MozimoProductRegisterComponent,
canActivate: [authGuard],
data: {
// permission: 'Mozimo Product Register',
permission: 'Ledger',
permission: 'Product Ledger',
},
resolve: {
info: mozimoProductRegisterResolver,