<commit_before>define(['StagingServiceClient', 'common/html', 'common/runtime', 'common/ui', 'util/string'], (
    StagingServiceClient,
    html,
    Runtime,
    UI,
    String
) => {
    'use strict';
    const div = html.tag('div'),
        p = html.tag('p'),
        ul = html.tag('ul'),
        li = html.tag('li'),
        select = html.tag('select'),
        option = html.tag('option'),
        input = html.tag('input'),
        label = html.tag('label'),
        fieldset = html.tag('fieldset'),
        cssBaseClass = 'kb-xsv-gen';

    const formConfig = {
        layout: [
            'types', // multiple choice
            'output_file_type', // CSV / TSV / EXCEL
            'output_directory', // file path
        ],
        specs: {
            types: {
                original: {
                    advanced: 0,
                    allow_multiple: 1,
                    default_values: [
                        '', // current type
                    ],
                    description: 'File types to generate templates for',
                    disabled: 0,
                    dropdown_options: {
                        multiselection: 1,
                        options: [
                            // map file type to display name
                        ],
                    },
                    field_type: 'dropdown',
                    id: 'types',
                    optional: 0,
                    short_hint: 'File types to generate templates for',
                    ui_class: 'parameter',
                    ui_name: 'Import types(s)',
                },
                _position: 1,
            },
            output_file_type: {
                original: {
                    advanced: 0,
                    allow_multiple: 0,
                    default_values: ['CSV'],
                    description: 'Format for the output file',
                    disabled: 0,
                    dropdown_options: {
                        multiselection: 1,
                        options: [
                            {
                                display: 'Comma-separated (CSV)',
                                value: 'CSV',
                            },
                            {
                                display: 'Tab-separated (TSV)',
                                value: 'TSV',
                            },
                            {
                                display: 'Excel (XLSX)',
                                value: 'EXCEL',
                            },
                        ],
                    },
                    field_type: 'dropdown',
                    id: 'output_file_type',
                    optional: 0,
                    short_hint: 'Format for the output file',
                    ui_class: 'parameter',
                    ui_name: 'Output file type',
                },
                _position: 1,
            },
            output_directory: {
                original: {
                    advanced: 0,
                    allow_multiple: 0,
                    default_values: [''],
                    description: 'Directory where template files will be created',
                    disabled: 0,
                    field_type: 'text',
                    id: 'output_directory',
                    optional: 0,
                    short_hint: 'Directory where template files will be created',
                    text_options: {
                        is_output_name: 1,
                        placeholder: 'bulk_import_templates',
                        regex_constraint: [],
                        valid_ws_types: [],
                    },
                    ui_class: 'output',
                    ui_name: 'Destination directory',
                },
                _position: 3,
            },
        },
    };

    /**
     * This widget allows the user to generate a downloadable template suitable for using
     * with the xSV bulk import interface.
     *
     * @param {object} options has keys:
     *     model: cell metadata
     *     typesToFiles: mapping of data types to app IDs
     * @returns
     */
    class XSVGenerator {
        /**
         *
         * @param {object} options configuration options, including
         *      model           object  cell metadata in one handy Props object
         *      typesToFiles    object  mapping of the data types to app IDs
         */
        constructor(options) {
            this._init(options);
        }

        _init(options) {
            ['model', 'typesToFiles', 'fileTypeMapping'].forEach((prop) => {
                if (!options || !options[prop]) {
                    throw new Error(`XSV Generator requires the param "${prop}" for instantiation`);
                }
                this[prop] = options[prop];
            });
            this.runtime = Runtime.make();
            this.cssBaseClass = cssBaseClass;
        }

        async run() {
            const body = this.renderLayout();
            const modalArgs = {
                title: 'Create CSV Template',
                body,
                okLabel: 'Generate template!',
                doThisFirst: (mdn) => {
                    const submit = mdn.querySelector('[data-element="ok"]');
                    // TODO: the form should monitor its state of validity and
                    // only allow the query to be submitted when the form is valid
                    submit.addEventListener('click', this.runRequest.bind(this));
                },
            };
            await UI.showConfirmDialog(modalArgs);
        }

        runRequest() {
            const form = document.querySelector('#' + this.id);
            const params = ['output_directory', 'output_file_type', 'types'];
            const formValues = {};

            // TODO: validate the form params
            params.forEach((param) => {
                const formEl = form.querySelector(`[name="${param}"]`);
                if (formEl) {
                    if (param === 'types') {
                        formValues[param] = Array.from(formEl.selectedOptions).map((x) => x.value);
                    } else {
                        formValues[param] = formEl.value;
                    }
                }
            });

            // generate the request params to send to the staging service
            // and send the request
            this.sendRequest(this.generateRequest(formValues)).then(
                // promise resolved
                (data) => {
                    // display the results
                    this.displayResult(data);
                },
                // promise rejected
                (jqXHR, _textStatus, errorThrown) => {
                    console.error(errorThrown);
                    let error;
                    if (jqXHR.responseJSON) {
                        error = jqXHR.responseJSON;
                    } else {
                        error = `${errorThrown} - ${jqXHR.responseText}`;
                    }
                    this.displayResult({ error });
                }
            );
        }

        /**
         * Generate the layout for the initial dialog box
         *
         * @returns {string} string of HTML to use as the body of the dialog box
         */
        renderLayout() {
            const state = this.model.getItem('state');
            let errorMessage;
            const hasErrors = Object.values(state.params).some((val) => val !== 'complete');
            if (hasErrors) {
                errorMessage =
                    'Please note that there are errors in the input parameters for the selected data types, and that data upload will fail if the template is used unaltered.';
            }
            return div(
                {
                    class: `${cssBaseClass}__container`,
                },
                [
                    div(
                        {
                            class: `${cssBaseClass}__container--errors`,
                        },
                        errorMessage ? p(errorMessage) : null
                    ),
                    this.createForm(),
                ]
            );
        }

        /**
         * Generate the appropriate form for creating template downloads
         *
         * @returns {string} HTML string containing the form
         */
        createForm() {
            this.id = html.genId();
            const typeArray = Object.keys(this.model.getItem('params'));
            const state = this.model.getItem('state');

            return div(
                {
                    id: this.id,
                },
                [
                    // input file types
                    fieldset([
                        p(
                            label(
                                {
                                    for: formConfig.specs.types.original.id,
                                },
                                formConfig.specs.types.original.ui_name +
                                    ' (select all appropriate)'
                            )
                        ),
                        select(
                            {
                                id: formConfig.specs.types.original.id,
                                name: formConfig.specs.types.original.id,
                                // er...
                                multiple: 'multiple',
                                required: 'required',
                            },
                            typeArray.sort().map((opt) => {
                                if (state.selectedFileType === opt) {
                                    return option(
                                        {
                                            value: opt,
                                            selected: 'selected',
                                        },
                                        this.fileTypeMapping[opt]
                                    );
                                }
                                return option(
                                    {
                                        value: opt,
                                    },
                                    this.fileTypeMapping[opt]
                                );
                            })
                        ),
                    ]),

                    // output_file_types
                    fieldset([
                        p(
                            label(
                                {
                                    for: formConfig.specs.output_file_type.original.id,
                                },
                                formConfig.specs.output_file_type.original.ui_name
                            )
                        ),
                        select(
                            {
                                id: formConfig.specs.output_file_type.original.id,
                                name: formConfig.specs.output_file_type.original.id,
                            },
                            formConfig.specs.output_file_type.original.dropdown_options.options.map(
                                (opt) => {
                                    if (
                                        formConfig.specs.output_file_type.original
                                            .default_values[0] === opt.value
                                    ) {
                                        return option(
                                            {
                                                value: opt.value,
                                                selected: 'selected',
                                            },
                                            opt.display
                                        );
                                    }
                                    return option(
                                        {
                                            value: opt.value,
                                        },
                                        opt.display
                                    );
                                }
                            )
                        ),
                    ]),

                    // output file directory
                    fieldset([
                        p(
                            `<label for="${formConfig.specs.output_directory.original.id}">${formConfig.specs.output_directory.original.ui_name}</label>`
                        ),
                        input(
                            {
                                id: formConfig.specs.output_directory.original.id,
                                type: 'text',
                                name: formConfig.specs.output_directory.original.id,
                                placeholder:
                                    formConfig.specs.output_directory.original.text_options
                                        .placeholder,
                                value: formConfig.specs.output_directory.original.text_options
                                    .placeholder,
                                minlength: 1,
                                required: true,
                            },
                            ''
                        ),
                    ]),
                ]
            );
        }

        /**
         *  Coerce the form and file upload params into the appropriate format
         *  for the staging service endpoint
         *
         * @param {object} formParams validated parameters from the form with keys
         *      output_file_type: format to output the file in
         *      output_directory: where to save the file
         *      types: validated array of input types to include in the request
         * @returns {object} output, suitable for sending to the staging service endpoint
         */
        generateRequest(formParams) {
            const { output_file_type, output_directory, types } = formParams;

            const params = this.model.getItem('params'),
                appSpecs = this.model.getItem('app.specs'),
                output = {
                    output_directory,
                    output_file_type,
                    types: {},
                };

            types.forEach((fileType) => {
                const appId = this.typesToFiles[fileType].appId;
                output.types[fileType] = {
                    order_and_display: appSpecs[appId].parameters.map((param) => {
                        return [param.id, param.ui_name];
                    }),
                    data: params[fileType].filePaths.map((element) => {
                        const datum = JSON.parse(JSON.stringify(element));
                        Object.keys(params[fileType].params).forEach((param) => {
                            datum[param] = params[fileType].params[param];
                        });
                        return datum;
                    }),
                };
            });
            return output;
        }

        /**
         * Send the request to the staging service client
         * @param {object} requestParams the body for the request
         * @returns jQuery.ajax
         */
        sendRequest(requestParams) {
            if (!this.stagingServiceClient) {
                this.stagingServiceClient = new StagingServiceClient({
                    root: this.runtime.config('services.staging_api_url.url'),
                    token: this.runtime.authToken(),
                });
            }
            return this.stagingServiceClient.write_bulk_specification({
                data: requestParams,
                dataType: 'json',
                headers: { 'Content-Type': 'application/json' },
            });
        }

        /**
         *  Display the results of the query to generate the template
         *
         * @param {object} resp response from the staging service endpoint
         */
        displayResult(resp) {
            if (resp.error || !resp.files_created || !Object.keys(resp.files_created).length) {
                const errorString = resp.error
                    ? String.escape(resp.error)
                    : 'No files were created.';

                return UI.showInfoDialog({
                    title: 'Template Generation Error',
                    body: div(
                        {
                            class: `${cssBaseClass}__result--error`,
                        },
                        [
                            p('Template generation failed with the following error:'),
                            p(errorString),
                            p('Oh shit!'),
                        ]
                    ),
                    okLabel: 'OK',
                    bsClass: 'danger',
                });
            }

            const newFiles = new Set(Object.values(resp.files_created));
            const s_have = newFiles.size === 1 ? ' has' : 's have';
            const body = div(
                {
                    class: `${cssBaseClass}__result--success`,
                },
                [
                    p(
                        {
                            class: `${cssBaseClass}__result_text`,
                        },
                        `The following file${s_have} been added to the staging area:`
                    ),
                    ul(
                        {
                            class: `${cssBaseClass}__file_list`,
                        },
                        Array.from(newFiles)
                            .sort()
                            .map((file) => {
                                return li(
                                    {
                                        class: `${cssBaseClass}__file_list_item`,
                                    },
                                    String.escape(file)
                                );
                            })
                    ),
                ]
            );

            return UI.showInfoDialog({
                title: 'Template Generation Successful!',
                body,
                bsClass: 'success',
            });
        }
    }

    return XSVGenerator;
});
<commit_msg>Adding a link to the docs<commit_after>define(['StagingServiceClient', 'common/html', 'common/runtime', 'common/ui', 'util/string'], (
    StagingServiceClient,
    html,
    Runtime,
    UI,
    String
) => {
    'use strict';
    const div = html.tag('div'),
        p = html.tag('p'),
        aTag = html.tag('a'),
        ul = html.tag('ul'),
        li = html.tag('li'),
        select = html.tag('select'),
        option = html.tag('option'),
        input = html.tag('input'),
        label = html.tag('label'),
        fieldset = html.tag('fieldset'),
        cssBaseClass = 'kb-xsv-gen';

    const DOCS_LINK = 'https://docs.kbase.us/data/upload-download-guide/csv';

    const formConfig = {
        layout: [
            'types', // multiple choice
            'output_file_type', // CSV / TSV / EXCEL
            'output_directory', // file path
        ],
        specs: {
            types: {
                original: {
                    advanced: 0,
                    allow_multiple: 1,
                    default_values: [
                        '', // current type
                    ],
                    description: 'File types to generate templates for',
                    disabled: 0,
                    dropdown_options: {
                        multiselection: 1,
                        options: [
                            // map file type to display name
                        ],
                    },
                    field_type: 'dropdown',
                    id: 'types',
                    optional: 0,
                    short_hint: 'File types to generate templates for',
                    ui_class: 'parameter',
                    ui_name: 'Import types(s)',
                },
                _position: 1,
            },
            output_file_type: {
                original: {
                    advanced: 0,
                    allow_multiple: 0,
                    default_values: ['CSV'],
                    description: 'Format for the output file',
                    disabled: 0,
                    dropdown_options: {
                        multiselection: 1,
                        options: [
                            {
                                display: 'Comma-separated (CSV)',
                                value: 'CSV',
                            },
                            {
                                display: 'Tab-separated (TSV)',
                                value: 'TSV',
                            },
                            {
                                display: 'Excel (XLSX)',
                                value: 'EXCEL',
                            },
                        ],
                    },
                    field_type: 'dropdown',
                    id: 'output_file_type',
                    optional: 0,
                    short_hint: 'Format for the output file',
                    ui_class: 'parameter',
                    ui_name: 'Output file type',
                },
                _position: 1,
            },
            output_directory: {
                original: {
                    advanced: 0,
                    allow_multiple: 0,
                    default_values: [''],
                    description: 'Directory where template files will be created',
                    disabled: 0,
                    field_type: 'text',
                    id: 'output_directory',
                    optional: 0,
                    short_hint: 'Directory where template files will be created',
                    text_options: {
                        is_output_name: 1,
                        placeholder: 'bulk_import_templates',
                        regex_constraint: [],
                        valid_ws_types: [],
                    },
                    ui_class: 'output',
                    ui_name: 'Destination directory',
                },
                _position: 3,
            },
        },
    };

    /**
     * This widget allows the user to generate a downloadable template suitable for using
     * with the xSV bulk import interface.
     *
     * @param {object} options has keys:
     *     model: cell metadata
     *     typesToFiles: mapping of data types to app IDs
     * @returns
     */
    class XSVGenerator {
        /**
         *
         * @param {object} options configuration options, including
         *      model           object  cell metadata in one handy Props object
         *      typesToFiles    object  mapping of the data types to app IDs
         */
        constructor(options) {
            this._init(options);
        }

        _init(options) {
            ['model', 'typesToFiles', 'fileTypeMapping'].forEach((prop) => {
                if (!options || !options[prop]) {
                    throw new Error(`XSV Generator requires the param "${prop}" for instantiation`);
                }
                this[prop] = options[prop];
            });
            this.runtime = Runtime.make();
            this.cssBaseClass = cssBaseClass;
        }

        async run() {
            const body = this.renderLayout();
            const modalArgs = {
                title: 'Create CSV Template',
                body,
                okLabel: 'Generate template!',
                doThisFirst: (mdn) => {
                    const submit = mdn.querySelector('[data-element="ok"]');
                    // TODO: the form should monitor its state of validity and
                    // only allow the query to be submitted when the form is valid
                    submit.addEventListener('click', this.runRequest.bind(this));
                },
            };
            await UI.showConfirmDialog(modalArgs);
        }

        runRequest() {
            const form = document.querySelector('#' + this.id);
            const params = ['output_directory', 'output_file_type', 'types'];
            const formValues = {};

            // TODO: validate the form params
            params.forEach((param) => {
                const formEl = form.querySelector(`[name="${param}"]`);
                if (formEl) {
                    if (param === 'types') {
                        formValues[param] = Array.from(formEl.selectedOptions).map((x) => x.value);
                    } else {
                        formValues[param] = formEl.value;
                    }
                }
            });

            // generate the request params to send to the staging service
            // and send the request
            this.sendRequest(this.generateRequest(formValues)).then(
                // promise resolved
                (data) => {
                    // display the results
                    this.displayResult(data);
                },
                // promise rejected
                (jqXHR, _textStatus, errorThrown) => {
                    console.error(errorThrown);
                    let error;
                    if (jqXHR.responseJSON) {
                        error = jqXHR.responseJSON;
                    } else {
                        error = `${errorThrown} - ${jqXHR.responseText}`;
                    }
                    this.displayResult({ error });
                }
            );
        }

        /**
         * Generate the layout for the initial dialog box
         *
         * @returns {string} string of HTML to use as the body of the dialog box
         */
        renderLayout() {
            const state = this.model.getItem('state');
            let errorMessage;
            const hasErrors = Object.values(state.params).some((val) => val !== 'complete');
            if (hasErrors) {
                errorMessage =
                    'Please note that there are errors in the input parameters for the selected data types, and that data upload will fail if the template is used unaltered.';
            }
            return div(
                {
                    class: `${cssBaseClass}__container`,
                },
                [
                    div(
                        {
                            class: `${cssBaseClass}__container--errors`,
                        },
                        errorMessage ? p(errorMessage) : null
                    ),
                    this.createForm(),
                    p(
                        {
                            class: `${cssBaseClass}__help`,
                        },
                        aTag(
                            {
                                href: DOCS_LINK,
                                target: '_blank',
                            },
                            'Help and documentation on using templates'
                        )
                    ),
                ]
            );
        }

        /**
         * Generate the appropriate form for creating template downloads
         *
         * @returns {string} HTML string containing the form
         */
        createForm() {
            this.id = html.genId();
            const typeArray = Object.keys(this.model.getItem('params'));
            const state = this.model.getItem('state');

            return div(
                {
                    id: this.id,
                },
                [
                    // input file types
                    fieldset([
                        p(
                            label(
                                {
                                    for: formConfig.specs.types.original.id,
                                },
                                formConfig.specs.types.original.ui_name +
                                    ' (select all appropriate)'
                            )
                        ),
                        select(
                            {
                                id: formConfig.specs.types.original.id,
                                name: formConfig.specs.types.original.id,
                                // er...
                                multiple: 'multiple',
                                required: 'required',
                            },
                            typeArray.sort().map((opt) => {
                                if (state.selectedFileType === opt) {
                                    return option(
                                        {
                                            value: opt,
                                            selected: 'selected',
                                        },
                                        this.fileTypeMapping[opt]
                                    );
                                }
                                return option(
                                    {
                                        value: opt,
                                    },
                                    this.fileTypeMapping[opt]
                                );
                            })
                        ),
                    ]),

                    // output_file_types
                    fieldset([
                        p(
                            label(
                                {
                                    for: formConfig.specs.output_file_type.original.id,
                                },
                                formConfig.specs.output_file_type.original.ui_name
                            )
                        ),
                        select(
                            {
                                id: formConfig.specs.output_file_type.original.id,
                                name: formConfig.specs.output_file_type.original.id,
                            },
                            formConfig.specs.output_file_type.original.dropdown_options.options.map(
                                (opt) => {
                                    if (
                                        formConfig.specs.output_file_type.original
                                            .default_values[0] === opt.value
                                    ) {
                                        return option(
                                            {
                                                value: opt.value,
                                                selected: 'selected',
                                            },
                                            opt.display
                                        );
                                    }
                                    return option(
                                        {
                                            value: opt.value,
                                        },
                                        opt.display
                                    );
                                }
                            )
                        ),
                    ]),

                    // output file directory
                    fieldset([
                        p(
                            `<label for="${formConfig.specs.output_directory.original.id}">${formConfig.specs.output_directory.original.ui_name}</label>`
                        ),
                        input(
                            {
                                id: formConfig.specs.output_directory.original.id,
                                type: 'text',
                                name: formConfig.specs.output_directory.original.id,
                                placeholder:
                                    formConfig.specs.output_directory.original.text_options
                                        .placeholder,
                                value: formConfig.specs.output_directory.original.text_options
                                    .placeholder,
                                minlength: 1,
                                required: true,
                            },
                            ''
                        ),
                    ]),
                ]
            );
        }

        /**
         *  Coerce the form and file upload params into the appropriate format
         *  for the staging service endpoint
         *
         * @param {object} formParams validated parameters from the form with keys
         *      output_file_type: format to output the file in
         *      output_directory: where to save the file
         *      types: validated array of input types to include in the request
         * @returns {object} output, suitable for sending to the staging service endpoint
         */
        generateRequest(formParams) {
            const { output_file_type, output_directory, types } = formParams;

            const params = this.model.getItem('params'),
                appSpecs = this.model.getItem('app.specs'),
                output = {
                    output_directory,
                    output_file_type,
                    types: {},
                };

            types.forEach((fileType) => {
                const appId = this.typesToFiles[fileType].appId;
                output.types[fileType] = {
                    order_and_display: appSpecs[appId].parameters.map((param) => {
                        return [param.id, param.ui_name];
                    }),
                    data: params[fileType].filePaths.map((element) => {
                        const datum = JSON.parse(JSON.stringify(element));
                        Object.keys(params[fileType].params).forEach((param) => {
                            datum[param] = params[fileType].params[param];
                        });
                        return datum;
                    }),
                };
            });
            return output;
        }

        /**
         * Send the request to the staging service client
         * @param {object} requestParams the body for the request
         * @returns jQuery.ajax
         */
        sendRequest(requestParams) {
            if (!this.stagingServiceClient) {
                this.stagingServiceClient = new StagingServiceClient({
                    root: this.runtime.config('services.staging_api_url.url'),
                    token: this.runtime.authToken(),
                });
            }
            return this.stagingServiceClient.write_bulk_specification({
                data: requestParams,
                dataType: 'json',
                headers: { 'Content-Type': 'application/json' },
            });
        }

        /**
         *  Display the results of the query to generate the template
         *
         * @param {object} resp response from the staging service endpoint
         */
        displayResult(resp) {
            if (resp.error || !resp.files_created || !Object.keys(resp.files_created).length) {
                const errorString = resp.error
                    ? String.escape(resp.error)
                    : 'No files were created.';

                return UI.showInfoDialog({
                    title: 'Template Generation Error',
                    body: div(
                        {
                            class: `${cssBaseClass}__result--error`,
                        },
                        [
                            p('Template generation failed with the following error:'),
                            p(errorString),
                            p('Oh shit!'),
                        ]
                    ),
                    okLabel: 'OK',
                    bsClass: 'danger',
                });
            }

            const newFiles = new Set(Object.values(resp.files_created));
            const s_have = newFiles.size === 1 ? ' has' : 's have';
            const body = div(
                {
                    class: `${cssBaseClass}__result--success`,
                },
                [
                    p(
                        {
                            class: `${cssBaseClass}__result_text`,
                        },
                        `The following file${s_have} been added to the staging area:`
                    ),
                    ul(
                        {
                            class: `${cssBaseClass}__file_list`,
                        },
                        Array.from(newFiles)
                            .sort()
                            .map((file) => {
                                return li(
                                    {
                                        class: `${cssBaseClass}__file_list_item`,
                                    },
                                    String.escape(file)
                                );
                            })
                    ),
                ]
            );

            return UI.showInfoDialog({
                title: 'Template Generation Successful!',
                body,
                bsClass: 'success',
            });
        }
    }

    return XSVGenerator;
});
<|endoftext|>


