1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

Merge branch 'dev' for release 4.4.0

This commit is contained in:
Sylvain 2020-05-12 11:14:01 +02:00
commit d72ff53972
187 changed files with 3319 additions and 8445 deletions

View File

@ -1,11 +1,36 @@
# Changelog Fab-manager
## v4.4.0 2020 May 12
- 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)
- Ask for confirmation before booking a slot for a member without the required tag
- Corrected the documentation about BOOK_SLOT_AT_SAME_TIME
- Auto-adjusts text colors based on the selected theme colors
- Check password length during installation
- Fix a bug: accounting periods totals are wrong for periods closed after 2019-08-01
- 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
- Fix a bug: "Free entry" label for events without reservation
- Fix a bug: updating a setting without any changes triggers an error
- 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
- Fix a bug: do not show disabled groups and plans during availability creation
- Fix a security issue: updated jquery to fix [CVE-2020-11023](https://nvd.nist.gov/vuln/detail/CVE-2020-11023)
- [TODO DEPLOY] `rails db:migrate`
## v4.3.4 2020 April 14
- Improved version check
- Improved setup script for installations without nginx
- Changed some default values for new installations
- Compatible database with Fab-manager v1, to allow upgrades
- Database is now compatible with Fab-manager v1, to allow upgrades
- Updated documentation
- Changed In-Context pseudo-language to Zulu instead of Acholi
- Allow removing contacts from the about page
@ -25,7 +50,7 @@
## v4.3.3 2020 April 1st
- Docker build will no longer embed development dependencies
- Updated instructions to setup a development environment
- Updated instructions to set up a development environment
- Updated translations
- Removed `MESSAGEFORMAT_LOCALE` as it is now handled by make-plural
- Updated rails framework to v5.2
@ -61,7 +86,7 @@
## v4.3.1 2020 March 04
- Updated user's manual for v4.3 (fr)
- Display user's manual when help is asked, if no tour is available
- Display user's manual when asking for help, if no tour is available
- Change style and pluralize the text of the slot division alert in new availability assistant
- Fix a bug: in feature tours, next and previous arrows may be broken on some systems
- Fix a bug: in the user's menu, two links to the personal wallet

44
Vagrantfile vendored
View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# -*- mode: ruby -*-
# vi: set ft=ruby :
@ -17,11 +19,11 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
1080, # mailcatcher web ui
4040 # ngrok web ui
].each do |port|
config.vm.network "forwarded_port", guest: port, host: port
config.vm.network 'forwarded_port', guest: port, host: port
end
# nginx server
config.vm.network "forwarded_port", guest: 80, host: 8080
config.vm.network 'forwarded_port', guest: 80, host: 8080
# Configuration to allocate resources fro the virtual machine
config.vm.provider 'virtualbox' do |vb|
@ -32,29 +34,29 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# configuration below for file syncronization
config.vm.synced_folder '.', '/vagrant', type: 'virtualbox'
# Copy default configuration files for the database conenction and the Rails application
config.vm.provision "file", source: "./config/database.yml.default", destination: "/vagrant/config/database.yml"
config.vm.provision "file", source: "./config/application.yml.default", destination: "/vagrant/config/application.yml"
# Copy default configuration files for the database connection and the Rails application
config.vm.provision 'file', source: './config/database.yml.default', destination: '/vagrant/config/database.yml'
config.vm.provision 'file', source: './env.example', destination: '/vagrant/.env'
# Copy default configuration files to allow reviewing the Docker Compose integration
config.vm.provision "file", source: "./docker/docker-compose.yml", destination: "/home/vagrant/docker-compose.yml"
config.vm.provision "file", source: "./docker/env.example", destination: "/home/vagrant/config/env"
config.vm.provision "file", source: "./docker/nginx.conf.example", destination: "/home/vagrant/config/nginx/fabmanager.conf"
config.vm.provision "file", source: "./docker/elasticsearch.yml", destination: "/home/vagrant/elasticsearch/config/elasticsearch.yml"
config.vm.provision "file", source: "./docker/log4j2.properties", destination: "/home/vagrant/elasticsearch/config/log4j2.properties"
config.vm.provision 'file', source: './docker/development/docker-compose.yml', destination: '/home/vagrant/docker-compose.yml'
config.vm.provision 'file', source: './setup/env.example', destination: '/home/vagrant/config/env'
config.vm.provision 'file', source: './setup/nginx.conf.example', destination: '/home/vagrant/config/nginx/fabmanager.conf'
config.vm.provision 'file', source: './setup/elasticsearch.yml', destination: '/home/vagrant/elasticsearch/config/elasticsearch.yml'
config.vm.provision 'file', source: './setup/log4j2.properties', destination: '/home/vagrant/elasticsearch/config/log4j2.properties'
## Provision software dependencies
config.vm.provision "shell", privileged: false, run: "once",
path: "provision/zsh_setup.sh"
config.vm.provision 'shell', privileged: false, run: 'once',
path: 'provision/zsh_setup.sh'
config.vm.provision "shell", privileged: false, run: "once",
path: "provision/box_setup.zsh",
env: {
"LC_ALL" => "en_US.UTF-8",
"LANG" => "en_US.UTF-8",
"LANGUAGE" => "en_US.UTF-8",
}
config.vm.provision 'shell', privileged: false, run: 'once',
path: 'provision/box_setup.zsh',
env: {
'LC_ALL' => 'en_US.UTF-8',
'LANG' => 'en_US.UTF-8',
'LANGUAGE' => 'en_US.UTF-8'
}
config.vm.provision "shell", privileged: true, run: "once",
path: "provision/box_tuning.zsh"
config.vm.provision 'shell', privileged: true, run: 'once',
path: 'provision/box_tuning.zsh'
end

View File

