1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-30 19:52:20 +01:00

(feat) client can show orders in dashbaord

This commit is contained in:
Du Peng 2022-09-12 19:44:13 +02:00
parent dff0cb26be
commit dbe4570c30
22 changed files with 368 additions and 147 deletions

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
# API Controller for resources of type Order
# Orders are used in store
class API::OrdersController < API::ApiController
before_action :authenticate_user!
before_action :set_order, only: %i[show update destroy]
def index
@result = ::Orders::OrderService.list(params, current_user)
end
def show; end
def update
authorize @order
if @order.update(order_parameters)
render status: :ok
else
render json: @order.errors.full_messages, status: :unprocessable_entity
end
end
def destroy
authorize @order
@order.destroy
head :no_content
end
private
def set_order
@order = Order.find(params[:id])
end
def order_params
params.require(:order).permit(:state)
end
end

View File

@ -0,0 +1,16 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { Order, OrderIndexFilter, OrderIndex } from '../models/order';
import ApiLib from '../lib/api';
export default class ProductAPI {
static async index (filters?: OrderIndexFilter): Promise<OrderIndex> {
const res: AxiosResponse<OrderIndex> = await apiClient.get(`/api/orders${ApiLib.filtersToQuery(filters)}`);
return res?.data;
}
static async get (id: number | string): Promise<Order> {
const res: AxiosResponse<Order> = await apiClient.get(`/api/orders/${id}`);
return res?.data;
}
}

View File

