mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-17 06:52:27 +01:00
Merge branch 'spaces_multiprices_front' into spaces_multiprices
This commit is contained in:
commit
c3cbc26a1e
@ -4,9 +4,10 @@
|
||||
# Prices are used in reservations (Machine, Space)
|
||||
class API::PricesController < API::ApiController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_price, only: %i[update destroy]
|
||||
|
||||
def create
|
||||
@price = Price.new(price_params)
|
||||
@price = Price.new(price_create_params)
|
||||
@price.amount *= 100
|
||||
|
||||
authorize @price
|
||||
@ -24,7 +25,6 @@ class API::PricesController < API::ApiController
|
||||
|
||||
def update
|
||||
authorize Price
|
||||
@price = Price.find(params[:id])
|
||||
price_parameters = price_params
|
||||
price_parameters[:amount] = price_parameters[:amount] * 100
|
||||
if @price.update(price_parameters)
|
||||
@ -34,6 +34,12 @@ class API::PricesController < API::ApiController
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @price
|
||||
@price.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def compute
|
||||
cs = CartService.new(current_user)
|
||||
cart = cs.from_hash(params)
|
||||
@ -42,6 +48,10 @@ class API::PricesController < API::ApiController
|
||||
|
||||
private
|
||||
|
||||
def set_price
|
||||
@price = Price.find(params[:id])
|
||||
end
|
||||
|
||||
def price_create_params
|
||||
params.require(:price).permit(:amount, :duration, :group_id, :plan_id, :priceable_id, :priceable_type)
|
||||
end
|
||||
|
@ -14,11 +14,21 @@ export default class PriceAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async create (price: Price): Promise<Price> {
|
||||
const res: AxiosResponse<Price> = await apiClient.post('/api/prices', { price });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async update (price: Price): Promise<Price> {
|
||||
const res: AxiosResponse<Price> = await apiClient.patch(`/api/prices/${price.id}`, { price });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async destroy (priceId: number): Promise<void> {
|
||||
const res: AxiosResponse<void> = await apiClient.delete(`/api/prices/${priceId}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
private static filtersToQuery (filters?: PriceIndexFilter): string {
|
||||
if (!filters) return '';
|
||||
|
||||
|
20
app/frontend/src/javascript/api/space.ts
Normal file
20
app/frontend/src/javascript/api/space.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
export default class MachineAPI {
|
||||
static async index (filters?: boolean): Promise<Array<any>> {
|
||||
const res: AxiosResponse<Array<any>> = await apiClient.get(`/api/spaces${this.filtersToQuery(filters)}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async get (id: number): Promise<any> {
|
||||
const res: AxiosResponse<any> = await apiClient.get(`/api/spaces/${id}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
private static filtersToQuery (filters?: boolean): string {
|
||||
if (!filters) return '';
|
||||
|
||||
return '?' + Object.entries(filters).map(f => `${f[0]}=${f[1]}`).join('&');
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { Price } from '../../models/price';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabPopover } from '../base/fab-popover';
|
||||
import { CreateExtendedPrice } from './create-extended-price';
|
||||
import PriceAPI from '../../api/price';
|
||||
import FormatLib from '../../lib/format';
|
||||
import { EditExtendedPrice } from './edit-extended-price';
|
||||
import { DeleteExtendedPrice } from './delete-extended-price';
|
||||
|
||||
interface ConfigureExtendedPriceButtonProps {
|
||||
prices: Array<Price>,
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void,
|
||||
groupId: number,
|
||||
priceableId: number,
|
||||
priceableType: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is a button that shows the list of extendedPrices.
|
||||
* It also triggers modal dialogs to configure (add/delete/edit/remove) extendedPrices.
|
||||
*/
|
||||
export const ConfigureExtendedPriceButton: React.FC<ConfigureExtendedPriceButtonProps> = ({ prices, onError, onSuccess, groupId, priceableId, priceableType }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [extendedPrices, setExtendedPrices] = useState<Array<Price>>(prices);
|
||||
const [showList, setShowList] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* Open/closes the popover listing the existing packs
|
||||
*/
|
||||
const toggleShowList = (): void => {
|
||||
setShowList(!showList);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the extendedPrice was successfully created/deleted/updated.
|
||||
* We refresh the list of extendedPrices.
|
||||
*/
|
||||
const handleSuccess = (message: string) => {
|
||||
onSuccess(message);
|
||||
PriceAPI.index({ group_id: groupId, priceable_id: priceableId, priceable_type: priceableType })
|
||||
.then(data => setExtendedPrices(data))
|
||||
.catch(error => onError(error));
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the button used to trigger the "new pack" modal
|
||||
*/
|
||||
const renderAddButton = (): ReactNode => {
|
||||
return <CreateExtendedPrice onSuccess={handleSuccess}
|
||||
onError={onError}
|
||||
groupId={groupId}
|
||||
priceableId={priceableId}
|
||||
priceableType={priceableType} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="configure-packs-button">
|
||||
<button className="packs-button" onClick={toggleShowList}>
|
||||
<i className="fas fa-box" />
|
||||
</button>
|
||||
{showList && <FabPopover title={t('app.admin.configure_extendedPrices_button.extendedPrices')} headerButton={renderAddButton()} className="fab-popover__right">
|
||||
<ul>
|
||||
{extendedPrices?.map(extendedPrice =>
|
||||
<li key={extendedPrice.id}>
|
||||
{extendedPrice.duration} {t('app.admin.calendar.minutes')} - {FormatLib.price(extendedPrice.amount)}
|
||||
<span className="pack-actions">
|
||||
<EditExtendedPrice onSuccess={handleSuccess} onError={onError} price={extendedPrice} />
|
||||
<DeleteExtendedPrice onSuccess={handleSuccess} onError={onError} price={extendedPrice} />
|
||||
</span>
|
||||
</li>)}
|
||||
</ul>
|
||||
{extendedPrices?.length === 0 && <span>{t('app.admin.configure_extendedPrices_button.no_extendedPrices')}</span>}
|
||||
</FabPopover>}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,69 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { ExtendedPriceForm } from './extended-price-form';
|
||||
import { Price } from '../../models/price';
|
||||
import PriceAPI from '../../api/price';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabAlert } from '../base/fab-alert';
|
||||
|
||||
interface CreateExtendedPriceProps {
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
groupId: number,
|
||||
priceableId: number,
|
||||
priceableType: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a button.
|
||||
* When clicked, we show a modal dialog handing the process of creating a new extended price
|
||||
*/
|
||||
export const CreateExtendedPrice: React.FC<CreateExtendedPriceProps> = ({ onSuccess, onError, groupId, priceableId, priceableType }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* Open/closes the "new pack" modal dialog
|
||||
*/
|
||||
const toggleModal = (): void => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user has validated the creation of the new extended price
|
||||
*/
|
||||
const handleSubmit = (extendedPrice: Price): void => {
|
||||
// set the already-known attributes of the new pack
|
||||
const newExtendedPrice = Object.assign<Price, Price>({} as Price, extendedPrice);
|
||||
newExtendedPrice.group_id = groupId;
|
||||
newExtendedPrice.priceable_id = priceableId;
|
||||
newExtendedPrice.priceable_type = priceableType;
|
||||
|
||||
// create it on the API
|
||||
PriceAPI.create(newExtendedPrice)
|
||||
.then(() => {
|
||||
onSuccess(t('app.admin.create_extendedPrice.extendedPrice_successfully_created'));
|
||||
toggleModal();
|
||||
})
|
||||
.catch(error => onError(error));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="create-pack">
|
||||
<button className="add-pack-button" onClick={toggleModal}><i className="fas fa-plus"/></button>
|
||||
<FabModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
title={t('app.admin.create_extendedPrice.new_extendedPrice')}
|
||||
className="new-pack-modal"
|
||||
closeButton
|
||||
confirmButton={t('app.admin.create_extendedPrice.create_extendedPrice')}
|
||||
onConfirmSendFormId="new-pack">
|
||||
<FabAlert level="info">
|
||||
{t('app.admin.create_extendedPrice.new_extendedPrice_info', { TYPE: priceableType })}
|
||||
</FabAlert>
|
||||
<ExtendedPriceForm formId="new-pack" onSubmit={handleSubmit} />
|
||||
</FabModal>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,56 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { Price } from '../../models/price';
|
||||
import PriceAPI from '../../api/price';
|
||||
|
||||
interface DeleteExtendedPriceProps {
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
price: Price,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a button.
|
||||
* When clicked, we show a modal dialog to ask the user for confirmation about the deletion of the provided extended price.
|
||||
*/
|
||||
export const DeleteExtendedPrice: React.FC<DeleteExtendedPriceProps> = ({ onSuccess, onError, price }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [deletionModal, setDeletionModal] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* Opens/closes the deletion modal
|
||||
*/
|
||||
const toggleDeletionModal = (): void => {
|
||||
setDeletionModal(!deletionModal);
|
||||
};
|
||||
|
||||
/**
|
||||
* The deletion has been confirmed by the user.
|
||||
* Call the API to trigger the deletion of the temporary set extended price
|
||||
*/
|
||||
const onDeleteConfirmed = (): void => {
|
||||
PriceAPI.destroy(price.id).then(() => {
|
||||
onSuccess(t('app.admin.delete_extendedPrice.extendedPrice_deleted'));
|
||||
}).catch((error) => {
|
||||
onError(t('app.admin.delete_extendedPrice.unable_to_delete') + error);
|
||||
});
|
||||
toggleDeletionModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="delete-pack">
|
||||
<FabButton type='button' className="remove-pack-button" icon={<i className="fa fa-trash" />} onClick={toggleDeletionModal} />
|
||||
<FabModal title={t('app.admin.delete_extendedPrice.delete_extendedPrice')}
|
||||
isOpen={deletionModal}
|
||||
toggleModal={toggleDeletionModal}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.admin.delete_extendedPrice.confirm_delete')}
|
||||
onConfirm={onDeleteConfirmed}>
|
||||
<span>{t('app.admin.delete_extendedPrice.delete_confirmation')}</span>
|
||||
</FabModal>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,66 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { ExtendedPriceForm } from './extended-price-form';
|
||||
import { Price } from '../../models/price';
|
||||
import PriceAPI from '../../api/price';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
|
||||
interface EditExtendedPriceProps {
|
||||
price: Price,
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a button.
|
||||
* When clicked, we show a modal dialog handing the process of creating a new extended price
|
||||
*/
|
||||
export const EditExtendedPrice: React.FC<EditExtendedPriceProps> = ({ price, onSuccess, onError }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [extendedPriceData, setExtendedPriceData] = useState<Price>(price);
|
||||
|
||||
/**
|
||||
* Open/closes the "edit extended price" modal dialog
|
||||
*/
|
||||
const toggleModal = (): void => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
/**
|
||||
* When the user clicks on the edition button open te edition modal
|
||||
*/
|
||||
const handleRequestEdit = (): void => {
|
||||
toggleModal();
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user has validated the changes of the extended price
|
||||
*/
|
||||
const handleUpdate = (price: Price): void => {
|
||||
PriceAPI.update(price)
|
||||
.then(() => {
|
||||
onSuccess(t('app.admin.edit_extendedPrice.extendedPrice_successfully_updated'));
|
||||
setExtendedPriceData(price);
|
||||
toggleModal();
|
||||
})
|
||||
.catch(error => onError(error));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="edit-pack">
|
||||
<FabButton type='button' className="edit-pack-button" icon={<i className="fas fa-edit" />} onClick={handleRequestEdit} />
|
||||
<FabModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
title={t('app.admin.edit_extendedPrice.edit_extendedPrice')}
|
||||
className="edit-pack-modal"
|
||||
closeButton
|
||||
confirmButton={t('app.admin.edit_extendedPrice.confirm_changes')}
|
||||
onConfirmSendFormId="edit-pack">
|
||||
{extendedPriceData && <ExtendedPriceForm formId="edit-pack" onSubmit={handleUpdate} price={extendedPriceData} />}
|
||||
</FabModal>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,74 @@
|
||||
import React, { BaseSyntheticEvent } from 'react';
|
||||
import { Price } from '../../models/price';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useImmer } from 'use-immer';
|
||||
import { FabInput } from '../base/fab-input';
|
||||
import { IFablab } from '../../models/fablab';
|
||||
|
||||
declare let Fablab: IFablab;
|
||||
|
||||
interface PackFormProps {
|
||||
formId: string,
|
||||
onSubmit: (pack: Price) => void,
|
||||
price?: Price,
|
||||
}
|
||||
|
||||
/**
|
||||
* A form component to create/edit a extended price.
|
||||
* The form validation must be created elsewhere, using the attribute form={formId}.
|
||||
*/
|
||||
export const ExtendedPriceForm: React.FC<PackFormProps> = ({ formId, onSubmit, price }) => {
|
||||
const [extendedPriceData, updateExtendedPriceData] = useImmer<Price>(price || {} as Price);
|
||||
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
/**
|
||||
* Callback triggered when the user sends the form.
|
||||
*/
|
||||
const handleSubmit = (event: BaseSyntheticEvent): void => {
|
||||
event.preventDefault();
|
||||
onSubmit(extendedPriceData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user inputs an amount for the current extended price.
|
||||
*/
|
||||
const handleUpdateAmount = (amount: string) => {
|
||||
updateExtendedPriceData(draft => {
|
||||
draft.amount = parseFloat(amount);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user inputs a number of minutes for the current extended price.
|
||||
*/
|
||||
const handleUpdateHours = (minutes: string) => {
|
||||
updateExtendedPriceData(draft => {
|
||||
draft.duration = parseInt(minutes, 10);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form id={formId} onSubmit={handleSubmit} className="pack-form">
|
||||
<label htmlFor="duration">{t('app.admin.calendar.minutes')} *</label>
|
||||
<FabInput id="duration"
|
||||
type="number"
|
||||
defaultValue={extendedPriceData?.duration || ''}
|
||||
onChange={handleUpdateHours}
|
||||
step={1}
|
||||
min={1}
|
||||
icon={<i className="fas fa-clock" />}
|
||||
required />
|
||||
<label htmlFor="amount">{t('app.admin.pack_form.amount')} *</label>
|
||||
<FabInput id="amount"
|
||||
type="number"
|
||||
step={0.01}
|
||||
min={0}
|
||||
defaultValue={extendedPriceData?.amount || ''}
|
||||
onChange={handleUpdateAmount}
|
||||
icon={<i className="fas fa-money-bill" />}
|
||||
addOn={Fablab.intl_currency}
|
||||
required />
|
||||
</form>
|
||||
);
|
||||
};
|
@ -0,0 +1,134 @@
|
||||
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 SpaceAPI from '../../api/space';
|
||||
import GroupAPI from '../../api/group';
|
||||
import { Group } from '../../models/group';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { EditablePrice } from './editable-price';
|
||||
import { ConfigureExtendedPriceButton } from './configure-extended-price-button';
|
||||
import PriceAPI from '../../api/price';
|
||||
import { Price } from '../../models/price';
|
||||
import { useImmer } from 'use-immer';
|
||||
import FormatLib from '../../lib/format';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface SpacesPricingProps {
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to set and edit the prices of spaces-hours, per group
|
||||
*/
|
||||
const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [spaces, setSpaces] = useState<Array<any>>(null);
|
||||
const [groups, setGroups] = useState<Array<Group>>(null);
|
||||
const [prices, updatePrices] = useImmer<Array<Price>>(null);
|
||||
|
||||
// retrieve the initial data
|
||||
useEffect(() => {
|
||||
SpaceAPI.index(false)
|
||||
.then(data => setSpaces(data))
|
||||
.catch(error => onError(error));
|
||||
GroupAPI.index({ disabled: false, admins: false })
|
||||
.then(data => setGroups(data))
|
||||
.catch(error => onError(error));
|
||||
PriceAPI.index({ priceable_type: 'Space', plan_id: null })
|
||||
.then(data => updatePrices(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 FormatLib.price(hourlyRate);
|
||||
}
|
||||
|
||||
const price = (hourlyRate / 60) * EXEMPLE_DURATION;
|
||||
return FormatLib.price(price);
|
||||
};
|
||||
|
||||
const findPricesBy = (spaceId, groupId): Array<Price> => {
|
||||
return prices.filter(price => price.priceable_id === spaceId && price.group_id === groupId);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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'));
|
||||
updatePrice(price);
|
||||
})
|
||||
.catch(error => onError(error));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="machines-pricing">
|
||||
<FabAlert level="warning">
|
||||
<p><HtmlTranslate trKey="app.admin.pricing.these_prices_match_space_hours_rates_html"/></p>
|
||||
<p><HtmlTranslate trKey="app.admin.pricing.prices_calculated_on_hourly_rate_html" options={{ DURATION: `${EXEMPLE_DURATION}`, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }} /></p>
|
||||
<p>{t('app.admin.pricing.you_can_override')}</p>
|
||||
</FabAlert>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('app.admin.pricing.spaces')}</th>
|
||||
{groups?.map(group => <th key={group.id} className="group-name">{group.name}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{spaces?.map(space => <tr key={space.id}>
|
||||
<td>{space.name}</td>
|
||||
{groups?.map(group => <td key={group.id}>
|
||||
{prices && <EditablePrice price={findPricesBy(space.id, group.id)[0]} onSave={handleUpdatePrice} />}
|
||||
<ConfigureExtendedPriceButton
|
||||
prices={findPricesBy(space.id, group.id)}
|
||||
onError={onError}
|
||||
onSuccess={onSuccess}
|
||||
groupId={group.id}
|
||||
priceableId={space.id}
|
||||
priceableType='Space' />
|
||||
</td>)}
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SpacesPricingWrapper: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<SpacesPricing onError={onError} onSuccess={onSuccess} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('spacesPricing', react2angular(SpacesPricingWrapper, ['onError', 'onSuccess']));
|
@ -1,29 +1 @@
|
||||
<div class="alert alert-warning m-t">
|
||||
<p ng-bind-html="'app.admin.pricing.these_prices_match_space_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.spaces' }}</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="space in enabledSpaces">
|
||||
<td>
|
||||
{{ space.name }}
|
||||
</td>
|
||||
<td ng-repeat="group in enabledGroups">
|
||||
<span editable-number="findPriceBy(spacesPrices, space.id, group.id).amount"
|
||||
e-step="any"
|
||||
onbeforesave="updatePrice($data, findPriceBy(spacesPrices, space.id, group.id))">
|
||||
{{ findPriceBy(spacesPrices, space.id, group.id).amount | currency}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<spaces-pricing on-success="onSuccess" on-error="onError">
|
||||
|
@ -7,5 +7,5 @@ class Price < ApplicationRecord
|
||||
belongs_to :priceable, polymorphic: true
|
||||
|
||||
validates :priceable, :group_id, :amount, presence: true
|
||||
validates :priceable_id, uniqueness: { scope: %i[priceable_type plan_id group_id] }
|
||||
validates :priceable_id, uniqueness: { scope: %i[priceable_type plan_id group_id duration] }
|
||||
end
|
||||
|
@ -6,6 +6,10 @@ class PricePolicy < ApplicationPolicy
|
||||
user.admin? && record.duration != 60
|
||||
end
|
||||
|
||||
def destroy?
|
||||
user.admin? && record.duration != 60
|
||||
end
|
||||
|
||||
def update?
|
||||
user.admin?
|
||||
end
|
||||
|
@ -378,6 +378,9 @@ en:
|
||||
packs: "Prepaid packs"
|
||||
no_packs: "No packs for now"
|
||||
pack_DURATION: "{DURATION} hours"
|
||||
configure_extendedPrices_button:
|
||||
extendedPrices: "Extended prices"
|
||||
no_extendedPrices: "No extended price for now"
|
||||
pack_form:
|
||||
hours: "Hours"
|
||||
amount: "Price"
|
||||
@ -404,6 +407,21 @@ en:
|
||||
edit_pack: "Edit the pack"
|
||||
confirm_changes: "Confirm changes"
|
||||
pack_successfully_updated: "The prepaid pack was successfully updated."
|
||||
create_extendedPrice:
|
||||
new_extendedPrice: "New extended price"
|
||||
new_extendedPrice_info: "..."
|
||||
create_extendedPrice: "Create extended price"
|
||||
extendedPrice_successfully_created: "The new extended price was successfully created."
|
||||
delete_extendedPrice:
|
||||
extendedPrice_deleted: "The extended price was successfully deleted."
|
||||
unable_to_delete: "Unable to delete the extended price: "
|
||||
delete_extendedPrice: "Delete the extended price"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this extended price? This won't be possible if it was already bought by users."
|
||||
edit_extendedPrice:
|
||||
edit_extendedPrice: "Edit the extended price"
|
||||
confirm_changes: "Confirm changes"
|
||||
extendedPrice_successfully_updated: "The extended price was successfully updated."
|
||||
#ajouter un code promotionnel
|
||||
coupons_new:
|
||||
add_a_coupon: "Add a coupon"
|
||||
|
@ -75,7 +75,7 @@ Rails.application.routes.draw do
|
||||
get 'pricing' => 'pricing#index'
|
||||
put 'pricing' => 'pricing#update'
|
||||
|
||||
resources :prices, only: %i[create index update] do
|
||||
resources :prices, only: %i[create index update destroy] do
|
||||
post 'compute', on: :collection
|
||||
end
|
||||
resources :prepaid_packs
|
||||
|
Loading…
x
Reference in New Issue
Block a user