<commit_before>import Realm from 'realm';

import { complement } from 'set-manipulator';

import {
  addBatchToParent,
  createRecord,
  getTotal,
  reuseNumber as reuseSerialNumber,
  NUMBER_SEQUENCE_KEYS,
} from '../utilities';

/**
 * A transaction.
 *
 * @property  {string}                  id
 * @property  {string}                  serialNumber
 * @property  {Name}                    otherParty
 * @property  {string}                  comment
 * @property  {Date}                    entryDate
 * @property  {string}                  type
 * @property  {string}                  status
 * @property  {Date}                    confirmDate
 * @property  {User}                    enteredBy
 * @property  {string}                  theirRef           External reference code.
 * @property  {TransactionCategory}     category
 * @property  {List.<TransactionItem>}  items
 * @property  {Requisition}             linkedRequisition
 */
export class Transaction extends Realm.Object {
  /**
   * Create new transaction.
   */
  constructor() {
    super();
    this.addItemsFromMasterList = this.addItemsFromMasterList.bind(this);
  }

  /**
   * Delete unfinalised transaction and associated transaction items. Throw error if transaction is
   * finalised.
   *
   * @param  {Realm}  database
   */
  destructor(database) {
    if (this.isFinalised) {
      throw new Error('Cannot delete finalised transaction');
    }

    // Recycle invoice serial number to be reused.
    if (this.isCustomerInvoice) {
      reuseSerialNumber(database, NUMBER_SEQUENCE_KEYS.CUSTOMER_INVOICE_NUMBER, this.serialNumber);
    }
    if (this.isSupplierInvoice) {
      reuseSerialNumber(database, NUMBER_SEQUENCE_KEYS.SUPPLIER_INVOICE_NUMBER, this.serialNumber);
    }
    database.delete('TransactionItem', this.items);
  }

