mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
(ui) refactor training form
This commit is contained in:
parent
4203da097c
commit
3d88266fe6
@ -8,4 +8,24 @@ export default class TrainingAPI {
|
||||
const res: AxiosResponse<Array<Training>> = await apiClient.get(`/api/trainings${ApiLib.filtersToQuery(filters)}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async create (training: Training): Promise<Training> {
|
||||
const data = ApiLib.serializeAttachments(training, 'training', ['training_image_attributes']);
|
||||
const res: AxiosResponse<Training> = await apiClient.post('/api/trainings', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async update (training: Training): Promise<Training> {
|
||||
const data = ApiLib.serializeAttachments(training, 'training', ['training_image_attributes']);
|
||||
const res: AxiosResponse<Training> = await apiClient.put(`/api/trainings/${training.id}`, data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,130 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { FormImageUpload } from '../form/form-image-upload';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { Loader } from '../base/loader';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { ErrorBoundary } from '../base/error-boundary';
|
||||
import { FormRichText } from '../form/form-rich-text';
|
||||
import { FormSwitch } from '../form/form-switch';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { Training } from '../../models/training';
|
||||
import TrainingAPI from '../../api/training';
|
||||
import { FormMultiSelect } from '../form/form-multi-select';
|
||||
import MachineAPI from '../../api/machine';
|
||||
import { Machine } from '../../models/machine';
|
||||
import { SelectOption } from '../../models/select';
|
||||
import SettingAPI from '../../api/setting';
|
||||
import { Setting } from '../../models/setting';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface TrainingFormProps {
|
||||
action: 'create' | 'update',
|
||||
training?: Training,
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* Form to edit or create trainings
|
||||
*/
|
||||
export const TrainingForm: React.FC<TrainingFormProps> = ({ action, training, onError, onSuccess }) => {
|
||||
const [machineModule, setMachineModule] = useState<Setting>(null);
|
||||
const { handleSubmit, register, control, setValue, formState } = useForm<Training>({ defaultValues: { ...training } });
|
||||
const output = useWatch<Training>({ control });
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
useEffect(() => {
|
||||
SettingAPI.get('machines_module').then(setMachineModule).catch(onError);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Callback triggered when the user validates the machine form: handle create or update
|
||||
*/
|
||||
const onSubmit: SubmitHandler<Training> = (data: Training) => {
|
||||
TrainingAPI[action](data).then((res) => {
|
||||
onSuccess(t(`app.admin.training_form.${action}_success`));
|
||||
window.location.href = `/#!/trainings/${res.slug}`;
|
||||
}).catch(error => {
|
||||
onError(error);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a machine to an option usable by react-select
|
||||
*/
|
||||
const machineToOption = (machine: Machine): SelectOption<number> => {
|
||||
return { value: machine.id, label: machine.name };
|
||||
};
|
||||
|
||||
/**
|
||||
* Asynchronously load the full list of enabled machines to display in the drop-down select field
|
||||
*/
|
||||
const loadMachines = (inputValue: string, callback: (options: Array<SelectOption<number>>) => void): void => {
|
||||
MachineAPI.index({ disabled: false }).then(data => {
|
||||
callback(data.map(m => machineToOption(m)));
|
||||
}).catch(error => onError(error));
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="training-form" onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormInput register={register} id="name"
|
||||
formState={formState}
|
||||
rules={{ required: true }}
|
||||
label={t('app.admin.training_form.name')} />
|
||||
<FormImageUpload setValue={setValue}
|
||||
register={register}
|
||||
control={control}
|
||||
formState={formState}
|
||||
rules={{ required: true }}
|
||||
id="training_image_attributes"
|
||||
accept="image/*"
|
||||
defaultImage={output.training_image_attributes}
|
||||
label={t('app.admin.training_form.illustration')} />
|
||||
<FormRichText control={control}
|
||||
id="description"
|
||||
rules={{ required: true }}
|
||||
label={t('app.admin.training_form.description')}
|
||||
limit={null}
|
||||
heading bulletList blockquote link video image />
|
||||
{machineModule?.value === 'true' && <FormMultiSelect control={control}
|
||||
id="machine_ids"
|
||||
formState={formState}
|
||||
label={t('app.admin.training_form.associated_machines')}
|
||||
loadOptions={loadMachines} />}
|
||||
<FormInput register={register}
|
||||
type="number"
|
||||
id="nb_total_places"
|
||||
formState={formState}
|
||||
rules={{ required: true }}
|
||||
label={t('app.admin.training_form.default_seats')} />
|
||||
<FormSwitch control={control}
|
||||
id="public_page"
|
||||
defaultValue={true}
|
||||
label={t('app.admin.training_form.public_page')}
|
||||
tooltip={t('app.admin.training_form.public_help')} />
|
||||
<FormSwitch control={control}
|
||||
id="disabled"
|
||||
label={t('app.admin.training_form.disable_training')}
|
||||
tooltip={t('app.admin.training_form.disabled_help')} />
|
||||
<FabButton type="submit" className="is-info submit-btn">
|
||||
{t('app.admin.training_form.ACTION_training', { ACTION: action })}
|
||||
</FabButton>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const TrainingFormWrapper: React.FC<TrainingFormProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<ErrorBoundary>
|
||||
<TrainingForm {...props} />
|
||||
</ErrorBoundary>
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('trainingForm', react2angular(TrainingFormWrapper, ['action', 'training', 'onError', 'onSuccess']));
|
@ -81,20 +81,23 @@ class TrainingsController {
|
||||
/**
|
||||
* Controller used in the training creation page (admin)
|
||||
*/
|
||||
Application.Controllers.controller('NewTrainingController', ['$scope', '$state', 'machinesPromise', 'settingsPromise', 'CSRF',
|
||||
function ($scope, $state, machinesPromise, settingsPromise, CSRF) {
|
||||
/* PUBLIC SCOPE */
|
||||
Application.Controllers.controller('NewTrainingController', ['$scope', '$state', 'CSRF', 'growl',
|
||||
function ($scope, $state, CSRF, growl) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// Form action on the following URL
|
||||
$scope.method = 'post';
|
||||
/**
|
||||
* Callback triggered by react components
|
||||
*/
|
||||
$scope.onSuccess = function (message) {
|
||||
growl.success(message);
|
||||
};
|
||||
|
||||
// API URL where the form will be posted
|
||||
$scope.actionUrl = '/api/trainings/';
|
||||
|
||||
// list of machines
|
||||
$scope.machines = machinesPromise;
|
||||
|
||||
$scope.enableMachinesModule = settingsPromise.machines_module === 'true';
|
||||
/**
|
||||
* Callback triggered by react components
|
||||
*/
|
||||
$scope.onError = function (message) {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
@ -116,23 +119,26 @@ Application.Controllers.controller('NewTrainingController', ['$scope', '$state',
|
||||
/**
|
||||
* Controller used in the training edition page (admin)
|
||||
*/
|
||||
Application.Controllers.controller('EditTrainingController', ['$scope', '$state', '$transition$', 'trainingPromise', 'machinesPromise', 'settingsPromise', 'CSRF',
|
||||
function ($scope, $state, $transition$, trainingPromise, machinesPromise, settingsPromise, CSRF) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// Form action on the following URL
|
||||
$scope.method = 'patch';
|
||||
|
||||
// API URL where the form will be posted
|
||||
$scope.actionUrl = `/api/trainings/${$transition$.params().id}`;
|
||||
Application.Controllers.controller('EditTrainingController', ['$scope', '$state', '$transition$', 'trainingPromise', 'CSRF', 'growl',
|
||||
function ($scope, $state, $transition$, trainingPromise, CSRF, growl) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// Details of the training to edit (id in URL)
|
||||
$scope.training = trainingPromise;
|
||||
$scope.training = cleanTraining(trainingPromise);
|
||||
|
||||
// list of machines
|
||||
$scope.machines = machinesPromise;
|
||||
/**
|
||||
* Callback triggered by react components
|
||||
*/
|
||||
$scope.onSuccess = function (message) {
|
||||
growl.success(message);
|
||||
};
|
||||
|
||||
$scope.enableMachinesModule = settingsPromise.machines_module === 'true';
|
||||
/**
|
||||
* Callback triggered by react components
|
||||
*/
|
||||
$scope.onError = function (message) {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
@ -146,6 +152,13 @@ Application.Controllers.controller('EditTrainingController', ['$scope', '$state'
|
||||
return new TrainingsController($scope, $state);
|
||||
};
|
||||
|
||||
// prepare the training for the react-hook-form
|
||||
function cleanTraining (training) {
|
||||
delete training.$promise;
|
||||
delete training.$resolved;
|
||||
return training;
|
||||
}
|
||||
|
||||
// !!! MUST BE CALLED AT THE END of the controller
|
||||
return initialize();
|
||||
}
|
||||
|
@ -234,16 +234,6 @@ Application.Controllers.controller('MachinesController', ['$scope', '$state', '_
|
||||
Application.Controllers.controller('NewMachineController', ['$scope', '$state', 'CSRF', 'growl', function ($scope, $state, CSRF, growl) {
|
||||
CSRF.setMetaTags();
|
||||
|
||||
// API URL where the form will be posted
|
||||
$scope.actionUrl = '/api/machines/';
|
||||
|
||||
// Form action on the above URL
|
||||
$scope.method = 'post';
|
||||
|
||||
// default machine parameters
|
||||
$scope.machine =
|
||||
{ machine_files_attributes: [] };
|
||||
|
||||
/**
|
||||
* Shows an error message forwarded from a child component
|
||||
*/
|
||||
@ -268,13 +258,7 @@ Application.Controllers.controller('NewMachineController', ['$scope', '$state',
|
||||
*/
|
||||
Application.Controllers.controller('EditMachineController', ['$scope', '$state', '$transition$', 'machinePromise', 'CSRF', 'growl',
|
||||
function ($scope, $state, $transition$, machinePromise, CSRF, growl) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// API URL where the form will be posted
|
||||
$scope.actionUrl = `/api/machines/${$transition$.params().id}`;
|
||||
|
||||
// Form action on the above URL
|
||||
$scope.method = 'put';
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
|
||||
$scope.machine = cleanMachine(machinePromise);
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { ApiFilter } from './api';
|
||||
import { TDateISO } from '../typings/date-iso';
|
||||
import { FileType } from './file';
|
||||
|
||||
export interface Training {
|
||||
id?: number,
|
||||
@ -6,11 +8,21 @@ export interface Training {
|
||||
description: string,
|
||||
machine_ids: number[],
|
||||
nb_total_places: number,
|
||||
slug: string,
|
||||
slug?: string,
|
||||
public_page?: boolean,
|
||||
disabled?: boolean,
|
||||
plan_ids?: number[],
|
||||
training_image?: string,
|
||||
training_image_attributes?: FileType,
|
||||
availabilities?: Array<{
|
||||
id: number,
|
||||
start_at: TDateISO,
|
||||
end_at: TDateISO,
|
||||
reservation_users: Array<{
|
||||
id: number,
|
||||
full_name: string,
|
||||
is_valid: boolean
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
export interface TrainingIndexFilter extends ApiFilter {
|
||||
|
@ -504,7 +504,7 @@ angular.module('application.router', ['ui.router'])
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
trainingsPromise: ['Training', function (Training) { return Training.query({ public_page: true }).$promise; }]
|
||||
trainingsPromise: ['Training', function (Training) { return Training.query({ public_page: true, disabled: false }).$promise; }]
|
||||
}
|
||||
})
|
||||
.state('app.public.training_show', {
|
||||
@ -761,10 +761,6 @@ angular.module('application.router', ['ui.router'])
|
||||
templateUrl: '/admin/trainings/new.html',
|
||||
controller: 'NewTrainingController'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['machines_module']" }).$promise; }]
|
||||
}
|
||||
})
|
||||
.state('app.admin.trainings_edit', {
|
||||
@ -777,9 +773,7 @@ angular.module('application.router', ['ui.router'])
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
trainingPromise: ['Training', '$transition$', function (Training, $transition$) { return Training.get({ id: $transition$.params().id }).$promise; }],
|
||||
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['machines_module']" }).$promise; }]
|
||||
trainingPromise: ['Training', '$transition$', function (Training, $transition$) { return Training.get({ id: $transition$.params().id }).$promise; }]
|
||||
}
|
||||
})
|
||||
// events
|
||||
|
@ -123,6 +123,7 @@
|
||||
@import "modules/supporting-documents/supporting-documents-validation";
|
||||
@import "modules/supporting-documents/supporting-documents-type-form";
|
||||
@import "modules/supporting-documents/supporting-documents-types-list";
|
||||
@import "modules/trainings/training-form";
|
||||
@import "modules/user/avatar";
|
||||
@import "modules/user/avatar-input";
|
||||
@import "modules/user/gender-input";
|
||||
|
@ -0,0 +1,5 @@
|
||||
.training-form {
|
||||
.submit-btn {
|
||||
float: right;
|
||||
}
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
<form role="form"
|
||||
name="trainingForm"
|
||||
class="form-horizontal"
|
||||
ng-attr-action="{{ actionUrl }}"
|
||||
ng-upload="submited(content)"
|
||||
upload-options-enable-rails-csrf="true"
|
||||
unsaved-warning-form
|
||||
novalidate>
|
||||
|
||||
<input name="_method" type="hidden" ng-value="method">
|
||||
|
||||
<section class="panel panel-default bg-light m-lg">
|
||||
<div class="panel-body m-r">
|
||||
|
||||
<uib-alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</uib-alert>
|
||||
|
||||
<div class="form-group m-b-lg" ng-class="{'has-error': trainingForm['training[name]'].$dirty && trainingForm['training[name]'].$invalid}">
|
||||
<label for="name" class="col-sm-2 control-label">{{ 'app.shared.trainings.name' | translate }} *</label>
|
||||
<div class="col-sm-4">
|
||||
<input name="training[name]"
|
||||
ng-model="training.name"
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="training_name"
|
||||
placeholder="{{'app.shared.trainings.name' | translate}}"
|
||||
required/>
|
||||
<span class="help-block" ng-show="trainingForm['training[name]'].$dirty && trainingForm['training[name]'].$error.required" translate>{{ 'app.shared.trainings.name_is_required' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group m-b-lg">
|
||||
<label for="training_image" class="col-sm-2 control-label">{{ 'app.shared.trainings.illustration' | translate }} *</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="fileinput" data-provides="fileinput" ng-class="fileinputClass(training.training_image)">
|
||||
<div class="fileinput-new thumbnail" style="width: 334px; height: 250px;">
|
||||
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:'Font Awesome 5 Free'/icon" bs-holder ng-if="!training.training_image">
|
||||
</div>
|
||||
<div class="fileinput-preview fileinput-exists thumbnail" style="max-width: 334px;">
|
||||
<img ng-src="{{ training.training_image }}" alt="" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="btn btn-default btn-file">
|
||||
<span class="fileinput-new">{{ 'app.shared.trainings.add_an_illustration' | translate }} <i class="fa fa-upload fa-fw"></i></span>
|
||||
<span class="fileinput-exists" translate>{{ 'app.shared.buttons.change' }}</span>
|
||||
<input type="file"
|
||||
ng-model="training.training_image"
|
||||
name="training[training_image_attributes][attachment]"
|
||||
accept="image/*"
|
||||
required
|
||||
bs-jasny-fileinput>
|
||||
</span>
|
||||
<a class="btn btn-danger fileinput-exists" data-dismiss="fileinput" translate>{{ 'app.shared.buttons.delete' }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group m-b-xl" ng-class="{'has-error': trainingForm['training[description]'].$dirty && trainingForm['training[description]'].$invalid}">
|
||||
<label for="training_description" class="col-sm-2 control-label">{{ 'app.shared.trainings.description' | translate }} *</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="hidden" name="training[description]" ng-value="training.description" />
|
||||
<summernote ng-model="training.description" id="training_description" placeholder="" config="summernoteOpts" name="training[description]" required></summernote>
|
||||
<span class="help-block" ng-show="trainingForm['training[description]'].$dirty && trainingForm['training[description]'].$error.required" translate>{{ 'app.shared.trainings.description_is_required' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="enableMachinesModule" class="form-group m-b-lg" ng-class="{'has-error': trainingForm['training[machine_ids]'].$dirty && trainingForm['training[machine_ids]'].$invalid}">
|
||||
<label for="training_machines" class="col-sm-2 control-label">{{ 'app.shared.trainings.associated_machines' | translate }}</label>
|
||||
<div class="col-sm-4">
|
||||
<ui-select multiple ng-model="training.machine_ids" class="form-control" id="training_machines">
|
||||
<ui-select-match>
|
||||
<span ng-bind="$item.name"></span>
|
||||
<input type="hidden" name="training[machine_ids][]" value="{{$item.id}}" />
|
||||
</ui-select-match>
|
||||
<ui-select-choices ui-disable-choice="m.disabled" repeat="m.id as m in (machines | filter: $select.search)">
|
||||
<span ng-bind-html="m.name | highlight: $select.search"></span>
|
||||
</ui-select-choices>
|
||||
<ui-select-no-choice>
|
||||
<input type="hidden" name="training[machine_ids][]" value="" />
|
||||
</ui-select-no-choice>
|
||||
</ui-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group m-b-lg" ng-class="{'has-error': trainingForm['training[nb_total_places]'].$dirty && trainingForm['training[nb_total_places]'].$invalid}">
|
||||
<label for="training_nb_total_places" class="col-sm-2 control-label">{{ 'app.shared.trainings.number_of_tickets' | translate }}</label>
|
||||
<div class="col-sm-4">
|
||||
<input ng-model="training.nb_total_places"
|
||||
type="number"
|
||||
min="0"
|
||||
name="training[nb_total_places]"
|
||||
class="form-control"
|
||||
id="training_nb_total_places">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="training[public_page]" class="control-label col-sm-2" translate>
|
||||
{{ 'app.shared.trainings.public_page' }}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input bs-switch
|
||||
ng-model="training.public_page"
|
||||
name="training[public_page]"
|
||||
type="checkbox"
|
||||
class="form-control"
|
||||
switch-on-text="{{ 'app.shared.buttons.yes' | translate }}"
|
||||
switch-off-text="{{ 'app.shared.buttons.no' | translate }}"
|
||||
switch-active="{{!training.disabled}}"
|
||||
switch-animate="true"/>
|
||||
<input type="hidden" name="training[public_page]" value="{{training.public_page}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="training[disabled]" class="control-label col-sm-2" translate>
|
||||
{{ 'app.shared.trainings.disable_training' }}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input bs-switch
|
||||
ng-model="training.disabled"
|
||||
name="training[disabled]"
|
||||
type="checkbox"
|
||||
class="form-control"
|
||||
switch-on-text="{{ 'app.shared.buttons.yes' | translate }}"
|
||||
switch-off-text="{{ 'app.shared.buttons.no' | translate }}"
|
||||
ng-change="onDisableToggled()"
|
||||
switch-animate="true"/>
|
||||
<input type="hidden" name="training[disabled]" value="{{training.disabled}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- ./panel-body -->
|
||||
|
||||
<div class="panel-footer no-padder">
|
||||
<input type="submit"
|
||||
value="{{ 'app.shared.trainings.validate_your_training' | translate }}"
|
||||
class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c"
|
||||
ng-disabled="trainingForm.$invalid"/>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
@ -22,6 +22,10 @@
|
||||
|
||||
<div class="row no-gutter">
|
||||
<div class="col-sm-12 col-md-12 col-lg-9 b-r-lg nopadding">
|
||||
<ng-include src="'/admin/trainings/_form.html'"></ng-include>
|
||||
<div class="panel panel-default bg-light m-lg">
|
||||
<div class="panel-body m-r">
|
||||
<training-form action="'update'" training="training" on-error="onError" on-success="onSuccess"></training-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -26,7 +26,11 @@
|
||||
{{ 'app.admin.trainings_new.dont_forget_to_change_them_before_creating_slots_for_this_training' | translate }}
|
||||
</div>
|
||||
|
||||
<ng-include src="'/admin/trainings/_form.html'"></ng-include>
|
||||
<div class="panel panel-default bg-light m-lg">
|
||||
<div class="panel-body m-r">
|
||||
<training-form action="'create'" on-error="onError" on-success="onSuccess"></training-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
|
@ -1,139 +0,0 @@
|
||||
<form role="form"
|
||||
name="machineForm"
|
||||
class="form-horizontal"
|
||||
action="{{ actionUrl }}"
|
||||
ng-upload="submited(content)"
|
||||
upload-options-enable-rails-csrf="true"
|
||||
unsaved-warning-form
|
||||
novalidate>
|
||||
|
||||
<input name="_method" type="hidden" ng-value="method">
|
||||
|
||||
<section class="panel panel-default bg-light m-lg">
|
||||
<div class="panel-body m-r">
|
||||
|
||||
<uib-alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</uib-alert>
|
||||
|
||||
<div class="form-group m-b-lg" ng-class="{'has-error': machineForm['machine[name]'].$dirty && machineForm['machine[name]'].$invalid}">
|
||||
<label for="name" class="col-sm-2 control-label">{{ 'app.shared.machine.name' | translate }} *</label>
|
||||
<div class="col-sm-4">
|
||||
<input ng-model="machine.name"
|
||||
type="text"
|
||||
name="machine[name]"
|
||||
class="form-control"
|
||||
id="machine_name"
|
||||
placeholder="{{'app.shared.machine.name' | translate}}"
|
||||
required>
|
||||
<span class="help-block" ng-show="machineForm['machine[name]'].$dirty && machineForm['machine[name]'].$error.required" translate>{{ 'app.shared.machine.name_is_required' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group m-b-lg">
|
||||
<label for="machine_image" class="col-sm-2 control-label">{{ 'app.shared.machine.illustration' | translate }} *</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="fileinput" data-provides="fileinput" ng-class="fileinputClass(machine.machine_image)">
|
||||
<div class="fileinput-new thumbnail" style="width: 334px; height: 250px;">
|
||||
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:'Font Awesome 5 Free'/icon" bs-holder ng-if="!machine.machine_image">
|
||||
</div>
|
||||
<div class="fileinput-preview fileinput-exists thumbnail" style="max-width: 334px;">
|
||||
<img ng-src="{{ machine.machine_image }}" alt="" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="btn btn-default btn-file">
|
||||
<span class="fileinput-new">{{ 'app.shared.machine.add_an_illustration' | translate }} <i class="fa fa-upload fa-fw"></i></span>
|
||||
<span class="fileinput-exists" translate>{{ 'app.shared.buttons.change' }}</span>
|
||||
<input type="file"
|
||||
ng-model="machine.machine_image"
|
||||
name="machine[machine_image_attributes][attachment]"
|
||||
accept="image/jpeg,image/gif,image/png"
|
||||
required
|
||||
bs-jasny-fileinput>
|
||||
</span>
|
||||
<button class="btn btn-danger fileinput-exists" data-dismiss="fileinput" translate>{{ 'app.shared.buttons.delete' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group m-b-xl" ng-class="{'has-error': machineForm['machine[description]'].$dirty && machineForm['machine[description]'].$invalid}">
|
||||
<label for="description" class="col-sm-2 control-label">{{ 'app.shared.machine.description' | translate }} *</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="hidden"
|
||||
name="machine[description]"
|
||||
ng-value="machine.description" />
|
||||
<summernote ng-model="machine.description"
|
||||
id="machine_description"
|
||||
placeholder=""
|
||||
config="summernoteOpts"
|
||||
name="machine[description]"
|
||||
required>
|
||||
</summernote>
|
||||
<span class="help-block" ng-show="machineForm['machine[description]'].$dirty && machineForm['machine[description]'].$error.required" translate>{{ 'app.shared.machine.description_is_required' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group m-b-xl" ng-class="{'has-error': machineForm['machine[spec]'].$dirty && machineForm['machine[spec]'].$invalid}">
|
||||
<label for="spec" class="col-sm-2 control-label">{{ 'app.shared.machine.technical_specifications' | translate }} *</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="hidden"
|
||||
name="machine[spec]"
|
||||
ng-value="machine.spec" />
|
||||
<summernote ng-model="machine.spec"
|
||||
id="machine_spec"
|
||||
placeholder=""
|
||||
config="summernoteOpts"
|
||||
name="machine[spec]"
|
||||
required>
|
||||
</summernote>
|
||||
<span class="help-block" ng-show="machineForm['machine[spec]'].$dirty && machineForm['machine[spec]'].$error.required" translate>{{ 'app.shared.machine.technical_specifications_are_required' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group m-b-xl">
|
||||
<label class="col-sm-2 control-label" translate>{{ 'app.shared.machine.attached_files_pdf' }}</label>
|
||||
<div class="col-sm-10">
|
||||
<div ng-repeat="file in machine.machine_files_attributes" ng-show="!file._destroy">
|
||||
<input type="hidden" ng-model="file.id" name="machine[machine_files_attributes][][id]" ng-value="file.id" />
|
||||
<input type="hidden" ng-model="file._destroy" name="machine[machine_files_attributes][][_destroy]" ng-value="file._destroy"/>
|
||||
|
||||
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(file.attachment)">
|
||||
<div class="form-control" data-trigger="fileinput">
|
||||
<i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{file.attachment}}</span>
|
||||
</div>
|
||||
<span class="input-group-addon btn btn-default btn-file"><span class="fileinput-new" translate>{{ 'app.shared.machine.attach_a_file' }}</span>
|
||||
<span class="fileinput-exists" translate>{{ 'app.shared.buttons.change' }}</span><input type="file" name="machine[machine_files_attributes][][attachment]" accept=".pdf"></span>
|
||||
<a class="input-group-addon btn btn-danger fileinput-exists" data-dismiss="fileinput" ng-click="deleteFile(file)"><i class="fa fa-trash-o"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<a class="btn btn-default" ng-click="addFile()" role="button"> {{ 'app.shared.machine.add_an_attachment' | translate }} <i class="fa fa-file-o fa-fw"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="machine[disabled]" class="control-label col-sm-2" translate>
|
||||
{{ 'app.shared.machine.disable_machine' }}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input bs-switch
|
||||
ng-model="machine.disabled"
|
||||
name="machine[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"/>
|
||||
<input type="hidden" name="machine[disabled]" value="{{machine.disabled}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- ./panel-body -->
|
||||
|
||||
<div class="panel-footer no-padder">
|
||||
<input type="submit"
|
||||
value="{{ 'app.shared.machine.validate_your_machine' | translate }}"
|
||||
class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c"
|
||||
ng-disabled="machineForm.$invalid"/>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
@ -23,10 +23,10 @@
|
||||
|
||||
|
||||
<div class="widget panel panel-default">
|
||||
<div class="panel-heading picture" ng-if="!training.training_image" ng-click="showTraining(training)">
|
||||
<div class="panel-heading picture" ng-if="!training.training_image_attributes" ng-click="showTraining(training)">
|
||||
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:'Font Awesome 5 Free'/icon" bs-holder class="img-responsive">
|
||||
</div>
|
||||
<div class="panel-heading picture" style="background-image:url({{training.training_image}})" ng-if="training.training_image" ng-click="showTraining(training)">
|
||||
<div class="panel-heading picture" style="background-image:url({{training.training_image_attributes.attachment_url}})" ng-if="training.training_image_attributes" ng-click="showTraining(training)">
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<h1 class="m-b text-center">{{training.name}}</h1>
|
||||
|
@ -29,8 +29,8 @@
|
||||
|
||||
<div class="article wrapper-lg" >
|
||||
|
||||
<div class="article-thumbnail" ng-if="training.training_image">
|
||||
<img ng-src="{{training.training_image}}" alt="{{training.name}}" class="img-responsive">
|
||||
<div class="article-thumbnail" ng-if="training.training_image_attributes">
|
||||
<img ng-src="{{training.training_image_attributes.attachment_url}}" alt="{{training.name}}" class="img-responsive">
|
||||
</div>
|
||||
|
||||
<p class="intro" ng-bind-html="training.description | toTrusted"></p>
|
||||
|
10
app/views/api/trainings/_training.json.jbuilder
Normal file
10
app/views/api/trainings/_training.json.jbuilder
Normal file
@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! training, :id, :name, :description, :machine_ids, :nb_total_places, :public_page, :disabled, :slug
|
||||
if training.training_image
|
||||
json.training_image_attributes do
|
||||
json.id training.training_image.id
|
||||
json.attachment_name training.training_image.attachment_identifier
|
||||
json.attachment_url training.training_image.attachment.url
|
||||
end
|
||||
end
|
@ -1,9 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
role = (current_user and current_user.admin?) ? 'admin' : 'user'
|
||||
|
||||
json.array!(@trainings) do |training|
|
||||
json.extract! training, :id, :name, :description, :machine_ids, :nb_total_places, :slug, :disabled
|
||||
json.training_image training.training_image.attachment.large.url if training.training_image
|
||||
json.plan_ids training.plan_ids if role == 'admin'
|
||||
json.partial! 'api/trainings/training', training: training
|
||||
json.plan_ids training.plan_ids if current_user&.admin?
|
||||
end
|
||||
|
@ -1,2 +1,3 @@
|
||||
json.extract! @training, :id, :name, :description, :machine_ids, :nb_total_places, :public_page, :disabled
|
||||
json.training_image @training.training_image.attachment.large.url if @training.training_image
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.partial! 'api/trainings/training', training: @training
|
||||
|
@ -11,8 +11,26 @@ en:
|
||||
attach_a_file: "Attach a file"
|
||||
add_an_attachment: "Add an attachment"
|
||||
disable_machine: "Disable machine"
|
||||
disabled_help: "When disabled, the machine won't be reservable and won't appear by default in the machine list."
|
||||
disabled_help: "When disabled, the machine won't be reservable and won't appear by default in the machines list."
|
||||
ACTION_machine: "{ACTION, select, create{Create} other{Update}} the machine"
|
||||
create_success: "The machine was created successfully"
|
||||
update_success: "The machine was updated successfully"
|
||||
training_form:
|
||||
name: "Name"
|
||||
illustration: "Illustration"
|
||||
add_an_illustration: "Add an illustration"
|
||||
description: "Description"
|
||||
add_a_new_training: "Add a new training"
|
||||
validate_your_training: "Validate your training"
|
||||
associated_machines: "Associated machines"
|
||||
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."
|
||||
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."
|
||||
ACTION_training: "{ACTION, select, create{Create} other{Update}} the training"
|
||||
create_success: "The training was created successfully"
|
||||
update_success: "The training was updated successfully"
|
||||
#add a new machine
|
||||
machines_new:
|
||||
declare_a_new_machine: "Declare a new machine"
|
||||
|
Loading…
x
Reference in New Issue
Block a user