mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
WIP: buy pack modal
This commit is contained in:
parent
99bd00949e
commit
f16cbc44ff
@ -1,5 +1,7 @@
|
|||||||
# Changelog Fab-manager
|
# Changelog Fab-manager
|
||||||
|
|
||||||
|
- [TODO DEPLOY] `rails db:seed`
|
||||||
|
|
||||||
## v5.0.7 2021 June 24
|
## v5.0.7 2021 June 24
|
||||||
|
|
||||||
- Fix a bug: unable to export members list if no subscriptions was taken
|
- Fix a bug: unable to export members list if no subscriptions was taken
|
||||||
|
@ -6,8 +6,6 @@ class API::PricesController < API::ApiController
|
|||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize Price
|
|
||||||
|
|
||||||
@prices = PriceService.list(params)
|
@prices = PriceService.list(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
import React, { BaseSyntheticEvent, useEffect, useState } from 'react';
|
||||||
|
import { Machine } from '../../models/machine';
|
||||||
|
import { FabModal, ModalSize } from '../base/fab-modal';
|
||||||
|
import PrepaidPackAPI from '../../api/prepaid-pack';
|
||||||
|
import { User } from '../../models/user';
|
||||||
|
import { PrepaidPack } from '../../models/prepaid-pack';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { IFablab } from '../../models/fablab';
|
||||||
|
import { FabButton } from '../base/fab-button';
|
||||||
|
import PriceAPI from '../../api/price';
|
||||||
|
import { Price } from '../../models/price';
|
||||||
|
|
||||||
|
declare var Fablab: IFablab;
|
||||||
|
|
||||||
|
interface ProposePacksModalProps {
|
||||||
|
isOpen: boolean,
|
||||||
|
toggleModal: () => void,
|
||||||
|
machine: Machine,
|
||||||
|
customer: User,
|
||||||
|
onError: (message: string) => void,
|
||||||
|
onDecline: (machine: Machine) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal dialog shown to offer prepaid-packs for purchase, to the current user.
|
||||||
|
*/
|
||||||
|
export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, toggleModal, machine, customer, onError, onDecline }) => {
|
||||||
|
const { t } = useTranslation('logged');
|
||||||
|
|
||||||
|
const [price, setPrice] = useState<Price>(null);
|
||||||
|
const [packs, setPacks] = useState<Array<PrepaidPack>>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
PrepaidPackAPI.index({ priceable_id: machine.id, priceable_type: 'Machine', group_id: customer.group_id, disabled: false })
|
||||||
|
.then(data => setPacks(data))
|
||||||
|
.catch(error => onError(error));
|
||||||
|
PriceAPI.index({ priceable_id: machine.id, priceable_type: 'Machine', group_id: customer.group_id, plan_id: null })
|
||||||
|
.then(data => setPrice(data[0]))
|
||||||
|
.catch(error => onError(error));
|
||||||
|
}, [machine]);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the formatted localized amount for the given price (e.g. 20.5 => "20,50 €")
|
||||||
|
*/
|
||||||
|
const formatPrice = (price: number): string => {
|
||||||
|
return new Intl.NumberFormat(Fablab.intl_locale, { style: 'currency', currency: Fablab.intl_currency }).format(price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the hourly-based price of the given prive, to a total price, based on the duration of the given pack
|
||||||
|
*/
|
||||||
|
const hourlyPriceToTotal = (price: Price, pack: PrepaidPack): number => {
|
||||||
|
const hours = pack.minutes / 60;
|
||||||
|
return price.amount * hours;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the number of hours, user-friendly formatted
|
||||||
|
*/
|
||||||
|
const formatDuration = (minutes: number): string => {
|
||||||
|
return t('app.logged.propose_packs_modal.pack_DURATION', { DURATION: minutes / 60 });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The user has declined to buy a pack
|
||||||
|
*/
|
||||||
|
const handlePacksRefused = (): void => {
|
||||||
|
onDecline(machine);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has accepted to buy the provided pack, process with teh payment
|
||||||
|
*/
|
||||||
|
const handleBuyPack = (pack: PrepaidPack) => {
|
||||||
|
return (event: BaseSyntheticEvent): void => {
|
||||||
|
console.log(pack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the given prepaid-pack
|
||||||
|
*/
|
||||||
|
const renderPack = (pack: PrepaidPack) => {
|
||||||
|
if (!price) return;
|
||||||
|
|
||||||
|
const normalPrice = hourlyPriceToTotal(price, pack)
|
||||||
|
return (
|
||||||
|
<div key={pack.id} className="pack">
|
||||||
|
<span className="duration">{formatDuration(pack.minutes)}</span>
|
||||||
|
<span className="amount">{formatPrice(pack.amount)}</span>
|
||||||
|
{pack.amount < normalPrice && <span className="crossed-out-price">{formatPrice(normalPrice)}</span>}
|
||||||
|
<FabButton className="buy-button" onClick={handleBuyPack(pack)} icon={<i className="fas fa-shopping-cart" />}>
|
||||||
|
{t('app.logged.propose_packs_modal.buy_this_pack')}
|
||||||
|
</FabButton>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FabModal isOpen={isOpen}
|
||||||
|
toggleModal={toggleModal}
|
||||||
|
width={ModalSize.large}
|
||||||
|
confirmButton={t('app.logged.propose_packs_modal.no_thanks')}
|
||||||
|
onConfirm={handlePacksRefused}
|
||||||
|
className="propose-packs-modal"
|
||||||
|
title={t('app.logged.propose_packs_modal.available_packs')}>
|
||||||
|
<p>{t('app.logged.propose_packs_modal.packs_proposed')}</p>
|
||||||
|
<div className="list-of-packs">
|
||||||
|
{packs?.map(p => renderPack(p))}
|
||||||
|
</div>
|
||||||
|
</FabModal>
|
||||||
|
);
|
||||||
|
}
|
@ -8,6 +8,7 @@ import { react2angular } from 'react2angular';
|
|||||||
import { IApplication } from '../../models/application';
|
import { IApplication } from '../../models/application';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Loader } from '../base/loader';
|
import { Loader } from '../base/loader';
|
||||||
|
import { ProposePacksModal } from './propose-packs-modal';
|
||||||
|
|
||||||
declare var Application: IApplication;
|
declare var Application: IApplication;
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, mac
|
|||||||
const [user, setUser] = useState<User>(currentUser);
|
const [user, setUser] = useState<User>(currentUser);
|
||||||
const [pendingTraining, setPendingTraining] = useState<boolean>(false);
|
const [pendingTraining, setPendingTraining] = useState<boolean>(false);
|
||||||
const [trainingRequired, setTrainingRequired] = useState<boolean>(false);
|
const [trainingRequired, setTrainingRequired] = useState<boolean>(false);
|
||||||
|
const [proposePacks, setProposePacks] = useState<boolean>(false);
|
||||||
|
|
||||||
// handle login from an external process
|
// handle login from an external process
|
||||||
useEffect(() => setUser(currentUser), [currentUser]);
|
useEffect(() => setUser(currentUser), [currentUser]);
|
||||||
@ -78,6 +80,13 @@ const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, mac
|
|||||||
setTrainingRequired(!trainingRequired);
|
setTrainingRequired(!trainingRequired);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open/closes the modal dialog inviting the user to buy a prepaid-pack
|
||||||
|
*/
|
||||||
|
const toggleProposePacksModal = (): void => {
|
||||||
|
setProposePacks(!proposePacks);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check that the current user has passed the required training before allowing him to book
|
* Check that the current user has passed the required training before allowing him to book
|
||||||
*/
|
*/
|
||||||
@ -94,11 +103,11 @@ const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, mac
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if the currently logged user has completed the training for this machine, or this machine does not require
|
// if the currently logged user has completed the training for this machine, or this machine does not require
|
||||||
// a prior training, just let him reserve.
|
// a prior training, move forward to the prepaid-packs verification.
|
||||||
// Moreover, if all associated trainings are disabled, let the user reserve too.
|
// Moreover, if there's no enabled associated trainings, also move to the next step.
|
||||||
if (machine.current_user_is_trained || machine.trainings.length === 0 ||
|
if (machine.current_user_is_trained || machine.trainings.length === 0 ||
|
||||||
machine.trainings.map(t => t.disabled).reduce((acc, val) => acc && val, true)) {
|
machine.trainings.map(t => t.disabled).reduce((acc, val) => acc && val, true)) {
|
||||||
return onReserveMachine(machine);
|
return checkPrepaidPack();
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the currently logged user booked a training for this machine, tell him that he must wait
|
// if the currently logged user booked a training for this machine, tell him that he must wait
|
||||||
@ -112,6 +121,20 @@ const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, mac
|
|||||||
setTrainingRequired(true);
|
setTrainingRequired(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Once the training condition has been verified, we check if there are prepaid-packs to propose to the customer.
|
||||||
|
*/
|
||||||
|
const checkPrepaidPack = (): void => {
|
||||||
|
// if the customer has already bought a pack or if there's no active packs for this machine,
|
||||||
|
// let the customer reserve
|
||||||
|
if (machine.current_user_has_packs || !machine.has_prepaid_packs_for_current_user) {
|
||||||
|
return onReserveMachine(machine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, we show a dialog modal to propose the customer to buy an available pack
|
||||||
|
setProposePacks(true);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<button onClick={handleClick} className={className ? className : ''}>
|
<button onClick={handleClick} className={className ? className : ''}>
|
||||||
@ -126,6 +149,12 @@ const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, mac
|
|||||||
user={user}
|
user={user}
|
||||||
machine={machine}
|
machine={machine}
|
||||||
onEnrollRequested={onEnrollRequested} />
|
onEnrollRequested={onEnrollRequested} />
|
||||||
|
{machine && <ProposePacksModal isOpen={proposePacks}
|
||||||
|
toggleModal={toggleProposePacksModal}
|
||||||
|
machine={machine}
|
||||||
|
onError={onError}
|
||||||
|
customer={currentUser}
|
||||||
|
onDecline={onReserveMachine} />}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
@ -5,7 +5,6 @@ import { FabPopover } from '../base/fab-popover';
|
|||||||
import { CreatePack } from './create-pack';
|
import { CreatePack } from './create-pack';
|
||||||
import PrepaidPackAPI from '../../api/prepaid-pack';
|
import PrepaidPackAPI from '../../api/prepaid-pack';
|
||||||
import { IFablab } from '../../models/fablab';
|
import { IFablab } from '../../models/fablab';
|
||||||
import { FabButton } from '../base/fab-button';
|
|
||||||
import { DeletePack } from './delete-pack';
|
import { DeletePack } from './delete-pack';
|
||||||
import { EditPack } from './edit-pack';
|
import { EditPack } from './edit-pack';
|
||||||
|
|
||||||
|
@ -24,6 +24,9 @@ export interface Machine {
|
|||||||
}>,
|
}>,
|
||||||
current_user_is_trained?: boolean,
|
current_user_is_trained?: boolean,
|
||||||
current_user_next_training_reservation?: Reservation,
|
current_user_next_training_reservation?: Reservation,
|
||||||
|
current_user_has_packs?: boolean,
|
||||||
|
current_user_available_for_packs_renewal?: boolean,
|
||||||
|
has_prepaid_packs_for_current_user?: boolean,
|
||||||
machine_projects?: Array<{
|
machine_projects?: Array<{
|
||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
|
|
||||||
export interface PackIndexFilter {
|
export interface PackIndexFilter {
|
||||||
group_id: number,
|
group_id?: number,
|
||||||
priceable_id: number,
|
priceable_id?: number,
|
||||||
priceable_type: string
|
priceable_type?: string,
|
||||||
|
disabled?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PrepaidPack {
|
export interface PrepaidPack {
|
||||||
id?: number,
|
id?: number,
|
||||||
priceable_id: number,
|
priceable_id?: number,
|
||||||
priceable_type: string,
|
priceable_type?: string,
|
||||||
group_id: number,
|
group_id: number,
|
||||||
validity_interval?: 'day' | 'week' | 'month' | 'year',
|
validity_interval?: 'day' | 'week' | 'month' | 'year',
|
||||||
validity_count?: number,
|
validity_count?: number,
|
||||||
|
@ -55,6 +55,7 @@
|
|||||||
@import "modules/machines/machines-list";
|
@import "modules/machines/machines-list";
|
||||||
@import "modules/machines/machines-filters";
|
@import "modules/machines/machines-filters";
|
||||||
@import "modules/machines/required-training-modal";
|
@import "modules/machines/required-training-modal";
|
||||||
|
@import "modules/machines/propose-packs-modal";
|
||||||
@import "modules/user/avatar";
|
@import "modules/user/avatar";
|
||||||
@import "modules/pricing/machines-pricing";
|
@import "modules/pricing/machines-pricing";
|
||||||
@import "modules/pricing/editable-price";
|
@import "modules/pricing/editable-price";
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
.propose-packs-modal {
|
||||||
|
.list-of-packs {
|
||||||
|
display: flex;
|
||||||
|
.pack {
|
||||||
|
&::before {
|
||||||
|
content: '\f466';
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 12px;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crossed-out-price {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buy-button {
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,7 @@ class Machine < ApplicationRecord
|
|||||||
validates :description, presence: true
|
validates :description, presence: true
|
||||||
|
|
||||||
has_many :prices, as: :priceable, dependent: :destroy
|
has_many :prices, as: :priceable, dependent: :destroy
|
||||||
|
has_many :prepaid_packs, as: :priceable, dependent: :destroy
|
||||||
|
|
||||||
has_many :reservations, as: :reservable, dependent: :destroy
|
has_many :reservations, as: :reservable, dependent: :destroy
|
||||||
has_many :credits, as: :creditable, dependent: :destroy
|
has_many :credits, as: :creditable, dependent: :destroy
|
||||||
@ -77,6 +78,13 @@ class Machine < ApplicationRecord
|
|||||||
reservations.empty?
|
reservations.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def packs?(user)
|
||||||
|
prepaid_packs.where(group_id: user.group_id)
|
||||||
|
.where(disabled: [false, nil])
|
||||||
|
.count
|
||||||
|
.positive?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def update_gateway_product
|
def update_gateway_product
|
||||||
|
@ -8,6 +8,9 @@
|
|||||||
# The number of hours in a pack is stored in minutes.
|
# The number of hours in a pack is stored in minutes.
|
||||||
class PrepaidPack < ApplicationRecord
|
class PrepaidPack < ApplicationRecord
|
||||||
belongs_to :priceable, polymorphic: true
|
belongs_to :priceable, polymorphic: true
|
||||||
|
belongs_to :machine, foreign_type: 'Machine', foreign_key: 'priceable_id'
|
||||||
|
belongs_to :space, foreign_type: 'Space', foreign_key: 'priceable_id'
|
||||||
|
|
||||||
belongs_to :group
|
belongs_to :group
|
||||||
|
|
||||||
has_many :statistic_profile_prepaid_packs
|
has_many :statistic_profile_prepaid_packs
|
||||||
|
@ -118,7 +118,8 @@ class Setting < ApplicationRecord
|
|||||||
payzen_public_key
|
payzen_public_key
|
||||||
payzen_hmac
|
payzen_hmac
|
||||||
payzen_currency
|
payzen_currency
|
||||||
public_agenda_module] }
|
public_agenda_module
|
||||||
|
renew_pack_threshold] }
|
||||||
# WARNING: when adding a new key, you may also want to add it in app/policies/setting_policy.rb#public_whitelist
|
# WARNING: when adding a new key, you may also want to add it in app/policies/setting_policy.rb#public_whitelist
|
||||||
|
|
||||||
def value
|
def value
|
||||||
|
@ -22,6 +22,7 @@ class Space < ApplicationRecord
|
|||||||
has_many :reservations, as: :reservable, dependent: :destroy
|
has_many :reservations, as: :reservable, dependent: :destroy
|
||||||
|
|
||||||
has_many :prices, as: :priceable, dependent: :destroy
|
has_many :prices, as: :priceable, dependent: :destroy
|
||||||
|
has_many :prepaid_packs, as: :priceable, dependent: :destroy
|
||||||
has_many :credits, as: :creditable, dependent: :destroy
|
has_many :credits, as: :creditable, dependent: :destroy
|
||||||
|
|
||||||
has_one :payment_gateway_object, as: :item
|
has_one :payment_gateway_object, as: :item
|
||||||
|
@ -128,6 +128,12 @@ class User < ApplicationRecord
|
|||||||
trainings.map(&:machines).flatten.uniq.include?(machine)
|
trainings.map(&:machines).flatten.uniq.include?(machine)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def packs?(item, threshold = Setting.get('renew_pack_threshold'))
|
||||||
|
return true if admin?
|
||||||
|
|
||||||
|
PrepaidPackService.user_packs(self, item, threshold).count.positive?
|
||||||
|
end
|
||||||
|
|
||||||
def next_training_reservation_by_machine(machine)
|
def next_training_reservation_by_machine(machine)
|
||||||
reservations.where(reservable_type: 'Training', reservable_id: machine.trainings.map(&:id))
|
reservations.where(reservable_type: 'Training', reservable_id: machine.trainings.map(&:id))
|
||||||
.includes(:slots)
|
.includes(:slots)
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
class PricePolicy < ApplicationPolicy
|
# frozen_string_literal: true
|
||||||
def index?
|
|
||||||
user.admin?
|
|
||||||
end
|
|
||||||
|
|
||||||
|
# Check the access policies for API::PricesController
|
||||||
|
class PricePolicy < ApplicationPolicy
|
||||||
def update?
|
def update?
|
||||||
user.admin?
|
user.admin?
|
||||||
end
|
end
|
||||||
|
@ -2,13 +2,38 @@
|
|||||||
|
|
||||||
# Provides methods for PrepaidPack
|
# Provides methods for PrepaidPack
|
||||||
class PrepaidPackService
|
class PrepaidPackService
|
||||||
def self.list(filters)
|
class << self
|
||||||
packs = PrepaidPack.where(nil)
|
def list(filters)
|
||||||
|
packs = PrepaidPack.where(nil)
|
||||||
|
|
||||||
packs = packs.where(group_id: filters[:group_id]) if filters[:group_id].present?
|
packs = packs.where(group_id: filters[:group_id]) if filters[:group_id].present?
|
||||||
packs = packs.where(priceable_id: filters[:priceable_id]) if filters[:priceable_id].present?
|
packs = packs.where(priceable_id: filters[:priceable_id]) if filters[:priceable_id].present?
|
||||||
packs = packs.where(priceable_type: filters[:priceable_type]) if filters[:priceable_type].present?
|
packs = packs.where(priceable_type: filters[:priceable_type]) if filters[:priceable_type].present?
|
||||||
|
|
||||||
packs
|
if filters[:disabled].present?
|
||||||
|
state = filters[:disabled] == 'false' ? [nil, false] : true
|
||||||
|
packs = packs.where(disabled: state)
|
||||||
|
end
|
||||||
|
|
||||||
|
packs
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_packs(user, priceable, threshold)
|
||||||
|
query = StatisticProfilePrepaidPack
|
||||||
|
.includes(:prepaid_pack)
|
||||||
|
.references(:prepaid_packs)
|
||||||
|
.where('statistic_profile_id = ?', user.statistic_profile.id)
|
||||||
|
.where('expires_at < ?', DateTime.current)
|
||||||
|
.where('prepaid_packs.priceable_id = ?', priceable.id)
|
||||||
|
.where('prepaid_packs.priceable_type = ?', priceable.class.name)
|
||||||
|
|
||||||
|
if threshold.class == Float
|
||||||
|
query = query.where('prepaid_packs.minutes - minutes_used >= prepaid_packs.minutes * ?', threshold)
|
||||||
|
elsif threshold.class == Integer
|
||||||
|
query = query.where('prepaid_packs.minutes - minutes_used >= ?', threshold)
|
||||||
|
end
|
||||||
|
|
||||||
|
query
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -8,11 +8,16 @@ json.machine_files_attributes @machine.machine_files do |f|
|
|||||||
json.attachment_url f.attachment_url
|
json.attachment_url f.attachment_url
|
||||||
end
|
end
|
||||||
json.trainings @machine.trainings.each, :id, :name, :disabled
|
json.trainings @machine.trainings.each, :id, :name, :disabled
|
||||||
json.current_user_is_trained current_user.training_machine?(@machine) if current_user
|
if current_user
|
||||||
if current_user && !current_user.training_machine?(@machine) && current_user.next_training_reservation_by_machine(@machine)
|
json.current_user_is_trained current_user.training_machine?(@machine)
|
||||||
json.current_user_next_training_reservation do
|
if !current_user.training_machine?(@machine) && current_user.next_training_reservation_by_machine(@machine)
|
||||||
json.partial! 'api/reservations/reservation', reservation: current_user.next_training_reservation_by_machine(@machine)
|
json.current_user_next_training_reservation do
|
||||||
|
json.partial! 'api/reservations/reservation', reservation: current_user.next_training_reservation_by_machine(@machine)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
json.current_user_has_packs current_user.packs?(@machine, nil)
|
||||||
|
json.current_user_available_for_packs_renewal current_user.packs?(@machine)
|
||||||
|
json.has_prepaid_packs_for_current_user @machine.packs?(current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
json.machine_projects @machine.projects.published.last(10) do |p|
|
json.machine_projects @machine.projects.published.last(10) do |p|
|
||||||
|
@ -178,6 +178,12 @@ en:
|
|||||||
enroll_now: "Enroll to the training"
|
enroll_now: "Enroll to the training"
|
||||||
no_enroll_for_now: "I don't want to enroll now"
|
no_enroll_for_now: "I don't want to enroll now"
|
||||||
close: "Close"
|
close: "Close"
|
||||||
|
propose_packs_modal:
|
||||||
|
available_packs: "Prepaid packs available"
|
||||||
|
packs_proposed: "You can buy a prepaid pack of hours for this machine. These packs allows you to benefit from volume discounts."
|
||||||
|
no_thanks: "No, thanks"
|
||||||
|
pack_DURATION: "{DURATION} hours"
|
||||||
|
buy_this_pack: "Buy this pack"
|
||||||
#book a training
|
#book a training
|
||||||
trainings_reserve:
|
trainings_reserve:
|
||||||
trainings_planning: "Trainings planning"
|
trainings_planning: "Trainings planning"
|
||||||
|
@ -897,6 +897,8 @@ Setting.set('trainings_module', true) unless Setting.find_by(name: 'trainings_mo
|
|||||||
|
|
||||||
Setting.set('public_agenda_module', true) unless Setting.find_by(name: 'public_agenda_module').try(:value)
|
Setting.set('public_agenda_module', true) unless Setting.find_by(name: 'public_agenda_module').try(:value)
|
||||||
|
|
||||||
|
Setting.set('renew_pack_threshold', 0.2) unless Setting.find_by(name: 'renew_pack_threshold').try(:value)
|
||||||
|
|
||||||
if StatisticCustomAggregation.count.zero?
|
if StatisticCustomAggregation.count.zero?
|
||||||
# available reservations hours for machines
|
# available reservations hours for machines
|
||||||
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2)
|
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user