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';
|
||||
|
||||
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)}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
private static filtersToQuery(filters?: Array<GroupIndexFilter>): string {
|
||||
private static filtersToQuery(filters?: GroupIndexFilter): string {
|
||||
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> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -18,12 +18,13 @@ interface FabInputProps {
|
||||
placeholder?: string,
|
||||
error?: string,
|
||||
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
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
@ -86,6 +87,7 @@ export const FabInput: React.FC<FabInputProps> = ({ id, onChange, defaultValue,
|
||||
{hasIcon() && <span className="fab-input--icon">{icon}</span>}
|
||||
<input id={id}
|
||||
type={type}
|
||||
step={step}
|
||||
className="fab-input--input"
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
|
@ -12,8 +12,6 @@ import { User, UserRole } from '../../models/user';
|
||||
import { IFablab } from '../../models/fablab';
|
||||
import { PaymentSchedule, PaymentScheduleItem, PaymentScheduleItemState } from '../../models/payment-schedule';
|
||||
import PaymentScheduleAPI from '../../api/payment-schedule';
|
||||
import { useImmer } from 'use-immer';
|
||||
import { SettingName } from '../../models/setting';
|
||||
|
||||
declare var Fablab: IFablab;
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { PrepaidPack } from '../../models/prepaid-pack';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
|
||||
interface ConfigurePacksButtonProps {
|
||||
packs: Array<PrepaidPack>,
|
||||
@ -12,19 +13,27 @@ interface ConfigurePacksButtonProps {
|
||||
*/
|
||||
export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ packs, onError }) => {
|
||||
const [showList, setShowList] = useState<boolean>(false);
|
||||
const [addPackModal, setAddPackModal] = useState<boolean>(false);
|
||||
|
||||
const toggleShowList = (): void => {
|
||||
setShowList(!showList);
|
||||
}
|
||||
|
||||
const toggleAddPackModal = (): void => {
|
||||
setAddPackModal(!addPackModal);
|
||||
}
|
||||
|
||||
const handleAddPack = (): void => {
|
||||
//TODO, open a modal to add a new pack
|
||||
toggleAddPackModal();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="configure-packs-button" onMouseOver={toggleShowList} onClick={handleAddPack}>
|
||||
<i className="fas fa-box-open" />
|
||||
{packs && showList && <div className="packs-overview">
|
||||
{packs.map(p => <div>{p.minutes / 60}h - {p.amount}</div>)}
|
||||
</div>}
|
||||
<FabModal isOpen={addPackModal} toggleModal={toggleAddPackModal}>NEW PACK</FabModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ interface EditablePriceProps {
|
||||
*/
|
||||
export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) => {
|
||||
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 €")
|
||||
@ -31,14 +31,15 @@ export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) =
|
||||
*/
|
||||
const handleValidateEdit = (): void => {
|
||||
const newPrice: Price = Object.assign({}, price);
|
||||
newPrice.amount = tempPrice;
|
||||
newPrice.amount = parseFloat(tempPrice);
|
||||
onSave(newPrice);
|
||||
toggleEdit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the edit mode
|
||||
*/
|
||||
const toggleEdit= (): void => {
|
||||
const toggleEdit = (): void => {
|
||||
setEdit(!edit);
|
||||
}
|
||||
|
||||
@ -46,7 +47,7 @@ export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) =
|
||||
<span className="editable-price">
|
||||
{!edit && <span className="display-price" onClick={toggleEdit}>{formatPrice()}</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-times" />} className="cancel-button" onClick={toggleEdit} />
|
||||
</span>}
|
||||
|
@ -16,6 +16,7 @@ import PriceAPI from '../../api/price';
|
||||
import { Price } from '../../models/price';
|
||||
import PrepaidPackAPI from '../../api/prepaid-pack';
|
||||
import { PrepaidPack } from '../../models/prepaid-pack';
|
||||
import { useImmer } from 'use-immer';
|
||||
|
||||
declare var Fablab: IFablab;
|
||||
declare var Application: IApplication;
|
||||
@ -33,7 +34,7 @@ const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess })
|
||||
|
||||
const [machines, setMachines] = useState<Array<Machine>>(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);
|
||||
|
||||
// retrieve the initial data
|
||||
@ -41,11 +42,11 @@ const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess })
|
||||
MachineAPI.index([{ key: 'disabled', value: false }])
|
||||
.then(data => setMachines(data))
|
||||
.catch(error => onError(error));
|
||||
GroupAPI.index([{ key: 'disabled', value: false }])
|
||||
GroupAPI.index({ disabled: false , admins: false })
|
||||
.then(data => setGroups(data))
|
||||
.catch(error => onError(error));
|
||||
PriceAPI.index([{ key: 'priceable_type', value: 'Machine'}, { key: 'plan_id', value: null }])
|
||||
.then(data => setPrices(data))
|
||||
.then(data => updatePrices(data))
|
||||
.catch(error => onError(error));
|
||||
PrepaidPackAPI.index()
|
||||
.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
|
||||
*/
|
||||
const handleUpdatePrice = (price: Price): void => {
|
||||
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))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="machine-pricing">
|
||||
<div className="machines-pricing">
|
||||
<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_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 {
|
||||
key: 'disabled',
|
||||
value: boolean,
|
||||
disabled?: boolean,
|
||||
admins?: boolean,
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
|
@ -55,5 +55,7 @@
|
||||
@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 "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)
|
||||
end
|
||||
|
||||
groups = groups.where.not(slug: 'admins') if filters[:admins] == 'false'
|
||||
|
||||
groups
|
||||
end
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user