@ -0,0 +1,24 @@ |
|||
# Catalog |
|||
|
|||
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.2.0. |
|||
|
|||
## Code scaffolding |
|||
|
|||
Run `ng generate component component-name --project catalog` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project catalog`. |
|||
> Note: Don't forget to add `--project catalog` or else it will be added to the default project in your `angular.json` file. |
|||
|
|||
## Build |
|||
|
|||
Run `ng build catalog` to build the project. The build artifacts will be stored in the `dist/` directory. |
|||
|
|||
## Publishing |
|||
|
|||
After building your library with `ng build catalog`, go to the dist folder `cd dist/catalog` and run `npm publish`. |
|||
|
|||
## Running unit tests |
|||
|
|||
Run `ng test catalog` to execute the unit tests via [Karma](https://karma-runner.github.io). |
|||
|
|||
## Further help |
|||
|
|||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../dist/catalog/config", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
import { ModuleWithProviders, NgModule } from '@angular/core'; |
|||
import { CATALOG_ROUTE_PROVIDERS } from './providers/route.provider'; |
|||
|
|||
@NgModule() |
|||
export class CatalogConfigModule { |
|||
static forRoot(): ModuleWithProviders<CatalogConfigModule> { |
|||
return { |
|||
ngModule: CatalogConfigModule, |
|||
providers: [CATALOG_ROUTE_PROVIDERS], |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
export * from './policy-names'; |
|||
export * from './route-names'; |
|||
@ -0,0 +1,8 @@ |
|||
export const enum eCatalogPolicyNames { |
|||
Catalog = 'CatalogService.Products', |
|||
ProductManagement = 'CatalogService.Products', |
|||
|
|||
ProductManagementCreate = 'CatalogService.Products.Create', |
|||
ProductManagementUpdate = 'CatalogService.Products.Update', |
|||
ProductManagementDelete = 'CatalogService.Products.Delete', |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
export const enum eCatalogRouteNames { |
|||
Catalog = 'CatalogService::Menu:CatalogManagement', |
|||
Products = 'CatalogService::Menu:Products', |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './route.provider'; |
|||
@ -0,0 +1,31 @@ |
|||
import { eLayoutType, RoutesService } from '@abp/ng.core'; |
|||
import { APP_INITIALIZER } from '@angular/core'; |
|||
import { eCatalogPolicyNames } from '../enums/policy-names'; |
|||
import { eCatalogRouteNames } from '../enums/route-names'; |
|||
|
|||
export const CATALOG_ROUTE_PROVIDERS = [ |
|||
{ provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true }, |
|||
]; |
|||
|
|||
export function configureRoutes(routesService: RoutesService) { |
|||
return () => { |
|||
routesService.add([ |
|||
{ |
|||
path: '/catalog', |
|||
name: eCatalogRouteNames.Catalog, |
|||
layout: eLayoutType.application, |
|||
parentName: null, |
|||
iconClass: 'bi bi-collection-fill', |
|||
requiredPolicy: eCatalogPolicyNames.Catalog, |
|||
}, |
|||
{ |
|||
path: '/catalog/products', |
|||
name: eCatalogRouteNames.Products, |
|||
parentName: eCatalogRouteNames.Catalog, |
|||
order: 1, |
|||
requiredPolicy: eCatalogPolicyNames.ProductManagement, |
|||
iconClass: 'bi bi-tag-fill', |
|||
}, |
|||
]); |
|||
}; |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export * from './catalog-config.module'; |
|||
export * from './enums'; |
|||
export * from './providers'; |
|||
@ -0,0 +1,44 @@ |
|||
// Karma configuration file, see link for more information
|
|||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
|||
|
|||
module.exports = function (config) { |
|||
config.set({ |
|||
basePath: '', |
|||
frameworks: ['jasmine', '@angular-devkit/build-angular'], |
|||
plugins: [ |
|||
require('karma-jasmine'), |
|||
require('karma-chrome-launcher'), |
|||
require('karma-jasmine-html-reporter'), |
|||
require('karma-coverage'), |
|||
require('@angular-devkit/build-angular/plugins/karma') |
|||
], |
|||
client: { |
|||
jasmine: { |
|||
// you can add configuration options for Jasmine here
|
|||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
|||
// for example, you can disable the random execution with `random: false`
|
|||
// or set a specific seed with `seed: 4321`
|
|||
}, |
|||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
|||
}, |
|||
jasmineHtmlReporter: { |
|||
suppressAll: true // removes the duplicated traces
|
|||
}, |
|||
coverageReporter: { |
|||
dir: require('path').join(__dirname, '../../coverage/catalog'), |
|||
subdir: '.', |
|||
reporters: [ |
|||
{ type: 'html' }, |
|||
{ type: 'text-summary' } |
|||
] |
|||
}, |
|||
reporters: ['progress', 'kjhtml'], |
|||
port: 9876, |
|||
colors: true, |
|||
logLevel: config.LOG_INFO, |
|||
autoWatch: true, |
|||
browsers: ['Chrome'], |
|||
singleRun: false, |
|||
restartOnFileChange: true |
|||
}); |
|||
}; |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../dist/catalog", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"name": "@eshoponabp/catalog", |
|||
"version": "0.0.1", |
|||
"peerDependencies": { |
|||
"@angular/common": "^12.2.0", |
|||
"@angular/core": "^12.2.0", |
|||
"@abp/ng.theme.shared": "~5.0.0-rc.2", |
|||
"@abp/ng.core": "~5.0.0-rc.2", |
|||
"@ngx-validate/core": "~0.1.1" |
|||
}, |
|||
"dependencies": { |
|||
"tslib": "^2.3.0" |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { RouterModule, Routes } from '@angular/router'; |
|||
|
|||
const routes: Routes = [ |
|||
{ path: '', redirectTo: 'products', pathMatch: 'full' }, |
|||
{ |
|||
path: 'products', |
|||
loadChildren: () => import('./pages/product/product.module').then(m => m.ProductModule), |
|||
}, |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule.forChild(routes)], |
|||
exports: [RouterModule], |
|||
}) |
|||
export class CatalogRoutingModule {} |
|||
@ -0,0 +1,7 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { CatalogRoutingModule } from './catalog-routing.module'; |
|||
|
|||
@NgModule({ |
|||
imports: [CatalogRoutingModule], |
|||
}) |
|||
export class CatalogModule {} |
|||
@ -0,0 +1,17 @@ |
|||
# Proxy Generation Output |
|||
|
|||
This directory includes the output of the latest proxy generation. |
|||
The files and folders in it will be overwritten when proxy generation is run again. |
|||
Therefore, please do not place your own content in this folder. |
|||
|
|||
In addition, `generate-proxy.json` works like a lock file. |
|||
It includes information used by the proxy generator, so please do not delete or modify it. |
|||
|
|||
Finally, the name of the files and folders should not be changed for two reasons: |
|||
- Proxy generator will keep creating them at those paths and you will have multiple copies of the same content. |
|||
- ABP Suite generates files which include imports from this folder. |
|||
|
|||
> **Important Notice:** If you are building a module and are planning to publish to npm, |
|||
> some of the generated proxies are likely to be exported from public-api.ts file. In such a case, |
|||
> please make sure you export files directly and not from barrel exports. In other words, |
|||
> do not include index.ts exports in your public-api.ts exports. |
|||
@ -0,0 +1,2 @@ |
|||
import * as Products from './products'; |
|||
export { Products }; |
|||
@ -0,0 +1,3 @@ |
|||
export * from './models'; |
|||
export * from './product.service'; |
|||
export * from './public-product.service'; |
|||
@ -0,0 +1,24 @@ |
|||
import type { AuditedEntityDto } from '@abp/ng.core'; |
|||
|
|||
export interface CreateProductDto { |
|||
code: string; |
|||
name: string; |
|||
imageName?: string; |
|||
price: number; |
|||
stockCount: number; |
|||
} |
|||
|
|||
export interface ProductDto extends AuditedEntityDto<string> { |
|||
code?: string; |
|||
name?: string; |
|||
imageName?: string; |
|||
price: number; |
|||
stockCount: number; |
|||
} |
|||
|
|||
export interface UpdateProductDto { |
|||
name: string; |
|||
imageName?: string; |
|||
price: number; |
|||
stockCount: number; |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
import type { CreateProductDto, ProductDto, UpdateProductDto } from './models'; |
|||
import { RestService } from '@abp/ng.core'; |
|||
import type { ListResultDto, PagedAndSortedResultRequestDto, PagedResultDto } from '@abp/ng.core'; |
|||
import { Injectable } from '@angular/core'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class ProductService { |
|||
apiName = 'Catalog'; |
|||
|
|||
create = (input: CreateProductDto) => |
|||
this.restService.request<any, ProductDto>({ |
|||
method: 'POST', |
|||
url: '/api/catalog/product', |
|||
body: input, |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
delete = (id: string) => |
|||
this.restService.request<any, void>({ |
|||
method: 'DELETE', |
|||
url: `/api/catalog/product/${id}`, |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
get = (id: string) => |
|||
this.restService.request<any, ProductDto>({ |
|||
method: 'GET', |
|||
url: `/api/catalog/product/${id}`, |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
getList = () => |
|||
this.restService.request<any, ListResultDto<ProductDto>>({ |
|||
method: 'GET', |
|||
url: '/api/catalog/product', |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
getListPaged = (input: PagedAndSortedResultRequestDto) => |
|||
this.restService.request<any, PagedResultDto<ProductDto>>({ |
|||
method: 'GET', |
|||
url: '/api/catalog/product/paged', |
|||
params: { sorting: input.sorting, skipCount: input.skipCount, maxResultCount: input.maxResultCount }, |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
update = (id: string, input: UpdateProductDto) => |
|||
this.restService.request<any, ProductDto>({ |
|||
method: 'PUT', |
|||
url: `/api/catalog/product/${id}`, |
|||
body: input, |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
constructor(private restService: RestService) {} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
import type { ProductDto } from './models'; |
|||
import { RestService } from '@abp/ng.core'; |
|||
import type { ListResultDto } from '@abp/ng.core'; |
|||
import { Injectable } from '@angular/core'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class PublicProductService { |
|||
apiName = 'Catalog'; |
|||
|
|||
get = (id: string) => |
|||
this.restService.request<any, ProductDto>({ |
|||
method: 'GET', |
|||
url: `/api/catalog/public-product/${id}`, |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
getList = () => |
|||
this.restService.request<any, ListResultDto<ProductDto>>({ |
|||
method: 'GET', |
|||
url: '/api/catalog/public-product', |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
constructor(private restService: RestService) {} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './product'; |
|||
@ -0,0 +1,3 @@ |
|||
export * from './product-routing.module'; |
|||
export * from './product.component'; |
|||
export * from './product.module'; |
|||
@ -0,0 +1,22 @@ |
|||
import { AuthGuard, PermissionGuard } from '@abp/ng.core'; |
|||
import { NgModule } from '@angular/core'; |
|||
import { RouterModule, Routes } from '@angular/router'; |
|||
import { eCatalogPolicyNames } from '@eshoponabp/catalog/config'; |
|||
import { ProductComponent } from './product.component'; |
|||
|
|||
const routes: Routes = [ |
|||
{ |
|||
path: '', |
|||
component: ProductComponent, |
|||
canActivate: [AuthGuard, PermissionGuard], |
|||
data: { |
|||
requiredPolicy: eCatalogPolicyNames.ProductManagement, |
|||
}, |
|||
}, |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule.forChild(routes)], |
|||
exports: [RouterModule], |
|||
}) |
|||
export class ProductRoutingModule {} |
|||
@ -0,0 +1,167 @@ |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<div class="row"> |
|||
<div class="col col-md-6"> |
|||
<h5 class="card-title">{{ 'CatalogService::Products' | abpLocalization }}</h5> |
|||
</div> |
|||
<div class="text-right col col-md-6"> |
|||
<button class="btn btn-primary" (click)="onCreate()" *abpPermission="permissions.create"> |
|||
{{ 'CatalogService::NewProduct' | abpLocalization }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="card-body"> |
|||
<ngx-datatable [rows]="items" [count]="count" [list]="list" default> |
|||
<ngx-datatable-column |
|||
[name]="'AbpUi::Actions' | abpLocalization" |
|||
[sortable]="false" |
|||
*abpPermission="permissions.update + ' || ' + permissions.delete" |
|||
> |
|||
<ng-template let-row="row" ngx-datatable-cell-template> |
|||
<div ngbDropdown container="body" class="d-inline-block"> |
|||
<button |
|||
class="btn btn-primary btn-sm dropdown-toggle" |
|||
data-toggle="dropdown" |
|||
aria-haspopup="true" |
|||
ngbDropdownToggle |
|||
> |
|||
<i class="mr-1 fa fa-cog"></i>{{ 'AbpUi::Actions' | abpLocalization }} |
|||
</button> |
|||
<div ngbDropdownMenu> |
|||
<button |
|||
class="dropdown-item" |
|||
(click)="onEdit(row)" |
|||
*abpPermission="permissions.update" |
|||
> |
|||
{{ 'AbpUi::Edit' | abpLocalization }} |
|||
</button> |
|||
<button |
|||
class="dropdown-item" |
|||
(click)="onDelete(row)" |
|||
*abpPermission="permissions.delete" |
|||
> |
|||
{{ 'AbpUi::Delete' | abpLocalization }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
</ngx-datatable-column> |
|||
<ngx-datatable-column |
|||
[name]="'CatalogService::DisplayName:Code' | abpLocalization" |
|||
prop="code" |
|||
></ngx-datatable-column> |
|||
<ngx-datatable-column |
|||
[name]="'CatalogService::DisplayName:Name' | abpLocalization" |
|||
prop="name" |
|||
></ngx-datatable-column> |
|||
<ngx-datatable-column |
|||
[name]="'CatalogService::DisplayName:ImageName' | abpLocalization" |
|||
prop="imageName" |
|||
></ngx-datatable-column> |
|||
<ngx-datatable-column |
|||
[name]="'CatalogService::DisplayName:Price' | abpLocalization" |
|||
prop="price" |
|||
></ngx-datatable-column> |
|||
<ngx-datatable-column |
|||
[name]="'CatalogService::DisplayName:StockCount' | abpLocalization" |
|||
prop="stockCount" |
|||
></ngx-datatable-column> |
|||
</ngx-datatable> |
|||
</div> |
|||
</div> |
|||
|
|||
<abp-modal [(visible)]="isModalVisible" [busy]="modalBusy"> |
|||
<ng-template #abpHeader> |
|||
<h3> |
|||
{{ (selected?.id ? 'AbpUi::Edit' : 'CatalogService::NewProduct') | abpLocalization }} |
|||
</h3> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
<ng-template #loaderRef |
|||
><div class="text-center"><i class="fa fa-pulse fa-spinner"></i></div |
|||
></ng-template> |
|||
|
|||
<form *ngIf="form; else loaderRef" [formGroup]="form" (ngSubmit)="save()"> |
|||
<div class="mb-3"> |
|||
<label class="form-label" for="code"> |
|||
{{ 'CatalogService::DisplayName:Code' | abpLocalization }} * |
|||
</label> |
|||
<input |
|||
type="text" |
|||
class="form-control" |
|||
id="code" |
|||
formControlName="code" |
|||
[readonly]="selected?.id" |
|||
[placeholder]="'CatalogService::DisplayName:Code' | abpLocalization" |
|||
/> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<label class="form-label" for="name"> |
|||
{{ 'CatalogService::DisplayName:Name' | abpLocalization }} * |
|||
</label> |
|||
<input |
|||
type="text" |
|||
class="form-control" |
|||
id="name" |
|||
formControlName="name" |
|||
[placeholder]="'CatalogService::DisplayName:Name' | abpLocalization" |
|||
/> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<label class="form-label" for="imageName"> |
|||
{{ 'CatalogService::DisplayName:ImageName' | abpLocalization }} * |
|||
</label> |
|||
<input |
|||
type="text" |
|||
class="form-control" |
|||
id="imageName" |
|||
formControlName="imageName" |
|||
[placeholder]="'CatalogService::DisplayName:ImageName' | abpLocalization" |
|||
/> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<label class="form-label" for="price"> |
|||
{{ 'CatalogService::DisplayName:Price' | abpLocalization }} * |
|||
</label> |
|||
<input |
|||
type="number" |
|||
class="form-control" |
|||
id="price" |
|||
formControlName="price" |
|||
[placeholder]="'CatalogService::DisplayName:Price' | abpLocalization" |
|||
/> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<label class="form-label" for="stockCount"> |
|||
{{ 'CatalogService::DisplayName:StockCount' | abpLocalization }} * |
|||
</label> |
|||
<input |
|||
type="number" |
|||
class="form-control" |
|||
id="stockCount" |
|||
formControlName="stockCount" |
|||
[placeholder]="'CatalogService::DisplayName:StockCount' | abpLocalization" |
|||
/> |
|||
</div> |
|||
</form> |
|||
|
|||
<ng-template #loaderRef |
|||
><div class="text-center"><i class="fa fa-pulse fa-spinner"></i></div |
|||
></ng-template> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-secondary" abpClose> |
|||
{{ 'AbpUi::Cancel' | abpLocalization }} |
|||
</button> |
|||
<abp-button iconClass="fa fa-check" [disabled]="form?.invalid" (click)="save()">{{ |
|||
'AbpUi::Save' | abpLocalization |
|||
}}</abp-button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
@ -0,0 +1,106 @@ |
|||
import { ListService } from '@abp/ng.core'; |
|||
import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; |
|||
import { Component, OnInit } from '@angular/core'; |
|||
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
|||
import { eCatalogPolicyNames } from '@eshoponabp/catalog/config'; |
|||
import { finalize } from 'rxjs/operators'; |
|||
import { ProductDto } from '../../lib/proxy/products/models'; |
|||
import { ProductService } from '../../lib/proxy/products/product.service'; |
|||
|
|||
@Component({ |
|||
selector: 'lib-product', |
|||
templateUrl: './product.component.html', |
|||
styleUrls: ['./product.component.css'], |
|||
providers: [ListService], |
|||
}) |
|||
export class ProductComponent implements OnInit { |
|||
permissions = { |
|||
create: eCatalogPolicyNames.ProductManagementCreate, |
|||
update: eCatalogPolicyNames.ProductManagementUpdate, |
|||
delete: eCatalogPolicyNames.ProductManagementDelete, |
|||
}; |
|||
|
|||
items: ProductDto[] = []; |
|||
count = 0; |
|||
|
|||
selected: ProductDto; |
|||
|
|||
isModalVisible: boolean; |
|||
|
|||
modalBusy = false; |
|||
|
|||
form: FormGroup; |
|||
constructor( |
|||
public readonly list: ListService, |
|||
private readonly service: ProductService, |
|||
private confirmationService: ConfirmationService, |
|||
private fb: FormBuilder |
|||
) { |
|||
this.list.maxResultCount = 10; |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
const productStreamCreator = query => this.service.getListPaged(query); |
|||
|
|||
this.list.hookToQuery(productStreamCreator).subscribe(response => { |
|||
this.items = response.items; |
|||
this.count = response.totalCount; |
|||
}); |
|||
} |
|||
|
|||
buildForm() { |
|||
this.form = this.fb.group({ |
|||
name: [this.selected.name, { validators: [Validators.required] }], |
|||
code: [this.selected.code, { validators: [Validators.required] }], |
|||
imageName: [this.selected.imageName, { validators: [Validators.required] }], |
|||
price: [this.selected.price, { validators: [Validators.required] }], |
|||
stockCount: [this.selected.stockCount, { validators: [Validators.required] }], |
|||
}); |
|||
} |
|||
|
|||
onEdit(product: ProductDto) { |
|||
this.selected = product; |
|||
this.openModal(); |
|||
} |
|||
|
|||
onCreate() { |
|||
this.selected = {} as ProductDto; |
|||
this.openModal(); |
|||
} |
|||
|
|||
openModal() { |
|||
this.isModalVisible = true; |
|||
this.buildForm(); |
|||
} |
|||
|
|||
onDelete(product: ProductDto) { |
|||
this.confirmationService |
|||
.warn('CatalogService::ProductDeletionConfirmationMessage', 'AbpUi::AreYouSure', { |
|||
messageLocalizationParams: [product.name], |
|||
}) |
|||
.subscribe((status: Confirmation.Status) => { |
|||
if (status === Confirmation.Status.confirm) { |
|||
this.service.delete(product.id).subscribe(() => this.list.get()); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
save() { |
|||
if (!this.form.valid || this.modalBusy) return; |
|||
this.modalBusy = true; |
|||
|
|||
const { id } = this.selected; |
|||
(id |
|||
? this.service.update(id, { |
|||
...this.selected, |
|||
...this.form.value, |
|||
}) |
|||
: this.service.create({ ...this.form.value }) |
|||
) |
|||
.pipe(finalize(() => (this.modalBusy = false))) |
|||
.subscribe(() => { |
|||
this.isModalVisible = false; |
|||
this.list.get(); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
import { CoreModule } from '@abp/ng.core'; |
|||
import { ThemeSharedModule } from '@abp/ng.theme.shared'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { NgModule } from '@angular/core'; |
|||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
import { NgxValidateCoreModule } from '@ngx-validate/core'; |
|||
import { ProductRoutingModule } from './product-routing.module'; |
|||
import { ProductComponent } from './product.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: [ProductComponent], |
|||
imports: [ |
|||
CommonModule, |
|||
ProductRoutingModule, |
|||
ThemeSharedModule, |
|||
CoreModule, |
|||
NgbDropdownModule, |
|||
NgxValidateCoreModule, |
|||
], |
|||
}) |
|||
export class ProductModule {} |
|||
@ -0,0 +1,4 @@ |
|||
export * from './catalog-routing.module'; |
|||
export * from './catalog.module'; |
|||
export * from './lib/proxy/products'; |
|||
export * from './pages'; |
|||
@ -0,0 +1,28 @@ |
|||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
|||
|
|||
import 'zone.js'; |
|||
import 'zone.js/testing'; |
|||
import { getTestBed } from '@angular/core/testing'; |
|||
import { |
|||
BrowserDynamicTestingModule, |
|||
platformBrowserDynamicTesting |
|||
} from '@angular/platform-browser-dynamic/testing'; |
|||
|
|||
declare const require: { |
|||
context(path: string, deep?: boolean, filter?: RegExp): { |
|||
keys(): string[]; |
|||
<T>(id: string): T; |
|||
}; |
|||
}; |
|||
|
|||
// First, initialize the Angular testing environment.
|
|||
getTestBed().initTestEnvironment( |
|||
BrowserDynamicTestingModule, |
|||
platformBrowserDynamicTesting(), |
|||
{ teardown: { destroyAfterEach: true }}, |
|||
); |
|||
|
|||
// Then we find all the tests.
|
|||
const context = require.context('./', true, /\.spec\.ts$/); |
|||
// And load the modules.
|
|||
context.keys().map(context); |
|||
@ -0,0 +1,20 @@ |
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */ |
|||
{ |
|||
"extends": "../../tsconfig.json", |
|||
"compilerOptions": { |
|||
"outDir": "../../out-tsc/lib", |
|||
"target": "es2015", |
|||
"declaration": true, |
|||
"declarationMap": true, |
|||
"inlineSources": true, |
|||
"types": [], |
|||
"lib": [ |
|||
"dom", |
|||
"es2018" |
|||
] |
|||
}, |
|||
"exclude": [ |
|||
"src/test.ts", |
|||
"**/*.spec.ts" |
|||
] |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */ |
|||
{ |
|||
"extends": "./tsconfig.lib.json", |
|||
"compilerOptions": { |
|||
"declarationMap": false |
|||
}, |
|||
"angularCompilerOptions": { |
|||
"compilationMode": "partial" |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */ |
|||
{ |
|||
"extends": "../../tsconfig.json", |
|||
"compilerOptions": { |
|||
"outDir": "../../out-tsc/spec", |
|||
"types": [ |
|||
"jasmine" |
|||
] |
|||
}, |
|||
"files": [ |
|||
"src/test.ts" |
|||
], |
|||
"include": [ |
|||
"**/*.spec.ts", |
|||
"**/*.d.ts" |
|||
] |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using Volo.Abp.AspNetCore.SignalR; |
|||
|
|||
namespace EShopOnAbp.PublicWeb; |
|||
|
|||
public class BasketHub : AbpHub |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System.Threading.Tasks; |
|||
using EShopOnAbp.BasketService; |
|||
using Microsoft.AspNetCore.SignalR; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
|
|||
namespace EShopOnAbp.PublicWeb; |
|||
|
|||
public class BasketProductEventHandler : IDistributedEventHandler<BasketProductUpdatedEto>, ITransientDependency |
|||
{ |
|||
private readonly IHubContext<BasketHub> _hubContext; |
|||
|
|||
public BasketProductEventHandler(IHubContext<BasketHub> hubContext) |
|||
{ |
|||
_hubContext = hubContext; |
|||
} |
|||
|
|||
public async Task HandleEventAsync(BasketProductUpdatedEto eventData) |
|||
{ |
|||
await _hubContext.Clients.All.SendAsync("BasketProductUpdated", eventData); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace EShopOnAbp.PublicWeb.Components.Basket; |
|||
|
|||
[DependsOn(typeof(SignalRBrowserScriptContributor))] |
|||
public class BasketWidgetScriptContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.AddIfNotContains("/components/basket-widget.js"); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
using EShopOnAbp.BasketService; |
|||
|
|||
namespace EShopOnAbp.PublicWeb.Components.Basket; |
|||
|
|||
[Widget( |
|||
AutoInitialize = true, |
|||
RefreshUrl = "/Widgets/Basket", |
|||
StyleFiles = new[] { "/components/basket-widget.css" }, |
|||
ScriptTypes = new[] { typeof(BasketWidgetScriptContributor) } |
|||
)] |
|||
public class BasketWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
private readonly IBasketAppService _basketAppService; |
|||
|
|||
public BasketWidgetViewComponent(IBasketAppService basketAppService) |
|||
{ |
|||
_basketAppService = basketAppService; |
|||
} |
|||
|
|||
public async Task<IViewComponentResult> InvokeAsync() |
|||
{ |
|||
var basketDto = await _basketAppService.GetAsync(); |
|||
return View("~/Components/Basket/Default.cshtml", basketDto); |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
@model EShopOnAbp.BasketService.BasketDto |
|||
<div> |
|||
@if (!Model.Items.Any()) |
|||
{ |
|||
<div class="basket-list"> |
|||
<p class="text-center"><strong><em>Your basket is empty. Click to a product to add it!</em></strong></p> |
|||
</div> |
|||
} |
|||
else |
|||
{ |
|||
<div class="basket-list"> |
|||
@foreach (var item in Model.Items) |
|||
{ |
|||
<div class="basket-list-item" data-product-id="@item.ProductId"> |
|||
@if (!item.ImageName.IsNullOrEmpty()) |
|||
{ |
|||
<img src="/product-images/@item.ImageName"/> |
|||
} |
|||
else |
|||
{ |
|||
<img src="/product-images/noimage.jpg"/> |
|||
} |
|||
@item.ProductName |
|||
@if (item.Count > 1) |
|||
{ |
|||
<text> (@("x" + item.Count))</text> |
|||
} |
|||
- $@item.TotalPrice.ToString("0.00") |
|||
<i class="fa fa-remove basket-item-remove" title="Remove from basket"></i> |
|||
</div> |
|||
} |
|||
<hr/> |
|||
<div class="basket-total-area"> |
|||
Total: $@Model.TotalPrice.ToString("0.00") |
|||
</div> |
|||
<div> |
|||
<abp-button class="basket-purchase-button" text="Purchase" button-type="Primary" /> |
|||
</div> |
|||
</div> |
|||
} |
|||
</div> |
|||
@ -0,0 +1,16 @@ |
|||
using EShopOnAbp.PublicWeb.Components.Basket; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
|
|||
namespace EShopOnAbp.PublicWeb.Controllers; |
|||
|
|||
[Route("Widgets")] |
|||
public class WidgetsController : AbpController |
|||
{ |
|||
[HttpGet] |
|||
[Route("Basket")] |
|||
public IActionResult Counters() |
|||
{ |
|||
return ViewComponent(typeof(BasketWidgetViewComponent)); |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
@page |
|||
@using EShopOnAbp.PublicWeb.Components.Basket |
|||
@model EShopOnAbp.PublicWeb.Pages.Catalog |
|||
@{ |
|||
const int productsColumnSize = 2; |
|||
} |
|||
|
|||
@section styles{ |
|||
<abp-style src="/Pages/Catalog.css"/> |
|||
} |
|||
@section scripts { |
|||
<abp-script src="/client-proxies/basket-proxy.js" /> |
|||
<abp-script src="/Pages/Catalog.cshtml.js" /> |
|||
} |
|||
<div class="row"> |
|||
<div class="col-lg-9"> |
|||
<h3 class="pt-5 pb-4 text-center">Our Products</h3> |
|||
<div class="product-list"> |
|||
@for (int i = 0; i < Math.Ceiling(Model.Products.Count / (double)productsColumnSize); i++) |
|||
{ |
|||
<abp-row> |
|||
@for (int j = 0; j < productsColumnSize; j++) |
|||
{ |
|||
<abp-column size-lg="_6" size-sm="_12"> |
|||
@if ((i * productsColumnSize) + j < Model.Products.Count) |
|||
{ |
|||
var product = Model.Products[(i * productsColumnSize) + j]; |
|||
<div class="product-list-item" data-product-id="@product.Id.ToString()"> |
|||
@if (!product.ImageName.IsNullOrEmpty()) |
|||
{ |
|||
<img src="/product-images/@product.ImageName"/> |
|||
} |
|||
else |
|||
{ |
|||
<img src="/product-images/noimage.jpg"/> |
|||
} |
|||
<div class="row mt-5"> |
|||
<div class="col"> |
|||
<div class="product-info-box"> |
|||
<h6>@product.Name</h6> |
|||
<em class="d-block text-muted">@product.Code - Stock count: @product.StockCount</em> |
|||
</div> |
|||
</div> |
|||
<div class="col-auto text-right text-danger"> |
|||
<strong class="product-price">$@product.Price</strong> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
} |
|||
</abp-column> |
|||
} |
|||
</abp-row> |
|||
} |
|||
</div> |
|||
</div> |
|||
<div class="col-lg-3"> |
|||
<h3 class="pt-5 pb-4 text-center">Your Basket</h3> |
|||
<div id="BasketArea"> |
|||
@await Component.InvokeAsync(typeof(BasketWidgetViewComponent)) |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,22 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc.RazorPages; |
|||
using EShopOnAbp.CatalogService.Products; |
|||
|
|||
namespace EShopOnAbp.PublicWeb.Pages; |
|||
|
|||
public class Catalog : PageModel |
|||
{ |
|||
public IReadOnlyList<ProductDto> Products { get; private set; } |
|||
private readonly IPublicProductAppService _productAppService; |
|||
|
|||
public Catalog(IPublicProductAppService productAppService) |
|||
{ |
|||
_productAppService = productAppService; |
|||
} |
|||
|
|||
public async Task OnGetAsync() |
|||
{ |
|||
Products = (await _productAppService.GetListAsync()).Items; |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
(function(){ |
|||
$(function(){ |
|||
|
|||
var basketWidget = new abp.WidgetManager('#BasketArea'); |
|||
|
|||
$('.product-list-item').click(function(){ |
|||
var $this = $(this); |
|||
var productId = $this.attr('data-product-id'); |
|||
eShopOnAbp.basketService.basket.addProduct({ |
|||
productId: productId |
|||
}).then(function(){ |
|||
basketWidget.refresh(); |
|||
abp.notify.success("Added product to your basket.", "Successfully added"); |
|||
}); |
|||
}); |
|||
}); |
|||
})(); |
|||
@ -0,0 +1,30 @@ |
|||
body { |
|||
background-color: #f9f9f9; |
|||
} |
|||
|
|||
.product-list-item { |
|||
background-color: #fff; |
|||
padding: 2em; |
|||
border: 1px solid #eee; |
|||
border-radius: 3px; |
|||
box-shadow: 0 0 10px rgba(0, 0, 0, .05); |
|||
margin-bottom: 2em; |
|||
transition: all .2s; |
|||
} |
|||
|
|||
.product-list-item:hover { |
|||
box-shadow: 0 0 20px rgba(0, 0, 0, .1); |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.product-list-item img { |
|||
width: 100%; |
|||
} |
|||
|
|||
.product-list-item .text-muted { |
|||
opacity: .8 |
|||
} |
|||
|
|||
.product-info-box { |
|||
min-height: 80px; |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
/* This file is automatically generated by ABP framework to use MVC Controllers from javascript. */ |
|||
|
|||
|
|||
// module basket
|
|||
|
|||
(function(){ |
|||
|
|||
// controller eShopOnAbp.basketService.basket
|
|||
|
|||
(function(){ |
|||
|
|||
abp.utils.createNamespace(window, 'eShopOnAbp.basketService.basket'); |
|||
|
|||
eShopOnAbp.basketService.basket.get = function(ajaxParams) { |
|||
return abp.ajax($.extend(true, { |
|||
url: abp.appPath + 'api/basket/basket', |
|||
type: 'GET' |
|||
}, ajaxParams)); |
|||
}; |
|||
|
|||
eShopOnAbp.basketService.basket.addProduct = function(input, ajaxParams) { |
|||
return abp.ajax($.extend(true, { |
|||
url: abp.appPath + 'api/basket/basket/product', |
|||
type: 'POST', |
|||
data: JSON.stringify(input) |
|||
}, ajaxParams)); |
|||
}; |
|||
|
|||
eShopOnAbp.basketService.basket.removeProduct = function(input, ajaxParams) { |
|||
return abp.ajax($.extend(true, { |
|||
url: abp.appPath + 'api/basket/basket/product' + abp.utils.buildQueryString([{ name: 'productId', value: input.productId }, { name: 'count', value: input.count }]) + '', |
|||
type: 'DELETE' |
|||
}, ajaxParams)); |
|||
}; |
|||
|
|||
eShopOnAbp.basketService.basket.purchase = function(ajaxParams) { |
|||
return abp.ajax($.extend(true, { |
|||
url: abp.appPath + 'api/basket/basket/purchase', |
|||
type: 'POST', |
|||
dataType: null |
|||
}, ajaxParams)); |
|||
}; |
|||
|
|||
})(); |
|||
|
|||
})(); |
|||
|
|||
|
|||
@ -0,0 +1,42 @@ |
|||
.basket-list |
|||
{ |
|||
background-color: #fff; |
|||
padding: 1em; |
|||
border: 1px solid #666; |
|||
border-radius: 3px; |
|||
box-shadow: 0 0 10px rgba(0, 0, 0, .05); |
|||
margin-bottom: 0.5em; |
|||
transition: all .2s; |
|||
} |
|||
|
|||
.basket-list:hover { |
|||
box-shadow: 0 0 20px rgba(0, 0, 0, .1); |
|||
} |
|||
|
|||
.basket-list-item { |
|||
margin-top: 1em; |
|||
} |
|||
|
|||
.basket-list-item:first-child{ |
|||
margin-top: 0; |
|||
} |
|||
|
|||
.basket-list-item img { |
|||
height: 1em; |
|||
} |
|||
|
|||
.basket-total-area |
|||
{ |
|||
margin-bottom: 1em; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.basket-item-remove { |
|||
cursor: pointer; |
|||
color: red; |
|||
opacity: 0.25; |
|||
} |
|||
|
|||
.basket-list-item:hover .basket-item-remove { |
|||
opacity: 1.0; |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
(function (){ |
|||
|
|||
abp.widgets.BasketWidget = function ($wrapper) { |
|||
var widgetManager = $wrapper.data('abp-widget-manager'); |
|||
|
|||
var init = function (filters) { |
|||
$wrapper |
|||
.find('.basket-item-remove') |
|||
.click(function(){ |
|||
var $this = $(this); |
|||
var productId = $this.parent('.basket-list-item').attr('data-product-id'); |
|||
eShopOnAbp.basketService.basket.removeProduct({ |
|||
productId: productId |
|||
}).then(function(){ |
|||
widgetManager.refresh(); |
|||
abp.notify.info("Removed the product from your basket.", "Removed basket item"); |
|||
}); |
|||
}); |
|||
|
|||
$wrapper |
|||
.find('.basket-purchase-button') |
|||
.click(function(){ |
|||
eShopOnAbp.basketService.basket.purchase().then(function(){ |
|||
widgetManager.refresh(); |
|||
abp.notify.info("Thank you for the payment!", "Purchase completed!"); |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
return { |
|||
init: init |
|||
}; |
|||
}; |
|||
|
|||
var connection = new signalR.HubConnectionBuilder() |
|||
.withUrl("/signalr-hubs/basket") |
|||
.build(); |
|||
|
|||
connection.on("BasketProductUpdated", function (data) { |
|||
$('.basket-list') |
|||
.closest('.abp-widget-wrapper') |
|||
.each(function(){ |
|||
var $wrapper = $(this); |
|||
if ($wrapper.find('[data-product-id=' + data.productId + ']').length){ |
|||
var widgetManager = new abp.WidgetManager({ |
|||
wrapper: $wrapper |
|||
}); |
|||
widgetManager.refresh(); |
|||
abp.notify.info('The product "' + data.productName + '" has been changed!', 'Your basket has been updated!'); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
connection.start().then(function () { |
|||
}).catch(function (err) { |
|||
return console.error(err.toString()); |
|||
}); |
|||
|
|||
})(); |
|||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 66 KiB |
@ -0,0 +1,5 @@ |
|||
{ |
|||
"dependencies": { |
|||
"@ngxs/store": "^3.7.3" |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public class AddProductDto |
|||
{ |
|||
public Guid ProductId { get; set; } |
|||
|
|||
[Range(1, int.MaxValue)] |
|||
public int Count { get; set; } = 1; |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public class BasketDto |
|||
{ |
|||
public float TotalPrice { get; set; } |
|||
public List<BasketItemDto> Items { get; set; } |
|||
|
|||
public BasketDto() |
|||
{ |
|||
Items = new List<BasketItemDto>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public class BasketItemDto |
|||
{ |
|||
public Guid ProductId { get; set; } |
|||
public string ProductName { get; set; } |
|||
public string ProductCode { get; set; } |
|||
public string ImageName { get; set; } |
|||
public int Count { get; set; } |
|||
public float TotalPrice { get; set; } |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public interface IBasketAppService : IApplicationService |
|||
{ |
|||
Task<BasketDto> GetAsync(); |
|||
Task<BasketDto> AddProductAsync(AddProductDto input); |
|||
Task<BasketDto> RemoveProductAsync(RemoveProductDto input); |
|||
Task PurchaseAsync(); |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public class RemoveProductDto |
|||
{ |
|||
public Guid ProductId { get; set; } |
|||
|
|||
public int? Count { get; set; } |
|||
} |
|||
@ -0,0 +1,116 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp.Users; |
|||
using EShopOnAbp.CatalogService.Products; |
|||
using Volo.Abp; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
[Authorize] |
|||
public class BasketAppService : BasketServiceAppService, IBasketAppService |
|||
{ |
|||
private readonly IBasketRepository _basketRepository; |
|||
private readonly IBasketProductService _basketProductService; |
|||
private readonly IDistributedEventBus _distributedEventBus; |
|||
|
|||
public BasketAppService( |
|||
IBasketRepository basketRepository, |
|||
IBasketProductService basketProductService, |
|||
IDistributedEventBus distributedEventBus) |
|||
{ |
|||
_basketRepository = basketRepository; |
|||
_basketProductService = basketProductService; |
|||
_distributedEventBus = distributedEventBus; |
|||
} |
|||
|
|||
public async Task<BasketDto> GetAsync() |
|||
{ |
|||
var basket = await _basketRepository.GetAsync(CurrentUser.GetId()); |
|||
return await GetBasketDtoAsync(basket); |
|||
} |
|||
|
|||
public async Task<BasketDto> AddProductAsync(AddProductDto input) |
|||
{ |
|||
var basket = await _basketRepository.GetAsync(CurrentUser.GetId()); |
|||
var product = await _basketProductService.GetAsync(input.ProductId); |
|||
|
|||
if (basket.GetProductCount(product.Id) >= product.StockCount) |
|||
{ |
|||
throw new UserFriendlyException("There is not enough product in stock, sorry :("); |
|||
} |
|||
|
|||
basket.AddProduct(product.Id); |
|||
|
|||
await _basketRepository.UpdateAsync(basket); |
|||
|
|||
return await GetBasketDtoAsync(basket); |
|||
} |
|||
|
|||
public async Task<BasketDto> RemoveProductAsync(RemoveProductDto input) |
|||
{ |
|||
var basket = await _basketRepository.GetAsync(CurrentUser.GetId()); |
|||
var product = await _basketProductService.GetAsync(input.ProductId); |
|||
|
|||
basket.RemoveProduct(product.Id, input.Count); |
|||
|
|||
await _basketRepository.UpdateAsync(basket); |
|||
|
|||
return await GetBasketDtoAsync(basket); |
|||
} |
|||
|
|||
public async Task PurchaseAsync() |
|||
{ |
|||
var basket = await _basketRepository.GetAsync(CurrentUser.GetId()); |
|||
var orderAcceptedEto = ObjectMapper.Map<Basket, OrderAcceptedEto>(basket); |
|||
await _distributedEventBus.PublishAsync(orderAcceptedEto); |
|||
basket.Clear(); |
|||
await _basketRepository.UpdateAsync(basket); |
|||
} |
|||
|
|||
private async Task<BasketDto> GetBasketDtoAsync(Basket basket) |
|||
{ |
|||
var products = new Dictionary<Guid, ProductDto>(); |
|||
var basketDto = new BasketDto(); |
|||
var basketChanged = false; |
|||
|
|||
foreach (var basketItem in basket.Items) |
|||
{ |
|||
var productDto = products.GetOrDefault(basketItem.ProductId); |
|||
if (productDto == null) |
|||
{ |
|||
productDto = await _basketProductService.GetAsync(basketItem.ProductId); |
|||
products[productDto.Id] = productDto; |
|||
} |
|||
|
|||
//Removing the products if not available in the stock
|
|||
if (basketItem.Count > productDto.StockCount) |
|||
{ |
|||
basket.RemoveProduct(basketItem.ProductId, basketItem.Count - productDto.StockCount); |
|||
basketChanged = true; |
|||
} |
|||
|
|||
basketDto.Items.Add(new BasketItemDto |
|||
{ |
|||
ProductId = basketItem.ProductId, |
|||
Count = basketItem.Count, |
|||
ProductCode = productDto.Code, |
|||
ImageName = productDto.ImageName, |
|||
ProductName = productDto.Name, |
|||
TotalPrice = productDto.Price * basketItem.Count |
|||
}); |
|||
} |
|||
|
|||
basketDto.TotalPrice = basketDto.Items.Sum(x => x.TotalPrice); |
|||
|
|||
if (basketChanged) |
|||
{ |
|||
await _basketRepository.UpdateAsync(basket); |
|||
} |
|||
|
|||
return basketDto; |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using EShopOnAbp.CatalogService.Products; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Entities.Events.Distributed; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.ObjectMapping; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public class BasketProductEventHandler : IDistributedEventHandler<EntityUpdatedEto<ProductEto>>, ITransientDependency |
|||
{ |
|||
private readonly IDistributedCache<ProductDto, Guid> _cache; |
|||
private readonly IObjectMapper _objectMapper; |
|||
private readonly IDistributedEventBus _distributedEventBus; |
|||
|
|||
public BasketProductEventHandler( |
|||
IDistributedCache<ProductDto, Guid> cache, |
|||
IObjectMapper objectMapper, |
|||
IDistributedEventBus distributedEventBus) |
|||
{ |
|||
_cache = cache; |
|||
_objectMapper = objectMapper; |
|||
_distributedEventBus = distributedEventBus; |
|||
} |
|||
|
|||
public async Task HandleEventAsync(EntityUpdatedEto<ProductEto> eventData) |
|||
{ |
|||
var cachedProductDto = await _cache.GetAsync(eventData.Entity.Id); |
|||
if (cachedProductDto == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_objectMapper.Map(eventData.Entity, cachedProductDto); |
|||
|
|||
await _cache.SetAsync(eventData.Entity.Id, cachedProductDto); |
|||
|
|||
await _distributedEventBus.PublishAsync( |
|||
new BasketProductUpdatedEto |
|||
{ |
|||
ProductId = eventData.Entity.Id, |
|||
ProductName = eventData.Entity.Name |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using EShopOnAbp.CatalogService.Products; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public class BasketProductService : IBasketProductService, ITransientDependency |
|||
{ |
|||
private readonly IPublicProductAppService _productAppService; |
|||
private readonly IDistributedCache<ProductDto, Guid> _cache; |
|||
|
|||
public BasketProductService( |
|||
IPublicProductAppService productAppService, |
|||
IDistributedCache<ProductDto, Guid> cache) |
|||
{ |
|||
_productAppService = productAppService; |
|||
_cache = cache; |
|||
} |
|||
|
|||
public async Task<ProductDto> GetAsync(Guid productId) |
|||
{ |
|||
return await _cache.GetOrAddAsync( |
|||
productId, |
|||
() => GetProductAsync(productId) |
|||
); |
|||
} |
|||
|
|||
private Task<ProductDto> GetProductAsync(Guid productId) |
|||
{ |
|||
return _productAppService.GetAsync(productId) ?? |
|||
throw new UserFriendlyException("Could not find the product!"); //TODO: Business exception with localization;
|
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using EShopOnAbp.CatalogService.Products; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public interface IBasketProductService |
|||
{ |
|||
[ItemNotNull] |
|||
Task<ProductDto> GetAsync(Guid productId); |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public class BasketProductUpdatedEto |
|||
{ |
|||
public Guid ProductId { get; set; } |
|||
public string ProductName { get; set; } |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public class OrderAcceptedEto |
|||
{ |
|||
public List<OrderItemEto> Items { get; set; } |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public class OrderItemEto |
|||
{ |
|||
public Guid ProductId { get; set; } |
|||
public int Count { get; set; } |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Volo.Abp.Domain.Entities; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public class Basket : AggregateRoot<Guid> |
|||
{ |
|||
public List<BasketItem> Items { get; set; } |
|||
|
|||
private Basket() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public Basket(Guid id) |
|||
: base(id) |
|||
{ |
|||
Items = new List<BasketItem>(); |
|||
} |
|||
|
|||
public void AddProduct(Guid productId, int count = 1) |
|||
{ |
|||
if (count < 1) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(count), "Product count should be 1 or more!"); |
|||
} |
|||
|
|||
var item = Items.FirstOrDefault(x => x.ProductId == productId); |
|||
if (item == null) |
|||
{ |
|||
Items.Add(new BasketItem(productId, count)); |
|||
} |
|||
else |
|||
{ |
|||
item.Count += count; |
|||
} |
|||
} |
|||
|
|||
public void RemoveProduct(Guid productId, int? count = null) |
|||
{ |
|||
if (count is < 1) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(count), "Product count should be null, 1 or more!"); |
|||
} |
|||
|
|||
var item = Items.FirstOrDefault(x => x.ProductId == productId); |
|||
if (item == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (count == null || item.Count <= count) |
|||
{ |
|||
Items.Remove(item); |
|||
return; |
|||
} |
|||
|
|||
item.Count -= count.Value; |
|||
} |
|||
|
|||
public int GetProductCount(Guid productId) |
|||
{ |
|||
var item = Items.FirstOrDefault(x => x.ProductId == productId); |
|||
return item?.Count ?? 0; |
|||
} |
|||
|
|||
public void Clear() |
|||
{ |
|||
Items.Clear(); |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using System; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public class BasketItem |
|||
{ |
|||
public Guid ProductId { get; private set; } |
|||
public int Count { get; internal set; } |
|||
|
|||
private BasketItem() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public BasketItem(Guid productId, int count = 1) |
|||
{ |
|||
ProductId = productId; |
|||
Count = count; |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Caching; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public class BasketRepository : IBasketRepository |
|||
{ |
|||
private readonly IDistributedCache<Basket, Guid> _cache; |
|||
|
|||
public BasketRepository(IDistributedCache<Basket, Guid> cache) |
|||
{ |
|||
_cache = cache; |
|||
} |
|||
|
|||
public async Task<Basket> GetAsync(Guid id) |
|||
{ |
|||
return await _cache.GetOrAddAsync(id, () => Task.FromResult(new Basket(id))); |
|||
} |
|||
|
|||
public async Task UpdateAsync(Basket basket) |
|||
{ |
|||
await _cache.SetAsync(basket.Id, basket); |
|||
} |
|||
} |
|||
@ -1,16 +1,35 @@ |
|||
using Volo.Abp.Domain; |
|||
using System; |
|||
using Microsoft.Extensions.Caching.Distributed; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.Domain; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace EShopOnAbp.BasketService |
|||
{ |
|||
[DependsOn( |
|||
typeof(BasketServiceDomainSharedModule), |
|||
typeof(AbpDddDomainModule) |
|||
typeof(AbpDddDomainModule), |
|||
typeof(AbpCachingModule) |
|||
)] |
|||
public class BasketServiceDomainModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpDistributedCacheOptions>(options => |
|||
{ |
|||
options.CacheConfigurators.Add(cacheName => |
|||
{ |
|||
if (cacheName == CacheNameAttribute.GetCacheName(typeof(Basket))) |
|||
{ |
|||
return new DistributedCacheEntryOptions |
|||
{ |
|||
SlidingExpiration = TimeSpan.FromDays(7) |
|||
}; |
|||
} |
|||
|
|||
return null; |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,12 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace EShopOnAbp.BasketService; |
|||
|
|||
public interface IBasketRepository : IRepository |
|||
{ |
|||
Task<Basket> GetAsync(Guid id); |
|||
|
|||
Task UpdateAsync(Basket basket); |
|||
} |
|||