1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-17 06:52:27 +01:00

[manager] manage agenda + book machines for himself&others

This commit is contained in:
Sylvain 2020-04-27 12:12:29 +02:00
parent c45c92e86a
commit 5312c13d3f
15 changed files with 89 additions and 63 deletions

View File

@ -604,16 +604,18 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat
angular.forEach($scope.events.reserved, function (machineSlot, key) {
machineSlot.is_reserved = true;
machineSlot.can_modify = true;
if ($scope.currentUser.role !== 'admin') {
machineSlot.title = _t('app.logged.machines_reserve.i_ve_reserved');
machineSlot.borderColor = BOOKED_SLOT_BORDER_COLOR;
updateMachineSlot(machineSlot, reservation, $scope.currentUser);
} else {
if ($scope.currentUser.role === 'admin' || ($scope.currentUser.role === 'manager' && reservation.user_id !== $scope.currentUser.id)) {
// an admin or a manager booked for someone else
machineSlot.title = _t('app.logged.machines_reserve.not_available');
machineSlot.borderColor = UNAVAILABLE_SLOT_BORDER_COLOR;
updateMachineSlot(machineSlot, reservation, $scope.ctrl.member);
} else {
// booked for "myself"
machineSlot.title = _t('app.logged.machines_reserve.i_ve_reserved');
machineSlot.borderColor = BOOKED_SLOT_BORDER_COLOR;
updateMachineSlot(machineSlot, reservation, $scope.currentUser);
}
return machineSlot.backgroundColor = 'white';
machineSlot.backgroundColor = 'white';
});
if ($scope.selectedPlan) {

View File

@ -10,8 +10,8 @@
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', 'growl', 'Auth', 'Price', 'Wallet', 'CustomAsset', 'Slot', 'helpers', '_t', '$uibModal',
function ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, Slot, helpers, _t, $uibModal) {
Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', 'growl', 'Auth', 'Price', 'Wallet', 'CustomAsset', 'Slot', 'AuthService', 'helpers', '_t',
function ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, Slot, AuthService, helpers, _t) {
return ({
restrict: 'E',
scope: {
@ -167,7 +167,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
// first, we check that a user was selected
if (Object.keys($scope.user).length > 0) {
// check user was selected a plan if slot is restricted for subscriptions
// check selected user has a subscription, if any slot is restricted for subscriptions
const slotValidations = [];
let slotNotValid;
let slotNotValidError;
@ -195,7 +195,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
});
const hasPlanForSlot = slotValidations.every(function (a) { return a; });
if (!hasPlanForSlot) {
if (!$scope.isAdmin()) {
if (!AuthService.isAuthorized(['admin', 'manager'])) {
return growl.error(_t('app.shared.cart.slot_restrict_subscriptions_must_select_plan'));
} else {
const modalInstance = $uibModal.open({
@ -216,7 +216,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
return paySlots();
}
} else {
// otherwise we alert, this error musn't occur when the current user is not admin
// otherwise we alert, this error musn't occur when the current user is not admin or manager
return growl.error(_t('app.shared.cart.please_select_a_member_first'));
}
};
@ -285,12 +285,6 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
return false;
};
/**
* Check if the currently logged user has teh 'admin' role?
* @returns {boolean}
*/
$scope.isAdmin = function () { return $rootScope.currentUser && ($rootScope.currentUser.role === 'admin'); };
/* PRIVATE SCOPE */
/**
@ -325,11 +319,13 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
/**
* Callback triggered when the selected slot changed
*/
var slotSelectionChanged = function () {
const slotSelectionChanged = function () {
if ($scope.slot) {
// build a list of plans if this slot is restricted for subscriptions
// if this slot is restricted for subscribers...
if ($scope.slot.plan_ids.length > 0) {
// ... we select all the plans matching these restrictions...
const _plans = _.filter($scope.plans, function (p) { return _.include($scope.slot.plan_ids, p.id) });
// ... and we group these plans, by Group...
$scope.slot.plansGrouped = [];
$scope.slot.group_ids = [];
for (let group of Array.from($scope.groups)) {
@ -338,7 +334,9 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
if (plan.group_id === group.id) { groupObj.plans.push(plan); }
}
if (groupObj.plans.length > 0) {
if ($scope.isAdmin()) {
// ... Finally, we only keep the plans matching the group of the current user
// OR all plans if the current user is admin or manager
if (AuthService.isAuthorized(['admin', 'manager'])) {
$scope.slot.plansGrouped.push(groupObj);
} else if ($scope.user.group_id === groupObj.id) {
$scope.slot.plansGrouped.push(groupObj);
@ -398,7 +396,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
}
}
, function (type) {
// the user has choosen an action, so we proceed
// the user has chosen an action, so we proceed
if (type === 'move') {
if (typeof $scope.onSlotStartToModify === 'function') { $scope.onSlotStartToModify(); }
return $scope.events.modifiable = $scope.slot;
@ -433,7 +431,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
/**
* Reset the parameters that may lead to a wrong price but leave the content (events added to cart)
*/
var resetCartState = function () {
const resetCartState = function () {
$scope.selectedPlan = null;
$scope.coupon.applied = null;
$scope.events.moved = null;
@ -446,8 +444,8 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
* Determines if the provided booked slot is able to be modified by the user.
* @param slot {Object} fullCalendar event object
*/
var slotCanBeModified = function (slot) {
if ($scope.isAdmin()) { return true; }
const slotCanBeModified = function (slot) {
if (AuthService.isAuthorized(['admin', 'manager'])) { return true; }
const slotStart = moment(slot.start);
const now = moment();
return (slot.can_modify && $scope.enableBookingMove && (slotStart.diff(now, 'hours') >= $scope.moveBookingDelay));
@ -457,8 +455,8 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
* Determines if the provided booked slot is able to be canceled by the user.
* @param slot {Object} fullCalendar event object
*/
var slotCanBeCanceled = function (slot) {
if ($scope.isAdmin()) { return true; }
const slotCanBeCanceled = function (slot) {
if (AuthService.isAuthorized(['admin', 'manager'])) { return true; }
const slotStart = moment(slot.start);
const now = moment();
return (slot.can_modify && $scope.enableBookingCancel && (slotStart.diff(now, 'hours') >= $scope.cancelBookingDelay));
@ -467,7 +465,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
/**
* Callback triggered when the selected slot changed
*/
var planSelectionChanged = function () {
const planSelectionChanged = function () {
if (Auth.isAuthenticated()) {
if ($scope.selectedPlan !== $scope.plan) {
$scope.selectedPlan = $scope.plan;
@ -486,7 +484,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
/**
* Update the total price of the current selection/reservation
*/
var updateCartPrice = function () {
const updateCartPrice = function () {
if (Object.keys($scope.user).length > 0) {
const r = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan);
return Price.compute(mkRequestParams(r, $scope.coupon.applied), function (res) {
@ -501,7 +499,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
}
};
var setSlotsDetails = function (details) {
const setSlotsDetails = function (details) {
angular.forEach($scope.events.reserved, function (slot) {
angular.forEach(details.slots, function (s) {
if (moment(s.start_at).isSame(slot.start)) {
@ -518,7 +516,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
* @param coupon {Object} Coupon as returned from the API
* @return {{reservation:Object, coupon_code:string}}
*/
var mkRequestParams = function (reservation, coupon) {
const mkRequestParams = function (reservation, coupon) {
return {
reservation,
coupon_code: ((coupon ? coupon.code : undefined))
@ -532,7 +530,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
* @param [plan] {Object} Plan as retrieved from the API: plan to buy with the current reservation
* @return {{user_id:Number, reservable_id:Number, reservable_type:String, slots_attributes:Array<Object>, plan_id:Number|null}}
*/
var mkReservation = function (member, slots, plan) {
const mkReservation = function (member, slots, plan) {
const reservation = {
user_id: member.id,
reservable_id: $scope.reservableId,
@ -555,7 +553,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
/**
* Open a modal window that allows the user to process a credit card payment for his current shopping cart.
*/
var payByStripe = function (reservation) {
const payByStripe = function (reservation) {
$uibModal.open({
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>',
size: 'md',
@ -612,7 +610,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
/**
* Open a modal window that allows the user to process a local payment for his current shopping cart (admin only).
*/
var payOnSite = function (reservation) {
const payOnSite = function (reservation) {
$uibModal.open({
templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>',
size: 'sm',
@ -681,7 +679,7 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
/**
* Actions to run after the payment was successful
*/
var afterPayment = function (reservation) {
const afterPayment = function (reservation) {
// we set the cart content as 'paid' to display a summary of the transaction
$scope.events.paid = $scope.events.reserved;
$scope.amountPaid = $scope.amountTotal;
@ -697,19 +695,22 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
/**
* Actions to pay slots
*/
var paySlots = function() {
const paySlots = function() {
const reservation = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan);
return Wallet.getWalletByUser({ user_id: $scope.user.id }, function (wallet) {
const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount);
if (!$scope.isAdmin() && (amountToPay > 0)) {
if ((AuthService.isAuthorized(['member']) && amountToPay > 0)
|| (AuthService.isAuthorized('manager') && $scope.user.id === $rootScope.currentUser.id && amountToPay > 0)) {
if ($rootScope.fablabWithoutOnlinePayment) {
growl.error(_t('app.shared.cart.online_payment_disabled'));
} else {
return payByStripe(reservation);
}
} else {
if ($scope.isAdmin() || (amountToPay === 0)) {
if (AuthService.isAuthorized(['admin'])
|| (AuthService.isAuthorized('manager') && $scope.user.id !== $rootScope.currentUser.id)
|| amountToPay === 0) {
return payOnSite(reservation);
}
}

View File

@ -45,7 +45,7 @@
</div>
<div class="col-sm-12 col-md-12 col-lg-3">
<div class="m text-center">
<div class="m text-center" ng-show="AuthService.isAuthorized('admin')">
<a class="btn btn-default export-xls-button"
ng-href="api/availabilities/export_index.xlsx"
target="export-frame"

View File

@ -25,7 +25,7 @@
<div class="col-sm-12 col-md-12 col-lg-3">
<div ng-if="currentUser.role === 'admin'">
<div ng-if="isAuthorized(['admin', 'manager'])">
<select-member></select-member>
</div>

View File

@ -16,7 +16,7 @@
<div class="panel-body">
<div class="font-sbold text-u-c">{{ 'app.shared.cart.datetime_to_time' | translate:{START_DATETIME:(slot.start | amDateFormat:'LLLL'), END_TIME:(slot.end | amDateFormat:'LT') } }}</div>
<div class="text-base">{{ 'app.shared.cart.cost_of_TYPE' | translate:{TYPE:reservableType} }} <span ng-class="{'text-blue': !slot.promo, 'red': slot.promo}">{{slot.price | currency}}</span></div>
<div ng-show="isAdmin()" class="m-t">
<div ng-show="isAuthorized(['admin', 'manager'])" class="m-t">
<label for="offerSlot" class="control-label m-r" translate>{{ 'app.shared.cart.offer_this_slot' }}</label>
<input bs-switch
ng-model="slot.offered"

View File

@ -25,12 +25,13 @@ class API::ReservationsController < API::ApiController
def show; end
# Admins can create any reservations. Members can directly create reservations if total = 0,
# otherwise, they must use payments_controller#confirm_payment
# otherwise, they must use payments_controller#confirm_payment.
# Managers can create reservations for other users
def create
user_id = current_user.admin? ? params[:reservation][:user_id] : current_user.id
user_id = current_user.admin? || current_user.manager? ? params[:reservation][:user_id] : current_user.id
amount = transaction_amount(current_user.admin?, user_id)
authorize ReservationContext.new(Reservation, amount)
authorize ReservationContext.new(Reservation, amount, user_id)
@reservation = Reservation.new(reservation_params)
is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id)

View File

@ -112,7 +112,7 @@ class User < ApplicationRecord
end
def training_machine?(machine)
return true if admin?
return true if admin? || manager?
trainings.map(&:machines).flatten.uniq.include?(machine)
end
@ -139,6 +139,14 @@ class User < ApplicationRecord
has_role? :member
end
def manager?
has_role? :manager
end
def partner?
has_role? :partner
end
def all_projects
my_projects.to_a.concat projects
end

View File

@ -1,7 +1,10 @@
# frozen_string_literal: true
# Check the access policies for API::AvailabilitiesController
class AvailabilityPolicy < ApplicationPolicy
%w(index? show? create? update? destroy? reservations? export? lock?).each do |action|
%w[index? show? create? update? destroy? reservations? export? lock?].each do |action|
define_method action do
user.admin?
user.admin? || user.manager?
end
end
end

View File

@ -3,10 +3,10 @@
# Check the access policies for API::ICalendarController
class ICalendarPolicy < ApplicationPolicy
def create?
user.admin?
user.admin? || user.manager?
end
def destroy?
user.admin?
user.admin? || user.manager?
end
end

View File

@ -2,11 +2,12 @@
# Pundit Additional context to validate the price of a reservation
class ReservationContext
attr_reader :reservation, :price
attr_reader :reservation, :price, :user_id
def initialize(reservation, price)
def initialize(reservation, price, user_id)
@reservation = reservation
@price = price
@user_id = user_id
end
def policy_class

View File

@ -3,10 +3,10 @@
# Check the access policies for API::ReservationsController
class ReservationPolicy < ApplicationPolicy
def create?
user.admin? || record.price.zero?
user.admin? || (user.manager? && record.user_id != user.id) || record.price.zero?
end
def update?
user.admin? || record.user == user
user.admin? || user.manager? || record.user == user
end
end

View File

@ -1,15 +1,18 @@
# frozen_string_literal: true
# Check the access policies for API::SlotsController
class SlotPolicy < ApplicationPolicy
def update?
# check that the update is allowed and the prevention delay has not expired
delay = Setting.find_by( name: 'booking_move_delay').value.to_i
enabled = (Setting.find_by( name: 'booking_move_enable').value == 'true')
delay = Setting.find_by(name: 'booking_move_delay').value.to_i
enabled = (Setting.find_by(name: 'booking_move_enable').value == 'true')
# these condition does not apply to admins
user.admin? or
(record.reservation.user == user and enabled and ((record.start_at - DateTime.current).to_i / 3600 >= delay))
user.admin? || user.manager? ||
(record.reservation.user == user && enabled && ((record.start_at - DateTime.current).to_i / 3600 >= delay))
end
def cancel?
user.admin? or record.reservation.user == user
user.admin? || user.manager? || record.reservation.user == user
end
end

View File

@ -1,10 +1,13 @@
# frozen_string_literal: true
# Check the access policies for API::WalletController
class WalletPolicy < ApplicationPolicy
def by_user?
user.admin? or user == record.user
user.admin? || user.manager? || user == record.user
end
def transactions?
user.admin? or user == record.user
user.admin? || user == record.user
end
def credit?

View File

@ -50,7 +50,7 @@ class Members::ListService
'SELECT max("created_at") ' \
'FROM "subscriptions" ' \
'WHERE "statistic_profile_id" = "statistic_profiles"."id")')
.where("users.is_active = 'true' AND roles.name = 'member'")
.where("users.is_active = 'true' AND (roles.name = 'member' OR roles.name = 'manager')")
.limit(50)
query.downcase.split(' ').each do |word|
members = members.where('lower(f_unaccent(profiles.first_name)) ~ :search OR ' \

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
json.extract! @machine, :id, :name, :description, :spec, :disabled, :created_at, :updated_at, :slug
json.machine_image @machine.machine_image.attachment.large.url if @machine.machine_image
json.machine_files_attributes @machine.machine_files do |f|
@ -7,9 +9,11 @@ json.machine_files_attributes @machine.machine_files do |f|
end
json.trainings @machine.trainings.each, :id, :name, :disabled
json.current_user_is_training current_user.training_machine?(@machine) if current_user
json.current_user_training_reservation do
json.partial! 'api/reservations/reservation', reservation: current_user.training_reservation_by_machine(@machine)
end if current_user and !current_user.training_machine?(@machine) and current_user.training_reservation_by_machine(@machine)
if current_user && !current_user.training_machine?(@machine) && current_user.training_reservation_by_machine(@machine)
json.current_user_training_reservation do
json.partial! 'api/reservations/reservation', reservation: current_user.training_reservation_by_machine(@machine)
end
end
json.machine_projects @machine.projects.published.last(10) do |p|
json.id p.id