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';
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('&');
}
}

View File

@ -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;
}

View File

@ -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}

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>}

View File

@ -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>

View File

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

View File

@ -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";

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)
end
groups = groups.where.not(slug: 'admins') if filters[:admins] == 'false'
groups
end
end