1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

edit machines pricings

This commit is contained in:
Sylvain 2021-06-22 17:56:13 +02:00
parent d7ba83f6a0
commit 8cc4811794
12 changed files with 92 additions and 19 deletions

View File

@ -3,15 +3,15 @@ import { AxiosResponse } from 'axios';
import { Group, GroupIndexFilter } from '../models/group'; import { Group, GroupIndexFilter } from '../models/group';
export default class GroupAPI { export default class GroupAPI {
static async index (filters?: Array<GroupIndexFilter>): Promise<Array<Group>> { static async index (filters?: GroupIndexFilter): Promise<Array<Group>> {
const res: AxiosResponse<Array<Group>> = await apiClient.get(`/api/groups${GroupAPI.filtersToQuery(filters)}`); const res: AxiosResponse<Array<Group>> = await apiClient.get(`/api/groups${GroupAPI.filtersToQuery(filters)}`);
return res?.data; return res?.data;
} }
private static filtersToQuery(filters?: Array<GroupIndexFilter>): string { private static filtersToQuery(filters?: GroupIndexFilter): string {
if (!filters) return ''; if (!filters) return '';
return '?' + filters.map(f => `${f.key}=${f.value}`).join('&'); return '?' + Object.entries(filters).map(f => `${f[0]}=${f[1]}`).join('&');
} }
} }

View File

@ -15,7 +15,7 @@ export default class PriceAPI {
} }
static async update (price: Price): Promise<Price> { static async update (price: Price): Promise<Price> {
const res: AxiosResponse<Price> = await apiClient.patch(`/api/price/${price.id}`, { price }); const res: AxiosResponse<Price> = await apiClient.patch(`/api/prices/${price.id}`, { price });
return res?.data; return res?.data;
} }

View File

@ -18,12 +18,13 @@ interface FabInputProps {
placeholder?: string, placeholder?: string,
error?: string, error?: string,
type?: 'text' | 'date' | 'password' | 'url' | 'time' | 'tel' | 'search' | 'number' | 'month' | 'email' | 'datetime-local' | 'week', type?: 'text' | 'date' | 'password' | 'url' | 'time' | 'tel' | 'search' | 'number' | 'month' | 'email' | 'datetime-local' | 'week',
step?: number | 'any',
} }
/** /**
* This component is a template for an input component that wraps the application style * This component is a template for an input component that wraps the application style
*/ */
export const FabInput: React.FC<FabInputProps> = ({ id, onChange, defaultValue, icon, className, disabled, type, required, debounce, addOn, addOnClassName, readOnly, maxLength, pattern, placeholder, error }) => { export const FabInput: React.FC<FabInputProps> = ({ id, onChange, defaultValue, icon, className, disabled, type, required, debounce, addOn, addOnClassName, readOnly, maxLength, pattern, placeholder, error, step }) => {
const [inputValue, setInputValue] = useState<any>(defaultValue); const [inputValue, setInputValue] = useState<any>(defaultValue);
/** /**
@ -86,6 +87,7 @@ export const FabInput: React.FC<FabInputProps> = ({ id, onChange, defaultValue,
{hasIcon() && <span className="fab-input--icon">{icon}</span>} {hasIcon() && <span className="fab-input--icon">{icon}</span>}
<input id={id} <input id={id}
type={type} type={type}
step={step}
className="fab-input--input" className="fab-input--input"
value={inputValue} value={inputValue}
onChange={handleChange} onChange={handleChange}

View File

@ -12,8 +12,6 @@ import { User, UserRole } from '../../models/user';
import { IFablab } from '../../models/fablab'; import { IFablab } from '../../models/fablab';
import { PaymentSchedule, PaymentScheduleItem, PaymentScheduleItemState } from '../../models/payment-schedule'; import { PaymentSchedule, PaymentScheduleItem, PaymentScheduleItemState } from '../../models/payment-schedule';
import PaymentScheduleAPI from '../../api/payment-schedule'; import PaymentScheduleAPI from '../../api/payment-schedule';
import { useImmer } from 'use-immer';
import { SettingName } from '../../models/setting';
declare var Fablab: IFablab; declare var Fablab: IFablab;

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { PrepaidPack } from '../../models/prepaid-pack'; import { PrepaidPack } from '../../models/prepaid-pack';
import { FabModal } from '../base/fab-modal';
interface ConfigurePacksButtonProps { interface ConfigurePacksButtonProps {
packs: Array<PrepaidPack>, packs: Array<PrepaidPack>,
@ -12,19 +13,27 @@ interface ConfigurePacksButtonProps {
*/ */
export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ packs, onError }) => { export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ packs, onError }) => {
const [showList, setShowList] = useState<boolean>(false); const [showList, setShowList] = useState<boolean>(false);
const [addPackModal, setAddPackModal] = useState<boolean>(false);
const toggleShowList = (): void => { const toggleShowList = (): void => {
setShowList(!showList); setShowList(!showList);
} }
const toggleAddPackModal = (): void => {
setAddPackModal(!addPackModal);
}
const handleAddPack = (): void => { const handleAddPack = (): void => {
//TODO, open a modal to add a new pack toggleAddPackModal();
} }
return ( return (
<div className="configure-packs-button" onMouseOver={toggleShowList} onClick={handleAddPack}> <div className="configure-packs-button" onMouseOver={toggleShowList} onClick={handleAddPack}>
<i className="fas fa-box-open" />
{packs && showList && <div className="packs-overview"> {packs && showList && <div className="packs-overview">
{packs.map(p => <div>{p.minutes / 60}h - {p.amount}</div>)} {packs.map(p => <div>{p.minutes / 60}h - {p.amount}</div>)}
</div>} </div>}
<FabModal isOpen={addPackModal} toggleModal={toggleAddPackModal}>NEW PACK</FabModal>
</div> </div>
); );
} }

