mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
Merge branch 'dev' for release 5.9.0
This commit is contained in:
commit
bf40eae950
14
CHANGELOG.md
14
CHANGELOG.md
@ -1,5 +1,19 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
## v5.9.0 2023 March 20
|
||||
|
||||
- Ability to restrict machine reservations per plan
|
||||
- Ability to restrict machine availabilities per plan
|
||||
- Ability to configure a prior period for each reservation type to prevent booking (#440)
|
||||
- Admins cannot select the date when creating a refund invoice anymore
|
||||
- Fix a bug: JS date is initalialized 1 day before in negative timezones (#445)
|
||||
- Fix a bug: user's profile field gender is now marked as required
|
||||
- Fix a bug: logical sequence of invoices references is broken, when using the store module or the payments schedules
|
||||
- Fix a bug: refund invoices may generate duplicates in invoices references
|
||||
- Fix a security issue: updated webpack to 5.76.0 to fix [CVE-2023-28154](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28154)
|
||||
- [TODO DEPLOY] `rails db:seed`
|
||||
- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet`
|
||||
|
||||
## v5.8.2 2023 March 13
|
||||
|
||||
- Improved upgrade script
|
||||
|
@ -63,7 +63,7 @@ class API::InvoicesController < API::ApiController
|
||||
private
|
||||
|
||||
def avoir_params
|
||||
params.require(:avoir).permit(:invoice_id, :avoir_date, :payment_method, :subscription_to_expire, :description,
|
||||
params.require(:avoir).permit(:invoice_id, :payment_method, :subscription_to_expire, :description,
|
||||
invoice_items_ids: [])
|
||||
end
|
||||
|
||||
|
@ -8,6 +8,8 @@ class API::LocalPaymentController < API::PaymentsController
|
||||
|
||||
authorize LocalPaymentContext.new(cart, price[:amount])
|
||||
|
||||
render json: cart.errors, status: :unprocessable_entity and return unless cart.valid?
|
||||
|
||||
render on_payment_success(nil, nil, cart)
|
||||
end
|
||||
|
||||
|
@ -55,7 +55,7 @@ class API::PayzenController < API::PaymentsController
|
||||
|
||||
def check_cart
|
||||
cart = shopping_cart
|
||||
render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid?
|
||||
render json: cart.errors, status: :unprocessable_entity and return unless cart.valid?
|
||||
|
||||
render json: { cart: 'ok' }, status: :ok
|
||||
end
|
||||
|
@ -80,11 +80,13 @@ class API::PlansController < API::ApiController
|
||||
end
|
||||
|
||||
@parameters = @parameters.require(:plan)
|
||||
.permit(:base_name, :type, :group_id, :amount, :interval, :interval_count, :is_rolling,
|
||||
.permit(:base_name, :type, :group_id, :amount, :interval, :interval_count, :is_rolling, :limiting,
|
||||
:training_credit_nb, :ui_weight, :disabled, :monthly_payment, :description, :plan_category_id,
|
||||
:machines_visibility,
|
||||
plan_file_attributes: %i[id attachment _destroy],
|
||||
prices_attributes: %i[id amount],
|
||||
advanced_accounting_attributes: %i[code analytical_section])
|
||||
advanced_accounting_attributes: %i[code analytical_section],
|
||||
plan_limitations_attributes: %i[id limitable_id limitable_type limit _destroy])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -46,6 +46,9 @@ class API::PricesController < API::ApiController
|
||||
cs = CartService.new(current_user)
|
||||
cart = cs.from_hash(params)
|
||||
@amount = cart.total
|
||||
# TODO, remove this when the cart is refactored
|
||||
cart.valid?
|
||||
@errors = cart.errors
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -19,7 +19,7 @@ class API::StripeController < API::PaymentsController
|
||||
res = nil # json of the API answer
|
||||
|
||||
cart = shopping_cart
|
||||
render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid?
|
||||
render json: cart.errors, status: :unprocessable_entity and return unless cart.valid?
|
||||
|
||||
begin
|
||||
amount = debit_amount(cart) # will contains the amount and the details of each invoice lines
|
||||
@ -73,7 +73,7 @@ class API::StripeController < API::PaymentsController
|
||||
|
||||
def setup_subscription
|
||||
cart = shopping_cart
|
||||
render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid?
|
||||
render json: cart.errors, status: :unprocessable_entity and return unless cart.valid?
|
||||
|
||||
service = Stripe::Service.new
|
||||
method = service.attach_method_as_default(
|
||||
|
@ -25,7 +25,7 @@ class API::WalletController < API::ApiController
|
||||
service = WalletService.new(user: current_user, wallet: @wallet)
|
||||
transaction = service.credit(credit_params[:amount].to_f)
|
||||
if transaction
|
||||
service.create_avoir(transaction, credit_params[:avoir_date], credit_params[:avoir_description]) if credit_params[:avoir]
|
||||
service.create_avoir(transaction, credit_params[:avoir_description]) if credit_params[:avoir]
|
||||
render :show
|
||||
else
|
||||
head :unprocessable_entity
|
||||
@ -35,6 +35,6 @@ class API::WalletController < API::ApiController
|
||||
private
|
||||
|
||||
def credit_params
|
||||
params.permit(:id, :amount, :avoir, :avoir_date, :avoir_description)
|
||||
params.permit(:id, :amount, :avoir, :avoir_description)
|
||||
end
|
||||
end
|
||||
|
@ -5,24 +5,34 @@ import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from './fab-button';
|
||||
import { FabModal } from './fab-modal';
|
||||
|
||||
interface EditDestroyButtonsProps {
|
||||
onDeleteSuccess: (message: string) => void,
|
||||
type EditDestroyButtonsCommon = {
|
||||
onError: (message: string) => void,
|
||||
onEdit: () => void,
|
||||
itemId: number,
|
||||
itemType: string,
|
||||
apiDestroy: (itemId: number) => Promise<void>,
|
||||
confirmationMessage?: string|ReactNode,
|
||||
destroy: (itemId: number) => Promise<void>,
|
||||
className?: string,
|
||||
iconSize?: number
|
||||
iconSize?: number,
|
||||
showEditButton?: boolean,
|
||||
}
|
||||
|
||||
type DeleteSuccess =
|
||||
{ onDeleteSuccess: (message: string) => void, deleteSuccessMessage: string } |
|
||||
{ onDeleteSuccess?: never, deleteSuccessMessage?: never }
|
||||
|
||||
type DestroyMessages =
|
||||
({ showDestroyConfirmation?: true } &
|
||||
({ itemType: string, confirmationTitle?: string, confirmationMessage?: string|ReactNode } |
|
||||
{ itemType?: never, confirmationTitle: string, confirmationMessage: string|ReactNode })) |
|
||||
{ showDestroyConfirmation: false, itemType?: never, confirmationTitle?: never, confirmationMessage?: never };
|
||||
|
||||
type EditDestroyButtonsProps = EditDestroyButtonsCommon & DeleteSuccess & DestroyMessages;
|
||||
|
||||
/**
|
||||
* This component shows a group of two buttons.
|
||||
* Destroy : shows a modal dialog to ask the user for confirmation about the deletion of the provided item.
|
||||
* Edit : triggers the provided function.
|
||||
*/
|
||||
export const EditDestroyButtons: React.FC<EditDestroyButtonsProps> = ({ onDeleteSuccess, onError, onEdit, itemId, itemType, apiDestroy, confirmationMessage, className, iconSize = 20 }) => {
|
||||
export const EditDestroyButtons: React.FC<EditDestroyButtonsProps> = ({ onDeleteSuccess, onError, onEdit, itemId, itemType, destroy, confirmationTitle, confirmationMessage, deleteSuccessMessage, className, iconSize = 20, showEditButton = true, showDestroyConfirmation = true }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [deletionModal, setDeletionModal] = useState<boolean>(false);
|
||||
@ -34,30 +44,41 @@ export const EditDestroyButtons: React.FC<EditDestroyButtonsProps> = ({ onDelete
|
||||
setDeletionModal(!deletionModal);
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on the 'destroy' button
|
||||
*/
|
||||
const handleDestroyRequest = (): void => {
|
||||
if (showDestroyConfirmation) {
|
||||
toggleDeletionModal();
|
||||
} else {
|
||||
onDeleteConfirmed();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The deletion has been confirmed by the user.
|
||||
* Call the API to trigger the deletion of the given item
|
||||
*/
|
||||
const onDeleteConfirmed = (): void => {
|
||||
apiDestroy(itemId).then(() => {
|
||||
onDeleteSuccess(t('app.admin.edit_destroy_buttons.deleted', { TYPE: itemType }));
|
||||
destroy(itemId).then(() => {
|
||||
typeof onDeleteSuccess === 'function' && onDeleteSuccess(deleteSuccessMessage || t('app.admin.edit_destroy_buttons.deleted'));
|
||||
}).catch((error) => {
|
||||
onError(t('app.admin.edit_destroy_buttons.unable_to_delete', { TYPE: itemType }) + error);
|
||||
onError(t('app.admin.edit_destroy_buttons.unable_to_delete') + error);
|
||||
});
|
||||
toggleDeletionModal();
|
||||
setDeletionModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`edit-destroy-buttons ${className || ''}`}>
|
||||
<FabButton className='edit-btn' onClick={onEdit}>
|
||||
{showEditButton && <FabButton className='edit-btn' onClick={onEdit}>
|
||||
<PencilSimple size={iconSize} weight="fill" />
|
||||
</FabButton>
|
||||
<FabButton type='button' className='delete-btn' onClick={toggleDeletionModal}>
|
||||
</FabButton>}
|
||||
<FabButton type='button' className='delete-btn' onClick={handleDestroyRequest}>
|
||||
<Trash size={iconSize} weight="fill" />
|
||||
</FabButton>
|
||||
</div>
|
||||
<FabModal title={t('app.admin.edit_destroy_buttons.delete_item', { TYPE: itemType })}
|
||||
<FabModal title={confirmationTitle || t('app.admin.edit_destroy_buttons.delete_item', { TYPE: itemType })}
|
||||
isOpen={deletionModal}
|
||||
toggleModal={toggleDeletionModal}
|
||||
closeButton={true}
|
||||
|
@ -65,7 +65,7 @@ export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, re
|
||||
return num;
|
||||
}
|
||||
if (type === 'date') {
|
||||
const date: Date = new Date(value);
|
||||
const date: Date = new Date(value + 'T00:00:00');
|
||||
if (Number.isNaN(date) && nullable) {
|
||||
return null;
|
||||
}
|
||||
|
@ -58,10 +58,10 @@ export const FormSelect = <TFieldValues extends FieldValues, TContext extends ob
|
||||
<AbstractSelect ref={ref}
|
||||
classNamePrefix="rs"
|
||||
className="rs"
|
||||
value={options.find(c => c.value === value)}
|
||||
value={value === null ? null : options.find(c => c.value === value)}
|
||||
onChange={val => {
|
||||
onChangeCb(val.value);
|
||||
onChange(val.value);
|
||||
onChangeCb(val?.value);
|
||||
onChange(val?.value);
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
isDisabled={isDisabled}
|
||||
|
@ -0,0 +1,74 @@
|
||||
import { FieldArrayWithId } from 'react-hook-form/dist/types/fieldArray';
|
||||
import { UseFormRegister } from 'react-hook-form';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { X } from 'phosphor-react';
|
||||
import { FormInput } from './form-input';
|
||||
import { FieldArrayPath } from 'react-hook-form/dist/types/path';
|
||||
|
||||
interface FormUnsavedListProps<TFieldValues, TFieldArrayName extends FieldArrayPath<TFieldValues>, TKeyName extends string> {
|
||||
fields: Array<FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>>,
|
||||
onRemove: (index: number) => void,
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
className?: string,
|
||||
title: string,
|
||||
shouldRenderField?: (field: FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>) => boolean,
|
||||
renderField: (field: FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>) => ReactNode,
|
||||
formAttributeName: `${string}_attributes`,
|
||||
formAttributes: Array<keyof FieldArrayWithId<TFieldValues, TFieldArrayName>>,
|
||||
saveReminderLabel?: string | ReactNode,
|
||||
cancelLabel?: string | ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* This component render a list of unsaved attributes, created elsewhere than in the form (e.g. in a modal dialog)
|
||||
* and pending for the form to be saved.
|
||||
*
|
||||
* The `renderField` attribute should return a JSX element composed like the following example:
|
||||
* ```
|
||||
* <> <!-- empty tag -->
|
||||
* <div className="group"> <!-- the group class is important -->
|
||||
* <span>Attribute 1</span> <!-- a span tag for the title -->
|
||||
* <p>{item.attr1}</p> <!-- a paragraph tag for the value -->
|
||||
* </div>
|
||||
* <div className="group">
|
||||
* ...
|
||||
* </div>
|
||||
* </>
|
||||
* ```
|
||||
*/
|
||||
export const FormUnsavedList = <TFieldValues extends FieldValues = FieldValues, TFieldArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>, TKeyName extends string = 'id'>({ fields, onRemove, register, className, title, shouldRenderField = () => true, renderField, formAttributeName, formAttributes, saveReminderLabel, cancelLabel }: FormUnsavedListProps<TFieldValues, TFieldArrayName, TKeyName>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
/**
|
||||
* Render an unsaved field
|
||||
*/
|
||||
const renderUnsavedField = (field: FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>, index: number): ReactNode => {
|
||||
return (
|
||||
<div key={index} className="unsaved-field">
|
||||
{renderField(field)}
|
||||
<p className="cancel-action" onClick={() => onRemove(index)}>
|
||||
{cancelLabel || t('app.shared.form_unsaved_list.cancel')}
|
||||
<X size={20} />
|
||||
</p>
|
||||
{formAttributes.map((attribute, attrIndex) => (
|
||||
<FormInput key={attrIndex} id={`${formAttributeName}.${index}.${attribute}`} register={register} type="hidden" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (fields.filter(shouldRenderField).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={`form-unsaved-list ${className || ''}`}>
|
||||
<span className="title">{title}</span>
|
||||
<span className="save-notice">{saveReminderLabel || t('app.shared.form_unsaved_list.save_reminder')}</span>
|
||||
{fields.map((field, index) => {
|
||||
if (!shouldRenderField(field)) return false;
|
||||
return renderUnsavedField(field, index);
|
||||
}).filter(Boolean)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -124,7 +124,7 @@ export const MachineCategoriesList: React.FC<MachineCategoriesListProps> = ({ on
|
||||
onEdit={editMachineCategory(category)}
|
||||
itemId={category.id}
|
||||
itemType={t('app.admin.machine_categories_list.machine_category')}
|
||||
apiDestroy={MachineCategoryAPI.destroy} />
|
||||
destroy={MachineCategoryAPI.destroy} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -24,6 +24,10 @@ import { UserPlus } from 'phosphor-react';
|
||||
import { PartnerModal } from './partner-modal';
|
||||
import { PlanPricingForm } from './plan-pricing-form';
|
||||
import { AdvancedAccountingForm } from '../accounting/advanced-accounting-form';
|
||||
import { FabTabs } from '../base/fab-tabs';
|
||||
import { PlanLimitForm } from './plan-limit-form';
|
||||
import { UnsavedFormAlert } from '../form/unsaved-form-alert';
|
||||
import { UIRouter } from '@uirouter/angularjs';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -33,13 +37,14 @@ interface PlanFormProps {
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void,
|
||||
beforeSubmit?: (data: Plan) => void,
|
||||
uiRouter: UIRouter
|
||||
}
|
||||
|
||||
/**
|
||||
* Form to edit or create subscription plans
|
||||
*/
|
||||
export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuccess, beforeSubmit }) => {
|
||||
const { handleSubmit, register, control, formState, setValue } = useForm<Plan>({ defaultValues: { ...plan } });
|
||||
export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuccess, beforeSubmit, uiRouter }) => {
|
||||
const { handleSubmit, register, control, formState, setValue, getValues, resetField } = useForm<Plan>({ defaultValues: { ...plan } });
|
||||
const output = useWatch<Plan>({ control }); // eslint-disable-line
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
@ -51,13 +56,19 @@ export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuc
|
||||
|
||||
useEffect(() => {
|
||||
GroupAPI.index({ disabled: false })
|
||||
.then(res => setGroups(res.map(g => { return { value: g.id, label: g.name }; })))
|
||||
.then(res => setGroups(res.map(g => {
|
||||
return { value: g.id, label: g.name };
|
||||
})))
|
||||
.catch(onError);
|
||||
PlanCategoryAPI.index()
|
||||
.then(res => setCategories(res.map(c => { return { value: c.id, label: c.name }; })))
|
||||
.then(res => setCategories(res.map(c => {
|
||||
return { value: c.id, label: c.name };
|
||||
})))
|
||||
.catch(onError);
|
||||
UserAPI.index({ role: 'partner' })
|
||||
.then(res => setPartners(res.map(p => { return { value: p.id, label: p.name }; })))
|
||||
.then(res => setPartners(res.map(p => {
|
||||
return { value: p.id, label: p.name };
|
||||
})))
|
||||
.catch(onError);
|
||||
}, []);
|
||||
|
||||
@ -101,7 +112,9 @@ export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuc
|
||||
* Return the available options for the plan period
|
||||
*/
|
||||
const buildPeriodsOptions = (): Array<SelectOption<string>> => {
|
||||
return ['week', 'month', 'year'].map(d => { return { value: d, label: t(`app.admin.plan_form.${d}`) }; });
|
||||
return ['week', 'month', 'year'].map(d => {
|
||||
return { value: d, label: t(`app.admin.plan_form.${d}`) };
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -130,140 +143,226 @@ export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuc
|
||||
setValue('partner_id', user.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="plan-form">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<h4>{t('app.admin.plan_form.general_information')}</h4>
|
||||
<FormInput register={register}
|
||||
id="base_name"
|
||||
formState={formState}
|
||||
rules={{
|
||||
required: true,
|
||||
maxLength: { value: 24, message: t('app.admin.plan_form.name_max_length') }
|
||||
}}
|
||||
label={t('app.admin.plan_form.name')} />
|
||||
{action === 'create' && <FormSwitch control={control}
|
||||
formState={formState}
|
||||
onChange={handleAllGroupsChange}
|
||||
defaultValue={false}
|
||||
label={t('app.admin.plan_form.transversal')}
|
||||
tooltip={t('app.admin.plan_form.transversal_help')}
|
||||
id="all_groups" />}
|
||||
{!allGroups && groups && <FormSelect options={groups}
|
||||
formState={formState}
|
||||
control={control}
|
||||
rules={{ required: !allGroups }}
|
||||
disabled={action === 'update'}
|
||||
label={t('app.admin.plan_form.group')}
|
||||
id="group_id" />}
|
||||
{categories?.length > 0 && <FormSelect options={categories}
|
||||
formState={formState}
|
||||
control={control}
|
||||
id="plan_category_id"
|
||||
tooltip={t('app.admin.plan_form.category_help')}
|
||||
label={t('app.admin.plan_form.category')} />}
|
||||
{action === 'update' && <FabAlert level="warning">
|
||||
{t('app.admin.plan_form.edit_amount_info')}
|
||||
</FabAlert>}
|
||||
<FormInput register={register}
|
||||
formState={formState}
|
||||
id="amount"
|
||||
type="number"
|
||||
step={0.01}
|
||||
addOn={FormatLib.currencySymbol()}
|
||||
rules={{ required: true, min: 0 }}
|
||||
label={t('app.admin.plan_form.subscription_price')} />
|
||||
<FormInput register={register}
|
||||
formState={formState}
|
||||
id="ui_weight"
|
||||
type="number"
|
||||
label={t('app.admin.plan_form.visual_prominence')}
|
||||
tooltip={t('app.admin.plan_form.visual_prominence_help')} />
|
||||
<FormSwitch control={control}
|
||||
formState={formState}
|
||||
id="is_rolling"
|
||||
label={t('app.admin.plan_form.rolling_subscription')}
|
||||
disabled={action === 'update'}
|
||||
tooltip={t('app.admin.plan_form.rolling_subscription_help')} />
|
||||
<FormSwitch control={control}
|
||||
formState={formState}
|
||||
id="monthly_payment"
|
||||
label={t('app.admin.plan_form.monthly_payment')}
|
||||
disabled={action === 'update' || output.interval === 'week'}
|
||||
tooltip={t('app.admin.plan_form.monthly_payment_help')} />
|
||||
<FormRichText control={control}
|
||||
formState={formState}
|
||||
id="description"
|
||||
label={t('app.admin.plan_form.description')}
|
||||
limit={200}
|
||||
heading link blockquote />
|
||||
<FormFileUpload setValue={setValue}
|
||||
register={register}
|
||||
formState={formState}
|
||||
defaultFile={output.plan_file_attributes}
|
||||
id="plan_file_attributes"
|
||||
className="plan-sheet"
|
||||
label={t('app.admin.plan_form.information_sheet')} />
|
||||
<FormSwitch control={control}
|
||||
formState={formState}
|
||||
id="disabled"
|
||||
defaultValue={false}
|
||||
label={t('app.admin.plan_form.disabled')}
|
||||
tooltip={t('app.admin.plan_form.disabled_help')} />
|
||||
<h4>{t('app.admin.plan_form.duration')}</h4>
|
||||
<div className="duration">
|
||||
/**
|
||||
* Render the content of the 'subscriptions settings' tab
|
||||
*/
|
||||
const renderSettingsTab = () => (
|
||||
<div className="plan-form-content">
|
||||
<section>
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_form.description')}</p>
|
||||
</header>
|
||||
<div className="content">
|
||||
<FormInput register={register}
|
||||
rules={{ required: true, min: 1 }}
|
||||
disabled={action === 'update'}
|
||||
id="base_name"
|
||||
formState={formState}
|
||||
label={t('app.admin.plan_form.number_of_periods')}
|
||||
type="number"
|
||||
id="interval_count" />
|
||||
<FormSelect options={buildPeriodsOptions()}
|
||||
control={control}
|
||||
disabled={action === 'update'}
|
||||
onChange={handlePeriodUpdate}
|
||||
id="interval"
|
||||
label={t('app.admin.plan_form.period')}
|
||||
formState={formState}
|
||||
rules={{ required: true }} />
|
||||
rules={{
|
||||
required: true,
|
||||
maxLength: { value: 24, message: t('app.admin.plan_form.name_max_length') }
|
||||
}}
|
||||
label={t('app.admin.plan_form.name')} />
|
||||
<FormRichText control={control}
|
||||
formState={formState}
|
||||
id="description"
|
||||
label={t('app.admin.plan_form.description')}
|
||||
limit={200}
|
||||
heading link blockquote />
|
||||
<FormFileUpload setValue={setValue}
|
||||
register={register}
|
||||
formState={formState}
|
||||
defaultFile={output.plan_file_attributes}
|
||||
id="plan_file_attributes"
|
||||
className="plan-sheet"
|
||||
label={t('app.admin.plan_form.information_sheet')} />
|
||||
</div>
|
||||
<h4>{t('app.admin.plan_form.partnership')}</h4>
|
||||
<div className="partnership">
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_form.general_settings')}</p>
|
||||
<p className="description">{t('app.admin.plan_form.general_settings_info')}</p>
|
||||
</header>
|
||||
<div className="content">
|
||||
{action === 'create' && <FormSwitch control={control}
|
||||
formState={formState}
|
||||
onChange={handleAllGroupsChange}
|
||||
defaultValue={false}
|
||||
label={t('app.admin.plan_form.transversal')}
|
||||
tooltip={t('app.admin.plan_form.transversal_help')}
|
||||
id="all_groups" />}
|
||||
{!allGroups && groups && <FormSelect options={groups}
|
||||
formState={formState}
|
||||
control={control}
|
||||
rules={{ required: !allGroups }}
|
||||
disabled={action === 'update'}
|
||||
label={t('app.admin.plan_form.group')}
|
||||
id="group_id" />}
|
||||
<div className="grp">
|
||||
<FormInput register={register}
|
||||
rules={{ required: true, min: 1 }}
|
||||
disabled={action === 'update'}
|
||||
formState={formState}
|
||||
label={t('app.admin.plan_form.number_of_periods')}
|
||||
type="number"
|
||||
id="interval_count" />
|
||||
<FormSelect options={buildPeriodsOptions()}
|
||||
control={control}
|
||||
disabled={action === 'update'}
|
||||
onChange={handlePeriodUpdate}
|
||||
id="interval"
|
||||
label={t('app.admin.plan_form.period')}
|
||||
formState={formState}
|
||||
rules={{ required: true }} />
|
||||
</div>
|
||||
{action === 'update' && <FabAlert level="info">
|
||||
{t('app.admin.plan_form.edit_amount_info')}
|
||||
</FabAlert>}
|
||||
<FormInput register={register}
|
||||
formState={formState}
|
||||
id="amount"
|
||||
type="number"
|
||||
step={0.01}
|
||||
addOn={FormatLib.currencySymbol()}
|
||||
rules={{ required: true, min: 0 }}
|
||||
label={t('app.admin.plan_form.subscription_price')} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_form.activation_and_payment')}</p>
|
||||
</header>
|
||||
<div className="content">
|
||||
<FormSwitch control={control}
|
||||
formState={formState}
|
||||
id="disabled"
|
||||
defaultValue={false}
|
||||
label={t('app.admin.plan_form.disabled')}
|
||||
tooltip={t('app.admin.plan_form.disabled_help')} />
|
||||
<FormSwitch control={control}
|
||||
formState={formState}
|
||||
id="is_rolling"
|
||||
label={t('app.admin.plan_form.rolling_subscription')}
|
||||
disabled={action === 'update'}
|
||||
tooltip={t('app.admin.plan_form.rolling_subscription_help')} />
|
||||
<FormSwitch control={control}
|
||||
formState={formState}
|
||||
id="monthly_payment"
|
||||
label={t('app.admin.plan_form.monthly_payment')}
|
||||
disabled={action === 'update' || output.interval === 'week'}
|
||||
tooltip={t('app.admin.plan_form.monthly_payment_help')} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_form.partnership')}</p>
|
||||
<p className="description">{t('app.admin.plan_form.partner_plan_help')}</p>
|
||||
</header>
|
||||
<div className="content">
|
||||
<FormSwitch control={control}
|
||||
id="partnership"
|
||||
disabled={action === 'update'}
|
||||
tooltip={t('app.admin.plan_form.partner_plan_help')}
|
||||
defaultValue={plan?.type === 'PartnerPlan'}
|
||||
onChange={handlePartnershipChange}
|
||||
formState={formState}
|
||||
label={t('app.admin.plan_form.partner_plan')} />
|
||||
<FormInput register={register} type="hidden" id="type" defaultValue="Plan" />
|
||||
{output.type === 'PartnerPlan' && <div className="partner">
|
||||
<FabButton className="add-partner is-info" icon={<UserPlus size={20} />} onClick={tooglePartnerModal}>
|
||||
{t('app.admin.plan_form.new_user')}
|
||||
</FabButton>
|
||||
{partners && <FormSelect id="partner_id"
|
||||
options={partners}
|
||||
control={control}
|
||||
formState={formState}
|
||||
rules={{ required: output.type === 'PartnerPlan' }}
|
||||
tooltip={t('app.admin.plan_form.alert_partner_notification')}
|
||||
label={t('app.admin.plan_form.notified_partner')} />}
|
||||
{output.partner_id && <FabAlert level="info">
|
||||
{t('app.admin.plan_form.alert_partner_notification')}
|
||||
</FabAlert>}
|
||||
<FabButton className="is-secondary" icon={<UserPlus size={20} />} onClick={tooglePartnerModal}>
|
||||
{t('app.admin.plan_form.new_user')}
|
||||
</FabButton>
|
||||
</div>}
|
||||
</div>
|
||||
<AdvancedAccountingForm register={register} onError={onError} />
|
||||
{action === 'update' && <PlanPricingForm formState={formState}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_form.slots_visibility')}</p>
|
||||
<p className="description">{t('app.admin.plan_form.slots_visibility_help')}</p>
|
||||
</header>
|
||||
<div className="content">
|
||||
<FormInput register={register}
|
||||
formState={formState}
|
||||
nullable
|
||||
id="machines_visibility"
|
||||
rules={{ validate: v => { return (v === null || v >= 7 || t('app.admin.plan_form.visibility_minimum') as string); } }}
|
||||
type="number"
|
||||
label={t('app.admin.plan_form.machines_visibility')} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_form.display')} </p>
|
||||
</header>
|
||||
<div className="content">
|
||||
{categories?.length > 0 && <FormSelect options={categories}
|
||||
formState={formState}
|
||||
control={control}
|
||||
onError={onError}
|
||||
setValue={setValue}
|
||||
register={register} />}
|
||||
<FabButton type="submit" className="is-info submit-btn">
|
||||
{t('app.admin.plan_form.ACTION_plan', { ACTION: action })}
|
||||
</FabButton>
|
||||
id="plan_category_id"
|
||||
tooltip={t('app.admin.plan_form.category_help')}
|
||||
label={t('app.admin.plan_form.category')} />}
|
||||
<FormInput register={register}
|
||||
formState={formState}
|
||||
id="ui_weight"
|
||||
type="number"
|
||||
label={t('app.admin.plan_form.visual_prominence')}
|
||||
tooltip={t('app.admin.plan_form.visual_prominence_help')} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<AdvancedAccountingForm register={register} onError={onError} />
|
||||
</section>
|
||||
|
||||
{action === 'update' && <PlanPricingForm formState={formState}
|
||||
control={control}
|
||||
onError={onError}
|
||||
setValue={setValue}
|
||||
register={register} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="plan-form">
|
||||
<header>
|
||||
<h2>{t('app.admin.plan_form.ACTION_title', { ACTION: action })}</h2>
|
||||
<div className="grpBtn">
|
||||
<FabButton type="submit" onClick={handleSubmit(onSubmit)} className="fab-button is-main">
|
||||
{t('app.admin.plan_form.save')}
|
||||
</FabButton>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<UnsavedFormAlert uiRouter={uiRouter} formState={formState} />
|
||||
<FabTabs tabs={[
|
||||
{
|
||||
id: 'settings',
|
||||
title: t('app.admin.plan_form.tab_settings'),
|
||||
content: renderSettingsTab()
|
||||
},
|
||||
{
|
||||
id: 'usageLimits',
|
||||
title: t('app.admin.plan_form.tab_usage_limits'),
|
||||
content: <PlanLimitForm control={control}
|
||||
register={register}
|
||||
formState={formState}
|
||||
onError={onError}
|
||||
getValues={getValues}
|
||||
resetField={resetField} />
|
||||
}
|
||||
]} />
|
||||
</form>
|
||||
|
||||
<PartnerModal isOpen={isOpenPartnerModal}
|
||||
toggleModal={tooglePartnerModal}
|
||||
onError={onError}
|
||||
@ -281,4 +380,4 @@ const PlanFormWrapper: React.FC<PlanFormProps> = (props) => {
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
Application.Components.component('planForm', react2angular(PlanFormWrapper, ['action', 'plan', 'onError', 'onSuccess']));
|
||||
Application.Components.component('planForm', react2angular(PlanFormWrapper, ['action', 'plan', 'onError', 'onSuccess', 'uiRouter']));
|
||||
|
261
app/frontend/src/javascript/components/plans/plan-limit-form.tsx
Normal file
261
app/frontend/src/javascript/components/plans/plan-limit-form.tsx
Normal file
@ -0,0 +1,261 @@
|
||||
import React, { ReactNode, useEffect, useState } from 'react';
|
||||
import { Control, FormState, UseFormGetValues, UseFormResetField } from 'react-hook-form/dist/types/form';
|
||||
import { FormSwitch } from '../form/form-switch';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { PlanLimitModal } from './plan-limit-modal';
|
||||
import { Plan, PlanLimitation } from '../../models/plan';
|
||||
import { useFieldArray, UseFormRegister, useWatch } from 'react-hook-form';
|
||||
import { Machine } from '../../models/machine';
|
||||
import { MachineCategory } from '../../models/machine-category';
|
||||
import MachineAPI from '../../api/machine';
|
||||
import MachineCategoryAPI from '../../api/machine-category';
|
||||
import { FormUnsavedList } from '../form/form-unsaved-list';
|
||||
import { EditDestroyButtons } from '../base/edit-destroy-buttons';
|
||||
import { X } from 'phosphor-react';
|
||||
|
||||
interface PlanLimitFormProps<TContext extends object> {
|
||||
register: UseFormRegister<Plan>,
|
||||
control: Control<Plan, TContext>,
|
||||
formState: FormState<Plan>,
|
||||
onError: (message: string) => void,
|
||||
getValues: UseFormGetValues<Plan>,
|
||||
resetField: UseFormResetField<Plan>
|
||||
}
|
||||
|
||||
/**
|
||||
* Form tab to manage a subscription's usage limit
|
||||
*/
|
||||
export const PlanLimitForm = <TContext extends object> ({ register, control, formState, onError, getValues, resetField }: PlanLimitFormProps<TContext>) => {
|
||||
const { t } = useTranslation('admin');
|
||||
const { fields, append, remove, update } = useFieldArray<Plan, 'plan_limitations_attributes'>({ control, name: 'plan_limitations_attributes' });
|
||||
const limiting = useWatch<Plan>({ control, name: 'limiting' });
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [machines, setMachines] = useState<Array<Machine>>([]);
|
||||
const [categories, setCategories] = useState<Array<MachineCategory>>([]);
|
||||
const [edited, setEdited] = useState<{index: number, limitation: PlanLimitation}>(null);
|
||||
|
||||
useEffect(() => {
|
||||
MachineAPI.index({ disabled: false })
|
||||
.then(setMachines)
|
||||
.catch(onError);
|
||||
MachineCategoryAPI.index()
|
||||
.then(setCategories)
|
||||
.catch(onError);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Opens/closes the product stock edition modal
|
||||
*/
|
||||
const toggleModal = (): void => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on 'add a limitation'
|
||||
*/
|
||||
const onAddLimitation = (): void => {
|
||||
setEdited(null);
|
||||
toggleModal();
|
||||
};
|
||||
/**
|
||||
* Triggered when a new limit was added or an existing limit was modified
|
||||
*/
|
||||
const onLimitationSuccess = (limitation: PlanLimitation): void => {
|
||||
const id = getValues(`plan_limitations_attributes.${edited?.index}.id`);
|
||||
if (id) {
|
||||
update(edited.index, { ...limitation, id });
|
||||
setEdited(null);
|
||||
} else {
|
||||
append({ ...limitation, id });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggered when an unsaved limit was removed from the "pending" list.
|
||||
*/
|
||||
const onRemoveUnsaved = (index: number): void => {
|
||||
const id = getValues(`plan_limitations_attributes.${index}.id`);
|
||||
if (id) {
|
||||
// will reset the field to its default values
|
||||
resetField(`plan_limitations_attributes.${index}`);
|
||||
// unmount and remount the field
|
||||
update(index, getValues(`plan_limitations_attributes.${index}`));
|
||||
} else {
|
||||
remove(index);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when a saved limitation is requested to be deleted
|
||||
*/
|
||||
const handleLimitationDelete = (index: number): () => Promise<void> => {
|
||||
return () => {
|
||||
return new Promise<void>((resolve) => {
|
||||
update(index, { ...getValues(`plan_limitations_attributes.${index}`), _destroy: true });
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on "cancel" for a limitated previsouly marked as deleted
|
||||
*/
|
||||
const cancelDeletion = (index: number): (event: React.MouseEvent<HTMLParagraphElement, MouseEvent>) => void => {
|
||||
return (event) => {
|
||||
event.preventDefault();
|
||||
update(index, { ...getValues(`plan_limitations_attributes.${index}`), _destroy: false });
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user wants to modify a limitation. Return a callback
|
||||
*/
|
||||
const onEditLimitation = (limitation: PlanLimitation, index: number): () => void => {
|
||||
return () => {
|
||||
setEdited({ index, limitation });
|
||||
toggleModal();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Render an unsaved limitation of use
|
||||
*/
|
||||
const renderOngoingLimit = (limit: PlanLimitation): ReactNode => (
|
||||
<>
|
||||
{(limit.limitable_type === 'MachineCategory' && <div className="group">
|
||||
<span>{t('app.admin.plan_limit_form.category')}</span>
|
||||
<p>{categories?.find(c => c.id === limit.limitable_id)?.name}</p>
|
||||
</div>) ||
|
||||
<div className="group">
|
||||
<span>{t('app.admin.plan_limit_form.machine')}</span>
|
||||
<p>{machines?.find(m => m.id === limit.limitable_id)?.name}</p>
|
||||
</div>}
|
||||
<div className="group">
|
||||
<span>{t('app.admin.plan_limit_form.max_hours_per_day')}</span>
|
||||
<p>{limit.limit}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="plan-limit-form">
|
||||
<section>
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_limit_form.usage_limitation')}</p>
|
||||
<p className="description">{t('app.admin.plan_limit_form.usage_limitation_info')}</p>
|
||||
</header>
|
||||
<div className="content">
|
||||
<FormSwitch control={control}
|
||||
formState={formState}
|
||||
defaultValue={false}
|
||||
label={t('app.admin.plan_limit_form.usage_limitation_switch')}
|
||||
id="limiting" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{limiting && <div className="plan-limit-grp">
|
||||
<header>
|
||||
<p>{t('app.admin.plan_limit_form.all_limitations')}</p>
|
||||
<div className="grpBtn">
|
||||
<FabButton onClick={onAddLimitation} className="is-main">
|
||||
{t('app.admin.plan_limit_form.new_usage_limitation')}
|
||||
</FabButton>
|
||||
</div>
|
||||
</header>
|
||||
<FormUnsavedList fields={fields}
|
||||
onRemove={onRemoveUnsaved}
|
||||
register={register}
|
||||
title={t('app.admin.plan_limit_form.ongoing_limitations')}
|
||||
shouldRenderField={(limit: PlanLimitation) => limit._modified}
|
||||
formAttributeName="plan_limitations_attributes"
|
||||
formAttributes={['id', 'limitable_type', 'limitable_id', 'limit']}
|
||||
renderField={renderOngoingLimit}
|
||||
cancelLabel={t('app.admin.plan_limit_form.cancel')} />
|
||||
|
||||
{fields.filter(f => f._modified).length > 0 &&
|
||||
<p className="title">{t('app.admin.plan_limit_form.saved_limitations')}</p>
|
||||
}
|
||||
|
||||
{fields.filter(f => f.limitable_type === 'MachineCategory' && !f._modified).length > 0 &&
|
||||
<div className='plan-limit-list'>
|
||||
<p className="title">{t('app.admin.plan_limit_form.by_category')}</p>
|
||||
{fields.map((limitation, index) => {
|
||||
if (limitation.limitable_type !== 'MachineCategory' || limitation._modified) return false;
|
||||
|
||||
return (
|
||||
<div className={`plan-limit-item ${limitation._destroy ? 'is-destroying' : ''}`} key={limitation.id}>
|
||||
<div className="grp">
|
||||
<div>
|
||||
<span>{t('app.admin.plan_limit_form.category')}</span>
|
||||
<p>{categories.find(c => c.id === limitation.limitable_id)?.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('app.admin.plan_limit_form.max_hours_per_day')}</span>
|
||||
<p>{limitation.limit}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='actions'>
|
||||
<EditDestroyButtons onError={onError}
|
||||
onEdit={onEditLimitation(limitation, index)}
|
||||
itemId={getValues(`plan_limitations_attributes.${index}.id`)}
|
||||
showDestroyConfirmation={false}
|
||||
destroy={handleLimitationDelete(index)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}).filter(Boolean)}
|
||||
</div>
|
||||
}
|
||||
|
||||
{fields.filter(f => f.limitable_type === 'Machine' && !f._modified).length > 0 &&
|
||||
<div className='plan-limit-list'>
|
||||
<p className="title">{t('app.admin.plan_limit_form.by_machine')}</p>
|
||||
{fields.map((limitation, index) => {
|
||||
if (limitation.limitable_type !== 'Machine' || limitation._modified) return false;
|
||||
|
||||
return (
|
||||
<div className={`plan-limit-item ${limitation._destroy ? 'is-destroying' : ''}`} key={limitation.id}>
|
||||
<div className="grp">
|
||||
<div>
|
||||
<span>{t('app.admin.plan_limit_form.machine')}</span>
|
||||
<p>{machines.find(m => m.id === limitation.limitable_id)?.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('app.admin.plan_limit_form.max_hours_per_day')}</span>
|
||||
<p>{limitation.limit}</p>
|
||||
</div>
|
||||
{limitation._destroy && <div className="marker">{t('app.admin.plan_limit_form.ongoing_deletion')}</div>}
|
||||
</div>
|
||||
|
||||
<div className='actions'>
|
||||
{(limitation._destroy &&
|
||||
<p className="cancel-action" onClick={cancelDeletion(index)}>
|
||||
{t('app.admin.plan_limit_form.cancel_deletion')}
|
||||
<X size={14} />
|
||||
</p>) ||
|
||||
<EditDestroyButtons onError={onError}
|
||||
onEdit={onEditLimitation(limitation, index)}
|
||||
itemId={getValues(`plan_limitations_attributes.${index}.id`)}
|
||||
showDestroyConfirmation={false}
|
||||
destroy={handleLimitationDelete(index)} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}).filter(Boolean)}
|
||||
</div>
|
||||
}
|
||||
</div>}
|
||||
|
||||
<PlanLimitModal isOpen={isOpen}
|
||||
machines={machines}
|
||||
categories={categories}
|
||||
toggleModal={toggleModal}
|
||||
onSuccess={onLimitationSuccess}
|
||||
limitation={edited?.limitation}
|
||||
existingLimitations={fields} />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,124 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabAlert } from '../base/fab-alert';
|
||||
import { FabModal, ModalSize } from '../base/fab-modal';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { FormSelect } from '../form/form-select';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { LimitableType, PlanLimitation } from '../../models/plan';
|
||||
import { Machine } from '../../models/machine';
|
||||
import { MachineCategory } from '../../models/machine-category';
|
||||
import { SelectOption } from '../../models/select';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface PlanLimitModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
onSuccess: (limit: PlanLimitation) => void,
|
||||
machines: Array<Machine>
|
||||
categories: Array<MachineCategory>,
|
||||
limitation?: PlanLimitation,
|
||||
existingLimitations: Array<PlanLimitation>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form to manage subscriptions limitations of use
|
||||
*/
|
||||
export const PlanLimitModal: React.FC<PlanLimitModalProps> = ({ isOpen, toggleModal, machines, categories, onSuccess, limitation, existingLimitations = [] }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const { register, control, formState, setValue, handleSubmit, reset } = useForm<PlanLimitation>({ defaultValues: limitation || { limitable_type: 'MachineCategory' } });
|
||||
const limitType = useWatch({ control, name: 'limitable_type' });
|
||||
|
||||
useEffect(() => {
|
||||
reset(limitation);
|
||||
}, [limitation]);
|
||||
|
||||
/**
|
||||
* Toggle the form between 'categories' and 'machine'
|
||||
*/
|
||||
const toggleLimitType = (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>, type: LimitableType) => {
|
||||
evt.preventDefault();
|
||||
setValue('limitable_type', type);
|
||||
setValue('limitable_id', null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user validates the new limit.
|
||||
* We do not use handleSubmit() directly to prevent the propagaion of the "submit" event to the parent form
|
||||
*/
|
||||
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
return handleSubmit((data: PlanLimitation) => {
|
||||
onSuccess({ ...data, _modified: true });
|
||||
reset({ limitable_type: 'MachineCategory', limitable_id: null, limit: null });
|
||||
toggleModal();
|
||||
})(event);
|
||||
};
|
||||
/**
|
||||
* Creates options to the react-select format
|
||||
*/
|
||||
const buildOptions = (): Array<SelectOption<number>> => {
|
||||
if (limitType === 'MachineCategory') {
|
||||
return categories
|
||||
.filter(c => limitation || !existingLimitations.filter(l => l.limitable_type === 'MachineCategory').map(l => l.limitable_id).includes(c.id))
|
||||
.map(cat => {
|
||||
return { value: cat.id, label: cat.name };
|
||||
});
|
||||
} else {
|
||||
return machines
|
||||
.filter(m => limitation || !existingLimitations.filter(l => l.limitable_type === 'Machine').map(l => l.limitable_id).includes(m.id))
|
||||
.map(machine => {
|
||||
return { value: machine.id, label: machine.name };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal title={t('app.admin.plan_limit_modal.title')}
|
||||
width={ModalSize.large}
|
||||
isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
onClose={() => reset({ limitable_type: 'MachineCategory' })}
|
||||
closeButton>
|
||||
<form className='plan-limit-modal' onSubmit={onSubmit}>
|
||||
<p className='subtitle'>{t('app.admin.plan_limit_modal.limit_reservations')}</p>
|
||||
<div className="grp">
|
||||
<button onClick={evt => toggleLimitType(evt, 'MachineCategory')}
|
||||
className={limitType === 'MachineCategory' ? 'is-active' : ''}
|
||||
disabled={!!limitation}>
|
||||
{t('app.admin.plan_limit_modal.by_category')}
|
||||
</button>
|
||||
<button onClick={evt => toggleLimitType(evt, 'Machine')}
|
||||
className={limitType === 'Machine' ? 'is-active' : ''}
|
||||
disabled={!!limitation}>
|
||||
{t('app.admin.plan_limit_modal.by_machine')}
|
||||
</button>
|
||||
</div>
|
||||
<FabAlert level='info'>{limitType === 'Machine' ? t('app.admin.plan_limit_modal.machine_info') : t('app.admin.plan_limit_modal.categories_info')}</FabAlert>
|
||||
<FormInput register={register} id="id" type="hidden" />
|
||||
<FormInput register={register} id="limitable_type" type="hidden" />
|
||||
<FormSelect options={buildOptions()}
|
||||
disabled={!!limitation}
|
||||
control={control}
|
||||
id="limitable_id"
|
||||
rules={{ required: true }}
|
||||
formState={formState}
|
||||
label={t('app.admin.plan_limit_modal.machine')} />
|
||||
<FormInput id="limit"
|
||||
type="number"
|
||||
register={register}
|
||||
rules={{ required: true, min: 1 }}
|
||||
nullable
|
||||
step={1}
|
||||
formState={formState}
|
||||
label={t('app.admin.plan_limit_modal.max_hours_per_day')} />
|
||||
<FabButton type="submit">{t('app.admin.plan_limit_modal.confirm')}</FabButton>
|
||||
</form>
|
||||
</FabModal>
|
||||
);
|
||||
};
|
@ -92,32 +92,37 @@ export const PlanPricingForm = <TContext extends object>({ register, control, fo
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="plan-pricing-form">
|
||||
<h4>{t('app.admin.plan_pricing_form.prices')}</h4>
|
||||
{plans && <FormSelect options={plans}
|
||||
label={t('app.admin.plan_pricing_form.copy_prices_from')}
|
||||
tooltip={t('app.admin.plan_pricing_form.copy_prices_from_help')}
|
||||
control={control}
|
||||
onChange={handleCopyPrices}
|
||||
id="parent_plan_id" />}
|
||||
{<FabTabs tabs={[
|
||||
machines && {
|
||||
id: 'machines',
|
||||
title: t('app.admin.plan_pricing_form.machines'),
|
||||
content: fields.map((price, index) => {
|
||||
if (price.priceable_type !== 'Machine') return false;
|
||||
return renderPriceElement(price, index);
|
||||
}).filter(Boolean)
|
||||
},
|
||||
spaces && {
|
||||
id: 'spaces',
|
||||
title: t('app.admin.plan_pricing_form.spaces'),
|
||||
content: fields.map((price, index) => {
|
||||
if (price.priceable_type !== 'Space') return false;
|
||||
return renderPriceElement(price, index);
|
||||
}).filter(Boolean)
|
||||
}
|
||||
]} />}
|
||||
</div>
|
||||
<section className="plan-pricing-form" data-testid="plan-pricing-form">
|
||||
<header>
|
||||
<p className="title">{t('app.admin.plan_pricing_form.prices')}</p>
|
||||
<p className="description">{t('app.admin.plan_pricing_form.about_prices')}</p>
|
||||
</header>
|
||||
<div className="content">
|
||||
{plans && <FormSelect options={plans}
|
||||
control={control}
|
||||
label={t('app.admin.plan_pricing_form.copy_prices_from')}
|
||||
tooltip={t('app.admin.plan_pricing_form.copy_prices_from_help')}
|
||||
onChange={handleCopyPrices}
|
||||
id="parent_plan_id" />}
|
||||
{<FabTabs tabs={[
|
||||
machines && {
|
||||
id: 'machines',
|
||||
title: t('app.admin.plan_pricing_form.machines'),
|
||||
content: fields.map((price, index) => {
|
||||
if (price.priceable_type !== 'Machine') return false;
|
||||
return renderPriceElement(price, index);
|
||||
}).filter(Boolean)
|
||||
},
|
||||
spaces && {
|
||||
id: 'spaces',
|
||||
title: t('app.admin.plan_pricing_form.spaces'),
|
||||
content: fields.map((price, index) => {
|
||||
if (price.priceable_type !== 'Space') return false;
|
||||
return renderPriceElement(price, index);
|
||||
}).filter(Boolean)
|
||||
}
|
||||
]} />}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -114,7 +114,7 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
|
||||
onEdit={() => handleRequestEdit(p)}
|
||||
itemId={p.id}
|
||||
itemType={t('app.admin.configure_packs_button.pack')}
|
||||
apiDestroy={PrepaidPackAPI.destroy}/>
|
||||
destroy={PrepaidPackAPI.destroy}/>
|
||||
<FabModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
title={t('app.admin.configure_packs_button.edit_pack')}
|
||||
|
@ -1,22 +1,23 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { Product } from '../../models/product';
|
||||
import { PencilSimple, Trash } from 'phosphor-react';
|
||||
import noImage from '../../../../images/no_image.png';
|
||||
import { FabStateLabel } from '../base/fab-state-label';
|
||||
import { ProductPrice } from './product-price';
|
||||
import { EditDestroyButtons } from '../base/edit-destroy-buttons';
|
||||
import ProductAPI from '../../api/product';
|
||||
|
||||
interface ProductItemProps {
|
||||
product: Product,
|
||||
onEdit: (product: Product) => void,
|
||||
onDelete: (productId: number) => void,
|
||||
onDelete: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a product item in the admin view
|
||||
*/
|
||||
export const ProductItem: React.FC<ProductItemProps> = ({ product, onEdit, onDelete }) => {
|
||||
export const ProductItem: React.FC<ProductItemProps> = ({ product, onEdit, onDelete, onError }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
/**
|
||||
@ -34,15 +35,6 @@ export const ProductItem: React.FC<ProductItemProps> = ({ product, onEdit, onDel
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Init the process of delete the given product
|
||||
*/
|
||||
const deleteProduct = (productId: number): () => void => {
|
||||
return (): void => {
|
||||
onDelete(productId);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns CSS class from stock status
|
||||
*/
|
||||
@ -80,14 +72,13 @@ export const ProductItem: React.FC<ProductItemProps> = ({ product, onEdit, onDel
|
||||
<ProductPrice product={product} className="price" />
|
||||
</div>
|
||||
<div className='actions'>
|
||||
<div className='manage'>
|
||||
<FabButton className='edit-btn' onClick={editProduct(product)}>
|
||||
<PencilSimple size={20} weight="fill" />
|
||||
</FabButton>
|
||||
<FabButton className='delete-btn' onClick={deleteProduct(product.id)}>
|
||||
<Trash size={20} weight="fill" />
|
||||
</FabButton>
|
||||
</div>
|
||||
<EditDestroyButtons onDeleteSuccess={onDelete}
|
||||
className="manage"
|
||||
onError={onError}
|
||||
onEdit={editProduct(product)}
|
||||
itemId={product.id}
|
||||
itemType={t('app.admin.store.product_item.product')}
|
||||
destroy={ProductAPI.destroy} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import Select from 'react-select';
|
||||
import { PencilSimple, X } from 'phosphor-react';
|
||||
import { PencilSimple } from 'phosphor-react';
|
||||
import { useFieldArray, UseFormRegister } from 'react-hook-form';
|
||||
import { Control, FormState, UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Product,
|
||||
Product, ProductStockMovement,
|
||||
stockMovementAllReasons, StockMovementIndex, StockMovementIndexFilter,
|
||||
StockMovementReason,
|
||||
StockType
|
||||
@ -20,6 +20,7 @@ import FormatLib from '../../lib/format';
|
||||
import ProductLib from '../../lib/product';
|
||||
import { useImmer } from 'use-immer';
|
||||
import { FabPagination } from '../base/fab-pagination';
|
||||
import { FormUnsavedList } from '../form/form-unsaved-list';
|
||||
|
||||
interface ProductStockFormProps<TContext extends object> {
|
||||
currentFormValues: Product,
|
||||
@ -159,6 +160,25 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Render an attribute of an unsaved stock movement
|
||||
*/
|
||||
const renderOngoingStockMovement = (movement: ProductStockMovement): ReactNode => (
|
||||
<>
|
||||
<div className="group">
|
||||
<p>{t(`app.admin.store.product_stock_form.type_${ProductLib.stockMovementType(movement.reason)}`)}</p>
|
||||
</div>
|
||||
<div className="group">
|
||||
<span>{t(`app.admin.store.product_stock_form.${movement.stock_type}`)}</span>
|
||||
<p>{ProductLib.absoluteStockMovement(movement.quantity, movement.reason)}</p>
|
||||
</div>
|
||||
<div className="group">
|
||||
<span>{t('app.admin.store.product_stock_form.reason')}</span>
|
||||
<p>{t(ProductLib.stockMovementReasonTrKey(movement.reason))}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='product-stock-form'>
|
||||
<h4>{t('app.admin.store.product_stock_form.stock_up_to_date')}
|
||||
@ -178,36 +198,19 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
|
||||
<span>{t('app.admin.store.product_stock_form.external')}</span>
|
||||
<p>{currentFormValues?.stock?.external}</p>
|
||||
</div>
|
||||
<FabButton onClick={toggleModal} icon={<PencilSimple size={20} weight="fill" />} className="is-black">Modifier</FabButton>
|
||||
<FabButton onClick={toggleModal} icon={<PencilSimple size={20} weight="fill" />} className="is-black">{t('app.admin.store.product_stock_form.edit')}</FabButton>
|
||||
</div>
|
||||
|
||||
{fields.length > 0 && <div className="ongoing-stocks">
|
||||
<span className="title">{t('app.admin.store.product_stock_form.ongoing_operations')}</span>
|
||||
<span className="save-notice">{t('app.admin.store.product_stock_form.save_reminder')}</span>
|
||||
{fields.map((newMovement, index) => (
|
||||
<div key={index} className="unsaved-stock-movement stock-item">
|
||||
<div className="group">
|
||||
<p>{t(`app.admin.store.product_stock_form.type_${ProductLib.stockMovementType(newMovement.reason)}`)}</p>
|
||||
</div>
|
||||
<div className="group">
|
||||
<span>{t(`app.admin.store.product_stock_form.${newMovement.stock_type}`)}</span>
|
||||
<p>{ProductLib.absoluteStockMovement(newMovement.quantity, newMovement.reason)}</p>
|
||||
</div>
|
||||
<div className="group">
|
||||
<span>{t('app.admin.store.product_stock_form.reason')}</span>
|
||||
<p>{t(ProductLib.stockMovementReasonTrKey(newMovement.reason))}</p>
|
||||
</div>
|
||||
<p className="cancel-action" onClick={() => remove(index)}>
|
||||
{t('app.admin.store.product_stock_form.cancel')}
|
||||
<X size={20} />
|
||||
</p>
|
||||
<FormInput id={`product_stock_movements_attributes.${index}.stock_type`} register={register}
|
||||
type="hidden" />
|
||||
<FormInput id={`product_stock_movements_attributes.${index}.quantity`} register={register} type="hidden" />
|
||||
<FormInput id={`product_stock_movements_attributes.${index}.reason`} register={register} type="hidden" />
|
||||
</div>
|
||||
))}
|
||||
</div>}
|
||||
<FormUnsavedList fields={fields}
|
||||
className="ongoing-stocks"
|
||||
onRemove={remove}
|
||||
register={register}
|
||||
title={t('app.admin.store.product_stock_form.ongoing_operations')}
|
||||
formAttributeName="product_stock_movements_attributes"
|
||||
formAttributes={['stock_type', 'quantity', 'reason']}
|
||||
renderField={renderOngoingStockMovement}
|
||||
saveReminderLabel={t('app.admin.store.product_stock_form.save_reminder')}
|
||||
cancelLabel={t('app.admin.store.product_stock_form.cancel')} />
|
||||
|
||||
<hr />
|
||||
|
||||
|
@ -111,14 +111,9 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError, uiRouter }) =>
|
||||
};
|
||||
|
||||
/** Delete a product */
|
||||
const deleteProduct = async (productId: number): Promise<void> => {
|
||||
try {
|
||||
await ProductAPI.destroy(productId);
|
||||
await fetchProducts();
|
||||
onSuccess(t('app.admin.store.products.successfully_deleted'));
|
||||
} catch (e) {
|
||||
onError(t('app.admin.store.products.unable_to_delete') + e);
|
||||
}
|
||||
const deleteProduct = async (message: string): Promise<void> => {
|
||||
await fetchProducts();
|
||||
onSuccess(message);
|
||||
};
|
||||
|
||||
/** Goto new product page */
|
||||
@ -244,6 +239,7 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError, uiRouter }) =>
|
||||
<ProductItem
|
||||
key={product.id}
|
||||
product={product}
|
||||
onError={onError}
|
||||
onEdit={editProduct}
|
||||
onDelete={deleteProduct}
|
||||
/>
|
||||
|
@ -199,7 +199,7 @@ export const Trainings: React.FC<TrainingsProps> = ({ onError, onSuccess }) => {
|
||||
onEdit={() => toTrainingEdit(training)}
|
||||
itemId={training.id}
|
||||
itemType={t('app.admin.trainings.training')}
|
||||
apiDestroy={TrainingAPI.destroy}/>
|
||||
destroy={TrainingAPI.destroy}/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
@ -7,12 +7,13 @@ import { useTranslation } from 'react-i18next';
|
||||
interface GenderInputProps<TFieldValues> {
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
disabled?: boolean|((id: string) => boolean),
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Input component to set the gender for the user
|
||||
*/
|
||||
export const GenderInput = <TFieldValues extends FieldValues>({ register, disabled = false }: GenderInputProps<TFieldValues>) => {
|
||||
export const GenderInput = <TFieldValues extends FieldValues>({ register, disabled = false, required }: GenderInputProps<TFieldValues>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [isDisabled, setIsDisabled] = useState<boolean>(false);
|
||||
@ -26,21 +27,25 @@ export const GenderInput = <TFieldValues extends FieldValues>({ register, disabl
|
||||
}, [disabled]);
|
||||
|
||||
return (
|
||||
<div className="gender-input">
|
||||
<fieldset className="gender-input">
|
||||
<legend className={required ? 'is-required' : ''}>{t('app.shared.gender_input.label')}</legend>
|
||||
<label>
|
||||
<p>{t('app.shared.gender_input.man')}</p>
|
||||
<input type="radio"
|
||||
name='gender'
|
||||
value="true"
|
||||
required={required}
|
||||
disabled={isDisabled}
|
||||
{...register('statistic_profile_attributes.gender' as FieldPath<TFieldValues>)} />
|
||||
</label>
|
||||
<label>
|
||||
<p>{t('app.shared.gender_input.woman')}</p>
|
||||
<input type="radio"
|
||||
name='gender'
|
||||
value="false"
|
||||
disabled={isDisabled}
|
||||
{...register('statistic_profile_attributes.gender' as FieldPath<TFieldValues>)} />
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
|
@ -184,7 +184,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
<div className="fields-group">
|
||||
<div className="personnal-data">
|
||||
<h4>{t('app.shared.user_profile_form.personal_data')}</h4>
|
||||
<GenderInput register={register} disabled={isDisabled} />
|
||||
<GenderInput register={register} disabled={isDisabled} required />
|
||||
<div className="names">
|
||||
<FormInput id="profile_attributes.last_name"
|
||||
register={register}
|
||||
|
@ -896,9 +896,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
// default: do not generate a refund invoice
|
||||
$scope.generate_avoir = false;
|
||||
|
||||
// date of the generated refund invoice
|
||||
$scope.avoir_date = null;
|
||||
|
||||
// optional description shown on the refund invoice
|
||||
$scope.description = '';
|
||||
|
||||
@ -929,7 +926,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
{
|
||||
amount: $scope.amount,
|
||||
avoir: $scope.generate_avoir,
|
||||
avoir_date: $scope.avoir_date,
|
||||
avoir_description: $scope.description
|
||||
},
|
||||
function (_wallet) {
|
||||
|
@ -21,11 +21,14 @@
|
||||
/**
|
||||
* Controller used in the plan creation form
|
||||
*/
|
||||
Application.Controllers.controller('NewPlanController', ['$scope', '$uibModal', 'groups', 'prices', 'partners', 'CSRF', '$state', 'growl', '_t', 'planCategories',
|
||||
function ($scope, $uibModal, groups, prices, partners, CSRF, $state, growl, _t, planCategories) {
|
||||
Application.Controllers.controller('NewPlanController', ['$scope', '$uibModal', 'groups', 'prices', 'partners', 'CSRF', '$state', 'growl', '_t', '$uiRouter',
|
||||
function ($scope, $uibModal, groups, prices, partners, CSRF, $state, growl, _t, $uiRouter) {
|
||||
// protection against request forgery
|
||||
CSRF.setMetaTags();
|
||||
|
||||
// the following item is used by the UnsavedFormAlert component to detect a page change
|
||||
$scope.uiRouter = $uiRouter;
|
||||
|
||||
/**
|
||||
* Shows an error message forwarded from a child component
|
||||
*/
|
||||
@ -46,13 +49,16 @@ Application.Controllers.controller('NewPlanController', ['$scope', '$uibModal',
|
||||
/**
|
||||
* Controller used in the plan edition form
|
||||
*/
|
||||
Application.Controllers.controller('EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'spaces', 'prices', 'partners', 'CSRF', '$state', '$transition$', 'growl', '$filter', '_t', 'Plan', 'planCategories',
|
||||
function ($scope, groups, plans, planPromise, machines, spaces, prices, partners, CSRF, $state, $transition$, growl, $filter, _t, Plan, planCategories) {
|
||||
Application.Controllers.controller('EditPlanController', ['$scope', 'groups', 'plans', 'planPromise', 'machines', 'spaces', 'prices', 'partners', 'CSRF', '$state', '$transition$', 'growl', '$filter', '_t', '$uiRouter',
|
||||
function ($scope, groups, plans, planPromise, machines, spaces, prices, partners, CSRF, $state, $transition$, growl, $filter, _t, $uiRouter) {
|
||||
// protection against request forgery
|
||||
CSRF.setMetaTags();
|
||||
|
||||
$scope.suscriptionPlan = cleanPlan(planPromise);
|
||||
|
||||
// the following item is used by the UnsavedFormAlert component to detect a page change
|
||||
$scope.uiRouter = $uiRouter;
|
||||
|
||||
/**
|
||||
* Shows an error message forwarded from a child component
|
||||
*/
|
||||
|
@ -687,6 +687,13 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
$scope.amountTotal = res.price;
|
||||
$scope.schedule.payment_schedule = res.schedule;
|
||||
$scope.totalNoCoupon = res.price_without_coupon;
|
||||
if (res.errors && Object.keys(res.errors).length > 0) {
|
||||
for (const error in res.errors) {
|
||||
for (const message of res.errors[error]) {
|
||||
growl.error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
setSlotsDetails(res.details);
|
||||
});
|
||||
} else {
|
||||
|
@ -12,6 +12,16 @@ export interface Partner {
|
||||
email: string
|
||||
}
|
||||
|
||||
export type LimitableType = 'Machine'|'MachineCategory';
|
||||
export interface PlanLimitation {
|
||||
id?: number,
|
||||
limitable_id: number,
|
||||
limitable_type: LimitableType,
|
||||
limit: number,
|
||||
_modified?: boolean,
|
||||
_destroy?: boolean,
|
||||
}
|
||||
|
||||
export interface Plan {
|
||||
id?: number,
|
||||
base_name: string,
|
||||
@ -34,8 +44,10 @@ export interface Plan {
|
||||
plan_file_url?: string,
|
||||
partner_id?: number,
|
||||
partnership?: boolean,
|
||||
limiting?: boolean,
|
||||
partners?: Array<Partner>,
|
||||
advanced_accounting_attributes?: AdvancedAccounting
|
||||
advanced_accounting_attributes?: AdvancedAccounting,
|
||||
plan_limitations_attributes?: Array<PlanLimitation>
|
||||
}
|
||||
|
||||
export interface PlansDuration {
|
||||
|
@ -81,7 +81,10 @@ export const bookingSettings = [
|
||||
'reminder_delay',
|
||||
'visibility_yearly',
|
||||
'visibility_others',
|
||||
'reservation_deadline',
|
||||
'machine_reservation_deadline',
|
||||
'training_reservation_deadline',
|
||||
'event_reservation_deadline',
|
||||
'space_reservation_deadline',
|
||||
'display_name_enable',
|
||||
'book_overlapping_slots',
|
||||
'slot_duration',
|
||||
|
@ -1175,7 +1175,8 @@ angular.module('application.router', ['ui.router'])
|
||||
"'renew_pack_threshold', 'pack_only_for_subscription', 'overlapping_categories', 'public_registrations'," +
|
||||
"'extended_prices_in_same_day', 'recaptcha_site_key', 'recaptcha_secret_key', 'user_validation_required', " +
|
||||
"'user_validation_required_list', 'machines_module', 'user_change_group', 'show_username_in_admin_list', " +
|
||||
"'store_module', 'reservation_deadline']"
|
||||
"'store_module', 'machine_reservation_deadline', 'training_reservation_deadline', 'event_reservation_deadline', " +
|
||||
"'space_reservation_deadline']"
|
||||
}).$promise;
|
||||
}],
|
||||
privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }],
|
||||
|
@ -62,6 +62,7 @@
|
||||
@import "modules/form/form-checklist";
|
||||
@import "modules/form/form-file-upload";
|
||||
@import "modules/form/form-image-upload";
|
||||
@import "modules/form/form-unsaved-list";
|
||||
@import "modules/group/change-group";
|
||||
@import "modules/invoices/invoices-settings-panel";
|
||||
@import "modules/invoices/vat-settings-modal";
|
||||
@ -97,6 +98,9 @@
|
||||
@import "modules/plan-categories/plan-categories-list";
|
||||
@import "modules/plans/plan-card";
|
||||
@import "modules/plans/plan-form";
|
||||
@import "modules/plans/plan-limit-form";
|
||||
@import "modules/plans/plan-limit-modal";
|
||||
@import "modules/plans/plan-pricing-form";
|
||||
@import "modules/plans/plans-filter";
|
||||
@import "modules/plans/plans-list";
|
||||
@import "modules/prepaid-packs/packs-summary";
|
||||
|
@ -12,16 +12,21 @@
|
||||
text-align: center;
|
||||
color: var(--main);
|
||||
border-bottom: 1px solid var(--gray-soft-dark);
|
||||
border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0;
|
||||
&:hover {
|
||||
background-color: var(--gray-soft-light);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.react-tabs__tab--selected {
|
||||
color: var(--gray-hard-dark);
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
border-bottom: none;
|
||||
border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--gray-soft-lightest);
|
||||
cursor: default;
|
||||
}
|
||||
&:focus { outline: none; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
.fab-text-editor {
|
||||
position: relative;
|
||||
margin-bottom: 1.6rem;
|
||||
//margin-bottom: 1.6rem;
|
||||
padding-bottom: 1.6rem;
|
||||
background-color: var(--gray-soft-lightest);
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
|
@ -15,8 +15,6 @@
|
||||
flex-direction: column;
|
||||
gap: 3.2rem;
|
||||
|
||||
.fab-alert { margin: 0; }
|
||||
|
||||
section { @include layout-settings; }
|
||||
.save-btn { align-self: flex-start; }
|
||||
}
|
||||
@ -25,7 +23,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2.4rem;
|
||||
gap: 1.6rem 2.4rem;
|
||||
|
||||
.add-price {
|
||||
max-width: fit-content;
|
||||
@ -37,6 +35,8 @@
|
||||
align-items: flex-end;
|
||||
gap: 0 2.4rem;
|
||||
|
||||
.form-item { margin: 0; }
|
||||
|
||||
.remove-price {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@ -61,8 +61,7 @@
|
||||
flex-direction: column;
|
||||
@media (min-width: 640px) {flex-direction: row; }
|
||||
|
||||
.form-item:first-child {
|
||||
margin-right: 2.4rem;
|
||||
}
|
||||
.form-item { margin: 0; }
|
||||
.form-item:first-child { margin-right: 2.4rem; }
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,47 @@
|
||||
.form-unsaved-list {
|
||||
.save-notice {
|
||||
@include text-xs;
|
||||
margin-left: 1rem;
|
||||
color: var(--alert);
|
||||
}
|
||||
.unsaved-field {
|
||||
background-color: var(--gray-soft-light);
|
||||
border: 0;
|
||||
padding: 1.2rem;
|
||||
margin-top: 1rem;width: 100%;
|
||||
display: flex;
|
||||
gap: 4.8rem;
|
||||
justify-items: flex-start;
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
& > * { flex: 1 1 45%; }
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
@include text-base;
|
||||
}
|
||||
.title {
|
||||
@include text-base(600);
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
.group {
|
||||
span {
|
||||
@include text-xs;
|
||||
color: var(--gray-hard-light);
|
||||
}
|
||||
p { @include text-base(600); }
|
||||
}
|
||||
|
||||
.cancel-action {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
svg {
|
||||
margin-left: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -15,8 +15,6 @@
|
||||
flex-direction: column;
|
||||
gap: 3.2rem;
|
||||
|
||||
.fab-alert { margin: 0; }
|
||||
|
||||
section { @include layout-settings; }
|
||||
.save-btn { align-self: flex-start; }
|
||||
}
|
||||
|
@ -1,25 +1,40 @@
|
||||
.plan-form {
|
||||
.plan-sheet {
|
||||
margin-top: 4rem;
|
||||
max-width: 1260px;
|
||||
margin: 2.4rem auto 0;
|
||||
padding: 0 3rem 6rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
& > header {
|
||||
padding-bottom: 0;
|
||||
@include header($sticky: true);
|
||||
gap: 2.4rem;
|
||||
}
|
||||
.duration {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.form-item:first-child {
|
||||
margin-right: 32px;
|
||||
}
|
||||
}
|
||||
.partner {
|
||||
&-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 3.2rem;
|
||||
|
||||
.fab-alert {
|
||||
width: 100%;
|
||||
section { @include layout-settings; }
|
||||
.grp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media (min-width: 640px) {flex-direction: row; }
|
||||
|
||||
.form-item { margin: 0; }
|
||||
.form-item:first-child { margin-right: 2.4rem; }
|
||||
}
|
||||
|
||||
.partner {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: flex-end;
|
||||
gap: 0 2.4rem;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
flex-direction: row;
|
||||
button { margin-bottom: 1.6rem; }
|
||||
}
|
||||
}
|
||||
}
|
||||
.submit-btn {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,85 @@
|
||||
.plan-limit-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3.2rem 0;
|
||||
|
||||
section { @include layout-settings; }
|
||||
|
||||
.plan-limit-grp {
|
||||
header {
|
||||
@include header();
|
||||
p {
|
||||
@include title-base;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.form-unsaved-list {
|
||||
margin-bottom: 6.4rem;
|
||||
}
|
||||
.plan-limit-list {
|
||||
max-height: 65vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
||||
& > .title { @include text-base(500); }
|
||||
.plan-limit-item {
|
||||
width: 100%;
|
||||
margin-bottom: 2.4rem;
|
||||
padding: 1.6rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 3.2rem;
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--gray-soft-lightest);
|
||||
|
||||
span {
|
||||
@include text-xs;
|
||||
color: var(--gray-hard-light);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
@include text-base(600);
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 540px) {
|
||||
.grp {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
& > * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-destroying {
|
||||
background-color: var(--alert-lightest);
|
||||
.marker {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
color: var(--alert-dark);
|
||||
margin: auto;
|
||||
}
|
||||
.actions > .cancel-action {
|
||||
font-weight: normal;
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
.plan-limit-modal {
|
||||
.grp {
|
||||
margin-bottom: 3.2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
button {
|
||||
flex: 1;
|
||||
padding: 1.6rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--gray-soft-lightest);
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
color: var(--gray-soft-darkest);
|
||||
@include text-base;
|
||||
&.is-active {
|
||||
border: 1px solid var(--gray-soft-darkest);
|
||||
background-color: var(--gray-hard-darkest);
|
||||
color: var(--gray-soft-lightest);
|
||||
}
|
||||
}
|
||||
button:first-of-type {
|
||||
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
||||
}
|
||||
button:last-of-type {
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
.plan-pricing-form {
|
||||
.fab-tabs .tabs li {
|
||||
margin-bottom: 1.6rem;
|
||||
&:hover { background-color: var(--gray-soft); }
|
||||
&.react-tabs__tab--selected:hover { background-color: transparent; }
|
||||
}
|
||||
|
||||
.react-tabs__tab-panel {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
@ -15,8 +15,6 @@
|
||||
flex-direction: column;
|
||||
gap: 3.2rem;
|
||||
|
||||
.fab-alert { margin: 0; }
|
||||
|
||||
section { @include layout-settings; }
|
||||
.save-btn { align-self: flex-start; }
|
||||
}
|
||||
|
@ -3,28 +3,6 @@
|
||||
|
||||
.ongoing-stocks {
|
||||
margin: 2.4rem 0;
|
||||
.save-notice {
|
||||
@include text-xs;
|
||||
margin-left: 1rem;
|
||||
color: var(--alert);
|
||||
}
|
||||
.unsaved-stock-movement {
|
||||
background-color: var(--gray-soft-light);
|
||||
border: 0;
|
||||
padding: 1.2rem;
|
||||
margin-top: 1rem;
|
||||
|
||||
.cancel-action {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
svg {
|
||||
margin-left: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.store-list {
|
||||
margin-top: 2.4rem;
|
||||
|
@ -15,8 +15,6 @@
|
||||
flex-direction: column;
|
||||
gap: 3.2rem;
|
||||
|
||||
.fab-alert { margin: 0; }
|
||||
|
||||
section { @include layout-settings; }
|
||||
.save-btn { align-self: flex-start; }
|
||||
}
|
||||
|
@ -1,5 +1,17 @@
|
||||
.gender-input {
|
||||
margin-bottom: 1.6rem;
|
||||
legend {
|
||||
@include text-sm;
|
||||
margin: 0 0 0.8rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
&::first-letter { text-transform: uppercase; }
|
||||
&.is-required::after {
|
||||
content: "*";
|
||||
margin-left: 0.5ch;
|
||||
color: var(--alert);
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-flex;
|
||||
|
@ -29,10 +29,15 @@
|
||||
}
|
||||
|
||||
& > .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.6rem;
|
||||
background-color: var(--gray-soft-light);
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
border-radius: var(--border-radius);
|
||||
& > * { margin-bottom: 0; }
|
||||
& > *:not(:last-child) { margin-bottom: 3.2rem; }
|
||||
.fab-alert { margin: 0 0 1.6rem; }
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
|
@ -3,24 +3,6 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="avoirForm" novalidate="novalidate">
|
||||
<div class="form-group" ng-class="{'has-error': avoirForm.avoir_date.$dirty && avoirForm.avoir_date.$invalid }">
|
||||
<label translate>{{ 'app.admin.invoices.creation_date_for_the_refund' }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="avoir_date"
|
||||
ng-model="avoir.avoir_date"
|
||||
uib-datepicker-popup="{{datePicker.format}}"
|
||||
datepicker-options="datePicker.options"
|
||||
is-open="datePicker.opened"
|
||||
min-date="lastClosingEnd"
|
||||
placeholder="{{datePicker.format}}"
|
||||
ng-click="openDatePicker($event)"
|
||||
required/>
|
||||
</div>
|
||||
<span class="help-block" ng-show="avoirForm.avoir_date.$dirty && avoirForm.avoir_date.$error.required" translate>{{ 'app.admin.invoices.creation_date_is_required' }}</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate>{{ 'app.admin.invoices.refund_mode' }}</label>
|
||||
<select class="form-control m-t-sm" name="payment_method" ng-model="avoir.payment_method" ng-options="mode.value as mode.name for mode in avoirModes" required></select>
|
||||
|
@ -10,24 +10,7 @@
|
||||
<h1>{{ 'app.admin.plans.edit.subscription_plan' | translate }} {{ suscriptionPlan.base_name }}</h1>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<section class="heading-actions wrapper">
|
||||
<a class="btn btn-lg btn-block btn-default m-t-xs" ui-sref="app.admin.pricing" translate>{{ 'app.shared.buttons.cancel' }}</a>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="row no-gutter">
|
||||
<div class=" col-sm-12 col-md-9 b-r nopadding">
|
||||
|
||||
<div class="panel panel-default bg-light m-lg">
|
||||
<div class="panel-body m-r">
|
||||
<plan-form action="'update'" plan="suscriptionPlan" on-error="onError" on-success="onSuccess"></plan-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<plan-form action="'update'" plan="suscriptionPlan" on-error="onError" on-success="onSuccess" ui-router="uiRouter"></plan-form>
|
||||
|
@ -14,14 +14,4 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="row no-gutter">
|
||||
<div class=" col-sm-12 col-md-9 b-r nopadding">
|
||||
|
||||
<div class="panel panel-default bg-light m-lg">
|
||||
<div class="panel-body m-r">
|
||||
<plan-form action="'create'" on-error="onError" on-success="onSuccess"></plan-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<plan-form action="'create'" on-error="onError" on-success="onSuccess" ui-router="uiRouter"></plan-form>
|
||||
|
@ -108,9 +108,33 @@
|
||||
<div class="row">
|
||||
<h3 class="m-l m-t-lg" translate>{{ 'app.admin.settings.reservation_deadline' }}</h3>
|
||||
<p class="alert alert-warning m-h-md" translate>{{ 'app.admin.settings.reservation_deadline_help' }}</p>
|
||||
<number-setting name="reservation_deadline"
|
||||
<number-setting name="machine_reservation_deadline"
|
||||
settings="allSettings"
|
||||
label="app.admin.settings.deadline_minutes"
|
||||
label="app.admin.settings.machine_deadline_minutes"
|
||||
classes="col-md-4"
|
||||
fa-icon="fas fa-clock"
|
||||
min="0"
|
||||
required="true">
|
||||
</number-setting>
|
||||
<number-setting name="training_reservation_deadline"
|
||||
settings="allSettings"
|
||||
label="app.admin.settings.training_deadline_minutes"
|
||||
classes="col-md-4"
|
||||
fa-icon="fas fa-clock"
|
||||
min="0"
|
||||
required="true">
|
||||
</number-setting>
|
||||
<number-setting name="event_reservation_deadline"
|
||||
settings="allSettings"
|
||||
label="app.admin.settings.event_deadline_minutes"
|
||||
classes="col-md-4"
|
||||
fa-icon="fas fa-clock"
|
||||
min="0"
|
||||
required="true">
|
||||
</number-setting>
|
||||
<number-setting name="space_reservation_deadline"
|
||||
settings="allSettings"
|
||||
label="app.admin.settings.space_deadline_minutes"
|
||||
classes="col-md-4"
|
||||
fa-icon="fas fa-clock"
|
||||
min="0"
|
||||
|
@ -55,25 +55,6 @@
|
||||
</div>
|
||||
|
||||
<div ng-show="generate_avoir">
|
||||
<div class="m-t" ng-class="{'has-error': walletForm.avoir_date.$dirty && walletForm.avoir_date.$invalid }">
|
||||
<label for="avoir_date" translate>{{ 'app.shared.wallet.creation_date_for_the_refund' }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="avoir_date"
|
||||
name="avoir_date"
|
||||
ng-model="avoir_date"
|
||||
uib-datepicker-popup="{{datePicker.format}}"
|
||||
datepicker-options="datePicker.options"
|
||||
is-open="datePicker.opened"
|
||||
placeholder="{{datePicker.format}}"
|
||||
ng-click="toggleDatePicker($event)"
|
||||
ng-required="generate_avoir"/>
|
||||
</div>
|
||||
<span class="help-block" ng-show="walletForm.avoir_date.$dirty && walletForm.avoir_date.$error.required" translate>{{ 'app.shared.wallet.creation_date_is_required' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="m-t">
|
||||
<label for="description" translate>{{ 'app.shared.wallet.description_optional' }}</label>
|
||||
<p translate>{{ 'app.shared.wallet.will_appear_on_the_refund_invoice' }}</p>
|
||||
|
12
app/helpers/db_helper.rb
Normal file
12
app/helpers/db_helper.rb
Normal file
@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Helpers for database operations
|
||||
module DbHelper
|
||||
# Ruby times are localised and does not have the same precision as database times do comparing them in .where() clauses may
|
||||
# result in unexpected results. This function worksaround this issue by converting the Time to a database-comparable format
|
||||
# @param [Time]
|
||||
# @return [String]
|
||||
def db_time(time)
|
||||
time.utc.strftime('%Y-%m-%d %H:%M:%S.%6N')
|
||||
end
|
||||
end
|
@ -58,7 +58,10 @@ module SettingsHelper
|
||||
space_explications_alert
|
||||
visibility_yearly
|
||||
visibility_others
|
||||
reservation_deadline
|
||||
machine_reservation_deadline
|
||||
training_reservation_deadline
|
||||
event_reservation_deadline
|
||||
space_reservation_deadline
|
||||
display_name_enable
|
||||
machines_sort_by
|
||||
accounting_sales_journal_code
|
||||
|
@ -12,6 +12,8 @@ class Avoir < Invoice
|
||||
|
||||
attr_accessor :invoice_items_ids
|
||||
|
||||
delegate :order_number, to: :invoice
|
||||
|
||||
def generate_reference
|
||||
super(created_at)
|
||||
end
|
||||
|
@ -71,4 +71,8 @@ class CartItem::EventReservation < CartItem::Reservation
|
||||
def total_tickets
|
||||
(normal_tickets || 0) + (cart_item_event_reservation_tickets.map(&:booked).reduce(:+) || 0)
|
||||
end
|
||||
|
||||
def reservation_deadline_minutes
|
||||
Setting.get('event_reservation_deadline').to_i
|
||||
end
|
||||
end
|
||||
|
@ -49,4 +49,8 @@ class CartItem::MachineReservation < CartItem::Reservation
|
||||
machine_credit = plan.machine_credits.find { |credit| credit.creditable_id == reservable.id }
|
||||
credits_hours(machine_credit, new_plan_being_bought: new_subscription)
|
||||
end
|
||||
|
||||
def reservation_deadline_minutes
|
||||
Setting.get('machine_reservation_deadline').to_i
|
||||
end
|
||||
end
|
||||
|
@ -13,18 +13,22 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [Plan,NilClass]
|
||||
def plan
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [User]
|
||||
def operator
|
||||
operator_profile.user
|
||||
end
|
||||
|
||||
# @return [User]
|
||||
def customer
|
||||
customer_profile.user
|
||||
end
|
||||
|
||||
# @return [Hash{Symbol=>Integer,Hash{Symbol=>Array<Hash{Symbol=>Integer,Float,Boolean,Time}>}}]
|
||||
def price
|
||||
is_privileged = operator.privileged? && operator.id != customer.id
|
||||
prepaid = { minutes: PrepaidPackService.minutes_available(customer, reservable) }
|
||||
@ -48,49 +52,33 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
{ elements: elements, amount: amount }
|
||||
end
|
||||
|
||||
# @return [String,NilClass]
|
||||
def name
|
||||
reservable&.name
|
||||
end
|
||||
|
||||
# @param all_items [Array<CartItem::BaseItem>]
|
||||
# @return [Boolean]
|
||||
def valid?(all_items = [])
|
||||
pending_subscription = all_items.find { |i| i.is_a?(CartItem::Subscription) }
|
||||
plan = pending_subscription&.plan || customer&.subscribed_plan
|
||||
|
||||
reservation_deadline_minutes = Setting.get('reservation_deadline').to_i
|
||||
reservation_deadline = reservation_deadline_minutes.minutes.since
|
||||
unless ReservationLimitService.authorized?(plan, customer, self, all_items)
|
||||
errors.add(:reservation, I18n.t('cart_item_validation.limit_reached', {
|
||||
HOURS: ReservationLimitService.limit(plan, reservable).limit,
|
||||
RESERVABLE: reservable.name
|
||||
}))
|
||||
return false
|
||||
end
|
||||
|
||||
cart_item_reservation_slots.each do |sr|
|
||||
slot = sr.slot
|
||||
if slot.nil?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.slot'))
|
||||
return false
|
||||
end
|
||||
|
||||
availability = slot.availability
|
||||
if availability.nil?
|
||||
errors.add(:availability, I18n.t('cart_item_validation.availability'))
|
||||
return false
|
||||
end
|
||||
|
||||
if slot.full?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.full'))
|
||||
return false
|
||||
end
|
||||
|
||||
if slot.start_at < reservation_deadline && !operator.privileged?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline_minutes }))
|
||||
return false
|
||||
end
|
||||
|
||||
next if availability.plan_ids.empty?
|
||||
next if required_subscription?(availability, pending_subscription)
|
||||
|
||||
errors.add(:availability, I18n.t('cart_item_validation.restricted'))
|
||||
return false
|
||||
return false unless validate_slot_reservation(sr, pending_subscription, errors)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# @return [Reservation]
|
||||
def to_object
|
||||
::Reservation.new(
|
||||
reservable_id: reservable_id,
|
||||
@ -107,7 +95,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
end
|
||||
|
||||
# Group the slots by date, if the extended_prices_in_same_day option is set to true
|
||||
# @return Hash{Symbol => Array<CartItem::ReservationSlot>}
|
||||
# @return [Hash{Symbol => Array<CartItem::ReservationSlot>}]
|
||||
def grouped_slots
|
||||
return { all: cart_item_reservation_slots } unless Setting.get('extended_prices_in_same_day')
|
||||
|
||||
@ -125,7 +113,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
# @option options [Boolean] :has_credits true if the user still has credits for the given slot, false if not provided
|
||||
# @option options [Boolean] :is_division false if the slot covers a full availability, true if it is a subdivision (default)
|
||||
# @option options [Number] :prepaid_minutes number of remaining prepaid minutes for the customer
|
||||
# @return [Float]
|
||||
# @return [Float,Integer]
|
||||
def get_slot_price_from_prices(prices, slot_reservation, is_privileged, options = {})
|
||||
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
||||
|
||||
@ -152,7 +140,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
# @option options [Boolean] :has_credits true if the user still has credits for the given slot, false if not provided
|
||||
# @option options [Boolean] :is_division false if the slot covers a full availability, true if it is a subdivision (default)
|
||||
# @option options [Number] :prepaid_minutes number of remaining prepaid minutes for the customer
|
||||
# @return [Float] price of the slot
|
||||
# @return [Float,Integer] price of the slot
|
||||
def get_slot_price(hourly_rate, slot_reservation, is_privileged, options = {})
|
||||
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
||||
|
||||
@ -245,14 +233,55 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
cart_item_reservation_slots.map { |sr| { id: sr.slots_reservation_id, slot_id: sr.slot_id, offered: sr.offered } }
|
||||
end
|
||||
|
||||
##
|
||||
# Check if the given availability requires a valid subscription. If so, check if the current customer
|
||||
# has the required susbcription, otherwise, check if the operator is privileged
|
||||
##
|
||||
# @param availability [Availability]
|
||||
# @param pending_subscription [CartItem::Subscription, NilClass]
|
||||
def required_subscription?(availability, pending_subscription)
|
||||
(customer.subscribed_plan && availability.plan_ids.include?(customer.subscribed_plan.id)) ||
|
||||
(pending_subscription && availability.plan_ids.include?(pending_subscription.plan.id)) ||
|
||||
(operator.manager? && customer.id != operator.id) ||
|
||||
operator.admin?
|
||||
end
|
||||
|
||||
# Gets the deadline in minutes for slots in this reservation
|
||||
# @return [Integer]
|
||||
def reservation_deadline_minutes
|
||||
0
|
||||
end
|
||||
|
||||
# @param reservation_slot [CartItem::ReservationSlot]
|
||||
# @param pending_subscription [CartItem::Subscription, NilClass]
|
||||
# @param errors [ActiveModel::Errors]
|
||||
# @return [Boolean]
|
||||
def validate_slot_reservation(reservation_slot, pending_subscription, errors)
|
||||
slot = reservation_slot.slot
|
||||
if slot.nil?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.slot'))
|
||||
return false
|
||||
end
|
||||
|
||||
availability = slot.availability
|
||||
if availability.nil?
|
||||
errors.add(:availability, I18n.t('cart_item_validation.availability'))
|
||||
return false
|
||||
end
|
||||
|
||||
if slot.full?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.full'))
|
||||
return false
|
||||
end
|
||||
|
||||
if slot.start_at < reservation_deadline_minutes.minutes.since && !operator.privileged?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline_minutes }))
|
||||
return false
|
||||
end
|
||||
|
||||
if availability.plan_ids.any? && !required_subscription?(availability, pending_subscription)
|
||||
errors.add(:availability, I18n.t('cart_item_validation.restricted'))
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
@ -34,4 +34,8 @@ class CartItem::SpaceReservation < CartItem::Reservation
|
||||
space_credit = plan.space_credits.find { |credit| credit.creditable_id == reservable.id }
|
||||
credits_hours(space_credit, new_plan_being_bought: new_subscription)
|
||||
end
|
||||
|
||||
def reservation_deadline_minutes
|
||||
Setting.get('space_reservation_deadline').to_i
|
||||
end
|
||||
end
|
||||
|
@ -45,4 +45,8 @@ class CartItem::TrainingReservation < CartItem::Reservation
|
||||
is_creditable = plan&.training_credits&.select { |credit| credit.creditable_id == reservable&.id }&.any?
|
||||
is_creditable ? plan&.training_credit_nb : 0
|
||||
end
|
||||
|
||||
def reservation_deadline_minutes
|
||||
Setting.get('training_reservation_deadline').to_i
|
||||
end
|
||||
end
|
||||
|
@ -14,8 +14,9 @@ class Invoice < PaymentDocument
|
||||
belongs_to :coupon
|
||||
|
||||
has_one :avoir, class_name: 'Invoice', dependent: :destroy, inverse_of: :avoir
|
||||
has_one :payment_schedule_item, dependent: :nullify
|
||||
has_one :payment_schedule_item, dependent: :restrict_with_error
|
||||
has_one :payment_gateway_object, as: :item, dependent: :destroy
|
||||
has_one :order, dependent: :restrict_with_error
|
||||
belongs_to :operator_profile, class_name: 'InvoicingProfile'
|
||||
|
||||
has_many :accounting_lines, dependent: :destroy
|
||||
@ -49,6 +50,12 @@ class Invoice < PaymentDocument
|
||||
end
|
||||
|
||||
def order_number
|
||||
return order.reference unless order.nil?
|
||||
|
||||
if !payment_schedule_item.nil? && !payment_schedule_item.first?
|
||||
return payment_schedule_item.payment_schedule.ordered_items.first.invoice.order_number
|
||||
end
|
||||
|
||||
PaymentDocumentService.generate_order_number(self)
|
||||
end
|
||||
|
||||
@ -66,8 +73,7 @@ class Invoice < PaymentDocument
|
||||
avoir.attributes = attrs
|
||||
avoir.reference = nil
|
||||
avoir.invoice_id = id
|
||||
# override created_at to compute CA in stats
|
||||
avoir.created_at = avoir.avoir_date
|
||||
avoir.avoir_date = Time.current
|
||||
avoir.total = 0
|
||||
# refunds of invoices with cash coupons: we need to ventilate coupons on paid items
|
||||
paid_items = 0
|
||||
@ -79,7 +85,6 @@ class Invoice < PaymentDocument
|
||||
|
||||
refund_items += 1 unless ii.amount.zero?
|
||||
avoir_ii = avoir.invoice_items.build(ii.dup.attributes)
|
||||
avoir_ii.created_at = avoir.avoir_date
|
||||
avoir_ii.invoice_item_id = ii.id
|
||||
avoir.total += avoir_ii.amount
|
||||
end
|
||||
|
@ -42,6 +42,8 @@ class Machine < ApplicationRecord
|
||||
|
||||
belongs_to :machine_category
|
||||
|
||||
has_many :plan_limitations, dependent: :destroy, inverse_of: :machine, foreign_type: 'limitable_type', foreign_key: 'limitable_id'
|
||||
|
||||
after_create :create_statistic_subtype
|
||||
after_create :create_machine_prices
|
||||
after_create :update_gateway_product
|
||||
|
@ -4,4 +4,5 @@
|
||||
class MachineCategory < ApplicationRecord
|
||||
has_many :machines, dependent: :nullify
|
||||
accepts_nested_attributes_for :machines, allow_destroy: true
|
||||
has_many :plan_limitations, dependent: :destroy, inverse_of: :machine_category, foreign_type: 'limitable_type', foreign_key: 'limitable_id'
|
||||
end
|
||||
|
@ -21,6 +21,9 @@ class Plan < ApplicationRecord
|
||||
has_many :cart_item_subscriptions, class_name: 'CartItem::Subscription', dependent: :destroy
|
||||
has_many :cart_item_payment_schedules, class_name: 'CartItem::PaymentSchedule', dependent: :destroy
|
||||
|
||||
has_many :plan_limitations, dependent: :destroy
|
||||
accepts_nested_attributes_for :plan_limitations, allow_destroy: true
|
||||
|
||||
extend FriendlyId
|
||||
friendly_id :base_name, use: :slugged
|
||||
|
||||
|
20
app/models/plan_limitation.rb
Normal file
20
app/models/plan_limitation.rb
Normal file
@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Allows to set booking limits on some resources, per plan.
|
||||
class PlanLimitation < ApplicationRecord
|
||||
belongs_to :plan
|
||||
|
||||
belongs_to :limitable, polymorphic: true
|
||||
belongs_to :machine, foreign_type: 'Machine', foreign_key: 'limitable_id', inverse_of: :plan_limitations
|
||||
belongs_to :machine_category, foreign_type: 'MachineCategory', foreign_key: 'limitable_id', inverse_of: :plan_limitations
|
||||
|
||||
validates :limitable_id, :limitable_type, :limit, :plan_id, presence: true
|
||||
validates :limitable_id, uniqueness: { scope: %i[limitable_type plan_id] }
|
||||
|
||||
# @return [Array<Machine,Event,Space,Training>]
|
||||
def reservables
|
||||
return limitable.machines if limitable_type == 'MachineCategory'
|
||||
|
||||
[limitable]
|
||||
end
|
||||
end
|
@ -32,6 +32,7 @@ class Reservation < ApplicationRecord
|
||||
after_commit :notify_member_create_reservation, on: :create
|
||||
after_commit :notify_admin_member_create_reservation, on: :create
|
||||
after_commit :extend_subscription, on: :create
|
||||
after_commit :notify_member_limitation_reached, on: :create
|
||||
|
||||
delegate :user, to: :statistic_profile
|
||||
|
||||
@ -137,4 +138,14 @@ class Reservation < ApplicationRecord
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: self
|
||||
end
|
||||
|
||||
def notify_member_limitation_reached
|
||||
date = ReservationLimitService.reached_limit_date(self)
|
||||
return if date.nil?
|
||||
|
||||
NotificationCenter.call type: 'notify_member_reservation_limit_reached',
|
||||
receiver: user,
|
||||
attached_object: ReservationLimitService.limit(user.subscribed_plan, reservable),
|
||||
meta_data: { date: date }
|
||||
end
|
||||
end
|
||||
|
@ -160,6 +160,7 @@ class ShoppingCart
|
||||
|
||||
# Check if the current cart needs the user to have been validated, and if the condition is satisfied.
|
||||
# Return an array of errors, if any; false otherwise
|
||||
# @return [Array<String>,FalseClass]
|
||||
def check_user_validation(items)
|
||||
user_validation_required = Setting.get('user_validation_required')
|
||||
user_validation_required_list = Setting.get('user_validation_required_list')
|
||||
|
@ -6,11 +6,6 @@ class Availabilities::AvailabilitiesService
|
||||
# @param level [String] 'slot' | 'availability'
|
||||
def initialize(current_user, level = 'slot')
|
||||
@current_user = current_user
|
||||
@maximum_visibility = {
|
||||
year: Setting.get('visibility_yearly').to_i.months.since,
|
||||
other: Setting.get('visibility_others').to_i.months.since
|
||||
}
|
||||
@minimum_visibility = Setting.get('reservation_deadline').to_i.minutes.since
|
||||
@level = level
|
||||
end
|
||||
|
||||
@ -119,33 +114,23 @@ class Availabilities::AvailabilitiesService
|
||||
end
|
||||
|
||||
# @param availabilities [ActiveRecord::Relation<Availability>]
|
||||
# @param type [String]
|
||||
# @param type [String] 'training', 'space', 'machines' or 'event'
|
||||
# @param user [User]
|
||||
# @param range_start [ActiveSupport::TimeWithZone]
|
||||
# @param range_end [ActiveSupport::TimeWithZone]
|
||||
# @return ActiveRecord::Relation<Availability>
|
||||
def availabilities(availabilities, type, user, range_start, range_end)
|
||||
# who made the request?
|
||||
# 1) an admin (he can see all availabilities from 1 month ago to anytime in the future)
|
||||
if @current_user&.admin? || @current_user&.manager?
|
||||
window_start = [range_start, 1.month.ago].max
|
||||
availabilities.includes(:tags, :slots)
|
||||
.joins(:slots)
|
||||
.where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', range_end, window_start, type)
|
||||
.where('slots.start_at > ? AND slots.end_at < ?', window_start, range_end)
|
||||
# 2) an user (he cannot see past availabilities neither those further than 1 (or 3) months in the future)
|
||||
else
|
||||
end_at = @maximum_visibility[:other]
|
||||
end_at = @maximum_visibility[:year] if subscription_year?(user) && type != 'training'
|
||||
end_at = @maximum_visibility[:year] if show_more_trainings?(user) && type == 'training'
|
||||
window_end = [end_at, range_end].min
|
||||
window_start = [range_start, @minimum_visibility].max
|
||||
availabilities.includes(:tags, :slots)
|
||||
.joins(:slots)
|
||||
.where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', window_end, window_start, type)
|
||||
.where('slots.start_at > ? AND slots.end_at < ?', window_start, window_end)
|
||||
.where('availability_tags.tag_id' => user&.tag_ids&.concat([nil]))
|
||||
.where(lock: false)
|
||||
window = Availabilities::VisibilityService.new.visibility(@current_user, type, range_start, range_end)
|
||||
qry = availabilities.includes(:tags, :slots)
|
||||
.joins(:slots)
|
||||
.where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', window[1], window[0], type)
|
||||
.where('slots.start_at > ? AND slots.end_at < ?', window[0], window[1])
|
||||
unless @current_user&.privileged?
|
||||
# non priviledged users cannot see availabilities with tags different than their own and locked tags
|
||||
qry = qry.where('availability_tags.tag_id' => user&.tag_ids&.concat([nil]))
|
||||
.where(lock: false)
|
||||
end
|
||||
|
||||
qry
|
||||
end
|
||||
end
|
||||
|
72
app/services/availabilities/visibility_service.rb
Normal file
72
app/services/availabilities/visibility_service.rb
Normal file
@ -0,0 +1,72 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Return the maximum available visibility for a user
|
||||
class Availabilities::VisibilityService
|
||||
def initialize
|
||||
@maximum_visibility = {
|
||||
year: Setting.get('visibility_yearly').to_i.months.since,
|
||||
other: Setting.get('visibility_others').to_i.months.since
|
||||
}
|
||||
@minimum_visibility = {
|
||||
machine: Setting.get('machine_reservation_deadline').to_i.minutes.since,
|
||||
training: Setting.get('training_reservation_deadline').to_i.minutes.since,
|
||||
event: Setting.get('event_reservation_deadline').to_i.minutes.since,
|
||||
space: Setting.get('space_reservation_deadline').to_i.minutes.since
|
||||
}
|
||||
end
|
||||
|
||||
# @param user [User,NilClass]
|
||||
# @param available_type [String] 'training', 'space', 'machines' or 'event'
|
||||
# @param range_start [ActiveSupport::TimeWithZone]
|
||||
# @param range_end [ActiveSupport::TimeWithZone]
|
||||
# @return [Array<ActiveSupport::TimeWithZone,Date,Time>] as: [start,end]
|
||||
def visibility(user, available_type, range_start, range_end)
|
||||
if user&.privileged?
|
||||
window_start = [range_start, 1.month.ago].max
|
||||
window_end = range_end
|
||||
else
|
||||
end_at = @maximum_visibility[:other]
|
||||
end_at = @maximum_visibility[:year] if subscription_year?(user) && available_type != 'training'
|
||||
end_at = @maximum_visibility[:year] if show_more_trainings?(user) && available_type == 'training'
|
||||
end_at = subscription_visibility(user, available_type) || end_at
|
||||
|
||||
minimum_visibility = 0.minutes.since
|
||||
minimum_visibility = @minimum_visibility[:machine] if available_type == 'machines'
|
||||
minimum_visibility = @minimum_visibility[:training] if available_type == 'training'
|
||||
minimum_visibility = @minimum_visibility[:event] if available_type == 'event'
|
||||
minimum_visibility = @minimum_visibility[:space] if available_type == 'space'
|
||||
|
||||
window_end = [end_at, range_end].min
|
||||
window_start = [range_start, minimum_visibility].max
|
||||
end
|
||||
[window_start, window_end]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @param user [User,NilClass]
|
||||
def subscription_year?(user)
|
||||
user&.subscribed_plan &&
|
||||
(user&.subscribed_plan&.interval == 'year' ||
|
||||
(user&.subscribed_plan&.interval == 'month' && user.subscribed_plan.interval_count >= 12))
|
||||
end
|
||||
|
||||
# @param user [User,NilClass]
|
||||
# @param available_type [String] 'training', 'space', 'machine' or 'event'
|
||||
# @return [Time,NilClass]
|
||||
def subscription_visibility(user, available_type)
|
||||
return nil unless user&.subscribed_plan
|
||||
return nil unless available_type == 'machines'
|
||||
|
||||
machines = user&.subscribed_plan&.machines_visibility
|
||||
machines&.hours&.since
|
||||
end
|
||||
|
||||
# members must have validated at least 1 training and must have a valid yearly subscription to view
|
||||
# the trainings further in the futur. This is used to prevent users with a rolling subscription to take
|
||||
# their first training in a very long delay.
|
||||
# @param user [User,NilClass]
|
||||
def show_more_trainings?(user)
|
||||
user&.trainings&.size&.positive? && subscription_year?(user)
|
||||
end
|
||||
end
|
@ -6,10 +6,9 @@ class CartService
|
||||
@operator = operator
|
||||
end
|
||||
|
||||
##
|
||||
# For details about the expected hash format
|
||||
# @see app/frontend/src/javascript/models/payment.ts > interface ShoppingCart
|
||||
##
|
||||
# @return [ShoppingCart]
|
||||
def from_hash(cart_items)
|
||||
cart_items.permit! if cart_items.is_a? ActionController::Parameters
|
||||
|
||||
|
@ -3,10 +3,13 @@
|
||||
# Provides methods to generate Invoice, Avoir or PaymentSchedule references
|
||||
class PaymentDocumentService
|
||||
class << self
|
||||
include DbHelper
|
||||
# @param document [PaymentDocument]
|
||||
# @param date [Time]
|
||||
def generate_reference(document, date: Time.current)
|
||||
pattern = Setting.get('invoice_reference')
|
||||
pattern = Setting.get('invoice_reference').to_s
|
||||
|
||||
reference = replace_invoice_number_pattern(pattern, document.created_at)
|
||||
reference = replace_document_number_pattern(pattern, document, document.created_at)
|
||||
reference = replace_date_pattern(reference, date)
|
||||
|
||||
case document
|
||||
@ -44,15 +47,16 @@ class PaymentDocumentService
|
||||
reference
|
||||
end
|
||||
|
||||
# @param document [PaymentDocument]
|
||||
def generate_order_number(document)
|
||||
pattern = Setting.get('invoice_order-nb')
|
||||
|
||||
# global document number (nn..nn)
|
||||
reference = pattern.gsub(/n+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices(document.is_a?(Order) ? 'order' : 'global'), match.to_s.length)
|
||||
pad_and_truncate(number_of_order('global', document, document.created_at), match.to_s.length)
|
||||
end
|
||||
|
||||
reference = replace_invoice_number_pattern(reference, document.created_at)
|
||||
reference = replace_document_number_pattern(reference, document, document.created_at, :number_of_order)
|
||||
replace_date_pattern(reference, document.created_at)
|
||||
end
|
||||
|
||||
@ -69,25 +73,55 @@ class PaymentDocumentService
|
||||
# Returns the number of current invoices in the given range around the current date.
|
||||
# If range is invalid or not specified, the total number of invoices is returned.
|
||||
# @param range [String] 'day', 'month', 'year'
|
||||
# @param date [Date] the ending date
|
||||
# @param document [PaymentDocument]
|
||||
# @param date [Time] the ending date
|
||||
# @return [Integer]
|
||||
def number_of_invoices(range, date = Time.current)
|
||||
case range.to_s
|
||||
when 'day'
|
||||
start = date.beginning_of_day
|
||||
when 'month'
|
||||
start = date.beginning_of_month
|
||||
when 'year'
|
||||
start = date.beginning_of_year
|
||||
else
|
||||
return get_max_id(Invoice) + get_max_id(PaymentSchedule) + get_max_id(Order)
|
||||
end
|
||||
def number_of_documents(range, document, date = Time.current)
|
||||
start = case range.to_s
|
||||
when 'day'
|
||||
date.beginning_of_day
|
||||
when 'month'
|
||||
date.beginning_of_month
|
||||
when 'year'
|
||||
date.beginning_of_year
|
||||
else
|
||||
nil
|
||||
end
|
||||
ending = date
|
||||
return Invoice.count + PaymentSchedule.count + Order.count unless defined? start
|
||||
|
||||
Invoice.where('created_at >= :start_date AND created_at <= :end_date', start_date: start, end_date: ending).length +
|
||||
PaymentSchedule.where('created_at >= :start_date AND created_at <= :end_date', start_date: start, end_date: ending).length +
|
||||
Order.where('created_at >= :start_date AND created_at <= :end_date', start_date: start, end_date: ending).length
|
||||
documents = document.class.base_class
|
||||
.where('created_at <= :end_date', end_date: db_time(ending))
|
||||
|
||||
documents = documents.where('created_at >= :start_date', start_date: db_time(start)) unless start.nil?
|
||||
|
||||
documents.count
|
||||
end
|
||||
|
||||
def number_of_order(range, _document, date = Time.current)
|
||||
start = case range.to_s
|
||||
when 'day'
|
||||
date.beginning_of_day
|
||||
when 'month'
|
||||
date.beginning_of_month
|
||||
when 'year'
|
||||
date.beginning_of_year
|
||||
else
|
||||
nil
|
||||
end
|
||||
ending = date
|
||||
orders = Order.where('created_at <= :end_date', end_date: db_time(ending))
|
||||
orders = orders.where('created_at >= :start_date', start_date: db_time(start)) unless start.nil?
|
||||
|
||||
schedules = PaymentSchedule.where('created_at <= :end_date', end_date: db_time(ending))
|
||||
schedules = schedules.where('created_at >= :start_date', start_date: db_time(start)) unless start.nil?
|
||||
|
||||
invoices = Invoice.where(type: nil)
|
||||
.where.not(id: orders.map(&:invoice_id))
|
||||
.where.not(id: schedules.map(&:payment_schedule_items).flatten.map(&:invoice_id).filter(&:present?))
|
||||
.where('created_at <= :end_date', end_date: db_time(ending))
|
||||
invoices = invoices.where('created_at >= :start_date', start_date: db_time(start)) unless start.nil?
|
||||
|
||||
orders.count + schedules.count + invoices.count
|
||||
end
|
||||
|
||||
# Replace the date elements in the provided pattern with the date values, from the provided date
|
||||
@ -102,7 +136,9 @@ class PaymentDocumentService
|
||||
copy.gsub!(/(?![^\[]*\])YY(?![^\[]*\])/, date.strftime('%y'))
|
||||
|
||||
# abbreviated month name (MMM)
|
||||
copy.gsub!(/(?![^\[]*\])MMM(?![^\[]*\])/, date.strftime('%^b'))
|
||||
# we cannot replace by the month name directly because it may contrains an M or a D
|
||||
# so we replace it by a special indicator and, at the end, we will replace it by the abbreviated month name
|
||||
copy.gsub!(/(?![^\[]*\])MMM(?![^\[]*\])/, '}~{')
|
||||
# month of the year, zero-padded (MM)
|
||||
copy.gsub!(/(?![^\[]*\])MM(?![^\[]*\])/, date.strftime('%m'))
|
||||
# month of the year, non zero-padded (M)
|
||||
@ -110,40 +146,37 @@ class PaymentDocumentService
|
||||
|
||||
# day of the month, zero-padded (DD)
|
||||
copy.gsub!(/(?![^\[]*\])DD(?![^\[]*\])/, date.strftime('%d'))
|
||||
# day of the month, non zero-padded (DD)
|
||||
copy.gsub!(/(?![^\[]*\])DD(?![^\[]*\])/, date.strftime('%-d'))
|
||||
# day of the month, non zero-padded (D)
|
||||
copy.gsub!(/(?![^\[]*\])D(?![^\[]*\])/, date.strftime('%-d'))
|
||||
|
||||
# abbreviated month name (MMM) (2)
|
||||
copy.gsub!(/(?![^\[]*\])}~\{(?![^\[]*\])/, date.strftime('%^b'))
|
||||
|
||||
copy
|
||||
end
|
||||
|
||||
# Replace the document number elements in the provided pattern with counts from the database
|
||||
# @param reference [String]
|
||||
# @param document [PaymentDocument]
|
||||
# @param date [Time]
|
||||
def replace_invoice_number_pattern(reference, date)
|
||||
# @param count_method [Symbol] :number_of_documents OR :number_of_order
|
||||
def replace_document_number_pattern(reference, document, date, count_method = :number_of_documents)
|
||||
copy = reference.dup
|
||||
|
||||
# document number per year (yy..yy)
|
||||
copy.gsub!(/y+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices('year', date), match.to_s.length)
|
||||
pad_and_truncate(send(count_method, 'year', document, date), match.to_s.length)
|
||||
end
|
||||
# document number per month (mm..mm)
|
||||
copy.gsub!(/m+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices('month', date), match.to_s.length)
|
||||
pad_and_truncate(send(count_method, 'month', document, date), match.to_s.length)
|
||||
end
|
||||
# document number per day (dd..dd)
|
||||
copy.gsub!(/d+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices('day', date), match.to_s.length)
|
||||
pad_and_truncate(send(count_method, 'day', document, date), match.to_s.length)
|
||||
end
|
||||
|
||||
copy
|
||||
end
|
||||
|
||||
##
|
||||
# Return the maximum ID from the database, for the given class
|
||||
# @param klass {ActiveRecord::Base}
|
||||
##
|
||||
def get_max_id(klass)
|
||||
ActiveRecord::Base.connection.execute("SELECT max(id) FROM #{klass.table_name}").getvalue(0, 0) || 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -73,7 +73,7 @@ class PrepaidPackService
|
||||
|
||||
# Total number of prepaid minutes available
|
||||
# @param user [User]
|
||||
# @param priceable [Machine,Space]
|
||||
# @param priceable [Machine,Space,NilClass]
|
||||
def minutes_available(user, priceable)
|
||||
return 0 if Setting.get('pack_only_for_subscription') && !user.subscribed_plan
|
||||
|
||||
|
97
app/services/reservation_limit_service.rb
Normal file
97
app/services/reservation_limit_service.rb
Normal file
@ -0,0 +1,97 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Check if a user if allowed to book a reservation without exceeding the limits set by his plan
|
||||
class ReservationLimitService
|
||||
class << self
|
||||
# @param plan [Plan,NilClass]
|
||||
# @param customer [User]
|
||||
# @param reservation [CartItem::Reservation]
|
||||
# @param cart_items [Array<CartItem::BaseItem>]
|
||||
# @return [Boolean]
|
||||
def authorized?(plan, customer, reservation, cart_items)
|
||||
return true if plan.nil? || !plan.limiting
|
||||
|
||||
return true if reservation.nil? || !reservation.is_a?(CartItem::Reservation)
|
||||
|
||||
limit = limit(plan, reservation.reservable)
|
||||
return true if limit.nil?
|
||||
|
||||
reservation.cart_item_reservation_slots.group_by { |sr| sr.slot.start_at.to_date }.each_pair do |date, reservation_slots|
|
||||
daily_duration = reservations_duration(customer, date, reservation, cart_items) +
|
||||
(reservation_slots.map { |sr| sr.slot.duration }.reduce(:+) || 0)
|
||||
return false if Rational(daily_duration / 3600).to_f > limit.limit
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# @param reservation [Reservation]
|
||||
# @return [Date,NilClass]
|
||||
def reached_limit_date(reservation)
|
||||
user = reservation.user
|
||||
plan = user.subscribed_plan
|
||||
return nil if plan.nil? || !plan.limiting
|
||||
|
||||
limit = limit(plan, reservation.reservable)
|
||||
return nil if limit.nil?
|
||||
|
||||
reservation.slots_reservations.group_by { |sr| sr.slot.start_at.to_date }.each_pair do |date, reservation_slots|
|
||||
daily_duration = saved_reservations_durations(user, reservation.reservable, date, reservation) +
|
||||
(reservation_slots.map { |sr| sr.slot.duration }.reduce(:+) || 0)
|
||||
return date if Rational(daily_duration / 3600).to_f >= limit.limit
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
# @param plan [Plan,NilClass]
|
||||
# @param reservable [Machine,Event,Space,Training]
|
||||
# @return [PlanLimitation] in hours
|
||||
def limit(plan, reservable)
|
||||
return nil unless plan&.limiting
|
||||
|
||||
limitations = plan&.plan_limitations&.filter { |limit| limit.reservables.include?(reservable) }
|
||||
limitations&.find { |limit| limit.limitable_type != 'MachineCategory' } || limitations&.first
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @param customer [User]
|
||||
# @param date [Date]
|
||||
# @param reservation [CartItem::Reservation]
|
||||
# @param cart_items [Array<CartItem::BaseItem>]
|
||||
# @return [Integer] in seconds
|
||||
def reservations_duration(customer, date, reservation, cart_items)
|
||||
daily_reservations_hours = saved_reservations_durations(customer, reservation.reservable, date)
|
||||
|
||||
cart_daily_reservations = cart_items.filter do |item|
|
||||
item.is_a?(CartItem::Reservation) &&
|
||||
item != reservation &&
|
||||
item.reservable == reservation.reservable &&
|
||||
item.cart_item_reservation_slots
|
||||
.includes(:slot)
|
||||
.where("date_trunc('day', slots.start_at) = :date", date: date)
|
||||
end
|
||||
|
||||
daily_reservations_hours +
|
||||
(cart_daily_reservations.map { |r| r.cart_item_reservation_slots.map { |sr| sr.slot.duration } }.flatten.reduce(:+) || 0)
|
||||
end
|
||||
|
||||
# @param customer [User]
|
||||
# @param reservable [Machine,Event,Space,Training]
|
||||
# @param date [Date]
|
||||
# @param reservation [Reservation]
|
||||
# @return [Integer] in seconds
|
||||
def saved_reservations_durations(customer, reservable, date, reservation = nil)
|
||||
daily_reservations = customer.reservations
|
||||
.includes(slots_reservations: :slot)
|
||||
.where(reservable: reservable)
|
||||
.where(slots_reservations: { canceled_at: nil })
|
||||
.where("date_trunc('day', slots.start_at) = :date", date: date)
|
||||
|
||||
daily_reservations = daily_reservations.where.not(id: reservation.id) unless reservation.nil?
|
||||
|
||||
(daily_reservations.map { |r| r.slots_reservations.map { |sr| sr.slot.duration } }.flatten.reduce(:+) || 0)
|
||||
end
|
||||
end
|
||||
end
|
@ -80,7 +80,7 @@ class Trainings::AutoCancelService
|
||||
|
||||
service = WalletService.new(user: reservation.user, wallet: reservation.user.wallet)
|
||||
transaction = service.credit(amount)
|
||||
service.create_avoir(transaction, Time.current, I18n.t('trainings.refund_for_auto_cancel')) if transaction
|
||||
service.create_avoir(transaction, I18n.t('trainings.refund_for_auto_cancel')) if transaction
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -50,11 +50,10 @@ class WalletService
|
||||
end
|
||||
|
||||
## create a refund invoice associated with the given wallet transaction
|
||||
def create_avoir(wallet_transaction, avoir_date, description)
|
||||
def create_avoir(wallet_transaction, description)
|
||||
avoir = Avoir.new
|
||||
avoir.type = 'Avoir'
|
||||
avoir.avoir_date = avoir_date
|
||||
avoir.created_at = avoir_date
|
||||
avoir.avoir_date = Time.current
|
||||
avoir.description = description
|
||||
avoir.payment_method = 'wallet'
|
||||
avoir.subscription_to_expire = false
|
||||
|
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.title notification.notification_type
|
||||
json.description t('.limit_reached',
|
||||
HOURS: notification.attached_object.limit,
|
||||
ITEM: notification.attached_object.limitable.name,
|
||||
DATE: I18n.l(notification.get_meta_data(:date).to_date))
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! plan, :id, :base_name, :name, :interval, :interval_count, :group_id, :training_credit_nb, :is_rolling, :description, :type,
|
||||
:ui_weight, :disabled, :monthly_payment, :plan_category_id
|
||||
:ui_weight, :disabled, :monthly_payment, :plan_category_id, :limiting, :machines_visibility
|
||||
json.amount plan.amount / 100.00
|
||||
json.prices_attributes plan.prices, partial: 'api/prices/price', as: :price
|
||||
if plan.plan_file
|
||||
@ -27,3 +27,7 @@ if plan.advanced_accounting
|
||||
end
|
||||
end
|
||||
|
||||
json.plan_limitations_attributes plan.plan_limitations do |limitation|
|
||||
json.extract! limitation, :id, :limitable_id, :limitable_type, :limit
|
||||
end
|
||||
|
||||
|
@ -1 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.partial! 'api/plans/plan', plan: @plan
|
||||
|
@ -21,3 +21,4 @@ if @amount[:schedule]
|
||||
end
|
||||
end
|
||||
end
|
||||
json.errors @errors
|
||||
|
@ -0,0 +1,9 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<p>
|
||||
<%= t('.body.limit_reached',
|
||||
HOURS: @attached_object.limit,
|
||||
ITEM: @attached_object.limitable.name,
|
||||
DATE: I18n.l(@notification.get_meta_data(:date).to_date)) %>
|
||||
</p>
|
||||
|
@ -2,8 +2,8 @@ de:
|
||||
app:
|
||||
admin:
|
||||
edit_destroy_buttons:
|
||||
deleted: "The {TYPE} was successfully deleted."
|
||||
unable_to_delete: "Unable to delete the {TYPE}: "
|
||||
deleted: "Successfully deleted."
|
||||
unable_to_delete: "Unable to delete: "
|
||||
delete_item: "Delete the {TYPE}"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this {TYPE}?"
|
||||
@ -24,7 +24,7 @@ de:
|
||||
save: "Save"
|
||||
successfully_saved: "Your banner was successfully saved."
|
||||
machine_categories_list:
|
||||
machine_categories: "Machines categories"
|
||||
machine_categories: "Maschinen-Kategorien"
|
||||
add_a_machine_category: "Add a machine category"
|
||||
name: "Name"
|
||||
machines_number: "Number of machines"
|
||||
@ -57,7 +57,7 @@ de:
|
||||
disabled_help: "When disabled, the machine won't be reservable and won't appear by default in the machines list."
|
||||
reservable: "Can this machine be reserved?"
|
||||
reservable_help: "When disabled, the machine will be shown in the default list of machines, but without the reservation button. If you already have created some availability slots for this machine, you may want to remove them: do it from the admin agenda."
|
||||
save: "Save"
|
||||
save: "Speichern"
|
||||
create_success: "The machine was created successfully"
|
||||
update_success: "The machine was updated successfully"
|
||||
training_form:
|
||||
@ -74,7 +74,7 @@ de:
|
||||
associated_machines_help: "If you associate a machine to this training, the members will need to successfully pass this training before being able to reserve the machine."
|
||||
default_seats: "Default number of seats"
|
||||
public_page: "Show in training lists"
|
||||
public_help: "When unchecked, this option will prevent the training from appearing in the trainings list."
|
||||
public_help: "Wenn diese Option deaktiviert ist, wird verhindert, dass das Training in der Trainingliste erscheint."
|
||||
disable_training: "Disable the training"
|
||||
disabled_help: "When disabled, the training won't be reservable and won't appear by default in the trainings list."
|
||||
automatic_cancellation: "Automatic cancellation"
|
||||
@ -90,39 +90,39 @@ de:
|
||||
validation_rule_info: "Define a rule that cancel an authorisation if the machines associated with the training are not reserved for a specific period of time. This rule prevails over the authorisations validity period."
|
||||
validation_rule_switch: "Activate the validation rule"
|
||||
validation_rule_period: "Time limit in months"
|
||||
save: "Save"
|
||||
create_success: "The training was created successfully"
|
||||
update_success: "The training was updated successfully"
|
||||
save: "Speichern"
|
||||
create_success: "Die Schulung wurde erfolgreich erstellt"
|
||||
update_success: "Die Schulung wurde erfolgreich aktualisiert"
|
||||
space_form:
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} space"
|
||||
watch_out_when_creating_a_new_space_its_prices_are_initialized_at_0_for_all_subscriptions: "Watch out! When creating a new space, its prices are initialized at 0 for all subscriptions."
|
||||
consider_changing_its_prices_before_creating_any_reservation_slot: "Consider changing its prices before creating any reservation slot."
|
||||
ACTION_title: "{ACTION, select, create{Neu} other{Aktualisiere den}} Raum"
|
||||
watch_out_when_creating_a_new_space_its_prices_are_initialized_at_0_for_all_subscriptions: "Achtung! Beim Erstellen eines neuen Raums wird sein Preis für alle Abonnements mit 0 angelegt."
|
||||
consider_changing_its_prices_before_creating_any_reservation_slot: "Ändern Sie ggf. die Preise, bevor Sie Reservierungs-Slots erstellen."
|
||||
name: "Name"
|
||||
illustration: "Illustration"
|
||||
description: "Description"
|
||||
characteristics: "Characteristics"
|
||||
attachments: "Attachments"
|
||||
attached_files_pdf: "Attached files (pdf)"
|
||||
add_an_attachment: "Add an attachment"
|
||||
settings: "Settings"
|
||||
default_seats: "Default number of seats"
|
||||
disable_space: "Disable the space"
|
||||
disabled_help: "When disabled, the space won't be reservable and won't appear by default in the spaces list."
|
||||
save: "Save"
|
||||
create_success: "The space was created successfully"
|
||||
update_success: "The space was updated successfully"
|
||||
illustration: "Abbildung"
|
||||
description: "Beschreibung"
|
||||
characteristics: "Eigenschaften"
|
||||
attachments: "Dateianhänge"
|
||||
attached_files_pdf: "Angehängte Dateien (pdf)"
|
||||
add_an_attachment: "Anhang hinzufügen"
|
||||
settings: "Einstellungen"
|
||||
default_seats: "Standardanzahl der Sitze"
|
||||
disable_space: "Raum deaktivieren"
|
||||
disabled_help: "Wenn deaktiviert, ist der Raum nicht reservierbar und erscheint standardmäßig nicht in der Liste der Leerzeichen."
|
||||
save: "Speichern"
|
||||
create_success: "Der Raum wurde erfolgreich erstellt"
|
||||
update_success: "Der Raum wurde erfolgreich aktualisiert"
|
||||
event_form:
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} event"
|
||||
title: "Title"
|
||||
ACTION_title: "{ACTION, select, create{Neue} other{Aktualisiere die}} Veranstaltung"
|
||||
title: "Titel"
|
||||
matching_visual: "Matching visual"
|
||||
description: "Description"
|
||||
attachments: "Attachments"
|
||||
attached_files_pdf: "Attached files (pdf)"
|
||||
add_a_new_file: "Add a new file"
|
||||
event_category: "Event category"
|
||||
event_category: "Veranstaltungskategorie"
|
||||
dates_and_opening_hours: "Dates and opening hours"
|
||||
all_day: "All day"
|
||||
all_day_help: "Will the event last all day or do you want to set times?"
|
||||
all_day_help: "Wird das Ereignis den ganzen Tag dauern oder möchtest du Zeiten festlegen?"
|
||||
start_date: "Start date"
|
||||
end_date: "End date"
|
||||
start_time: "Start time"
|
||||
@ -135,29 +135,36 @@ de:
|
||||
fare_class: "Fare class"
|
||||
price: "Price"
|
||||
seats_available: "Seats available"
|
||||
seats_help: "If you leave this field empty, this event will be available without reservations."
|
||||
event_themes: "Event themes"
|
||||
seats_help: "Wenn sie dieses Feld leer lassen, ist diese Veranstaltung ohne Reservierung."
|
||||
event_themes: "Veranstaltungsthemen"
|
||||
age_range: "Age range"
|
||||
add_price: "Add a price"
|
||||
save: "Save"
|
||||
create_success: "The event was created successfully"
|
||||
events_updated: "{COUNT, plural, =1{One event was} other{{COUNT} Events were}} successfully updated"
|
||||
events_not_updated: "{TOTAL, plural, =1{The event was} other{On {TOTAL} events {COUNT, plural, =1{one was} other{{COUNT} were}}}} not updated."
|
||||
error_deleting_reserved_price: "Unable to remove the requested price because it is associated with some existing reservations"
|
||||
other_error: "An unexpected error occurred while updating the event"
|
||||
create_success: "Die Veranstaltung wurde erfolgreich erstellt"
|
||||
events_updated: "{COUNT, plural, one {}=1{Eine Veranstaltung wurde} other{{COUNT} Veranstaltungen wurden}} erfolgreich aktualisiert"
|
||||
events_not_updated: "{TOTAL, plural, =1{Die Veranstaltung war} other{Auf {TOTAL} Veranstaltungen {COUNT, plural, =1{eins war} other{{COUNT} waren}}}} nicht aktualisiert."
|
||||
error_deleting_reserved_price: "Der angeforderte Preis konnte nicht gelöscht werden, da er mit einigen Reservierungen verknüpft ist"
|
||||
other_error: "Beim Aktualisieren der Veranstaltung ist ein unerwarteter Fehler aufgetreten"
|
||||
recurring:
|
||||
none: "None"
|
||||
every_days: "Every days"
|
||||
every_week: "Every week"
|
||||
every_month: "Every month"
|
||||
every_year: "Every year"
|
||||
every_days: "Täglich"
|
||||
every_week: "Wöchentlich"
|
||||
every_month: "Monatlich"
|
||||
every_year: "Jährlich"
|
||||
plan_form:
|
||||
general_information: "General information"
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} plan"
|
||||
tab_settings: "Einstellungen"
|
||||
tab_usage_limits: "Usage limits"
|
||||
description: "Beschreibung"
|
||||
general_settings: "Generelle Einstellungen"
|
||||
general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods."
|
||||
activation_and_payment: "Subscription activation and payment"
|
||||
name: "Name"
|
||||
name_max_length: "Name length must be less than 24 characters."
|
||||
group: "Group"
|
||||
group: "Gruppe"
|
||||
transversal: "Transversal plan"
|
||||
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups."
|
||||
display: "Display"
|
||||
category: "Category"
|
||||
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
|
||||
number_of_periods: "Number of periods"
|
||||
@ -173,10 +180,9 @@ de:
|
||||
rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought."
|
||||
monthly_payment: "Monthly payment?"
|
||||
monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months."
|
||||
description: "Description"
|
||||
information_sheet: "Information sheet"
|
||||
notified_partner: "Notified partner"
|
||||
new_user: "New user ..."
|
||||
new_user: "New user"
|
||||
alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
|
||||
disabled: "Disable subscription"
|
||||
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it."
|
||||
@ -185,9 +191,40 @@ de:
|
||||
partner_plan: "Partner plan"
|
||||
partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan."
|
||||
partner_created: "The partner was successfully created"
|
||||
ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan"
|
||||
slots_visibility: "Slots visibility"
|
||||
slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings."
|
||||
machines_visibility: "Visibility time limit, in hours (machines)"
|
||||
visibility_minimum: "Visibility cannot be less than 7 hours"
|
||||
save: "Save"
|
||||
create_success: "Plan(s) successfully created. Don't forget to redefine prices."
|
||||
update_success: "The plan was updated successfully"
|
||||
plan_limit_form:
|
||||
usage_limitation: "Limitation of use"
|
||||
usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation."
|
||||
usage_limitation_switch: "Restrict machine reservations to a number of hours per day."
|
||||
new_usage_limitation: "Add a limitation of use"
|
||||
all_limitations: "All limitations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
max_hours_per_day: "Max. hours/day"
|
||||
ongoing_limitations: "Ongoing limitations"
|
||||
saved_limitations: "Saved limitations"
|
||||
cancel: "Cancel this limitation"
|
||||
cancel_deletion: "Cancel"
|
||||
ongoing_deletion: "Ongoing deletion"
|
||||
plan_limit_modal:
|
||||
title: "Manage limitation of use"
|
||||
limit_reservations: "Limit reservations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
categories_info: "If you select all machine categories, the limits will apply across the board."
|
||||
machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten."
|
||||
max_hours_per_day: "Maximum number of reservation hours per day"
|
||||
confirm: "Confirm"
|
||||
partner_modal:
|
||||
title: "Create a new partner"
|
||||
create_partner: "Create the partner"
|
||||
@ -196,16 +233,17 @@ de:
|
||||
email: "Email address"
|
||||
plan_pricing_form:
|
||||
prices: "Prices"
|
||||
about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour."
|
||||
copy_prices_from: "Copy prices from"
|
||||
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
|
||||
machines: "Machines"
|
||||
spaces: "Spaces"
|
||||
update_recurrent_modal:
|
||||
title: "Periodic event update"
|
||||
edit_recurring_event: "You're about to update a periodic event. What do you want to update?"
|
||||
edit_this_event: "Only this event"
|
||||
edit_recurring_event: "Sie bearbeiten eine wiederkehrende Veranstaltung. Was möchten Sie ändern?"
|
||||
edit_this_event: "Nur diese Veranstaltung"
|
||||
edit_this_and_next: "This event and the followings"
|
||||
edit_all: "All events"
|
||||
edit_all: "Alle Veranstaltungen"
|
||||
date_wont_change: "Warning: you have changed the event date. This modification won't be propagated to other occurrences of the periodic event."
|
||||
confirm: "Update the {MODE, select, single{event} other{events}}"
|
||||
advanced_accounting_form:
|
||||
@ -227,7 +265,7 @@ de:
|
||||
subscriptions: "Subscriptions"
|
||||
machine: "Machine reservation"
|
||||
training: "Training reservation"
|
||||
event: "Event reservation"
|
||||
event: "Veranstaltungsreservierung"
|
||||
space: "Space reservation"
|
||||
prepaid_pack: "Pack of prepaid-hours"
|
||||
product: "Product of the store"
|
||||
@ -286,7 +324,7 @@ de:
|
||||
manage_trainings: "Klicke hier, um Schulungen hinzuzufügen oder zu entfernen."
|
||||
number_of_tickets: "Anzahl der Tickets: "
|
||||
adjust_the_opening_hours: "Öffnungszeiten anpassen"
|
||||
to_time: "bis" #eg. from 18:00 to 21:00
|
||||
to_time: "bis" #e.g. from 18:00 to 21:00
|
||||
restrict_options: "Einschränkungsoptionen"
|
||||
restrict_with_labels: "Diesen Slot mit Labels einschränken"
|
||||
restrict_for_subscriptions: "Diesen Slot auf Abonnenten einschränken"
|
||||
@ -508,8 +546,8 @@ de:
|
||||
on_DATE: "am {DATE}"
|
||||
from_DATE: "von {DATE}"
|
||||
from_TIME: "ab {TIME}"
|
||||
to_date: "bis" #eg: from 01/01 to 01/05
|
||||
to_time: "bis" #eg. from 18:00 to 21:00
|
||||
to_date: "bis" #e.g.: from 01/01 to 01/05
|
||||
to_time: "bis" #e.g. from 18:00 to 21:00
|
||||
title: "Titel"
|
||||
dates: "Datum"
|
||||
booking: "Buchung"
|
||||
@ -586,13 +624,13 @@ de:
|
||||
events_settings:
|
||||
title: "Settings"
|
||||
generic_text_block: "Editorial text block"
|
||||
generic_text_block_info: "Displays an editorial block above the list of events visible to members."
|
||||
generic_text_block_info: "Zeigt einen redaktionellen Block oberhalb der für Mitglieder sichtbaren Veranstaltungsliste."
|
||||
generic_text_block_switch: "Display editorial block"
|
||||
cta_switch: "Display a button"
|
||||
cta_label: "Button label"
|
||||
cta_url: "url"
|
||||
save: "Save"
|
||||
update_success: "The events settings were successfully updated"
|
||||
update_success: "Die Einstellungen wurden erfolgreich aktualisiert"
|
||||
#subscriptions, prices, credits and coupons management
|
||||
pricing:
|
||||
pricing_management: "Preisverwaltung"
|
||||
@ -885,8 +923,6 @@ de:
|
||||
deleted_user: "Gelöschter Nutzer"
|
||||
refund_invoice_successfully_created: "Rückerstattungsrechnung erfolgreich erstellt."
|
||||
create_a_refund_on_this_invoice: "Erstelle eine Rückerstattung mit dieser Rechnung"
|
||||
creation_date_for_the_refund: "Erstellungsdatum für die Erstattung"
|
||||
creation_date_is_required: "Erstellungsdatum ist erforderlich."
|
||||
refund_mode: "Erstattungsmodus:"
|
||||
do_you_want_to_disable_the_user_s_subscription: "Möchten Sie das Abonnement des Benutzers deaktivieren:"
|
||||
elements_to_refund: "Erstattungselemente"
|
||||
@ -1453,8 +1489,8 @@ de:
|
||||
statistics: "Statistiken"
|
||||
evolution: "Entwicklung"
|
||||
age_filter: "Altersfilter"
|
||||
from_age: "Von" #eg. from 8 to 40 years old
|
||||
to_age: "bis" #eg. from 8 to 40 years old
|
||||
from_age: "Von" #e.g. from 8 to 40 years old
|
||||
to_age: "bis" #e.g. from 8 to 40 years old
|
||||
start: "Start:"
|
||||
end: "Ende:"
|
||||
custom_filter: "Benutzerderfinierter Filter"
|
||||
@ -1572,7 +1608,10 @@ de:
|
||||
visibility_for_other_members: "Für alle anderen Mitglieder"
|
||||
reservation_deadline: "Prevent last minute booking"
|
||||
reservation_deadline_help: "If you increase the prior period, members won't be able to book a slot X minutes before its start."
|
||||
deadline_minutes: "Prior period (minutes)"
|
||||
machine_deadline_minutes: "Machine prior period (minutes)"
|
||||
training_deadline_minutes: "Training prior period (minutes)"
|
||||
event_deadline_minutes: "Event prior period (minutes)"
|
||||
space_deadline_minutes: "Space prior period (minutes)"
|
||||
ability_for_the_users_to_move_their_reservations: "Möglichkeit für die Benutzer, ihre Reservierungen zu verschieben"
|
||||
reservations_shifting: "Verschiebung von Reservierungen"
|
||||
prior_period_hours: "Vorheriger Zeitraum (Stunden)"
|
||||
@ -2215,8 +2254,6 @@ de:
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
all_products: "All products"
|
||||
create_a_product: "Create a product"
|
||||
successfully_deleted: "The product has been successfully deleted"
|
||||
unable_to_delete: "Unable to delete the product: "
|
||||
filter: "Filter"
|
||||
filter_clear: "Clear all"
|
||||
filter_apply: "Apply"
|
||||
@ -2238,6 +2275,7 @@ de:
|
||||
sort: "Sort:"
|
||||
visible_only: "Visible products only"
|
||||
product_item:
|
||||
product: "product"
|
||||
visible: "visible"
|
||||
hidden: "hidden"
|
||||
stock:
|
||||
@ -2288,12 +2326,13 @@ de:
|
||||
low_stock: "Low stock"
|
||||
threshold_level: "Minimum threshold level"
|
||||
threshold_alert: "Notify me when the threshold is reached"
|
||||
events_history: "Events history"
|
||||
event_type: "Events:"
|
||||
events_history: "Veranstaltungsverlauf"
|
||||
event_type: "Veranstaltungen:"
|
||||
reason: "Reason"
|
||||
stocks: "Stock:"
|
||||
internal: "Private stock"
|
||||
external: "Public stock"
|
||||
edit: "Edit"
|
||||
all: "All types"
|
||||
remaining_stock: "Remaining stock"
|
||||
type_in: "Add"
|
||||
@ -2385,7 +2424,7 @@ de:
|
||||
VAT_rate_machine: "Machine reservation"
|
||||
VAT_rate_space: "Space reservation"
|
||||
VAT_rate_training: "Training reservation"
|
||||
VAT_rate_event: "Event reservation"
|
||||
VAT_rate_event: "Veranstaltungsreservierung"
|
||||
VAT_rate_subscription: "Subscription"
|
||||
VAT_rate_product: "Products (store)"
|
||||
multi_VAT_notice: "<strong>Please note</strong>: The current general rate is {RATE}%. You can define different VAT rates for each category.<br><br>For example, you can override this value, only for machine reservations, by filling in the corresponding field beside. If you don't fill any value, the general rate will apply."
|
||||
|
@ -2,8 +2,8 @@ en:
|
||||
app:
|
||||
admin:
|
||||
edit_destroy_buttons:
|
||||
deleted: "The {TYPE} was successfully deleted."
|
||||
unable_to_delete: "Unable to delete the {TYPE}: "
|
||||
deleted: "Successfully deleted."
|
||||
unable_to_delete: "Unable to delete: "
|
||||
delete_item: "Delete the {TYPE}"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this {TYPE}?"
|
||||
@ -152,12 +152,19 @@ en:
|
||||
every_month: "Every month"
|
||||
every_year: "Every year"
|
||||
plan_form:
|
||||
general_information: "General information"
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} plan"
|
||||
tab_settings: "Settings"
|
||||
tab_usage_limits: "Usage limits"
|
||||
description: "Description"
|
||||
general_settings: "General settings"
|
||||
general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods."
|
||||
activation_and_payment: "Subscription activation and payment"
|
||||
name: "Name"
|
||||
name_max_length: "Name length must be less than 24 characters."
|
||||
group: "Group"
|
||||
transversal: "Transversal plan"
|
||||
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups."
|
||||
display: "Display"
|
||||
category: "Category"
|
||||
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
|
||||
number_of_periods: "Number of periods"
|
||||
@ -173,10 +180,9 @@ en:
|
||||
rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought."
|
||||
monthly_payment: "Monthly payment?"
|
||||
monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months."
|
||||
description: "Description"
|
||||
information_sheet: "Information sheet"
|
||||
notified_partner: "Notified partner"
|
||||
new_user: "New user ..."
|
||||
new_user: "New user"
|
||||
alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
|
||||
disabled: "Disable subscription"
|
||||
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it."
|
||||
@ -185,9 +191,40 @@ en:
|
||||
partner_plan: "Partner plan"
|
||||
partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan."
|
||||
partner_created: "The partner was successfully created"
|
||||
ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan"
|
||||
slots_visibility: "Slots visibility"
|
||||
slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings."
|
||||
machines_visibility: "Visibility time limit, in hours (machines)"
|
||||
visibility_minimum: "Visibility cannot be less than 7 hours"
|
||||
save: "Save"
|
||||
create_success: "Plan(s) successfully created. Don't forget to redefine prices."
|
||||
update_success: "The plan was updated successfully"
|
||||
plan_limit_form:
|
||||
usage_limitation: "Limitation of use"
|
||||
usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation."
|
||||
usage_limitation_switch: "Restrict machine reservations to a number of hours per day."
|
||||
new_usage_limitation: "Add a limitation of use"
|
||||
all_limitations: "All limitations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
max_hours_per_day: "Max. hours/day"
|
||||
ongoing_limitations: "Ongoing limitations"
|
||||
saved_limitations: "Saved limitations"
|
||||
cancel: "Cancel this limitation"
|
||||
cancel_deletion: "Cancel"
|
||||
ongoing_deletion: "Ongoing deletion"
|
||||
plan_limit_modal:
|
||||
title: "Manage limitation of use"
|
||||
limit_reservations: "Limit reservations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
categories_info: "If you select all machine categories, the limits will apply across the board."
|
||||
machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten."
|
||||
max_hours_per_day: "Maximum number of reservation hours per day"
|
||||
confirm: "Confirm"
|
||||
partner_modal:
|
||||
title: "Create a new partner"
|
||||
create_partner: "Create the partner"
|
||||
@ -196,6 +233,7 @@ en:
|
||||
email: "Email address"
|
||||
plan_pricing_form:
|
||||
prices: "Prices"
|
||||
about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour."
|
||||
copy_prices_from: "Copy prices from"
|
||||
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
|
||||
machines: "Machines"
|
||||
@ -286,7 +324,7 @@ en:
|
||||
manage_trainings: "Click here to add or remove trainings."
|
||||
number_of_tickets: "Number of tickets: "
|
||||
adjust_the_opening_hours: "Adjust the opening hours"
|
||||
to_time: "to" #eg. from 18:00 to 21:00
|
||||
to_time: "to" #e.g. from 18:00 to 21:00
|
||||
restrict_options: "Restriction options"
|
||||
restrict_with_labels: "Restrict this slot with labels"
|
||||
restrict_for_subscriptions: "Restrict this slot for subscription users"
|
||||
@ -508,8 +546,8 @@ en:
|
||||
on_DATE: "on {DATE}"
|
||||
from_DATE: "from {DATE}"
|
||||
from_TIME: "from {TIME}"
|
||||
to_date: "to" #eg: from 01/01 to 01/05
|
||||
to_time: "to" #eg. from 18:00 to 21:00
|
||||
to_date: "to" #e.g.: from 01/01 to 01/05
|
||||
to_time: "to" #e.g. from 18:00 to 21:00
|
||||
title: "Title"
|
||||
dates: "Dates"
|
||||
booking: "Booking"
|
||||
@ -885,8 +923,6 @@ en:
|
||||
deleted_user: "Deleted user"
|
||||
refund_invoice_successfully_created: "Refund invoice successfully created."
|
||||
create_a_refund_on_this_invoice: "Create a refund on this invoice"
|
||||
creation_date_for_the_refund: "Creation date for the refund"
|
||||
creation_date_is_required: "Creation date is required."
|
||||
refund_mode: "Refund mode:"
|
||||
do_you_want_to_disable_the_user_s_subscription: "Do you want to disabled the user's subscription:"
|
||||
elements_to_refund: "Elements to refund"
|
||||
@ -1453,8 +1489,8 @@ en:
|
||||
statistics: "Statistics"
|
||||
evolution: "Evolution"
|
||||
age_filter: "Age filter"
|
||||
from_age: "From" #eg. from 8 to 40 years old
|
||||
to_age: "to" #eg. from 8 to 40 years old
|
||||
from_age: "From" #e.g. from 8 to 40 years old
|
||||
to_age: "to" #e.g. from 8 to 40 years old
|
||||
start: "Start:"
|
||||
end: "End:"
|
||||
custom_filter: "Custom filter"
|
||||
@ -1572,7 +1608,10 @@ en:
|
||||
visibility_for_other_members: "For all other members"
|
||||
reservation_deadline: "Prevent last minute booking"
|
||||
reservation_deadline_help: "If you increase the prior period, members won't be able to book a slot X minutes before its start."
|
||||
deadline_minutes: "Prior period (minutes)"
|
||||
machine_deadline_minutes: "Machine prior period (minutes)"
|
||||
training_deadline_minutes: "Training prior period (minutes)"
|
||||
event_deadline_minutes: "Event prior period (minutes)"
|
||||
space_deadline_minutes: "Space prior period (minutes)"
|
||||
ability_for_the_users_to_move_their_reservations: "Ability for the users to move their reservations"
|
||||
reservations_shifting: "Reservations shifting"
|
||||
prior_period_hours: "Prior period (hours)"
|
||||
@ -2215,8 +2254,6 @@ en:
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
all_products: "All products"
|
||||
create_a_product: "Create a product"
|
||||
successfully_deleted: "The product has been successfully deleted"
|
||||
unable_to_delete: "Unable to delete the product: "
|
||||
filter: "Filter"
|
||||
filter_clear: "Clear all"
|
||||
filter_apply: "Apply"
|
||||
@ -2238,6 +2275,7 @@ en:
|
||||
sort: "Sort:"
|
||||
visible_only: "Visible products only"
|
||||
product_item:
|
||||
product: "product"
|
||||
visible: "visible"
|
||||
hidden: "hidden"
|
||||
stock:
|
||||
@ -2294,6 +2332,7 @@ en:
|
||||
stocks: "Stock:"
|
||||
internal: "Private stock"
|
||||
external: "Public stock"
|
||||
edit: "Edit"
|
||||
all: "All types"
|
||||
remaining_stock: "Remaining stock"
|
||||
type_in: "Add"
|
||||
|
@ -2,8 +2,8 @@ es:
|
||||
app:
|
||||
admin:
|
||||
edit_destroy_buttons:
|
||||
deleted: "The {TYPE} was successfully deleted."
|
||||
unable_to_delete: "Unable to delete the {TYPE}: "
|
||||
deleted: "Successfully deleted."
|
||||
unable_to_delete: "Unable to delete: "
|
||||
delete_item: "Delete the {TYPE}"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this {TYPE}?"
|
||||
@ -152,12 +152,19 @@ es:
|
||||
every_month: "Every month"
|
||||
every_year: "Every year"
|
||||
plan_form:
|
||||
general_information: "General information"
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} plan"
|
||||
tab_settings: "Settings"
|
||||
tab_usage_limits: "Usage limits"
|
||||
description: "Description"
|
||||
general_settings: "General settings"
|
||||
general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods."
|
||||
activation_and_payment: "Subscription activation and payment"
|
||||
name: "Name"
|
||||
name_max_length: "Name length must be less than 24 characters."
|
||||
group: "Group"
|
||||
transversal: "Transversal plan"
|
||||
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups."
|
||||
display: "Display"
|
||||
category: "Category"
|
||||
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
|
||||
number_of_periods: "Number of periods"
|
||||
@ -173,10 +180,9 @@ es:
|
||||
rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought."
|
||||
monthly_payment: "Monthly payment?"
|
||||
monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months."
|
||||
description: "Description"
|
||||
information_sheet: "Information sheet"
|
||||
notified_partner: "Notified partner"
|
||||
new_user: "New user ..."
|
||||
new_user: "New user"
|
||||
alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
|
||||
disabled: "Disable subscription"
|
||||
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it."
|
||||
@ -185,9 +191,40 @@ es:
|
||||
partner_plan: "Partner plan"
|
||||
partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan."
|
||||
partner_created: "The partner was successfully created"
|
||||
ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan"
|
||||
slots_visibility: "Slots visibility"
|
||||
slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings."
|
||||
machines_visibility: "Visibility time limit, in hours (machines)"
|
||||
visibility_minimum: "Visibility cannot be less than 7 hours"
|
||||
save: "Save"
|
||||
create_success: "Plan(s) successfully created. Don't forget to redefine prices."
|
||||
update_success: "The plan was updated successfully"
|
||||
plan_limit_form:
|
||||
usage_limitation: "Limitation of use"
|
||||
usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation."
|
||||
usage_limitation_switch: "Restrict machine reservations to a number of hours per day."
|
||||
new_usage_limitation: "Add a limitation of use"
|
||||
all_limitations: "All limitations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
max_hours_per_day: "Max. hours/day"
|
||||
ongoing_limitations: "Ongoing limitations"
|
||||
saved_limitations: "Saved limitations"
|
||||
cancel: "Cancel this limitation"
|
||||
cancel_deletion: "Cancel"
|
||||
ongoing_deletion: "Ongoing deletion"
|
||||
plan_limit_modal:
|
||||
title: "Manage limitation of use"
|
||||
limit_reservations: "Limit reservations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
categories_info: "If you select all machine categories, the limits will apply across the board."
|
||||
machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten."
|
||||
max_hours_per_day: "Maximum number of reservation hours per day"
|
||||
confirm: "Confirm"
|
||||
partner_modal:
|
||||
title: "Create a new partner"
|
||||
create_partner: "Create the partner"
|
||||
@ -196,6 +233,7 @@ es:
|
||||
email: "Email address"
|
||||
plan_pricing_form:
|
||||
prices: "Prices"
|
||||
about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour."
|
||||
copy_prices_from: "Copy prices from"
|
||||
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
|
||||
machines: "Machines"
|
||||
@ -286,7 +324,7 @@ es:
|
||||
manage_trainings: "Click here to add or remove trainings."
|
||||
number_of_tickets: "Número de tickets: "
|
||||
adjust_the_opening_hours: "Ajustar el horario de apertura"
|
||||
to_time: "a" #eg. from 18:00 to 21:00
|
||||
to_time: "a" #e.g. from 18:00 to 21:00
|
||||
restrict_options: "Restriction options"
|
||||
restrict_with_labels: "Restringir este horario con etiquetas"
|
||||
restrict_for_subscriptions: "Restrict this slot for subscription users"
|
||||
@ -508,8 +546,8 @@ es:
|
||||
on_DATE: "on {DATE}"
|
||||
from_DATE: "Desde {DATE}"
|
||||
from_TIME: "Desde {TIME}"
|
||||
to_date: "to" #eg: from 01/01 to 01/05
|
||||
to_time: "to" #eg. from 18:00 to 21:00
|
||||
to_date: "to" #e.g.: from 01/01 to 01/05
|
||||
to_time: "to" #e.g. from 18:00 to 21:00
|
||||
title: "Title"
|
||||
dates: "Dates"
|
||||
booking: "Booking"
|
||||
@ -885,8 +923,6 @@ es:
|
||||
deleted_user: "Usario eliminado"
|
||||
refund_invoice_successfully_created: "Factura de reembolso creada correctamente."
|
||||
create_a_refund_on_this_invoice: "Crear un reembolso en esta factura"
|
||||
creation_date_for_the_refund: "Fecha de creación del reembolso"
|
||||
creation_date_is_required: "Se requiere la fecha de creación."
|
||||
refund_mode: "Modo de reembolso:"
|
||||
do_you_want_to_disable_the_user_s_subscription: "¿Quieres inhabilitar la suscripción del usuario?:"
|
||||
elements_to_refund: "Elementos a reembolsar"
|
||||
@ -1453,8 +1489,8 @@ es:
|
||||
statistics: "Statistics"
|
||||
evolution: "Evolución"
|
||||
age_filter: "Filtro de edad"
|
||||
from_age: "Desde" #eg. from 8 to 40 years old
|
||||
to_age: "a" #eg. from 8 to 40 years old
|
||||
from_age: "Desde" #e.g. from 8 to 40 years old
|
||||
to_age: "a" #e.g. from 8 to 40 years old
|
||||
start: "Principio:"
|
||||
end: "Final:"
|
||||
custom_filter: "Filtro personalizado"
|
||||
@ -1572,7 +1608,10 @@ es:
|
||||
visibility_for_other_members: "Para todos los demás miembros"
|
||||
reservation_deadline: "Prevent last minute booking"
|
||||
reservation_deadline_help: "If you increase the prior period, members won't be able to book a slot X minutes before its start."
|
||||
deadline_minutes: "Prior period (minutes)"
|
||||
machine_deadline_minutes: "Machine prior period (minutes)"
|
||||
training_deadline_minutes: "Training prior period (minutes)"
|
||||
event_deadline_minutes: "Event prior period (minutes)"
|
||||
space_deadline_minutes: "Space prior period (minutes)"
|
||||
ability_for_the_users_to_move_their_reservations: "Capacidad para que los usuarios muevan sus reservas"
|
||||
reservations_shifting: "Cambio de reservas"
|
||||
prior_period_hours: "Período anterior (horas)"
|
||||
@ -2215,8 +2254,6 @@ es:
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
all_products: "All products"
|
||||
create_a_product: "Create a product"
|
||||
successfully_deleted: "The product has been successfully deleted"
|
||||
unable_to_delete: "Unable to delete the product: "
|
||||
filter: "Filter"
|
||||
filter_clear: "Clear all"
|
||||
filter_apply: "Apply"
|
||||
@ -2238,6 +2275,7 @@ es:
|
||||
sort: "Sort:"
|
||||
visible_only: "Visible products only"
|
||||
product_item:
|
||||
product: "product"
|
||||
visible: "visible"
|
||||
hidden: "hidden"
|
||||
stock:
|
||||
@ -2294,6 +2332,7 @@ es:
|
||||
stocks: "Stock:"
|
||||
internal: "Private stock"
|
||||
external: "Public stock"
|
||||
edit: "Edit"
|
||||
all: "All types"
|
||||
remaining_stock: "Remaining stock"
|
||||
type_in: "Add"
|
||||
|
@ -152,12 +152,19 @@ fr:
|
||||
every_month: "Chaque mois"
|
||||
every_year: "Chaque année"
|
||||
plan_form:
|
||||
general_information: "Informations générales"
|
||||
ACTION_title: "{ACTION, select, create{Nouvelle} other{Mettre à jour la}} formule d'abonnement"
|
||||
tab_settings: "Paramètres"
|
||||
tab_usage_limits: "Limites d'utilisation"
|
||||
description: "Description"
|
||||
general_settings: "Paramètres généraux"
|
||||
general_settings_info: "Déterminez à quel groupe cet abonnement est dédié. Définissez également son prix et sa durée en périodes."
|
||||
activation_and_payment: "Activation et paiement de l'abonnement"
|
||||
name: "Nom"
|
||||
name_max_length: "Le nom doit faire moins de 24 caractères."
|
||||
group: "Groupe"
|
||||
transversal: "Abonnement transversal"
|
||||
transversal_help: "Si cette option est cochée, une copie de cette formule d'abonnement sera créée pour chaque groupe actuellement activé."
|
||||
display: "Affichage"
|
||||
category: "Catégorie"
|
||||
category_help: "Les catégories vous permettent de regrouper les formules d'abonnement, sur la vue publique des abonnements."
|
||||
number_of_periods: "Nombre de périodes"
|
||||
@ -173,7 +180,6 @@ fr:
|
||||
rolling_subscription_help: "Un abonnement glissant commencera le jour de la première formation. Sinon, il commencera dès qu'il est acheté."
|
||||
monthly_payment: "Paiement mensuel ?"
|
||||
monthly_payment_help: "Si le paiement mensuel est activé, les membres pourront choisir entre un paiement unique ou un échéancier de paiement échelonné chaque mois."
|
||||
description: "Description"
|
||||
information_sheet: "Fiche descriptive"
|
||||
notified_partner: "Partenaire notifié"
|
||||
new_user: "Nouvel utilisateur ..."
|
||||
@ -185,9 +191,40 @@ fr:
|
||||
partner_plan: "Abonnement partenaire"
|
||||
partner_plan_help: "Vous pouvez vendre des abonnements en partenariat avec un autre organisme. Ce faisant, l'autre entité sera informée lorsqu'un membre s'abonne à cette formule d'abonnement."
|
||||
partner_created: "Le partenaire a bien été créé"
|
||||
ACTION_plan: "{ACTION, select, create{Créer} other{Mettre à jour}} la formule d'abonnement"
|
||||
slots_visibility: "Visibilité des créneaux"
|
||||
slots_visibility_help: "Vous pouvez déterminer combien de temps en avance les abonnés peuvent voir et réserver les créneaux machines. Lorsque ce paramètre est défini, il devient prioritaire sur les paramètres généraux."
|
||||
machines_visibility: "Délai de visibilité, en heures (machines)"
|
||||
visibility_minimum: "La visibilité ne peut pas être inférieure à 7 heures"
|
||||
save: "Enregistrer"
|
||||
create_success: "Création du/des formule(s) d'abonnement réussie(s). N'oubliez pas de redéfinir les tarifs."
|
||||
update_success: "La formule d'abonnement a bien été mise à jour"
|
||||
plan_limit_form:
|
||||
usage_limitation: "Limite d'usage"
|
||||
usage_limitation_info: "Définissez un nombre maximum d'heures de réservation par jour et par catégorie de machine. Les catégories de machines qui n'ont aucun paramètre configuré ne seront soumises à aucune limitation."
|
||||
usage_limitation_switch: "Restreindre les réservations de machines à un certain nombre d'heures par jour."
|
||||
new_usage_limitation: "Ajouter une limite d'usage"
|
||||
all_limitations: "Toutes les limitations"
|
||||
by_category: "Par catégorie de machines"
|
||||
by_machine: "Par machine"
|
||||
category: "Catégorie de machines"
|
||||
machine: "Nom machine"
|
||||
max_hours_per_day: "Max. heures/jour"
|
||||
ongoing_limitations: "Limites en cours"
|
||||
saved_limitations: "Limites enregistrées"
|
||||
cancel: "Annuler cette limite"
|
||||
cancel_deletion: "Annuler"
|
||||
ongoing_deletion: "Suppression en cours"
|
||||
plan_limit_modal:
|
||||
title: "Gérer la limite d'usage"
|
||||
limit_reservations: "Limiter les réservations"
|
||||
by_category: "Par catégorie de machines"
|
||||
by_machine: "Par machine"
|
||||
category: "Catégorie de machines"
|
||||
machine: "Nom machine"
|
||||
categories_info: "Si vous sélectionnez toutes les catégories de machines, les limites s'appliqueront à tous les niveaux."
|
||||
machine_info: "Veuillez noter que si vous avez déjà créé une limite pour la catégorie de machines incluant la machine sélectionnée, elle sera définitivement écrasée."
|
||||
max_hours_per_day: "Nombre maximum d'heures de réservation par jour"
|
||||
confirm: "Confirmer"
|
||||
partner_modal:
|
||||
title: "Créer un nouveau partenaire"
|
||||
create_partner: "Créer le partenaire"
|
||||
@ -196,6 +233,7 @@ fr:
|
||||
email: "Adresse électronique"
|
||||
plan_pricing_form:
|
||||
prices: "Tarifs"
|
||||
about_prices: "Les prix définis ici s'appliqueront aux membres qui s'abonneront à cette formule d'abonnement, pour les machines et les espaces."
|
||||
copy_prices_from: "Copier les prix depuis"
|
||||
copy_prices_from_help: "Cela remplacera tous les prix de cette formule d'abonnement par les prix de la formule sélectionnée"
|
||||
machines: "Machines"
|
||||
@ -286,7 +324,7 @@ fr:
|
||||
manage_trainings: "Cliquez-ici pour ajouter ou supprimer des formations."
|
||||
number_of_tickets: "Nombre de places : "
|
||||
adjust_the_opening_hours: "Ajuster l'horaire"
|
||||
to_time: "à" #eg. from 18:00 to 21:00
|
||||
to_time: "à" #e.g. from 18:00 to 21:00
|
||||
restrict_options: "Options de restriction"
|
||||
restrict_with_labels: "Restreindre ce créneau avec des étiquettes"
|
||||
restrict_for_subscriptions: "Restreindre ce créneau pour les abonnements"
|
||||
@ -508,8 +546,8 @@ fr:
|
||||
on_DATE: "le {DATE}"
|
||||
from_DATE: "du {DATE}"
|
||||
from_TIME: "de {TIME}"
|
||||
to_date: "au" #eg: from 01/01 to 01/05
|
||||
to_time: "à" #eg. from 18:00 to 21:00
|
||||
to_date: "au" #e.g.: from 01/01 to 01/05
|
||||
to_time: "à" #e.g. from 18:00 to 21:00
|
||||
title: "Titre"
|
||||
dates: "Dates"
|
||||
booking: "Réservations"
|
||||
@ -885,8 +923,6 @@ fr:
|
||||
deleted_user: "Utilisateur supprimé"
|
||||
refund_invoice_successfully_created: "La facture d'avoir a bien été créée."
|
||||
create_a_refund_on_this_invoice: "Générer un avoir sur cette facture"
|
||||
creation_date_for_the_refund: "Date d'émission de l'avoir"
|
||||
creation_date_is_required: "La date d'émission est requise."
|
||||
refund_mode: "Mode de remboursement :"
|
||||
do_you_want_to_disable_the_user_s_subscription: "Souhaitez-vous désactiver l'abonnement de l'utilisateur :"
|
||||
elements_to_refund: "Éléments à rembourser"
|
||||
@ -1453,8 +1489,8 @@ fr:
|
||||
statistics: "Statistiques"
|
||||
evolution: "Évolution"
|
||||
age_filter: "Filtre d'âge"
|
||||
from_age: "De" #eg. from 8 to 40 years old
|
||||
to_age: "à" #eg. from 8 to 40 years old
|
||||
from_age: "De" #e.g. from 8 to 40 years old
|
||||
to_age: "à" #e.g. from 8 to 40 years old
|
||||
start: "Début :"
|
||||
end: "Fin :"
|
||||
custom_filter: "Filtre personnalisé"
|
||||
@ -1572,7 +1608,10 @@ fr:
|
||||
visibility_for_other_members: "Pour tous les autres membres"
|
||||
reservation_deadline: "Empêcher les réservations de dernière minute"
|
||||
reservation_deadline_help: "Si vous augmentez le délai préalable, les membres ne pourront pas réserver un créneau X minutes avant le début de celui-ci."
|
||||
deadline_minutes: "Délai préalable (en minutes)"
|
||||
machine_deadline_minutes: "Délai préalable pour les machines (en minutes)"
|
||||
training_deadline_minutes: "Délai préalable pour les formations (en minutes)"
|
||||
event_deadline_minutes: "Délai préalable pour les événements (en minutes)"
|
||||
space_deadline_minutes: "Délai préalable pour les espaces (en minutes)"
|
||||
ability_for_the_users_to_move_their_reservations: "Possibilité pour l'utilisateur de déplacer ses réservations"
|
||||
reservations_shifting: "Déplacement des réservations"
|
||||
prior_period_hours: "Délai préalable (en heures)"
|
||||
@ -2215,8 +2254,6 @@ fr:
|
||||
unexpected_error_occurred: "Une erreur inattendue s'est produite. Veuillez réessayer ultérieurement."
|
||||
all_products: "Tous les produits"
|
||||
create_a_product: "Créer un produit"
|
||||
successfully_deleted: "Le produit a bien été supprimé"
|
||||
unable_to_delete: "Impossible de supprimer le produit : "
|
||||
filter: "Filter"
|
||||
filter_clear: "Tout effacer"
|
||||
filter_apply: "Appliquer"
|
||||
@ -2238,6 +2275,7 @@ fr:
|
||||
sort: "Trier :"
|
||||
visible_only: "Produits visibles uniquement"
|
||||
product_item:
|
||||
product: "produit"
|
||||
visible: "visible"
|
||||
hidden: "caché"
|
||||
stock:
|
||||
@ -2294,6 +2332,7 @@ fr:
|
||||
stocks: "Stock :"
|
||||
internal: "Stock interne"
|
||||
external: "Stock externe"
|
||||
edit: "Modifier"
|
||||
all: "Tous types"
|
||||
remaining_stock: "Stock restant"
|
||||
type_in: "Ajouter"
|
||||
|
@ -2,8 +2,8 @@
|
||||
app:
|
||||
admin:
|
||||
edit_destroy_buttons:
|
||||
deleted: "The {TYPE} was successfully deleted."
|
||||
unable_to_delete: "Unable to delete the {TYPE}: "
|
||||
deleted: "Successfully deleted."
|
||||
unable_to_delete: "Unable to delete: "
|
||||
delete_item: "Delete the {TYPE}"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this {TYPE}?"
|
||||
@ -152,12 +152,19 @@
|
||||
every_month: "Every month"
|
||||
every_year: "Every year"
|
||||
plan_form:
|
||||
general_information: "General information"
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} plan"
|
||||
tab_settings: "Settings"
|
||||
tab_usage_limits: "Usage limits"
|
||||
description: "Description"
|
||||
general_settings: "General settings"
|
||||
general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods."
|
||||
activation_and_payment: "Subscription activation and payment"
|
||||
name: "Name"
|
||||
name_max_length: "Name length must be less than 24 characters."
|
||||
group: "Group"
|
||||
transversal: "Transversal plan"
|
||||
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups."
|
||||
display: "Display"
|
||||
category: "Category"
|
||||
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
|
||||
number_of_periods: "Number of periods"
|
||||
@ -173,10 +180,9 @@
|
||||
rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought."
|
||||
monthly_payment: "Monthly payment?"
|
||||
monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months."
|
||||
description: "Description"
|
||||
information_sheet: "Information sheet"
|
||||
notified_partner: "Notified partner"
|
||||
new_user: "New user ..."
|
||||
new_user: "New user"
|
||||
alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
|
||||
disabled: "Disable subscription"
|
||||
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it."
|
||||
@ -185,9 +191,40 @@
|
||||
partner_plan: "Partner plan"
|
||||
partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan."
|
||||
partner_created: "The partner was successfully created"
|
||||
ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan"
|
||||
slots_visibility: "Slots visibility"
|
||||
slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings."
|
||||
machines_visibility: "Visibility time limit, in hours (machines)"
|
||||
visibility_minimum: "Visibility cannot be less than 7 hours"
|
||||
save: "Save"
|
||||
create_success: "Plan(s) successfully created. Don't forget to redefine prices."
|
||||
update_success: "The plan was updated successfully"
|
||||
plan_limit_form:
|
||||
usage_limitation: "Limitation of use"
|
||||
usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation."
|
||||
usage_limitation_switch: "Restrict machine reservations to a number of hours per day."
|
||||
new_usage_limitation: "Add a limitation of use"
|
||||
all_limitations: "All limitations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
max_hours_per_day: "Max. hours/day"
|
||||
ongoing_limitations: "Ongoing limitations"
|
||||
saved_limitations: "Saved limitations"
|
||||
cancel: "Cancel this limitation"
|
||||
cancel_deletion: "Cancel"
|
||||
ongoing_deletion: "Ongoing deletion"
|
||||
plan_limit_modal:
|
||||
title: "Manage limitation of use"
|
||||
limit_reservations: "Limit reservations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
categories_info: "If you select all machine categories, the limits will apply across the board."
|
||||
machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten."
|
||||
max_hours_per_day: "Maximum number of reservation hours per day"
|
||||
confirm: "Confirm"
|
||||
partner_modal:
|
||||
title: "Create a new partner"
|
||||
create_partner: "Create the partner"
|
||||
@ -196,6 +233,7 @@
|
||||
email: "Email address"
|
||||
plan_pricing_form:
|
||||
prices: "Prices"
|
||||
about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour."
|
||||
copy_prices_from: "Copy prices from"
|
||||
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
|
||||
machines: "Machines"
|
||||
@ -286,7 +324,7 @@
|
||||
manage_trainings: "Klikk her for å legge til eller endre opplæring."
|
||||
number_of_tickets: "Antall billetter: "
|
||||
adjust_the_opening_hours: "Endre åpningstid"
|
||||
to_time: "til" #eg. from 18:00 to 21:00
|
||||
to_time: "til" #e.g. from 18:00 to 21:00
|
||||
restrict_options: "Alternativer for begrensning"
|
||||
restrict_with_labels: "Begrens denne reservasjonen med etiketter"
|
||||
restrict_for_subscriptions: "Begrens denne reservasjoen til medlemmer"
|
||||
@ -508,8 +546,8 @@
|
||||
on_DATE: "{DATE}"
|
||||
from_DATE: "fra {DATE}"
|
||||
from_TIME: "fra {TIME}"
|
||||
to_date: "til" #eg: from 01/01 to 01/05
|
||||
to_time: "til" #eg. from 18:00 to 21:00
|
||||
to_date: "til" #e.g.: from 01/01 to 01/05
|
||||
to_time: "til" #e.g. from 18:00 to 21:00
|
||||
title: "Tittel"
|
||||
dates: "Datoer"
|
||||
booking: "Reservasjon"
|
||||
@ -885,8 +923,6 @@
|
||||
deleted_user: "Slettet bruker"
|
||||
refund_invoice_successfully_created: "Refusjon ble opprettet."
|
||||
create_a_refund_on_this_invoice: "Opprett en refusjon på denne fakturaen"
|
||||
creation_date_for_the_refund: "Refusjonsdato"
|
||||
creation_date_is_required: "Opprettelsesdato er påkrevd."
|
||||
refund_mode: "Refusjonsmodus:"
|
||||
do_you_want_to_disable_the_user_s_subscription: "Ønsker du å deaktivere brukerens abonnement/medlemskap:"
|
||||
elements_to_refund: "Elementer for tilbakebetaling"
|
||||
@ -1453,8 +1489,8 @@
|
||||
statistics: "Statistikk"
|
||||
evolution: "Utvikling"
|
||||
age_filter: "Aldersfilter"
|
||||
from_age: "Fra" #eg. from 8 to 40 years old
|
||||
to_age: "til" #eg. from 8 to 40 years old
|
||||
from_age: "Fra" #e.g. from 8 to 40 years old
|
||||
to_age: "til" #e.g. from 8 to 40 years old
|
||||
start: "Start:"
|
||||
end: "Slutt:"
|
||||
custom_filter: "Egendefinerte filtre"
|
||||
@ -1572,7 +1608,10 @@
|
||||
visibility_for_other_members: "For alle andre medlemmer"
|
||||
reservation_deadline: "Prevent last minute booking"
|
||||
reservation_deadline_help: "If you increase the prior period, members won't be able to book a slot X minutes before its start."
|
||||
deadline_minutes: "Prior period (minutes)"
|
||||
machine_deadline_minutes: "Machine prior period (minutes)"
|
||||
training_deadline_minutes: "Training prior period (minutes)"
|
||||
event_deadline_minutes: "Event prior period (minutes)"
|
||||
space_deadline_minutes: "Space prior period (minutes)"
|
||||
ability_for_the_users_to_move_their_reservations: "Om brukerne kan flytte sine bestillinger"
|
||||
reservations_shifting: "Reservasjonsendringer"
|
||||
prior_period_hours: "Tidligere perioder (timer)"
|
||||
@ -2215,8 +2254,6 @@
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
all_products: "All products"
|
||||
create_a_product: "Create a product"
|
||||
successfully_deleted: "The product has been successfully deleted"
|
||||
unable_to_delete: "Unable to delete the product: "
|
||||
filter: "Filter"
|
||||
filter_clear: "Clear all"
|
||||
filter_apply: "Apply"
|
||||
@ -2238,6 +2275,7 @@
|
||||
sort: "Sort:"
|
||||
visible_only: "Visible products only"
|
||||
product_item:
|
||||
product: "product"
|
||||
visible: "visible"
|
||||
hidden: "hidden"
|
||||
stock:
|
||||
@ -2294,6 +2332,7 @@
|
||||
stocks: "Stock:"
|
||||
internal: "Private stock"
|
||||
external: "Public stock"
|
||||
edit: "Edit"
|
||||
all: "All types"
|
||||
remaining_stock: "Remaining stock"
|
||||
type_in: "Add"
|
||||
|
@ -2,8 +2,8 @@ pt:
|
||||
app:
|
||||
admin:
|
||||
edit_destroy_buttons:
|
||||
deleted: "The {TYPE} was successfully deleted."
|
||||
unable_to_delete: "Unable to delete the {TYPE}: "
|
||||
deleted: "Successfully deleted."
|
||||
unable_to_delete: "Unable to delete: "
|
||||
delete_item: "Delete the {TYPE}"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this {TYPE}?"
|
||||
@ -152,12 +152,19 @@ pt:
|
||||
every_month: "Every month"
|
||||
every_year: "Every year"
|
||||
plan_form:
|
||||
general_information: "General information"
|
||||
ACTION_title: "{ACTION, select, create{New} other{Update the}} plan"
|
||||
tab_settings: "Settings"
|
||||
tab_usage_limits: "Usage limits"
|
||||
description: "Description"
|
||||
general_settings: "General settings"
|
||||
general_settings_info: "Determine to which group this subscription is dedicated. Also set its price and duration in periods."
|
||||
activation_and_payment: "Subscription activation and payment"
|
||||
name: "Name"
|
||||
name_max_length: "Name length must be less than 24 characters."
|
||||
group: "Group"
|
||||
transversal: "Transversal plan"
|
||||
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups."
|
||||
display: "Display"
|
||||
category: "Category"
|
||||
category_help: "Categories allow you to group the subscription plans, on the public view of the subscriptions."
|
||||
number_of_periods: "Number of periods"
|
||||
@ -173,10 +180,9 @@ pt:
|
||||
rolling_subscription_help: "A rolling subscription will begin the day of the first trainings. Otherwise, it will begin as soon as it is bought."
|
||||
monthly_payment: "Monthly payment?"
|
||||
monthly_payment_help: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months."
|
||||
description: "Description"
|
||||
information_sheet: "Information sheet"
|
||||
notified_partner: "Notified partner"
|
||||
new_user: "New user ..."
|
||||
new_user: "New user"
|
||||
alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
|
||||
disabled: "Disable subscription"
|
||||
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it."
|
||||
@ -185,9 +191,40 @@ pt:
|
||||
partner_plan: "Partner plan"
|
||||
partner_plan_help: "You can sell subscriptions in partnership with another organization. By doing so, the other organization will be notified when a member subscribes to this subscription plan."
|
||||
partner_created: "The partner was successfully created"
|
||||
ACTION_plan: "{ACTION, select, create{Create} other{Update}} the plan"
|
||||
slots_visibility: "Slots visibility"
|
||||
slots_visibility_help: "You can determine how far in advance subscribers can view and reserve machine slots. When this setting is set, it takes precedence over the general settings."
|
||||
machines_visibility: "Visibility time limit, in hours (machines)"
|
||||
visibility_minimum: "Visibility cannot be less than 7 hours"
|
||||
save: "Save"
|
||||
create_success: "Plan(s) successfully created. Don't forget to redefine prices."
|
||||
update_success: "The plan was updated successfully"
|
||||
plan_limit_form:
|
||||
usage_limitation: "Limitation of use"
|
||||
usage_limitation_info: "Define a maximum number of reservation hours per day and per machine category. Machine categories that have no parameters configured will not be subject to any limitation."
|
||||
usage_limitation_switch: "Restrict machine reservations to a number of hours per day."
|
||||
new_usage_limitation: "Add a limitation of use"
|
||||
all_limitations: "All limitations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
max_hours_per_day: "Max. hours/day"
|
||||
ongoing_limitations: "Ongoing limitations"
|
||||
saved_limitations: "Saved limitations"
|
||||
cancel: "Cancel this limitation"
|
||||
cancel_deletion: "Cancel"
|
||||
ongoing_deletion: "Ongoing deletion"
|
||||
plan_limit_modal:
|
||||
title: "Manage limitation of use"
|
||||
limit_reservations: "Limit reservations"
|
||||
by_category: "By machines category"
|
||||
by_machine: "By machine"
|
||||
category: "Machines category"
|
||||
machine: "Machine name"
|
||||
categories_info: "If you select all machine categories, the limits will apply across the board."
|
||||
machine_info: "Please note that if you have already created a limitation for the machines category including the selected machine, it will be permanently overwritten."
|
||||
max_hours_per_day: "Maximum number of reservation hours per day"
|
||||
confirm: "Confirm"
|
||||
partner_modal:
|
||||
title: "Create a new partner"
|
||||
create_partner: "Create the partner"
|
||||
@ -196,6 +233,7 @@ pt:
|
||||
email: "Email address"
|
||||
plan_pricing_form:
|
||||
prices: "Prices"
|
||||
about_prices: "The prices defined here will apply to members subscribing to this plan, for machines and spaces. All prices are per hour."
|
||||
copy_prices_from: "Copy prices from"
|
||||
copy_prices_from_help: "This will replace all the prices of this plan with the prices of the selected plan"
|
||||
machines: "Machines"
|
||||
@ -286,7 +324,7 @@ pt:
|
||||
manage_trainings: "Clique aqui para adicionar ou remover treinamentos."
|
||||
number_of_tickets: "Número de vagas: "
|
||||
adjust_the_opening_hours: "Ajustar o horário de funcionamento"
|
||||
to_time: "ás" #eg. from 18:00 to 21:00
|
||||
to_time: "ás" #e.g. from 18:00 to 21:00
|
||||
restrict_options: "Opções de restrição"
|
||||
restrict_with_labels: "Restrinja este slot com etiquetas"
|
||||
restrict_for_subscriptions: "Restringir este slot para os usuários da assinatura"
|
||||
@ -508,8 +546,8 @@ pt:
|
||||
on_DATE: "No {DATE}"
|
||||
from_DATE: "Em {DATE}"
|
||||
from_TIME: "Ás {TIME}"
|
||||
to_date: "ás" #eg: from 01/01 to 01/05
|
||||
to_time: "ás" #eg. from 18:00 to 21:00
|
||||
to_date: "ás" #e.g.: from 01/01 to 01/05
|
||||
to_time: "ás" #e.g. from 18:00 to 21:00
|
||||
title: "Título"
|
||||
dates: "Datas"
|
||||
booking: "Reserva"
|
||||
@ -885,8 +923,6 @@ pt:
|
||||
deleted_user: "Usuário deletado"
|
||||
refund_invoice_successfully_created: "Restituição de fatura criada com sucesso."
|
||||
create_a_refund_on_this_invoice: "Criar restituição de fatura"
|
||||
creation_date_for_the_refund: "Criação de data de restituição"
|
||||
creation_date_is_required: "Data de criação é obrigatório."
|
||||
refund_mode: "Modo de restituição:"
|
||||
do_you_want_to_disable_the_user_s_subscription: "Você deseja desativar a inscrição de usuários:"
|
||||
elements_to_refund: "Elementos para restituição"
|
||||
@ -1453,8 +1489,8 @@ pt:
|
||||
statistics: "Estatísticas"
|
||||
evolution: "Evolução"
|
||||
age_filter: "Filtro de idade"
|
||||
from_age: "Dos" #eg. from 8 to 40 years old
|
||||
to_age: "aos" #eg. from 8 to 40 years old
|
||||
from_age: "Dos" #e.g. from 8 to 40 years old
|
||||
to_age: "aos" #e.g. from 8 to 40 years old
|
||||
start: "Início:"
|
||||
end: "Fim:"
|
||||
custom_filter: "Filtro customizado"
|
||||
@ -1572,7 +1608,10 @@ pt:
|
||||
visibility_for_other_members: "Para todos os outros membros"
|
||||
reservation_deadline: "Impedir a reserva da última hora"
|
||||
reservation_deadline_help: "Se você aumentar o período prévio, os membros não serão capazes de reservar um slot X minutos antes do seu início."
|
||||
deadline_minutes: "Período prévio (minutos)"
|
||||
machine_deadline_minutes: "Machine prior period (minutes)"
|
||||
training_deadline_minutes: "Training prior period (minutes)"
|
||||
event_deadline_minutes: "Event prior period (minutes)"
|
||||
space_deadline_minutes: "Space prior period (minutes)"
|
||||
ability_for_the_users_to_move_their_reservations: "Habilidade para os usuários mover suas reservas"
|
||||
reservations_shifting: "Mudança de reservas"
|
||||
prior_period_hours: "Período anterior (horas)"
|
||||
@ -2215,8 +2254,6 @@ pt:
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
all_products: "All products"
|
||||
create_a_product: "Create a product"
|
||||
successfully_deleted: "The product has been successfully deleted"
|
||||
unable_to_delete: "Unable to delete the product: "
|
||||
filter: "Filter"
|
||||
filter_clear: "Clear all"
|
||||
filter_apply: "Apply"
|
||||
@ -2238,6 +2275,7 @@ pt:
|
||||
sort: "Sort:"
|
||||
visible_only: "Visible products only"
|
||||
product_item:
|
||||
product: "product"
|
||||
visible: "visible"
|
||||
hidden: "hidden"
|
||||
stock:
|
||||
@ -2294,6 +2332,7 @@ pt:
|
||||
stocks: "Stock:"
|
||||
internal: "Private stock"
|
||||
external: "Public stock"
|
||||
edit: "Edit"
|
||||
all: "All types"
|
||||
remaining_stock: "Remaining stock"
|
||||
type_in: "Add"
|
||||
|
@ -2,8 +2,8 @@ zu:
|
||||
app:
|
||||
admin:
|
||||
edit_destroy_buttons:
|
||||
deleted: "crwdns36793:0{TYPE}crwdne36793:0"
|
||||
unable_to_delete: "crwdns36795:0{TYPE}crwdne36795:0"
|
||||
deleted: "crwdns36793:0crwdne36793:0"
|
||||
unable_to_delete: "crwdns36795:0crwdne36795:0"
|
||||
delete_item: "crwdns36797:0{TYPE}crwdne36797:0"
|
||||
confirm_delete: "crwdns36799:0crwdne36799:0"
|
||||
delete_confirmation: "crwdns36801:0{TYPE}crwdne36801:0"
|
||||
@ -152,12 +152,19 @@ zu:
|
||||
every_month: "crwdns31885:0crwdne31885:0"
|
||||
every_year: "crwdns31887:0crwdne31887:0"
|
||||
plan_form:
|
||||
general_information: "crwdns31889:0crwdne31889:0"
|
||||
ACTION_title: "crwdns37397:0ACTION={ACTION}crwdne37397:0"
|
||||
tab_settings: "crwdns37399:0crwdne37399:0"
|
||||
tab_usage_limits: "crwdns37401:0crwdne37401:0"
|
||||
description: "crwdns31931:0crwdne31931:0"
|
||||
general_settings: "crwdns37403:0crwdne37403:0"
|
||||
general_settings_info: "crwdns37405:0crwdne37405:0"
|
||||
activation_and_payment: "crwdns37407:0crwdne37407:0"
|
||||
name: "crwdns31891:0crwdne31891:0"
|
||||
name_max_length: "crwdns31893:0crwdne31893:0"
|
||||
group: "crwdns31895:0crwdne31895:0"
|
||||
transversal: "crwdns31897:0crwdne31897:0"
|
||||
transversal_help: "crwdns31899:0crwdne31899:0"
|
||||
display: "crwdns37409:0crwdne37409:0"
|
||||
category: "crwdns31901:0crwdne31901:0"
|
||||
category_help: "crwdns31903:0crwdne31903:0"
|
||||
number_of_periods: "crwdns31905:0crwdne31905:0"
|
||||
@ -173,7 +180,6 @@ zu:
|
||||
rolling_subscription_help: "crwdns31925:0crwdne31925:0"
|
||||
monthly_payment: "crwdns31927:0crwdne31927:0"
|
||||
monthly_payment_help: "crwdns31929:0crwdne31929:0"
|
||||
description: "crwdns31931:0crwdne31931:0"
|
||||
information_sheet: "crwdns31933:0crwdne31933:0"
|
||||
notified_partner: "crwdns31935:0crwdne31935:0"
|
||||
new_user: "crwdns31937:0crwdne31937:0"
|
||||
@ -185,9 +191,40 @@ zu:
|
||||
partner_plan: "crwdns31949:0crwdne31949:0"
|
||||
partner_plan_help: "crwdns31951:0crwdne31951:0"
|
||||
partner_created: "crwdns31953:0crwdne31953:0"
|
||||
ACTION_plan: "crwdns31955:0ACTION={ACTION}crwdne31955:0"
|
||||
slots_visibility: "crwdns37491:0crwdne37491:0"
|
||||
slots_visibility_help: "crwdns37493:0crwdne37493:0"
|
||||
machines_visibility: "crwdns37495:0crwdne37495:0"
|
||||
visibility_minimum: "crwdns37589:0crwdne37589:0"
|
||||
save: "crwdns37411:0crwdne37411:0"
|
||||
create_success: "crwdns31957:0crwdne31957:0"
|
||||
update_success: "crwdns31959:0crwdne31959:0"
|
||||
plan_limit_form:
|
||||
usage_limitation: "crwdns37413:0crwdne37413:0"
|
||||
usage_limitation_info: "crwdns37415:0crwdne37415:0"
|
||||
usage_limitation_switch: "crwdns37417:0crwdne37417:0"
|
||||
new_usage_limitation: "crwdns37419:0crwdne37419:0"
|
||||
all_limitations: "crwdns37421:0crwdne37421:0"
|
||||
by_category: "crwdns37477:0crwdne37477:0"
|
||||
by_machine: "crwdns37425:0crwdne37425:0"
|
||||
category: "crwdns37427:0crwdne37427:0"
|
||||
machine: "crwdns37429:0crwdne37429:0"
|
||||
max_hours_per_day: "crwdns37431:0crwdne37431:0"
|
||||
ongoing_limitations: "crwdns37433:0crwdne37433:0"
|
||||
saved_limitations: "crwdns37435:0crwdne37435:0"
|
||||
cancel: "crwdns37437:0crwdne37437:0"
|
||||
cancel_deletion: "crwdns37487:0crwdne37487:0"
|
||||
ongoing_deletion: "crwdns37489:0crwdne37489:0"
|
||||
plan_limit_modal:
|
||||
title: "crwdns37445:0crwdne37445:0"
|
||||
limit_reservations: "crwdns37447:0crwdne37447:0"
|
||||
by_category: "crwdns37479:0crwdne37479:0"
|
||||
by_machine: "crwdns37451:0crwdne37451:0"
|
||||
category: "crwdns37453:0crwdne37453:0"
|
||||
machine: "crwdns37455:0crwdne37455:0"
|
||||
categories_info: "crwdns37457:0crwdne37457:0"
|
||||
machine_info: "crwdns37459:0crwdne37459:0"
|
||||
max_hours_per_day: "crwdns37461:0crwdne37461:0"
|
||||
confirm: "crwdns37463:0crwdne37463:0"
|
||||
partner_modal:
|
||||
title: "crwdns31961:0crwdne31961:0"
|
||||
create_partner: "crwdns31963:0crwdne31963:0"
|
||||
@ -196,6 +233,7 @@ zu:
|
||||
email: "crwdns31969:0crwdne31969:0"
|
||||
plan_pricing_form:
|
||||
prices: "crwdns31971:0crwdne31971:0"
|
||||
about_prices: "crwdns37465:0crwdne37465:0"
|
||||
copy_prices_from: "crwdns31973:0crwdne31973:0"
|
||||
copy_prices_from_help: "crwdns31975:0crwdne31975:0"
|
||||
machines: "crwdns31977:0crwdne31977:0"
|
||||
@ -286,7 +324,7 @@ zu:
|
||||
manage_trainings: "crwdns24132:0crwdne24132:0"
|
||||
number_of_tickets: "crwdns24134:0crwdne24134:0"
|
||||
adjust_the_opening_hours: "crwdns24136:0crwdne24136:0"
|
||||
to_time: "crwdns24138:0crwdne24138:0" #eg. from 18:00 to 21:00
|
||||
to_time: "crwdns24138:0crwdne24138:0" #e.g. from 18:00 to 21:00
|
||||
restrict_options: "crwdns24140:0crwdne24140:0"
|
||||
restrict_with_labels: "crwdns24142:0crwdne24142:0"
|
||||
restrict_for_subscriptions: "crwdns24144:0crwdne24144:0"
|
||||
@ -508,8 +546,8 @@ zu:
|
||||
on_DATE: "crwdns24452:0{DATE}crwdne24452:0"
|
||||
from_DATE: "crwdns24454:0{DATE}crwdne24454:0"
|
||||
from_TIME: "crwdns24456:0{TIME}crwdne24456:0"
|
||||
to_date: "crwdns24458:0crwdne24458:0" #eg: from 01/01 to 01/05
|
||||
to_time: "crwdns24460:0crwdne24460:0" #eg. from 18:00 to 21:00
|
||||
to_date: "crwdns24458:0crwdne24458:0" #e.g.: from 01/01 to 01/05
|
||||
to_time: "crwdns24460:0crwdne24460:0" #e.g. from 18:00 to 21:00
|
||||
title: "crwdns24462:0crwdne24462:0"
|
||||
dates: "crwdns24464:0crwdne24464:0"
|
||||
booking: "crwdns24466:0crwdne24466:0"
|
||||
@ -885,8 +923,6 @@ zu:
|
||||
deleted_user: "crwdns25138:0crwdne25138:0"
|
||||
refund_invoice_successfully_created: "crwdns25140:0crwdne25140:0"
|
||||
create_a_refund_on_this_invoice: "crwdns25142:0crwdne25142:0"
|
||||
creation_date_for_the_refund: "crwdns25144:0crwdne25144:0"
|
||||
creation_date_is_required: "crwdns25146:0crwdne25146:0"
|
||||
refund_mode: "crwdns25148:0crwdne25148:0"
|
||||
do_you_want_to_disable_the_user_s_subscription: "crwdns25150:0crwdne25150:0"
|
||||
elements_to_refund: "crwdns25152:0crwdne25152:0"
|
||||
@ -1453,8 +1489,8 @@ zu:
|
||||
statistics: "crwdns26224:0crwdne26224:0"
|
||||
evolution: "crwdns26226:0crwdne26226:0"
|
||||
age_filter: "crwdns26228:0crwdne26228:0"
|
||||
from_age: "crwdns26230:0crwdne26230:0" #eg. from 8 to 40 years old
|
||||
to_age: "crwdns26232:0crwdne26232:0" #eg. from 8 to 40 years old
|
||||
from_age: "crwdns26230:0crwdne26230:0" #e.g. from 8 to 40 years old
|
||||
to_age: "crwdns26232:0crwdne26232:0" #e.g. from 8 to 40 years old
|
||||
start: "crwdns26234:0crwdne26234:0"
|
||||
end: "crwdns26236:0crwdne26236:0"
|
||||
custom_filter: "crwdns26238:0crwdne26238:0"
|
||||
@ -1572,7 +1608,10 @@ zu:
|
||||
visibility_for_other_members: "crwdns26448:0crwdne26448:0"
|
||||
reservation_deadline: "crwdns31751:0crwdne31751:0"
|
||||
reservation_deadline_help: "crwdns36265:0crwdne36265:0"
|
||||
deadline_minutes: "crwdns31753:0crwdne31753:0"
|
||||
machine_deadline_minutes: "crwdns37593:0crwdne37593:0"
|
||||
training_deadline_minutes: "crwdns37595:0crwdne37595:0"
|
||||
event_deadline_minutes: "crwdns37597:0crwdne37597:0"
|
||||
space_deadline_minutes: "crwdns37599:0crwdne37599:0"
|
||||
ability_for_the_users_to_move_their_reservations: "crwdns26450:0crwdne26450:0"
|
||||
reservations_shifting: "crwdns26452:0crwdne26452:0"
|
||||
prior_period_hours: "crwdns26454:0crwdne26454:0"
|
||||
@ -2215,8 +2254,6 @@ zu:
|
||||
unexpected_error_occurred: "crwdns31338:0crwdne31338:0"
|
||||
all_products: "crwdns31340:0crwdne31340:0"
|
||||
create_a_product: "crwdns31342:0crwdne31342:0"
|
||||
successfully_deleted: "crwdns31344:0crwdne31344:0"
|
||||
unable_to_delete: "crwdns31346:0crwdne31346:0"
|
||||
filter: "crwdns31348:0crwdne31348:0"
|
||||
filter_clear: "crwdns31350:0crwdne31350:0"
|
||||
filter_apply: "crwdns31352:0crwdne31352:0"
|
||||
@ -2238,6 +2275,7 @@ zu:
|
||||
sort: "crwdns31380:0crwdne31380:0"
|
||||
visible_only: "crwdns31382:0crwdne31382:0"
|
||||
product_item:
|
||||
product: "crwdns37467:0crwdne37467:0"
|
||||
visible: "crwdns31384:0crwdne31384:0"
|
||||
hidden: "crwdns31386:0crwdne31386:0"
|
||||
stock:
|
||||
@ -2294,6 +2332,7 @@ zu:
|
||||
stocks: "crwdns31480:0crwdne31480:0"
|
||||
internal: "crwdns31482:0crwdne31482:0"
|
||||
external: "crwdns31484:0crwdne31484:0"
|
||||
edit: "crwdns37469:0crwdne37469:0"
|
||||
all: "crwdns31486:0crwdne31486:0"
|
||||
remaining_stock: "crwdns31488:0crwdne31488:0"
|
||||
type_in: "crwdns31490:0crwdne31490:0"
|
||||
|
@ -103,6 +103,7 @@ de:
|
||||
must_accept_terms: "You must accept the terms and conditions"
|
||||
save: "Save"
|
||||
gender_input:
|
||||
label: "Gender"
|
||||
man: "Man"
|
||||
woman: "Woman"
|
||||
change_password:
|
||||
@ -233,8 +234,6 @@ de:
|
||||
credit_label: 'Legen Sie den Betrag der Gutschrift fest'
|
||||
confirm_credit_label: 'Bestätigen Sie den Betrag der Gutschrift'
|
||||
generate_a_refund_invoice: "Erstelle eine Rückerstattungs-Rechnung"
|
||||
creation_date_for_the_refund: "Erstellungsdatum für die Erstattung"
|
||||
creation_date_is_required: "Erstellungsdatum ist erforderlich."
|
||||
description_optional: "Beschreibung (optional):"
|
||||
will_appear_on_the_refund_invoice: "Wird auf der Rückerstattungsrechnung angezeigt."
|
||||
to_credit: 'Guthaben'
|
||||
@ -540,3 +539,6 @@ de:
|
||||
show_reserved_uniq: "Show only slots with reservations"
|
||||
machine:
|
||||
machine_uncategorized: "Uncategorized machines"
|
||||
form_unsaved_list:
|
||||
save_reminder: "Do not forget to save your changes"
|
||||
cancel: "Cancel"
|
||||
|
@ -103,6 +103,7 @@ en:
|
||||
must_accept_terms: "You must accept the terms and conditions"
|
||||
save: "Save"
|
||||
gender_input:
|
||||
label: "Gender"
|
||||
man: "Man"
|
||||
woman: "Woman"
|
||||
change_password:
|
||||
@ -233,8 +234,6 @@ en:
|
||||
credit_label: 'Set the amount to be credited'
|
||||
confirm_credit_label: 'Confirm the amount to be credited'
|
||||
generate_a_refund_invoice: "Generate a refund invoice"
|
||||
creation_date_for_the_refund: "Creation date for the refund"
|
||||
creation_date_is_required: "Creation date is required."
|
||||
description_optional: "Description (optional):"
|
||||
will_appear_on_the_refund_invoice: "Will appear on the refund invoice."
|
||||
to_credit: 'Credit'
|
||||
@ -540,3 +539,6 @@ en:
|
||||
show_reserved_uniq: "Show only slots with reservations"
|
||||
machine:
|
||||
machine_uncategorized: "Uncategorized machines"
|
||||
form_unsaved_list:
|
||||
save_reminder: "Do not forget to save your changes"
|
||||
cancel: "Cancel"
|
||||
|
@ -103,6 +103,7 @@ es:
|
||||
must_accept_terms: "You must accept the terms and conditions"
|
||||
save: "Save"
|
||||
gender_input:
|
||||
label: "Gender"
|
||||
man: "Man"
|
||||
woman: "Woman"
|
||||
change_password:
|
||||
@ -233,8 +234,6 @@ es:
|
||||
credit_label: 'Selecciona la cantidad a creditar'
|
||||
confirm_credit_label: 'Confirma la cantidad a creditar'
|
||||
generate_a_refund_invoice: "Generar informe de devolución"
|
||||
creation_date_for_the_refund: "Fecha de creación del informe de devolución"
|
||||
creation_date_is_required: "Se requiere fecha de creación."
|
||||
description_optional: "Descripción (opcional):"
|
||||
will_appear_on_the_refund_invoice: "Aparecerá en el informe de devolución."
|
||||
to_credit: 'Credito'
|
||||
@ -540,3 +539,6 @@ es:
|
||||
show_reserved_uniq: "Show only slots with reservations"
|
||||
machine:
|
||||
machine_uncategorized: "Uncategorized machines"
|
||||
form_unsaved_list:
|
||||
save_reminder: "Do not forget to save your changes"
|
||||
cancel: "Cancel"
|
||||
|
@ -103,6 +103,7 @@ fr:
|
||||
must_accept_terms: "Vous devez accepter les conditions générales"
|
||||
save: "Enregistrer"
|
||||
gender_input:
|
||||
label: "Genre"
|
||||
man: "Homme"
|
||||
woman: "Femme"
|
||||
change_password:
|
||||
@ -233,8 +234,6 @@ fr:
|
||||
credit_label: 'Indiquez le montant à créditer'
|
||||
confirm_credit_label: 'Confirmez le montant à créditer'
|
||||
generate_a_refund_invoice: "Générer une facture d'avoir"
|
||||
creation_date_for_the_refund: "Date d'émission de l'avoir"
|
||||
creation_date_is_required: "La date d'émission est requise."
|
||||
description_optional: "Description (optionnelle) :"
|
||||
will_appear_on_the_refund_invoice: "Apparaîtra sur la facture de remboursement."
|
||||
to_credit: 'Créditer'
|
||||
@ -540,3 +539,6 @@ fr:
|
||||
show_reserved_uniq: "Afficher uniquement les créneaux avec réservation"
|
||||
machine:
|
||||
machine_uncategorized: "Machines non classés"
|
||||
form_unsaved_list:
|
||||
save_reminder: "N'oubliez pas d'enregistrer vos modifications"
|
||||
cancel: "Annuler"
|
||||
|
@ -103,6 +103,7 @@
|
||||
must_accept_terms: "You must accept the terms and conditions"
|
||||
save: "Save"
|
||||
gender_input:
|
||||
label: "Gender"
|
||||
man: "Man"
|
||||
woman: "Woman"
|
||||
change_password:
|
||||
@ -233,8 +234,6 @@
|
||||
credit_label: 'Velg beløp for kreditering'
|
||||
confirm_credit_label: 'Bekreft beløpet som skal krediteres'
|
||||
generate_a_refund_invoice: "Genererer en refusjons- faktura"
|
||||
creation_date_for_the_refund: "Refusjonsdato"
|
||||
creation_date_is_required: "Opprettelsesdato er påkrevd."
|
||||
description_optional: "Beskrivelse (valgfritt):"
|
||||
will_appear_on_the_refund_invoice: "Vises på refusjonsfakturaen."
|
||||
to_credit: 'Kreditt'
|
||||
@ -540,3 +539,6 @@
|
||||
show_reserved_uniq: "Show only slots with reservations"
|
||||
machine:
|
||||
machine_uncategorized: "Uncategorized machines"
|
||||
form_unsaved_list:
|
||||
save_reminder: "Do not forget to save your changes"
|
||||
cancel: "Cancel"
|
||||
|
@ -103,6 +103,7 @@ pt:
|
||||
must_accept_terms: "Você deve aceitar os termos e condições"
|
||||
save: "Salvar"
|
||||
gender_input:
|
||||
label: "Gender"
|
||||
man: "Homem"
|
||||
woman: "Mulher"
|
||||
change_password:
|
||||
@ -233,8 +234,6 @@ pt:
|
||||
credit_label: 'Digite a quantia a ser creditada'
|
||||
confirm_credit_label: 'Confirme a quantia a ser creditada'
|
||||
generate_a_refund_invoice: "Gerar uma fatura de reembolso"
|
||||
creation_date_for_the_refund: "Data de criação de reembolso"
|
||||
creation_date_is_required: "Data de criação é obrigatório."
|
||||
description_optional: "Descrição (opcional):"
|
||||
will_appear_on_the_refund_invoice: "Aparecerá na fatura de reembolso."
|
||||
to_credit: 'Crédito'
|
||||
@ -540,3 +539,6 @@ pt:
|
||||
show_reserved_uniq: "Show only slots with reservations"
|
||||
machine:
|
||||
machine_uncategorized: "Uncategorized machines"
|
||||
form_unsaved_list:
|
||||
save_reminder: "Do not forget to save your changes"
|
||||
cancel: "Cancel"
|
||||
|
@ -103,6 +103,7 @@ zu:
|
||||
must_accept_terms: "crwdns28700:0crwdne28700:0"
|
||||
save: "crwdns28702:0crwdne28702:0"
|
||||
gender_input:
|
||||
label: "crwdns37601:0crwdne37601:0"
|
||||
man: "crwdns28704:0crwdne28704:0"
|
||||
woman: "crwdns28706:0crwdne28706:0"
|
||||
change_password:
|
||||
@ -233,8 +234,6 @@ zu:
|
||||
credit_label: 'crwdns29096:0crwdne29096:0'
|
||||
confirm_credit_label: 'crwdns29098:0crwdne29098:0'
|
||||
generate_a_refund_invoice: "crwdns29100:0crwdne29100:0"
|
||||
creation_date_for_the_refund: "crwdns29102:0crwdne29102:0"
|
||||
creation_date_is_required: "crwdns29104:0crwdne29104:0"
|
||||
description_optional: "crwdns29106:0crwdne29106:0"
|
||||
will_appear_on_the_refund_invoice: "crwdns29108:0crwdne29108:0"
|
||||
to_credit: 'crwdns29110:0crwdne29110:0'
|
||||
@ -540,3 +539,6 @@ zu:
|
||||
show_reserved_uniq: "crwdns36249:0crwdne36249:0"
|
||||
machine:
|
||||
machine_uncategorized: "crwdns36219:0crwdne36219:0"
|
||||
form_unsaved_list:
|
||||
save_reminder: "crwdns37471:0crwdne37471:0"
|
||||
cancel: "crwdns37473:0crwdne37473:0"
|
||||
|
@ -433,6 +433,8 @@ de:
|
||||
schedule_deadline: "Sie müssen den Scheck zur %{DATE} -Frist einlösen, für den Zeitplan %{REFERENCE}"
|
||||
notify_admin_payment_schedule_transfer_deadline:
|
||||
schedule_deadline: "Sie müssen das Lastschriftverfahren für die %{DATE} -Frist bestätigen, für Zeitplan %{REFERENCE}"
|
||||
notify_member_reservation_limit_reached:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>."
|
||||
notify_admin_user_supporting_document_files_updated:
|
||||
@ -519,6 +521,7 @@ de:
|
||||
availability: "The availaility doesn't exist"
|
||||
full: "The slot is already fully reserved"
|
||||
deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start"
|
||||
limit_reached: "You have reached the booking limit of %{HOURS}H per day for the %{RESERVABLE}, for your current subscription. Please adjust your reservation."
|
||||
restricted: "This availability is restricted for subscribers"
|
||||
plan: "This subscription plan is disabled"
|
||||
plan_group: "This subscription plan is reserved for members of group %{GROUP}"
|
||||
|
0
config/locales/devise.pt.yml
Executable file → Normal file
0
config/locales/devise.pt.yml
Executable file → Normal file
@ -2,62 +2,62 @@
|
||||
zu:
|
||||
devise:
|
||||
confirmations:
|
||||
confirmed: "crwdns3771:0crwdne3771:0"
|
||||
send_instructions: "crwdns3773:0crwdne3773:0"
|
||||
send_paranoid_instructions: "crwdns3775:0crwdne3775:0"
|
||||
confirmed: "crwdns37497:0crwdne37497:0"
|
||||
send_instructions: "crwdns37499:0crwdne37499:0"
|
||||
send_paranoid_instructions: "crwdns37501:0crwdne37501:0"
|
||||
failure:
|
||||
already_authenticated: "crwdns3777:0crwdne3777:0"
|
||||
inactive: "crwdns3779:0crwdne3779:0"
|
||||
invalid: "crwdns3781:0crwdne3781:0"
|
||||
locked: "crwdns3783:0crwdne3783:0"
|
||||
last_attempt: "crwdns3785:0crwdne3785:0"
|
||||
not_found_in_database: "crwdns3787:0crwdne3787:0"
|
||||
timeout: "crwdns3789:0crwdne3789:0"
|
||||
unauthenticated: "crwdns3791:0crwdne3791:0"
|
||||
unconfirmed: "crwdns19573:0crwdne19573:0"
|
||||
already_authenticated: "crwdns37503:0crwdne37503:0"
|
||||
inactive: "crwdns37505:0crwdne37505:0"
|
||||
invalid: "crwdns37507:0crwdne37507:0"
|
||||
locked: "crwdns37509:0crwdne37509:0"
|
||||
last_attempt: "crwdns37511:0crwdne37511:0"
|
||||
not_found_in_database: "crwdns37513:0crwdne37513:0"
|
||||
timeout: "crwdns37515:0crwdne37515:0"
|
||||
unauthenticated: "crwdns37517:0crwdne37517:0"
|
||||
unconfirmed: "crwdns37519:0crwdne37519:0"
|
||||
mailer:
|
||||
confirmation_instructions:
|
||||
action: "crwdns20182:0crwdne20182:0"
|
||||
instruction: "crwdns20184:0crwdne20184:0"
|
||||
subject: "crwdns3799:0crwdne3799:0"
|
||||
action: "crwdns37521:0crwdne37521:0"
|
||||
instruction: "crwdns37523:0crwdne37523:0"
|
||||
subject: "crwdns37525:0crwdne37525:0"
|
||||
reset_password_instructions:
|
||||
action: "crwdns20186:0crwdne20186:0"
|
||||
instruction: "crwdns20188:0crwdne20188:0"
|
||||
ignore_otherwise: "crwdns20190:0crwdne20190:0"
|
||||
subject: "crwdns3807:0crwdne3807:0"
|
||||
action: "crwdns37527:0crwdne37527:0"
|
||||
instruction: "crwdns37529:0crwdne37529:0"
|
||||
ignore_otherwise: "crwdns37531:0crwdne37531:0"
|
||||
subject: "crwdns37533:0crwdne37533:0"
|
||||
unlock_instructions:
|
||||
subject: "crwdns3809:0crwdne3809:0"
|
||||
subject: "crwdns37535:0crwdne37535:0"
|
||||
omniauth_callbacks:
|
||||
failure: "crwdns3811:0%{kind}crwdnd3811:0%{reason}crwdne3811:0"
|
||||
success: "crwdns3813:0%{kind}crwdne3813:0"
|
||||
failure: "crwdns37537:0%{kind}crwdnd37537:0%{reason}crwdne37537:0"
|
||||
success: "crwdns37539:0%{kind}crwdne37539:0"
|
||||
passwords:
|
||||
no_token: "crwdns3815:0crwdne3815:0"
|
||||
send_instructions: "crwdns3817:0crwdne3817:0"
|
||||
send_paranoid_instructions: "crwdns3819:0crwdne3819:0"
|
||||
updated: "crwdns3821:0crwdne3821:0"
|
||||
updated_not_active: "crwdns3823:0crwdne3823:0"
|
||||
no_token: "crwdns37541:0crwdne37541:0"
|
||||
send_instructions: "crwdns37543:0crwdne37543:0"
|
||||
send_paranoid_instructions: "crwdns37545:0crwdne37545:0"
|
||||
updated: "crwdns37547:0crwdne37547:0"
|
||||
updated_not_active: "crwdns37549:0crwdne37549:0"
|
||||
registrations:
|
||||
destroyed: "crwdns3825:0crwdne3825:0"
|
||||
signed_up: "crwdns3827:0crwdne3827:0"
|
||||
signed_up_but_inactive: "crwdns3829:0crwdne3829:0"
|
||||
signed_up_but_locked: "crwdns3831:0crwdne3831:0"
|
||||
signed_up_but_unconfirmed: "crwdns3833:0crwdne3833:0"
|
||||
update_needs_confirmation: "crwdns3835:0crwdne3835:0"
|
||||
updated: "crwdns3837:0crwdne3837:0"
|
||||
destroyed: "crwdns37551:0crwdne37551:0"
|
||||
signed_up: "crwdns37553:0crwdne37553:0"
|
||||
signed_up_but_inactive: "crwdns37555:0crwdne37555:0"
|
||||
signed_up_but_locked: "crwdns37557:0crwdne37557:0"
|
||||
signed_up_but_unconfirmed: "crwdns37559:0crwdne37559:0"
|
||||
update_needs_confirmation: "crwdns37561:0crwdne37561:0"
|
||||
updated: "crwdns37563:0crwdne37563:0"
|
||||
sessions:
|
||||
signed_in: "crwdns3839:0crwdne3839:0"
|
||||
signed_out: "crwdns3841:0crwdne3841:0"
|
||||
signed_in: "crwdns37565:0crwdne37565:0"
|
||||
signed_out: "crwdns37567:0crwdne37567:0"
|
||||
unlocks:
|
||||
send_instructions: "crwdns3843:0crwdne3843:0"
|
||||
send_paranoid_instructions: "crwdns3845:0crwdne3845:0"
|
||||
unlocked: "crwdns3847:0crwdne3847:0"
|
||||
send_instructions: "crwdns37569:0crwdne37569:0"
|
||||
send_paranoid_instructions: "crwdns37571:0crwdne37571:0"
|
||||
unlocked: "crwdns37573:0crwdne37573:0"
|
||||
errors:
|
||||
messages:
|
||||
already_confirmed: "crwdns19575:0crwdne19575:0"
|
||||
confirmation_period_expired: "crwdns3851:0%{period}crwdne3851:0"
|
||||
expired: "crwdns3853:0crwdne3853:0"
|
||||
not_found: "crwdns19577:0crwdne19577:0"
|
||||
not_locked: "crwdns3857:0crwdne3857:0"
|
||||
already_confirmed: "crwdns37575:0crwdne37575:0"
|
||||
confirmation_period_expired: "crwdns37577:0%{period}crwdne37577:0"
|
||||
expired: "crwdns37579:0crwdne37579:0"
|
||||
not_found: "crwdns37581:0crwdne37581:0"
|
||||
not_locked: "crwdns37583:0crwdne37583:0"
|
||||
not_saved:
|
||||
one: "crwdns3859:1%{resource}crwdne3859:1"
|
||||
other: "crwdns3859:5%{count}crwdnd3859:5%{resource}crwdne3859:5"
|
||||
one: "crwdns37585:1%{resource}crwdne37585:1"
|
||||
other: "crwdns37585:5%{count}crwdnd37585:5%{resource}crwdne37585:5"
|
||||
|
@ -433,6 +433,8 @@ en:
|
||||
schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}"
|
||||
notify_admin_payment_schedule_transfer_deadline:
|
||||
schedule_deadline: "You must confirm the bank direct debit for the %{DATE} deadline, for schedule %{REFERENCE}"
|
||||
notify_member_reservation_limit_reached:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>."
|
||||
notify_admin_user_supporting_document_files_updated:
|
||||
@ -519,6 +521,7 @@ en:
|
||||
availability: "The availaility doesn't exist"
|
||||
full: "The slot is already fully reserved"
|
||||
deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start"
|
||||
limit_reached: "You have reached the booking limit of %{HOURS}H per day for the %{RESERVABLE}, for your current subscription. Please adjust your reservation."
|
||||
restricted: "This availability is restricted for subscribers"
|
||||
plan: "This subscription plan is disabled"
|
||||
plan_group: "This subscription plan is reserved for members of group %{GROUP}"
|
||||
|
@ -433,6 +433,8 @@ es:
|
||||
schedule_deadline: "You must cash the check for the %{DATE} deadline, for schedule %{REFERENCE}"
|
||||
notify_admin_payment_schedule_transfer_deadline:
|
||||
schedule_deadline: "You must confirm the bank direct debit for the %{DATE} deadline, for schedule %{REFERENCE}"
|
||||
notify_member_reservation_limit_reached:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
supporting_document_files_uploaded: "Supporting document uploaded by member <strong><em>%{NAME}</strong></em>."
|
||||
notify_admin_user_supporting_document_files_updated:
|
||||
@ -519,6 +521,7 @@ es:
|
||||
availability: "The availaility doesn't exist"
|
||||
full: "The slot is already fully reserved"
|
||||
deadline: "You can't reserve a slot %{MINUTES} minutes prior to its start"
|
||||
limit_reached: "You have reached the booking limit of %{HOURS}H per day for the %{RESERVABLE}, for your current subscription. Please adjust your reservation."
|
||||
restricted: "This availability is restricted for subscribers"
|
||||
plan: "This subscription plan is disabled"
|
||||
plan_group: "This subscription plan is reserved for members of group %{GROUP}"
|
||||
|
@ -433,6 +433,8 @@ fr:
|
||||
schedule_deadline: "Vous devez encaisser le chèque de l'échéance du %{DATE}, pour l'échéancier %{REFERENCE}"
|
||||
notify_admin_payment_schedule_transfer_deadline:
|
||||
schedule_deadline: "Vous devez confirmer le prélèvement bancaire pour l'échéance du %{DATE} , pour l'échéancier %{REFERENCE}"
|
||||
notify_member_reservation_limit_reached:
|
||||
limit_reached: "Pour le %{DATE}, vous avez atteint votre limite quotidienne de %{HOURS} heures de réservation de la %{ITEM}."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
supporting_document_files_uploaded: "Le membre <strong><em>%{NAME}</strong></em> a téléversé un nouveau justificatif."
|
||||
notify_admin_user_supporting_document_files_updated:
|
||||
@ -519,6 +521,7 @@ fr:
|
||||
availability: "La disponibilité n'existe pas"
|
||||
full: "Le créneau est déjà entièrement réservé"
|
||||
deadline: "Vous ne pouvez pas réserver un créneau %{MINUTES} minutes avant son début"
|
||||
limit_reached: "Vous avez atteint la limite de réservation de %{HOURS}H par jour pour la %{RESERVABLE}, pour votre abonnement actuel. Merci d'ajuster votre réservation."
|
||||
restricted: "Cette disponibilité n'est disponible que pour les abonnés"
|
||||
plan: "Cette formule d'abonnement est désactivé"
|
||||
plan_group: "Cette formule d'abonnement est réservée aux membres du groupe %{GROUP}"
|
||||
|
@ -375,6 +375,10 @@ de:
|
||||
remember: "Gemäß Ihrem Zahlungsplan von %{REFERENCE} wurde zum %{DATE} eine Belastung der Karte in Höhe von %{AMOUNT} geplant."
|
||||
date: "Dies ist eine Erinnerung zur Prüfung, ob das Bankkonto erfolgreich belastet werden konnte."
|
||||
confirm: "Bitte bestätigen Sie den Erhalt des Guthabens in Ihrer Zahlungsverwaltung, damit die entsprechende Rechnung generiert werden kann."
|
||||
notify_member_reservation_limit_reached:
|
||||
subject: "Daily reservation limit reached"
|
||||
body:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
subject: "Supporting documents uploaded by a member"
|
||||
body:
|
||||
|
@ -375,6 +375,10 @@ en:
|
||||
remember: "In accordance with your %{REFERENCE} payment schedule, %{AMOUNT} was due to be debited on %{DATE}."
|
||||
date: "This is a reminder to verify that the direct bank debit was successfull."
|
||||
confirm: "Please confirm the receipt of funds in your payment schedule management interface, so that the corresponding invoice will be generated."
|
||||
notify_member_reservation_limit_reached:
|
||||
subject: "Daily reservation limit reached"
|
||||
body:
|
||||
limit_reached: "For %{DATE}, you have reached your daily limit of %{HOURS} hours of %{ITEM} reservation."
|
||||
notify_admin_user_supporting_document_files_created:
|
||||
subject: "Supporting documents uploaded by a member"
|
||||
body:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user