  /**
   * Get if transaction is finalised.
   *
   * @return  {boolean}
   */
  get isFinalised() {
    return this.status === 'finalised';
  }

  /**
   * Get if transaction is confirmed.
   *
   * @return  {boolean}
   */
  get isConfirmed() {
    return this.status === 'confirmed';
  }

  /**
   * Get if transaction is incoming (i.e. a supplier invoice).
   *
   * @return  {boolean}
   */
  get isIncoming() {
    return this.type === 'supplier_invoice' || this.type === 'customer_credit';
  }

  /**
   * Get if transaction is outgoing (i.e. a customer invoice or supplier credit).
   *
   * @return  {boolean}
   */
  get isOutgoing() {
    // TODO: replace string matching with already defined getter methods.
    return this.type === 'customer_invoice' || this.type === 'supplier_credit';
  }

  /**
   * Get if transaction is a customer invoice.
   *
   * @return  {boolean}
   */
  get isCustomerInvoice() {
    return this.type === 'customer_invoice';
  }

  /**
   * Get if transaction is a supplier invoice.
   *
   * @return  {boolean]}
   */
  get isSupplierInvoice() {
    return this.type === 'supplier_invoice';
  }

  get isPrescription() {
    return this.mode === 'dispensary' && this.type === 'customer_invoice';
  }

  /**
   * Get if transaction is an external supplier invoice.
   *
   * @return  {boolean}
   */
  get isExternalSupplierInvoice() {
    return this.isSupplierInvoice && this.otherParty && this.otherParty.isExternalSupplier;
  }

  /**
   * Get if transaction is an internal supplier invoice.
   *
   * @return  {boolean}
   */
  get isInternalSupplierInvoice() {
    return this.isSupplierInvoice && this.otherParty && this.otherParty.isInternalSupplier;
  }

  /**
   * Get if transaction is an inventory adjustment.
   *
   * @return  {boolean}
   */
  get isInventoryAdjustment() {
    return this.otherParty && this.otherParty.type === 'inventory_adjustment';
  }

  /**
   * Get name of other party to transaction.
   *
   * @return  {string}
   */
  get otherPartyName() {
    return this.otherParty ? this.otherParty.name : '';
  }

  /**
   * @return {String} This transaction reason title, or an empty string.
   */
  get reasonTitle() {
    return (this.option && this.option.title) || '';
  }

  /**
   * Set other party to this transaction.
   *
   * @param  {Name}  name
   */
  setOtherParty(name) {
    this.otherParty = name;
  }

  /**
   * Get total price of items in this transaction.
   *
   * @return  {number}
   */
  get totalPrice() {
    return getTotal(this.items, 'totalPrice');
  }

  /**
   * Get total quantity of items in this transaction.
   *
   * @return  {number}
   */
  get totalQuantity() {
    return getTotal(this.items, 'totalQuantity');
  }

  /**
   * Get total number of batches in this transaction.
   *
   * @return  {number}
   */
  get numberOfBatches() {
    return getTotal(this.items, 'numberOfBatches');
  }

  /**
   * Get total number of items in this transaction.
   *
   * @return  {number}
   */
  get numberOfItems() {
    return this.items.length;
  }

  /**
   * Get if this transaction is linked to a requisition.
   *
   * @return  {boolean}
   */
  get isLinkedToRequisition() {
    return !!this.linkedRequisition;
  }

  /**
   * Get if this transaction includes a given item.
   *
   * @param   {Item}  item
   * @return  {boolean}
   */
  hasItem(item) {
    const itemId = item.realItem.id;
    return this.items.filtered('item.id == $0', itemId).length > 0;
  }

  /**
   * Add an item to this transaction. If item already exists, do nothing.
   *
   * @param  {TransactionItem}  transactionItem
   */
  addItem(transactionItem) {
    this.items.push(transactionItem);
  }

