<commit_before>import { dispatch as d3_dispatch } from 'd3-dispatch';
import { event as d3_event, select as d3_select } from 'd3-selection';
import { utilGetSetValue, utilRebind, utilTriggerEvent } from '../util';

// This code assumes that the combobox values will not have duplicate entries.
// It is keyed on the `value` of the entry. Data should be an array of objects like:
//   [{
//       value:  'display text',  // required
//       title:  'hover text'     // optional
//   }, ...]

var _comboHideTimerID;

export function uiCombobox(context, klass) {
    var dispatch = d3_dispatch('accept', 'cancel');
    var container = context.container();

    var _suggestions = [];
    var _data = [];
    var _fetched = {};
    var _selected = null;
    var _canAutocomplete = true;
    var _caseSensitive = false;
    var _cancelFetch = false;
    var _minItems = 2;
    var _tDown = 0;
    var _mouseEnterHandler, _mouseLeaveHandler;

    var _fetcher = function(val, cb) {
        cb(_data.filter(function(d) {
            var terms = d.terms || [];
            terms.push(d.value);
            return terms.some(function(term) {
                return term
                    .toString()
                    .toLowerCase()
                    .indexOf(val.toLowerCase()) !== -1;
            });
        }));
    };

    var combobox = function(input, attachTo) {
        if (!input || input.empty()) return;

        input
            .classed('combobox-input', true)
            .on('focus.combo-input', focus)
            .on('blur.combo-input', blur)
            .on('keydown.combo-input', keydown)
            .on('keyup.combo-input', keyup)
            .on('input.combo-input', change)
            .on('mousedown.combo-input', mousedown)
            .each(function() {
                var parent = this.parentNode;
                var sibling = this.nextSibling;

                d3_select(parent).selectAll('.combobox-caret')
                    .filter(function(d) { return d === input.node(); })
                    .data([input.node()])
                    .enter()
                    .insert('div', function() { return sibling; })
                    .attr('class', 'combobox-caret')
                    .on('mousedown.combo-caret', function() {
                        d3_event.preventDefault(); // don't steal focus from input
                        input.node().focus(); // focus the input as if it was clicked
                        mousedown();
                    })
                    .on('mouseup.combo-caret', function() {
                        d3_event.preventDefault(); // don't steal focus from input
                        mouseup();
                    });
            });

        function mousedown() {
            if (d3_event.button !== 0) return;    // left click only
            _tDown = +new Date();

            // clear selection
            var start = input.property('selectionStart');
            var end = input.property('selectionEnd');
            if (start !== end) {
                var val = utilGetSetValue(input);
                input.node().setSelectionRange(val.length, val.length);
                return;
            }

            input.on('mouseup.combo-input', mouseup);
        }

        function mouseup() {
            input.on('mouseup.combo-input', null);
            if (d3_event.button !== 0) return;    // left click only
            if (input.node() !== document.activeElement) return;   // exit if this input is not focused

            var start = input.property('selectionStart');
            var end = input.property('selectionEnd');
            if (start !== end) return;  // exit if user is selecting

            // not showing or showing for a different field - try to show it.
            var combo = container.selectAll('.combobox');
            if (combo.empty() || combo.datum() !== input.node()) {
                var tOrig = _tDown;
                window.setTimeout(function() {
                    if (tOrig !== _tDown) return;   // exit if user double clicked
                    fetchComboData('', function() {
                        show();
                        render();
                    });
                }, 250);

            } else {
                hide();
            }
        }

        function focus() {
            fetchComboData('');   // prefetch values (may warm taginfo cache)
        }

        function blur() {
            _comboHideTimerID = window.setTimeout(hide, 75);
        }

        function show() {
            hide();   // remove any existing

            container
                .insert('div', ':first-child')
                .datum(input.node())
                .attr('class', 'combobox' + (klass ? ' combobox-' + klass : ''))
                .style('position', 'absolute')
                .style('display', 'block')
                .style('left', '0px')
                .on('mousedown.combo-container', function () {
                    // prevent moving focus out of the input field
                    d3_event.preventDefault();
                });

            container
                .on('scroll.combo-scroll', render, true);
        }

        function hide() {
            if (_comboHideTimerID) {
                window.clearTimeout(_comboHideTimerID);
                _comboHideTimerID = undefined;
            }

            container.selectAll('.combobox')
                .remove();

            container
                .on('scroll.combo-scroll', null);
        }

        function keydown() {
            var shown = !container.selectAll('.combobox').empty();
            var tagName = input.node() ? input.node().tagName.toLowerCase() : '';

            switch (d3_event.keyCode) {
                case 8:   // ⌫ Backspace
                case 46:  // ⌦ Delete
                    d3_event.stopPropagation();
                    _selected = null;
                    render();
                    input.on('input.combo-input', function() {
                        var start = input.property('selectionStart');
                        input.node().setSelectionRange(start, start);
                        input.on('input.combo-input', change);
                    });
                    break;

                case 9:   // ⇥ Tab
                    accept();
                    break;

                case 13:  // ↩ Return
                    d3_event.preventDefault();
                    d3_event.stopPropagation();
                    break;

                case 38:  // ↑ Up arrow
                    if (tagName === 'textarea' && !shown) return;
                    d3_event.preventDefault();
                    if (tagName === 'input' && !shown) {
                        show();
                    }
                    nav(-1);
                    break;

                case 40:  // ↓ Down arrow
                    if (tagName === 'textarea' && !shown) return;
                    d3_event.preventDefault();
                    if (tagName === 'input' && !shown) {
                        show();
                    }
                    nav(+1);
                    break;
            }
        }

        function keyup() {
            switch (d3_event.keyCode) {
                case 27:  // ⎋ Escape
                    cancel();
                    break;

                case 13:  // ↩ Return
                    accept();
                    break;
            }
        }

        // Called whenever the input value is changed (e.g. on typing)
        function change() {
            fetchComboData(value(), function() {
                _selected = null;
                var val = input.property('value');

                if (_suggestions.length) {
                    if (input.property('selectionEnd') === val.length) {
                        _selected = tryAutocomplete();
                    }

                    if (!_selected) {
                        _selected = val;
                    }
                }

                if (val.length) {
                    var combo = container.selectAll('.combobox');
                    if (combo.empty()) {
                        show();
                    }
                } else {
                    hide();
                }

                render();
            });
        }

        // Called when the user presses up/down arrows to navigate the list
        function nav(dir) {
            if (_suggestions.length) {
                // try to determine previously selected index..
                var index = -1;
                for (var i = 0; i < _suggestions.length; i++) {
                    if (_selected && _suggestions[i].value === _selected) {
                        index = i;
                        break;
                    }
                }

                // pick new _selected
                index = Math.max(Math.min(index + dir, _suggestions.length - 1), 0);
                _selected = _suggestions[index].value;
                input.property('value', _selected);
            }

            render();
            ensureVisible();
        }

        function ensureVisible() {
            var combo = container.selectAll('.combobox');
            if (combo.empty()) return;

            var containerRect = container.node().getBoundingClientRect();
            var comboRect = combo.node().getBoundingClientRect();

            if (comboRect.bottom > containerRect.bottom) {
                var node = attachTo ? attachTo.node() : input.node();
                node.scrollIntoView({ behavior: 'instant', block: 'center' });
                render();
            }

            // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move
            var selected = combo.selectAll('.combobox-option.selected').node();
            if (selected) {
                selected.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
            }
        }

        function value() {
            var value = input.property('value');
            var start = input.property('selectionStart');
            var end = input.property('selectionEnd');

            if (start && end) {
                value = value.substring(0, start);
            }

            return value;
        }

        function fetchComboData(v, cb) {
            _cancelFetch = false;

            _fetcher.call(input, v, function(results) {
                // already chose a value, don't overwrite or autocomplete it
                if (_cancelFetch) return;

                _suggestions = results;
                results.forEach(function(d) { _fetched[d.value] = d; });

                if (cb) {
                    cb();
                }
            });
        }

        function tryAutocomplete() {
            if (!_canAutocomplete) return;

            var val = _caseSensitive ? value() : value().toLowerCase();
            if (!val) return;

            // Don't autocomplete if user is typing a number - #4935
            if (!isNaN(parseFloat(val)) && isFinite(val)) return;

            var bestIndex = -1;
            for (var i = 0; i < _suggestions.length; i++) {
                var suggestion = _suggestions[i].value;
                var compare = _caseSensitive ? suggestion : suggestion.toLowerCase();

                // if search string matches suggestion exactly, pick it..
                if (compare === val) {
                    bestIndex = i;
                    break;

                // otherwise lock in the first result that starts with the search string..
                } else if (bestIndex === -1 && compare.indexOf(val) === 0) {
                    bestIndex = i;
                }
            }

            if (bestIndex !== -1) {
                var bestVal = _suggestions[bestIndex].value;
                input.property('value', bestVal);
                input.node().setSelectionRange(val.length, bestVal.length);
                return bestVal;
            }
        }

        function render() {
            if (_suggestions.length < _minItems || document.activeElement !== input.node()) {
                hide();
                return;
            }

            var shown = !container.selectAll('.combobox').empty();
            if (!shown) return;

            var combo = container.selectAll('.combobox');
            var options = combo.selectAll('.combobox-option')
                .data(_suggestions, function(d) { return d.value; });

            options.exit()
                .remove();

            // enter/update
            options.enter()
                .append('a')
                .attr('class', 'combobox-option')
                .attr('title', function(d) { return d.title; })
                .text(function(d) { return d.display || d.value; })
                .on('mouseenter', _mouseEnterHandler)
                .on('mouseleave', _mouseLeaveHandler)
                .merge(options)
                .classed('selected', function(d) { return d.value === _selected; })
                .on('click.combo-option', accept)
                .order();

            var node = attachTo ? attachTo.node() : input.node();
            var rect = node.getBoundingClientRect();

            combo
                .style('left', (rect.left + 5) + 'px')
                .style('width', (rect.width - 10) + 'px')
                .style('top', rect.height + rect.top + 'px');
        }

        // Dispatches an 'accept' event
        // Then hides the combobox.
        function accept(d) {
            _cancelFetch = true;
            var thiz = input.node();

            if (d) {   // user clicked on a suggestion
                utilGetSetValue(input, d.value);    // replace field contents
                utilTriggerEvent(input, 'change');
            }

            // clear (and keep) selection
            var val = utilGetSetValue(input);
            thiz.setSelectionRange(val.length, val.length);

            d = _fetched[val];
            dispatch.call('accept', thiz, d, val);
            hide();
        }

        // Dispatches an 'cancel' event
        // Then hides the combobox.
        function cancel() {
            _cancelFetch = true;
            var thiz = input.node();

            // clear (and remove) selection, and replace field contents
            var val = utilGetSetValue(input);
            var start = input.property('selectionStart');
            var end = input.property('selectionEnd');
            val = val.slice(0, start) + val.slice(end);
            utilGetSetValue(input, val);
            thiz.setSelectionRange(val.length, val.length);

            dispatch.call('cancel', thiz);
            hide();
        }

    };

    combobox.canAutocomplete = function(val) {
        if (!arguments.length) return _canAutocomplete;
        _canAutocomplete = val;
        return combobox;
    };

    combobox.caseSensitive = function(val) {
        if (!arguments.length) return _caseSensitive;
        _caseSensitive = val;
        return combobox;
    };

    combobox.data = function(val) {
        if (!arguments.length) return _data;
        _data = val;
        return combobox;
    };

    combobox.fetcher = function(val) {
        if (!arguments.length) return _fetcher;
        _fetcher = val;
        return combobox;
    };

    combobox.minItems = function(val) {
        if (!arguments.length) return _minItems;
        _minItems = val;
        return combobox;
    };

    combobox.itemsMouseEnter = function(val) {
        if (!arguments.length) return _mouseEnterHandler;
        _mouseEnterHandler = val;
        return combobox;
    };

    combobox.itemsMouseLeave = function(val) {
        if (!arguments.length) return _mouseLeaveHandler;
        _mouseLeaveHandler = val;
        return combobox;
    };

    return utilRebind(combobox, dispatch, 'on');
}

