gadael/gadael

View on GitHub
public/js/services/absence-edit.js

Summary

Maintainability
D
2 days
Test Coverage
define(['angular', 'services/request-edit'], function(angular, loadRequestEdit) {
    'use strict';

    return function(gettextCatalog) {
        var RequestEdit = loadRequestEdit(gettextCatalog);

        /**
         * store quantity_unit for each loaded right ID
         */
        var quantity_unit = {};

        /**
         * available quantity per renewal ID
         * @var {object}
         */
        var available = {};

        /**
         * Callback used to watch the distribution object in scope
         * @param {object} distribution
         */
        function distributionWatch(distribution, $scope) {
            /**
             * Browse the rights appliquable for distribution
             * @param {function} action     function to call on each rights
             *
             */
            function browseInputValue(action) {

                for(var renewalId in distribution.renewal) {
                    if (distribution.renewal.hasOwnProperty(renewalId)) {
                        action(renewalId, distribution.renewal[renewalId].right);
                    }
                }
            }

            /**
             * Test if distribution is completed
             * @param {Number} days     Sum of days distributed on rights
             * @param {Number} hours    Sum of hours distributed on rights
             *
             * @return {Boolean}
             */
            function isCompleted(days, hours) {
                var daysCompleted = ($scope.selection.businessDays === days) && !hours;
                var hoursCompleted = ($scope.selection.duration === (hours*3600000)) && !days;
                return (daysCompleted || hoursCompleted);
            }

            /**
             * Get a classname for the input field
             * @param {Number} value            Consumed quantity
             * @param {Number} availableQte
             * @param {Boolean} completed
             * @return string
             */
            function getValueClass(value, availableQte, completed) {

                if (undefined === value || null === value || null === availableQte) {
                    return '';
                }

                if (0 === value) {
                    return 'has-warning';
                }

                if (value < 0) {
                    return 'has-error';
                }

                if (availableQte < value) {
                    return 'has-error';
                }

                if (!completed) {
                    return 'has-warning';
                }

                return 'has-success';
            }

            if (distribution === undefined) {
                $scope.distribution = {
                    class: {},
                    renewal: {},
                    total: 0,
                    completed: false
                };
            } else {


                var inputQuantity, days = 0, hours = 0, daysConsumed = 0, hoursConsumed = 0;

                // first pass, compute total
                browseInputValue(function(renewalId, rightId) {
                    var inputValue = distribution.renewal[renewalId].quantity;
                    if (!inputValue) {
                        return;
                    }

                    inputQuantity = parseFloat(inputValue);
                    var inputConsumption = distribution.renewal[renewalId].consumedQuantity;

                    switch(quantity_unit[rightId]) {
                        case 'D':
                            days  += inputQuantity;
                            daysConsumed += inputConsumption;
                            break;

                        case 'H':
                            hours += inputQuantity;
                            hoursConsumed += inputConsumption;
                            break;
                    }
                });


                var completed = isCompleted(days, hours);
                $scope.distribution.completed = completed;

                // second pass, apply styles on cells
                browseInputValue(function(renewalId) {
                    var consumedQuantity = distribution.renewal[renewalId].consumedQuantity;
                    var avail = available[renewalId];
                    if (null !== avail && undefined !== $scope.initialQuantity && undefined !== $scope.initialQuantity[renewalId]) {
                        avail = avail + $scope.initialQuantity[renewalId];
                    }
                    $scope.distribution.class[renewalId] = getValueClass(consumedQuantity, avail, completed);
                });

                // Assigned duration to display to the user
                distribution.total = RequestEdit.getDuration(days, hours);
                distribution.totalConsuption = RequestEdit.getDuration(daysConsumed, hoursConsumed);
            }
        }



        /**
         * Create distribution to post for a absence request
         * @param {object} renewals   Rights renenwals distribution with renewal id as property, right id and quantity (the form input) in the value
         * @param {object} periods    The list of selected periods, provided by the period picker widget
         * @param {Boolean} matchQuantity   If true, throw Error if events duration does not match input quantity
         * @return {Array}
         */
        function createDistribution(renewals, periods, accountRights, matchQuantity) {

            var totalSeconds = 0;
            var totalDays = 0;

            for(var j=0; j<periods.length; j++) {
                if (undefined === periods[j].businessDays) {

                    //throw new Error('Missing businessDays on period from '+periods[j].dtstart+' to '+periods[j].dtend);
                    // This is a partial half day, this is not ideal but here we consider the half day is used

                    periods[j].businessDays = 0.5;
                }

                totalDays += periods[j].businessDays;
                totalSeconds += (periods[j].dtend.getTime() - periods[j].dtstart.getTime()) / 1000;
            }



            /**
             *
             *
             * @param {object} selectePeriod    selected working period
             * @param {Date} startDate          Start date for the element
             * @param {Number} secQuantity      Duration for the element
             *
             *
             * @return {object}
             */
            function extractEvent(selectedPeriod, startDate, secQuantity)
            {
                if (selectedPeriod.dtstart > startDate) {
                    // this ignore gaps between selected periods
                    startDate = selectedPeriod.dtstart;
                }

                var validInterval = true;

                if (selectedPeriod.dtend <= startDate) {
                    validInterval = false;
                }

                var endDate = new Date(startDate);
                endDate.setTime(endDate.getTime() + (1000* secQuantity));

                if (selectedPeriod.dtstart >= endDate) {
                    validInterval = false;
                }

                /*
                console.log({
                    selectedPeriod: selectedPeriod,
                    startDate: startDate,
                    endDate: endDate,
                    secQuantity: secQuantity
                });
                */

                var dtstart = selectedPeriod.dtstart > startDate ? selectedPeriod.dtstart : startDate;
                var dtend = selectedPeriod.dtend < endDate ? selectedPeriod.dtend : endDate;

                return {
                    validInterval: validInterval,
                    dtstart: dtstart,
                    dtend: dtend
                };
            }


            /**
             * create events to embed in distribution
             * One event per working period
             *
             * @param {Number} secQuantity              Duration for the element
             * @param {Date} startDate                  Start date for the element
             * @param {Boolean} testIntervalValidity    Verify duration match periods
             *
             * @return {array}
             */
            function createEvents(secQuantity, startDate, testIntervalValidity)
            {

                var event, events = [], consumed;

                periods.forEach(function(selectedPeriod) {
                    event = extractEvent(selectedPeriod, startDate, secQuantity);
                    if (event.validInterval || !testIntervalValidity) {
                        events.push(event);
                        consumed = Math.round((event.dtend.getTime() - event.dtstart.getTime())/1000);
                        //console.log('consumed='+consumed+' secQuantity='+secQuantity);
                        startDate = event.dtend;
                        secQuantity = secQuantity - consumed;

                    }
                });

                if (0 === events.length) {
                    throw new Error(gettextCatalog.getString('Wrong events count'));
                }

                if (matchQuantity && (0 !== secQuantity)) {
                    throw new Error(
                        gettextCatalog.getString('Duration mismatch: {duration} hours remain unconsumed after the creation of {nbEvents} events, please check your absence period, it must not overlap another leave')
                        .replace(/\{duration\}/, secQuantity /3600)
                        .replace(/\{nbEvents\}/,events.length)
                    );
                }

                return events;
            }

            /**
             * Get quantity unit from right ID
             * @return {String} D|H
             */
            function getQuantityUnit(rightId)
            {
                for(var i=0; i<accountRights.length; i++) {
                    if (accountRights[i]._id === rightId) {
                        return accountRights[i].quantity_unit;
                    }
                }

                if (0 === accountRights.length) {
                    throw new Error('No accountRights');
                }

                //console.log(accountRights);
                throw new Error('Right '+rightId+' not found in available accountRights');
            }


            /**
             * Get quantity in seconds
             * @param {String} rightId
             * @param {Number} inputQuantity
             * @return {Int}
             */
            function getSecQuantity(rightId, inputQuantity)
            {
                if (null === inputQuantity) {
                    throw new Error(gettextCatalog.getString('Input quantity must be set'));
                }

                var u = getQuantityUnit(rightId);

                if ('H' === u) {
                    return Math.round(inputQuantity*3600);
                }

                if ('D' === u) {

                    if (0 === totalDays || isNaN(totalDays)) {
                        throw new Error('Conversion not appliquable, totalDays must be greater than 0');
                    }

                    return (totalSeconds * inputQuantity / totalDays);
                }

                throw new Error('Invalid quantity unit');
            }


            var distribution = [];
            var startDate = periods[0].dtstart;
            var rightId, quantity, elem;



            for(var renewalId in renewals) {
                if (renewals.hasOwnProperty(renewalId)) {

                    quantity = renewals[renewalId].quantity;

                    if (undefined === quantity || null === quantity || 0 === quantity) {
                        continue;
                    }

                    rightId = renewals[renewalId].right;
                    if (undefined !== rightId._id) {
                        rightId = rightId._id;
                    }

                    elem = {
                        right: {
                            id: rightId,
                            renewal:renewalId
                        },
                        quantity: quantity,
                        events: createEvents(getSecQuantity(rightId, quantity), startDate, matchQuantity)
                    };

                    distribution.push(elem);

                    // set startDate for the next element
                    startDate = elem.events[elem.events.length-1].dtend;
                }
            }

            if (0 === distribution.length) {
                throw new Error(gettextCatalog.getString('Nothing to save'));
            }

            return distribution;
        }

        /**
         * Initialize distribution using data from the request
         */
        function setDistributionRequest($scope) {
            var ai, aj;
            var accessiblesRenewals = [];
            for (ai=0; ai<$scope.accountRights.length; ai++) {
                for (aj=0; aj<$scope.accountRights[ai].renewals.length; aj++) {
                    accessiblesRenewals.push($scope.accountRights[ai].renewals[aj]._id);
                }
            }

            $scope.initialQuantity = {};

            $scope.request.absence.distribution.forEach(function(element) {

                // add renewal only if exists in accountRights

                if (-1 !== accessiblesRenewals.indexOf(element.right.renewal.id)) {

                    $scope.distribution.renewal[element.right.renewal.id] = {
                        quantity: element.quantity,
                        right: element.right.id
                    };

                    $scope.initialQuantity[element.right.renewal.id] = element.quantity;
                }
            });
        }



        /**
         * Initialize distribution for rights where it has been allowed
         */
        function initializeDistribution($scope) {

            //days
            var dispatch = $scope.selection.businessDays;

            var ai, aj;
            for (ai=0; ai<$scope.accountRights.length; ai++) {
                for (aj=0; aj<$scope.accountRights[ai].renewals.length; aj++) {
                    var renewalId = $scope.accountRights[ai].renewals[aj]._id;
                    var right = $scope.accountRights[ai].renewals[aj].right;

                    if (!right.autoDistribution) {
                        continue;
                    }

                    if ('D' !== $scope.accountRights[ai].quantity_unit) {
                        continue;
                    }

                    var quantity = Math.min(dispatch, $scope.accountRights[ai].available_quantity);

                    dispatch -= quantity;

                    $scope.distribution.renewal[renewalId] = {
                        quantity: quantity,
                        right: right._id
                    };

                }
            }
        }

        function cleanDocument(request) {

            if (!request) {
                throw new Error('The request does not exists');
            }

            delete request.requestLog;
            delete request.approvalSteps;

            if (undefined !== request.time_saving_deposit) {
                delete request.time_saving_deposit;
            }

            if (undefined !== request.workperiod_recover) {
                delete request.workperiod_recover;
            }

        }


        /**
         * remove consuption on all rows
         */
        function removeAllConsumptions($scope) {
            var row;

            for (var id in $scope.distribution.renewal) {
                if (!$scope.distribution.renewal.hasOwnProperty(id)) {
                    continue;
                }

                row = $scope.distribution.renewal[id];

                row.consumedQuantity = undefined;
                //row.isLoading = true;
            }
        }


        /**
         * @param {Object} $scope
         * @param {Resource} consumption
         * @param {String} user             User ID when admin create/modify absence for another user
         */
        function setConsumedQuantities($scope, consumption, user) {

            var renewals = $scope.distribution.renewal;
            var periods = $scope.selection.periods;
            var distribution;

            if (0 === periods.length) {
                throw new Error('No periods in selection');
            }

            try {
                distribution = createDistribution(renewals, periods, $scope.accountRights, false);
            } catch(e) {
                removeAllConsumptions($scope);
                return;
            }

            var params = {
                selection: {
                    begin: $scope.selection.begin,
                    end: $scope.selection.end
                },
                distribution: distribution,
                collection: $scope.collection._id
            };


            if (user) {
                params.user = user;
            }

            var row;

            // this is a GET in POST:
            consumption.create(params).$promise
            .then(function(renewalCons) {
                removeAllConsumptions($scope);
                for (var id in renewalCons) {
                    if (!renewalCons.hasOwnProperty(id)) {
                        continue;
                    }

                    row = $scope.distribution.renewal[id];
                    if (undefined === row) {
                        continue;
                    }
                    row.consumedQuantity = renewalCons[id];
                    //row.isLoading = false;
                }

            });

        }




        /**
         * The next button, from the period selection to the right distribution interface
         *
         * @param {object} $scope
         * @param {object} user
         * @param {Resource} accountRights
         * @param {Resource} consumption
         */
        function getNextButtonJob($scope, user, accountRights) {

            return function() {
                // hide the period selection
                $scope.periodSelection = false;

                // show the right assignement
                $scope.assignments = true;

                // init distribution if this is a request modification after accountRights.$promise
                $scope.distribution = {
                    class: {},
                    renewal: {},
                    total: 0,
                    completed: false
                };

                /**
                 * Load accountRights
                 * the list of rights accessible on the selected period
                 */
                $scope.accountRights = accountRights.query({
                    user: user._id, // need that if the request is created by the admin, ignored if created by the user
                    dtstart: $scope.selection.begin,
                    dtend: $scope.selection.end
                });

                function createAccountRenewal(item, renewalIndex)
                {
                    var accountRightRenewal = Object.create(item);
                    accountRightRenewal.renewal = item.renewals[renewalIndex];

                    $scope.distribution.renewal[accountRightRenewal.renewal._id] = {
                        quantity: null,
                        right: item._id
                    };

                    return accountRightRenewal;
                }

                $scope.getIcon = function(listItem) {
                    if (listItem.fold) {
                        return 'fa-minus-square';
                    } else {
                        return 'fa-plus-square';
                    }
                };

                $scope.accountRights.$promise.then(function(ar) {
                    // loaded
                    var days=0, hours=0;

                    function updateTotal(right, accountRightRenewal, groupAvailable) {
                        available[accountRightRenewal.renewal._id] = ar[i].available_quantity;
                        quantity_unit[accountRightRenewal._id] = ar[i].quantity_unit;

                        switch (accountRightRenewal.quantity_unit) {
                            case 'D':
                                groupAvailable.days  += accountRightRenewal.renewal.available_quantity;
                                days                 += accountRightRenewal.renewal.available_quantity;
                                break;

                            case 'H':
                                groupAvailable.hours += accountRightRenewal.renewal.available_quantity;
                                hours                += accountRightRenewal.renewal.available_quantity;
                                break;
                        }
                    }

                    // Group by types
                    var typesIndex = {};
                    $scope.types = [];
                    var accountRightRenewal;

                    for (var i=0; i<ar.length; i++) {
                        var type = ar[i].type;
                        if (ar[i].renewals.length > 0) {
                            if (undefined === typesIndex[type._id]) {
                                $scope.types.push({
                                    fold: !type.group,
                                    type: type,
                                    accountRightsRenewals: [],
                                    available: {
                                        days: 0,
                                        hours: 0
                                    }
                                });
                                typesIndex[type._id] = $scope.types.length-1;
                            }

                            for(var j=0; j<ar[i].renewals.length; j++) {
                                accountRightRenewal = createAccountRenewal(ar[i], j);
                                $scope.types[typesIndex[type._id]].accountRightsRenewals.push(accountRightRenewal);
                                updateTotal(ar[i], accountRightRenewal, $scope.types[typesIndex[type._id]].available);
                            }
                        }
                    }

                    // total on each type group
                    $scope.types.forEach(function(item) {
                        item.available.display = RequestEdit.getDuration(item.available.days, item.available.hours);
                    });

                    // grand total
                    $scope.available = {
                        total: RequestEdit.getDuration(days, hours)
                    };

                    // load distribution if this is a request modification
                    if (undefined !== $scope.request.absence && $scope.request.absence.distribution.length > 0) {

                        setDistributionRequest($scope);
                        distributionWatch($scope.distribution, $scope);
                    }
                    else {
                        // This is a new request, set input quantity for rights where it is allowed
                        initializeDistribution($scope);
                    }


                }, function(err) {
                    err.data.$outcome.alert.forEach(function(e) {
                        $scope.pageAlerts.push({
                            message: e.message,
                            type: 'danger'
                        });
                    });

                });

            };
        }



        /**
         * Process scope once the user document is available
         * Add the watchs
         *
         * @param   {Object} $scope
         * @param   {Object} user           user document as object
         * @param   {Resource} calendarEvents
         * @param   {Resource} consumption
         */
        function onceUserLoaded($scope, user, calendarEvents, consumption)
        {
            if (!user.roles.account) {
                throw new Error('the user must have a vacation account');
            }

            RequestEdit.onceUserLoaded($scope, user, calendarEvents);

            $scope.$watch('distribution', function(distribution) {
                distributionWatch(distribution, $scope);
            }, true);

            $scope.$watch('distribution.renewal', function(newValue, oldValue) {
                // detect renewal modifications
                for (var rId in newValue) {
                    if (newValue.hasOwnProperty(rId)) {
                        if (undefined === oldValue[rId] || (newValue[rId].quantity !== oldValue[rId].quantity)) {
                            return setConsumedQuantities($scope, consumption, user._id);
                        }
                    }
                }
            }, true);
        }


        /**
         * Get Period picker callback for working times
         * @param {Resource} calendarEvents
         * @param {Array} personalEventList personal events list, the list of events curently modified
         * @param {Object} user optional parameter used for admin query
         *
         * @return function
         */
        function getLoadWorkingTimes(calendarEvents, personalEventList, user) {
            return function(interval) {
                var queryParams = {
                    type: 'workschedule',
                    dtstart: interval.from,
                    dtend: interval.to,
                    subtractNonWorkingDays: true,
                    subtractPersonalEvents: true
                };

                if (user) {
                    queryParams.user = user._id;
                }

                if (angular.isArray(personalEventList) && personalEventList.length > 0) {
                    queryParams.subtractException = [];
                    personalEventList.forEach(function(personalEvent) {
                        queryParams.subtractException.push(personalEvent._id);
                    });
                }

                return calendarEvents.query(queryParams).$promise;
            };
        }


        // Service to edit an absence,
        // shared by account/request/absence-edit and admin/request/absence-edit

        return {
            initScope: RequestEdit.initScope,
            setSelectionFromRequest: RequestEdit.setSelectionFromRequest,
            getLoadPersonalEvents: RequestEdit.getLoadPersonalEvents,
            getLoadNonWorkingDaysEvents: RequestEdit.getLoadNonWorkingDaysEvents,
            getLoadEvents: RequestEdit.getLoadEvents,
            getLoadScholarHolidays: RequestEdit.getLoadScholarHolidays,

            createDistribution: createDistribution,
            cleanDocument: cleanDocument,
            getNextButtonJob: getNextButtonJob,
            onceUserLoaded: onceUserLoaded,
            getLoadWorkingTimes: getLoadWorkingTimes
        };
    };
});