  /**
   * Add all items from the customer's master list to this transaction. Transaction
   * must be an unfinalised customer invoice.
   *
   * @param  {Realm}            database
   * @param  {Array.<string>}   selected masterlists from multiselect
   */
  addItemsFromMasterList({ database, selected }) {
    if (!this.isCustomerInvoice) {
      throw new Error(`Cannot add master lists to ${this.type}`);
    }
    if (this.isFinalised) {
      throw new Error('Cannot add items to a finalised transaction');
    }
    if (this.otherParty) {
      // Filter through masterList ids that are on multiselect list
      this.otherParty.masterLists
        .filter(masterList => selected.indexOf(masterList.id) !== -1)
        .forEach(masterList => {
          const itemsToAdd = complement(masterList.items, this.items, item => item.itemId);
          itemsToAdd.forEach(masterListItem => {
            if (!masterListItem.item.crossReferenceItem && masterListItem.item.isVisible) {
              // Do not add cross reference items as will cause duplicates.
              createRecord(database, 'TransactionItem', this, masterListItem.item);
            }
          });
        });
    }
  }

  /**
   * Remove items with the given ids from this transaction.
   *
   * @param   {Realm}           database
   * @param   {Array.<string>}  itemIds
   */
  removeItemsById(database, itemIds) {
    const itemsToDelete = [];
    for (let i = 0; i < itemIds.length; i += 1) {
      const transactionItem = this.items.find(testItem => testItem.id === itemIds[i]);
      if (transactionItem.isValid()) {
        itemsToDelete.push(transactionItem);
      }
    }
    database.delete('transactionItem', itemsToDelete);
  }

  /**
   * Remove batches with given ids from this transaction. Prune any items with no remaining
   * associated batches.
   *
   * @param   {Realm}           database
   * @param   {Array.<string>}  transactionBatchIds
   */
  removeTransactionBatchesById(database, transactionBatchIds) {
    if (this.isFinalised) {
      throw new Error('Cannot modify finalised transaction');
    }
    const transactionBatches = this.getTransactionBatches(database);
    const transactionBatchesToDelete = [];
    transactionBatchIds.forEach(transactionBatchId => {
      const transactionBatch = transactionBatches.find(
        matchTransactionBatch => matchTransactionBatch.id === transactionBatchId
      );
      transactionBatchesToDelete.push(transactionBatch);
    });
    database.delete('TransactionBatch', transactionBatchesToDelete);
    this.pruneBatchlessTransactionItems(database);
  }

  /**
   * Remove given item from transaction.
   *
   * @param   {Realm}            database
   * @param   {TransactionItem}  TransactionItem
   */
  // eslint-disable-next-line class-methods-use-this
  removeTransactionItem(database, transactionItem) {
    database.delete('TransactionItem', transactionItem);
  }

  /**
   * Adds a batch to transaction, incorporating it into a matching item or creating a new
   * item if none already exist.
   *
   * @param  {Realm}             database
   * @param  {TransactionBatch}  transactionBatch
   */
  addBatchIfUnique(database, transactionBatch) {
    const { itemBatch } = transactionBatch;
    const { item } = itemBatch;

    addBatchToParent(transactionBatch, this, () =>
      createRecord(database, 'TransactionItem', this, item)
    );
  }

  /**
   * Delete any batches and items that are not contributing to this transaction.
   *
   * @param  {Realm}  database
   */
  pruneRedundantBatchesAndItems(database) {
    const batchesToRemove = this.getTransactionBatches(database).filtered('numberOfPacks = 0');

    database.delete('TransactionBatch', batchesToRemove);
    this.pruneBatchlessTransactionItems(database);
  }

  /**
   * Delete any items with no associated transaction batches.
   *
   * @param  {Realm}  database
   */
  pruneBatchlessTransactionItems(database) {
    const itemsToRemove = [];
    this.items.forEach(transactionItem => {
      if (transactionItem.batches.length === 0) {
        itemsToRemove.push(transactionItem);
      }
    });
    database.delete('TransactionItem', itemsToRemove);
  }

  /**
   * Returns all transaction batches for this transaction as
   * a realm collection (can be filtered).
   *
   * @param   {Realm}                               database
   * @return  {RealmCollection.<TransactionBatch>}
   */
  getTransactionBatches(database) {
    return database.objects('TransactionBatch').filtered('transaction.id == $0', this.id);
  }

  /**
   * Confirm this transaction, generating the associated item batches, linking them
   * to their items, and setting the status to confirmed.
   *
   * @param  {Realm}  database
   */
  confirm(database) {
    if (this.isConfirmed) {
      throw new Error('Cannot confirm as transaction is already confirmed');
    }
    if (this.isFinalised) {
      throw new Error('Cannot confirm as transaction is already finalised');
    }
    const isIncomingInvoice = this.isIncoming;

    this.getTransactionBatches(database).forEach(transactionBatch => {
      const {
        itemBatch,
        batch,
        packSize,
        numberOfPacks,
        expiryDate,
        costPrice,
        sellPrice,
      } = transactionBatch;

      // Pack to one all transactions in mobile, so multiply by |packSize| to get
      // quantity and price.
      const packedToOneQuantity = numberOfPacks * packSize;
      const packedToOneCostPrice = costPrice / packSize;
      const packedToOneSellPrice = sellPrice / packSize;

      const newNumberOfPacks = isIncomingInvoice
        ? itemBatch.numberOfPacks + packedToOneQuantity
        : itemBatch.numberOfPacks - packedToOneQuantity;
      itemBatch.packSize = 1;
      itemBatch.numberOfPacks = newNumberOfPacks;
      itemBatch.expiryDate = expiryDate;
      itemBatch.batch = this.adjustBatchName(batch);
      itemBatch.costPrice = packedToOneCostPrice;
      itemBatch.sellPrice = packedToOneSellPrice;
      if (isIncomingInvoice) itemBatch.supplier = this.otherParty;

      database.save('ItemBatch', itemBatch);
    });

    this.confirmDate = new Date();
    this.status = 'confirmed';
  }

  /**
   * Get `supplierinvoice_{invoiceNumber}` of batch. Name is empty and
   * transaction is supplier invoice.
   *
   * @param   {string}  batchName
   * @return  {string}
   */
  adjustBatchName(batchName) {
    if (this.isSupplierInvoice && (!batchName || batchName === '')) {
      return `supplierinvoice_${this.serialNumber}`;
    }
    return batchName;
  }

  /**
   * Finalise this transaction, setting the status so that this transaction is
   * locked down. If it has not already been confirmed (i.e. adjustments to inventory
   * made), confirm it first.
   *
   * @param  {Realm}  database
   */
  finalise(database) {
    if (this.isFinalised) {
      throw new Error('Cannot finalise as transaction is already finalised');
    }
    // Prune all invoices except internal supplier invoices.
    if (!this.isInternalSupplierInvoice) {
      this.pruneRedundantBatchesAndItems(database);
    }
    if (!this.isConfirmed) this.confirm(database);

    this.status = 'finalised';
    database.save('Transaction', this);
  }
}

Transaction.schema = {
  name: 'Transaction',
  primaryKey: 'id',
  properties: {
    id: 'string',
    serialNumber: { type: 'string', default: 'placeholderSerialNumber' },
    otherParty: { type: 'Name', optional: true },
    comment: { type: 'string', optional: true },
    entryDate: { type: 'date', optional: true },
    type: { type: 'string', default: 'placeholderType' },
    status: { type: 'string', default: 'new' },
    confirmDate: { type: 'date', optional: true },
    enteredBy: { type: 'User', optional: true },
    theirRef: { type: 'string', optional: true },
    category: { type: 'TransactionCategory', optional: true },
    items: { type: 'list', objectType: 'TransactionItem' },
    mode: { type: 'string', default: 'store' },
    prescriber: { type: 'Prescriber', optional: true },
    linkedRequisition: { type: 'Requisition', optional: true },
    total: { type: 'float', optional: true },
    outstanding: { type: 'float', optional: true },
    insurancePolicy: { type: 'InsurancePolicy', optional: true },
    option: { type: 'Options', optional: true },
    linkedTransaction: { type: 'Transaction', optional: true },
    user1: { type: 'string', optional: true },
    insuranceDiscountRate: { type: 'double', optional: true },
    insuranceDiscountAmount: { type: 'double', optional: true },
  },
};

export default Transaction;
<commit_msg>Add set total when finalising<commit_after>import Realm from 'realm';

import { complement } from 'set-manipulator';

import {
  addBatchToParent,
  createRecord,
  getTotal,
  reuseNumber as reuseSerialNumber,
  NUMBER_SEQUENCE_KEYS,
} from '../utilities';

/**
 * A transaction.
 *
 * @property  {string}                  id
 * @property  {string}                  serialNumber
 * @property  {Name}                    otherParty
 * @property  {string}                  comment
 * @property  {Date}                    entryDate
 * @property  {string}                  type
 * @property  {string}                  status
 * @property  {Date}                    confirmDate
 * @property  {User}                    enteredBy
 * @property  {string}                  theirRef           External reference code.
 * @property  {TransactionCategory}     category
 * @property  {List.<TransactionItem>}  items
 * @property  {Requisition}             linkedRequisition
 */
export class Transaction extends Realm.Object {
  /**
   * Create new transaction.
   */
  constructor() {
    super();
    this.addItemsFromMasterList = this.addItemsFromMasterList.bind(this);
  }

  /**
   * Delete unfinalised transaction and associated transaction items. Throw error if transaction is
   * finalised.
   *
   * @param  {Realm}  database
   */
  destructor(database) {
    if (this.isFinalised) {
      throw new Error('Cannot delete finalised transaction');
    }

    // Recycle invoice serial number to be reused.
    if (this.isCustomerInvoice) {
      reuseSerialNumber(database, NUMBER_SEQUENCE_KEYS.CUSTOMER_INVOICE_NUMBER, this.serialNumber);
    }
    if (this.isSupplierInvoice) {
      reuseSerialNumber(database, NUMBER_SEQUENCE_KEYS.SUPPLIER_INVOICE_NUMBER, this.serialNumber);
    }
    database.delete('TransactionItem', this.items);
  }

