mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-29 18:52:22 +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
|
||||
|
||||
def index
|
||||
@groups = if current_user&.admin?
|
||||
Group.all
|
||||
else
|
||||
Group.where.not(slug: 'admins')
|
||||
end
|
||||
|
||||
@groups = GroupService.list(current_user, params)
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -7,12 +7,7 @@ class API::MachinesController < API::ApiController
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
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
|
||||
@machines = MachineService.list(params)
|
||||
end
|
||||
|
||||
def show
|
||||
|
@ -7,21 +7,8 @@ class API::PricesController < API::ApiController
|
||||
|
||||
def index
|
||||
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]
|
||||
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]
|
||||
@prices = PriceService.list(params)
|
||||
end
|
||||
|
||||
def update
|
||||
|
@ -1,11 +1,17 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Group } from '../models/group';
|
||||
import { Group, GroupIndexFilter } from '../models/group';
|
||||
|
||||
export default class GroupAPI {
|
||||
static async index (): Promise<Array<Group>> {
|
||||
const res: AxiosResponse<Array<Group>> = await apiClient.get('/api/groups');
|
||||
static async index (filters?: Array<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 {
|
||||
if (!filters) return '';
|
||||
|
||||
return '?' + filters.map(f => `${f.key}=${f.value}`).join('&');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Machine } from '../models/machine';
|
||||
import { Machine, MachineIndexFilter } from '../models/machine';
|
||||
|
||||
export default class MachineAPI {
|
||||
static async index (): Promise<Array<Machine>> {
|
||||
const res: AxiosResponse<Array<Machine>> = await apiClient.get(`/api/machines`);
|
||||
static async index (filters?: Array<MachineIndexFilter>): Promise<Array<Machine>> {
|
||||
const res: AxiosResponse<Array<Machine>> = await apiClient.get(`/api/machines${MachineAPI.filtersToQuery(filters)}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
@ -12,5 +12,11 @@ export default class MachineAPI {
|
||||
const res: AxiosResponse<Machine> = await apiClient.get(`/api/machines/${id}`);
|
||||
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 { AxiosResponse } from 'axios';
|
||||
import { IndexFilter, PrepaidPack } from '../models/prepaid-pack';
|
||||
import { PackIndexFilter, PrepaidPack } from '../models/prepaid-pack';
|
||||
|
||||
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)}`);
|
||||
return res?.data;
|
||||
}
|
||||
@ -28,7 +28,7 @@ export default class PrepaidPackAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
private static filtersToQuery(filters?: Array<IndexFilter>): string {
|
||||
private static filtersToQuery(filters?: Array<PackIndexFilter>): string {
|
||||
if (!filters) return '';
|
||||
|
||||
return '?' + filters.map(f => `${f.key}=${f.value}`).join('&');
|
||||
|
@ -1,12 +1,28 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { ShoppingCart } from '../models/payment';
|
||||
import { ComputePriceResult } from '../models/price';
|
||||
import { ComputePriceResult, Price, PriceIndexFilter } from '../models/price';
|
||||
|
||||
export default class PriceAPI {
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
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',
|
||||
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) {
|
||||
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, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, spacesPromise, spacesPricesPromise, spacesCreditsPromise, settingsPromise, _t, Member, uiTourService, planCategories) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// List of machines prices (not considering any plan)
|
||||
$scope.machinesPrices = machinesPricesPromise;
|
||||
|
||||
// List of trainings pricing
|
||||
$scope.trainingsPricings = trainingsPricingsPromise;
|
||||
|
||||
@ -640,6 +637,20 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
|
||||
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.
|
||||
* This is intended as a contextual help (when pressing F1)
|
||||
|
@ -1,3 +1,8 @@
|
||||
export interface GroupIndexFilter {
|
||||
key: 'disabled',
|
||||
value: boolean,
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: number,
|
||||
slug: string,
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { Reservation } from './reservation';
|
||||
|
||||
export interface MachineIndexFilter {
|
||||
key: 'disabled',
|
||||
value: boolean,
|
||||
}
|
||||
|
||||
export interface Machine {
|
||||
id: number,
|
||||
name: string,
|
||||
|
@ -1,5 +1,5 @@
|
||||
|
||||
export interface IndexFilter {
|
||||
export interface PackIndexFilter {
|
||||
key: 'group_id' | 'priceable_id' | 'priceable_type',
|
||||
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 {
|
||||
id: number,
|
||||
group_id: number,
|
||||
|
@ -771,7 +771,6 @@ angular.module('application.router', ['ui.router'])
|
||||
resolve: {
|
||||
plans: ['Plan', function (Plan) { return Plan.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; }],
|
||||
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
|
||||
machineCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Machine' }).$promise; }],
|
||||
|
@ -1,30 +1 @@
|
||||
<div class="alert alert-warning m-t">
|
||||
<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>
|
||||
<machines-pricing on-success="onSuccess" on-error="onError">
|
||||
|
@ -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"
|
||||
price: "Price"
|
||||
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>."
|
||||
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"
|
||||
@ -369,6 +368,12 @@ en:
|
||||
status_enabled: "Enabled"
|
||||
status_disabled: "Disabled"
|
||||
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
|
||||
coupons_new:
|
||||
add_a_coupon: "Add a coupon"
|
||||
|
Loading…
x
Reference in New Issue
Block a user