1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-03-21 12:29:03 +01:00

(merge) 'origin/product-store'

This commit is contained in:
vincent 2022-09-09 13:54:42 +02:00
commit dd26a1d9af
36 changed files with 214 additions and 112 deletions

View File

@ -31,6 +31,12 @@ class API::CartController < API::ApiController
render 'api/orders/show'
end
def set_offer
authorize @current_order, policy_class: CartPolicy
@order = Cart::SetOfferService.new.call(@current_order, orderable, cart_params[:is_offered])
render 'api/orders/show'
end
private
def orderable

View File

@ -3,7 +3,7 @@
# API Controller for resources of type Coupon
# Coupons are used in payments
class API::CouponsController < API::ApiController
before_action :authenticate_user!
before_action :authenticate_user!, except: %i[validate]
before_action :set_coupon, only: %i[show update destroy]
# Number of notifications added to the page when the user clicks on 'load next notifications'
@ -31,18 +31,18 @@ class API::CouponsController < API::ApiController
if @coupon.nil?
render json: { status: 'rejected' }, status: :not_found
else
_user_id = if !current_user.admin?
current_user.id
else
_user_id = if current_user&.admin?
params[:user_id]
else
current_user&.id
end
amount = params[:amount].to_f * 100.0
status = @coupon.status(_user_id, amount)
if status != 'active'
render json: { status: status }, status: :unprocessable_entity
else
if status == 'active'
render :validate, status: :ok, location: @coupon
else
render json: { status: status }, status: :unprocessable_entity
end
end
end

View File

@ -17,6 +17,6 @@ module API::OrderConcern
end
def cart_params
params.permit(:order_token, :orderable_id, :quantity, :user_id)
params.permit(:order_token, :orderable_id, :quantity, :user_id, :is_offered)
end
end

View File

@ -22,4 +22,9 @@ export default class CartAPI {
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/set_quantity', { order_token: order.token, orderable_id: orderableId, quantity });
return res?.data;
}
static async setOffer (order: Order, orderableId: number, isOffered: boolean): Promise<Order> {
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/set_offer', { order_token: order.token, orderable_id: orderableId, is_offered: isOffered });
return res?.data;
}
}

View File

