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:
parent
dff0cb26be
commit
dbe4570c30
40
app/controllers/api/orders_controller.rb
Normal file
40
app/controllers/api/orders_controller.rb
Normal 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
|
16
app/frontend/src/javascript/api/order.ts
Normal file
16
app/frontend/src/javascript/api/order.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -14,9 +14,9 @@ import { Order } from '../../models/order';
|
||||
import { MemberSelect } from '../user/member-select';
|
||||
import { CouponInput } from '../coupon/coupon-input';
|
||||
import { Coupon } from '../../models/coupon';
|
||||
import { computePriceWithCoupon } from '../../lib/coupon';
|
||||
import noImage from '../../../../images/no_image.png';
|
||||
import Switch from 'react-switch';
|
||||
import OrderLib from '../../lib/order';
|
||||
|
||||
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 (
|
||||
<div className='store-cart'>
|
||||
<div className="store-cart-list">
|
||||
@ -185,7 +139,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
||||
{cart && cart.order_items_attributes.map(item => (
|
||||
<article key={item.id} className='store-cart-list-item'>
|
||||
<div className='picture'>
|
||||
<img alt=''src={noImage} />
|
||||
<img alt=''src={item.orderable_main_image_url || noImage} />
|
||||
</div>
|
||||
<div className="ref">
|
||||
<span>{t('app.public.store_cart.reference_short')} </span>
|
||||
@ -203,7 +157,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
||||
</select>
|
||||
<div className='total'>
|
||||
<span>{t('app.public.store_cart.total')}</span>
|
||||
<p>{FormatLib.price(itemAmount(item))}</p>
|
||||
<p>{FormatLib.price(OrderLib.itemAmount(item))}</p>
|
||||
</div>
|
||||
<FabButton className="main-action-btn" onClick={removeProductFromCart(item)}>
|
||||
<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>
|
||||
<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(totalBeforeOfferedAmount())}</span></p>
|
||||
{hasOfferedItem() &&
|
||||
<p className='gift'>{t('app.public.store_cart.checkout_gift_total')} <span>-{FormatLib.price(offeredAmount())}</span></p>
|
||||
<p>{t('app.public.store_cart.checkout_products_total')} <span>{FormatLib.price(OrderLib.totalBeforeOfferedAmount(cart))}</span></p>
|
||||
{OrderLib.hasOfferedItem(cart) &&
|
||||
<p className='gift'>{t('app.public.store_cart.checkout_gift_total')} <span>-{FormatLib.price(OrderLib.offeredAmount(cart))}</span></p>
|
||||
}
|
||||
{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>
|
||||
<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>
|
||||
<FabButton className='checkout-btn' onClick={checkout} disabled={!cart.user}>
|
||||
{t('app.public.store_cart.checkout')}
|
||||
|
@ -19,10 +19,10 @@ export const OrderItem: React.FC<OrderItemProps> = ({ order, currentUser }) => {
|
||||
/**
|
||||
* Go to order page
|
||||
*/
|
||||
const showOrder = (ref: string) => {
|
||||
const showOrder = (order: Order) => {
|
||||
isPrivileged()
|
||||
? window.location.href = `/#!/admin/store/o/${ref}`
|
||||
: window.location.href = `/#!/store/o/${ref}`;
|
||||
? window.location.href = `/#!/admin/store/orders/${order.id}`
|
||||
: window.location.href = `/#!/dashboard/orders/${order.id}`;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -41,7 +41,7 @@ export const OrderItem: React.FC<OrderItemProps> = ({ order, currentUser }) => {
|
||||
return 'error';
|
||||
case 'canceled':
|
||||
return 'canceled';
|
||||
case 'pending' || 'under_preparation':
|
||||
case 'in_progress':
|
||||
return 'pending';
|
||||
default:
|
||||
return 'normal';
|
||||
@ -50,24 +50,24 @@ export const OrderItem: React.FC<OrderItemProps> = ({ order, currentUser }) => {
|
||||
|
||||
return (
|
||||
<div className='order-item'>
|
||||
<p className="ref">order.ref</p>
|
||||
<p className="ref">{order.reference}</p>
|
||||
<div>
|
||||
<FabStateLabel status={statusColor('pending')} background>
|
||||
order.state
|
||||
<FabStateLabel status={statusColor(order.state)} background>
|
||||
{t(`app.shared.store.order_item.state.${order.state}`)}
|
||||
</FabStateLabel>
|
||||
</div>
|
||||
{isPrivileged() &&
|
||||
<div className='client'>
|
||||
<span>{t('app.shared.store.order_item.client')}</span>
|
||||
<p>order.user.name</p>
|
||||
<p>{order?.user?.name || ''}</p>
|
||||
</div>
|
||||
}
|
||||
<p className="date">order.created_at</p>
|
||||
<p className="date">{FormatLib.date(order.created_at)}</p>
|
||||
<div className='price'>
|
||||
<span>{t('app.shared.store.order_item.total')}</span>
|
||||
<p>{FormatLib.price(order?.total)}</p>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from '../base/loader';
|
||||
@ -6,10 +6,14 @@ import { IApplication } from '../../models/application';
|
||||
import { StoreListHeader } from './store-list-header';
|
||||
import { OrderItem } from './order-item';
|
||||
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;
|
||||
|
||||
interface OrdersDashboardProps {
|
||||
currentUser: User,
|
||||
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
|
||||
*/
|
||||
// TODO: delete next eslint disable
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const OrdersDashboard: React.FC<OrdersDashboardProps> = ({ onError }) => {
|
||||
export const OrdersDashboard: React.FC<OrdersDashboardProps> = ({ currentUser, onError }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
// TODO: delete next eslint disable
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [orders, setOrders] = useState<Array<Order>>([]);
|
||||
const [pageCount, setPageCount] = useState<number>(0);
|
||||
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
|
||||
@ -44,7 +54,26 @@ export const OrdersDashboard: React.FC<OrdersDashboardProps> = ({ onError }) =>
|
||||
* Display option: sorting
|
||||
*/
|
||||
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 (
|
||||
@ -55,15 +84,17 @@ export const OrdersDashboard: React.FC<OrdersDashboardProps> = ({ onError }) =>
|
||||
|
||||
<div className="store-list">
|
||||
<StoreListHeader
|
||||
productsCount={0}
|
||||
productsCount={totalCount}
|
||||
selectOptions={buildOptions()}
|
||||
onSelectOptionsChange={handleSorting}
|
||||
/>
|
||||
<div className="orders-list">
|
||||
<OrderItem />
|
||||
{orders.map(order => (
|
||||
<OrderItem key={order.id} order={order} currentUser={currentUser} />
|
||||
))}
|
||||
</div>
|
||||
{pageCount > 1 &&
|
||||
<FabPagination pageCount={pageCount} currentPage={currentPage} selectPage={setCurrentPage} />
|
||||
<FabPagination pageCount={pageCount} currentPage={currentPage} selectPage={handlePagination} />
|
||||
}
|
||||
</div>
|
||||
</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']));
|
||||
|
@ -13,6 +13,8 @@ import { MemberSelect } from '../user/member-select';
|
||||
import { User } from '../../models/user';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { TDateISODate } from '../../typings/date-iso';
|
||||
import OrderAPI from '../../api/order';
|
||||
import { Order } from '../../models/order';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -42,10 +44,17 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
|
||||
|
||||
const { register, getValues } = useForm();
|
||||
|
||||
const [orders, setOrders] = useState<Array<Order>>([]);
|
||||
const [filters, setFilters] = useImmer<Filters>(initFilters);
|
||||
const [clearFilters, setClearFilters] = useState<boolean>(false);
|
||||
const [accordion, setAccordion] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
OrderAPI.index({}).then(res => {
|
||||
setOrders(res.data);
|
||||
}).catch(onError);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
applyFilters();
|
||||
setClearFilters(false);
|
||||
@ -228,7 +237,9 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
|
||||
onSelectOptionsChange={handleSorting}
|
||||
/>
|
||||
<div className="orders-list">
|
||||
<OrderItem currentUser={currentUser} />
|
||||
{orders.map(order => (
|
||||
<OrderItem key={order.id} order={order} currentUser={currentUser} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { User } from '../../models/user';
|
||||
@ -7,11 +7,15 @@ import { Loader } from '../base/loader';
|
||||
import noImage from '../../../../images/no_image.png';
|
||||
import { FabStateLabel } from '../base/fab-state-label';
|
||||
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;
|
||||
|
||||
interface ShowOrderProps {
|
||||
orderRef: string,
|
||||
orderId: string,
|
||||
currentUser?: User,
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void
|
||||
@ -27,9 +31,17 @@ type selectOption = { value: number, label: string };
|
||||
*/
|
||||
// TODO: delete next eslint disable
|
||||
// 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 [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
|
||||
*/
|
||||
@ -42,14 +54,14 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderRef, currentUser, onE
|
||||
*/
|
||||
const buildOptions = (): Array<selectOption> => {
|
||||
return [
|
||||
{ value: 0, label: t('app.shared.store.show_order.status.error') },
|
||||
{ value: 1, label: t('app.shared.store.show_order.status.canceled') },
|
||||
{ value: 2, label: t('app.shared.store.show_order.status.pending') },
|
||||
{ value: 3, label: t('app.shared.store.show_order.status.under_preparation') },
|
||||
{ value: 4, label: t('app.shared.store.show_order.status.paid') },
|
||||
{ value: 5, label: t('app.shared.store.show_order.status.ready') },
|
||||
{ value: 6, label: t('app.shared.store.show_order.status.collected') },
|
||||
{ value: 7, label: t('app.shared.store.show_order.status.refunded') }
|
||||
{ value: 0, label: t('app.shared.store.show_order.state.error') },
|
||||
{ value: 1, label: t('app.shared.store.show_order.state.canceled') },
|
||||
{ value: 2, label: t('app.shared.store.show_order.state.pending') },
|
||||
{ value: 3, label: t('app.shared.store.show_order.state.under_preparation') },
|
||||
{ value: 4, label: t('app.shared.store.show_order.state.paid') },
|
||||
{ value: 5, label: t('app.shared.store.show_order.state.ready') },
|
||||
{ value: 6, label: t('app.shared.store.show_order.state.collected') },
|
||||
{ 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';
|
||||
case 'canceled':
|
||||
return 'canceled';
|
||||
case 'pending' || 'under_preparation':
|
||||
case 'in_progress':
|
||||
return 'pending';
|
||||
default:
|
||||
return 'normal';
|
||||
}
|
||||
};
|
||||
|
||||
if (!order) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='show-order'>
|
||||
<header>
|
||||
<h2>[order.ref]</h2>
|
||||
<h2>[{order.reference}]</h2>
|
||||
<div className="grpBtn">
|
||||
{isPrivileged() &&
|
||||
<Select
|
||||
@ -100,19 +116,21 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderRef, currentUser, onE
|
||||
styles={customStyles}
|
||||
/>
|
||||
}
|
||||
<a href={''}
|
||||
target='_blank'
|
||||
className='fab-button is-black'
|
||||
rel='noreferrer'>
|
||||
{t('app.shared.store.show_order.see_invoice')}
|
||||
</a>
|
||||
{order?.invoice_id && (
|
||||
<a href={`/api/invoices/${order?.invoice_id}/download`}
|
||||
target='_blank'
|
||||
className='fab-button is-black'
|
||||
rel='noreferrer'>
|
||||
{t('app.shared.store.show_order.see_invoice')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="client-info">
|
||||
<label>{t('app.shared.store.show_order.tracking')}</label>
|
||||
<div className="content">
|
||||
{isPrivileged() &&
|
||||
{isPrivileged() && order.user &&
|
||||
<div className='group'>
|
||||
<span>{t('app.shared.store.show_order.client')}</span>
|
||||
<p>order.user.name</p>
|
||||
@ -120,14 +138,14 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderRef, currentUser, onE
|
||||
}
|
||||
<div className='group'>
|
||||
<span>{t('app.shared.store.show_order.created_at')}</span>
|
||||
<p>order.created_at</p>
|
||||
<p>{FormatLib.date(order.created_at)}</p>
|
||||
</div>
|
||||
<div className='group'>
|
||||
<span>{t('app.shared.store.show_order.last_update')}</span>
|
||||
<p>order.???</p>
|
||||
<p>{FormatLib.date(order.updated_at)}</p>
|
||||
</div>
|
||||
<FabStateLabel status={statusColor('error')} background>
|
||||
order.state
|
||||
<FabStateLabel status={statusColor(order.state)} background>
|
||||
{t(`app.shared.store.show_order.state.${order.state}`)}
|
||||
</FabStateLabel>
|
||||
</div>
|
||||
</div>
|
||||
@ -135,29 +153,30 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderRef, currentUser, onE
|
||||
<div className="cart">
|
||||
<label>{t('app.shared.store.show_order.cart')}</label>
|
||||
<div>
|
||||
{/* loop sur les articles du panier */}
|
||||
<article className='store-cart-list-item'>
|
||||
<div className='picture'>
|
||||
<img alt=''src={noImage} />
|
||||
</div>
|
||||
<div className="ref">
|
||||
<span>{t('app.shared.store.show_order.reference_short')} orderable_id?</span>
|
||||
<p>o.orderable_name</p>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<div className='price'>
|
||||
<p>o.amount</p>
|
||||
<span>/ {t('app.shared.store.show_order.unit')}</span>
|
||||
{order.order_items_attributes.map(item => (
|
||||
<article className='store-cart-list-item' key={item.id}>
|
||||
<div className='picture'>
|
||||
<img alt=''src={item.orderable_main_image_url || noImage} />
|
||||
</div>
|
||||
|
||||
<span className="count">o.quantity</span>
|
||||
|
||||
<div className='total'>
|
||||
<span>{t('app.shared.store.show_order.item_total')}</span>
|
||||
<p>o.quantity * o.amount</p>
|
||||
<div className="ref">
|
||||
<span>{t('app.shared.store.show_order.reference_short')}</span>
|
||||
<p>{item.orderable_name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<div className="actions">
|
||||
<div className='price'>
|
||||
<p>{FormatLib.price(item.amount)}</p>
|
||||
<span>/ {t('app.shared.store.show_order.unit')}</span>
|
||||
</div>
|
||||
|
||||
<span className="count">{item.quantity}</span>
|
||||
|
||||
<div className='total'>
|
||||
<span>{t('app.shared.store.show_order.item_total')}</span>
|
||||
<p>{FormatLib.price(OrderLib.itemAmount(item))}</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -168,10 +187,14 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderRef, currentUser, onE
|
||||
</div>
|
||||
<div className="amount">
|
||||
<label>{t('app.shared.store.show_order.amount')}</label>
|
||||
<p>{t('app.shared.store.show_order.products_total')}<span>order.amount</span></p>
|
||||
<p className='gift'>{t('app.shared.store.show_order.gift_total')}<span>-order.amount</span></p>
|
||||
<p>{t('app.shared.store.show_order.coupon')}<span>order.amount</span></p>
|
||||
<p className='total'>{t('app.shared.store.show_order.cart_total')} <span>order.total</span></p>
|
||||
<p>{t('app.shared.store.show_order.products_total')}<span>{FormatLib.price(OrderLib.totalBeforeOfferedAmount(order))}</span></p>
|
||||
{OrderLib.hasOfferedItem(order) &&
|
||||
<p className='gift'>{t('app.shared.store.show_order.gift_total')}<span>-{FormatLib.price(OrderLib.offeredAmount(order))}</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>
|
||||
@ -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']));
|
||||
|
@ -9,7 +9,7 @@ Application.Controllers.controller('ShowOrdersController', ['$rootScope', '$scop
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/* PUBLIC SCOPE */
|
||||
$scope.orderToken = $transition$.params().token;
|
||||
$scope.orderId = $transition$.params().id;
|
||||
|
||||
/**
|
||||
* Callback triggered in case of error
|
||||
|
50
app/frontend/src/javascript/lib/order.ts
Normal file
50
app/frontend/src/javascript/lib/order.ts
Normal 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);
|
||||
};
|
||||
}
|
@ -20,11 +20,14 @@ export interface Order {
|
||||
total?: number,
|
||||
coupon?: Coupon,
|
||||
created_at?: TDateISO,
|
||||
updated_at?: TDateISO,
|
||||
invoice_id?: number,
|
||||
order_items_attributes: Array<{
|
||||
id: number,
|
||||
orderable_type: string,
|
||||
orderable_id: number,
|
||||
orderable_name: string,
|
||||
orderable_main_image_url?: string,
|
||||
quantity: number,
|
||||
amount: number,
|
||||
is_offered: boolean
|
||||
@ -35,3 +38,17 @@ export interface OrderPayment {
|
||||
order: Order,
|
||||
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'
|
||||
}
|
||||
|
@ -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', {
|
||||
url: '/wallet',
|
||||
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
|
||||
.state('app.public.store_cart', {
|
||||
url: '/store/cart',
|
||||
@ -926,7 +924,7 @@ angular.module('application.router', ['ui.router'])
|
||||
|
||||
// show order
|
||||
.state('app.admin.order_show', {
|
||||
url: '/admin/store/o/:token',
|
||||
url: '/admin/store/orders/:id',
|
||||
views: {
|
||||
'main@': {
|
||||
templateUrl: '/admin/orders/show.html',
|
||||
|
@ -5,5 +5,5 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<orders-dashboard on-error="onError" />
|
||||
</div>
|
||||
<orders-dashboard current-user="currentUser" on-error="onError" />
|
||||
</div>
|
||||
|
@ -12,6 +12,6 @@
|
||||
<span translate>{{ 'app.shared.store.show_order.back_to_list' }}</span>
|
||||
</a>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -23,4 +23,8 @@ class Product < ApplicationRecord
|
||||
validates :amount, numericality: { greater_than: 0, allow_nil: true }
|
||||
|
||||
scope :active, -> { where(is_active: true) }
|
||||
|
||||
def main_image
|
||||
product_images.find_by(is_main: true)
|
||||
end
|
||||
end
|
||||
|
16
app/policies/order_policy.rb
Normal file
16
app/policies/order_policy.rb
Normal 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
|
@ -2,6 +2,32 @@
|
||||
|
||||
# Provides methods for Order
|
||||
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')
|
||||
order.order_items.each do |item|
|
||||
return false if item.orderable.stock[stock_type] < item.quantity
|
||||
|
@ -1,7 +1,14 @@
|
||||
# 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?
|
||||
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
|
||||
json.user do
|
||||
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_id item.orderable_id
|
||||
json.orderable_name item.orderable.name
|
||||
json.orderable_main_image_url item.orderable.main_image&.attachment_url
|
||||
json.quantity item.quantity
|
||||
json.amount item.amount / 100.0
|
||||
json.is_offered item.is_offered
|
||||
|
17
app/views/api/orders/index.json.jbuilder
Normal file
17
app/views/api/orders/index.json.jbuilder
Normal 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
|
3
app/views/api/orders/update.json.jbuilder
Normal file
3
app/views/api/orders/update.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.partial! 'api/orders/order', order: @order
|
@ -23,6 +23,7 @@ fr:
|
||||
my_invoices: "Mes factures"
|
||||
my_payment_schedules: "Mes échéanciers"
|
||||
my_wallet: "Mon porte-monnaie"
|
||||
my_orders: "Mes commandes"
|
||||
#contextual help
|
||||
help: "Aide"
|
||||
#login/logout
|
||||
|
@ -562,8 +562,11 @@ en:
|
||||
main_image: "Main image"
|
||||
store:
|
||||
order_item:
|
||||
total: "Total"
|
||||
client: "Client"
|
||||
total: "Total"
|
||||
client: "Client"
|
||||
state:
|
||||
cart: 'Cart'
|
||||
in_progress: 'In progress'
|
||||
show_order:
|
||||
back_to_list: "Back to list"
|
||||
see_invoice: "See invoice"
|
||||
@ -581,7 +584,9 @@ en:
|
||||
gift_total: "Discount total"
|
||||
coupon: "Coupon"
|
||||
cart_total: "Cart total"
|
||||
status:
|
||||
state:
|
||||
cart: 'Cart'
|
||||
in_progress: 'In progress'
|
||||
error: "Payment error"
|
||||
canceled: "Canceled"
|
||||
pending: "Pending payment"
|
||||
|
@ -165,6 +165,7 @@ Rails.application.routes.draw do
|
||||
post 'payment', on: :collection
|
||||
post 'confirm_payment', on: :collection
|
||||
end
|
||||
resources :orders, except: %i[create]
|
||||
|
||||
# for admin
|
||||
resources :trainings do
|
||||
|
Loading…
x
Reference in New Issue
Block a user