@ -14,9 +14,9 @@ import { Order } 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 { Coupon } from '../../models/coupon'; import { Coupon } from '../../models/coupon';
import { computePriceWithCoupon } from '../../lib/coupon';
import noImage from '../../../../images/no_image.png'; import noImage from '../../../../images/no_image.png';
import Switch from 'react-switch'; import Switch from 'react-switch';
import OrderLib from '../../lib/order';
declare const Application: IApplication; declare const Application: IApplication;
@ -132,52 +132,6 @@ 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 => 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 ( return (
<div className='store-cart'> <div className='store-cart'>
<div className="store-cart-list"> <div className="store-cart-list">
@ -185,7 +139,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
{cart && cart.order_items_attributes.map(item => ( {cart && cart.order_items_attributes.map(item => (
<article key={item.id} className='store-cart-list-item'> <article key={item.id} className='store-cart-list-item'>
<div className='picture'> <div className='picture'>
<img alt=''src={noImage} /> <img alt=''src={item.orderable_main_image_url || noImage} />
</div> </div>
<div className="ref"> <div className="ref">
<span>{t('app.public.store_cart.reference_short')} </span> <span>{t('app.public.store_cart.reference_short')} </span>
@ -203,7 +157,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
</select> </select>
<div className='total'> <div className='total'>
<span>{t('app.public.store_cart.total')}</span> <span>{t('app.public.store_cart.total')}</span>
<p>{FormatLib.price(itemAmount(item))}</p> <p>{FormatLib.price(OrderLib.itemAmount(item))}</p>
</div> </div>
<FabButton className="main-action-btn" onClick={removeProductFromCart(item)}> <FabButton className="main-action-btn" onClick={removeProductFromCart(item)}>
<i className="fa fa-trash" /> <i className="fa fa-trash" />
@ -251,15 +205,15 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
<h3>{t('app.public.store_cart.checkout_header')}</h3> <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> <span>{t('app.public.store_cart.checkout_products_COUNT', { COUNT: cart?.order_items_attributes.length })}</span>
<div className="list"> <div className="list">
<p>{t('app.public.store_cart.checkout_products_total')} <span>{FormatLib.price(totalBeforeOfferedAmount())}</span></p> <p>{t('app.public.store_cart.checkout_products_total')} <span>{FormatLib.price(OrderLib.totalBeforeOfferedAmount(cart))}</span></p>
{hasOfferedItem() && {OrderLib.hasOfferedItem(cart) &&
<p className='gift'>{t('app.public.store_cart.checkout_gift_total')} <span>-{FormatLib.price(offeredAmount())}</span></p> <p className='gift'>{t('app.public.store_cart.checkout_gift_total')} <span>-{FormatLib.price(OrderLib.offeredAmount(cart))}</span></p>
} }
{cart.coupon && {cart.coupon &&
<p>{t('app.public.store_cart.checkout_coupon')} <span>-{FormatLib.price(couponAmount())}</span></p> <p>{t('app.public.store_cart.checkout_coupon')} <span>-{FormatLib.price(OrderLib.couponAmount(cart))}</span></p>
} }
</div> </div>
<p className='total'>{t('app.public.store_cart.checkout_total')} <span>{FormatLib.price(paidTotal())}</span></p> <p className='total'>{t('app.public.store_cart.checkout_total')} <span>{FormatLib.price(OrderLib.paidTotal(cart))}</span></p>
</div> </div>
<FabButton className='checkout-btn' onClick={checkout} disabled={!cart.user}> <FabButton className='checkout-btn' onClick={checkout} disabled={!cart.user}>
{t('app.public.store_cart.checkout')} {t('app.public.store_cart.checkout')}

View File

@ -19,10 +19,10 @@ export const OrderItem: React.FC<OrderItemProps> = ({ order, currentUser }) => {
/** /**
* Go to order page * Go to order page
*/ */
const showOrder = (ref: string) => { const showOrder = (order: Order) => {
isPrivileged() isPrivileged()
? window.location.href = `/#!/admin/store/o/${ref}` ? window.location.href = `/#!/admin/store/orders/${order.id}`
: window.location.href = `/#!/store/o/${ref}`; : window.location.href = `/#!/dashboard/orders/${order.id}`;
}; };
/** /**
@ -41,7 +41,7 @@ export const OrderItem: React.FC<OrderItemProps> = ({ order, currentUser }) => {
return 'error'; return 'error';
case 'canceled': case 'canceled':
return 'canceled'; return 'canceled';
case 'pending' || 'under_preparation': case 'in_progress':
return 'pending'; return 'pending';
default: default:
return 'normal'; return 'normal';
@ -50,24 +50,24 @@ export const OrderItem: React.FC<OrderItemProps> = ({ order, currentUser }) => {
return ( return (
<div className='order-item'> <div className='order-item'>
<p className="ref">order.ref</p> <p className="ref">{order.reference}</p>
<div> <div>
<FabStateLabel status={statusColor('pending')} background> <FabStateLabel status={statusColor(order.state)} background>
order.state {t(`app.shared.store.order_item.state.${order.state}`)}
</FabStateLabel> </FabStateLabel>
</div> </div>
{isPrivileged() && {isPrivileged() &&
<div className='client'> <div className='client'>
<span>{t('app.shared.store.order_item.client')}</span> <span>{t('app.shared.store.order_item.client')}</span>
<p>order.user.name</p> <p>{order?.user?.name || ''}</p>
</div> </div>
} }
<p className="date">order.created_at</p> <p className="date">{FormatLib.date(order.created_at)}</p>
<div className='price'> <div className='price'>
<span>{t('app.shared.store.order_item.total')}</span> <span>{t('app.shared.store.order_item.total')}</span>
<p>{FormatLib.price(order?.total)}</p> <p>{FormatLib.price(order?.total)}</p>
</div> </div>
<FabButton onClick={() => showOrder('orderRef')} icon={<i className="fas fa-eye" />} className="is-black" /> <FabButton onClick={() => showOrder(order)} icon={<i className="fas fa-eye" />} className="is-black" />
</div> </div>
); );
}; };

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular'; import { react2angular } from 'react2angular';
import { Loader } from '../base/loader'; import { Loader } from '../base/loader';
@ -6,10 +6,14 @@ import { IApplication } from '../../models/application';
import { StoreListHeader } from './store-list-header'; import { StoreListHeader } from './store-list-header';
import { OrderItem } from './order-item'; import { OrderItem } from './order-item';
import { FabPagination } from '../base/fab-pagination'; import { FabPagination } from '../base/fab-pagination';
import OrderAPI from '../../api/order';
import { Order } from '../../models/order';
import { User } from '../../models/user';
declare const Application: IApplication; declare const Application: IApplication;
interface OrdersDashboardProps { interface OrdersDashboardProps {
currentUser: User,
onError: (message: string) => void onError: (message: string) => void
} }
/** /**
@ -21,15 +25,21 @@ type selectOption = { value: number, label: string };
/** /**
* This component shows a list of all orders from the store for the current user * This component shows a list of all orders from the store for the current user
*/ */
// TODO: delete next eslint disable export const OrdersDashboard: React.FC<OrdersDashboardProps> = ({ currentUser, onError }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const OrdersDashboard: React.FC<OrdersDashboardProps> = ({ onError }) => {
const { t } = useTranslation('public'); const { t } = useTranslation('public');
// TODO: delete next eslint disable const [orders, setOrders] = useState<Array<Order>>([]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [pageCount, setPageCount] = useState<number>(0); const [pageCount, setPageCount] = useState<number>(0);
const [currentPage, setCurrentPage] = useState<number>(1); const [currentPage, setCurrentPage] = useState<number>(1);
const [totalCount, setTotalCount] = useState<number>(1);
useEffect(() => {
OrderAPI.index({}).then(res => {
setPageCount(res.total_pages);
setTotalCount(res.total_count);
setOrders(res.data);
}).catch(onError);
}, []);
/** /**
* Creates sorting options to the react-select format * Creates sorting options to the react-select format
@ -44,7 +54,26 @@ export const OrdersDashboard: React.FC<OrdersDashboardProps> = ({ onError }) =>
* Display option: sorting * Display option: sorting
*/ */
const handleSorting = (option: selectOption) => { const handleSorting = (option: selectOption) => {
console.log('Sort option:', option); OrderAPI.index({ page: 1, sort: option.value ? 'ASC' : 'DESC' }).then(res => {
setCurrentPage(1);
setOrders(res.data);
setPageCount(res.total_pages);
setTotalCount(res.total_count);
}).catch(onError);
};
/**
* Handle orders pagination
*/
const handlePagination = (page: number) => {
if (page !== currentPage) {
OrderAPI.index({ page }).then(res => {
setCurrentPage(page);
setOrders(res.data);
setPageCount(res.total_pages);
setTotalCount(res.total_count);
}).catch(onError);
}
}; };
return ( return (
@ -55,15 +84,17 @@ export const OrdersDashboard: React.FC<OrdersDashboardProps> = ({ onError }) =>
<div className="store-list"> <div className="store-list">
<StoreListHeader <StoreListHeader
productsCount={0} productsCount={totalCount}
selectOptions={buildOptions()} selectOptions={buildOptions()}
onSelectOptionsChange={handleSorting} onSelectOptionsChange={handleSorting}
/> />
<div className="orders-list"> <div className="orders-list">
<OrderItem /> {orders.map(order => (
<OrderItem key={order.id} order={order} currentUser={currentUser} />
))}
</div> </div>
{pageCount > 1 && {pageCount > 1 &&
<FabPagination pageCount={pageCount} currentPage={currentPage} selectPage={setCurrentPage} /> <FabPagination pageCount={pageCount} currentPage={currentPage} selectPage={handlePagination} />
} }
</div> </div>
</section> </section>
@ -78,4 +109,4 @@ const OrdersDashboardWrapper: React.FC<OrdersDashboardProps> = (props) => {
); );
}; };
Application.Components.component('ordersDashboard', react2angular(OrdersDashboardWrapper, ['onError'])); Application.Components.component('ordersDashboard', react2angular(OrdersDashboardWrapper, ['onError', 'currentUser']));

View File

@ -13,6 +13,8 @@ import { MemberSelect } from '../user/member-select';
import { User } from '../../models/user'; import { User } from '../../models/user';
import { FormInput } from '../form/form-input'; import { FormInput } from '../form/form-input';
import { TDateISODate } from '../../typings/date-iso'; import { TDateISODate } from '../../typings/date-iso';
import OrderAPI from '../../api/order';
import { Order } from '../../models/order';
declare const Application: IApplication; declare const Application: IApplication;
@ -42,10 +44,17 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
const { register, getValues } = useForm(); const { register, getValues } = useForm();
const [orders, setOrders] = useState<Array<Order>>([]);
const [filters, setFilters] = useImmer<Filters>(initFilters); const [filters, setFilters] = useImmer<Filters>(initFilters);
const [clearFilters, setClearFilters] = useState<boolean>(false); const [clearFilters, setClearFilters] = useState<boolean>(false);
const [accordion, setAccordion] = useState({}); const [accordion, setAccordion] = useState({});
useEffect(() => {
OrderAPI.index({}).then(res => {
setOrders(res.data);
}).catch(onError);
}, []);
useEffect(() => { useEffect(() => {
applyFilters(); applyFilters();
setClearFilters(false); setClearFilters(false);
@ -228,7 +237,9 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
onSelectOptionsChange={handleSorting} onSelectOptionsChange={handleSorting}
/> />
<div className="orders-list"> <div className="orders-list">
<OrderItem currentUser={currentUser} /> {orders.map(order => (
<OrderItem key={order.id} order={order} currentUser={currentUser} />
))}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IApplication } from '../../models/application'; import { IApplication } from '../../models/application';
import { User } from '../../models/user'; import { User } from '../../models/user';
@ -7,11 +7,15 @@ import { Loader } from '../base/loader';
import noImage from '../../../../images/no_image.png'; import noImage from '../../../../images/no_image.png';
import { FabStateLabel } from '../base/fab-state-label'; import { FabStateLabel } from '../base/fab-state-label';
import Select from 'react-select'; import Select from 'react-select';
import OrderAPI from '../../api/order';
import { Order } from '../../models/order';
import FormatLib from '../../lib/format';
import OrderLib from '../../lib/order';
declare const Application: IApplication; declare const Application: IApplication;
interface ShowOrderProps { interface ShowOrderProps {
orderRef: string, orderId: string,
currentUser?: User, currentUser?: User,
onError: (message: string) => void, onError: (message: string) => void,
onSuccess: (message: string) => void onSuccess: (message: string) => void
@ -27,9 +31,17 @@ type selectOption = { value: number, label: string };
*/ */
// TODO: delete next eslint disable // TODO: delete next eslint disable
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
export const ShowOrder: React.FC<ShowOrderProps> = ({ orderRef, currentUser, onError, onSuccess }) => { export const ShowOrder: React.FC<ShowOrderProps> = ({ orderId, currentUser, onError, onSuccess }) => {
const { t } = useTranslation('shared'); const { t } = useTranslation('shared');
const [order, setOrder] = useState<Order>();
useEffect(() => {
OrderAPI.get(orderId).then(data => {
setOrder(data);
});
}, []);
/** /**
* Check if the current operator has administrative rights or is a normal member * Check if the current operator has administrative rights or is a normal member
*/ */
@ -42,14 +54,14 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderRef, currentUser, onE
*/ */
const buildOptions = (): Array<selectOption> => { const buildOptions = (): Array<selectOption> => {
return [ return [
{ value: 0, label: t('app.shared.store.show_order.status.error') }, { value: 0, label: t('app.shared.store.show_order.state.error') },
{ value: 1, label: t('app.shared.store.show_order.status.canceled') }, { value: 1, label: t('app.shared.store.show_order.state.canceled') },
{ value: 2, label: t('app.shared.store.show_order.status.pending') }, { value: 2, label: t('app.shared.store.show_order.state.pending') },
{ value: 3, label: t('app.shared.store.show_order.status.under_preparation') }, { value: 3, label: t('app.shared.store.show_order.state.under_preparation') },
{ value: 4, label: t('app.shared.store.show_order.status.paid') }, { value: 4, label: t('app.shared.store.show_order.state.paid') },
{ value: 5, label: t('app.shared.store.show_order.status.ready') }, { value: 5, label: t('app.shared.store.show_order.state.ready') },
{ value: 6, label: t('app.shared.store.show_order.status.collected') }, { value: 6, label: t('app.shared.store.show_order.state.collected') },
{ value: 7, label: t('app.shared.store.show_order.status.refunded') } { value: 7, label: t('app.shared.store.show_order.state.refunded') }
]; ];
}; };
@ -81,17 +93,21 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderRef, currentUser, onE
return 'error'; return 'error';
case 'canceled': case 'canceled':
return 'canceled'; return 'canceled';
case 'pending' || 'under_preparation': case 'in_progress':
return 'pending'; return 'pending';
default: default:
return 'normal'; return 'normal';
} }
}; };
if (!order) {
return null;
}
return ( return (
<div className='show-order'> <div className='show-order'>
<header> <header>
<h2>[order.ref]</h2> <h2>[{order.reference}]</h2>
<div className="grpBtn"> <div className="grpBtn">
{isPrivileged() && {isPrivileged() &&
<Select <Select
@ -100,19 +116,21 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderRef, currentUser, onE
styles={customStyles} styles={customStyles}
/> />
} }
<a href={''} {order?.invoice_id && (
<a href={`/api/invoices/${order?.invoice_id}/download`}
target='_blank' target='_blank'
className='fab-button is-black' className='fab-button is-black'
rel='noreferrer'> rel='noreferrer'>
{t('app.shared.store.show_order.see_invoice')} {t('app.shared.store.show_order.see_invoice')}
</a> </a>
)}
</div> </div>
</header> </header>
<div className="client-info"> <div className="client-info">
<label>{t('app.shared.store.show_order.tracking')}</label> <label>{t('app.shared.store.show_order.tracking')}</label>
<div className="content"> <div className="content">
{isPrivileged() && {isPrivileged() && order.user &&
<div className='group'> <div className='group'>
<span>{t('app.shared.store.show_order.client')}</span> <span>{t('app.shared.store.show_order.client')}</span>
<p>order.user.name</p> <p>order.user.name</p>
@ -120,14 +138,14 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderRef, currentUser, onE
} }
<div className='group'> <div className='group'>
<span>{t('app.shared.store.show_order.created_at')}</span> <span>{t('app.shared.store.show_order.created_at')}</span>
<p>order.created_at</p> <p>{FormatLib.date(order.created_at)}</p>
</div> </div>
<div className='group'> <div className='group'>
<span>{t('app.shared.store.show_order.last_update')}</span> <span>{t('app.shared.store.show_order.last_update')}</span>
<p>order.???</p> <p>{FormatLib.date(order.updated_at)}</p>
</div> </div>
<FabStateLabel status={statusColor('error')} background> <FabStateLabel status={statusColor(order.state)} background>
order.state {t(`app.shared.store.show_order.state.${order.state}`)}
</FabStateLabel> </FabStateLabel>
</div> </div>
</div> </div>
@ -135,29 +153,30 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderRef, currentUser, onE
<div className="cart"> <div className="cart">
<label>{t('app.shared.store.show_order.cart')}</label> <label>{t('app.shared.store.show_order.cart')}</label>
<div> <div>
{/* loop sur les articles du panier */} {order.order_items_attributes.map(item => (
<article className='store-cart-list-item'> <article className='store-cart-list-item' key={item.id}>
<div className='picture'> <div className='picture'>
<img alt=''src={noImage} /> <img alt=''src={item.orderable_main_image_url || noImage} />
</div> </div>
<div className="ref"> <div className="ref">
<span>{t('app.shared.store.show_order.reference_short')} orderable_id?</span> <span>{t('app.shared.store.show_order.reference_short')}</span>
<p>o.orderable_name</p> <p>{item.orderable_name}</p>
</div> </div>
<div className="actions"> <div className="actions">
<div className='price'> <div className='price'>
<p>o.amount</p> <p>{FormatLib.price(item.amount)}</p>
<span>/ {t('app.shared.store.show_order.unit')}</span> <span>/ {t('app.shared.store.show_order.unit')}</span>
</div> </div>
<span className="count">o.quantity</span> <span className="count">{item.quantity}</span>
<div className='total'> <div className='total'>
<span>{t('app.shared.store.show_order.item_total')}</span> <span>{t('app.shared.store.show_order.item_total')}</span>
<p>o.quantity * o.amount</p> <p>{FormatLib.price(OrderLib.itemAmount(item))}</p>
</div> </div>
</div> </div>
</article> </article>
))}
</div> </div>
</div> </div>
@ -168,10 +187,14 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderRef, currentUser, onE
</div> </div>
<div className="amount"> <div className="amount">
<label>{t('app.shared.store.show_order.amount')}</label> <label>{t('app.shared.store.show_order.amount')}</label>
<p>{t('app.shared.store.show_order.products_total')}<span>order.amount</span></p> <p>{t('app.shared.store.show_order.products_total')}<span>{FormatLib.price(OrderLib.totalBeforeOfferedAmount(order))}</span></p>
<p className='gift'>{t('app.shared.store.show_order.gift_total')}<span>-order.amount</span></p> {OrderLib.hasOfferedItem(order) &&
<p>{t('app.shared.store.show_order.coupon')}<span>order.amount</span></p> <p className='gift'>{t('app.shared.store.show_order.gift_total')}<span>-{FormatLib.price(OrderLib.offeredAmount(order))}</span></p>
<p className='total'>{t('app.shared.store.show_order.cart_total')} <span>order.total</span></p> }
{order.coupon &&
<p>{t('app.shared.store.show_order.coupon')}<span>-{FormatLib.price(OrderLib.couponAmount(order))}</span></p>
}
<p className='total'>{t('app.shared.store.show_order.cart_total')} <span>{FormatLib.price(OrderLib.paidTotal(order))}</span></p>
</div> </div>
</div> </div>
</div> </div>
@ -186,4 +209,4 @@ const ShowOrderWrapper: React.FC<ShowOrderProps> = (props) => {
); );
}; };
Application.Components.component('showOrder', react2angular(ShowOrderWrapper, ['orderRef', 'currentUser', 'onError', 'onSuccess'])); Application.Components.component('showOrder', react2angular(ShowOrderWrapper, ['orderId', 'currentUser', 'onError', 'onSuccess']));

View File

@ -9,7 +9,7 @@ Application.Controllers.controller('ShowOrdersController', ['$rootScope', '$scop
/* PRIVATE SCOPE */ /* PRIVATE SCOPE */
/* PUBLIC SCOPE */ /* PUBLIC SCOPE */
$scope.orderToken = $transition$.params().token; $scope.orderId = $transition$.params().id;
/** /**
* Callback triggered in case of error * Callback triggered in case of error

View File

@ -0,0 +1,50 @@
import { computePriceWithCoupon } from './coupon';
import { Order } from '../models/order';
export default class OrderLib {
/**
* Get the order item total
*/
static itemAmount = (item): number => {
return item.quantity * Math.trunc(item.amount * 100) / 100;
};
/**
* return true if order has offered item
*/
static hasOfferedItem = (order: Order): boolean => {
return order.order_items_attributes
.filter(i => i.is_offered).length > 0;
};
/**
* Get the offered item total
*/
static offeredAmount = (order: Order): number => {
return order.order_items_attributes
.filter(i => i.is_offered)
.map(i => Math.trunc(i.amount * 100) * i.quantity)
.reduce((acc, curr) => acc + curr, 0) / 100;
};
/**
* Get the total amount before offered amount
*/
static totalBeforeOfferedAmount = (order: Order): number => {
return (Math.trunc(order.total * 100) + Math.trunc(this.offeredAmount(order) * 100)) / 100;
};
/**
* Get the coupon amount
*/
static couponAmount = (order: Order): number => {
return (Math.trunc(order.total * 100) - Math.trunc(computePriceWithCoupon(order.total, order.coupon) * 100)) / 100.00;
};
/**
* Get the paid total amount
*/
static paidTotal = (order: Order): number => {
return computePriceWithCoupon(order.total, order.coupon);
};
}

View File

@ -20,11 +20,14 @@ export interface Order {
total?: number, total?: number,
coupon?: Coupon, coupon?: Coupon,
created_at?: TDateISO, created_at?: TDateISO,
updated_at?: TDateISO,
invoice_id?: number,
order_items_attributes: Array<{ order_items_attributes: Array<{
id: number, id: number,
orderable_type: string, orderable_type: string,
orderable_id: number, orderable_id: number,
orderable_name: string, orderable_name: string,
orderable_main_image_url?: string,
quantity: number, quantity: number,
amount: number, amount: number,
is_offered: boolean is_offered: boolean
@ -35,3 +38,17 @@ export interface OrderPayment {
order: Order, order: Order,
payment?: PaymentConfirmation|CreateTokenResponse payment?: PaymentConfirmation|CreateTokenResponse
} }
export interface OrderIndex {
page: number,
total_pages: number,
page_size: number,
total_count: number,
data: Array<Order>
}
export interface OrderIndexFilter {
user_id?: number,
page?: number,
sort?: 'DESC'|'ASC'
}

View File

@ -236,6 +236,15 @@ angular.module('application.router', ['ui.router'])
} }
} }
}) })
.state('app.logged.dashboard.order_show', {
url: '/orders/:id',
views: {
'main@': {
templateUrl: '/orders/show.html',
controller: 'ShowOrdersController'
}
}
})
.state('app.logged.dashboard.wallet', { .state('app.logged.dashboard.wallet', {
url: '/wallet', url: '/wallet',
abstract: !Fablab.walletModule, abstract: !Fablab.walletModule,
@ -631,17 +640,6 @@ angular.module('application.router', ['ui.router'])
} }
}) })
// show order
.state('app.public.order_show', {
url: '/store/o/:token',
views: {
'main@': {
templateUrl: '/orders/show.html',
controller: 'ShowOrdersController'
}
}
})
// cart // cart
.state('app.public.store_cart', { .state('app.public.store_cart', {
url: '/store/cart', url: '/store/cart',
@ -926,7 +924,7 @@ angular.module('application.router', ['ui.router'])
// show order // show order
.state('app.admin.order_show', { .state('app.admin.order_show', {
url: '/admin/store/o/:token', url: '/admin/store/orders/:id',
views: { views: {
'main@': { 'main@': {
templateUrl: '/admin/orders/show.html', templateUrl: '/admin/orders/show.html',

View File

@ -5,5 +5,5 @@
</div> </div>
</section> </section>
<orders-dashboard on-error="onError" /> <orders-dashboard current-user="currentUser" on-error="onError" />
</div> </div>

View File

@ -12,6 +12,6 @@
<span translate>{{ 'app.shared.store.show_order.back_to_list' }}</span> <span translate>{{ 'app.shared.store.show_order.back_to_list' }}</span>
</a> </a>
</div> </div>
<show-order current-user="currentUser" order-token="orderToken" on-error="onError" on-success="onSuccess" /> <show-order current-user="currentUser" order-id="orderId" on-error="onError" on-success="onSuccess" />
</section> </section>
</div> </div>

View File

@ -23,4 +23,8 @@ class Product < ApplicationRecord
validates :amount, numericality: { greater_than: 0, allow_nil: true } validates :amount, numericality: { greater_than: 0, allow_nil: true }
scope :active, -> { where(is_active: true) } scope :active, -> { where(is_active: true) }
def main_image
product_images.find_by(is_main: true)
end
end end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
# Check the access policies for API::OrdersController
class OrderPolicy < ApplicationPolicy
def show?
user.privileged? || (record.statistic_profile_id == user.statistic_profile.id)
end
def update?
user.privileged?
end
def destroy?
user.privileged?
end
end

View File

@ -2,6 +2,32 @@
# Provides methods for Order # Provides methods for Order
class Orders::OrderService class Orders::OrderService
ORDERS_PER_PAGE = 20
def self.list(filters, current_user)
orders = Order.where(nil)
if filters[:user_id]
statistic_profile_id = current_user.statistic_profile.id
if (current_user.member? && current_user.id == filters[:user_id].to_i) || current_user.privileged?
user = User.find(filters[:user_id])
statistic_profile_id = user.statistic_profile.id
end
orders = orders.where(statistic_profile_id: statistic_profile_id)
elsif current_user.member?
orders = orders.where(statistic_profile_id: current_user.statistic_profile.id)
end
orders = orders.where.not(state: 'cart') if current_user.member?
orders = orders.order(created_at: filters[:page].present? ? filters[:sort] : 'DESC')
orders = orders.page(filters[:page]).per(ORDERS_PER_PAGE) if filters[:page].present?
{
data: orders,
page: filters[:page] || 1,
total_pages: orders.page(1).per(ORDERS_PER_PAGE).total_pages,
page_size: ORDERS_PER_PAGE,
total_count: orders.count
}
end
def in_stock?(order, stock_type = 'external') def in_stock?(order, stock_type = 'external')
order.order_items.each do |item| order.order_items.each do |item|
return false if item.orderable.stock[stock_type] < item.quantity return false if item.orderable.stock[stock_type] < item.quantity

View File

@ -1,7 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
json.extract! order, :id, :token, :statistic_profile_id, :operator_profile_id, :reference, :state, :created_at json.extract! order, :id, :token, :statistic_profile_id, :operator_profile_id, :reference, :state, :created_at, :updated_at, :invoice_id,
:payment_method
json.total order.total / 100.0 if order.total.present? json.total order.total / 100.0 if order.total.present?
if order.coupon_id
json.coupon do
json.extract! order.coupon, :id, :code, :type, :percent_off, :validity_per_user
json.amount_off order.coupon.amount_off / 100.00 unless order.coupon.amount_off.nil?
end
end
if order&.statistic_profile&.user if order&.statistic_profile&.user
json.user do json.user do
json.id order.statistic_profile.user.id json.id order.statistic_profile.user.id
@ -15,6 +22,7 @@ json.order_items_attributes order.order_items.order(created_at: :asc) do |item|
json.orderable_type item.orderable_type json.orderable_type item.orderable_type
json.orderable_id item.orderable_id json.orderable_id item.orderable_id
json.orderable_name item.orderable.name json.orderable_name item.orderable.name
json.orderable_main_image_url item.orderable.main_image&.attachment_url
json.quantity item.quantity json.quantity item.quantity
json.amount item.amount / 100.0 json.amount item.amount / 100.0
json.is_offered item.is_offered json.is_offered item.is_offered

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
json.page @result[:page]
json.total_pages @result[:total_pages]
json.page_size @result[:page_size]
json.total_count @result[:total_count]
json.data @result[:data] do |order|
json.extract! order, :id, :statistic_profile_id, :reference, :state, :created_at
json.total order.total / 100.0 if order.total.present?
if order&.statistic_profile&.user
json.user do
json.id order.statistic_profile.user.id
json.role order.statistic_profile.user.roles.first.name
json.name order.statistic_profile.user.profile.full_name
end
end
end

View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
json.partial! 'api/orders/order', order: @order

View File

@ -23,6 +23,7 @@ fr:
my_invoices: "Mes factures" my_invoices: "Mes factures"
my_payment_schedules: "Mes échéanciers" my_payment_schedules: "Mes échéanciers"
my_wallet: "Mon porte-monnaie" my_wallet: "Mon porte-monnaie"
my_orders: "Mes commandes"
#contextual help #contextual help
help: "Aide" help: "Aide"
#login/logout #login/logout

View File

@ -564,6 +564,9 @@ en:
order_item: order_item:
total: "Total" total: "Total"
client: "Client" client: "Client"
state:
cart: 'Cart'
in_progress: 'In progress'
show_order: show_order:
back_to_list: "Back to list" back_to_list: "Back to list"
see_invoice: "See invoice" see_invoice: "See invoice"
@ -581,7 +584,9 @@ en:
gift_total: "Discount total" gift_total: "Discount total"
coupon: "Coupon" coupon: "Coupon"
cart_total: "Cart total" cart_total: "Cart total"
status: state:
cart: 'Cart'
in_progress: 'In progress'
error: "Payment error" error: "Payment error"
canceled: "Canceled" canceled: "Canceled"
pending: "Pending payment" pending: "Pending payment"

View File

@ -165,6 +165,7 @@ Rails.application.routes.draw do
post 'payment', on: :collection post 'payment', on: :collection
post 'confirm_payment', on: :collection post 'confirm_payment', on: :collection
end end
resources :orders, except: %i[create]
# for admin # for admin
resources :trainings do resources :trainings do