1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

(feat) filter and paginate stock movements

This commit is contained in:
Sylvain 2022-10-05 16:58:40 +02:00
parent 3cb70c5cf0
commit d750885f3b
8 changed files with 107 additions and 30 deletions

View File

@ -6,8 +6,6 @@ class API::ProductsController < API::ApiController
before_action :authenticate_user!, except: %i[index show]
before_action :set_product, only: %i[update destroy]
MOVEMENTS_PER_PAGE = 10
def index
@products = ProductService.list(params, current_user)
end
@ -45,7 +43,7 @@ class API::ProductsController < API::ApiController
def stock_movements
authorize Product
@movements = ProductStockMovement.where(product_id: params[:id]).order(date: :desc).page(params[:page]).per(MOVEMENTS_PER_PAGE)
@movements = ProductService.stock_movements(params)
end
private

View File

@ -1,7 +1,12 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { serialize } from 'object-to-formdata';
import { Product, ProductIndexFilter, ProductsIndex, ProductStockMovement } from '../models/product';
import {
Product,
ProductIndexFilter,
ProductsIndex,
StockMovementIndex, StockMovementIndexFilter
} from '../models/product';
import ApiLib from '../lib/api';
import ProductLib from '../lib/product';
@ -91,8 +96,8 @@ export default class ProductAPI {
return res?.data;
}
static async stockMovements (productId: number, page = 1): Promise<Array<ProductStockMovement>> {
const res: AxiosResponse<Array<ProductStockMovement>> = await apiClient.get(`/api/products/${productId}/stock_movements?page=${page}`);
static async stockMovements (productId: number, filters: StockMovementIndexFilter): Promise<StockMovementIndex> {
const res: AxiosResponse<StockMovementIndex> = await apiClient.get(`/api/products/${productId}/stock_movements${ApiLib.filtersToQuery(filters)}`);
return res?.data;
}
}

View File