uiCombobox.off = function(input, context) {
    input
        .on('focus.combo-input', null)
        .on('blur.combo-input', null)
        .on('keydown.combo-input', null)
        .on('keyup.combo-input', null)
        .on('input.combo-input', null)
        .on('mousedown.combo-input', null)
        .on('mouseup.combo-input', null);

    context.container()
        .on('scroll.combo-scroll', null);
};
<commit_msg>Account for container offset when positioning combobox dropdowns (close #7458)<commit_after>import { dispatch as d3_dispatch } from 'd3-dispatch';
import { event as d3_event, select as d3_select } from 'd3-selection';
import { utilGetSetValue, utilRebind, utilTriggerEvent } from '../util';

// This code assumes that the combobox values will not have duplicate entries.
// It is keyed on the `value` of the entry. Data should be an array of objects like:
//   [{
//       value:  'display text',  // required
//       title:  'hover text'     // optional
//   }, ...]

var _comboHideTimerID;

export function uiCombobox(context, klass) {
    var dispatch = d3_dispatch('accept', 'cancel');
    var container = context.container();

    var _suggestions = [];
    var _data = [];
    var _fetched = {};
    var _selected = null;
    var _canAutocomplete = true;
    var _caseSensitive = false;
    var _cancelFetch = false;
    var _minItems = 2;
    var _tDown = 0;
    var _mouseEnterHandler, _mouseLeaveHandler;

    var _fetcher = function(val, cb) {
        cb(_data.filter(function(d) {
            var terms = d.terms || [];
            terms.push(d.value);
            return terms.some(function(term) {
                return term
                    .toString()
                    .toLowerCase()
                    .indexOf(val.toLowerCase()) !== -1;
            });
        }));
    };

    var combobox = function(input, attachTo) {
        if (!input || input.empty()) return;

        input
            .classed('combobox-input', true)
            .on('focus.combo-input', focus)
            .on('blur.combo-input', blur)
            .on('keydown.combo-input', keydown)
            .on('keyup.combo-input', keyup)
            .on('input.combo-input', change)
            .on('mousedown.combo-input', mousedown)
            .each(function() {
                var parent = this.parentNode;
                var sibling = this.nextSibling;

                d3_select(parent).selectAll('.combobox-caret')
                    .filter(function(d) { return d === input.node(); })
                    .data([input.node()])
                    .enter()
                    .insert('div', function() { return sibling; })
                    .attr('class', 'combobox-caret')
                    .on('mousedown.combo-caret', function() {
                        d3_event.preventDefault(); // don't steal focus from input
                        input.node().focus(); // focus the input as if it was clicked
                        mousedown();
                    })
                    .on('mouseup.combo-caret', function() {
                        d3_event.preventDefault(); // don't steal focus from input
                        mouseup();
                    });
            });

        function mousedown() {
            if (d3_event.button !== 0) return;    // left click only
            _tDown = +new Date();

            // clear selection
            var start = input.property('selectionStart');
            var end = input.property('selectionEnd');
            if (start !== end) {
                var val = utilGetSetValue(input);
                input.node().setSelectionRange(val.length, val.length);
                return;
            }

            input.on('mouseup.combo-input', mouseup);
        }

        function mouseup() {
            input.on('mouseup.combo-input', null);
            if (d3_event.button !== 0) return;    // left click only
            if (input.node() !== document.activeElement) return;   // exit if this input is not focused

            var start = input.property('selectionStart');
            var end = input.property('selectionEnd');
            if (start !== end) return;  // exit if user is selecting

            // not showing or showing for a different field - try to show it.
            var combo = container.selectAll('.combobox');
            if (combo.empty() || combo.datum() !== input.node()) {
                var tOrig = _tDown;
                window.setTimeout(function() {
                    if (tOrig !== _tDown) return;   // exit if user double clicked
                    fetchComboData('', function() {
                        show();
                        render();
                    });
                }, 250);

            } else {
                hide();
            }
        }

        function focus() {
            fetchComboData('');   // prefetch values (may warm taginfo cache)
        }

        function blur() {
            _comboHideTimerID = window.setTimeout(hide, 75);
        }

        function show() {
            hide();   // remove any existing

            container
                .insert('div', ':first-child')
                .datum(input.node())
                .attr('class', 'combobox' + (klass ? ' combobox-' + klass : ''))
                .style('position', 'absolute')
                .style('display', 'block')
                .style('left', '0px')
                .on('mousedown.combo-container', function () {
                    // prevent moving focus out of the input field
                    d3_event.preventDefault();
                });

            container
                .on('scroll.combo-scroll', render, true);
        }

        function hide() {
            if (_comboHideTimerID) {
                window.clearTimeout(_comboHideTimerID);
                _comboHideTimerID = undefined;
            }

            container.selectAll('.combobox')
                .remove();

            container
                .on('scroll.combo-scroll', null);
        }

        function keydown() {
            var shown = !container.selectAll('.combobox').empty();
            var tagName = input.node() ? input.node().tagName.toLowerCase() : '';

            switch (d3_event.keyCode) {
                case 8:   // ⌫ Backspace
                case 46:  // ⌦ Delete
                    d3_event.stopPropagation();
                    _selected = null;
                    render();
                    input.on('input.combo-input', function() {
                        var start = input.property('selectionStart');
                        input.node().setSelectionRange(start, start);
                        input.on('input.combo-input', change);
                    });
                    break;

                case 9:   // ⇥ Tab
                    accept();
                    break;

                case 13:  // ↩ Return
                    d3_event.preventDefault();
                    d3_event.stopPropagation();
                    break;

                case 38:  // ↑ Up arrow
                    if (tagName === 'textarea' && !shown) return;
                    d3_event.preventDefault();
                    if (tagName === 'input' && !shown) {
                        show();
                    }
                    nav(-1);
                    break;

                case 40:  // ↓ Down arrow
                    if (tagName === 'textarea' && !shown) return;
                    d3_event.preventDefault();
                    if (tagName === 'input' && !shown) {
                        show();
                    }
                    nav(+1);
                    break;
            }
        }

        function keyup() {
            switch (d3_event.keyCode) {
                case 27:  // ⎋ Escape
                    cancel();
                    break;

                case 13:  // ↩ Return
                    accept();
                    break;
            }
        }

        // Called whenever the input value is changed (e.g. on typing)
        function change() {
            fetchComboData(value(), function() {
                _selected = null;
                var val = input.property('value');

                if (_suggestions.length) {
                    if (input.property('selectionEnd') === val.length) {
                        _selected = tryAutocomplete();
                    }

                    if (!_selected) {
                        _selected = val;
                    }
                }

                if (val.length) {
                    var combo = container.selectAll('.combobox');
                    if (combo.empty()) {
                        show();
                    }
                } else {
                    hide();
                }

                render();
            });
        }

        // Called when the user presses up/down arrows to navigate the list
        function nav(dir) {
            if (_suggestions.length) {
                // try to determine previously selected index..
                var index = -1;
                for (var i = 0; i < _suggestions.length; i++) {
                    if (_selected && _suggestions[i].value === _selected) {
                        index = i;
                        break;
                    }
                }

                // pick new _selected
                index = Math.max(Math.min(index + dir, _suggestions.length - 1), 0);
                _selected = _suggestions[index].value;
                input.property('value', _selected);
            }

            render();
            ensureVisible();
        }

        function ensureVisible() {
            var combo = container.selectAll('.combobox');
            if (combo.empty()) return;

            var containerRect = container.node().getBoundingClientRect();
            var comboRect = combo.node().getBoundingClientRect();

            if (comboRect.bottom > containerRect.bottom) {
                var node = attachTo ? attachTo.node() : input.node();
                node.scrollIntoView({ behavior: 'instant', block: 'center' });
                render();
            }

            // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move
            var selected = combo.selectAll('.combobox-option.selected').node();
            if (selected) {
                selected.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
            }
        }

        function value() {
            var value = input.property('value');
            var start = input.property('selectionStart');
            var end = input.property('selectionEnd');

            if (start && end) {
                value = value.substring(0, start);
            }

            return value;
        }

        function fetchComboData(v, cb) {
            _cancelFetch = false;

            _fetcher.call(input, v, function(results) {
                // already chose a value, don't overwrite or autocomplete it
                if (_cancelFetch) return;

                _suggestions = results;
                results.forEach(function(d) { _fetched[d.value] = d; });

                if (cb) {
                    cb();
                }
            });
        }

        function tryAutocomplete() {
            if (!_canAutocomplete) return;

            var val = _caseSensitive ? value() : value().toLowerCase();
            if (!val) return;

            // Don't autocomplete if user is typing a number - #4935
            if (!isNaN(parseFloat(val)) && isFinite(val)) return;

            var bestIndex = -1;
            for (var i = 0; i < _suggestions.length; i++) {
                var suggestion = _suggestions[i].value;
                var compare = _caseSensitive ? suggestion : suggestion.toLowerCase();

                // if search string matches suggestion exactly, pick it..
                if (compare === val) {
                    bestIndex = i;
                    break;

                // otherwise lock in the first result that starts with the search string..
                } else if (bestIndex === -1 && compare.indexOf(val) === 0) {
                    bestIndex = i;
                }
            }

            if (bestIndex !== -1) {
                var bestVal = _suggestions[bestIndex].value;
                input.property('value', bestVal);
                input.node().setSelectionRange(val.length, bestVal.length);
                return bestVal;
            }
        }

        function render() {
            if (_suggestions.length < _minItems || document.activeElement !== input.node()) {
                hide();
                return;
            }

            var shown = !container.selectAll('.combobox').empty();
            if (!shown) return;

            var combo = container.selectAll('.combobox');
            var options = combo.selectAll('.combobox-option')
                .data(_suggestions, function(d) { return d.value; });

            options.exit()
                .remove();

            // enter/update
            options.enter()
                .append('a')
                .attr('class', 'combobox-option')
                .attr('title', function(d) { return d.title; })
                .text(function(d) { return d.display || d.value; })
                .on('mouseenter', _mouseEnterHandler)
                .on('mouseleave', _mouseLeaveHandler)
                .merge(options)
                .classed('selected', function(d) { return d.value === _selected; })
                .on('click.combo-option', accept)
                .order();

            var node = attachTo ? attachTo.node() : input.node();
            var containerRect = container.node().getBoundingClientRect();
            var rect = node.getBoundingClientRect();

            combo
                .style('left', (rect.left + 5 - containerRect.left) + 'px')
                .style('width', (rect.width - 10) + 'px')
                .style('top', (rect.height + rect.top - containerRect.top) + 'px');
        }

        // Dispatches an 'accept' event
        // Then hides the combobox.
        function accept(d) {
            _cancelFetch = true;
            var thiz = input.node();

            if (d) {   // user clicked on a suggestion
                utilGetSetValue(input, d.value);    // replace field contents
                utilTriggerEvent(input, 'change');
            }

            // clear (and keep) selection
            var val = utilGetSetValue(input);
            thiz.setSelectionRange(val.length, val.length);

            d = _fetched[val];
            dispatch.call('accept', thiz, d, val);
            hide();
        }

        // Dispatches an 'cancel' event
        // Then hides the combobox.
        function cancel() {
            _cancelFetch = true;
            var thiz = input.node();

            // clear (and remove) selection, and replace field contents
            var val = utilGetSetValue(input);
            var start = input.property('selectionStart');
            var end = input.property('selectionEnd');
            val = val.slice(0, start) + val.slice(end);
            utilGetSetValue(input, val);
            thiz.setSelectionRange(val.length, val.length);

            dispatch.call('cancel', thiz);
            hide();
        }

    };

    combobox.canAutocomplete = function(val) {
        if (!arguments.length) return _canAutocomplete;
        _canAutocomplete = val;
        return combobox;
    };

    combobox.caseSensitive = function(val) {
        if (!arguments.length) return _caseSensitive;
        _caseSensitive = val;
        return combobox;
    };

    combobox.data = function(val) {
        if (!arguments.length) return _data;
        _data = val;
        return combobox;
    };

    combobox.fetcher = function(val) {
        if (!arguments.length) return _fetcher;
        _fetcher = val;
        return combobox;
    };

    combobox.minItems = function(val) {
        if (!arguments.length) return _minItems;
        _minItems = val;
        return combobox;
    };

    combobox.itemsMouseEnter = function(val) {
        if (!arguments.length) return _mouseEnterHandler;
        _mouseEnterHandler = val;
        return combobox;
    };

    combobox.itemsMouseLeave = function(val) {
        if (!arguments.length) return _mouseLeaveHandler;
        _mouseLeaveHandler = val;
        return combobox;
    };

    return utilRebind(combobox, dispatch, 'on');
}

uiCombobox.off = function(input, context) {
    input
        .on('focus.combo-input', null)
        .on('blur.combo-input', null)
        .on('keydown.combo-input', null)
        .on('keyup.combo-input', null)
        .on('input.combo-input', null)
        .on('mousedown.combo-input', null)
        .on('mouseup.combo-input', null);

    context.container()
        .on('scroll.combo-scroll', null);
};
<|endoftext|>


<commit_before>'use strict';

class Scanner {
    static get SCAN_INTERVAL_MILLS() {
        return 1500;
    }

    static _isObject(target) {
        return Object.prototype.toString.call(target) === '[object Object]';
    }

    constructor() {
        this.serialport = require('@serialport/stream');
        this.serialport.Binding = require('@entrylabs/bindings');
    }

    startScan(extension, config, callback, router) {
        this.router = router;
        this.config = config;
        this.slaveTimers = {};
        this.connectors = {};
        this.scanCount = 0;
        this.closeConnectors();
        this.clearTimers();

        this.scan(extension, callback);
        this.timer = setInterval(() => {
            this.scan(extension, callback);
        }, Scanner.SCAN_INTERVAL_MILLS);
    };

    stopScan() {
        this.config = undefined;
        this.clearTimers();
        this.closeConnectors();
    };

    scan(extension, callback) {
        this.serialport.list(
            /**
             * @param error{Error}
             * @param devices{Array<Object>}
             */
            (error, devices) => {
                if (error) {
                    if (callback) {
                        callback(error);
                    }
                    return;
                }

                const { serverMode, hardware } = this.config;
                let { select_com_port:selectComPort } = this.config;
                const { scanType, comName:checkComName, control, duration, firmwarecheck, pnpId, type } = hardware;
                let { vendor } = hardware;

                if (vendor && Scanner._isObject(vendor)) {
                    vendor = vendor[process.platform];
                }

                if (selectComPort && Scanner._isObject(selectComPort)) {
                    selectComPort = selectComPort[process.platform];
                }

                const checkComPort = (selectComPort || type === 'bluetooth' || serverMode === 1) || false;
                const myComPort = this.config.this_com_port;

                if (checkComPort && !myComPort) {
                    this.router.emit('state', 'select_port', devices);
                    callback();
                    return;
                }

                if (scanType === 'data') {
                    if (this.scanCount < 5) {
                        this.scanCount++;
                    } else {
                        if (devices.some(function(device) {
                            let isVendor = false;
                            if (Array.isArray(vendor)) {
                                isVendor = vendor.some(function(v) {
                                    return device.manufacturer && device.manufacturer.indexOf(v) >= 0;
                                });
                            } else {
                                if (device.manufacturer && device.manufacturer.indexOf(vendor) >= 0) {
                                    isVendor = true;
                                }
                            }

                            return device.manufacturer && isVendor;
                        }) === false) {
                            vendor = undefined;
                        }
                    }
                }

                devices.forEach((device) => {
                    let isVendor = false;
                    let isComName = false;
                    const comName = device.comName || this.config.hardware.name;

                    if (Array.isArray(vendor)) {
                        isVendor = vendor.some(function(name) {
                            return device.manufacturer && device.manufacturer.indexOf(name) >= 0;
                        });
                    } else if (vendor && device.manufacturer && device.manufacturer.indexOf(vendor) >= 0) {
                        isVendor = true;
                    }

                    if (Array.isArray(checkComName)) {
                        isComName = checkComName.some(function(name) {
                            return comName.indexOf(name) >= 0;
                        });
                    } else if (checkComName && comName.indexOf(checkComName) >= 0) {
                        isComName = true;
                    }

                    if (!vendor || (device.manufacturer && isVendor) || (device.pnpId && device.pnpId.indexOf(pnpId) >= 0) || isComName || checkComPort) {

                        if (checkComPort && comName !== myComPort) {
                            return;
                        }

                        // comName = '/dev/tty.EV3-SerialPort';
                        let connector = this.connectors[comName];
                        if (connector === undefined) {
                            connector = require('../connector/serial').create();
                            this.connectors[comName] = connector;
                            connector.open(comName, this.config.hardware, (error, sp) => {
                                if (error) {
                                    delete this.connectors[comName];
                                    if (callback) {
                                        callback(error);
                                    }
                                } else {
                                    this.setConnector(connector);
                                    if (control) {
                                        let flashFirmware;
                                        if (firmwarecheck) {
                                            flashFirmware = setTimeout(() => {
                                                sp.removeAllListeners('data');
                                                connector.executeFlash = true;
                                                this.finalizeScan(comName, connector, callback);
                                            }, 3000);
                                        }

                                        if (control === 'master') {
                                            if (extension.checkInitialData && extension.requestInitialData) {

                                                let source = sp;
                                                if (sp.parser){
                                                    source = sp.parser;
                                                }
                                                source.on('data', (data) => {
                                                    if (this.config.hardware.stream === 'string') {
                                                        data = data.toString();
                                                    }

                                                    const result = extension.checkInitialData(data, this.config);
                                                    if (result === undefined) {
                                                        connector.send(extension.requestInitialData());
                                                    } else {
                                                        source.removeAllListeners('data'); // modify  sp --> source
                                                        clearTimeout(flashFirmware);
                                                        if (result === true) {

                                                            if (extension.setSerialPort) {
                                                                extension.setSerialPort(sp);
                                                            }

                                                            this.finalizeScan(comName, connector, callback);
                                                        } else if (callback) {
                                                            callback(new Error('Invalid hardware'));
                                                        }
                                                    }
                                                });
                                            }
                                        } else {
                                            if (duration && extension.checkInitialData && extension.requestInitialData) {
                                                sp.on('data', (data) => {
                                                    if (this.config.hardware.stream === 'string') {
                                                        data = data.toString();
                                                    }

                                                    const result = extension.checkInitialData(data, this.config);
                                                    if (result !== undefined) {
                                                        sp.removeAllListeners('data');
                                                        clearTimeout(flashFirmware);
                                                        if (result === true) {
                                                            if (extension.setSerialPort) {
                                                                extension.setSerialPort(sp);
                                                            }
                                                            if (extension.resetProperty) {
                                                                connector.send(extension.resetProperty());
                                                            }
                                                            this.finalizeScan(comName, connector, callback);
                                                        } else if (callback) {
                                                            callback(new Error('Invalid hardware'));
                                                        }
                                                    }
                                                });
                                                let slaveTimer = this.slaveTimers[comName];
                                                if (slaveTimer) {
                                                    clearInterval(slaveTimer);
                                                }
                                                slaveTimer = setInterval(() => {
                                                    connector.send(extension.requestInitialData(sp));
                                                }, duration);
                                                this.slaveTimers[comName] = slaveTimer;
                                            }
                                        }
                                    } else {
                                        this.finalizeScan(comName, connector, callback);
                                    }
                                }
                            });
                        }
                    }
                });
            });
    };

    clearTimers() {
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = undefined;
        }
        const slaveTimers = this.slaveTimers;
        if (slaveTimers) {
            let slaveTimer;
            for (const key in slaveTimers) {
                slaveTimer = slaveTimers[key];
                if (slaveTimer) {
                    clearInterval(slaveTimer);
                }
            }
        }
        this.slaveTimers = {};
    };

    setConnector(connector) {
        this.router.connector = connector;
        this.router.emit('state', 'before_connect');
    };

    finalizeScan(comName, connector, callback) {
        if (this.connectors && comName) {
            this.connectors[comName] = undefined;
        }
        this.stopScan();

        if (callback) {
            callback(null, connector);
        }
    };

    closeConnectors() {
        const connectors = this.connectors;
        if (connectors) {
            let connector;
            for (const key in connectors) {
                connector = connectors[key];
                if (connector) {
                    connector.close();
                }
            }
        }
        this.connectors = {};
    };
}