  /**
   * Get if transaction is finalised.
   *
   * @return  {boolean}
   */
  get isFinalised() {
    return this.status === 'finalised';
  }

  /**
   * Get if transaction is confirmed.
   *
   * @return  {boolean}
   */
  get isConfirmed() {
    return this.status === 'confirmed';
  }

  /**
   * Get if transaction is incoming (i.e. a supplier invoice).
   *
   * @return  {boolean}
   */
  get isIncoming() {
    return this.type === 'supplier_invoice' || this.type === 'customer_credit';
  }

  /**
   * Get if transaction is outgoing (i.e. a customer invoice or supplier credit).
   *
   * @return  {boolean}
   */
  get isOutgoing() {
    // TODO: replace string matching with already defined getter methods.
    return this.type === 'customer_invoice' || this.type === 'supplier_credit';
  }

  /**
   * Get if transaction is a customer invoice.
   *
   * @return  {boolean}
   */
  get isCustomerInvoice() {
    return this.type === 'customer_invoice';
  }

  /**
   * Get if transaction is a supplier invoice.
   *
   * @return  {boolean]}
   */
  get isSupplierInvoice() {
    return this.type === 'supplier_invoice';
  }

  get isPrescription() {
    return this.mode === 'dispensary' && this.type === 'customer_invoice';
  }

  /**
   * Get if transaction is an external supplier invoice.
   *
   * @return  {boolean}
   */
  get isExternalSupplierInvoice() {
    return this.isSupplierInvoice && this.otherParty && this.otherParty.isExternalSupplier;
  }

  /**
   * Get if transaction is an internal supplier invoice.
   *
   * @return  {boolean}
   */
  get isInternalSupplierInvoice() {
    return this.isSupplierInvoice && this.otherParty && this.otherParty.isInternalSupplier;
  }

  /**
   * Get if transaction is an inventory adjustment.
   *
   * @return  {boolean}
   */
  get isInventoryAdjustment() {
    return this.otherParty && this.otherParty.type === 'inventory_adjustment';
  }

  /**
   * Get name of other party to transaction.
   *
   * @return  {string}
   */
  get otherPartyName() {
    return this.otherParty ? this.otherParty.name : '';
  }

  /**
   * @return {String} This transaction reason title, or an empty string.
   */
  get reasonTitle() {
    return (this.option && this.option.title) || '';
  }

  /**
   * Set other party to this transaction.
   *
   * @param  {Name}  name
   */
  setOtherParty(name) {
    this.otherParty = name;
  }

  /**
   * Get total price of items in this transaction.
   *
   * @return  {number}
   */
  get totalPrice() {
    return getTotal(this.items, 'totalPrice');
  }

  /**
   * Get total quantity of items in this transaction.
   *
   * @return  {number}
   */
  get totalQuantity() {
    return getTotal(this.items, 'totalQuantity');
  }

  /**
   * Get total number of batches in this transaction.
   *
   * @return  {number}
   */
  get numberOfBatches() {
    return getTotal(this.items, 'numberOfBatches');
  }

  /**
   * Get total number of items in this transaction.
   *
   * @return  {number}
   */
  get numberOfItems() {
    return this.items.length;
  }

  /**
   * Get if this transaction is linked to a requisition.
   *
   * @return  {boolean}
   */
  get isLinkedToRequisition() {
    return !!this.linkedRequisition;
  }

  /**
   * Get if this transaction includes a given item.
   *
   * @param   {Item}  item
   * @return  {boolean}
   */
  hasItem(item) {
    const itemId = item.realItem.id;
    return this.items.filtered('item.id == $0', itemId).length > 0;
  }

  /**
   * Add an item to this transaction. If item already exists, do nothing.
   *
   * @param  {TransactionItem}  transactionItem
   */
  addItem(transactionItem) {
    this.items.push(transactionItem);
  }

  /**
   * Add all items from the customer's master list to this transaction. Transaction
   * must be an unfinalised customer invoice.
   *
   * @param  {Realm}            database
   * @param  {Array.<string>}   selected masterlists from multiselect
   */
  addItemsFromMasterList({ database, selected }) {
    if (!this.isCustomerInvoice) {
      throw new Error(`Cannot add master lists to ${this.type}`);
    }
    if (this.isFinalised) {
      throw new Error('Cannot add items to a finalised transaction');
    }
    if (this.otherParty) {
      // Filter through masterList ids that are on multiselect list
      this.otherParty.masterLists
        .filter(masterList => selected.indexOf(masterList.id) !== -1)
        .forEach(masterList => {
          const itemsToAdd = complement(masterList.items, this.items, item => item.itemId);
          itemsToAdd.forEach(masterListItem => {
            if (!masterListItem.item.crossReferenceItem && masterListItem.item.isVisible) {
              // Do not add cross reference items as will cause duplicates.
              createRecord(database, 'TransactionItem', this, masterListItem.item);
            }
          });
        });
    }
  }

  /**
   * Remove items with the given ids from this transaction.
   *
   * @param   {Realm}           database
   * @param   {Array.<string>}  itemIds
   */
  removeItemsById(database, itemIds) {
    const itemsToDelete = [];
    for (let i = 0; i < itemIds.length; i += 1) {
      const transactionItem = this.items.find(testItem => testItem.id === itemIds[i]);
      if (transactionItem.isValid()) {
        itemsToDelete.push(transactionItem);
      }
    }
    database.delete('transactionItem', itemsToDelete);
  }

  /**
   * Remove batches with given ids from this transaction. Prune any items with no remaining
   * associated batches.
   *
   * @param   {Realm}           database
   * @param   {Array.<string>}  transactionBatchIds
   */
  removeTransactionBatchesById(database, transactionBatchIds) {
    if (this.isFinalised) {
      throw new Error('Cannot modify finalised transaction');
    }
    const transactionBatches = this.getTransactionBatches(database);
    const transactionBatchesToDelete = [];
    transactionBatchIds.forEach(transactionBatchId => {
      const transactionBatch = transactionBatches.find(
        matchTransactionBatch => matchTransactionBatch.id === transactionBatchId
      );
      transactionBatchesToDelete.push(transactionBatch);
    });
    database.delete('TransactionBatch', transactionBatchesToDelete);
    this.pruneBatchlessTransactionItems(database);
  }

  /**
   * Remove given item from transaction.
   *
   * @param   {Realm}            database
   * @param   {TransactionItem}  TransactionItem
   */
  // eslint-disable-next-line class-methods-use-this
  removeTransactionItem(database, transactionItem) {
    database.delete('TransactionItem', transactionItem);
  }

  /**
   * Adds a batch to transaction, incorporating it into a matching item or creating a new
   * item if none already exist.
   *
   * @param  {Realm}             database
   * @param  {TransactionBatch}  transactionBatch
   */
  addBatchIfUnique(database, transactionBatch) {
    const { itemBatch } = transactionBatch;
    const { item } = itemBatch;

    addBatchToParent(transactionBatch, this, () =>
      createRecord(database, 'TransactionItem', this, item)
    );
  }

  /**
   * Delete any batches and items that are not contributing to this transaction.
   *
   * @param  {Realm}  database
   */
  pruneRedundantBatchesAndItems(database) {
    const batchesToRemove = this.getTransactionBatches(database).filtered('numberOfPacks = 0');

    database.delete('TransactionBatch', batchesToRemove);
    this.pruneBatchlessTransactionItems(database);
  }

  /**
   * Delete any items with no associated transaction batches.
   *
   * @param  {Realm}  database
   */
  pruneBatchlessTransactionItems(database) {
    const itemsToRemove = [];
    this.items.forEach(transactionItem => {
      if (transactionItem.batches.length === 0) {
        itemsToRemove.push(transactionItem);
      }
    });
    database.delete('TransactionItem', itemsToRemove);
  }

  /**
   * Returns all transaction batches for this transaction as
   * a realm collection (can be filtered).
   *
   * @param   {Realm}                               database
   * @return  {RealmCollection.<TransactionBatch>}
   */
  getTransactionBatches(database) {
    return database.objects('TransactionBatch').filtered('transaction.id == $0', this.id);
  }

  /**
   * Confirm this transaction, generating the associated item batches, linking them
   * to their items, and setting the status to confirmed.
   *
   * @param  {Realm}  database
   */
  confirm(database) {
    if (this.isConfirmed) {
      throw new Error('Cannot confirm as transaction is already confirmed');
    }
    if (this.isFinalised) {
      throw new Error('Cannot confirm as transaction is already finalised');
    }
    const isIncomingInvoice = this.isIncoming;

    this.getTransactionBatches(database).forEach(transactionBatch => {
      const {
        itemBatch,
        batch,
        packSize,
        numberOfPacks,
        expiryDate,
        costPrice,
        sellPrice,
      } = transactionBatch;

      // Pack to one all transactions in mobile, so multiply by |packSize| to get
      // quantity and price.
      const packedToOneQuantity = numberOfPacks * packSize;
      const packedToOneCostPrice = costPrice / packSize;
      const packedToOneSellPrice = sellPrice / packSize;

      const newNumberOfPacks = isIncomingInvoice
        ? itemBatch.numberOfPacks + packedToOneQuantity
        : itemBatch.numberOfPacks - packedToOneQuantity;
      itemBatch.packSize = 1;
      itemBatch.numberOfPacks = newNumberOfPacks;
      itemBatch.expiryDate = expiryDate;
      itemBatch.batch = this.adjustBatchName(batch);
      itemBatch.costPrice = packedToOneCostPrice;
      itemBatch.sellPrice = packedToOneSellPrice;
      if (isIncomingInvoice) itemBatch.supplier = this.otherParty;

      database.save('ItemBatch', itemBatch);
    });

    this.confirmDate = new Date();
    this.status = 'confirmed';
  }