View File

@ -17,7 +17,7 @@ interface EditablePriceProps {
*/ */
export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) => { export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) => {
const [edit, setEdit] = useState<boolean>(false); const [edit, setEdit] = useState<boolean>(false);
const [tempPrice, setTempPrice] = useState<number>(price.amount); const [tempPrice, setTempPrice] = useState<string>(`${price.amount}`);
/** /**
* Return the formatted localized amount for the price (eg. 20.5 => "20,50 €") * Return the formatted localized amount for the price (eg. 20.5 => "20,50 €")
@ -31,14 +31,15 @@ export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) =
*/ */
const handleValidateEdit = (): void => { const handleValidateEdit = (): void => {
const newPrice: Price = Object.assign({}, price); const newPrice: Price = Object.assign({}, price);
newPrice.amount = tempPrice; newPrice.amount = parseFloat(tempPrice);
onSave(newPrice); onSave(newPrice);
toggleEdit();
} }
/** /**
* Enable or disable the edit mode * Enable or disable the edit mode
*/ */
const toggleEdit= (): void => { const toggleEdit = (): void => {
setEdit(!edit); setEdit(!edit);
} }
@ -46,7 +47,7 @@ export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) =
<span className="editable-price"> <span className="editable-price">
{!edit && <span className="display-price" onClick={toggleEdit}>{formatPrice()}</span>} {!edit && <span className="display-price" onClick={toggleEdit}>{formatPrice()}</span>}
{edit && <span> {edit && <span>
<FabInput id="price" defaultValue={price.amount} addOn={Fablab.intl_currency} onChange={setTempPrice} required/> <FabInput id="price" type="number" step={0.01} defaultValue={price.amount} addOn={Fablab.intl_currency} onChange={setTempPrice} required/>
<FabButton icon={<i className="fas fa-check" />} className="approve-button" onClick={handleValidateEdit} /> <FabButton icon={<i className="fas fa-check" />} className="approve-button" onClick={handleValidateEdit} />
<FabButton icon={<i className="fas fa-times" />} className="cancel-button" onClick={toggleEdit} /> <FabButton icon={<i className="fas fa-times" />} className="cancel-button" onClick={toggleEdit} />
</span>} </span>}

View File