module.exports = new Scanner();
<commit_msg>extension -> hwModule 로 변수명 변경<commit_after>'use strict';

class Scanner {
    static get SCAN_INTERVAL_MILLS() {
        return 1500;
    }

    static _isObject(target) {
        return Object.prototype.toString.call(target) === '[object Object]';
    }

    constructor() {
        this.serialport = require('@serialport/stream');
        this.serialport.Binding = require('@entrylabs/bindings');
    }

    startScan(extension, config, callback, router) {
        this.router = router;
        this.config = config;
        this.slaveTimers = {};
        this.connectors = {};
        this.scanCount = 0;
        this.closeConnectors();
        this.clearTimers();

        this.scan(extension, callback);
        this.timer = setInterval(() => {
            this.scan(extension, callback);
        }, Scanner.SCAN_INTERVAL_MILLS);
    };

    stopScan() {
        this.config = undefined;
        this.clearTimers();
        this.closeConnectors();
    };

    scan(hwModule, callback) {
        this.serialport.list(
            /**
             * @param error{Error}
             * @param devices{Array<Object>}
             */
            (error, devices) => {
                if (error) {
                    if (callback) {
                        callback(error);
                    }
                    return;
                }

                const { serverMode, hardware } = this.config;
                let { select_com_port:selectComPort } = this.config;
                const { scanType, comName:checkComName, control, duration, firmwarecheck, pnpId, type } = hardware;
                let { vendor } = hardware;

                if (vendor && Scanner._isObject(vendor)) {
                    vendor = vendor[process.platform];
                }

                if (selectComPort && Scanner._isObject(selectComPort)) {
                    selectComPort = selectComPort[process.platform];
                }

                const checkComPort = (selectComPort || type === 'bluetooth' || serverMode === 1) || false;
                const myComPort = this.config.this_com_port;

                if (checkComPort && !myComPort) {
                    this.router.emit('state', 'select_port', devices);
                    callback();
                    return;
                }

                if (scanType === 'data') {
                    if (this.scanCount < 5) {
                        this.scanCount++;
                    } else {
                        if (devices.some(function(device) {
                            let isVendor = false;
                            if (Array.isArray(vendor)) {
                                isVendor = vendor.some(function(v) {
                                    return device.manufacturer && device.manufacturer.indexOf(v) >= 0;
                                });
                            } else {
                                if (device.manufacturer && device.manufacturer.indexOf(vendor) >= 0) {
                                    isVendor = true;
                                }
                            }

                            return device.manufacturer && isVendor;
                        }) === false) {
                            vendor = undefined;
                        }
                    }
                }

                devices.forEach((device) => {
                    let isVendor = false;
                    let isComName = false;
                    const comName = device.comName || this.config.hardware.name;

                    if (Array.isArray(vendor)) {
                        isVendor = vendor.some(function(name) {
                            return device.manufacturer && device.manufacturer.indexOf(name) >= 0;
                        });
                    } else if (vendor && device.manufacturer && device.manufacturer.indexOf(vendor) >= 0) {
                        isVendor = true;
                    }

                    if (Array.isArray(checkComName)) {
                        isComName = checkComName.some(function(name) {
                            return comName.indexOf(name) >= 0;
                        });
                    } else if (checkComName && comName.indexOf(checkComName) >= 0) {
                        isComName = true;
                    }

                    if (!vendor || (device.manufacturer && isVendor) || (device.pnpId && device.pnpId.indexOf(pnpId) >= 0) || isComName || checkComPort) {

                        if (checkComPort && comName !== myComPort) {
                            return;
                        }

                        // comName = '/dev/tty.EV3-SerialPort';
                        let connector = this.connectors[comName];
                        if (connector === undefined) {
                            connector = require('../connector/serial').create();
                            this.connectors[comName] = connector;
                            connector.open(comName, this.config.hardware, (error, sp) => {
                                if (error) {
                                    delete this.connectors[comName];
                                    if (callback) {
                                        callback(error);
                                    }
                                } else {
                                    this.setConnector(connector);
                                    if (control) {
                                        let flashFirmware;
                                        if (firmwarecheck) {
                                            flashFirmware = setTimeout(() => {
                                                sp.removeAllListeners('data');
                                                connector.executeFlash = true;
                                                this.finalizeScan(comName, connector, callback);
                                            }, 3000);
                                        }

                                        if (control === 'master') {
                                            if (hwModule.checkInitialData && hwModule.requestInitialData) {

                                                let source = sp;
                                                if (sp.parser){
                                                    source = sp.parser;
                                                }
                                                source.on('data', (data) => {
                                                    if (this.config.hardware.stream === 'string') {
                                                        data = data.toString();
                                                    }

                                                    const result = hwModule.checkInitialData(data, this.config);
                                                    if (result === undefined) {
                                                        connector.send(hwModule.requestInitialData());
                                                    } else {
                                                        source.removeAllListeners('data'); // modify  sp --> source
                                                        clearTimeout(flashFirmware);
                                                        if (result === true) {

                                                            if (hwModule.setSerialPort) {
                                                                hwModule.setSerialPort(sp);
                                                            }

                                                            this.finalizeScan(comName, connector, callback);
                                                        } else if (callback) {
                                                            callback(new Error('Invalid hardware'));
                                                        }
                                                    }
                                                });
                                            }
                                        } else {
                                            if (duration && hwModule.checkInitialData && hwModule.requestInitialData) {
                                                sp.on('data', (data) => {
                                                    if (this.config.hardware.stream === 'string') {
                                                        data = data.toString();
                                                    }

                                                    const result = hwModule.checkInitialData(data, this.config);
                                                    if (result !== undefined) {
                                                        sp.removeAllListeners('data');
                                                        clearTimeout(flashFirmware);
                                                        if (result === true) {
                                                            if (hwModule.setSerialPort) {
                                                                hwModule.setSerialPort(sp);
                                                            }
                                                            if (hwModule.resetProperty) {
                                                                connector.send(hwModule.resetProperty());
                                                            }
                                                            this.finalizeScan(comName, connector, callback);
                                                        } else if (callback) {
                                                            callback(new Error('Invalid hardware'));
                                                        }
                                                    }
                                                });
                                                let slaveTimer = this.slaveTimers[comName];
                                                if (slaveTimer) {
                                                    clearInterval(slaveTimer);
                                                }
                                                slaveTimer = setInterval(() => {
                                                    connector.send(hwModule.requestInitialData(sp));
                                                }, duration);
                                                this.slaveTimers[comName] = slaveTimer;
                                            }
                                        }
                                    } else {
                                        this.finalizeScan(comName, connector, callback);
                                    }
                                }
                            });
                        }
                    }
                });
            });
    };

