mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-12-01 12:24:28 +01:00
Merge branch 'dev' into l10n_dev
This commit is contained in:
commit
f1e0bce08b
@ -1,8 +1,12 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
- Manager: a new role between the member and the administrator
|
||||
- The invoices list displays the operator in case of offline payment
|
||||
- Interface to manage partners
|
||||
- Ability to define, per availability, a custom duration for the reservation slots
|
||||
- Ability to promote a user to a higher role (member > manager > admin)
|
||||
- Corrected the documentation about BOOK_SLOT_AT_SAME_TIME
|
||||
- Auto-adjusts text colors based on the selected theme colors
|
||||
- Fix a bug: unable to change group if the previous was deactivated
|
||||
- Fix a bug: unable to create events or trainings that are not multiples of SLOT_DURATION
|
||||
- Fix a bug: unable to delete an unreserved event
|
||||
@ -11,6 +15,7 @@
|
||||
- Fix a bug: plan edition does not show the associated group
|
||||
- Fix a bug: subscription page shows the groups without any active plans
|
||||
- Fix a bug: cart price inconsistently updated after a subscription
|
||||
- Fix a bug: background image of the profile is not shown and wrong menu hover color
|
||||
|
||||
## v4.3.4 2020 April 14
|
||||
|
||||
|
@ -575,6 +575,15 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of the operator that creates the invoice
|
||||
*/
|
||||
$scope.operatorName = function(invoice) {
|
||||
if (!invoice.operator) return '';
|
||||
|
||||
return `${invoice.operator.first_name} ${invoice.operator.last_name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the feature-tour for the admin/invoices page.
|
||||
* This is intended as a contextual help (when pressing F1)
|
||||
|
@ -691,6 +691,54 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
// current active authentication provider
|
||||
$scope.activeProvider = activeProviderPromise;
|
||||
|
||||
/**
|
||||
* Open a modal dialog asking for confirmation to change the role of the given user
|
||||
* @param userId {number} id of the user to "promote"
|
||||
* @returns {*}
|
||||
*/
|
||||
$scope.changeUserRole = function() {
|
||||
const modalInstance = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: '<%= asset_path "admin/members/change_role_modal.html" %>',
|
||||
size: 'lg',
|
||||
resolve: {
|
||||
user() { return $scope.user; }
|
||||
},
|
||||
controller: ['$scope', '$uibModalInstance', 'Member', 'user', '_t', function ($scope, $uibModalInstance, Member, user, _t) {
|
||||
$scope.user = user;
|
||||
|
||||
$scope.role = user.role;
|
||||
|
||||
$scope.roles = [
|
||||
{ key: 'admin', label: _t('app.admin.members_edit.admin') },
|
||||
{ key: 'manager', label: _t('app.admin.members_edit.manager'), notAnOption: (user.role === 'admin') },
|
||||
{ key: 'member', label: _t('app.admin.members_edit.member'), notAnOption: (user.role === 'admin' || user.role === 'manager') },
|
||||
];
|
||||
|
||||
$scope.ok = function () {
|
||||
Member.updateRole(
|
||||
{ id: $scope.user.id },
|
||||
{ role: $scope.role },
|
||||
function (_res) {
|
||||
growl.success(_t('app.admin.members_edit.role_changed', { OLD: _t(`app.admin.members_edit.${user.role}`), NEW: _t(`app.admin.members_edit.${$scope.role}`) }));
|
||||
return $uibModalInstance.close(_res);
|
||||
},
|
||||
function (error) {
|
||||
growl.error(_t('app.admin.members_edit.error_while_changing_role'));
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||
}]
|
||||
});
|
||||
// once the form was validated successfully ...
|
||||
return modalInstance.result.then(function (user) {
|
||||
// remove the user for the old list add to the new
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a modal dialog, allowing the admin to extend the current user's subscription (freely or not)
|
||||
* @param subscription {Object} User's subscription object
|
||||
|
@ -44,6 +44,10 @@ Application.Services.factory('Member', ['$resource', '$q', function ($resource,
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRole: {
|
||||
method: 'PATCH',
|
||||
url: '/api/members/:id/update_role'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -107,7 +107,6 @@
|
||||
> li.menu-spacer {
|
||||
height: 1px;
|
||||
margin: 6px 80% 6px 5px;
|
||||
background: linear-gradient(45deg, black, transparent);
|
||||
}
|
||||
|
||||
ul {
|
||||
|
4
app/assets/stylesheets/modules/members.scss
Normal file
4
app/assets/stylesheets/modules/members.scss
Normal file
@ -0,0 +1,4 @@
|
||||
.promote-member img {
|
||||
width: 16px;
|
||||
height: 21px;
|
||||
}
|
@ -36,12 +36,12 @@
|
||||
<table class="table invoices-list" ng-if="invoices.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:5%"></th>
|
||||
<th style="width:15%"><a href="" ng-click="setOrderInvoice('reference')">{{ 'app.admin.invoices.invoice_num' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='reference', 'fa fa-sort-numeric-desc': orderInvoice=='-reference', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
<th style="width:8%"></th>
|
||||
<th style="width:14%"><a href="" ng-click="setOrderInvoice('reference')">{{ 'app.admin.invoices.invoice_num' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='reference', 'fa fa-sort-numeric-desc': orderInvoice=='-reference', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
|
||||
<th style="width:20%"><a href="" ng-click="setOrderInvoice('date')">{{ 'app.admin.invoices.date' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='date', 'fa fa-sort-numeric-desc': orderInvoice=='-date', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
<th style="width:19%"><a href="" ng-click="setOrderInvoice('date')">{{ 'app.admin.invoices.date' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='date', 'fa fa-sort-numeric-desc': orderInvoice=='-date', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
|
||||
<th style="width:10%"><a href="" ng-click="setOrderInvoice('total')"> {{ 'app.admin.invoices.price' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='total', 'fa fa-sort-numeric-desc': orderInvoice=='-total', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
<th style="width:9%"><a href="" ng-click="setOrderInvoice('total')"> {{ 'app.admin.invoices.price' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='total', 'fa fa-sort-numeric-desc': orderInvoice=='-total', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
|
||||
<th style="width:20%"><a href="" ng-click="setOrderInvoice('name')">{{ 'app.admin.invoices.customer' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderInvoice=='name', 'fa fa-sort-alpha-desc': orderInvoice=='-name', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
|
||||
|
||||
@ -50,9 +50,14 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="invoice in invoices">
|
||||
<td class="chained-indicator">
|
||||
<i class="fa fa-link chained" ng-show="invoice.chained_footprint"/>
|
||||
<i class="fa fa-chain-broken broken" ng-hide="invoice.chained_footprint"/>
|
||||
<td>
|
||||
<span class="chained-indicator">
|
||||
<i class="fa fa-link chained" ng-show="invoice.chained_footprint"></i>
|
||||
<i class="fa fa-chain-broken broken" ng-hide="invoice.chained_footprint"></i>
|
||||
</span>
|
||||
<span class="operator help-cursor m-l-sm" ng-show="invoice.operator && invoice.operator.id !== invoice.user_id">
|
||||
<i class="fa fa-user-circle" title="{{ 'app.admin.invoices.operator_' | translate }} {{operatorName(invoice)}}"></i>
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ invoice.reference }}</td>
|
||||
<td ng-if="!invoice.is_avoir">{{ invoice.date | amDateFormat:'L LTS' }}</td>
|
||||
|
@ -0,0 +1,12 @@
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{logoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{logo.custom_asset_file_attributes.attachment}}" class="modal-logo"/>
|
||||
<h1 translate>{{ 'app.admin.members_edit.change_role' }}</h1>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="alert alert-warning" translate>{{ 'app.admin.members_edit.warning_role_change' }}</p>
|
||||
<select ng-model="role" class="form-control" ng-options="role.key as role.label disable when role.notAnOption for role in roles"></select>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-info" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
|
||||
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
|
||||
</div>
|
@ -17,8 +17,8 @@
|
||||
|
||||
<div class="col-md-3">
|
||||
<section class="heading-actions wrapper">
|
||||
<div class="btn btn-lg btn-block btn-default m-t-xs" ng-click="cancel()" translate>
|
||||
{{ 'app.shared.buttons.cancel' }}
|
||||
<div class="btn btn-lg btn-block btn-default promote-member m-t-xs" ng-click="changeUserRole()" ng-show="isAuthorized('admin')">
|
||||
<img src="/rank-icon.svg" alt="role icon" /><span class="m-l" translate>{{ 'app.admin.members_edit.change_role' }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
@ -41,9 +41,9 @@
|
||||
<th style="width:15%"><a href="" ng-click="setOrderMember('first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='first_name', 'fa fa-sort-alpha-desc': member.order=='-first_name', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:15%" class="hidden-xs"><a href="" ng-click="setOrderMember('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='email', 'fa fa-sort-alpha-desc': member.order=='-email', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:10%" class="hidden-xs hidden-sm hidden-md"><a href="" ng-click="setOrderMember('phone')">{{ 'app.admin.members.phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': member.order=='phone', 'fa fa-sort-numeric-desc': member.order=='-phone', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:20%" class="hidden-xs hidden-sm"><a href="" ng-click="setOrderMember('group')">{{ 'app.admin.members.user_type' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='group', 'fa fa-sort-alpha-desc': member.order=='-group', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:15%" class="hidden-xs hidden-sm"><a href="" ng-click="setOrderMember('group')">{{ 'app.admin.members.user_type' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='group', 'fa fa-sort-alpha-desc': member.order=='-group', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:15%" class="hidden-xs hidden-sm hidden-md"><a href="" ng-click="setOrderMember('plan')">{{ 'app.admin.members.subscription' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='plan', 'fa fa-sort-alpha-desc': member.order=='-plan', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:10%" class="buttons-col"></th>
|
||||
<th style="width:15%" class="buttons-col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -10,7 +10,7 @@
|
||||
<span class="avatar avatar-block text-center">
|
||||
<fab-user-avatar ng-model="member.profile.user_avatar" avatar-class="thumb-50"></fab-user-avatar>
|
||||
<!-- <i class="on b-white bottom"></i> -->
|
||||
<a ><span class="user-name m-l-sm text-black m-t-xs">{{member.name}}</span></a>
|
||||
<a ><span class="user-name m-l-sm m-t-xs">{{member.name}}</span></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md" ng-if="isAuthorized(['admin','member'])">
|
||||
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md" ng-if="isAuthorized(['admin','member', 'manager'])">
|
||||
<section class="heading-actions wrapper">
|
||||
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-sm upper text-sm" ui-sref="app.logged.projects_new" role="button" translate>{{ 'app.public.projects_list.add_a_project' }}</a>
|
||||
</section>
|
||||
|
@ -47,6 +47,6 @@ class API::ICalendarController < API::ApiController
|
||||
end
|
||||
|
||||
def i_calendar_params
|
||||
params.require(:i_calendar).permit(:name, :url, :color, :text_color, :text_hidden)
|
||||
params.require(:i_calendar).permit(:name, :url, :color, :primary_text_color, :text_hidden)
|
||||
end
|
||||
end
|
||||
|
@ -3,7 +3,7 @@
|
||||
# API Controller for resources of type User with role 'member'
|
||||
class API::MembersController < API::ApiController
|
||||
before_action :authenticate_user!, except: [:last_subscribed]
|
||||
before_action :set_member, only: %i[update destroy merge complete_tour]
|
||||
before_action :set_member, only: %i[update destroy merge complete_tour update_role]
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@ -202,6 +202,35 @@ class API::MembersController < API::ApiController
|
||||
end
|
||||
end
|
||||
|
||||
def update_role
|
||||
authorize @member
|
||||
|
||||
# we do not allow dismissing a user to a lower role
|
||||
if params[:role] == 'member'
|
||||
render 403 and return if @member.role == 'admin' || @member.role == 'manager'
|
||||
elsif params[:role] == 'manager'
|
||||
render 403 and return if @member.role == 'admin'
|
||||
end
|
||||
|
||||
# do nothing if the role does not change
|
||||
render json: @member and return if params[:role] == @member.role
|
||||
|
||||
ex_role = @member.role.to_sym
|
||||
@member.remove_role ex_role
|
||||
@member.add_role params[:role]
|
||||
|
||||
NotificationCenter.call type: 'notify_user_role_update',
|
||||
receiver: @member,
|
||||
attached_object: @member
|
||||
|
||||
NotificationCenter.call type: 'notify_admins_role_update',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: @member,
|
||||
meta_data: { ex_role: ex_role }
|
||||
|
||||
render json: @member
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_member
|
||||
|
@ -51,6 +51,8 @@ class NotificationType
|
||||
notify_privacy_policy_changed
|
||||
notify_admin_import_complete
|
||||
notify_admin_refund_created
|
||||
notify_admins_role_update
|
||||
notify_user_role_update
|
||||
]
|
||||
# deprecated:
|
||||
# - notify_member_subscribed_plan_is_changed
|
||||
|
@ -212,7 +212,8 @@ class Reservation < ApplicationRecord
|
||||
end
|
||||
|
||||
def save_with_payment(operator_profile_id, coupon_code = nil, payment_intent_id = nil)
|
||||
method = InvoicingProfile.find(operator_profile_id)&.user&.admin? ? nil : 'stripe'
|
||||
operator = InvoicingProfile.find(operator_profile_id)&.user
|
||||
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
|
||||
|
||||
build_invoice(
|
||||
invoicing_profile: user.invoicing_profile,
|
||||
|
@ -4,9 +4,14 @@
|
||||
# a picture for the background of the user's profile.
|
||||
# There's only one stylesheet record in the database, which is updated on each colour change.
|
||||
class Stylesheet < ApplicationRecord
|
||||
|
||||
# brightness limits to change the font color to black or white
|
||||
BRIGHTNESS_HIGH_LIMIT = 180
|
||||
BRIGHTNESS_LOW_LIMIT = 40
|
||||
|
||||
validates_presence_of :contents
|
||||
|
||||
## ===== THEME =====
|
||||
## ===== COMMON =====
|
||||
|
||||
def rebuild!
|
||||
if Stylesheet.primary && Stylesheet.secondary && name == 'theme'
|
||||
@ -16,7 +21,9 @@ class Stylesheet < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def self.build_sheet!
|
||||
## ===== THEME =====
|
||||
|
||||
def self.build_theme!
|
||||
return unless Stylesheet.primary && Stylesheet.secondary
|
||||
|
||||
if Stylesheet.theme
|
||||
@ -58,45 +65,59 @@ class Stylesheet < ApplicationRecord
|
||||
Stylesheet.find_by(name: 'theme')
|
||||
end
|
||||
|
||||
def self.primary_text_color
|
||||
Stylesheet.primary.paint.brightness >= BRIGHTNESS_HIGH_LIMIT ? 'black' : 'white'
|
||||
end
|
||||
|
||||
def self.primary_decoration_color
|
||||
Stylesheet.primary.paint.brightness <= BRIGHTNESS_LOW_LIMIT ? 'white' : 'black'
|
||||
end
|
||||
|
||||
def self.secondary_text_color
|
||||
Stylesheet.secondary.paint.brightness <= BRIGHTNESS_LOW_LIMIT ? 'white' : 'black'
|
||||
end
|
||||
|
||||
def self.css # rubocop:disable Metrics/AbcSize
|
||||
<<~CSS
|
||||
.bg-red { background-color: #{Stylesheet.primary}; }
|
||||
.bg-red-dark { background-color: #{Stylesheet.primary}; }
|
||||
#nav .nav { background-color: #{Stylesheet.primary}; }
|
||||
#nav .nav > li > a { color: white; }
|
||||
#nav .nav > li > a:hover, #nav .nav > li > a:focus { background-color: #{Stylesheet.primary_light}; }
|
||||
#nav .nav > li > a.active { border-left: 3px solid #{Stylesheet.primary_dark}; background-color: #{Stylesheet.primary_light}; }
|
||||
#nav .nav > li > a.active { border-left: 3px solid #{Stylesheet.primary_dark}; background-color: #{Stylesheet.primary_light}; }
|
||||
.btn-theme { background-color: #{Stylesheet.primary}; color: white; }
|
||||
.btn-theme:active, .btn-theme:hover { background-color: #{Stylesheet.primary_dark}; }
|
||||
.label-theme { background-color: #{Stylesheet.primary} }
|
||||
#nav .nav > li > a { color: #{Stylesheet.primary_text_color}; }
|
||||
#nav .nav > li > a:hover, #nav .nav > li > a:focus { background-color: #{Stylesheet.primary_light}; color: #{Stylesheet.primary_text_color}; }
|
||||
#nav .nav > li > a.active { border-left: 3px solid #{Stylesheet.primary_dark}; background-color: #{Stylesheet.primary_light}; color: #{Stylesheet.primary_text_color}; }
|
||||
.nav-primary ul.nav > li.menu-spacer { background: linear-gradient(45deg, #{Stylesheet.primary_decoration_color}, transparent); }
|
||||
.nav-primary .text-bordeau { color: #{Stylesheet.primary_decoration_color}; }
|
||||
.btn-theme { background-color: #{Stylesheet.primary}; color: #{Stylesheet.primary_text_color}; }
|
||||
.btn-theme:active, .btn-theme:hover { background-color: #{Stylesheet.primary_dark}; color: #{Stylesheet.primary_text_color}; }
|
||||
.label-theme { background-color: #{Stylesheet.primary}; color: #{Stylesheet.primary_text_color}; }
|
||||
.btn-link { color: #{Stylesheet.primary} !important; }
|
||||
.btn-link:hover { color: #{Stylesheet.primary_dark} !important; }
|
||||
a { color: #{Stylesheet.primary}; }
|
||||
a:hover, a:focus { color: #{Stylesheet.primary_dark}; }
|
||||
h2, h3, h5 { color: #{Stylesheet.primary}; }
|
||||
h5:after { background-color: #{Stylesheet.primary}; }
|
||||
.bg-yellow { background-color: #{Stylesheet.secondary} !important; }
|
||||
.event:hover { background-color: #{Stylesheet.primary}; }
|
||||
.bg-yellow { background-color: #{Stylesheet.secondary} !important; color: #{Stylesheet.secondary_text_color}; }
|
||||
.event:hover { background-color: #{Stylesheet.primary}; color: #{Stylesheet.secondary_text_color}; }
|
||||
.widget h3 { color: #{Stylesheet.primary}; }
|
||||
.modal-header h1, .custom-invoice .modal-header h1 { color: #{Stylesheet.primary}; }
|
||||
.block-link:hover, .fc-toolbar .fc-button:hover, .fc-toolbar .fc-button:active, .fc-toolbar .fc-button.fc-state-active { background-color: #{Stylesheet.secondary}; }
|
||||
.block-link:hover, .fc-toolbar .fc-button:hover, .fc-toolbar .fc-button:active, .fc-toolbar .fc-button.fc-state-active { background-color: #{Stylesheet.secondary}; color: #{Stylesheet.secondary_text_color} !important; }
|
||||
.block-link:hover .user-name { color: #{Stylesheet.secondary_text_color} !important; }
|
||||
.carousel-control:hover, .carousel-control:focus, .carousel-caption .title a:hover { color: #{Stylesheet.secondary}; }
|
||||
.well.well-warning { border-color: #{Stylesheet.secondary}; background-color: #{Stylesheet.secondary}; }
|
||||
.well.well-warning { border-color: #{Stylesheet.secondary}; background-color: #{Stylesheet.secondary}; color: #{Stylesheet.secondary_text_color}; }
|
||||
.text-yellow { color: #{Stylesheet.secondary} !important; }
|
||||
.red { color: #{Stylesheet.primary} !important; }
|
||||
.btn-warning, .editable-buttons button[type=submit].btn-primary { background-color: #{Stylesheet.secondary} !important; border-color: #{Stylesheet.secondary} !important; }
|
||||
.btn-warning:hover, .editable-buttons button[type=submit].btn-primary:hover, .btn-warning:focus, .editable-buttons button[type=submit].btn-primary:focus, .btn-warning.focus, .editable-buttons button.focus[type=submit].btn-primary, .btn-warning:active, .editable-buttons button[type=submit].btn-primary:active, .btn-warning.active, .editable-buttons button.active[type=submit].btn-primary, .open > .btn-warning.dropdown-toggle, .editable-buttons .open > button.dropdown-toggle[type=submit].btn-primary { background-color: #{Stylesheet.secondary_dark} !important; border-color: #{Stylesheet.secondary_dark} !important; }
|
||||
.btn-warning-full { border-color: #{Stylesheet.secondary}; background-color: #{Stylesheet.secondary}; }
|
||||
.heading .heading-btn a:hover { background-color: #{Stylesheet.secondary}; }
|
||||
.btn-warning, .editable-buttons button[type=submit].btn-primary { background-color: #{Stylesheet.secondary} !important; border-color: #{Stylesheet.secondary} !important; color: #{Stylesheet.secondary_text_color}; }
|
||||
.btn-warning:hover, .editable-buttons button[type=submit].btn-primary:hover, .btn-warning:focus, .editable-buttons button[type=submit].btn-primary:focus, .btn-warning.focus, .editable-buttons button.focus[type=submit].btn-primary, .btn-warning:active, .editable-buttons button[type=submit].btn-primary:active, .btn-warning.active, .editable-buttons button.active[type=submit].btn-primary, .open > .btn-warning.dropdown-toggle, .editable-buttons .open > button.dropdown-toggle[type=submit].btn-primary { background-color: #{Stylesheet.secondary_dark} !important; border-color: #{Stylesheet.secondary_dark} !important; color: #{Stylesheet.secondary_text_color}; }
|
||||
.btn-warning-full { border-color: #{Stylesheet.secondary}; background-color: #{Stylesheet.secondary}; color: #{Stylesheet.secondary_text_color} !important; }
|
||||
.heading .heading-btn a:hover { background-color: #{Stylesheet.secondary}; color: #{Stylesheet.secondary_text_color}; }
|
||||
.pricing-panel .content .wrap { border-color: #{Stylesheet.secondary}; }
|
||||
.pricing-panel .cta-button .btn:hover, .pricing-panel .cta-button .custom-invoice .modal-body .elements li:hover, .custom-invoice .modal-body .elements .pricing-panel .cta-button li:hover { background-color: #{Stylesheet.secondary} !important; }
|
||||
.pricing-panel .cta-button .btn:hover, .pricing-panel .cta-button .custom-invoice .modal-body .elements li:hover, .custom-invoice .modal-body .elements .pricing-panel .cta-button li:hover { background-color: #{Stylesheet.secondary} !important; color: #{Stylesheet.secondary_text_color}; }
|
||||
a.label:hover, .form-control.form-control-ui-select .select2-choices a.select2-search-choice:hover, a.label:focus, .form-control.form-control-ui-select .select2-choices a.select2-search-choice:focus { color: #{Stylesheet.primary}; }
|
||||
.about-picture { background: linear-gradient( rgba(255,255,255,0.12), rgba(255,255,255,0.13) ), linear-gradient( #{Stylesheet.primary_with_alpha(0.78)}, #{Stylesheet.primary_with_alpha(0.82)} ), url('/about-fablab.jpg') no-repeat; }
|
||||
.social-icons > div:hover { background-color: #{Stylesheet.secondary}; }
|
||||
.social-icons > div:hover { background-color: #{Stylesheet.secondary}; color: #{Stylesheet.secondary_text_color}; }
|
||||
.profile-top { background: linear-gradient( rgba(255,255,255,0.12), rgba(255,255,255,0.13) ), linear-gradient(#{Stylesheet.primary_with_alpha(0.78)}, #{Stylesheet.primary_with_alpha(0.82)} ), url('#{CustomAsset.get_url('profile-image-file') || '/about-fablab.jpg'}') no-repeat; }
|
||||
.profile-top .social-links a:hover { background-color: #{Stylesheet.secondary} !important; border-color: #{Stylesheet.secondary} !important; }
|
||||
section#cookies-modal div.cookies-consent .cookies-actions button.accept { background-color: #{Stylesheet.secondary}; }
|
||||
.profile-top .social-links a:hover { background-color: #{Stylesheet.secondary} !important; border-color: #{Stylesheet.secondary} !important; color: #{Stylesheet.secondary_text_color}; }
|
||||
section#cookies-modal div.cookies-consent .cookies-actions button.accept { background-color: #{Stylesheet.secondary}; color: #{Stylesheet.secondary_text_color}; }
|
||||
CSS
|
||||
end
|
||||
|
||||
|
@ -46,7 +46,8 @@ class Subscription < ApplicationRecord
|
||||
def generate_invoice(operator_profile_id, coupon_code = nil, payment_intent_id = nil)
|
||||
coupon_id = nil
|
||||
total = plan.amount
|
||||
method = InvoicingProfile.find(operator_profile_id)&.user&.admin? ? nil : 'stripe'
|
||||
operator = InvoicingProfile.find(operator_profile_id)&.user
|
||||
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
|
||||
|
||||
unless coupon_code.nil?
|
||||
@coupon = Coupon.find_by(code: coupon_code)
|
||||
|
@ -160,6 +160,8 @@ class User < ApplicationRecord
|
||||
'admin'
|
||||
elsif manager?
|
||||
'manager'
|
||||
elsif member?
|
||||
'member'
|
||||
else
|
||||
'other'
|
||||
end
|
||||
|
@ -39,7 +39,7 @@ class UserPolicy < ApplicationPolicy
|
||||
end
|
||||
end
|
||||
|
||||
%w[create mapping].each do |action|
|
||||
%w[create mapping update_role].each do |action|
|
||||
define_method "#{action}?" do
|
||||
user.admin?
|
||||
end
|
||||
|
@ -16,4 +16,10 @@ json.array!(@invoices) do |invoice|
|
||||
json.date invoice.is_a?(Avoir) ? invoice.avoir_date : invoice.created_at
|
||||
json.prevent_refund invoice.prevent_refund?
|
||||
json.chained_footprint invoice.check_footprint
|
||||
if invoice.operator_profile
|
||||
json.operator do
|
||||
json.id invoice.operator_profile.user_id
|
||||
json.extract! invoice.operator_profile, :first_name, :last_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.title notification.notification_type
|
||||
json.description t('.user_NAME_changed_ROLE_html',
|
||||
NAME: notification.attached_object&.profile&.full_name || t('api.notifications.deleted_user'),
|
||||
ROLE: t("roles.#{notification.attached_object&.role}"))
|
||||
json.url notification_url(notification, format: :json)
|
@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.title notification.notification_type
|
||||
json.description t('.your_role_is_ROLE', ROLE: t("roles.#{notification.attached_object&.role}"))
|
||||
json.url notification_url(notification, format: :json)
|
@ -1 +1 @@
|
||||
<%= @stylesheet.contents %>
|
||||
<%= @stylesheet.contents.html_safe %>
|
@ -0,0 +1,8 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<p><%= t('.body.user_role_changed_html', NAME: @attached_object&.profile&.full_name || t('api.notifications.deleted_user')) %></p>
|
||||
|
||||
<p>
|
||||
<small><%= t('.body.previous_role') %> <strong><%= t("roles.#{@notification.get_meta_data(:ex_role)}") %></strong></small>
|
||||
<br/><%= t('.body.new_role') %> <strong><%= t("roles.#{@attached_object.role}") %></strong>
|
||||
</p>
|
@ -0,0 +1,7 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<%
|
||||
name = Setting.find_by(name: 'fablab_name').value
|
||||
gender = Setting.find_by(name: 'name_genre').value
|
||||
%>
|
||||
<p><%= t('.body.role_changed', NAME: name, GENDER: gender, ROLE: t("roles.#{@attached_object.role}")) %></p>
|
@ -374,6 +374,7 @@ en:
|
||||
accounting_periods: "Accounting periods"
|
||||
invoices_list: "Invoices list"
|
||||
filter_invoices: "Filter invoices"
|
||||
operator_: "Operator:"
|
||||
invoice_num_: "Invoice #:"
|
||||
customer_: "Customer:"
|
||||
date_: "Date:"
|
||||
@ -722,6 +723,13 @@ en:
|
||||
error_details: "Error's details:"
|
||||
#edit a member
|
||||
members_edit:
|
||||
change_role: "Change role"
|
||||
warning_role_change: "<p><strong>Warning:</strong> changing the role of a user is not a harmless operation. Is not currently possible to dismiss a user to a lower privileged role.</p><ul><li><strong>Members</strong> can only book reservations for themselves, paying by card or wallet.</li><li><strong>Managers</strong> can book reservations for themselves, paying by card or wallet, and for other members and managers, by collecting payments at the checkout.</li><li><strong>Administrators</strong> can only book reservations for members and managers, by collecting payments at the checkout. Moreover, they can change every settings of the application.</li></ul>"
|
||||
admin: "Administrator"
|
||||
manager: "Manager"
|
||||
member: "Member"
|
||||
role_changed: "Role successfully changed from {OLD} to {NEW}."
|
||||
error_while_changing_role: "An error occurred while changing the role. Please try again later."
|
||||
subscription: "Subscription"
|
||||
duration: "Duration:"
|
||||
expires_at: "Expires at:"
|
||||
|
@ -374,7 +374,7 @@ fr:
|
||||
accounting_periods: "Périodes comptables"
|
||||
invoices_list: "Liste des factures"
|
||||
filter_invoices: "Filtrer les factures"
|
||||
operator_: "Opérateur:"
|
||||
operator_: "Opérateur :"
|
||||
invoice_num_: "Facture n° :"
|
||||
customer_: "Client :"
|
||||
date_: "Date :"
|
||||
|
@ -214,6 +214,10 @@ en:
|
||||
event: "Event"
|
||||
reservations: "Reservations"
|
||||
available_seats: "Available seats"
|
||||
roles:
|
||||
member: "Member"
|
||||
manager: "Manager"
|
||||
admin: "Administrator"
|
||||
api:
|
||||
#internal app notifications
|
||||
notifications:
|
||||
@ -329,6 +333,10 @@ en:
|
||||
click_to_show: "Click here to consult"
|
||||
notify_admin_refund_created:
|
||||
refund_created: "A refund of %{AMOUNT} has been created for user %{USER}"
|
||||
notify_user_role_update:
|
||||
your_role_is_ROLE: "Your role has been changed to %{ROLE}."
|
||||
notify_admins_role_update:
|
||||
user_NAME_changed_ROLE_html: "User <strong><em>%{NAME}</strong></em> is now %{ROLE}."
|
||||
#statistics tools for admins
|
||||
statistics:
|
||||
subscriptions: "Subscriptions"
|
||||
|
@ -273,5 +273,15 @@ en:
|
||||
body:
|
||||
refund_created: "A refund of %{AMOUNT} has been generated on invoice %{INVOICE} of user %{USER}"
|
||||
download: "Click here to download this refund invoice"
|
||||
notify_admins_role_update:
|
||||
subject: "The role of a user has changed"
|
||||
body:
|
||||
user_role_changed_html: "The role of the user <em><strong>%{NAME}</strong></em> has changed."
|
||||
previous_role: "Previous role:"
|
||||
new_role: "New role:"
|
||||
notify_user_role_update:
|
||||
subject: "Your role has changed"
|
||||
body:
|
||||
role_changed_html: "Your role at {GENDER, select, male{the} female{the} neutral{} other{the}} {NAME} has changed. You are now <strong>{ROLE}</strong>.<br/>With great power comes great responsibility, use your new privileges fairly and respectfully."
|
||||
shared:
|
||||
hello: "Hello %{user_name}"
|
||||
|
@ -55,6 +55,7 @@ Rails.application.routes.draw do
|
||||
get 'search/:query', action: 'search', on: :collection
|
||||
get 'mapping', action: 'mapping', on: :collection
|
||||
patch ':id/complete_tour', action: 'complete_tour', on: :collection
|
||||
patch ':id/update_role', action: 'update_role', on: :collection
|
||||
end
|
||||
resources :reservations, only: %i[show create index update]
|
||||
resources :notifications, only: %i[index show update] do
|
||||
|
@ -8,7 +8,7 @@ class CreateICalendars < ActiveRecord::Migration[4.2]
|
||||
t.string :url
|
||||
t.string :name
|
||||
t.string :color
|
||||
t.string :text_color
|
||||
t.string :primary_text_color
|
||||
t.boolean :text_hidden
|
||||
|
||||
t.timestamps null: false
|
||||
|
@ -699,7 +699,7 @@ unless Setting.find_by(name: 'secondary_color').try(:value)
|
||||
setting.save
|
||||
end
|
||||
|
||||
Stylesheet.build_sheet!
|
||||
Stylesheet.build_theme!
|
||||
Stylesheet.build_home!
|
||||
|
||||
unless Setting.find_by(name: 'training_information_message').try(:value)
|
||||
|
@ -66,7 +66,7 @@ namespace :fablab do
|
||||
|
||||
desc '(re)build customization stylesheet'
|
||||
task rebuild_stylesheet: :environment do
|
||||
Stylesheet.build_sheet!
|
||||
Stylesheet.build_theme!
|
||||
end
|
||||
|
||||
desc 'migration notifications from Fab-manager v1'
|
||||
|
61
public/rank-icon.svg
Normal file
61
public/rank-icon.svg
Normal file
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
||||
height="74.5"
|
||||
width="56"
|
||||
sodipodi:docname="rank-icon.svg"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 56 74.499999"
|
||||
enable-background="new 0 0 100 100"
|
||||
xml:space="preserve"
|
||||
id="svg102"><sodipodi:namedview
|
||||
inkscape:current-layer="svg102"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-x="0"
|
||||
inkscape:cy="60.2"
|
||||
inkscape:cx="27.8"
|
||||
inkscape:zoom="6.744"
|
||||
lock-margins="false"
|
||||
fit-margin-bottom="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-top="0"
|
||||
showgrid="false"
|
||||
id="namedview11"
|
||||
inkscape:window-height="1007"
|
||||
inkscape:window-width="1374"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff"
|
||||
inkscape:document-rotation="0" /><metadata
|
||||
id="metadata108"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs106" /><g
|
||||
style="stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="translate(-22.2,-2.3)"
|
||||
id="g92"><path
|
||||
style="stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 50.2,75.8 c -0.2,0 -0.3,0 -0.5,-0.1 L 23.7,60.8 C 23.4,60.6 23.2,60.3 23.2,59.9 V 45.1 c 0,-0.4 0.2,-0.7 0.5,-0.9 0.3,-0.2 0.7,-0.2 1,0 L 50.2,58.8 75.7,44.2 c 0.3,-0.2 0.7,-0.2 1,0 0.3,0.2 0.5,0.5 0.5,0.9 V 60 c 0,0.4 -0.2,0.7 -0.5,0.9 l -26,14.9 c -0.1,0 -0.3,0 -0.5,0 z m -25,-16.4 25,14.3 25,-14.3 V 46.9 l -24.5,14 c -0.3,0.2 -0.7,0.2 -1,0 l -24.5,-14 z"
|
||||
id="path90" /></g><g
|
||||
style="stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="translate(-22.2,-2.3)"
|
||||
id="g96"><path
|
||||
style="stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 62.5,45.1 c -0.3,0 -0.7,-0.1 -1,-0.2 L 50.3,39 39,44.8 C 38.1,45.3 37,45 36.4,44.3 36,43.8 35.8,43.2 35.9,42.6 L 38,30.1 29,21.2 c -0.6,-0.6 -0.8,-1.4 -0.5,-2.1 0.3,-0.7 0.9,-1.3 1.7,-1.4 L 42.7,15.9 48.4,4.5 c 0.4,-0.7 1.1,-1.2 1.9,-1.2 0.8,0 1.5,0.4 1.9,1.2 l 5.6,11.3 12.6,1.9 c 0.8,0.1 1.4,0.7 1.7,1.4 0.2,0.8 0,1.6 -0.5,2.1 l -9.1,8.8 2.1,12.5 c 0.1,0.6 -0.1,1.2 -0.5,1.7 -0.4,0.6 -1,0.9 -1.6,0.9 z M 50.2,36.9 c 0.3,0 0.7,0.1 1,0.2 L 62.4,43 c 0,0 0.1,0 0.1,0 L 60.4,30.5 c -0.1,-0.7 0.1,-1.4 0.6,-1.9 l 9.1,-8.8 c 0,0 0,0 0,-0.1 0,-0.1 -0.1,-0.1 -0.1,-0.1 L 57.5,17.8 C 56.8,17.7 56.2,17.3 55.9,16.7 L 50.3,5.3 c 0,0 -0.2,0 -0.2,0 l -5.6,11.3 c -0.3,0.6 -0.9,1 -1.6,1.1 l -12.5,1.8 c 0,0 -0.1,0 -0.1,0.1 0,0.1 0,0.1 0,0.1 l 9.1,8.8 c 0.5,0.5 0.7,1.2 0.6,1.9 L 37.9,42.9 38,43 49.2,37.1 c 0.4,-0.1 0.7,-0.2 1,-0.2 z"
|
||||
id="path94" /></g></svg>
|
After Width: | Height: | Size: 3.3 KiB |
Loading…
Reference in New Issue
Block a user