mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-30 19:52:20 +01:00
WIP: migrate machine pricing edition interface to react
This commit is contained in:
parent
d54f30e048
commit
d7ba83f6a0
@ -6,12 +6,7 @@ class API::GroupsController < API::ApiController
|
|||||||
before_action :authenticate_user!, except: :index
|
before_action :authenticate_user!, except: :index
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@groups = if current_user&.admin?
|
@groups = GroupService.list(current_user, params)
|
||||||
Group.all
|
|
||||||
else
|
|
||||||
Group.where.not(slug: 'admins')
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -7,12 +7,7 @@ class API::MachinesController < API::ApiController
|
|||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
def index
|
def index
|
||||||
sort_by = Setting.get('machines_sort_by') || 'default'
|
@machines = MachineService.list(params)
|
||||||
@machines = if sort_by == 'default'
|
|
||||||
Machine.includes(:machine_image, :plans)
|
|
||||||
else
|
|
||||||
Machine.includes(:machine_image, :plans).order(sort_by)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -7,21 +7,8 @@ class API::PricesController < API::ApiController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
authorize Price
|
authorize Price
|
||||||
@prices = Price.all
|
|
||||||
if params[:priceable_type]
|
|
||||||
@prices = @prices.where(priceable_type: params[:priceable_type])
|
|
||||||
|
|
||||||
@prices = @prices.where(priceable_id: params[:priceable_id]) if params[:priceable_id]
|
@prices = PriceService.list(params)
|
||||||
end
|
|
||||||
if params[:plan_id]
|
|
||||||
plan_id = if /no|nil|null|undefined/i.match?(params[:plan_id])
|
|
||||||
nil
|
|
||||||
else
|
|
||||||
params[:plan_id]
|
|
||||||
end
|
|
||||||
@prices = @prices.where(plan_id: plan_id)
|
|
||||||
end
|
|
||||||
@prices = @prices.where(group_id: params[:group_id]) if params[:group_id]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import apiClient from './clients/api-client';
|
import apiClient from './clients/api-client';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { Group } from '../models/group';
|
import { Group, GroupIndexFilter } from '../models/group';
|
||||||
|
|
||||||
export default class GroupAPI {
|
export default class GroupAPI {
|
||||||
static async index (): Promise<Array<Group>> {
|
static async index (filters?: Array<GroupIndexFilter>): Promise<Array<Group>> {
|
||||||
const res: AxiosResponse<Array<Group>> = await apiClient.get('/api/groups');
|
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 {
|
||||||
|
if (!filters) return '';
|
||||||
|
|
||||||
|
return '?' + filters.map(f => `${f.key}=${f.value}`).join('&');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import apiClient from './clients/api-client';
|
import apiClient from './clients/api-client';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { Machine } from '../models/machine';
|
import { Machine, MachineIndexFilter } from '../models/machine';
|
||||||
|
|
||||||
export default class MachineAPI {
|
export default class MachineAPI {
|
||||||
static async index (): Promise<Array<Machine>> {
|
static async index (filters?: Array<MachineIndexFilter>): Promise<Array<Machine>> {
|
||||||
const res: AxiosResponse<Array<Machine>> = await apiClient.get(`/api/machines`);
|
const res: AxiosResponse<Array<Machine>> = await apiClient.get(`/api/machines${MachineAPI.filtersToQuery(filters)}`);
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12,5 +12,11 @@ export default class MachineAPI {
|
|||||||
const res: AxiosResponse<Machine> = await apiClient.get(`/api/machines/${id}`);
|
const res: AxiosResponse<Machine> = await apiClient.get(`/api/machines/${id}`);
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static filtersToQuery(filters?: Array<MachineIndexFilter>): string {
|
||||||
|
if (!filters) return '';
|
||||||
|
|
||||||
|
return '?' + filters.map(f => `${f.key}=${f.value}`).join('&');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import apiClient from './clients/api-client';
|
import apiClient from './clients/api-client';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { IndexFilter, PrepaidPack } from '../models/prepaid-pack';
|
import { PackIndexFilter, PrepaidPack } from '../models/prepaid-pack';
|
||||||
|
|
||||||
export default class PrepaidPackAPI {
|
export default class PrepaidPackAPI {
|
||||||
static async index (filters?: Array<IndexFilter>): Promise<Array<PrepaidPack>> {
|
static async index (filters?: Array<PackIndexFilter>): Promise<Array<PrepaidPack>> {
|
||||||
const res: AxiosResponse<Array<PrepaidPack>> = await apiClient.get(`/api/prepaid_packs${PrepaidPackAPI.filtersToQuery(filters)}`);
|
const res: AxiosResponse<Array<PrepaidPack>> = await apiClient.get(`/api/prepaid_packs${PrepaidPackAPI.filtersToQuery(filters)}`);
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
@ -28,7 +28,7 @@ export default class PrepaidPackAPI {
|
|||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static filtersToQuery(filters?: Array<IndexFilter>): string {
|
private static filtersToQuery(filters?: Array<PackIndexFilter>): string {
|
||||||
if (!filters) return '';
|
if (!filters) return '';
|
||||||
|
|
||||||
return '?' + filters.map(f => `${f.key}=${f.value}`).join('&');
|
return '?' + filters.map(f => `${f.key}=${f.value}`).join('&');
|
||||||
|
@ -1,12 +1,28 @@
|
|||||||
import apiClient from './clients/api-client';
|
import apiClient from './clients/api-client';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { ShoppingCart } from '../models/payment';
|
import { ShoppingCart } from '../models/payment';
|
||||||
import { ComputePriceResult } from '../models/price';
|
import { ComputePriceResult, Price, PriceIndexFilter } from '../models/price';
|
||||||
|
|
||||||
export default class PriceAPI {
|
export default class PriceAPI {
|
||||||
static async compute (cart: ShoppingCart): Promise<ComputePriceResult> {
|
static async compute (cart: ShoppingCart): Promise<ComputePriceResult> {
|
||||||
const res: AxiosResponse = await apiClient.post(`/api/prices/compute`, cart);
|
const res: AxiosResponse<ComputePriceResult> = await apiClient.post(`/api/prices/compute`, cart);
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async index (filters?: Array<PriceIndexFilter>): Promise<Array<Price>> {
|
||||||
|
const res: AxiosResponse = await apiClient.get(`/api/prices${this.filtersToQuery(filters)}`);
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update (price: Price): Promise<Price> {
|
||||||
|
const res: AxiosResponse<Price> = await apiClient.patch(`/api/price/${price.id}`, { price });
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static filtersToQuery(filters?: Array<PriceIndexFilter>): string {
|
||||||
|
if (!filters) return '';
|
||||||
|
|
||||||
|
return '?' + filters.map(f => `${f.key}=${f.value}`).join('&');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import PrepaidPackAPI from '../api/prepaid-pack';
|
|
||||||
import { IndexFilter, PrepaidPack } from '../models/prepaid-pack';
|
|
||||||
import { Loader } from './base/loader';
|
|
||||||
import { react2angular } from 'react2angular';
|
|
||||||
import { IApplication } from '../models/application';
|
|
||||||
|
|
||||||
declare var Application: IApplication;
|
|
||||||
|
|
||||||
interface ConfigurePacksButtonParams {
|
|
||||||
groupId: number,
|
|
||||||
priceableId: number,
|
|
||||||
priceableType: string,
|
|
||||||
onError: (message: string) => void,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This component is a button that shows the list of prepaid-packs when moving the mouse over it.
|
|
||||||
* When clicked, it opens a modal dialog to configure (add/delete/edit/remove) prepaid-packs.
|
|
||||||
*/
|
|
||||||
const ConfigurePacksButton: React.FC<ConfigurePacksButtonParams> = ({ groupId, priceableId, priceableType, onError }) => {
|
|
||||||
const [packs, setPacks] = useState<Array<PrepaidPack>>(null);
|
|
||||||
const [showList, setShowList] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
PrepaidPackAPI.index(buildFilters())
|
|
||||||
.then(data => setPacks(data))
|
|
||||||
.catch(error => onError(error))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the filters for the current ConfigurePackButton, to query the API and get the concerned packs.
|
|
||||||
*/
|
|
||||||
const buildFilters = (): Array<IndexFilter> => {
|
|
||||||
const res = [];
|
|
||||||
if (groupId) res.push({ key: 'group_id', value: groupId });
|
|
||||||
if (priceableId) res.push({ key: 'priceable_id', value: priceableId });
|
|
||||||
if (priceableType) res.push({ key: 'priceable_type', value: priceableType });
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleShowList = (): void => {
|
|
||||||
setShowList(!showList);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="configure-packs-button" onMouseOver={toggleShowList}>
|
|
||||||
{packs && showList && <div className="packs-overview">
|
|
||||||
{packs.map(p => <div>{p.minutes / 60}h - {p.amount}</div>)}
|
|
||||||
</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const ConfigurePacksButtonWrapper: React.FC<ConfigurePacksButtonParams> = ({ groupId, priceableId, priceableType, onError }) => {
|
|
||||||
return (
|
|
||||||
<Loader>
|
|
||||||
<ConfigurePacksButton groupId={groupId} priceableId={priceableId} priceableType={priceableType} onError={onError}/>
|
|
||||||
</Loader>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Application.Components.component('configurePacksButton', react2angular(ConfigurePacksButtonWrapper, ['groupId', 'priceableId', 'priceableType', 'onError']));
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { PrepaidPack } from '../../models/prepaid-pack';
|
||||||
|
|
||||||
|
interface ConfigurePacksButtonProps {
|
||||||
|
packs: Array<PrepaidPack>,
|
||||||
|
onError: (message: string) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component is a button that shows the list of prepaid-packs when moving the mouse over it.
|
||||||
|
* When clicked, it opens a modal dialog to configure (add/delete/edit/remove) prepaid-packs.
|
||||||
|
*/
|
||||||
|
export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ packs, onError }) => {
|
||||||
|
const [showList, setShowList] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const toggleShowList = (): void => {
|
||||||
|
setShowList(!showList);
|
||||||
|
}
|
||||||
|
const handleAddPack = (): void => {
|
||||||
|
//TODO, open a modal to add a new pack
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="configure-packs-button" onMouseOver={toggleShowList} onClick={handleAddPack}>
|
||||||
|
{packs && showList && <div className="packs-overview">
|
||||||
|
{packs.map(p => <div>{p.minutes / 60}h - {p.amount}</div>)}
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { IFablab } from '../../models/fablab';
|
||||||
|
import { FabInput } from '../base/fab-input';
|
||||||
|
import { FabButton } from '../base/fab-button';
|
||||||
|
import { Price } from '../../models/price';
|
||||||
|
|
||||||
|
declare var Fablab: IFablab;
|
||||||
|
|
||||||
|
interface EditablePriceProps {
|
||||||
|
price: Price,
|
||||||
|
onSave: (price: Price) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the given price.
|
||||||
|
* When the user clics on the price, switch to the edition mode to allow him modifying the price.
|
||||||
|
*/
|
||||||
|
export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) => {
|
||||||
|
const [edit, setEdit] = useState<boolean>(false);
|
||||||
|
const [tempPrice, setTempPrice] = useState<number>(price.amount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the formatted localized amount for the price (eg. 20.5 => "20,50 €")
|
||||||
|
*/
|
||||||
|
const formatPrice = (): string => {
|
||||||
|
return new Intl.NumberFormat(Fablab.intl_locale, { style: 'currency', currency: Fablab.intl_currency }).format(price.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the new price
|
||||||
|
*/
|
||||||
|
const handleValidateEdit = (): void => {
|
||||||
|
const newPrice: Price = Object.assign({}, price);
|
||||||
|
newPrice.amount = tempPrice;
|
||||||
|
onSave(newPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable the edit mode
|
||||||
|
*/
|
||||||
|
const toggleEdit= (): void => {
|
||||||
|
setEdit(!edit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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/>
|
||||||
|
<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>}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { react2angular } from 'react2angular';
|
||||||
|
import { Loader } from '../base/loader';
|
||||||
|
import { FabAlert } from '../base/fab-alert';
|
||||||
|
import { HtmlTranslate } from '../base/html-translate';
|
||||||
|
import MachineAPI from '../../api/machine';
|
||||||
|
import GroupAPI from '../../api/group';
|
||||||
|
import { IFablab } from '../../models/fablab';
|
||||||
|
import { Machine } from '../../models/machine';
|
||||||
|
import { Group } from '../../models/group';
|
||||||
|
import { IApplication } from '../../models/application';
|
||||||
|
import { EditablePrice } from './editable-price';
|
||||||
|
import { ConfigurePacksButton } from './configure-packs-button';
|
||||||
|
import PriceAPI from '../../api/price';
|
||||||
|
import { Price } from '../../models/price';
|
||||||
|
import PrepaidPackAPI from '../../api/prepaid-pack';
|
||||||
|
import { PrepaidPack } from '../../models/prepaid-pack';
|
||||||
|
|
||||||
|
declare var Fablab: IFablab;
|
||||||
|
declare var Application: IApplication;
|
||||||
|
|
||||||
|
interface MachinesPricingProps {
|
||||||
|
onError: (message: string) => void,
|
||||||
|
onSuccess: (message: string) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to set and edit the prices of machines-hours, per group
|
||||||
|
*/
|
||||||
|
const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess }) => {
|
||||||
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
|
const [machines, setMachines] = useState<Array<Machine>>(null);
|
||||||
|
const [groups, setGroups] = useState<Array<Group>>(null);
|
||||||
|
const [prices, setPrices] = useState<Array<Price>>(null);
|
||||||
|
const [packs, setPacks] = useState<Array<PrepaidPack>>(null);
|
||||||
|
|
||||||
|
// retrieve the initial data
|
||||||
|
useEffect(() => {
|
||||||
|
MachineAPI.index([{ key: 'disabled', value: false }])
|
||||||
|
.then(data => setMachines(data))
|
||||||
|
.catch(error => onError(error));
|
||||||
|
GroupAPI.index([{ key: 'disabled', value: 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))
|
||||||
|
.catch(error => onError(error));
|
||||||
|
PrepaidPackAPI.index()
|
||||||
|
.then(data => setPacks(data))
|
||||||
|
.catch(error => onError(error))
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// duration of the example slot
|
||||||
|
const EXEMPLE_DURATION = 20;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the exemple price, formatted
|
||||||
|
*/
|
||||||
|
const examplePrice = (type: 'hourly_rate' | 'final_price'): string => {
|
||||||
|
const hourlyRate = 10;
|
||||||
|
|
||||||
|
if (type === 'hourly_rate') {
|
||||||
|
return new Intl.NumberFormat(Fablab.intl_locale, { style: 'currency', currency: Fablab.intl_currency }).format(hourlyRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = (hourlyRate / 60) * EXEMPLE_DURATION;
|
||||||
|
return new Intl.NumberFormat(Fablab.intl_locale, { style: 'currency', currency: Fablab.intl_currency }).format(price);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the price matching the given criterion
|
||||||
|
*/
|
||||||
|
const findPriceBy = (machineId, groupId): Price => {
|
||||||
|
for (const price of prices) {
|
||||||
|
if ((price.priceable_id === machineId) && (price.group_id === groupId)) {
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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')))
|
||||||
|
.catch(error => onError(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="machine-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>
|
||||||
|
<p>{t('app.admin.machines_pricing.you_can_override')}</p>
|
||||||
|
</FabAlert>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{t('app.admin.machines_pricing.machines')}</th>
|
||||||
|
{groups?.map(group => <th key={group.id} className="group-name">{group.name}</th>)}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{machines?.map(machine => <tr key={machine.id}>
|
||||||
|
<td>{machine.name}</td>
|
||||||
|
{groups?.map(group => <td key={group.id}>
|
||||||
|
{prices && <EditablePrice price={findPriceBy(machine.id, group.id)} onSave={handleUpdatePrice} />}
|
||||||
|
{packs && <ConfigurePacksButton packs={packs} onError={onError} />}
|
||||||
|
</td>)}
|
||||||
|
</tr>)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MachinesPricingWrapper: React.FC<MachinesPricingProps> = ({ onError, onSuccess }) => {
|
||||||
|
return (
|
||||||
|
<Loader>
|
||||||
|
<MachinesPricing onError={onError} onSuccess={onSuccess} />
|
||||||
|
</Loader>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Application.Components.component('machinesPricing', react2angular(MachinesPricingWrapper, ['onError', 'onSuccess']));
|
||||||
|
|
||||||
|
|
@ -18,13 +18,10 @@
|
|||||||
/**
|
/**
|
||||||
* Controller used in the prices edition page
|
* Controller used in the prices edition page
|
||||||
*/
|
*/
|
||||||
Application.Controllers.controller('EditPricingController', ['$scope', '$state', '$uibModal', '$filter', 'TrainingsPricing', 'Credit', 'Pricing', 'Plan', 'Coupon', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', 'couponsPromise', 'spacesPromise', 'spacesPricesPromise', 'spacesCreditsPromise', 'settingsPromise', '_t', 'Member', 'uiTourService', 'planCategories',
|
Application.Controllers.controller('EditPricingController', ['$scope', '$state', '$uibModal', '$filter', 'TrainingsPricing', 'Credit', 'Pricing', 'Plan', 'Coupon', 'plans', 'groups', 'growl', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', 'couponsPromise', 'spacesPromise', 'spacesPricesPromise', 'spacesCreditsPromise', 'settingsPromise', '_t', 'Member', 'uiTourService', 'planCategories',
|
||||||
function ($scope, $state, $uibModal, $filter, TrainingsPricing, Credit, Pricing, Plan, Coupon, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, spacesPromise, spacesPricesPromise, spacesCreditsPromise, settingsPromise, _t, Member, uiTourService, planCategories) {
|
function ($scope, $state, $uibModal, $filter, TrainingsPricing, Credit, Pricing, Plan, Coupon, plans, groups, growl, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, spacesPromise, spacesPricesPromise, spacesCreditsPromise, settingsPromise, _t, Member, uiTourService, planCategories) {
|
||||||
/* PUBLIC SCOPE */
|
/* PUBLIC SCOPE */
|
||||||
|
|
||||||
// List of machines prices (not considering any plan)
|
|
||||||
$scope.machinesPrices = machinesPricesPromise;
|
|
||||||
|
|
||||||
// List of trainings pricing
|
// List of trainings pricing
|
||||||
$scope.trainingsPricings = trainingsPricingsPromise;
|
$scope.trainingsPricings = trainingsPricingsPromise;
|
||||||
|
|
||||||
@ -640,6 +637,20 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
|
|||||||
return $filter('currency')(price);
|
return $filter('currency')(price);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered by react components
|
||||||
|
*/
|
||||||
|
$scope.onSuccess = function (message) {
|
||||||
|
growl.success(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered by react components
|
||||||
|
*/
|
||||||
|
$scope.onError = function (message) {
|
||||||
|
growl.error(message);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup the feature-tour for the admin/pricing page.
|
* Setup the feature-tour for the admin/pricing page.
|
||||||
* This is intended as a contextual help (when pressing F1)
|
* This is intended as a contextual help (when pressing F1)
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
export interface GroupIndexFilter {
|
||||||
|
key: 'disabled',
|
||||||
|
value: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
export interface Group {
|
export interface Group {
|
||||||
id: number,
|
id: number,
|
||||||
slug: string,
|
slug: string,
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { Reservation } from './reservation';
|
import { Reservation } from './reservation';
|
||||||
|
|
||||||
|
export interface MachineIndexFilter {
|
||||||
|
key: 'disabled',
|
||||||
|
value: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
export interface Machine {
|
export interface Machine {
|
||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
export interface IndexFilter {
|
export interface PackIndexFilter {
|
||||||
key: 'group_id' | 'priceable_id' | 'priceable_type',
|
key: 'group_id' | 'priceable_id' | 'priceable_type',
|
||||||
value: number|string,
|
value: number|string,
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
export interface PriceIndexFilter {
|
||||||
|
key: 'priceable_type' | 'priceable_id' | 'group_id' | 'plan_id',
|
||||||
|
value?: number|string,
|
||||||
|
}
|
||||||
|
|
||||||
export interface Price {
|
export interface Price {
|
||||||
id: number,
|
id: number,
|
||||||
group_id: number,
|
group_id: number,
|
||||||
|
@ -771,7 +771,6 @@ angular.module('application.router', ['ui.router'])
|
|||||||
resolve: {
|
resolve: {
|
||||||
plans: ['Plan', function (Plan) { return Plan.query().$promise; }],
|
plans: ['Plan', function (Plan) { return Plan.query().$promise; }],
|
||||||
groups: ['Group', function (Group) { return Group.query().$promise; }],
|
groups: ['Group', function (Group) { return Group.query().$promise; }],
|
||||||
machinesPricesPromise: ['Price', function (Price) { return Price.query({ priceable_type: 'Machine', plan_id: 'null' }).$promise; }],
|
|
||||||
trainingsPricingsPromise: ['TrainingsPricing', function (TrainingsPricing) { return TrainingsPricing.query().$promise; }],
|
trainingsPricingsPromise: ['TrainingsPricing', function (TrainingsPricing) { return TrainingsPricing.query().$promise; }],
|
||||||
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
|
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
|
||||||
machineCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Machine' }).$promise; }],
|
machineCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Machine' }).$promise; }],
|
||||||
|
@ -1,30 +1 @@
|
|||||||
<div class="alert alert-warning m-t">
|
<machines-pricing on-success="onSuccess" on-error="onError">
|
||||||
<p ng-bind-html="'app.admin.pricing.these_prices_match_machine_hours_rates_html' | translate"></p>
|
|
||||||
<p ng-bind-html="'app.admin.pricing.prices_calculated_on_hourly_rate_html' | translate:{ DURATION:slotDuration, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }"></p>
|
|
||||||
<p translate>{{ 'app.admin.pricing.you_can_override' }}</p>
|
|
||||||
</div>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width:20%" translate>{{ 'app.admin.pricing.machines' }}</th>
|
|
||||||
<th style="width:20%" ng-repeat="group in enabledGroups">
|
|
||||||
<span class="text-u-c text-sm">{{group.name}}</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr ng-repeat="machine in enabledMachines">
|
|
||||||
<td>
|
|
||||||
{{ machine.name }}
|
|
||||||
</td>
|
|
||||||
<td ng-repeat="group in enabledGroups">
|
|
||||||
<span editable-number="findPriceBy(machinesPrices, machine.id, group.id).amount"
|
|
||||||
e-step="any"
|
|
||||||
onbeforesave="updatePrice($data, findPriceBy(machinesPrices, machine.id, group.id))">
|
|
||||||
{{ findPriceBy(machinesPrices, machine.id, group.id).amount | currency}}
|
|
||||||
</span>
|
|
||||||
<configure-packs-button group-id="group.id" priceable-id="machine.id" priceable-type="'Machine'" />
|
|
||||||
</td> <!-- FIXME: too much API calls -->
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
<div class="modal-header">
|
|
||||||
<img ng-src="{{logoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{logo.custom_asset_file_attributes.attachment}}" class="modal-logo"/>
|
|
||||||
|
|
||||||
<div class="padder-v">
|
|
||||||
<span class="avatar text-center">
|
|
||||||
<fab-user-avatar ng-model="member.user_avatar" avatar-class="thumb-50"></fab-user-avatar>
|
|
||||||
<span class="user-name m-l-sm">{{member.name}}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p class="text-center font-sbold" translate translate-values="{MACHINE:machine.name, TRAINING: humanizeTrainings()}">{{ 'app.shared.request_training_modal.to_book_the_MACHINE_you_must_have_completed_the_TRAINING' }}</p>
|
|
||||||
<p class="text-center"><button ng-click="ok()" class="btn btn-warning-full rounded width-70" translate>{{ 'app.shared.request_training_modal.register_for_the_training' }}</button></p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<p class="text-center">{{ 'app.shared.request_training_modal.i_dont_want_to_register_now' | translate }} <br> <a class="text-u-l" href="#" ng-click="cancel($event)" translate>{{ 'app.shared.buttons.close' }}</a></p>
|
|
||||||
</div>
|
|
@ -1,10 +0,0 @@
|
|||||||
<div class="modal-header">
|
|
||||||
<h1 translate>{{ 'app.shared.training_reservation_modal.machine_reservation' }}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p translate> {{ 'app.shared.training_reservation_modal.you_must_wait_for_your_training_is_being_validated_by_the_fablab_team_to_book_this_machine' }}</p>
|
|
||||||
<p>{{ 'app.shared.training_reservation_modal.your_training_will_occur_' | translate }} <span class="sbold">{{machine.current_user_next_training_reservation.slots_attributes[0].start_at | amDateFormat: 'LL'}} : {{machine.current_user_next_training_reservation.slots_attributes[0].start_at | amDateFormat:'LT'}} - {{machine.current_user_next_training_reservation.slots_attributes[0].end_at | amDateFormat:'LT'}}</span></p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn btn-warning" ng-click="cancel()" translate>{{ 'app.shared.buttons.close' }}</button>
|
|
||||||
</div>
|
|
19
app/services/group_service.rb
Normal file
19
app/services/group_service.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Provides methods for Groups
|
||||||
|
class GroupService
|
||||||
|
def self.list(operator, filters = {})
|
||||||
|
groups = if operator&.admin?
|
||||||
|
Group.where(nil)
|
||||||
|
else
|
||||||
|
Group.where.not(slug: 'admins')
|
||||||
|
end
|
||||||
|
|
||||||
|
if filters[:disabled].present?
|
||||||
|
state = filters[:disabled] == 'false' ? [nil, false] : true
|
||||||
|
groups = groups.where(disabled: state)
|
||||||
|
end
|
||||||
|
|
||||||
|
groups
|
||||||
|
end
|
||||||
|
end
|
19
app/services/machine_service.rb
Normal file
19
app/services/machine_service.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Provides methods for Machines
|
||||||
|
class MachineService
|
||||||
|
def self.list(filters)
|
||||||
|
sort_by = Setting.get('machines_sort_by') || 'default'
|
||||||
|
machines = if sort_by == 'default'
|
||||||
|
Machine.includes(:machine_image, :plans)
|
||||||
|
else
|
||||||
|
Machine.includes(:machine_image, :plans).order(sort_by)
|
||||||
|
end
|
||||||
|
if filters[:disabled].present?
|
||||||
|
state = filters[:disabled] == 'false' ? [nil, false] : true
|
||||||
|
machines = machines.where(disabled: state)
|
||||||
|
end
|
||||||
|
|
||||||
|
machines
|
||||||
|
end
|
||||||
|
end
|
18
app/services/price_service.rb
Normal file
18
app/services/price_service.rb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Provides methods for Prices
|
||||||
|
class PriceService
|
||||||
|
def self.list(filters)
|
||||||
|
prices = Price.where(nil)
|
||||||
|
|
||||||
|
prices = prices.where(priceable_type: filters[:priceable_type]) if filters[:priceable_type].present?
|
||||||
|
prices = prices.where(priceable_id: filters[:priceable_id]) if filters[:priceable_id].present?
|
||||||
|
prices = prices.where(group_id: filters[:group_id]) if filters[:group_id].present?
|
||||||
|
if filters[:plan_id].present?
|
||||||
|
plan_id = /no|nil|null|undefined/i.match?(filters[:plan_id]) ? nil : filters[:plan_id]
|
||||||
|
prices = prices.where(plan_id: plan_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
prices
|
||||||
|
end
|
||||||
|
end
|
@ -307,7 +307,6 @@ en:
|
|||||||
prominence: "Prominence"
|
prominence: "Prominence"
|
||||||
price: "Price"
|
price: "Price"
|
||||||
machine_hours: "Machine slots"
|
machine_hours: "Machine slots"
|
||||||
these_prices_match_machine_hours_rates_html: "The prices below match one hour of machine usage, <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 (default), will be charged <strong>{PRICE}</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 (default), 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."
|
you_can_override: "You can override this duration for each availability you create in the agenda. The price will then be adjusted accordingly."
|
||||||
machines: "Machines"
|
machines: "Machines"
|
||||||
@ -369,6 +368,12 @@ en:
|
|||||||
status_enabled: "Enabled"
|
status_enabled: "Enabled"
|
||||||
status_disabled: "Disabled"
|
status_disabled: "Disabled"
|
||||||
status_all: "All"
|
status_all: "All"
|
||||||
|
machines_pricing:
|
||||||
|
prices_match_machine_hours_rates_html: "The prices below match one hour of machine usage, <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 (default), 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."
|
||||||
|
machines: "Machines"
|
||||||
|
price_updated: "Price successfully updated"
|
||||||
#ajouter un code promotionnel
|
#ajouter un code promotionnel
|
||||||
coupons_new:
|
coupons_new:
|
||||||
add_a_coupon: "Add a coupon"
|
add_a_coupon: "Add a coupon"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user