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:
parent
3cb70c5cf0
commit
d750885f3b
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user