diff --git a/src/Client/Logistics.OfficeApp/src/app/core/models/payroll/getPayrollsQuery.ts b/src/Client/Logistics.OfficeApp/src/app/core/models/payroll/getPayrollsQuery.ts new file mode 100644 index 000000000..8dc13cf2f --- /dev/null +++ b/src/Client/Logistics.OfficeApp/src/app/core/models/payroll/getPayrollsQuery.ts @@ -0,0 +1,5 @@ +import {SearchableQuery} from '../searchableQuery'; + +export interface GetPayrollsQuery extends SearchableQuery { + employeeId?: string; +} diff --git a/src/Client/Logistics.OfficeApp/src/app/core/models/payroll/index.ts b/src/Client/Logistics.OfficeApp/src/app/core/models/payroll/index.ts index b75bcd463..11d012198 100644 --- a/src/Client/Logistics.OfficeApp/src/app/core/models/payroll/index.ts +++ b/src/Client/Logistics.OfficeApp/src/app/core/models/payroll/index.ts @@ -1,3 +1,4 @@ export * from './payroll'; export * from './updatePayroll'; export * from './createPayroll'; +export * from './getPayrollsQuery'; diff --git a/src/Client/Logistics.OfficeApp/src/app/core/services/api.service.ts b/src/Client/Logistics.OfficeApp/src/app/core/services/api.service.ts index 390db8091..72f09d142 100644 --- a/src/Client/Logistics.OfficeApp/src/app/core/services/api.service.ts +++ b/src/Client/Logistics.OfficeApp/src/app/core/services/api.service.ts @@ -34,7 +34,6 @@ import { Payment, CreatePayment, UpdatePayment, - PagedQuery, PagedIntervalQuery, Invoice, CreateInvoice, @@ -43,6 +42,7 @@ import { UpdatePayroll, CreatePayroll, ProcessPayment, + GetPayrollsQuery, } from '../models'; @@ -287,7 +287,7 @@ export class ApiService { // #region Payments API - + getPayment(id: string): Observable> { const url = `/payments/${id}`; return this.get(url); @@ -322,7 +322,7 @@ export class ApiService { // #region Invoices API - + getInvoice(id: string): Observable> { const url = `/invoices/${id}`; return this.get(url); @@ -352,14 +352,19 @@ export class ApiService { // #region Payrolls API - + getPayroll(id: string): Observable> { const url = `/payrolls/${id}`; return this.get(url); } - getPayrolls(query?: SearchableQuery): Observable> { - const url = `/payrolls?${this.stringfySearchableQuery(query)}`; + getPayrolls(query?: GetPayrollsQuery): Observable> { + let url = `/payrolls?${this.stringfySearchableQuery(query)}`; + + if (query?.employeeId) { + url += `&employeeId=${query.employeeId}`; + } + return this.get(url); } @@ -435,13 +440,6 @@ export class ApiService { return `search=${search}&orderBy=${orderBy}&page=${page}&pageSize=${pageSize}`; } - private stringfyPagedQuery(query?: PagedQuery): string { - const orderBy = query?.orderBy ?? ''; - const page = query?.page ?? 1; - const pageSize = query?.pageSize ?? 10; - return `orderBy=${orderBy}&page=${page}&pageSize=${pageSize}`; - } - private stringfyPagedIntervalQuery(query?: PagedIntervalQuery): string { const startDate = query?.startDate.toJSON() ?? new Date().toJSON(); const endDate = query?.endDate?.toJSON(); diff --git a/src/Client/Logistics.OfficeApp/src/app/pages/accounting/accounting.routes.ts b/src/Client/Logistics.OfficeApp/src/app/pages/accounting/accounting.routes.ts index 7efc92af3..dcd7c8e51 100644 --- a/src/Client/Logistics.OfficeApp/src/app/pages/accounting/accounting.routes.ts +++ b/src/Client/Logistics.OfficeApp/src/app/pages/accounting/accounting.routes.ts @@ -7,6 +7,7 @@ import {ListInvoicesComponent} from './list-invoices/list-invoices.component'; import {ViewInvoiceComponent} from './view-invoice/view-invoice.component'; import {ListPayrollComponent} from './list-payroll/list-payroll.component'; import {EditPayrollComponent} from './edit-payroll/edit-payroll.component'; +import {ViewEmployeePayrollsComponent} from './view-employee-payrolls/view-employee-payrolls.component'; export const ACCOUNTING_ROUTES: Routes = [ @@ -82,4 +83,13 @@ export const ACCOUNTING_ROUTES: Routes = [ permission: Permissions.Payrolls.Edit, }, }, + { + path: 'employee-payrolls/:employeeId', + component: ViewEmployeePayrollsComponent, + canActivate: [AuthGuard], + data: { + breadcrumb: 'View Employee Payrolls', + permission: Permissions.Payrolls.View, + }, + }, ]; diff --git a/src/Client/Logistics.OfficeApp/src/app/pages/accounting/view-employee-payrolls/view-employee-payrolls.component.html b/src/Client/Logistics.OfficeApp/src/app/pages/accounting/view-employee-payrolls/view-employee-payrolls.component.html new file mode 100644 index 000000000..0ed4d09b7 --- /dev/null +++ b/src/Client/Logistics.OfficeApp/src/app/pages/accounting/view-employee-payrolls/view-employee-payrolls.component.html @@ -0,0 +1,131 @@ +
+

Employee Payrolls

+ + +
+
+ +@if (isLoadingEmployee || !employee) { +
+ +
Loading employee data
+
+} +@else { + + +

Payroll details

+
+
+ +
+
+
+
Employee:
+
+
+ {{employee.fullName}} +
+
+
+
+
Position:
+
+
+ {{employee.roles[0].displayName}} +
+
+
+
+
Salary type:
+
+
+ {{getSalaryTypeDesc(employee.salaryType)}} +
+
+
+
+
Salary:
+
+
+ @if (employee.salaryType === salaryType.ShareOfGross) { + {{employee.salary | percent}} + } + @else { + {{employee.salary | currency}} + } +
+
+
+ +
+} + + +
+
+ + + + + + Period + + + Payment Amount + + + + Payment Status + + + + Payment Method + + + Action + + + + + + {{payroll.startDate | date:'MMM d'}} - {{payroll.endDate | date:'mediumDate'}} + {{payroll.payment.amount | currency}} + + + + {{getPaymentMethodDesc(payroll.payment.method)}} + + @if (payroll.payment.status === paymentStatus.Pending) { + + + } + + + + + + + +
+
+
diff --git a/src/Client/Logistics.OfficeApp/src/app/pages/accounting/view-employee-payrolls/view-employee-payrolls.component.ts b/src/Client/Logistics.OfficeApp/src/app/pages/accounting/view-employee-payrolls/view-employee-payrolls.component.ts new file mode 100644 index 000000000..1c9ec8bc8 --- /dev/null +++ b/src/Client/Logistics.OfficeApp/src/app/pages/accounting/view-employee-payrolls/view-employee-payrolls.component.ts @@ -0,0 +1,107 @@ +import {Component, OnInit} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ActivatedRoute, RouterModule} from '@angular/router'; +import {TableLazyLoadEvent, TableModule} from 'primeng/table'; +import {TooltipModule} from 'primeng/tooltip'; +import {CardModule} from 'primeng/card'; +import {ButtonModule} from 'primeng/button'; +import {ProgressSpinnerModule} from 'primeng/progressspinner'; +import { + SalaryType, + PaymentStatus, + PaymentMethod, + PaymentMethodEnum, + SalaryTypeEnum, +} from '@core/enums'; +import {Employee, Payroll} from '@core/models'; +import {ApiService, ToastService} from '@core/services'; +import {PaymentStatusTagComponent} from '@shared/components'; + + +@Component({ + selector: 'app-view-employee-payrolls', + standalone: true, + templateUrl: './view-employee-payrolls.component.html', + imports: [ + CommonModule, + CardModule, + TooltipModule, + TableModule, + ButtonModule, + RouterModule, + PaymentStatusTagComponent, + ProgressSpinnerModule, + ], +}) +export class ViewEmployeePayrollsComponent implements OnInit { + private employeeId!: string; + public salaryType = SalaryType; + public paymentStatus = PaymentStatus; + public payrolls: Payroll[] = []; + public employee?: Employee; + public isLoadingEmployee = false; + public isLoadingPayrolls = false; + public totalRecords = 0; + public first = 0; + + constructor( + private readonly apiService: ApiService, + private readonly route: ActivatedRoute, + private readonly toastService: ToastService) + { + } + + ngOnInit(): void { + this.route.params.subscribe((params) => { + this.employeeId = params['employeeId']; + }); + + this.fetchEmployee(); + } + + load(event: TableLazyLoadEvent) { + this.isLoadingPayrolls = true; + const first = event.first ?? 1; + const rows = event.rows ?? 10; + const page = first / rows + 1; + const sortField = this.apiService.parseSortProperty(event.sortField as string, event.sortOrder); + + this.apiService.getPayrolls({ + orderBy: sortField, + page: page, + pageSize: rows, + employeeId: this.employeeId, + }).subscribe((result) => { + if (result.isSuccess && result.data) { + this.payrolls = result.data; + this.totalRecords = result.totalItems; + } + + this.isLoadingPayrolls = false; + }); + } + + getPaymentMethodDesc(enumValue?: PaymentMethod): string { + if (enumValue == null) { + return 'N/A'; + } + + return PaymentMethodEnum.getValue(enumValue).description; + } + + getSalaryTypeDesc(enumValue: SalaryType): string { + return SalaryTypeEnum.getValue(enumValue).description; + } + + private fetchEmployee() { + this.isLoadingEmployee = true; + + this.apiService.getEmployee(this.employeeId).subscribe((result) => { + if (result.data) { + this.employee = result.data; + } + + this.isLoadingEmployee = false; + }); + } +} diff --git a/src/Client/Logistics.OfficeApp/src/app/pages/employee/list-employees/list-employees.component.html b/src/Client/Logistics.OfficeApp/src/app/pages/employee/list-employees/list-employees.component.html index 81c5602d2..0b1504af6 100644 --- a/src/Client/Logistics.OfficeApp/src/app/pages/employee/list-employees/list-employees.component.html +++ b/src/Client/Logistics.OfficeApp/src/app/pages/employee/list-employees/list-employees.component.html @@ -79,6 +79,13 @@

List Employees

tooltipPosition="bottom" [routerLink]="['/employees/edit', employee.id]"> + + diff --git a/src/Core/Logistics.Application.Tenant/Queries/Payroll/GetPayrolls/GetPayrollsHandler.cs b/src/Core/Logistics.Application.Tenant/Queries/Payroll/GetPayrolls/GetPayrollsHandler.cs index a544f2c47..ae350e706 100644 --- a/src/Core/Logistics.Application.Tenant/Queries/Payroll/GetPayrolls/GetPayrollsHandler.cs +++ b/src/Core/Logistics.Application.Tenant/Queries/Payroll/GetPayrolls/GetPayrollsHandler.cs @@ -3,29 +3,34 @@ namespace Logistics.Application.Tenant.Queries; -internal sealed class GetPayrollsHandler : RequestHandler> +internal sealed class GetPayrollsHandler(ITenantRepository tenantRepository) + : RequestHandler> { - private readonly ITenantRepository _tenantRepository; - - public GetPayrollsHandler(ITenantRepository tenantRepository) - { - _tenantRepository = tenantRepository; - } - - protected override Task> HandleValidated( + protected override async Task> HandleValidated( GetPayrollsQuery req, CancellationToken cancellationToken) { - var totalItems = _tenantRepository.Query().Count(); - var specification = new GetPayrolls(req.Search, req.OrderBy, req.Descending); + int totalItems; + BaseSpecification specification; + + if (!string.IsNullOrEmpty(req.EmployeeId)) + { + specification = new GetEmployeePayrolls(req.EmployeeId, req.OrderBy, req.Descending); + totalItems = await tenantRepository.CountAsync(i => i.EmployeeId == req.EmployeeId); + } + else + { + specification = new GetPayrolls(req.Search, req.OrderBy, req.Descending); + totalItems = await tenantRepository.CountAsync(); + } - var payrolls = _tenantRepository.ApplySpecification(specification) + var payrolls = tenantRepository.ApplySpecification(specification) .Skip((req.Page - 1) * req.PageSize) .Take(req.PageSize) .Select(i => i.ToDto()) .ToArray(); var totalPages = (int)Math.Ceiling(totalItems / (double)req.PageSize); - return Task.FromResult(PagedResponseResult.Create(payrolls, totalItems, totalPages)); + return PagedResponseResult.Create(payrolls, totalItems, totalPages); } } diff --git a/src/Core/Logistics.Application.Tenant/Queries/Payroll/GetPayrolls/GetPayrollsQuery.cs b/src/Core/Logistics.Application.Tenant/Queries/Payroll/GetPayrolls/GetPayrollsQuery.cs index 567fce821..f92529661 100644 --- a/src/Core/Logistics.Application.Tenant/Queries/Payroll/GetPayrolls/GetPayrollsQuery.cs +++ b/src/Core/Logistics.Application.Tenant/Queries/Payroll/GetPayrolls/GetPayrollsQuery.cs @@ -5,4 +5,5 @@ namespace Logistics.Application.Tenant.Queries; public class GetPayrollsQuery : SearchableQuery, IRequest> { + public string? EmployeeId { get; set; } } diff --git a/src/Core/Logistics.Domain/Specifications/GetEmployeePayrolls.cs b/src/Core/Logistics.Domain/Specifications/GetEmployeePayrolls.cs new file mode 100644 index 000000000..f590568c9 --- /dev/null +++ b/src/Core/Logistics.Domain/Specifications/GetEmployeePayrolls.cs @@ -0,0 +1,35 @@ +using System.Linq.Expressions; +using Logistics.Domain.Entities; + +namespace Logistics.Domain.Specifications; + +public class GetEmployeePayrolls : BaseSpecification +{ + public GetEmployeePayrolls( + string employeeId, + string? orderBy, + bool descending = false) + { + Descending = descending; + OrderBy = InitOrderBy(orderBy); + Criteria = i => i.EmployeeId == employeeId; + } + + private static Expression> InitOrderBy(string? propertyName) + { + propertyName = propertyName?.ToLower() ?? string.Empty; + return propertyName switch + { + "paymentamount" => i => i.Payment.Amount, + "paymentdate" => i => i.Payment.PaymentDate, + "paymentmethod" => i => i.Payment.Method, + "paymentstatus" => i => i.Payment.Status, + "employeefirstname" => i => i.Employee.FirstName, + "employeelastname" => i => i.Employee.LastName, + "employeeemail" => i => i.Employee.Email, + "employeesalary" => i => i.Employee.Salary, + "employeesalarytype" => i => i.Employee.SalaryType, + _ => i => i.Payment.Status + }; + } +} diff --git a/src/Core/Logistics.Domain/Specifications/GetPayrolls.cs b/src/Core/Logistics.Domain/Specifications/GetPayrolls.cs index adcc39e36..f1a7ffb1a 100644 --- a/src/Core/Logistics.Domain/Specifications/GetPayrolls.cs +++ b/src/Core/Logistics.Domain/Specifications/GetPayrolls.cs @@ -7,7 +7,7 @@ public class GetPayrolls : BaseSpecification { public GetPayrolls( string? search, - string? orderBy, + string? orderBy, bool descending = false) { Descending = descending;