    clearTimers() {
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = undefined;
        }
        const slaveTimers = this.slaveTimers;
        if (slaveTimers) {
            let slaveTimer;
            for (const key in slaveTimers) {
                slaveTimer = slaveTimers[key];
                if (slaveTimer) {
                    clearInterval(slaveTimer);
                }
            }
        }
        this.slaveTimers = {};
    };

    setConnector(connector) {
        this.router.connector = connector;
        this.router.emit('state', 'before_connect');
    };

    finalizeScan(comName, connector, callback) {
        if (this.connectors && comName) {
            this.connectors[comName] = undefined;
        }
        this.stopScan();

        if (callback) {
            callback(null, connector);
        }
    };

    closeConnectors() {
        const connectors = this.connectors;
        if (connectors) {
            let connector;
            for (const key in connectors) {
                connector = connectors[key];
                if (connector) {
                    connector.close();
                }
            }
        }
        this.connectors = {};
    };
}

module.exports = new Scanner();
<|endoftext|>


<commit_before>import { Report, Position, Task, Location } from "models"
import faker from "faker"
import _isEmpty from "lodash/isEmpty"
import _isEqual from "lodash/isEqual"
import _uniqWith from "lodash/uniqWith"
import { runGQL, populate } from "../simutils"

async function getRandomPerson(user, variables) {
  const positionsQuery = Object.assign({}, variables, {
    pageNum: 0,
    pageSize: 1
  })
  const totalCount = (await runGQL(user, {
    query: `
        query ($positionsQuery: PositionSearchQueryInput) {
          positionList(query: $positionsQuery) {
            totalCount
          }
        }
      `,
    variables: {
      positionsQuery
    }
  })).data.positionList.totalCount
  let positions = null
  if (totalCount > 0) {
    positionsQuery.pageNum = faker.random.number({ max: totalCount - 1 })
    positions = (await runGQL(user, {
      query: `
          query ($positionsQuery: PositionSearchQueryInput) {
            positionList(query: $positionsQuery) {
              list {
                uuid
                type
                person {
                  uuid
                  name
                  role
                }
              }
            }
          }
        `,
      variables: {
        positionsQuery
      }
    })).data.positionList.list
  }
  return _isEmpty(positions) ? null : positions[0].person
}

