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

(feat) store withdrawal instructions

This commit is contained in:
Sylvain 2022-09-28 17:45:48 +02:00
parent a30eac10de
commit a260f88555
13 changed files with 106 additions and 32 deletions

View File

@ -1,6 +1,13 @@
import apiClient from './clients/api-client'; import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
import { Setting, SettingBulkResult, SettingError, SettingName, SettingValue } from '../models/setting'; import {
Setting,
SettingBulkArray,
SettingBulkResult,
SettingError,
SettingName,
SettingValue
} from '../models/setting';
export default class SettingAPI { export default class SettingAPI {
static async get (name: SettingName): Promise<Setting> { static async get (name: SettingName): Promise<Setting> {
@ -60,7 +67,7 @@ export default class SettingAPI {
return map; return map;
} }
private static toObjectArray (data: Map<SettingName, SettingValue>): Array<Record<string, SettingValue>> { private static toObjectArray (data: Map<SettingName, SettingValue>): SettingBulkArray {
const array = []; const array = [];
data.forEach((value, key) => { data.forEach((value, key) => {
array.push({ array.push({

View File

@ -83,6 +83,12 @@ export const FabTextEditor: React.ForwardRefRenderFunction<FabTextEditorRef, Fab
editor?.setEditable(!disabled); editor?.setEditable(!disabled);
}, [disabled]); }, [disabled]);
useEffect(() => {
if (editor?.getHTML() !== content) {
editor?.commands.setContent(content);
}
}, [content]);
// bind the editor to the ref, once it is ready // bind the editor to the ref, once it is ready
if (!editor) return null; if (!editor) return null;
editorRef.current = editor; editorRef.current = editor;

View File

@ -79,12 +79,6 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, heading, bulletList, b
} }
}; };
// prevent form submition propagation to parent forms
const handleSubmit = (event) => {
event.preventDefault();
event.stopPropagation();
};
// Update the selected link // Update the selected link
const setLink = useCallback((closeLinkMenu?: boolean) => { const setLink = useCallback((closeLinkMenu?: boolean) => {
if (url.href === '') { if (url.href === '') {
@ -241,7 +235,7 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, heading, bulletList, b
} }
</div> </div>
<form ref={ref} className={`fab-text-editor-subMenu ${submenu ? 'is-active' : ''}`} onSubmit={handleSubmit}> <div ref={ref} className={`fab-text-editor-subMenu ${submenu ? 'is-active' : ''}`}>
{ submenu === 'link' && { submenu === 'link' &&
(<> (<>
<h6>{t('app.shared.text_editor.menu_bar.add_link')}</h6> <h6>{t('app.shared.text_editor.menu_bar.add_link')}</h6>
@ -290,7 +284,7 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, heading, bulletList, b
</div> </div>
</>) </>)
} }
</form> </div>
</> </>
); );
}; };

View File

@ -18,6 +18,8 @@ import noImage from '../../../../images/no_image.png';
import Switch from 'react-switch'; import Switch from 'react-switch';
import OrderLib from '../../lib/order'; import OrderLib from '../../lib/order';
import { CaretDown, CaretUp } from 'phosphor-react'; import { CaretDown, CaretUp } from 'phosphor-react';
import SettingAPI from '../../api/setting';
import { SettingName } from '../../models/setting';
declare const Application: IApplication; declare const Application: IApplication;
@ -35,8 +37,15 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
const { t } = useTranslation('public'); const { t } = useTranslation('public');
const { cart, setCart } = useCart(currentUser); const { cart, setCart } = useCart(currentUser);
const [itemsQuantity, setItemsQuantity] = useState<{ id: number; quantity: number; }[]>(); const [itemsQuantity, setItemsQuantity] = useState<{ id: number; quantity: number; }[]>([]);
const [paymentModal, setPaymentModal] = useState<boolean>(false); const [paymentModal, setPaymentModal] = useState<boolean>(false);
const [settings, setSettings] = useState<Map<SettingName, string>>(null);
useEffect(() => {
SettingAPI.query(['store_withdrawal_instructions', 'fablab_name'])
.then(res => setSettings(res))
.catch(onError);
}, []);
useEffect(() => { useEffect(() => {
const quantities = cart?.order_items_attributes.map(i => { const quantities = cart?.order_items_attributes.map(i => {
@ -153,6 +162,17 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
} }
}; };
/**
* Text instructions for the customer
*/
const withdrawalInstructions = (): string => {
const instructions = settings?.get('store_withdrawal_instructions');
if (instructions) {
return instructions;
}
return t('app.public.store_cart.please_contact_FABLAB', { FABLAB: settings?.get('fablab_name') });
};
return ( return (
<div className='store-cart'> <div className='store-cart'>
<div className="store-cart-list"> <div className="store-cart-list">
@ -160,7 +180,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
{cart && cart.order_items_attributes.map(item => ( {cart && cart.order_items_attributes.map(item => (
<article key={item.id} className='store-cart-list-item'> <article key={item.id} className='store-cart-list-item'>
<div className='picture'> <div className='picture'>
<img alt=''src={item.orderable_main_image_url || noImage} /> <img alt='' src={item.orderable_main_image_url || noImage} />
</div> </div>
<div className="ref"> <div className="ref">
<span>{t('app.public.store_cart.reference_short')} {item.orderable_ref || ''}</span> <span>{t('app.public.store_cart.reference_short')} {item.orderable_ref || ''}</span>
@ -179,7 +199,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
onChange={e => changeProductQuantity(e, item)} onChange={e => changeProductQuantity(e, item)}
min={item.quantity_min} min={item.quantity_min}
max={item.orderable_external_stock} max={item.orderable_external_stock}
value={itemsQuantity?.find(i => i.id === item.id).quantity} value={itemsQuantity?.find(i => i.id === item.id)?.quantity || 1}
/> />
<button onClick={() => handleInputNumber(item, 'up')}><CaretUp size={12} weight="fill" /></button> <button onClick={() => handleInputNumber(item, 'up')}><CaretUp size={12} weight="fill" /></button>
<button onClick={() => handleInputNumber(item, 'down')}><CaretDown size={12} weight="fill" /></button> <button onClick={() => handleInputNumber(item, 'down')}><CaretDown size={12} weight="fill" /></button>
@ -197,7 +217,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
<label> <label>
<span>{t('app.public.store_cart.offer_product')}</span> <span>{t('app.public.store_cart.offer_product')}</span>
<Switch <Switch
checked={item.is_offered} checked={item.is_offered || false}
onChange={toggleProductOffer(item)} onChange={toggleProductOffer(item)}
width={40} width={40}
height={19} height={19}
@ -214,7 +234,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
<div className="group"> <div className="group">
<div className='store-cart-info'> <div className='store-cart-info'>
<h3>{t('app.public.store_cart.pickup')}</h3> <h3>{t('app.public.store_cart.pickup')}</h3>
<p>[TODO: texte venant des paramètres de la boutique]</p> <p dangerouslySetInnerHTML={{ __html: withdrawalInstructions() }} />
</div> </div>
{cart && !cartIsEmpty() && {cart && !cartIsEmpty() &&

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useEffect } from 'react';
import { react2angular } from 'react2angular'; import { react2angular } from 'react2angular';
import { Loader } from '../base/loader'; import { Loader } from '../base/loader';
import { IApplication } from '../../models/application'; import { IApplication } from '../../models/application';
@ -8,6 +8,9 @@ import { useForm, SubmitHandler } from 'react-hook-form';
import { FabAlert } from '../base/fab-alert'; import { FabAlert } from '../base/fab-alert';
import { FormRichText } from '../form/form-rich-text'; import { FormRichText } from '../form/form-rich-text';
import { FabButton } from '../base/fab-button'; import { FabButton } from '../base/fab-button';
import SettingAPI from '../../api/setting';
import SettingLib from '../../lib/setting';
import { SettingName, SettingValue, storeSettings } from '../../models/setting';
declare const Application: IApplication; declare const Application: IApplication;
@ -15,25 +18,32 @@ interface StoreSettingsProps {
onError: (message: string) => void, onError: (message: string) => void,
onSuccess: (message: string) => void onSuccess: (message: string) => void
} }
interface Settings {
withdrawal: string
}
/** /**
* Shows store settings * Store settings display and edition
*/ */
// TODO: delete next eslint disable export const StoreSettings: React.FC<StoreSettingsProps> = ({ onError, onSuccess }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const StoreSettings: React.FC<StoreSettingsProps> = (onError, onSuccess) => {
const { t } = useTranslation('admin'); const { t } = useTranslation('admin');
const { control, handleSubmit, reset } = useForm<Record<SettingName, SettingValue>>();
const { control, handleSubmit } = useForm<Settings>(); useEffect(() => {
SettingAPI.query(storeSettings)
.then(settings => {
const data = SettingLib.mapToBulkObject(settings);
reset(data);
})
.catch(onError);
}, []);
/** /**
* Callback triggered when the form is submitted: process with the product creation or update. * Callback triggered when the form is submitted: save the settings
*/ */
const onSubmit: SubmitHandler<Settings> = (data) => { const onSubmit: SubmitHandler<Record<SettingName, SettingValue>> = (data) => {
console.log(data); SettingAPI.bulkUpdate(SettingLib.bulkObjectToMap(data)).then(() => {
onSuccess(t('app.admin.store_settings.update_success'));
}, reason => {
onError(reason);
});
}; };
return ( return (
@ -51,7 +61,7 @@ export const StoreSettings: React.FC<StoreSettingsProps> = (onError, onSuccess)
bulletList bulletList
link link
limit={400} limit={400}
id="withdrawal" /> id="store_withdrawal_instructions" />
<FabButton type='submit' className='save-btn'>{t('app.admin.store_settings.save')}</FabButton> <FabButton type='submit' className='save-btn'>{t('app.admin.store_settings.save')}</FabButton>
</form> </form>
</div> </div>

View File

@ -150,11 +150,12 @@ export default class ProductLib {
const value = ParsingLib.parse(params[key]) || initialFilters[key]; const value = ParsingLib.parse(params[key]) || initialFilters[key];
switch (key) { switch (key) {
case 'category': case 'category': {
const parents = categories?.filter(c => (value as Array<string>)?.includes(c.slug)); const parents = categories?.filter(c => (value as Array<string>)?.includes(c.slug));
// we may also add to the selection children categories // we may also add to the selection children categories
res.categories = [...parents, ...categories?.filter(c => parents.map(c => c.id).includes(c.parent_id))]; res.categories = [...parents, ...categories?.filter(c => parents.map(c => c.id).includes(c.parent_id))];
break; break;
}
case 'categories': case 'categories':
res.categories = [...categories?.filter(c => (value as Array<string>)?.includes(c.slug))]; res.categories = [...categories?.filter(c => (value as Array<string>)?.includes(c.slug))];
break; break;

View File

@ -0,0 +1,25 @@
import { SettingName, SettingValue } from '../models/setting';
export default class SettingLib {
/**
* Convert the provided data to a map, as expected by BulkUpdate
*/
static bulkObjectToMap = (data: Record<SettingName, SettingValue>): Map<SettingName, SettingValue> => {
const res = new Map<SettingName, SettingValue>();
for (const key in data) {
res.set(key as SettingName, data[key]);
}
return res;
};
/**
* Convert the provided map to a simple javascript object
*/
static mapToBulkObject = (data: Map<SettingName, SettingValue>): Record<SettingName, SettingValue> => {
const res = {} as Record<SettingName, SettingValue>;
data.forEach((value, key) => {
res[key] = value;
});
return res;
};
}

View File

@ -213,6 +213,10 @@ export const displaySettings = [
'email_from' 'email_from'
] as const; ] as const;
export const storeSettings = [
'store_withdrawal_instructions'
] as const;
export const allSettings = [ export const allSettings = [
...homePageSettings, ...homePageSettings,
...privacyPolicySettings, ...privacyPolicySettings,
@ -237,7 +241,8 @@ export const allSettings = [
...adminSettings, ...adminSettings,
...pricingSettings, ...pricingSettings,
...poymentSettings, ...poymentSettings,
...displaySettings ...displaySettings,
...storeSettings
] as const; ] as const;
export type SettingName = typeof allSettings[number]; export type SettingName = typeof allSettings[number];
@ -264,3 +269,5 @@ export interface SettingBulkResult {
error?: string, error?: string,
localized?: string, localized?: string,
} }
export type SettingBulkArray = Array<{ name: SettingName, value: SettingValue }>;

View File

@ -153,7 +153,8 @@ class Setting < ApplicationRecord
user_validation_required user_validation_required
user_validation_required_list user_validation_required_list
show_username_in_admin_list show_username_in_admin_list
store_module] } store_module
store_withdrawal_instructions] }
# WARNING: when adding a new key, you may also want to add it in: # WARNING: when adding a new key, you may also want to add it in:
# - config/locales/en.yml#settings # - config/locales/en.yml#settings
# - app/frontend/src/javascript/models/setting.ts#SettingName # - app/frontend/src/javascript/models/setting.ts#SettingName

View File

@ -42,7 +42,7 @@ class SettingPolicy < ApplicationPolicy
payment_gateway payzen_endpoint payzen_public_key public_agenda_module renew_pack_threshold statistics_module payment_gateway payzen_endpoint payzen_public_key public_agenda_module renew_pack_threshold statistics_module
pack_only_for_subscription overlapping_categories public_registrations facebook twitter viadeo linkedin instagram pack_only_for_subscription overlapping_categories public_registrations facebook twitter viadeo linkedin instagram
youtube vimeo dailymotion github echosciences pinterest lastfm flickr machines_module user_change_group youtube vimeo dailymotion github echosciences pinterest lastfm flickr machines_module user_change_group
user_validation_required user_validation_required_list store_module] user_validation_required user_validation_required_list store_module store_withdrawal_instructions]
end end
## ##

View File

@ -2095,3 +2095,4 @@ en:
withdrawal_instructions: 'Product withdrawal instructions' withdrawal_instructions: 'Product withdrawal instructions'
withdrawal_info: "This text is displayed on the checkout page to inform the client about the products withdrawal method" withdrawal_info: "This text is displayed on the checkout page to inform the client about the products withdrawal method"
save: "Save" save: "Save"
update_success: "The settings were successfully updated"

View File

@ -442,6 +442,7 @@ en:
checkout_error: "An unexpected error occurred. Please contact the administrator." checkout_error: "An unexpected error occurred. Please contact the administrator."
checkout_success: "Purchase confirmed. Thanks!" checkout_success: "Purchase confirmed. Thanks!"
select_user: "Please select a user before continuing." select_user: "Please select a user before continuing."
please_contact_FABLAB: "Please contact {FABLAB, select, undefined{us} other{{FABLAB}}} for withdrawal instructions."
orders_dashboard: orders_dashboard:
heading: "My orders" heading: "My orders"
sort: sort:

View File

@ -609,3 +609,4 @@ en:
user_change_group: "Allow users to change their group" user_change_group: "Allow users to change their group"
show_username_in_admin_list: "Show the username in the admin's members list" show_username_in_admin_list: "Show the username in the admin's members list"
store_module: "Store module" store_module: "Store module"
store_withdrawal_instructions: "Withdrawal instructions"