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

Interface to manage partners

This commit is contained in:
Sylvain 2020-04-21 16:47:35 +02:00
parent d717ed704c
commit f88472eeb3
13 changed files with 251 additions and 23 deletions

View File

@ -1,5 +1,6 @@
# Changelog Fab-manager
- Interface to manage partners
- Ability to define, per availability, a custom duration for the reservation slots
## v4.3.4 2020 April 14

View File

@ -126,8 +126,8 @@ class MembersController {
/**
* Controller used in the members/groups management page
*/
Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member', 'Export', 'uiTourService',
function ($scope, $sce, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member, Export, uiTourService) {
Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', '$uibModal', 'membersPromise', 'adminsPromise', 'partnersPromise', 'managersPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member', 'Export', 'User', 'uiTourService',
function ($scope, $sce, $uibModal, membersPromise, adminsPromise, partnersPromise, managersPromise, growl, Admin, dialogs, _t, Member, Export, User, uiTourService) {
/* PRIVATE STATIC CONSTANTS */
// number of users loaded each time we click on 'load more...'
@ -163,8 +163,20 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
// Admins ordering/sorting. Default: not sorted
$scope.orderAdmin = null;
// partners list
$scope.partners = partnersPromise.users;
// Partners ordering/sorting. Default: not sorted
$scope.orderPartner = null;
// managers list
$scope.managers = managersPromise.users;
// Managers ordering/sorting. Default: not sorted
$scope.orderManager = null;
// default tab: members list
$scope.tabs = { active: 0 };
$scope.tabs = { active: 0, sub: 0 };
/**
* Change the members ordering criterion to the one provided
@ -193,6 +205,55 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
}
};
/**
* Change the partners ordering criterion to the one provided
* @param orderPartner {string} ordering criterion
*/
$scope.setOrderPartner = function (orderPartner) {
if ($scope.orderPartner === orderPartner) {
return $scope.orderPartner = `-${orderPartner}`;
} else {
return $scope.orderPartner = orderPartner;
}
};
/**
* Open a modal dialog allowing the admin to create a new partner user
*/
$scope.openPartnerNewModal = function () {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '<%= asset_path "shared/_partner_new_modal.html" %>',
size: 'lg',
controller: ['$scope', '$uibModalInstance', 'User', function ($scope, $uibModalInstance, User) {
$scope.partner = {};
$scope.ok = function () {
User.save(
{},
{ user: $scope.partner },
function (user) {
$scope.partner.id = user.id;
$scope.partner.name = `${user.first_name} ${user.last_name}`;
$uibModalInstance.close($scope.partner);
},
function (error) {
growl.error(_t('app.admin.plans.new.unable_to_save_this_user_check_that_there_isnt_an_already_a_user_with_the_same_name'));
console.error(error);
}
);
};
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}]
});
// once the form was validated successfully ...
return modalInstance.result.then(function (partner) {
$scope.partners.push(partner);
});
};
/**
* Ask for confirmation then delete the specified user
* @param memberId {number} identifier of the user to delete
@ -252,6 +313,36 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
);
};
/**
* Ask for confirmation then delete the specified partner
* @param partners {Array} full list of partners
* @param partner {Object} partner to delete
*/
$scope.destroyPartner = function (partners, partner) {
dialogs.confirm(
{
resolve: {
object () {
return {
title: _t('app.admin.members.confirmation_required'),
msg: $sce.trustAsHtml(_t('app.admin.members.delete_this_partner') + '<br/><br/>' + _t('app.admin.members.this_may_take_a_while_please_wait'))
};
}
}
},
function () { // cancel confirmed
User.delete(
{ id: partner.id },
function () {
partners.splice(findItemIdxById(partners, partner.id), 1);
return growl.success(_t('app.admin.members.partner_successfully_deleted'));
},
function (error) { growl.error(_t('app.admin.members.unable_to_delete_the_partner')); }
);
}
);
}
/**
* Callback for the 'load more' button.
* Will load the next results of the current search, if any
@ -405,18 +496,20 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
uitour.on('stepChanged', function (nextStep) {
if (nextStep.stepId === 'list' || nextStep.stepId === 'import') {
$scope.tabs.active = 0;
$scope.tabs.sub = 0;
}
if (nextStep.stepId === 'admins') {
$scope.tabs.active = 1;
$scope.tabs.active = 0;
$scope.tabs.sub = 1;
}
if (nextStep.stepId === 'groups') {
$scope.tabs.active = 2;
$scope.tabs.active = 1;
}
if (nextStep.stepId === 'labels') {
$scope.tabs.active = 3;
$scope.tabs.active = 2;
}
if (nextStep.stepId === 'sso') {
$scope.tabs.active = 4;
$scope.tabs.active = 3;
}
});
// on tour end, save the status in database

View File

@ -866,6 +866,8 @@ angular.module('application.router', ['ui.router'])
resolve: {
membersPromise: ['Member', function (Member) { return Member.list({ query: { search: '', order_by: 'id', page: 1, size: 20 } }).$promise; }],
adminsPromise: ['Admin', function (Admin) { return Admin.query().$promise; }],
partnersPromise: ['User', function (User) { return User.query({ role: 'partner' }).$promise; }],
managersPromise: ['User', function (User) { return User.query({ role: 'manager' }).$promise; }],
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }],
authProvidersPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.query().$promise; }]

View File

@ -1,8 +1,8 @@
'use strict';
Application.Services.factory('User', ['$resource', function ($resource) {
return $resource('/api/users',
{}, {
return $resource('/api/users/:id',
{ id: '@id' }, {
query: {
isArray: false
}

View File

@ -535,3 +535,14 @@
display: inline-block !important;
padding: 11px 44px !important;
}
li.level-2-tab > a {
background: #eee;
line-height: 14px;
font-size: 12px;
}
li.active.level-2-tab > a {
background: linear-gradient(#eee, #fff);
}

View File

@ -32,23 +32,19 @@
<div class="col-md-12">
<uib-tabset justified="true" active="tabs.active">
<uib-tab heading="{{ 'app.admin.members.members' | translate }}" index="0">
<ng-include src="'<%= asset_path "admin/members/members.html" %>'"></ng-include>
<uib-tab heading="{{ 'app.admin.members.users' | translate }}" index="0">
<ng-include src="'<%= asset_path "admin/members/users.html" %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'app.admin.members.administrators' | translate }}" class="admins-tab" index="1">
<ng-include src="'<%= asset_path "admin/members/administrators.html" %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'app.admin.members.groups' | translate }}" class="groups-tab" index="2">
<uib-tab heading="{{ 'app.admin.members.groups' | translate }}" class="groups-tab" index="1">
<div ui-view="groups"></div>
</uib-tab>
<uib-tab heading="{{ 'app.admin.members.tags' | translate }}" class="labels-tab" index="3">
<uib-tab heading="{{ 'app.admin.members.tags' | translate }}" class="labels-tab" index="2">
<div ui-view="tags"></div>
</uib-tab>
<uib-tab heading="{{ 'app.admin.members.authentication' | translate }}" class="sso-tab" index="4">
<uib-tab heading="{{ 'app.admin.members.authentication' | translate }}" class="sso-tab" index="3">
<div ui-view="authentification"></div>
</uib-tab>
</uib-tabset>

View File

@ -0,0 +1,39 @@
<div class="col-md-5 m-t-lg">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-filter"></i></span>
<input type="text" ng-model="searchFilter" class="form-control" placeholder="{{ 'app.admin.members.search_for_an_administrator' | translate }}">
</div>
</div>
</div>
<div class="col-md-12">
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.admins_new" translate>{{ 'app.admin.members.add_a_new_administrator' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('profile_attributes.last_name')">{{ 'app.admin.members.surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='profile_attributes.last_name', 'fa fa-sort-alpha-desc': orderAdmin =='-profile_attributes.last_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('profile_attributes.first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='profile_attributes.first_name', 'fa fa-sort-alpha-desc': orderAdmin =='-profile_attributes.first_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderAdmin('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='email', 'fa fa-sort-alpha-desc': orderAdmin =='-email', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:10%"><a href="" ng-click="setOrderAdmin('profile_attributes.phone')">{{ 'app.admin.members.phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderAdmin =='profile_attributes.phone', 'fa fa-sort-numeric-desc': orderAdmin =='-profile_attributes.phone', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:10%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="admin in admins | filter:searchFilter | orderBy: orderAdmin">
<td class="text-c">{{ admin.profile_attributes.last_name }}</td>
<td class="text-c">{{ admin.profile_attributes.first_name }}</td>
<td>{{ admin.email }}</td>
<td>{{ admin.profile_attributes.phone }}</td>
<td>
<button class="btn btn-danger" ng-if="admin.id != currentUser.id" ng-click="destroyAdmin(admins, admin)">
<i class="fa fa-trash-o"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,43 @@
<p class="alert alert-info m-t-lg" translate>
{{ 'app.admin.members.partners_info' }}
</p>
<div class="col-md-5">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-filter"></i></span>
<input type="text" ng-model="searchFilter" class="form-control" placeholder="{{ 'app.admin.members.search_for_a_partner' | translate }}">
</div>
</div>
</div>
<div class="col-md-12">
<button type="button" class="btn btn-warning m-t m-b" ng-click="openPartnerNewModal()" translate>{{ 'app.admin.members.add_a_new_partner' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:15%"><a href="" ng-click="setOrderPartner('last_name')">{{ 'app.admin.members.surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPartner =='last_name', 'fa fa-sort-alpha-desc': orderPartner =='-last_name', 'fa fa-arrows-v': orderPartner }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderPartner('first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPartner =='first_name', 'fa fa-sort-alpha-desc': orderPartner =='-first_name', 'fa fa-arrows-v': orderPartner }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderPartner('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderPartner =='email', 'fa fa-sort-alpha-desc': orderPartner =='-email', 'fa fa-arrows-v': orderPartner }"></i></a></th>
<th style="width:10%"><a href="" ng-click="setOrderPartner('resource')">{{ 'app.admin.members.associated_plan' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderPartner =='resource', 'fa fa-sort-numeric-desc': orderPartner =='-resource', 'fa fa-arrows-v': orderPartner }"></i></a></th>
<th style="width:10%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="partner in partners | filter:searchFilter | orderBy: orderPartner">
<td class="text-c">{{ partner.last_name }}</td>
<td class="text-c">{{ partner.first_name }}</td>
<td>{{ partner.email }}</td>
<td><a ui-sref="app.admin.plans.edit({id:partner.resource.id})">{{ partner.resource ? partner.resource.base_name : '' }}</a></td>
<td>
<button class="btn btn-danger" ng-if="partner.id != currentUser.id" ng-click="destroyPartner(partners, partner)">
<i class="fa fa-trash-o"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,19 @@
<uib-tabset justified="true" active="tabs.sub" class="m-t">
<uib-tab classes="level-2-tab" heading="{{ 'app.admin.members.members' | translate }}" index="0">
<ng-include src="'<%= asset_path "admin/members/members.html" %>'"></ng-include>
</uib-tab>
<uib-tab classes="level-2-tab" heading="{{ 'app.admin.members.administrators' | translate }}" class="admins-tab" index="1">
<ng-include src="'<%= asset_path "admin/members/administrators.html" %>'"></ng-include>
</uib-tab>
<!--<uib-tab classes="level-2-tab" heading="{{ 'app.admin.members.managers' | translate }}" class="admins-tab" index="2">
<ng-include src="'<%= asset_path "admin/members/managers.html" %>'"></ng-include>
</uib-tab>-->
<uib-tab classes="level-2-tab" heading="{{ 'app.admin.members.partners' | translate }}" class="admins-tab" index="3">
<ng-include src="'<%= asset_path "admin/members/partners.html" %>'"></ng-include>
</uib-tab>
</uib-tabset>

View File

@ -1,12 +1,13 @@
# frozen_string_literal: true
# API Controller for resources of type Users with role :partner
# API Controller for resources of type Users with role :partner or :manager
class API::UsersController < API::ApiController
before_action :authenticate_user!
before_action :set_user, only: %i[destroy]
def index
if current_user.admin? && params[:role] == 'partner'
@users = User.with_role(:partner).includes(:profile)
if current_user.admin? && %w[partner manager].include?(params[:role])
@users = User.with_role(params[:role].to_sym).includes(:profile)
else
head 403
end
@ -19,13 +20,23 @@ class API::UsersController < API::ApiController
if res[:saved]
@user = res[:user]
render status: :created
else
else²
render json: res[:user].errors.full_messages, status: :unprocessable_entity
end
end
def destroy
authorize User
@user.destroy
head :no_content
end
private
def set_user
@user = User.find(params[:id])
end
def partner_params
params.require(:user).permit(:email, :first_name, :last_name)
end

View File

@ -1,4 +1,7 @@
# frozen_string_literal: true
json.users @users do |user|
json.extract! user, :id, :email, :first_name, :last_name
json.name user.profile.full_name
json.resource user.roles.last.resource
end

View File

@ -588,6 +588,7 @@ en:
#management of users, labels, groups, and so on
members:
users_management: "Users management"
users: "Users"
members: "Members"
subscriptions: "Subscriptions"
search_for_an_user: "Search for an user"
@ -603,6 +604,15 @@ en:
administrators: "Administrators"
search_for_an_administrator: "Search for an administrator"
add_a_new_administrator: "Add a new administrator"
partners: "Partners"
partners_info: "A partner is a special user that can be associated with the «Partner» plans. These users will only receive notifications about subscriptions to these plans."
search_for_a_partner: "Search for a partner"
add_a_new_partner: "Add a new partner"
delete_this_partner: "Do you really want to delete this partner? This cannot be undone."
partner_successfully_deleted: "Partner successfully deleted."
unable_to_delete_the_partner: "Unable to delete the partner."
associated_plan: "Associated plan"
managers: "Managers"
groups: "Groups"
tags: "Tags"
authentication: "Authentication"

View File

@ -45,7 +45,7 @@ Rails.application.routes.draw do
patch '/bulk_update', action: 'bulk_update', on: :collection
put '/reset/:name', action: 'reset', on: :collection
end
resources :users, only: %i[index create]
resources :users, only: %i[index create destroy]
resources :members, only: %i[index show create update destroy] do
get '/export_subscriptions', action: 'export_subscriptions', on: :collection
get '/export_reservations', action: 'export_reservations', on: :collection