export const randomAdvisor = async function(user) {
  return getRandomPerson(user, {
    status: Position.STATUS.ACTIVE,
    isFilled: true,
    type: [Position.TYPE.ADVISOR]
  })
}

export const randomPrincipal = async function(user) {
  return getRandomPerson(user, {
    status: Position.STATUS.ACTIVE,
    isFilled: true,
    type: [Position.TYPE.PRINCIPAL]
  })
}

async function populateReport(report, user, args) {
  const emptyArray = () => {
    return []
  }

  /* eslint-disable no-unused-vars */
  const emptyObject = () => {
    return {}
  }
  /* eslint-enable no-unused-vars */

  async function activeLocation() {
    const totalCount = (await runGQL(user, {
      query: `
        query ($locationQuery: LocationSearchQueryInput) {
          locationList(query: $locationQuery) {
            totalCount
          }
        }
      `,
      variables: {
        locationQuery: {
          pageNum: 0,
          pageSize: 1,
          status: Location.STATUS.ACTIVE
        }
      }
    })).data.locationList.totalCount
    let activeLocations = null
    if (totalCount > 0) {
      const random = faker.random.number({ max: totalCount - 1 })
      activeLocations = (await runGQL(user, {
        query: `
          query ($locationQuery: LocationSearchQueryInput) {
            locationList(query: $locationQuery) {
              list {
                uuid
              }
            }
          }
        `,
        variables: {
          locationQuery: {
            pageNum: random,
            pageSize: 1,
            status: Location.STATUS.ACTIVE
          }
        }
      })).data.locationList.list
    }
    return _isEmpty(activeLocations) ? null : activeLocations[0]
  }

  async function activeTask() {
    const totalCount = (await runGQL(user, {
      query: `
        query ($taskQuery: TaskSearchQueryInput) {
          taskList(query: $taskQuery) {
            totalCount
          }
        }
      `,
      variables: {
        taskQuery: {
          pageNum: 0,
          pageSize: 1,
          status: Task.STATUS.ACTIVE
        }
      }
    })).data.taskList.totalCount
    let activeTasks = null
    if (totalCount > 0) {
      const random = faker.random.number({ max: totalCount - 1 })
      activeTasks = (await runGQL(user, {
        query: `
          query ($taskQuery: TaskSearchQueryInput) {
            taskList(query: $taskQuery) {
              list {
                uuid
              }
            }
          }
        `,
        variables: {
          taskQuery: {
            pageNum: random,
            pageSize: 1,
            status: Task.STATUS.ACTIVE
          }
        }
      })).data.taskList.list
    }
    return _isEmpty(activeTasks) ? null : activeTasks[0]
  }

  const location = await activeLocation()
  async function getAttendees() {
    const attendees = []
    const nbOfAdvisors = faker.random.number({ min: 1, max: 5 })
    let primary = true
    for (let i = 0; i < nbOfAdvisors; i++) {
      const advisor = await randomAdvisor(user)
      if (advisor) {
        advisor.primary = primary
        primary = false
        attendees.push(advisor)
      }
    }

    const nbOfPrincipals = faker.random.number({ min: 1, max: 5 })
    primary = true
    for (let i = 0; i < nbOfPrincipals; i++) {
      const principal = await randomPrincipal(user)
      if (principal) {
        principal.primary = primary
        primary = false
        attendees.push(principal)
      }
    }

    return [..._uniqWith(attendees, _isEqual)]
  }
  const attendees = await getAttendees()
  async function getTasks() {
    const reportTasks = []
    const nbOfTasks = faker.random.number({ min: 1, max: 3 })

    for (let i = 0; i < nbOfTasks; i++) {
      reportTasks.push(await activeTask())
    }

    return [..._uniqWith(reportTasks, _isEqual)]
  }
  const tasks = await getTasks()
  const engagementDate = faker.date.recent(365)

  const template = {
    intent: () => faker.lorem.paragraph(),
    engagementDate: engagementDate.toISOString(),
    duration: () => faker.random.number({ min: 1, max: 480 }),
    cancelledReason: () =>
      faker.random.arrayElement([
        "CANCELLED_BY_ADVISOR",
        "CANCELLED_BY_PRINCIPAL",
        "CANCELLED_DUE_TO_TRANSPORTATION",
        "CANCELLED_DUE_TO_FORCE_PROTECTION",
        "CANCELLED_DUE_TO_ROUTES",
        "CANCELLED_DUE_TO_THREAT"
      ]),
    atmosphere: () =>
      faker.random.arrayElement(["POSITIVE", "NEUTRAL", "NEGATIVE"]),
    atmosphereDetails: () => faker.lorem.sentence(),
    location,
    attendees,
    tasks,
    reportText: () => faker.lorem.paragraphs(),
    nextSteps: () => faker.lorem.sentence(),
    keyOutcomes: () => faker.lorem.sentence(),
    tags: emptyArray,
    reportSensitiveInformation: () => null,
    authorizationGroups: emptyArray,
    state: (args && args.state) || Report.STATE.DRAFT,
    releasedAt: () => {
      // Set the releasedAt value on a random date between 1 and 7 days after the engagement
      let result = new Date(engagementDate)
      result.setSeconds(
        result.getSeconds() +
          (Math.floor(Math.random() * (60 * 60 * 24 * 7)) + 1)
      )
      return result
    }
  }

  populate(report, template)
    .intent.always()
    .engagementDate.always()
    .duration.often()
    .cancelledReason.often()
    .atmosphere.always()
    .atmosphereDetails.always()
    .location.always()
    .attendees.always()
    .tasks.always()
    .reportText.always()
    .nextSteps.always()
    .keyOutcomes.always()
    .tags.rarely()
    .reportSensitiveInformation.and()
    .authorizationGroups.rarely()
    .state.always()
    .releasedAt.always()

  return report
}

const createReport = async function(user, grow, args) {
  const report = new Report()
  if (await populateReport(report, user, args)) {
    console.debug(`Creating report ${report.intent.green}`)
    const { reportTags, cancelled, ...reportStripped } = report // TODO: we need to do this more generically

    return (await runGQL(user, {
      query:
        "mutation($report: ReportInput!) { createReport(report: $report) { uuid } }",
      variables: { report: reportStripped }
    })).data.createReport
  }
}

const updateDraftReport = async function(user) {
  const totalCount = (await runGQL(user, {
    query: `
      query {
        reportList(query: {
          pageNum: 0,
          pageSize: 1,
          state: ${Report.STATE.DRAFT},
          authorUuid: "${user.person.uuid}"
        }) {
          totalCount
        }
      }
    `,
    variables: {}
  })).data.reportList.totalCount
  if (totalCount === 0) {
    return null
  }
  const random = faker.random.number({ max: totalCount - 1 })
  const reports = (await runGQL(user, {
    query: `
      query {
        reportList(query: {
          pageNum: ${random},
          pageSize: 1,
          state: ${Report.STATE.DRAFT},
          authorUuid: "${user.person.uuid}"
        }) {
          list {
            uuid
            intent
            engagementDate
            duration
            keyOutcomes
            nextSteps
            cancelledReason
            atmosphere
            atmosphereDetails
            attendees {
              uuid
              primary
            }
          }
        }
      }
    `,
    variables: {}
  })).data.reportList.list
  const report = !_isEmpty(reports) && reports[0]
  if (!report) {
    return null
  }

  if (await populateReport(report, user)) {
    return (await runGQL(user, {
      query:
        "mutation($report: ReportInput!) { updateReport(report: $report) { uuid } }",
      variables: { report: report }
    })).data.updateReport
  }
}

const submitDraftReport = async function(user) {
  const totalCount = (await runGQL(user, {
    query: `
      query {
        reportList(query: {
          pageNum: 0,
          pageSize: 1,
          state: ${Report.STATE.DRAFT},
          authorUuid: "${user.person.uuid}"
        }) {
          totalCount
        }
      }
    `,
    variables: {}
  })).data.reportList.totalCount
  if (totalCount === 0) {
    return null
  }
  const random = faker.random.number({ max: totalCount - 1 })
  const reports = (await runGQL(user, {
    query: `
      query {
        reportList(query: {
          pageNum: ${random},
          pageSize: 1,
          state: ${Report.STATE.DRAFT},
          authorUuid: "${user.person.uuid}"
        }) {
          list {
            uuid
          }
        }
      }
    `,
    variables: {}
  })).data.reportList.list
  const report = _isEmpty(reports) && reports[0]
  if (!report) {
    return null
  }

  return (await runGQL(user, {
    query: "mutation($uuid: String!) { submitReport(uuid: $uuid) { uuid } }",
    variables: { uuid: report.uuid }
  })).data.submitReport
}

const approveReport = async function(user) {
  const totalCount = (await runGQL(user, {
    query: `
      query {
        reportList(query: {
          pageNum: 0,
          pageSize: 1,
          pendingApprovalOf: "${user.person.uuid}"
        }) {
          totalCount
        }
      }
    `,
    variables: {}
  })).data.reportList.totalCount
  if (totalCount === 0) {
    return null
  }
  const random = faker.random.number({ max: totalCount - 1 })
  const reports = (await runGQL(user, {
    query: `
      query {
        reportList(query: {
          pageNum: ${random},
          pageSize: 1,
          pendingApprovalOf: "${user.person.uuid}"
        }) {
          list {
            uuid
          }
        }
      }
    `,
    variables: {}
  })).data.reportList.list
  const report = _isEmpty(reports) && reports[0]
  if (!report) {
    return null
  }

  return (await runGQL(user, {
    query: "mutation ($uuid: String!) { approveReport(uuid: $uuid) { uuid } }",
    variables: { uuid: report.uuid }
  })).data.approveReport
}

