mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-19 13:54:25 +01:00
API+ interface to CRUD plan categories
This commit is contained in:
parent
3f044513e9
commit
e1e446ab3f
52
app/controllers/api/plan_categories_controller.rb
Normal file
52
app/controllers/api/plan_categories_controller.rb
Normal file
@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# API Controller for resources of type PlanCategory
|
||||
# PlanCategory are used to sort plans
|
||||
class API::PlanCategoriesController < API::ApiController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_category, only: %i[show update destroy]
|
||||
|
||||
def index
|
||||
authorize PlanCategory
|
||||
|
||||
@categories = PlanCategory.all
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
authorize PlanCategory
|
||||
|
||||
@category = PlanCategory.new(plan_category_params)
|
||||
if @category.save
|
||||
render :show, status: :created, location: @category
|
||||
else
|
||||
render json: @category.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @category
|
||||
if @category.update(plan_category_params)
|
||||
render :show, status: :ok
|
||||
else
|
||||
render json: @category.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @category
|
||||
@category.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_category
|
||||
@category = PlanCategory.find(params[:id])
|
||||
end
|
||||
|
||||
def plan_category_params
|
||||
params.require(:plan_category).permit(:name, :weight)
|
||||
end
|
||||
end
|
26
app/frontend/src/javascript/api/plan-category.ts
Normal file
26
app/frontend/src/javascript/api/plan-category.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { PlanCategory } from '../models/plan-category';
|
||||
|
||||
export default class PlanCategoryAPI {
|
||||
static async index (): Promise<Array<PlanCategory>> {
|
||||
const res: AxiosResponse<Array<PlanCategory>> = await apiClient.get('/api/plan_categories');
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async create (category: PlanCategory): Promise<PlanCategory> {
|
||||
const res: AxiosResponse<PlanCategory> = await apiClient.post('/api/plan_categories', { plan_category: category });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async update (category: PlanCategory): Promise<PlanCategory> {
|
||||
const res: AxiosResponse<PlanCategory> = await apiClient.patch(`/api/plan_categories/${category.id}`, { plan_category: category });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async destroy (categoryId: number): Promise<void> {
|
||||
const res: AxiosResponse<void> = await apiClient.delete(`/api/plan_categories/${categoryId}`);
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { ReactNode, BaseSyntheticEvent } from 'react';
|
||||
import React, { ReactNode, BaseSyntheticEvent, useEffect } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Loader } from './loader';
|
||||
@ -24,7 +24,8 @@ interface FabModalProps {
|
||||
width?: ModalSize,
|
||||
customFooter?: ReactNode,
|
||||
onConfirm?: (event: BaseSyntheticEvent) => void,
|
||||
preventConfirm?: boolean
|
||||
preventConfirm?: boolean,
|
||||
onCreation?: () => void,
|
||||
}
|
||||
|
||||
// initial request to the API
|
||||
@ -33,9 +34,15 @@ const blackLogoFile = CustomAssetAPI.get(CustomAssetName.LogoBlackFile);
|
||||
/**
|
||||
* This component is a template for a modal dialog that wraps the application style
|
||||
*/
|
||||
export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal, children, confirmButton, className, width = 'sm', closeButton, customFooter, onConfirm, preventConfirm }) => {
|
||||
export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal, children, confirmButton, className, width = 'sm', closeButton, customFooter, onConfirm, preventConfirm, onCreation }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof onCreation === 'function' && isOpen) {
|
||||
onCreation();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// the theme's logo, for back backgrounds
|
||||
const blackLogo = blackLogoFile.read();
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import React, { BaseSyntheticEvent } from 'react';
|
||||
|
||||
interface LabelledInputProps {
|
||||
id: string,
|
||||
type: string,
|
||||
type?: 'text' | 'date' | 'password' | 'url' | 'time' | 'tel' | 'search' | 'number' | 'month' | 'email' | 'datetime-local' | 'week',
|
||||
label: string,
|
||||
value: any,
|
||||
onChange: (value: any) => void
|
||||
onChange: (event: BaseSyntheticEvent) => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,228 @@
|
||||
import React, { BaseSyntheticEvent, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PlanCategoryAPI from '../../api/plan-category';
|
||||
import { PlanCategory } from '../../models/plan-category';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { LabelledInput } from '../base/labelled-input';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from '../base/loader';
|
||||
import { IApplication } from '../../models/application';
|
||||
|
||||
declare var Application: IApplication;
|
||||
|
||||
interface PlanCategoriesListProps {
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a list of all plan-categories and offer to manager them by deleting, modifying
|
||||
* and reordering each plan-categories.
|
||||
*/
|
||||
export const PlanCategoriesList: React.FC<PlanCategoriesListProps> = ({ onSuccess, onError }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
// list of all categories
|
||||
const [categories, setCategories] = useState<Array<PlanCategory>>(null);
|
||||
// when editing or deleting a category, it will be stored here until the edition is over
|
||||
const [tempCategory, setTempCategory] = useState<PlanCategory>(null);
|
||||
// is the creation modal open?
|
||||
const [creationModal, setCreationModal] = useState<boolean>(false);
|
||||
// is the edition modal open?
|
||||
const [editionModal, setEditionModal] = useState<boolean>(false);
|
||||
// is the deletion modal open?
|
||||
const [deletionModal, setDeletionModal] = useState<boolean>(false);
|
||||
|
||||
// load the categories list on component mount
|
||||
useEffect(() => {
|
||||
refreshCategories();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Opens/closes the new plan-category (creation) modal
|
||||
*/
|
||||
const toggleCreationModal = (): void => {
|
||||
setCreationModal(!creationModal);
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens/closes the edition modal
|
||||
*/
|
||||
const toggleEditionModal = (): void => {
|
||||
setEditionModal(!editionModal);
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens/closes the deletion modal
|
||||
*/
|
||||
const toggleDeletionModal = (): void => {
|
||||
setDeletionModal(!deletionModal);
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggered when the edit-category button is pushed.
|
||||
* Set the provided category to the currently edited category then open the edition modal.
|
||||
*/
|
||||
const handleEditCategory = (category: PlanCategory) => {
|
||||
return () => {
|
||||
setTempCategory(category);
|
||||
toggleEditionModal();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggered when the delete-category button is pushed.
|
||||
* Set the provided category to the currently deleted category then open the deletion modal.
|
||||
*/
|
||||
const handleDeleteCategory = (category: PlanCategory) => {
|
||||
return () => {
|
||||
setTempCategory(category);
|
||||
toggleDeletionModal();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The creation has been confirmed by the user.
|
||||
* Push the new plan-category to the API.
|
||||
*/
|
||||
const onCreateConfirmed = (): void => {
|
||||
PlanCategoryAPI.create(tempCategory).then(() => {
|
||||
onSuccess(t('app.admin.plan_categories_list.category_created'));
|
||||
resetTempCategory();
|
||||
toggleCreationModal();
|
||||
refreshCategories();
|
||||
}).catch((error) => {
|
||||
onError(t('app.admin.plan_categories_list.unable_to_create') + error);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* The deletion has been confirmed by the user.
|
||||
* Call the API to trigger the deletion of the temporary set plan-category
|
||||
*/
|
||||
const onDeleteConfirmed = (): void => {
|
||||
PlanCategoryAPI.destroy(tempCategory.id).then(() => {
|
||||
onSuccess(t('app.admin.plan_categories_list.category_deleted'));
|
||||
refreshCategories();
|
||||
}).catch((error) => {
|
||||
onError(t('app.admin.plan_categories_list.unable_to_delete') + error);
|
||||
});
|
||||
resetTempCategory();
|
||||
toggleDeletionModal();
|
||||
};
|
||||
|
||||
/**
|
||||
* The edit has been confirmed by the user.
|
||||
* Call the API to trigger the update of the temporary set plan-category
|
||||
*/
|
||||
const onEditConfirmed = (): void => {
|
||||
PlanCategoryAPI.update(tempCategory).then(() => {
|
||||
onSuccess(t('app.admin.plan_categories_list.category_updated'));
|
||||
resetTempCategory();
|
||||
refreshCategories();
|
||||
toggleEditionModal();
|
||||
}).catch((error) => {
|
||||
onError(t('app.admin.plan_categories_list.unable_to_update') + error);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user is changing the name of the category in the modal dialog.
|
||||
* We update the name of the temporary set plan-category accordingly.
|
||||
*/
|
||||
const onCategoryNameChange = (event: BaseSyntheticEvent) => {
|
||||
setTempCategory({...tempCategory, name: event.target.value });
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize a new plan-category for creation
|
||||
*/
|
||||
const initCategoryCreation = () => {
|
||||
setTempCategory({ name: '', weight: 0 });
|
||||
};
|
||||
|
||||
/**
|
||||
* Reinitialize the temporary category to prevent ghost data
|
||||
*/
|
||||
const resetTempCategory = () => {
|
||||
setTempCategory(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the list of categories
|
||||
*/
|
||||
const refreshCategories = () => {
|
||||
PlanCategoryAPI.index().then((data) => {
|
||||
setCategories(data);
|
||||
}).catch((error) => onError(error));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="plan-categories-list">
|
||||
<FabButton type='button'
|
||||
icon={<i className='fa fa-plus' />}
|
||||
className="add-category"
|
||||
onClick={toggleCreationModal}>
|
||||
{t('app.admin.plan_categories_list.new_category')}
|
||||
</FabButton>
|
||||
<h3>{t('app.admin.plan_categories_list.categories_list')}</h3>
|
||||
<table className="categories-table">
|
||||
{categories && categories.map(c =>
|
||||
<tr key={c.id}>
|
||||
<td className="category-name">{c.name}</td>
|
||||
<td className="category-actions">
|
||||
<FabButton type='button' className="edit-button" icon={<i className="fa fa-edit" />} onClick={handleEditCategory(c)} />
|
||||
<FabButton type='button' className="delete-button" icon={<i className="fa fa-trash" />} onClick={handleDeleteCategory(c)} />
|
||||
</td>
|
||||
</tr>)}
|
||||
</table>
|
||||
<FabModal title={t('app.admin.plan_categories_list.new_category')}
|
||||
isOpen={creationModal}
|
||||
toggleModal={toggleCreationModal}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.admin.plan_categories_list.confirm_create')}
|
||||
onConfirm={onCreateConfirmed}
|
||||
onCreation={initCategoryCreation}>
|
||||
{tempCategory && <LabelledInput id="name"
|
||||
label={t('app.admin.plan_categories_list.name')}
|
||||
type="text"
|
||||
value={tempCategory.name}
|
||||
onChange={onCategoryNameChange} />}
|
||||
</FabModal>
|
||||
<FabModal title={t('app.admin.plan_categories_list.delete_category')}
|
||||
isOpen={deletionModal}
|
||||
toggleModal={toggleDeletionModal}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.admin.plan_categories_list.confirm_delete')}
|
||||
onConfirm={onDeleteConfirmed}>
|
||||
<span>{t('app.admin.plan_categories_list.delete_confirmation')}</span>
|
||||
</FabModal>
|
||||
<FabModal title={t('app.admin.plan_categories_list.edit_category')}
|
||||
isOpen={editionModal}
|
||||
toggleModal={toggleEditionModal}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.admin.plan_categories_list.confirm_edition')}
|
||||
onConfirm={onEditConfirmed}>
|
||||
{tempCategory && <div>
|
||||
<LabelledInput id="category-name"
|
||||
type="text"
|
||||
label={t('app.admin.plan_categories_list.name')}
|
||||
value={tempCategory.name}
|
||||
onChange={onCategoryNameChange} />
|
||||
</div>}
|
||||
</FabModal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const PlanCategoriesListWrapper: React.FC<PlanCategoriesListProps> = ({ onSuccess, onError }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PlanCategoriesList onSuccess={onSuccess} onError={onError} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('planCategoriesList', react2angular(PlanCategoriesListWrapper, ['onSuccess', 'onError']));
|
@ -344,3 +344,20 @@ Application.Controllers.controller('EditPlanController', ['$scope', 'groups', 'p
|
||||
return new PlanController($scope, groups, prices, partners, CSRF, _t);
|
||||
}
|
||||
]);
|
||||
|
||||
/**
|
||||
* Controller used the plan-categories administration page.
|
||||
* This is just a wrapper to integrate the React component in the angular app
|
||||
*/
|
||||
Application.Controllers.controller('PlanCategoriesController', ['$scope', 'growl',
|
||||
function ($scope, growl) {
|
||||
/* PUBLIC SCOPE */
|
||||
$scope.onSuccess = function (message) {
|
||||
growl.success(message);
|
||||
};
|
||||
|
||||
$scope.onError = function (message) {
|
||||
growl.error(message);
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
5
app/frontend/src/javascript/models/plan-category.ts
Normal file
5
app/frontend/src/javascript/models/plan-category.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface PlanCategory {
|
||||
id?: number,
|
||||
name: string,
|
||||
weight: number,
|
||||
}
|
@ -818,6 +818,16 @@ angular.module('application.router', ['ui.router'])
|
||||
planPromise: ['Plan', '$stateParams', function (Plan, $stateParams) { return Plan.get({ id: $stateParams.id }).$promise; }]
|
||||
}
|
||||
})
|
||||
// plan categories
|
||||
.state('app.admin.plan_categories', {
|
||||
url: '/admin/plan_categories',
|
||||
views: {
|
||||
'main@': {
|
||||
templateUrl: '/admin/plans/categories.html',
|
||||
controller: 'PlanCategoriesController'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// coupons
|
||||
.state('app.admin.coupons_new', {
|
||||
|
@ -43,5 +43,6 @@
|
||||
@import "modules/payzen-modal";
|
||||
@import "modules/stripe-update-card-modal";
|
||||
@import "modules/payzen-update-card-modal";
|
||||
@import "modules/plan-categories-list";
|
||||
|
||||
@import "app.responsive";
|
||||
|
@ -0,0 +1,26 @@
|
||||
.plan-categories-list {
|
||||
padding: 1.5em;
|
||||
|
||||
.categories-table {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 24px;
|
||||
|
||||
& > tr > td {
|
||||
padding: 8px;
|
||||
line-height: 1.5;
|
||||
vertical-align: top;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
color: #fff;
|
||||
background-color: #cb1117;
|
||||
border-color: #c9c9c9;
|
||||
}
|
||||
|
||||
.category-actions {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
21
app/frontend/templates/admin/plans/categories.html
Normal file
21
app/frontend/templates/admin/plans/categories.html
Normal file
@ -0,0 +1,21 @@
|
||||
<section class="heading b-b">
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
<section class="heading-title">
|
||||
<h1 translate>{{ 'app.admin.plans_categories.manage_plans_categories' }}</h1>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="row no-gutter">
|
||||
<div class=" col-sm-12 col-md-9 b-r nopadding">
|
||||
<plan-categories-list on-success="onSuccess" on-error="onError" />
|
||||
</div>
|
||||
</div>
|
@ -5,11 +5,16 @@
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r">
|
||||
<section class="heading-title">
|
||||
<h1 translate>{{ 'app.admin.pricing.pricing_management' }}</h1>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-3 col-md-3">
|
||||
<section class="heading-actions wrapper">
|
||||
<a class="btn btn-default rounded m-t-sm" ui-sref="app.admin.plan_categories"><i class="fa fa-list"></i></a>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
@ -4,6 +4,7 @@
|
||||
# Subscribers can also get some Credits for some reservable items
|
||||
class Plan < ApplicationRecord
|
||||
belongs_to :group
|
||||
belongs_to :plan_category
|
||||
|
||||
has_many :credits, dependent: :destroy
|
||||
has_many :training_credits, -> { where(creditable_type: 'Training') }, class_name: 'Credit'
|
||||
|
10
app/models/plan_category.rb
Normal file
10
app/models/plan_category.rb
Normal file
@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Allows to sort plans into categories. Plans are sorted by multiple criterion,
|
||||
# ordered as follow:
|
||||
# - group
|
||||
# - plan_category
|
||||
# - plan
|
||||
class PlanCategory < ApplicationRecord
|
||||
has_many :plan, dependent: :nullify
|
||||
end
|
10
app/policies/plan_category_policy.rb
Normal file
10
app/policies/plan_category_policy.rb
Normal file
@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Check the access policies for API::PlanCategoriesController
|
||||
class PlanCategoryPolicy < ApplicationPolicy
|
||||
%w[index show create update destroy].each do |action|
|
||||
define_method "#{action}?" do
|
||||
user.admin?
|
||||
end
|
||||
end
|
||||
end
|
@ -374,3 +374,12 @@ section#cookies-modal div.cookies-consent .cookies-actions button.accept {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plan-categories-list {
|
||||
.add-category,
|
||||
.add-category:hover {
|
||||
background-color: $secondary-dark;
|
||||
border-color: $secondary-dark;
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
}
|
||||
|
5
app/views/api/plan_categories/index.json.jbuilder
Normal file
5
app/views/api/plan_categories/index.json.jbuilder
Normal file
@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.array!(@categories) do |category|
|
||||
json.extract! category, :id, :name, :weight
|
||||
end
|
3
app/views/api/plan_categories/show.json.jbuilder
Normal file
3
app/views/api/plan_categories/show.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! @category, :id, :name, :weight
|
@ -1288,6 +1288,22 @@ en:
|
||||
report_will_be_destroyed: "Once the report has been processed, it will be deleted. This can't be undone, continue?"
|
||||
report_removed: "The report has been deleted"
|
||||
failed_to_remove: "An error occurred, unable to delete the report"
|
||||
plans_categories:
|
||||
manage_plans_categories: "Manage plans' categories"
|
||||
plan_categories_list:
|
||||
categories_list: "List of the plan's categories"
|
||||
new_category: "New category"
|
||||
name: "Name"
|
||||
confirm_create: "Create the category"
|
||||
category_created: "The new category was successfully created"
|
||||
unable_to_create: "Unable to create the category: "
|
||||
edit_category: "Edit the category"
|
||||
confirm_edition: "Validate"
|
||||
category_updated: "The category was successfully updated"
|
||||
unable_to_update: "Unable to update the category: "
|
||||
delete_category: "Delete a category"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this category? If you do, the plans associated with this category won't be sorted anymore."
|
||||
#feature tour
|
||||
tour:
|
||||
conclusion:
|
||||
|
@ -1288,6 +1288,8 @@ fr:
|
||||
report_will_be_destroyed: "Une fois le signalement traité, le rapport sera supprimé. Cette action est irréversible, continuer ?"
|
||||
report_removed: "Le rapport a bien été supprimé"
|
||||
failed_to_remove: "Une erreur est survenue, impossible de supprimer le rapport"
|
||||
plans_categories:
|
||||
manage_plans_categories: "Gérer les catégories des formules d'abonnement"
|
||||
#feature tour
|
||||
tour:
|
||||
conclusion:
|
||||
|
@ -96,7 +96,8 @@ Rails.application.routes.draw do
|
||||
|
||||
resources :groups, only: %i[index create update destroy]
|
||||
resources :subscriptions, only: %i[show update]
|
||||
resources :plans, only: %i[index create update destroy show]
|
||||
resources :plan_categories
|
||||
resources :plans
|
||||
resources :slots, only: [:update] do
|
||||
put 'cancel', on: :member
|
||||
end
|
||||
|
14
db/migrate/20210608082748_create_plan_categories.rb
Normal file
14
db/migrate/20210608082748_create_plan_categories.rb
Normal file
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Allows to sort plans into categories
|
||||
class CreatePlanCategories < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :plan_categories do |t|
|
||||
t.string :name
|
||||
t.integer :weight
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_reference :plans, :plan_category, index: true, foreign_key: true
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user