  /**
   * Get `supplierinvoice_{invoiceNumber}` of batch. Name is empty and
   * transaction is supplier invoice.
   *
   * @param   {string}  batchName
   * @return  {string}
   */
  adjustBatchName(batchName) {
    if (this.isSupplierInvoice && (!batchName || batchName === '')) {
      return `supplierinvoice_${this.serialNumber}`;
    }
    return batchName;
  }

  /**
   * Finalise this transaction, setting the status so that this transaction is
   * locked down. If it has not already been confirmed (i.e. adjustments to inventory
   * made), confirm it first.
   *
   * @param  {Realm}  database
   */
  finalise(database) {
    if (this.isFinalised) {
      throw new Error('Cannot finalise as transaction is already finalised');
    }
    // Prune all invoices except internal supplier invoices.
    if (!this.isInternalSupplierInvoice) {
      this.pruneRedundantBatchesAndItems(database);
    }
    if (!this.isConfirmed) this.confirm(database);

    this.status = 'finalised';
    this.total = this.totalPrice;
    database.save('Transaction', this);
  }
}

Transaction.schema = {
  name: 'Transaction',
  primaryKey: 'id',
  properties: {
    id: 'string',
    serialNumber: { type: 'string', default: 'placeholderSerialNumber' },
    otherParty: { type: 'Name', optional: true },
    comment: { type: 'string', optional: true },
    entryDate: { type: 'date', optional: true },
    type: { type: 'string', default: 'placeholderType' },
    status: { type: 'string', default: 'new' },
    confirmDate: { type: 'date', optional: true },
    enteredBy: { type: 'User', optional: true },
    theirRef: { type: 'string', optional: true },
    category: { type: 'TransactionCategory', optional: true },
    items: { type: 'list', objectType: 'TransactionItem' },
    mode: { type: 'string', default: 'store' },
    prescriber: { type: 'Prescriber', optional: true },
    linkedRequisition: { type: 'Requisition', optional: true },
    total: { type: 'float', optional: true },
    outstanding: { type: 'float', optional: true },
    insurancePolicy: { type: 'InsurancePolicy', optional: true },
    option: { type: 'Options', optional: true },
    linkedTransaction: { type: 'Transaction', optional: true },
    user1: { type: 'string', optional: true },
    insuranceDiscountRate: { type: 'double', optional: true },
    insuranceDiscountAmount: { type: 'double', optional: true },
  },
};

export default Transaction;
<|endoftext|>


<commit_before>;(function($) {

$.fn.extend({
});

$.TableTools = function(tbody, options) {
    var $tbody = $(tbody);
    options = $.extend({}, $.TableTools.defaults, options);
    var shiftDown = false;
    var shift1 = -1, shift2 = -1;

    // бɫ
    if (options.cross) {
        $tbody.children('tr:even').addClass(options.evenClass);
        $tbody.children('tr:odd').addClass(options.oddClass);
    }

    // 껬бɫ
    if (options.over) {
        $tbody.children('tr').mouseover( function() {
            $(this).addClass(options.overClass);
        }).mouseout( function() {
            $(this).removeClass(options.overClass);
        });
    }

    // checkboxѡӦбɫ
    if (options.selected) {
        var $boxs = $tbody.find(options.checkbox);
        $boxs.click(function(event) {
            var $box = $(this);
            if ($box.attr('checked')) {
                $box.parents('tr').removeClass(options.clickedTrClass).addClass(options.selectedClass);
                // ShiftѡĴ
                if (shiftDown) {
                    if (shift1 == -1) {
                        shift1 = $boxs.index($box);
                    } else if (shift2 == -1) {
                        shift2 = $boxs.index($box);
                        if (shift2 >= shift1) {
                            $boxs.slice(shift1, shift2).attr('checked', true).parents('tr').removeClass(options.clickedTrClass).addClass(options.selectedClass);
                        } else {
                            $boxs.slice(shift2, shift1).attr('checked', true).parents('tr').removeClass(options.clickedTrClass).addClass(options.selectedClass);
                        }
                    }
                }
            } else {
                $box.parents('tr').removeClass(options.clickedTrClass).removeClass(options.selectedClass);
            }
            event.stopPropagation();
        });
    }

    // ȫѡť
    if (options.checkAll) {
        $(options.checkAllBox).click(function() {
            var $boxs = $tbody.find(options.checkbox);
            var checked = $(this).prop('checked');
            $boxs.prop('checked', checked);
            if (options.selected) {
                if (checked) {
                    $boxs.parents('tr').removeClass(options.clickedTrClass).addClass(options.selectedClass);
                } else {
                    $boxs.parents('tr').removeClass(options.clickedTrClass).removeClass(options.selectedClass);
                }
            }
        });
    }

    // ѡѡcheckbox
    if (options.clickTr) {
        $tbody.find('tr').click(function() {
            var $clickedTr = $(this);
            if (options.clickedTrChecked) {
                if ($clickedTr.hasClass(options.selectedClass)) {
                    $clickedTr.removeClass(options.selectedClass).find(options.checkbox).attr('checked', false);
                } else {
                    $clickedTr.addClass(options.selectedClass).find(options.checkbox).attr('checked', true);
                }
            } else {
                if (!$clickedTr.hasClass(options.selectedClass)) {
                    if ($clickedTr.hasClass(options.clickedTrClass)) {
                        $clickedTr.removeClass(options.clickedTrClass);
                    } else {
                        $clickedTr.addClass(options.clickedTrClass);
                    }
                }
            }
        });
    }

    // Shiftť
    $(document).keydown(function(e){
        switch(e.keyCode){
            case 16:
                shiftDown = true;
                break;
            default:
                break;
        }
    }).keyup(function(e){
        switch(e.keyCode){
            case 16:
                shiftDown = false;
                shift1 = -1;
                shift2 = -1;
                break;
            default:
                break;
        }
    });
};

// Ĭ
$.TableTools.defaults = {
    cross: false,
    evenClass: "even",
    oddClass: "odd",

    over: false,
    overClass: "over",

    selected: false,
    selectedClass: "selected",
    checkbox: "td > input:checkbox",

    checkAll: false,
    checkAllBox: "#check_all",

    clickTr: false,
    clickedTrClass: "clicked",
    clickedTrChecked: false
};

})(jQuery);<commit_msg>Change all attr('checked') to prop('checked')<commit_after>;(function($) {

$.fn.extend({
});

$.TableTools = function(tbody, options) {
    var $tbody = $(tbody);
    options = $.extend({}, $.TableTools.defaults, options);
    var shiftDown = false;
    var shift1 = -1, shift2 = -1;

    // бɫ
    if (options.cross) {
        $tbody.children('tr:even').addClass(options.evenClass);
        $tbody.children('tr:odd').addClass(options.oddClass);
    }

    // 껬бɫ
    if (options.over) {
        $tbody.children('tr').mouseover( function() {
            $(this).addClass(options.overClass);
        }).mouseout( function() {
            $(this).removeClass(options.overClass);
        });
    }

    // checkboxѡӦбɫ
    if (options.selected) {
        var $boxs = $tbody.find(options.checkbox);
        $boxs.click(function(event) {
            var $box = $(this);
            if ($box.prop('checked')) {
                $box.parents('tr').removeClass(options.clickedTrClass).addClass(options.selectedClass);
                // ShiftѡĴ
                if (shiftDown) {
                    if (shift1 == -1) {
                        shift1 = $boxs.index($box);
                    } else if (shift2 == -1) {
                        shift2 = $boxs.index($box);
                        if (shift2 >= shift1) {
                            $boxs.slice(shift1, shift2).prop('checked', true).parents('tr').removeClass(options.clickedTrClass).addClass(options.selectedClass);
                        } else {
                            $boxs.slice(shift2, shift1).prop('checked', true).parents('tr').removeClass(options.clickedTrClass).addClass(options.selectedClass);
                        }
                    }
                }
            } else {
                $box.parents('tr').removeClass(options.clickedTrClass).removeClass(options.selectedClass);
            }
            event.stopPropagation();
        });
    }

    // ȫѡť
    if (options.checkAll) {
        $(options.checkAllBox).click(function() {
            var $boxs = $tbody.find(options.checkbox);
            var checked = $(this).prop('checked');
            $boxs.prop('checked', checked);
            if (options.selected) {
                if (checked) {
                    $boxs.parents('tr').removeClass(options.clickedTrClass).addClass(options.selectedClass);
                } else {
                    $boxs.parents('tr').removeClass(options.clickedTrClass).removeClass(options.selectedClass);
                }
            }
        });
    }

    // ѡѡcheckbox
    if (options.clickTr) {
        $tbody.find('tr').click(function() {
            var $clickedTr = $(this);
            if (options.clickedTrChecked) {
                if ($clickedTr.hasClass(options.selectedClass)) {
                    $clickedTr.removeClass(options.selectedClass).find(options.checkbox).prop('checked', false);
                } else {
                    $clickedTr.addClass(options.selectedClass).find(options.checkbox).prop('checked', true);
                }
            } else {
                if (!$clickedTr.hasClass(options.selectedClass)) {
                    if ($clickedTr.hasClass(options.clickedTrClass)) {
                        $clickedTr.removeClass(options.clickedTrClass);
                    } else {
                        $clickedTr.addClass(options.clickedTrClass);
                    }
                }
            }
        });
    }

    // Shiftť
    $(document).keydown(function(e){
        switch(e.keyCode){
            case 16:
                shiftDown = true;
                break;
            default:
                break;
        }
    }).keyup(function(e){
        switch(e.keyCode){
            case 16:
                shiftDown = false;
                shift1 = -1;
                shift2 = -1;
                break;
            default:
                break;
        }
    });
};

// Ĭ
$.TableTools.defaults = {
    cross: false,
    evenClass: "even",
    oddClass: "odd",

    over: false,
    overClass: "over",

    selected: false,
    selectedClass: "selected",
    checkbox: "td > input:checkbox",

    checkAll: false,
    checkAllBox: "#check_all",

    clickTr: false,
    clickedTrClass: "clicked",
    clickedTrChecked: false
};

})(jQuery);<|endoftext|>