@ -14,29 +14,29 @@ export const FabPagination: React.FC<FabPaginationProps> = ({ pageCount, current
return (
<nav className='fab-pagination'>
{currentPage - 2 > 1 &&
<button onClick={() => selectPage(1)}><CaretDoubleLeft size={24} /></button>
<button type="button" onClick={() => selectPage(1)}><CaretDoubleLeft size={24} /></button>
}
{currentPage - 1 >= 1 &&
<button onClick={() => selectPage(currentPage - 1)}><CaretLeft size={24} /></button>
<button type="button" onClick={() => selectPage(currentPage - 1)}><CaretLeft size={24} /></button>
}
{currentPage - 2 >= 1 &&
<button onClick={() => selectPage(currentPage - 2)}>{currentPage - 2}</button>
<button type="button" onClick={() => selectPage(currentPage - 2)}>{currentPage - 2}</button>
}
{currentPage - 1 >= 1 &&
<button onClick={() => selectPage(currentPage - 1)}>{currentPage - 1}</button>
<button type="button" onClick={() => selectPage(currentPage - 1)}>{currentPage - 1}</button>
}
<button className='is-active'>{currentPage}</button>
<button type="button" className='is-active'>{currentPage}</button>
{currentPage + 1 <= pageCount &&
<button onClick={() => selectPage(currentPage + 1)}>{currentPage + 1}</button>
<button type="button" onClick={() => selectPage(currentPage + 1)}>{currentPage + 1}</button>
}
{currentPage + 2 <= pageCount &&
<button onClick={() => selectPage(currentPage + 2)}>{currentPage + 2}</button>
<button type="button" onClick={() => selectPage(currentPage + 2)}>{currentPage + 2}</button>
}
{currentPage + 1 <= pageCount &&
<button onClick={() => selectPage(currentPage + 1)}><CaretRight size={24} /></button>
<button type="button" onClick={() => selectPage(currentPage + 1)}><CaretRight size={24} /></button>
}
{currentPage + 2 < pageCount &&
<button onClick={() => selectPage(pageCount)}><CaretDoubleRight size={24} /></button>
<button type="button" onClick={() => selectPage(pageCount)}><CaretDoubleRight size={24} /></button>
}
</nav>
);

View File

@ -4,7 +4,12 @@ import { PencilSimple, X } from 'phosphor-react';
import { useFieldArray, UseFormRegister } from 'react-hook-form';
import { Control, FormState, UseFormSetValue } from 'react-hook-form/dist/types/form';
import { useTranslation } from 'react-i18next';
import { Product, ProductStockMovement, StockMovementReason, StockType } from '../../models/product';
import {
Product,
stockMovementAllReasons, StockMovementIndex, StockMovementIndexFilter,
StockMovementReason,
StockType
} from '../../models/product';
import { HtmlTranslate } from '../base/html-translate';
import { FormSwitch } from '../form/form-switch';
import { FormInput } from '../form/form-input';
@ -15,6 +20,8 @@ import { FabStateLabel } from '../base/fab-state-label';
import ProductAPI from '../../api/product';
import FormatLib from '../../lib/format';
import ProductLib from '../../lib/product';
import { useImmer } from 'use-immer';
import { FabPagination } from '../base/fab-pagination';
interface ProductStockFormProps<TContext extends object> {
currentFormValues: Product,
@ -37,15 +44,16 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
const [activeThreshold, setActiveThreshold] = useState<boolean>(currentFormValues.low_stock_threshold != null);
// is the update stock modal open?
const [isOpen, setIsOpen] = useState<boolean>(false);
const [stockMovements, setStockMovements] = useState<Array<ProductStockMovement>>([]);
const [stockMovements, setStockMovements] = useState<StockMovementIndex>(null);
const [filters, setFilters] = useImmer<StockMovementIndexFilter>({ page: 1 });
const { fields, append, remove } = useFieldArray({ control, name: 'product_stock_movements_attributes' });
useEffect(() => {
if (!currentFormValues?.id) return;
ProductAPI.stockMovements(currentFormValues.id).then(setStockMovements).catch(onError);
}, []);
ProductAPI.stockMovements(currentFormValues.id, filters).then(setStockMovements).catch(onError);
}, [filters]);
// Styles the React-select component
const customStyles = {
@ -65,7 +73,7 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
* Creates sorting options to the react-select format
*/
const buildReasonsOptions = (): Array<reasonSelectOption> => {
return (['inward_stock', 'returned', 'cancelled', 'inventory_fix', 'sold', 'missing', 'damaged'] as Array<StockMovementReason>).map(key => {
return stockMovementAllReasons.map(key => {
return { value: key, label: t(ProductLib.stockMovementReasonTrKey(key)) };
});
};
@ -83,16 +91,38 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
};
/**
* On events option change
* On stock movement reason filter change
*/
const eventsOptionsChange = (evt: reasonSelectOption) => {
console.log('Event option:', evt);
setFilters(draft => {
return {
...draft,
reason: evt.value
};
});
};
/**
* On stocks option change
* On stocks type filter change
*/
const stocksOptionsChange = (evt: typeSelectOption) => {
console.log('Stocks option:', evt);
setFilters(draft => {
return {
...draft,
stock_type: evt.value
};
});
};
/**
* Callback triggered when the user wants to swich the current page of stock movements
*/
const handlePagination = (page: number) => {
setFilters(draft => {
return {
...draft,
page
};
});
};
/**
@ -124,8 +154,8 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
* Return the data of the update of the stock for the current product
*/
const lastStockUpdate = () => {
if (stockMovements[0]) {
return stockMovements[0].date;
if (stockMovements?.data[0]) {
return stockMovements?.data[0].date;
} else {
return currentFormValues?.created_at || new Date();
}
@ -235,7 +265,7 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
/>
</div>
</div>
{stockMovements.map(movement => <div className="stock-history" key={movement.id}>
{stockMovements?.data?.map(movement => <div className="stock-history" key={movement.id}>
<div className="stock-item">
<p className='title'>{currentFormValues.name}</p>
<p>{FormatLib.date(movement.date)}</p>
@ -253,6 +283,11 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
</div>
</div>
</div>)}
{stockMovements?.total_pages > 1 &&
<FabPagination pageCount={stockMovements.total_pages}
currentPage={stockMovements.page}
selectPage={handlePagination} />
}
</div>
<ProductStockModal onSuccess={onNewStockMovement}
isOpen={isOpen}

View File

@ -89,6 +89,14 @@ export interface ProductStockMovement {
date?: TDateISO
}
export type StockMovementIndex = PaginatedIndex<ProductStockMovement>;
export interface StockMovementIndexFilter extends ApiFilter {
reason?: StockMovementReason,
stock_type?: StockType,
page?: number,
}
export interface Product {
id?: number,
name: string,

View File

@ -4,6 +4,7 @@
class ProductService
class << self
PRODUCTS_PER_PAGE = 12
MOVEMENTS_PER_PAGE = 10
def list(filters, operator)
products = Product.includes(:product_images)
@ -88,6 +89,22 @@ class ProductService
end
end
def stock_movements(filters)
movements = ProductStockMovement.where(product_id: filters[:id]).order(date: :desc)
movements = filter_by_stock_type(movements, filters)
movements = filter_by_reason(movements, filters)
total_count = movements.count
movements = movements.page(filters[:page] || 1).per(MOVEMENTS_PER_PAGE)
{
data: movements,
page: filters[:page]&.to_i || 1,
total_pages: movements.page(1).per(MOVEMENTS_PER_PAGE).total_pages,
page_size: MOVEMENTS_PER_PAGE,
total_count: total_count
}
end
private
def filter_by_active(products, filters)
@ -144,5 +161,17 @@ class ProductService
products.order(key => order)
end
end
def filter_by_stock_type(movements, filters)
return movements if filters[:stock_type].blank? || filters[:stock_type] == 'all'
movements.where(stock_type: filters[:stock_type])
end
def filter_by_reason(movements, filters)
return movements if filters[:reason].blank?
movements.where(reason: filters[:reason])
end
end
end

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
json.array! @movements do |movement|
json.extract! @movements, :page, :total_pages, :page_size, :total_count
json.data @movements[:data] do |movement|
json.partial! 'api/products/stock_movement', stock_movement: movement
json.extract! movement, :product_id
end

View File

@ -2046,6 +2046,7 @@ en:
type_in: "Add"
type_out: "Remove"
cancel: "Cancel this operation"
load_more: "Load more"
product_stock_modal:
modal_title: "Manage stock"
internal: "Private stock"
@ -2065,8 +2066,8 @@ en:
sold: "Sold"
missing: "Missing in stock"
damaged: "Damaged product"
other_in: "Other"
other_out: "Other"
other_in: "Other (in)"
other_out: "Other (out)"
orders:
heading: "Orders"
create_order: "Create an order"