From 5ebc1017d20e6425d1556723373010473aa66afa Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 20 Dec 2021 15:47:57 +0100 Subject: [PATCH 01/17] save prices duration in db --- app/frontend/src/javascript/models/price.ts | 3 ++- app/models/cart_item/reservation.rb | 2 +- app/views/api/prices/_price.json.jbuilder | 4 +++- app/views/api/prices/compute.json.jbuilder | 2 ++ app/views/api/prices/index.json.jbuilder | 2 ++ db/migrate/20211220143400_add_duration_to_price.rb | 10 ++++++++++ db/schema.rb | 4 +++- 7 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20211220143400_add_duration_to_price.rb diff --git a/app/frontend/src/javascript/models/price.ts b/app/frontend/src/javascript/models/price.ts index cb90077ca..fe84f4ba5 100644 --- a/app/frontend/src/javascript/models/price.ts +++ b/app/frontend/src/javascript/models/price.ts @@ -11,7 +11,8 @@ export interface Price { plan_id: number, priceable_type: string, priceable_id: number, - amount: number + amount: number, + duration: number // in minutes } export interface ComputePriceResult { diff --git a/app/models/cart_item/reservation.rb b/app/models/cart_item/reservation.rb index 2d54022b0..5e337e6ca 100644 --- a/app/models/cart_item/reservation.rb +++ b/app/models/cart_item/reservation.rb @@ -16,7 +16,7 @@ class CartItem::Reservation < CartItem::BaseItem end def price - base_amount = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id)).amount + base_amount = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id), duration: 60).amount is_privileged = @operator.privileged? && @operator.id != @customer.id prepaid = { minutes: PrepaidPackService.minutes_available(@customer, @reservable) } diff --git a/app/views/api/prices/_price.json.jbuilder b/app/views/api/prices/_price.json.jbuilder index 19a0ffc06..ce2900702 100644 --- a/app/views/api/prices/_price.json.jbuilder +++ b/app/views/api/prices/_price.json.jbuilder @@ -1,2 +1,4 @@ -json.extract! price, :id, :group_id, :plan_id, :priceable_type, :priceable_id +# frozen_string_literal: true + +json.extract! price, :id, :group_id, :plan_id, :priceable_type, :priceable_id, :duration json.amount price.amount / 100.0 diff --git a/app/views/api/prices/compute.json.jbuilder b/app/views/api/prices/compute.json.jbuilder index 6dfd2318b..10e3cc8d8 100644 --- a/app/views/api/prices/compute.json.jbuilder +++ b/app/views/api/prices/compute.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.price @amount[:total] / 100.00 json.price_without_coupon @amount[:before_coupon] / 100.00 if @amount[:elements] diff --git a/app/views/api/prices/index.json.jbuilder b/app/views/api/prices/index.json.jbuilder index 1d35eab98..24b3ea1f8 100644 --- a/app/views/api/prices/index.json.jbuilder +++ b/app/views/api/prices/index.json.jbuilder @@ -1 +1,3 @@ +# frozen_string_literal: true + json.partial! 'api/prices/price', collection: @prices, as: :price diff --git a/db/migrate/20211220143400_add_duration_to_price.rb b/db/migrate/20211220143400_add_duration_to_price.rb new file mode 100644 index 000000000..3fd1112fe --- /dev/null +++ b/db/migrate/20211220143400_add_duration_to_price.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# From this migration, we allow Prices to be configured by duration. +# For example, a Price for a 30-minute session could be configured to be twice the price of a 60-minute session. +# This is useful for things like "half-day" sessions, or full-day session when the price is different than the default hour-based price. +class AddDurationToPrice < ActiveRecord::Migration[5.2] + def change + add_column :prices, :duration, :integer, default: 60 + end +end diff --git a/db/schema.rb b/db/schema.rb index 406fda14c..647b326ab 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_10_18_121822) do +ActiveRecord::Schema.define(version: 2021_12_20_143400) do # These are extensions that must be enabled in order to support this database enable_extension "fuzzystrmatch" @@ -492,6 +492,7 @@ ActiveRecord::Schema.define(version: 2021_10_18_121822) do t.integer "weight" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.text "description" end create_table "plans", id: :serial, force: :cascade do |t| @@ -554,6 +555,7 @@ ActiveRecord::Schema.define(version: 2021_10_18_121822) do t.integer "amount" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "duration", default: 60 t.index ["group_id"], name: "index_prices_on_group_id" t.index ["plan_id"], name: "index_prices_on_plan_id" t.index ["priceable_type", "priceable_id"], name: "index_prices_on_priceable_type_and_priceable_id" From d6b30875a45098e40e462469015c86d6486d5ba6 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 20 Dec 2021 17:08:14 +0100 Subject: [PATCH 02/17] compute the price based on custom durations --- app/controllers/api/prices_controller.rb | 2 +- app/models/cart_item/reservation.rb | 33 ++++++++++++++++++++++-- app/services/prepaid_pack_service.rb | 5 +--- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/prices_controller.rb b/app/controllers/api/prices_controller.rb index 61d7f80ba..777dffa42 100644 --- a/app/controllers/api/prices_controller.rb +++ b/app/controllers/api/prices_controller.rb @@ -30,6 +30,6 @@ class API::PricesController < API::ApiController private def price_params - params.require(:price).permit(:amount) + params.require(:price).permit(:amount, :duration) end end diff --git a/app/models/cart_item/reservation.rb b/app/models/cart_item/reservation.rb index 5e337e6ca..d3df985a2 100644 --- a/app/models/cart_item/reservation.rb +++ b/app/models/cart_item/reservation.rb @@ -3,7 +3,7 @@ MINUTES_PER_HOUR = 60.0 SECONDS_PER_MINUTE = 60.0 -GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division: true, prepaid: { minutes: 0 } }.freeze +GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division: true, prepaid: { minutes: 0 }, custom_duration: nil }.freeze # A generic reservation added to the shopping cart class CartItem::Reservation < CartItem::BaseItem @@ -16,7 +16,7 @@ class CartItem::Reservation < CartItem::BaseItem end def price - base_amount = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id), duration: 60).amount + base_amount = get_hourly_rate is_privileged = @operator.privileged? && @operator.id != @customer.id prepaid = { minutes: PrepaidPackService.minutes_available(@customer, @reservable) } @@ -103,6 +103,35 @@ class CartItem::Reservation < CartItem::BaseItem real_price end + # We compute the hourly rate according to the prices of the current reservation + # If there are prices for durations longer than 1 hour, but shorter than the total duration, + # we use these prices before using the hourly rate. + # Eg. If the reservation is for 12 hours, and there are prices for 3 hours, 7 hours, + # and the base price (1 hours), we use the 7 hours price, then 3 hours price, and finally the base price. + # Then we divide the total price by the total duration to get the hourly rate. + def get_hourly_rate + total_duration = @slots.map { |slot| (slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE }.reduce(:+) + price = 0 + + remaining_duration = total_duration + while remaining_duration > 60 + max_duration = @reservable.prices.where(group_id: @customer.group_id, plan_id: @plan.try(:id)) + .where(Price.arel_table[:duration].lteq(remaining_duration)) + .maximum(:duration) + max_duration_amount = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id), duration: max_duration) + .amount + + price += max_duration_amount + remaining_duration -= max_duration + end + + # base price for the last hour or less + base_amount = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id), duration: 60).amount + price += (base_amount / MINUTES_PER_HOUR) * remaining_duration + + price / (total_duration / MINUTES_PER_HOUR) + end + ## # Compute the number of remaining hours in the users current credits (for machine or space) ## diff --git a/app/services/prepaid_pack_service.rb b/app/services/prepaid_pack_service.rb index 58ba51d8a..49ef7772c 100644 --- a/app/services/prepaid_pack_service.rb +++ b/app/services/prepaid_pack_service.rb @@ -61,10 +61,7 @@ class PrepaidPackService ## Total number of prepaid minutes available def minutes_available(user, priceable) - is_pack_only_for_subscription = Setting.find_by(name: "pack_only_for_subscription")&.value - if is_pack_only_for_subscription == 'true' && !user.subscribed_plan - return 0 - end + return 0 if Setting.get('pack_only_for_subscription') && !user.subscribed_plan user_packs = user_packs(user, priceable) total_available = user_packs.map { |up| up.prepaid_pack.minutes }.reduce(:+) || 0 From 4a8fa65e5fb6fbc48d373ff6308e429d1c7321c3 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 20 Dec 2021 17:19:43 +0100 Subject: [PATCH 03/17] workaround pending for UI --- app/frontend/src/javascript/controllers/admin/pricing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend/src/javascript/controllers/admin/pricing.js b/app/frontend/src/javascript/controllers/admin/pricing.js index bf4baf11d..1337cbd22 100644 --- a/app/frontend/src/javascript/controllers/admin/pricing.js +++ b/app/frontend/src/javascript/controllers/admin/pricing.js @@ -461,7 +461,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', */ $scope.findPriceBy = function (prices, machineId, groupId) { for (const price of Array.from(prices)) { - if ((price.priceable_id === machineId) && (price.group_id === groupId)) { + if ((price.priceable_id === machineId) && (price.group_id === groupId) && (price.duration === 60)) { return price; } } From f8798e28b5e2d7981e84003f3a0a1bede851ac03 Mon Sep 17 00:00:00 2001 From: vincent Date: Mon, 20 Dec 2021 19:12:01 +0100 Subject: [PATCH 04/17] Convert [spaces] to React --- app/frontend/src/javascript/api/space.ts | 20 +++ .../components/pricing/spaces-pricing.tsx | 129 ++++++++++++++++++ .../templates/admin/pricing/spaces.html | 7 +- 3 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 app/frontend/src/javascript/api/space.ts create mode 100644 app/frontend/src/javascript/components/pricing/spaces-pricing.tsx diff --git a/app/frontend/src/javascript/api/space.ts b/app/frontend/src/javascript/api/space.ts new file mode 100644 index 000000000..f2e5b2c23 --- /dev/null +++ b/app/frontend/src/javascript/api/space.ts @@ -0,0 +1,20 @@ +import apiClient from './clients/api-client'; +import { AxiosResponse } from 'axios'; + +export default class MachineAPI { + static async index (filters?: boolean): Promise> { + const res: AxiosResponse> = await apiClient.get(`/api/spaces${this.filtersToQuery(filters)}`); + return res?.data; + } + + static async get (id: number): Promise { + const res: AxiosResponse = await apiClient.get(`/api/spaces/${id}`); + return res?.data; + } + + private static filtersToQuery (filters?: boolean): string { + if (!filters) return ''; + + return '?' + Object.entries(filters).map(f => `${f[0]}=${f[1]}`).join('&'); + } +} diff --git a/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx b/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx new file mode 100644 index 000000000..5ea7220c5 --- /dev/null +++ b/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx @@ -0,0 +1,129 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { react2angular } from 'react2angular'; +import { Loader } from '../base/loader'; +import { FabAlert } from '../base/fab-alert'; +import { HtmlTranslate } from '../base/html-translate'; +import SpaceAPI from '../../api/space'; +import GroupAPI from '../../api/group'; +import { Group } from '../../models/group'; +import { IApplication } from '../../models/application'; +import { EditablePrice } from './editable-price'; +import PriceAPI from '../../api/price'; +import { Price } from '../../models/price'; +import { useImmer } from 'use-immer'; +import FormatLib from '../../lib/format'; + +declare const Application: IApplication; + +interface SpacesPricingProps { + onError: (message: string) => void, + onSuccess: (message: string) => void, +} + +/** + * Interface to set and edit the prices of spaces-hours, per group + */ +const SpacesPricing: React.FC = ({ onError, onSuccess }) => { + const { t } = useTranslation('admin'); + + const [spaces, setSpaces] = useState>(null); + const [groups, setGroups] = useState>(null); + const [prices, updatePrices] = useImmer>(null); + + // retrieve the initial data + useEffect(() => { + SpaceAPI.index(false) + .then(data => setSpaces(data)) + .catch(error => onError(error)); + GroupAPI.index({ disabled: false, admins: false }) + .then(data => setGroups(data)) + .catch(error => onError(error)); + PriceAPI.index({ priceable_type: 'Space', plan_id: null }) + .then(data => updatePrices(data)) + .catch(error => onError(error)); + }, []); + + // duration of the example slot + const EXEMPLE_DURATION = 20; + + /** + * Return the exemple price, formatted + */ + const examplePrice = (type: 'hourly_rate' | 'final_price'): string => { + const hourlyRate = 10; + + if (type === 'hourly_rate') { + return FormatLib.price(hourlyRate); + } + + const price = (hourlyRate / 60) * EXEMPLE_DURATION; + return FormatLib.price(price); + }; + + /** + * Find the price matching the given criterion + */ + const findPriceBy = (spaceId, groupId): Price => { + return prices.find(price => price.priceable_id === spaceId && price.group_id === groupId); + }; + + /** + * Update the given price in the internal state + */ + const updatePrice = (price: Price): void => { + updatePrices(draft => { + const index = draft.findIndex(p => p.id === price.id); + draft[index] = price; + return draft; + }); + }; + + /** + * Callback triggered when the user has confirmed to update a price + */ + const handleUpdatePrice = (price: Price): void => { + PriceAPI.update(price) + .then(() => { + onSuccess(t('app.admin.machines_pricing.price_updated')); + updatePrice(price); + }) + .catch(error => onError(error)); + }; + + return ( +
+ +

+

+

{t('app.admin.pricing.you_can_override')}

+
+ + + + + {groups?.map(group => )} + + + + {spaces?.map(space => + + {groups?.map(group => )} + )} + +
{t('app.admin.pricing.spaces')}{group.name}
{space.name} + {prices && } +
+
+ ); +}; + +const SpacesPricingWrapper: React.FC = ({ onError, onSuccess }) => { + return ( + + + + ); +}; + +Application.Components.component('spacesPricing', react2angular(SpacesPricingWrapper, ['onError', 'onSuccess'])); diff --git a/app/frontend/templates/admin/pricing/spaces.html b/app/frontend/templates/admin/pricing/spaces.html index 41f6d0045..4165e5ca8 100644 --- a/app/frontend/templates/admin/pricing/spaces.html +++ b/app/frontend/templates/admin/pricing/spaces.html @@ -1,8 +1,11 @@ -
+ + + From 6dc2e8e41e1efb932b0ade657e0783b786b8612b Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 21 Dec 2021 09:51:40 +0100 Subject: [PATCH 05/17] modelise space object from API in TS --- app/frontend/src/javascript/models/price.ts | 2 +- app/frontend/src/javascript/models/space.ts | 15 +++++++++++++++ app/views/api/spaces/index.json.jbuilder | 2 ++ app/views/api/spaces/show.json.jbuilder | 4 +++- 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 app/frontend/src/javascript/models/space.ts diff --git a/app/frontend/src/javascript/models/price.ts b/app/frontend/src/javascript/models/price.ts index fe84f4ba5..fcd1b0e8e 100644 --- a/app/frontend/src/javascript/models/price.ts +++ b/app/frontend/src/javascript/models/price.ts @@ -12,7 +12,7 @@ export interface Price { priceable_type: string, priceable_id: number, amount: number, - duration: number // in minutes + duration?: number // in minutes } export interface ComputePriceResult { diff --git a/app/frontend/src/javascript/models/space.ts b/app/frontend/src/javascript/models/space.ts new file mode 100644 index 000000000..a5337c437 --- /dev/null +++ b/app/frontend/src/javascript/models/space.ts @@ -0,0 +1,15 @@ + +export interface Space { + id: number, + name: string, + description: string, + slug: string, + default_places: number, + disabled: boolean, + space_image: string, + space_file_attributes?: { + id: number, + attachment: string, + attachement_url: string, + } +} diff --git a/app/views/api/spaces/index.json.jbuilder b/app/views/api/spaces/index.json.jbuilder index e0dc6c7d7..97572fe74 100644 --- a/app/views/api/spaces/index.json.jbuilder +++ b/app/views/api/spaces/index.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.array!(@spaces) do |space| json.extract! space, :id, :name, :description, :slug, :default_places, :disabled json.space_image space.space_image.attachment.medium.url if space.space_image diff --git a/app/views/api/spaces/show.json.jbuilder b/app/views/api/spaces/show.json.jbuilder index 6ce0b7843..15147739f 100644 --- a/app/views/api/spaces/show.json.jbuilder +++ b/app/views/api/spaces/show.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.extract! @space, :id, :name, :description, :characteristics, :created_at, :updated_at, :slug, :default_places, :disabled json.space_image @space.space_image.attachment.large.url if @space.space_image json.space_files_attributes @space.space_files do |f| @@ -9,4 +11,4 @@ end # using the space in the space_show screen # json.space_projects @space.projects do |p| # json.extract! p, :slug, :name -# end \ No newline at end of file +# end From 2b834045efe96f4b67df120c05d838b1aba2d24b Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 21 Dec 2021 11:14:09 +0100 Subject: [PATCH 06/17] improve price calculation --- app/models/cart_item/reservation.rb | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/models/cart_item/reservation.rb b/app/models/cart_item/reservation.rb index d3df985a2..b60bfdce5 100644 --- a/app/models/cart_item/reservation.rb +++ b/app/models/cart_item/reservation.rb @@ -16,7 +16,7 @@ class CartItem::Reservation < CartItem::BaseItem end def price - base_amount = get_hourly_rate + base_amount = hourly_rate is_privileged = @operator.privileged? && @operator.id != @customer.id prepaid = { minutes: PrepaidPackService.minutes_available(@customer, @reservable) } @@ -109,26 +109,24 @@ class CartItem::Reservation < CartItem::BaseItem # Eg. If the reservation is for 12 hours, and there are prices for 3 hours, 7 hours, # and the base price (1 hours), we use the 7 hours price, then 3 hours price, and finally the base price. # Then we divide the total price by the total duration to get the hourly rate. - def get_hourly_rate + def hourly_rate total_duration = @slots.map { |slot| (slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE }.reduce(:+) price = 0 remaining_duration = total_duration - while remaining_duration > 60 + while remaining_duration.positive? max_duration = @reservable.prices.where(group_id: @customer.group_id, plan_id: @plan.try(:id)) .where(Price.arel_table[:duration].lteq(remaining_duration)) .maximum(:duration) + max_duration = 60 if max_duration.nil? max_duration_amount = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id), duration: max_duration) .amount - price += max_duration_amount + current_duration = [remaining_duration, max_duration].min + price += (max_duration_amount / max_duration) * current_duration remaining_duration -= max_duration end - # base price for the last hour or less - base_amount = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id), duration: 60).amount - price += (base_amount / MINUTES_PER_HOUR) * remaining_duration - price / (total_duration / MINUTES_PER_HOUR) end From 6091cec82eb5b6b001bde258a2360e4123e5f65d Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 21 Dec 2021 14:18:03 +0100 Subject: [PATCH 07/17] api endpoint to create prices --- app/controllers/api/prices_controller.rb | 17 +++++++++++ app/policies/price_policy.rb | 4 +++ app/views/api/prices/create.json.jbuilder | 3 ++ app/views/api/prices/update.json.jbuilder | 2 ++ config/routes.rb | 2 +- test/fixtures/prices.yml | 36 +++++++++++++++++++++++ 6 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 app/views/api/prices/create.json.jbuilder diff --git a/app/controllers/api/prices_controller.rb b/app/controllers/api/prices_controller.rb index 777dffa42..9eb21c145 100644 --- a/app/controllers/api/prices_controller.rb +++ b/app/controllers/api/prices_controller.rb @@ -5,6 +5,19 @@ class API::PricesController < API::ApiController before_action :authenticate_user! + def create + @price = Price.new(price_params) + @price.amount *= 100 + + authorize @price + + if @price.save + render json: @price, status: :created + else + render json: @price.errors, status: :unprocessable_entity + end + end + def index @prices = PriceService.list(params) end @@ -29,6 +42,10 @@ class API::PricesController < API::ApiController private + def price_create_params + params.require(:price).permit(:amount, :duration, :group_id, :plan_id, :priceable_id, :priceable_type) + end + def price_params params.require(:price).permit(:amount, :duration) end diff --git a/app/policies/price_policy.rb b/app/policies/price_policy.rb index d403d9ab2..4a2370a0f 100644 --- a/app/policies/price_policy.rb +++ b/app/policies/price_policy.rb @@ -2,6 +2,10 @@ # Check the access policies for API::PricesController class PricePolicy < ApplicationPolicy + def create? + user.admin? && record.duration != 60 + end + def update? user.admin? end diff --git a/app/views/api/prices/create.json.jbuilder b/app/views/api/prices/create.json.jbuilder new file mode 100644 index 000000000..a44c04cb6 --- /dev/null +++ b/app/views/api/prices/create.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.partial! 'api/prices/price', price: @price diff --git a/app/views/api/prices/update.json.jbuilder b/app/views/api/prices/update.json.jbuilder index a59de38a1..a44c04cb6 100644 --- a/app/views/api/prices/update.json.jbuilder +++ b/app/views/api/prices/update.json.jbuilder @@ -1 +1,3 @@ +# frozen_string_literal: true + json.partial! 'api/prices/price', price: @price diff --git a/config/routes.rb b/config/routes.rb index 4fca0d997..155971cf6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -75,7 +75,7 @@ Rails.application.routes.draw do get 'pricing' => 'pricing#index' put 'pricing' => 'pricing#update' - resources :prices, only: %i[index update] do + resources :prices, only: %i[create index update] do post 'compute', on: :collection end resources :prepaid_packs diff --git a/test/fixtures/prices.yml b/test/fixtures/prices.yml index 0e7ac37cd..a4e8b0b8f 100644 --- a/test/fixtures/prices.yml +++ b/test/fixtures/prices.yml @@ -6,6 +6,7 @@ price_1: priceable_id: 1 priceable_type: Machine amount: 2400 + duration: 60 created_at: 2016-04-04 14:11:34.242608000 Z updated_at: 2016-04-04 14:11:34.242608000 Z @@ -16,6 +17,7 @@ price_2: priceable_id: 1 priceable_type: Machine amount: 5300 + duration: 60 created_at: 2016-04-04 14:11:34.247363000 Z updated_at: 2016-04-04 14:11:34.247363000 Z @@ -26,6 +28,7 @@ price_5: priceable_id: 2 priceable_type: Machine amount: 4200 + duration: 60 created_at: 2016-04-04 14:11:34.290427000 Z updated_at: 2016-04-04 14:11:34.290427000 Z @@ -36,6 +39,7 @@ price_6: priceable_id: 2 priceable_type: Machine amount: 1100 + duration: 60 created_at: 2016-04-04 14:11:34.293603000 Z updated_at: 2016-04-04 14:11:34.293603000 Z @@ -46,6 +50,7 @@ price_9: priceable_id: 3 priceable_type: Machine amount: 4100 + duration: 60 created_at: 2016-04-04 14:11:34.320809000 Z updated_at: 2016-04-04 14:11:34.320809000 Z @@ -56,6 +61,7 @@ price_10: priceable_id: 3 priceable_type: Machine amount: 5300 + duration: 60 created_at: 2016-04-04 14:11:34.325274000 Z updated_at: 2016-04-04 14:11:34.325274000 Z @@ -66,6 +72,7 @@ price_13: priceable_id: 4 priceable_type: Machine amount: 900 + duration: 60 created_at: 2016-04-04 14:11:34.362313000 Z updated_at: 2016-04-04 14:11:34.362313000 Z @@ -76,6 +83,7 @@ price_14: priceable_id: 4 priceable_type: Machine amount: 5100 + duration: 60 created_at: 2016-04-04 14:11:34.366049000 Z updated_at: 2016-04-04 14:11:34.366049000 Z @@ -86,6 +94,7 @@ price_17: priceable_id: 5 priceable_type: Machine amount: 1600 + duration: 60 created_at: 2016-04-04 14:11:34.398206000 Z updated_at: 2016-04-04 14:11:34.398206000 Z @@ -96,6 +105,7 @@ price_18: priceable_id: 5 priceable_type: Machine amount: 2000 + duration: 60 created_at: 2016-04-04 14:11:34.407216000 Z updated_at: 2016-04-04 14:11:34.407216000 Z @@ -106,6 +116,7 @@ price_21: priceable_id: 6 priceable_type: Machine amount: 3200 + duration: 60 created_at: 2016-04-04 14:11:34.442054000 Z updated_at: 2016-04-04 14:11:34.442054000 Z @@ -116,6 +127,7 @@ price_22: priceable_id: 6 priceable_type: Machine amount: 3400 + duration: 60 created_at: 2016-04-04 14:11:34.445147000 Z updated_at: 2016-04-04 14:11:34.445147000 Z @@ -126,6 +138,7 @@ price_25: priceable_id: 1 priceable_type: Machine amount: 1500 + duration: 60 created_at: 2016-04-04 15:15:21.038387000 Z updated_at: 2016-04-04 15:15:45.691674000 Z @@ -136,6 +149,7 @@ price_26: priceable_id: 2 priceable_type: Machine amount: 1500 + duration: 60 created_at: 2016-04-04 15:15:21.048838000 Z updated_at: 2016-04-04 15:15:45.693896000 Z @@ -146,6 +160,7 @@ price_27: priceable_id: 3 priceable_type: Machine amount: 2500 + duration: 60 created_at: 2016-04-04 15:15:21.053412000 Z updated_at: 2016-04-04 15:15:45.697794000 Z @@ -156,6 +171,7 @@ price_28: priceable_id: 4 priceable_type: Machine amount: 1500 + duration: 60 created_at: 2016-04-04 15:15:21.057117000 Z updated_at: 2016-04-04 15:15:45.700657000 Z @@ -166,6 +182,7 @@ price_29: priceable_id: 5 priceable_type: Machine amount: 1300 + duration: 60 created_at: 2016-04-04 15:15:21.061171000 Z updated_at: 2016-04-04 15:15:45.707564000 Z @@ -176,6 +193,7 @@ price_30: priceable_id: 6 priceable_type: Machine amount: 1500 + duration: 60 created_at: 2016-04-04 15:15:21.065166000 Z updated_at: 2016-04-04 15:15:45.710945000 Z @@ -186,6 +204,7 @@ price_31: priceable_id: 1 priceable_type: Machine amount: 1500 + duration: 60 created_at: 2016-04-04 15:17:24.920457000 Z updated_at: 2016-04-04 15:17:34.255229000 Z @@ -196,6 +215,7 @@ price_32: priceable_id: 2 priceable_type: Machine amount: 1500 + duration: 60 created_at: 2016-04-04 15:17:24.926967000 Z updated_at: 2016-04-04 15:17:34.257285000 Z @@ -206,6 +226,7 @@ price_33: priceable_id: 3 priceable_type: Machine amount: 2500 + duration: 60 created_at: 2016-04-04 15:17:24.932723000 Z updated_at: 2016-04-04 15:17:34.258741000 Z @@ -216,6 +237,7 @@ price_34: priceable_id: 4 priceable_type: Machine amount: 1500 + duration: 60 created_at: 2016-04-04 15:17:24.937168000 Z updated_at: 2016-04-04 15:17:34.260503000 Z @@ -226,6 +248,7 @@ price_35: priceable_id: 5 priceable_type: Machine amount: 1300 + duration: 60 created_at: 2016-04-04 15:17:24.940520000 Z updated_at: 2016-04-04 15:17:34.263627000 Z @@ -236,6 +259,7 @@ price_36: priceable_id: 6 priceable_type: Machine amount: 1500 + duration: 60 created_at: 2016-04-04 15:17:24.944460000 Z updated_at: 2016-04-04 15:17:34.267328000 Z @@ -246,6 +270,7 @@ price_37: priceable_id: 1 priceable_type: Machine amount: 1000 + duration: 60 created_at: 2016-04-04 15:18:28.836899000 Z updated_at: 2016-04-04 15:18:50.507019000 Z @@ -256,6 +281,7 @@ price_38: priceable_id: 2 priceable_type: Machine amount: 1000 + duration: 60 created_at: 2016-04-04 15:18:28.842674000 Z updated_at: 2016-04-04 15:18:50.508799000 Z @@ -266,6 +292,7 @@ price_39: priceable_id: 3 priceable_type: Machine amount: 1500 + duration: 60 created_at: 2016-04-04 15:18:28.847736000 Z updated_at: 2016-04-04 15:18:50.510437000 Z @@ -276,6 +303,7 @@ price_40: priceable_id: 4 priceable_type: Machine amount: 1000 + duration: 60 created_at: 2016-04-04 15:18:28.852783000 Z updated_at: 2016-04-04 15:18:50.512239000 Z @@ -286,6 +314,7 @@ price_41: priceable_id: 5 priceable_type: Machine amount: 800 + duration: 60 created_at: 2016-04-04 15:18:28.856602000 Z updated_at: 2016-04-04 15:18:50.514062000 Z @@ -296,6 +325,7 @@ price_42: priceable_id: 6 priceable_type: Machine amount: 1000 + duration: 60 created_at: 2016-04-04 15:18:28.860220000 Z updated_at: 2016-04-04 15:18:50.517702000 Z @@ -306,6 +336,7 @@ price_43: priceable_id: 1 priceable_type: Machine amount: 1000 + duration: 60 created_at: 2016-04-04 15:18:28.836899000 Z updated_at: 2016-04-04 15:18:50.507019000 Z @@ -316,6 +347,7 @@ price_44: priceable_id: 2 priceable_type: Machine amount: 1000 + duration: 60 created_at: 2016-04-04 15:18:28.842674000 Z updated_at: 2016-04-04 15:18:50.508799000 Z @@ -326,6 +358,7 @@ price_45: priceable_id: 3 priceable_type: Machine amount: 1500 + duration: 60 created_at: 2016-04-04 15:18:28.847736000 Z updated_at: 2016-04-04 15:18:50.510437000 Z @@ -336,6 +369,7 @@ price_46: priceable_id: 4 priceable_type: Machine amount: 1000 + duration: 60 created_at: 2016-04-04 15:18:28.852783000 Z updated_at: 2016-04-04 15:18:50.512239000 Z @@ -346,6 +380,7 @@ price_47: priceable_id: 5 priceable_type: Machine amount: 800 + duration: 60 created_at: 2016-04-04 15:18:28.856602000 Z updated_at: 2016-04-04 15:18:50.514062000 Z @@ -356,5 +391,6 @@ price_48: priceable_id: 6 priceable_type: Machine amount: 1000 + duration: 60 created_at: 2016-04-04 15:18:28.860220000 Z updated_at: 2016-04-04 15:18:50.517702000 Z From d6a4675209b18d2e2ac6b70deb8a6b65bc77c3c1 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 21 Dec 2021 14:37:38 +0100 Subject: [PATCH 08/17] wip --- .../pricing/configure-timeslot-button.tsx | 75 +++++++++++++++++++ .../components/pricing/create-timeslot.tsx | 70 +++++++++++++++++ .../components/pricing/spaces-pricing.tsx | 17 +++-- .../components/pricing/timeslot-form.tsx | 74 ++++++++++++++++++ .../templates/admin/pricing/spaces.html | 33 +------- config/locales/app.admin.en.yml | 18 +++++ 6 files changed, 249 insertions(+), 38 deletions(-) create mode 100644 app/frontend/src/javascript/components/pricing/configure-timeslot-button.tsx create mode 100644 app/frontend/src/javascript/components/pricing/create-timeslot.tsx create mode 100644 app/frontend/src/javascript/components/pricing/timeslot-form.tsx diff --git a/app/frontend/src/javascript/components/pricing/configure-timeslot-button.tsx b/app/frontend/src/javascript/components/pricing/configure-timeslot-button.tsx new file mode 100644 index 000000000..71c2e6393 --- /dev/null +++ b/app/frontend/src/javascript/components/pricing/configure-timeslot-button.tsx @@ -0,0 +1,75 @@ +import React, { ReactNode, useState } from 'react'; +import { Price } from '../../models/price'; +import { useTranslation } from 'react-i18next'; +import { FabPopover } from '../base/fab-popover'; +import { CreateTimeslot } from './create-timeslot'; +import PriceAPI from '../../api/price'; +import FormatLib from '../../lib/format'; + +interface ConfigureTimeslotButtonProps { + prices: Array, + onError: (message: string) => void, + onSuccess: (message: string) => void, + groupId: number, + priceableId: number, + priceableType: string, +} + +/** + * This component is a button that shows the list of timeslots. + * It also triggers modal dialogs to configure (add/delete/edit/remove) timeslots. + */ +export const ConfigureTimeslotButton: React.FC = ({ prices, onError, onSuccess, groupId, priceableId, priceableType }) => { + const { t } = useTranslation('admin'); + + const [timeslots, setTimeslots] = useState>(prices); + const [showList, setShowList] = useState(false); + + /** + * Open/closes the popover listing the existing packs + */ + const toggleShowList = (): void => { + setShowList(!showList); + }; + + /** + * Callback triggered when the timeslot was successfully created/deleted/updated. + * We refresh the list of timeslots. + */ + const handleSuccess = (message: string) => { + onSuccess(message); + PriceAPI.index({ group_id: groupId, priceable_id: priceableId, priceable_type: priceableType }) + .then(data => setTimeslots(data)) + .catch(error => onError(error)); + }; + + /** + * Render the button used to trigger the "new pack" modal + */ + const renderAddButton = (): ReactNode => { + return ; + }; + + return ( +
+ + {showList && +
    + {timeslots?.map(timeslot => +
  • + {timeslot.duration} {t('app.admin.calendar.minutes')} - {FormatLib.price(timeslot.amount)} + + +
  • )} +
+ {timeslots?.length === 0 && {t('app.admin.configure_timeslots_button.no_timeslots')}} +
} +
+ ); +}; diff --git a/app/frontend/src/javascript/components/pricing/create-timeslot.tsx b/app/frontend/src/javascript/components/pricing/create-timeslot.tsx new file mode 100644 index 000000000..b3e15f7a2 --- /dev/null +++ b/app/frontend/src/javascript/components/pricing/create-timeslot.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import { FabModal } from '../base/fab-modal'; +import { TimeslotForm } from './timeslot-form'; +import { Price } from '../../models/price'; +import PrepaidPackAPI from '../../api/prepaid-pack'; +import { useTranslation } from 'react-i18next'; +import { FabAlert } from '../base/fab-alert'; + +interface CreateTimeslotProps { + onSuccess: (message: string) => void, + onError: (message: string) => void, + groupId: number, + priceableId: number, + priceableType: string, +} + +/** + * This component shows a button. + * When clicked, we show a modal dialog handing the process of creating a new time slot + */ +export const CreateTimeslot: React.FC = ({ onSuccess, onError, groupId, priceableId, priceableType }) => { + const { t } = useTranslation('admin'); + + const [isOpen, setIsOpen] = useState(false); + + /** + * Open/closes the "new pack" modal dialog + */ + const toggleModal = (): void => { + setIsOpen(!isOpen); + }; + + /** + * Callback triggered when the user has validated the creation of the new time slot + */ + const handleSubmit = (timeslot: Price): void => { + // set the already-known attributes of the new pack + const newTimeslot = Object.assign({} as Price, timeslot); + newTimeslot.group_id = groupId; + newTimeslot.priceable_id = priceableId; + newTimeslot.priceable_type = priceableType; + + // create it on the API + console.log('newTimeslot :', newTimeslot); + // PrepaidPackAPI.create(newPack) + // .then(() => { + // onSuccess(t('app.admin.create_timeslot.timeslot_successfully_created')); + // toggleModal(); + // }) + // .catch(error => onError(error)); + }; + + return ( +
+ + + + {t('app.admin.create_timeslot.new_timeslot_info', { TYPE: priceableType })} + + + +
+ ); +}; diff --git a/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx b/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx index 5ea7220c5..5536000dc 100644 --- a/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx @@ -9,6 +9,7 @@ import GroupAPI from '../../api/group'; import { Group } from '../../models/group'; import { IApplication } from '../../models/application'; import { EditablePrice } from './editable-price'; +import { ConfigureTimeslotButton } from './configure-timeslot-button'; import PriceAPI from '../../api/price'; import { Price } from '../../models/price'; import { useImmer } from 'use-immer'; @@ -61,11 +62,8 @@ const SpacesPricing: React.FC = ({ onError, onSuccess }) => return FormatLib.price(price); }; - /** - * Find the price matching the given criterion - */ - const findPriceBy = (spaceId, groupId): Price => { - return prices.find(price => price.priceable_id === spaceId && price.group_id === groupId); + const findPricesBy = (spaceId, groupId): Array => { + return prices.filter(price => price.priceable_id === spaceId && price.group_id === groupId); }; /** @@ -109,7 +107,14 @@ const SpacesPricing: React.FC = ({ onError, onSuccess }) => {spaces?.map(space => {space.name} {groups?.map(group => - {prices && } + {prices && } + )} )} diff --git a/app/frontend/src/javascript/components/pricing/timeslot-form.tsx b/app/frontend/src/javascript/components/pricing/timeslot-form.tsx new file mode 100644 index 000000000..bc31fba97 --- /dev/null +++ b/app/frontend/src/javascript/components/pricing/timeslot-form.tsx @@ -0,0 +1,74 @@ +import React, { BaseSyntheticEvent } from 'react'; +import { Price } from '../../models/price'; +import { useTranslation } from 'react-i18next'; +import { useImmer } from 'use-immer'; +import { FabInput } from '../base/fab-input'; +import { IFablab } from '../../models/fablab'; + +declare let Fablab: IFablab; + +interface PackFormProps { + formId: string, + onSubmit: (pack: Price) => void, + price?: Price, +} + +/** + * A form component to create/edit a time slot. + * The form validation must be created elsewhere, using the attribute form={formId}. + */ +export const TimeslotForm: React.FC = ({ formId, onSubmit, price }) => { + const [timeslotData, updateTimeslotData] = useImmer(price || {} as Price); + + const { t } = useTranslation('admin'); + + /** + * Callback triggered when the user sends the form. + */ + const handleSubmit = (event: BaseSyntheticEvent): void => { + event.preventDefault(); + onSubmit(timeslotData); + }; + + /** + * Callback triggered when the user inputs an amount for the current time slot. + */ + const handleUpdateAmount = (amount: string) => { + updateTimeslotData(draft => { + draft.amount = parseFloat(amount); + }); + }; + + /** + * Callback triggered when the user inputs a number of minutes for the current time slot. + */ + const handleUpdateHours = (minutes: string) => { + updateTimeslotData(draft => { + draft.duration = parseInt(minutes, 10); + }); + }; + + return ( +
+ + } + required /> + + } + addOn={Fablab.intl_currency} + required /> + + ); +}; diff --git a/app/frontend/templates/admin/pricing/spaces.html b/app/frontend/templates/admin/pricing/spaces.html index 4165e5ca8..eb4ca3899 100644 --- a/app/frontend/templates/admin/pricing/spaces.html +++ b/app/frontend/templates/admin/pricing/spaces.html @@ -1,32 +1 @@ - - - + diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 570dd1a24..784b2874c 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -378,6 +378,9 @@ en: packs: "Prepaid packs" no_packs: "No packs for now" pack_DURATION: "{DURATION} hours" + configure_timeslots_button: + timeslots: "Time slots" + no_timeslots: "No time slot for now" pack_form: hours: "Hours" amount: "Price" @@ -404,6 +407,21 @@ en: edit_pack: "Edit the pack" confirm_changes: "Confirm changes" pack_successfully_updated: "The prepaid pack was successfully updated." + create_timeslot: + new_timeslot: "New time slot" + new_timeslot_info: "..." + create_timeslot: "Create this time slot" + timeslot_successfully_created: "The new time slot was successfully created." + delete_timeslot: + timeslot_deleted: "The time slot was successfully deleted." + unable_to_delete: "Unable to delete the time slot: " + delete_timeslot: "Delete the time slot" + confirm_delete: "Delete" + delete_confirmation: "Are you sure you want to delete this time slot? This won't be possible if it was already bought by users." + edit_timeslot: + edit_timeslot: "Edit the time slot" + confirm_changes: "Confirm changes" + timeslot_successfully_updated: "The time slot was successfully updated." #ajouter un code promotionnel coupons_new: add_a_coupon: "Add a coupon" From 2d807e6c9493d0a439eb3abca7826388d0b7304d Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 21 Dec 2021 15:30:08 +0100 Subject: [PATCH 09/17] improved slot price calculation --- app/models/cart_item/reservation.rb | 54 ++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/app/models/cart_item/reservation.rb b/app/models/cart_item/reservation.rb index b60bfdce5..fddc51f04 100644 --- a/app/models/cart_item/reservation.rb +++ b/app/models/cart_item/reservation.rb @@ -16,7 +16,6 @@ class CartItem::Reservation < CartItem::BaseItem end def price - base_amount = hourly_rate is_privileged = @operator.privileged? && @operator.id != @customer.id prepaid = { minutes: PrepaidPackService.minutes_available(@customer, @reservable) } @@ -25,10 +24,10 @@ class CartItem::Reservation < CartItem::BaseItem hours_available = credits @slots.each_with_index do |slot, index| - amount += get_slot_price(base_amount, slot, is_privileged, - elements: elements, - has_credits: (index < hours_available), - prepaid: prepaid) + amount += get_slot_price_from_prices(applicable_prices, slot, is_privileged, + elements: elements, + has_credits: (index < hours_available), + prepaid: prepaid) end { elements: elements, amount: amount } @@ -61,6 +60,27 @@ class CartItem::Reservation < CartItem::BaseItem 0 end + ## + # Compute the price of a single slot, according to the list of applicable prices. + # @param prices {{ prices: Array<{price: Price, duration: number}> }} list of prices to use with the current reservation + # @see get_slot_price + ## + def get_slot_price_from_prices(prices, slot, is_privileged, options = {}) + options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options) + + slot_minutes = (slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE + price = prices[:prices].find { |p| p[:duration] <= slot_minutes && p[:duration].positive? } + price = prices[:prices].first if price.nil? + hourly_rate = (price[:price].amount.to_f / price[:price].duration) * MINUTES_PER_HOUR + + # apply the base price to the real slot duration + real_price = get_slot_price(hourly_rate, slot, is_privileged, options) + + price[:duration] -= slot_minutes + + real_price + end + ## # Compute the price of a single slot, according to the base price and the ability for an admin # to offer the slot. @@ -103,15 +123,14 @@ class CartItem::Reservation < CartItem::BaseItem real_price end - # We compute the hourly rate according to the prices of the current reservation - # If there are prices for durations longer than 1 hour, but shorter than the total duration, - # we use these prices before using the hourly rate. + # We determine the list of prices applicable to current reservation + # The longest available price is always used in priority. # Eg. If the reservation is for 12 hours, and there are prices for 3 hours, 7 hours, - # and the base price (1 hours), we use the 7 hours price, then 3 hours price, and finally the base price. - # Then we divide the total price by the total duration to get the hourly rate. - def hourly_rate + # and the base price (1 hours), we use the 7 hours price, then 3 hours price, and finally the base price twice (7+3+1+1 = 12). + # All these prices are returned to be applied to the reservation. + def applicable_prices total_duration = @slots.map { |slot| (slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE }.reduce(:+) - price = 0 + rates = { prices: [] } remaining_duration = total_duration while remaining_duration.positive? @@ -119,15 +138,16 @@ class CartItem::Reservation < CartItem::BaseItem .where(Price.arel_table[:duration].lteq(remaining_duration)) .maximum(:duration) max_duration = 60 if max_duration.nil? - max_duration_amount = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id), duration: max_duration) - .amount + max_duration_price = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id), duration: max_duration) current_duration = [remaining_duration, max_duration].min - price += (max_duration_amount / max_duration) * current_duration - remaining_duration -= max_duration + rates[:prices].push(price: max_duration_price, duration: current_duration) + + remaining_duration -= current_duration end - price / (total_duration / MINUTES_PER_HOUR) + rates[:prices].sort! { |a, b| b[:duration] <=> a[:duration] } + rates end ## From ffb0f3e19e8ed759225ffffe5413540b2579d819 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 21 Dec 2021 16:32:02 +0100 Subject: [PATCH 10/17] ability to limit extended prices to slots in the same day --- CHANGELOG.md | 1 + app/frontend/src/javascript/models/setting.ts | 3 +- app/frontend/src/javascript/router.js | 2 +- .../admin/settings/reservations.html | 33 ++++++++++++++----- app/models/cart_item/reservation.rb | 7 ++-- app/models/setting.rb | 3 +- config/locales/app.admin.en.yml | 3 ++ config/locales/en.yml | 1 + db/seeds.rb | 2 ++ 9 files changed, 41 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83d544527..885c8a5f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Updated portuguese translation - Refactored the ReserveButton component to use the same user's data across all the component +- [TODO DEPLOY] `rails db:seed` ## v5.1.13 2021 November 16 diff --git a/app/frontend/src/javascript/models/setting.ts b/app/frontend/src/javascript/models/setting.ts index e92dff2b7..08c01cd01 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -111,7 +111,8 @@ export enum SettingName { PublicAgendaModule = 'public_agenda_module', RenewPackThreshold = 'renew_pack_threshold', PackOnlyForSubscription = 'pack_only_for_subscription', - OverlappingCategories = 'overlapping_categories' + OverlappingCategories = 'overlapping_categories', + ExtendedPricesInSameDay = 'extended_prices_in_same_day' } export type SettingValue = string|boolean|number; diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index 394a64ce1..0f24a91b5 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -1080,7 +1080,7 @@ angular.module('application.router', ['ui.router']) "'reminder_delay', 'visibility_yearly', 'visibility_others', 'wallet_module', 'trainings_module', " + "'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', 'address_required', " + "'link_name', 'home_content', 'home_css', 'phone_required', 'upcoming_events_shown', 'public_agenda_module'," + - "'renew_pack_threshold', 'pack_only_for_subscription', 'overlapping_categories']" + "'renew_pack_threshold', 'pack_only_for_subscription', 'overlapping_categories', 'extended_prices_in_same_day']" }).$promise; }], privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }], diff --git a/app/frontend/templates/admin/settings/reservations.html b/app/frontend/templates/admin/settings/reservations.html index 05e599930..e0f2d9ccc 100644 --- a/app/frontend/templates/admin/settings/reservations.html +++ b/app/frontend/templates/admin/settings/reservations.html @@ -117,6 +117,28 @@ required="true">
+ +
+
+