export { createReport, updateDraftReport, submitDraftReport, approveReport }
<commit_msg>Only set cancelledReason when state is CANCELLED<commit_after>import { Report, Position, Task, Location } from "models"
import faker from "faker"
import _isEmpty from "lodash/isEmpty"
import _isEqual from "lodash/isEqual"
import _uniqWith from "lodash/uniqWith"
import { fuzzy, runGQL, populate } from "../simutils"

async function getRandomPerson(user, variables) {
  const positionsQuery = Object.assign({}, variables, {
    pageNum: 0,
    pageSize: 1
  })
  const totalCount = (await runGQL(user, {
    query: `
        query ($positionsQuery: PositionSearchQueryInput) {
          positionList(query: $positionsQuery) {
            totalCount
          }
        }
      `,
    variables: {
      positionsQuery
    }
  })).data.positionList.totalCount
  let positions = null
  if (totalCount > 0) {
    positionsQuery.pageNum = faker.random.number({ max: totalCount - 1 })
    positions = (await runGQL(user, {
      query: `
          query ($positionsQuery: PositionSearchQueryInput) {
            positionList(query: $positionsQuery) {
              list {
                uuid
                type
                person {
                  uuid
                  name
                  role
                }
              }
            }
          }
        `,
      variables: {
        positionsQuery
      }
    })).data.positionList.list
  }
  return _isEmpty(positions) ? null : positions[0].person
}

export const randomAdvisor = async function(user) {
  return getRandomPerson(user, {
    status: Position.STATUS.ACTIVE,
    isFilled: true,
    type: [Position.TYPE.ADVISOR]
  })
}

export const randomPrincipal = async function(user) {
  return getRandomPerson(user, {
    status: Position.STATUS.ACTIVE,
    isFilled: true,
    type: [Position.TYPE.PRINCIPAL]
  })
}

async function populateReport(report, user, args) {
  const emptyArray = () => {
    return []
  }

  /* eslint-disable no-unused-vars */
  const emptyObject = () => {
    return {}
  }
  /* eslint-enable no-unused-vars */

  async function activeLocation() {
    const totalCount = (await runGQL(user, {
      query: `
        query ($locationQuery: LocationSearchQueryInput) {
          locationList(query: $locationQuery) {
            totalCount
          }
        }
      `,
      variables: {
        locationQuery: {
          pageNum: 0,
          pageSize: 1,
          status: Location.STATUS.ACTIVE
        }
      }
    })).data.locationList.totalCount
    let activeLocations = null
    if (totalCount > 0) {
      const random = faker.random.number({ max: totalCount - 1 })
      activeLocations = (await runGQL(user, {
        query: `
          query ($locationQuery: LocationSearchQueryInput) {
            locationList(query: $locationQuery) {
              list {
                uuid
              }
            }
          }
        `,
        variables: {
          locationQuery: {
            pageNum: random,
            pageSize: 1,
            status: Location.STATUS.ACTIVE
          }
        }
      })).data.locationList.list
    }
    return _isEmpty(activeLocations) ? null : activeLocations[0]
  }

  async function activeTask() {
    const totalCount = (await runGQL(user, {
      query: `
        query ($taskQuery: TaskSearchQueryInput) {
          taskList(query: $taskQuery) {
            totalCount
          }
        }
      `,
      variables: {
        taskQuery: {
          pageNum: 0,
          pageSize: 1,
          status: Task.STATUS.ACTIVE
        }
      }
    })).data.taskList.totalCount
    let activeTasks = null
    if (totalCount > 0) {
      const random = faker.random.number({ max: totalCount - 1 })
      activeTasks = (await runGQL(user, {
        query: `
          query ($taskQuery: TaskSearchQueryInput) {
            taskList(query: $taskQuery) {
              list {
                uuid
              }
            }
          }
        `,
        variables: {
          taskQuery: {
            pageNum: random,
            pageSize: 1,
            status: Task.STATUS.ACTIVE
          }
        }
      })).data.taskList.list
    }
    return _isEmpty(activeTasks) ? null : activeTasks[0]
  }

  const location = await activeLocation()
  async function getAttendees() {
    const attendees = []
    const nbOfAdvisors = faker.random.number({ min: 1, max: 5 })
    let primary = true
    for (let i = 0; i < nbOfAdvisors; i++) {
      const advisor = await randomAdvisor(user)
      if (advisor) {
        advisor.primary = primary
        primary = false
        attendees.push(advisor)
      }
    }

    const nbOfPrincipals = faker.random.number({ min: 1, max: 5 })
    primary = true
    for (let i = 0; i < nbOfPrincipals; i++) {
      const principal = await randomPrincipal(user)
      if (principal) {
        principal.primary = primary
        primary = false
        attendees.push(principal)
      }
    }

    return [..._uniqWith(attendees, _isEqual)]
  }
  const attendees = await getAttendees()
  async function getTasks() {
    const reportTasks = []
    const nbOfTasks = faker.random.number({ min: 1, max: 3 })

    for (let i = 0; i < nbOfTasks; i++) {
      reportTasks.push(await activeTask())
    }

    return [..._uniqWith(reportTasks, _isEqual)]
  }
  const tasks = await getTasks()
  const engagementDate = faker.date.recent(365)
  const state = fuzzy.withProbability(0.05)
    ? Report.STATE.CANCELLED
    : (args && args.state) || Report.STATE.DRAFT
  const cancelledReason =
    state !== Report.STATE.CANCELLED
      ? null
      : faker.random.arrayElement([
        "CANCELLED_BY_ADVISOR",
        "CANCELLED_BY_PRINCIPAL",
        "CANCELLED_DUE_TO_TRANSPORTATION",
        "CANCELLED_DUE_TO_FORCE_PROTECTION",
        "CANCELLED_DUE_TO_ROUTES",
        "CANCELLED_DUE_TO_THREAT"
      ])

  const template = {
    intent: () => faker.lorem.paragraph(),
    engagementDate: engagementDate.toISOString(),
    duration: () => faker.random.number({ min: 1, max: 480 }),
    cancelledReason,
    atmosphere: () =>
      faker.random.arrayElement(["POSITIVE", "NEUTRAL", "NEGATIVE"]),
    atmosphereDetails: () => faker.lorem.sentence(),
    location,
    attendees,
    tasks,
    reportText: () => faker.lorem.paragraphs(),
    nextSteps: () => faker.lorem.sentence(),
    keyOutcomes: () => faker.lorem.sentence(),
    tags: emptyArray,
    reportSensitiveInformation: () => null,
    authorizationGroups: emptyArray,
    state,
    releasedAt: () => {
      // Set the releasedAt value on a random date between 1 and 7 days after the engagement
      let result = new Date(engagementDate)
      result.setSeconds(
        result.getSeconds() +
          (Math.floor(Math.random() * (60 * 60 * 24 * 7)) + 1)
      )
      return result
    }
  }

  populate(report, template)
    .intent.always()
    .engagementDate.always()
    .duration.often()
    .cancelledReason.always()
    .atmosphere.always()
    .atmosphereDetails.always()
    .location.always()
    .attendees.always()
    .tasks.always()
    .reportText.always()
    .nextSteps.always()
    .keyOutcomes.always()
    .tags.rarely()
    .reportSensitiveInformation.and()
    .authorizationGroups.rarely()
    .state.always()
    .releasedAt.always()

  return report
}

const createReport = async function(user, grow, args) {
  const report = new Report()
  if (await populateReport(report, user, args)) {
    console.debug(`Creating report ${report.intent.green}`)
    const { reportTags, cancelled, ...reportStripped } = report // TODO: we need to do this more generically

    return (await runGQL(user, {
      query:
        "mutation($report: ReportInput!) { createReport(report: $report) { uuid } }",
      variables: { report: reportStripped }
    })).data.createReport
  }
}

const updateDraftReport = async function(user) {
  const totalCount = (await runGQL(user, {
    query: `
      query {
        reportList(query: {
          pageNum: 0,
          pageSize: 1,
          state: ${Report.STATE.DRAFT},
          authorUuid: "${user.person.uuid}"
        }) {
          totalCount
        }
      }
    `,
    variables: {}
  })).data.reportList.totalCount
  if (totalCount === 0) {
    return null
  }
  const random = faker.random.number({ max: totalCount - 1 })
  const reports = (await runGQL(user, {
    query: `
      query {
        reportList(query: {
          pageNum: ${random},
          pageSize: 1,
          state: ${Report.STATE.DRAFT},
          authorUuid: "${user.person.uuid}"
        }) {
          list {
            uuid
            intent
            engagementDate
            duration
            keyOutcomes
            nextSteps
            cancelledReason
            atmosphere
            atmosphereDetails
            attendees {
              uuid
              primary
            }
          }
        }
      }
    `,
    variables: {}
  })).data.reportList.list
  const report = !_isEmpty(reports) && reports[0]
  if (!report) {
    return null
  }

  if (await populateReport(report, user)) {
    return (await runGQL(user, {
      query:
        "mutation($report: ReportInput!) { updateReport(report: $report) { uuid } }",
      variables: { report: report }
    })).data.updateReport
  }
}

const submitDraftReport = async function(user) {
  const totalCount = (await runGQL(user, {
    query: `
      query {
        reportList(query: {
          pageNum: 0,
          pageSize: 1,
          state: ${Report.STATE.DRAFT},
          authorUuid: "${user.person.uuid}"
        }) {
          totalCount
        }
      }
    `,
    variables: {}
  })).data.reportList.totalCount
  if (totalCount === 0) {
    return null
  }
  const random = faker.random.number({ max: totalCount - 1 })
  const reports = (await runGQL(user, {
    query: `
      query {
        reportList(query: {
          pageNum: ${random},
          pageSize: 1,
          state: ${Report.STATE.DRAFT},
          authorUuid: "${user.person.uuid}"
        }) {
          list {
            uuid
          }
        }
      }
    `,
    variables: {}
  })).data.reportList.list
  const report = _isEmpty(reports) && reports[0]
  if (!report) {
    return null
  }

  return (await runGQL(user, {
    query: "mutation($uuid: String!) { submitReport(uuid: $uuid) { uuid } }",
    variables: { uuid: report.uuid }
  })).data.submitReport
}