<commit_before>!function(e){var n=function(e,n){var t=new XMLHttpRequest;t.open("GET",e,!0),t.responseType="arraybuffer",t.send(),t.onload=function(){var e=new Uint8Array(t.response),r=new Blob([e],{type:"video/webm"});n(r)}},t=function(e,n,t,r){var o=new FileReader;o.onload=function(n){e.timestampOffset=t,e.appendBuffer(new Uint8Array(n.target.result)),r&&r()},o.readAsArrayBuffer(n)},r=function(e,r,o,i){n(r,function(n){t(e,n,o,i)})},o=function(e,n,t){e.addEventListener("sourceopen",function(){var o=e.addSourceBuffer(t),i=n.shift(),u=i.source,a=i.timestampOffset;r(o,u,a,function(){o.addEventListener("updateend",function(){if(n.length>0){var t=n.shift(),i=t.source,u=t.timestampOffset;r(o,i,u)}else e.endOfStream()})})})},i=function(e,n,t){var r=new MediaSource;e.src=window.URL.createObjectURL(r),o(r,n,t)},u=function(n,t,r){n.prop("tagName")!==r&&e.error("Called $."+t+" on a non "+r+" tag.")},a=function(e,n,t,r){return u(n,e+"sequence",e.toUpperCase()),i(n[0],t,r),n};e.fn.videosequence=function(e){return a("video",this,e,'video/webm; codecs="vorbis,vp8"')},e.fn.audiosequence=function(e){return a("audio",this,e,'audio/webm; codecs="vorbis"')}}(jQuery);<commit_msg>Updated minified version<commit_after>!function(e){var n=function(e,n){var t=new XMLHttpRequest;t.open("GET",e,!0),t.responseType="arraybuffer",t.send(),t.onload=function(){var e=new Uint8Array(t.response),o=new Blob([e],{type:"video/webm"});n(o)}},t=function(e,n,t,o){var r=new FileReader;r.onload=function(n){e.timestampOffset=t,e.appendBuffer(new Uint8Array(n.target.result)),o&&o()},r.readAsArrayBuffer(n)},o=function(e,o,r,i){n(o,function(n){t(e,n,r,i)})},r=function(e,n,t){e.addEventListener("sourceopen",function(){var r=e.addSourceBuffer(t),i=n.shift(),u=i.source,a=i.timestampOffset;o(r,u,a,function(){r.addEventListener("updateend",function(){if(n.length>0){var t=n.shift(),i=t.source,u=t.timestampOffset;o(r,i,u)}else e.endOfStream()})})})},i=function(e,n,t){var o=new MediaSource;e.src=window.URL.createObjectURL(o),r(o,n,t)},u=function(n,t,o){n.prop("tagName")!==o&&e.error("Called $."+t+" on a non "+o+" tag.")},a=function(e,n,t){return JSON.parse(e.attr(n+t))},c=function(n,t){var o="data-"+n+"sequence-",r=a(t,o,"sources"),i=a(t,o,"offsets"),u=[];return e.each(r,function(e,n){var t=i[e];u.push({source:n,timestampOffset:t})}),u},s=function(e,n,t,o){return u(n,e+"sequence",e.toUpperCase()),i(n[0],t,o),n};e.fn.videosequence=function(e){return s("video",this,e,'video/webm; codecs="vorbis,vp8"')},e.fn.audiosequence=function(e){return s("audio",this,e,'audio/webm; codecs="vorbis"')};var f=function(n){var t=n+"sequence";e.each(e('[data-rel="'+n+'sequence"]'),function(o,r){var i=e(r),u=c(n,i);i[t].call(i,u)})},d=function(){f("video"),f("audio")};e(d),e(document).on("videosequence:initialize",d),e(document).on("audiosequence:initialize",d)}(jQuery);<|endoftext|>


<commit_before>/**
 * Backbone localStorage Adapter
 * Version 1.0
 *
 * https://github.com/jeromegn/Backbone.localStorage
 */
(function (root, factory) {
   if (typeof define === "function" && define.amd) {
      // AMD. Register as an anonymous module.
      define(["underscore","backbone"], function(_, Backbone) {
        // Use global variables if the locals is undefined.
        return factory(_ || root._, Backbone || root.Backbone);
      });
   } else {
      // RequireJS isn't being used. Assume underscore and backbone is loaded in <script> tags
      factory(_, Backbone);
   }
}(this, function(_, Backbone) {
// A simple module to replace `Backbone.sync` with *localStorage*-based
// persistence. Models are given GUIDS, and saved into a JSON object. Simple
// as that.

// Hold reference to Underscore.js and Backbone.js in the closure in order
// to make things work even if they are removed from the global namespace

// Generate four random hex digits.
function S4() {
   return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
};

// Generate a pseudo-GUID by concatenating random hexadecimal.
function guid() {
   return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
};

// Our Store is represented by a single JS object in *localStorage*. Create it
// with a meaningful name, like the name you'd give a table.
// window.Store is deprectated, use Backbone.LocalStorage instead
Backbone.LocalStorage = window.Store = function(name) {
  this.name = name;
  var store = this.localStorage().getItem(this.name);
  this.records = (store && store.split(",")) || [];
};

_.extend(Backbone.LocalStorage.prototype, {

  // Save the current state of the **Store** to *localStorage*.
  save: function() {
    this.localStorage().setItem(this.name, this.records.join(","));
  },

  // Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
  // have an id of it's own.
  create: function(model) {
    if (!model.id) {
      model.id = guid();
      model.set(model.idAttribute, model.id);
    }
    this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
    this.records.push(model.id.toString());
    this.save();
    return this.find(model);
  },

  // Update a model by replacing its copy in `this.data`.
  update: function(model) {
    this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
    if (!_.include(this.records, model.id.toString()))
      this.records.push(model.id.toString()); this.save();
    return this.find(model);
  },

  // Retrieve a model from `this.data` by id.
  find: function(model) {
    return this.jsonData(this.localStorage().getItem(this.name+"-"+model.id));
  },

  // Return the array of all models currently in storage.
  findAll: function() {
    return _(this.records).chain()
      .map(function(id){
        return this.jsonData(this.localStorage().getItem(this.name+"-"+id));
      }, this)
      .compact()
      .value();
  },

  // Delete a model from `this.data`, returning it.
  destroy: function(model) {
    if (model.isNew())
      return false
    this.localStorage().removeItem(this.name+"-"+model.id);
    this.records = _.reject(this.records, function(id){
      return id === model.id.toString();
    });
    this.save();
    return model;
  },

  localStorage: function() {
    return localStorage;
  },

  // fix for "illegal access" error on Android when JSON.parse is passed null
  jsonData: function (data) {
      return data && JSON.parse(data);
  }

});

// localSync delegate to the model or collection's
// *localStorage* property, which should be an instance of `Store`.
// window.Store.sync and Backbone.localSync is deprectated, use Backbone.LocalStorage.sync instead
Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) {
  var store = model.localStorage || model.collection.localStorage;

  var resp, errorMessage,
    // If $ and $ is having Deferred - use it.
    syncDfd = window.$ && $.Deferred && $.Deferred();

  try {

    switch (method) {
      case "read":
        resp = model.id != undefined ? store.find(model) : store.findAll();
        break;
      case "create":
        resp = store.create(model);
        break;
      case "update":
        resp = store.update(model);
        break;
      case "delete":
        resp = store.destroy(model);
        break;
    }

  } catch(error) {
    if (error.code === DOMException.QUOTA_EXCEEDED_ERR && window.localStorage.length === 0)
      errorMessage = "Private browsing is unsupported";
    else
      errorMessage = error.message;
  }

  if (resp) {
    if (options && options.success)
      if (Backbone.VERSION === "0.9.10") {
        options.success(model, resp, options);
      } else {
        options.success(resp);
      }
    if (syncDfd)
      syncDfd.resolve(resp);

  } else {
    errorMessage = errorMessage ? errorMessage
                                : "Record Not Found";

    if (options && options.error)
      if (Backbone.VERSION === "0.9.10") {
        options.error(model, errorMessage, options);
      } else {
        options.error(errorMessage);
      }

    if (syncDfd)
      syncDfd.reject(errorMessage);
  }

  // add compatibility with $.ajax
  // always execute callback for success and error
  if (options && options.complete) options.complete(resp);

  return syncDfd && syncDfd.promise();
};

Backbone.ajaxSync = Backbone.sync;

Backbone.getSyncMethod = function(model) {
  if(model.localStorage || (model.collection && model.collection.localStorage)) {
    return Backbone.localSync;
  }

  return Backbone.ajaxSync;
};

// Override 'Backbone.sync' to default to localSync,
// the original 'Backbone.sync' is still available in 'Backbone.ajaxSync'
Backbone.sync = function(method, model, options) {
  return Backbone.getSyncMethod(model).apply(this, [method, model, options]);
};

return Backbone.LocalStorage;
}));<commit_msg>Remove unused JS file that's now in Bower<commit_after><|endoftext|>