@ -42,7 +42,7 @@ export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal,
return (
<Modal isOpen={isOpen}
className={`fab-modal fab-modal-${width} ${className}`}
className={`fab-modal fab-modal-${width} ${className || ''}`}
overlayClassName="fab-modal-overlay"
onRequestClose={toggleModal}>
{closeButton && <FabButton className="modal-btn--close" onClick={toggleModal}>{t('app.shared.fab_modal.close')}</FabButton>}

View File

@ -22,13 +22,15 @@ const CartButton: React.FC = () => {
* Goto cart page
*/
const showCart = () => {
window.location.href = '/#!/cart';
window.location.href = '/#!/store/cart';
};
return (
<div className="cart-button" onClick={showCart}>
<i className="fas fa-cart-arrow-down" />
<span>{cart?.order_items_attributes?.length}</span>
{cart && cart.order_items_attributes.length > 0 &&
<span>{cart.order_items_attributes.length}</span>
}
<p>{t('app.public.cart_button.my_cart')}</p>
</div>
);

View File

@ -115,8 +115,12 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
/**
* Toggle product offer
*/
const onSwitch = (product, checked: boolean) => {
console.log('Offer ', product.orderable_name, ': ', checked);
const toogleProductOffer = (item) => {
return (checked: boolean) => {
CartAPI.setOffer(cart, item.orderable_id, checked).then(data => {
setCart(data);
}).catch(onError);
};
};
/**
@ -128,14 +132,50 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
}
};
/**
* Get the item total
*/
const itemAmount = (item): number => {
return item.quantity * Math.trunc(item.amount * 100) / 100;
};
/**
* return true if cart has offered item
*/
const hasOfferedItem = (): boolean => {
return cart.order_items_attributes
.filter(i => i.is_offered).length > 0;
};
/**
* Get the offered item total
*/
const offeredAmount = (): number => {
return cart.order_items_attributes
.filter(i => i.is_offered)
.map(i => i.amount)
.reduce((acc, curr) => acc + curr, 0);
.map(i => Math.trunc(i.amount * 100) * i.quantity)
.reduce((acc, curr) => acc + curr, 0) / 100;
};
/**
* Get the total amount before offered amount
*/
const totalBeforeOfferedAmount = (): number => {
return (Math.trunc(cart.total * 100) + Math.trunc(offeredAmount() * 100)) / 100;
};
/**
* Get the coupon amount
*/
const couponAmount = (): number => {
return (Math.trunc(cart.total * 100) - Math.trunc(computePriceWithCoupon(cart.total, cart.coupon) * 100)) / 100.00;
};
/**
* Get the paid total amount
*/
const paidTotal = (): number => {
return computePriceWithCoupon(cart.total, cart.coupon);
};
return (
@ -163,7 +203,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
</select>
<div className='total'>
<span>{t('app.public.store_cart.total')}</span>
<p>{FormatLib.price(item.quantity * item.amount)}</p>
<p>{FormatLib.price(itemAmount(item))}</p>
</div>
<FabButton className="main-action-btn" onClick={removeProductFromCart(item)}>
<i className="fa fa-trash" />
@ -175,7 +215,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
<span>Offer the product</span>
<Switch
checked={item.is_offered}
onChange={(checked) => onSwitch(item, checked)}
onChange={toogleProductOffer(item)}
width={40}
height={19}
uncheckedIcon={false}
@ -194,7 +234,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
<p>[TODO: texte venant des paramètres de la boutique]</p>
</div>
{cart && !cartIsEmpty() && cart.user &&
{cart && !cartIsEmpty() &&
<div className='store-cart-coupon'>
<CouponInput user={cart.user as User} amount={cart.total} onChange={applyCoupon} />
</div>
@ -203,7 +243,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
<aside>
{cart && !cartIsEmpty() && isPrivileged() &&
<div> <MemberSelect onSelected={handleChangeMember} /></div>
<div> <MemberSelect onSelected={handleChangeMember} defaultUser={cart.user as User} /></div>
}
{cart && !cartIsEmpty() && <>
@ -211,17 +251,17 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
<h3>{t('app.public.store_cart.checkout_header')}</h3>
<span>{t('app.public.store_cart.checkout_products_COUNT', { COUNT: cart?.order_items_attributes.length })}</span>
<div className="list">
<p>{t('app.public.store_cart.checkout_products_total')} <span>{FormatLib.price(cart.total)}</span></p>
{offeredAmount() > 0 &&
<p>{t('app.public.store_cart.checkout_products_total')} <span>{FormatLib.price(totalBeforeOfferedAmount())}</span></p>
{hasOfferedItem() &&
<p className='gift'>{t('app.public.store_cart.checkout_gift_total')} <span>-{FormatLib.price(offeredAmount())}</span></p>
}
{cart.coupon && computePriceWithCoupon(cart.total, cart.coupon) !== cart.total &&
<p>{t('app.public.store_cart.checkout_coupon')} <span>{FormatLib.price(-(cart.total - computePriceWithCoupon(cart.total, cart.coupon)))}</span></p>
{cart.coupon &&
<p>{t('app.public.store_cart.checkout_coupon')} <span>-{FormatLib.price(couponAmount())}</span></p>
}
</div>
<p className='total'>{t('app.public.store_cart.checkout_total')} <span>{FormatLib.price(computePriceWithCoupon(cart.total, cart.coupon))}</span></p>
<p className='total'>{t('app.public.store_cart.checkout_total')} <span>{FormatLib.price(paidTotal())}</span></p>
</div>
<FabButton className='checkout-btn' onClick={checkout}>
<FabButton className='checkout-btn' onClick={checkout} disabled={!cart.user}>
{t('app.public.store_cart.checkout')}
</FabButton>
</>}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { FabInput } from '../base/fab-input';
import { FabAlert } from '../base/fab-alert';
@ -27,6 +27,19 @@ export const CouponInput: React.FC<CouponInputProps> = ({ user, amount, onChange
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<boolean>(false);
const [coupon, setCoupon] = useState<Coupon>();
const [code, setCode] = useState<string>();
useEffect(() => {
if (user && code) {
handleChange(code);
}
}, [user]);
useEffect(() => {
if (code) {
handleChange(code);
}
}, [amount]);
/**
* callback for validate the code
@ -36,6 +49,7 @@ export const CouponInput: React.FC<CouponInputProps> = ({ user, amount, onChange
setMessages([]);
setError(false);
setCoupon(null);
setCode(value);
if (value) {
setLoading(true);
CouponAPI.validate(value, amount, user?.id).then((res) => {

View File

@ -95,7 +95,7 @@ export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, on
};
return (
<form onSubmit={handleSubmit} id="stripe-card" className={`stripe-card-update ${className}`}>
<form onSubmit={handleSubmit} id="stripe-card" className={`stripe-card-update ${className || ''}`}>
<CardElement options={cardOptions} />
{children}
</form>

View File

@ -69,7 +69,6 @@ export const ManageProductCategory: React.FC<ManageProductCategoryProps> = ({ pr
<div className='manage-product-category'>
{ toggleBtn() }
<FabModal title={t(`app.admin.store.manage_product_category.${action}`)}
className="fab-modal-lg"
width={ModalSize.large}
isOpen={isOpen}
toggleModal={toggleModal}

View File

@ -10,6 +10,7 @@ import { HtmlTranslate } from '../../base/html-translate';
import { IApplication } from '../../../models/application';
import { Loader } from '../../base/loader';
import { react2angular } from 'react2angular';
import ProductLib from '../../../lib/product';
declare const Application: IApplication;
@ -54,18 +55,7 @@ const ProductCategories: React.FC<ProductCategoriesProps> = ({ onSuccess, onErro
*/
const refreshCategories = () => {
ProductCategoryAPI.index().then(data => {
// Map product categories by position
const sortedCategories = data
.filter(c => !c.parent_id)
.sort((a, b) => a.position - b.position);
const childrenCategories = data
.filter(c => typeof c.parent_id === 'number')
.sort((a, b) => b.position - a.position);
childrenCategories.forEach(c => {
const parentIndex = sortedCategories.findIndex(i => i.id === c.parent_id);
sortedCategories.splice(parentIndex + 1, 0, c);
});
setProductCategories(sortedCategories);
setProductCategories(new ProductLib().sortCategories(data));
}).catch((error) => onError(error));
};

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import slugify from 'slugify';
import _ from 'lodash';
@ -19,6 +19,7 @@ import MachineAPI from '../../api/machine';
import ProductAPI from '../../api/product';
import { Plus } from 'phosphor-react';
import { ProductStockForm } from './product-stock-form';
import ProductLib from '../../lib/product';
interface ProductFormProps {
product: Product,
@ -53,18 +54,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
useEffect(() => {
ProductCategoryAPI.index().then(data => {
// Map product categories by position
const sortedCategories = data
.filter(c => !c.parent_id)
.sort((a, b) => a.position - b.position);
const childrenCategories = data
.filter(c => typeof c.parent_id === 'number')
.sort((a, b) => b.position - a.position);
childrenCategories.forEach(c => {
const parentIndex = sortedCategories.findIndex(i => i.id === c.parent_id);
sortedCategories.splice(parentIndex + 1, 0, c);
});
setProductCategories(buildSelectOptions(sortedCategories));
setProductCategories(buildSelectOptions(new ProductLib().sortCategories(data)));
}).catch(onError);
MachineAPI.index({ disabled: false }).then(data => {
setMachines(buildChecklistOptions(data));
@ -111,10 +101,8 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
/**
* Callback triggered when the form is submitted: process with the product creation or update.
*/
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
return handleSubmit((data: Product) => {
saveProduct(data);
})(event);
const onSubmit: SubmitHandler<Product> = (data: Product) => {
saveProduct(data);
};
/**
@ -236,13 +224,13 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
<FabButton className="main-action-btn" onClick={handleSubmit(saveProduct)}>{t('app.admin.store.product_form.save')}</FabButton>
</div>
</header>
<form className="product-form" onSubmit={onSubmit}>
<form className="product-form" onSubmit={handleSubmit(onSubmit)}>
<div className='tabs'>
<p className={!stockTab ? 'is-active' : ''} onClick={() => setStockTab(false)}>{t('app.admin.store.product_form.product_parameters')}</p>
<p className={stockTab ? 'is-active' : ''} onClick={() => setStockTab(true)}>{t('app.admin.store.product_form.stock_management')}</p>
</div>
{stockTab
? <ProductStockForm product={product} register={register} control={control} id="stock" onError={onError} onSuccess={onSuccess} />
? <ProductStockForm product={product} register={register} control={control} formState={formState} onError={onError} onSuccess={onSuccess} />
: <section>
<div className="subgrid">
<FormInput id="name"

View File

@ -12,6 +12,7 @@ import { FabButton } from '../base/fab-button';
import { PencilSimple } from 'phosphor-react';
import { FabModal, ModalSize } from '../base/fab-modal';
import { ProductStockModal } from './product-stock-modal';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { FabStateLabel } from '../base/fab-state-label';
interface ProductStockFormProps<TFieldValues, TContext extends object> {
@ -26,7 +27,7 @@ interface ProductStockFormProps<TFieldValues, TContext extends object> {
/**
* Form tab to manage a product's stock
*/
export const ProductStockForm = <TFieldValues, TContext extends object> ({ product, register, control, formState, onError, onSuccess }: ProductStockFormProps<TFieldValues, TContext>) => {
export const ProductStockForm = <TFieldValues extends FieldValues, TContext extends object> ({ product, register, control, formState, onError, onSuccess }: ProductStockFormProps<TFieldValues, TContext>) => {
const { t } = useTranslation('admin');
const [activeThreshold, setActiveThreshold] = useState<boolean>(false);
@ -197,12 +198,11 @@ export const ProductStockForm = <TFieldValues, TContext extends object> ({ produ
</div>
<FabModal title={t('app.admin.store.product_stock_form.modal_title')}
className="fab-modal-lg"
width={ModalSize.large}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton>
<ProductStockModal product={product} register={register} control={control} id="stock-modal" onError={onError} onSuccess={onSuccess} />
<ProductStockModal product={product} register={register} control={control} formState={formState} onError={onError} onSuccess={onSuccess} />
</FabModal>
</section>
);

View File

@ -6,6 +6,7 @@ import { Control, FormState } from 'react-hook-form/dist/types/form';
import { FormSelect } from '../form/form-select';
import { FormInput } from '../form/form-input';
import { FabButton } from '../base/fab-button';
import { FieldValues } from 'react-hook-form/dist/types/fields';
type selectOption = { value: number, label: string };
@ -23,7 +24,7 @@ interface ProductStockModalProps<TFieldValues, TContext extends object> {
*/
// TODO: delete next eslint disable
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const ProductStockModal = <TFieldValues, TContext extends object> ({ product, register, control, formState, onError, onSuccess }: ProductStockModalProps<TFieldValues, TContext>) => {
export const ProductStockModal = <TFieldValues extends FieldValues, TContext extends object> ({ product, register, control, formState, onError, onSuccess }: ProductStockModalProps<TFieldValues, TContext>) => {
const { t } = useTranslation('admin');
const [movement, setMovement] = useState<'in' | 'out'>('in');

View File

@ -15,6 +15,7 @@ import { AccordionItem } from './accordion-item';
import { X } from 'phosphor-react';
import { StoreListHeader } from './store-list-header';
import { FabPagination } from '../base/fab-pagination';
import ProductLib from '../../lib/product';
declare const Application: IApplication;
@ -53,18 +54,7 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
});
ProductCategoryAPI.index().then(data => {
// Map product categories by position
const sortedCategories = data
.filter(c => !c.parent_id)
.sort((a, b) => a.position - b.position);
const childrenCategories = data
.filter(c => typeof c.parent_id === 'number')
.sort((a, b) => b.position - a.position);
childrenCategories.forEach(c => {
const parentIndex = sortedCategories.findIndex(i => i.id === c.parent_id);
sortedCategories.splice(parentIndex + 1, 0, c);
});
setProductCategories(sortedCategories);
setProductCategories(new ProductLib().sortCategories(data));
}).catch(onError);
MachineAPI.index({ disabled: false }).then(data => {

View File

@ -12,13 +12,14 @@ import noImage from '../../../../images/no_image.png';
interface StoreProductItemProps {
product: Product,
cart: Order,
onSuccessAddProductToCart: (cart: Order) => void
onSuccessAddProductToCart: (cart: Order) => void,
onError: (message: string) => void
}
/**
* This component shows a product item in store
*/
export const StoreProductItem: React.FC<StoreProductItemProps> = ({ product, cart, onSuccessAddProductToCart }) => {
export const StoreProductItem: React.FC<StoreProductItemProps> = ({ product, cart, onSuccessAddProductToCart, onError }) => {
const { t } = useTranslation('public');
/**
@ -38,7 +39,7 @@ export const StoreProductItem: React.FC<StoreProductItemProps> = ({ product, car
const addProductToCart = (e: React.BaseSyntheticEvent) => {
e.preventDefault();
e.stopPropagation();
CartAPI.addItem(cart, product.id, 1).then(onSuccessAddProductToCart);
CartAPI.addItem(cart, product.id, 1).then(onSuccessAddProductToCart).catch(onError);
};
/**

View File

@ -7,9 +7,12 @@ import { Loader } from '../base/loader';
import { IApplication } from '../../models/application';
import _ from 'lodash';
import { Product } from '../../models/product';
import { User } from '../../models/user';
import ProductAPI from '../../api/product';
import CartAPI from '../../api/cart';
import noImage from '../../../../images/no_image.png';
import { FabButton } from '../base/fab-button';
import useCart from '../../hooks/use-cart';
import { FilePdf, Minus, Plus } from 'phosphor-react';
import { FabStateLabel } from '../base/fab-state-label';
@ -17,15 +20,18 @@ declare const Application: IApplication;
interface StoreProductProps {
productSlug: string,
onSuccess: (message: string) => void,
onError: (message: string) => void,
currentUser?: User
}
/**
* This component shows a product
*/
export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, onError }) => {
export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, currentUser, onSuccess, onError }) => {
const { t } = useTranslation('public');
const { cart, setCart } = useCart(currentUser);
const [product, setProduct] = useState<Product>();
const [showImage, setShowImage] = useState<number>(null);
const [toCartCount, setToCartCount] = useState<number>(0);
@ -37,11 +43,13 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, onError
ProductAPI.get(productSlug).then(data => {
setProduct(data);
const productImage = _.find(data.product_images_attributes, { is_main: true });
setShowImage(productImage.id);
if (productImage) {
setShowImage(productImage.id);
}
setToCartCount(data.quantity_min ? data.quantity_min : 1);
setDisplayToggle(descContainer.current.offsetHeight < descContainer.current.scrollHeight);
}).catch(() => {
onError(t('app.public.store_product.unexpected_error_occurred'));
}).catch((e) => {
onError(t('app.public.store_product.unexpected_error_occurred') + e);
});
}, []);
@ -108,7 +116,10 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, onError
* Add product to cart
*/
const addToCart = () => {
console.log('Add', toCartCount, 'to cart');
CartAPI.addItem(cart, product.id, toCartCount).then(data => {
setCart(data);
onSuccess(t('app.public.store.add_to_cart_success'));
}).catch(onError);
};
if (product) {
@ -188,12 +199,12 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, onError
return null;
};
const StoreProductWrapper: React.FC<StoreProductProps> = ({ productSlug, onError }) => {
const StoreProductWrapper: React.FC<StoreProductProps> = (props) => {
return (
<Loader>
<StoreProduct productSlug={productSlug} onError={onError} />
<StoreProduct {...props} />
</Loader>
);
};
Application.Components.component('storeProduct', react2angular(StoreProductWrapper, ['productSlug', 'onError']));
Application.Components.component('storeProduct', react2angular(StoreProductWrapper, ['productSlug', 'currentUser', 'onSuccess', 'onError']));

View File

@ -11,7 +11,6 @@ import ProductCategoryAPI from '../../api/product-category';
import MachineAPI from '../../api/machine';
import { StoreProductItem } from './store-product-item';
import useCart from '../../hooks/use-cart';
import { emitCustomEvent } from 'react-custom-events';
import { User } from '../../models/user';
import { Order } from '../../models/order';
import { AccordionItem } from './accordion-item';
@ -71,10 +70,6 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser }) => {
});
}, []);
useEffect(() => {
emitCustomEvent('CartUpdate', cart);
}, [cart]);
useEffect(() => {
ProductAPI.index({ page: currentPage }).then(data => {
setProducts(data.products);
@ -246,7 +241,7 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser }) => {
/>
<div className="products-grid">
{products.map((product) => (
<StoreProductItem key={product.id} product={product} cart={cart} onSuccessAddProductToCart={addToCart} />
<StoreProductItem key={product.id} product={product} cart={cart} onSuccessAddProductToCart={addToCart} onError={onError} />
))}
</div>
{pageCount > 1 &&

View File

@ -29,6 +29,12 @@ export const MemberSelect: React.FC<MemberSelectProps> = ({ defaultUser, onSelec
}
}, []);
useEffect(() => {
if (!defaultUser && value) {
onSelected(value.value);
}
}, [defaultUser]);
/**
* search members by name
*/

View File

@ -179,7 +179,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
const userNetworks = new UserLib(user).getUserSocialNetworks();
return (
<form className={`user-profile-form user-profile-form--${size} ${className}`} onSubmit={onSubmit}>
<form className={`user-profile-form user-profile-form--${size} ${className || ''}`} onSubmit={onSubmit}>
<div className="avatar-group">
<AvatarInput currentAvatar={output.profile_attributes?.user_avatar_attributes?.attachment_url}
userName={`${output.profile_attributes?.first_name} ${output.profile_attributes?.last_name}`}

View File

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
import { emitCustomEvent } from 'react-custom-events';
import { Order } from '../models/order';
import CartAPI from '../api/cart';
import { getCartToken, setCartToken } from '../lib/cart-token';
@ -13,7 +14,7 @@ export default function useCart (user?: User) {
async function createCart () {
const currentCartToken = getCartToken();
const data = await CartAPI.create(currentCartToken);
setCart(data);
_setCart(data);
setLoading(false);
setCartToken(data.token);
}
@ -30,15 +31,20 @@ export default function useCart (user?: User) {
setLoading(true);
const currentCartToken = getCartToken();
const data = await CartAPI.create(currentCartToken);
setCart(data);
_setCart(data);
setLoading(false);
};
useEffect(() => {
if (user && cart && (!cart.statistic_profile_id || !cart.operator_id)) {
if (user && cart && (!cart.statistic_profile_id || !cart.operator_profile_id)) {
reloadCart();
}
}, [user]);
return { loading, cart, error, setCart, reloadCart };
const _setCart = (data: Order) => {
setCart(data);
emitCustomEvent('CartUpdate', data);
};
return { loading, cart, error, setCart: _setCart, reloadCart };
}

View File

@ -5,9 +5,9 @@ export const computePriceWithCoupon = (price: number, coupon?: Coupon): number =
return price;
}
if (coupon.type === 'percent_off') {
return price - (price * coupon.percent_off / 100.00);
return (Math.trunc(price * 100) - (Math.trunc(price * 100) * coupon.percent_off / 100)) / 100;
} else if (coupon.type === 'amount_off' && price > coupon.amount_off) {
return price - coupon.amount_off;
return (Math.trunc(price * 100) - Math.trunc(coupon.amount_off * 100)) / 100;
}
return price;
};

View File

@ -0,0 +1,21 @@
import { ProductCategory } from '../models/product-category';
export default class ProductLib {
/**
* Map product categories by position
* @param categories unsorted categories, as returned by the API
*/
sortCategories = (categories: Array<ProductCategory>): Array<ProductCategory> => {
const sortedCategories = categories
.filter(c => !c.parent_id)
.sort((a, b) => a.position - b.position);
const childrenCategories = categories
.filter(c => typeof c.parent_id === 'number')
.sort((a, b) => b.position - a.position);
childrenCategories.forEach(c => {
const parentIndex = sortedCategories.findIndex(i => i.id === c.parent_id);
sortedCategories.splice(parentIndex + 1, 0, c);
});
return sortedCategories;
};
}

View File

@ -22,11 +22,11 @@ export interface ProductsIndex {
}
export interface Product {
id: number,
id?: number,
name: string,
slug: string,
sku: string,
description: string,
sku?: string,
description?: string,
is_active: boolean,
product_category_id?: number,
amount?: number,

View File

@ -643,8 +643,8 @@ angular.module('application.router', ['ui.router'])
})
// cart
.state('app.public.cart', {
url: '/cart',
.state('app.public.store_cart', {
url: '/store/cart',
views: {
'main@': {
templateUrl: '/cart/index.html',

View File

@ -1,6 +1,6 @@
<div class="header-page">
<div class="back">
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
<a ui-sref="app.public.store"><i class="fas fa-long-arrow-alt-left "></i></a>
</div>
<div class="center">

View File

@ -13,5 +13,5 @@
</div>
<section class="m-lg">
<store-product product-slug="productSlug" on-error="onError" on-success="onSuccess" />
<store-product product-slug="productSlug" current-user="currentUser" on-error="onError" on-success="onSuccess" />
</section>

View File

@ -13,4 +13,8 @@ class CartPolicy < ApplicationPolicy
record.statistic_profile_id.nil? && record.operator_profile_id.nil?
end
end
def set_offer?
user.privileged?
end
end

View File

@ -15,7 +15,7 @@ class Cart::AddItemService
else
item.quantity += quantity.to_i
end
order.total += (orderable.amount * quantity.to_i)
order.total += (item.amount * quantity.to_i)
ActiveRecord::Base.transaction do
item.save
order.save

View File

@ -7,7 +7,7 @@ class Cart::RemoveItemService
raise ActiveRecord::RecordNotFound if item.nil?
order.total -= (item.amount * item.quantity.to_i)
order.total -= (item.amount * item.quantity.to_i) unless item.is_offered
ActiveRecord::Base.transaction do
item.destroy!
order.save

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
# Provides methods for set offer to item in cart
class Cart::SetOfferService
def call(order, orderable, is_offered)
item = order.order_items.find_by(orderable: orderable)
raise ActiveRecord::RecordNotFound if item.nil?
if !item.is_offered && is_offered
order.total -= (item.amount * item.quantity)
elsif item.is_offered && !is_offered
order.total += (item.amount * item.quantity)
end
item.is_offered = is_offered
ActiveRecord::Base.transaction do
item.save
order.save
end
order.reload
end
end

View File

@ -12,7 +12,7 @@ class Cart::SetQuantityService
raise ActiveRecord::RecordNotFound if item.nil?
different_quantity = quantity.to_i - item.quantity
order.total += (orderable.amount * different_quantity)
order.total += (item.amount * different_quantity) unless item.is_offered
ActiveRecord::Base.transaction do
item.update(quantity: quantity.to_i)
order.save

View File

@ -11,7 +11,7 @@ class Checkout::PaymentService
raise Cart::InactiveProductError unless Orders::OrderService.new.all_products_is_active?(order)
CouponService.new.validate(coupon_code, order.statistic_profile.user)
CouponService.new.validate(coupon_code, order.statistic_profile.user.id)
amount = debit_amount(order)
if operator.privileged? || amount.zero?

View File

@ -10,7 +10,7 @@ module Payments::PaymentConcern
end
def debit_amount(order, coupon_code = nil)
total = CouponService.new.apply(order.total, coupon_code, order.statistic_profile.user)
total = CouponService.new.apply(order.total, coupon_code, order.statistic_profile.user.id)
wallet_debit = get_wallet_debit(order.statistic_profile.user, total)
total - wallet_debit
end

View File

@ -10,7 +10,7 @@ if order&.statistic_profile&.user
end
end
json.order_items_attributes order.order_items do |item|
json.order_items_attributes order.order_items.order(created_at: :asc) do |item|
json.id item.id
json.orderable_type item.orderable_type
json.orderable_id item.orderable_id

View File

@ -159,6 +159,7 @@ Rails.application.routes.draw do
put 'add_item', on: :collection
put 'remove_item', on: :collection
put 'set_quantity', on: :collection
put 'set_offer', on: :collection
end
resources :checkout, only: %i[] do
post 'payment', on: :collection