1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-19 13:54:25 +01:00

(feat) orders filter by admin

This commit is contained in:
Du Peng 2022-09-14 19:54:24 +02:00
parent 9b3a1c0634
commit b87355bc5a
20 changed files with 188 additions and 113 deletions

View File

@ -82,7 +82,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
* Handle payment
*/
const handlePaymentSuccess = (data: Order): void => {
if (data.payment_state === 'paid') {
if (data.state === 'paid') {
setPaymentModal(false);
window.location.href = '/#!/store';
onSuccess(t('app.public.store_cart.checkout_success'));
@ -94,8 +94,8 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
/**
* Change cart's customer by admin/manger
*/
const handleChangeMember = (userId: number): void => {
setCart({ ...cart, user: { id: userId, role: 'member' } });
const handleChangeMember = (user: User): void => {
setCart({ ...cart, user: { id: user.id, role: 'member' } });
};
/**

View File

@ -31,7 +31,7 @@ export const OrdersDashboard: React.FC<OrdersDashboardProps> = ({ currentUser, o
const [orders, setOrders] = useState<Array<Order>>([]);
const [pageCount, setPageCount] = useState<number>(0);
const [currentPage, setCurrentPage] = useState<number>(1);
const [totalCount, setTotalCount] = useState<number>(1);
const [totalCount, setTotalCount] = useState<number>(0);
useEffect(() => {
OrderAPI.index({}).then(res => {

View File

@ -12,9 +12,10 @@ import { OrderItem } from './order-item';
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';
import { Order, OrderIndexFilter } from '../../models/order';
import { FabPagination } from '../base/fab-pagination';
import { TDateISO } from '../../typings/date-iso';
declare const Application: IApplication;
@ -32,7 +33,7 @@ type selectOption = { value: number, label: string };
/**
* Option format, expected by checklist
*/
type checklistOption = { value: number, label: string };
type checklistOption = { value: string, label: string };
/**
* Admin list of orders
@ -42,23 +43,26 @@ type checklistOption = { value: number, label: string };
const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
const { t } = useTranslation('admin');
const { register, getValues } = useForm();
const { register, getValues, setValue } = useForm();
const [orders, setOrders] = useState<Array<Order>>([]);
const [filters, setFilters] = useImmer<Filters>(initFilters);
const [clearFilters, setClearFilters] = useState<boolean>(false);
const [filters, setFilters] = useImmer<OrderIndexFilter>(initFilters);
const [accordion, setAccordion] = useState({});
const [pageCount, setPageCount] = useState<number>(0);
const [totalCount, setTotalCount] = useState<number>(0);
const [reference, setReference] = useState<string>(filters.reference);
const [states, setStates] = useState<Array<string>>(filters.states);
const [user, setUser] = useState<User>();
const [periodFrom, setPeriodFrom] = useState<string>();
const [periodTo, setPeriodTo] = useState<string>();
useEffect(() => {
OrderAPI.index({}).then(res => {
OrderAPI.index(filters).then(res => {
setPageCount(res.total_pages);
setTotalCount(res.total_count);
setOrders(res.data);
}).catch(onError);
}, []);
useEffect(() => {
applyFilters();
setClearFilters(false);
}, [clearFilters]);
}, [filters]);
/**
* Create a new order
@ -68,21 +72,43 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
};
const statusOptions: checklistOption[] = [
{ value: 0, label: t('app.admin.store.orders.status.error') },
{ value: 1, label: t('app.admin.store.orders.status.canceled') },
{ value: 2, label: t('app.admin.store.orders.status.pending') },
{ value: 3, label: t('app.admin.store.orders.status.under_preparation') },
{ value: 4, label: t('app.admin.store.orders.status.paid') },
{ value: 5, label: t('app.admin.store.orders.status.ready') },
{ value: 6, label: t('app.admin.store.orders.status.collected') },
{ value: 7, label: t('app.admin.store.orders.status.refunded') }
{ value: 'cart', label: t('app.admin.store.orders.state.cart') },
{ value: 'paid', label: t('app.admin.store.orders.state.paid') },
{ value: 'payment_failed', label: t('app.admin.store.orders.state.payment_failed') },
{ value: 'in_progress', label: t('app.admin.store.orders.state.in_progress') },
{ value: 'ready', label: t('app.admin.store.orders.state.ready') },
{ value: 'canceled', label: t('app.admin.store.orders.state.canceled') }
];
/**
* Apply filters
*/
const applyFilters = () => {
console.log('Apply filters:', filters);
const applyFilters = (filterType: string) => {
return () => {
setFilters(draft => {
switch (filterType) {
case 'reference':
draft.reference = reference;
break;
case 'states':
draft.states = states;
break;
case 'user':
draft.user_id = user.id;
break;
case 'period':
if (periodFrom && periodTo) {
draft.period_from = periodFrom;
draft.period_to = periodTo;
} else {
draft.period_from = '';
draft.period_to = '';
}
break;
default:
}
});
};
};
/**
@ -90,8 +116,13 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
*/
const clearAllFilters = () => {
setFilters(initFilters);
setClearFilters(true);
console.log('Clear all filters');
setReference('');
setStates([]);
setUser(null);
setPeriodFrom(null);
setPeriodTo(null);
setValue('period_from', '');
setValue('period_to', '');
};
/**
@ -103,40 +134,54 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
{ value: 1, label: t('app.admin.store.orders.sort.oldest') }
];
};
/**
* Display option: sorting
*/
const handleSorting = (option: selectOption) => {
console.log('Sort option:', option);
setFilters(draft => {
draft.sort = option.value ? 'ASC' : 'DESC';
});
};
/**
* Filter: by reference
*/
const handleReferenceChanged = (value: string) => {
setReference(value);
};
/**
* Filter: by status
*/
const handleSelectStatus = (s: checklistOption, checked) => {
const list = [...filters.status];
const handleSelectStatus = (s: checklistOption, checked: boolean) => {
const list = [...states];
checked
? list.push(s)
: list.splice(list.indexOf(s), 1);
setFilters(draft => {
return { ...draft, status: list };
});
? list.push(s.value)
: list.splice(list.indexOf(s.value), 1);
setStates(list);
};
/**
* Filter: by member
*/
const handleSelectMember = (userId: number) => {
setFilters(draft => {
return { ...draft, memberId: userId };
});
const handleSelectMember = (user: User) => {
setUser(user);
};
/**
* Filter: by period
*/
const handlePeriod = () => {
console.log(getValues(['period_from', 'period_to']));
const handlePeriodChanged = (period: string) => {
return (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
if (period === 'period_from') {
setPeriodFrom(value);
}
if (period === 'period_to') {
setPeriodTo(value);
}
};
};
/**
@ -146,6 +191,15 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
setAccordion({ ...accordion, [id]: state });
};
/**
* Handle orders pagination
*/
const handlePagination = (page: number) => {
setFilters(draft => {
draft.page = page;
});
};
return (
<div className='orders'>
<header>
@ -164,6 +218,12 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
<FabButton onClick={clearAllFilters} className="is-black">{t('app.admin.store.orders.filter_clear')}</FabButton>
</div>
</header>
<div>
{filters.reference && <div>{filters.reference}</div>}
{filters.states.length > 0 && <div>{filters.states.join(', ')}</div>}
{filters.user_id > 0 && <div>{user?.name}</div>}
{filters.period_from && <div>{filters.period_from} - {filters.period_to}</div>}
</div>
<div className="accordion">
<AccordionItem id={0}
isOpen={accordion[0]}
@ -172,8 +232,8 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
>
<div className='content'>
<div className="group">
<input type="text" />
<FabButton onClick={applyFilters} className="is-info">{t('app.admin.store.orders.filter_apply')}</FabButton>
<input type="text" value={reference} onChange={(event) => handleReferenceChanged(event.target.value)}/>
<FabButton onClick={applyFilters('reference')} className="is-info">{t('app.admin.store.orders.filter_apply')}</FabButton>
</div>
</div>
</AccordionItem>
@ -186,12 +246,12 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
<div className="group u-scrollbar">
{statusOptions.map(s => (
<label key={s.value}>
<input type="checkbox" checked={filters.status.some(o => o.label === s.label)} onChange={(event) => handleSelectStatus(s, event.target.checked)} />
<input type="checkbox" checked={states.some(o => o === s.value)} onChange={(event) => handleSelectStatus(s, event.target.checked)} />
<p>{s.label}</p>
</label>
))}
</div>
<FabButton onClick={applyFilters} className="is-info">{t('app.admin.store.orders.filter_apply')}</FabButton>
<FabButton onClick={applyFilters('states')} className="is-info">{t('app.admin.store.orders.filter_apply')}</FabButton>
</div>
</AccordionItem>
<AccordionItem id={2}
@ -201,8 +261,8 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
>
<div className='content'>
<div className="group">
<MemberSelect noHeader onSelected={handleSelectMember} />
<FabButton onClick={applyFilters} className="is-info">{t('app.admin.store.orders.filter_apply')}</FabButton>
<MemberSelect noHeader value={user} onSelected={handleSelectMember} />
<FabButton onClick={applyFilters('user')} className="is-info">{t('app.admin.store.orders.filter_apply')}</FabButton>
</div>
</div>
</AccordionItem>
@ -217,13 +277,15 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
from
<FormInput id="period_from"
register={register}
onChange={handlePeriodChanged('period_from')}
type="date" />
to
<FormInput id="period_to"
register={register}
onChange={handlePeriodChanged('period_to')}
type="date" />
</div>
<FabButton onClick={handlePeriod} className="is-info">{t('app.admin.store.orders.filter_apply')}</FabButton>
<FabButton onClick={applyFilters('period')} className="is-info">{t('app.admin.store.orders.filter_apply')}</FabButton>
</div>
</div>
</AccordionItem>
@ -232,7 +294,7 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
<div className="store-list">
<StoreListHeader
productsCount={0}
productsCount={totalCount}
selectOptions={buildOptions()}
onSelectOptionsChange={handleSorting}
/>
@ -241,6 +303,9 @@ const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
<OrderItem key={order.id} order={order} currentUser={currentUser} />
))}
</div>
{orders.length > 0 &&
<FabPagination pageCount={pageCount} currentPage={filters.page} selectPage={handlePagination} />
}
</div>
</div>
);
@ -256,18 +321,9 @@ const OrdersWrapper: React.FC<OrdersProps> = (props) => {
Application.Components.component('orders', react2angular(OrdersWrapper, ['currentUser', 'onSuccess', 'onError']));
interface Filters {
reference: string,
status: checklistOption[],
memberId: number,
period_from: TDateISODate,
period_to: TDateISODate
}
const initFilters: Filters = {
const initFilters: OrderIndexFilter = {
reference: '',
status: [],
memberId: null,
period_from: null,
period_to: null
states: [],
page: 1,
sort: 'DESC'
};

View File

@ -6,7 +6,8 @@ import { User } from '../../models/user';
interface MemberSelectProps {
defaultUser?: User,
onSelected?: (userId: number) => void,
value?: User,
onSelected?: (user: { id: number, name: string }) => void,
noHeader?: boolean
}
@ -19,22 +20,31 @@ type selectOption = { value: number, label: string };
/**
* This component renders the member select for manager.
*/
export const MemberSelect: React.FC<MemberSelectProps> = ({ defaultUser, onSelected, noHeader }) => {
export const MemberSelect: React.FC<MemberSelectProps> = ({ defaultUser, value, onSelected, noHeader }) => {
const { t } = useTranslation('public');
const [value, setValue] = useState<selectOption>();
const [option, setOption] = useState<selectOption>();
useEffect(() => {
if (defaultUser) {
setValue({ value: defaultUser.id, label: defaultUser.name });
setOption({ value: defaultUser.id, label: defaultUser.name });
}
}, []);
useEffect(() => {
if (!defaultUser && value) {
onSelected(value.value);
if (!defaultUser && option) {
onSelected({ id: option.value, name: option.label });
}
}, [defaultUser]);
useEffect(() => {
if (value && value?.id !== option?.value) {
setOption({ value: value.id, label: value.name });
}
if (!value) {
setOption(null);
}
}, [value]);
/**
* search members by name
*/
@ -52,8 +62,8 @@ export const MemberSelect: React.FC<MemberSelectProps> = ({ defaultUser, onSelec
* callback for handle select changed
*/
const onChange = (v: selectOption) => {
setValue(v);
onSelected(v.value);
setOption(v);
onSelected({ id: v.value, name: v.label });
};
return (
@ -68,7 +78,7 @@ export const MemberSelect: React.FC<MemberSelectProps> = ({ defaultUser, onSelec
loadOptions={loadMembers}
defaultOptions
onChange={onChange}
value={value}
value={option}
/>
</div>
);

View File

@ -55,11 +55,8 @@ export default class OrderLib {
switch (order.state) {
case 'cart':
return 'cart';
case 'payment':
if (order.payment_state === 'failed') {
return 'error';
}
return 'normal';
case 'payment_failed':
return 'error';
case 'canceled':
return 'canceled';
case 'in_progress':
@ -73,9 +70,6 @@ export default class OrderLib {
* Returns a status text according to the status
*/
static statusText = (order: Order) => {
if (order.state === 'payment') {
return `payment_${order.payment_state}`;
}
return order.state;
};
}

View File

@ -16,7 +16,6 @@ export interface Order {
operator_profile_id?: number,
reference?: string,
state?: string,
payment_state?: string,
total?: number,
coupon?: Coupon,
created_at?: TDateISO,
@ -54,7 +53,11 @@ export interface OrderIndex {
}
export interface OrderIndexFilter {
reference?: string,
user_id?: number,
page?: number,
sort?: 'DESC'|'ASC'
states?: Array<string>,
period_from?: string,
period_to?: string
}

View File

@ -17,7 +17,7 @@
}
.fab-state-label {
--status-color: var(--success);
&.cart { --status-color: var(--secondary-light); }
&.cart { --status-color: var(--secondary-dark); }
&.error { --status-color: var(--alert); }
&.canceled { --status-color: var(--alert-light); }
&.pending { --status-color: var(--information); }

View File

@ -102,7 +102,7 @@
.fab-state-label {
--status-color: var(--success);
&.cart { --status-color: var(--secondary-light); }
&.cart { --status-color: var(--secondary-dark); }
&.error { --status-color: var(--alert); }
&.canceled { --status-color: var(--alert-light); }
&.pending { --status-color: var(--information); }

View File

@ -9,12 +9,9 @@ class Order < PaymentDocument
has_many :order_items, dependent: :destroy
has_one :payment_gateway_object, as: :item
ALL_STATES = %w[cart payment in_progress ready canceled return].freeze
ALL_STATES = %w[cart paid payment_failed refunded in_progress ready canceled return].freeze
enum state: ALL_STATES.zip(ALL_STATES).to_h
PAYMENT_STATES = %w[paid failed refunded].freeze
enum payment_state: PAYMENT_STATES.zip(PAYMENT_STATES).to_h
validates :token, :state, presence: true
before_create :add_environment

View File

@ -47,11 +47,11 @@ class Cart::FindOrCreateService
end
@order = nil if @order && !@user && (@order.statistic_profile_id.present? || @order.operator_profile_id.present?)
if @order && @order.statistic_profile_id.present? && Order.where(statistic_profile_id: @order.statistic_profile_id,
payment_state: 'paid').where('created_at > ?', @order.created_at).last.present?
state: 'paid').where('created_at > ?', @order.created_at).last.present?
@order = nil
end
if @order && @order.operator_profile_id.present? && Order.where(operator_profile_id: @order.operator_profile_id,
payment_state: 'paid').where('created_at > ?', @order.created_at).last.present?
state: 'paid').where('created_at > ?', @order.created_at).last.present?
@order = nil
end
end
@ -60,7 +60,7 @@ class Cart::FindOrCreateService
def set_last_cart_if_user_login
if @user&.member?
last_paid_order = Order.where(statistic_profile_id: @user.statistic_profile.id,
payment_state: 'paid').last
state: 'paid').last
@order = if last_paid_order
Order.where(statistic_profile_id: @user.statistic_profile.id,
state: 'cart').where('created_at > ?', last_paid_order.created_at).last
@ -70,7 +70,7 @@ class Cart::FindOrCreateService
end
if @user&.privileged?
last_paid_order = Order.where(operator_profile_id: @user.invoicing_profile.id,
payment_state: 'paid').last
state: 'paid').last
@order = if last_paid_order
Order.where(operator_profile_id: @user.invoicing_profile.id,
state: 'cart').where('created_at > ?', last_paid_order.created_at).last
@ -85,7 +85,7 @@ class Cart::FindOrCreateService
last_unpaid_order = nil
if @user&.member?
last_paid_order = Order.where(statistic_profile_id: @user.statistic_profile.id,
payment_state: 'paid').last
state: 'paid').last
last_unpaid_order = if last_paid_order
Order.where(statistic_profile_id: @user.statistic_profile.id,
state: 'cart').where('created_at > ?', last_paid_order.created_at).last
@ -95,7 +95,7 @@ class Cart::FindOrCreateService
end
if @user&.privileged?
last_paid_order = Order.where(operator_profile_id: @user.invoicing_profile.id,
payment_state: 'paid').last
state: 'paid').last
last_unpaid_order = if last_paid_order
Order.where(operator_profile_id: @user.invoicing_profile.id,
state: 'cart').where('created_at > ?', last_paid_order.created_at).last

View File

@ -18,15 +18,28 @@ class Orders::OrderService
else
orders = orders.where.not(statistic_profile_id: nil)
end
orders = orders.where(reference: filters[:reference]) if filters[:reference].present? && current_user.privileged?
if filters[:states].present?
state = filters[:states].split(',')
orders = orders.where(state: state) unless state.empty?
end
if filters[:period_from].present? && filters[:period_to].present?
orders = orders.where(created_at: DateTime.parse(filters[:period_from])..DateTime.parse(filters[:period_to]).end_of_day)
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?
orders = orders.order(created_at: filters[:sort] || 'DESC')
total_count = orders.count
orders = orders.page(filters[:page] || 1).per(ORDERS_PER_PAGE)
{
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
total_count: total_count
}
end

View File

@ -27,8 +27,7 @@ module Payments::PaymentConcern
else
payment_method
end
order.state = 'payment'
order.payment_state = 'paid'
order.state = 'paid'
if payment_id && payment_type
order.payment_gateway_object = PaymentGatewayObject.new(gateway_object_id: payment_id, gateway_object_type: payment_type)
end

View File

@ -31,7 +31,7 @@ class Payments::PayzenService
o = payment_success(order, coupon_code, 'card', payment_id, 'PayZen::Order')
{ order: o }
else
order.update(payment_state: 'failed')
order.update(state: 'payment_failed')
{ order: order, payment: { error: { statusText: payzen_order['answer'] } } }
end
end

View File

@ -39,7 +39,7 @@ class Payments::StripeService
o = payment_success(order, coupon_code, 'card', intent.id, intent.class.name)
{ order: o }
else
order.update(payment_state: 'failed')
order.update(state: 'payment_failed')
{ order: order, payment: { error: { statusText: 'payment failed' } } }
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
json.extract! order, :id, :token, :statistic_profile_id, :operator_profile_id, :reference, :state, :created_at, :updated_at, :invoice_id,
:payment_method, :payment_state
:payment_method
json.total order.total / 100.0 if order.total.present?
json.payment_date order.invoice.created_at if order.invoice_id.present?
json.wallet_amount order.wallet_amount / 100.0 if order.wallet_amount.present?

View File

@ -5,7 +5,7 @@ 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, :payment_state, :updated_at
json.extract! order, :id, :statistic_profile_id, :reference, :state, :created_at, :updated_at
json.total order.total / 100.0 if order.total.present?
json.paid_total order.paid_total / 100.0 if order.paid_total.present?
if order&.statistic_profile&.user

View File

@ -2043,14 +2043,13 @@ en:
filter_status: "By status"
filter_client: "By client"
filter_period: "By period"
status:
error: "Payment error"
canceled: "Canceled"
pending: "Pending payment"
under_preparation: "Under preparation"
state:
cart: 'Cart'
in_progress: 'In progress'
paid: "Paid"
payment_failed: "Payment error"
canceled: "Canceled"
ready: "Ready"
collected: "Collected"
refunded: "Refunded"
sort:
newest: "Newest first"

View File

@ -567,7 +567,7 @@ en:
state:
cart: 'Cart'
in_progress: 'In progress'
payment_paid: "Paid"
paid: "Paid"
payment_failed: "Payment error"
canceled: "Canceled"
ready: "Ready"
@ -592,7 +592,7 @@ en:
state:
cart: 'Cart'
in_progress: 'In progress'
payment_paid: "Paid"
paid: "Paid"
payment_failed: "Payment error"
canceled: "Canceled"
ready: "Ready"

View File

@ -0,0 +1,5 @@
class RemovePaymentStateFromOrders < ActiveRecord::Migration[5.2]
def change
remove_column :orders, :payment_state
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_09_09_131300) do
ActiveRecord::Schema.define(version: 2022_09_14_145334) do
# These are extensions that must be enabled in order to support this database
enable_extension "fuzzystrmatch"
@ -467,7 +467,6 @@ ActiveRecord::Schema.define(version: 2022_09_09_131300) do
t.integer "total"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "payment_state"
t.integer "wallet_amount"
t.integer "wallet_transaction_id"
t.string "payment_method"