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:
parent
d7ba83f6a0
commit
8cc4811794
@ -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('&');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>}
|
||||||
|
@ -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>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export interface GroupIndexFilter {
|
export interface GroupIndexFilter {
|
||||||
key: 'disabled',
|
disabled?: boolean,
|
||||||
value: boolean,
|
admins?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Group {
|
export interface Group {
|
||||||
|
@ -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";
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user