@ -16,6 +16,7 @@ import PriceAPI from '../../api/price';
import { Price } from '../../models/price'; import { Price } from '../../models/price';
import PrepaidPackAPI from '../../api/prepaid-pack'; import PrepaidPackAPI from '../../api/prepaid-pack';
import { PrepaidPack } from '../../models/prepaid-pack'; import { PrepaidPack } from '../../models/prepaid-pack';
import { useImmer } from 'use-immer';
declare var Fablab: IFablab; declare var Fablab: IFablab;
declare var Application: IApplication; declare var Application: IApplication;
@ -33,7 +34,7 @@ const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess })
const [machines, setMachines] = useState<Array<Machine>>(null); const [machines, setMachines] = useState<Array<Machine>>(null);
const [groups, setGroups] = useState<Array<Group>>(null); const [groups, setGroups] = useState<Array<Group>>(null);
const [prices, setPrices] = useState<Array<Price>>(null); const [prices, updatePrices] = useImmer<Array<Price>>(null);
const [packs, setPacks] = useState<Array<PrepaidPack>>(null); const [packs, setPacks] = useState<Array<PrepaidPack>>(null);
// retrieve the initial data // retrieve the initial data
@ -41,11 +42,11 @@ const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess })
MachineAPI.index([{ key: 'disabled', value: false }]) MachineAPI.index([{ key: 'disabled', value: false }])
.then(data => setMachines(data)) .then(data => setMachines(data))
.catch(error => onError(error)); .catch(error => onError(error));
GroupAPI.index([{ key: 'disabled', value: false }]) GroupAPI.index({ disabled: false , admins: false })
.then(data => setGroups(data)) .then(data => setGroups(data))
.catch(error => onError(error)); .catch(error => onError(error));
PriceAPI.index([{ key: 'priceable_type', value: 'Machine'}, { key: 'plan_id', value: null }]) PriceAPI.index([{ key: 'priceable_type', value: 'Machine'}, { key: 'plan_id', value: null }])
.then(data => setPrices(data)) .then(data => updatePrices(data))
.catch(error => onError(error)); .catch(error => onError(error));
PrepaidPackAPI.index() PrepaidPackAPI.index()
.then(data => setPacks(data)) .then(data => setPacks(data))
@ -80,17 +81,31 @@ const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess })
} }
}; };
/**
* Update the given price in the internal state
*/
const updatePrice = (price: Price): void => {
updatePrices(draft => {
const index = draft.findIndex(p => p.id === price.id);
draft[index] = price;
return draft;
});
}
/** /**
* Callback triggered when the user has confirmed to update a price * Callback triggered when the user has confirmed to update a price
*/ */
const handleUpdatePrice = (price: Price): void => { const handleUpdatePrice = (price: Price): void => {
PriceAPI.update(price) PriceAPI.update(price)
.then(() => onSuccess(t('app.admin.machines_pricing.price_updated'))) .then(() => {
onSuccess(t('app.admin.machines_pricing.price_updated'));
updatePrice(price);
})
.catch(error => onError(error)) .catch(error => onError(error))
} }
return ( return (
<div className="machine-pricing"> <div className="machines-pricing">
<FabAlert level="warning"> <FabAlert level="warning">
<p><HtmlTranslate trKey="app.admin.machines_pricing.prices_match_machine_hours_rates_html"/></p> <p><HtmlTranslate trKey="app.admin.machines_pricing.prices_match_machine_hours_rates_html"/></p>
<p><HtmlTranslate trKey="app.admin.machines_pricing.prices_calculated_on_hourly_rate_html" options={{ DURATION: EXEMPLE_DURATION, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }} /></p> <p><HtmlTranslate trKey="app.admin.machines_pricing.prices_calculated_on_hourly_rate_html" options={{ DURATION: EXEMPLE_DURATION, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }} /></p>

View File

@ -1,6 +1,6 @@
export interface GroupIndexFilter { export interface GroupIndexFilter {
key: 'disabled', disabled?: boolean,
value: boolean, admins?: boolean,
} }
export interface Group { export interface Group {

View File

@ -55,5 +55,7 @@
@import "modules/machines/machines-filters"; @import "modules/machines/machines-filters";
@import "modules/machines/required-training-modal"; @import "modules/machines/required-training-modal";
@import "modules/user/avatar"; @import "modules/user/avatar";
@import "modules/pricing/machines-pricing";
@import "modules/pricing/editable-price";
@import "app.responsive"; @import "app.responsive";

View File

@ -0,0 +1,13 @@
.editable-price {
.display-price {
text-decoration: none;
color: #428bca;
border-bottom: dashed 1px #428bca;
&:hover {
text-decoration: none;
color: #2a6496;
border-bottom-color: #2a6496;
}
}
}

View File

@ -0,0 +1,31 @@
.machines-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

@ -14,6 +14,8 @@ class GroupService
groups = groups.where(disabled: state) groups = groups.where(disabled: state)
end end
groups = groups.where.not(slug: 'admins') if filters[:admins] == 'false'
groups groups
end end
end end