mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-30 19:52:20 +01:00
(feat) add reservations in cart/order
This commit is contained in:
parent
05a6f517cd
commit
473aedbdcb
@ -4,16 +4,21 @@ import FormatLib from '../../lib/format';
|
|||||||
import OrderLib from '../../lib/order';
|
import OrderLib from '../../lib/order';
|
||||||
import { FabButton } from '../base/fab-button';
|
import { FabButton } from '../base/fab-button';
|
||||||
import Switch from 'react-switch';
|
import Switch from 'react-switch';
|
||||||
import type { OrderItem } from '../../models/order';
|
import type { ItemError, OrderItem } from '../../models/order';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
import { Order } from '../../models/order';
|
||||||
|
import CartAPI from '../../api/cart';
|
||||||
|
|
||||||
interface AbstractItemProps {
|
interface AbstractItemProps {
|
||||||
item: OrderItem,
|
item: OrderItem,
|
||||||
hasError: boolean,
|
errors: Array<ItemError>,
|
||||||
|
cart: Order,
|
||||||
|
setCart: (cart: Order) => void,
|
||||||
|
reloadCart: () => Promise<void>,
|
||||||
|
onError: (message: string) => void,
|
||||||
className?: string,
|
className?: string,
|
||||||
removeItemFromCart: (item: OrderItem) => void,
|
offerItemLabel?: string,
|
||||||
toggleItemOffer: (item: OrderItem, checked: boolean) => void,
|
|
||||||
privilegedOperator: boolean,
|
privilegedOperator: boolean,
|
||||||
actions?: ReactNode
|
actions?: ReactNode
|
||||||
}
|
}
|
||||||
@ -21,7 +26,7 @@ interface AbstractItemProps {
|
|||||||
/**
|
/**
|
||||||
* This component shares the common code for items in the cart (product, cart-item, etc)
|
* This component shares the common code for items in the cart (product, cart-item, etc)
|
||||||
*/
|
*/
|
||||||
export const AbstractItem: React.FC<AbstractItemProps> = ({ item, hasError, className, removeItemFromCart, toggleItemOffer, privilegedOperator, actions, children }) => {
|
export const AbstractItem: React.FC<AbstractItemProps> = ({ item, errors, cart, setCart, reloadCart, onError, className, offerItemLabel, privilegedOperator, actions, children }) => {
|
||||||
const { t } = useTranslation('public');
|
const { t } = useTranslation('public');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,7 +37,13 @@ export const AbstractItem: React.FC<AbstractItemProps> = ({ item, hasError, clas
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
removeItemFromCart(item);
|
if (errors.length === 1 && errors[0].error === 'not_found') {
|
||||||
|
reloadCart().catch(onError);
|
||||||
|
} else {
|
||||||
|
CartAPI.removeItem(cart, item.orderable_id, item.orderable_type).then(data => {
|
||||||
|
setCart(data);
|
||||||
|
}).catch(onError);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -40,11 +51,21 @@ export const AbstractItem: React.FC<AbstractItemProps> = ({ item, hasError, clas
|
|||||||
* Return the callback triggered when the privileged user enable/disable the offered attribute for the given item
|
* Return the callback triggered when the privileged user enable/disable the offered attribute for the given item
|
||||||
*/
|
*/
|
||||||
const handleToggleOffer = (item: OrderItem) => {
|
const handleToggleOffer = (item: OrderItem) => {
|
||||||
return (checked: boolean) => toggleItemOffer(item, checked);
|
return (checked: boolean) => {
|
||||||
|
CartAPI.setOffer(cart, item.orderable_id, item.orderable_type, checked).then(data => {
|
||||||
|
setCart(data);
|
||||||
|
}).catch(e => {
|
||||||
|
if (e.match(/code 403/)) {
|
||||||
|
onError(t('app.public.abstract_item.errors.unauthorized_offering_product'));
|
||||||
|
} else {
|
||||||
|
onError(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className={`item ${className || ''} ${hasError ? 'error' : ''}`}>
|
<article className={`item ${className || ''} ${errors.length > 0 ? 'error' : ''}`}>
|
||||||
<div className='picture'>
|
<div className='picture'>
|
||||||
<img alt='' src={item.orderable_main_image_url || noImage} />
|
<img alt='' src={item.orderable_main_image_url || noImage} />
|
||||||
</div>
|
</div>
|
||||||
@ -62,7 +83,7 @@ export const AbstractItem: React.FC<AbstractItemProps> = ({ item, hasError, clas
|
|||||||
{privilegedOperator &&
|
{privilegedOperator &&
|
||||||
<div className='offer'>
|
<div className='offer'>
|
||||||
<label>
|
<label>
|
||||||
<span>{t('app.public.abstract_item.offer_product')}</span>
|
<span>{offerItemLabel || t('app.public.abstract_item.offer_product')}</span>
|
||||||
<Switch
|
<Switch
|
||||||
checked={item.is_offered || false}
|
checked={item.is_offered || false}
|
||||||
onChange={handleToggleOffer(item)}
|
onChange={handleToggleOffer(item)}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import FormatLib from '../../lib/format';
|
import FormatLib from '../../lib/format';
|
||||||
import { CaretDown, CaretUp } from 'phosphor-react';
|
import { CaretDown, CaretUp } from 'phosphor-react';
|
||||||
import type { OrderProduct, OrderErrors, Order } from '../../models/order';
|
import type { OrderProduct, OrderErrors, Order, ItemError } from '../../models/order';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import CartAPI from '../../api/cart';
|
import CartAPI from '../../api/cart';
|
||||||
@ -14,22 +14,21 @@ interface CartOrderProductProps {
|
|||||||
className?: string,
|
className?: string,
|
||||||
cart: Order,
|
cart: Order,
|
||||||
setCart: (cart: Order) => void,
|
setCart: (cart: Order) => void,
|
||||||
|
reloadCart: () => Promise<void>,
|
||||||
onError: (message: string) => void,
|
onError: (message: string) => void,
|
||||||
removeProductFromCart: (item: OrderProduct) => void,
|
|
||||||
toggleProductOffer: (item: OrderProduct, checked: boolean) => void,
|
|
||||||
privilegedOperator: boolean,
|
privilegedOperator: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component shows a product in the cart
|
* This component shows a product in the cart
|
||||||
*/
|
*/
|
||||||
export const CartOrderProduct: React.FC<CartOrderProductProps> = ({ item, cartErrors, className, cart, setCart, onError, removeProductFromCart, toggleProductOffer, privilegedOperator }) => {
|
export const CartOrderProduct: React.FC<CartOrderProductProps> = ({ item, cartErrors, className, cart, setCart, reloadCart, onError, privilegedOperator }) => {
|
||||||
const { t } = useTranslation('public');
|
const { t } = useTranslation('public');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the given item's errors
|
* Get the given item's errors
|
||||||
*/
|
*/
|
||||||
const getItemErrors = (item: OrderProduct) => {
|
const getItemErrors = (item: OrderProduct): Array<ItemError> => {
|
||||||
if (!cartErrors) return [];
|
if (!cartErrors) return [];
|
||||||
const errors = _.find(cartErrors.details, (e) => e.item_id === item.id);
|
const errors = _.find(cartErrors.details, (e) => e.item_id === item.id);
|
||||||
return errors?.errors || [{ error: 'not_found' }];
|
return errors?.errors || [{ error: 'not_found' }];
|
||||||
@ -120,11 +119,13 @@ export const CartOrderProduct: React.FC<CartOrderProductProps> = ({ item, cartEr
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AbstractItem className={`cart-order-product ${className || ''}`}
|
<AbstractItem className={`cart-order-product ${className || ''}`}
|
||||||
hasError={getItemErrors(item).length > 0}
|
errors={getItemErrors(item)}
|
||||||
|
setCart={setCart}
|
||||||
|
cart={cart}
|
||||||
|
onError={onError}
|
||||||
|
reloadCart={reloadCart}
|
||||||
item={item}
|
item={item}
|
||||||
removeItemFromCart={removeProductFromCart}
|
|
||||||
privilegedOperator={privilegedOperator}
|
privilegedOperator={privilegedOperator}
|
||||||
toggleItemOffer={toggleProductOffer}
|
|
||||||
actions={buildActions()}>
|
actions={buildActions()}>
|
||||||
<div className="ref">
|
<div className="ref">
|
||||||
<span>{t('app.public.cart_order_product.reference_short')} {item.orderable_ref || ''}</span>
|
<span>{t('app.public.cart_order_product.reference_short')} {item.orderable_ref || ''}</span>
|
||||||
|
@ -3,30 +3,30 @@ import type { OrderErrors, Order } from '../../models/order';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { AbstractItem } from './abstract-item';
|
import { AbstractItem } from './abstract-item';
|
||||||
import { OrderCartItem } from '../../models/order';
|
import { OrderCartItemReservation } from '../../models/order';
|
||||||
|
import FormatLib from '../../lib/format';
|
||||||
|
|
||||||
interface CartOrderReservationProps {
|
interface CartOrderReservationProps {
|
||||||
item: OrderCartItem,
|
item: OrderCartItemReservation,
|
||||||
cartErrors: OrderErrors,
|
cartErrors: OrderErrors,
|
||||||
className?: string,
|
className?: string,
|
||||||
cart: Order,
|
cart: Order,
|
||||||
setCart: (cart: Order) => void,
|
setCart: (cart: Order) => void,
|
||||||
|
reloadCart: () => Promise<void>,
|
||||||
onError: (message: string) => void,
|
onError: (message: string) => void,
|
||||||
removeProductFromCart: (item: OrderCartItem) => void,
|
|
||||||
toggleProductOffer: (item: OrderCartItem, checked: boolean) => void,
|
|
||||||
privilegedOperator: boolean,
|
privilegedOperator: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component shows a product in the cart
|
* This component shows a product in the cart
|
||||||
*/
|
*/
|
||||||
export const CartOrderReservation: React.FC<CartOrderReservationProps> = ({ item, cartErrors, className, cart, setCart, onError, removeProductFromCart, toggleProductOffer, privilegedOperator }) => {
|
export const CartOrderReservation: React.FC<CartOrderReservationProps> = ({ item, cartErrors, className, cart, setCart, reloadCart, onError, privilegedOperator }) => {
|
||||||
const { t } = useTranslation('public');
|
const { t } = useTranslation('public');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the given item's errors
|
* Get the given item's errors
|
||||||
*/
|
*/
|
||||||
const getItemErrors = (item: OrderCartItem) => {
|
const getItemErrors = (item: OrderCartItemReservation) => {
|
||||||
if (!cartErrors) return [];
|
if (!cartErrors) return [];
|
||||||
const errors = _.find(cartErrors.details, (e) => e.item_id === item.id);
|
const errors = _.find(cartErrors.details, (e) => e.item_id === item.id);
|
||||||
return errors?.errors || [{ error: 'not_found' }];
|
return errors?.errors || [{ error: 'not_found' }];
|
||||||
@ -34,13 +34,26 @@ export const CartOrderReservation: React.FC<CartOrderReservationProps> = ({ item
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AbstractItem className={`cart-order-reservation ${className || ''}`}
|
<AbstractItem className={`cart-order-reservation ${className || ''}`}
|
||||||
hasError={getItemErrors(item).length > 0}
|
errors={getItemErrors(item)}
|
||||||
item={item}
|
item={item}
|
||||||
removeItemFromCart={removeProductFromCart}
|
cart={cart}
|
||||||
privilegedOperator={privilegedOperator}
|
setCart={setCart}
|
||||||
toggleItemOffer={toggleProductOffer}>
|
onError={onError}
|
||||||
|
reloadCart={reloadCart}
|
||||||
|
actions={<div/>}
|
||||||
|
offerItemLabel={t('app.public.cart_order_reservation.offer_reservation')}
|
||||||
|
privilegedOperator={privilegedOperator}>
|
||||||
<div className="ref">
|
<div className="ref">
|
||||||
<p>Réservation {item.orderable_name}</p>
|
<p>{t('app.public.cart_order_reservation.reservation')} {item.orderable_name}</p>
|
||||||
|
<ul>{item.slots_reservations.map(sr => (
|
||||||
|
<li key={sr.id}>
|
||||||
|
{
|
||||||
|
t('app.public.cart_order_reservation.slot',
|
||||||
|
{ DATE: FormatLib.date(sr.slot.start_at), START: FormatLib.time(sr.slot.start_at), END: FormatLib.time(sr.slot.end_at) })
|
||||||
|
}
|
||||||
|
<span>{sr.offered ? t('app.public.cart_order_reservation.offered') : ''}</span>
|
||||||
|
</li>
|
||||||
|
))}</ul>
|
||||||
{getItemErrors(item)}
|
{getItemErrors(item)}
|
||||||
</div>
|
</div>
|
||||||
</AbstractItem>
|
</AbstractItem>
|
||||||
|
@ -11,7 +11,7 @@ import CartAPI from '../../api/cart';
|
|||||||
import type { User } from '../../models/user';
|
import type { User } from '../../models/user';
|
||||||
import { PaymentModal } from '../payment/stripe/payment-modal';
|
import { PaymentModal } from '../payment/stripe/payment-modal';
|
||||||
import { PaymentMethod } from '../../models/payment';
|
import { PaymentMethod } from '../../models/payment';
|
||||||
import type { Order, OrderCartItem, OrderErrors, OrderItem, OrderProduct } from '../../models/order';
|
import type { Order, OrderCartItemReservation, OrderErrors, OrderProduct } from '../../models/order';
|
||||||
import { MemberSelect } from '../user/member-select';
|
import { MemberSelect } from '../user/member-select';
|
||||||
import { CouponInput } from '../coupon/coupon-input';
|
import { CouponInput } from '../coupon/coupon-input';
|
||||||
import type { Coupon } from '../../models/coupon';
|
import type { Coupon } from '../../models/coupon';
|
||||||
@ -53,20 +53,6 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
}
|
}
|
||||||
}, [cart]);
|
}, [cart]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the product from cart
|
|
||||||
*/
|
|
||||||
const removeProductFromCart = (item) => {
|
|
||||||
const errors = getItemErrors(item);
|
|
||||||
if (errors.length === 1 && errors[0].error === 'not_found') {
|
|
||||||
reloadCart().catch(onError);
|
|
||||||
} else {
|
|
||||||
CartAPI.removeItem(cart, item.orderable_id, item.orderable_type).then(data => {
|
|
||||||
setCart(data);
|
|
||||||
}).catch(onError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the current cart's items (available, price, stock, quantity_min)
|
* Check the current cart's items (available, price, stock, quantity_min)
|
||||||
*/
|
*/
|
||||||
@ -109,15 +95,6 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* get givean item's error
|
|
||||||
*/
|
|
||||||
const getItemErrors = (item) => {
|
|
||||||
if (!cartErrors) return [];
|
|
||||||
const errors = _.find(cartErrors.details, (e) => e.item_id === item.id);
|
|
||||||
return errors?.errors || [{ error: 'not_found' }];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open/closes the payment modal
|
* Open/closes the payment modal
|
||||||
*/
|
*/
|
||||||
@ -159,21 +136,6 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
return cart && cart.order_items_attributes.length === 0;
|
return cart && cart.order_items_attributes.length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle product offer
|
|
||||||
*/
|
|
||||||
const toggleProductOffer = (item: OrderItem, checked: boolean) => {
|
|
||||||
CartAPI.setOffer(cart, item.orderable_id, item.orderable_type, checked).then(data => {
|
|
||||||
setCart(data);
|
|
||||||
}).catch(e => {
|
|
||||||
if (e.match(/code 403/)) {
|
|
||||||
onError(t('app.public.store_cart.errors.unauthorized_offering_product'));
|
|
||||||
} else {
|
|
||||||
onError(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply coupon to current cart
|
* Apply coupon to current cart
|
||||||
*/
|
*/
|
||||||
@ -196,22 +158,20 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
cartErrors={cartErrors}
|
cartErrors={cartErrors}
|
||||||
cart={cart}
|
cart={cart}
|
||||||
setCart={setCart}
|
setCart={setCart}
|
||||||
|
reloadCart={reloadCart}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
removeProductFromCart={removeProductFromCart}
|
|
||||||
toggleProductOffer={toggleProductOffer}
|
|
||||||
privilegedOperator={isPrivileged()} />
|
privilegedOperator={isPrivileged()} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<CartOrderReservation item={item as OrderCartItem}
|
<CartOrderReservation item={item as OrderCartItemReservation}
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="store-cart-list-item"
|
className="store-cart-list-item"
|
||||||
cartErrors={cartErrors}
|
cartErrors={cartErrors}
|
||||||
cart={cart}
|
cart={cart}
|
||||||
|
reloadCart={reloadCart}
|
||||||
setCart={setCart}
|
setCart={setCart}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
removeProductFromCart={removeProductFromCart}
|
|
||||||
toggleProductOffer={toggleProductOffer}
|
|
||||||
privilegedOperator={isPrivileged()} />
|
privilegedOperator={isPrivileged()} />
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -39,8 +39,9 @@ export interface CartItemPrepaidPack extends CartItem {
|
|||||||
export interface CartItemFreeExtension extends CartItem {
|
export interface CartItemFreeExtension extends CartItem {
|
||||||
free_extension: { end_at: Date }
|
free_extension: { end_at: Date }
|
||||||
}
|
}
|
||||||
|
export type CartItemReservationType = 'CartItem::MachineReservation' | 'CartItem::SpaceReservation' | 'CartItem::TrainingReservation';
|
||||||
|
|
||||||
export type CartItemType = 'CartItem::EventReservation' | 'CartItem::MachineReservation' | 'CartItem::PrepaidPack' | 'CartItem::SpaceReservation' | 'CartItem::Subscription' | 'CartItem::TrainingReservation';
|
export type CartItemType = 'CartItem::EventReservation' | 'CartItem::PrepaidPack' | 'CartItem::Subscription' | CartItemReservationType;
|
||||||
|
|
||||||
export interface CartItemResponse {
|
export interface CartItemResponse {
|
||||||
id: number,
|
id: number,
|
||||||
|
@ -4,7 +4,7 @@ import { CreateTokenResponse } from './payzen';
|
|||||||
import { UserRole } from './user';
|
import { UserRole } from './user';
|
||||||
import { Coupon } from './coupon';
|
import { Coupon } from './coupon';
|
||||||
import { ApiFilter, PaginatedIndex } from './api';
|
import { ApiFilter, PaginatedIndex } from './api';
|
||||||
import { CartItemType } from './cart_item';
|
import type { CartItemReservationType, CartItemType } from './cart_item';
|
||||||
|
|
||||||
export type OrderableType = 'Product' | CartItemType;
|
export type OrderableType = 'Product' | CartItemType;
|
||||||
|
|
||||||
@ -13,6 +13,7 @@ export interface OrderItem {
|
|||||||
orderable_type: OrderableType,
|
orderable_type: OrderableType,
|
||||||
orderable_id: number,
|
orderable_id: number,
|
||||||
orderable_name: string,
|
orderable_name: string,
|
||||||
|
orderable_slug: string,
|
||||||
orderable_main_image_url?: string;
|
orderable_main_image_url?: string;
|
||||||
quantity: number,
|
quantity: number,
|
||||||
amount: number,
|
amount: number,
|
||||||
@ -21,14 +22,22 @@ export interface OrderItem {
|
|||||||
|
|
||||||
export interface OrderProduct extends OrderItem {
|
export interface OrderProduct extends OrderItem {
|
||||||
orderable_type: 'Product',
|
orderable_type: 'Product',
|
||||||
orderable_slug: string,
|
|
||||||
orderable_ref?: string,
|
orderable_ref?: string,
|
||||||
orderable_external_stock: number,
|
orderable_external_stock: number,
|
||||||
quantity_min: number
|
quantity_min: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderCartItem extends OrderItem {
|
export interface OrderCartItemReservation extends OrderItem {
|
||||||
orderable_type: CartItemType
|
orderable_type: CartItemReservationType
|
||||||
|
slots_reservations: Array<{
|
||||||
|
id: number,
|
||||||
|
offered: boolean,
|
||||||
|
slot: {
|
||||||
|
id: number,
|
||||||
|
start_at: TDateISO,
|
||||||
|
end_at: TDateISO
|
||||||
|
}
|
||||||
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Order {
|
export interface Order {
|
||||||
@ -78,13 +87,14 @@ export interface OrderIndexFilter extends ApiFilter {
|
|||||||
period_to?: string
|
period_to?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ItemError {
|
||||||
|
error: string,
|
||||||
|
value?: string|number
|
||||||
|
}
|
||||||
export interface OrderErrors {
|
export interface OrderErrors {
|
||||||
order_id: number,
|
order_id: number,
|
||||||
details: Array<{
|
details: Array<{
|
||||||
item_id: number,
|
item_id: number,
|
||||||
errors: Array<{
|
errors: Array<ItemError>
|
||||||
error: string,
|
|
||||||
value: string|number
|
|
||||||
}>
|
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
@import "modules/base/labelled-input";
|
@import "modules/base/labelled-input";
|
||||||
@import "modules/calendar/calendar";
|
@import "modules/calendar/calendar";
|
||||||
@import "modules/cart/cart-button";
|
@import "modules/cart/cart-button";
|
||||||
|
@import "modules/cart/cart-order-reservation";
|
||||||
@import "modules/cart/store-cart";
|
@import "modules/cart/store-cart";
|
||||||
@import "modules/dashboard/reservations/credits-panel";
|
@import "modules/dashboard/reservations/credits-panel";
|
||||||
@import "modules/dashboard/reservations/prepaid-packs-panel";
|
@import "modules/dashboard/reservations/prepaid-packs-panel";
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
.cart-order-reservation {
|
||||||
|
.actions:before {
|
||||||
|
content: ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
.ref > ul > li > span {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
}
|
@ -4,4 +4,18 @@
|
|||||||
# This is a single spot to configure app-wide model behavior.
|
# This is a single spot to configure app-wide model behavior.
|
||||||
class ApplicationRecord < ActiveRecord::Base
|
class ApplicationRecord < ActiveRecord::Base
|
||||||
self.abstract_class = true
|
self.abstract_class = true
|
||||||
|
|
||||||
|
# Update attributes with validation context.
|
||||||
|
# In Rails you can provide a context while you save, for example: `.save(:step1)`, but no way to
|
||||||
|
# provide a context while you update. This method just adds the way to update with validation
|
||||||
|
# context.
|
||||||
|
#
|
||||||
|
# @param attributes [Hash] attributes to assign
|
||||||
|
# @param context [*] validation context
|
||||||
|
def update_with_context(attributes, context)
|
||||||
|
with_transaction_returning_status do
|
||||||
|
assign_attributes(attributes)
|
||||||
|
save(context: context)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
# An event reservation added to the shopping cart
|
# An event reservation added to the shopping cart
|
||||||
class CartItem::EventReservation < CartItem::Reservation
|
class CartItem::EventReservation < CartItem::Reservation
|
||||||
|
self.table_name = 'cart_item_event_reservations'
|
||||||
|
|
||||||
has_many :cart_item_event_reservation_tickets, class_name: 'CartItem::EventReservationTicket', dependent: :destroy,
|
has_many :cart_item_event_reservation_tickets, class_name: 'CartItem::EventReservationTicket', dependent: :destroy,
|
||||||
inverse_of: :cart_item_event_reservation,
|
inverse_of: :cart_item_event_reservation,
|
||||||
foreign_key: 'cart_item_event_reservation_id'
|
foreign_key: 'cart_item_event_reservation_id'
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
# A machine reservation added to the shopping cart
|
# A machine reservation added to the shopping cart
|
||||||
class CartItem::MachineReservation < CartItem::Reservation
|
class CartItem::MachineReservation < CartItem::Reservation
|
||||||
self.table_name = 'cart_item_reservations'
|
|
||||||
|
|
||||||
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy, inverse_of: :cart_item,
|
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy, inverse_of: :cart_item,
|
||||||
foreign_key: 'cart_item_id', foreign_type: 'cart_item_type'
|
foreign_key: 'cart_item_id', foreign_type: 'cart_item_type'
|
||||||
accepts_nested_attributes_for :cart_item_reservation_slots
|
accepts_nested_attributes_for :cart_item_reservation_slots
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
MINUTES_PER_HOUR = 60.0
|
|
||||||
SECONDS_PER_MINUTE = 60.0
|
|
||||||
|
|
||||||
GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division: true, prepaid: { minutes: 0 }, custom_duration: nil }.freeze
|
|
||||||
|
|
||||||
# A generic reservation added to the shopping cart
|
# A generic reservation added to the shopping cart
|
||||||
class CartItem::Reservation < CartItem::BaseItem
|
class CartItem::Reservation < CartItem::BaseItem
|
||||||
self.abstract_class = true
|
MINUTES_PER_HOUR = 60.0
|
||||||
|
SECONDS_PER_MINUTE = 60.0
|
||||||
|
|
||||||
|
GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division: true, prepaid: { minutes: 0 }, custom_duration: nil }.freeze
|
||||||
|
|
||||||
def reservable
|
def reservable
|
||||||
nil
|
nil
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
# A space reservation added to the shopping cart
|
# A space reservation added to the shopping cart
|
||||||
class CartItem::SpaceReservation < CartItem::Reservation
|
class CartItem::SpaceReservation < CartItem::Reservation
|
||||||
self.table_name = 'cart_item_reservations'
|
|
||||||
|
|
||||||
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy, inverse_of: :cart_item,
|
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy, inverse_of: :cart_item,
|
||||||
foreign_key: 'cart_item_id', foreign_type: 'cart_item_type'
|
foreign_key: 'cart_item_id', foreign_type: 'cart_item_type'
|
||||||
accepts_nested_attributes_for :cart_item_reservation_slots
|
accepts_nested_attributes_for :cart_item_reservation_slots
|
||||||
|
@ -29,7 +29,7 @@ class Cart::AddItemService
|
|||||||
def add_product(order, orderable, quantity)
|
def add_product(order, orderable, quantity)
|
||||||
raise Cart::InactiveProductError unless orderable.is_active
|
raise Cart::InactiveProductError unless orderable.is_active
|
||||||
|
|
||||||
item = order.order_items.find_by(orderable: orderable)
|
item = order.order_items.find_by(orderable_type: orderable.class.name, orderable_id: orderable.id)
|
||||||
quantity = orderable.quantity_min > quantity.to_i && item.nil? ? orderable.quantity_min : quantity.to_i
|
quantity = orderable.quantity_min > quantity.to_i && item.nil? ? orderable.quantity_min : quantity.to_i
|
||||||
|
|
||||||
if item.nil?
|
if item.nil?
|
||||||
@ -43,6 +43,6 @@ class Cart::AddItemService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def add_cart_item(order, orderable)
|
def add_cart_item(order, orderable)
|
||||||
order.order_items.new(quantity: 1, orderable: orderable, amount: orderable.price[:amount] || 0)
|
order.order_items.new(quantity: 1, orderable_type: orderable.class.name, orderable_id: orderable.id, amount: orderable.price[:amount] || 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
# Provides methods to create new cart items, based on an existing Order
|
# Provides methods to create new cart items, based on an existing Order
|
||||||
class Cart::CreateCartItemService
|
class Cart::CreateCartItemService
|
||||||
def initialize(order)
|
def initialize(order)
|
||||||
raise Cart::AnonymousError if order.statistic_profile.nil? || order.operator_profile.nil?
|
raise Cart::AnonymousError, I18n.t('cart_validation.select_user') if order.statistic_profile.nil? || order.operator_profile.nil?
|
||||||
|
|
||||||
@order = order
|
@order = order
|
||||||
@customer = order.user
|
@customer = order.user
|
||||||
@operator = order.user.privileged? ? order.operator_profile.user : order.user
|
@operator = order.operator_profile.user
|
||||||
end
|
end
|
||||||
|
|
||||||
def create(item)
|
def create(item)
|
||||||
|
@ -5,7 +5,7 @@ class Cart::RefreshItemService
|
|||||||
def call(order, orderable)
|
def call(order, orderable)
|
||||||
raise Cart::InactiveProductError unless orderable.is_active
|
raise Cart::InactiveProductError unless orderable.is_active
|
||||||
|
|
||||||
item = order.order_items.find_by(orderable: orderable)
|
item = order.order_items.find_by(orderable_type: orderable.class.name, orderable_id: orderable.id)
|
||||||
|
|
||||||
raise ActiveRecord::RecordNotFound if item.nil?
|
raise ActiveRecord::RecordNotFound if item.nil?
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
# Provides methods for remove order item to cart
|
# Provides methods for remove order item to cart
|
||||||
class Cart::RemoveItemService
|
class Cart::RemoveItemService
|
||||||
def call(order, orderable)
|
def call(order, orderable)
|
||||||
item = order.order_items.find_by(orderable: orderable)
|
item = order.order_items.find_by(orderable_type: orderable.class.name, orderable_id: orderable.id)
|
||||||
|
|
||||||
raise ActiveRecord::RecordNotFound if item.nil?
|
raise ActiveRecord::RecordNotFound if item.nil?
|
||||||
|
|
||||||
|
@ -5,10 +5,13 @@ module Cart; end
|
|||||||
|
|
||||||
# Provides methods to update the customer of the given cart
|
# Provides methods to update the customer of the given cart
|
||||||
class Cart::SetCustomerService
|
class Cart::SetCustomerService
|
||||||
|
# @param operator [User]
|
||||||
def initialize(operator)
|
def initialize(operator)
|
||||||
@operator = operator
|
@operator = operator
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param order[Order]
|
||||||
|
# @param customer [User]
|
||||||
def call(order, customer)
|
def call(order, customer)
|
||||||
return order unless @operator.privileged?
|
return order unless @operator.privileged?
|
||||||
|
|
||||||
@ -19,6 +22,9 @@ class Cart::SetCustomerService
|
|||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
order.statistic_profile_id = customer.statistic_profile.id
|
order.statistic_profile_id = customer.statistic_profile.id
|
||||||
order.operator_profile_id = @operator.invoicing_profile.id
|
order.operator_profile_id = @operator.invoicing_profile.id
|
||||||
|
order.order_items.each do |item|
|
||||||
|
update_item_user(item, customer)
|
||||||
|
end
|
||||||
unset_offer(order, customer)
|
unset_offer(order, customer)
|
||||||
Cart::UpdateTotalService.new.call(order)
|
Cart::UpdateTotalService.new.call(order)
|
||||||
order.save
|
order.save
|
||||||
@ -26,7 +32,11 @@ class Cart::SetCustomerService
|
|||||||
order.reload
|
order.reload
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
# If the operator is also the customer, he cannot offer items to himself, so we unset all the offers
|
# If the operator is also the customer, he cannot offer items to himself, so we unset all the offers
|
||||||
|
# @param order[Order]
|
||||||
|
# @param customer [User]
|
||||||
def unset_offer(order, customer)
|
def unset_offer(order, customer)
|
||||||
return unless @operator == customer
|
return unless @operator == customer
|
||||||
|
|
||||||
@ -35,4 +45,15 @@ class Cart::SetCustomerService
|
|||||||
item.save
|
item.save
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param item[OrderItem]
|
||||||
|
# @param customer [User]
|
||||||
|
def update_item_user(item, customer)
|
||||||
|
return unless item.orderable_type.match(/^CartItem::/)
|
||||||
|
|
||||||
|
item.orderable.update_with_context({
|
||||||
|
customer_profile_id: customer.invoicing_profile.id,
|
||||||
|
operator_profile_id: @operator.invoicing_profile.id
|
||||||
|
}, item.order.order_items)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -6,7 +6,7 @@ module Cart; end
|
|||||||
# Provides methods for set offer to item in cart
|
# Provides methods for set offer to item in cart
|
||||||
class Cart::SetOfferService
|
class Cart::SetOfferService
|
||||||
def call(order, orderable, is_offered)
|
def call(order, orderable, is_offered)
|
||||||
item = order.order_items.find_by(orderable: orderable)
|
item = order.order_items.find_by(orderable_type: orderable.class.name, orderable_id: orderable.id)
|
||||||
|
|
||||||
raise ActiveRecord::RecordNotFound if item.nil?
|
raise ActiveRecord::RecordNotFound if item.nil?
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ class Cart::SetQuantityService
|
|||||||
|
|
||||||
raise Cart::OutStockError if quantity.to_i > orderable.stock['external']
|
raise Cart::OutStockError if quantity.to_i > orderable.stock['external']
|
||||||
|
|
||||||
item = order.order_items.find_by(orderable: orderable)
|
item = order.order_items.find_by(orderable_type: orderable.class.name, orderable_id: orderable.id)
|
||||||
|
|
||||||
raise ActiveRecord::RecordNotFound if item.nil?
|
raise ActiveRecord::RecordNotFound if item.nil?
|
||||||
|
|
||||||
|
@ -2,13 +2,24 @@
|
|||||||
|
|
||||||
# Provides methods for update total of cart
|
# Provides methods for update total of cart
|
||||||
class Cart::UpdateTotalService
|
class Cart::UpdateTotalService
|
||||||
|
# @param order[Order]
|
||||||
def call(order)
|
def call(order)
|
||||||
total = 0
|
total = 0
|
||||||
order.order_items.each do |item|
|
order.order_items.each do |item|
|
||||||
|
update_item_price(item)
|
||||||
total += (item.amount * item.quantity) unless item.is_offered
|
total += (item.amount * item.quantity) unless item.is_offered
|
||||||
end
|
end
|
||||||
order.total = total
|
order.total = total
|
||||||
order.save
|
order.save
|
||||||
order
|
order
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# @param item[OrderItem]
|
||||||
|
def update_item_price(item)
|
||||||
|
return unless item.orderable_type.match(/^CartItem::/)
|
||||||
|
|
||||||
|
item.update(amount: item.orderable.price[:amount] || 0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
11
app/views/api/orders/_cart_item_reservation.json.jbuilder
Normal file
11
app/views/api/orders/_cart_item_reservation.json.jbuilder
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
json.orderable_name item.orderable.reservable.name
|
||||||
|
json.orderable_slug item.orderable.reservable.slug
|
||||||
|
json.orderable_main_image_url item.orderable.reservable&.try("#{item.orderable.reservable_type.downcase}_image")&.attachment&.medium&.url
|
||||||
|
json.slots_reservations item.orderable.cart_item_reservation_slots do |sr|
|
||||||
|
json.extract! sr, :id, :offered
|
||||||
|
json.slot do
|
||||||
|
json.extract! sr.slot, :id, :start_at, :end_at
|
||||||
|
end
|
||||||
|
end
|
@ -28,4 +28,7 @@ json.order_items_attributes order.order_items.order(created_at: :asc) do |item|
|
|||||||
json.amount item.amount / 100.0
|
json.amount item.amount / 100.0
|
||||||
json.is_offered item.is_offered
|
json.is_offered item.is_offered
|
||||||
json.partial! 'api/orders/product', item: item if item.orderable_type == 'Product'
|
json.partial! 'api/orders/product', item: item if item.orderable_type == 'Product'
|
||||||
|
if %w[CartItem::MachineReservation CartItem::SpaceReservation CartItem::TrainingReservation].include?(item.orderable_type)
|
||||||
|
json.partial! 'api/orders/cart_item_reservation', item: item
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -446,6 +446,8 @@ en:
|
|||||||
abstract_item:
|
abstract_item:
|
||||||
offer_product: "Offer the product"
|
offer_product: "Offer the product"
|
||||||
total: "Total"
|
total: "Total"
|
||||||
|
errors:
|
||||||
|
unauthorized_offering_product: "You can't offer anything to yourself"
|
||||||
cart_order_product:
|
cart_order_product:
|
||||||
reference_short: "ref:"
|
reference_short: "ref:"
|
||||||
minimum_purchase: "Minimum purchase: "
|
minimum_purchase: "Minimum purchase: "
|
||||||
@ -458,7 +460,11 @@ en:
|
|||||||
stock_limit_QUANTITY: "Only {QUANTITY} {QUANTITY, plural, =1{unit} other{units}} left in stock, please adjust the quantity of items."
|
stock_limit_QUANTITY: "Only {QUANTITY} {QUANTITY, plural, =1{unit} other{units}} left in stock, please adjust the quantity of items."
|
||||||
quantity_min_QUANTITY: "Minimum number of product was changed to {QUANTITY}, please adjust the quantity of items."
|
quantity_min_QUANTITY: "Minimum number of product was changed to {QUANTITY}, please adjust the quantity of items."
|
||||||
price_changed_PRICE: "The product price was modified to {PRICE}"
|
price_changed_PRICE: "The product price was modified to {PRICE}"
|
||||||
unauthorized_offering_product: "You can't offer anything to yourself"
|
cart_order_reservation:
|
||||||
|
reservation: "Reservation"
|
||||||
|
offer_reservation: "Offer the reservation"
|
||||||
|
slot: "{DATE}: {START} - {END}"
|
||||||
|
offered: "offered"
|
||||||
orders_dashboard:
|
orders_dashboard:
|
||||||
heading: "My orders"
|
heading: "My orders"
|
||||||
sort:
|
sort:
|
||||||
|
@ -524,6 +524,8 @@ en:
|
|||||||
space: "This space is disabled"
|
space: "This space is disabled"
|
||||||
machine: "This machine is disabled"
|
machine: "This machine is disabled"
|
||||||
reservable: "This machine is not reservable"
|
reservable: "This machine is not reservable"
|
||||||
|
cart_validation:
|
||||||
|
select_user: "Please select a user before continuing"
|
||||||
settings:
|
settings:
|
||||||
locked_setting: "the setting is locked."
|
locked_setting: "the setting is locked."
|
||||||
about_title: "\"About\" page title"
|
about_title: "\"About\" page title"
|
||||||
|
@ -518,6 +518,8 @@ fr:
|
|||||||
space: "Cet espace est désactivé"
|
space: "Cet espace est désactivé"
|
||||||
machine: "Cette machine est désactivée"
|
machine: "Cette machine est désactivée"
|
||||||
reservable: "Cette machine n'est pas réservable"
|
reservable: "Cette machine n'est pas réservable"
|
||||||
|
cart_validation:
|
||||||
|
select_user: "Veuillez sélectionner un utilisateur avant de continuer"
|
||||||
settings:
|
settings:
|
||||||
locked_setting: "le paramètre est verrouillé."
|
locked_setting: "le paramètre est verrouillé."
|
||||||
about_title: "Le titre de la page \"À propos\""
|
about_title: "Le titre de la page \"À propos\""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user