mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
(ui) refactor plan form
Also: add advanced accounting to plans
This commit is contained in:
parent
85fcc71d6b
commit
be8ae01ba4
@ -83,7 +83,8 @@ class API::PlansController < API::ApiController
|
||||
.permit(:base_name, :type, :group_id, :amount, :interval, :interval_count, :is_rolling,
|
||||
:training_credit_nb, :ui_weight, :disabled, :monthly_payment, :description, :plan_category_id,
|
||||
plan_file_attributes: %i[id attachment _destroy],
|
||||
prices_attributes: %i[id amount])
|
||||
prices_attributes: %i[id amount],
|
||||
advanced_accounting_attributes: %i[code analytical_section])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -11,7 +11,7 @@ class API::UsersController < API::ApiController
|
||||
if %w[partner manager].include?(params[:role])
|
||||
@users = User.with_role(params[:role].to_sym).includes(:profile)
|
||||
else
|
||||
head 403
|
||||
head :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { serialize } from 'object-to-formdata';
|
||||
import { User, UserIndexFilter, UserRole } from '../models/user';
|
||||
import { User, MemberIndexFilter, UserRole } from '../models/user';
|
||||
|
||||
export default class MemberAPI {
|
||||
static async list (filters: UserIndexFilter): Promise<Array<User>> {
|
||||
static async list (filters: MemberIndexFilter): Promise<Array<User>> {
|
||||
const res: AxiosResponse<Array<User>> = await apiClient.post('/api/members/list', filters);
|
||||
return res?.data;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Plan, PlansDuration } from '../models/plan';
|
||||
import ApiLib from '../lib/api';
|
||||
|
||||
export default class PlanAPI {
|
||||
static async index (): Promise<Array<Plan>> {
|
||||
@ -12,4 +13,29 @@ export default class PlanAPI {
|
||||
const res: AxiosResponse<Array<PlansDuration>> = await apiClient.get('/api/plans/durations');
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async create (plan: Plan): Promise<Plan> {
|
||||
const data = ApiLib.serializeAttachments(plan, 'plan', ['plan_file_attributes']);
|
||||
const res: AxiosResponse<Plan> = await apiClient.post('/api/plans', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async update (plan: Plan): Promise<Plan> {
|
||||
const data = ApiLib.serializeAttachments(plan, 'plan', ['plan_file_attributes']);
|
||||
const res: AxiosResponse<Plan> = await apiClient.put(`/api/plans/${plan.id}`, data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async get (id: number): Promise<Plan> {
|
||||
const res: AxiosResponse<Plan> = await apiClient.get(`/api/plans/${id}`);
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
21
app/frontend/src/javascript/api/user.ts
Normal file
21
app/frontend/src/javascript/api/user.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import ApiLib from '../lib/api';
|
||||
import { UserIndexFilter, User } from '../models/user';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Partner } from '../models/plan';
|
||||
|
||||
export default class UserAPI {
|
||||
static async index (filters: UserIndexFilter): Promise<Array<User>> {
|
||||
const res: AxiosResponse<Array<User>> = await apiClient.get(`/api/users${ApiLib.filtersToQuery(filters)}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async create (user: Partner|User): Promise<User> {
|
||||
const data = {
|
||||
user: user as Partner,
|
||||
manager: user as User
|
||||
};
|
||||
const res: AxiosResponse<User> = await apiClient.post('/api/users', data);
|
||||
return res?.data;
|
||||
}
|
||||
}
|
46
app/frontend/src/javascript/components/base/fab-tabs.tsx
Normal file
46
app/frontend/src/javascript/components/base/fab-tabs.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { ReactNode, useEffect, useState } from 'react';
|
||||
|
||||
type tabId = string|number;
|
||||
|
||||
interface Tab {
|
||||
id: tabId,
|
||||
title: ReactNode,
|
||||
content: ReactNode,
|
||||
onSelected?: () => void,
|
||||
}
|
||||
|
||||
interface FabTabsProps {
|
||||
tabs: Array<Tab>,
|
||||
defaultTab?: tabId,
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Tabulation system
|
||||
*/
|
||||
export const FabTabs: React.FC<FabTabsProps> = ({ tabs, defaultTab, className }) => {
|
||||
const [active, setActive] = useState<Tab>(tabs.filter(Boolean).find(t => t.id === defaultTab) || tabs.filter(Boolean)[0]);
|
||||
|
||||
useEffect(() => {
|
||||
setActive(tabs.filter(Boolean).find(t => t.id === defaultTab) || tabs.filter(Boolean)[0]);
|
||||
}, [tabs]);
|
||||
|
||||
/**
|
||||
* Callback triggered when a tab a selected
|
||||
*/
|
||||
const onTabSelected = (tab: Tab) => {
|
||||
setActive(tab);
|
||||
if (typeof tab.onSelected === 'function') tab.onSelected();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`fab-tabs ${className || ''}`}>
|
||||
<div className="tabs">
|
||||
{tabs.filter(Boolean).map((tab, index) => (
|
||||
<p key={index} className={active?.id === tab.id ? 'is-active' : ''} onClick={() => onTabSelected(tab)}>{tab.title}</p>
|
||||
))}
|
||||
</div>
|
||||
{active?.content}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -23,7 +23,7 @@ interface FormFileUploadProps<TFieldValues> extends FormComponent<TFieldValues>,
|
||||
/**
|
||||
* This component allows to upload file, in forms managed by react-hook-form.
|
||||
*/
|
||||
export const FormFileUpload = <TFieldValues extends FieldValues>({ id, register, defaultFile, className, rules, disabled, error, warning, formState, onFileChange, onFileRemove, accept, setValue }: FormFileUploadProps<TFieldValues>) => {
|
||||
export const FormFileUpload = <TFieldValues extends FieldValues>({ id, label, register, defaultFile, className, rules, disabled, error, warning, formState, onFileChange, onFileRemove, accept, setValue }: FormFileUploadProps<TFieldValues>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [file, setFile] = useState<FileType>(defaultFile);
|
||||
@ -72,7 +72,7 @@ export const FormFileUpload = <TFieldValues extends FieldValues>({ id, register,
|
||||
const placeholder = (): string => hasFile() ? t('app.shared.form_file_upload.edit') : t('app.shared.form_file_upload.browse');
|
||||
|
||||
return (
|
||||
<div className={`form-file-upload ${classNames}`}>
|
||||
<div className={`form-file-upload ${label ? 'with-label' : ''} ${classNames}`}>
|
||||
{hasFile() && (
|
||||
<span>{file.attachment_name}</span>
|
||||
)}
|
||||
@ -89,6 +89,7 @@ export const FormFileUpload = <TFieldValues extends FieldValues>({ id, register,
|
||||
className="image-file-input"
|
||||
accept={accept}
|
||||
register={register}
|
||||
label={label}
|
||||
formState={formState}
|
||||
rules={rules}
|
||||
disabled={disabled}
|
||||
|
@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { Partner } from '../../models/plan';
|
||||
import UserAPI from '../../api/user';
|
||||
import { User } from '../../models/user';
|
||||
import { FormInput } from '../form/form-input';
|
||||
|
||||
interface PartnerModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
onError: (message: string) => void,
|
||||
onPartnerCreated: (partner: User) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal dialog to add o new user with role 'partner'
|
||||
*/
|
||||
export const PartnerModal: React.FC<PartnerModalProps> = ({ isOpen, toggleModal, onError, onPartnerCreated }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
const { handleSubmit, register, formState } = useForm<Partner>();
|
||||
|
||||
/**
|
||||
* Callback triggered when the user validates the partner form: create the partner on the API
|
||||
*/
|
||||
const onSubmit: SubmitHandler<Partner> = (data: Partner) => {
|
||||
UserAPI.create(data).then(onPartnerCreated).catch(onError);
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal isOpen={isOpen}
|
||||
title={t('app.admin.partner_modal.title')}
|
||||
toggleModal={toggleModal}
|
||||
confirmButton={t('app.admin.partner_modal.create_partner')}
|
||||
onConfirmSendFormId="partner-form"
|
||||
closeButton>
|
||||
<form onSubmit={handleSubmit(onSubmit)} id="partner-form">
|
||||
<FormInput register={register}
|
||||
label={t('app.admin.partner_modal.first_name')}
|
||||
id="first_name"
|
||||
rules={{ required: true }}
|
||||
formState={formState} />
|
||||
<FormInput register={register}
|
||||
label={t('app.admin.partner_modal.surname')}
|
||||
id="last_name"
|
||||
rules={{ required: true }}
|
||||
formState={formState} />
|
||||
<FormInput register={register}
|
||||
label={t('app.admin.partner_modal.email')}
|
||||
id="email"
|
||||
rules={{ required: true }}
|
||||
formState={formState} />
|
||||
</form>
|
||||
</FabModal>
|
||||
);
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
|
||||
import { Plan } from '../../models/plan';
|
||||
import { Interval, Plan } from '../../models/plan';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import PlanAPI from '../../api/plan';
|
||||
@ -12,6 +12,17 @@ import GroupAPI from '../../api/group';
|
||||
import { SelectOption } from '../../models/select';
|
||||
import { FormSelect } from '../form/form-select';
|
||||
import { FormSwitch } from '../form/form-switch';
|
||||
import PlanCategoryAPI from '../../api/plan-category';
|
||||
import FormatLib from '../../lib/format';
|
||||
import { FabAlert } from '../base/fab-alert';
|
||||
import { FormRichText } from '../form/form-rich-text';
|
||||
import { FormFileUpload } from '../form/form-file-upload';
|
||||
import UserAPI from '../../api/user';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { UserPlus } from 'phosphor-react';
|
||||
import { PartnerModal } from './partner-modal';
|
||||
import { PlanPricingForm } from './plan-pricing-form';
|
||||
import { AdvancedAccountingForm } from '../accounting/advanced-accounting-form';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -27,16 +38,25 @@ interface PlanFormProps {
|
||||
*/
|
||||
export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuccess }) => {
|
||||
const { handleSubmit, register, control, formState, setValue } = useForm<Plan>({ defaultValues: { ...plan } });
|
||||
const _output = useWatch<Plan>({ control }); // eslint-disable-line
|
||||
const output = useWatch<Plan>({ control }); // eslint-disable-line
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [groups, setGroups] = useState<Array<SelectOption<number>>>(null);
|
||||
const [categories, setCategories] = useState<Array<SelectOption<number>>>(null);
|
||||
const [allGroups, setAllGroups] = useState<boolean>(false);
|
||||
const [partners, setPartners] = useState<Array<SelectOption<number>>>(null);
|
||||
const [isOpenPartnerModal, setIsOpenPartnerModal] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
GroupAPI.index({ disabled: false })
|
||||
.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 }; })))
|
||||
.catch(onError);
|
||||
UserAPI.index({ role: 'partner' })
|
||||
.then(res => setPartners(res.map(p => { return { value: p.id, label: p.name }; })))
|
||||
.catch(onError);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
@ -63,26 +83,187 @@ export const PlanForm: React.FC<PlanFormProps> = ({ action, plan, onError, onSuc
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggere when the user switches the 'partner plan' button.
|
||||
*/
|
||||
const handlePartnershipChange = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setValue('type', 'PartnerPlan');
|
||||
} else {
|
||||
setValue('type', 'Plan');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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}`) }; });
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user changes the period of the current plan
|
||||
*/
|
||||
const handlePeriodUpdate = (period: Interval) => {
|
||||
if (period === 'week') {
|
||||
setValue('monthly_payment', false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Open/closes the partner creation modal
|
||||
*/
|
||||
const tooglePartnerModal = () => {
|
||||
setIsOpenPartnerModal(!isOpenPartnerModal);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when a user with role partner was created in the dedicated modal form
|
||||
*/
|
||||
const handleNewPartner = (user) => {
|
||||
tooglePartnerModal();
|
||||
onSuccess(t('app.admin.plan_form.partner_created'));
|
||||
partners.push({ value: user.id, label: user.name });
|
||||
setValue('partner_id', user.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="plan-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')} />
|
||||
<FormSwitch control={control}
|
||||
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}
|
||||
control={control}
|
||||
rules={{ required: !allGroups }}
|
||||
label={t('app.admin.plan_form.group')}
|
||||
id="group_id" />}
|
||||
</form>
|
||||
<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 && <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"
|
||||
addOn={FormatLib.currencySymbol()}
|
||||
rules={{ required: true }}
|
||||
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"
|
||||
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">
|
||||
<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>
|
||||
<h4>{t('app.admin.plan_form.partnership')}</h4>
|
||||
<div className="partnership">
|
||||
<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" />
|
||||
{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' }}
|
||||
label={t('app.admin.plan_form.notified_partner')} />}
|
||||
{output.partner_id && <FabAlert level="info">
|
||||
{t('app.admin.plan_form.alert_partner_notification')}
|
||||
</FabAlert>}
|
||||
</div>}
|
||||
</div>
|
||||
<AdvancedAccountingForm register={register} onError={onError} />
|
||||
{action === 'update' && <PlanPricingForm 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>
|
||||
</form>
|
||||
<PartnerModal isOpen={isOpenPartnerModal}
|
||||
toggleModal={tooglePartnerModal}
|
||||
onError={onError}
|
||||
onPartnerCreated={handleNewPartner} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,121 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFieldArray, UseFormRegister } from 'react-hook-form';
|
||||
import { Control, FormState, UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
import { Plan } from '../../models/plan';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { Machine } from '../../models/machine';
|
||||
import { Space } from '../../models/space';
|
||||
import SettingAPI from '../../api/setting';
|
||||
import { SettingName } from '../../models/setting';
|
||||
import MachineAPI from '../../api/machine';
|
||||
import SpaceAPI from '../../api/space';
|
||||
import { Price } from '../../models/price';
|
||||
import FormatLib from '../../lib/format';
|
||||
import { FabTabs } from '../base/fab-tabs';
|
||||
import PlanAPI from '../../api/plan';
|
||||
import { FormSelect } from '../form/form-select';
|
||||
import { SelectOption } from '../../models/select';
|
||||
|
||||
interface PlanPricingFormProps<TContext extends object> {
|
||||
register: UseFormRegister<Plan>,
|
||||
control: Control<Plan, TContext>
|
||||
formState: FormState<Plan>,
|
||||
setValue: UseFormSetValue<Plan>,
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-form to define prices for machines and spaces, for the current plan
|
||||
*/
|
||||
export const PlanPricingForm = <TContext extends object>({ register, control, formState, setValue, onError }: PlanPricingFormProps<TContext>) => {
|
||||
const { t } = useTranslation('admin');
|
||||
const { fields } = useFieldArray({ control, name: 'prices_attributes' });
|
||||
|
||||
const [machines, setMachines] = useState<Array<Machine>>(null);
|
||||
const [spaces, setSpaces] = useState<Array<Space>>(null);
|
||||
const [settings, setSettings] = useState<Map<SettingName, string>>(null);
|
||||
const [plans, setPlans] = useState<Array<SelectOption<number>>>(null);
|
||||
|
||||
useEffect(() => {
|
||||
SettingAPI.query(['spaces_module', 'machines_module']).then(setSettings).catch(onError);
|
||||
PlanAPI.index()
|
||||
.then(res => setPlans(res.map(p => { return { value: p.id, label: p.name }; })))
|
||||
.catch(onError);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings?.get('machines_module') === 'true') {
|
||||
MachineAPI.index().then(setMachines).catch(onError);
|
||||
}
|
||||
if (settings?.get('spaces_module') === 'true') {
|
||||
SpaceAPI.index().then(setSpaces).catch(onError);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
/**
|
||||
* Copy prices from the selected plan
|
||||
*/
|
||||
const handleCopyPrices = (planId: number) => {
|
||||
PlanAPI.get(planId).then(parent => {
|
||||
parent.prices_attributes.forEach(price => {
|
||||
const index = fields.findIndex(p => p.priceable_type === price.priceable_type && p.priceable_id === price.priceable_id);
|
||||
setValue(`prices_attributes.${index}.amount`, price.amount);
|
||||
});
|
||||
}).catch(onError);
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the form element for the given price
|
||||
*/
|
||||
const renderPriceElement = (price: Price, index: number) => {
|
||||
const item: Space | Machine = (price.priceable_type === 'Machine' && machines?.find(m => m.id === price.priceable_id)) ||
|
||||
(price.priceable_type === 'Space' && spaces?.find(s => s.id === price.priceable_id));
|
||||
if (!item?.disabled) {
|
||||
return (
|
||||
<div key={index}>
|
||||
<FormInput register={register}
|
||||
id={`prices_attributes.${index}.id`}
|
||||
formState={formState}
|
||||
type="hidden" />
|
||||
<FormInput register={register}
|
||||
label={item?.name}
|
||||
id={`prices_attributes.${index}.amount`}
|
||||
rules={{ required: true, min: 0 }}
|
||||
step={0.01}
|
||||
formState={formState}
|
||||
type="number"
|
||||
addOn={FormatLib.currencySymbol()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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.filter(p => p.priceable_type === 'Machine').map((price, index) =>
|
||||
renderPriceElement(price, index)
|
||||
)
|
||||
},
|
||||
spaces && {
|
||||
id: 'spaces',
|
||||
title: t('app.admin.plan_pricing_form.spaces'),
|
||||
content: fields.filter(p => p.priceable_type === 'Space').map((price, index) =>
|
||||
renderPriceElement(price, index)
|
||||
)
|
||||
}
|
||||
]} />}
|
||||
</>
|
||||
);
|
||||
};
|
@ -24,6 +24,7 @@ import { SelectOption, ChecklistOption } from '../../models/select';
|
||||
import { FormMultiFileUpload } from '../form/form-multi-file-upload';
|
||||
import { FormMultiImageUpload } from '../form/form-multi-image-upload';
|
||||
import { AdvancedAccountingForm } from '../accounting/advanced-accounting-form';
|
||||
import { FabTabs } from '../base/fab-tabs';
|
||||
|
||||
interface ProductFormProps {
|
||||
product: Product,
|
||||
@ -44,7 +45,6 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
||||
const [isActivePrice, setIsActivePrice] = useState<boolean>(product.id && _.isFinite(product.amount));
|
||||
const [productCategories, setProductCategories] = useState<SelectOption<number, string | JSX.Element>[]>([]);
|
||||
const [machines, setMachines] = useState<ChecklistOption<number>[]>([]);
|
||||
const [stockTab, setStockTab] = useState<boolean>(false);
|
||||
const [openCloneModal, setOpenCloneModal] = useState<boolean>(false);
|
||||
const [saving, setSaving] = useState<boolean>(false);
|
||||
|
||||
@ -149,6 +149,157 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
||||
setOpenCloneModal(!openCloneModal);
|
||||
};
|
||||
|
||||
/**
|
||||
* This function render the content of the 'products settings' tab
|
||||
*/
|
||||
const renderSettingsTab = () => (
|
||||
<section>
|
||||
<div className="subgrid">
|
||||
<FormInput id="name"
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
formState={formState}
|
||||
onChange={handleNameChange}
|
||||
label={t('app.admin.store.product_form.name')}
|
||||
className="span-7" />
|
||||
<FormInput id="sku"
|
||||
register={register}
|
||||
formState={formState}
|
||||
label={t('app.admin.store.product_form.sku')}
|
||||
className="span-3" />
|
||||
</div>
|
||||
<div className="subgrid">
|
||||
<FormInput id="slug"
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
formState={formState}
|
||||
label={t('app.admin.store.product_form.slug')}
|
||||
className='span-7' />
|
||||
<FormSwitch control={control}
|
||||
id="is_active"
|
||||
formState={formState}
|
||||
label={t('app.admin.store.product_form.is_show_in_store')}
|
||||
tooltip={t('app.admin.store.product_form.active_price_info')}
|
||||
onChange={handleIsActiveChanged}
|
||||
className='span-3' />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div className="price-data">
|
||||
<div className="header-switch">
|
||||
<h4>{t('app.admin.store.product_form.price_and_rule_of_selling_product')}</h4>
|
||||
<FormSwitch control={control}
|
||||
id="is_active_price"
|
||||
label={t('app.admin.store.product_form.is_active_price')}
|
||||
defaultValue={isActivePrice}
|
||||
onChange={toggleIsActivePrice} />
|
||||
</div>
|
||||
{isActivePrice && <div className="price-data-content">
|
||||
<FormInput id="amount"
|
||||
type="number"
|
||||
register={register}
|
||||
rules={{ required: isActivePrice, min: 0 }}
|
||||
step={0.01}
|
||||
formState={formState}
|
||||
label={t('app.admin.store.product_form.price')}
|
||||
nullable />
|
||||
<FormInput id="quantity_min"
|
||||
type="number"
|
||||
rules={{ required: true }}
|
||||
register={register}
|
||||
formState={formState}
|
||||
label={t('app.admin.store.product_form.quantity_min')} />
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h4>{t('app.admin.store.product_form.product_images')}</h4>
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.store.product_form.product_images_info" />
|
||||
</FabAlert>
|
||||
<FormMultiImageUpload setValue={setValue}
|
||||
addButtonLabel={t('app.admin.store.product_form.add_product_image')}
|
||||
register={register}
|
||||
control={control}
|
||||
id="product_images_attributes"
|
||||
className="product-images" />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h4>{t('app.admin.store.product_form.assigning_category')}</h4>
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.store.product_form.assigning_category_info" />
|
||||
</FabAlert>
|
||||
<FormSelect options={productCategories}
|
||||
control={control}
|
||||
id="product_category_id"
|
||||
formState={formState}
|
||||
label={t('app.admin.store.product_form.linking_product_to_category')} />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h4>{t('app.admin.store.product_form.assigning_machines')}</h4>
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.store.product_form.assigning_machines_info" />
|
||||
</FabAlert>
|
||||
<FormChecklist options={machines}
|
||||
control={control}
|
||||
id="machine_ids"
|
||||
formState={formState} />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h4>{t('app.admin.store.product_form.product_description')}</h4>
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.store.product_form.product_description_info" />
|
||||
</FabAlert>
|
||||
<FormRichText control={control}
|
||||
heading
|
||||
bulletList
|
||||
blockquote
|
||||
link
|
||||
limit={6000}
|
||||
id="description" />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h4>{t('app.admin.store.product_form.product_files')}</h4>
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.store.product_form.product_files_info" />
|
||||
</FabAlert>
|
||||
<FormMultiFileUpload setValue={setValue}
|
||||
addButtonLabel={t('app.admin.store.product_form.add_product_file')}
|
||||
control={control}
|
||||
accept="application/pdf"
|
||||
register={register}
|
||||
id="product_files_attributes"
|
||||
className="product-documents" />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<AdvancedAccountingForm register={register} onError={onError} />
|
||||
|
||||
<div className="main-actions">
|
||||
<FabButton type="submit" className="main-action-btn" disabled={saving}>
|
||||
{!saving && t('app.admin.store.product_form.save')}
|
||||
{saving && <i className="fa fa-spinner fa-pulse fa-fw" />}
|
||||
</FabButton>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header>
|
||||
@ -168,158 +319,24 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
||||
</header>
|
||||
<form className="product-form" onSubmit={handleSubmit(onSubmit)}>
|
||||
<UnsavedFormAlert uiRouter={uiRouter} formState={formState} />
|
||||
<div className='tabs'>
|
||||
<p className={!stockTab ? 'is-active' : ''} onClick={() => setStockTab(false)}>{t('app.admin.store.product_form.product_parameters')}</p>
|
||||
<p className={stockTab ? 'is-active' : ''} onClick={() => setStockTab(true)}>{t('app.admin.store.product_form.stock_management')}</p>
|
||||
</div>
|
||||
{stockTab
|
||||
? <ProductStockForm currentFormValues={output as Product} register={register} control={control} formState={formState} setValue={setValue} onError={onError} onSuccess={onSuccess} />
|
||||
: <section>
|
||||
<div className="subgrid">
|
||||
<FormInput id="name"
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
formState={formState}
|
||||
onChange={handleNameChange}
|
||||
label={t('app.admin.store.product_form.name')}
|
||||
className="span-7" />
|
||||
<FormInput id="sku"
|
||||
register={register}
|
||||
formState={formState}
|
||||
label={t('app.admin.store.product_form.sku')}
|
||||
className="span-3" />
|
||||
</div>
|
||||
<div className="subgrid">
|
||||
<FormInput id="slug"
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
formState={formState}
|
||||
label={t('app.admin.store.product_form.slug')}
|
||||
className='span-7' />
|
||||
<FormSwitch control={control}
|
||||
id="is_active"
|
||||
formState={formState}
|
||||
label={t('app.admin.store.product_form.is_show_in_store')}
|
||||
tooltip={t('app.admin.store.product_form.active_price_info')}
|
||||
onChange={handleIsActiveChanged}
|
||||
className='span-3' />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div className="price-data">
|
||||
<div className="header-switch">
|
||||
<h4>{t('app.admin.store.product_form.price_and_rule_of_selling_product')}</h4>
|
||||
<FormSwitch control={control}
|
||||
id="is_active_price"
|
||||
label={t('app.admin.store.product_form.is_active_price')}
|
||||
defaultValue={isActivePrice}
|
||||
onChange={toggleIsActivePrice} />
|
||||
</div>
|
||||
{isActivePrice && <div className="price-data-content">
|
||||
<FormInput id="amount"
|
||||
type="number"
|
||||
register={register}
|
||||
rules={{ required: isActivePrice, min: 0 }}
|
||||
step={0.01}
|
||||
formState={formState}
|
||||
label={t('app.admin.store.product_form.price')}
|
||||
nullable />
|
||||
<FormInput id="quantity_min"
|
||||
type="number"
|
||||
rules={{ required: true }}
|
||||
register={register}
|
||||
formState={formState}
|
||||
label={t('app.admin.store.product_form.quantity_min')} />
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h4>{t('app.admin.store.product_form.product_images')}</h4>
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.store.product_form.product_images_info" />
|
||||
</FabAlert>
|
||||
<FormMultiImageUpload setValue={setValue}
|
||||
addButtonLabel={t('app.admin.store.product_form.add_product_image')}
|
||||
register={register}
|
||||
control={control}
|
||||
id="product_images_attributes"
|
||||
className="product-images" />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h4>{t('app.admin.store.product_form.assigning_category')}</h4>
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.store.product_form.assigning_category_info" />
|
||||
</FabAlert>
|
||||
<FormSelect options={productCategories}
|
||||
control={control}
|
||||
id="product_category_id"
|
||||
formState={formState}
|
||||
label={t('app.admin.store.product_form.linking_product_to_category')} />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h4>{t('app.admin.store.product_form.assigning_machines')}</h4>
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.store.product_form.assigning_machines_info" />
|
||||
</FabAlert>
|
||||
<FormChecklist options={machines}
|
||||
control={control}
|
||||
id="machine_ids"
|
||||
formState={formState} />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h4>{t('app.admin.store.product_form.product_description')}</h4>
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.store.product_form.product_description_info" />
|
||||
</FabAlert>
|
||||
<FormRichText control={control}
|
||||
heading
|
||||
bulletList
|
||||
blockquote
|
||||
link
|
||||
limit={6000}
|
||||
id="description" />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h4>{t('app.admin.store.product_form.product_files')}</h4>
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.store.product_form.product_files_info" />
|
||||
</FabAlert>
|
||||
<FormMultiFileUpload setValue={setValue}
|
||||
addButtonLabel={t('app.admin.store.product_form.add_product_file')}
|
||||
control={control}
|
||||
accept="application/pdf"
|
||||
register={register}
|
||||
id="product_files_attributes"
|
||||
className="product-documents" />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<AdvancedAccountingForm register={register} onError={onError} />
|
||||
|
||||
<div className="main-actions">
|
||||
<FabButton type="submit" className="main-action-btn" disabled={saving}>
|
||||
{!saving && t('app.admin.store.product_form.save')}
|
||||
{saving && <i className="fa fa-spinner fa-pulse fa-fw" />}
|
||||
</FabButton>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
<FabTabs tabs={[
|
||||
{
|
||||
id: 'settings',
|
||||
title: t('app.admin.store.product_form.product_parameters'),
|
||||
content: renderSettingsTab()
|
||||
},
|
||||
{
|
||||
id: 'stock',
|
||||
title: t('app.admin.store.product_form.stock_management'),
|
||||
content: <ProductStockForm currentFormValues={output as Product}
|
||||
register={register}
|
||||
control={control}
|
||||
formState={formState}
|
||||
setValue={setValue}
|
||||
onError={onError}
|
||||
onSuccess={onSuccess} />
|
||||
}
|
||||
]} />
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
@ -170,13 +170,13 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
|
||||
$scope.orderAdmin = null;
|
||||
|
||||
// partners list
|
||||
$scope.partners = partnersPromise.users;
|
||||
$scope.partners = partnersPromise;
|
||||
|
||||
// Partners ordering/sorting. Default: not sorted
|
||||
$scope.orderPartner = null;
|
||||
|
||||
// managers list
|
||||
$scope.managers = managersPromise.users;
|
||||
$scope.managers = managersPromise;
|
||||
|
||||
// Managers ordering/sorting. Default: not sorted
|
||||
$scope.orderManager = null;
|
||||
|
@ -18,204 +18,27 @@
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
/* COMMON CODE */
|
||||
|
||||
class PlanController {
|
||||
constructor ($scope, groups, prices, partners, CSRF, _t) {
|
||||
// protection against request forgery
|
||||
CSRF.setMetaTags();
|
||||
|
||||
// groups list
|
||||
$scope.groups = groups
|
||||
.filter(function (g) { return !g.disabled; })
|
||||
.map(e => Object.assign({}, e, { category: 'app.shared.plan.groups', id: `${e.id}` }));
|
||||
$scope.groups.push({ id: 'all', name: 'app.shared.plan.transversal_all_groups', category: 'app.shared.plan.all' });
|
||||
|
||||
// dynamically translate a label if needed
|
||||
$scope.translateLabel = function (group, prop) {
|
||||
return group[prop] && group[prop].match(/^app\./) ? _t(group[prop]) : group[prop];
|
||||
};
|
||||
|
||||
// users with role 'partner', notifiable for a partner plan
|
||||
$scope.partners = partners.users;
|
||||
|
||||
// Subscriptions prices, machines prices and training prices, per groups
|
||||
$scope.group_pricing = prices;
|
||||
|
||||
/**
|
||||
* For use with 'ng-class', returns the CSS class name for the uploads previews.
|
||||
* The preview may show a placeholder or the content of the file depending on the upload state.
|
||||
* @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
|
||||
*/
|
||||
$scope.fileinputClass = function (v) {
|
||||
if (v) {
|
||||
return 'fileinput-exists';
|
||||
} else {
|
||||
return 'fileinput-new';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark the provided file for deletion
|
||||
* @param file {Object}
|
||||
*/
|
||||
$scope.deleteFile = function (file) {
|
||||
if ((file != null) && (file.id != null)) {
|
||||
return file._destroy = true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check and limit
|
||||
* @param content
|
||||
*/
|
||||
$scope.limitDescriptionSize = function (content) {
|
||||
alert(content);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// current form is used to create a new plan
|
||||
$scope.mode = 'creation';
|
||||
|
||||
// list of all plan categories
|
||||
$scope.planCategories = planCategories;
|
||||
// we add an empty element to let the user select 'no category'
|
||||
$scope.planCategories.unshift({ id: null, name: '' });
|
||||
|
||||
// prices bindings
|
||||
$scope.prices = {
|
||||
training: {},
|
||||
machine: {}
|
||||
};
|
||||
|
||||
// form inputs bindings
|
||||
$scope.plan = {
|
||||
type: null,
|
||||
group_id: null,
|
||||
interval: null,
|
||||
intervalCount: 0,
|
||||
amount: null,
|
||||
is_rolling: false,
|
||||
partnerId: null,
|
||||
partnerContact: null,
|
||||
ui_weight: 0,
|
||||
monthly_payment: false,
|
||||
plan_category_id: null
|
||||
};
|
||||
|
||||
// API URL where the form will be posted
|
||||
$scope.actionUrl = '/api/plans/';
|
||||
|
||||
// HTTP method for the rest API
|
||||
$scope.method = 'POST';
|
||||
// protection against request forgery
|
||||
CSRF.setMetaTags();
|
||||
|
||||
/**
|
||||
* Checks if the partner contact is a valid data. Used in the form validation process
|
||||
* @returns {boolean}
|
||||
* Shows an error message forwarded from a child component
|
||||
*/
|
||||
$scope.partnerIsValid = function () { return ($scope.plan.type === 'Plan') || ($scope.plan.partnerId || ($scope.plan.partnerContact && $scope.plan.partnerContact.email)); };
|
||||
|
||||
/**
|
||||
* Open a modal dialog allowing the admin to create a new partner user
|
||||
*/
|
||||
$scope.openPartnerNewModal = function (subscription) {
|
||||
const modalInstance = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: '/shared/_partner_new_modal.html',
|
||||
size: 'lg',
|
||||
controller: ['$scope', '$uibModalInstance', 'User', function ($scope, $uibModalInstance, User) {
|
||||
$scope.partner = {};
|
||||
|
||||
$scope.ok = function () {
|
||||
User.save(
|
||||
{},
|
||||
{ user: $scope.partner },
|
||||
function (user) {
|
||||
$scope.partner.id = user.id;
|
||||
$scope.partner.name = `${user.first_name} ${user.last_name}`;
|
||||
$uibModalInstance.close($scope.partner);
|
||||
},
|
||||
function (error) {
|
||||
growl.error(_t('app.admin.plans.new.unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name'));
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||
}]
|
||||
});
|
||||
// once the form was validated successfully ...
|
||||
return modalInstance.result.then(function (partner) {
|
||||
$scope.partners.push(partner);
|
||||
return $scope.plan.partnerId = partner.id;
|
||||
});
|
||||
$scope.onError = function (message) {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* This will update the monthly_payment value when the user toggles the switch button
|
||||
* @param checked {Boolean}
|
||||
* Shows a success message forwarded from a child react components
|
||||
*/
|
||||
$scope.toggleMonthlyPayment = function (checked) {
|
||||
toggle('monthly_payment', checked);
|
||||
$scope.onSuccess = function (message) {
|
||||
growl.success(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* This will update the is_rolling value when the user toggles the switch button
|
||||
* @param checked {Boolean}
|
||||
*/
|
||||
$scope.toggleIsRolling = function (checked) {
|
||||
toggle('is_rolling', checked);
|
||||
};
|
||||
|
||||
/**
|
||||
* Display some messages and redirect the user, once the form was submitted, depending on the result status
|
||||
* (failed/succeeded).
|
||||
* @param content {Object}
|
||||
*/
|
||||
$scope.afterSubmit = function (content) {
|
||||
if (content.plan_ids === null || content.plan_ids === undefined) {
|
||||
return growl.error(_t('app.admin.plans.new.unable_to_create_the_subscription_please_try_again'));
|
||||
} else {
|
||||
growl.success(_t('app.admin.plans.new.successfully_created_subscriptions_dont_forget_to_redefine_prices'));
|
||||
if (content.plan_ids.length > 1) {
|
||||
return $state.go('app.admin.pricing');
|
||||
} else {
|
||||
return $state.go('app.admin.plans.edit', { id: content.plan_ids[0] });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
const initialize = function () {
|
||||
$scope.$watch(scope => scope.plan.interval,
|
||||
(newValue, oldValue) => {
|
||||
if (newValue === 'week') { $scope.plan.monthly_payment = false; }
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Asynchronously updates the given property with the new provided value
|
||||
* @param property {string}
|
||||
* @param value {*}
|
||||
*/
|
||||
const toggle = function (property, value) {
|
||||
setTimeout(() => {
|
||||
$scope.plan[property] = value;
|
||||
$scope.$apply();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
initialize();
|
||||
return new PlanController($scope, groups, prices, partners, CSRF, _t);
|
||||
}
|
||||
]);
|
||||
|
||||
@ -224,133 +47,11 @@ Application.Controllers.controller('NewPlanController', ['$scope', '$uibModal',
|
||||
*/
|
||||
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) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// List of spaces
|
||||
$scope.spaces = spaces;
|
||||
|
||||
// List of plans
|
||||
$scope.plans = plans;
|
||||
|
||||
// List of machines
|
||||
$scope.machines = machines;
|
||||
|
||||
// List of groups
|
||||
$scope.allGroups = groups;
|
||||
|
||||
// list of all plan categories
|
||||
$scope.planCategories = planCategories;
|
||||
// we add an empty element to let the user select 'no category'
|
||||
$scope.planCategories.unshift({ id: null, name: '' });
|
||||
|
||||
// current form is used for edition mode
|
||||
$scope.mode = 'edition';
|
||||
|
||||
// edited plan data
|
||||
$scope.plan = Object.assign({}, planPromise, { group_id: `${planPromise.group_id}` });
|
||||
if ($scope.plan.type === null) { $scope.plan.type = 'Plan'; }
|
||||
if ($scope.plan.disabled) { $scope.plan.disabled = 'true'; }
|
||||
// protection against request forgery
|
||||
CSRF.setMetaTags();
|
||||
|
||||
$scope.suscriptionPlan = cleanPlan(planPromise);
|
||||
|
||||
// API URL where the form will be posted
|
||||
$scope.actionUrl = `/api/plans/${$transition$.params().id}`;
|
||||
|
||||
// HTTP method for the rest API
|
||||
$scope.method = 'PATCH';
|
||||
|
||||
$scope.selectedGroup = function () {
|
||||
const group = $scope.groups.filter(g => g.id === $scope.plan.group_id);
|
||||
return $scope.translateLabel(group[0], 'name');
|
||||
};
|
||||
|
||||
/**
|
||||
* If a parent plan was set ($scope.plan.parent), the prices will be copied from this parent plan into
|
||||
* the current plan prices list. Otherwise, the current plan prices will be erased.
|
||||
*/
|
||||
$scope.copyPricesFromPlan = function () {
|
||||
if ($scope.plan.parent) {
|
||||
return Plan.get({ id: $scope.plan.parent }, function (parentPlan) {
|
||||
Array.from(parentPlan.prices).map(function (parentPrice) {
|
||||
return (function () {
|
||||
const result = [];
|
||||
for (const childKey in $scope.plan.prices) {
|
||||
const childPrice = $scope.plan.prices[childKey];
|
||||
if ((childPrice.priceable_type === parentPrice.priceable_type) && (childPrice.priceable_id === parentPrice.priceable_id)) {
|
||||
$scope.plan.prices[childKey].amount = parentPrice.amount;
|
||||
break;
|
||||
} else {
|
||||
result.push(undefined);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
})();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// if no plan were selected, unset every prices
|
||||
} else {
|
||||
return (function () {
|
||||
const result = [];
|
||||
for (const key in $scope.plan.prices) {
|
||||
const price = $scope.plan.prices[key];
|
||||
result.push($scope.plan.prices[key].amount = 0);
|
||||
}
|
||||
return result;
|
||||
})();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Display some messages once the form was submitted, depending on the result status (failed/succeeded)
|
||||
* @param content {Object}
|
||||
*/
|
||||
$scope.afterSubmit = function (content) {
|
||||
if ((content.id == null) && (content.plan_ids == null)) {
|
||||
return growl.error(_t('app.admin.plans.edit.unable_to_save_subscription_changes_please_try_again'));
|
||||
} else {
|
||||
growl.success(_t('app.admin.plans.edit.subscription_successfully_changed'));
|
||||
return $state.go('app.admin.pricing');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a string identifying the given plan by literal humain-readable name
|
||||
* @param plan {Object} Plan object, as recovered from GET /api/plan/:id
|
||||
* @param groups {Array} List of Groups objects, as recovered from GET /api/groups
|
||||
* @param short {boolean} If true, the generated name will contains the group slug, otherwise the group full name
|
||||
* will be included.
|
||||
* @returns {String}
|
||||
*/
|
||||
$scope.humanReadablePlanName = function (plan, groups, short) { return `${$filter('humanReadablePlanName')(plan, groups, short)}`; };
|
||||
|
||||
/**
|
||||
* Retrieve the machine from its ID
|
||||
* @param machine_id {number} machine identifier
|
||||
* @returns {Object} Machine
|
||||
*/
|
||||
$scope.getMachine = function (machine_id) {
|
||||
for (const machine of Array.from($scope.machines)) {
|
||||
if (machine.id === machine_id) {
|
||||
return machine;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the space from its ID
|
||||
* @param space_id {number} space identifier
|
||||
* @returns {Object} Space
|
||||
*/
|
||||
$scope.getSpace = function (space_id) {
|
||||
for (const space of Array.from($scope.spaces)) {
|
||||
if (space.id === space_id) {
|
||||
return space;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows an error message forwarded from a child component
|
||||
*/
|
||||
@ -371,9 +72,6 @@ Application.Controllers.controller('EditPlanController', ['$scope', 'groups', 'p
|
||||
delete plan.$resolved;
|
||||
return plan;
|
||||
}
|
||||
|
||||
// Using the PlansController
|
||||
return new PlanController($scope, groups, prices, partners, CSRF, _t);
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -1,15 +1,9 @@
|
||||
import { Price } from './price';
|
||||
import { FileType } from './file';
|
||||
|
||||
export enum Interval {
|
||||
Year = 'year',
|
||||
Month = 'month',
|
||||
Week = 'week'
|
||||
}
|
||||
export type Interval = 'year' | 'month' | 'week';
|
||||
|
||||
export enum PlanType {
|
||||
Plan = 'Plan',
|
||||
PartnerPlan = 'PartnerPlan'
|
||||
}
|
||||
export type PlanType = 'Plan' | 'PartnerPlan';
|
||||
|
||||
export interface Partner {
|
||||
first_name: string,
|
||||
@ -33,13 +27,11 @@ export interface Plan {
|
||||
disabled: boolean,
|
||||
monthly_payment: boolean
|
||||
amount: number
|
||||
prices: Array<Price>,
|
||||
plan_file_attributes: {
|
||||
id: number,
|
||||
attachment_identifier: string
|
||||
},
|
||||
prices_attributes: Array<Price>,
|
||||
plan_file_attributes: FileType,
|
||||
plan_file_url: string,
|
||||
partners: Array<Partner>
|
||||
partner_id?: number,
|
||||
partners?: Array<Partner>
|
||||
}
|
||||
|
||||
export interface PlansDuration {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Plan } from './plan';
|
||||
import { TDateISO, TDateISODate } from '../typings/date-iso';
|
||||
import { supportedNetworks, SupportedSocialNetwork } from './social-network';
|
||||
import { ApiFilter } from './api';
|
||||
|
||||
export type UserRole = 'member' | 'manager' | 'admin';
|
||||
|
||||
@ -10,13 +11,13 @@ type ProfileAttributesSocial = {
|
||||
|
||||
export interface User {
|
||||
id: number,
|
||||
username: string,
|
||||
username?: string,
|
||||
email: string,
|
||||
group_id: number,
|
||||
role: UserRole
|
||||
group_id?: number,
|
||||
role?: UserRole
|
||||
name: string,
|
||||
need_completion: boolean,
|
||||
ip_address: string,
|
||||
ip_address?: string,
|
||||
mapped_from_sso?: string[],
|
||||
password?: string,
|
||||
password_confirmation?: string,
|
||||
@ -25,13 +26,13 @@ export interface User {
|
||||
id: number,
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
interest: string,
|
||||
software_mastered: string,
|
||||
phone: string,
|
||||
website: string,
|
||||
job: string,
|
||||
tours: Array<string>,
|
||||
user_avatar_attributes: {
|
||||
interest?: string,
|
||||
software_mastered?: string,
|
||||
phone?: string,
|
||||
website?: string,
|
||||
job?: string,
|
||||
tours?: Array<string>,
|
||||
user_avatar_attributes?: {
|
||||
id: number,
|
||||
attachment?: File,
|
||||
attachment_url?: string,
|
||||
@ -39,21 +40,21 @@ export interface User {
|
||||
_destroy?: boolean
|
||||
}
|
||||
},
|
||||
invoicing_profile_attributes: {
|
||||
id: number,
|
||||
invoicing_profile_attributes?: {
|
||||
id?: number,
|
||||
address_attributes: {
|
||||
id: number,
|
||||
id?: number,
|
||||
address: string
|
||||
},
|
||||
organization_attributes: {
|
||||
id: number,
|
||||
organization_attributes?: {
|
||||
id?: number,
|
||||
name: string,
|
||||
address_attributes: {
|
||||
id: number,
|
||||
id?: number,
|
||||
address: string
|
||||
}
|
||||
},
|
||||
user_profile_custom_fields_attributes: Array<
|
||||
user_profile_custom_fields_attributes?: Array<
|
||||
{
|
||||
id?: number,
|
||||
value: string,
|
||||
@ -62,14 +63,14 @@ export interface User {
|
||||
}
|
||||
>
|
||||
},
|
||||
statistic_profile_attributes: {
|
||||
id: number,
|
||||
statistic_profile_attributes?: {
|
||||
id?: number,
|
||||
gender: string,
|
||||
birthday: TDateISODate
|
||||
training_ids: Array<number>
|
||||
},
|
||||
subscribed_plan: Plan,
|
||||
subscription: {
|
||||
subscribed_plan?: Plan,
|
||||
subscription?: {
|
||||
id: number,
|
||||
expired_at: TDateISO,
|
||||
canceled_at: TDateISO,
|
||||
@ -83,16 +84,17 @@ export interface User {
|
||||
amount: number
|
||||
}
|
||||
},
|
||||
training_credits: Array<number>,
|
||||
machine_credits: Array<{ machine_id: number, hours_used: number }>,
|
||||
last_sign_in_at: TDateISO
|
||||
validated_at: TDateISO,
|
||||
tag_ids: Array<number>
|
||||
training_credits?: Array<number>,
|
||||
machine_credits?: Array<{ machine_id: number, hours_used: number }>,
|
||||
last_sign_in_at?: TDateISO
|
||||
validated_at?: TDateISO,
|
||||
tag_ids?: Array<number>,
|
||||
resource?: Plan // for users with role=partner, there will be the associated plan
|
||||
}
|
||||
|
||||
type OrderingKey = 'last_name' | 'first_name' | 'email' | 'phone' | 'group' | 'plan' | 'id'
|
||||
|
||||
export interface UserIndexFilter {
|
||||
export interface MemberIndexFilter {
|
||||
search?: string,
|
||||
filter?: 'inactive_for_3_years' | 'not_confirmed',
|
||||
order_by?: OrderingKey | `-${OrderingKey}`,
|
||||
@ -100,6 +102,10 @@ export interface UserIndexFilter {
|
||||
size?: number
|
||||
}
|
||||
|
||||
export interface UserIndexFilter extends ApiFilter {
|
||||
role?: 'partner' | 'manager'
|
||||
}
|
||||
|
||||
const socialMappings = supportedNetworks.map(network => {
|
||||
return { [`profile_attributes.${network}`]: `profile.${network}` };
|
||||
});
|
||||
|
@ -2,10 +2,6 @@
|
||||
|
||||
Application.Services.factory('User', ['$resource', function ($resource) {
|
||||
return $resource('/api/users/:id',
|
||||
{ id: '@id' }, {
|
||||
query: {
|
||||
isArray: false
|
||||
}
|
||||
}
|
||||
{ id: '@id' }
|
||||
);
|
||||
}]);
|
||||
|
@ -29,6 +29,7 @@
|
||||
@import "modules/base/fab-panel";
|
||||
@import "modules/base/fab-popover";
|
||||
@import "modules/base/fab-state-label";
|
||||
@import "modules/base/fab-tabs";
|
||||
@import "modules/base/fab-text-editor";
|
||||
@import "modules/base/fab-tooltip";
|
||||
@import "modules/base/labelled-input";
|
||||
|
26
app/frontend/src/stylesheets/modules/base/fab-tabs.scss
Normal file
26
app/frontend/src/stylesheets/modules/base/fab-tabs.scss
Normal file
@ -0,0 +1,26 @@
|
||||
.fab-tabs {
|
||||
.tabs {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
p {
|
||||
flex: 1;
|
||||
margin-bottom: 4rem;
|
||||
padding: 0.8rem;
|
||||
text-align: center;
|
||||
color: var(--main);
|
||||
border-bottom: 1px solid var(--gray-soft-dark);
|
||||
|
||||
&.is-active {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,12 @@
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--gray-soft-lightest);
|
||||
|
||||
&.with-label {
|
||||
margin-top: 2.6rem;
|
||||
position: relative;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
@ -21,5 +27,11 @@
|
||||
.image-file-input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.form-item-header { // label
|
||||
position: absolute;
|
||||
top: -1.5em;
|
||||
left: 0;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@
|
||||
.image-file-input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.form-item-header {
|
||||
.form-item-header { // label
|
||||
position: absolute;
|
||||
top: -1.5em;
|
||||
left: 0;
|
||||
|
@ -1,4 +1,24 @@
|
||||
.plan-form {
|
||||
.plan-sheet {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
.duration {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.form-item:first-child {
|
||||
margin-right: 32px;
|
||||
}
|
||||
}
|
||||
.partner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
|
||||
.fab-alert {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.submit-btn {
|
||||
float: right;
|
||||
}
|
||||
|
@ -1,26 +1,6 @@
|
||||
.product-form {
|
||||
grid-column: 2 / -2;
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
p {
|
||||
flex: 1;
|
||||
margin-bottom: 4rem;
|
||||
padding: 0.8rem;
|
||||
text-align: center;
|
||||
color: var(--main);
|
||||
border-bottom: 1px solid var(--gray-soft-dark);
|
||||
&.is-active {
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 2.4rem;
|
||||
@include title-base;
|
||||
|
@ -30,10 +30,10 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="manager in managers | filter:searchFilter | orderBy: orderManager">
|
||||
<td class="text-c">{{ manager.last_name }}</td>
|
||||
<td class="text-c">{{ manager.first_name }}</td>
|
||||
<td class="text-c">{{ manager.profile_attributes.last_name }}</td>
|
||||
<td class="text-c">{{ manager.profile_attributes.first_name }}</td>
|
||||
<td>{{ manager.email }}</td>
|
||||
<td>{{ manager.phone }}</td>
|
||||
<td>{{ manager.profile_attributes.phone }}</td>
|
||||
<td>
|
||||
<button class="btn btn-default edit-member" ui-sref="app.admin.members_edit({id: manager.id})">
|
||||
<i class="fa fa-edit"></i>
|
||||
|
@ -28,8 +28,8 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="partner in partners | filter:searchFilter | orderBy: orderPartner">
|
||||
<td class="text-c">{{ partner.last_name }}</td>
|
||||
<td class="text-c">{{ partner.first_name }}</td>
|
||||
<td class="text-c">{{ partner.profile_attributes.last_name }}</td>
|
||||
<td class="text-c">{{ partner.profile_attributes.first_name }}</td>
|
||||
<td>{{ partner.email }}</td>
|
||||
<td><a ui-sref="app.admin.plans.edit({id:partner.resource.id})">{{ partner.resource ? partner.resource.base_name : '' }}</a></td>
|
||||
<td>
|
||||
|
@ -1,194 +0,0 @@
|
||||
<h2 translate>{{ 'app.shared.plan.general_information' }}</h2>
|
||||
<input type="hidden" name="_method" value="{{method}}">
|
||||
|
||||
<div class="form-group" ng-class="{'has-error': planForm['plan[base_name]'].$dirty && planForm['plan[base_name]'].$invalid}">
|
||||
<label for="plan[base_name]">{{ 'app.shared.plan.name' | translate }} *</label>
|
||||
<input type="text" id="plan[base_name]"
|
||||
name="plan[base_name]"
|
||||
class="form-control"
|
||||
ng-maxlength="24"
|
||||
ng-trim="false"
|
||||
ng-model="plan.base_name"
|
||||
required="true"/>
|
||||
<span class="help-block error" ng-show="planForm['plan[base_name]'].$dirty && planForm['plan[base_name]'].$error.required" translate>{{ 'app.shared.plan.name_is_required' }}</span>
|
||||
<span class="help-block error" ng-show="planForm['plan[base_name]'].$dirty && planForm['plan[base_name]'].$error.maxlength" translate>{{ 'app.shared.plan.name_length_must_be_less_than_24_characters' }}</span>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': planForm['plan[type]'].$dirty && planForm['plan[type]'].$invalid}">
|
||||
<label for="plan[type]">{{ 'app.shared.plan.type' | translate }} *</label>
|
||||
<select id="plan[type]"
|
||||
name="plan[type]"
|
||||
class="form-control"
|
||||
ng-model="plan.type"
|
||||
required="true"
|
||||
ng-disabled="method == 'PATCH'">
|
||||
<option value="Plan" ng-selected="plan.type == 'Plan'" translate>{{ 'app.shared.plan.standard' }}</option>
|
||||
<option value="PartnerPlan" ng-selected="plan.type == 'PartnerPlan'" translate>{{ 'app.shared.plan.partner' }}</option>
|
||||
</select>
|
||||
<span class="help-block error" ng-show="planForm['plan[type]'].$dirty && planForm['plan[type]'].$error.required" translate>{{ 'app.shared.plan.type_is_required' }}</span>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': planForm['plan[group_id]'].$dirty && planForm['plan[group_id]'].$invalid}">
|
||||
<label for="plan[group_id]">{{ 'app.shared.plan.group' | translate }} *</label>
|
||||
<select id="plan[group_id]"
|
||||
name="plan[group_id]"
|
||||
class="form-control"
|
||||
ng-model="plan.group_id"
|
||||
required="true"
|
||||
ng-if="method !== 'PATCH'"
|
||||
ng-options="item.id as translateLabel(item, 'name') group by translateLabel(item, 'category') for item in groups track by item.id">
|
||||
</select>
|
||||
<input type="text"
|
||||
id="plan[group_id]"
|
||||
ng-value="selectedGroup()"
|
||||
ng-if="method == 'PATCH'"
|
||||
class="form-control"
|
||||
disabled />
|
||||
<input type="hidden"
|
||||
name="plan[group_id]"
|
||||
ng-value="plan.group_id"
|
||||
ng-if="method == 'PATCH'"/>
|
||||
<span class="help-block" ng-show="planForm['plan[group_id]'].$dirty && planForm['plan[group_id]'].$error.required" translate>{{ 'app.shared.plan.group_is_required' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{'has-error': planForm['plan[plan_category_id]'].$dirty && planForm['plan[group_id]'].$invalid}">
|
||||
<label for="plan[plan_category_id]">{{ 'app.shared.plan.category' | translate }}</label>
|
||||
<select id="plan[plan_category_id]"
|
||||
class="form-control"
|
||||
ng-model="plan.plan_category_id"
|
||||
ng-options="cat.id as cat.name for cat in planCategories">
|
||||
</select>
|
||||
<input type="hidden"
|
||||
name="plan[plan_category_id]"
|
||||
ng-value="plan.plan_category_id"/>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': planForm['plan[interval]'].$dirty && planForm['plan[interval]'].$invalid}">
|
||||
<label for="plan[interval]">{{ 'app.shared.plan.period' | translate }} *</label>
|
||||
<select id="plan[interval]"
|
||||
name="plan[interval]"
|
||||
class="form-control"
|
||||
ng-model="plan.interval"
|
||||
ng-disabled="method == 'PATCH'"
|
||||
required="true">
|
||||
<option value="week" ng-selected="plan.interval == 'week'" translate>{{ 'app.shared.plan.week' }}</option>
|
||||
<option value="month" ng-selected="plan.interval == 'month'" translate>{{ 'app.shared.plan.month' }}</option>
|
||||
<option value="year" ng-selected="plan.interval == 'year'" translate>{{ 'app.shared.plan.year' }}</option>
|
||||
</select>
|
||||
<span class="help-block" ng-show="planForm['plan[interval]'].$dirty && planForm['plan[interval]'].$error.required" translate>{{ 'app.shared.plan.period_is_required' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{'has-error': planForm['plan[interval_count]'].$dirty && planForm['plan[interval_count]'].$invalid}">
|
||||
<label for="plan[interval]">{{ 'app.shared.plan.number_of_periods' | translate }} *</label>
|
||||
<input id="plan[interval_count]"
|
||||
name="plan[interval_count]"
|
||||
class="form-control"
|
||||
type="number"
|
||||
ng-model="plan.interval_count"
|
||||
ng-disabled="method == 'PATCH'"
|
||||
required="true"
|
||||
min="1"/>
|
||||
<span class="help-block" ng-show="planForm['plan[interval_count]'].$dirty && planForm['plan[interval_count]'].$error.required" translate>{{ 'app.shared.plan.number_of_periods_is_required' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="input-group" ng-class="{'has-error': planForm['plan[amount]'].$dirty && planForm['plan[amount]'].$invalid}">
|
||||
<label for="plan[amount]">{{ 'app.shared.plan.subscription_price' | translate }} *</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">{{currencySymbol}}</span>
|
||||
<input id="plan[amount]"
|
||||
name="plan[amount]"
|
||||
type="number"
|
||||
class="form-control"
|
||||
ng-required="true"
|
||||
ng-model="plan.amount"/>
|
||||
</div>
|
||||
<span class="help-block" ng-show="planForm['plan[amount]'].$dirty && planForm['plan[amount]'].$error.required" translate>{{ 'app.shared.plan.price_is_required' }}</span>
|
||||
<span class="help-block alert alert-warning" ng-if="method == 'PATCH'">
|
||||
<i class="fa fa-warning"></i>
|
||||
{{ 'app.shared.plan.edit_amount_info' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label translate>{{ 'app.shared.plan.visual_prominence_of_the_subscription' }}</label>
|
||||
<input ng-model="plan.ui_weight"
|
||||
type="number"
|
||||
name="plan[ui_weight]"
|
||||
class="form-control">
|
||||
<span class="help-block">
|
||||
{{ 'app.shared.plan.on_the_subscriptions_page_the_most_prominent_subscriptions_will_be_placed_at_the_top_of_the_list' | translate }}
|
||||
{{ 'app.shared.plan.an_evelated_number_means_a_higher_prominence' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="input-group m-t-md">
|
||||
<label for="plan[is_rolling]" class="control-label m-r-md">{{ 'app.shared.plan.rolling_subscription' | translate }} *</label>
|
||||
<switch id="plan[is_rolling]" checked="plan.is_rolling" on-change="toggleIsRolling" class-name="'v-middle'" ng-if="plan && method != 'PATCH'"></switch>
|
||||
<span ng-if="method == 'PATCH'">{{ (plan.is_rolling ? 'app.shared.buttons.yes' : 'app.shared.buttons.no') | translate }}</span>
|
||||
<input type="hidden" name="plan[is_rolling]" value="{{plan.is_rolling}}"/>
|
||||
<span class="help-block">
|
||||
{{ 'app.shared.plan.a_rolling_subscription_will_begin_the_day_of_the_first_training' | translate }}
|
||||
{{ 'app.shared.plan.otherwise_it_will_begin_as_soon_as_it_is_bought' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="input-group m-t-md">
|
||||
<label for="plan[monthly_payment]" class="control-label m-r-md">{{ 'app.shared.plan.monthly_payment' | translate }} *</label>
|
||||
<switch id="plan[monthly_payment]" disabled="plan.interval === 'week' || (plan.interval === 'month' && plan.interval_count === 1)" checked="plan.monthly_payment" on-change="toggleMonthlyPayment" class-name="'v-middle'" ng-if="plan && method != 'PATCH'"></switch>
|
||||
<span ng-if="method == 'PATCH'">{{ (plan.monthly_payment ? 'app.shared.buttons.yes' : 'app.shared.buttons.no') | translate }}</span>
|
||||
<input type="hidden" id="plan_monthly_input" name="plan[monthly_payment]" value="{{plan.monthly_payment}}" />
|
||||
<span class="help-block" translate>{{ 'app.shared.plan.monthly_payment_info' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="input-group m-t-md plan-description-input">
|
||||
<label for="plan[description]" class="control-label m-r-md" translate>{{ 'app.shared.plan.description' }}</label>
|
||||
<div class="medium-editor-input">
|
||||
<div ng-model="plan.description"
|
||||
medium-editor
|
||||
options='{
|
||||
"placeholder": "{{ "app.shared.plan.type_a_short_description" | translate }}",
|
||||
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ],
|
||||
"targetBlank": true,
|
||||
}'>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="plan[description]" name="plan[description]" value="{{plan.description}}" />
|
||||
</div>
|
||||
|
||||
<!-- PDF description attachement -->
|
||||
<input type="hidden" ng-model="plan.plan_file_attributes.id" name="plan[plan_file_attributes][id]" ng-value="plan.plan_file_attributes.id" />
|
||||
<input type="hidden" ng-model="plan.plan_file_attributes._destroy" name="plan[plan_file_attributes][_destroy]" ng-value="plan.plan_file_attributes._destroy"/>
|
||||
<label class="m-t-md" translate>{{ 'app.shared.plan.information_sheet' }}</label>
|
||||
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(plan.plan_file_attributes)">
|
||||
<div class="form-control" data-trigger="fileinput">
|
||||
<i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{file.attachment || plan.plan_file_attributes.attachment_identifier}}</span>
|
||||
</div>
|
||||
<span class="input-group-addon btn btn-default btn-file"><span class="fileinput-new" translate>{{ 'app.shared.plan.attach_an_information_sheet' }}</span>
|
||||
<span class="fileinput-exists" translate>{{ 'app.shared.buttons.change' }}</span><input type="file"
|
||||
name="plan[plan_file_attributes][attachment]"
|
||||
accept="image/jpeg,image/gif,image/png,application/pdf"></span>
|
||||
<a class="input-group-addon btn btn-danger fileinput-exists" data-dismiss="fileinput" ng-click="deleteFile(file || plan.plan_file_attributes)"><i class="fa fa-trash-o"></i></a>
|
||||
</div>
|
||||
|
||||
<div class="form-group m-t-md" ng-show="plan.type == 'PartnerPlan' && method != 'PATCH'">
|
||||
<input type="hidden" ng-model="plan.partnerId" name="plan[partner_id]" ng-value="plan.partnerId" />
|
||||
<label for="plan[partner_id]">{{ 'app.shared.plan.notified_partner' | translate }} *</label>
|
||||
<div class="input-group">
|
||||
<select class="form-control"
|
||||
ng-model="plan.partnerId"
|
||||
ng-options="p.id as (p.name + ' <'+p.email+'>') for p in partners"
|
||||
id="plan[partner_id]">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="openPartnerNewModal()"><i class="fa fa-user-plus"></i> {{ 'app.shared.plan.new_user' | translate }}</button>
|
||||
</span>
|
||||
</div>
|
||||
<span class="help-block" translate>{{ 'app.shared.plan.as_part_of_a_partner_subscription_some_notifications_may_be_sent_to_this_user' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="plan.partners">
|
||||
<label>Partenaire notifié</label>
|
||||
<span ng-repeat="partner in plan.partners">
|
||||
<input type="text" class="form-control" disabled value="{{ partner.first_name}} {{partner.last_name }}">
|
||||
</span>
|
||||
</div>
|
@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
<section class="heading-title">
|
||||
<h1>{{ 'app.admin.plans.edit.subscription_plan' | translate }} {{ plan.base_name }}</h1>
|
||||
<h1>{{ 'app.admin.plans.edit.subscription_plan' | translate }} {{ suscriptionPlan.base_name }}</h1>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@ -29,94 +29,5 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="planForm">
|
||||
<form name="planForm" novalidate="novalidate" class="col-lg-7 col-lg-offset-2 m-t-lg form-group" action="{{ actionUrl }}" ng-upload="afterSubmit(content)" upload-options-enable-rails-csrf="true">
|
||||
|
||||
<ng-include src="'/admin/plans/_form.html'"></ng-include>
|
||||
|
||||
<div class="input-group m-t-md">
|
||||
<label for="plan[disabled]" class="control-label m-r-md">{{ 'app.shared.plan.disabled' | translate }}</label>
|
||||
<input bs-switch
|
||||
ng-model="plan.disabled"
|
||||
id="plan[disabled]"
|
||||
type="checkbox"
|
||||
class="form-control"
|
||||
switch-on-text="{{ 'app.shared.buttons.yes' | translate }}"
|
||||
switch-off-text="{{ 'app.shared.buttons.no' | translate }}"
|
||||
switch-animate="true"
|
||||
ng-true-value="'true'"
|
||||
ng-false-value="'false'"/>
|
||||
<input type="hidden" name="plan[disabled]" value="{{plan.disabled}}"/>
|
||||
<span class="help-block" translate>{{ 'app.shared.plan.disable_plan_will_not_unsubscribe_users' }}</span>
|
||||
</div>
|
||||
|
||||
<h2 class="m-t-xl" translate>{{ 'app.admin.plans.edit.prices' }}</h2>
|
||||
<div class="form-group col-md-6 col-lg-offset-6">
|
||||
<input type="hidden" ng-model="plan.parent" name="plan[parent_id]" ng-value="plan.parent"/>
|
||||
<label for="parentPlan" translate>{{ 'app.admin.plans.edit.copy_prices_from' }}</label>
|
||||
<select id="parentPlan" ng-options="plan.id as humanReadablePlanName(plan, allGroups) for plan in plans" ng-model="plan.parent" ng-change="copyPricesFromPlan()" class="form-control">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div ng-show="$root.modules.machines">
|
||||
<h3 translate>{{ 'app.admin.plans.edit.machines' }}</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th translate>{{ 'app.admin.plans.edit.machine' }}</th>
|
||||
<th translate>{{ 'app.admin.plans.edit.hourly_rate' }}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tr ng-repeat="price in plan.prices" ng-if="price.priceable_type === 'Machine'" ng-hide="getMachine(price.priceable_id).disabled">
|
||||
<td style="width: 60%;">{{ getMachine(price.priceable_id).name }} (id {{ price.priceable_id }}) *</td>
|
||||
<td>
|
||||
<div class="input-group" ng-class="{'has-error': planForm['plan[prices_attributes][][amount]'].$dirty && planForm['plan[prices_attributes][][amount]'].$invalid}">
|
||||
<span class="input-group-addon">{{currencySymbol}}</span>
|
||||
<input type="number" class="form-control" name="plan[prices_attributes][][amount]" ng-value="price.amount" required="true"/>
|
||||
<input type="hidden" class="form-control" name="plan[prices_attributes][][id]" ng-value="price.id"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div ng-show="$root.modules.spaces">
|
||||
<h3 translate>{{ 'app.admin.plans.edit.spaces' }}</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th translate>{{ 'app.admin.plans.edit.space' }}</th>
|
||||
<th translate>{{ 'app.admin.plans.edit.hourly_rate' }}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tr ng-repeat="price in plan.prices" ng-if="price.priceable_type === 'Space'" ng-hide="getSpace(price.priceable_id).disabled">
|
||||
<td style="width: 60%;">{{ getSpace(price.priceable_id).name }} *</td>
|
||||
<td>
|
||||
<div class="input-group" ng-class="{'has-error': planForm['plan[prices_attributes][][amount]'].$dirty && planForm['plan[prices_attributes][][amount]'].$invalid}">
|
||||
<span class="input-group-addon">{{currencySymbol}}</span>
|
||||
<input type="number" class="form-control" name="plan[prices_attributes][][amount]" ng-value="price.amount" required="true"/>
|
||||
<input type="hidden" class="form-control" name="plan[prices_attributes][][id]" ng-value="price.id"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<ul>
|
||||
<li ng-repeat="(key, errors) in planForm.$error track by $index"> <strong>{{ key }}</strong> errors
|
||||
<ul>
|
||||
<li ng-repeat="e in errors">{{ e.$name }} has an error: <strong>{{ key }}</strong>.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="panel-footer no-padder">
|
||||
<input type="submit" value="{{ 'app.shared.buttons.confirm_changes' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="planForm.$invalid"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,16 +17,10 @@
|
||||
<div class="row no-gutter">
|
||||
<div class=" col-sm-12 col-md-9 b-r nopadding">
|
||||
|
||||
<div id="planForm">
|
||||
<form name="planForm" novalidate="novalidate" class="col-lg-10 col-lg-offset-2 m-t-lg form-group" action="{{ actionUrl }}" ng-upload="afterSubmit(content)" upload-options-enable-rails-csrf="true">
|
||||
|
||||
<ng-include src="'/admin/plans/_form.html'"></ng-include>
|
||||
|
||||
<div class="panel-footer no-padder">
|
||||
<input type="submit" value="{{ 'app.shared.buttons.save' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="planForm.$invalid || !partnerIsValid()"/>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<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>
|
||||
|
@ -11,6 +11,10 @@ class PartnerPlan < Plan
|
||||
User.joins(:roles).where(roles: { name: 'partner', resource_type: 'PartnerPlan', resource_id: id })
|
||||
end
|
||||
|
||||
def partner_id
|
||||
partners.first.id
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_default_values
|
||||
|
@ -21,6 +21,9 @@ class Plan < ApplicationRecord
|
||||
accepts_nested_attributes_for :prices
|
||||
accepts_nested_attributes_for :plan_file, allow_destroy: true, reject_if: :all_blank
|
||||
|
||||
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
||||
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
|
||||
|
||||
after_create :create_machines_prices
|
||||
after_create :create_spaces_prices
|
||||
after_create :create_statistic_type
|
||||
|
@ -3,11 +3,12 @@
|
||||
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
|
||||
json.amount plan.amount / 100.00
|
||||
json.prices plan.prices, partial: 'api/prices/price', as: :price
|
||||
json.prices_attributes plan.prices, partial: 'api/prices/price', as: :price
|
||||
if plan.plan_file
|
||||
json.plan_file_attributes do
|
||||
json.id plan.plan_file.id
|
||||
json.attachment_identifier plan.plan_file.attachment_identifier
|
||||
json.attachment_name plan.plan_file.attachment_identifier
|
||||
json.attachment_url plan.plan_file.attachment.url
|
||||
end
|
||||
end
|
||||
|
||||
@ -17,4 +18,12 @@ if plan.respond_to?(:partners)
|
||||
json.last_name partner.last_name
|
||||
json.email partner.email
|
||||
end
|
||||
json.partner_id plan.partner_id
|
||||
end
|
||||
|
||||
if plan.advanced_accounting
|
||||
json.advanced_accounting_attributes do
|
||||
json.partial! 'api/advanced_accounting/advanced_accounting', advanced_accounting: plan.advanced_accounting
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1,4 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! @user, :id, :email, :first_name, :last_name
|
||||
json.extract! @user, :id, :email
|
||||
json.name @user.profile.full_name
|
||||
json.profile_attributes do
|
||||
json.extract! @user.profile, :first_name, :last_name
|
||||
end
|
||||
|
@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.users @users do |user|
|
||||
json.extract! user, :id, :email, :first_name, :last_name
|
||||
json.phone user.profile.phone
|
||||
json.array!(@users) do |user|
|
||||
json.extract! user, :id, :email
|
||||
json.name user.profile.full_name
|
||||
json.profile_attributes do
|
||||
json.extract! user.profile, :first_name, :last_name, :phone
|
||||
end
|
||||
json.resource user.roles.last.resource
|
||||
end
|
||||
|
@ -83,54 +83,51 @@ en:
|
||||
general_information: "General information"
|
||||
name: "Name"
|
||||
name_max_length: "Name length must be less than 24 characters."
|
||||
type: "Type"
|
||||
partner: "Partner"
|
||||
standard: "Standard"
|
||||
type_is_required: "Type is required."
|
||||
group: "Group"
|
||||
groups: "Groups"
|
||||
all: "All"
|
||||
transversal: "Transversal plan"
|
||||
transversal_help: "If this option is checked, a copy of this plan will be created for each currently enabled groups."
|
||||
group_is_required: "Group is required."
|
||||
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"
|
||||
number_of_periods_is_required: "Number of periods is required."
|
||||
period: "Period"
|
||||
year: "Year"
|
||||
month: "Month"
|
||||
week: "Week"
|
||||
period_is_required: "Period is required."
|
||||
subscription_price: "Subscription price"
|
||||
price_is_required: "Price is required."
|
||||
edit_amount_info: "Please note that if you change the price of this plan, the new price will only apply to new subscribers. Current subscriptions will stay unchanged, even those with running payment schedule."
|
||||
visual_prominence_of_the_subscription: "Visual prominence of the subscription"
|
||||
on_the_subscriptions_page_the_most_prominent_subscriptions_will_be_placed_at_the_top_of_the_list: "On the subscriptions page, the most prominent subscriptions will be placed at the top of the list."
|
||||
an_evelated_number_means_a_higher_prominence: "An elevated number means a higher prominence."
|
||||
edit_amount_info: "Please note that if you change the price of this plan, the new price will only apply to new subscribers. Current subscriptions will stay unchanged, even those with a running payment schedule."
|
||||
visual_prominence: "Visual prominence of the subscription"
|
||||
visual_prominence_help: "On the subscriptions page, the most prominent subscriptions will be placed at the top of the list. An elevated number means a higher prominence."
|
||||
rolling_subscription: "Rolling subscription?"
|
||||
a_rolling_subscription_will_begin_the_day_of_the_first_training: "A rolling subscription will begin the day of the first trainings."
|
||||
otherwise_it_will_begin_as_soon_as_it_is_bought: "Otherwise, it will begin as soon as it is bought."
|
||||
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_info: "If monthly payment is enabled, the members will be able to choose between a one-time payment or a payment schedule staged each months."
|
||||
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"
|
||||
type_a_short_description: "Type a short description"
|
||||
information_sheet: "Information sheet"
|
||||
attach_an_information_sheet: "Attach an information sheet"
|
||||
notified_partner: "Notified partner"
|
||||
new_user: "New user ..."
|
||||
as_part_of_a_partner_subscription_some_notifications_may_be_sent_to_this_user: "As part of a partner subscription, some notifications may be sent to this user."
|
||||
new_partner: "New partner"
|
||||
first_name: "First name"
|
||||
first_name_is_required: "First name is required."
|
||||
surname: "Last name"
|
||||
surname_is_required: "Last name is required."
|
||||
email_address: "Email address"
|
||||
email_address_is_required: "Email address is required."
|
||||
alert_partner_notification: "As part of a partner subscription, some notifications may be sent to this user."
|
||||
disabled: "Disable subscription"
|
||||
disable_plan_will_not_unsubscribe_users: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it."
|
||||
disabled_help: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it."
|
||||
duration: "Duration"
|
||||
partnership: "Partnership"
|
||||
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"
|
||||
create_success: "The plan was created successfully"
|
||||
create_success: "Plan(s) successfully created. Don't forget to redefine prices."
|
||||
update_success: "The plan was updated successfully"
|
||||
partner_modal:
|
||||
title: "Create a new partner"
|
||||
create_partner: "Create the partner"
|
||||
first_name: "First name"
|
||||
surname: "Last name"
|
||||
email: "Email address"
|
||||
plan_pricing_form:
|
||||
prices: "Prices"
|
||||
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?"
|
||||
@ -624,21 +621,9 @@ en:
|
||||
#add a subscription plan on the platform
|
||||
new:
|
||||
add_a_subscription_plan: "Add a subscription plan"
|
||||
unable_to_create_the_subscription_please_try_again: "Unable to create the subscription plan. Please try again."
|
||||
successfully_created_subscriptions_dont_forget_to_redefine_prices: "Subscription(s) successfully created. Don't forget to redefine prices."
|
||||
unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name: "Unable to save this user. Check that there isn't an already defined user with the same name."
|
||||
#edit a subscription plan / machine slots prices
|
||||
edit:
|
||||
subscription_plan: "Subscription plan:"
|
||||
prices: "Prices"
|
||||
copy_prices_from: "Copy prices from"
|
||||
machines: "Machines"
|
||||
machine: "Machine"
|
||||
hourly_rate: "Hourly rate"
|
||||
spaces: "Spaces"
|
||||
space: "Space"
|
||||
unable_to_save_subscription_changes_please_try_again: "Unable to save subscription changes. Please try again."
|
||||
subscription_successfully_changed: "Subscription successfully changed."
|
||||
#list of all invoices & invoicing parameters
|
||||
invoices:
|
||||
invoices: "Invoices"
|
||||
|
@ -172,86 +172,6 @@ en:
|
||||
method_check: "By check"
|
||||
card_collection_info: "By validating, you'll be prompted for the member's card number. This card will be automatically charged at the deadlines."
|
||||
check_collection_info: "By validating, you confirm that you have {DEADLINES} checks, allowing you to collect all the monthly payments."
|
||||
event_themes:
|
||||
title: "Event themes"
|
||||
select_theme: "Pick up a theme…"
|
||||
#event edition form
|
||||
event:
|
||||
title: "Title"
|
||||
title_is_required: "Title is required."
|
||||
matching_visual: "Matching visual"
|
||||
choose_a_picture: "Choose a picture"
|
||||
description: "Description"
|
||||
description_is_required: "Description is required."
|
||||
attachments: "Attachments"
|
||||
add_a_new_file: "Add a new file"
|
||||
event_type: "Event type"
|
||||
dates_and_opening_hours: "Dates and opening hours"
|
||||
all_day: "All day"
|
||||
start_date: "Start date"
|
||||
end_date: "End date"
|
||||
start_time: "Start time"
|
||||
end_time: "End time"
|
||||
recurrence: "Recurrence"
|
||||
_and_ends_on: "and ends on"
|
||||
prices_and_availabilities: "Prices and availabilities"
|
||||
standard_rate: "Standard rate"
|
||||
0_equal_free: "0 = free"
|
||||
tickets_available: "Tickets available"
|
||||
event_themes: "Event themes"
|
||||
select_theme: "Pick up a theme..."
|
||||
age_range: "Age range"
|
||||
add_price: "Add a price"
|
||||
#subscription plan edition form
|
||||
plan:
|
||||
general_information: "General information"
|
||||
name: "Name"
|
||||
name_is_required: "Name is required"
|
||||
name_length_must_be_less_than_24_characters: "Name length must be less than 24 characters."
|
||||
type: "Type"
|
||||
partner: "Partner"
|
||||
standard: "Standard"
|
||||
type_is_required: "Type is required."
|
||||
group: "Group"
|
||||
groups: "Groups"
|
||||
all: "All"
|
||||
transversal_all_groups: "Transversal (all groups)"
|
||||
group_is_required: "Group is required."
|
||||
category: "Category"
|
||||
number_of_periods: "Number of periods"
|
||||
number_of_periods_is_required: "Number of periods is required."
|
||||
period: "Period"
|
||||
year: "Year"
|
||||
month: "Month"
|
||||
week: "Week"
|
||||
period_is_required: "Period is required."
|
||||
subscription_price: "Subscription price"
|
||||
price_is_required: "Price is required."
|
||||
edit_amount_info: "Please note that if you change the price of this plan, the new price will only apply to new subscribers. Current subscriptions will stay unchanged, even those with running payment schedule."
|
||||
visual_prominence_of_the_subscription: "Visual prominence of the subscription"
|
||||
on_the_subscriptions_page_the_most_prominent_subscriptions_will_be_placed_at_the_top_of_the_list: "On the subscriptions page, the most prominent subscriptions will be placed at the top of the list."
|
||||
an_evelated_number_means_a_higher_prominence: "An elevated number means a higher prominence."
|
||||
rolling_subscription: "Rolling subscription?"
|
||||
a_rolling_subscription_will_begin_the_day_of_the_first_training: "A rolling subscription will begin the day of the first trainings."
|
||||
otherwise_it_will_begin_as_soon_as_it_is_bought: "Otherwise, it will begin as soon as it is bought."
|
||||
monthly_payment: "Monthly payment?"
|
||||
monthly_payment_info: "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"
|
||||
type_a_short_description: "Type a short description"
|
||||
information_sheet: "Information sheet"
|
||||
attach_an_information_sheet: "Attach an information sheet"
|
||||
notified_partner: "Notified partner"
|
||||
new_user: "New user ..."
|
||||
as_part_of_a_partner_subscription_some_notifications_may_be_sent_to_this_user: "As part of a partner subscription, some notifications may be sent to this user."
|
||||
new_partner: "New partner"
|
||||
first_name: "First name"
|
||||
first_name_is_required: "First name is required."
|
||||
surname: "Last name"
|
||||
surname_is_required: "Last name is required."
|
||||
email_address: "Email address"
|
||||
email_address_is_required: "Email address is required."
|
||||
disabled: "Disable subscription"
|
||||
disable_plan_will_not_unsubscribe_users: "Beware: disabling this plan won't unsubscribe users having active subscriptions with it."
|
||||
#partial form to edit/create a user (admin view)
|
||||
user_admin:
|
||||
user: "User"
|
||||
|
Loading…
x
Reference in New Issue
Block a user