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

(feat) display/remove ongoing stocks operations

This commit is contained in:
Sylvain 2022-09-12 15:55:41 +02:00
parent c968f7b1aa
commit f705f71c4f
9 changed files with 146 additions and 77 deletions

View File

@ -8,7 +8,7 @@ export default class SettingAPI {
return res?.data?.setting;
}
static async query (names: Array<SettingName>): Promise<Map<SettingName, string>> {
static async query (names: readonly SettingName[]): Promise<Map<SettingName, string>> {
const params = new URLSearchParams();
params.append('names', `['${names.join("','")}']`);
@ -32,7 +32,7 @@ export default class SettingAPI {
return res?.data?.isPresent;
}
private static toSettingsMap (names: Array<SettingName>, data: Record<string, string|null>): Map<SettingName, string> {
private static toSettingsMap (names: readonly SettingName[], data: Record<string, string|null>): Map<SettingName, string> {
const map = new Map();
names.forEach(name => {
map.set(name, data[name] || '');

View File

@ -1,9 +1,8 @@
import React, { useEffect, useState } from 'react';
import Select from 'react-select';
import { PencilSimple } from 'phosphor-react';
import { ArrayPath, Path, useFieldArray, UseFormRegister } from 'react-hook-form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { Control, FormState, UnpackNestedValue, UseFormSetValue } from 'react-hook-form/dist/types/form';
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 { HtmlTranslate } from '../base/html-translate';
@ -15,15 +14,14 @@ import { ProductStockModal } from './product-stock-modal';
import { FabStateLabel } from '../base/fab-state-label';
import ProductAPI from '../../api/product';
import FormatLib from '../../lib/format';
import { FieldPathValue } from 'react-hook-form/dist/types/path';
import ProductLib from '../../lib/product';
interface ProductStockFormProps<TFieldValues, TContext extends object> {
interface ProductStockFormProps<TContext extends object> {
currentFormValues: Product,
register: UseFormRegister<TFieldValues>,
control: Control<TFieldValues, TContext>,
formState: FormState<TFieldValues>,
setValue: UseFormSetValue<TFieldValues>,
register: UseFormRegister<Product>,
control: Control<Product, TContext>,
formState: FormState<Product>,
setValue: UseFormSetValue<Product>,
onSuccess: (product: Product) => void,
onError: (message: string) => void,
}
@ -33,7 +31,7 @@ const DEFAULT_LOW_STOCK_THRESHOLD = 30;
/**
* Form tab to manage a product's stock
*/
export const ProductStockForm = <TFieldValues extends FieldValues, TContext extends object> ({ currentFormValues, register, control, formState, setValue, onError }: ProductStockFormProps<TFieldValues, TContext>) => {
export const ProductStockForm = <TContext extends object> ({ currentFormValues, register, control, formState, setValue, onError }: ProductStockFormProps<TContext>) => {
const { t } = useTranslation('admin');
const [activeThreshold, setActiveThreshold] = useState<boolean>(currentFormValues.low_stock_threshold != null);
@ -41,7 +39,7 @@ export const ProductStockForm = <TFieldValues extends FieldValues, TContext exte
const [isOpen, setIsOpen] = useState<boolean>(false);
const [stockMovements, setStockMovements] = useState<Array<ProductStockMovement>>([]);
const { fields, append } = useFieldArray({ control, name: 'product_stock_movements_attributes' as ArrayPath<TFieldValues> });
const { fields, append, remove } = useFieldArray({ control, name: 'product_stock_movements_attributes' });
useEffect(() => {
if (!currentFormValues?.id) return;
@ -103,8 +101,8 @@ export const ProductStockForm = <TFieldValues extends FieldValues, TContext exte
const toggleStockThreshold = (checked: boolean) => {
setActiveThreshold(checked);
setValue(
'low_stock_threshold' as Path<TFieldValues>,
(checked ? DEFAULT_LOW_STOCK_THRESHOLD : null) as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
'low_stock_threshold',
(checked ? DEFAULT_LOW_STOCK_THRESHOLD : null)
);
};
@ -154,6 +152,35 @@ export const ProductStockForm = <TFieldValues extends FieldValues, TContext exte
</div>
<FabButton onClick={toggleModal} icon={<PencilSimple size={20} weight="fill" />} className="is-black">Modifier</FabButton>
</div>
{fields.length > 0 && <div className="ongoing-stocks">
<span className="title">{t('app.admin.store.product_stock_form.ongoing_operations')}</span>
<span className="save-notice">{t('app.admin.store.product_stock_form.save_reminder')}</span>
{fields.map((newMovement, index) => (
<div key={index} className="unsaved-stock-movement stock-item">
<div className="group">
<p>{t(`app.admin.store.product_stock_form.type_${ProductLib.stockMovementType(newMovement.reason)}`)}</p>
</div>
<div className="group">
<span>{t(`app.admin.store.product_stock_form.${newMovement.stock_type}`)}</span>
<p>{ProductLib.absoluteStockMovement(newMovement.quantity, newMovement.reason)}</p>
</div>
<div className="group">
<span>{t('app.admin.store.product_stock_form.reason')}</span>
<p>{t(ProductLib.stockMovementReasonTrKey(newMovement.reason))}</p>
</div>
<p className="cancel-action" onClick={() => remove(index)}>
{t('app.admin.store.product_stock_form.cancel')}
<X size={20} />
</p>
<FormInput id={`product_stock_movements_attributes.${index}.stock_type`} register={register}
type="hidden" />
<FormInput id={`product_stock_movements_attributes.${index}.quantity`} register={register} type="hidden" />
<FormInput id={`product_stock_movements_attributes.${index}.reason`} register={register} type="hidden" />
</div>
))}
</div>}
<hr />
<div className="threshold-data">
@ -213,15 +240,15 @@ export const ProductStockForm = <TFieldValues extends FieldValues, TContext exte
<p className='title'>{currentFormValues.name}</p>
<p>{FormatLib.date(movement.date)}</p>
<div className="group">
<span>{movement.stock_type}</span>
<p>{movement.quantity}</p>
<span>{t(`app.admin.store.product_stock_form.${movement.stock_type}`)}</span>
<p>{ProductLib.absoluteStockMovement(movement.quantity, movement.reason)}</p>
</div>
<div className="group">
<span>{t('app.admin.store.product_stock_form.event_type')}</span>
<p>{movement.reason}</p>
<span>{t('app.admin.store.product_stock_form.reason')}</span>
<p>{t(ProductLib.stockMovementReasonTrKey(movement.reason))}</p>
</div>
<div className="group">
<span>{t('app.admin.store.product_stock_form.stock_level')}</span>
<span>{t('app.admin.store.product_stock_form.remaining_stock')}</span>
<p>{movement.remaining_stock}</p>
</div>
</div>
@ -231,13 +258,6 @@ export const ProductStockForm = <TFieldValues extends FieldValues, TContext exte
onSuccess={onNewStockMovement}
isOpen={isOpen}
toggleModal={toggleModal} />
{fields.map((newMovement, index) => (
<div key={index}>
<FormInput id={`product_stock_movements_attributes.${index}.stock_type`} register={register} type="hidden" />
<FormInput id={`product_stock_movements_attributes.${index}.quantity`} register={register} type="hidden" />
<FormInput id={`product_stock_movements_attributes.${index}.reason`} register={register} type="hidden" />
</div>
))}
</section>
);
};

View File

@ -1,6 +1,12 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ProductStockMovement, StockMovementReason, StockType } from '../../models/product';
import {
ProductStockMovement,
stockMovementInReasons,
stockMovementOutReasons,
StockMovementReason,
StockType
} from '../../models/product';
import { FormSelect } from '../form/form-select';
import { FormInput } from '../form/form-input';
import { FabButton } from '../base/fab-button';
@ -57,12 +63,7 @@ export const ProductStockModal: React.FC<ProductStockModalProps> = ({ onError, o
* Creates sorting options to the react-select format
*/
const buildEventsOptions = (): Array<reasonSelectOption> => {
const options: Record<string, Array<StockMovementReason>> = {
in: ['inward_stock', 'returned', 'cancelled', 'inventory_fix'],
out: ['sold', 'missing', 'damaged']
};
return options[movement].map(key => {
return (movement === 'in' ? stockMovementInReasons : stockMovementOutReasons).map(key => {
return { value: key, label: t(ProductLib.stockMovementReasonTrKey(key)) };
});
};

View File

@ -1,5 +1,5 @@
import { ProductCategory } from '../models/product-category';
import { StockMovementReason } from '../models/product';
import { stockMovementInReasons, stockMovementOutReasons, StockMovementReason } from '../models/product';
export default class ProductLib {
/**
@ -26,4 +26,25 @@ export default class ProductLib {
static stockMovementReasonTrKey = (reason: StockMovementReason): string => {
return `app.admin.store.stock_movement_reason.${reason}`;
};
/**
* Check if the given stock movement is of type 'in' or 'out'
*/
static stockMovementType = (reason: StockMovementReason): 'in' | 'out' => {
if ((stockMovementInReasons as readonly StockMovementReason[]).includes(reason)) return 'in';
if ((stockMovementOutReasons as readonly StockMovementReason[]).includes(reason)) return 'out';
throw new Error(`Unexpected stock movement reason: ${reason}`);
};
/**
* Return the given quantity, prefixed by its addition operator (- or +)
*/
static absoluteStockMovement = (quantity: number, reason: StockMovementReason): string => {
if (ProductLib.stockMovementType(reason) === 'in') {
return `+${quantity}`;
} else {
return `-${quantity}`;
}
};
}

View File

@ -6,7 +6,12 @@ export interface ProductIndexFilter extends ApiFilter {
}
export type StockType = 'internal' | 'external' | 'all';
export type StockMovementReason = 'inward_stock' | 'returned' | 'cancelled' | 'inventory_fix' | 'sold' | 'missing' | 'damaged';
export const stockMovementInReasons = ['inward_stock', 'returned', 'cancelled', 'inventory_fix'] as const;
export const stockMovementOutReasons = ['sold', 'missing', 'damaged'] as const;
export const stockMovementAllReasons = [...stockMovementInReasons, ...stockMovementOutReasons] as const;
export type StockMovementReason = typeof stockMovementAllReasons[number];
export interface Stock {
internal: number,

View File

@ -7,20 +7,20 @@ export const homePageSettings = [
'home_content',
'home_css',
'upcoming_events_shown'
];
] as const;
export const privacyPolicySettings = [
'privacy_draft',
'privacy_body',
'privacy_dpo'
];
] as const;
export const aboutPageSettings = [
'about_title',
'about_body',
'about_contacts',
'link_name'
];
] as const;
export const socialNetworksSettings = [
'facebook',
@ -36,7 +36,7 @@ export const socialNetworksSettings = [
'pinterest',
'lastfm',
'flickr'
];
] as const;
export const messagesSettings = [
'machine_explications_alert',
@ -45,7 +45,7 @@ export const messagesSettings = [
'subscription_explications_alert',
'event_explications_alert',
'space_explications_alert'
];
] as const;
export const invoicesSettings = [
'invoice_logo',
@ -64,7 +64,7 @@ export const invoicesSettings = [
'invoice_legals',
'invoice_prefix',
'payment_schedule_prefix'
];
] as const;
export const bookingSettings = [
'booking_window_start',
@ -81,17 +81,17 @@ export const bookingSettings = [
'book_overlapping_slots',
'slot_duration',
'overlapping_categories'
];
] as const;
export const themeSettings = [
'main_color',
'secondary_color'
];
] as const;
export const titleSettings = [
'fablab_name',
'name_genre'
];
] as const;
export const accountingSettings = [
'accounting_journal_code',
@ -115,7 +115,7 @@ export const accountingSettings = [
'accounting_Event_label',
'accounting_Space_code',
'accounting_Space_label'
];
] as const;
export const modulesSettings = [
'spaces_module',
@ -127,13 +127,13 @@ export const modulesSettings = [
'online_payment_module',
'public_agenda_module',
'invoicing_module'
];
] as const;
export const stripeSettings = [
'stripe_public_key',
'stripe_secret_key',
'stripe_currency'
];
] as const;
export const payzenSettings = [
'payzen_username',
@ -142,13 +142,13 @@ export const payzenSettings = [
'payzen_public_key',
'payzen_hmac',
'payzen_currency'
];
] as const;
export const openLabSettings = [
'openlab_app_id',
'openlab_app_secret',
'openlab_default'
];
] as const;
export const accountSettings = [
'phone_required',
@ -157,13 +157,13 @@ export const accountSettings = [
'user_change_group',
'user_validation_required',
'user_validation_required_list'
];
] as const;
export const analyticsSettings = [
'tracking_id',
'facebook_app_id',
'twitter_analytics'
];
] as const;
export const fabHubSettings = [
'hub_last_version',
@ -171,43 +171,43 @@ export const fabHubSettings = [
'fab_analytics',
'origin',
'uuid'
];
] as const;
export const projectsSettings = [
'allowed_cad_extensions',
'allowed_cad_mime_types',
'disqus_shortname'
];
] as const;
export const prepaidPacksSettings = [
'renew_pack_threshold',
'pack_only_for_subscription'
];
] as const;
export const registrationSettings = [
'public_registrations',
'recaptcha_site_key',
'recaptcha_secret_key'
];
] as const;
export const adminSettings = [
'feature_tour_display',
'show_username_in_admin_list'
];
] as const;
export const pricingSettings = [
'extended_prices_in_same_day'
];
] as const;
export const poymentSettings = [
'payment_gateway'
];
] as const;
export const displaySettings = [
'machines_sort_by',
'events_in_calendar',
'email_from'
];
] as const;
export const allSettings = [
...homePageSettings,

View File

@ -1,6 +1,31 @@
.product-stock-form {
h4 span { @include text-sm; }
.ongoing-stocks {
margin: 2.4rem 0;
.save-notice {
@include text-xs;
margin-left: 1rem;
color: var(--alert);
}
.unsaved-stock-movement {
background-color: var(--gray-soft-light);
border: 0;
padding: 1.2rem;
margin-top: 1rem;
.cancel-action {
&:hover {
text-decoration: underline;
cursor: pointer;
}
svg {
margin-left: 1rem;
vertical-align: middle;
}
}
}
}
.store-list {
h4 { margin: 0; }
}

View File

@ -1992,18 +1992,25 @@ en:
product_stock_form:
stock_up_to_date: "Stock up to date"
date_time: "{DATE} - {TIME}"
ongoing_operations: "Ongoing stocks operations"
save_reminder: "Don't forget to save your operations"
low_stock_threshold: "Define a low stock threshold"
stock_threshold_toggle: "Activate stock threshold"
stock_threshold_information: "<strong>Information</strong></br>Define a low stock threshold and receive a notification when it's reached.<br>Above the threshold, the product is available in the store. When the threshold is reached, the product quantity is labeled as low."
low_stock: "Low stock"
threshold_level: "Minimum threshold level"
threshold_alert: "Notify me when the threshold is reached"
events_history: "Events history"
event_type: "Events:"
reason: "Reason"
stocks: "Stocks:"
internal: "Private stock"
external: "Public stock"
all: "All types"
stock_level: "Stock level"
remaining_stock: "Remaining stock"
type_in: "Add"
type_out: "Remove"
cancel: "Cancel this operation"
product_stock_modal:
modal_title: "Manage stock"
internal: "Private stock"
@ -2018,7 +2025,7 @@ en:
stock_movement_reason:
inward_stock: "Inward stock"
returned: "Returned by client"
canceled: "Canceled by client"
cancelled: "Canceled by client"
inventory_fix: "Inventory fix"
sold: "Sold"
missing: "Missing in stock"

View File

@ -3299,20 +3299,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001313:
version "1.0.30001314"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001314.tgz#65c7f9fb7e4594fca0a333bec1d8939662377596"
integrity sha512-0zaSO+TnCHtHJIbpLroX7nsD+vYuOVjl3uzFbJO1wMVbuveJA0RK2WcQA9ZUIOiO0/ArMiMgHJLxfEZhQiC0kw==
caniuse-lite@^1.0.30001219:
version "1.0.30001296"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz"
integrity sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q==
caniuse-lite@^1.0.30001332:
version "1.0.30001335"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001335.tgz#899254a0b70579e5a957c32dced79f0727c61f2a"
integrity sha512-ddP1Tgm7z2iIxu6QTtbZUv6HJxSaV/PZeSrWFZtbY4JZ69tOeNhBCl3HyRQgeNZKE5AOn1kpV7fhljigy0Ty3w==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001313, caniuse-lite@^1.0.30001332:
version "1.0.30001397"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001397.tgz"
integrity sha512-SW9N2TbCdLf0eiNDRrrQXx2sOkaakNZbCjgNpPyMJJbiOrU5QzMIrXOVMRM1myBXTD5iTkdrtU/EguCrBocHlA==
chalk@^2.0.0, chalk@^2.4.2:
version "2.4.2"