{{ 'app.admin.settings.pack_only_for_subscription_info' }}

+

+ + +
+ +
+
+

{{ 'app.admin.settings.extended_prices' }}

+

+ + +
@@ -170,6 +192,8 @@ label="app.admin.settings.show_event" classes="m-l"> + +

{{ 'app.admin.settings.display_invite_to_renew_pack' }}

@@ -182,14 +206,5 @@ step="0.01">
-
-

{{ 'app.admin.settings.pack_only_for_subscription_info' }}

-

- - -
diff --git a/app/models/cart_item/reservation.rb b/app/models/cart_item/reservation.rb index fddc51f04..9f97b1a45 100644 --- a/app/models/cart_item/reservation.rb +++ b/app/models/cart_item/reservation.rb @@ -18,13 +18,14 @@ class CartItem::Reservation < CartItem::BaseItem def price is_privileged = @operator.privileged? && @operator.id != @customer.id prepaid = { minutes: PrepaidPackService.minutes_available(@customer, @reservable) } + prices = applicable_prices elements = { slots: [] } amount = 0 hours_available = credits @slots.each_with_index do |slot, index| - amount += get_slot_price_from_prices(applicable_prices, slot, is_privileged, + amount += get_slot_price_from_prices(prices, slot, is_privileged, elements: elements, has_credits: (index < hours_available), prepaid: prepaid) @@ -129,6 +130,8 @@ class CartItem::Reservation < CartItem::BaseItem # and the base price (1 hours), we use the 7 hours price, then 3 hours price, and finally the base price twice (7+3+1+1 = 12). # All these prices are returned to be applied to the reservation. def applicable_prices + all_slots_in_same_day = @slots.map { |slot| slot[:start_at].to_date }.uniq.size == 1 + total_duration = @slots.map { |slot| (slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE }.reduce(:+) rates = { prices: [] } @@ -137,7 +140,7 @@ class CartItem::Reservation < CartItem::BaseItem max_duration = @reservable.prices.where(group_id: @customer.group_id, plan_id: @plan.try(:id)) .where(Price.arel_table[:duration].lteq(remaining_duration)) .maximum(:duration) - max_duration = 60 if max_duration.nil? + max_duration = 60 if max_duration.nil? || Setting.get('extended_prices_in_same_day') && !all_slots_in_same_day max_duration_price = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id), duration: max_duration) current_duration = [remaining_duration, max_duration].min diff --git a/app/models/setting.rb b/app/models/setting.rb index d0742a62d..e6ed14298 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -121,7 +121,8 @@ class Setting < ApplicationRecord public_agenda_module renew_pack_threshold pack_only_for_subscription - overlapping_categories] } + overlapping_categories + extended_prices_in_same_day] } # WARNING: when adding a new key, you may also want to add it in: # - config/locales/en.yml#settings # - app/frontend/src/javascript/models/setting.ts#SettingName diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 570dd1a24..e2eb3d17f 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -1235,6 +1235,9 @@ en: pack_only_for_subscription_info_html: "If this option is activated, the purchase and use of a prepaid pack is only possible for the user with a valid subscription." pack_only_for_subscription: "Subscription valid for purchase and use of a prepaid pack" pack_only_for_subscription_info: "Make subscription mandatory for prepaid packs" + extended_prices: "Extended prices" + extended_prices_info_html: "Spaces can have different prices depending on the cumulated duration of the booking. You can choose if this apply to all bookings or only to those starting within the same day." + extended_prices_in_same_day: "Extended prices in the same day" overlapping_options: training_reservations: "Trainings" machine_reservations: "Machines" diff --git a/config/locales/en.yml b/config/locales/en.yml index 688e92473..a6e36f3f4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -535,3 +535,4 @@ en: renew_pack_threshold: "Threshold for packs renewal" pack_only_for_subscription: "Restrict packs for subscribers" overlapping_categories: "Categories for overlapping booking prevention" + extended_prices_in_same_day: "Extended prices in the same day" diff --git a/db/seeds.rb b/db/seeds.rb index 73c9ff89e..67653a44b 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -905,6 +905,8 @@ unless Setting.find_by(name: 'overlapping_categories').try(:value) Setting.set('overlapping_categories', 'training_reservations,machine_reservations,space_reservations,events_reservations') end +Setting.set('extended_prices_in_same_day', true) unless Setting.find_by(name: 'extended_prices_in_same_day').try(:value) + if StatisticCustomAggregation.count.zero? # available reservations hours for machines machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2) From bd781a14e952b68b5b98af782cddec1388c9b116 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 21 Dec 2021 17:13:40 +0100 Subject: [PATCH 11/17] Add extended price --- app/controllers/api/prices_controller.rb | 14 +++- app/frontend/src/javascript/api/price.ts | 10 +++ ...sx => configure-extended-price-button.tsx} | 34 +++++----- ...timeslot.tsx => create-extended-price.tsx} | 43 ++++++------ .../pricing/delete-extended-price.tsx | 56 ++++++++++++++++ .../pricing/edit-extended-price.tsx | 66 +++++++++++++++++++ ...eslot-form.tsx => extended-price-form.tsx} | 20 +++--- .../components/pricing/spaces-pricing.tsx | 4 +- app/models/price.rb | 2 +- app/policies/price_policy.rb | 4 ++ config/locales/app.admin.en.yml | 32 ++++----- config/routes.rb | 2 +- 12 files changed, 218 insertions(+), 69 deletions(-) rename app/frontend/src/javascript/components/pricing/{configure-timeslot-button.tsx => configure-extended-price-button.tsx} (51%) rename app/frontend/src/javascript/components/pricing/{create-timeslot.tsx => create-extended-price.tsx} (52%) create mode 100644 app/frontend/src/javascript/components/pricing/delete-extended-price.tsx create mode 100644 app/frontend/src/javascript/components/pricing/edit-extended-price.tsx rename app/frontend/src/javascript/components/pricing/{timeslot-form.tsx => extended-price-form.tsx} (77%) diff --git a/app/controllers/api/prices_controller.rb b/app/controllers/api/prices_controller.rb index 9eb21c145..de84774a2 100644 --- a/app/controllers/api/prices_controller.rb +++ b/app/controllers/api/prices_controller.rb @@ -4,9 +4,10 @@ # Prices are used in reservations (Machine, Space) class API::PricesController < API::ApiController before_action :authenticate_user! + before_action :set_price, only: %i[update destroy] def create - @price = Price.new(price_params) + @price = Price.new(price_create_params) @price.amount *= 100 authorize @price @@ -24,7 +25,6 @@ class API::PricesController < API::ApiController def update authorize Price - @price = Price.find(params[:id]) price_parameters = price_params price_parameters[:amount] = price_parameters[:amount] * 100 if @price.update(price_parameters) @@ -34,6 +34,12 @@ class API::PricesController < API::ApiController end end + def destroy + authorize @price + @price.destroy + head :no_content + end + def compute cs = CartService.new(current_user) cart = cs.from_hash(params) @@ -42,6 +48,10 @@ class API::PricesController < API::ApiController private + def set_price + @price = Price.find(params[:id]) + end + def price_create_params params.require(:price).permit(:amount, :duration, :group_id, :plan_id, :priceable_id, :priceable_type) end diff --git a/app/frontend/src/javascript/api/price.ts b/app/frontend/src/javascript/api/price.ts index d5ab87ec4..d85616a1e 100644 --- a/app/frontend/src/javascript/api/price.ts +++ b/app/frontend/src/javascript/api/price.ts @@ -14,11 +14,21 @@ export default class PriceAPI { return res?.data; } + static async create (price: Price): Promise { + const res: AxiosResponse = await apiClient.post('/api/prices', { price }); + return res?.data; + } + static async update (price: Price): Promise { const res: AxiosResponse = await apiClient.patch(`/api/prices/${price.id}`, { price }); return res?.data; } + static async destroy (priceId: number): Promise { + const res: AxiosResponse = await apiClient.delete(`/api/prices/${priceId}`); + return res?.data; + } + private static filtersToQuery (filters?: PriceIndexFilter): string { if (!filters) return ''; diff --git a/app/frontend/src/javascript/components/pricing/configure-timeslot-button.tsx b/app/frontend/src/javascript/components/pricing/configure-extended-price-button.tsx similarity index 51% rename from app/frontend/src/javascript/components/pricing/configure-timeslot-button.tsx rename to app/frontend/src/javascript/components/pricing/configure-extended-price-button.tsx index 71c2e6393..66705ccc0 100644 --- a/app/frontend/src/javascript/components/pricing/configure-timeslot-button.tsx +++ b/app/frontend/src/javascript/components/pricing/configure-extended-price-button.tsx @@ -2,11 +2,13 @@ import React, { ReactNode, useState } from 'react'; import { Price } from '../../models/price'; import { useTranslation } from 'react-i18next'; import { FabPopover } from '../base/fab-popover'; -import { CreateTimeslot } from './create-timeslot'; +import { CreateExtendedPrice } from './create-extended-price'; import PriceAPI from '../../api/price'; import FormatLib from '../../lib/format'; +import { EditExtendedPrice } from './edit-extended-price'; +import { DeleteExtendedPrice } from './delete-extended-price'; -interface ConfigureTimeslotButtonProps { +interface ConfigureExtendedPriceButtonProps { prices: Array, onError: (message: string) => void, onSuccess: (message: string) => void, @@ -16,13 +18,13 @@ interface ConfigureTimeslotButtonProps { } /** - * This component is a button that shows the list of timeslots. - * It also triggers modal dialogs to configure (add/delete/edit/remove) timeslots. + * This component is a button that shows the list of extendedPrices. + * It also triggers modal dialogs to configure (add/delete/edit/remove) extendedPrices. */ -export const ConfigureTimeslotButton: React.FC = ({ prices, onError, onSuccess, groupId, priceableId, priceableType }) => { +export const ConfigureExtendedPriceButton: React.FC = ({ prices, onError, onSuccess, groupId, priceableId, priceableType }) => { const { t } = useTranslation('admin'); - const [timeslots, setTimeslots] = useState>(prices); + const [extendedPrices, setExtendedPrices] = useState>(prices); const [showList, setShowList] = useState(false); /** @@ -33,13 +35,13 @@ export const ConfigureTimeslotButton: React.FC = ( }; /** - * Callback triggered when the timeslot was successfully created/deleted/updated. - * We refresh the list of timeslots. + * Callback triggered when the extendedPrice was successfully created/deleted/updated. + * We refresh the list of extendedPrices. */ const handleSuccess = (message: string) => { onSuccess(message); PriceAPI.index({ group_id: groupId, priceable_id: priceableId, priceable_type: priceableType }) - .then(data => setTimeslots(data)) + .then(data => setExtendedPrices(data)) .catch(error => onError(error)); }; @@ -47,7 +49,7 @@ export const ConfigureTimeslotButton: React.FC = ( * Render the button used to trigger the "new pack" modal */ const renderAddButton = (): ReactNode => { - return = ( - {showList && + {showList &&
    - {timeslots?.map(timeslot => -
  • - {timeslot.duration} {t('app.admin.calendar.minutes')} - {FormatLib.price(timeslot.amount)} + {extendedPrices?.map(extendedPrice => +
  • + {extendedPrice.duration} {t('app.admin.calendar.minutes')} - {FormatLib.price(extendedPrice.amount)} + +
  • )}
- {timeslots?.length === 0 && {t('app.admin.configure_timeslots_button.no_timeslots')}} + {extendedPrices?.length === 0 && {t('app.admin.configure_extendedPrices_button.no_extendedPrices')}}
} ); diff --git a/app/frontend/src/javascript/components/pricing/create-timeslot.tsx b/app/frontend/src/javascript/components/pricing/create-extended-price.tsx similarity index 52% rename from app/frontend/src/javascript/components/pricing/create-timeslot.tsx rename to app/frontend/src/javascript/components/pricing/create-extended-price.tsx index b3e15f7a2..d965fd4e5 100644 --- a/app/frontend/src/javascript/components/pricing/create-timeslot.tsx +++ b/app/frontend/src/javascript/components/pricing/create-extended-price.tsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; import { FabModal } from '../base/fab-modal'; -import { TimeslotForm } from './timeslot-form'; +import { ExtendedPriceForm } from './extended-price-form'; import { Price } from '../../models/price'; -import PrepaidPackAPI from '../../api/prepaid-pack'; +import PriceAPI from '../../api/price'; import { useTranslation } from 'react-i18next'; import { FabAlert } from '../base/fab-alert'; -interface CreateTimeslotProps { +interface CreateExtendedPriceProps { onSuccess: (message: string) => void, onError: (message: string) => void, groupId: number, @@ -16,9 +16,9 @@ interface CreateTimeslotProps { /** * This component shows a button. - * When clicked, we show a modal dialog handing the process of creating a new time slot + * When clicked, we show a modal dialog handing the process of creating a new extended price */ -export const CreateTimeslot: React.FC = ({ onSuccess, onError, groupId, priceableId, priceableType }) => { +export const CreateExtendedPrice: React.FC = ({ onSuccess, onError, groupId, priceableId, priceableType }) => { const { t } = useTranslation('admin'); const [isOpen, setIsOpen] = useState(false); @@ -31,23 +31,22 @@ export const CreateTimeslot: React.FC = ({ onSuccess, onErr }; /** - * Callback triggered when the user has validated the creation of the new time slot + * Callback triggered when the user has validated the creation of the new extended price */ - const handleSubmit = (timeslot: Price): void => { + const handleSubmit = (extendedPrice: Price): void => { // set the already-known attributes of the new pack - const newTimeslot = Object.assign({} as Price, timeslot); - newTimeslot.group_id = groupId; - newTimeslot.priceable_id = priceableId; - newTimeslot.priceable_type = priceableType; + const newExtendedPrice = Object.assign({} as Price, extendedPrice); + newExtendedPrice.group_id = groupId; + newExtendedPrice.priceable_id = priceableId; + newExtendedPrice.priceable_type = priceableType; // create it on the API - console.log('newTimeslot :', newTimeslot); - // PrepaidPackAPI.create(newPack) - // .then(() => { - // onSuccess(t('app.admin.create_timeslot.timeslot_successfully_created')); - // toggleModal(); - // }) - // .catch(error => onError(error)); + PriceAPI.create(newExtendedPrice) + .then(() => { + onSuccess(t('app.admin.create_extendedPrice.extendedPrice_successfully_created')); + toggleModal(); + }) + .catch(error => onError(error)); }; return ( @@ -55,15 +54,15 @@ export const CreateTimeslot: React.FC = ({ onSuccess, onErr - {t('app.admin.create_timeslot.new_timeslot_info', { TYPE: priceableType })} + {t('app.admin.create_extendedPrice.new_extendedPrice_info', { TYPE: priceableType })} - + ); diff --git a/app/frontend/src/javascript/components/pricing/delete-extended-price.tsx b/app/frontend/src/javascript/components/pricing/delete-extended-price.tsx new file mode 100644 index 000000000..2d1807f99 --- /dev/null +++ b/app/frontend/src/javascript/components/pricing/delete-extended-price.tsx @@ -0,0 +1,56 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FabButton } from '../base/fab-button'; +import { FabModal } from '../base/fab-modal'; +import { Price } from '../../models/price'; +import PriceAPI from '../../api/price'; + +interface DeleteExtendedPriceProps { + onSuccess: (message: string) => void, + onError: (message: string) => void, + price: Price, +} + +/** + * This component shows a button. + * When clicked, we show a modal dialog to ask the user for confirmation about the deletion of the provided extended price. + */ +export const DeleteExtendedPrice: React.FC = ({ onSuccess, onError, price }) => { + const { t } = useTranslation('admin'); + + const [deletionModal, setDeletionModal] = useState(false); + + /** + * Opens/closes the deletion modal + */ + const toggleDeletionModal = (): void => { + setDeletionModal(!deletionModal); + }; + + /** + * The deletion has been confirmed by the user. + * Call the API to trigger the deletion of the temporary set extended price + */ + const onDeleteConfirmed = (): void => { + PriceAPI.destroy(price.id).then(() => { + onSuccess(t('app.admin.delete_extendedPrice.extendedPrice_deleted')); + }).catch((error) => { + onError(t('app.admin.delete_extendedPrice.unable_to_delete') + error); + }); + toggleDeletionModal(); + }; + + return ( +
+ } onClick={toggleDeletionModal} /> + + {t('app.admin.delete_extendedPrice.delete_confirmation')} + +
+ ); +}; diff --git a/app/frontend/src/javascript/components/pricing/edit-extended-price.tsx b/app/frontend/src/javascript/components/pricing/edit-extended-price.tsx new file mode 100644 index 000000000..1324da069 --- /dev/null +++ b/app/frontend/src/javascript/components/pricing/edit-extended-price.tsx @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import { FabModal } from '../base/fab-modal'; +import { ExtendedPriceForm } from './extended-price-form'; +import { Price } from '../../models/price'; +import PriceAPI from '../../api/price'; +import { useTranslation } from 'react-i18next'; +import { FabButton } from '../base/fab-button'; + +interface EditExtendedPriceProps { + price: Price, + onSuccess: (message: string) => void, + onError: (message: string) => void +} + +/** + * This component shows a button. + * When clicked, we show a modal dialog handing the process of creating a new extended price + */ +export const EditExtendedPrice: React.FC = ({ price, onSuccess, onError }) => { + const { t } = useTranslation('admin'); + + const [isOpen, setIsOpen] = useState(false); + const [extendedPriceData, setExtendedPriceData] = useState(price); + + /** + * Open/closes the "edit extended price" modal dialog + */ + const toggleModal = (): void => { + setIsOpen(!isOpen); + }; + + /** + * When the user clicks on the edition button open te edition modal + */ + const handleRequestEdit = (): void => { + toggleModal(); + }; + + /** + * Callback triggered when the user has validated the changes of the extended price + */ + const handleUpdate = (price: Price): void => { + PriceAPI.update(price) + .then(() => { + onSuccess(t('app.admin.edit_extendedPrice.extendedPrice_successfully_updated')); + setExtendedPriceData(price); + toggleModal(); + }) + .catch(error => onError(error)); + }; + + return ( +
+ } onClick={handleRequestEdit} /> + + {extendedPriceData && } + +
+ ); +}; diff --git a/app/frontend/src/javascript/components/pricing/timeslot-form.tsx b/app/frontend/src/javascript/components/pricing/extended-price-form.tsx similarity index 77% rename from app/frontend/src/javascript/components/pricing/timeslot-form.tsx rename to app/frontend/src/javascript/components/pricing/extended-price-form.tsx index bc31fba97..f8bca2487 100644 --- a/app/frontend/src/javascript/components/pricing/timeslot-form.tsx +++ b/app/frontend/src/javascript/components/pricing/extended-price-form.tsx @@ -14,11 +14,11 @@ interface PackFormProps { } /** - * A form component to create/edit a time slot. + * A form component to create/edit a extended price. * The form validation must be created elsewhere, using the attribute form={formId}. */ -export const TimeslotForm: React.FC = ({ formId, onSubmit, price }) => { - const [timeslotData, updateTimeslotData] = useImmer(price || {} as Price); +export const ExtendedPriceForm: React.FC = ({ formId, onSubmit, price }) => { + const [extendedPriceData, updateExtendedPriceData] = useImmer(price || {} as Price); const { t } = useTranslation('admin'); @@ -27,23 +27,23 @@ export const TimeslotForm: React.FC = ({ formId, onSubmit, price */ const handleSubmit = (event: BaseSyntheticEvent): void => { event.preventDefault(); - onSubmit(timeslotData); + onSubmit(extendedPriceData); }; /** - * Callback triggered when the user inputs an amount for the current time slot. + * Callback triggered when the user inputs an amount for the current extended price. */ const handleUpdateAmount = (amount: string) => { - updateTimeslotData(draft => { + updateExtendedPriceData(draft => { draft.amount = parseFloat(amount); }); }; /** - * Callback triggered when the user inputs a number of minutes for the current time slot. + * Callback triggered when the user inputs a number of minutes for the current extended price. */ const handleUpdateHours = (minutes: string) => { - updateTimeslotData(draft => { + updateExtendedPriceData(draft => { draft.duration = parseInt(minutes, 10); }); }; @@ -53,7 +53,7 @@ export const TimeslotForm: React.FC = ({ formId, onSubmit, price = ({ formId, onSubmit, price type="number" step={0.01} min={0} - defaultValue={timeslotData?.amount || ''} + defaultValue={extendedPriceData?.amount || ''} onChange={handleUpdateAmount} icon={} addOn={Fablab.intl_currency} diff --git a/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx b/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx index 5536000dc..b2f6dbc35 100644 --- a/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx @@ -9,7 +9,7 @@ import GroupAPI from '../../api/group'; import { Group } from '../../models/group'; import { IApplication } from '../../models/application'; import { EditablePrice } from './editable-price'; -import { ConfigureTimeslotButton } from './configure-timeslot-button'; +import { ConfigureExtendedPriceButton } from './configure-extended-price-button'; import PriceAPI from '../../api/price'; import { Price } from '../../models/price'; import { useImmer } from 'use-immer'; @@ -108,7 +108,7 @@ const SpacesPricing: React.FC = ({ onError, onSuccess }) => {space.name} {groups?.map(group => {prices && } - 'pricing#index' put 'pricing' => 'pricing#update' - resources :prices, only: %i[create index update] do + resources :prices, only: %i[create index update destroy] do post 'compute', on: :collection end resources :prepaid_packs From 1d38d6ae50d7cfe9fa791e3273be4cfade742c9d Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 21 Dec 2021 18:02:39 +0100 Subject: [PATCH 12/17] fix + new icon --- .../pricing/configure-extended-price-button.tsx | 4 ++-- .../components/pricing/spaces-pricing.tsx | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/frontend/src/javascript/components/pricing/configure-extended-price-button.tsx b/app/frontend/src/javascript/components/pricing/configure-extended-price-button.tsx index 66705ccc0..95f3a9149 100644 --- a/app/frontend/src/javascript/components/pricing/configure-extended-price-button.tsx +++ b/app/frontend/src/javascript/components/pricing/configure-extended-price-button.tsx @@ -41,7 +41,7 @@ export const ConfigureExtendedPriceButton: React.FC { onSuccess(message); PriceAPI.index({ group_id: groupId, priceable_id: priceableId, priceable_type: priceableType }) - .then(data => setExtendedPrices(data)) + .then(data => setExtendedPrices(data.filter(p => p.duration !== 60))) .catch(error => onError(error)); }; @@ -59,7 +59,7 @@ export const ConfigureExtendedPriceButton: React.FC {showList &&
    diff --git a/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx b/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx index b2f6dbc35..3d33cf60d 100644 --- a/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx @@ -8,6 +8,7 @@ import SpaceAPI from '../../api/space'; import GroupAPI from '../../api/group'; import { Group } from '../../models/group'; import { IApplication } from '../../models/application'; +import { Space } from '../../models/space'; import { EditablePrice } from './editable-price'; import { ConfigureExtendedPriceButton } from './configure-extended-price-button'; import PriceAPI from '../../api/price'; @@ -28,9 +29,9 @@ interface SpacesPricingProps { const SpacesPricing: React.FC = ({ onError, onSuccess }) => { const { t } = useTranslation('admin'); - const [spaces, setSpaces] = useState>(null); + const [spaces, setSpaces] = useState>(null); const [groups, setGroups] = useState>(null); - const [prices, updatePrices] = useImmer>(null); + const [prices, updatePrices] = useImmer>([]); // retrieve the initial data useEffect(() => { @@ -62,8 +63,14 @@ const SpacesPricing: React.FC = ({ onError, onSuccess }) => return FormatLib.price(price); }; + /** + * Find the price matching the given criterion + */ + const findPriceBy = (spaceId, groupId): Price => { + return prices.find(price => price.priceable_id === spaceId && price.group_id === groupId); + }; const findPricesBy = (spaceId, groupId): Array => { - return prices.filter(price => price.priceable_id === spaceId && price.group_id === groupId); + return prices.filter(price => price.priceable_id === spaceId && price.group_id === groupId && price.duration !== 60); }; /** @@ -107,7 +114,7 @@ const SpacesPricing: React.FC = ({ onError, onSuccess }) => {spaces?.map(space => {space.name} {groups?.map(group => - {prices && } + {prices && } Date: Tue, 21 Dec 2021 18:19:13 +0100 Subject: [PATCH 13/17] Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 885c8a5f5..601ea3336 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog Fab-manager +## v5.1.14 2021 December 21 + +- Ability to configure prices for spaces by time slots different than the default hourly rate - Updated portuguese translation - Refactored the ReserveButton component to use the same user's data across all the component - [TODO DEPLOY] `rails db:seed` From fc83b4d9c34b5faae75874a4fcd2993a5414e953 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 21 Dec 2021 20:13:55 +0100 Subject: [PATCH 14/17] Create [pricing] sub folders --- .../{ => machines}/configure-packs-button.tsx | 8 +++--- .../pricing/{ => machines}/create-pack.tsx | 8 +++--- .../pricing/{ => machines}/delete-pack.tsx | 10 +++---- .../pricing/{ => machines}/edit-pack.tsx | 8 +++--- .../{ => machines}/machines-pricing.tsx | 28 +++++++++---------- .../pricing/{ => machines}/pack-form.tsx | 6 ++-- .../configure-extended-price-button.tsx | 8 +++--- .../{ => spaces}/create-extended-price.tsx | 8 +++--- .../{ => spaces}/delete-extended-price.tsx | 8 +++--- .../{ => spaces}/edit-extended-price.tsx | 8 +++--- .../{ => spaces}/extended-price-form.tsx | 6 ++-- .../pricing/{ => spaces}/spaces-pricing.tsx | 24 ++++++++-------- 12 files changed, 65 insertions(+), 65 deletions(-) rename app/frontend/src/javascript/components/pricing/{ => machines}/configure-packs-button.tsx (93%) rename app/frontend/src/javascript/components/pricing/{ => machines}/create-pack.tsx (90%) rename app/frontend/src/javascript/components/pricing/{ => machines}/delete-pack.tsx (88%) rename app/frontend/src/javascript/components/pricing/{ => machines}/edit-pack.tsx (90%) rename app/frontend/src/javascript/components/pricing/{ => machines}/machines-pricing.tsx (86%) rename app/frontend/src/javascript/components/pricing/{ => machines}/pack-form.tsx (96%) rename app/frontend/src/javascript/components/pricing/{ => spaces}/configure-extended-price-button.tsx (93%) rename app/frontend/src/javascript/components/pricing/{ => spaces}/create-extended-price.tsx (92%) rename app/frontend/src/javascript/components/pricing/{ => spaces}/delete-extended-price.tsx (90%) rename app/frontend/src/javascript/components/pricing/{ => spaces}/edit-extended-price.tsx (91%) rename app/frontend/src/javascript/components/pricing/{ => spaces}/extended-price-form.tsx (93%) rename app/frontend/src/javascript/components/pricing/{ => spaces}/spaces-pricing.tsx (88%) diff --git a/app/frontend/src/javascript/components/pricing/configure-packs-button.tsx b/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx similarity index 93% rename from app/frontend/src/javascript/components/pricing/configure-packs-button.tsx rename to app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx index fc470f051..ec3cbd2be 100644 --- a/app/frontend/src/javascript/components/pricing/configure-packs-button.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx @@ -1,12 +1,12 @@ import React, { ReactNode, useState } from 'react'; -import { PrepaidPack } from '../../models/prepaid-pack'; +import { PrepaidPack } from '../../../models/prepaid-pack'; import { useTranslation } from 'react-i18next'; -import { FabPopover } from '../base/fab-popover'; +import { FabPopover } from '../../base/fab-popover'; import { CreatePack } from './create-pack'; -import PrepaidPackAPI from '../../api/prepaid-pack'; +import PrepaidPackAPI from '../../../api/prepaid-pack'; import { DeletePack } from './delete-pack'; import { EditPack } from './edit-pack'; -import FormatLib from '../../lib/format'; +import FormatLib from '../../../lib/format'; interface ConfigurePacksButtonProps { packsData: Array, diff --git a/app/frontend/src/javascript/components/pricing/create-pack.tsx b/app/frontend/src/javascript/components/pricing/machines/create-pack.tsx similarity index 90% rename from app/frontend/src/javascript/components/pricing/create-pack.tsx rename to app/frontend/src/javascript/components/pricing/machines/create-pack.tsx index a38683315..4d457bcd3 100644 --- a/app/frontend/src/javascript/components/pricing/create-pack.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/create-pack.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; -import { FabModal } from '../base/fab-modal'; +import { FabModal } from '../../base/fab-modal'; import { PackForm } from './pack-form'; -import { PrepaidPack } from '../../models/prepaid-pack'; -import PrepaidPackAPI from '../../api/prepaid-pack'; +import { PrepaidPack } from '../../../models/prepaid-pack'; +import PrepaidPackAPI from '../../../api/prepaid-pack'; import { useTranslation } from 'react-i18next'; -import { FabAlert } from '../base/fab-alert'; +import { FabAlert } from '../../base/fab-alert'; interface CreatePackProps { onSuccess: (message: string) => void, diff --git a/app/frontend/src/javascript/components/pricing/delete-pack.tsx b/app/frontend/src/javascript/components/pricing/machines/delete-pack.tsx similarity index 88% rename from app/frontend/src/javascript/components/pricing/delete-pack.tsx rename to app/frontend/src/javascript/components/pricing/machines/delete-pack.tsx index 20343629e..bfd8a40a0 100644 --- a/app/frontend/src/javascript/components/pricing/delete-pack.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/delete-pack.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { FabButton } from '../base/fab-button'; -import { FabModal } from '../base/fab-modal'; -import { Loader } from '../base/loader'; -import { PrepaidPack } from '../../models/prepaid-pack'; -import PrepaidPackAPI from '../../api/prepaid-pack'; +import { FabButton } from '../../base/fab-button'; +import { FabModal } from '../../base/fab-modal'; +import { Loader } from '../../base/loader'; +import { PrepaidPack } from '../../../models/prepaid-pack'; +import PrepaidPackAPI from '../../../api/prepaid-pack'; interface DeletePackProps { onSuccess: (message: string) => void, diff --git a/app/frontend/src/javascript/components/pricing/edit-pack.tsx b/app/frontend/src/javascript/components/pricing/machines/edit-pack.tsx similarity index 90% rename from app/frontend/src/javascript/components/pricing/edit-pack.tsx rename to app/frontend/src/javascript/components/pricing/machines/edit-pack.tsx index 6385efbe9..7af5c7507 100644 --- a/app/frontend/src/javascript/components/pricing/edit-pack.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/edit-pack.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; -import { FabModal } from '../base/fab-modal'; +import { FabModal } from '../../base/fab-modal'; import { PackForm } from './pack-form'; -import { PrepaidPack } from '../../models/prepaid-pack'; -import PrepaidPackAPI from '../../api/prepaid-pack'; +import { PrepaidPack } from '../../../models/prepaid-pack'; +import PrepaidPackAPI from '../../../api/prepaid-pack'; import { useTranslation } from 'react-i18next'; -import { FabButton } from '../base/fab-button'; +import { FabButton } from '../../base/fab-button'; interface EditPackProps { pack: PrepaidPack, diff --git a/app/frontend/src/javascript/components/pricing/machines-pricing.tsx b/app/frontend/src/javascript/components/pricing/machines/machines-pricing.tsx similarity index 86% rename from app/frontend/src/javascript/components/pricing/machines-pricing.tsx rename to app/frontend/src/javascript/components/pricing/machines/machines-pricing.tsx index a099b5896..80334aab0 100644 --- a/app/frontend/src/javascript/components/pricing/machines-pricing.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/machines-pricing.tsx @@ -1,22 +1,22 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { react2angular } from 'react2angular'; -import { Loader } from '../base/loader'; -import { FabAlert } from '../base/fab-alert'; -import { HtmlTranslate } from '../base/html-translate'; -import MachineAPI from '../../api/machine'; -import GroupAPI from '../../api/group'; -import { Machine } from '../../models/machine'; -import { Group } from '../../models/group'; -import { IApplication } from '../../models/application'; -import { EditablePrice } from './editable-price'; +import { Loader } from '../../base/loader'; +import { FabAlert } from '../../base/fab-alert'; +import { HtmlTranslate } from '../../base/html-translate'; +import MachineAPI from '../../../api/machine'; +import GroupAPI from '../../../api/group'; +import { Machine } from '../../../models/machine'; +import { Group } from '../../../models/group'; +import { IApplication } from '../../../models/application'; +import { EditablePrice } from '../editable-price'; import { ConfigurePacksButton } from './configure-packs-button'; -import PriceAPI from '../../api/price'; -import { Price } from '../../models/price'; -import PrepaidPackAPI from '../../api/prepaid-pack'; -import { PrepaidPack } from '../../models/prepaid-pack'; +import PriceAPI from '../../../api/price'; +import { Price } from '../../../models/price'; +import PrepaidPackAPI from '../../../api/prepaid-pack'; +import { PrepaidPack } from '../../../models/prepaid-pack'; import { useImmer } from 'use-immer'; -import FormatLib from '../../lib/format'; +import FormatLib from '../../../lib/format'; declare const Application: IApplication; diff --git a/app/frontend/src/javascript/components/pricing/pack-form.tsx b/app/frontend/src/javascript/components/pricing/machines/pack-form.tsx similarity index 96% rename from app/frontend/src/javascript/components/pricing/pack-form.tsx rename to app/frontend/src/javascript/components/pricing/machines/pack-form.tsx index b97257a1f..ba4381c46 100644 --- a/app/frontend/src/javascript/components/pricing/pack-form.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/pack-form.tsx @@ -1,11 +1,11 @@ import React, { BaseSyntheticEvent } from 'react'; import Select from 'react-select'; import Switch from 'react-switch'; -import { PrepaidPack } from '../../models/prepaid-pack'; +import { PrepaidPack } from '../../../models/prepaid-pack'; import { useTranslation } from 'react-i18next'; import { useImmer } from 'use-immer'; -import { FabInput } from '../base/fab-input'; -import { IFablab } from '../../models/fablab'; +import { FabInput } from '../../base/fab-input'; +import { IFablab } from '../../../models/fablab'; declare let Fablab: IFablab; diff --git a/app/frontend/src/javascript/components/pricing/configure-extended-price-button.tsx b/app/frontend/src/javascript/components/pricing/spaces/configure-extended-price-button.tsx similarity index 93% rename from app/frontend/src/javascript/components/pricing/configure-extended-price-button.tsx rename to app/frontend/src/javascript/components/pricing/spaces/configure-extended-price-button.tsx index 95f3a9149..8f6d892fc 100644 --- a/app/frontend/src/javascript/components/pricing/configure-extended-price-button.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/configure-extended-price-button.tsx @@ -1,10 +1,10 @@ import React, { ReactNode, useState } from 'react'; -import { Price } from '../../models/price'; +import { Price } from '../../../models/price'; import { useTranslation } from 'react-i18next'; -import { FabPopover } from '../base/fab-popover'; +import { FabPopover } from '../../base/fab-popover'; import { CreateExtendedPrice } from './create-extended-price'; -import PriceAPI from '../../api/price'; -import FormatLib from '../../lib/format'; +import PriceAPI from '../../../api/price'; +import FormatLib from '../../../lib/format'; import { EditExtendedPrice } from './edit-extended-price'; import { DeleteExtendedPrice } from './delete-extended-price'; diff --git a/app/frontend/src/javascript/components/pricing/create-extended-price.tsx b/app/frontend/src/javascript/components/pricing/spaces/create-extended-price.tsx similarity index 92% rename from app/frontend/src/javascript/components/pricing/create-extended-price.tsx rename to app/frontend/src/javascript/components/pricing/spaces/create-extended-price.tsx index d965fd4e5..12349d4a2 100644 --- a/app/frontend/src/javascript/components/pricing/create-extended-price.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/create-extended-price.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; -import { FabModal } from '../base/fab-modal'; +import { FabModal } from '../../base/fab-modal'; import { ExtendedPriceForm } from './extended-price-form'; -import { Price } from '../../models/price'; -import PriceAPI from '../../api/price'; +import { Price } from '../../../models/price'; +import PriceAPI from '../../../api/price'; import { useTranslation } from 'react-i18next'; -import { FabAlert } from '../base/fab-alert'; +import { FabAlert } from '../../base/fab-alert'; interface CreateExtendedPriceProps { onSuccess: (message: string) => void, diff --git a/app/frontend/src/javascript/components/pricing/delete-extended-price.tsx b/app/frontend/src/javascript/components/pricing/spaces/delete-extended-price.tsx similarity index 90% rename from app/frontend/src/javascript/components/pricing/delete-extended-price.tsx rename to app/frontend/src/javascript/components/pricing/spaces/delete-extended-price.tsx index 2d1807f99..82307709f 100644 --- a/app/frontend/src/javascript/components/pricing/delete-extended-price.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/delete-extended-price.tsx @@ -1,9 +1,9 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { FabButton } from '../base/fab-button'; -import { FabModal } from '../base/fab-modal'; -import { Price } from '../../models/price'; -import PriceAPI from '../../api/price'; +import { FabButton } from '../../base/fab-button'; +import { FabModal } from '../../base/fab-modal'; +import { Price } from '../../../models/price'; +import PriceAPI from '../../../api/price'; interface DeleteExtendedPriceProps { onSuccess: (message: string) => void, diff --git a/app/frontend/src/javascript/components/pricing/edit-extended-price.tsx b/app/frontend/src/javascript/components/pricing/spaces/edit-extended-price.tsx similarity index 91% rename from app/frontend/src/javascript/components/pricing/edit-extended-price.tsx rename to app/frontend/src/javascript/components/pricing/spaces/edit-extended-price.tsx index 1324da069..2edd9abef 100644 --- a/app/frontend/src/javascript/components/pricing/edit-extended-price.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/edit-extended-price.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; -import { FabModal } from '../base/fab-modal'; +import { FabModal } from '../../base/fab-modal'; import { ExtendedPriceForm } from './extended-price-form'; -import { Price } from '../../models/price'; -import PriceAPI from '../../api/price'; +import { Price } from '../../../models/price'; +import PriceAPI from '../../../api/price'; import { useTranslation } from 'react-i18next'; -import { FabButton } from '../base/fab-button'; +import { FabButton } from '../../base/fab-button'; interface EditExtendedPriceProps { price: Price, diff --git a/app/frontend/src/javascript/components/pricing/extended-price-form.tsx b/app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx similarity index 93% rename from app/frontend/src/javascript/components/pricing/extended-price-form.tsx rename to app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx index f8bca2487..39d5789f5 100644 --- a/app/frontend/src/javascript/components/pricing/extended-price-form.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx @@ -1,9 +1,9 @@ import React, { BaseSyntheticEvent } from 'react'; -import { Price } from '../../models/price'; +import { Price } from '../../../models/price'; import { useTranslation } from 'react-i18next'; import { useImmer } from 'use-immer'; -import { FabInput } from '../base/fab-input'; -import { IFablab } from '../../models/fablab'; +import { FabInput } from '../../base/fab-input'; +import { IFablab } from '../../../models/fablab'; declare let Fablab: IFablab; diff --git a/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx b/app/frontend/src/javascript/components/pricing/spaces/spaces-pricing.tsx similarity index 88% rename from app/frontend/src/javascript/components/pricing/spaces-pricing.tsx rename to app/frontend/src/javascript/components/pricing/spaces/spaces-pricing.tsx index 3d33cf60d..45b7e3bac 100644 --- a/app/frontend/src/javascript/components/pricing/spaces-pricing.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/spaces-pricing.tsx @@ -1,20 +1,20 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { react2angular } from 'react2angular'; -import { Loader } from '../base/loader'; -import { FabAlert } from '../base/fab-alert'; -import { HtmlTranslate } from '../base/html-translate'; -import SpaceAPI from '../../api/space'; -import GroupAPI from '../../api/group'; -import { Group } from '../../models/group'; -import { IApplication } from '../../models/application'; -import { Space } from '../../models/space'; -import { EditablePrice } from './editable-price'; +import { Loader } from '../../base/loader'; +import { FabAlert } from '../../base/fab-alert'; +import { HtmlTranslate } from '../../base/html-translate'; +import SpaceAPI from '../../../api/space'; +import GroupAPI from '../../../api/group'; +import { Group } from '../../../models/group'; +import { IApplication } from '../../../models/application'; +import { Space } from '../../../models/space'; +import { EditablePrice } from '../editable-price'; import { ConfigureExtendedPriceButton } from './configure-extended-price-button'; -import PriceAPI from '../../api/price'; -import { Price } from '../../models/price'; +import PriceAPI from '../../../api/price'; +import { Price } from '../../../models/price'; import { useImmer } from 'use-immer'; -import FormatLib from '../../lib/format'; +import FormatLib from '../../../lib/format'; declare const Application: IApplication; From 5be06babd780410adbf9d943b004145b0896d057 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 22 Dec 2021 13:51:26 +0000 Subject: [PATCH 15/17] Apply 1 suggestion(s) to 1 file(s) --- app/frontend/src/javascript/api/space.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend/src/javascript/api/space.ts b/app/frontend/src/javascript/api/space.ts index f2e5b2c23..2f45630c6 100644 --- a/app/frontend/src/javascript/api/space.ts +++ b/app/frontend/src/javascript/api/space.ts @@ -1,7 +1,7 @@ import apiClient from './clients/api-client'; import { AxiosResponse } from 'axios'; -export default class MachineAPI { +export default class SpaceAPI { static async index (filters?: boolean): Promise> { const res: AxiosResponse> = await apiClient.get(`/api/spaces${this.filtersToQuery(filters)}`); return res?.data; From b864ba66da228fc0a5095aec02fedcf54b29e429 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 22 Dec 2021 14:07:48 +0000 Subject: [PATCH 16/17] Apply 22 suggestion(s) to 6 file(s) --- app/frontend/src/javascript/api/space.ts | 14 +++++--------- .../spaces/configure-extended-price-button.tsx | 6 +++--- .../pricing/spaces/create-extended-price.tsx | 8 ++++---- .../pricing/spaces/extended-price-form.tsx | 2 +- .../components/pricing/spaces/spaces-pricing.tsx | 14 +++++++++----- config/locales/app.admin.en.yml | 6 +++++- 6 files changed, 27 insertions(+), 23 deletions(-) diff --git a/app/frontend/src/javascript/api/space.ts b/app/frontend/src/javascript/api/space.ts index 2f45630c6..6f1d9c9b9 100644 --- a/app/frontend/src/javascript/api/space.ts +++ b/app/frontend/src/javascript/api/space.ts @@ -1,20 +1,16 @@ import apiClient from './clients/api-client'; import { AxiosResponse } from 'axios'; +import { Space } from '../models/space'; export default class SpaceAPI { - static async index (filters?: boolean): Promise> { - const res: AxiosResponse> = await apiClient.get(`/api/spaces${this.filtersToQuery(filters)}`); + static async index (): Promise> { + const res: AxiosResponse> = await apiClient.get('/api/spaces'); return res?.data; } - static async get (id: number): Promise { - const res: AxiosResponse = await apiClient.get(`/api/spaces/${id}`); + static async get (id: number): Promise { + const res: AxiosResponse = await apiClient.get(`/api/spaces/${id}`); return res?.data; } - private static filtersToQuery (filters?: boolean): string { - if (!filters) return ''; - - return '?' + Object.entries(filters).map(f => `${f[0]}=${f[1]}`).join('&'); - } } diff --git a/app/frontend/src/javascript/components/pricing/spaces/configure-extended-price-button.tsx b/app/frontend/src/javascript/components/pricing/spaces/configure-extended-price-button.tsx index 8f6d892fc..80c93ba1e 100644 --- a/app/frontend/src/javascript/components/pricing/spaces/configure-extended-price-button.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/configure-extended-price-button.tsx @@ -19,7 +19,7 @@ interface ConfigureExtendedPriceButtonProps { /** * This component is a button that shows the list of extendedPrices. - * It also triggers modal dialogs to configure (add/delete/edit/remove) extendedPrices. + * It also triggers modal dialogs to configure (add/edit/remove) extendedPrices. */ export const ConfigureExtendedPriceButton: React.FC = ({ prices, onError, onSuccess, groupId, priceableId, priceableType }) => { const { t } = useTranslation('admin'); @@ -46,7 +46,7 @@ export const ConfigureExtendedPriceButton: React.FC { return +
    diff --git a/app/frontend/src/javascript/components/pricing/spaces/create-extended-price.tsx b/app/frontend/src/javascript/components/pricing/spaces/create-extended-price.tsx index 12349d4a2..c01be253a 100644 --- a/app/frontend/src/javascript/components/pricing/spaces/create-extended-price.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/create-extended-price.tsx @@ -24,7 +24,7 @@ export const CreateExtendedPrice: React.FC = ({ onSucc const [isOpen, setIsOpen] = useState(false); /** - * Open/closes the "new pack" modal dialog + * Open/closes the "new extended price" modal dialog */ const toggleModal = (): void => { setIsOpen(!isOpen); @@ -34,7 +34,7 @@ export const CreateExtendedPrice: React.FC = ({ onSucc * Callback triggered when the user has validated the creation of the new extended price */ const handleSubmit = (extendedPrice: Price): void => { - // set the already-known attributes of the new pack + // set the already-known attributes of the new extended price const newExtendedPrice = Object.assign({} as Price, extendedPrice); newExtendedPrice.group_id = groupId; newExtendedPrice.priceable_id = priceableId; @@ -58,11 +58,11 @@ export const CreateExtendedPrice: React.FC = ({ onSucc className="new-pack-modal" closeButton confirmButton={t('app.admin.create_extendedPrice.create_extendedPrice')} - onConfirmSendFormId="new-pack"> + onConfirmSendFormId="new-extended-price"> {t('app.admin.create_extendedPrice.new_extendedPrice_info', { TYPE: priceableType })} - +
    ); diff --git a/app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx b/app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx index 39d5789f5..34e8e354c 100644 --- a/app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx @@ -59,7 +59,7 @@ export const ExtendedPriceForm: React.FC = ({ formId, onSubmit, p min={1} icon={} required /> - + = ({ onError, onSuccess }) => }; /** - * Find the price matching the given criterion + * Find the default price (hourly rate) matching the given criterion */ const findPriceBy = (spaceId, groupId): Price => { - return prices.find(price => price.priceable_id === spaceId && price.group_id === groupId); + return prices.find(price => price.priceable_id === spaceId && price.group_id === groupId && price.duration == 60); }; - const findPricesBy = (spaceId, groupId): Array => { + + /** + * Find prices matching the given criterion, except the default hourly rate + */ + const findExtendedPricesBy = (spaceId, groupId): Array => { return prices.filter(price => price.priceable_id === spaceId && price.group_id === groupId && price.duration !== 60); }; @@ -90,7 +94,7 @@ const SpacesPricing: React.FC = ({ onError, onSuccess }) => const handleUpdatePrice = (price: Price): void => { PriceAPI.update(price) .then(() => { - onSuccess(t('app.admin.machines_pricing.price_updated')); + onSuccess(t('app.admin.spaces_pricing.price_updated')); updatePrice(price); }) .catch(error => onError(error)); @@ -116,7 +120,7 @@ const SpacesPricing: React.FC = ({ onError, onSuccess }) => {groups?.map(group => {prices && } without subscription." prices_calculated_on_hourly_rate_html: "All the prices will be automatically calculated based on the hourly rate defined here.
    For example, if you define an hourly rate at {RATE}: a slot of {DURATION} minutes, will be charged {PRICE}." @@ -381,6 +383,8 @@ en: configure_extendedPrices_button: extendedPrices: "Extended prices" no_extendedPrices: "No extended price for now" + extended_prices_form: + amount: "Price" pack_form: hours: "Hours" amount: "Price" @@ -409,7 +413,7 @@ en: pack_successfully_updated: "The prepaid pack was successfully updated." create_extendedPrice: new_extendedPrice: "New extended price" - new_extendedPrice_info: "..." + new_extendedPrice_info: "Extended prices allows you to define prices based on custom durations, intead on the default hourly rates." create_extendedPrice: "Create extended price" extendedPrice_successfully_created: "The new extended price was successfully created." delete_extendedPrice: From c394b3a2757493044e1589decd96fea90a33641a Mon Sep 17 00:00:00 2001 From: vincent Date: Wed, 22 Dec 2021 17:25:02 +0100 Subject: [PATCH 17/17] Fix classes names --- app/frontend/src/javascript/api/space.ts | 1 - .../pricing/machines/configure-packs-button.tsx | 6 +++--- .../components/pricing/machines/delete-pack.tsx | 4 ++-- .../components/pricing/machines/edit-pack.tsx | 9 ++++----- .../components/pricing/machines/machines-pricing.tsx | 2 +- .../components/pricing/machines/pack-form.tsx | 2 +- .../pricing/spaces/configure-extended-price-button.tsx | 8 ++++---- .../pricing/spaces/delete-extended-price.tsx | 4 ++-- .../components/pricing/spaces/edit-extended-price.tsx | 9 ++++----- .../components/pricing/spaces/extended-price-form.tsx | 6 +++--- .../components/pricing/spaces/spaces-pricing.tsx | 8 ++++---- app/frontend/src/stylesheets/application.scss | 10 +++++----- ...e-packs-button.scss => configure-group-button.scss} | 6 +++--- .../pricing/{delete-pack.scss => delete-group.scss} | 4 ++-- .../pricing/{edit-pack.scss => edit-group.scss} | 2 +- .../pricing/{pack-form.scss => group-form.scss} | 2 +- .../{machines-pricing.scss => pricing-list.scss} | 4 ++-- 17 files changed, 42 insertions(+), 45 deletions(-) rename app/frontend/src/stylesheets/modules/pricing/{configure-packs-button.scss => configure-group-button.scss} (93%) rename app/frontend/src/stylesheets/modules/pricing/{delete-pack.scss => delete-group.scss} (65%) rename app/frontend/src/stylesheets/modules/pricing/{edit-pack.scss => edit-group.scss} (65%) rename app/frontend/src/stylesheets/modules/pricing/{pack-form.scss => group-form.scss} (89%) rename app/frontend/src/stylesheets/modules/pricing/{machines-pricing.scss => pricing-list.scss} (95%) diff --git a/app/frontend/src/javascript/api/space.ts b/app/frontend/src/javascript/api/space.ts index 6f1d9c9b9..5633bd658 100644 --- a/app/frontend/src/javascript/api/space.ts +++ b/app/frontend/src/javascript/api/space.ts @@ -12,5 +12,4 @@ export default class SpaceAPI { const res: AxiosResponse = await apiClient.get(`/api/spaces/${id}`); return res?.data; } - } diff --git a/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx b/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx index ec3cbd2be..c5b1ff43c 100644 --- a/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx @@ -64,8 +64,8 @@ export const ConfigurePacksButton: React.FC = ({ pack }; return ( -
    - {showList && @@ -73,7 +73,7 @@ export const ConfigurePacksButton: React.FC = ({ pack {packs?.map(p =>
  • {formatDuration(p.minutes)} - {FormatLib.price(p.amount)} - + diff --git a/app/frontend/src/javascript/components/pricing/machines/delete-pack.tsx b/app/frontend/src/javascript/components/pricing/machines/delete-pack.tsx index bfd8a40a0..dc1d01089 100644 --- a/app/frontend/src/javascript/components/pricing/machines/delete-pack.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/delete-pack.tsx @@ -42,8 +42,8 @@ const DeletePackComponent: React.FC = ({ onSuccess, onError, pa }; return ( -
    - } onClick={toggleDeletionModal} /> +
    + } onClick={toggleDeletionModal} /> = ({ pack, onSuccess, onError }) }; return ( -
    - } onClick={handleRequestEdit} /> +
    + } onClick={handleRequestEdit} /> - {packData && } + onConfirmSendFormId="edit-group"> + {packData && }
    ); diff --git a/app/frontend/src/javascript/components/pricing/machines/machines-pricing.tsx b/app/frontend/src/javascript/components/pricing/machines/machines-pricing.tsx index 80334aab0..5b3618b7d 100644 --- a/app/frontend/src/javascript/components/pricing/machines/machines-pricing.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/machines-pricing.tsx @@ -107,7 +107,7 @@ const MachinesPricing: React.FC = ({ onError, onSuccess }) }; return ( -
    +

    diff --git a/app/frontend/src/javascript/components/pricing/machines/pack-form.tsx b/app/frontend/src/javascript/components/pricing/machines/pack-form.tsx index ba4381c46..831e0d899 100644 --- a/app/frontend/src/javascript/components/pricing/machines/pack-form.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/pack-form.tsx @@ -103,7 +103,7 @@ export const PackForm: React.FC = ({ formId, onSubmit, pack }) => }; return ( -
    + (false); /** - * Open/closes the popover listing the existing packs + * Open/closes the popover listing the existing extended prices */ const toggleShowList = (): void => { setShowList(!showList); @@ -57,8 +57,8 @@ export const ConfigureExtendedPriceButton: React.FC - {showList && @@ -66,7 +66,7 @@ export const ConfigureExtendedPriceButton: React.FC
  • {extendedPrice.duration} {t('app.admin.calendar.minutes')} - {FormatLib.price(extendedPrice.amount)} - + diff --git a/app/frontend/src/javascript/components/pricing/spaces/delete-extended-price.tsx b/app/frontend/src/javascript/components/pricing/spaces/delete-extended-price.tsx index 82307709f..56af8784e 100644 --- a/app/frontend/src/javascript/components/pricing/spaces/delete-extended-price.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/delete-extended-price.tsx @@ -41,8 +41,8 @@ export const DeleteExtendedPrice: React.FC = ({ onSucc }; return ( -
    - } onClick={toggleDeletionModal} /> +
    + } onClick={toggleDeletionModal} /> = ({ price, onS }; return ( -
    - } onClick={handleRequestEdit} /> +
    + } onClick={handleRequestEdit} /> - {extendedPriceData && } + onConfirmSendFormId="edit-group"> + {extendedPriceData && }
    ); diff --git a/app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx b/app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx index 34e8e354c..31445ee81 100644 --- a/app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx @@ -7,7 +7,7 @@ import { IFablab } from '../../../models/fablab'; declare let Fablab: IFablab; -interface PackFormProps { +interface ExtendedPriceFormProps { formId: string, onSubmit: (pack: Price) => void, price?: Price, @@ -17,7 +17,7 @@ interface PackFormProps { * A form component to create/edit a extended price. * The form validation must be created elsewhere, using the attribute form={formId}. */ -export const ExtendedPriceForm: React.FC = ({ formId, onSubmit, price }) => { +export const ExtendedPriceForm: React.FC = ({ formId, onSubmit, price }) => { const [extendedPriceData, updateExtendedPriceData] = useImmer(price || {} as Price); const { t } = useTranslation('admin'); @@ -49,7 +49,7 @@ export const ExtendedPriceForm: React.FC = ({ formId, onSubmit, p }; return ( - + = ({ onError, onSuccess }) => // retrieve the initial data useEffect(() => { - SpaceAPI.index(false) + SpaceAPI.index() .then(data => setSpaces(data)) .catch(error => onError(error)); GroupAPI.index({ disabled: false, admins: false }) @@ -67,7 +67,7 @@ const SpacesPricing: React.FC = ({ onError, onSuccess }) => * Find the default price (hourly rate) matching the given criterion */ const findPriceBy = (spaceId, groupId): Price => { - return prices.find(price => price.priceable_id === spaceId && price.group_id === groupId && price.duration == 60); + return prices.find(price => price.priceable_id === spaceId && price.group_id === groupId && price.duration === 60); }; /** @@ -101,7 +101,7 @@ const SpacesPricing: React.FC = ({ onError, onSuccess }) => }; return ( -
    +

    @@ -118,7 +118,7 @@ const SpacesPricing: React.FC = ({ onError, onSuccess }) => {spaces?.map(space => {space.name} {groups?.map(group => - {prices && } + {prices.length && }