добавил и модифицировал корзину и отправку заказа
This commit is contained in:
gofnnp 2022-10-28 21:13:08 +04:00
parent b5fd880379
commit aea275e705
25 changed files with 1198 additions and 64 deletions

View File

@ -1,4 +1,4 @@
import {MainPageCode, OrderStatus, Page, PageCode} from "./interface/data";
import {MainPageCode, OrderStatus, Page, PageCode, PaymentMethod} from "./interface/data";
export const PageList: Page[] = [
{
@ -85,4 +85,15 @@ export const orderStatuses: OrderStatus = {
'OnWay': 'В пути',
'Delivered': 'Выполнен',
'Closed': 'Выполнен',
};
};
export const paymentMethods: PaymentMethod[] = [
{
type: 'Card',
label: 'Безналичный расчет'
},
{
type: 'Cash',
label: 'Наличными'
}
]

View File

@ -38,6 +38,10 @@ import { CartComponent } from './pages/cart/cart.component';
import {ListboxModule} from 'primeng/listbox';
import { ProductModalComponent } from './components/product-modal/product-modal.component';
import { CheckboxGroupComponent } from './components/checkbox-group/checkbox-group.component';
import { TreeSelectModule } from 'primeng/treeselect';
import { UserDataOrderComponent } from './components/user-data-order/user-data-order.component';
import {DropdownModule} from "primeng/dropdown";
import {SelectButtonModule} from 'primeng/selectbutton';
@NgModule({
declarations: [
@ -58,7 +62,8 @@ import { CheckboxGroupComponent } from './components/checkbox-group/checkbox-gro
ProductsComponent,
CartComponent,
ProductModalComponent,
CheckboxGroupComponent
CheckboxGroupComponent,
UserDataOrderComponent
],
imports: [
BrowserModule,
@ -91,7 +96,10 @@ import { CheckboxGroupComponent } from './components/checkbox-group/checkbox-gro
debug: true
}),
ShareIconsModule,
ListboxModule
ListboxModule,
TreeSelectModule,
DropdownModule,
SelectButtonModule
],
providers: [DialogService, MessageService, MessagingService ],
bootstrap: [AppComponent]

View File

@ -0,0 +1,50 @@
<div *ngIf="mainFormGroup && !loading; else loadingEl" class="woocommerce-shipping-fields__field-wrapper">
<form (ngSubmit)="submit()" [formGroup]="mainFormGroup" action="false" autocomplete="on">
<h2 class="order_form__title">Оформление заказа</h2>
<p *ngIf="hasError" class="request-error-message">
Произошла ошибка. Попробуйте позже.
</p>
<div class="order_form" formGroupName="userDataForm">
<p сlass="form-row form-row-wide">
<input formControlName="first_name" id="first_name" pInputText placeholder="Ваше имя" type="text">
</p>
<p *ngIf="deliverData.deliveryType?.title === 'Доставка'" сlass="form-row form-row-wide">
<input formControlName="street" id="street" pInputText placeholder="Улица" type="text">
</p>
<p *ngIf="deliverData.deliveryType?.title === 'Доставка'" сlass="form-row form-row-first">
<input formControlName="house" id="house" pInputText placeholder="Номер дома" type="text">
</p>
<p *ngIf="deliverData.deliveryType?.title === 'Доставка'" сlass="form-row form-row-last">
<input formControlName="flat" id="flat" pInputText placeholder="Квартира" type="number" min="1">
</p>
</div>
<div formGroupName="deliveryDataForm">
<p сlass="form-row form-row-wide">
<p-dropdown [options]="deliveryTypes" formControlName="deliveryType" placeholder="Доставка"
optionLabel="title" (onChange)="changeDeliveryType($event)"></p-dropdown>
</p>
<p сlass="form-row form-row-wide">
<p-selectButton [options]="paymentMethods" formControlName="paymentMethod" optionLabel="label">
</p-selectButton>
</p>
<p сlass="form-row form-row-last">
<input [maxLength]="255" id="promo-code" pInputText placeholder="Промокод" type="text">
</p>
<p сlass="form-row form-row-wide">
<textarea [maxLength]="255" cols="30" formControlName="comment" pInputTextarea placeholder="Комментарий"
rows="1"></textarea>
</p>
</div>
<button [disabled]="!mainFormGroup.valid" class="elementor-button elementor-button--checkout elementor-size-md"
(click)="submit()">
<span class="elementor-button-text">Оформить заказ</span>
</button>
<p *ngIf="showMyMessage" style="color: red; font-size: 20px">Такой адрес не найден! Введите правильный адрес</p>
</form>
</div>
<ng-template #loadingEl>
<div class="angular-spinner-container" style="width: fit-content; height: 100%; margin: 16px auto;">
<p-progressSpinner styleClass="angular-spinner"></p-progressSpinner>
</div>
</ng-template>

View File

@ -0,0 +1,67 @@
:host {
.woocommerce-shipping-fields__field-wrapper {
margin-top: 8px;
}
.order_form__title {
font-weight: 700;
font-size: 18px;
margin-bottom: 12px;
}
input {
width: 100%;
color: #000000;
border: 1px solid #000000;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", Segoe UI Symbol;
font-size: 1rem;
color: #495057;
background: #ffffff;
padding: 0.5rem 0.75rem;
border: 1px solid #ced4da;
transition: background-color .15s, border-color .15s, box-shadow .15s;
-webkit-appearance: none;
appearance: none;
border-radius: 4px;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
textarea {
width: 100%;
height: 52px;
color: #000000;
border: 1px solid #000000;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", Segoe UI Symbol;
font-size: 1rem;
color: #495057;
background: #ffffff;
padding: 0.5rem 0.75rem;
border: 1px solid #ced4da;
transition: background-color .15s, border-color .15s, box-shadow .15s;
-webkit-appearance: none;
appearance: none;
border-radius: 4px;
}
form {
&>button {
background-color: #09467f;
color: #fff;
border-radius: 6px;
width: calc(100% - 66px);
display: flex;
justify-content: center;
align-items: center;
border: none;
height: 40px;
width: 100%;
cursor: pointer;
}
}
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserDataOrderComponent } from './user-data-order.component';
describe('UserDataOrderComponent', () => {
let component: UserDataOrderComponent;
let fixture: ComponentFixture<UserDataOrderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ UserDataOrderComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(UserDataOrderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,200 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DeliveryData, DeliveryType, UserData } from 'src/app/interface/data';
import { paymentMethods } from "../../app.constants";
import { OrderService } from "../../services/order.service";
import { AutocompleteService } from "../../services/autocomplete.service";
import { StreetValidator } from "../../validators/street.validator";
import { CartService } from 'src/app/services/cart.service';
import { environment } from "../../../environments/environment";
import { MessageService } from "primeng/api";
import { WpJsonService } from "../../services/wp-json.service";
import { HttpClientModule } from '@angular/common/http';
@Component({
selector: 'app-user-data-order',
templateUrl: './user-data-order.component.html',
styleUrls: ['./user-data-order.component.scss']
})
export class UserDataOrderComponent implements OnInit {
@Output() orderSubmitted = new EventEmitter<void>();
readonly cities = environment.cities;
readonly paymentMethods = paymentMethods;
public loading = false;
public hasError = false;
public mainFormGroup!: FormGroup;
public deliveryTypes: DeliveryType[] = [];
public minDate!: Date;
public new_street!: string | null;
public street!: string;
public new_house!: string | null;
public checkAddress: boolean = true;
public showMyMessage: boolean = false;
public userData: UserData = {
first_name: null,
last_name: null,
street: null,
house: null,
flat: null,
city: this.cities[0],
phone: null,
};
public deliverData: DeliveryData = {
deliveryDate: null,
deliveryType: null,
paymentMethod: paymentMethods[0],
comment: '',
persons: 1,
};
constructor(
private fb: FormBuilder,
private orderService: OrderService,
private autoCompleteService: AutocompleteService,
private streetValidator: StreetValidator,
private cartService: CartService,
private messageService: MessageService,
private wpJsonService: WpJsonService,
) {
}
ngOnInit(): void {
this.minDate = new Date();
this._createMainForm();
}
changeDeliveryType(event: any) {
this.deliverData.deliveryType = event.value;
if (this.deliverData.deliveryType?.title) {
this.changeValidators(this.deliverData.deliveryType.title)
}
}
changeValidators(title: string) {
const comment = this.mainFormGroup.controls['deliveryDataForm'].value.comment;
const streetValidators = title === 'Доставка' ? [Validators.required, Validators.minLength(2), Validators.maxLength(255),] : []
const houseValidators = title === 'Доставка' ? [Validators.required, Validators.maxLength(10),] : []
const userDataForm = this.fb.group({
phone: [this.userData.phone],
first_name: [this.userData.first_name, [Validators.required, Validators.minLength(2), Validators.maxLength(255),]],
// last_name: [this.userData.last_name, [Validators.required, Validators.minLength(2), Validators.maxLength(255),]],
street: [this.userData.street, streetValidators],
house: [this.userData.house, houseValidators],
flat: [this.userData.flat, []],
// city: [this.userData.city, [Validators.required]],
});
const deliveryDataForm = this.fb.group({
deliveryDate: [this.deliverData.deliveryDate, []],
deliveryType: [this.deliverData.deliveryType, [Validators.required]],
paymentMethod: [this.deliverData.paymentMethod, [Validators.required]],
// persons: [this.deliverData.persons, [Validators.required, Validators.minLength(2), Validators.maxLength(255),]],
comment: [comment, [Validators.maxLength(255),]],
});
this.mainFormGroup = this.fb.group({
userDataForm,
deliveryDataForm,
});
}
submit(): void {
const mainControls = this.mainFormGroup.controls;
if (this.mainFormGroup.invalid) {
Object.keys(mainControls).forEach(groupName => {
const childGroupControls = (mainControls[groupName] as FormGroup).controls;
Object.keys(mainControls).forEach(controlName => {
childGroupControls[controlName].markAsTouched();
});
});
return;
}
this.submitOrder();
}
submitOrder(): void {
this.loading = true;
const userData: UserData = this.mainFormGroup.controls['userDataForm'].value;
userData.phone = this.userData.phone;
this.orderService.setUserData(userData);
this.orderService.setDeliveryData(this.mainFormGroup.controls['deliveryDataForm'].value);
this.orderService.submit().subscribe({
next: (_) => {
this.loading = false;
this.cartService.clearCart();
this.orderSubmitted.next();
},
error: () => {
this.loading = false;
this.hasError = true;
}
})
}
private async _createMainForm(): Promise<void> {
try {
this.loading = true;
const userDataForm = await this._createUserDataForm();
const deliveryDataForm = await this._createDeliveryDataForm();
this.mainFormGroup = this.fb.group({
userDataForm,
deliveryDataForm,
});
this.loading = false;
}
catch (e) {
console.error('Erroe: ', e);
this.messageService.add({
severity: 'error',
summary: 'Произошла ошибка',
})
}
}
private async _createUserDataForm(): Promise<FormGroup> {
const order = await this.orderService.getOrder(true);
this.userData = Object.assign({}, this.userData, order.userData);
this.userData.city = this.cities[0];
this.userData.phone = order.phone;
// await this.autoCompleteService.setCity(this.userData.city);
return this.fb.group({
phone: [this.userData.phone],
first_name: [this.userData.first_name, [Validators.required, Validators.minLength(2), Validators.maxLength(255),]],
// last_name: [this.userData.last_name, [Validators.required, Validators.minLength(2), Validators.maxLength(255),]],
street: [this.userData.street, [Validators.required, Validators.minLength(2), Validators.maxLength(255),]],
house: [this.userData.house, [Validators.required, Validators.maxLength(10), Validators.pattern('^\\d+[-|\\d]+\\d+$|^\\d*$')]],
flat: [this.userData.flat, []],
// city: [this.userData.city, [Validators.required]],
});
}
private async _createDeliveryDataForm(): Promise<FormGroup> {
this.deliveryTypes = [
{
"cost": 100,
"title": "Доставка",
"id": 11,
"type": "delivery"
},
{
"cost": 0,
"title": "Самовывоз",
"id": 16,
"type": "self_delivery"
}
];
this.deliverData.deliveryType = this.deliveryTypes[0];
return this.fb.group({
// deliveryDate: [this.deliverData.deliveryDate, []],
deliveryType: [this.deliverData.deliveryType, [Validators.required]],
paymentMethod: [this.deliverData.paymentMethod, [Validators.required]],
// persons: [this.deliverData.persons, [Validators.required, Validators.minLength(2), Validators.maxLength(255),]],
comment: [this.deliverData.comment, [Validators.maxLength(255),]],
});
}
}

View File

@ -1,3 +1,4 @@
import { CartProduct } from "../models/cart-product";
export enum PageCode {
@ -79,6 +80,7 @@ export interface DeliveryType {
cost: number;
title: string;
id: number;
type: string;
}
export interface AcceptedOrder {
@ -111,7 +113,7 @@ export interface Product {
description: string;
stock_status: string;
currency_symbol: string;
modifier_data: Modifier[];
modifier_data: CartModifier[];
short_description: string;
guid: string;
groupId: string;
@ -129,7 +131,7 @@ export interface AllData {
export interface Group {
id: string;
name: string;
label: string;
}
export interface ModifiersGroup {
@ -147,6 +149,7 @@ export interface Modifier {
id: string,
name: string,
groupId: string,
price?: number,
restrictions: {
minQuantity: number,
maxQuantity: number,
@ -155,6 +158,16 @@ export interface Modifier {
}
}
export interface CartModifier {
id: string;
name: string;
options: Modifier[];
}
export interface Cart {
products: CartProduct[];
}
// export interface Modifier {
// id: number;
// name: string;
@ -170,9 +183,13 @@ export interface Modifier {
export interface Option {
id: number;
name: string;
price: string;
prechecked: string;
active?: boolean;
groupId: string;
restrictions: {
minQuantity: number,
maxQuantity: number,
freeQuantity: number,
byDefault: number
}
}
export interface OrderProduct {

View File

@ -0,0 +1,44 @@
import {CartModifier, Modifier, ModifiersGroup, Option} from "../interface/data";
import { v4 as uuidv4 } from 'uuid';
export class CartProduct {
constructor(id: string, name: string, modifiers: ModifiersGroup[] = [], options: Modifier[], amount: number = 1) {
this.id = id;
this.guid = uuidv4();
this.amount = amount;
this.name = name;
this.modifiers = modifiers.map(modifier => ({name: modifier.name, id: modifier.id, options: []}));
}
id: string;
guid: string;
amount: number;
name: string;
modifiers: CartModifier[];
increment(): void{
this.amount++;
}
decrement(): void{
if (this.amount > 0){
this.amount--;
}
}
addOption(modifier: ModifiersGroup, option: Modifier): void{
const productModifier = this.modifiers.find(value => value.id === modifier.id);
if (productModifier){
const optionIndex = productModifier.options.findIndex(value => value.id === option.id);
if(optionIndex === -1){
productModifier.options.push(option);
}
else {
productModifier.options.splice(optionIndex, 1)
}
}
}
}

View File

@ -1,4 +1,4 @@
import {Modifier, Product} from "../interface/data";
import {CartModifier, Modifier, Product} from "../interface/data";
export class OrderProduct implements Product{
@ -29,7 +29,7 @@ export class OrderProduct implements Product{
public id: string;
public image_gallery: string[];
public image: string;
public modifier_data: Modifier[];
public modifier_data: CartModifier[];
public name: string;
public price: number;
public stock_status: string;
@ -39,29 +39,27 @@ export class OrderProduct implements Product{
get finalPrice(): number{
// const modifiersPrice = this.modifier_data.reduce<number>((previousValue, currentValue) => {
// return previousValue + currentValue.options.reduce<number>((previousOptionValue, currentOptionValue) => {
// return previousOptionValue + Number(currentOptionValue.price);
// }, 0);
// }, 0);
// return (Number(this.price) + modifiersPrice) * this.amount;
return 1
new Date()
const modifiersPrice = this.modifier_data.reduce<number>((previousValue, currentValue) => {
return previousValue + currentValue.options.reduce<number>((previousOptionValue, currentOptionValue) => {
return previousOptionValue + Number(currentOptionValue.price ? currentOptionValue.price : 0);
}, 0);
}, 0);
return (Number(this.price) + modifiersPrice) * this.amount;
}
toJson(){
return {
id: this.id,
amount: this.amount,
name: this.name,
modifiers: this.modifier_data?.map(modifier => {
amount: this.amount * this.price,
price: this.price,
options: this.modifier_data?.map((modifier) => {
return {
id: modifier.id,
// options: modifier.options,
option: modifier.name,
variant: modifier.options[0]?.name || null
}
}),
quantity: this.amount,
name: this.name,
}
}
}

View File

@ -2,6 +2,7 @@ import {DeliveryData, UserData} from "../interface/data";
import {OrderProduct} from "./order-product";
import * as moment from 'moment';
import { CookiesService } from "../services/cookies.service";
import { environment } from "src/environments/environment";
export interface OrderInfo {
products: OrderProduct[];
@ -36,33 +37,23 @@ export class Order {
toJson(): any {
const date = moment(this.deliveryData?.deliveryDate ?? Date.now());
return {
items: this.products.map(product => {
return product.toJson();
}),
user_data: {
phone: this.phone,
...this.userData
},
payment_method: this.deliveryData?.paymentMethod.type,
delivery_time: date.format('HH:mm'),
delivery_date: date.format("YYYY-MM-DD"),
delivery_instance_id: this.deliveryData?.deliveryType?.id,
formname: "Cart",
paymentsystem: this.deliveryData?.paymentMethod.type,
phone: this.phone,
persons: 1,
payments: [
{
type: this.deliveryData?.paymentMethod.type,
summ: this.price,
},
{
type: "crm4retail",
summ: 0,
payload: {
id: "c07a10d8-ba7e-43b0-92aa-ae470060bc7d"
}
}
],
comment: this.deliveryData?.comment,
token: this.token
name: "31",
payment: {
delivery_price: 100,
products: this.products.map(product => {
return product.toJson();
}),
delivery_fio: this.userData?.first_name,
subtotal: this.price,
delivery_comment: this.deliveryData?.comment,
delivery: this.deliveryData?.deliveryType?.type,
delivery_address: `${environment.cities[0]}, ул ${this.userData?.street}, ${this.userData?.house}`,
amount: this.price + 100
},
}
}
}

View File

@ -7,12 +7,14 @@
<ng-container *ngFor="let page of mainPageList; let index = index; let last = last; let first = first;">
<li *ngIf="page.onSideBar" class="main-menu-container__item"
[ngClass]="{
'cart': page.resName === 'cart',
'is-active': page === currentPageMain
}"
[ngStyle]="{
border: last && 0,
'border-radius': first ? '6px 0 0 6px' : (last ? '0 6px 6px 0' : 0)
}"
[attr.data-counter]="page.resName === 'cart' ? cartCount : null"
(click)="changeMainPage(page, $event)">
<span>
{{page.name}}

View File

@ -2,6 +2,7 @@
.woocommerce {
min-height: calc(100vh - 39px);
padding: 20px 18px;
position: relative;
&.auth-page {
display: flex;
@ -13,6 +14,7 @@
max-width: 600px;
height: 50px;
margin: -20px auto 0 auto;
ul {
display: flex;
justify-content: space-between;
@ -20,16 +22,36 @@
align-items: center;
font-size: 14px;
padding: 0 16px;
li {
width: 100%;
text-align: center;
border-right: solid #e1e1e1 1px;
padding: 8px 0;
cursor: pointer;
&.is-active {
background: #0d457e;
color: #fff;
}
&.cart {
position: relative;
&::before {
content: attr(data-counter);
color: #fff;
position: absolute;
right: 3px;
top: 1px;
background: #D7120B;
border-radius: 50px;
min-width: 1.2rem;
line-height: 1.2rem;
font-size: .8rem;
text-align: center;
}
}
}
}
}
@ -118,6 +140,8 @@
.version {
opacity: 0.5;
position: absolute;
bottom: 12px;
}
}
}

View File

@ -10,6 +10,7 @@ import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';
import { JsonrpcService, RpcService } from 'src/app/services/jsonrpc.service';
import { MessageService } from 'primeng/api';
import { lastValueFrom } from 'rxjs';
import { CartService } from 'src/app/services/cart.service';
@Component({
selector: 'app-account',
@ -27,7 +28,8 @@ export class AccountComponent implements OnInit {
private route: ActivatedRoute,
private dialogService: DialogService,
private jsonRpcService: JsonrpcService,
private messageService: MessageService
private messageService: MessageService,
private cartService: CartService,
) { }
public currentPage!: Page;
@ -42,7 +44,8 @@ export class AccountComponent implements OnInit {
readonly MainPageCode = MainPageCode;
readonly mainPageList = PageListMain;
public currentPageMain: Page = this.mainPageList[0];
public currentPageMain: Page = this.mainPageList[environment.production ? 0 : 1];
public cartCount = 0;
ngOnInit(): void {
if (!this.getToken()) {
@ -62,6 +65,13 @@ export class AccountComponent implements OnInit {
this.currentPage = currentPage;
}
});
this.cartCount = this.cartService.cartCount;
this.cartService.cartCount$.subscribe({
next: (count) => {
this.cartCount = count;
document.querySelectorAll('.cart')[0].setAttribute("data-counter", this.cartCount.toString())
}
});
}
document.body.classList.add(
'woocommerce-account',

View File

@ -8,6 +8,9 @@
font-weight: 700;
font-size: 20px;
line-height: 24px;
max-width: 400px;
margin: 0 auto;
width: 100%;
}
&>p {

View File

@ -1 +1,77 @@
<p>cart works!</p>
<div class="cart" *ngIf="!loading && order && !orderConfirmed" [ngStyle]="{margin: !order.products.length && 0}">
<div class="widget_shopping_cart_content" style="opacity: 1;">
<div class="elementor-menu-cart__products woocommerce-mini-cart cart woocommerce-cart-form__contents" [ngStyle]="{margin: !order.products.length && 0}">
<div *ngFor="let product of order.products"
class="elementor-menu-cart__product woocommerce-cart-form__cart-item cart_item"
style="grid-template-columns: 70px auto;">
<div class="elementor-menu-cart__product-image product-thumbnail">
<img *ngIf="product.image" width="70" height="70" src="{{product.image}}"
class="attachment-woocommerce_thumbnail size-woocommerce_thumbnail" alt="{{product.name}}" loading="lazy">
<img *ngIf="!product.image" width="70" height="70" src="./assets/no-image.png"
class="attachment-woocommerce_thumbnail size-woocommerce_thumbnail" alt="{{product.name}}" loading="lazy">
</div>
<div class="elementor-menu-cart__product-name product-name" data-title="Product">
<span>{{product.name}}</span>
<dl *ngFor="let modifier of product.modifier_data" class="variation" [ngStyle]="{margin: !modifier.options.length && 0}" >
<ng-container *ngFor="let option of modifier.options">
<dt style="max-width: 160px;" class="variation-">{{option.name}}:</dt>
<dd style="display: flex; align-items: flex-end; margin-bottom: 0;" class="variation-"><p>{{product.currency_symbol}}{{option.price ?? 0}}</p>
</dd>
</ng-container>
</dl>
</div>
<div class="elementor-menu-cart__product-price product-price" data-title="Price">
<span class="quantity">
<span class="product-quantity">{{product.amount}} ×</span>
<span class="woocommerce-Price-amount amount">
<bdi>
<span class="woocommerce-Price-currencySymbol">{{product.currency_symbol}}</span>
{{product.finalPrice}}
</bdi>
</span>
</span>
<div class="product-change-amount">
<div class="product-change-amount__symbol" (click)="setAmount(product, 'minus')">
-
</div>
<div class="product-change-amount__symbol" (click)="setAmount(product, 'plus')">
+
</div>
</div>
</div>
<div class="elementor-menu-cart__product-remove product-remove">
<a href="#" class="remove_from_cart_button" aria-label="Remove this item"
(click)="removeFromCart($event, product.guid)"></a>
</div>
</div>
</div>
<div *ngIf="order.products.length != 0" class="elementor-menu-cart__bottom-info">
<div class="elementor-menu-cart__subtotal">
<strong>К оплате: </strong>
<span class="woocommerce-Price-amount amount"><bdi><span
class="woocommerce-Price-currencySymbol">{{order.products[0].currency_symbol}}</span>{{order.price}}</bdi></span>
</div>
<div class="elementor-menu-cart__footer-buttons">
<a href="#" class="elementor-button elementor-button--checkout elementor-size-md"
(click)="confirmOrder($event)">
<span class="elementor-button-text">Оформление заказа</span>
</a>
</div>
</div>
</div>
</div>
<app-user-data-order *ngIf="orderConfirmed" (orderSubmitted)="orderSubmitted()"></app-user-data-order>
<div #loadingEl *ngIf="loading">
<div class="angular-spinner-container" style="width: fit-content; height: 100%; margin: 16px auto;">
<p-progressSpinner styleClass="angular-spinner"></p-progressSpinner>
</div>
</div>
<div #empty *ngIf="!loading && (!order || !order.products.length)">
<div class="woocommerce-mini-cart__empty-message jupiterx-icon-shopping-cart-6">Корзина пустая.</div>
</div>

View File

@ -0,0 +1,196 @@
:host {
.cart {
margin-top: 16px;
margin-bottom: 100px;
}
.elementor-menu-cart {
&__product {
grid-template-columns: 71px auto;
grid-template-rows: var(--price-quantity-position--grid-template-rows, auto auto);
position: relative;
display: grid;
padding-bottom: 20px;
padding-right: 30px;
.variation {
display: grid;
grid-template-columns: max-content auto;
margin: 10px 8px;
color: var(--product-variations-color, #373a3c);
dt {
grid-column-start: 1;
font-weight: 700;
}
dd {
grid-column-start: 2;
-webkit-margin-start: 5px;
margin-inline-start: 5px;
margin-bottom: 0.5rem;
margin-left: 6px;
}
}
}
&__product-image {
grid-row-start: 1;
grid-row-end: 3;
width: 100%;
img {
border-radius: 6px;
}
}
&__product-name {
grid-column-start: 2;
grid-column-end: 3;
margin: 0;
font-size: 14px;
padding-left: 20px;
}
&__product-price {
font-size: 14px;
padding-left: 20px;
grid-column-start: 2;
grid-column-end: 3;
-ms-flex-item-align: var(--price-quantity-position--align-self, end);
align-self: var(--price-quantity-position--align-self, end);
font-weight: 300;
}
&__product-remove {
color: #818a91;
width: var(--remove-item-button-size, 22px);
height: var(--remove-item-button-size, 22px);
border-radius: var(--remove-item-button-size, 22px);
border: 1px solid var(--remove-item-button-color, #d4d4d4);
text-align: center;
overflow: hidden;
position: absolute;
top: 0px;
right: 0;
bottom: 20px;
-webkit-transition: .3s;
-o-transition: .3s;
transition: .3s;
&::before,
&::after {
content: "";
position: absolute;
height: 1px;
width: 50%;
top: 50%;
left: 25%;
margin-top: -1px;
background: var(--remove-item-button-color, #d4d4d4);
z-index: 1;
-webkit-transition: .3s;
-o-transition: .3s;
transition: .3s;
}
&::before {
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
&::after {
-webkit-transform: rotate(-45deg);
-ms-transform: rotate(-45deg);
transform: rotate(-45deg);
}
&>a {
display: block;
z-index: 2;
width: 100%;
height: 100%;
overflow: hidden;
opacity: 0;
position: absolute;
}
}
&__bottom-info {
position: fixed;
width: 100%;
left: 0;
display: flex;
justify-content: space-between;
align-items: flex-start;
bottom: 0;
padding: 18px;
background: #fff;
border-top: solid #d9d9d9 1px;
z-index: 3;
}
&__subtotal {
font-weight: 600;
}
&__footer-buttons {
a {
padding: 12px;
display: block;
width: fit-content;
background: #09467f;
border-radius: 4px;
text-decoration: none;
color: #fff;
font-size: 12px;
}
}
}
.product-thumbnail {
background: #eee;
border-radius: 9px;
height: 70px;
}
.product-change-amount {
width: 50px;
height: 30px;
margin-top: 4px;
border-radius: 5px;
display: flex;
border: solid #cbcbcb 1px;
color: #525252;
cursor: pointer;
user-select: none;
&__symbol {
width: 50%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
border-right: solid #cbcbcb 1px;
}
}
.cart-product {
&__supplements {
position: absolute;
right: 0;
bottom: 26px;
padding: 8px;
background: #f9b004;
color: #fff;
border-radius: 4px;
cursor: pointer;
}
}
.woocommerce-mini-cart__empty-message {
text-align: center;
margin-top: 16px;
}
}

View File

@ -1,4 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { Order } from 'src/app/models/order';
import { OrderProduct } from 'src/app/models/order-product';
import { CartService, ProductAmountAction } from 'src/app/services/cart.service';
import { OrderService } from 'src/app/services/order.service';
@Component({
selector: 'app-cart',
@ -6,10 +10,52 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./cart.component.scss']
})
export class CartComponent implements OnInit {
public loading = false;
public orderConfirmed = false;
public order!: Order;
public price!: number;
constructor() { }
constructor(
private orderService: OrderService,
private cartService: CartService
) { }
ngOnInit(): void {
this.loadCart()
}
async loadCart(): Promise<void> {
this.loading = true;
this.order = await this.orderService.getOrder(true);
if (this.order) this.price = this.order.price
this.loading = false;
}
removeFromCart(event: Event, guid: string): void{
event.preventDefault();
this.orderService.removeFromCart(guid);
}
confirmOrder(event: Event): void{
event.preventDefault();
this.orderConfirmed = true;
// this.confirm.emit();
}
setAmount(product: OrderProduct, method: 'plus' | 'minus') {
if (method === 'plus') {
this.cartService.changeAmountProduct(product.guid, ProductAmountAction.increment)
product.amount++
this.price = this.price + Number(product.price);
} else if (method === 'minus' && product.amount > 1) {
this.cartService.changeAmountProduct(product.guid, ProductAmountAction.decrement)
product.amount--
this.price = this.price - Number(product.price);
}
}
orderSubmitted() {
}
}

View File

@ -0,0 +1,77 @@
import {Injectable} from '@angular/core';
import {HttpClient, HttpHeaders} from "@angular/common/http";
import {map} from "rxjs/operators";
import {lastValueFrom, Observable} from "rxjs";
enum CompleteType {
city,
street,
}
@Injectable({
providedIn: 'root'
})
export class AutocompleteService {
private city!: string;
private cityId!: string;
private query?: string
private streets: string[] = [];
constructor(private http: HttpClient) {
}
async setCity(city: string | null): Promise<boolean> {
if (city && this.city != city) {
this.city = city;
let headers = new HttpHeaders();
const cityData = await lastValueFrom(
this._request(`query=${city}&contentType=city`)
.pipe(
map(
(res: any) => res.result.filter(
(city: any) => city.id != 'Free'
)[0]
),
)
);
this.cityId = cityData.id;
return true;
}
return false;
}
async queryStreet(query: string, city: string | null = null): Promise<string[]> {
let headers = new HttpHeaders();
headers = headers.set('Content-Type', 'application/json');
let newCityId = await this.setCity(city);
if (!this.cityId) {
return [];
}
if (!this.query || this.query !== query || newCityId) {
this.query = query;
this.streets = await lastValueFrom(
this._request(`query=${query}&offset=0&limit=20&cityId=${this.cityId}&contentType=street`)
.pipe(
map(
(res: any) => res.result
.filter(
(street: any) => street.id != 'Free'
)
.map(
(street: any) => street.name
)
)
)
);
}
return this.streets;
}
//jsonp запрос для обхода cors кладра
_request(params: String): Observable<any> {
const src = '//kladr-api.ru/api.php?';
return this.http.jsonp(src + params, 'callback');
};
}

View File

@ -0,0 +1,111 @@
import {Injectable} from '@angular/core';
import {CookiesService} from "./cookies.service";
import {Cart} from "../interface/data";
import {isEqual} from 'lodash/fp';
import {CartProduct} from "../models/cart-product";
import {Subject} from "rxjs";
import { update } from 'lodash';
import { WpJsonService } from './wp-json.service';
export enum ProductAmountAction {
increment,
decrement,
}
@Injectable({
providedIn: 'root'
})
export class CartService {
constructor(
private cookieService: CookiesService,
private wpJsonService: WpJsonService,
) { }
private cart!: Cart;
public cartCount$ = new Subject<number>();
getCart(){
return this._getCartProducts();
}
addToCart(product: CartProduct): void{
const cart = this._getCartProducts();
cart.products = cart.products ?? [];
const sameProduct = cart.products.find((value) => value.id === product.id && isEqual(value.modifiers, product.modifiers));
if(sameProduct){
sameProduct.amount ++;
}
else {
cart.products.push(product);
this.cartCount$.next(cart.products.length);
}
this.cookieService.setCookie('cart', JSON.stringify(cart));
}
removeFromCart(guid: string): void{
const cart = this._getCartProducts();
if(!cart.products){
return;
}
cart.products = cart.products.filter((value) => value.guid !== guid);
this.cookieService.setCookie('cart', JSON.stringify(cart));
this.cartCount$.next(cart.products.length);
}
updateProductFromCart(product: CartProduct): void{
// const cart = this._getCartProducts();
// if(!cart.products){
// return;
// }
// const updateProduct = cart.products.find((value) => Number(value.id) === product.id)
// if (updateProduct) {
// updateProduct.modifiers = JSON.parse(JSON.stringify(product.modifiers))
// }
// this.cookieService.setCookie('cart', JSON.stringify(cart));
}
changeAmountProduct(productTempId: string,action: ProductAmountAction): void{
const cart = this._getCartProducts();
if(!cart.products){
return;
}
const product: CartProduct | undefined = cart.products.find((value) => value.guid === productTempId);
if(product && action === ProductAmountAction.increment){
product.amount++
// product.increment();
}
else if(product && action === ProductAmountAction.decrement){
product.amount--
// product.decrement();
}
this.cookieService.setCookie('cart', JSON.stringify(cart));
this.cartCount$.next(cart.products.length);
}
clearCart(){
this.cart = {products: []};
this.cookieService.setCookie('cart', JSON.stringify(this.cart));
this.cartCount$.next(0);
}
_getCartProducts(): Cart{
if(this.cart){
return this.cart;
}
const cartJson = this.cookieService.getItem('cart');
this.cart = cartJson ? JSON.parse(cartJson) : {products: []};
return this.cart;
}
get cartCount(): number{
return this._getCartProducts().products.length;
}
}

View File

@ -0,0 +1,113 @@
import {Injectable} from '@angular/core';
import {CartService} from "./cart.service";
import {WpJsonService} from "./wp-json.service";
import {forkJoin, lastValueFrom, Observable, tap} from "rxjs";
import {Cart, DeliveryData, DeliveryType, Modifier, Product, UserData} from "../interface/data";
import {Order} from "../models/order";
import {OrderProduct} from "../models/order-product";
import {JsonrpcService, RpcService} from "./jsonrpc.service";
import {CookiesService} from "./cookies.service";
import {MessageService} from "primeng/api";
import {map} from "rxjs/operators";
import { cloneDeep } from 'lodash';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root',
})
export class OrderService {
private order!: Order;
constructor(
private cartService: CartService,
private wpJsonService: WpJsonService,
private jsonRpcService: JsonrpcService,
private cookiesService: CookiesService,
private messageService: MessageService,
) {
}
async getDeliveryTypes(): Promise<DeliveryType[]> {
return await lastValueFrom(this.wpJsonService.getDeliveryTypes());
}
async getOrder(refresh = false): Promise<Order> {
if (!this.order || refresh) {
const cart = this.cartService.getCart();
if (cart.products.length) {
const products = await this.getProducts(cart);
const additionalInfo = this.jsonRpcService.rpc({
method: 'getAdditionalInfo',
params: []
}, RpcService.authService, true);
const tokenData = this.jsonRpcService.rpc({
method: 'getTokenData',
params: [this.cookiesService.getItem('token')],
}, RpcService.authService, true);
const info = await lastValueFrom(forkJoin([additionalInfo, tokenData, products]));
const token = this.cookiesService.getItem('token')
this.order = new Order({products: products, userData: info[0]?.data, phone: info[1].data?.mobile_number, token: token});
}
}
return this.order;
}
async getProducts(cart: Cart): Promise<OrderProduct[]> {
const allData = await lastValueFrom(this.wpJsonService.getAllData())
const products: OrderProduct[] = []
for (let i = 0; i < cart.products.length; i++) {
const productSub = allData.products.find((product: any) => product.id === cart.products[i].id)
const product = Object.assign(cloneDeep(cart.products[i]), {
category_id: 0,
price: productSub.price,
currency_symbol: '₽',
description: '',
short_description: '',
image_gallery: [],
image: productSub.image,
modifier_data: cart.products[i].modifiers,
stock_status: 'instock',
groupId: productSub.groupId,
modifiers_group: productSub.modifiers_group
})
const orderProduct: OrderProduct = new OrderProduct(product, cart.products[i].guid, cart.products[i].amount)
products.push(orderProduct)
}
return products
}
removeFromCart(productGuid: string): void {
this.order.products = this.order.products.filter(value => value.guid !== productGuid);
this.cartService.removeFromCart(productGuid);
}
setUserData(userData: UserData): void {
this.order.userData = userData;
}
setDeliveryData(deliveryData: DeliveryData): void {
this.order.deliveryData = deliveryData;
}
submit(): Observable<any> {
return this.wpJsonService.createOrder(this.order.toJson(), environment.webhookItRetail).pipe(
tap({
next: (_) => {
this.jsonRpcService.rpc({
method: 'updateAdditionalInfo',
params: [this.order.userData, this.order.deliveryData]
}, RpcService.authService, true).subscribe();
this.messageService.add({
severity:'success',
summary: 'Заказ создан',
});
},
}),
);
}
}

View File

@ -30,8 +30,8 @@ export class WpJsonService {
return this._request('orders/delivery-types', 'GET');
}
createOrder(order: any){
return this._request('orders', 'POST', order);
createOrder(order: any, url: string){
return this._request('', 'POST', order);
}
getOrders(): Observable<AcceptedOrder[]>{
@ -46,7 +46,7 @@ export class WpJsonService {
return this._request('static/nomen_1eb3fb56-3c4c-43b7-9a04-ce532ab7548f.json', 'GET')
}
_request(path: string, method: string, body?: any, auth = false): Observable<any> {
_request(path: string, method: string, body?: any, auth = false, baseUrl = null): Observable<any> {
const token = decodeURI(this.cookiesService.getItem('token') ?? '');
let headers = new HttpHeaders();
headers = headers.set('Content-Type', 'application/json');
@ -60,8 +60,10 @@ export class WpJsonService {
body: this.body,
};
const url = environment.production ? window.location.origin + '/' : this.api
let url = environment.production ? window.location.origin + '/' : this.api
if (baseUrl) {
url = baseUrl
}
return this.http
.request( method, url + path + urlToken, options);
}

View File

@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import {AbstractControl, ValidationErrors, AsyncValidator} from '@angular/forms';
import {AutocompleteService} from "../services/autocomplete.service";
@Injectable({ providedIn: 'root' })
export class StreetValidator implements AsyncValidator {
constructor(private autocompleteService: AutocompleteService) {}
async validate(
control: AbstractControl
): Promise<ValidationErrors | null> {
try{
const streets = await this.autocompleteService.queryStreet(control.value);
if(streets.includes(control.value)){
return null;
}
return { validStreet: false }
}
catch (e){
return { validStreet: false }
}
}
}

View File

@ -20,5 +20,7 @@ export const environment = {
version: packageJson.version,
appleWalletEndpoint: 'https://apple-push-notifications.it-retail.tech/apns/api',
appleWalletSecret: 'Token F5mbzEERAznGKVbB6l',
clientName: 'Sakura'
webhookItRetail: 'https://webhook.it-retail.tech/handlers/tillda/1eb3fb56-3c4c-43b7-9a04-ce532ab7548f',
clientName: 'Sakura',
cities: ['Менделеевск'],
}

View File

@ -4,7 +4,7 @@ export const environment = {
production: false,
appAuthEndpoint: 'https://auth.crm4retail.ru/tnt',
appBonusEndpoint: 'https://customerapi2.mi.crm4retail.ru/json.rpc/',
appWPEndpoint: './assets/',
appWPEndpoint: './',
hasBonusProgram: true,
systemId: 'g6zyv8tj53w28ov7cl',
defaultUrl: 'http://192.168.0.179:4200',
@ -20,5 +20,7 @@ export const environment = {
version: packageJson.version,
appleWalletEndpoint: 'http://192.168.0.179:4200/apns/api',
appleWalletSecret: 'Token F5mbzEERAznGKVbB6l',
clientName: 'Sakura'
webhookItRetail: 'https://webhook.it-retail.tech/handlers/tillda/1eb3fb56-3c4c-43b7-9a04-ce532ab7548f',
clientName: 'Sakura',
cities: ['Менделеевск'],
};

View File

@ -22,6 +22,29 @@ table{border-collapse:collapse;border-spacing:0}
border-color: red;
}
.p-dropdown {
width: 100%;
height: 39px;
}
.p-selectbutton {
display: flex;
&>.p-button{
flex: 1;
font-size: 12px;
}
}
.p-selectbutton .p-button.p-highlight {
background: #f9b004;
border-color: #f9b004;
}
.p-selectbutton .p-button.p-highlight:hover {
background: #f9b004;
border-color: #f9b004;
}
mark {
padding: 4px;
background: #009688;
@ -64,3 +87,16 @@ button {
input::-webkit-date-and-time-value {
text-align: left;
}
.p-treeselect {
min-width: 180px;
}
.p-tree .p-tree-container .p-treenode {
font-size: 12px;
padding: 0;
}
.p-tree .p-tree-container .p-treenode .p-treenode-content {
padding: 0;
}