mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-28 09:24:24 +01:00
(feat) add machine category
This commit is contained in:
parent
032a05713b
commit
f47440c85a
@ -11,12 +11,12 @@ class API::AvailabilitiesController < API::ApiController
|
|||||||
def index
|
def index
|
||||||
authorize Availability
|
authorize Availability
|
||||||
display_window = window
|
display_window = window
|
||||||
@availabilities = Availability.includes(:machines, :tags, :trainings, :spaces)
|
service = Availabilities::AvailabilitiesService.new(@current_user, 'availability')
|
||||||
.where('start_at >= ? AND end_at <= ?', display_window[:start], display_window[:end])
|
machine_ids = params[:m] || []
|
||||||
|
@availabilities = service.index(display_window,
|
||||||
@availabilities = @availabilities.where.not(available_type: 'event') unless Setting.get('events_in_calendar')
|
{ machines: machine_ids, spaces: params[:s], trainings: params[:t] },
|
||||||
|
(params[:evt] && params[:evt] == 'true'))
|
||||||
@availabilities = @availabilities.where.not(available_type: 'space') unless Setting.get('spaces_module')
|
@availabilities = filter_availabilites(@availabilities)
|
||||||
end
|
end
|
||||||
|
|
||||||
def public
|
def public
|
||||||
|
52
app/controllers/api/machine_categories_controller.rb
Normal file
52
app/controllers/api/machine_categories_controller.rb
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# API Controller for resources of type Machine Category
|
||||||
|
# Categories are used to classify Machine
|
||||||
|
class API::MachineCategoriesController < API::ApiController
|
||||||
|
before_action :authenticate_user!, except: [:index]
|
||||||
|
before_action :set_machine_category, only: %i[show update destroy]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@machine_categories = MachineCategory.all.order(name: :asc)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize MachineCategory
|
||||||
|
@machine_category = MachineCategory.new(machine_category_params)
|
||||||
|
if @machine_category.save
|
||||||
|
render :show, status: :created, location: @category
|
||||||
|
else
|
||||||
|
render json: @machine_category.errors, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize MachineCategory
|
||||||
|
if @machine_category.update(machine_category_params)
|
||||||
|
render :show, status: :ok, location: @category
|
||||||
|
else
|
||||||
|
render json: @machine_category.errors, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize MachineCategory
|
||||||
|
if @machine_category.destroy
|
||||||
|
head :no_content
|
||||||
|
else
|
||||||
|
render json: @machine_category.errors, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_machine_category
|
||||||
|
@machine_category = MachineCategory.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def machine_category_params
|
||||||
|
params.require(:machine_category).permit(:name, machine_ids: [])
|
||||||
|
end
|
||||||
|
end
|
@ -49,7 +49,7 @@ class API::MachinesController < API::ApiController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def machine_params
|
def machine_params
|
||||||
params.require(:machine).permit(:name, :description, :spec, :disabled, :plan_ids,
|
params.require(:machine).permit(:name, :description, :spec, :disabled, :machine_category_id, :plan_ids,
|
||||||
plan_ids: [], machine_image_attributes: [:attachment],
|
plan_ids: [], machine_image_attributes: [:attachment],
|
||||||
machine_files_attributes: %i[id attachment _destroy])
|
machine_files_attributes: %i[id attachment _destroy])
|
||||||
end
|
end
|
||||||
|
25
app/frontend/src/javascript/api/machine-category.ts
Normal file
25
app/frontend/src/javascript/api/machine-category.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import apiClient from './clients/api-client';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { MachineCategory } from '../models/machine-category';
|
||||||
|
|
||||||
|
export default class MachineCategoryAPI {
|
||||||
|
static async index (): Promise<Array<MachineCategory>> {
|
||||||
|
const res: AxiosResponse<Array<MachineCategory>> = await apiClient.get('/api/machine_categories');
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create (category: MachineCategory): Promise<MachineCategory> {
|
||||||
|
const res: AxiosResponse<MachineCategory> = await apiClient.post('/api/machine_categories', { machine_category: category });
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update (category: MachineCategory): Promise<MachineCategory> {
|
||||||
|
const res: AxiosResponse<MachineCategory> = await apiClient.patch(`/api/machine_categories/${category.id}`, { machine_category: category });
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async destroy (categoryId: number): Promise<void> {
|
||||||
|
const res: AxiosResponse<void> = await apiClient.delete(`/api/machine_categories/${categoryId}`);
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FabModal } from '../base/fab-modal';
|
||||||
|
import MachineCategoryAPI from '../../api/machine-category';
|
||||||
|
|
||||||
|
interface DeleteMachineCategoryModalProps {
|
||||||
|
isOpen: boolean,
|
||||||
|
machineCategoryId: number,
|
||||||
|
toggleModal: () => void,
|
||||||
|
onSuccess: (message: string) => void,
|
||||||
|
onError: (message: string) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal dialog to remove a requested machine category
|
||||||
|
*/
|
||||||
|
export const DeleteMachineCategoryModal: React.FC<DeleteMachineCategoryModalProps> = ({ isOpen, toggleModal, onSuccess, machineCategoryId, onError }) => {
|
||||||
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has confirmed the deletion of the requested machine category
|
||||||
|
*/
|
||||||
|
const handleDeleteMachineCategory = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await MachineCategoryAPI.destroy(machineCategoryId);
|
||||||
|
onSuccess(t('app.admin.machines.delete_machine_category_modal.deleted'));
|
||||||
|
} catch (e) {
|
||||||
|
onError(t('app.admin.machines.delete_machine_category_modal.unable_to_delete') + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FabModal title={t('app.admin.machines.delete_machine_category_modal.confirmation_required')}
|
||||||
|
isOpen={isOpen}
|
||||||
|
toggleModal={toggleModal}
|
||||||
|
closeButton={true}
|
||||||
|
confirmButton={t('app.admin.machines.delete_machine_category_modal.confirm')}
|
||||||
|
onConfirm={handleDeleteMachineCategory}
|
||||||
|
className="delete-machine-category-modal">
|
||||||
|
<p>{t('app.admin.machines.delete_machine_category_modal.confirm_machine_category')}</p>
|
||||||
|
</FabModal>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,178 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { MachineCategory } from '../../models/machine-category';
|
||||||
|
import { Machine } from '../../models/machine';
|
||||||
|
import { IApplication } from '../../models/application';
|
||||||
|
import { react2angular } from 'react2angular';
|
||||||
|
import { Loader } from '../base/loader';
|
||||||
|
import MachineCategoryAPI from '../../api/machine-category';
|
||||||
|
import MachineAPI from '../../api/machine';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FabButton } from '../base/fab-button';
|
||||||
|
import { MachineCategoryModal } from './machine-category-modal';
|
||||||
|
import { DeleteMachineCategoryModal } from './delete-machine-category-modal';
|
||||||
|
|
||||||
|
declare const Application: IApplication;
|
||||||
|
|
||||||
|
interface MachineCategoriesListProps {
|
||||||
|
onError: (message: string) => void,
|
||||||
|
onSuccess: (message: string) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component shows a list of all machines and allows filtering on that list.
|
||||||
|
*/
|
||||||
|
export const MachineCategoriesList: React.FC<MachineCategoriesListProps> = ({ onError, onSuccess }) => {
|
||||||
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
|
// shown machine categories
|
||||||
|
const [machineCategories, setMachineCategories] = useState<Array<MachineCategory>>([]);
|
||||||
|
// all machines, for assign to category
|
||||||
|
const [machines, setMachines] = useState<Array<Machine>>([]);
|
||||||
|
// creation/edition modal
|
||||||
|
const [modalIsOpen, setModalIsOpen] = useState<boolean>(false);
|
||||||
|
// currently added/edited category
|
||||||
|
const [machineCategory, setMachineCategory] = useState<MachineCategory>(null);
|
||||||
|
// deletion modal
|
||||||
|
const [destroyModalIsOpen, setDestroyModalIsOpen] = useState<boolean>(false);
|
||||||
|
// currently deleted machine category
|
||||||
|
const [machineCategoryId, setMachineCategoryId] = useState<number>(null);
|
||||||
|
|
||||||
|
// retrieve the full list of machine categories on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
MachineCategoryAPI.index()
|
||||||
|
.then(data => setMachineCategories(data))
|
||||||
|
.catch(e => onError(e));
|
||||||
|
MachineAPI.index()
|
||||||
|
.then(data => setMachines(data))
|
||||||
|
.catch(e => onError(e));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the modal dialog to create/edit a machine category
|
||||||
|
*/
|
||||||
|
const toggleCreateAndEditModal = (): void => {
|
||||||
|
setModalIsOpen(!modalIsOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggred when the current machine category was successfully saved
|
||||||
|
*/
|
||||||
|
const onSaveTypeSuccess = (message: string): void => {
|
||||||
|
setModalIsOpen(false);
|
||||||
|
MachineCategoryAPI.index().then(data => {
|
||||||
|
setMachineCategories(data);
|
||||||
|
onSuccess(message);
|
||||||
|
}).catch((error) => {
|
||||||
|
onError('Unable to load machine categories' + error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init the process of creating a new machine category
|
||||||
|
*/
|
||||||
|
const addMachineCategory = (): void => {
|
||||||
|
setMachineCategory({} as MachineCategory);
|
||||||
|
setModalIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init the process of editing the given machine category
|
||||||
|
*/
|
||||||
|
const editMachineCategory = (category: MachineCategory): () => void => {
|
||||||
|
return (): void => {
|
||||||
|
setMachineCategory(category);
|
||||||
|
setModalIsOpen(true);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init the process of deleting a machine category (ask for confirmation)
|
||||||
|
*/
|
||||||
|
const destroyMachineCategory = (id: number): () => void => {
|
||||||
|
return (): void => {
|
||||||
|
setMachineCategoryId(id);
|
||||||
|
setDestroyModalIsOpen(true);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open/closes the confirmation before deletion modal
|
||||||
|
*/
|
||||||
|
const toggleDestroyModal = (): void => {
|
||||||
|
setDestroyModalIsOpen(!destroyModalIsOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggred when the current machine category was successfully deleted
|
||||||
|
*/
|
||||||
|
const onDestroySuccess = (message: string): void => {
|
||||||
|
setDestroyModalIsOpen(false);
|
||||||
|
MachineCategoryAPI.index().then(data => {
|
||||||
|
setMachineCategories(data);
|
||||||
|
onSuccess(message);
|
||||||
|
}).catch((error) => {
|
||||||
|
onError('Unable to load machine categories' + error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="machine-categories-list">
|
||||||
|
<h3 className="machines-categories">{t('app.admin.machine_categories_list.machine_categories')}</h3>
|
||||||
|
<FabButton onClick={addMachineCategory} className="is-secondary" >{t('app.admin.machine_categories_list.add_a_machine_category')}</FabButton>
|
||||||
|
<MachineCategoryModal isOpen={modalIsOpen}
|
||||||
|
machines={machines}
|
||||||
|
machineCategory={machineCategory}
|
||||||
|
toggleModal={toggleCreateAndEditModal}
|
||||||
|
onSuccess={onSaveTypeSuccess}
|
||||||
|
onError={onError} />
|
||||||
|
<DeleteMachineCategoryModal isOpen={destroyModalIsOpen}
|
||||||
|
machineCategoryId={machineCategoryId}
|
||||||
|
toggleModal={toggleDestroyModal}
|
||||||
|
onSuccess={onDestroySuccess}
|
||||||
|
onError={onError}/>
|
||||||
|
<table className="machine-categories-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: '50%' }}>{t('app.admin.machine_categories_list.name')}</th>
|
||||||
|
<th style={{ width: '30%' }}>{t('app.admin.machine_categories_list.machines_number')}</th>
|
||||||
|
<th style={{ width: '20%' }}></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{machineCategories.map(category => {
|
||||||
|
return (
|
||||||
|
<tr key={category.id}>
|
||||||
|
<td>
|
||||||
|
<span>{category.name}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>{category.machine_ids.length}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="buttons">
|
||||||
|
<FabButton className="edit-btn" onClick={editMachineCategory(category)}>
|
||||||
|
<i className="fa fa-edit" /> {t('app.admin.machine_categories_list.edit')}
|
||||||
|
</FabButton>
|
||||||
|
<FabButton className="delete-btn" onClick={destroyMachineCategory(category.id)}>
|
||||||
|
<i className="fa fa-trash" />
|
||||||
|
</FabButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MachineCategoriesListWrapper: React.FC<MachineCategoriesListProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<Loader>
|
||||||
|
<MachineCategoriesList {...props} />
|
||||||
|
</Loader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Application.Components.component('machineCategoriesList', react2angular(MachineCategoriesListWrapper, ['onError', 'onSuccess']));
|
@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||||
|
import { FormInput } from '../form/form-input';
|
||||||
|
import { FormChecklist } from '../form/form-checklist';
|
||||||
|
import { MachineCategory } from '../../models/machine-category';
|
||||||
|
import { FabButton } from '../base/fab-button';
|
||||||
|
import { Machine } from '../../models/machine';
|
||||||
|
import { SelectOption } from '../../models/select';
|
||||||
|
|
||||||
|
interface MachineCategoryFormProps {
|
||||||
|
machines: Array<Machine>,
|
||||||
|
machineCategory?: MachineCategory,
|
||||||
|
saveMachineCategory: (data: MachineCategory) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form to set create/edit machine category
|
||||||
|
*/
|
||||||
|
export const MachineCategoryForm: React.FC<MachineCategoryFormProps> = ({ machines, machineCategory, saveMachineCategory }) => {
|
||||||
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
|
const { handleSubmit, register, control, formState } = useForm<MachineCategory>({ defaultValues: { ...machineCategory } });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert all machines to the checklist format
|
||||||
|
*/
|
||||||
|
const buildOptions = (): Array<SelectOption<number>> => {
|
||||||
|
return machines.map(t => {
|
||||||
|
return { value: t.id, label: t.name };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when the form is submitted: process with the machine category creation or update.
|
||||||
|
*/
|
||||||
|
const onSubmit: SubmitHandler<MachineCategory> = (data: MachineCategory) => {
|
||||||
|
saveMachineCategory(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="machine-category-form">
|
||||||
|
<form name="machineCategoryForm" onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<FormInput id="name"
|
||||||
|
register={register}
|
||||||
|
rules={{ required: true }}
|
||||||
|
formState={formState}
|
||||||
|
label={t('app.admin.machine_category_form.name')}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h4>{t('app.admin.machine_category_form.assigning_machines')}</h4>
|
||||||
|
<FormChecklist options={buildOptions()}
|
||||||
|
control={control}
|
||||||
|
id="machine_ids"
|
||||||
|
formState={formState} />
|
||||||
|
</div>
|
||||||
|
<div className="main-actions">
|
||||||
|
<FabButton type="submit">
|
||||||
|
{t('app.admin.machine_category_form.save')}
|
||||||
|
</FabButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FabModal, ModalSize } from '../base/fab-modal';
|
||||||
|
import { MachineCategory } from '../../models/machine-category';
|
||||||
|
import { Machine } from '../../models/machine';
|
||||||
|
import MachineCategoryAPI from '../../api/machine-category';
|
||||||
|
import { MachineCategoryForm } from './machine-category-form';
|
||||||
|
|
||||||
|
interface MachineCategoryModalProps {
|
||||||
|
isOpen: boolean,
|
||||||
|
toggleModal: () => void,
|
||||||
|
onSuccess: (message: string) => void,
|
||||||
|
onError: (message: string) => void,
|
||||||
|
machines: Array<Machine>,
|
||||||
|
machineCategory?: MachineCategory,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal dialog to create/edit a machine category
|
||||||
|
*/
|
||||||
|
export const MachineCategoryModal: React.FC<MachineCategoryModalProps> = ({ isOpen, toggleModal, onSuccess, onError, machines, machineCategory }) => {
|
||||||
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the current machine category to the API
|
||||||
|
*/
|
||||||
|
const handleSaveMachineCategory = async (data: MachineCategory): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (machineCategory?.id) {
|
||||||
|
await MachineCategoryAPI.update(data);
|
||||||
|
onSuccess(t('app.admin.machine_category_modal.successfully_updated'));
|
||||||
|
} else {
|
||||||
|
await MachineCategoryAPI.create(data);
|
||||||
|
onSuccess(t('app.admin.machine_category_modal.successfully_created'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (machineCategory?.id) {
|
||||||
|
onError(t('app.admin.machine_category_modal.unable_to_update') + e);
|
||||||
|
} else {
|
||||||
|
onError(t('app.admin.machine_category_modal.unable_to_create') + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FabModal title={t(`app.admin.machine_category_modal.${machineCategory?.id ? 'edit' : 'new'}_machine_category`)}
|
||||||
|
width={ModalSize.large}
|
||||||
|
isOpen={isOpen}
|
||||||
|
toggleModal={toggleModal}
|
||||||
|
closeButton={false}>
|
||||||
|
<MachineCategoryForm machineCategory={machineCategory} machines={machines} saveMachineCategory={handleSaveMachineCategory}/>
|
||||||
|
</FabModal>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
|
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
|
||||||
import { Machine } from '../../models/machine';
|
import { Machine } from '../../models/machine';
|
||||||
import MachineAPI from '../../api/machine';
|
import MachineAPI from '../../api/machine';
|
||||||
@ -11,6 +11,10 @@ import { react2angular } from 'react2angular';
|
|||||||
import { ErrorBoundary } from '../base/error-boundary';
|
import { ErrorBoundary } from '../base/error-boundary';
|
||||||
import { FormRichText } from '../form/form-rich-text';
|
import { FormRichText } from '../form/form-rich-text';
|
||||||
import { FormSwitch } from '../form/form-switch';
|
import { FormSwitch } from '../form/form-switch';
|
||||||
|
import { FormSelect } from '../form/form-select';
|
||||||
|
import { SelectOption } from '../../models/select';
|
||||||
|
import MachineCategoryAPI from '../../api/machine-category';
|
||||||
|
import { MachineCategory } from '../../models/machine-category';
|
||||||
|
|
||||||
declare const Application: IApplication;
|
declare const Application: IApplication;
|
||||||
|
|
||||||
@ -29,6 +33,15 @@ export const MachineForm: React.FC<MachineFormProps> = ({ action, machine, onErr
|
|||||||
const output = useWatch<Machine>({ control });
|
const output = useWatch<Machine>({ control });
|
||||||
const { t } = useTranslation('admin');
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
|
const [machineCategories, setMachineCategories] = useState<Array<MachineCategory>>([]);
|
||||||
|
|
||||||
|
// retrieve the full list of machine categories on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
MachineCategoryAPI.index()
|
||||||
|
.then(data => setMachineCategories(data))
|
||||||
|
.catch(e => onError(e));
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback triggered when the user validates the machine form: handle create or update
|
* Callback triggered when the user validates the machine form: handle create or update
|
||||||
*/
|
*/
|
||||||
@ -40,6 +53,15 @@ export const MachineForm: React.FC<MachineFormProps> = ({ action, machine, onErr
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert all machine categories to the select format
|
||||||
|
*/
|
||||||
|
const buildOptions = (): Array<SelectOption<number>> => {
|
||||||
|
return machineCategories.map(t => {
|
||||||
|
return { value: t.id, label: t.name };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="machine-form" onSubmit={handleSubmit(onSubmit)}>
|
<form className="machine-form" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<FormInput register={register} id="name"
|
<FormInput register={register} id="name"
|
||||||
@ -67,6 +89,11 @@ export const MachineForm: React.FC<MachineFormProps> = ({ action, machine, onErr
|
|||||||
label={t('app.admin.machine_form.technical_specifications')}
|
label={t('app.admin.machine_form.technical_specifications')}
|
||||||
limit={null}
|
limit={null}
|
||||||
heading bulletList blockquote link video image />
|
heading bulletList blockquote link video image />
|
||||||
|
<FormSelect options={buildOptions()}
|
||||||
|
control={control}
|
||||||
|
id="machine_category_id"
|
||||||
|
formState={formState}
|
||||||
|
label={t('app.admin.machine_form.assigning_machine_to_category')} />
|
||||||
|
|
||||||
<FormSwitch control={control}
|
<FormSwitch control={control}
|
||||||
id="disabled"
|
id="disabled"
|
||||||
|
@ -2,18 +2,21 @@ import React from 'react';
|
|||||||
import Select from 'react-select';
|
import Select from 'react-select';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SelectOption } from '../../models/select';
|
import { SelectOption } from '../../models/select';
|
||||||
|
import { MachineCategory } from '../../models/machine-category';
|
||||||
|
|
||||||
interface MachinesFiltersProps {
|
interface MachinesFiltersProps {
|
||||||
onStatusSelected: (enabled: boolean) => void,
|
onFilterChangedBy: (type: string, value: number | boolean | void) => void,
|
||||||
|
machineCategories: Array<MachineCategory>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows filtering on machines list
|
* Allows filtering on machines list
|
||||||
*/
|
*/
|
||||||
export const MachinesFilters: React.FC<MachinesFiltersProps> = ({ onStatusSelected }) => {
|
export const MachinesFilters: React.FC<MachinesFiltersProps> = ({ onFilterChangedBy, machineCategories }) => {
|
||||||
const { t } = useTranslation('public');
|
const { t } = useTranslation('public');
|
||||||
|
|
||||||
const defaultValue = { value: true, label: t('app.public.machines_filters.status_enabled') };
|
const defaultValue = { value: true, label: t('app.public.machines_filters.status_enabled') };
|
||||||
|
const categoryDefaultValue = { value: null, label: t('app.public.machines_filters.all_machines') };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides boolean options in the react-select format (yes/no/all)
|
* Provides boolean options in the react-select format (yes/no/all)
|
||||||
@ -26,16 +29,33 @@ export const MachinesFilters: React.FC<MachinesFiltersProps> = ({ onStatusSelect
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides categories options in the react-select format
|
||||||
|
*/
|
||||||
|
const buildCategoriesOptions = (): Array<SelectOption<number|void>> => {
|
||||||
|
const options = machineCategories.map(c => {
|
||||||
|
return { value: c.id, label: c.name };
|
||||||
|
});
|
||||||
|
return [categoryDefaultValue].concat(options);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback triggered when the user selects a machine status in the dropdown list
|
* Callback triggered when the user selects a machine status in the dropdown list
|
||||||
*/
|
*/
|
||||||
const handleStatusSelected = (option: SelectOption<boolean>): void => {
|
const handleStatusSelected = (option: SelectOption<boolean>): void => {
|
||||||
onStatusSelected(option.value);
|
onFilterChangedBy('status', option.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when the user selects a machine category in the dropdown list
|
||||||
|
*/
|
||||||
|
const handleCategorySelected = (option: SelectOption<number>): void => {
|
||||||
|
onFilterChangedBy('category', option.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="machines-filters">
|
<div className="machines-filters">
|
||||||
<div className="status-filter">
|
<div className="filter-item">
|
||||||
<label htmlFor="status">{t('app.public.machines_filters.show_machines')}</label>
|
<label htmlFor="status">{t('app.public.machines_filters.show_machines')}</label>
|
||||||
<Select defaultValue={defaultValue}
|
<Select defaultValue={defaultValue}
|
||||||
id="status"
|
id="status"
|
||||||
@ -43,6 +63,16 @@ export const MachinesFilters: React.FC<MachinesFiltersProps> = ({ onStatusSelect
|
|||||||
onChange={handleStatusSelected}
|
onChange={handleStatusSelected}
|
||||||
options={buildBooleanOptions()}/>
|
options={buildBooleanOptions()}/>
|
||||||
</div>
|
</div>
|
||||||
|
{machineCategories.length > 0 &&
|
||||||
|
<div className="filter-item">
|
||||||
|
<label htmlFor="category">{t('app.public.machines_filters.filter_by_machine_category')}</label>
|
||||||
|
<Select defaultValue={categoryDefaultValue}
|
||||||
|
id="machine_category"
|
||||||
|
className="category-select"
|
||||||
|
onChange={handleCategorySelected}
|
||||||
|
options={buildCategoriesOptions()}/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Machine } from '../../models/machine';
|
import { Machine, MachineListFilter } from '../../models/machine';
|
||||||
import { IApplication } from '../../models/application';
|
import { IApplication } from '../../models/application';
|
||||||
import { react2angular } from 'react2angular';
|
import { react2angular } from 'react2angular';
|
||||||
import { Loader } from '../base/loader';
|
import { Loader } from '../base/loader';
|
||||||
import MachineAPI from '../../api/machine';
|
import MachineAPI from '../../api/machine';
|
||||||
|
import MachineCategoryAPI from '../../api/machine-category';
|
||||||
|
import { MachineCategory } from '../../models/machine-category';
|
||||||
import { MachineCard } from './machine-card';
|
import { MachineCard } from './machine-card';
|
||||||
import { MachinesFilters } from './machines-filters';
|
import { MachinesFilters } from './machines-filters';
|
||||||
import { User } from '../../models/user';
|
import { User } from '../../models/user';
|
||||||
@ -32,31 +34,65 @@ export const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess,
|
|||||||
const [machines, setMachines] = useState<Array<Machine>>(null);
|
const [machines, setMachines] = useState<Array<Machine>>(null);
|
||||||
// we keep the full list of machines, for filtering
|
// we keep the full list of machines, for filtering
|
||||||
const [allMachines, setAllMachines] = useState<Array<Machine>>(null);
|
const [allMachines, setAllMachines] = useState<Array<Machine>>(null);
|
||||||
|
// shown machine categories
|
||||||
|
const [machineCategories, setMachineCategories] = useState<Array<MachineCategory>>([]);
|
||||||
|
// machine list filter
|
||||||
|
const [filter, setFilter] = useState<MachineListFilter>({
|
||||||
|
status: true,
|
||||||
|
category: null
|
||||||
|
});
|
||||||
|
|
||||||
// retrieve the full list of machines on component mount
|
// retrieve the full list of machines on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
MachineAPI.index()
|
MachineAPI.index()
|
||||||
.then(data => setAllMachines(data))
|
.then(data => setAllMachines(data))
|
||||||
.catch(e => onError(e));
|
.catch(e => onError(e));
|
||||||
|
MachineCategoryAPI.index()
|
||||||
|
.then(data => setMachineCategories(data))
|
||||||
|
.catch(e => onError(e));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// filter the machines shown when the full list was retrieved
|
// filter the machines shown when the full list was retrieved
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleFilterByStatus(true);
|
handleFilter();
|
||||||
}, [allMachines]);
|
}, [allMachines]);
|
||||||
|
|
||||||
/**
|
// filter the machines shown when the filter was changed
|
||||||
* Callback triggered when the user changes the status filter.
|
useEffect(() => {
|
||||||
* Set the 'machines' state to a filtered list, depending on the provided parameter.
|
handleFilter();
|
||||||
* @param status, true = enabled machines, false = disabled machines, null = all machines
|
}, [filter]);
|
||||||
*/
|
|
||||||
const handleFilterByStatus = (status: boolean): void => {
|
|
||||||
if (!allMachines) return;
|
|
||||||
if (status === null) return setMachines(allMachines);
|
|
||||||
|
|
||||||
// enabled machines may have the m.disabled property null (for never disabled machines)
|
/**
|
||||||
// or false (for re-enabled machines)
|
* Callback triggered when the user changes the filter.
|
||||||
setMachines(allMachines.filter(m => !!m.disabled === !status));
|
* filter the machines shown when the filter was changed.
|
||||||
|
*/
|
||||||
|
const handleFilter = (): void => {
|
||||||
|
let machinesFiltered = [];
|
||||||
|
if (allMachines) {
|
||||||
|
if (filter.status === null) {
|
||||||
|
machinesFiltered = allMachines;
|
||||||
|
} else {
|
||||||
|
// enabled machines may have the m.disabled property null (for never disabled machines)
|
||||||
|
// or false (for re-enabled machines)
|
||||||
|
machinesFiltered = allMachines.filter(m => !!m.disabled === !filter.status);
|
||||||
|
}
|
||||||
|
if (filter.category !== null) {
|
||||||
|
machinesFiltered = machinesFiltered.filter(m => m.machine_category_id === filter.category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setMachines(machinesFiltered);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when the user changes the filter.
|
||||||
|
* @param type, status, category
|
||||||
|
* @param value, status and category value
|
||||||
|
*/
|
||||||
|
const handleFilterChangedBy = (type: string, value: number | boolean | void) => {
|
||||||
|
setFilter({
|
||||||
|
...filter,
|
||||||
|
[type]: value
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,7 +105,7 @@ export const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess,
|
|||||||
// TODO: Conditionally display the store ad
|
// TODO: Conditionally display the store ad
|
||||||
return (
|
return (
|
||||||
<div className="machines-list">
|
<div className="machines-list">
|
||||||
<MachinesFilters onStatusSelected={handleFilterByStatus} />
|
<MachinesFilters onFilterChangedBy={handleFilterChangedBy} machineCategories={machineCategories}/>
|
||||||
<div className="all-machines">
|
<div className="all-machines">
|
||||||
{false &&
|
{false &&
|
||||||
<div className='store-ad' onClick={() => linkToStore}>
|
<div className='store-ad' onClick={() => linkToStore}>
|
||||||
|
@ -18,9 +18,15 @@
|
|||||||
* Controller used in the calendar management page
|
* Controller used in the calendar management page
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'AuthService', 'Availability', 'SlotsReservation', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', 'plansPromise', 'groupsPromise', 'settingsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Member', 'uiTourService',
|
Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'AuthService', 'Availability', 'SlotsReservation', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', 'plansPromise', 'groupsPromise', 'settingsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Member', 'uiTourService', 'trainingsPromise', 'spacesPromise', 'machineCategoriesPromise', '$aside',
|
||||||
function ($scope, $state, $uibModal, moment, AuthService, Availability, SlotsReservation, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, plansPromise, groupsPromise, settingsPromise, _t, uiCalendarConfig, CalendarConfig, Member, uiTourService) {
|
function ($scope, $state, $uibModal, moment, AuthService, Availability, SlotsReservation, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, plansPromise, groupsPromise, settingsPromise, _t, uiCalendarConfig, CalendarConfig, Member, uiTourService, trainingsPromise, spacesPromise, machineCategoriesPromise, $aside) {
|
||||||
/* PRIVATE STATIC CONSTANTS */
|
/* PRIVATE STATIC CONSTANTS */
|
||||||
|
machinesPromise.forEach(m => m.checked = true);
|
||||||
|
trainingsPromise.forEach(t => t.checked = true);
|
||||||
|
spacesPromise.forEach(s => s.checked = true);
|
||||||
|
|
||||||
|
// check all formation/machine is select in filter
|
||||||
|
const isSelectAll = (type, scope) => scope[type].length === scope[type].filter(t => t.checked).length;
|
||||||
|
|
||||||
// The calendar is divided in slots of 30 minutes
|
// The calendar is divided in slots of 30 minutes
|
||||||
const BASE_SLOT = '00:30:00';
|
const BASE_SLOT = '00:30:00';
|
||||||
@ -33,9 +39,21 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
|
|||||||
|
|
||||||
/* PUBLIC SCOPE */
|
/* PUBLIC SCOPE */
|
||||||
|
|
||||||
|
// List of trainings
|
||||||
|
$scope.trainings = trainingsPromise.filter(t => !t.disabled);
|
||||||
|
|
||||||
// list of the FabLab machines
|
// list of the FabLab machines
|
||||||
$scope.machines = machinesPromise;
|
$scope.machines = machinesPromise;
|
||||||
|
|
||||||
|
// List of machine categories
|
||||||
|
$scope.machineCategories = machineCategoriesPromise;
|
||||||
|
|
||||||
|
// List of machines group by category
|
||||||
|
$scope.machinesGroupByCategory = [];
|
||||||
|
|
||||||
|
// List of spaces
|
||||||
|
$scope.spaces = spacesPromise.filter(t => !t.disabled);
|
||||||
|
|
||||||
// currently selected availability
|
// currently selected availability
|
||||||
$scope.availability = null;
|
$scope.availability = null;
|
||||||
|
|
||||||
@ -45,12 +63,6 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
|
|||||||
// Should we show the scheduled events in the calendar?
|
// Should we show the scheduled events in the calendar?
|
||||||
$scope.eventsInCalendar = (settingsPromise.events_in_calendar === 'true');
|
$scope.eventsInCalendar = (settingsPromise.events_in_calendar === 'true');
|
||||||
|
|
||||||
// bind the availabilities slots with full-Calendar events
|
|
||||||
$scope.eventSources = [{
|
|
||||||
url: '/api/availabilities',
|
|
||||||
textColor: 'black'
|
|
||||||
}];
|
|
||||||
|
|
||||||
// fullCalendar (v2) configuration
|
// fullCalendar (v2) configuration
|
||||||
$scope.calendarConfig = CalendarConfig({
|
$scope.calendarConfig = CalendarConfig({
|
||||||
slotDuration: BASE_SLOT,
|
slotDuration: BASE_SLOT,
|
||||||
@ -356,12 +368,132 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// filter availabilities if have change
|
||||||
|
$scope.filterAvailabilities = function (filter, scope) {
|
||||||
|
if (!scope) { scope = $scope; }
|
||||||
|
scope.filter = ($scope.filter = {
|
||||||
|
trainings: isSelectAll('trainings', scope),
|
||||||
|
machines: isSelectAll('machines', scope),
|
||||||
|
spaces: isSelectAll('spaces', scope),
|
||||||
|
evt: filter.evt,
|
||||||
|
dispo: filter.dispo
|
||||||
|
});
|
||||||
|
scope.machinesGroupByCategory.forEach(c => c.checked = _.every(c.machines, 'checked'));
|
||||||
|
// remove all
|
||||||
|
$scope.eventSources.splice(0, $scope.eventSources.length);
|
||||||
|
// recreate source for trainings/machines/events with new filters
|
||||||
|
$scope.eventSources.push({
|
||||||
|
url: availabilitySourceUrl(),
|
||||||
|
textColor: 'black'
|
||||||
|
});
|
||||||
|
uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents');
|
||||||
|
};
|
||||||
|
|
||||||
|
// a variable for formation/machine/event/dispo checkbox is or not checked
|
||||||
|
$scope.filter = {
|
||||||
|
trainings: isSelectAll('trainings', $scope),
|
||||||
|
machines: isSelectAll('machines', $scope),
|
||||||
|
spaces: isSelectAll('spaces', $scope),
|
||||||
|
evt: true,
|
||||||
|
dispo: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// toggle to select all formation/machine
|
||||||
|
$scope.toggleFilter = function (type, filter, machineCategoryId) {
|
||||||
|
if (type === 'machineCategory') {
|
||||||
|
const category = _.find($scope.machinesGroupByCategory, (c) => (c.id).toString() === machineCategoryId);
|
||||||
|
if (category) {
|
||||||
|
category.machines.forEach(m => m.checked = category.checked);
|
||||||
|
}
|
||||||
|
filter.machines = isSelectAll('machines', $scope);
|
||||||
|
} else {
|
||||||
|
$scope[type].forEach(t => t.checked = filter[type]);
|
||||||
|
if (type === 'machines') {
|
||||||
|
$scope.machinesGroupByCategory.forEach(t => t.checked = filter[type]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$scope.filterAvailabilities(filter, $scope);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.openFilterAside = () =>
|
||||||
|
$aside.open({
|
||||||
|
templateUrl: '/calendar/filterAside.html',
|
||||||
|
placement: 'right',
|
||||||
|
size: 'md',
|
||||||
|
backdrop: false,
|
||||||
|
resolve: {
|
||||||
|
trainings () {
|
||||||
|
return $scope.trainings;
|
||||||
|
},
|
||||||
|
machines () {
|
||||||
|
return $scope.machines;
|
||||||
|
},
|
||||||
|
machinesGroupByCategory () {
|
||||||
|
return $scope.machinesGroupByCategory;
|
||||||
|
},
|
||||||
|
spaces () {
|
||||||
|
return $scope.spaces;
|
||||||
|
},
|
||||||
|
filter () {
|
||||||
|
return $scope.filter;
|
||||||
|
},
|
||||||
|
toggleFilter () {
|
||||||
|
return $scope.toggleFilter;
|
||||||
|
},
|
||||||
|
filterAvailabilities () {
|
||||||
|
return $scope.filterAvailabilities;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'machinesGroupByCategory', 'spaces', 'filter', 'toggleFilter', 'filterAvailabilities', function ($scope, $uibModalInstance, trainings, machines, machinesGroupByCategory, spaces, filter, toggleFilter, filterAvailabilities) {
|
||||||
|
$scope.trainings = trainings;
|
||||||
|
$scope.machines = machines;
|
||||||
|
$scope.machinesGroupByCategory = machinesGroupByCategory;
|
||||||
|
$scope.hasMachineCategory = _.some(machines, 'machine_category_id');
|
||||||
|
$scope.spaces = spaces;
|
||||||
|
$scope.filter = filter;
|
||||||
|
|
||||||
|
$scope.toggleFilter = (type, filter, machineCategoryId) => toggleFilter(type, filter, machineCategoryId);
|
||||||
|
|
||||||
|
$scope.filterAvailabilities = filter => filterAvailabilities(filter, $scope);
|
||||||
|
|
||||||
|
return $scope.close = function (e) {
|
||||||
|
$uibModalInstance.dismiss();
|
||||||
|
return e.stopPropagation();
|
||||||
|
};
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
/* PRIVATE SCOPE */
|
/* PRIVATE SCOPE */
|
||||||
|
const getFilter = function () {
|
||||||
|
const t = $scope.trainings.filter(t => t.checked).map(t => t.id);
|
||||||
|
const m = $scope.machines.filter(m => m.checked).map(m => m.id);
|
||||||
|
const s = $scope.spaces.filter(s => s.checked).map(s => s.id);
|
||||||
|
return { t, m, s, evt: $scope.filter.evt, dispo: $scope.filter.dispo };
|
||||||
|
};
|
||||||
|
|
||||||
|
const availabilitySourceUrl = () => `/api/availabilities?${$.param(getFilter())}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kind of constructor: these actions will be realized first when the controller is loaded
|
* Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
*/
|
*/
|
||||||
const initialize = function () {};
|
const initialize = function () {
|
||||||
|
// bind the availabilities slots with full-Calendar events
|
||||||
|
$scope.eventSources = [{
|
||||||
|
url: availabilitySourceUrl(),
|
||||||
|
textColor: 'black'
|
||||||
|
}];
|
||||||
|
// group machines by category
|
||||||
|
_.forIn(_.groupBy($scope.machines, 'machine_category_id'), (ms, categoryId) => {
|
||||||
|
const category = _.find($scope.machineCategories, (c) => (c.id).toString() === categoryId);
|
||||||
|
$scope.machinesGroupByCategory.push({
|
||||||
|
id: categoryId,
|
||||||
|
name: category ? category.name : _t('app.shared.machine.machine_uncategorized'),
|
||||||
|
checked: true,
|
||||||
|
machine_ids: category ? category.machine_ids : [],
|
||||||
|
machines: ms
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an enumerable meaninful string for the gender of the provider user
|
* Return an enumerable meaninful string for the gender of the provider user
|
||||||
|
152
app/frontend/src/javascript/controllers/admin/machines.js
Normal file
152
app/frontend/src/javascript/controllers/admin/machines.js
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
/* eslint-disable
|
||||||
|
no-return-assign,
|
||||||
|
no-undef,
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
Application.Controllers.controller('AdminMachinesController', ['$scope', 'CSRF', 'growl', '$state', '_t', 'AuthService', 'settingsPromise', 'Member', 'uiTourService', 'machinesPromise', 'helpers',
|
||||||
|
function ($scope, CSRF, growl, $state, _t, AuthService, settingsPromise, Member, uiTourService, machinesPromise, helpers) {
|
||||||
|
/* PUBLIC SCOPE */
|
||||||
|
|
||||||
|
// default tab: machines list
|
||||||
|
$scope.tabs = { active: 0 };
|
||||||
|
|
||||||
|
// the application global settings
|
||||||
|
$scope.settings = settingsPromise;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect the user to the machine details page
|
||||||
|
*/
|
||||||
|
$scope.showMachine = function (machine) { $state.go('app.public.machines_show', { id: machine.slug }); };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows an error message forwarded from a child component
|
||||||
|
*/
|
||||||
|
$scope.onError = function (message) {
|
||||||
|
growl.error(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a success message forwarded from a child react components
|
||||||
|
*/
|
||||||
|
$scope.onSuccess = function (message) {
|
||||||
|
growl.success(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the modal dialog to log the user and resolves the returned promise when the logging process
|
||||||
|
* was successfully completed.
|
||||||
|
*/
|
||||||
|
$scope.onLoginRequest = function (e) {
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
$scope.login(e, resolve);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect the user to the training reservation page
|
||||||
|
*/
|
||||||
|
$scope.onEnrollRequest = function (trainingId) {
|
||||||
|
$state.go('app.logged.trainings_reserve', { id: trainingId });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to book a reservation for the current machine
|
||||||
|
*/
|
||||||
|
$scope.reserveMachine = function (machine) {
|
||||||
|
$state.go('app.logged.machines_reserve', { id: machine.slug });
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.canProposePacks = function () {
|
||||||
|
return AuthService.isAuthorized(['admin', 'manager']) || !helpers.isUserValidationRequired($scope.settings, 'pack') || (helpers.isUserValidationRequired($scope.settings, 'pack') && helpers.isUserValidated($scope.currentUser));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the feature-tour for the machines page. (admins only)
|
||||||
|
* This is intended as a contextual help (when pressing F1)
|
||||||
|
*/
|
||||||
|
$scope.setupMachinesTour = function () {
|
||||||
|
// setup the tour for admins only
|
||||||
|
if (AuthService.isAuthorized(['admin', 'manager'])) {
|
||||||
|
// get the tour defined by the ui-tour directive
|
||||||
|
const uitour = uiTourService.getTourByName('machines');
|
||||||
|
if (AuthService.isAuthorized('admin')) {
|
||||||
|
uitour.createStep({
|
||||||
|
selector: 'body',
|
||||||
|
stepId: 'welcome',
|
||||||
|
order: 0,
|
||||||
|
title: _t('app.public.tour.machines.welcome.title'),
|
||||||
|
content: _t('app.public.tour.machines.welcome.content'),
|
||||||
|
placement: 'bottom',
|
||||||
|
orphan: true
|
||||||
|
});
|
||||||
|
if (machinesPromise.length > 0) {
|
||||||
|
uitour.createStep({
|
||||||
|
selector: '.machines-list .show-button',
|
||||||
|
stepId: 'view',
|
||||||
|
order: 1,
|
||||||
|
title: _t('app.public.tour.machines.view.title'),
|
||||||
|
content: _t('app.public.tour.machines.view.content'),
|
||||||
|
placement: 'top'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uitour.createStep({
|
||||||
|
selector: 'body',
|
||||||
|
stepId: 'welcome_manager',
|
||||||
|
order: 0,
|
||||||
|
title: _t('app.public.tour.machines.welcome_manager.title'),
|
||||||
|
content: _t('app.public.tour.machines.welcome_manager.content'),
|
||||||
|
placement: 'bottom',
|
||||||
|
orphan: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (machinesPromise.length > 0) {
|
||||||
|
uitour.createStep({
|
||||||
|
selector: '.machines-list .reserve-button',
|
||||||
|
stepId: 'reserve',
|
||||||
|
order: 2,
|
||||||
|
title: _t('app.public.tour.machines.reserve.title'),
|
||||||
|
content: _t('app.public.tour.machines.reserve.content'),
|
||||||
|
placement: 'top'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
uitour.createStep({
|
||||||
|
selector: 'body',
|
||||||
|
stepId: 'conclusion',
|
||||||
|
order: 3,
|
||||||
|
title: _t('app.public.tour.conclusion.title'),
|
||||||
|
content: _t('app.public.tour.conclusion.content'),
|
||||||
|
placement: 'bottom',
|
||||||
|
orphan: true
|
||||||
|
});
|
||||||
|
// on tour end, save the status in database
|
||||||
|
uitour.on('ended', function () {
|
||||||
|
if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile_attributes.tours.indexOf('machines') < 0) {
|
||||||
|
Member.completeTour({ id: $scope.currentUser.id }, { tour: 'machines' }, function (res) {
|
||||||
|
$scope.currentUser.profile_attributes.tours = res.tours;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// if the user has never seen the tour, show him now
|
||||||
|
if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile_attributes.tours.indexOf('machines') < 0) {
|
||||||
|
uitour.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* PRIVATE SCOPE */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kind of constructor: these actions will be realized first when the controller is loaded
|
||||||
|
*/
|
||||||
|
const initialize = function () {
|
||||||
|
// set the authenticity tokens in the forms
|
||||||
|
CSRF.setMetaTags();
|
||||||
|
};
|
||||||
|
|
||||||
|
// init the controller (call at the end !)
|
||||||
|
return initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
]);
|
@ -16,8 +16,8 @@
|
|||||||
* Controller used in the public calendar global
|
* Controller used in the public calendar global
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Application.Controllers.controller('CalendarController', ['$scope', '$state', '$aside', 'moment', 'Availability', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise', 'iCalendarPromise',
|
Application.Controllers.controller('CalendarController', ['$scope', '$state', '$aside', 'moment', 'Availability', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise', 'iCalendarPromise', 'machineCategoriesPromise',
|
||||||
function ($scope, $state, $aside, moment, Availability, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise, iCalendarPromise) {
|
function ($scope, $state, $aside, moment, Availability, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise, iCalendarPromise, machineCategoriesPromise) {
|
||||||
/* PRIVATE STATIC CONSTANTS */
|
/* PRIVATE STATIC CONSTANTS */
|
||||||
let currentMachineEvent = null;
|
let currentMachineEvent = null;
|
||||||
machinesPromise.forEach(m => m.checked = true);
|
machinesPromise.forEach(m => m.checked = true);
|
||||||
@ -35,6 +35,12 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
|||||||
// List of machines
|
// List of machines
|
||||||
$scope.machines = machinesPromise.filter(t => !t.disabled);
|
$scope.machines = machinesPromise.filter(t => !t.disabled);
|
||||||
|
|
||||||
|
// List of machine categories
|
||||||
|
$scope.machineCategories = machineCategoriesPromise;
|
||||||
|
|
||||||
|
// List of machines group by category
|
||||||
|
$scope.machinesGroupByCategory = [];
|
||||||
|
|
||||||
// List of spaces
|
// List of spaces
|
||||||
$scope.spaces = spacesPromise.filter(t => !t.disabled);
|
$scope.spaces = spacesPromise.filter(t => !t.disabled);
|
||||||
|
|
||||||
@ -55,6 +61,7 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
|||||||
evt: filter.evt,
|
evt: filter.evt,
|
||||||
dispo: filter.dispo
|
dispo: filter.dispo
|
||||||
});
|
});
|
||||||
|
scope.machinesGroupByCategory.forEach(c => c.checked = _.every(c.machines, 'checked'));
|
||||||
// remove all
|
// remove all
|
||||||
$scope.eventSources.splice(0, $scope.eventSources.length);
|
$scope.eventSources.splice(0, $scope.eventSources.length);
|
||||||
// recreate source for trainings/machines/events with new filters
|
// recreate source for trainings/machines/events with new filters
|
||||||
@ -104,8 +111,19 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
|||||||
};
|
};
|
||||||
|
|
||||||
// toggle to select all formation/machine
|
// toggle to select all formation/machine
|
||||||
$scope.toggleFilter = function (type, filter) {
|
$scope.toggleFilter = function (type, filter, machineCategoryId) {
|
||||||
$scope[type].forEach(t => t.checked = filter[type]);
|
if (type === 'machineCategory') {
|
||||||
|
const category = _.find($scope.machinesGroupByCategory, (c) => (c.id).toString() === machineCategoryId);
|
||||||
|
if (category) {
|
||||||
|
category.machines.forEach(m => m.checked = category.checked);
|
||||||
|
}
|
||||||
|
filter.machines = isSelectAll('machines', $scope);
|
||||||
|
} else {
|
||||||
|
$scope[type].forEach(t => t.checked = filter[type]);
|
||||||
|
if (type === 'machines') {
|
||||||
|
$scope.machinesGroupByCategory.forEach(t => t.checked = filter[type]);
|
||||||
|
}
|
||||||
|
}
|
||||||
$scope.filterAvailabilities(filter, $scope);
|
$scope.filterAvailabilities(filter, $scope);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -122,6 +140,9 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
|||||||
machines () {
|
machines () {
|
||||||
return $scope.machines;
|
return $scope.machines;
|
||||||
},
|
},
|
||||||
|
machinesGroupByCategory () {
|
||||||
|
return $scope.machinesGroupByCategory;
|
||||||
|
},
|
||||||
spaces () {
|
spaces () {
|
||||||
return $scope.spaces;
|
return $scope.spaces;
|
||||||
},
|
},
|
||||||
@ -138,14 +159,16 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
|||||||
return $scope.filterAvailabilities;
|
return $scope.filterAvailabilities;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'spaces', 'externals', 'filter', 'toggleFilter', 'filterAvailabilities', function ($scope, $uibModalInstance, trainings, machines, spaces, externals, filter, toggleFilter, filterAvailabilities) {
|
controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'machinesGroupByCategory', 'spaces', 'externals', 'filter', 'toggleFilter', 'filterAvailabilities', function ($scope, $uibModalInstance, trainings, machines, machinesGroupByCategory, spaces, externals, filter, toggleFilter, filterAvailabilities) {
|
||||||
$scope.trainings = trainings;
|
$scope.trainings = trainings;
|
||||||
$scope.machines = machines;
|
$scope.machines = machines;
|
||||||
|
$scope.machinesGroupByCategory = machinesGroupByCategory;
|
||||||
|
$scope.hasMachineCategory = _.some(machines, 'machine_category_id');
|
||||||
$scope.spaces = spaces;
|
$scope.spaces = spaces;
|
||||||
$scope.externals = externals;
|
$scope.externals = externals;
|
||||||
$scope.filter = filter;
|
$scope.filter = filter;
|
||||||
|
|
||||||
$scope.toggleFilter = (type, filter) => toggleFilter(type, filter);
|
$scope.toggleFilter = (type, filter, machineCategoryId) => toggleFilter(type, filter, machineCategoryId);
|
||||||
|
|
||||||
$scope.filterAvailabilities = filter => filterAvailabilities(filter, $scope);
|
$scope.filterAvailabilities = filter => filterAvailabilities(filter, $scope);
|
||||||
|
|
||||||
@ -196,6 +219,18 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// group machines by category
|
||||||
|
_.forIn(_.groupBy($scope.machines, 'machine_category_id'), (ms, categoryId) => {
|
||||||
|
const category = _.find($scope.machineCategories, (c) => (c.id).toString() === categoryId);
|
||||||
|
$scope.machinesGroupByCategory.push({
|
||||||
|
id: categoryId,
|
||||||
|
name: category ? category.name : _t('app.shared.machine.machine_uncategorized'),
|
||||||
|
checked: true,
|
||||||
|
machine_ids: category ? category.machine_ids : [],
|
||||||
|
machines: ms
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -252,8 +252,8 @@ Application.Controllers.controller('NewMachineController', ['$scope', '$state',
|
|||||||
/**
|
/**
|
||||||
* Controller used in the machine edition page (admin)
|
* Controller used in the machine edition page (admin)
|
||||||
*/
|
*/
|
||||||
Application.Controllers.controller('EditMachineController', ['$scope', '$state', '$transition$', 'machinePromise', 'CSRF',
|
Application.Controllers.controller('EditMachineController', ['$scope', '$state', '$transition$', 'machinePromise', 'machineCategoriesPromise', 'CSRF',
|
||||||
function ($scope, $state, $transition$, machinePromise, CSRF) {
|
function ($scope, $state, $transition$, machinePromise, machineCategoriesPromise, CSRF) {
|
||||||
/* PUBLIC SCOPE */
|
/* PUBLIC SCOPE */
|
||||||
|
|
||||||
// API URL where the form will be posted
|
// API URL where the form will be posted
|
||||||
@ -265,6 +265,9 @@ Application.Controllers.controller('EditMachineController', ['$scope', '$state',
|
|||||||
// Retrieve the details for the machine id in the URL, if an error occurs redirect the user to the machines list
|
// 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);
|
$scope.machine = cleanMachine(machinePromise);
|
||||||
|
|
||||||
|
// Retrieve all machine categories
|
||||||
|
$scope.machineCategories = machineCategoriesPromise;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows an error message forwarded from a child component
|
* Shows an error message forwarded from a child component
|
||||||
*/
|
*/
|
||||||
|
@ -99,7 +99,7 @@ Application.Controllers.controller('MainNavController', ['$scope', 'settingsProm
|
|||||||
authorizedRoles: ['admin', 'manager']
|
authorizedRoles: ['admin', 'manager']
|
||||||
},
|
},
|
||||||
$scope.$root.modules.machines && {
|
$scope.$root.modules.machines && {
|
||||||
state: 'app.public.machines_list',
|
state: 'app.admin.machines_list',
|
||||||
linkText: 'app.public.common.manage_the_machines',
|
linkText: 'app.public.common.manage_the_machines',
|
||||||
linkIcon: 'cogs',
|
linkIcon: 'cogs',
|
||||||
authorizedRoles: ['admin', 'manager']
|
authorizedRoles: ['admin', 'manager']
|
||||||
|
5
app/frontend/src/javascript/models/machine-category.ts
Normal file
5
app/frontend/src/javascript/models/machine-category.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface MachineCategory {
|
||||||
|
id?: number,
|
||||||
|
name: string,
|
||||||
|
machine_ids: Array<number>,
|
||||||
|
}
|
@ -5,6 +5,11 @@ export interface MachineIndexFilter extends ApiFilter {
|
|||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MachineListFilter {
|
||||||
|
status: boolean | void,
|
||||||
|
category: number | void,
|
||||||
|
}
|
||||||
|
|
||||||
export interface Machine {
|
export interface Machine {
|
||||||
id?: number,
|
id?: number,
|
||||||
name: string,
|
name: string,
|
||||||
@ -31,5 +36,6 @@ export interface Machine {
|
|||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
slug: string,
|
slug: string,
|
||||||
}>
|
}>,
|
||||||
|
machine_category_id: number | void
|
||||||
}
|
}
|
||||||
|
@ -359,6 +359,20 @@ angular.module('application.router', ['ui.router'])
|
|||||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display', 'user_validation_required', 'user_validation_required_list']" }).$promise; }]
|
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display', 'user_validation_required', 'user_validation_required_list']" }).$promise; }]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.state('app.admin.machines_list', {
|
||||||
|
url: '/admin/machines',
|
||||||
|
abstract: !Fablab.machinesModule,
|
||||||
|
views: {
|
||||||
|
'main@': {
|
||||||
|
templateUrl: '/admin/machines/index.html',
|
||||||
|
controller: 'AdminMachinesController'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
|
||||||
|
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display', 'user_validation_required', 'user_validation_required_list']" }).$promise; }]
|
||||||
|
}
|
||||||
|
})
|
||||||
.state('app.admin.machines_new', {
|
.state('app.admin.machines_new', {
|
||||||
url: '/machines/new',
|
url: '/machines/new',
|
||||||
abstract: !Fablab.machinesModule,
|
abstract: !Fablab.machinesModule,
|
||||||
@ -414,7 +428,8 @@ angular.module('application.router', ['ui.router'])
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
machinePromise: ['Machine', '$transition$', function (Machine, $transition$) { return Machine.get({ id: $transition$.params().id }).$promise; }]
|
machinePromise: ['Machine', '$transition$', function (Machine, $transition$) { return Machine.get({ id: $transition$.params().id }).$promise; }],
|
||||||
|
machineCategoriesPromise: ['MachineCategory', function (MachineCategory) { return MachineCategory.query().$promise; }]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -620,7 +635,8 @@ angular.module('application.router', ['ui.router'])
|
|||||||
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
|
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
|
||||||
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
|
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
|
||||||
spacesPromise: ['Space', function (Space) { return Space.query().$promise; }],
|
spacesPromise: ['Space', function (Space) { return Space.query().$promise; }],
|
||||||
iCalendarPromise: ['ICalendar', function (ICalendar) { return ICalendar.query().$promise; }]
|
iCalendarPromise: ['ICalendar', function (ICalendar) { return ICalendar.query().$promise; }],
|
||||||
|
machineCategoriesPromise: ['MachineCategory', function (MachineCategory) { return MachineCategory.query().$promise; }]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -687,7 +703,10 @@ angular.module('application.router', ['ui.router'])
|
|||||||
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
|
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
|
||||||
plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }],
|
plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }],
|
||||||
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
|
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
|
||||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['slot_duration', 'events_in_calendar', 'feature_tour_display']" }).$promise; }]
|
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['slot_duration', 'events_in_calendar', 'feature_tour_display']" }).$promise; }],
|
||||||
|
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
|
||||||
|
spacesPromise: ['Space', function (Space) { return Space.query().$promise; }],
|
||||||
|
machineCategoriesPromise: ['MachineCategory', function (MachineCategory) { return MachineCategory.query().$promise; }]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.state('app.admin.calendar.icalendar', {
|
.state('app.admin.calendar.icalendar', {
|
||||||
|
11
app/frontend/src/javascript/services/machine_category.js
Normal file
11
app/frontend/src/javascript/services/machine_category.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
Application.Services.factory('MachineCategory', ['$resource', function ($resource) {
|
||||||
|
return $resource('/api/machine_categories/:id',
|
||||||
|
{ id: '@id' }, {
|
||||||
|
update: {
|
||||||
|
method: 'PUT'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}]);
|
@ -458,6 +458,11 @@ p, .widget p {
|
|||||||
.p-l {
|
.p-l {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-l-sm {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.p-h-0 {
|
.p-h-0 {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
@ -55,6 +55,7 @@
|
|||||||
@import "modules/machines/machines-filters";
|
@import "modules/machines/machines-filters";
|
||||||
@import "modules/machines/machines-list";
|
@import "modules/machines/machines-list";
|
||||||
@import "modules/machines/required-training-modal";
|
@import "modules/machines/required-training-modal";
|
||||||
|
@import "modules/machines/machine-categories";
|
||||||
@import "modules/payment-schedule/payment-schedule-dashboard";
|
@import "modules/payment-schedule/payment-schedule-dashboard";
|
||||||
@import "modules/payment-schedule/payment-schedule-summary";
|
@import "modules/payment-schedule/payment-schedule-summary";
|
||||||
@import "modules/payment-schedule/payment-schedules-list";
|
@import "modules/payment-schedule/payment-schedules-list";
|
||||||
|
@ -7,6 +7,9 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap-reverse;
|
flex-wrap: wrap-reverse;
|
||||||
|
.calendar-actions {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&-info {
|
&-info {
|
||||||
display: contents;
|
display: contents;
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
.machine-categories-list {
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
button {
|
||||||
|
border-radius: 5;
|
||||||
|
&:hover { opacity: 0.75; }
|
||||||
|
}
|
||||||
|
.edit-btn {
|
||||||
|
color: var(--gray-hard-darkest);
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.delete-btn {
|
||||||
|
color: var(--gray-soft-lightest);
|
||||||
|
background: var(--main);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-categories-table {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
& thead:first-child > tr:first-child > th {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
& thead > tr > th {
|
||||||
|
vertical-align: bottom;
|
||||||
|
border-bottom: 2px solid #ddd;
|
||||||
|
}
|
||||||
|
& th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
& thead > tr > th, & tbody > tr > td {
|
||||||
|
padding: 8px;
|
||||||
|
line-height: 1.5;
|
||||||
|
vertical-align: top;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-category-form {
|
||||||
|
.form-checklist {
|
||||||
|
.actions {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.checklist {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 0.3rem 3.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-actions {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,14 @@
|
|||||||
.machines-filters {
|
.machines-filters {
|
||||||
margin: 1.5em;
|
margin: 1.5em 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
.status-filter {
|
.filter-item {
|
||||||
|
&:first-child {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
& {
|
& {
|
||||||
display: inline-flex;
|
display: block;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
& > label {
|
& > label {
|
||||||
@ -13,18 +18,18 @@
|
|||||||
& > * {
|
& > * {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.status-select {
|
.status-select, .category-select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-left: 10px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 720px){
|
@media screen and (max-width: 720px){
|
||||||
.machines-filters {
|
.machines-filters {
|
||||||
.status-filter {
|
display: block;
|
||||||
padding-right: 0;
|
.filter-item {
|
||||||
display: inline-block;
|
padding-right: 0 !important;
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
.machines-list {
|
.machines-list {
|
||||||
.all-machines {
|
.all-machines {
|
||||||
max-width: 1600px;
|
max-width: 1600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||||
gap: 3.2rem;
|
gap: 3.2rem;
|
||||||
|
|
||||||
.store-ad {
|
.store-ad {
|
||||||
|
@ -41,17 +41,22 @@
|
|||||||
<span class="calendar-legend-item text-sm border-event" ng-show="eventsInCalendar" translate>{{ 'app.admin.calendar.events' }}</span>
|
<span class="calendar-legend-item text-sm border-event" ng-show="eventsInCalendar" translate>{{ 'app.admin.calendar.events' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-show="isAuthorized('admin')">
|
<div class="calendar-actions">
|
||||||
<a class="btn btn-default export-xls-button"
|
<div ng-show="isAuthorized('admin')">
|
||||||
ng-href="api/availabilities/export_index.xlsx"
|
<a class="btn btn-default export-xls-button"
|
||||||
target="export-frame"
|
ng-href="api/availabilities/export_index.xlsx"
|
||||||
ng-click="alertExport('index')"
|
target="export-frame"
|
||||||
uib-popover="{{ 'app.admin.calendar.availabilities_notice' | translate}}"
|
ng-click="alertExport('index')"
|
||||||
popover-trigger="mouseenter"
|
uib-popover="{{ 'app.admin.calendar.availabilities_notice' | translate}}"
|
||||||
popover-placement="bottom-left">
|
popover-trigger="mouseenter"
|
||||||
<i class="fa fa-file-excel-o"></i> {{ 'app.admin.calendar.availabilities' | translate }}
|
popover-placement="bottom-left">
|
||||||
</a>
|
<i class="fa fa-file-excel-o"></i> {{ 'app.admin.calendar.availabilities' | translate }}
|
||||||
<iframe name="export-frame" height="0" width="0" class="none"></iframe>
|
</a>
|
||||||
|
<iframe name="export-frame" height="0" width="0" class="none"></iframe>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-default m-l" ng-click="openFilterAside()">
|
||||||
|
<span class="fa fa-filter"></span> {{ 'app.shared.calendar.filter_calendar' | translate }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
7
app/frontend/templates/admin/machines/categories.html
Normal file
7
app/frontend/templates/admin/machines/categories.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<div class="m-t">
|
||||||
|
<machine-categories-list
|
||||||
|
on-error="onError"
|
||||||
|
on-success="onSuccess"
|
||||||
|
>
|
||||||
|
<machine-categories-list />
|
||||||
|
</div>
|
33
app/frontend/templates/admin/machines/index.html
Normal file
33
app/frontend/templates/admin/machines/index.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<div class="header-page">
|
||||||
|
<div class="back">
|
||||||
|
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="center">
|
||||||
|
<h1 translate>{{ 'app.admin.machines.the_fablab_s_machines' }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="m-lg admin-machines-manage">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<uib-tabset justified="true" active="tabs.active">
|
||||||
|
|
||||||
|
<uib-tab heading="{{ 'app.admin.machines.all_machines' | translate }}" index="0" select="selectTab()">
|
||||||
|
<div ng-if="tabs.active === 0">
|
||||||
|
<ng-include src="'/admin/machines/machines.html'"></ng-include>
|
||||||
|
</div>
|
||||||
|
</uib-tab>
|
||||||
|
|
||||||
|
<uib-tab heading="{{ 'app.admin.machines.manage_machines_categories' | translate }}" index="1" select="selectTab()">
|
||||||
|
<div ng-if="tabs.active === 1">
|
||||||
|
<ng-include src="'/admin/machines/categories.html'"></ng-include>
|
||||||
|
</div>
|
||||||
|
</uib-tab>
|
||||||
|
|
||||||
|
</uib-tabset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
19
app/frontend/templates/admin/machines/machines.html
Normal file
19
app/frontend/templates/admin/machines/machines.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<section class="m-lg"
|
||||||
|
ui-tour="machines"
|
||||||
|
ui-tour-backdrop="true"
|
||||||
|
ui-tour-template-url="'/shared/tour-step-template.html'"
|
||||||
|
ui-tour-use-hotkeys="true"
|
||||||
|
ui-tour-scroll-parent-id="content-main"
|
||||||
|
post-render="setupMachinesTour">
|
||||||
|
|
||||||
|
<machines-list user="currentUser"
|
||||||
|
on-error="onError"
|
||||||
|
on-success="onSuccess"
|
||||||
|
on-show-machine="showMachine"
|
||||||
|
on-reserve-machine="reserveMachine"
|
||||||
|
on-login-requested="onLoginRequest"
|
||||||
|
on-enroll-requested="onEnrollRequest"
|
||||||
|
can-propose-packs="canProposePacks()">
|
||||||
|
</machines-list>
|
||||||
|
|
||||||
|
</section>
|
@ -14,7 +14,7 @@
|
|||||||
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
|
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
|
||||||
<div class="heading-actions wrapper">
|
<div class="heading-actions wrapper">
|
||||||
<button type="button" class="btn btn-default m-t m-b" ng-click="openFilterAside()">
|
<button type="button" class="btn btn-default m-t m-b" ng-click="openFilterAside()">
|
||||||
<span class="fa fa-filter"></span> {{ 'app.public.calendar.filter_calendar' | translate }}
|
<span class="fa fa-filter"></span> {{ 'app.shared.calendar.filter_calendar' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
|
<div class="m-b row">
|
||||||
|
<h3 class="col-md-11 col-sm-11 col-xs-11 text-black" translate>{{ 'app.shared.calendar.show_unavailables' }}</h3>
|
||||||
|
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.dispo" ng-change="filterAvailabilities(filter)">
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h3 class="col-md-11 col-sm-11 col-xs-11 text-purple" translate>{{ 'app.public.calendar.trainings' }}</h3>
|
<h3 class="col-md-11 col-sm-11 col-xs-11 text-purple" translate>{{ 'app.shared.calendar.trainings' }}</h3>
|
||||||
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.trainings" ng-change="toggleFilter('trainings', filter)">
|
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.trainings" ng-change="toggleFilter('trainings', filter)">
|
||||||
</div>
|
</div>
|
||||||
<div ng-repeat="t in trainings" class="row">
|
<div ng-repeat="t in trainings" class="row">
|
||||||
@ -10,17 +14,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="m-t">
|
<div class="m-t">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h3 class="col-md-11 col-sm-11 col-xs-11 text-beige" translate>{{ 'app.public.calendar.machines' }}</h3>
|
<h3 class="col-md-11 col-sm-11 col-xs-11 text-beige" translate>{{ 'app.shared.calendar.machines' }}</h3>
|
||||||
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.machines" ng-change="toggleFilter('machines', filter)">
|
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.machines" ng-change="toggleFilter('machines', filter)">
|
||||||
</div>
|
</div>
|
||||||
<div ng-repeat="m in machines" class="row">
|
<div ng-if="!hasMachineCategory" ng-repeat="m in machines" class="row">
|
||||||
<span class="col-md-11 col-sm-11 col-xs-11">{{::m.name}}</span>
|
<span class="col-md-11 col-sm-11 col-xs-11">{{::m.name}}</span>
|
||||||
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="m.checked" ng-change="filterAvailabilities(filter)">
|
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="m.checked" ng-change="filterAvailabilities(filter)">
|
||||||
</div>
|
</div>
|
||||||
|
<div ng-if="hasMachineCategory" ng-repeat="category in machinesGroupByCategory" class="row">
|
||||||
|
<span class="col-md-11 col-sm-11 col-xs-11">{{::category.name}}</span>
|
||||||
|
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="category.checked" ng-change="toggleFilter('machineCategory', filter, category.id)">
|
||||||
|
<div ng-repeat="m in category.machines" class="col-md-12">
|
||||||
|
<div class="row p-l-sm">
|
||||||
|
<span class="col-md-11 col-sm-11 col-xs-11">{{::m.name}}</span>
|
||||||
|
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="m.checked" ng-change="filterAvailabilities(filter)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-t" ng-show="$root.modules.spaces">
|
<div class="m-t" ng-show="$root.modules.spaces">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h3 class="col-md-11 col-sm-11 col-xs-11 text-cyan" translate>{{ 'app.public.calendar.spaces' }}</h3>
|
<h3 class="col-md-11 col-sm-11 col-xs-11 text-cyan" translate>{{ 'app.shared.calendar.spaces' }}</h3>
|
||||||
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.spaces" ng-change="toggleFilter('spaces', filter)">
|
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.spaces" ng-change="toggleFilter('spaces', filter)">
|
||||||
</div>
|
</div>
|
||||||
<div ng-repeat="s in spaces" class="row">
|
<div ng-repeat="s in spaces" class="row">
|
||||||
@ -29,16 +43,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-t row">
|
<div class="m-t row">
|
||||||
<h3 class="col-md-11 col-sm-11 col-xs-11 text-japonica" translate>{{ 'app.public.calendar.events' }}</h3>
|
<h3 class="col-md-11 col-sm-11 col-xs-11 text-japonica" translate>{{ 'app.shared.calendar.events' }}</h3>
|
||||||
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.evt" ng-change="filterAvailabilities(filter)">
|
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.evt" ng-change="filterAvailabilities(filter)">
|
||||||
</div>
|
</div>
|
||||||
<div class="m-t row">
|
<div class="m-t" ng-hide="!externals || externals.length == 0">
|
||||||
<h3 class="col-md-11 col-sm-11 col-xs-11 text-black" translate>{{ 'app.public.calendar.show_unavailables' }}</h3>
|
|
||||||
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.dispo" ng-change="filterAvailabilities(filter)">
|
|
||||||
</div>
|
|
||||||
<div class="m-t" ng-hide="externals.length == 0">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h3 class="col-md-11 col-sm-11 col-xs-11 text-black" translate>{{ 'app.public.calendar.externals' }}</h3>
|
<h3 class="col-md-11 col-sm-11 col-xs-11 text-black" translate>{{ 'app.shared.calendar.externals' }}</h3>
|
||||||
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.externals" ng-change="toggleFilter('externals', filter)">
|
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.externals" ng-change="toggleFilter('externals', filter)">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -89,6 +89,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group m-b-xl">
|
||||||
|
<label for="machine[machine_category_id]" class="control-label col-sm-2" translate>
|
||||||
|
{{ 'app.shared.machine.assigning_machine_to_category' }}
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<span class="help-block alert alert-warning m-b" translate>
|
||||||
|
{{ 'app.shared.machine.assigning_machine_to_category_info_html' }}
|
||||||
|
</span>
|
||||||
|
<small translate>{{ 'app.shared.machine.linking_machine_to_category' }}</small>
|
||||||
|
<ui-select ng-model="machine.machine_category_id" name="machine[machine_category_id]">
|
||||||
|
<ui-select-match>
|
||||||
|
<span ng-bind="$select.selected.name"></span>
|
||||||
|
<input type="hidden" name="machine[machine_category_id]" value="{{$select.selected.id}}" />
|
||||||
|
</ui-select-match>
|
||||||
|
<ui-select-choices repeat="c.id as c in (machineCategories | filter: $select.search)">
|
||||||
|
<span ng-bind-html="c.name | highlight: $select.search"></span>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group m-b-xl">
|
<div class="form-group m-b-xl">
|
||||||
<label class="col-sm-2 control-label" translate>{{ 'app.shared.machine.attached_files_pdf' }}</label>
|
<label class="col-sm-2 control-label" translate>{{ 'app.shared.machine.attached_files_pdf' }}</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
|
@ -34,6 +34,8 @@ class Machine < ApplicationRecord
|
|||||||
has_many :machines_products, dependent: :destroy
|
has_many :machines_products, dependent: :destroy
|
||||||
has_many :products, through: :machines_products
|
has_many :products, through: :machines_products
|
||||||
|
|
||||||
|
belongs_to :category
|
||||||
|
|
||||||
after_create :create_statistic_subtype
|
after_create :create_statistic_subtype
|
||||||
after_create :create_machine_prices
|
after_create :create_machine_prices
|
||||||
after_create :update_gateway_product
|
after_create :update_gateway_product
|
||||||
|
7
app/models/machine_category.rb
Normal file
7
app/models/machine_category.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# MachineCategory used to categorize Machines.
|
||||||
|
class MachineCategory < ApplicationRecord
|
||||||
|
has_many :machines, dependent: :nullify
|
||||||
|
accepts_nested_attributes_for :machines, allow_destroy: true
|
||||||
|
end
|
7
app/policies/machine_category_policy.rb
Normal file
7
app/policies/machine_category_policy.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
class MachineCategoryPolicy < ApplicationPolicy
|
||||||
|
%w[create update destroy show].each do |action|
|
||||||
|
define_method "#{action}?" do
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# List all Availability's slots for the given resources
|
# List all Availability's slots for the given resources
|
||||||
class Availabilities::AvailabilitiesService
|
class Availabilities::AvailabilitiesService
|
||||||
|
|
||||||
def initialize(current_user, level = 'slot')
|
def initialize(current_user, level = 'slot')
|
||||||
@current_user = current_user
|
@current_user = current_user
|
||||||
@maximum_visibility = {
|
@maximum_visibility = {
|
||||||
@ -13,6 +12,19 @@ class Availabilities::AvailabilitiesService
|
|||||||
@level = level
|
@level = level
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def index(window, ids, events = false)
|
||||||
|
machines_availabilities = Setting.get('machines_module') ? machines(Machine.where(id: ids[:machines]), @current_user, window) : []
|
||||||
|
spaces_availabilities = Setting.get('spaces_module') ? spaces(Space.where(id: ids[:spaces]), @current_user, window) : []
|
||||||
|
trainings_availabilities = Setting.get('trainings_module') ? trainings(Training.where(id: ids[:trainings]), @current_user, window) : []
|
||||||
|
events_availabilities = if events && Setting.get('events_in_calendar')
|
||||||
|
events(Event.all, @current_user, window)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
[].concat(trainings_availabilities).concat(events_availabilities).concat(machines_availabilities).concat(spaces_availabilities)
|
||||||
|
end
|
||||||
|
|
||||||
# list all slots for the given machines, with visibility relative to the given user
|
# list all slots for the given machines, with visibility relative to the given user
|
||||||
def machines(machines, user, window)
|
def machines(machines, user, window)
|
||||||
ma_availabilities = Availability.includes('machines_availabilities')
|
ma_availabilities = Availability.includes('machines_availabilities')
|
||||||
|
3
app/views/api/machine_categories/index.json.jbuilder
Normal file
3
app/views/api/machine_categories/index.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
json.array!(@machine_categories) do |category|
|
||||||
|
json.extract! category, :id, :name, :machine_ids
|
||||||
|
end
|
1
app/views/api/machine_categories/show.json.jbuilder
Normal file
1
app/views/api/machine_categories/show.json.jbuilder
Normal file
@ -0,0 +1 @@
|
|||||||
|
json.extract! @machine_category, :id, :name, :machine_ids
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
json.extract! machine, :id, :name, :slug, :disabled
|
json.extract! machine, :id, :name, :slug, :disabled, :machine_category_id
|
||||||
|
|
||||||
if machine.machine_image
|
if machine.machine_image
|
||||||
json.machine_image_attributes do
|
json.machine_image_attributes do
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
json.array!(@machines) do |machine|
|
json.array!(@machines) do |machine|
|
||||||
json.extract! machine, :id, :name, :slug, :disabled
|
json.extract! machine, :id, :name, :slug, :disabled, :machine_category_id
|
||||||
|
|
||||||
json.machine_image machine.machine_image.attachment.medium.url if machine.machine_image
|
json.machine_image machine.machine_image.attachment.medium.url if machine.machine_image
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
json.extract! @machine, :id, :name, :description, :spec, :disabled, :slug
|
json.extract! @machine, :id, :name, :description, :spec, :disabled, :slug, :machine_category_id
|
||||||
json.machine_image @machine.machine_image.attachment.large.url if @machine.machine_image
|
json.machine_image @machine.machine_image.attachment.large.url if @machine.machine_image
|
||||||
|
|
||||||
json.machine_files_attributes @machine.machine_files do |f|
|
json.machine_files_attributes @machine.machine_files do |f|
|
||||||
|
@ -1,6 +1,27 @@
|
|||||||
en:
|
en:
|
||||||
app:
|
app:
|
||||||
admin:
|
admin:
|
||||||
|
machines:
|
||||||
|
the_fablab_s_machines: "The FabLab's machines"
|
||||||
|
all_machines: "All machines"
|
||||||
|
manage_machines_categories: "Manage machines categories"
|
||||||
|
machine_categories_list:
|
||||||
|
machine_categories: "Machines Categories"
|
||||||
|
add_a_machine_category: "Add a machine category"
|
||||||
|
name: "Name"
|
||||||
|
machines_number: "Nb of machines"
|
||||||
|
edit: "Edit"
|
||||||
|
machine_category_modal:
|
||||||
|
new_machine_category: "New categorie"
|
||||||
|
edit_machine_category: "Edit catégorie"
|
||||||
|
successfully_created: "The new machine category request has been created."
|
||||||
|
unable_to_create: "Unable to delete the machine category request: "
|
||||||
|
successfully_updated: "The machine category request has been updated."
|
||||||
|
unable_to_update: "Unable to modify the machine category request: "
|
||||||
|
machine_category_form:
|
||||||
|
name: "Name of category"
|
||||||
|
assigning_machines: "Assigning the machines to this category"
|
||||||
|
save: "Save"
|
||||||
machine_form:
|
machine_form:
|
||||||
name: "Name"
|
name: "Name"
|
||||||
illustration: "Visual"
|
illustration: "Visual"
|
||||||
|
@ -1,6 +1,27 @@
|
|||||||
fr:
|
fr:
|
||||||
app:
|
app:
|
||||||
admin:
|
admin:
|
||||||
|
machines:
|
||||||
|
the_fablab_s_machines: "Les machines du FabLab"
|
||||||
|
all_machines: "Toutes les machines"
|
||||||
|
manage_machines_categories: "Gérer les catégories machines"
|
||||||
|
machine_categories_list:
|
||||||
|
machine_categories: "Catégories Machines"
|
||||||
|
add_a_machine_category: "Ajouter une catégorie machine"
|
||||||
|
name: "Nom"
|
||||||
|
machines_number: "Nb de machines"
|
||||||
|
edit: "Editer"
|
||||||
|
machine_category_modal:
|
||||||
|
new_machine_category: "Créer une nouvelle catégorie"
|
||||||
|
edit_machine_category: "Modifier la catégorie"
|
||||||
|
successfully_created: "La nouvelle catégorie machine a bien été créée."
|
||||||
|
unable_to_create: "Impossible de supprimer la catégorie machine : "
|
||||||
|
successfully_updated: "La nouvelle catégorie machine a bien été mise à jour."
|
||||||
|
unable_to_update: "Impossible de modifier la catégorie machine : "
|
||||||
|
machine_category_form:
|
||||||
|
name: "Nom de la catégorie"
|
||||||
|
assigning_machines: "Assigner les machines à cette catégorie"
|
||||||
|
save: "Enregistrer"
|
||||||
machine_form:
|
machine_form:
|
||||||
name: "Nom"
|
name: "Nom"
|
||||||
illustration: "Illustration"
|
illustration: "Illustration"
|
||||||
|
@ -230,6 +230,8 @@ en:
|
|||||||
status_enabled: "Enabled"
|
status_enabled: "Enabled"
|
||||||
status_disabled: "Disabled"
|
status_disabled: "Disabled"
|
||||||
status_all: "All"
|
status_all: "All"
|
||||||
|
filter_by_machine_category: "Filter by category"
|
||||||
|
all_machines: "All machines"
|
||||||
machine_card:
|
machine_card:
|
||||||
book: "Book"
|
book: "Book"
|
||||||
consult: "Consult"
|
consult: "Consult"
|
||||||
|
@ -230,6 +230,8 @@ fr:
|
|||||||
status_enabled: "Actives"
|
status_enabled: "Actives"
|
||||||
status_disabled: "Désactivées"
|
status_disabled: "Désactivées"
|
||||||
status_all: "Toutes"
|
status_all: "Toutes"
|
||||||
|
filter_by_machine_category: "Filtrer par catégorie"
|
||||||
|
all_machines: "Toutes les machines"
|
||||||
machine_card:
|
machine_card:
|
||||||
book: "Réserver"
|
book: "Réserver"
|
||||||
consult: "Consulter"
|
consult: "Consulter"
|
||||||
|
@ -156,6 +156,10 @@ en:
|
|||||||
add_an_attachment: "Add an attachment"
|
add_an_attachment: "Add an attachment"
|
||||||
disable_machine: "Disable machine"
|
disable_machine: "Disable machine"
|
||||||
validate_your_machine: "Validate your machine"
|
validate_your_machine: "Validate your machine"
|
||||||
|
assigning_machine_to_category: "Assign a category"
|
||||||
|
assigning_machine_to_category_info_html: "<strong>Information</strong><br>You can only assign one category per machine."
|
||||||
|
linking_machine_to_category: "Link this product to a machine category"
|
||||||
|
machine_uncategorized: 'Uncategorized'
|
||||||
#button to book a machine reservation
|
#button to book a machine reservation
|
||||||
reserve_button:
|
reserve_button:
|
||||||
book_this_machine: "Book this machine"
|
book_this_machine: "Book this machine"
|
||||||
@ -641,3 +645,12 @@ en:
|
|||||||
keyword: "Keyword: {KEYWORD}"
|
keyword: "Keyword: {KEYWORD}"
|
||||||
stock_internal: "Private stock"
|
stock_internal: "Private stock"
|
||||||
stock_external: "Public stock"
|
stock_external: "Public stock"
|
||||||
|
calendar:
|
||||||
|
calendar: "Calendar"
|
||||||
|
show_unavailables: "Show unavailable slots"
|
||||||
|
filter_calendar: "Filter calendar"
|
||||||
|
trainings: "Trainings"
|
||||||
|
machines: "Machines"
|
||||||
|
spaces: "Spaces"
|
||||||
|
events: "Events"
|
||||||
|
externals: "Other calendars"
|
||||||
|
@ -156,6 +156,10 @@ fr:
|
|||||||
add_an_attachment: "Ajouter une pièce jointe"
|
add_an_attachment: "Ajouter une pièce jointe"
|
||||||
disable_machine: "Désactiver la machine"
|
disable_machine: "Désactiver la machine"
|
||||||
validate_your_machine: "Valider votre machine"
|
validate_your_machine: "Valider votre machine"
|
||||||
|
assigning_machine_to_category: "Attributer une catégorie"
|
||||||
|
assigning_machine_to_category_info_html: "<strong>Information</strong><br>Vous ne pouvez attribuer qu'une seule catégoriez par machine."
|
||||||
|
linking_machine_to_category: "Lier ce produit à une catégorie machine"
|
||||||
|
machine_uncategorized: 'Non catégorisé'
|
||||||
#button to book a machine reservation
|
#button to book a machine reservation
|
||||||
reserve_button:
|
reserve_button:
|
||||||
book_this_machine: "Réserver cette machine"
|
book_this_machine: "Réserver cette machine"
|
||||||
@ -641,3 +645,12 @@ fr:
|
|||||||
keyword: "Mot-clef : {KEYWORD}"
|
keyword: "Mot-clef : {KEYWORD}"
|
||||||
stock_internal: "Stock interne"
|
stock_internal: "Stock interne"
|
||||||
stock_external: "Stock externe"
|
stock_external: "Stock externe"
|
||||||
|
calendar:
|
||||||
|
calendar: "Calendrier"
|
||||||
|
show_unavailables: "Afficher les créneaux non disponibles"
|
||||||
|
filter_calendar: "Filtrer le calendrier"
|
||||||
|
trainings: "Formations"
|
||||||
|
machines: "Machines"
|
||||||
|
spaces: "Espaces"
|
||||||
|
events: "Événements"
|
||||||
|
externals: "Autres calendriers"
|
||||||
|
@ -41,6 +41,7 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
resources :openlab_projects, only: :index
|
resources :openlab_projects, only: :index
|
||||||
resources :machines
|
resources :machines
|
||||||
|
resources :machine_categories
|
||||||
resources :components
|
resources :components
|
||||||
resources :themes
|
resources :themes
|
||||||
resources :licences
|
resources :licences
|
||||||
|
9
db/migrate/20221212162655_create_machine_categories.rb
Normal file
9
db/migrate/20221212162655_create_machine_categories.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
class CreateMachineCategories < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_table :machine_categories do |t|
|
||||||
|
t.string :name
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal:true
|
||||||
|
|
||||||
|
class AddMachineCategoryIdToMachine < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_reference :machines, :machine_category, index: true, foreign_key: true
|
||||||
|
end
|
||||||
|
end
|
11
db/schema.rb
11
db/schema.rb
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2022_11_22_123605) do
|
ActiveRecord::Schema.define(version: 2022_12_16_090005) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "fuzzystrmatch"
|
enable_extension "fuzzystrmatch"
|
||||||
@ -350,6 +350,12 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do
|
|||||||
t.text "description"
|
t.text "description"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "machine_categories", force: :cascade do |t|
|
||||||
|
t.string "name"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
end
|
||||||
|
|
||||||
create_table "machines", id: :serial, force: :cascade do |t|
|
create_table "machines", id: :serial, force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.text "description"
|
t.text "description"
|
||||||
@ -359,7 +365,9 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do
|
|||||||
t.string "slug"
|
t.string "slug"
|
||||||
t.boolean "disabled"
|
t.boolean "disabled"
|
||||||
t.datetime "deleted_at"
|
t.datetime "deleted_at"
|
||||||
|
t.bigint "machine_category_id"
|
||||||
t.index ["deleted_at"], name: "index_machines_on_deleted_at"
|
t.index ["deleted_at"], name: "index_machines_on_deleted_at"
|
||||||
|
t.index ["machine_category_id"], name: "index_machines_on_machine_category_id"
|
||||||
t.index ["slug"], name: "index_machines_on_slug", unique: true
|
t.index ["slug"], name: "index_machines_on_slug", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1187,6 +1195,7 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do
|
|||||||
add_foreign_key "invoices", "statistic_profiles"
|
add_foreign_key "invoices", "statistic_profiles"
|
||||||
add_foreign_key "invoices", "wallet_transactions"
|
add_foreign_key "invoices", "wallet_transactions"
|
||||||
add_foreign_key "invoicing_profiles", "users"
|
add_foreign_key "invoicing_profiles", "users"
|
||||||
|
add_foreign_key "machines", "machine_categories"
|
||||||
add_foreign_key "order_activities", "invoicing_profiles", column: "operator_profile_id"
|
add_foreign_key "order_activities", "invoicing_profiles", column: "operator_profile_id"
|
||||||
add_foreign_key "order_activities", "orders"
|
add_foreign_key "order_activities", "orders"
|
||||||
add_foreign_key "order_items", "orders"
|
add_foreign_key "order_items", "orders"
|
||||||
|
Loading…
Reference in New Issue
Block a user