const approveReport = async function(user) {
  const totalCount = (await runGQL(user, {
    query: `
      query {
        reportList(query: {
          pageNum: 0,
          pageSize: 1,
          pendingApprovalOf: "${user.person.uuid}"
        }) {
          totalCount
        }
      }
    `,
    variables: {}
  })).data.reportList.totalCount
  if (totalCount === 0) {
    return null
  }
  const random = faker.random.number({ max: totalCount - 1 })
  const reports = (await runGQL(user, {
    query: `
      query {
        reportList(query: {
          pageNum: ${random},
          pageSize: 1,
          pendingApprovalOf: "${user.person.uuid}"
        }) {
          list {
            uuid
          }
        }
      }
    `,
    variables: {}
  })).data.reportList.list
  const report = _isEmpty(reports) && reports[0]
  if (!report) {
    return null
  }

  return (await runGQL(user, {
    query: "mutation ($uuid: String!) { approveReport(uuid: $uuid) { uuid } }",
    variables: { uuid: report.uuid }
  })).data.approveReport
}

export { createReport, updateDraftReport, submitDraftReport, approveReport }
<|endoftext|>


<commit_before>import {
  fetchHistory
} from '../js/redux/services';

import {
  clone,
  updateGroupFromChildren,
  safeAccess,
  fixTotals,
  openGroupedAccounts,
  bumpDateOneMonth,
  bumpDateOneMonthBack,
  statToNumber
} from '../js/redux/utilities.js';
import { newGroupClick } from '../js/redux/actions';

function receiveAccounts (state, action){
  var newState = clone(state);
  if (action.payload.error) {
    newState = Object.assign({}, state, action.payload);
    return newState;
  }
  newState.accounts = clone(action.payload) || {};
  return newState;
}

function receiveHistory(state, action){
  var newState = undefined;
  clone(state);
  if (action.payload.error) {
    newState.history = { error: action.payload.error };
    return newState;
  }
  newState = clone(state);
  newState.history.error = false;
  newState.history.data = action.payload;
  return newState;
}

function popupAccount(state, action){
  var newState = clone(state);
  const liabilities = (state.liabilities || (state.accounts||{}).liabilities || []);
  const assets = (state.assets || (state.accounts||{}).assets || []);

  newState.account = [].concat(liabilities, assets)
    .filter(a => a.title.toLowerCase() === action.payload.title.toLowerCase());
  newState.account = clone(newState.account[0] || {});

  //console.log(liabilities);

  if (newState.account.items) {
    if (newState.selected && newState.selected.length) {
      newState.selected
        .forEach(x => {
          if (!newState.account.items
            .map(y => y.title.toLowerCase())
            .includes(x.title.toLowerCase())
          ) {
            newState.account.items.push(clone(x));
          }
        })
    }
    // newState.account.items = newState.account.items
    //   .map(x => {
    //     return liabilities.filter(y => y.title.toLowerCase() === x.title.toLowerCase())[0]
    //   })
      //.sort((a, b) => b.total_owed - a.total_owed);
  }
  // newState = Object.assign({}, state, {
  //   error: newState.account ? false : 'could not find account',
  //   dateDirty: false,
  //   account: clone(newState.account || false)
  // });
  newState.dateDirty = false;
  newState.error = newState.account ? false : 'could not find account';

  //console.log('^^^^^', newState)

  return newState;
}

function popupUpdate(state, action){
  var newState = undefined;
  newState = clone(state);
  ///console.log('!!!', newState);
  if(typeof newState.account !== 'object'){
    newState.error = 'could not update popup state';
    return newState;
  }

  const liabilities = (state.liabilities || (state.accounts||{}).liabilities || []);
  const assets = (state.assets || (state.accounts||{}).assets || []);

  var oldAccount = [].concat(liabilities, assets)
    .filter(a => a.title.toLowerCase() === newState.account.title.toLowerCase());
  oldAccount = clone(oldAccount[0] || {});

  Object.keys(action.payload)
    .forEach(fieldName => {
      if (fieldName === 'title') {
        newState.account.oldTitle = newState.account.title;
      }
      newState.account[fieldName] = action.payload[fieldName]
    });
  // change date based on status change
  if (action.payload.status) {
    const isNewPaid = action.payload.status.toLowerCase() === 'paid';
    const isPrevPaid = state.account.status.toLowerCase() === 'paid';
    const shouldAutoReduce = !!state.account.autoReduce;

    if (isPrevPaid && state.dateDirty) {
      newState.account.date = bumpDateOneMonthBack(newState.account.date);
      newState.dateDirty = false;
      if (shouldAutoReduce) {
        newState.account.total_owed += Number(newState.account.amount);
      }
    }

    if (!isPrevPaid && isNewPaid) {
      newState.account.date = bumpDateOneMonth(newState.account.date);
      newState.dateDirty = oldAccount.status !== 'paid';
      if (shouldAutoReduce) {
        newState.account.total_owed -= Number(newState.account.amount);
      }
    }
  }
  return newState;
}

function popupNewGroup (state, action){
  var newState = undefined;

  var selectedAmount = state.selected.reduce((all, g) => { return all + Number(g.amount); }, 0);
  selectedAmount = parseFloat(selectedAmount).toFixed(2);
  var selectedOwed = state.selected.reduce((all, g) => { return all + Number(g.total_owed || 0); }, 0);
  selectedOwed = parseFloat(selectedOwed).toFixed(2);
  var selectedLatestDate = state.selected
    .map(x => x.date)
    .sort(function (a, b) {
      return new Date(a.date) - new Date(b.date);
    })[0];
  newState = Object.assign({}, state, {
    error: false,
    dateDirty: false,
    account: JSON.parse(JSON.stringify(state.account || false))
  });
  newState.account = {
    type: "group",
    hidden: false,
    title: "New Group",
    note: "",
    items: state.selected,
    isNew: true,
    status: "paid", // TODO: update from selected accounts
    date: selectedLatestDate,
    amount: selectedAmount,
    total_owed: selectedOwed,
    auto: false
  };
  return newState;
}

function popupNewAccount(state, action){
  var newState = undefined;
  var account = {
    type: "",
    hidden: false,
    title: "New Group",
    note: "",
    isNew: true,
    status: "paid",
    date: "2017-10-18", //TODO: a month from now?
    amount: 0,
    total_owed: 0,
    auto: false
  };
  newState = Object.assign({}, state, {
    error: false,
    account
  });
  newState.dateDirty = false;
  return newState;
}

function popupCancel(state, action){
  var newState = undefined;
  newState = Object.assign({}, state, {
    error: 'not initialized',
    account: undefined,
    history: undefined,
    dateDirty: false
  });
  return newState;
}

function popupHistory(state, action){
  var newState = undefined;
  const { field } = action.payload;
  const title = (state.account || {}).title || 'Total Owed';
  const history = { error: 'loading', field, title };
  newState = Object.assign({}, state, {
    account: state.account,
    history,
    error: false
  });
  const type = state.account ? 'liabilities' : 'balance'; //TODO: get type in a better way
  fetchHistory({ type, title, field });
  return newState;
}

function popupHistoryBack(state, action){
  var newState = undefined;
  newState = Object.assign({}, state, {
    account: state.account,
    history: undefined,
    error: false
  });
  return newState;
}

function groupRemove(state, action){
  var newState = undefined;
  newState = Object.assign({}, state, { error: 'not initialized', account: undefined });
  return newState;
}

function removeItem(state, action){
  var newState = clone(state);
  const itemTitle = action.payload.title;
  newState.account.items = newState.account.items
    .filter(x => x.title.toLowerCase() !== itemTitle.toLowerCase())
    .map(x => newState.accounts.liabilities
      .filter(y => x.title.toLowerCase() === y.title.toLowerCase())[0]
    );

  // update amount / total_owed / date / status for group
  newState.account.amount = newState.account.items
    .map(x => x.amount)
    .reduce((total, z) => Number(total) + Number(z), 0)
    .toFixed(2);
  newState.account.total_owed = newState.account.items
    .map(x => x.total_owed)
    .reduce((total, z) => Number(total) + Number(z), 0)
    .toFixed(2);
  newState.account.date = newState.account.items
    .map(x => x.date)
    .sort(function (a, b) {
      return new Date(a) - new Date(b);
    })[0];
  newState.account.status = newState.account.items
    .map(x => x.status)
    .reduce((status, z) => statToNumber[status.toLowerCase()] < statToNumber[z.toLowerCase()]
      ? status.toLowerCase()
      : z.toLowerCase()
    , 'paid');
  newState.account.items = newState.account.items.map(x => ({ title: x.title }));
  return newState;
}

function accountSave(state, action){
  var newState = undefined;
  newState = Object.assign({}, state, { error: 'not initialized' });
  newState.account = undefined;
  newState.dateDirty = false;
  return newState;
}

function selectAccountClick(state, action){
  var newState = clone(state);
  newState.accounts.liabilities.forEach(liab => {
      if (liab.title === action.payload.title) {
          liab.selected = !liab.selected;
      }
  });
  newState.selected = newState.accounts.liabilities.filter(x => x.selected);
  return newState;
}

function popup(state, action) {
  var newState = undefined;

  switch (action.type) {
    case 'RECEIVE_ACCOUNTS':
      newState = receiveAccounts(state, action);
      break;
    case 'RECEIVE_HISTORY':
      newState = receiveHistory(state, action);
      break;
    case 'POPUP_ACCOUNT':
      newState = popupAccount(state, action);
      break;
    case 'POPUP_UPDATE':
      newState = popupUpdate(state, action);
      break;
    case 'POPUP_NEW_GROUP':
      newState = popupNewGroup(state, action);
      break;
    case 'POPUP_NEW_ACCOUNT':
      newState = popupNewAccount(state, action);
      break;
    case 'POPUP_CANCEL':
      newState = popupCancel(state, action);
      break;
    case 'POPUP_HISTORY':
      newState = popupHistory(state, action);
      break;
    case 'POPUP_HISTORY_BACK':
      newState = popupHistoryBack(state, action);
      break;
    case 'GROUP_REMOVE':
      newState = groupRemove(state, action);
      break;
    case 'REMOVE_ITEM':
      newState = removeItem(state, action);
      break;
    case 'ACCOUNT_SAVE':
      newState = accountSave(state, action);
      break;
    case 'SELECT_ACCOUNT_CLICK':
      newState = selectAccountClick(state, action);
      break;
  }
  return newState || clone(state);
}