@ -64,8 +64,6 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ui.rout
$translateProvider.useMessageFormatInterpolation();
// Set the language of the instance (from ruby configuration)
$translateProvider.preferredLanguage(Fablab.locale);
// In any cases, fallback to english
$translateProvider.fallbackLanguage('en');
// End the tour when the user clicks the forward or back buttons of the browser
TourConfigProvider.enableNavigationInterceptors();
}]).run(['$rootScope', '$log', 'AuthService', 'Auth', 'amMoment', '$state', 'editableOptions', 'Analytics',

View File

@ -18,8 +18,8 @@
* Controller used in the calendar management page
*/
Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'Availability', 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', 'plansPromise', 'groupsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Member', 'uiTourService',
function ($scope, $state, $uibModal, moment, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, plansPromise, groupsPromise, _t, uiCalendarConfig, CalendarConfig, Member, uiTourService) {
Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'AuthService', 'Availability', 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', 'plansPromise', 'groupsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Member', 'uiTourService',
function ($scope, $state, $uibModal, moment, AuthService, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, plansPromise, groupsPromise, _t, uiCalendarConfig, CalendarConfig, Member, uiTourService) {
/* PRIVATE STATIC CONSTANTS */
// The calendar is divided in slots of 30 minutes
@ -313,14 +313,16 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
placement: 'right',
popupClass: 'width-350'
});
uitour.createStep({
selector: '.admin-calendar .export-xls-button',
stepId: 'export',
order: 2,
title: _t('app.admin.tour.calendar.export.title'),
content: _t('app.admin.tour.calendar.export.content'),
placement: 'left'
});
if (AuthService.isAuthorized('admin')) {
uitour.createStep({
selector: '.admin-calendar .export-xls-button',
stepId: 'export',
order: 2,
title: _t('app.admin.tour.calendar.export.title'),
content: _t('app.admin.tour.calendar.export.content'),
placement: 'left'
});
}
uitour.createStep({
selector: '.heading .import-ics-button',
stepId: 'import',
@ -416,9 +418,12 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
const modalInstance = $uibModal.open({
templateUrl: '<%= asset_path "admin/calendar/eventModal.html" %>',
controller: 'CreateEventModalController',
backdrop: 'static',
keyboard: false,
resolve: {
start () { return start; },
end () { return end; },
start() { return start; },
end() { return end; },
slots() { return Math.ceil(slots); },
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
spacesPromise: ['Space', function (Space) { return Space.query().$promise; }],
@ -526,8 +531,8 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
/**
* Controller used in the slot creation modal window
*/
Application.Controllers.controller('CreateEventModalController', ['$scope', '$uibModalInstance', '$sce', 'moment', 'start', 'end', 'machinesPromise', 'Availability', 'trainingsPromise', 'spacesPromise', 'tagsPromise', 'plansPromise', 'groupsPromise', 'growl', '_t',
function ($scope, $uibModalInstance, $sce, moment, start, end, machinesPromise, Availability, trainingsPromise, spacesPromise, tagsPromise, plansPromise, groupsPromise, growl, _t) {
Application.Controllers.controller('CreateEventModalController', ['$scope', '$uibModalInstance', '$sce', 'moment', 'start', 'end', 'slots', 'machinesPromise', 'Availability', 'trainingsPromise', 'spacesPromise', 'tagsPromise', 'plansPromise', 'groupsPromise', 'growl', '_t',
function ($scope, $uibModalInstance, $sce, moment, start, end, slots, machinesPromise, Availability, trainingsPromise, spacesPromise, tagsPromise, plansPromise, groupsPromise, growl, _t) {
// $uibModal parameter
$scope.start = start;
@ -551,15 +556,6 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
$scope.selectedPlansBinding = {};
// list of plans, classified by group
$scope.plansClassifiedByGroup = [];
for (let group of Array.from(groupsPromise)) {
const groupObj = { id: group.id, name: group.name, plans: [] };
for (let plan of Array.from(plansPromise)) {
if (plan.group_id === group.id) { groupObj.plans.push(plan); }
}
if (groupObj.plans.length > 0) {
$scope.plansClassifiedByGroup.push(groupObj);
}
}
// machines associated with the created slot
$scope.selectedMachines = [];
@ -598,7 +594,8 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
is_recurrent: false,
period: 'week',
nb_periods: 1,
end_date: undefined // recurrence end
end_date: undefined, // recurrence end
slot_duration: Fablab.slotDuration
};
// recurrent slots
@ -613,8 +610,8 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
// localized name(s) of the selected plan(s)
$scope.plansName = '';
// make the duration available for display
$scope.slotDuration = Fablab.slotDuration;
// number of slots for this availability
$scope.slots_nb = slots;
/**
* Adds or removes the provided machine from the current slot
@ -731,6 +728,13 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
}
};
/*
* Test if the current availability type is divided in slots
*/
$scope.isTypeDivided = function () {
return isTypeDivided($scope.availability.available_type);
}
/* PRIVATE SCOPE */
/**
@ -752,35 +756,64 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
}
});
// group plans by Group
for (let group of groupsPromise.filter(g => !g.disabled)) {
const groupObj = { id: group.id, name: group.name, plans: [] };
for (let plan of plansPromise.filter(g => !g.disabled)) {
if (plan.group_id === group.id) { groupObj.plans.push(plan); }
}
if (groupObj.plans.length > 0) {
$scope.plansClassifiedByGroup.push(groupObj);
}
}
// When the slot duration changes, we increment the availability to match the value
$scope.$watch('availability.slot_duration', function (newValue, oldValue, scope) {
start = moment($scope.start);
start.add(newValue * $scope.slots_nb, 'minutes');
$scope.end = start.toDate();
});
// When the number of slot changes, we increment the availability to match the value
$scope.$watch('slots_nb', function (newValue, oldValue, scope) {
start = moment($scope.start);
start.add($scope.availability.slot_duration * newValue, 'minutes');
$scope.end = start.toDate();
});
// When we configure a machine/space availability, do not let the user change the end time, as the total
// time must be dividable by Fablab.slotDuration minutes (base slot duration). For training availabilities, the user
// time must be dividable by $scope.availability.slot_duration minutes (base slot duration). For training availabilities, the user
// can configure any duration as it does not matters.
$scope.$watch('availability.available_type', function (newValue, oldValue, scope) {
if ((newValue === 'machines') || (newValue === 'space')) {
if (isTypeDivided(newValue)) {
$scope.endDateReadOnly = true;
const slots = Math.trunc(($scope.end.valueOf() - $scope.start.valueOf()) / (60 * 1000)) / Fablab.slotDuration;
if (!Number.isInteger(slots)) {
const slotsCurrentRange = Math.trunc(($scope.end.valueOf() - $scope.start.valueOf()) / (60 * 1000)) / $scope.availability.slot_duration;
if (!Number.isInteger(slotsCurrentRange)) {
// otherwise, round it to upper decimal
const upper = Math.ceil(slots) * Fablab.slotDuration;
const upperSlots = Math.ceil(slotsCurrentRange);
const upper = upperSlots * $scope.availability.slot_duration;
$scope.end = moment($scope.start).add(upper, 'minutes').toDate();
$scope.slots_nb = upperSlots;
} else {
$scope.slots_nb = slotsCurrentRange;
}
return $scope.availability.end_at = $scope.end;
$scope.availability.end_at = $scope.end;
} else {
return $scope.endDateReadOnly = false;
$scope.endDateReadOnly = false;
}
});
// When the start date is changed, if we are configuring a machine/space availability,
// maintain the relative length of the slot (ie. change the end time accordingly)
$scope.$watch('start', function (newValue, oldValue, scope) {
// for machine or space availabilities, adjust the end time
if (($scope.availability.available_type === 'machines') || ($scope.availability.available_type === 'space')) {
// for machine or space availabilities, adjust the end time
if ($scope.isTypeDivided()) {
end = moment($scope.end);
end.add(moment(newValue).diff(oldValue), 'milliseconds');
$scope.end = end.toDate();
} else { // for training availabilities
// prevent the admin from setting the beginning after the end
if (moment(newValue).add(Fablab.slotDuration, 'minutes').isAfter($scope.end)) {
// prevent the admin from setting the beginning after the end
if (moment(newValue).add($scope.availability.slot_duration, 'minutes').isAfter($scope.end)) {
$scope.start = oldValue;
}
}
@ -791,7 +824,7 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
// Maintain consistency between the end time and the date object in the availability object
$scope.$watch('end', function (newValue, oldValue, scope) {
// we prevent the admin from setting the end of the availability before its beginning
if (moment($scope.start).add(Fablab.slotDuration, 'minutes').isAfter(newValue)) {
if (moment($scope.start).add($scope.availability.slot_duration, 'minutes').isAfter(newValue)) {
$scope.end = oldValue;
}
// update availability object
@ -799,6 +832,13 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
});
};
/*
* Test if the provided availability type is divided in slots
*/
const isTypeDivided = function (type) {
return ((type === 'machines') || (type === 'space'));
}
/**
* Validates that a machine or more was/were selected before continuing to step 3 (adjust time + tags)
*/

View File

@ -153,8 +153,8 @@ class EventsController {
/**
* Controller used in the events listing page (admin view)
*/
Application.Controllers.controller('AdminEventsController', ['$scope', '$state', 'dialogs', '$uibModal', 'growl', 'Event', 'Category', 'EventTheme', 'AgeRange', 'PriceCategory', 'eventsPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '_t', 'Member', 'uiTourService',
function ($scope, $state, dialogs, $uibModal, growl, Event, Category, EventTheme, AgeRange, PriceCategory, eventsPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, _t, Member, uiTourService) {
Application.Controllers.controller('AdminEventsController', ['$scope', '$state', 'dialogs', '$uibModal', 'growl', 'AuthService', 'Event', 'Category', 'EventTheme', 'AgeRange', 'PriceCategory', 'eventsPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '_t', 'Member', 'uiTourService',
function ($scope, $state, dialogs, $uibModal, growl, AuthService, Event, Category, EventTheme, AgeRange, PriceCategory, eventsPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, _t, Member, uiTourService) {
/* PUBLIC SCOPE */
// By default, the pagination mode is activated to limit the page size
@ -407,38 +407,40 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state',
content: _t('app.admin.tour.events.filter.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.events-management .events-categories',
stepId: 'categories',
order: 3,
title: _t('app.admin.tour.events.categories.title'),
content: _t('app.admin.tour.events.categories.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.events-management .events-themes',
stepId: 'themes',
order: 4,
title: _t('app.admin.tour.events.themes.title'),
content: _t('app.admin.tour.events.themes.content'),
placement: 'top'
});
uitour.createStep({
selector: '.events-management .events-age-ranges',
stepId: 'ages',
order: 5,
title: _t('app.admin.tour.events.ages.title'),
content: _t('app.admin.tour.events.ages.content'),
placement: 'top'
});
uitour.createStep({
selector: '.events-management .prices-tab',
stepId: 'prices',
order: 6,
title: _t('app.admin.tour.events.prices.title'),
content: _t('app.admin.tour.events.prices.content'),
placement: 'bottom'
});
if (AuthService.isAuthorized('admin')) {
uitour.createStep({
selector: '.events-management .events-categories',
stepId: 'categories',
order: 3,
title: _t('app.admin.tour.events.categories.title'),
content: _t('app.admin.tour.events.categories.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.events-management .events-themes',
stepId: 'themes',
order: 4,
title: _t('app.admin.tour.events.themes.title'),
content: _t('app.admin.tour.events.themes.content'),
placement: 'top'
});
uitour.createStep({
selector: '.events-management .events-age-ranges',
stepId: 'ages',
order: 5,
title: _t('app.admin.tour.events.ages.title'),
content: _t('app.admin.tour.events.ages.content'),
placement: 'top'
});
uitour.createStep({
selector: '.events-management .prices-tab',
stepId: 'prices',
order: 6,
title: _t('app.admin.tour.events.prices.title'),
content: _t('app.admin.tour.events.prices.content'),
placement: 'bottom'
});
}
uitour.createStep({
selector: 'body',
stepId: 'conclusion',

View File

@ -17,8 +17,8 @@
/**
* Controller used in the admin invoices listing page
*/
Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'Invoice', 'AccountingPeriod', 'invoices', 'closedPeriods', '$uibModal', 'growl', '$filter', 'Setting', 'settings', '_t', 'Member', 'uiTourService',
function ($scope, $state, Invoice, AccountingPeriod, invoices, closedPeriods, $uibModal, growl, $filter, Setting, settings, _t, Member, uiTourService) {
Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'Invoice', 'AccountingPeriod', 'AuthService', 'invoices', 'closedPeriods', '$uibModal', 'growl', '$filter', 'Setting', 'settings', '_t', 'Member', 'uiTourService',
function ($scope, $state, Invoice, AccountingPeriod, AuthService, invoices, closedPeriods, $uibModal, growl, $filter, Setting, settings, _t, Member, uiTourService) {
/* PRIVATE STATIC CONSTANTS */
// number of invoices loaded each time we click on 'load more...'
@ -291,8 +291,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
growl.success(_t('app.admin.invoices.invoice_reference_successfully_saved'));
}
, function (error) {
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_invoice_reference'));
console.error(error);
if (error.status === 304) return;
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_invoice_reference'));
console.error(error);
});
});
};
@ -330,8 +332,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
}
}
, function (error) {
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_invoicing_code'));
return console.error(error);
if (error.status === 304) return;
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_invoicing_code'));
console.error(error);
});
return Setting.update({ name: 'invoice_code-active' }, { value: result.active ? 'true' : 'false' }, function (data) {
@ -343,8 +347,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
}
}
, function (error) {
growl.error(_t('app.admin.invoices.an_error_occurred_while_activating_the_invoicing_code'));
return console.error(error);
if (error.status === 304) return;
growl.error(_t('app.admin.invoices.an_error_occurred_while_activating_the_invoicing_code'));
console.error(error);
});
});
};
@ -375,8 +381,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
return growl.success(_t('app.admin.invoices.order_number_successfully_saved'));
}
, function (error) {
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_order_number'));
return console.error(error);
if (error.status === 304) return;
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_order_number'));
console.error(error);
});
});
};
@ -434,8 +442,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
}
}
, function (error) {
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_VAT_rate'));
return console.error(error);
if (error.status === 304) return;
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_VAT_rate'));
console.error(error);
});
return Setting.update({ name: 'invoice_VAT-active' }, { value: result.active ? 'true' : 'false' }, function (data) {
@ -447,8 +457,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
}
}
, function (error) {
growl.error(_t('app.admin.invoices.an_error_occurred_while_activating_the_VAT'));
return console.error(error);
if (error.status === 304) return;
growl.error(_t('app.admin.invoices.an_error_occurred_while_activating_the_VAT'));
console.error(error);
});
});
};
@ -463,8 +475,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
return growl.success(_t('app.admin.invoices.text_successfully_saved'));
}
, function (error) {
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_text'));
return console.error(error);
if (error.status === 304) return;
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_text'));
console.error(error);
});
};
@ -478,8 +492,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
return growl.success(_t('app.admin.invoices.address_and_legal_information_successfully_saved'));
}
, function (error) {
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_address_and_the_legal_information'));
return console.error(error);
if (error.status === 304) return;
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_address_and_the_legal_information'));
console.error(error);
});
};
@ -559,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)
@ -566,15 +591,27 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
$scope.setupInvoicesTour = function () {
// get the tour defined by the ui-tour directive
const uitour = uiTourService.getTourByName('invoices');
uitour.createStep({
selector: 'body',
stepId: 'welcome',
order: 0,
title: _t('app.admin.tour.invoices.welcome.title'),
content: _t('app.admin.tour.invoices.welcome.content'),
placement: 'bottom',
orphan: true
});
if (AuthService.isAuthorized('admin')) {
uitour.createStep({
selector: 'body',
stepId: 'welcome',
order: 0,
title: _t('app.admin.tour.invoices.welcome.title'),
content: _t('app.admin.tour.invoices.welcome.content'),
placement: 'bottom',
orphan: true
});
} else {
uitour.createStep({
selector: 'body',
stepId: 'welcome_manager',
order: 0,
title: _t('app.admin.tour.invoices.welcome_manager.title'),
content: _t('app.admin.tour.invoices.welcome_manager.content'),
placement: 'bottom',
orphan: true
});
}
if (!Fablab.withoutInvoices && $scope.invoices.length > 0) {
uitour.createStep({
selector: '.invoices-management .invoices-list',
@ -609,39 +646,41 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
placement: 'left'
});
}
uitour.createStep({
selector: '.invoices-management .invoices-settings',
stepId: 'settings',
order: 5,
title: _t('app.admin.tour.invoices.settings.title'),
content: _t('app.admin.tour.invoices.settings.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.invoices-management .accounting-codes-tab',
stepId: 'codes',
order: 6,
title: _t('app.admin.tour.invoices.codes.title'),
content: _t('app.admin.tour.invoices.codes.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.heading .export-accounting-button',
stepId: 'export',
order: 7,
title: _t('app.admin.tour.invoices.export.title'),
content: _t('app.admin.tour.invoices.export.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.heading .close-accounting-periods-button',
stepId: 'periods',
order: 8,
title: _t('app.admin.tour.invoices.periods.title'),
content: _t('app.admin.tour.invoices.periods.content'),
placement: 'bottom',
popupClass: 'shift-left-50'
});
if (AuthService.isAuthorized('admin')) {
uitour.createStep({
selector: '.invoices-management .invoices-settings',
stepId: 'settings',
order: 5,
title: _t('app.admin.tour.invoices.settings.title'),
content: _t('app.admin.tour.invoices.settings.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.invoices-management .accounting-codes-tab',
stepId: 'codes',
order: 6,
title: _t('app.admin.tour.invoices.codes.title'),
content: _t('app.admin.tour.invoices.codes.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.heading .export-accounting-button',
stepId: 'export',
order: 7,
title: _t('app.admin.tour.invoices.export.title'),
content: _t('app.admin.tour.invoices.export.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.heading .close-accounting-periods-button',
stepId: 'periods',
order: 8,
title: _t('app.admin.tour.invoices.periods.title'),
content: _t('app.admin.tour.invoices.periods.content'),
placement: 'bottom',
popupClass: 'shift-left-50'
});
}
uitour.createStep({
selector: 'body',
stepId: 'conclusion',
@ -710,8 +749,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
{ value: $scope.invoice.logo.base64 },
function (data) { growl.success(_t('app.admin.invoices.logo_successfully_saved')); },
function (error) {
if (error.status === 304) return;
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_logo'));
return console.error(error);
console.error(error);
}
);
}

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', 'AuthService', 'dialogs', '_t', 'Member', 'Export', 'User', 'uiTourService',
function ($scope, $sce, $uibModal, membersPromise, adminsPromise, partnersPromise, managersPromise, growl, Admin, AuthService, 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,67 @@ 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;
}
};
/**
* Change the managers ordering criterion to the one provided
* @param orderManager {string} ordering criterion
*/
$scope.setOrderManager = function (orderManager) {
if ($scope.orderManager === orderManager) {
return $scope.orderManager = `-${orderManager}`;
} else {
return $scope.orderManager = orderManager;
}
};
/**
* 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 +325,66 @@ 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')); }
);
}
);
}
/**
* Ask for confirmation then delete the specified manager
* @param managers {Array} full list of managers
* @param manager {Object} manager to delete
*/
$scope.destroyManager = function (managers, manager) {
dialogs.confirm(
{
resolve: {
object () {
return {
title: _t('app.admin.members.confirmation_required'),
msg: $sce.trustAsHtml(_t('app.admin.members.delete_this_manager') + '<br/><br/>' + _t('app.admin.members.this_may_take_a_while_please_wait'))
};
}
}
},
function () { // cancel confirmed
User.delete(
{ id: manager.id },
function () {
managers.splice(findItemIdxById(managers, manager.id), 1);
return growl.success(_t('app.admin.members.manager_successfully_deleted'));
},
function (error) { growl.error(_t('app.admin.members.unable_to_delete_the_manager')); }
);
}
);
}
/**
* Callback for the 'load more' button.
* Will load the next results of the current search, if any
@ -343,22 +476,24 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
placement: 'left'
});
}
uitour.createStep({
selector: '.members-management .exports-buttons',
stepId: 'exports',
order: 5,
title: _t('app.admin.tour.members.exports.title'),
content: _t('app.admin.tour.members.exports.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.heading .import-members',
stepId: 'import',
order: 6,
title: _t('app.admin.tour.members.import.title'),
content: _t('app.admin.tour.members.import.content'),
placement: 'left'
});
if (AuthService.isAuthorized('admin')) {
uitour.createStep({
selector: '.members-management .exports-buttons',
stepId: 'exports',
order: 5,
title: _t('app.admin.tour.members.exports.title'),
content: _t('app.admin.tour.members.exports.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.heading .import-members',
stepId: 'import',
order: 6,
title: _t('app.admin.tour.members.import.title'),
content: _t('app.admin.tour.members.import.content'),
placement: 'left'
});
}
uitour.createStep({
selector: '.members-management .admins-tab',
stepId: 'admins',
@ -367,31 +502,33 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
content: _t('app.admin.tour.members.admins.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.members-management .groups-tab',
stepId: 'groups',
order: 8,
title: _t('app.admin.tour.members.groups.title'),
content: _t('app.admin.tour.members.groups.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.members-management .labels-tab',
stepId: 'labels',
order: 9,
title: _t('app.admin.tour.members.labels.title'),
content: _t('app.admin.tour.members.labels.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.members-management .sso-tab',
stepId: 'sso',
order: 10,
title: _t('app.admin.tour.members.sso.title'),
content: _t('app.admin.tour.members.sso.content'),
placement: 'bottom',
popupClass: 'shift-left-50'
});
if (AuthService.isAuthorized('admin')) {
uitour.createStep({
selector: '.members-management .groups-tab',
stepId: 'groups',
order: 8,
title: _t('app.admin.tour.members.groups.title'),
content: _t('app.admin.tour.members.groups.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.members-management .labels-tab',
stepId: 'labels',
order: 9,
title: _t('app.admin.tour.members.labels.title'),
content: _t('app.admin.tour.members.labels.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.members-management .sso-tab',
stepId: 'sso',
order: 10,
title: _t('app.admin.tour.members.sso.title'),
content: _t('app.admin.tour.members.sso.content'),
placement: 'bottom',
popupClass: 'shift-left-50'
});
}
uitour.createStep({
selector: 'body',
stepId: 'conclusion',
@ -405,18 +542,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
@ -552,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
@ -936,3 +1123,72 @@ Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'A
}
]);
/**
* Controller used in the manager's creation page (admin view)
*/
Application.Controllers.controller('NewManagerController', ['$state', '$scope', 'User', 'groupsPromise', 'tagsPromise', 'growl', '_t',
function ($state, $scope, User, groupsPromise, tagsPromise, growl, _t) {
// default admin profile
$scope.manager = {
statistic_profile_attributes: {
gender: true
},
profile_attributes: {},
invoicing_profile_attributes: {}
};
// Default parameters for AngularUI-Bootstrap datepicker
$scope.datePicker = {
format: Fablab.uibDateFormat,
opened: false,
options: {
startingDay: Fablab.weekStartingDay
}
};
// list of all groups
$scope.groups = groupsPromise.filter(function (g) { return (g.slug !== 'admins') && !g.disabled; });
// list of all tags
$scope.tags = tagsPromise;
/**
* Shows the birth day datepicker
* @param $event {Object} jQuery event object
*/
$scope.openDatePicker = function ($event) { $scope.datePicker.opened = true; };
/**
* Send the new manager, currently stored in $scope.manager, to the server for database saving
*/
$scope.saveManager = function () {
User.save(
{},
{ manager: $scope.manager },
function () {
growl.success(_t('app.admin.manager_new.manager_successfully_created', { GENDER: getGender($scope.manager) }));
return $state.go('app.admin.members');
}
, function (error) {
growl.error(_t('app.admin.admins_new.failed_to_create_manager') + JSON.stringify(error.data ? error.data : error));
console.error(error);
}
);
};
/* PRIVATE SCOPE */
/**
* Return an enumerable meaningful string for the gender of the provider user
* @param user {Object} Database user record
* @return {string} 'male' or 'female'
*/
const getGender = function (user) {
if (user.statistic_profile_attributes) {
if (user.statistic_profile_attributes.gender) { return 'male'; } else { return 'female'; }
} else { return 'other'; }
};
}
]);

View File

@ -617,6 +617,22 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
});
}
/**
* Return the exemple price based on the configuration of the default slot duration.
* @param type {string} 'hourly_rate' | *
* @returns {number} price for Fablab.slotDuration minutes.
*/
$scope.examplePrice = function(type) {
const hourlyRate = 10;
if (type === 'hourly_rate') {
return $filter('currency')(hourlyRate);
}
const price = (hourlyRate / 60) * Fablab.slotDuration;
return $filter('currency')(price);
}
/**
* Setup the feature-tour for the admin/pricing page.
* This is intended as a contextual help (when pressing F1)

View File

@ -209,7 +209,12 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
{ name: setting.name },
{ value },
function () { growl.success(_t('app.admin.settings.customization_of_SETTING_successfully_saved', { SETTING: _t(`app.admin.settings.${setting.name}`) })); },
function (error) { console.log(error); }
function (error) {
if (error.status === 304) return;
growl.error(_t('app.admin.settings.an_error_occurred_saving_the_setting'));
console.log(error);
}
);
};

View File

@ -126,8 +126,8 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve
}
]);
Application.Controllers.controller('ShowEventController', ['$scope', '$state', '$stateParams', '$rootScope', 'Event', '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'Slot', 'eventPromise', 'growl', '_t', 'Wallet', 'helpers', 'dialogs', 'priceCategoriesPromise', 'settingsPromise',
function ($scope, $state, $stateParams, $rootScope, Event, $uibModal, Member, Reservation, Price, CustomAsset, Slot, eventPromise, growl, _t, Wallet, helpers, dialogs, priceCategoriesPromise, settingsPromise) {
Application.Controllers.controller('ShowEventController', ['$scope', '$state', '$stateParams', '$rootScope', 'Event', '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'Slot', 'eventPromise', 'growl', '_t', 'Wallet', 'AuthService', 'helpers', 'dialogs', 'priceCategoriesPromise', 'settingsPromise',
function ($scope, $state, $stateParams, $rootScope, Event, $uibModal, Member, Reservation, Price, CustomAsset, Slot, eventPromise, growl, _t, Wallet, AuthService, helpers, dialogs, priceCategoriesPromise, settingsPromise) {
/* PUBLIC SCOPE */
// reservations for the currently shown event
@ -245,32 +245,32 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
if ($scope.event.nb_total_places > 0) {
$scope.reserveSuccess = false;
if (!$scope.isAuthenticated()) {
return $scope.login(null, function (user) {
if (user.role !== 'admin') {
return $scope.ctrl.member = user;
$scope.login(null, function (user) {
if (user.role !== 'admin' || user.role !== 'manager') {
$scope.ctrl.member = user;
}
const sameTimeReservations = findReservationsAtSameTime();
if (sameTimeReservations.length > 0) {
showReserveSlotSameTimeModal(sameTimeReservations, function(res) {
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
$scope.reserve.toReserve = !$scope.reserve.toReserve;
});
} else {
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
$scope.reserve.toReserve = !$scope.reserve.toReserve;
}
});
} else {
if ($scope.currentUser.role === 'admin') {
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
if (AuthService.isAuthorized(['admin', 'manager'])) {
$scope.reserve.toReserve = !$scope.reserve.toReserve;
} else {
Member.get({ id: $scope.currentUser.id }, function (member) {
$scope.ctrl.member = member;
const sameTimeReservations = findReservationsAtSameTime();
if (sameTimeReservations.length > 0) {
showReserveSlotSameTimeModal(sameTimeReservations, function(res) {
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
$scope.reserve.toReserve = !$scope.reserve.toReserve;
});
} else {
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
$scope.reserve.toReserve = !$scope.reserve.toReserve;
}
});
}
@ -286,9 +286,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
resetEventReserve();
$scope.reserveSuccess = false;
if ($scope.ctrl.member) {
return Member.get({ id: $scope.ctrl.member.id }, function (member) {
Member.get({ id: $scope.ctrl.member.id }, function (member) {
$scope.ctrl.member = member;
return getReservations($scope.event.id, 'Event', $scope.ctrl.member.id);
getReservations($scope.event.id, 'Event', $scope.ctrl.member.id);
});
}
};
@ -303,14 +303,17 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
return Wallet.getWalletByUser({ user_id: $scope.ctrl.member.id }, function (wallet) {
const amountToPay = helpers.getAmountToPay($scope.reserve.amountTotal, wallet.amount);
if (($scope.currentUser.role !== 'admin') && (amountToPay > 0)) {
if ((AuthService.isAuthorized(['member']) && amountToPay > 0)
|| (AuthService.isAuthorized('manager') && $scope.ctrl.member.id === $rootScope.currentUser.id && amountToPay > 0)) {
if ($rootScope.fablabWithoutOnlinePayment) {
growl.error(_t('app.public.events_show.online_payment_disabled'));
} else {
return payByStripe(reservation);
}
} else {
if (($scope.currentUser.role === 'admin') || (amountToPay === 0)) {
if (AuthService.isAuthorized('admin')
|| (AuthService.isAuthorized('manager') && $scope.ctrl.member.id !== $rootScope.currentUser.id)
|| amountToPay === 0) {
return payOnSite(reservation);
}
}
@ -564,7 +567,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
}
// watch when a coupon is applied to re-compute the total price
return $scope.$watch('coupon.applied', function (newValue, oldValue) {
$scope.$watch('coupon.applied', function (newValue, oldValue) {
if ((newValue !== null) || (oldValue !== null)) {
return $scope.computeEventAmount();
}
@ -577,7 +580,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
* @param reservable_type {string} 'Event'
* @param user_id {number} the user's id (current or managed)
*/
var getReservations = function (reservable_id, reservable_type, user_id) {
const getReservations = function (reservable_id, reservable_type, user_id) {
Reservation.query({
reservable_id,
reservable_type,
@ -592,7 +595,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
* @param event {Object} Current event
* @return {{user_id:number, reservable_id:number, reservable_type:string, slots_attributes:Array<Object>, nb_reserve_places:number}}
*/
var mkReservation = function (member, reserve, event) {
const mkReservation = function (member, reserve, event) {
const reservation = {
user_id: member.id,
reservable_id: event.id,
@ -628,7 +631,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
* @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) {
const params = {
reservation,
coupon_code: ((coupon ? coupon.code : undefined))
@ -640,7 +643,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
/**
* Set the current reservation to the default values. This implies the reservation form to be hidden.
*/
var resetEventReserve = function () {
const resetEventReserve = function () {
if ($scope.event) {
$scope.reserve = {
nbPlaces: {
@ -666,7 +669,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
* Open a modal window which trigger the stripe payment process
* @param reservation {Object} to book
*/
var payByStripe = function (reservation) {
const payByStripe = function (reservation) {
$uibModal.open({
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>',
size: 'md',
@ -730,7 +733,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
* Open a modal window which trigger the local payment process
* @param reservation {Object} to book
*/
var payOnSite = function (reservation) {
const payOnSite = function (reservation) {
$uibModal.open({
templateUrl: '<%= asset_path "shared/valid_reservation_modal.html" %>',
size: 'sm',
@ -808,7 +811,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
* What to do after the payment was successful
* @param reservation {Object} booked reservation
*/
var afterPayment = function (reservation) {
const afterPayment = function (reservation) {
$scope.event.nb_free_places = $scope.event.nb_free_places - reservation.total_booked_seats;
resetEventReserve();
$scope.reserveSuccess = true;
@ -822,7 +825,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
/**
* Find user's reservations, the same date at the same time, with event
*/
var findReservationsAtSameTime = function () {
const findReservationsAtSameTime = function () {
let sameTimeReservations = [
'training_reservations',
'machine_reservations',
@ -848,7 +851,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
* @param sameTimeReservations {Array} reservations the same date at the same time
* @param callback {function} callback will invoke when user confirm
*/
var showReserveSlotSameTimeModal = function(sameTimeReservations, callback) {
const showReserveSlotSameTimeModal = function(sameTimeReservations, callback) {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '<%= asset_path "shared/_reserve_slot_same_time.html" %>',

View File

@ -1,7 +1,7 @@
'use strict';
Application.Controllers.controller('HomeController', ['$scope', '$stateParams', 'settingsPromise', 'Member', 'uiTourService', '_t', 'Help',
function ($scope, $stateParams, settingsPromise, Member, uiTourService, _t, Help) {
Application.Controllers.controller('HomeController', ['$scope', '$stateParams', '$translatePartialLoader', 'AuthService', 'settingsPromise', 'Member', 'uiTourService', '_t', 'Help',
function ($scope, $stateParams, $translatePartialLoader, AuthService, settingsPromise, Member, uiTourService, _t, Help) {
/* PUBLIC SCOPE */
// Home page HTML content
@ -21,8 +21,12 @@ Application.Controllers.controller('HomeController', ['$scope', '$stateParams',
* This is intended as a contextual help (when pressing F1)
*/
$scope.setupHomeTour = function () {
if ($scope.currentUser && $scope.currentUser.role === 'admin') {
setupWelcomeTour();
if (AuthService.isAuthorized(['admin', 'manager'])) {
// Workaround for the following bug: sometimes, when the feature tour is shown, the translations keys are not
// interpreted. This is an ugly hack, but we can't do better for now because angular-ui-tour does not support
// removing steps (this would allow us to recreate the steps when the translations are loaded), and we can't use
// promises with _t's translations (this would be a very big refactoring)
setTimeout(setupWelcomeTour, 1000);
}
};
@ -182,7 +186,7 @@ Application.Controllers.controller('HomeController', ['$scope', '$stateParams',
selector: '.nav-primary .admin-section',
stepId: 'admin',
order: 9,
title: _t('app.public.tour.welcome.admin.title'),
title: _t('app.public.tour.welcome.admin.title', { ROLE: _t(`app.public.common.${$scope.currentUser.role}`) }),
content: _t('app.public.tour.welcome.admin.content'),
placement: 'right'
});
@ -271,14 +275,16 @@ Application.Controllers.controller('HomeController', ['$scope', '$stateParams',
placement: 'bottom',
orphan: 'true'
});
uitour.createStep({
selector: '.app-generator .app-version',
stepId: 'version',
order: 19,
title: _t('app.public.tour.welcome.version.title'),
content: _t('app.public.tour.welcome.version.content'),
placement: 'top'
});
if (AuthService.isAuthorized('admin')) {
uitour.createStep({
selector: '.app-generator .app-version',
stepId: 'version',
order: 19,
title: _t('app.public.tour.welcome.version.title'),
content: _t('app.public.tour.welcome.version.content'),
placement: 'top'
});
}
uitour.createStep({
selector: 'body',
stepId: 'conclusion',

View File

@ -180,8 +180,8 @@ const _reserveMachine = function (machine, e) {
/**
* Controller used in the public listing page, allowing everyone to see the list of machines
*/
Application.Controllers.controller('MachinesController', ['$scope', '$state', '_t', 'Machine', '$uibModal', 'machinesPromise', 'Member', 'uiTourService',
function ($scope, $state, _t, Machine, $uibModal, machinesPromise, Member, uiTourService) {
Application.Controllers.controller('MachinesController', ['$scope', '$state', '_t', 'AuthService', 'Machine', '$uibModal', 'machinesPromise', 'Member', 'uiTourService',
function ($scope, $state, _t, AuthService, Machine, $uibModal, machinesPromise, Member, uiTourService) {
/* PUBLIC SCOPE */
// Retrieve the list of machines
@ -219,32 +219,54 @@ Application.Controllers.controller('MachinesController', ['$scope', '$state', '_
*/
$scope.setupMachinesTour = function () {
// setup the tour for admins only
if ($scope.currentUser && $scope.currentUser.role === 'admin') {
if (AuthService.isAuthorized(['admin', 'manager'])) {
// get the tour defined by the ui-tour directive
const uitour = uiTourService.getTourByName('machines');
uitour.createStep({
selector: 'body',
stepId: 'welcome',
order: 0,
title: _t('app.public.tour.machines.welcome.title'),
content: _t('app.public.tour.machines.welcome.content'),
placement: 'bottom',
orphan: true
});
if (AuthService.isAuthorized('admin')) {
uitour.createStep({
selector: 'body',
stepId: 'welcome',
order: 0,
title: _t('app.public.tour.machines.welcome.title'),
content: _t('app.public.tour.machines.welcome.content'),
placement: 'bottom',
orphan: true
});
if ($scope.machines.length > 0) {
uitour.createStep({
selector: '.machines-list .show-button',
stepId: 'view',
order: 1,
title: _t('app.public.tour.machines.view.title'),
content: _t('app.public.tour.machines.view.content'),
placement: 'top'
});
}
} else {
uitour.createStep({
selector: 'body',
stepId: 'welcome_manager',
order: 0,
title: _t('app.public.tour.machines.welcome_manager.title'),
content: _t('app.public.tour.machines.welcome_manager.content'),
placement: 'bottom',
orphan: true
});
}
if ($scope.machines.length > 0) {
uitour.createStep({
selector: '.machines-list .show-button',
stepId: 'view',
order: 1,
title: _t('app.public.tour.machines.view.title'),
content: _t('app.public.tour.machines.view.content'),
selector: '.machines-list .reserve-button',
stepId: 'reserve',
order: 2,
title: _t('app.public.tour.machines.reserve.title'),
content: _t('app.public.tour.machines.reserve.content'),
placement: 'top'
});
}
uitour.createStep({
selector: 'body',
stepId: 'conclusion',
order: 2,
order: 3,
title: _t('app.public.tour.conclusion.title'),
content: _t('app.public.tour.conclusion.content'),
placement: 'bottom',
@ -406,7 +428,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat
// indicates the state of the current view : calendar or plans information
$scope.plansAreShown = false;
// will store the user's plan if he choosed to buy one
// will store the user's plan if he chose to buy one
$scope.selectedPlan = null;
// the moment when the plan selection changed for the last time, used to trigger changes in the cart
@ -524,12 +546,13 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat
* When modifying an already booked reservation, callback when the modification was successfully done.
*/
$scope.modifyMachineSlot = function () {
$scope.events.placable.title = $scope.currentUser.role !== 'admin' ? _t('app.logged.machines_reserve.i_ve_reserved') : _t('app.logged.machines_reserve.not_available');
$scope.events.placable.title = $scope.currentUser.id === $scope.events.modifiable.user.id ? _t('app.logged.machines_reserve.i_ve_reserved') : _t('app.logged.machines_reserve.not_available');
$scope.events.placable.backgroundColor = 'white';
$scope.events.placable.borderColor = $scope.events.modifiable.borderColor;
$scope.events.placable.id = $scope.events.modifiable.id;
$scope.events.placable.is_reserved = true;
$scope.events.placable.can_modify = true;
$scope.events.placable.user = angular.copy($scope.events.modifiable.user);
$scope.events.modifiable.backgroundColor = 'white';
$scope.events.modifiable.title = '';
@ -549,7 +572,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat
$scope.events.placable.backgroundColor = 'white';
$scope.events.placable.title = '';
}
$scope.events.modifiable.title = $scope.currentUser.role !== 'admin' ? _t('app.logged.machines_reserve.i_ve_reserved') : _t('app.logged.machines_reserve.not_available');
$scope.events.modifiable.title = $scope.currentUser.id === $scope.events.modifiable.user.id ? _t('app.logged.machines_reserve.i_ve_reserved') : _t('app.logged.machines_reserve.not_available');
$scope.events.modifiable.backgroundColor = 'white';
return updateCalendar();
@ -604,16 +627,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

@ -81,59 +81,73 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc
{
state: 'app.admin.calendar',
linkText: 'app.public.common.manage_the_calendar',
linkIcon: 'calendar'
linkIcon: 'calendar',
authorizedRoles: ['admin', 'manager']
},
{
state: 'app.public.machines_list',
linkText: 'app.public.common.manage_the_machines',
linkIcon: 'cogs'
linkIcon: 'cogs',
authorizedRoles: ['admin', 'manager']
},
{
state: 'app.admin.trainings',
linkText: 'app.public.common.trainings_monitoring',
linkIcon: 'graduation-cap'
linkIcon: 'graduation-cap',
authorizedRoles: ['admin', 'manager']
},
{
state: 'app.admin.events',
linkText: 'app.public.common.manage_the_events',
linkIcon: 'tags'
linkIcon: 'tags',
authorizedRoles: ['admin', 'manager']
},
{ class: 'menu-spacer' },
{
state: 'app.admin.members',
linkText: 'app.public.common.manage_the_users',
linkIcon: 'users'
linkIcon: 'users',
authorizedRoles: ['admin', 'manager']
},
{
state: 'app.admin.pricing',
linkText: 'app.public.common.subscriptions_and_prices',
linkIcon: 'money'
linkIcon: 'money',
authorizedRoles: ['admin']
},
{
state: 'app.admin.invoices',
linkText: 'app.public.common.manage_the_invoices',
linkIcon: 'file-pdf-o'
linkIcon: 'file-pdf-o',
authorizedRoles: ['admin', 'manager']
},
{
state: 'app.admin.statistics',
linkText: 'app.public.common.statistics',
linkIcon: 'bar-chart-o'
linkIcon: 'bar-chart-o',
authorizedRoles: ['admin']
},
{
class: 'menu-spacer',
authorizedRoles: ['admin']
},
{ class: 'menu-spacer' },
{
state: 'app.admin.settings',
linkText: 'app.public.common.customization',
linkIcon: 'gear'
linkIcon: 'gear',
authorizedRoles: ['admin']
},
{
state: 'app.admin.project_elements',
linkText: 'app.public.common.manage_the_projects_elements',
linkIcon: 'tasks'
linkIcon: 'tasks',
authorizedRoles: ['admin']
},
{
state: 'app.admin.open_api_clients',
linkText: 'app.public.common.open_api_clients',
linkIcon: 'cloud'
linkIcon: 'cloud',
authorizedRoles: ['admin']
}
].concat(Fablab.adminNavLinks);

View File

@ -12,8 +12,8 @@
*/
'use strict';
Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScope', '$state', '$uibModal', 'Auth', 'dialogs', 'growl', 'plansPromise', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t', 'Wallet', 'helpers',
function ($scope, $rootScope, $state, $uibModal, Auth, dialogs, growl, plansPromise, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t, Wallet, helpers) {
Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScope', '$state', '$uibModal', 'Auth', 'AuthService', 'dialogs', 'growl', 'plansPromise', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t', 'Wallet', 'helpers',
function ($scope, $rootScope, $state, $uibModal, Auth, AuthService, dialogs, growl, plansPromise, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t, Wallet, helpers) {
/* PUBLIC SCOPE */
// list of groups
@ -28,13 +28,6 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
// list of plans, classified by group
$scope.plansClassifiedByGroup = [];
for (var group of Array.from($scope.groups)) {
const groupObj = { id: group.id, name: group.name, plans: [] };
for (let plan of Array.from(plansPromise)) {
if (plan.group_id === group.id) { groupObj.plans.push(plan); }
}
$scope.plansClassifiedByGroup.push(groupObj);
}
// user to deal with
$scope.ctrl = {
@ -62,15 +55,15 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
/**
* Callback to deal with the subscription of the user selected in the dropdown list instead of the current user's
* subscription. (admins only)
* subscription. (admins and managers only)
*/
$scope.updateMember = function () {
$scope.selectedPlan = null;
$scope.paid.plan = null;
$scope.group.change = false;
return Member.get({ id: $scope.ctrl.member.id }, function (member) {
Member.get({ id: $scope.ctrl.member.id }, function (member) {
$scope.ctrl.member = member;
return $scope.group.id = $scope.ctrl.member.group_id;
$scope.group.id = $scope.ctrl.member.group_id;
});
};
@ -97,14 +90,17 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
$scope.openSubscribePlanModal = function () {
Wallet.getWalletByUser({ user_id: $scope.ctrl.member.id }, function (wallet) {
const amountToPay = helpers.getAmountToPay($scope.cart.total, wallet.amount);
if (($scope.currentUser.role !== 'admin') && (amountToPay > 0)) {
if ((AuthService.isAuthorized('member') && amountToPay > 0)
|| (AuthService.isAuthorized('manager') && $scope.ctrl.member.id === $rootScope.currentUser.id && amountToPay > 0)) {
if ($rootScope.fablabWithoutOnlinePayment) {
growl.error(_t('app.public.plans.online_payment_disabled'));
} else {
return payByStripe();
}
} else {
if (($scope.currentUser.role === 'admin') || (amountToPay === 0)) {
if (AuthService.isAuthorized('admin')
|| (AuthService.isAuthorized('manager') && $scope.ctrl.member.id !== $rootScope.currentUser.id)
|| amountToPay === 0) {
return payOnSite();
}
}
@ -115,7 +111,7 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
* Return the group object, identified by the ID set in $scope.group.id
*/
$scope.getUserGroup = function () {
for (group of Array.from($scope.groups)) {
for (const group of Array.from($scope.groups)) {
if (group.id === $scope.group.id) {
return group;
}
@ -130,7 +126,8 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
$scope.ctrl.member = user;
$scope.group.change = false;
$scope.selectedPlan = null;
if ($scope.currentUser.role !== 'admin') {
if (AuthService.isAuthorized('member') ||
(AuthService.isAuthorized('manager') && $scope.currentUser.id !== $scope.ctrl.member.id)) {
$rootScope.currentUser = user;
Auth._currentUser.group_id = user.group_id;
growl.success(_t('app.public.plans.your_group_was_successfully_changed'));
@ -139,7 +136,8 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
}
}
, function (err) {
if ($scope.currentUser.role !== 'admin') {
if (AuthService.isAuthorized('member') ||
(AuthService.isAuthorized('manager') && $scope.currentUser.id !== $scope.ctrl.member.id)) {
growl.error(_t('app.public.plans.an_error_prevented_your_group_from_being_changed'));
} else {
growl.error(_t('app.public.plans.an_error_prevented_to_change_the_user_s_group'));
@ -179,8 +177,20 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
// group all plans by Group
for (const group of $scope.groups) {
const groupObj = { id: group.id, name: group.name, plans: [], actives: 0 };
for (let plan of plansPromise) {
if (plan.group_id === group.id) {
groupObj.plans.push(plan);
if (!plan.disabled) { groupplansClassifiedByGroupObj.actives++; }
}
}
$scope.plansClassifiedByGroup.push(groupObj);
}
if ($scope.currentUser) {
if ($scope.currentUser.role !== 'admin') {
if (!AuthService.isAuthorized('admin')) {
$scope.ctrl.member = $scope.currentUser;
$scope.paid.plan = $scope.currentUser.subscribed_plan;
$scope.group.id = $scope.currentUser.group_id;
@ -201,9 +211,9 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
* Compute the total amount for the current reservation according to the previously set parameters
* and assign the result in $scope.reserve.amountTotal
*/
var updateCartPrice = function () {
// first we check that a user was selected
if (Object.keys($scope.ctrl.member).length > 0) {
const updateCartPrice = function () {
// first we check the selection of a user
if (Object.keys($scope.ctrl.member).length > 0 && $scope.selectedPlan) {
$scope.cart.total = $scope.selectedPlan.amount;
// apply the coupon if any
if ($scope.coupon.applied) {
@ -223,7 +233,7 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
/**
* Open a modal window which trigger the stripe payment process
*/
var payByStripe = function () {
const payByStripe = function () {
$uibModal.open({
templateUrl: '<%= asset_path "stripe/payment_modal.html" %>',
size: 'md',
@ -262,7 +272,7 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
CustomAsset.get({ name: 'cgv-file' }, function (cgv) { $scope.cgv = cgv.custom_asset; });
/**
* Callback for click on the 'proceed' button.
* Callback for a click on the 'proceed' button.
* Handle the stripe's card tokenization process response and save the subscription to the API with the
* card token just created.
*/
@ -283,7 +293,7 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
/**
* Open a modal window which trigger the local payment process
*/
var payOnSite = function () {
const payOnSite = function () {
$uibModal.open({
templateUrl: '<%= asset_path "plans/payment_modal.html" %>',
size: 'sm',
@ -301,7 +311,7 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
// user wallet amount
$scope.walletAmount = wallet.amount;
// subcription price, coupon subtracted if any
// subscription price, coupon subtracted if any
$scope.price = price;
// price to pay

View File

@ -98,8 +98,8 @@ class SpacesController {
/**
* Controller used in the public listing page, allowing everyone to see the list of spaces
*/
Application.Controllers.controller('SpacesController', ['$scope', '$state', 'spacesPromise', '_t', 'Member', 'uiTourService',
function ($scope, $state, spacesPromise, _t, Member, uiTourService) {
Application.Controllers.controller('SpacesController', ['$scope', '$state', 'spacesPromise', 'AuthService', '_t', 'Member', 'uiTourService',
function ($scope, $state, spacesPromise, AuthService, _t, Member, uiTourService) {
/* PUBLIC SCOPE */
// Retrieve the list of spaces
@ -131,9 +131,10 @@ Application.Controllers.controller('SpacesController', ['$scope', '$state', 'spa
*/
$scope.setupSpacesTour = function () {
// setup the tour for admins only
if ($scope.currentUser && $scope.currentUser.role === 'admin') {
if (AuthService.isAuthorized(['admin', 'manager'])) {
// get the tour defined by the ui-tour directive
const uitour = uiTourService.getTourByName('spaces');
if (AuthService.isAuthorized('admin')) {
uitour.createStep({
selector: 'body',
stepId: 'welcome',
@ -153,10 +154,31 @@ Application.Controllers.controller('SpacesController', ['$scope', '$state', 'spa
placement: 'top'
});
}
} else {
uitour.createStep({
selector: 'body',
stepId: 'welcome_manager',
order: 0,
title: _t('app.public.tour.spaces.welcome_manager.title'),
content: _t('app.public.tour.spaces.welcome_manager.content'),
placement: 'bottom',
orphan: true
});
}
if ($scope.spaces.length > 0) {
uitour.createStep({
selector: '.spaces-list .reserve-button',
stepId: 'reserve',
order: 2,
title: _t('app.public.tour.spaces.reserve.title'),
content: _t('app.public.tour.spaces.reserve.content'),
placement: 'top'
});
}
uitour.createStep({
selector: 'body',
stepId: 'conclusion',
order: 2,
order: 3,
title: _t('app.public.tour.conclusion.title'),
content: _t('app.public.tour.conclusion.content'),
placement: 'bottom',

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: {
@ -74,38 +74,12 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
* @param slot {Object} fullCalendar event object
*/
$scope.validateSlot = function (slot) {
let sameTimeReservations = [
'training_reservations',
'machine_reservations',
'space_reservations',
'events_reservations'
].map(function (k) {
return _.filter($scope.user[k], function(r) {
return slot.start.isSame(r.start_at) ||
(slot.end.isAfter(r.start_at) && slot.end.isBefore(r.end_at)) ||
(slot.start.isAfter(r.start_at) && slot.start.isBefore(r.end_at)) ||
(slot.start.isBefore(r.start_at) && slot.end.isAfter(r.end_at));
})
});
sameTimeReservations = _.union.apply(null, sameTimeReservations);
if (sameTimeReservations.length > 0) {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '<%= asset_path "shared/_reserve_slot_same_time.html" %>',
size: 'md',
controller: 'ReserveSlotSameTimeController',
resolve: {
sameTimeReservations: function() { return sameTimeReservations; }
}
});
modalInstance.result.then(function(res) {
validateTags(slot, function () {
validateSameTimeReservations(slot, function () {
slot.isValid = true;
return updateCartPrice();
});
} else {
slot.isValid = true;
return updateCartPrice();
}
updateCartPrice();
})
})
};
/**
@ -167,7 +141,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 +169,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 +190,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'));
}
};
@ -286,10 +260,18 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
};
/**
* Check if the currently logged user has teh 'admin' role?
* Check if the currently logged user has the 'admin' role OR the 'manager' role, but is not taking reseravtion for himself
* @returns {boolean}
*/
$scope.isAdmin = function () { return $rootScope.currentUser && ($rootScope.currentUser.role === 'admin'); };
$scope.isAuthorized = function () {
if (AuthService.isAuthorized('admin')) return true;
if (AuthService.isAuthorized('manager')) {
return ($rootScope.currentUser.id !== $scope.user.id);
}
return false;
}
/* PRIVATE SCOPE */
@ -322,14 +304,84 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
});
};
/**
* Validates that the current slot is reserved by a member with an authorized tag. Admin and managers can overpass
* the mismatch.
* @param slot {Object} fullCalendar event object.
* @param callback {function}
*/
const validateTags = function (slot, callback) {
const interTags = _.intersection.apply(null, [slot.tag_ids, $scope.user.tag_ids]);
if (slot.tag_ids.length === 0 || interTags.length > 0) {
if (typeof callback === 'function') callback();
} else {
// ask confirmation
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '<%= asset_path "shared/_reserve_slot_tags_mismatch.html" %>',
size: 'md',
controller: 'ReserveSlotTagsMismatchController',
resolve: {
slotTags: function() { return slot.tags; },
userTags: function () { return $scope.user.tags; },
userName: function () { return $scope.user.name; }
}
});
modalInstance.result.then(function(res) {
if (typeof callback === 'function') callback(res);
});
}
}
/**
* Validates that no other reservations were made that conflict the current slot and alert the user about the conflict.
* If the user is an administrator or a manager, he can overpass the conflict.
* @param slot {Object} fullCalendar event object.
* @param callback {function}
*/
const validateSameTimeReservations = function (slot, callback) {
let sameTimeReservations = [
'training_reservations',
'machine_reservations',
'space_reservations',
'events_reservations'
].map(function (k) {
return _.filter($scope.user[k], function(r) {
return slot.start.isSame(r.start_at) ||
(slot.end.isAfter(r.start_at) && slot.end.isBefore(r.end_at)) ||
(slot.start.isAfter(r.start_at) && slot.start.isBefore(r.end_at)) ||
(slot.start.isBefore(r.start_at) && slot.end.isAfter(r.end_at));
})
});
sameTimeReservations = _.union.apply(null, sameTimeReservations);
if (sameTimeReservations.length > 0) {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '<%= asset_path "shared/_reserve_slot_same_time.html" %>',
size: 'md',
controller: 'ReserveSlotSameTimeController',
resolve: {
sameTimeReservations: function() { return sameTimeReservations; }
}
});
modalInstance.result.then(function(res) {
if (typeof callback === 'function') callback(res);
});
} else {
if (typeof callback === 'function') callback();
}
}
/**
* 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 +390,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 +452,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 +487,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 +500,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 +511,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 +521,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,27 +540,27 @@ 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) {
$scope.amountTotal = res.price;
$scope.totalNoCoupon = res.price_without_coupon;
return setSlotsDetails(res.details);
setSlotsDetails(res.details);
});
} else {
// otherwise we alert, this error musn't occur when the current user is not admin
growl.warning(_t('app.shared.cart.please_select_a_member_first'));
return $scope.amountTotal = null;
$scope.amountTotal = null;
}
};
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)) {
slot.promo = s.promo;
return slot.price = s.price;
slot.price = s.price;
}
});
});
@ -518,7 +572,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 +586,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 +609,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 +666,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 +735,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 +751,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);
}
}
@ -724,12 +781,38 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
]);
/**
* Controller of modal for show reservations the same date at the same time
* Controller of the modal showing the reservations the same date at the same time
*/
Application.Controllers.controller('ReserveSlotSameTimeController', ['$scope', '$uibModalInstance', 'sameTimeReservations', 'growl', '_t',
function ($scope, $uibModalInstance, sameTimeReservations, growl, _t) {
Application.Controllers.controller('ReserveSlotSameTimeController', ['$scope', '$uibModalInstance', 'AuthService', 'sameTimeReservations',
function ($scope, $uibModalInstance, AuthService, sameTimeReservations) {
$scope.sameTimeReservations = sameTimeReservations;
$scope.bookSlotAtSameTime = Fablab.bookSlotAtSameTime;
$scope.isAuthorized = AuthService.isAuthorized;
/**
* Confirmation callback
*/
$scope.ok = function () {
$uibModalInstance.close({});
}
/**
* Cancellation callback
*/
$scope.cancel = function () {
$uibModalInstance.dismiss('cancel');
}
}
]);
/**
* Controller of the modal showing the slot tags
*/
Application.Controllers.controller('ReserveSlotTagsMismatchController', ['$scope', '$uibModalInstance', 'AuthService', 'slotTags', 'userTags', 'userName',
function ($scope, $uibModalInstance, AuthService, slotTags, userTags, userName) {
$scope.slotTags = slotTags;
$scope.userTags = userTags;
$scope.userName = userName;
$scope.isAuthorized = AuthService.isAuthorized;
/**
* Confirmation callback
*/

View File

@ -55,7 +55,7 @@ angular.module('application.router', ['ui.router'])
.state('app.logged', {
abstract: true,
data: {
authorizedRoles: ['member', 'admin']
authorizedRoles: ['member', 'admin', 'manager']
},
resolve: {
currentUser: ['Auth', function (Auth) { return Auth.currentUser(); }],
@ -68,7 +68,7 @@ angular.module('application.router', ['ui.router'])
.state('app.admin', {
abstract: true,
data: {
authorizedRoles: ['admin']
authorizedRoles: ['admin', 'manager']
},
resolve: {
currentUser: ['Auth', function (Auth) { return Auth.currentUser(); }],
@ -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; }]
@ -929,8 +931,21 @@ angular.module('application.router', ['ui.router'])
}
}
})
.state('app.admin.managers_new', {
url: '/admin/managers/new',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/managers/new.html" %>',
controller: 'NewManagerController'
}
},
resolve: {
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }]
}
})
// authentification providers
// authentication providers
.state('app.admin.authentication_new', {
url: '/admin/authentications/new',
views: {

View File

@ -1,9 +1,8 @@
'use strict';
Application.Services.factory('_t', ['$filter', function ($filter) {
return function (key, interpolation, options) {
if (interpolation == null) { interpolation = undefined; }
if (options == null) { options = undefined; }
return $filter('translate')(key, interpolation, options);
Application.Services.factory('_t', ['$translate', function ($translate) {
return function (key, interpolations) {
if (interpolations == null) { interpolations = undefined; }
return $translate.instant(key, interpolations);
};
}]);

View File

@ -1,6 +1,7 @@
'use strict';
Application.Services.factory('Help', ['$rootScope', '$uibModal', '$state', function ($rootScope, $uibModal, $state) {
Application.Services.factory('Help', ['$rootScope', '$uibModal', '$state', 'AuthService',
function ($rootScope, $uibModal, $state, AuthService) {
const TOURS = {
'app.public.home': 'welcome',
'app.public.machines_list': 'machines',
@ -19,7 +20,7 @@ Application.Services.factory('Help', ['$rootScope', '$uibModal', '$state', funct
return function (e) {
if (!$rootScope.currentUser || $rootScope.currentUser.role !== 'admin') return;
if (!AuthService.isAuthorized(['admin', 'manager'])) return;
if (e.key === 'F1') {
e.preventDefault();

View File

@ -44,6 +44,10 @@ Application.Services.factory('Member', ['$resource', '$q', function ($resource,
return response.data;
}
}
},
updateRole: {
method: 'PATCH',
url: '/api/members/:id/update_role'
}
}
);

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

@ -678,3 +678,11 @@ body.container {
left: -4px;
}
}
.middle-of-inputs {
line-height: 24px;
padding: 6px;
text-align: center;
vertical-align: middle;
}

View File

@ -107,7 +107,6 @@
> li.menu-spacer {
height: 1px;
margin: 6px 80% 6px 5px;
background: linear-gradient(45deg, black, transparent);
}
ul {
@ -535,3 +534,13 @@
display: inline-block !important;
padding: 11px 44px !important;
}
li.level-2-tab > a {
line-height: 14px;
font-size: 12px;
}
li.active.level-2-tab > a {
background: linear-gradient(#eee, #fff);
}

View File

@ -0,0 +1,4 @@
.promote-member img {
width: 16px;
height: 21px;
}

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="isAuthorized('admin')">
<a class="btn btn-default export-xls-button"
ng-href="api/availabilities/export_index.xlsx"
target="export-frame"
@ -58,6 +58,21 @@
<iframe name="export-frame" height="0" width="0" class="none"></iframe>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="availability" ng-hide="availability.available_type == 'event'">
<div class="panel-heading b-b small">
<h3><i class="fa fa-info-circle m-r" aria-hidden="true"></i><span translate>{{ 'app.admin.calendar.info' }}</span></h3>
</div>
<div class="widget-content no-bg auto wrapper">
<div translate translate-values="{DURATION: availability.slot_duration}">{{ 'app.admin.calendar.slot_duration' }}</div>
<div class="m-t-sm" ng-show="availability.tags.length > 0">
<span translate>{{ 'app.admin.calendar.tags' }}</span>
<ul>
<li ng-repeat="tag in availability.tags">{{tag.name}}</li>
</ul>
</div>
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="availability" ng-hide="availability.available_type == 'event'">
<div class="panel-heading b-b small">
<h3 translate>{{ 'app.admin.calendar.ongoing_reservations' }}</h3>
@ -71,7 +86,7 @@
<span class="btn btn-warning btn-xs" ng-click="cancelBooking(r)" ng-if="!r.canceled_at"><i class="fa fa-times red"></i></span>
</li>
</ul>
<div ng-show="reservations.length == 0" translate>{{ 'app.admin.calendar.no_reservations' }}</div>
<div ng-show="reservations.length == 0" translate>{{ 'app.admin.calendar.without_reservation' }}</div>
<div class="m-t" ng-show="availability.lock"><i class="fa fa-ban"></i> <span class="m-l-xs" translate>{{ 'app.admin.calendar.reservations_locked' }}</span></div>
</div>
</div>

View File

@ -75,6 +75,26 @@
</div>
</div>
<div class="modal-body" ng-show="step === 3">
<div id="slotDuration" class="m-t-sm" ng-show="isTypeDivided()">
<p class="text-center font-sbold" translate>{{ 'app.admin.calendar.divide_this_availability' }}</p>
<div class="row">
<div class="col-md-5">
<div class="input-group">
<input type="number" class="form-control" ng-model="slots_nb" step="1" />
<span class="input-group-addon" translate>{{ 'app.admin.calendar.slots' }}</span>
</div>
</div>
<p class="col-md-2 middle-of-inputs" translate>
{{ 'app.admin.calendar.slots_of' }}
</p>
<div class="col-md-5">
<div class="input-group">
<input type="number" class="form-control" ng-model="availability.slot_duration" step="5" />
<span class="input-group-addon" translate>{{ 'app.admin.calendar.minutes' }}</span>
</div>
</div>
</div>
</div>
<div id="timeAdjust" class="m-t-sm">
<p class="text-center font-sbold" translate>{{ 'app.admin.calendar.adjust_the_opening_hours' }}</p>
<div class="row">
@ -204,7 +224,7 @@
</ul>
<div class="alert alert-info text-xs">
<i class="fa fa-lightbulb-o m-r" aria-hidden="true"></i>
<span translate translate-values="{DURATION: slotDuration, COUNT: occurrences.length}"> {{ 'app.admin.calendar.divided_in_slots' }}</span>
<span translate translate-values="{DURATION: availability.slot_duration, COUNT: occurrences.length}"> {{ 'app.admin.calendar.divided_in_slots' }}</span>
</div>
<div>
<span class="underline" translate>{{ 'app.admin.calendar.reservable' }}</span>

View File

@ -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'])">
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md" ng-if="isAuthorized(['admin', '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.admin.events_new" role="button" translate>{{ 'app.admin.events.add_an_event' }}</a>
</section>
@ -26,7 +26,7 @@
ui-tour-scroll-parent-id="content-main"
post-render="setupEventsTour">
<div class="row">
<div class="col-md-12">
<div class="col-md-12" ng-if="isAuthorized('admin')">
<uib-tabset justified="true" active="tabs.active">
<uib-tab heading="{{ 'app.admin.events.events_monitoring' | translate }}" index="0">
<ng-include src="'<%= asset_path "admin/events/monitoring.html" %>'"></ng-include>
@ -41,6 +41,9 @@
</uib-tab>
</uib-tabset>
</div>
<div class="col-md-12" ng-if="isAuthorized('manager')">
<ng-include src="'<%= asset_path "admin/events/monitoring.html" %>'"></ng-include>
</div>
</div>
</section>
</section>

View File

@ -51,7 +51,7 @@
<td style="vertical-align:middle">
<span class="ng-binding" ng-if="event.nb_total_places > 0">{{ event.nb_total_places - event.nb_free_places }} / {{ event.nb_total_places }}</span>
<span class="badge font-sbold cancelled" ng-if="event.nb_total_places == -1" translate>{{ 'app.admin.events.cancelled' }}</span>
<span class="badge font-sbold" ng-if="!event.nb_total_places" translate>{{ 'app.admin.events.free_entry' }}</span>
<span class="badge font-sbold" ng-if="!event.nb_total_places" translate>{{ 'app.admin.events.without_reservation' }}</span>
</td>
<td style="vertical-align:middle">

View File

@ -0,0 +1,111 @@
<div class="panel panel-default m-t-md accounting-codes">
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<label for="journalCode" translate>{{ 'app.admin.invoices.accounting_journal_code' }}</label>
<input type="text" id="journalCode" ng-model="settings.journalCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_journal_code' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="cardClientCode" translate>{{ 'app.admin.invoices.accounting_card_client_code' }}</label>
<input type="text" id="cardClientCode" ng-model="settings.cardClientCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.card_client_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="cardClientLabel" translate>{{ 'app.admin.invoices.accounting_card_client_label' }}</label>
<input type="text" id="cardClientLabel" ng-model="settings.cardClientLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.card_client_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="walletClientCode" translate>{{ 'app.admin.invoices.accounting_wallet_client_code' }}</label>
<input type="text" id="walletClientCode" ng-model="settings.walletClientCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.wallet_client_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="walletClientLabel" translate>{{ 'app.admin.invoices.accounting_wallet_client_label' }}</label>
<input type="text" id="walletClientLabel" ng-model="settings.walletClientLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.wallet_client_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="otherClientCode" translate>{{ 'app.admin.invoices.accounting_other_client_code' }}</label>
<input type="text" id="otherClientCode" ng-model="settings.otherClientCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.other_client_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="otherClientLabel" translate>{{ 'app.admin.invoices.accounting_other_client_label' }}</label>
<input type="text" id="otherClientLabel" ng-model="settings.otherClientLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.other_client_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="walletCode" translate>{{ 'app.admin.invoices.accounting_wallet_code' }}</label>
<input type="text" id="walletCode" ng-model="settings.walletCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_wallet_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="walletLabel" translate>{{ 'app.admin.invoices.accounting_wallet_label' }}</label>
<input type="text" id="walletLabel" ng-model="settings.walletLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_wallet_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="vatCode" translate>{{ 'app.admin.invoices.accounting_vat_code' }}</label>
<input type="text" id="vatCode" ng-model="settings.vatCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_vat_code' | translate }}"/>
</div>
<div class="col-md-6">
<label for="vatLabel" translate>{{ 'app.admin.invoices.accounting_vat_label' }}</label>
<input type="text" id="vatLabel" ng-model="settings.vatLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_vat_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="subscriptionCode" translate>{{ 'app.admin.invoices.accounting_subscription_code' }}</label>
<input type="text" id="subscriptionCode" ng-model="settings.subscriptionCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_subscription_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="subscriptionLabel" translate>{{ 'app.admin.invoices.accounting_subscription_label' }}</label>
<input type="text" id="subscriptionLabel" ng-model="settings.subscriptionLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_subscription_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="machineCode" translate>{{ 'app.admin.invoices.accounting_Machine_code' }}</label>
<input type="text" id="machineCode" ng-model="settings.machineCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_machine_code' | translate }}"/>
</div>
<div class="col-md-6">
<label for="machineLabel" translate>{{ 'app.admin.invoices.accounting_Machine_label' }}</label>
<input type="text" id="machineLabel" ng-model="settings.machineLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_machine_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="trainingCode" translate>{{ 'app.admin.invoices.accounting_Training_code' }}</label>
<input type="text" id="trainingCode" ng-model="settings.trainingCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_training_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="trainingLabel" translate>{{ 'app.admin.invoices.accounting_Training_label' }}</label>
<input type="text" id="trainingLabel" ng-model="settings.trainingLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_training_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="eventCode" translate>{{ 'app.admin.invoices.accounting_Event_code' }}</label>
<input type="text" id="eventCode" ng-model="settings.eventCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_event_code' | translate }}"/>
</div>
<div class="col-md-6">
<label for="eventLabel" translate>{{ 'app.admin.invoices.accounting_Event_label' }}</label>
<input type="text" id="eventLabel" ng-model="settings.eventLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_event_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="spaceCode" translate>{{ 'app.admin.invoices.accounting_Space_code' }}</label>
<input type="text" id="spaceCode" ng-model="settings.spaceCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_space_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="spaceLabel" translate>{{ 'app.admin.invoices.accounting_Space_label' }}</label>
<input type="text" id="spaceLabel" ng-model="settings.spaceLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_space_label' | translate }}"/>
</div>
</div>
<button name="button" class="btn btn-warning m-t-lg" ng-click="save()" translate>{{ 'app.shared.buttons.save' }}</button>
</div>
</div>

View File

@ -11,7 +11,7 @@
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3">
<section class="heading-actions wrapper">
<section class="heading-actions wrapper" ng-show="isAuthorized('admin')">
<a class="btn btn-default rounded m-t-sm export-accounting-button" ng-click="toggleExportModal()"><i class="fa fa-book"></i></a>
<iframe name="export-frame" height="0" width="0" class="none" id="accounting-export-frame"></iframe>
<a class="btn btn-lg btn-default rounded m-t-sm text-sm close-accounting-periods-button" ng-click="closeAnAccountingPeriod()"><i class="fa fa-calendar-check-o"></i> {{ 'app.admin.invoices.accounting_periods' | translate }}</a>
@ -28,530 +28,25 @@
ui-tour-scroll-parent-id="content-main"
post-render="setupInvoicesTour">
<div class="row">
<div class="col-md-12">
<div class="col-md-12" ng-if="isAuthorized('admin')">
<uib-tabset justified="true" active="tabs.active">
<uib-tab heading="{{ 'app.admin.invoices.invoices_list' | translate }}" ng-hide="fablabWithoutInvoices" index="0">
<h3 class="m-t-xs"><i class="fa fa-filter"></i> {{ 'app.admin.invoices.filter_invoices' | translate }}</h3>
<ng-include src="'<%= asset_path "admin/invoices/list.html" %>'"></ng-include>
</uib-tab>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon" translate>{{ 'app.admin.invoices.invoice_num_' }}</span>
<input type="text" ng-model="searchInvoice.reference" class="form-control" placeholder="" ng-change="handleFilterChange()">
</div>
</div>
</div>
<uib-tab heading="{{ 'app.admin.invoices.invoicing_settings' | translate }}" index="1" class="invoices-settings">
<ng-include src="'<%= asset_path "admin/invoices/settings.html" %>'"></ng-include>
</uib-tab>
<div class="col-md-4">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon" translate>{{ 'app.admin.invoices.customer_' }}</span>
<input type="text" ng-model="searchInvoice.name" class="form-control" placeholder="" ng-change="handleFilterChange()">
</div>
</div>
</div>
<uib-tab heading="{{ 'app.admin.invoices.accounting_codes' | translate }}" index="2" class="accounting-codes-tab">
<ng-include src="'<%= asset_path "admin/invoices/codes.html" %>'"></ng-include>
</uib-tab>
</uib-tabset>
</div>
<div class="col-md-4">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon">{{ "app.admin.invoices.date_" | translate }}</span>
<input type="date" ng-model="searchInvoice.date" class="form-control" ng-change="handleFilterChange()">
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-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: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: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: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>
<th style="width:30%"></th>
</tr>
</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>
<td>{{ invoice.reference }}</td>
<td ng-if="!invoice.is_avoir">{{ invoice.date | amDateFormat:'L LTS' }}</td>
<td ng-if="invoice.is_avoir">{{ invoice.date | amDateFormat:'L' }}</td>
<td>{{ invoice.total | currency}}</td>
<td>
<a href="" ui-sref="app.admin.members_edit({id: invoice.user_id})" ng-show="invoice.user_id">{{ invoice.name }}</a>
<span ng-hide="invoice.user_id">{{ invoice.name }}</span>
<td>
<div class="buttons">
<a class="btn btn-default download-button" ng-href="api/invoices/{{invoice.id}}/download" target="_blank" ng-if="!invoice.is_avoir">
<i class="fa fa-file-pdf-o"></i> {{ 'app.admin.invoices.download_the_invoice' | translate }}
</a>
<a class="btn btn-default" ng-href="api/invoices/{{invoice.id}}/download" target="_blank" ng-if="invoice.is_avoir">
<i class="fa fa-file-pdf-o"></i> {{ 'app.admin.invoices.download_the_credit_note' | translate }}
</a>
<a class="btn btn-default refund-button" ng-click="generateAvoirForInvoice(invoice)" ng-if="(!invoice.has_avoir || invoice.has_avoir == 'partial') && !invoice.is_avoir && !invoice.prevent_refund && !isDateClosed(invoice.created_at)">
<i class="fa fa-reply"></i> {{ 'app.admin.invoices.credit_note' | translate }}
</a>
</div>
</td>
</tr>
</tbody>
</table>
<div class="text-center">
<button class="btn btn-warning" ng-click="showNextInvoices()" ng-hide="noMoreResults"><i class="fa fa-search-plus" aria-hidden="true"></i> {{ 'app.admin.invoices.display_more_invoices' | translate }}</button>
</div>
<p ng-if="invoices.length == 0" translate>{{ 'app.admin.invoices.no_invoices_for_now' }}</p>
</div>
</div>
</uib-tab>
<uib-tab heading="{{ 'app.admin.invoices.invoicing_settings' | translate }}" index="1" class="invoices-settings">
<div class="alert alert-warning p-md m-t" role="alert" ng-show="fablabWithoutInvoices">
<i class="fa fa-warning m-r"></i>
<span translate>{{ 'app.admin.invoices.warning_invoices_disabled' }}</span>
</div>
<form class="invoice-placeholder">
<div class="invoice-logo">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon" bs-holder ng-if="!invoice.logo" class="img-responsive">
<img base-sixty-four-image="invoice.logo" ng-if="invoice.logo && invoice.logo.base64">
<div class="tools-box">
<div class="btn-group">
<div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'app.admin.invoices.change_logo' | translate }}
<input type="file" accept="image/png,image/jpeg,image/x-png,image/pjpeg" name="invoice[logo][attachment]" ng-model="invoice.logo" base-sixty-four-input>
</div>
</div>
</div>
</div>
<div class="invoice-buyer-infos">
<strong translate>{{ 'app.admin.invoices.john_smith' }}</strong>
<div translate>{{ 'app.admin.invoices.john_smith_at_example_com' }}</div>
</div>
<div class="invoice-reference invoice-editable" ng-click="openEditReference()">{{ 'app.admin.invoices.invoice_reference_' | translate }} {{mkReference()}}</div>
<div class="invoice-code invoice-editable" ng-show="invoice.code.active" ng-click="openEditCode()">{{ 'app.admin.invoices.code_' | translate }} {{invoice.code.model}}</div>
<div class="invoice-code invoice-activable" ng-show="!invoice.code.active" ng-click="openEditCode()" translate>{{ 'app.admin.invoices.code_disabled' }}</div>
<div class="invoice-order invoice-editable" ng-click="openEditInvoiceNb()"> {{ 'app.admin.invoices.order_num' | translate }} {{mkNumber()}}</div>
<div class="invoice-date">{{ 'app.admin.invoices.invoice_issued_on_DATE_at_TIME' | translate:{DATE:(today | amDateFormat:'L'), TIME:(today | amDateFormat:'LT')} }}</div>
<div class="invoice-object">
{{ 'app.admin.invoices.object_reservation_of_john_smith_on_DATE_at_TIME' | translate:{DATE:(inOneWeek | amDateFormat:'L'), TIME:(inOneWeek | amDateFormat:'LT')} }}
</div>
<div class="invoice-data">
{{ 'app.admin.invoices.order_summary' | translate }}
<table>
<thead>
<tr>
<th translate>{{ 'app.admin.invoices.details' }}</th>
<th class="right" translate>{{ 'app.admin.invoices.amount' }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ 'app.admin.invoices.machine_booking-3D_printer' | translate }} {{inOneWeek | amDateFormat:'LLL'}} - {{inOneWeekAndOneHour | amDateFormat:'LT'}}</td>
<td class="right">{{30.0 | currency}}</td>
</tr>
<tr class="invoice-total" ng-class="{'bold vat-line':invoice.VAT.active}">
<td ng-show="!invoice.VAT.active" translate>{{ 'app.admin.invoices.total_amount' }}</td>
<td ng-show="invoice.VAT.active" translate>{{ 'app.admin.invoices.total_including_all_taxes' }}</td>
<td class="right">{{30.0 | currency}}</td>
</tr>
<tr class="invoice-vat invoice-activable" ng-click="openEditVAT()" ng-show="!invoice.VAT.active">
<td translate>{{ 'app.admin.invoices.VAT_disabled' }}</td>
<td></td>
</tr>
<tr class="invoice-vat invoice-editable vat-line italic" ng-click="openEditVAT()" ng-show="invoice.VAT.active">
<td>{{ 'app.admin.invoices.including_VAT' | translate }} {{invoice.VAT.rate}} %</td>
<td>{{30-(30/(invoice.VAT.rate/100+1)) | currency}}</td>
</tr>
<tr class="invoice-ht vat-line italic" ng-show="invoice.VAT.active">
<td translate>{{ 'app.admin.invoices.including_total_excluding_taxes' }}</td>
<td>{{30/(invoice.VAT.rate/100+1) | currency}}</td>
</tr>
<tr class="invoice-payed vat-line bold" ng-show="invoice.VAT.active">
<td translate>{{ 'app.admin.invoices.including_amount_payed_on_ordering' }}</td>
<td>{{30.0 | currency}}</td>
</tr>
</tbody>
</table>
<p class="invoice-payment" translate translate-values="{DATE:(today | amDateFormat:'L'), TIME:(today | amDateFormat:'LT'), AMOUNT:(30.0 | currency)}">
{{ 'app.admin.invoices.settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT' }}
</p>
</div>
<div medium-editor class="invoice-text invoice-editable" ng-model="invoice.text.content"
options='{
"placeholder": "{{ "app.admin.invoices.important_notes" | translate }}",
"buttons": ["underline"]
}'
ng-blur="textEditEnd($event)">
</div>
<div medium-editor class="invoice-legals invoice-editable" ng-model="invoice.legals.content"
options='{
"placeholder": "{{ "app.admin.invoices.address_and_legal_information" | translate }}",
"buttons": ["bold", "underline"]
}'
ng-blur="legalsEditEnd($event)">
</div>
</form>
</uib-tab>
<uib-tab heading="{{ 'app.admin.invoices.accounting_codes' | translate }}" index="2" class="accounting-codes-tab">
<div class="panel panel-default m-t-md accounting-codes">
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<label for="journalCode" translate>{{ 'app.admin.invoices.accounting_journal_code' }}</label>
<input type="text" id="journalCode" ng-model="settings.journalCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_journal_code' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="cardClientCode" translate>{{ 'app.admin.invoices.accounting_card_client_code' }}</label>
<input type="text" id="cardClientCode" ng-model="settings.cardClientCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.card_client_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="cardClientLabel" translate>{{ 'app.admin.invoices.accounting_card_client_label' }}</label>
<input type="text" id="cardClientLabel" ng-model="settings.cardClientLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.card_client_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="walletClientCode" translate>{{ 'app.admin.invoices.accounting_wallet_client_code' }}</label>
<input type="text" id="walletClientCode" ng-model="settings.walletClientCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.wallet_client_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="walletClientLabel" translate>{{ 'app.admin.invoices.accounting_wallet_client_label' }}</label>
<input type="text" id="walletClientLabel" ng-model="settings.walletClientLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.wallet_client_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="otherClientCode" translate>{{ 'app.admin.invoices.accounting_other_client_code' }}</label>
<input type="text" id="otherClientCode" ng-model="settings.otherClientCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.other_client_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="otherClientLabel" translate>{{ 'app.admin.invoices.accounting_other_client_label' }}</label>
<input type="text" id="otherClientLabel" ng-model="settings.otherClientLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.other_client_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="walletCode" translate>{{ 'app.admin.invoices.accounting_wallet_code' }}</label>
<input type="text" id="walletCode" ng-model="settings.walletCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_wallet_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="walletLabel" translate>{{ 'app.admin.invoices.accounting_wallet_label' }}</label>
<input type="text" id="walletLabel" ng-model="settings.walletLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_wallet_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="vatCode" translate>{{ 'app.admin.invoices.accounting_vat_code' }}</label>
<input type="text" id="vatCode" ng-model="settings.vatCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_vat_code' | translate }}"/>
</div>
<div class="col-md-6">
<label for="vatLabel" translate>{{ 'app.admin.invoices.accounting_vat_label' }}</label>
<input type="text" id="vatLabel" ng-model="settings.vatLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_vat_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="subscriptionCode" translate>{{ 'app.admin.invoices.accounting_subscription_code' }}</label>
<input type="text" id="subscriptionCode" ng-model="settings.subscriptionCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_subscription_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="subscriptionLabel" translate>{{ 'app.admin.invoices.accounting_subscription_label' }}</label>
<input type="text" id="subscriptionLabel" ng-model="settings.subscriptionLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_subscription_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="machineCode" translate>{{ 'app.admin.invoices.accounting_Machine_code' }}</label>
<input type="text" id="machineCode" ng-model="settings.machineCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_machine_code' | translate }}"/>
</div>
<div class="col-md-6">
<label for="machineLabel" translate>{{ 'app.admin.invoices.accounting_Machine_label' }}</label>
<input type="text" id="machineLabel" ng-model="settings.machineLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_machine_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="trainingCode" translate>{{ 'app.admin.invoices.accounting_Training_code' }}</label>
<input type="text" id="trainingCode" ng-model="settings.trainingCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_training_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="trainingLabel" translate>{{ 'app.admin.invoices.accounting_Training_label' }}</label>
<input type="text" id="trainingLabel" ng-model="settings.trainingLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_training_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="eventCode" translate>{{ 'app.admin.invoices.accounting_Event_code' }}</label>
<input type="text" id="eventCode" ng-model="settings.eventCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_event_code' | translate }}"/>
</div>
<div class="col-md-6">
<label for="eventLabel" translate>{{ 'app.admin.invoices.accounting_Event_label' }}</label>
<input type="text" id="eventLabel" ng-model="settings.eventLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_event_label' | translate }}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label for="spaceCode" translate>{{ 'app.admin.invoices.accounting_Space_code' }}</label>
<input type="text" id="spaceCode" ng-model="settings.spaceCode.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_space_code' | translate }}" />
</div>
<div class="col-md-6">
<label for="spaceLabel" translate>{{ 'app.admin.invoices.accounting_Space_label' }}</label>
<input type="text" id="spaceLabel" ng-model="settings.spaceLabel.value" class="form-control" placeholder="{{ 'app.admin.invoices.general_space_label' | translate }}"/>
</div>
</div>
<button name="button" class="btn btn-warning m-t-lg" ng-click="save()" translate>{{ 'app.shared.buttons.save' }}</button>
</div>
</div>
</uib-tab>
</uib-tabset>
</div>
<div class="col-md-12" ng-if="isAuthorized('manager')">
<ng-include src="'<%= asset_path "admin/invoices/list.html" %>'"></ng-include>
</div>
</div>
</section>
<script type="text/ng-template" id="editReference.html">
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'app.admin.invoices.invoice_reference' }}</h3>
</div>
<div class="modal-body row">
<div class="elements col-md-4">
<h4>Éléments</h4>
<ul>
<li ng-click="invoice.reference.help = 'addYear.html'">{{ 'app.admin.invoices.year' | translate }}</li>
<li ng-click="invoice.reference.help = 'addMonth.html'">{{ 'app.admin.invoices.month' | translate }}</li>
<li ng-click="invoice.reference.help = 'addDay.html'">{{ 'app.admin.invoices.day' | translate }}</li>
<li ng-click="invoice.reference.help = 'addInvoiceNumber.html'">{{ 'app.admin.invoices.num_of_invoice' | translate }}</li>
<li ng-click="invoice.reference.help = 'addOnlineInfo.html'">{{ 'app.admin.invoices.online_sales' | translate }}</li>
<%# <li ng-click="invoice.reference.help = 'addWalletInfo.html'">{{ 'app.admin.invoices.wallet' | translate }}</li> %>
<li ng-click="invoice.reference.help = 'addRefundInfo.html'">{{ 'app.admin.invoices.refund' | translate }}</li>
</ul>
</div>
<div class="col-md-8">
<div class="model">
<h4 translate>{{ 'app.admin.invoices.model' }}</h4>
<input type="text" class="form-control" ng-model="model">
</div>
<div class="help">
<h4 translate>{{ 'app.admin.invoices.documentation' }}</h4>
<ng-include src="invoice.reference.help" autoscroll="true">
</ng-include>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>
</script>
<script type="text/ng-template" id="addYear.html">
<table class="invoice-element-legend">
<tr><td><strong>YY</strong></td><td translate>{{ 'app.admin.invoices.2_digits_year' }}</td></tr>
<tr><td><strong>YYYY</strong></td><td translate>{{ 'app.admin.invoices.4_digits_year' }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addMonth.html">
<table class="invoice-element-legend">
<tr><td><strong>M</strong></td><td translate>{{ 'app.admin.invoices.month_number' }}</td></tr>
<tr><td><strong>MM</strong></td><td translate>{{ 'app.admin.invoices.2_digits_month_number' }}</td></tr>
<tr><td><strong>MMM</strong></td><td translate>{{ 'app.admin.invoices.3_characters_month_name' }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addDay.html">
<table class="invoice-element-legend">
<tr><td><strong>D</strong></td><td translate>{{ 'app.admin.invoices.day_in_the_month' }}</td></tr>
<tr><td><strong>DD</strong></td><td translate>{{ 'app.admin.invoices.2_digits_day_in_the_month' }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addInvoiceNumber.html">
<table class="invoice-element-legend">
<tr><td><strong>dd...dd</strong></td><td translate>{{ 'app.admin.invoices.n_digits_daily_count_of_invoices' }}</td></tr>
<tr><td><strong>mm...mm</strong></td><td translate>{{ 'app.admin.invoices.n_digits_monthly_count_of_invoices' }}</td></tr>
<tr><td><strong>yy...yy</strong></td><td translate>{{ 'app.admin.invoices.n_digits_annual_amount_of_invoices' }}</td></tr>
</table>
<span class="bottom-notes" translate>{{ 'app.admin.invoices.beware_if_the_number_exceed_the_specified_length_it_will_be_truncated_by_the_left' }}</span>
</script>
<script type="text/ng-template" id="addOrderNumber.html">
<table class="invoice-element-legend">
<tr><td><strong>nn...nn</strong></td><td translate>{{ 'app.admin.invoices.n_digits_count_of_orders' }}</td></tr>
<tr><td><strong>dd...dd</strong></td><td translate>{{ 'app.admin.invoices.n_digits_daily_count_of_orders' }}</td></tr>
<tr><td><strong>mm...mm</strong></td><td translate>{{ 'app.admin.invoices.n_digits_monthly_count_of_orders' }}</td></tr>
<tr><td><strong>yy...yy</strong></td><td translate>{{ 'app.admin.invoices.n_digits_annual_amount_of_orders' }}</td></tr>
</table>
<span class="bottom-notes" translate>{{ 'app.admin.invoices.beware_if_the_number_exceed_the_specified_length_it_will_be_truncated_by_the_left' }}</span>
</script>
<script type="text/ng-template" id="addOnlineInfo.html">
<table class="invoice-element-legend">
<tr><td><strong>X[texte]</strong></td><td>{{ 'app.admin.invoices.add_a_notice_regarding_the_online_sales_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'app.admin.invoices.this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ 'app.admin.invoices.eg_XVL_will_add_VL_to_the_invoices_settled_with_stripe' | translate }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addWalletInfo.html">
<table class="invoice-element-legend">
<tr><td><strong>W[texte]</strong></td><td>{{ 'app.admin.invoices.add_a_notice_regarding_the_wallet_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'app.admin.invoices.this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ 'app.admin.invoices.eg_WPM_will_add_PM_to_the_invoices_settled_with_wallet' | translate }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addRefundInfo.html">
<table class="invoice-element-legend">
<tr><td><strong>R[texte]</strong></td><td>{{ 'app.admin.invoices.add_a_notice_regarding_refunds_only_if_the_invoice_is_concerned' | translate }}<mark translate>{{ 'app.admin.invoices.this_will_never_be_added_when_an_online_sales_notice_is_present' }}</mark> {{ 'app.admin.invoices.eg_RA_will_add_A_to_the_refund_invoices' | translate }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="editCode.html">
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'app.admin.invoices.code' }}</h3>
</div>
<div class="modal-body">
<div class="form-group">
<label for="enableCode" class="control-label" translate>{{ 'app.admin.invoices.enable_the_code' }}</label>
<input bs-switch
ng-model="isSelected"
id="enableCode"
type="checkbox"
class="form-control m-l-sm"
switch-on-text="{{ 'app.admin.invoices.enabled' | translate }}"
switch-off-text="{{ 'app.admin.invoices.disabled' | translate }}"
switch-animate="true"/>
</div>
<div class="form-group" ng-show="isSelected">
<label for="codeModel" class="control-label" translate>{{ 'app.admin.invoices.code' }}</label>
<input id="codeModel" type="text" ng-model="codeModel" class="form-control"/>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>
</script>
<script type="text/ng-template" id="editNumber.html">
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'app.admin.invoices.order_number' }}</h3>
</div>
<div class="modal-body row">
<div class="elements col-md-4">
<h4 translate>{{ 'app.admin.invoices.elements' }}</h4>
<ul>
<li ng-click="invoice.number.help = 'addYear.html'">{{ 'app.admin.invoices.year' | translate }}</li>
<li ng-click="invoice.number.help = 'addMonth.html'">{{ 'app.admin.invoices.month' | translate }}</li>
<li ng-click="invoice.number.help = 'addDay.html'">{{ 'app.admin.invoices.day' | translate }}</li>
<li ng-click="invoice.number.help = 'addOrderNumber.html'">{{ 'app.admin.invoices.order_num' | translate }}</li>
</ul>
</div>
<div class="col-md-8">
<div class="model">
<h4 translate>{{ 'app.admin.invoices.model' }}</h4>
<input type="text" class="form-control" ng-model="model">
</div>
<div class="help">
<h4 translate>{{ 'app.admin.invoices.documentation' }}</h4>
<ng-include src="invoice.number.help" autoscroll="true">
</ng-include>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>
</script>
<script type="text/ng-template" id="editVAT.html">
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'app.admin.invoices.VAT' }}</h3>
</div>
<div class="modal-body">
<div class="form-group">
<label for="enableVAT" class="control-label" translate>{{ 'app.admin.invoices.enable_VAT' }}</label>
<input bs-switch
ng-model="isSelected"
id="enableVAT"
type="checkbox"
class="form-control m-l-sm"
switch-on-text="{{ 'app.admin.invoices.enabled' | translate }}"
switch-off-text="{{ 'app.admin.invoices.disabled' | translate }}"
switch-animate="true"/>
</div>
<div class="form-group" ng-show="isSelected">
<label for="vatRate" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate' }}</label>
<div class="input-group">
<span class="input-group-addon">% </span>
<input id="vatRate" type="number" ng-model="rate" class="form-control" min="0" max="100"/>
</div>
</div>
<div class="m-t-lg">
<h4 translate>{{ 'app.admin.invoices.VAT_history' }}</h4>
<table class="table scrollable-3-cols">
<thead>
<tr>
<th translate>{{ 'app.admin.invoices.VAT_rate' }}</th>
<th translate>{{ 'app.admin.invoices.changed_at' }}</th>
<th translate>{{ 'app.admin.invoices.changed_by' }}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="value in history | orderBy:'-date'">
<td>
<span class="no-user-label" ng-show="value.enabled === false" translate>{{'app.admin.invoices.VAT_disabled'}}</span>
<span class="no-user-label" ng-show="value.enabled === true" translate>{{'app.admin.invoices.VAT_enabled'}}</span>
<span ng-show="value.rate">{{value.rate}}</span>
</td>
<td>{{value.date | amDateFormat:'L LT'}}</td>
<td>{{value.user.name}}<span class="no-user-label" ng-hide="value.user" translate>{{ 'app.admin.invoices.deleted_user' }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>
</script>

View File

@ -0,0 +1,91 @@
<h3 class="m-t-xs"><i class="fa fa-filter"></i> {{ 'app.admin.invoices.filter_invoices' | translate }}</h3>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon" translate>{{ 'app.admin.invoices.invoice_num_' }}</span>
<input type="text" ng-model="searchInvoice.reference" class="form-control" placeholder="" ng-change="handleFilterChange()">
</div>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon" translate>{{ 'app.admin.invoices.customer_' }}</span>
<input type="text" ng-model="searchInvoice.name" class="form-control" placeholder="" ng-change="handleFilterChange()">
</div>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon">{{ "app.admin.invoices.date_" | translate }}</span>
<input type="date" ng-model="searchInvoice.date" class="form-control" ng-change="handleFilterChange()">
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table class="table invoices-list" ng-if="invoices.length > 0">
<thead>
<tr>
<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: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: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>
<th style="width:30%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="invoice in invoices">
<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>
<td ng-if="invoice.is_avoir">{{ invoice.date | amDateFormat:'L' }}</td>
<td>{{ invoice.total | currency}}</td>
<td>
<a href="" ui-sref="app.admin.members_edit({id: invoice.user_id})" ng-show="invoice.user_id">{{ invoice.name }}</a>
<span ng-hide="invoice.user_id">{{ invoice.name }}</span>
<td>
<div class="buttons">
<a class="btn btn-default download-button" ng-href="api/invoices/{{invoice.id}}/download" target="_blank" ng-if="!invoice.is_avoir">
<i class="fa fa-file-pdf-o"></i> {{ 'app.admin.invoices.download_the_invoice' | translate }}
</a>
<a class="btn btn-default" ng-href="api/invoices/{{invoice.id}}/download" target="_blank" ng-if="invoice.is_avoir">
<i class="fa fa-file-pdf-o"></i> {{ 'app.admin.invoices.download_the_credit_note' | translate }}
</a>
<a class="btn btn-default refund-button" ng-click="generateAvoirForInvoice(invoice)" ng-if="(!invoice.has_avoir || invoice.has_avoir == 'partial') && !invoice.is_avoir && !invoice.prevent_refund && !isDateClosed(invoice.created_at)">
<i class="fa fa-reply"></i> {{ 'app.admin.invoices.credit_note' | translate }}
</a>
</div>
</td>
</tr>
</tbody>
</table>
<div class="text-center">
<button class="btn btn-warning" ng-click="showNextInvoices()" ng-hide="noMoreResults"><i class="fa fa-search-plus" aria-hidden="true"></i> {{ 'app.admin.invoices.display_more_invoices' | translate }}</button>
</div>
<p ng-if="invoices.length == 0" translate>{{ 'app.admin.invoices.no_invoices_for_now' }}</p>
</div>
</div>

View File

@ -0,0 +1,310 @@
<div class="alert alert-warning p-md m-t" role="alert" ng-show="fablabWithoutInvoices">
<i class="fa fa-warning m-r"></i>
<span translate>{{ 'app.admin.invoices.warning_invoices_disabled' }}</span>
</div>
<form class="invoice-placeholder">
<div class="invoice-logo">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon" bs-holder ng-if="!invoice.logo" class="img-responsive">
<img base-sixty-four-image="invoice.logo" ng-if="invoice.logo && invoice.logo.base64">
<div class="tools-box">
<div class="btn-group">
<div class="btn btn-default btn-file">
<i class="fa fa-edit"></i> {{ 'app.admin.invoices.change_logo' | translate }}
<input type="file" accept="image/png,image/jpeg,image/x-png,image/pjpeg" name="invoice[logo][attachment]" ng-model="invoice.logo" base-sixty-four-input>
</div>
</div>
</div>
</div>
<div class="invoice-buyer-infos">
<strong translate>{{ 'app.admin.invoices.john_smith' }}</strong>
<div translate>{{ 'app.admin.invoices.john_smith_at_example_com' }}</div>
</div>
<div class="invoice-reference invoice-editable" ng-click="openEditReference()">{{ 'app.admin.invoices.invoice_reference_' | translate }} {{mkReference()}}</div>
<div class="invoice-code invoice-editable" ng-show="invoice.code.active" ng-click="openEditCode()">{{ 'app.admin.invoices.code_' | translate }} {{invoice.code.model}}</div>
<div class="invoice-code invoice-activable" ng-show="!invoice.code.active" ng-click="openEditCode()" translate>{{ 'app.admin.invoices.code_disabled' }}</div>
<div class="invoice-order invoice-editable" ng-click="openEditInvoiceNb()"> {{ 'app.admin.invoices.order_num' | translate }} {{mkNumber()}}</div>
<div class="invoice-date">{{ 'app.admin.invoices.invoice_issued_on_DATE_at_TIME' | translate:{DATE:(today | amDateFormat:'L'), TIME:(today | amDateFormat:'LT')} }}</div>
<div class="invoice-object">
{{ 'app.admin.invoices.object_reservation_of_john_smith_on_DATE_at_TIME' | translate:{DATE:(inOneWeek | amDateFormat:'L'), TIME:(inOneWeek | amDateFormat:'LT')} }}
</div>
<div class="invoice-data">
{{ 'app.admin.invoices.order_summary' | translate }}
<table>
<thead>
<tr>
<th translate>{{ 'app.admin.invoices.details' }}</th>
<th class="right" translate>{{ 'app.admin.invoices.amount' }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ 'app.admin.invoices.machine_booking-3D_printer' | translate }} {{inOneWeek | amDateFormat:'LLL'}} - {{inOneWeekAndOneHour | amDateFormat:'LT'}}</td>
<td class="right">{{30.0 | currency}}</td>
</tr>
<tr class="invoice-total" ng-class="{'bold vat-line':invoice.VAT.active}">
<td ng-show="!invoice.VAT.active" translate>{{ 'app.admin.invoices.total_amount' }}</td>
<td ng-show="invoice.VAT.active" translate>{{ 'app.admin.invoices.total_including_all_taxes' }}</td>
<td class="right">{{30.0 | currency}}</td>
</tr>
<tr class="invoice-vat invoice-activable" ng-click="openEditVAT()" ng-show="!invoice.VAT.active">
<td translate>{{ 'app.admin.invoices.VAT_disabled' }}</td>
<td></td>
</tr>
<tr class="invoice-vat invoice-editable vat-line italic" ng-click="openEditVAT()" ng-show="invoice.VAT.active">
<td>{{ 'app.admin.invoices.including_VAT' | translate }} {{invoice.VAT.rate}} %</td>
<td>{{30-(30/(invoice.VAT.rate/100+1)) | currency}}</td>
</tr>
<tr class="invoice-ht vat-line italic" ng-show="invoice.VAT.active">
<td translate>{{ 'app.admin.invoices.including_total_excluding_taxes' }}</td>
<td>{{30/(invoice.VAT.rate/100+1) | currency}}</td>
</tr>
<tr class="invoice-payed vat-line bold" ng-show="invoice.VAT.active">
<td translate>{{ 'app.admin.invoices.including_amount_payed_on_ordering' }}</td>
<td>{{30.0 | currency}}</td>
</tr>
</tbody>
</table>
<p class="invoice-payment" translate translate-values="{DATE:(today | amDateFormat:'L'), TIME:(today | amDateFormat:'LT'), AMOUNT:(30.0 | currency)}">
{{ 'app.admin.invoices.settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT' }}
</p>
</div>
<div medium-editor class="invoice-text invoice-editable" ng-model="invoice.text.content"
options='{
"placeholder": "{{ "app.admin.invoices.important_notes" | translate }}",
"buttons": ["underline"]
}'
ng-blur="textEditEnd($event)">
</div>
<div medium-editor class="invoice-legals invoice-editable" ng-model="invoice.legals.content"
options='{
"placeholder": "{{ "app.admin.invoices.address_and_legal_information" | translate }}",
"buttons": ["bold", "underline"]
}'
ng-blur="legalsEditEnd($event)">
</div>
</form>
<script type="text/ng-template" id="editReference.html">
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'app.admin.invoices.invoice_reference' }}</h3>
</div>
<div class="modal-body row">
<div class="elements col-md-4">
<h4>Éléments</h4>
<ul>
<li ng-click="invoice.reference.help = 'addYear.html'">{{ 'app.admin.invoices.year' | translate }}</li>
<li ng-click="invoice.reference.help = 'addMonth.html'">{{ 'app.admin.invoices.month' | translate }}</li>
<li ng-click="invoice.reference.help = 'addDay.html'">{{ 'app.admin.invoices.day' | translate }}</li>
<li ng-click="invoice.reference.help = 'addInvoiceNumber.html'">{{ 'app.admin.invoices.num_of_invoice' | translate }}</li>
<li ng-click="invoice.reference.help = 'addOnlineInfo.html'">{{ 'app.admin.invoices.online_sales' | translate }}</li>
<%# <li ng-click="invoice.reference.help = 'addWalletInfo.html'">{{ 'app.admin.invoices.wallet' | translate }}</li> %>
<li ng-click="invoice.reference.help = 'addRefundInfo.html'">{{ 'app.admin.invoices.refund' | translate }}</li>
</ul>
</div>
<div class="col-md-8">
<div class="model">
<h4 translate>{{ 'app.admin.invoices.model' }}</h4>
<input type="text" class="form-control" ng-model="model">
</div>
<div class="help">
<h4 translate>{{ 'app.admin.invoices.documentation' }}</h4>
<ng-include src="invoice.reference.help" autoscroll="true">
</ng-include>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>
</script>
<script type="text/ng-template" id="addYear.html">
<table class="invoice-element-legend">
<tr><td><strong>YY</strong></td><td translate>{{ 'app.admin.invoices.2_digits_year' }}</td></tr>
<tr><td><strong>YYYY</strong></td><td translate>{{ 'app.admin.invoices.4_digits_year' }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addMonth.html">
<table class="invoice-element-legend">
<tr><td><strong>M</strong></td><td translate>{{ 'app.admin.invoices.month_number' }}</td></tr>
<tr><td><strong>MM</strong></td><td translate>{{ 'app.admin.invoices.2_digits_month_number' }}</td></tr>
<tr><td><strong>MMM</strong></td><td translate>{{ 'app.admin.invoices.3_characters_month_name' }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addDay.html">
<table class="invoice-element-legend">
<tr><td><strong>D</strong></td><td translate>{{ 'app.admin.invoices.day_in_the_month' }}</td></tr>
<tr><td><strong>DD</strong></td><td translate>{{ 'app.admin.invoices.2_digits_day_in_the_month' }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addInvoiceNumber.html">
<table class="invoice-element-legend">
<tr><td><strong>dd...dd</strong></td><td translate>{{ 'app.admin.invoices.n_digits_daily_count_of_invoices' }}</td></tr>
<tr><td><strong>mm...mm</strong></td><td translate>{{ 'app.admin.invoices.n_digits_monthly_count_of_invoices' }}</td></tr>
<tr><td><strong>yy...yy</strong></td><td translate>{{ 'app.admin.invoices.n_digits_annual_amount_of_invoices' }}</td></tr>
</table>
<span class="bottom-notes" translate>{{ 'app.admin.invoices.beware_if_the_number_exceed_the_specified_length_it_will_be_truncated_by_the_left' }}</span>
</script>
<script type="text/ng-template" id="addOrderNumber.html">
<table class="invoice-element-legend">
<tr><td><strong>nn...nn</strong></td><td translate>{{ 'app.admin.invoices.n_digits_count_of_orders' }}</td></tr>
<tr><td><strong>dd...dd</strong></td><td translate>{{ 'app.admin.invoices.n_digits_daily_count_of_orders' }}</td></tr>
<tr><td><strong>mm...mm</strong></td><td translate>{{ 'app.admin.invoices.n_digits_monthly_count_of_orders' }}</td></tr>
<tr><td><strong>yy...yy</strong></td><td translate>{{ 'app.admin.invoices.n_digits_annual_amount_of_orders' }}</td></tr>
</table>
<span class="bottom-notes" translate>{{ 'app.admin.invoices.beware_if_the_number_exceed_the_specified_length_it_will_be_truncated_by_the_left' }}</span>
</script>
<script type="text/ng-template" id="addOnlineInfo.html">
<table class="invoice-element-legend">
<tr><td><strong>X[texte]</strong></td><td>{{ 'app.admin.invoices.add_a_notice_regarding_the_online_sales_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'app.admin.invoices.this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ 'app.admin.invoices.eg_XVL_will_add_VL_to_the_invoices_settled_with_stripe' | translate }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addWalletInfo.html">
<table class="invoice-element-legend">
<tr><td><strong>W[texte]</strong></td><td>{{ 'app.admin.invoices.add_a_notice_regarding_the_wallet_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'app.admin.invoices.this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ 'app.admin.invoices.eg_WPM_will_add_PM_to_the_invoices_settled_with_wallet' | translate }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addRefundInfo.html">
<table class="invoice-element-legend">
<tr><td><strong>R[texte]</strong></td><td>{{ 'app.admin.invoices.add_a_notice_regarding_refunds_only_if_the_invoice_is_concerned' | translate }}<mark translate>{{ 'app.admin.invoices.this_will_never_be_added_when_an_online_sales_notice_is_present' }}</mark> {{ 'app.admin.invoices.eg_RA_will_add_A_to_the_refund_invoices' | translate }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="editCode.html">
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'app.admin.invoices.code' }}</h3>
</div>
<div class="modal-body">
<div class="form-group">
<label for="enableCode" class="control-label" translate>{{ 'app.admin.invoices.enable_the_code' }}</label>
<input bs-switch
ng-model="isSelected"
id="enableCode"
type="checkbox"
class="form-control m-l-sm"
switch-on-text="{{ 'app.admin.invoices.enabled' | translate }}"
switch-off-text="{{ 'app.admin.invoices.disabled' | translate }}"
switch-animate="true"/>
</div>
<div class="form-group" ng-show="isSelected">
<label for="codeModel" class="control-label" translate>{{ 'app.admin.invoices.code' }}</label>
<input id="codeModel" type="text" ng-model="codeModel" class="form-control"/>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>
</script>
<script type="text/ng-template" id="editNumber.html">
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'app.admin.invoices.order_number' }}</h3>
</div>
<div class="modal-body row">
<div class="elements col-md-4">
<h4 translate>{{ 'app.admin.invoices.elements' }}</h4>
<ul>
<li ng-click="invoice.number.help = 'addYear.html'">{{ 'app.admin.invoices.year' | translate }}</li>
<li ng-click="invoice.number.help = 'addMonth.html'">{{ 'app.admin.invoices.month' | translate }}</li>
<li ng-click="invoice.number.help = 'addDay.html'">{{ 'app.admin.invoices.day' | translate }}</li>
<li ng-click="invoice.number.help = 'addOrderNumber.html'">{{ 'app.admin.invoices.order_num' | translate }}</li>
</ul>
</div>
<div class="col-md-8">
<div class="model">
<h4 translate>{{ 'app.admin.invoices.model' }}</h4>
<input type="text" class="form-control" ng-model="model">
</div>
<div class="help">
<h4 translate>{{ 'app.admin.invoices.documentation' }}</h4>
<ng-include src="invoice.number.help" autoscroll="true">
</ng-include>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>
</script>
<script type="text/ng-template" id="editVAT.html">
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'app.admin.invoices.VAT' }}</h3>
</div>
<div class="modal-body">
<div class="form-group">
<label for="enableVAT" class="control-label" translate>{{ 'app.admin.invoices.enable_VAT' }}</label>
<input bs-switch
ng-model="isSelected"
id="enableVAT"
type="checkbox"
class="form-control m-l-sm"
switch-on-text="{{ 'app.admin.invoices.enabled' | translate }}"
switch-off-text="{{ 'app.admin.invoices.disabled' | translate }}"
switch-animate="true"/>
</div>
<div class="form-group" ng-show="isSelected">
<label for="vatRate" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate' }}</label>
<div class="input-group">
<span class="input-group-addon">% </span>
<input id="vatRate" type="number" ng-model="rate" class="form-control" min="0" max="100"/>
</div>
</div>
<div class="m-t-lg">
<h4 translate>{{ 'app.admin.invoices.VAT_history' }}</h4>
<table class="table scrollable-3-cols">
<thead>
<tr>
<th translate>{{ 'app.admin.invoices.VAT_rate' }}</th>
<th translate>{{ 'app.admin.invoices.changed_at' }}</th>
<th translate>{{ 'app.admin.invoices.changed_by' }}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="value in history | orderBy:'-date'">
<td>
<span class="no-user-label" ng-show="value.enabled === false" translate>{{'app.admin.invoices.VAT_disabled'}}</span>
<span class="no-user-label" ng-show="value.enabled === true" translate>{{'app.admin.invoices.VAT_enabled'}}</span>
<span ng-show="value.rate">{{value.rate}}</span>
</td>
<td>{{value.date | amDateFormat:'L LT'}}</td>
<td>{{value.user.name}}<span class="no-user-label" ng-hide="value.user" translate>{{ 'app.admin.invoices.deleted_user' }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>
</script>

View File

@ -0,0 +1,34 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'app.admin.manager_new.add_a_manager' }}</h1>
</section>
</div>
</div>
</section>
<div class="row no-gutter">
<div class=" col-sm-12 col-md-9 b-r nopadding">
<form role="form" name="managerForm" class="form-horizontal" novalidate>
<section class="panel panel-default bg-light m-lg">
<div class="panel-body m-r">
<ng-include src="'<%= asset_path "shared/_manager_form.html" %>'"></ng-include>
</div>
<div class="panel-footer no-padder">
<input type="submit" value="{{ 'app.shared.buttons.save' | translate}}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-click="saveManager()" ng-disabled="managerForm.$invalid"/>
</div>
</section>
</form>
</div>
</div>

View File

@ -7,7 +7,9 @@
</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>
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.admins_new" ng-show="isAuthorized('admin')" translate>
{{ 'app.admin.members.add_a_new_administrator' }}
</button>
<table class="table">
<thead>
@ -29,7 +31,7 @@
<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)">
<button class="btn btn-danger" ng-if="isAuthorized('admin') && admin.id != currentUser.id" ng-click="destroyAdmin(admins, admin)">
<i class="fa fa-trash-o"></i>
</button>
</td>

View File

@ -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>

View File

@ -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>
@ -76,8 +76,13 @@
<p>
{{ 'app.admin.members_edit.price_' | translate }} {{ subscription.plan.amount | currency}}
</p>
<button class="btn btn-default" ng-click="updateSubscriptionModal(subscription, true)" translate>{{ 'app.admin.members_edit.offer_free_days' }}</button>
<button class="btn btn-default" ng-click="updateSubscriptionModal(subscription, false)" translate>{{ 'app.admin.members_edit.extend_subscription' }}</button>
<div ng-hide="user.id === currentUser.id">
<button class="btn btn-default" ng-click="updateSubscriptionModal(subscription, true)" translate>{{ 'app.admin.members_edit.offer_free_days' }}</button>
<button class="btn btn-default" ng-click="updateSubscriptionModal(subscription, false)" translate>{{ 'app.admin.members_edit.extend_subscription' }}</button>
</div>
<p class="alert alert-info" ng-show="user.id === currentUser.id" translate>
{{ 'app.admin.members_edit.cannot_extend_own_subscription' }}
</p>
</div>
@ -229,9 +234,12 @@
<ng-include src="'<%= asset_path "wallet/show.html" %>'"></ng-include>
<div class="clearfix"></div>
<div class="col-sm-4 text-center">
<div class="col-sm-4 text-center" ng-hide="user.id === currentUser.id">
<button type="button" class="btn btn-warning m-t m-b" ng-click="createWalletCreditModal(user, wallet)" translate>{{ 'app.admin.members_edit.to_credit' }}</button>
</div>
<p class="col-sm-4 alert alert-info" ng-show="user.id === currentUser.id" translate>
{{ 'app.admin.members_edit.cannot_credit_own_wallet' }}
</p>
</div>

View File

@ -11,7 +11,7 @@
</section>
</div>
<div class="col-xs-1 col-xs-offset-1 col-md-offset-2 b-l">
<section class="heading-actions wrapper">
<section class="heading-actions wrapper" ng-show="isAuthorized('admin')">
<a role="button" class="btn btn-default b-2x rounded m-t-sm import-members" ui-sref="app.admin.members_import">
<i class="fa fa-cloud-upload"></i>
</a>
@ -30,28 +30,28 @@
post-render="setupMembersTour">
<div class="row">
<div class="col-md-12">
<uib-tabset justified="true" active="tabs.active">
<uib-tabset justified="true" active="tabs.active" ng-if="isAuthorized('admin')">
<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>
<div ng-if="isAuthorized('manager')">
<ng-include src="'<%= asset_path "admin/members/users.html" %>'"></ng-include>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,48 @@
<p class="alert alert-info m-t-lg" translate>
{{ 'app.admin.members.managers_info' }}
</p>
<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_a_manager' | translate }}">
</div>
</div>
</div>
<div class="col-md-12">
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.managers_new" ng-show="isAuthorized('admin')" translate>
{{ 'app.admin.members.add_a_new_manager' }}
</button>
<table class="table">
<thead>
<tr>
<th style="width:15%"><a href="" ng-click="setOrderManager('last_name')">{{ 'app.admin.members.surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='last_name', 'fa fa-sort-alpha-desc': orderAdmin =='-last_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderManager('first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': orderAdmin =='first_name', 'fa fa-sort-alpha-desc': orderAdmin =='-first_name', 'fa fa-arrows-v': orderAdmin }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderManager('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="setOrderManager('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="manager in managers | filter:searchFilter | orderBy: orderManager">
<td class="text-c">{{ manager.last_name }}</td>
<td class="text-c">{{ manager.first_name }}</td>
<td>{{ manager.email }}</td>
<td>{{ manager.phone }}</td>
<td>
<button class="btn btn-default edit-member" ui-sref="app.admin.members_edit({id: manager.id})">
<i class="fa fa-edit"></i>
</button>
<button class="btn btn-danger" ng-if="isAuthorized('admin')" ng-click="destroyManager(managers, manager)">
<i class="fa fa-trash-o"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -18,8 +18,10 @@
</div>
</div>
<div class="col-md-12">
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.members_new" translate>{{ 'app.admin.members.add_a_new_member' }}</button>
<div class="pull-right exports-buttons">
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.members_new" translate>
{{ 'app.admin.members.add_a_new_member' }}
</button>
<div class="pull-right exports-buttons" ng-show="isAuthorized('admin')">
<a class="btn btn-default" ng-href="api/members/export_members.xlsx" target="export-frame" ng-click="alertExport('members')">
<i class="fa fa-file-excel-o"></i> {{ 'app.admin.members.members' | translate }}
</a>
@ -39,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>
@ -57,7 +59,7 @@
<button class="btn btn-default edit-member" ui-sref="app.admin.members_edit({id: m.id})">
<i class="fa fa-edit"></i>
</button>
<button class="btn btn-danger delete-member" ng-click="deleteMember(m.id)">
<button class="btn btn-danger delete-member" ng-click="deleteMember(m.id)" ng-show="isAuthorized('admin')">
<i class="fa fa-trash"></i>
</button>
<span class="label label-danger text-white" ng-show="m.need_completion" translate>{{ 'app.shared.user_admin.incomplete_profile' }}</span>

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.resource" 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="{{isAuthorized('admin') ? '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="{{isAuthorized('admin') ? '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="{{isAuthorized('admin') ? '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="{{isAuthorized('admin') ? 'level-2-tab' : ''}}" heading="{{ 'app.admin.members.partners' | translate }}" class="admins-tab" index="3" ng-show="isAuthorized('admin')">
<ng-include src="'<%= asset_path "admin/members/partners.html" %>'"></ng-include>
</uib-tab>
</uib-tabset>

View File

@ -35,7 +35,7 @@
ng-disabled="method == 'PATCH'">
<option value="all" translate>{{ 'app.shared.plan.transversal_all_groups' }}</option>
<optgroup label="Groupes">
<option ng-repeat="group in groups" value="{{group.id}}" ng-selected="plan.group_id == group.id">{{group.name}}</option>
<option ng-repeat="group in groups" ng-value="group.id" ng-selected="plan.group_id == group.id">{{group.name}}</option>
</optgroup>
</select>
<span class="help-block" ng-show="planForm['plan[group_id]'].$dirty && planForm['plan[group_id]'].$error.required" translate>{{ 'app.shared.plan.group_is_required' }}</span>

View File

@ -17,8 +17,6 @@
</section>
</div>
</div>
</section>

View File

@ -1,5 +1,7 @@
<div class="alert alert-warning m-t">
{{ 'app.admin.pricing.these_prices_match_machine_hours_rates_' | translate:{DURATION:slotDuration} }} <span class="font-bold" translate>{{ 'app.admin.pricing._without_subscriptions' }}</span>.
<p ng-bind-html="'app.admin.pricing.these_prices_match_machine_hours_rates_html' | translate"></p>
<p ng-bind-html="'app.admin.pricing.prices_calculated_on_hourly_rate_html' | translate:{ DURATION:slotDuration, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }"></p>
<p translate>{{ 'app.admin.pricing.you_can_override' }}</p>
</div>
<table class="table">
<thead>

View File

@ -1,5 +1,7 @@
<div class="alert alert-warning m-t">
{{ 'app.admin.pricing.these_prices_match_space_hours_rates_' | translate:{DURATION:slotDuration} }} <span class="font-bold" translate>{{ 'app.admin.pricing._without_subscriptions' }}</span>.
<p ng-bind-html="'app.admin.pricing.these_prices_match_space_hours_rates_html' | translate"></p>
<p ng-bind-html="'app.admin.pricing.prices_calculated_on_hourly_rate_html' | translate:{ DURATION:slotDuration, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }"></p>
<p translate>{{ 'app.admin.pricing.you_can_override' }}</p>
</div>
<table class="table">
<thead>

View File

@ -35,7 +35,7 @@
<uib-tabset justified="true" active="tabs.active">
<uib-tab heading="{{ 'app.admin.trainings.trainings' | translate }}" index="0" class="manage-trainings">
<div class="m-t m-b">
<button type="button" class="btn btn-warning" ui-sref="app.admin.trainings_new">
<button type="button" class="btn btn-warning" ui-sref="app.admin.trainings_new" ng-show="isAuthorized('admin')">
<i class="fa fa-plus m-r"></i>
<span translate>{{ 'app.admin.trainings.add_a_new_training' }}</span>
</button>
@ -64,7 +64,7 @@
<td>{{ showMachines(training) }}</td>
<td>{{ training.nb_total_places }}</td>
<td>
<div class="buttons">
<div class="buttons" ng-show="isAuthorized('admin')">
<button class="btn btn-default" ui-sref="app.admin.trainings_edit({id:training.id})">
<i class="fa fa-edit"></i> {{ 'app.shared.buttons.edit' | translate }}
</button>

View File

@ -1,5 +1,4 @@
<div>
<section class="heading">
<div class="row no-gutter">
<ng-include src="'<%= asset_path "dashboard/nav.html" %>'"></ng-include>
@ -7,7 +6,6 @@
</section>
<div class="row no-gutter">
<div class="col-md-12 m m-t-lg">
<ng-include src="'<%= asset_path "wallet/show.html" %>'"></ng-include>
@ -17,5 +15,4 @@
<ng-include src="'<%= asset_path "wallet/transactions.html" %>'"></ng-include>
</div>
</div>
</div>
</div>

View File

@ -67,7 +67,7 @@
<span class="text-black-light text-xs" ng-if="event.nb_free_places > 0">{{event.nb_free_places}} {{ 'app.public.events_list.still_available' | translate }}</span>
<span class="text-black-light text-xs" ng-if="event.nb_total_places > 0 && event.nb_free_places <= 0" translate>{{ 'app.public.events_list.sold_out' }}</span>
<span class="text-black-light text-xs" ng-if="event.nb_total_places == -1" translate>{{ 'app.public.events_list.cancelled' }}</span>
<span class="text-black-light text-xs" ng-if="!event.nb_total_places" translate>{{ 'app.public.events_list.free_entry' }}</span>
<span class="text-black-light text-xs" ng-if="!event.nb_total_places" translate>{{ 'app.public.events_list.without_reservation' }}</span>
</div>
</div>

View File

@ -14,11 +14,11 @@
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md" ng-if="isAuthorized('admin')">
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md" ng-if="isAuthorized(['admin', 'manager'])">
<section class="heading-actions wrapper">
<a ui-sref="app.admin.events_edit({id: event.id})" ng-if="isAuthorized('admin')" class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs text-u-c text-sm"><i class="fa fa-edit"></i> {{ 'app.shared.buttons.edit' | translate }}</a>
<a ng-click="deleteEvent(event)" ng-if="isAuthorized('admin')" class="btn btn-lg btn-danger b-2x rounded no-b m-t-xs"><i class="fa fa-trash-o"></i></a>
<a ui-sref="app.admin.events_edit({id: event.id})" class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs text-u-c text-sm"><i class="fa fa-edit"></i> {{ 'app.shared.buttons.edit' | translate }}</a>
<a ng-click="deleteEvent(event)" class="btn btn-lg btn-danger b-2x rounded no-b m-t-xs"><i class="fa fa-trash-o"></i></a>
</section>
</div>
@ -62,7 +62,7 @@
</ul>
</section>
<div ng-if="currentUser.role === 'admin'">
<div ng-if="isAuthorized(['admin', 'manager'])">
<select-member></select-member>
</div>
@ -102,7 +102,7 @@
<div ng-if="event.nb_total_places == -1"><span class="badge font-sbold" translate>{{ 'app.public.events_show.cancelled' }}</span></div>
</div>
<div class="text-sm m-b" ng-if="!event.nb_total_places">
<div><span class="badge font-sbold" translate>{{ 'app.public.events_show.free_entry' }}</span></div>
<div><span class="badge font-sbold" translate>{{ 'app.public.events_show.without_reservation' }}</span></div>
</div>

View File

@ -42,7 +42,7 @@
<i class="fa fa-user red col-xs-3 padder-icon"></i>
<h6 class="m-n col-xs-9 ">
<span ng-if="event.nb_free_places > 0">{{ 'app.public.home.still_available' | translate }} {{event.nb_free_places}}</span>
<span ng-if="!event.nb_total_places" translate>{{ 'app.public.home.free_entry' }}</span>
<span ng-if="!event.nb_total_places" translate>{{ 'app.public.home.without_reservation' }}</span>
<span ng-if="event.nb_total_places > 0 && event.nb_free_places <= 0" translate>{{ 'app.public.home.event_full' }}</span>
</h6>
</div>

View File

@ -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>

View File

@ -37,7 +37,7 @@
</select>
</div>
</div>
<div class="col-md-3 col-md-offset-6 m-t-n row-centered" ng-if="isAuthorized('admin')">
<div class="col-md-3 col-md-offset-6 m-t-n row-centered" ng-if="isAuthorized(['admin', 'manager'])">
<a role="button" ui-sref="app.admin.calendar" class="btn btn-lg btn-default rounded m-t-sm text-sm">
<i class="fa fa-calendar-check-o m-r" aria-hidden="true"></i><span translate>{{ 'app.public.machines_list.new_availability' }}</span>
</a>
@ -59,7 +59,7 @@
<div class="text-center clearfix">
<div class="col-sm-6 b-r no-padder">
<div class="btn btn-default btn-block no-b padder-v red" ng-click="reserveMachine(machine, $event)" ng-hide="machine.disabled">
<div class="btn btn-default btn-block no-b padder-v red reserve-button" ng-click="reserveMachine(machine, $event)" ng-hide="machine.disabled">
<i class="fa fa-bookmark m-r-xs"></i>
<span class="hidden-sm" translate>{{ 'app.public.machines_list.book' }}</span>
</div>

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

@ -17,7 +17,7 @@
<div class="row no-gutter">
<div class="col-sm-12 col-md-9 b-r">
<div class="row m-t m-b padder" ng-repeat="plansGroup in plansClassifiedByGroup | groupFilter:ctrl.member">
<div class="row m-t m-b padder" ng-repeat="plansGroup in plansClassifiedByGroup | groupFilter:ctrl.member" ng-show="plansGroup.actives > 0">
<div class="col-md-12 text-center">
<h2 class="text-u-c">{{plansGroup.name}}</h2>
@ -36,25 +36,24 @@
<div class="wrap">
<div class="price">
<div class="amount" data-fittext>{{plan.amount | currency}}</div>
<span class="period">{{ plan.interval | planIntervalFilter: plan.interval_count }}</span>
<span class="period">{{ plan.interval | planIntervalFilter: plan.interval_count }}</span>
</div>
</div>
</div>
<div class="cta-button" ng-if="!currentUser || currentUser.role == 'member'">
<button class="btn btn-default rounded" ng-click="selectPlan(plan)" ng-if="currentUser.subscribed_plan.id != plan.id" ng-disabled="currentUser.subscribed_plan" ng-class="{ 'bg-yellow': selectedPlan==plan }">
<span ng-if="currentUser" translate>{{ 'app.public.plans.i_choose_that_plan' }}</span>
<span ng-if="!currentUser" translate>{{ 'app.public.plans.i_subscribe_online' }}</span>
<div class="cta-button" ng-if="!ctrl.member || ctrl.member.role == 'member' || (ctrl.member.role == 'manager' && ctrl.member.id === currentUser.id)">
<button class="btn btn-default rounded" ng-click="selectPlan(plan)" ng-if="ctrl.member.subscribed_plan.id != plan.id" ng-disabled="ctrl.member.subscribed_plan" ng-class="{ 'bg-yellow': selectedPlan==plan }">
<span ng-if="ctrl.member" translate>{{ 'app.public.plans.i_choose_that_plan' }}</span>
<span ng-if="!ctrl.member" translate>{{ 'app.public.plans.i_subscribe_online' }}</span>
</button>
<button class="btn btn-warning bg-yellow rounded" ng-if="currentUser.subscribed_plan.id == plan.id" ng-disabled="currentUser.subscribed_plan.id == plan.id" translate>{{ 'app.public.plans.i_already_subscribed' }}</button>
<button class="btn btn-warning bg-yellow rounded" ng-if="ctrl.member.subscribed_plan.id == plan.id" ng-disabled="ctrl.member.subscribed_plan.id == plan.id" translate>
{{ 'app.public.plans.i_already_subscribed' }}
</button>
</div>
<div class="cta-button" ng-if="currentUser.role == 'admin'">
<div class="cta-button" ng-if="isAuthorized('admin') || (ctrl.member.role == 'manager' && ctrl.member.id != currentUser.id)">
<button class="btn btn-default rounded" ng-click="selectPlan(plan)" ng-class="{ 'bg-yellow': selectedPlan==plan }" ng-disabled="!ctrl.member">
<span translate>{{ 'app.public.plans.i_choose_that_plan' }}</span>
</button>
@ -67,9 +66,9 @@
</div>
<div class="col-xs-12 col-md-12 col-lg-10 col-centered no-gutter" ng-if="currentUser.subscription && isInPast(currentUser.subscription.expired_at)">
<div class="col-xs-12 col-md-12 col-lg-10 col-centered no-gutter" ng-if="ctrl.member.subscription && isInPast(ctrl.member.subscription.expired_at)">
<uib-alert type="info">
{{ 'app.public.plans.your_subscription_expires_on_the_DATE' | translate:{DATE:(currentUser.subscription.expired_at | amDateFormat:'L' )} }}
{{ 'app.public.plans.your_subscription_expires_on_the_DATE' | translate:{DATE:(ctrl.member.subscription.expired_at | amDateFormat:'L' )} }}
</uib-alert>
</div>
@ -81,14 +80,14 @@
<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 subscription="false"></select-member>
</div>
<section class="widget panel b-a m m-t-lg" ng-show="ctrl.member">
<div class="panel-heading b-b">
<h3 ng-show="currentUser.role != 'admin'" translate>{{ 'app.public.plans.my_group' }}</h3>
<h3 ng-show="currentUser.role === 'admin'" translate translate-values="{GENDER:getGender(currentUser)}">{{ 'app.public.plans.his_group' }}</h3>
<h3 ng-show="isAuthorized('member')" translate>{{ 'app.public.plans.my_group' }}</h3>
<h3 ng-show="isAuthorized(['admin', 'manager'])" translate translate-values="{GENDER:getGender(ctrl.member)}">{{ 'app.public.plans.his_group' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<div ng-show="!group.change">
@ -99,14 +98,14 @@
ng-click="group.change = !group.change"
ng-show="(!selectedPlan && ctrl.member && !ctrl.member.subscribed_plan && ctrl.member.subscription) || (!paid.plan)"
translate
translate-values="{ROLE:currentUser.role}">{{ 'app.public.plans.he_wants_to_change_group' }}</button>
translate-values="{ROLE:ctrl.member.role}">{{ 'app.public.plans.he_wants_to_change_group' }}</button>
</div>
<div ng-show="group.change">
<select class="form-control" ng-options="g.id as g.name for g in groups" ng-model="group.id"></select>
<button class="btn btn-success m-t"
ng-click="selectGroup()"
translate
translate-values="{ROLE:currentUser.role, GENDER:getGender(currentUser)}">{{ 'app.public.plans.change_my_group' }}</button>
translate-values="{ROLE:ctrl.member.role, GENDER:getGender(ctrl.member)}">{{ 'app.public.plans.change_my_group' }}</button>
</div>
</div>
</section>

View File

@ -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>

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()" 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

@ -0,0 +1,157 @@
<div class="row m-t">
<div class="col-sm-offset-3 col-sm-6">
<div class="form-group" ng-class="{'has-error': managerForm['manager[statistic_profile_attributes][gender]'].$dirty && managerForm['manager[statistic_profile_attributes][gender]'].$invalid}">
<label class="checkbox-inline btn btn-default">
<input type="radio"
name="manager[statistic_profile_attributes][gender]"
ng-model="manager.statistic_profile_attributes.gender"
ng-value="true"
required/>
<i class="fa fa-male m-l-sm"></i> {{ 'app.admin.manager_new.man' | translate }}
</label>
<label class="checkbox-inline btn btn-default">
<input type="radio"
name="manager[statistic_profile_attributes][gender]"
ng-model="manager.statistic_profile_attributes.gender"
ng-value="false"/>
<i class="fa fa-female m-l-sm"></i> {{ 'app.admin.manager_new.woman' | translate }}
</label>
<span class="exponent m-l-xs help-cursor"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
</div>
<div class="form-group" ng-class="{'has-error': managerForm['manager[username]'].$dirty && managerForm['manager[username]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input ng-model="manager.username"
type="text" name="manager[username]"
class="form-control"
id="user_username"
placeholder="{{ 'app.admin.manager_new.pseudonym' | translate }}"
required>
</div>
<span class="help-block" ng-show="managerForm['manager[username]'].$dirty && managerForm['manager[username]'].$error.required" translate>{{ 'app.admin.manager_new.pseudonym_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': managerForm['manager[profile_attributes][last_name]'].$dirty && managerForm['manager[profile_attributes][last_name]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input ng-model="manager.profile_attributes.last_name"
type="text"
name="manager[profile_attributes][last_name]"
class="form-control"
id="user_last_name"
placeholder="{{ 'app.admin.manager_new.surname' | translate }}"
required>
</div>
<span class="help-block" ng-show="managerForm['manager[profile_attributes][last_name]'].$dirty && managerForm['manager[profile_attributes][last_name]'].$error.required" translate>{{ 'app.admin.manager_new.surname_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': managerForm['manager[profile_attributes][first_name]'].$dirty && managerForm['manager[profile_attributes][first_name]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input ng-model="manager.profile_attributes.first_name"
type="text"
name="manager[profile_attributes][first_name]"
class="form-control"
id="user_first_name"
placeholder="{{ 'app.admin.manager_new.first_name' | translate }}"
required>
</div>
<span class="help-block" ng-show="managerForm['manager[profile_attributes][first_name]'].$dirty && managerForm['manager[profile_attributes][first_name]'].$error.required" translate>{{ 'app.admin.manager_new.first_name_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': managerForm['manager[email]'].$dirty && managerForm['manager[email]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-envelope"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input ng-model="manager.email"
type="email"
name="manager[email]"
class="form-control"
id="user_email"
placeholder="{{ 'app.admin.manager_new.email_address' | translate }}"
required>
</div>
<span class="help-block" ng-show="managerForm['manager[email]'].$dirty && managerForm['manager[email]'].$error.required" translate>{{ 'app.admin.manager_new.email_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': managerForm['manager[statistic_profile_attributes][birthday]'].$dirty && managerForm['manager[statistic_profile_attributes][birthday]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar-o"></i> </span>
<input type="text"
id="user_birthday"
class="form-control"
ng-model="manager.statistic_profile_attributes.birthday"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.opened"
placeholder="{{ 'app.admin.manager_new.birth_date' | translate }}"
ng-click="openDatePicker($event)"
/>
<input type="hidden"
name="manager[statistic_profile_attributes][birthday]"
value="{{manager.statistic_profile_attributes.birthday | toIsoDate}}" />
</div>
</div>
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-map-marker"></i> </span>
<input type="hidden"
name="manager[invoicing_profile_attributes][address_attributes][id]"
ng-value="manager.invoicing_profile_attributes.address.id" />
<input ng-model="manager.invoicing_profile_attributes.address_attributes.address"
type="text"
name="manager[invoicing_profile_attributes][address_attributes][address]"
class="form-control"
id="user_address"
placeholder="{{ 'app.admin.manager_new.address' | translate }}">
</div>
</div>
<div class="form-group" ng-class="{'has-error': managerForm['manager[profile_attributes][phone]'].$dirty && managerForm['manager[profile_attributes][phone]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-phone"></i> </span>
<input ng-model="manager.profile_attributes.phone"
type="text"
name="manager[profile_attributes][phone]"
class="form-control" id="user_phone"
placeholder="{{ 'app.admin.manager_new.phone_number' | translate }}">
</div>
</div>
</div>
</div>
<div class="row">
<div class="form-group" ng-class="{'has-error': managerForm['manager[group_id]'].$dirty && managerForm['manager[group_id]'].$invalid}">
<label for="manager_group_id" class="col-sm-3 control-label">
<span translate>{{ 'app.shared.user_admin.group' }}</span>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
</label>
<div class="col-sm-6">
<select ng-model="manager.group_id" class="form-control" name="manager[group_id]" id="manager_group_id"
ng-options="g.id as g.name for g in groups" required>
</select>
<input type="hidden" name="manager[group_id]" ng-value="manager.group_id" />
<span class="help-block" ng-show="managerForm['manager[group_id]'].$dirty && managerForm['manager[group_id]'].$error.required" translate>{{ 'app.shared.user_admin.group_is_required' }}</span>
</div>
</div>
</div>
<div>
<div class="form-group">
<label class="col-sm-3 control-label" translate>{{ 'app.shared.user_admin.tags' }}</label>
<div class="col-sm-6 nopadding">
<input type="hidden" name="manager[tag_ids][]" value="" />
<ui-select multiple ng-model="manager.tag_ids" name="user[tag_ids][]" class="form-control">
<ui-select-match>
<span ng-bind="$item.name"></span>
<input type="hidden" name="manager[tag_ids][]" value="{{$item.id}}" />
</ui-select-match>
<ui-select-choices repeat="t.id as t in (tags | filter: $select.search)">
<span ng-bind-html="t.name | highlight: $select.search"></span>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>

View File

@ -3,16 +3,16 @@
<h1 translate>{{ 'app.shared.cart.slot_at_same_time' }}</h1>
</div>
<div class="modal-body">
<p ng-if="bookSlotAtSameTime || currentUser.role === 'admin'" translate>{{ 'app.shared.cart.do_you_really_want_to_book_slot_at_same_time' }}</p>
<p ng-if="!bookSlotAtSameTime && currentUser.role !== 'admin'" translate>{{ 'app.shared.cart.unable_to_book_slot_because_really_have_reservation_at_same_time' }}</p>
<p ng-if="bookSlotAtSameTime || isAuthorized(['admin', 'manager'])" translate>{{ 'app.shared.cart.do_you_really_want_to_book_slot_at_same_time' }}</p>
<p ng-if="!bookSlotAtSameTime && !isAuthorized(['admin', 'manager'])" translate>{{ 'app.shared.cart.unable_to_book_slot_because_really_have_reservation_at_same_time' }}</p>
<ul>
<li ng-repeat="r in sameTimeReservations">
<span>{{::r.reservable.name}}{{::r.reservable.title}}</span>
<div class="font-sbold text-u-c">{{ 'app.shared.cart.datetime_to_time' | translate:{START_DATETIME:(r.start_at | amDateFormat:'LLLL'), END_TIME:(r.end_at | amDateFormat:'LT') } }}</div>
</li>
</div>
</ul>
</div>
<div class="modal-footer">
<button ng-if="bookSlotAtSameTime || currentUser.role === 'admin'" class="btn btn-info" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button ng-if="bookSlotAtSameTime || isAuthorized(['admin', 'manager'])" 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>

View File

@ -0,0 +1,26 @@
<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.shared.cart.tags_mismatch' }}</h1>
</div>
<div class="modal-body">
<p ng-show="isAuthorized(['admin', 'manager'])" translate translate-values="{USER: userName}">{{ 'app.shared.cart.confirm_book_slot_tags_mismatch' }}</p>
<p ng-hide="isAuthorized(['admin', 'manager'])" translate>{{ 'app.shared.cart.unable_to_book_slot_tags_mismatch' }}</p>
<h3 translate>{{ 'app.shared.cart.slot_tags' }}</h3>
<ul class="list-unstyled" ng-show="slotTags.length > 0">
<li ng-repeat="t in slotTags">
<span class="label label-default">{{t.name}}</span>
</li>
</ul>
<span ng-hide="slotTags.length > 0" translate>{{ 'app.shared.cart.no_tags' }}</span>
<h3 translate>{{ 'app.shared.cart.user_tags' }}</h3>
<ul class="list-unstyled">
<li ng-repeat="t in userTags" ng-show="userTags.length > 0">
<span class="label label-default">{{t.name}}</span>
</li>
</ul>
<span ng-hide="userTags.length > 0" translate>{{ 'app.shared.cart.no_tags' }}</span>
</div>
<div class="modal-footer">
<button ng-if="isAuthorized(['admin', 'manager'])" 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>

View File

@ -3,8 +3,8 @@
<h1 translate>{{ 'app.shared.confirm_modify_slot_modal.change_the_slot' }}</h1>
</div>
<div class="modal-body">
<p ng-show="currentUser.role != 'admin'" translate>{{ 'app.shared.confirm_modify_slot_modal.do_you_want_to_change_your_booking_slot_initially_planned_at' }} </p>
<p ng-show="currentUser.role == 'admin'" translate translate-values="{NAME: object.user.name}">{{ 'app.shared.confirm_modify_slot_modal.do_you_want_to_change_NAME_s_booking_slot_initially_planned_at' }}</p>
<p ng-show="currentUser.id === object.user.id" translate>{{ 'app.shared.confirm_modify_slot_modal.do_you_want_to_change_your_booking_slot_initially_planned_at' }} </p>
<p ng-show="currentUser.id !== object.user.id" translate translate-values="{NAME: object.user.name}">{{ 'app.shared.confirm_modify_slot_modal.do_you_want_to_change_NAME_s_booking_slot_initially_planned_at' }}</p>
<p><strong>{{object.start | amDateFormat: 'LL'}} : {{object.start | amDateFormat:'LT'}} - {{object.end | amDateFormat:'LT'}}</strong></p>
</div>
<div class="modal-footer">

View File

@ -40,8 +40,8 @@
<li><a ui-sref="app.logged.dashboard.events" translate>{{ 'app.public.common.my_events' }}</a></li>
<li><a ui-sref="app.logged.dashboard.invoices" ng-hide="fablabWithoutInvoices" translate>{{ 'app.public.common.my_invoices' }}</a></li>
<li ng-hide="fablabWithoutWallet"><a ui-sref="app.logged.dashboard.wallet" translate>{{ 'app.public.common.my_wallet' }}</a></li>
<li class="divider" ng-if="currentUser.role === 'admin'"></li>
<li><a class="text-black pointer" ng-click="help($event)" ng-if="currentUser.role === 'admin'"><i class="fa fa-question-circle"></i> <span translate>{{ 'app.public.common.help' }}</span> </a></li>
<li class="divider" ng-if="isAuthorized(['admin', 'manager'])"></li>
<li><a class="text-black pointer" ng-click="help($event)" ng-if="isAuthorized(['admin', 'manager'])"><i class="fa fa-question-circle"></i> <span translate>{{ 'app.public.common.help' }}</span> </a></li>
<li class="divider"></li>
<li><a class="text-black pointer" ng-click="logout($event)"><i class="fa fa-power-off"></i> {{ 'app.public.common.sign_out' | translate }}</a></li>
</ul>

View File

@ -71,10 +71,11 @@
</ul>
<!-- Admin entries -->
<div class="line-s bg-red-dark dk " ng-if="isAuthorized('admin')"></div>
<div class="line-s bg-red-dark dk " ng-if="isAuthorized(['admin', 'manager'])"></div>
<div class="text-xs font-bold text-bordeau hidden-nav-xs padder m-t-lg m-b-sm admin-section" ng-if="isAuthorized('admin')" translate>{{ 'app.public.common.admin' }}</div>
<ul class="nav" ng-if="isAuthorized('admin')">
<li class="{{navLink.class}}" ng-repeat="navLink in adminNavLinks">
<div class="text-xs font-bold text-bordeau hidden-nav-xs padder m-t-lg m-b-sm admin-section" ng-if="isAuthorized('manager')" translate>{{ 'app.public.common.manager' }}</div>
<ul class="nav" ng-if="isAuthorized(['admin', 'manager'])">
<li class="{{navLink.class}}" ng-repeat="navLink in adminNavLinks" ng-if="!navLink.authorizedRoles || isAuthorized(navLink.authorizedRoles)">
<a ng-click="toggleNavSize($event)" ga ui-sref="{{navLink.state}}" ui-sref-active="active" class="auto" data-toggle="class:nav-off-screen" data-target="#nav" ng-if="navLink.state">
<i class="fa fa-{{navLink.linkIcon}} fa-lg"></i>
<span>{{navLink.linkText | translate}}</span>

View File

@ -37,7 +37,7 @@
</select>
</div>
</div>
<div class="col-md-3 col-md-offset-6 m-t-n row-centered" ng-if="isAuthorized('admin')">
<div class="col-md-3 col-md-offset-6 m-t-n row-centered" ng-if="isAuthorized(['admin', 'manager'])">
<a role="button" ui-sref="app.admin.calendar" class="btn btn-lg btn-default rounded m-t-sm text-sm">
<i class="fa fa-calendar-check-o m-r" aria-hidden="true"></i><span translate>{{ 'app.public.spaces_list.new_availability' }}</span>
</a>
@ -60,7 +60,7 @@
<div class="text-center clearfix">
<div class="col-sm-6 b-r no-padder">
<div class="btn btn-default btn-block no-b padder-v red" ng-click="reserveSpace(space, $event)" ng-hide="space.disabled">
<div class="btn btn-default btn-block no-b padder-v red reserve-button" ng-click="reserveSpace(space, $event)" ng-hide="space.disabled">
<i class="fa fa-bookmark m-r-xs"></i>
<span class="hidden-sm" translate>{{ 'app.public.spaces_list.book' }}</span>
</div>

View File

@ -23,7 +23,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

@ -80,7 +80,7 @@ class API::AvailabilitiesController < API::ApiController
end
def machine
@current_user_role = current_user.admin? ? 'admin' : 'user'
@current_user_role = current_user.role
service = Availabilities::AvailabilitiesService.new(current_user, other: @visi_max_other, year: @visi_max_year)
@slots = service.machines(params[:machine_id], user)
@ -92,7 +92,7 @@ class API::AvailabilitiesController < API::ApiController
end
def spaces
@current_user_role = current_user.admin? ? 'admin' : 'user'
@current_user_role = current_user.role
service = Availabilities::AvailabilitiesService.new(current_user, other: @visi_max_other, year: @visi_max_year)
@slots = service.spaces(params[:space_id], user)
@ -147,7 +147,7 @@ class API::AvailabilitiesController < API::ApiController
def availability_params
params.require(:availability).permit(:start_at, :end_at, :available_type, :machine_ids, :training_ids, :nb_total_places,
:is_recurrent, :period, :nb_periods, :end_date,
:is_recurrent, :period, :nb_periods, :end_date, :slot_duration,
machine_ids: [], training_ids: [], space_ids: [], tag_ids: [], plan_ids: [],
machines_attributes: %i[id _destroy], plans_attributes: %i[id _destroy])
end

View File

@ -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
@ -38,7 +38,7 @@ class API::MembersController < API::ApiController
end
def create
authorize User
authorize :user, :create_member?
@member = User.new(user_params.permit!)
members_service = Members::MembersService.new(@member)
@ -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
@ -222,7 +251,7 @@ class API::MembersController < API::ApiController
],
statistic_profile_attributes: %i[id gender birthday])
elsif current_user.admin?
elsif current_user.admin? || current_user.manager?
params.require(:user).permit(:username, :email, :password, :password_confirmation, :is_allow_contact, :is_allow_newsletter, :group_id,
tag_ids: [],
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,

View File

@ -12,7 +12,12 @@ class API::PaymentsController < API::ApiController
def confirm_payment
render(json: { error: 'Online payment is disabled' }, status: :unauthorized) and return if Rails.application.secrets.fablab_without_online_payments
amount = nil # will contains the amount and the details of each invoice lines
intent = nil # stripe's payment intent
res = nil # json of the API answer
begin
amount = card_amount
if params[:payment_method_id].present?
check_coupon
check_plan
@ -20,7 +25,7 @@ class API::PaymentsController < API::ApiController
# Create the PaymentIntent
intent = Stripe::PaymentIntent.create(
payment_method: params[:payment_method_id],
amount: card_amount,
amount: amount[:amount],
currency: Rails.application.secrets.stripe_currency,
confirmation_method: 'manual',
confirm: true,
@ -40,7 +45,7 @@ class API::PaymentsController < API::ApiController
if intent&.status == 'succeeded'
if params[:cart_items][:reservation]
res = on_reservation_success(intent)
res = on_reservation_success(intent, amount[:details])
elsif params[:cart_items][:subscription]
res = on_subscription_success(intent)
end
@ -51,10 +56,10 @@ class API::PaymentsController < API::ApiController
private
def on_reservation_success(intent)
def on_reservation_success(intent, details)
@reservation = Reservation.new(reservation_params)
is_reserve = Reservations::Reserve.new(current_user.id, current_user.invoicing_profile.id)
.pay_and_save(@reservation, coupon: coupon_params[:coupon_code], payment_intent_id: intent.id)
.pay_and_save(@reservation, payment_details: details, payment_intent_id: intent.id)
Stripe::PaymentIntent.update(
intent.id,
description: "Invoice reference: #{@reservation.invoice.reference}"
@ -142,7 +147,7 @@ class API::PaymentsController < API::ApiController
# Subtract wallet amount from total
total = price_details[:total]
wallet_debit = get_wallet_debit(current_user, total)
total - wallet_debit
{ amount: total - wallet_debit, details: price_details }
end
def check_coupon

View File

@ -45,7 +45,7 @@ class API::PricesController < API::ApiController
@amount = { elements: nil, total: 0, before_coupon: 0 }
else
reservable = price_parameters[:reservable_type].constantize.find(price_parameters[:reservable_id])
@amount = Price.compute(current_user.admin?,
@amount = Price.compute(current_user.admin? || (current_user.manager? && current_user.id != user.id),
user,
reservable,
price_parameters[:slots_attributes] || [],

View File

@ -9,13 +9,13 @@ class API::ReservationsController < API::ApiController
def index
if params[:reservable_id] && params[:reservable_type] && params[:user_id]
params[:user_id] = current_user.id unless current_user.admin?
params[:user_id] = current_user.id unless current_user.admin? || current_user.manager?
where_clause = params.permit(:reservable_id, :reservable_type).to_h
where_clause[:statistic_profile_id] = StatisticProfile.find_by!(user_id: params[:user_id])
@reservations = Reservation.where(where_clause)
elsif params[:reservable_id] && params[:reservable_type] && current_user.admin?
elsif params[:reservable_id] && params[:reservable_type] && (current_user.admin? || current_user.manager?)
@reservations = Reservation.where(params.permit(:reservable_id, :reservable_type))
else
@reservations = []
@ -25,16 +25,17 @@ 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
amount = transaction_amount(current_user.admin?, user_id)
user_id = current_user.admin? || current_user.manager? ? params[:reservation][:user_id] : current_user.id
price = transaction_amount(current_user.admin? || (current_user.manager? && current_user.id != user_id), user_id)
authorize ReservationContext.new(Reservation, amount)
authorize ReservationContext.new(Reservation, price[:amount], user_id)
@reservation = Reservation.new(reservation_params)
is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id)
.pay_and_save(@reservation, coupon: coupon_params[:coupon_code])
.pay_and_save(@reservation, payment_details: price[:price_details])
if is_reserve
SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible
@ -72,7 +73,8 @@ class API::ReservationsController < API::ApiController
# Subtract wallet amount from total
total = price_details[:total]
wallet_debit = get_wallet_debit(user, total)
total - wallet_debit
{ price_details: price_details, amount: (total - wallet_debit) }
end
def get_wallet_debit(user, total_amount)

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
# API Controller for resources of type Slot
# Slots are used to cut Availabilities into reservable slots of ApplicationHelper::SLOT_DURATION minutes
# Slots are used to cut Availabilities into reservable slots. The duration of these slots is configured per
# availability by Availability.slot_duration, or otherwise globally by ApplicationHelper::SLOT_DURATION minutes
class API::SlotsController < API::ApiController
before_action :authenticate_user!
before_action :set_slot, only: %i[update cancel]

View File

@ -10,12 +10,13 @@ class API::SubscriptionsController < API::ApiController
end
# Admins can create any subscriptions. Members can directly create subscriptions if total = 0,
# otherwise, they must use payments_controller#confirm_payment
# otherwise, they must use payments_controller#confirm_payment.
# Managers can create subscriptions for other users
def create
user_id = current_user.admin? ? params[:subscription][:user_id] : current_user.id
amount = transaction_amount(current_user.admin?, user_id)
user_id = current_user.admin? || current_user.manager? ? params[:subscription][:user_id] : current_user.id
amount = transaction_amount(current_user.admin? || (current_user.manager? && current_user.id != user_id), user_id)
authorize SubscriptionContext.new(Subscription, amount)
authorize SubscriptionContext.new(Subscription, amount, user_id)
@subscription = Subscription.new(subscription_params)
is_subscribe = Subscriptions::Subscribe.new(current_user.invoicing_profile.id, user_id)

View File

@ -41,7 +41,8 @@ class API::TrainingsController < API::ApiController
end
head :no_content
elsif @training.update(training_params)
elsif current_user.admin? && @training.update(training_params)
# only admins can fully update a training, not managers
render :show, status: :ok, location: @training
else
render json: @training.errors, status: :unprocessable_entity

View File

@ -1,12 +1,15 @@
# 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)
authorize User
if %w[partner manager].include?(params[:role])
@users = User.with_role(params[:role].to_sym).includes(:profile)
else
head 403
end
@ -14,7 +17,13 @@ class API::UsersController < API::ApiController
def create
authorize User
res = UserService.create_partner(partner_params)
res = if !params[:user].empty?
UserService.create_partner(partner_params)
elsif !params[:manager].empty?
UserService.create_manager(manager_params)
else
nil
end
if res[:saved]
@user = res[:user]
@ -24,9 +33,29 @@ class API::UsersController < API::ApiController
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
def manager_params
params.require(:manager).permit(
:username, :email, :group_id,
tag_ids: [],
profile_attributes: %i[first_name last_name phone],
invoicing_profile_attributes: [address_attributes: [:address]],
statistic_profile_attributes: %i[gender birthday]
)
end
end

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# Devise controller to handle validation of email addresses
class ConfirmationsController < Devise::ConfirmationsController
# The path used after confirmation.
def after_confirmation_path_for(resource_name, resource)

View File

@ -1,5 +1,8 @@
# frozen_string_literal: true
# Devise controller used for the "forgotten password" feature
class PasswordsController < Devise::PasswordsController
# POST /resource/password
# POST /users/password.json
def create
self.resource = resource_class.send_reset_password_instructions(resource_params)
yield resource if block_given?

View File

@ -1,17 +1,20 @@
# frozen_string_literal: true
# Handle requests originated by indexer bots of social networks
class SocialBotController < ActionController::Base
def share
case request.original_fullpath
when /(=%2F|\/)projects(%2F|\/)([\-0-9a-z_]+)/
@project = Project.friendly.find("#{$3}")
render :project, status: :ok
when /(=%2F|\/)events(%2F|\/)([0-9]+)/
@event = Event.find("#{$3}".to_i)
render :event, status: :ok
when /(=%2F|\/)trainings(%2F|\/)([\-0-9a-z_]+)/
@training = Training.friendly.find("#{$3}")
when %r{(=%2F|/)projects(%2F|/)([\-0-9a-z_]+)}
@project = Project.friendly.find(Regexp.last_match(3).to_s)
render :project, status: :ok
when %r{(=%2F|/)events(%2F|/)([0-9]+)}
@event = Event.find(Regexp.last_match(3).to_s.to_i)
render :event, status: :ok
when %r{(=%2F|/)trainings(%2F|/)([\-0-9a-z_]+)}
@training = Training.friendly.find(Regexp.last_match(3).to_s)
render :training, status: :ok
else
puts "unknown bot request : #{request.original_url}"
else
puts "unknown bot request : #{request.original_url}"
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Raised when reserving on a locked availability
class LockedError < StandardError
end

View File

@ -30,7 +30,7 @@ class AccountingPeriod < ApplicationRecord
def invoices_with_vat(invoices)
vat_service = VatHistoryService.new
invoices.map do |i|
{ invoice: i, vat_rate: vat_service.invoice_vat(i) }
{ invoice: i, vat_rate: vat_service.invoice_vat(i) / 100.0 }
end
end

View File

@ -89,7 +89,8 @@ class Availability < ApplicationRecord
def available_space_places
return unless available_type == 'space'
((end_at - start_at) / ApplicationHelper::SLOT_DURATION.minutes).to_i * nb_total_places
duration = slot_duration || ApplicationHelper::SLOT_DURATION
((end_at - start_at) / duration.minutes).to_i * nb_total_places
end
def title(filter = {})
@ -159,9 +160,12 @@ class Availability < ApplicationRecord
private
def length_must_be_slot_multiple
return unless end_at < (start_at + ApplicationHelper::SLOT_DURATION.minutes)
return unless available_type == 'machines' || available_type == 'space'
errors.add(:end_at, I18n.t('availabilities.length_must_be_slot_multiple', MIN: ApplicationHelper::SLOT_DURATION))
duration = slot_duration || ApplicationHelper::SLOT_DURATION
return unless end_at < (start_at + duration.minutes)
errors.add(:end_at, I18n.t('availabilities.length_must_be_slot_multiple', MIN: duration))
end
def should_be_associated

View File

@ -23,7 +23,7 @@ class Avoir < Invoice
def notify_admins_refund_created
NotificationCenter.call type: 'notify_admin_refund_created',
receiver: User.admins,
receiver: User.admins_and_managers,
attached_object: self
end
end

View File

@ -15,7 +15,7 @@ class EventPriceCategory < ApplicationRecord
protected
def verify_no_associated_tickets
throw(:abort) if tickets.count.zero?
throw(:abort) unless tickets.count.zero?
end
end

View File

@ -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

View File

@ -1,5 +1,8 @@
# frozen_string_literal: true
MINUTES_PER_HOUR = 60.0
SECONDS_PER_MINUTE = 60.0
# Store customized price for various items (Machine, Space), depending on the group and on the plan
# Also provides a static helper method to compute the price details of a shopping cart
class Price < ApplicationRecord
@ -48,20 +51,20 @@ class Price < ApplicationRecord
when Machine
base_amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount
if plan
space_credit = plan.machine_credits.select { |credit| credit.creditable_id == reservable.id }.first
if space_credit
hours_available = credits_hours(space_credit, user, new_plan_being_bought)
machine_credit = plan.machine_credits.select { |credit| credit.creditable_id == reservable.id }.first
if machine_credit
hours_available = credits_hours(machine_credit, user, new_plan_being_bought)
slots.each_with_index do |slot, index|
total_amount += get_slot_price(base_amount, slot, admin, all_elements, (index < hours_available))
total_amount += get_slot_price(base_amount, slot, admin, elements: all_elements, has_credits: (index < hours_available))
end
else
slots.each do |slot|
total_amount += get_slot_price(base_amount, slot, admin, all_elements)
total_amount += get_slot_price(base_amount, slot, admin, elements: all_elements)
end
end
else
slots.each do |slot|
total_amount += get_slot_price(base_amount, slot, admin, all_elements)
total_amount += get_slot_price(base_amount, slot, admin, elements: all_elements)
end
end
@ -83,7 +86,7 @@ class Price < ApplicationRecord
end
end
slots.each do |slot|
total_amount += get_slot_price(amount, slot, admin, all_elements)
total_amount += get_slot_price(amount, slot, admin, elements: all_elements, is_division: false)
end
# Event reservation
@ -93,7 +96,7 @@ class Price < ApplicationRecord
amount += ticket[:booked] * EventPriceCategory.find(ticket[:event_price_category_id]).amount
end
slots.each do |slot|
total_amount += get_slot_price(amount, slot, admin, all_elements)
total_amount += get_slot_price(amount, slot, admin, elements: all_elements, is_division: false)
end
# Space reservation
@ -105,16 +108,16 @@ class Price < ApplicationRecord
if space_credit
hours_available = credits_hours(space_credit, user, new_plan_being_bought)
slots.each_with_index do |slot, index|
total_amount += get_slot_price(base_amount, slot, admin, all_elements, (index < hours_available))
total_amount += get_slot_price(base_amount, slot, admin, elements: all_elements, has_credits: (index < hours_available))
end
else
slots.each do |slot|
total_amount += get_slot_price(base_amount, slot, admin, all_elements)
total_amount += get_slot_price(base_amount, slot, admin, elements: all_elements)
end
end
else
slots.each do |slot|
total_amount += get_slot_price(base_amount, slot, admin, all_elements)
total_amount += get_slot_price(base_amount, slot, admin, elements: all_elements)
end
end
@ -135,29 +138,48 @@ class Price < ApplicationRecord
# === apply Coupon if any ===
_amount_no_coupon = total_amount
total_amount = CouponService.new.apply(total_amount, coupon_code)
cs = CouponService.new
cp = cs.validate(coupon_code, user.id)
total_amount = cs.apply(total_amount, cp)
# return result
{ elements: all_elements, total: total_amount.to_i, before_coupon: _amount_no_coupon.to_i }
{ elements: all_elements, total: total_amount.to_i, before_coupon: _amount_no_coupon.to_i, coupon: cp }
end
private
GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division: true }.freeze
##
# Compute the price of a single slot, according to the base price and the ability for an admin
# to offer the slot.
# @param base_amount {Number} base price of a slot
# @param hourly_rate {Number} base price of a slot
# @param slot {Hash} Slot object
# @param is_admin {Boolean} true if the current user has the 'admin' role
# @param [elements] {Array} optional, if provided the resulting price will be append into elements.slots
# @param [has_credits] {Boolean} true if the user still has credits for the given slot
# @param [options] {Hash} optional parameters, allowing the following options:
# - elements {Array} if provided the resulting price will be append into elements.slots
# - has_credits {Boolean} true if the user still has credits for the given slot, false if not provided
# - is_division {boolean} false if the slot covers an full availability, true if it is a subdivision (default)
# @return {Number} price of the slot
##
def get_slot_price(base_amount, slot, is_admin, elements = nil, has_credits = false)
ii_amount = has_credits || (slot[:offered] && is_admin) ? 0 : base_amount
elements[:slots].push(start_at: slot[:start_at], price: ii_amount, promo: (ii_amount != base_amount)) unless elements.nil?
ii_amount
def get_slot_price(hourly_rate, slot, is_admin, options = {})
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
slot_rate = options[:has_credits] || (slot[:offered] && is_admin) ? 0 : hourly_rate
real_price = if options[:is_division]
(slot_rate / MINUTES_PER_HOUR) * ((slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE)
else
slot_rate
end
unless options[:elements].nil?
options[:elements][:slots].push(
start_at: slot[:start_at],
price: real_price,
promo: (slot_rate != hourly_rate)
)
end
real_price
end
##

View File

@ -144,7 +144,7 @@ class Project < ApplicationRecord
def notify_admin_when_project_published
NotificationCenter.call type: 'notify_admin_when_project_published',
receiver: User.admins,
receiver: User.admins_and_managers,
attached_object: self
end

View File

@ -33,75 +33,17 @@ class Reservation < ApplicationRecord
##
# Generate an array of {Stripe::InvoiceItem} with the elements in the current reservation, price included.
# The training/machine price is depending of the member's group, subscription and credits already used
# @param on_site {Boolean} true if an admin triggered the call
# @param coupon_code {String} pass a valid code to appy a coupon
# @param payment_details {Hash} as generated by Price.compute
##
def generate_invoice_items(on_site = false, coupon_code = nil)
# prepare the plan
plan = if user.subscribed_plan
user.subscribed_plan
elsif plan_id
Plan.find(plan_id)
else
nil
end
def generate_invoice_items(payment_details = nil)
# check that none of the reserved availabilities was locked
slots.each do |slot|
raise LockedError if slot.availability.lock
end
case reservable
# === Machine reservation ===
when Machine
base_amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount
users_credits_manager = UsersCredits::Manager.new(reservation: self, plan: plan)
slots.each_with_index do |slot, index|
description = reservable.name +
" #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
ii_amount = base_amount # ii_amount default to base_amount
if users_credits_manager.will_use_credits?
ii_amount = index < users_credits_manager.free_hours_count ? 0 : base_amount
end
ii_amount = 0 if slot.offered && on_site # if it's a local payment and slot is offered free
invoice.invoice_items.push InvoiceItem.new(
amount: ii_amount,
description: description
)
end
# === Training reservation ===
when Training
base_amount = reservable.amount_by_group(user.group_id).amount
# be careful, variable plan can be the user's plan OR the plan user is currently purchasing
users_credits_manager = UsersCredits::Manager.new(reservation: self, plan: plan)
base_amount = 0 if users_credits_manager.will_use_credits?
slots.each do |slot|
description = reservable.name +
" #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
ii_amount = base_amount
ii_amount = 0 if slot.offered && on_site
invoice.invoice_items.push InvoiceItem.new(
amount: ii_amount,
description: description
)
end
# === Event reservation ===
when Event
amount = reservable.amount * nb_reserve_places
tickets.each do |ticket|
amount += ticket.booked * ticket.event_price_category.amount
end
slots.each do |slot|
description = "#{reservable.name}\n"
description += if slot.start_at.to_date != slot.end_at.to_date
@ -115,69 +57,32 @@ class Reservation < ApplicationRecord
"#{I18n.l slot.start_at.to_date, format: :long} #{I18n.l slot.start_at, format: :hour_minute}" \
" - #{I18n.l slot.end_at, format: :hour_minute}"
end
ii_amount = amount
ii_amount = 0 if slot.offered && on_site
price_slot = payment_details[:elements][:slots].detect { |p_slot| p_slot[:start_at].to_time.in_time_zone == slot[:start_at] }
invoice.invoice_items.push InvoiceItem.new(
amount: ii_amount,
amount: price_slot[:price],
description: description
)
end
# === Space reservation ===
when Space
base_amount = reservable.prices.find_by(group_id: user.group_id, plan_id: plan.try(:id)).amount
users_credits_manager = UsersCredits::Manager.new(reservation: self, plan: plan)
slots.each_with_index do |slot, index|
description = reservable.name + " #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
ii_amount = base_amount # ii_amount default to base_amount
if users_credits_manager.will_use_credits?
ii_amount = index < users_credits_manager.free_hours_count ? 0 : base_amount
end
ii_amount = 0 if slot.offered && on_site # if it's a local payment and slot is offered free
invoice.invoice_items.push InvoiceItem.new(
amount: ii_amount,
description: description
)
end
# === Unknown reservation type ===
# === Space|Machine|Training reservation ===
else
raise NotImplementedError
slots.each do |slot|
description = reservable.name +
" #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}"
price_slot = payment_details[:elements][:slots].detect { |p_slot| p_slot[:start_at].to_time.in_time_zone == slot[:start_at] }
invoice.invoice_items.push InvoiceItem.new(
amount: price_slot[:price],
description: description
)
end
end
# === Coupon ===
unless coupon_code.nil?
@coupon = Coupon.find_by(code: coupon_code)
raise InvalidCouponError if @coupon.nil? || @coupon.status(user.id) != 'active'
total = cart_total
discount = if @coupon.type == 'percent_off'
(total * @coupon.percent_off / 100).to_i
elsif @coupon.type == 'amount_off'
@coupon.amount_off
else
raise InvalidCouponError
end
end
@coupon = payment_details[:coupon]
# === Wallet ===
@wallet_amount_debit = wallet_amount_debit
# if @wallet_amount_debit != 0 && !on_site
# invoice_items << Stripe::InvoiceItem.create(
# customer: user.stp_customer_id,
# amount: -@wallet_amount_debit.to_i,
# currency: Rails.application.secrets.stripe_currency,
# description: "wallet -#{@wallet_amount_debit / 100.0}"
# )
# end
true
end
# check reservation amount total and strip invoice total to pay is equal
@ -211,8 +116,9 @@ class Reservation < ApplicationRecord
pending_invoice_items.each(&:delete)
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'
def save_with_payment(operator_profile_id, payment_details, payment_intent_id = nil)
operator = InvoicingProfile.find(operator_profile_id)&.user
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
build_invoice(
invoicing_profile: user.invoicing_profile,
@ -221,7 +127,7 @@ class Reservation < ApplicationRecord
stp_payment_intent_id: payment_intent_id,
payment_method: method
)
generate_invoice_items(true, coupon_code)
generate_invoice_items(payment_details)
return false unless valid?
@ -230,18 +136,18 @@ class Reservation < ApplicationRecord
subscription.attributes = { plan_id: plan_id, statistic_profile_id: statistic_profile_id, expiration_date: nil }
if subscription.save_with_payment(operator_profile_id, false)
invoice.invoice_items.push InvoiceItem.new(
amount: subscription.plan.amount,
amount: payment_details[:elements][:plan],
description: subscription.plan.name,
subscription_id: subscription.id
)
set_total_and_coupon(coupon_code)
set_total_and_coupon(payment_details[:coupon])
save!
else
errors[:card] << subscription.errors[:card].join
return false
end
else
set_total_and_coupon(coupon_code)
set_total_and_coupon(payment_details[:coupon])
save!
end
@ -306,7 +212,7 @@ class Reservation < ApplicationRecord
def notify_admin_member_create_reservation
NotificationCenter.call type: 'notify_admin_member_create_reservation',
receiver: User.admins,
receiver: User.admins_and_managers,
attached_object: self
end
@ -354,17 +260,14 @@ class Reservation < ApplicationRecord
##
# Set the total price to the reservation's invoice, summing its whole items.
# Additionally a coupon may be applied to this invoice to make a discount on the total price
# @param [coupon_code] {String} optional coupon code to apply to the invoice
# @param [coupon] {Coupon} optional coupon to apply to the invoice
##
def set_total_and_coupon(coupon_code = nil)
def set_total_and_coupon(coupon = nil)
total = invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+)
unless coupon_code.nil?
cp = Coupon.find_by(code: coupon_code)
raise InvalidCouponError unless !cp.nil? && cp.status(user.id) == 'active'
total = CouponService.new.apply(total, cp, user.id)
invoice.coupon_id = cp.id
unless coupon.nil?
total = CouponService.new.apply(total, coupon, user.id)
invoice.coupon_id = coupon.id
end
invoice.total = total

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
# Time range of duration defined by ApplicationHelper::SLOT_DURATION, slicing an Availability.
# Time range, slicing an Availability.
# Its duration is defined by globally by ApplicationHelper::SLOT_DURATION but can be overridden per availability
# During a slot a Reservation is possible
# Only reserved slots are persisted in DB, others are instantiated on the fly
class Slot < ApplicationRecord
@ -39,7 +40,7 @@ class Slot < ApplicationRecord
receiver: reservation.user,
attached_object: self
NotificationCenter.call type: 'notify_admin_slot_is_modified',
receiver: User.admins,
receiver: User.admins_and_managers,
attached_object: self
end
@ -48,7 +49,7 @@ class Slot < ApplicationRecord
receiver: reservation.user,
attached_object: self
NotificationCenter.call type: 'notify_admin_slot_is_canceled',
receiver: User.admins,
receiver: User.admins_and_managers,
attached_object: self
end

View File

@ -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 = 160
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

View File

@ -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)
@ -148,7 +149,7 @@ class Subscription < ApplicationRecord
def notify_admin_subscription_canceled
NotificationCenter.call type: 'notify_admin_subscription_canceled',
receiver: User.admins,
receiver: User.admins_and_managers,
attached_object: self
end
@ -173,7 +174,7 @@ class Subscription < ApplicationRecord
meta_data: meta_data
NotificationCenter.call type: :notify_admin_subscription_extended,
receiver: User.admins,
receiver: User.admins_and_managers,
attached_object: self,
meta_data: meta_data
end

View File

@ -97,6 +97,22 @@ class User < ApplicationRecord
User.with_role(:member)
end
def self.partners
User.with_role(:partner)
end
def self.managers
User.with_role(:manager)
end
def self.admins_and_managers
User.with_any_role(:admin, :manager)
end
def self.online_payers
User.with_any_role(:manager, :member)
end
def self.superadmin
return unless Rails.application.secrets.superadmin_email.present?
@ -104,7 +120,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
@ -131,6 +147,26 @@ class User < ApplicationRecord
has_role? :member
end
def manager?
has_role? :manager
end
def partner?
has_role? :partner
end
def role
if admin?
'admin'
elsif manager?
'manager'
elsif member?
'member'
else
'other'
end
end
def all_projects
my_projects.to_a.concat projects
end
@ -160,7 +196,7 @@ class User < ApplicationRecord
auth.info.mapping.each do |key, value|
user.set_data_from_sso_mapping(key, value)
end
user.password = Devise.friendly_token[0,20]
user.password = Devise.friendly_token[0, 20]
end
end
@ -285,7 +321,7 @@ class User < ApplicationRecord
protected
# remove projets drafts that are not linked to another user
# remove projects drafts that are not linked to another user
def remove_orphan_drafts
orphans = my_projects
.joins('LEFT JOIN project_users ON projects.id = project_users.project_id')
@ -332,19 +368,19 @@ class User < ApplicationRecord
attached_object: self
else
NotificationCenter.call type: 'notify_admin_when_user_is_created',
receiver: User.admins,
receiver: User.admins_and_managers,
attached_object: self
end
end
def notify_group_changed
return if changes[:group_id].first.nil?
return unless changes[:group_id]&.first
ex_group = Group.find(changes[:group_id].first)
meta_data = { ex_group_name: ex_group.name }
NotificationCenter.call type: :notify_admin_user_group_changed,
receiver: User.admins,
receiver: User.admins_and_managers,
attached_object: self,
meta_data: meta_data

View File

@ -2,9 +2,13 @@
# Check the access policies for API::AccountingPeriodsController
class AccountingPeriodPolicy < ApplicationPolicy
%w[index show create last_closing_end download_archive].each do |action|
%w[index show create download_archive].each do |action|
define_method "#{action}?" do
user.admin?
end
end
def last_closing_end?
user.admin? || user.manager?
end
end

View File

@ -1,6 +1,6 @@
class AdminPolicy < ApplicationPolicy
def index?
user.admin?
user.admin? || user.manager?
end
def create?

View File

@ -1,7 +1,14 @@
# 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? lock?].each do |action|
define_method action do
user.admin?
user.admin? || user.manager?
end
end
def export?
user.admin?
end
end

View File

@ -18,7 +18,7 @@ class EventPolicy < ApplicationPolicy
end
def create?
user.admin?
user.admin? || user.manager?
end
def update?

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

@ -1,18 +1,21 @@
# frozen_string_literal: true
# Check the access policies for API::InvoicesController
class InvoicePolicy < ApplicationPolicy
def index?
user.admin?
end
def download?
user.admin? or (record.invoicing_profile.user_id == user.id)
user.admin? || user.manager? || (record.invoicing_profile.user_id == user.id)
end
def create?
user.admin?
user.admin? || user.manager?
end
def list?
user.admin?
user.admin? || user.manager?
end
def first?

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

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

View File

@ -4,7 +4,7 @@
class SubscriptionPolicy < ApplicationPolicy
include FablabConfiguration
def create?
!fablab_plans_deactivated? && (user.admin? || record.price.zero?)
!fablab_plans_deactivated? && (user.admin? || (user.manager? && record.user_id != user.id) || record.price.zero?)
end
def show?
@ -12,6 +12,6 @@ class SubscriptionPolicy < ApplicationPolicy
end
def update?
user.admin?
user.admin? || (user.manager? && record.user.id != user.id)
end
end

Some files were not shown because too many files have changed in this diff Show More