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:
commit
d72ff53972
31
CHANGELOG.md
31
CHANGELOG.md
@ -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
44
Vagrantfile
vendored
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
*/
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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'; }
|
||||
};
|
||||
}
|
||||
|
||||
]);
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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" %>',
|
||||
|
@ -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',
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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: {
|
||||
|
@ -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);
|
||||
};
|
||||
}]);
|
||||
|
@ -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();
|
||||
|
@ -44,6 +44,10 @@ Application.Services.factory('Member', ['$resource', '$q', function ($resource,
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRole: {
|
||||
method: 'PATCH',
|
||||
url: '/api/members/:id/update_role'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -678,3 +678,11 @@ body.container {
|
||||
left: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.middle-of-inputs {
|
||||
line-height: 24px;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
4
app/assets/stylesheets/modules/members.scss
Normal file
4
app/assets/stylesheets/modules/members.scss
Normal file
@ -0,0 +1,4 @@
|
||||
.promote-member img {
|
||||
width: 16px;
|
||||
height: 21px;
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
111
app/assets/templates/admin/invoices/codes.html.erb
Normal file
111
app/assets/templates/admin/invoices/codes.html.erb
Normal 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>
|
@ -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:/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>
|
||||
|
91
app/assets/templates/admin/invoices/list.html.erb
Normal file
91
app/assets/templates/admin/invoices/list.html.erb
Normal 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>
|
310
app/assets/templates/admin/invoices/settings.html.erb
Normal file
310
app/assets/templates/admin/invoices/settings.html.erb
Normal 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:/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>
|
34
app/assets/templates/admin/managers/new.html.erb
Normal file
34
app/assets/templates/admin/managers/new.html.erb
Normal 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>
|
@ -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>
|
||||
|
@ -0,0 +1,12 @@
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{logoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{logo.custom_asset_file_attributes.attachment}}" class="modal-logo"/>
|
||||
<h1 translate>{{ 'app.admin.members_edit.change_role' }}</h1>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="alert alert-warning" translate>{{ 'app.admin.members_edit.warning_role_change' }}</p>
|
||||
<select ng-model="role" class="form-control" ng-options="role.key as role.label disable when role.notAnOption for role in roles"></select>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-info" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
|
||||
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
|
||||
</div>
|
@ -17,8 +17,8 @@
|
||||
|
||||
<div class="col-md-3">
|
||||
<section class="heading-actions wrapper">
|
||||
<div class="btn btn-lg btn-block btn-default m-t-xs" ng-click="cancel()" translate>
|
||||
{{ 'app.shared.buttons.cancel' }}
|
||||
<div class="btn btn-lg btn-block btn-default promote-member m-t-xs" ng-click="changeUserRole()" ng-show="isAuthorized('admin')">
|
||||
<img src="/rank-icon.svg" alt="role icon" /><span class="m-l" translate>{{ 'app.admin.members_edit.change_role' }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
48
app/assets/templates/admin/members/managers.html.erb
Normal file
48
app/assets/templates/admin/members/managers.html.erb
Normal 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>
|
@ -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>
|
||||
|
43
app/assets/templates/admin/members/partners.html.erb
Normal file
43
app/assets/templates/admin/members/partners.html.erb
Normal 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>
|
19
app/assets/templates/admin/members/users.html.erb
Normal file
19
app/assets/templates/admin/members/users.html.erb
Normal 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>
|
@ -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>
|
||||
|
@ -17,8 +17,6 @@
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
157
app/assets/templates/shared/_manager_form.html
Normal file
157
app/assets/templates/shared/_manager_form.html
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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] || [],
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Raised when reserving on a locked availability
|
||||
class LockedError < StandardError
|
||||
end
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
##
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,6 @@
|
||||
class AdminPolicy < ApplicationPolicy
|
||||
def index?
|
||||
user.admin?
|
||||
user.admin? || user.manager?
|
||||
end
|
||||
|
||||
def create?
|
||||
|
@ -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
|
||||
|
@ -18,7 +18,7 @@ class EventPolicy < ApplicationPolicy
|
||||
end
|
||||
|
||||
def create?
|
||||
user.admin?
|
||||
user.admin? || user.manager?
|
||||
end
|
||||
|
||||
def update?
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user