export default popup;
export {
  receiveAccounts, receiveHistory, popupAccount, popupUpdate, popupNewGroup,
  popupNewAccount, popupCancel, popupHistory, popupHistoryBack, groupRemove,
  removeItem, accountSave
};
<commit_msg>fix grouped items total issue<commit_after>import {
  fetchHistory
} from '../js/redux/services';

import {
  clone,
  updateGroupFromChildren,
  safeAccess,
  fixTotals,
  openGroupedAccounts,
  bumpDateOneMonth,
  bumpDateOneMonthBack,
  statToNumber
} from '../js/redux/utilities.js';
import { newGroupClick } from '../js/redux/actions';

function receiveAccounts (state, action){
  var newState = clone(state);
  if (action.payload.error) {
    newState = Object.assign({}, state, action.payload);
    return newState;
  }
  newState.accounts = clone(action.payload) || {};
  return newState;
}

function receiveHistory(state, action){
  var newState = undefined;
  clone(state);
  if (action.payload.error) {
    newState.history = { error: action.payload.error };
    return newState;
  }
  newState = clone(state);
  newState.history.error = false;
  newState.history.data = action.payload;
  return newState;
}

function popupAccount(state, action){
  var newState = clone(state);
  const liabilities = (state.liabilities || (state.accounts||{}).liabilities || []);
  const assets = (state.assets || (state.accounts||{}).assets || []);

  newState.account = [].concat(liabilities, assets)
    .filter(a => a.title.toLowerCase() === action.payload.title.toLowerCase());
  newState.account = clone(newState.account[0] || {});

  //console.log(liabilities);

  if (newState.account.items) {
    if (newState.selected && newState.selected.length) {
      newState.selected
        .forEach(x => {
          if (!newState.account.items
            .map(y => y.title.toLowerCase())
            .includes(x.title.toLowerCase())
          ) {
            newState.account.items.push(clone(x));
          }
        })
    }
    newState.account.items = newState.account.items
      .map(x => {
        return state.accounts.liabilities.filter(y => y.title.toLowerCase() === x.title.toLowerCase())[0]
      })
      .sort((a, b) => b.total_owed - a.total_owed);
  }
  newState.dateDirty = false;
  newState.error = newState.account ? false : 'could not find account';

  return newState;
}

function popupUpdate(state, action){
  var newState = undefined;
  newState = clone(state);
  ///console.log('!!!', newState);
  if(typeof newState.account !== 'object'){
    newState.error = 'could not update popup state';
    return newState;
  }

  const liabilities = (state.liabilities || (state.accounts||{}).liabilities || []);
  const assets = (state.assets || (state.accounts||{}).assets || []);

  var oldAccount = [].concat(liabilities, assets)
    .filter(a => a.title.toLowerCase() === newState.account.title.toLowerCase());
  oldAccount = clone(oldAccount[0] || {});

  Object.keys(action.payload)
    .forEach(fieldName => {
      if (fieldName === 'title') {
        newState.account.oldTitle = newState.account.title;
      }
      newState.account[fieldName] = action.payload[fieldName]
    });
  // change date based on status change
  if (action.payload.status) {
    const isNewPaid = action.payload.status.toLowerCase() === 'paid';
    const isPrevPaid = state.account.status.toLowerCase() === 'paid';
    const shouldAutoReduce = !!state.account.autoReduce;

    if (isPrevPaid && state.dateDirty) {
      newState.account.date = bumpDateOneMonthBack(newState.account.date);
      newState.dateDirty = false;
      if (shouldAutoReduce) {
        newState.account.total_owed += Number(newState.account.amount);
      }
    }

    if (!isPrevPaid && isNewPaid) {
      newState.account.date = bumpDateOneMonth(newState.account.date);
      newState.dateDirty = oldAccount.status !== 'paid';
      if (shouldAutoReduce) {
        newState.account.total_owed -= Number(newState.account.amount);
      }
    }
  }
  return newState;
}

function popupNewGroup (state, action){
  var newState = undefined;

  var selectedAmount = state.selected.reduce((all, g) => { return all + Number(g.amount); }, 0);
  selectedAmount = parseFloat(selectedAmount).toFixed(2);
  var selectedOwed = state.selected.reduce((all, g) => { return all + Number(g.total_owed || 0); }, 0);
  selectedOwed = parseFloat(selectedOwed).toFixed(2);
  var selectedLatestDate = state.selected
    .map(x => x.date)
    .sort(function (a, b) {
      return new Date(a.date) - new Date(b.date);
    })[0];
  newState = Object.assign({}, state, {
    error: false,
    dateDirty: false,
    account: JSON.parse(JSON.stringify(state.account || false))
  });
  newState.account = {
    type: "group",
    hidden: false,
    title: "New Group",
    note: "",
    items: state.selected,
    isNew: true,
    status: "paid", // TODO: update from selected accounts
    date: selectedLatestDate,
    amount: selectedAmount,
    total_owed: selectedOwed,
    auto: false
  };
  return newState;
}

function popupNewAccount(state, action){
  var newState = undefined;
  var account = {
    type: "",
    hidden: false,
    title: "New Group",
    note: "",
    isNew: true,
    status: "paid",
    date: "2017-10-18", //TODO: a month from now?
    amount: 0,
    total_owed: 0,
    auto: false
  };
  newState = Object.assign({}, state, {
    error: false,
    account
  });
  newState.dateDirty = false;
  return newState;
}

function popupCancel(state, action){
  var newState = undefined;
  newState = Object.assign({}, state, {
    error: 'not initialized',
    account: undefined,
    history: undefined,
    dateDirty: false
  });
  return newState;
}

function popupHistory(state, action){
  var newState = undefined;
  const { field } = action.payload;
  const title = (state.account || {}).title || 'Total Owed';
  const history = { error: 'loading', field, title };
  newState = Object.assign({}, state, {
    account: state.account,
    history,
    error: false
  });
  const type = state.account ? 'liabilities' : 'balance'; //TODO: get type in a better way
  fetchHistory({ type, title, field });
  return newState;
}

function popupHistoryBack(state, action){
  var newState = undefined;
  newState = Object.assign({}, state, {
    account: state.account,
    history: undefined,
    error: false
  });
  return newState;
}

function groupRemove(state, action){
  var newState = undefined;
  newState = Object.assign({}, state, { error: 'not initialized', account: undefined });
  return newState;
}

function removeItem(state, action){
  var newState = clone(state);
  const itemTitle = action.payload.title;
  console.log(newState.account.items);
  newState.account.items = newState.account.items
    .filter(x => x.title.toLowerCase() !== itemTitle.toLowerCase())
    .map(x => newState.accounts.liabilities
      .filter(y => x.title.toLowerCase() === y.title.toLowerCase())[0]
    );

  // update amount / total_owed / date / status for group
  newState.account.amount = newState.account.items
    .map(x => x.amount)
    .reduce((total, z) => Number(total) + Number(z), 0)
    .toFixed(2);
  newState.account.total_owed = newState.account.items
    .map(x => x.total_owed)
    .reduce((total, z) => Number(total) + Number(z), 0)
    .toFixed(2);
  newState.account.date = newState.account.items
    .map(x => x.date)
    .sort(function (a, b) {
      return new Date(a) - new Date(b);
    })[0];
  newState.account.status = newState.account.items
    .map(x => x.status)
    .reduce((status, z) => statToNumber[status.toLowerCase()] < statToNumber[z.toLowerCase()]
      ? status.toLowerCase()
      : z.toLowerCase()
    , 'paid');
  newState.account.items = newState.account.items.map(x => ({ title: x.title }));
  return newState;
}

function accountSave(state, action){
  var newState = undefined;
  newState = Object.assign({}, state, { error: 'not initialized' });
  newState.account = undefined;
  newState.dateDirty = false;
  return newState;
}

function selectAccountClick(state, action){
  var newState = clone(state);
  newState.accounts.liabilities.forEach(liab => {
      if (liab.title === action.payload.title) {
          liab.selected = !liab.selected;
      }
  });
  newState.selected = newState.accounts.liabilities.filter(x => x.selected);
  return newState;
}

function popup(state, action) {
  var newState = undefined;

  switch (action.type) {
    case 'RECEIVE_ACCOUNTS':
      newState = receiveAccounts(state, action);
      break;
    case 'RECEIVE_HISTORY':
      newState = receiveHistory(state, action);
      break;
    case 'POPUP_ACCOUNT':
      newState = popupAccount(state, action);
      break;
    case 'POPUP_UPDATE':
      newState = popupUpdate(state, action);
      break;
    case 'POPUP_NEW_GROUP':
      newState = popupNewGroup(state, action);
      break;
    case 'POPUP_NEW_ACCOUNT':
      newState = popupNewAccount(state, action);
      break;
    case 'POPUP_CANCEL':
      newState = popupCancel(state, action);
      break;
    case 'POPUP_HISTORY':
      newState = popupHistory(state, action);
      break;
    case 'POPUP_HISTORY_BACK':
      newState = popupHistoryBack(state, action);
      break;
    case 'GROUP_REMOVE':
      newState = groupRemove(state, action);
      break;
    case 'REMOVE_ITEM':
      newState = removeItem(state, action);
      break;
    case 'ACCOUNT_SAVE':
      newState = accountSave(state, action);
      break;
    case 'SELECT_ACCOUNT_CLICK':
      newState = selectAccountClick(state, action);
      break;
  }
  return newState || state || {};
}

export default popup;
export {
  receiveAccounts, receiveHistory, popupAccount, popupUpdate, popupNewGroup,
  popupNewAccount, popupCancel, popupHistory, popupHistoryBack, groupRemove,
  removeItem, accountSave
};
<|endoftext|>