<commit_before>( function() {
    /*global RealtimeUpdate: false, BrowserNotifications: false*/
    "use strict";

    if ( !( "Notification" in window ) || typeof( RealtimeUpdate ) === "undefined" || BrowserNotifications.enabled === false ) {
        return;
    }

    var gs_makeNoticeItem = RealtimeUpdate.makeNoticeItem, // Monkey-patch
        showNotification,
        isMention;

    // Ask for notification permissions on page load
    Notification.requestPermission();

    showNotification = function( node ) {
        /* Request permission again if previous requests were missed/ignored
           If request was previouly granted, it doesn't ask again; it just shows
           If request was previouly denied, nothing happens */
        Notification.requestPermission( function( permission ) {
            if ( permission === "granted" ) {
                var $node = $( node ),
                    noticeText = $.trim( $node.find( ".e-content" ).text() ),
                    isRepeat = $node.find( ".repeat" ).length > 0,
                    author,
                    repeater,
                    title;

                if ( isRepeat ) {
                    author = $.trim( $node.find( ".notice-headers .h-card" ).text() );
                    repeater = $.trim( $node.find( ".repeat .p-author" ).text() );
                    title = repeater + " repeated a notice by " + author;
                } else {
                    author = $.trim( $node.find( ".notice-headers .p-author" ).text() );
                    title = "New notice from " + author;
                }

                new Notification( title, { body: noticeText } );
            }
        } );
    };

    isMention = function( /* node */ ) {
        /* TODO */

        return true;
    };

    RealtimeUpdate.makeNoticeItem = function( data, callback ) {
        /* Monkey-patch */
        var gs_callback = callback;

        // Call the original "makeNoticeItem", but with our own callback
        gs_makeNoticeItem( data, function( node ) {
            /* Call the original callback */
            gs_callback( node );

            if ( BrowserNotifications.mentions_only === true ) {
                if ( isMention( node ) === true ) {
                    showNotification( node );
                }
            } else {
                showNotification( node );
            }
        } );
    };
}() );
<commit_msg>Discard reply h-cards when getting author info<commit_after>( function() {
    /*global RealtimeUpdate: false, BrowserNotifications: false*/
    "use strict";

    if ( !( "Notification" in window ) || typeof( RealtimeUpdate ) === "undefined" || BrowserNotifications.enabled === false ) {
        return;
    }

    var gs_makeNoticeItem = RealtimeUpdate.makeNoticeItem, // Monkey-patch
        showNotification,
        isMention;

    // Ask for notification permissions on page load
    Notification.requestPermission();

    showNotification = function( node ) {
        /* Request permission again if previous requests were missed/ignored
           If request was previouly granted, it doesn't ask again; it just shows
           If request was previouly denied, nothing happens */
        Notification.requestPermission( function( permission ) {
            if ( permission === "granted" ) {
                var $node = $( node ),
                    noticeText = $.trim( $node.find( ".e-content" ).text() ),
                    isRepeat = $node.find( ".repeat" ).length > 0,
                    author,
                    repeater,
                    title;

                if ( isRepeat ) {
                    author = $.trim( $node.find( ".notice-headers .h-card" ).first().text() );
                    repeater = $.trim( $node.find( ".repeat .p-author" ).text() );
                    title = repeater + " repeated a notice by " + author;
                } else {
                    author = $.trim( $node.find( ".notice-headers .p-author" ).text() );
                    title = "New notice from " + author;
                }

                new Notification( title, { body: noticeText } );
            }
        } );
    };

    isMention = function( /* node */ ) {
        /* TODO */

        return true;
    };

    RealtimeUpdate.makeNoticeItem = function( data, callback ) {
        /* Monkey-patch */
        var gs_callback = callback;

        // Call the original "makeNoticeItem", but with our own callback
        gs_makeNoticeItem( data, function( node ) {
            /* Call the original callback */
            gs_callback( node );

            if ( BrowserNotifications.mentions_only === true ) {
                if ( isMention( node ) === true ) {
                    showNotification( node );
                }
            } else {
                showNotification( node );
            }
        } );
    };
}() );
<|endoftext|>


<commit_before>var _ = require('lodash');
var Reflux = require('reflux');
var cardDataActions = require('./actions').cardData;

// I could stick this in a config file or something but it's just as easy for
// me to change this as anything else.
// Might as well keep this value in line with Solforge card releases.
var cardDataVersion = '6.1.1';

var _dictionaryState = {
  cardList: undefined,
  cardDict: undefined,
  state: 'loading'
}

var loadDataFromServer = function() {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', '/data/cards.json');
  xhr.responseType = 'json';
  xhr.setRequestHeader('Accept', 'application/json, text/javascript, */*; q=0.01');
  xhr.setRequestHeader('Content-Type', 'application/json');

  xhr.onreadystatechange = function(xhrEvent) {
    if (xhr.readyState === 4)  { // Done
      var success = xhr.status < 400;
      if (success) {
        var data = xhr.response;

        // IE keeps the data as a string, unline Chrome and FF, who pay attention
        // to xhr.responseType.
        if (typeof data === 'string') {
          data = JSON.parse(data);
        }

        localStorage.setItem('solforgeCardData', JSON.stringify(data));
        localStorage.setItem('solforgeCardDataVersion', cardDataVersion);

        cardDataActions.loaded(xhr.response);
      } else {
        cardDataActions.failedToLoad();
      }
    }
  };

  xhr.send();
}

var loadDataFromLocalStorage = function() {
  if (!window.localStorage) {
    return false;
  }

  var localVersion = localStorage.getItem('solforgeCardDataVersion');
  if (localVersion === cardDataVersion) {
    cardDataActions.loaded(JSON.parse(localStorage.getItem('solforgeCardData')));
    return true;
  } else {
    return false;
  }
};

var loadData = function() {
  var loadedFromLocalStorage = loadDataFromLocalStorage();
  if (!loadedFromLocalStorage) {
    loadDataFromServer();
  }
};

var CardDictionary = Reflux.createStore({
  listenables: cardDataActions,

  onLoaded: function(cards) {
    _dictionaryState.cardList = cards;
    _dictionaryState.cardDict = _.reduce(cards, function(dict, card) {
      dict[card.cardId] = card;
      return dict;
    }, {});
    _dictionaryState.state = 'loaded';
    this.trigger(_dictionaryState);
  },

  onFailedToLoad: function() {
    _dictionaryState.cardList = undefined;
    _dictionaryState.cardDict = undefined;
    _dictionaryState.state = 'failed';
    this.trigger(_dictionaryState);
  },

  getInitialState: function() {
    return _dictionaryState;
  },

  getDictionary: function() {
    return _dictionaryState.cardDict;
  },

  getList: function() {
    return _dictionaryState.cardList;
  },

  getBaseLevel: function(card) {
    return _dictionaryState.cardDict[card.cardId].levels[card.level - 1];
  }
});

loadData();

module.exports = CardDictionary;
<commit_msg>Bump data version for new cards.<commit_after>var _ = require('lodash');
var Reflux = require('reflux');
var cardDataActions = require('./actions').cardData;

// I could stick this in a config file or something but it's just as easy for
// me to change this as anything else.
// Might as well keep this value in line with Solforge card releases.
var cardDataVersion = '6.2';

var _dictionaryState = {
  cardList: undefined,
  cardDict: undefined,
  state: 'loading'
}

var loadDataFromServer = function() {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', '/data/cards.json');
  xhr.responseType = 'json';
  xhr.setRequestHeader('Accept', 'application/json, text/javascript, */*; q=0.01');
  xhr.setRequestHeader('Content-Type', 'application/json');

  xhr.onreadystatechange = function(xhrEvent) {
    if (xhr.readyState === 4)  { // Done
      var success = xhr.status < 400;
      if (success) {
        var data = xhr.response;

        // IE keeps the data as a string, unline Chrome and FF, who pay attention
        // to xhr.responseType.
        if (typeof data === 'string') {
          data = JSON.parse(data);
        }

        localStorage.setItem('solforgeCardData', JSON.stringify(data));
        localStorage.setItem('solforgeCardDataVersion', cardDataVersion);

        cardDataActions.loaded(xhr.response);
      } else {
        cardDataActions.failedToLoad();
      }
    }
  };

  xhr.send();
}

var loadDataFromLocalStorage = function() {
  if (!window.localStorage) {
    return false;
  }

  var localVersion = localStorage.getItem('solforgeCardDataVersion');
  if (localVersion === cardDataVersion) {
    cardDataActions.loaded(JSON.parse(localStorage.getItem('solforgeCardData')));
    return true;
  } else {
    return false;
  }
};

var loadData = function() {
  var loadedFromLocalStorage = loadDataFromLocalStorage();
  if (!loadedFromLocalStorage) {
    loadDataFromServer();
  }
};

var CardDictionary = Reflux.createStore({
  listenables: cardDataActions,

  onLoaded: function(cards) {
    _dictionaryState.cardList = cards;
    _dictionaryState.cardDict = _.reduce(cards, function(dict, card) {
      dict[card.cardId] = card;
      return dict;
    }, {});
    _dictionaryState.state = 'loaded';
    this.trigger(_dictionaryState);
  },

  onFailedToLoad: function() {
    _dictionaryState.cardList = undefined;
    _dictionaryState.cardDict = undefined;
    _dictionaryState.state = 'failed';
    this.trigger(_dictionaryState);
  },

  getInitialState: function() {
    return _dictionaryState;
  },

  getDictionary: function() {
    return _dictionaryState.cardDict;
  },

  getList: function() {
    return _dictionaryState.cardList;
  },

  getBaseLevel: function(card) {
    return _dictionaryState.cardDict[card.cardId].levels[card.level - 1];
  }
});

loadData();

module.exports = CardDictionary;
<|endoftext|>


<commit_before>/**
 * cbpAnimatedHeader.min.js v1.0.0
 * http://www.codrops.com
 *
 * Licensed under the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 *
 * Copyright 2013, Codrops
 * http://www.codrops.com
 */
var cbpAnimatedHeader=(function(){var b=document.documentElement,g=document.querySelector(".cbp-af-header"),e=false,a=300;function f(){window.addEventListener("scroll",function(h){if(!e){e=true;setTimeout(d,250)}},false)}function d(){var h=c();if(h>=a){classie.add(g,"cbp-af-header-shrink")}else{classie.remove(g,"cbp-af-header-shrink")}e=false}function c(){return window.pageYOffset||b.scrollTop}f()})();<commit_msg>Delete cbpAnimatedHeader.min.js<commit_after><|endoftext|>