1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-28 09:24:24 +01:00

refactor spaces/extended_prices code architecture to match the FM style guide

This commit is contained in:
Sylvain 2021-12-28 11:25:10 +01:00
parent db4230def1
commit fe96e01b7f
22 changed files with 200 additions and 67 deletions

View File

@ -36,7 +36,7 @@ class API::PricesController < API::ApiController
def destroy
authorize @price
@price.destroy
@price.safe_destroy
head :no_content
end

View File

@ -27,6 +27,13 @@ export const ConfigureExtendedPriceButton: React.FC<ConfigureExtendedPriceButton
const [extendedPrices, setExtendedPrices] = useState<Array<Price>>(prices);
const [showList, setShowList] = useState<boolean>(false);
/**
* Return the number of minutes, user-friendly formatted
*/
const formatDuration = (minutes: number): string => {
return t('app.admin.configure_extended_prices_button.extended_price_DURATION', { DURATION: minutes });
};
/**
* Open/closes the popover listing the existing packs
*/
@ -58,21 +65,21 @@ export const ConfigureExtendedPriceButton: React.FC<ConfigureExtendedPriceButton
return (
<div className="configure-extended-prices-button">
<button className="packs-button" onClick={toggleShowList}>
<button className="extended-prices-button" onClick={toggleShowList}>
<i className="fas fa-stopwatch" />
</button>
{showList && <FabPopover title={t('app.admin.configure_extendedPrices_button.extendedPrices')} headerButton={renderAddButton()} className="fab-popover__right">
{showList && <FabPopover title={t('app.admin.configure_extended_prices_button.extended_prices')} headerButton={renderAddButton()} className="fab-popover__right">
<ul>
{extendedPrices?.map(extendedPrice =>
<li key={extendedPrice.id}>
{extendedPrice.duration} {t('app.admin.calendar.minutes')} - {FormatLib.price(extendedPrice.amount)}
<span className="pack-actions">
{formatDuration(extendedPrice.duration)} - {FormatLib.price(extendedPrice.amount)}
<span className="extended-prices-actions">
<EditExtendedPrice onSuccess={handleSuccess} onError={onError} price={extendedPrice} />
<DeleteExtendedPrice onSuccess={handleSuccess} onError={onError} price={extendedPrice} />
</span>
</li>)}
</ul>
{extendedPrices?.length === 0 && <span>{t('app.admin.configure_extendedPrices_button.no_extendedPrices')}</span>}
{extendedPrices?.length === 0 && <span>{t('app.admin.configure_extended_prices_button.no_extended_prices')}</span>}
</FabPopover>}
</div>
);

View File

@ -43,24 +43,24 @@ export const CreateExtendedPrice: React.FC<CreateExtendedPriceProps> = ({ onSucc
// create it on the API
PriceAPI.create(newExtendedPrice)
.then(() => {
onSuccess(t('app.admin.create_extendedPrice.extendedPrice_successfully_created'));
onSuccess(t('app.admin.create_extended_price.extended_price_successfully_created'));
toggleModal();
})
.catch(error => onError(error));
};
return (
<div className="create-pack">
<button className="add-pack-button" onClick={toggleModal}><i className="fas fa-plus"/></button>
<div className="create-extended-price">
<button className="add-price-button" onClick={toggleModal}><i className="fas fa-plus"/></button>
<FabModal isOpen={isOpen}
toggleModal={toggleModal}
title={t('app.admin.create_extendedPrice.new_extendedPrice')}
className="new-pack-modal"
title={t('app.admin.create_extended_price.new_extended_price')}
className="new-extended-price-modal"
closeButton
confirmButton={t('app.admin.create_extendedPrice.create_extendedPrice')}
confirmButton={t('app.admin.create_extended_price.create_extended_price')}
onConfirmSendFormId="new-extended-price">
<FabAlert level="info">
{t('app.admin.create_extendedPrice.new_extendedPrice_info', { TYPE: priceableType })}
{t('app.admin.create_extended_price.new_extended_price_info', { TYPE: priceableType })}
</FabAlert>
<ExtendedPriceForm formId="new-extended-price" onSubmit={handleSubmit} />
</FabModal>

View File

@ -33,23 +33,23 @@ export const DeleteExtendedPrice: React.FC<DeleteExtendedPriceProps> = ({ onSucc
*/
const onDeleteConfirmed = (): void => {
PriceAPI.destroy(price.id).then(() => {
onSuccess(t('app.admin.delete_extendedPrice.extendedPrice_deleted'));
onSuccess(t('app.admin.delete_extended_price.extended_price_deleted'));
}).catch((error) => {
onError(t('app.admin.delete_extendedPrice.unable_to_delete') + error);
onError(t('app.admin.delete_extended_price.unable_to_delete') + error);
});
toggleDeletionModal();
};
return (
<div className="delete-pack">
<FabButton type='button' className="remove-pack-button" icon={<i className="fa fa-trash" />} onClick={toggleDeletionModal} />
<FabModal title={t('app.admin.delete_extendedPrice.delete_extendedPrice')}
<div className="delete-extended-price">
<FabButton type='button' className="remove-price-button" icon={<i className="fa fa-trash" />} onClick={toggleDeletionModal} />
<FabModal title={t('app.admin.delete_extended_price.delete_extended_price')}
isOpen={deletionModal}
toggleModal={toggleDeletionModal}
closeButton={true}
confirmButton={t('app.admin.delete_extendedPrice.confirm_delete')}
confirmButton={t('app.admin.delete_extended_price.confirm_delete')}
onConfirm={onDeleteConfirmed}>
<span>{t('app.admin.delete_extendedPrice.delete_confirmation')}</span>
<span>{t('app.admin.delete_extended_price.delete_confirmation')}</span>
</FabModal>
</div>
);

View File

@ -42,7 +42,7 @@ export const EditExtendedPrice: React.FC<EditExtendedPriceProps> = ({ price, onS
const handleUpdate = (price: Price): void => {
PriceAPI.update(price)
.then(() => {
onSuccess(t('app.admin.edit_extendedPrice.extendedPrice_successfully_updated'));
onSuccess(t('app.admin.edit_extended_price.extended_price_successfully_updated'));
setExtendedPriceData(price);
toggleModal();
})
@ -50,16 +50,16 @@ export const EditExtendedPrice: React.FC<EditExtendedPriceProps> = ({ price, onS
};
return (
<div className="edit-pack">
<FabButton type='button' className="edit-pack-button" icon={<i className="fas fa-edit" />} onClick={handleRequestEdit} />
<div className="edit-extended-price">
<FabButton type='button' className="edit-price-button" icon={<i className="fas fa-edit" />} onClick={handleRequestEdit} />
<FabModal isOpen={isOpen}
toggleModal={toggleModal}
title={t('app.admin.edit_extendedPrice.edit_extendedPrice')}
title={t('app.admin.edit_extended_price.edit_extended_price')}
className="edit-pack-modal"
closeButton
confirmButton={t('app.admin.edit_extendedPrice.confirm_changes')}
onConfirmSendFormId="edit-pack">
{extendedPriceData && <ExtendedPriceForm formId="edit-pack" onSubmit={handleUpdate} price={extendedPriceData} />}
confirmButton={t('app.admin.edit_extended_price.confirm_changes')}
onConfirmSendFormId="edit-extended-price">
{extendedPriceData && <ExtendedPriceForm formId="edit-extended-price" onSubmit={handleUpdate} price={extendedPriceData} />}
</FabModal>
</div>
);

View File

@ -7,9 +7,9 @@ import { IFablab } from '../../../models/fablab';
declare let Fablab: IFablab;
interface PackFormProps {
interface ExtendedPriceFormProps {
formId: string,
onSubmit: (pack: Price) => void,
onSubmit: (price: Price) => void,
price?: Price,
}
@ -17,7 +17,7 @@ interface PackFormProps {
* A form component to create/edit a extended price.
* The form validation must be created elsewhere, using the attribute form={formId}.
*/
export const ExtendedPriceForm: React.FC<PackFormProps> = ({ formId, onSubmit, price }) => {
export const ExtendedPriceForm: React.FC<ExtendedPriceFormProps> = ({ formId, onSubmit, price }) => {
const [extendedPriceData, updateExtendedPriceData] = useImmer<Price>(price || {} as Price);
const { t } = useTranslation('admin');
@ -49,8 +49,8 @@ export const ExtendedPriceForm: React.FC<PackFormProps> = ({ formId, onSubmit, p
};
return (
<form id={formId} onSubmit={handleSubmit} className="pack-form">
<label htmlFor="duration">{t('app.admin.calendar.minutes')} *</label>
<form id={formId} onSubmit={handleSubmit} className="extended-price-form">
<label htmlFor="duration">{t('app.admin.extended_price_form.duration')} *</label>
<FabInput id="duration"
type="number"
defaultValue={extendedPriceData?.duration || ''}

View File

@ -35,7 +35,7 @@ const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) =>
// retrieve the initial data
useEffect(() => {
SpaceAPI.index(false)
SpaceAPI.index()
.then(data => setSpaces(data))
.catch(error => onError(error));
GroupAPI.index({ disabled: false, admins: false })
@ -67,7 +67,7 @@ const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) =>
* Find the default price (hourly rate) matching the given criterion
*/
const findPriceBy = (spaceId, groupId): Price => {
return prices.find(price => price.priceable_id === spaceId && price.group_id === groupId && price.duration == 60);
return prices.find(price => price.priceable_id === spaceId && price.group_id === groupId && price.duration === 60);
};
/**
@ -101,16 +101,17 @@ const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) =>
};
return (
<div className="machines-pricing">
<div className="spaces-pricing">
<FabAlert level="warning">
<p><HtmlTranslate trKey="app.admin.pricing.these_prices_match_space_hours_rates_html"/></p>
<p><HtmlTranslate trKey="app.admin.pricing.prices_calculated_on_hourly_rate_html" options={{ DURATION: `${EXEMPLE_DURATION}`, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }} /></p>
<p>{t('app.admin.pricing.you_can_override')}</p>
<p><HtmlTranslate trKey="app.admin.spaces_pricing.prices_match_space_hours_rates_html"/></p>
<p><HtmlTranslate trKey="app.admin.spaces_pricing.prices_calculated_on_hourly_rate_html" options={{ DURATION: `${EXEMPLE_DURATION}`, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }} /></p>
<p>{t('app.admin.spaces_pricing.you_can_override')}</p>
<p>{t('app.admin.spaces_pricing.extended_prices')}</p>
</FabAlert>
<table>
<thead>
<tr>
<th>{t('app.admin.pricing.spaces')}</th>
<th>{t('app.admin.spaces_pricing.spaces')}</th>
{groups?.map(group => <th key={group.id} className="group-name">{group.name}</th>)}
</tr>
</thead>
@ -118,7 +119,7 @@ const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) =>
{spaces?.map(space => <tr key={space.id}>
<td>{space.name}</td>
{groups?.map(group => <td key={group.id}>
{prices && <EditablePrice price={findPriceBy(space.id, group.id)} onSave={handleUpdatePrice} />}
{prices.length && <EditablePrice price={findPriceBy(space.id, group.id)} onSave={handleUpdatePrice} />}
<ConfigureExtendedPriceButton
prices={findExtendedPricesBy(space.id, group.id)}
onError={onError}

View File

@ -57,12 +57,18 @@
@import "modules/machines/machines-filters";
@import "modules/machines/required-training-modal";
@import "modules/user/avatar";
@import "modules/pricing/machines-pricing";
@import "modules/pricing/editable-price";
@import "modules/pricing/configure-packs-button";
@import "modules/pricing/pack-form";
@import "modules/pricing/delete-pack";
@import "modules/pricing/edit-pack";
@import "modules/pricing/machines/machines-pricing";
@import "modules/pricing/machines/configure-packs-button";
@import "modules/pricing/machines/pack-form";
@import "modules/pricing/machines/delete-pack";
@import "modules/pricing/machines/edit-pack";
@import "modules/pricing/machines/create-pack";
@import "modules/pricing/spaces/configure-extended-prices-button";
@import "modules/pricing/spaces/create-extended-price";
@import "modules/pricing/spaces/delete-extended-price";
@import "modules/pricing/spaces/edit-extended-price";
@import "modules/pricing/spaces/spaces-pricing";
@import "modules/settings/check-list-setting";
@import "modules/prepaid-packs/propose-packs-modal";
@import "modules/prepaid-packs/packs-summary";

View File

@ -18,13 +18,6 @@
color: white;
}
}
.popover-title {
.add-pack-button {
position: absolute;
right: 5px;
top: 10px;
}
}
.popover-content {
ul {

View File

@ -0,0 +1,7 @@
.create-pack {
.add-pack-button {
position: absolute;
right: 5px;
top: 10px;
}
}

View File

@ -0,0 +1,49 @@
.configure-extended-prices-button {
display: inline-block;
margin-left: 6px;
position: relative;
.extended-prices-button {
border: 1px solid #d0cccc;
border-radius: 50%;
cursor: pointer;
width: 30px;
height: 30px;
display: inline-block;
padding: 2px 6px;
box-shadow: 0 1px 1px 0 #abaaaa;
&:hover {
background-color: #b9b9b9;
color: white;
}
}
.popover-content {
ul {
padding-left: 19px;
li {
display: flex;
justify-content: space-between;
&::before {
content: '\f466';
font-family: 'Font Awesome 5 Free';
position: absolute;
left: 11px;
font-weight: 800;
font-size: 12px;
vertical-align: middle;
line-height: 24px;
}
.extended-prices-actions button {
font-size: 10px;
vertical-align: middle;
line-height: 10px;
height: auto;
}
}
}
}
}

View File

@ -0,0 +1,7 @@
.create-extended-price {
.add-price-button {
position: absolute;
right: 5px;
top: 10px;
}
}

View File

@ -0,0 +1,8 @@
.delete-extended-price {
display: inline;
.remove-price-button {
background-color: #cb1117;
color: white;
}
}

View File

@ -0,0 +1,3 @@
.edit-extended-price {
display: inline-block;
}

View File

@ -0,0 +1,10 @@
.extended-price-form {
.interval-inputs {
display: flex;
.select-interval {
min-width: 49%;
margin-left: 4px;
}
}
}

View File

@ -0,0 +1,31 @@
.spaces-pricing {
.fab-alert {
margin: 15px 0;
}
table {
overflow-y: scroll;
thead > tr > th:first-child {
width: 20%;
}
thead > tr > th.group-name {
width: 20%;
text-transform: uppercase;
font-size: 1.4rem;
}
thead > tr > th {
vertical-align: bottom;
border-bottom: 2px solid #ddd;
padding: 8px;
line-height: 1.5;
}
tbody > tr > td {
padding: 8px;
line-height: 1.5;
vertical-align: top;
border-top: 1px solid #ddd;
}
}
}

View File

@ -8,4 +8,8 @@ class Price < ApplicationRecord
validates :priceable, :group_id, :amount, presence: true
validates :priceable_id, uniqueness: { scope: %i[priceable_type plan_id group_id duration] }
def safe_destroy
destroy unless duration == 60
end
end

View File

@ -369,6 +369,11 @@ en:
status_disabled: "Disabled"
status_all: "All"
spaces_pricing:
prices_match_space_hours_rates_html: "The prices below match one hour of space reservation, <strong>without subscription</strong>."
prices_calculated_on_hourly_rate_html: "All the prices will be automatically calculated based on the hourly rate defined here.<br/><em>For example</em>, if you define an hourly rate at {RATE}: a slot of {DURATION} minutes, will be charged <strong>{PRICE}</strong>."
you_can_override: "You can override this duration for each availability you create in the agenda. The price will then be adjusted accordingly."
extended_prices: "Moreover, you can define extended prices which will apply in priority over the hourly rate below. Extended prices allow you, for example, to set a favorable price for a booking of several hours."
spaces: "Spaces"
price_updated: "Price successfully updated"
machines_pricing:
prices_match_machine_hours_rates_html: "The prices below match one hour of machine usage, <strong>without subscription</strong>."
@ -380,10 +385,12 @@ en:
packs: "Prepaid packs"
no_packs: "No packs for now"
pack_DURATION: "{DURATION} hours"
configure_extendedPrices_button:
extendedPrices: "Extended prices"
no_extendedPrices: "No extended price for now"
configure_extended_prices_button:
extended_prices: "Extended prices"
no_extended_prices: "No extended price for now"
extended_price_DURATION: "{DURATION} minutes"
extended_price_form:
duration: "Duration (minutes)"
amount: "Price"
pack_form:
hours: "Hours"
@ -411,21 +418,21 @@ en:
edit_pack: "Edit the pack"
confirm_changes: "Confirm changes"
pack_successfully_updated: "The prepaid pack was successfully updated."
create_extendedPrice:
new_extendedPrice: "New extended price"
new_extendedPrice_info: "Extended prices allows you to define prices based on custom durations, intead on the default hourly rates."
create_extendedPrice: "Create extended price"
extendedPrice_successfully_created: "The new extended price was successfully created."
delete_extendedPrice:
extendedPrice_deleted: "The extended price was successfully deleted."
create_extended_price:
new_extended_price: "New extended price"
new_extended_price_info: "Extended prices allows you to define prices based on custom durations, instead of the default hourly rates."
create_extended_price: "Create extended price"
extended_price_successfully_created: "The new extended price was successfully created."
delete_extended_price:
extended_price_deleted: "The extended price was successfully deleted."
unable_to_delete: "Unable to delete the extended price: "
delete_extendedPrice: "Delete the extended price"
delete_extended_price: "Delete the extended price"
confirm_delete: "Delete"
delete_confirmation: "Are you sure you want to delete this extended price? This won't be possible if it was already bought by users."
edit_extendedPrice:
edit_extendedPrice: "Edit the extended price"
delete_confirmation: "Are you sure you want to delete this extended price?"
edit_extended_price:
edit_extended_price: "Edit the extended price"
confirm_changes: "Confirm changes"
extendedPrice_successfully_updated: "The extended price was successfully updated."
extended_price_successfully_updated: "The extended price was successfully updated."
#ajouter un code promotionnel
coupons_new:
add_a_coupon: "Add a coupon"