Link to home
Start Free TrialLog in
Avatar of Bruce Gust
Bruce GustFlag for United States of America

asked on

How do I make this update method work?

Here's what I'm trying to do:

I need to update a particular row in a collection.

The Activity collection looks like this:

User generated image
The current "add-activity" route looks like this (and this works):

router.post('/companies/add-activity', async (req, res) => {
    // make sure the request has a type
    if (typeof req.body.type == 'undefined' || req.body.type === '') {
        return res.send({ error: true, msg: 'Invalid or missing service/object type. Please refresh the page and try again.' });
    } else if (typeof req.body.activityType == 'undefined' || req.body.activityType === '') {
        req.body.activityType = req.body.type;
        //return res.send({ error: true, msg: 'Invalid or missing service/object type. Please refresh the page and try again.' });
    } else if (typeof req.body.company == 'undefined' || req.body.company === '') {
        return res.send({ error: true, msg: 'Invalid or missing company ID. Please refresh the page and try again.' });
    }

    // set the activity type as the main type - I added this in after a request to
    // combine several types
    req.body.type = req.body.activityType;

    // validate the content based on the type
    try {
        if (Array.isArray(req.body.company)) {
            let ids = [];
            //var companies = new Set(req.body.company); 
            var companies = req.body.company.filter(function (elem, pos) { return req.body.company.indexOf(elem) == pos; });// Remove duplicates
            var counter = companies.length;
            for (var i = 0; i < companies.length; i++) {
                try {
                    counter -= 1;
                    let _companyId = companies[i];
                    if (_companyId == 'bulk') {
                        ids.push("_0");
                        if (counter === 0) {
                            let topId = ids[0];
                            res.send(Company.success({ id: companies[0] }));
                        }
                    } else {
                        let id = await Company.addActivity(_companyId, req.body.type, req.body);
                        //let data = { id: id, company: bdata.company };
                        ids.push(id);
                        if (counter === 0) {
                            let topId = ids[0];
                            res.send(Company.success({ id: companies[0] }));
                        }
                    }
                } catch (e) {
                    console.log(e);
                }
            }
        } else {
            let id = await Company.addActivity(req.body.company, req.body.type, req.body);
            res.send(Company.success({ id }));
        }

    } catch (err) {
        res.send(Company.error(err));
    }
});

Open in new window


It's hitting the "Company.addActivity" method and that looks like this:

addActivity(companyId, type, data) {
        return new Promise(async(resolve, reject) => {
            if (typeof type != 'string' || ['call', 'visit', 'reminder', 'event'].indexOf(type.trim()) == -1) {
                return reject('Invalid type provided');
            }
            const Schema = mongoose.Schema;
            const Base = mongoose.model('Activity', ActivitySchema);
            // to track activity on each pipeline stage
            if (data.proposal != 'undefined') {
                if (data.proposal != 'bulk') {
                    if (data.proposal) {
                        await Proposal.findById(this._getId(data.proposal), (err, res) => {
                            if (err) { console.log(err); }
                            if (res) {
                                data.pipeline = res.pipeline;
                            }
                        });
                    }
                }
            }
                       
            if (type == 'event') {
                // define dynamic Schema
                const Event = Base.discriminator('event', new mongoose.Schema({meta: {start: Date,end: Date,subject: String,participants: String,location: String,details: String}}));
                let _data = {
                    company: new ObjectId(companyId),
                    type: type.trim(),
                    meta: {
                        start: data.start,
                        end: data.end,
                        subject: data.subject,
                        participants: data.participants,
                        location: data.location,
                        details: data.details
                    },
                    user: global._user._id,
                    account: global._user.account._id
                };
                if (data.proposal != 'undefined') {
                    if (data.proposal != 'bulk') {
                        if (data.proposal) {
                            _data.proposal = data.proposal;
                            _data.pipeline = data.pipeline;
                        }
                    }
                }
                Event.create(_data, (err, record) => {
                    if (err) {
                        console.log('Error: ', err);
                        reject('An unexpected error was encountered. Please try again.');
                    } else {
                        resolve(record._id);
                    }
                });
            } else {
                // define dynamic Schema
                //FIX: descriminator with name 'call' already exists.
                const ActivityModel = Base.discriminators ? Object.keys(Base.discriminators).indexOf(type.trim()) >= 0 ? Base.discriminators[type.trim()] : Base.discriminator(type.trim(), new mongoose.Schema({ meta: { date: Date, notes: String } })) : Base.discriminator(type.trim(), new mongoose.Schema({ meta: { date: Date, notes: String } })); 
                //const ActivityModel = Base.discriminator(type.trim(), new mongoose.Schema({ meta: { date: Date, notes: String } })); 
                let _data = {
                    company: new ObjectId(companyId),
                    type: type.trim(),
                    meta: {
                        date: data.date,
                        notes: data.notes
                    },
                    user: global._user._id,
                    account: global._user.account._id
                };
                if (data.proposal != 'undefined') {
                    if (data.proposal != 'bulk') {
                        if (data.proposal) {
                            _data.proposal = data.proposal; 
                            _data.pipeline = data.pipeline;
                        }
                    }
                }
                ActivityModel.create(_data, (err, record) => {
                    if (err) {
                        console.log('Error: ', err);
                        reject('An unexpected error was encountered. Please try again.');
                    } else {
                        resolve(record._id);
                    }
                });
            }
        });
    }

Open in new window


Now, "addActivity" works fine. I don't need to "add" anything, however. Rather I need to "update" an already existing record.

The form is sound. I'm using "formit.js" and the fields are all intact:

this.displayActivityForm = function (companyId, activityId) {
			//let url = `/companies/activities/${activityId}`;
			//console.log("DisplayActivityForm", url);
		 	$.get('/companies/activities/' +activityId, function(resp) {
			    if (resp.error) {
                    CAMS.alert('Error', resp.msg, 'error');
                    return false;
                }
			let type = resp.type;
			let notes = resp.meta.notes;
			let date = moment(resp.meta.date).format("MM/DD/YYYY h:mm:s A");
		
			var model = objectModels[type],
				fc = model.singular.substring(0, 1),
				config = {
					title: 'Edit a' + ((fc == 'a' || fc == 'e' || fc == 'i' || fc == 'o' || fc == 'u') ? 'n' : '') + ' ' + ucwords(model.singular),
					directionsText: 'Make your changes below and then click, "SAVE."',
					fields: model.fields,
					onShow: function (self) {
						// add in the additional events and plugins
						// add in datetime support
						self.$el.find('input.field-datetime').datepicker({
							autoClose: true,
							language: 'en',
							timepicker: true,
							dateFormat: 'mm/dd/yyyy',
							timeFormat: 'h:ii AA',
							showEvent: 'focus',
							minutesStep: 5
						});
						self.$el.find('#formit-notes').val(notes);
						self.$el.find('#formit-date').val(date);
						self.$el.find('select.form-control.displayType').val(type);

					},
					ajax: {
						path: '/companies/add-activity',
						params: {
							notes: $('#formit-notes').val(),
							date: $('#formit-date').val(),
							type: $('#formit-activityType').val(),
							company: companyId,
							activityId: activityId
						},
						onComplete: function (resp, self) {
							if (resp.error) {
								self.addMsg(resp.msg);
								return false;
							}
							companyId = (companyId == 'bulk' ? resp.data.id : companyId);
							recordId = companyId;
							$(window).trigger('open-company-details', [companyId, ((typeof route != 'undefined') ? route : '/companies/'), 'history']);
						}
					}
				};

			// show the form
			formIt.show(config);
		});
    };

Open in new window


When I attempt to update the row, I'm operating under the assumption, based on a tutorial that I'm working through, that the "save" method works in that the system will automatically recognize an already existing Id and "save" what amounts to changes to a record rather than adding a new one.

When I attempt to run my update process, I get this:

An unexpected error was encountered. Please try again.

This is coming from line #81 of the "addActivity" method.

I don't know why and I'm not ever certain if my understanding of all this is correct, as far as believing that a "save" method automatically kicks into an "update" mode if the ID in question already exists.

Where am I blowing it?
Avatar of Chris Stanyon
Chris Stanyon
Flag of United Kingdom of Great Britain and Northern Ireland image

Hey Bruce,

There's nothing in your addActivity method that caters for updating an existing model - it does what it says on the tin - it adds an Activity. When you click on Save, it makes an AJAX request to /companies/add-activity and sends the data - that route calls the addActivity on your Company class, which in turn calls the ActivityModel.create() method with the data.

So .... you're going to have to add/edit code to take into account an update. Potentially, several ways to do this, so you'll need to do some digging. Probably the preferred method, to keep things well structured is to add a new route, specifically for updating an existing Activity. If you try and edit what you currently have, you'll end up causing more confusion (an addActivity method that may add a new Activity, or it may updated an existing one). You also run the risk of adding in a lot of logic to decide whether you're running and update or an insert.

Take a look at your ActivityModel and see if there's an update/save method (either on that class or a parent, service class). If there is, then ultimately that's what need calling.
Avatar of Bruce Gust

ASKER

Hey, Chris!

We're in brand new territory here as the original developer didn't anticipate any need to edit an activity, so here's what I've got on the current activity.js page:

const { ObjectId } = require('mongodb');
const { Util } = require('node-utils');
const moment = require('moment-timezone');
const Service = require('./service');

const mongoose = require('mongoose');
const ProposalSchema = require('../schemas/ProposalSchema');
const CompanySchema = require('../schemas/CompanySchema');
const UserSchema = require('../schemas/UserSchema');
const ActivitySchema = require('../schemas/ActivitySchema');

class Activity extends Service {

    constructor() {
        super();

        this.collection = 'activities';
        this.plural = 'activities';
        this.singular = 'activity';
        this.model = mongoose.model('Activity', ActivitySchema);        
        //this.fields = require('../lib/model-proposal');
    }  
   
      getOne(activityId) {
         return new Promise((resolve, reject) => {
          // console.log(activityId);
         let activity_Id = this._getId(activityId);
            this.model.findOne({ _id: activity_Id }, (err, res) => {
                if (err) { console.log(err); reject(err); }
                resolve(res);
            });
        });
    }
}

module.exports = new Activity();

Yest, the only method we've got on there is the one you helped me create, as far as displaying activity info on the new pop up that was built.

Right now, the only reference to the Activity model, as far as adding info is on the "companies.js" route page. I've got that in full below. As far as any code that deals specifically with the Activity collection on the activity.js service page, there isn't anything.

Smell that?

The aroma of new code...

const { ObjectId } = require('mongodb');


const express = require('express');
const router = express.Router();
const twig = require('twig');
const moment = require('moment-timezone');
const fs = require('fs');
const { Util } = require('node-utils');


const Company = require('../services/company.js');
const CompanyContact = require('../services/companycontact.js');
const CompanyAddress = require('../services/companyaddress.js');
const Proposal = require('../services/proposal.js');
const Activity = require('../services/activity.js');


const Prospect = require('../services/prospect.js');
const ServiceModel = require('../services/servicemodel');
const AccountManagement = require('../services/account-management');
const GeoLocations = require('../services/GeoLocations');


// set the default variables we'll be passing to the template engine for ease
// of use.
let base = {
    selected: '/companies',
    error: false,
    msg: '',
    data: {}
};


router.post('/companies/ignore', async (req, res) => {
    if (typeof req.body.companies == 'undefined' || req.body.companies.length === 0) {
        return res.send(Prospect.error('Please provide one or more company ID\'s.'));
    }
    try {
        let deleted = await Company.ignoreTargetSelected(req.body.companies);
        res.send(Prospect.success(deleted));
    } catch (e) {
        res.send(Prospect.error('An unexpected error was encountered.'));
    }  
});


router.post('/companies/target', async (req, res) => {
    if ((typeof req.body.companies == 'undefined' || req.body.companies.length === 0) && (typeof req.body.user == 'undefined')) {
        return res.send(Prospect.error('Please provide one or more company ID\'s.'));
    }
    try {
        let _target = await Company.targetSelected(req.body.companies, req.body.user);


        res.send(Prospect.success());
    } catch (e) {
        res.send(Prospect.error('An unexpected error was encountered.'));
    }  
});




// get the full list/index view
router.get('/companies', async (req, res) => {
    let vars = Object.assign({}, base);


   //I made a little change here. vars.service_models, in my opinion needs to be called once and only after it's been conclusively determined that there's data in place to be retrieved. So I established "brucesters" as a var to hold the results of the "getList()" function.
   // If it's empty, then you seed the appropriate collection. If not, you simply move forward to the "vars.service_models = await ServiceModel.getList) function and all is well. What you see commented out below is the original code
   vars.service_models = await ServiceModel.getList();
    //Seed data
    if (vars.service_models.length == 0) {
        let defaultServiceModels = [{ key: 'perFte', name: 'Per FTE' }, { key: 'oneTime', name: 'One Time' }, { key: 'perPerson', name: 'Per Person' }, { key: 'perMonth', name: 'Per Month' }, { key: 'perQuarter', name: 'Per Quarter' }, { key: 'semiAnnually', name: 'Semiannually' }, { key: 'perYear', name: 'Per Year' }];
        defaultServiceModels.forEach(async (item) => {
            let _id = await ServiceModel.save('new', item);
        });
        service_models = await ServiceModel.getList();
    }
 
   //console.log(vars.service_models);
    vars.pipelineColumns = await Company.getPipelineColumns();
    let resolveDatesArray = await Company.resolveDatesArray();
    res.render('companies.html.twig', vars);
});


router.post('/companies/datatable', async (req, res) => {
    let resp = await Company.datatable(req.body, { pipeline: { $ne: '' } });
   //console.log(resp);
    res.send(resp);
});
router.post('/companies/heatmap', async (req, res) => {
    try {
        let resp = await Company.heatmap(req.body.ids);
        res.send(Company.success(resp));
    } catch (err) {
        res.status(500).send(Company.error(err));
    }
});
router.post('/companies/heatmap/click/draw', async (req, res) => {
    try {
        let resp = await GeoLocations.heatmapdraw(req.body, 'companies');
        res.send(Company.success(resp));
    } catch (err) {
        console.log(err);
        res.status(500).send(Company.error(err));
    }
});
router.get('/companies/map/:id?', async (req, res) => {
    let vars = Object.assign({}, base);
    vars.service_models = await ServiceModel.getList();
    vars.pipelineColumns = await Company.getPipelineColumns();
    if (typeof req.params.id != 'undefined' || req.params.id != '') {
        vars._id = req.params.id;
    }
    res.render('companies-map.html.twig', vars);
});
router.post('/companies/map/:id?', async (req, res) => {
    try {
        var pipeline_filters = {};
        pipeline_filters.$or = [{ active: { $exists: false } }, { active: true }];
        if (typeof req.params.id != 'undefined') {
            req.body.filters = typeof req.body.filters == 'undefined' ? [] : req.body.filters;
            req.body.filters.push({ "field": "_id", "op": "$eq", "val": Company._getId(req.params.id) });
        }
        let resp = await Company.datatable(req.body, pipeline_filters);
        res.send(resp);
    } catch (err) {
        console.log('Datatables Error: ', err);
        res.status(500).send(Company.error(err));
    }
});


router.get('/companies/select2', async (req, res) => {
    try {
        let data = await Company.select2(req.query.q.term);


        res.send(data);
    } catch (err) {
        res.status(500).send({});
    }
});


router.get('/companies/saved-filters', async (req, res) => {
    try {
        var data = await Company.getSavedFilters();


        res.send(data);
    } catch (err) {
        console.log('Error: ', err);
        res.status(500).send(Company.error());
    }
});


router.post('/companies/save-filters', async (req, res) => {
    try {
        let id = await Company.saveFilter(req.body.id, {
            type: 'companies',
            name: req.body.name,
            filters: req.body.filters,
            account: global._user.account._id,
            active: true,
            owner: global._user._id
        });


        res.send(Company.success({
            id: id
        }));
    } catch (err) {
        console.log('Error: ', err);
        res.send(Company.error('An unexpected error was encountered. Please try again.'));
    }
});


router.post('/companies/delete-saved-filter', async (req, res) => {
    try {
        await Company.deleteSavedFilter(req.body.id);


        res.send(Company.success());
    } catch (err) {
        res.status(500).send(Company.error());
    }
});


// bulk assign companies to a single user - must come before the edit route
router.post('/companies/assign', async (req, res) => {
    let vars = Object.assign({}, base);


    if (typeof req.body.user == 'undefined' || typeof req.body.companies == 'undefined') {
        return res.send(Company.error('Invalid or missing parameters.'));
    } else if (typeof req.body.companies == 'string') {
        req.body.companies = [req.body.companies];
    }


    try {
        let resp = await Company.assign(req.body.companies, req.body.user);
    } catch (err) {
        console.log('Error: ', err);
        return res.send(Company.error('Invalid parameters provided.'));
    }


    res.send(vars);
});


router.post('/companies/unassign', (req, res) => {
    if (req.body.companies == 'undefined') {
        return res.send(Company.error('Invalid or missing parameters.'));
    } else if (typeof req.body.companies == 'string') {
        req.body.companies = [req.body.companies];
    }


    try {
        let success = Company.unassign(req.body.companies);


        res.send(Company.success());
    } catch (err) {
        res.send(Company.error('An unexpected error was encountered while trying to unassign the user associated with the provided companies. Please try again.'));
    }
});


router.post('/companies/add-activity', async (req, res) => {
    // make sure the request has a type
    if (typeof req.body.type == 'undefined' || req.body.type === '') {
        return res.send({ error: true, msg: 'Invalid or missing service/object type. Please refresh the page and try again.' });
    } else if (typeof req.body.activityType == 'undefined' || req.body.activityType === '') {
        req.body.activityType = req.body.type;
        //return res.send({ error: true, msg: 'Invalid or missing service/object type. Please refresh the page and try again.' });
    } else if (typeof req.body.company == 'undefined' || req.body.company === '') {
        return res.send({ error: true, msg: 'Invalid or missing company ID. Please refresh the page and try again.' });
    }


    // set the activity type as the main type - I added this in after a request to
    // combine several types
    req.body.type = req.body.activityType;


    // validate the content based on the type
    try {
        if (Array.isArray(req.body.company)) {
            let ids = [];
            //var companies = new Set(req.body.company);
            var companies = req.body.company.filter(function (elem, pos) { return req.body.company.indexOf(elem) == pos; });// Remove duplicates
            var counter = companies.length;
            for (var i = 0; i < companies.length; i++) {
                try {
                    counter -= 1;
                    let _companyId = companies[i];
                    if (_companyId == 'bulk') {
                        ids.push("_0");
                        if (counter === 0) {
                            let topId = ids[0];
                            res.send(Company.success({ id: companies[0] }));
                        }
                    } else {
                        let id = await Company.addActivity(_companyId, req.body.type, req.body);
                        //let data = { id: id, company: bdata.company };
                        ids.push(id);
                        if (counter === 0) {
                            let topId = ids[0];
                            res.send(Company.success({ id: companies[0] }));
                        }
                    }
                } catch (e) {
                    console.log(e);
                }
            }
        } else {
            let id = await Company.addActivity(req.body.company, req.body.type, req.body);
            res.send(Company.success({ id }));
        }


    } catch (err) {
        res.send(Company.error(err));
    }
});


// add an address or contact to a company record
router.post('/companies/:orgId/:type(contacts|addresses|brokers)/:id', async (req, res) => {
    // make sure the request has a type
    if (typeof req.params.type == 'undefined' || req.params.type === '') {
        return res.send({ error: true, msg: 'Invalid or missing service/object type. Please refresh the page and try again.' });
    } else if (typeof req.params.orgId == 'undefined' || req.params.orgId === '') {
        return res.send({ error: true, msg: 'Invalid or missing company ID. Please refresh the page and try again.' });
    }


    let resp;


    // enforce some scoping
    req.body.account = global._user.account._id;
    req.body.company = Company._getId(req.params.orgId);


    // validate the content based on the type
    try {
        let oldRec = {}, newRec = {}, meta = {};
        switch (req.params.type) {
            case 'contacts':
                if (req.params.id != 'new') { oldRec = await Company.getOneContact(req.params.id, req.params.orgId); }
                resp = await Company.saveContact(req.params.id, req.body);
                //newRec = await resp;
                meta = Company.jsonDiff(oldRec, resp);
                Company.addHistory((req.params.id == 'new' ? 'Contact added' : 'Contact updated'), req.params.orgId, '', global._user._id, meta);
                break;


            case 'addresses':
                if (req.params.id != 'new') { oldRec = await Company.getOneAddress(req.params.id, req.params.orgId); }
                resp = await Company.saveAddress(req.params.id, req.body);                
                meta = Company.jsonDiff(oldRec, resp);
                Company.addHistory((req.params.id == 'new' ? 'Address added' : 'Address updated'), req.params.orgId, '', global._user._id, meta);
                break;
            case 'brokers':
                if (req.params.id != 'new') { oldRec = await Company.getOneBroker(req.params.id, req.params.orgId); }
                resp = await Company.saveBroker(req.params.id, req.body);
                delete req.body.company;
                delete req.body.account;
                newRec = req.body;//await Company.getOneBroker(req.params.id, req.params.orgId);
                meta = Company.jsonDiff(oldRec, newRec);
                Company.addHistory((req.params.id == 'new' ? 'Broker info added' : 'Broker info updated'), req.params.orgId, '', global._user._id, meta);
                break;


            default:
                return res.send(Company.error('Invalid type provided.'));
                break;
        }
    } catch (err) {
        console.log('Save Contacts/Addresses/Brokers Error: ', err);


        // if the user is having issues with an invalid address from google, we need to let them know
        let msg = (typeof err == 'string' && err.indexOf('Google') > -1) ? err : 'An unexpected error was encountered.';


        return res.send(Company.error(msg));
    }


    res.send(resp);
});


router.get('/companies/:orgId/:type(contacts|addresses|brokers)/:id', async (req, res) => {
    try {
        let data = null;


        switch (req.params.type) {
            case 'contacts':
                data = await Company.getOneContact(req.params.id, req.params.orgId);//await CompanyContact.getOne(req.params.id);
                break;


            case 'addresses':
                data = await Company.getOneAddress(req.params.id, req.params.orgId);//await CompanyAddress.getOne(req.params.id);
                break;
            case 'brokers':
                data = await Company.getOneBroker(req.params.id, req.params.orgId);
                break;


            default:
                return res.status(500).send(Company.error('Invalid type provided.'));
                break;
        }
   
        //res.send(Company.success(data));
    } catch (err) {
        console.log('Get Contact/Address/Brokers Error: ', err);
        res.status(500).send(Company.error());
    }
});


router.get('/companies/:orgId/:type(contacts|addresses)/delete/:id', async (req, res) => {
   console.log("post");
    try {
        switch (req.params.type) {
            case 'contacts':
                await CompanyContact.deleteItem(req.params.id);
                break;


            case 'addresses':
                await CompanyAddress.deleteItem(req.params.id);
                break;


            default:
                return res.status(500).send(Company.error('Invalid type provided.'));
                break;
        }


        res.send(Company.success());
    } catch (err) {
        console.log('Delete Contact/Address Error: ', err);
        res.status(500).send(Company.error());
    }
});


router.post('/companies/export', async (req, res) => {
    try {
        let rows = await Company.exportToCSV(req.body);


        res.send(Company.success(rows));
    } catch (err) {
        console.log('Export Error: ', err);
        res.status(500).send(Company.error());
    }
});


router.get('/companies/proposals/:proposalId/:route', async (req, res) => {
    if (typeof req.params.proposalId == 'undefined') {
        return res.send(Company.error('It looks like your request was improperly defined. Please try again.'));
    }


    try {
        var paths = [
            { path: 'owner', model: 'User' },
            { path: 'company', model: 'Company' },
            { path: 'company.addresses', model: 'CompanyAddress' },
            { path: 'company.contacts', model: 'CompanyContact' },
            { path: 'services.service', model: 'Service' }
        ];


        if (typeof req.params.route != 'undefined') {
            if (req.params.route == 'market') {
                paths[1] = { path: 'prospect', model: 'prospect' };
                //paths.push({ path: 'prospect', model: 'prospect' });
                let proposal = await Proposal.getOne(req.params.proposalId, paths);
                if (paths[1].path == 'prospect') {
                    if (proposal) {
                        var prospect_proposal = {
                            _id: proposal._id,
                            name: proposal.name,
                            pipelineIndex: proposal.pipelineIndex,
                            _pipelineDate: proposal._pipelineDate,
                            pipeline: proposal.pipeline,
                            company: await Prospect.getOne(proposal.company),
                            owner: proposal.owner,
                            dates: proposal.dates,
                            _range: proposal._range,
                            files: proposal.files,
                            services: proposal.services
                        };
                    }
                    return res.send(Proposal.success(prospect_proposal));
                }
            }
        }
        let proposal = await Proposal.getOne(req.params.proposalId);        
        res.send(Proposal.success(proposal));
    } catch (err) {
        res.send(Proposal.error('An unexpected error was encountered while trying to load your proposal. Please try again.' + '\nDetails:' + JSON.stringify(err)));
    }
});


router.get('/companies/proposal/pdf/:proposalId/', async (req, res) => {
    if (typeof req.params.proposalId == 'undefined') {
        return res.send(Company.error('It looks like your request was improperly defined. Please try again.'));
    }


    try {        
        let proposal = await Proposal.getOne(req.params.proposalId);        
        proposal.account = await AccountManagement.getOne(proposal.account);
       
        proposal.value = 0;
        proposal.services.forEach((service, index) => {
            let price = service.amount || service.price;
            if (service.model === 'perFte' || service.model === 'oneTime' || service.model ==='perPerson') {                
                proposal.services[index].total = parseFloat(price) * service.numOfEmp;
            } else {
                proposal.services[index].total = parseFloat(price) * service.numOfEmp * service.duration;
            }
            proposal.value = proposal.value + proposal.services[index].total;
        });
        proposal.company.addresses = proposal.company.addresses.length > 0 ? proposal.company.addresses[proposal.company.addresses.length - 1] : proposal.company.addresses;
        proposal.company.contacts = proposal.company.contacts.length > 0 ? proposal.company.contacts[proposal.company.contacts.length - 1] : proposal.company.contacts;
        res.render('proposal-template-pdf.html.twig', proposal, (error, html) => {
            if (error) {
                console.log(error);
                res.send(Proposal.error('An unexpected error was encountered while trying to load your proposal. Please try again.' + '\nDetails:' + JSON.stringify(error)));
            }
            res.send(Proposal.success({ pdf: html, logo: proposal.account.logo, address: proposal.account.address }));
        });
       
    } catch (err) {
        res.send(Proposal.error('An unexpected error was encountered while trying to load your proposal. Please try again.' + '\nDetails:' + JSON.stringify(error)));
    }
});


router.post('/companies/proposals/save/:proposalId', async (req, res) => {
    // make sure the proposal object was provided
    if (typeof req.body.proposal == 'undefined') {
        return res.send(Company.error('It looks like your request was improperly defined. Please try again.'));
    } else if (typeof req.body.proposal.services == 'undefined') {
        // make sure the services object is provided an not empty
        return res.send(Company.error('Please add at least one service to the proposal.'));
    }


    // properly format the services since I did it the lazy way - mitch
    let services = [];


    for (let i in req.body.proposal.services.service) {
        let service = {
            service: req.body.proposal.services.service[i],
            model: req.body.proposal.services.model[i],
            serviceDates: req.body.proposal.services.serviceDates[i],
            duration: req.body.proposal.services.duration[i],
            numOfEmp: req.body.proposal.services.numOfEmp[i],
            description: req.body.proposal.services.description[i]
        };
        service.price = (typeof req.body.proposal.services.price != 'undefined') ? req.body.proposal.services.price[i] : req.body.proposal.services.amount[i];
        //if (typeof req.body.proposal.services.pipelineIndex != 'undefined') { service.pipelineIndex = req.body.proposal.services.pipelineIndex[i];}
        services.push(service);
    }


    req.body.proposal.services = services;


    // format the file uploads - for the db schema we don't need the files to be
    // indexed by their key.
    req.body.proposal.files = Company.array_values(req.body.proposal.files);


    // in a couple of required fields
    req.body.proposal.owner = req.body.proposal.user;//global._user._id;
    req.body.proposal.active = true;


    // try to save it now
    try {
        //req.body.proposal._pipelineDate = Date.now;
        if (req.params.proposalId == 'new') {
            req.body.proposal._range = [new Date(moment().toISOString()), new Date(moment().toISOString())];
            req.body.proposal.pipelineDuration = [{
                pipelineFrom: req.body.proposal.pipeline, fDate: new Date(moment().toISOString()),
                pipelineTo: req.body.proposal.pipeline, tDate: new Date(moment().toISOString())
            }];
        } else {
            req.body.proposal._range = (typeof req.body.proposal._range == 'undefined') ? [] : (Array.isArray(req.body.proposal._range) ? req.body.proposal._range : [req.body.proposal._range]);
            req.body.proposal._range[0] = new Date(moment().toISOString());
            req.body.proposal._range[1] = (typeof req.body.proposal._range[1] == 'undefined') ? new Date(moment().toISOString()) : req.body.proposal._range[1];
            let exProposal = await Proposal.getOne(req.params.proposalId);
            if (exProposal) {
                //exProposal = exProposal.toObject();
                exProposal.pipelineDuration = typeof exProposal.pipelineDuration == 'undefined' ? [] : exProposal.pipelineDuration;
                exProposal.pipelineDuration.push({
                    pipelineFrom: exProposal.pipeline, fDate: exProposal._pipelineDate,
                    pipelineTo: req.body.proposal.pipeline, tDate: new Date(moment().toISOString())
                });
                req.body.proposal.pipelineDuration = exProposal.pipelineDuration;
            }
        }
        //update['$set']['_range.0'] = new Date(moment().toISOString());


        if (Array.isArray(req.body.proposal.company)) {
            let ids = [];
            var companies = req.body.proposal.company.filter(function (elem, pos) { return req.body.proposal.company.indexOf(elem) == pos; });// Remove duplicates;
            var counter = companies.length;
            for (var i = 0; i < companies.length; i++) {
                try {
                    counter -= 1;
                    var bdata = req.body.proposal;
                    bdata.company = companies[i];
                    let id = await Proposal.save(req.params.proposalId, bdata);
                    let data = { id: id, company: bdata.company };
                    ids.push(data);
                    //Include ProposalId in Prospect
                    Prospect.includeTargetId(id, bdata.company);
                    if (counter === 0) {
                        res.send(Proposal.success(ids[0]));
                    }
                } catch (e) {
                    console.log(e);
                }
            }
        } else {
            let oldProposal = req.params.proposalId == 'new' ? {} : await Proposal.getOneForHistory(req.params.proposalId);
            let id = await Proposal.save(req.params.proposalId, req.body.proposal);
            if (id) {
                let newProposal = await Proposal.getOneForHistory(id);                
                let meta = Proposal.jsonDiff(oldProposal, newProposal);
                await Proposal.addHistory((req.params.proposalId == 'new' ? 'Proposal created' : 'Proposal updated'), req.body.proposal.company, id, global._user._id, meta);
                let clientMessage = req.body['proposal-company-clientMessage'];
                if (typeof clientMessage != 'undefined' || clientMessage.length > 0) {
                    let oldMessage = await Company.getOne(req.body.proposal.company);
                    await Company.addClientMessage(req.body.proposal.company, clientMessage);
                    if (oldMessage.clientMessage != clientMessage) {
                        let meta = await Proposal.jsonDiff({ 'Message': oldMessage.clientMessage }, { 'Message': clientMessage });
                        await Proposal.addHistory('Company updated', req.body.proposal.company, '', global._user._id, meta);
                    }
                }
            }          
            let data = { id: id, company: req.body.proposal.company };
            res.send(Proposal.success(data));
        }




    } catch (err) {
        res.send(Proposal.error('An unexpected error was encountered while trying to save your proposal. Please try again.' + '\nDetails:' + JSON.stringify(err)));
    }
});


router.post('/companies/proposals/:proposalId/delete-file/:key', async (req, res) => {
    try {
        // load the proposal record
        let proposal = await Proposal.getOne(req.params.proposalId);


        // make sure the proposal even has files
        if (typeof proposal.files == 'undefined') {
            return res.send(Proposal.success());
        }
        let filterfiles = [];
        let fileUrl = '';


        proposal.files.forEach(function (file) {
            if (file.key == req.params.key) {
                fileUrl = file.url;
            } else {
                filterfiles.push(file);
            }
        });
        // update the proposal
        Proposal.model.updateOne({ _id: proposal._id }, { $set: { files: filterfiles } }, { runValidators: true }, (err) => {
            if (err) {
                return res.send(Proposal.error('An unexpected error was encountered while trying to delete the file. Please try again.'));
            }
            // delete the file
            fs.unlinkSync(`public${fileUrl}`);
            // and wrap it up
            res.send(Proposal.success());
        });
    } catch (err) {
        console.log('Error: ', err);
        res.status(500).send();
    }
});


router.post('/companies/save-file/:companyId', async (req, res) => {
    try {
        // convert the file date to an object from string
        req.body.file.date = new Date(moment(req.body.file.date, 'MM/DD/YYYY hh:mm A'));


        // save the file to the company record
        Company.model.updateOne(Company._enforceScope({
            _id: ObjectId(req.params.companyId)
        }), {
                $push: {
                    files: req.body.file
                }
            }, {
                runValidators: true
            }, (err) => {
                if (err) {
                    res.status(500).send();
                } else {
                    let meta = Company.jsonDiff({}, { files: '<a href="' + req.body.file.url + '" target="_blank">' + req.body.file.name + '</a>' });
                    Company.addHistory('File attached', req.params.companyId, '', global._user._id, meta);
                    res.send({ success: true });
                }
            });
    } catch (err) {
        res.status(500).send();
    }
});


router.post('/companies/delete-file/:companyId/:key', async (req, res) => {
    try {


        // load the proposal record
        let company = await Company.getOne(req.params.companyId);


        // make sure the company even has files
        if (typeof company.files == 'undefined') {
            return res.send(Company.success());
        }
        let filterfiles = [];
        let fileUrl = '';


        company.files.forEach(function (file) {
            if (file.key == req.params.key) {
                fileUrl = file.url;
            } else {
                filterfiles.push(file);
            }
        });


        Company.model.updateOne({ _id: company._id }, { $set: { files: filterfiles } }, { runValidators: true }, (err) => {
            if (err) {
                return res.send(Company.error('An unexpected error was encountered while trying to delete the file. Please try again.'));
            }
            // delete the file
            fs.unlinkSync(`public${fileUrl}`);
            // and wrap it up
            res.send(Company.success());
        });
    } catch (err) {
        console.log('Error: ', err);
        res.status(500).send();
    }
});


// get the details of a single company
router.get('/companies/:orgId', async (req, res) => {
    let vars = Object.assign({
        userSettings: global._user.account.settings
    }, base);


    try {
        //let company = await Prospect.getOne(req.params.orgId);


        //vars.data = Prospect.success(company);
        let company = await Company.getOne(req.params.orgId);
        if (company) {
            let meta = company.meta;
            if (meta) { meta.fundingStatus = (typeof meta.fundingType != 'undefined') ? meta.fundingType : meta.fundingStatus; }
        }


        // get the company proposals
        company.proposals = typeof company.proposals == 'undefined' ? {} : company.proposals;
        company.proposals = await Company.getProposals(company._id);
        if (company.proposals) {
            if (company.proposals.length > 0) {
                company.proposals.forEach((row, rsi) => {
                    if (typeof row.services != 'undefined') {
                        row.services.forEach(async (item, ri) => {
                            if (item.service) {
                                company.proposals[rsi].services[ri].service = await Company.getService(item.service);
                            }
                        });
                    }
                });
            }
        }


        // get the company activity
        company.activity = typeof company.activity == 'undefined' ? [] : company.activity;
        company.activity = await Company.getActivity(company._id);


        // make sure there is a default file key
        company.files = typeof company.files == 'undefined' ? {} : company.files;
        company.files = company.files || {};


        // load the proposal files
        company.proposalFiles = typeof company.proposalFiles == 'undefined' ? [] : company.proposalFiles;
        company.proposalFiles = await Company.getFiles(company._id);
        company.route = 'companies';


        // check if the request just wants a JSON payload - Used for Proposal form
        if (req.query.json) {
            return res.send(Company.success(company));
        }
        vars.data = company;


    } catch (err) {
        vars.msg = `<div class="alert alert-danger">${err.message}</div>`;
    }
   console.log(vars);
    res.render('companies-edit.html.twig', vars);
});


router.get('/companies/getActivity/:activityId', async(req, res) => {
   company.activity = await Company.getActivity(company._id);
});


// save a company record
router.post('/companies/save/:companyId', async (req, res) => {
    try {
        // save the record normally
        let oldObj = {}, newObj = {};
        if (req.params.companyId != 'new') { oldObj = await Company.getOneForHistory(req.params.companyId); }


        if (req.params.companyId === 'new') {
            // Newly added companies should be saved and then appear on the Companies and Market Analysis pages
            req.body.source = "Companies";
            req.body.clientImport = global._user.account._id; // Source Account ID
            let marketId = await Prospect.save(req.params.companyId, req.body);
            req.body.market = marketId;
        }


        let id = await Company.save(req.params.companyId, req.body);


        newObj = await Company.getOneForHistory(id);
        let meta = Company.jsonDiff(oldObj, newObj);
        let history = await Company.addHistory((req.params.companyId == 'new' ? 'Company created' : 'Company updated'), id, '', global._user._id, meta);


        res.send(Company.success({ id }));
    } catch (err) {
        console.log('Error: ', err);
        res.send(Company.error('An unexpected error was encountered while trying to save your company.'));
    }
});


router.get('/companies/:companyId/get/custom/fields', async (req, res) => {
    try {
        if (req.params.companyId === undefined) { return res.send(Company.error('Undefined parameter company ID')); }
        let customFields = await Company.getOne(req.params.companyId);
        res.send(Company.success(customFields));
    } catch (err) {
        console.log('Error: ', err);
        res.send(Company.error('An unexpected error was encountered while trying to save your company.'));
    }
});
router.post('/companies/:companyId/save/custom/fields', async (req, res) => {
    try {
        if (req.params.companyId === undefined) { return res.send(Company.error('Undefined parameter company ID')); }
        let body = {};
        Object.keys(req.body).forEach((key) => {
            body[req.body[key][0]] = req.body[key][1];  
        });
        Object.keys(body).forEach((key) => {
            if (key === null || key === '') {
                delete body[key];
            }            
        });
        let beforeCF = await Company.getOne(req.params.companyId);
        let customFields = await Company.save(req.params.companyId, { customFields: body });
        let afterCF = await Company.getOne(req.params.companyId);
        let diff = await Company.jsonDiff(beforeCF.customFields, afterCF.customFields);
        let history = await Company.addHistory('Custom Fields Updated', req.params.companyId, '', global._user._id, diff);
        res.send(Company.success(customFields));
    } catch (err) {
        console.log('Error: ', err);
        res.send(Company.error('An unexpected error was encountered while trying to save your company.'));
    }
});


router.post('/companies/delete', async (req, res) => {
    if (typeof req.body.companies == 'undefined' || req.body.companies.length === 0) {
        return res.send(Company.error('Please provide one or more company ID\'s.'));
    }


    try {
        await Company.softDeleteItems(req.body.companies);
        res.send(Company.success());
    } catch (err) {
        res.send(Company.error('An unexpected error was encountered.'));
    }
});


router.post('/companies/proposal/delete/:proposalId', async (req, res) => {
    if (typeof req.params.proposalId == 'undefined') {
        return res.send(Company.error('It looks like your request was improperly defined. Please try again.'));
    }


    try {
        await Proposal.softDeleteItems([req.params.proposalId]);
        res.send(Company.success());
    } catch (err) {
        res.send(Proposal.error('An unexpected error was encountered while trying to load your proposal. Please try again.'));
    }
});


router.get('/companies/activities/:activityId', async(req, res)=> {
   try {
      //console.log(req.params.activityId);
      let activityId=req.params.activityId;
      console.log("routerId");
      console.log(activityId);
      let activity = await Activity.getOne(activityId);
      res.send(activity);
    } catch (err) {
        res.send(error('An unexpected error was encountered while trying to load your proposal. Please try again.'));
    }
});


module.exports = router;

Open in new window


Right - I think we're going to have to work through this, so I'll start by giving you a general approach I'd consider taking.

Firstly, a consideration to make - you have your displayActivityForm, which as you have it is currently used to view the details of an existing Activity - can we assume this isn't used to add a new activity?

If you take a look at that method, you'll see an ajax property on your config - the ajax property is fired when the Save button on the form is clicked, so we need to change the path to a 'new' update url as that's where the data from the form is sent to. You can call it whatever you like because we haven't set up the route yet, but it makes sense that it's something logical like:

path: '/companies/update-activity'

Open in new window

We can see from that method that 5 properties are sent along with the request (notes / date / type / company / activityId)  - the data! Technically, we probably don't actually need company, but let's leave it there for the minute.

Now you need to set up your route (the one you set in your ajax path property above:

router.post('/companies/update-activity', async (req, res) => {

    // we probably want to add in some validation later one - it goes here!
    // the properties we mentioned above come in through request body : req.body.notes / req.body.activityId / req.body.date etc

    try {
        // this is where we'll update the Activity ... shortly!
        let _data = {
            notes : req.body.notes,
            date : req.body.date,
            company : req.body.company
            type : req.body.type,
        }

        res.send(Company.success({ message : "Done!", activityId : req.body.activityId, sentData : _data }))
    } catch (err) {
        res.send(Company.error(err))
    }

});

Open in new window

Now is a good time to test things as they stand. What we've done is change the URL that the Save button submits data to and we've set up that route. All that route does at the moment is send a success object back to the AJAX success handler with a message of Done. To see that message, we need to tweak the onComplete method of the config.ajax object (in your displayActivityForm method):

onComplete: function (resp, self) {
    if (resp.error) {
        self.addMsg(resp.msg);
        return false;
    }

    console.log("Response from Update", resp.data.message, resp.data.activityId, resp.data.sentData);

Open in new window

All we''ve done here is console logged the response from the update-activity method, which should include the message ("Done!"), the activityId as it was sent, plus the actual data that was sent (we've just written a simple echo test - effectively sending back what we recevied).

Give that lot a test - click to activate your form, fill in some details, click save and check the console. If everything's worked as expected, we know we're successfully reaching an end-point and sending data back.

Then we can move on to the actual update.
Alright, Chris!

We're poised on the threshold of greatness!

Here's what I get back in the console:

User generated image
Everything is looking good, I think. Is the "undefined" type important?
OK - looking good(ish).

We don't actually need the type, but it would bug me that it's undefined. If you look at the params property of the ajax call, you'll see this:

type: $('#formit-activityType').val(),

For some reason, that value (type) is not being populated. Have a look at your form when it pops up and make sure the <select> has an ID of activityType.

The other concern here is that somehow notes and date are coming in as arrays - they should be coming in as single values. I think the reasoning behind both these issues lies in the formit class - can you post that again.

Once you've got that sorted, you can change your method to make the call to update:

router.post('/companies/update-activity', async (req, res) => {

    try {
        let _data = {
            notes : req.body.notes,
            date : req.body.date,
            company : req.body.company
            type : req.body.type,
        }

        Activity.model.updateOne({ _id: req.body.activityId }, {
            $set: {
                // set up the data you want to persist to the DB
                notes: _data.notes,
                date = _data.date, 
            }
        }, { runValidators: true }, (err) => {
            if (err) {
                console.log(err)
                return res.send(Activity.error('Hmmm. Something went wrong!'))
            }
            res.send(Activity.success( { message : "Updated!" } ))
        });

    } catch (err) {
        res.send(Activity.error(err))
    }

});

Open in new window

In the $set property, you'll need to set the fields that you want updating. My example will update the notes and date - not sure if you want the type updating as well - I'll leave that up to you.
Chris, I'm trying to do my due dilligence.

Here's what I've come up with:

router.post('/companies/update-activity', async (req, res) => {


    // we probably want to add in some validation later one - it goes here!
    // the properties we mentioned above come in through request body : req.body.notes / req.body.activityId / req.body.date etc


    try {
        // this is where we'll update the Activity ... shortly!
        let _data = {
            notes : req.body.notes,
            date : req.body.date,
            company : req.body.company,
            type : req.body.type,
         activityId : req.body.activityId
        }
      /*
      async lastLogin() {
        try {
            Activity.updateOne(
            { _id: activityId },
            { $set:
               { "meta.date": date },
               { "meta.notes" : notes },
               { "type": type }
            },
            (err, raw) => {
               if (err) { return this.error(); }
               return this.success();
               });
         } catch (e) {
            return this.error();
         }
      }
      */


        res.send(Company.success({ message : "Done!", activityId : req.body.activityId, sentData : _data }))
    } catch (err) {
        res.send(Company.error(err))
    }


});

Open in new window

We crossed paths...

Stand by!
I fixed the problem with the "type." I changed the parameter to "activityType" and that error has been corrected.

Here's the formit.js

'use strict';


function FormIt(options) {
  // Create the defaults we will extend
  this.defaults = {
    fields: {},
    title: 'New Form',
    directionsText: 'Complete the form below.',
    directionsClass: 'lead',
    containerClass: 'mfp-hide white-popup white-popup-md',
    submitButtonText: '<i class="fas fa-fw fa-check"></i> Save',
    submitButtonClass: 'btn btn-success',
    cancelButtonText: 'Cancel',
    cancelButtonClass: 'btn btn-link',
    loadDataFromUrl: '',
    onShow: function(self){},
    onDataReceived: function(data, self){ return data; },
    onDataLoaded: function(data, self){},
    onValid: function(data, self){},
    onSubmit: function(data, self){}
  };


  // create a new form uniq id
  this.formId = this._uniqid();


  // establish the opts that will be used
  this.opts = {};
  this.$el;
}


FormIt.prototype.init = function() {
  // create the base container to use
  var html = '<div id="formit-' + this.formId + '" class="' + this.opts.containerClass + '">' +
      '<h3>' + this.opts.title + '</h3>' +
      '<hr />' +
      '<p class="' + this.opts.directionsClass + '">' + this.opts.directionsText + '</p>' +
      '<div class="formit-errors"></div>' +
      '<div class="loading">' +
        '<h3 class="text-center"><i class="fas fa-fw fa-cog fa-spin"></i> Loading...</h3>' +
      '</div>' +
      '<form class="needs-validation d-none" novalidate>' +
        '<div class="field-list"></div>' +
        '<hr />' +
        '<div class="buttons-footer">' +
          '<button type="submit" class="btn-formit-submit ' + this.opts.submitButtonClass + '">' + this.opts.submitButtonText + '</button>' +
          '<button type="button" class="btn-formit-cancel ' + this.opts.cancelButtonClass + '">' + this.opts.cancelButtonText + '</button>' +
        '</div>' +
      '</form>' +
    '</div>';


  if ($('#formit-' + this.formId).length === 0) {
    $('body').append(html);
  } else {
    $('#formit-' + this.formId).replaceWith(html);
  }


  this.$el = $('#formit-' + this.formId);


  // add in the events
  this._addEvents();
};


FormIt.prototype.show = function(opts) {
  // create the instance specific options
  this.opts = $.extend({}, this.defaults, opts);


  // ensure there are fields for the form
  if ( this._getObjectKeys(this.opts.fields).length === 0 ) {
    throw 'Invalid fields option provided.';
  }


  // delete any existing form
  $('div[id^="formit-"]').remove();


  // create the base form
  this.init();


  // build the html form
  this._buildForm();


  // open the form
  $.magnificPopup.open({
    alignTop: true,
    items: {
      type: 'inline',
      src: this.$el
    }
  });


  // hide the loading screen if we do not need to load any external data
  if (this.opts.loadDataFromUrl === '') {
    this.hideLoading();


    this.opts.onDataLoaded({}, this);
  } else {
    this.loadData();
  }


  // call the callback
  if (typeof this.opts.onShow == 'function') {
    this.opts.onShow(this);
  }
};


FormIt.prototype.close = function() {
  $.magnificPopup.close();
};


FormIt.prototype.getData = function() {
  var data = {},
      fields = this.$el.find('form').serializeArray();


  for (var i in fields) {
    data[ fields[i].name ] = fields[i].value;
  }


  return data;
};


FormIt.prototype.loadData = function() {
  // set a local reference to the object for inside the ajax callback
  var self = this;


  // show the loading screen
  this.showLoading();


  // look up the data
  $.get(this.opts.loadDataFromUrl, {}, function(resp){
    if (resp.error) {
      self.close();
      return CAMS.alert('Oh no!', resp.msg, 'error');
    }


    // set a reference
    var data = (typeof resp.data.rows != 'undefined') ? resp.data.rows[0] : resp.data;


    // call a lifecycle method
    data = self.opts.onDataReceived(data, this);


    // stop on false
    if (!data) {
      return self.close();
    }


    // load the form
    self._loadDataIntoForm(data);


    // show the form
    self.hideLoading();
  }, 'json').fail(function(){
    self.close();
    CAMS.alert('Oh no!', 'An unexpected error was encountered while trying to load your data. Please try again.', 'error');
  });
};


FormIt.prototype.reset = function(options) {


};


FormIt.prototype.addMsg = function(msg, type) {
  type = type || 'danger';


  this.clearMsg();


  this.$el.find('.formit-errors').append('<p class="alert alert-'+type+'">'+msg+'</p>');
};


FormIt.prototype.clearMsg = function() {
  this.$el.find('.formit-errors').empty();
};


FormIt.prototype.hideLoading = function() {
  this.$el.find('.loading').addClass('d-none');
  this.$el.find('form').removeClass('d-none');
};


FormIt.prototype.showLoading = function() {
  this.$el.find('form').addClass('d-none');
  this.$el.find('.loading').removeClass('d-none');
};


FormIt.prototype._addEvents = function() {
  var self = this;


  // add in the submission form
  this.$el.on('submit', 'form', function(e){
    var $f = $(this);


    e.preventDefault();
    e.stopPropagation();


    $f.addClass('was-validated');


    if ($f[0].checkValidity() === false) {
      self.addMsg('Please check the highlighted fields.');
      return false;
    }


    self.clearMsg();


    var data = self.getData(),
        resp;


    if (typeof self.opts.onValid == 'function') {
      resp = self.opts.onValid(data, self);


      if (resp === false) {
        return false;
      }
    }


    if (typeof self.opts.onSubmit == 'function') {
      resp = self.opts.onSubmit(data, self);


      if (resp === false) {
        return false;
      }
    }


    if (typeof self.opts.ajax == 'undefined') {
      self.close();
    }


    self._ajax();
  });


  this.$el.on('click', 'button.btn-formit-cancel', function(){
    self.close();
  });


};


FormIt.prototype._ajax = function() {
  if (typeof this.opts.ajax.path == 'undefined' || this.opts.ajax.path === '') {
    throw 'Invalid or missing ajax.path value.';
  }


  var self = this,
      $btn = this.$el.find('button[type="submit"]'),
      btnText = $btn.html(),
      params = (typeof this.opts.ajax.params != 'undefined') ? '&' + this._serialize(this.opts.ajax.params) : '',
      data = this.$el.find('form').serialize() + params;


  $btn.html('<i class="fas fa-fw fa-spinner-third fa-spin"></i> Processing...');


  $.post(this.opts.ajax.path, data, function(resp){
    if (typeof self.opts.ajax.onComplete == 'function') {
      var check = self.opts.ajax.onComplete(resp, self);


      if (check === false) {
        return false;
      }
    }


    self.close();
  }, 'json').fail(function(){
    if (typeof self.opts.ajax.onError == 'function') {
      self.opts.ajax.onError(self);
    }
  }).always(function(){
    $btn.html(btnText)
  });
};


FormIt.prototype._buildForm = function() {
  var fields = [];


  for (var fid in this.opts.fields) {
    fields.push( this._createField(fid, this.opts.fields[fid]) );
  }


  this.$el.find('form .field-list').empty().append( fields.join('') );
};


FormIt.prototype._getObjectKeys = function(obj) {
  var keys = [];


  for (var k in obj) {
    keys.push(k);
  }


  return keys;
}


FormIt.prototype._loadDataIntoForm = function(data) {
  var $field, model;


  for (var fid in data) {
    // load the field
    $field = $('#formit-' + fid);


    if ($field.length === 0) {
      continue;
    }


    // look for the field model
    model = this.opts.fields[fid];


    // load the value based on the type of field
    switch (true) {
      // look for a select2
      case (typeof model.lookup != 'undefined'):
        if (typeof data[fid] == 'object' && typeof data[fid]._id != 'undefined') {
          $field.empty().append('<option value="'+data[fid]._id+'">'+data[fid].text+'</option>');
        }
        break;


      default:
        $field.val( data[fid] );
        break;
    }
  }


  // call the loaded lifecycle method
  this.opts.onDataLoaded(data, this);
};


FormIt.prototype._createField = function(fid, field) {
  var name = field.name || fid,
      label = field.label || this._fieldLabelFromId(fid),
      placeholder = field.placeholder || label,
      classes = field.class || '',
      help = field.help || '',
      input = '';


  switch (true) {
    // static text
    case (field.type == 'static'):
      var htmlType = field.htmlType || 'p';


      return '<' + htmlType + ' class="' + classes + '">' + label + '</' + htmlType + '>';
      break;


    // check for a select2
    case (typeof field.lookup != 'undefined'):
      input = '<select id="formit-' + fid + '" name="' + name + '" class="form-control ' + classes + '"' + ((field.required) ? ' required' : '') + '></select>';
      break;


    // multiline string = textarea
    case (field.type == 'string' && typeof field.multiline != 'undefined' && field.multiline):
      input = '<textarea id="formit-' + fid + '" name="' + name + '" placeholder="' + placeholder + '" class="form-control ' + classes + '"' + ((field.required) ? ' required' : '') + '></textarea>';
      break;


    // string with contacts is a select2 calling the contacts of the associated company
    case (field.type == 'string' && typeof field.contacts != 'undefined' && field.contacts):
      var opts = ''; // this is a placeholder for when I implement loading data into the form


      input = '<select id="formit-' + fid + '" name="' + name + '" class="form-control contacts select2 ' + classes + '"' + ((field.required) ? ' required' : '') + ' data-route="/companies/contacts">' + opts + '</select>';
      break;


    // string with options = select
    case (field.type == 'string' && typeof field.options != 'undefined'):
      var opts = '',
          isArr = Array.isArray(field.options),
          val = '';


      for (var k in field.options) {
        val = (isArr) ? field.options[k] : k;


        opts += '<option value="' + val + '">' + field.options[k] + '</option>';
      }


      input = '<select id="formit-' + fid + '" name="' + name + '" class="form-control ' + classes + '"' + ((field.required) ? ' required' : '') + '>' + opts + '</select>';
      break;


    // string, double, integer, email, datetime
    default:
      let inputType = 'text';
      let attr = ' ';


      if (field.type == 'double' || field.type == 'integer') {
        inputType = 'number';
        attr = (field.type == 'double') ? ' step="0.01"' : ' ';
      } else if (field.type == 'email') {
        inputType = 'email';
      }


      if (field.type == 'datetime') {
        classes += ' field-datetime';
      }


      input = '<input type="' + inputType + '" id="formit-' + fid + '" name="' + name + '" placeholder="' + placeholder + '" class="form-control ' + classes + '"' + ((field.required) ? ' required' : '') + attr + '>';
      break;
  }


  return '<div class="form-group">' +
    '<label for="formit-' + fid + '" class="control-label">' + label + ((field.required) ? '<strong>*</strong>' : '') + '</label>' +
    input +
    ((help !== '') ? '<small class="text-muted"><i class="fas fa-fw fa-question-circle"></i> ' + help + '</small>' : '') +
  '</div>';
};


FormIt.prototype._fieldLabelFromId = function(fid) {
  return (fid + '')
    .replace(/^(.)|\s+(.)/g, function ($1) {
      return $1.toUpperCase()
    });
};


FormIt.prototype._serialize = function(obj, prefix) {
  var str = [],
    p;
  for (p in obj) {
    if (obj.hasOwnProperty(p)) {
      var k = prefix ? prefix + "[" + p + "]" : p,
        v = obj[p];
      str.push((v !== null && typeof v === "object") ?
        serialize(v, k) :
        encodeURIComponent(k) + "=" + encodeURIComponent(v));
    }
  }
  return str.join("&");
};


FormIt.prototype._uniqid = function(prefix, moreEntropy) {
  //  discuss at: http://locutus.io/php/uniqid/
  // original by: Kevin van Zonneveld (http://kvz.io)
  //  revised by: Kankrelune (http://www.webfaktory.info/)
  //      note 1: Uses an internal counter (in locutus global) to avoid collision
  //   example 1: var $id = uniqid()
  //   example 1: var $result = $id.length === 13
  //   returns 1: true
  //   example 2: var $id = uniqid('foo')
  //   example 2: var $result = $id.length === (13 + 'foo'.length)
  //   returns 2: true
  //   example 3: var $id = uniqid('bar', true)
  //   example 3: var $result = $id.length === (23 + 'bar'.length)
  //   returns 3: true


  if (typeof prefix === 'undefined') {
    prefix = ''
  }


  var retId
  var _formatSeed = function (seed, reqWidth) {
    seed = parseInt(seed, 10).toString(16) // to hex str
    if (reqWidth < seed.length) {
      // so long we split
      return seed.slice(seed.length - reqWidth)
    }
    if (reqWidth > seed.length) {
      // so short we pad
      return Array(1 + (reqWidth - seed.length)).join('0') + seed
    }
    return seed
  }


  var $global = (typeof window !== 'undefined' ? window : global)
  $global.$locutus = $global.$locutus || {}
  var $locutus = $global.$locutus
  $locutus.php = $locutus.php || {}


  if (!$locutus.php.uniqidSeed) {
    // init seed with big random int
    $locutus.php.uniqidSeed = Math.floor(Math.random() * 0x75bcd15)
  }
  $locutus.php.uniqidSeed++


  // start with prefix, add current milliseconds hex string
  retId = prefix
  retId += _formatSeed(parseInt(new Date().getTime() / 1000, 10), 8)
  // add seed hex string
  retId += _formatSeed($locutus.php.uniqidSeed, 5)
  if (moreEntropy) {
    // for more entropy we add a float lower to 10
    retId += (Math.random() * 10).toFixed(8).toString()
  }


  return retId
};


if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}

Open in new window



Chris, one thing:

The "date" that needs to be updated is "meta.date." I say that based on the way it appears in the collection:

User generated imagei'm getting an error when I try to accommodate that which looks like this:

router.post('/companies/update-activity', async (req, res) => {

    try {
        let _data = {
            notes : req.body.notes,
            date : req.body.date,
            company : req.body.company,
            type : req.body.type
        }

        Activity.model.updateOne({ _id: req.body.activityId }, {
            $set: {
                // set up the data you want to persist to the DB
                notes: _data.notes,
                meta.date: _data.date,
            type: _data.type            
            }
        }, { runValidators: true }, (err) => {
            if (err) {
                console.log(err)
                return res.send(Activity.error('Hmmm. Something went wrong!'))
            }
            res.send(Company.success( { message : "Updated!" } ))
        });

    } catch (err) {
        res.send(Company.error(err))
    }

});

The error is this:

C:\wamp64\www\bSmart\server\routes\companies.js:283
                meta.date: _data.date,

                    ^
How do I fix that?


Right - I can see why the data is coming in as an array - basically, we don't need all those params in the ajax object.

In your ajax object, drop the properties that exist as form fields:

params: {
    company: companyId,
    activityId: activityId,
},

Open in new window

When submitted the form will serialize() it's own data, so anything that has a field (input / select / textarea etc.) doesn't need to be set as a param - doing so will mean the is sent twice - once from the form itself and once from the param.
Done!

   path: '/companies/update-activity',
                  params: {
                     /*
                     notes: $('#formit-notes').val(),
                     date: $('#formit-date').val(),
                     type: $('#formit-activityType').val(),
                     */
                     company: companyId,
                     activityId: activityId
                  },
Regarding the date issue - why is it the meta.date that needs updating and not the date property ?? What's the thinking behind having the 2 dates in there ?
the way the app is constructed, at least as far as the "notes" are concerned, is that you have the date the note was created and, in some cases, the date of the activity being documented.

I know...

It's not good logic and it's not good infrastructure, but it is not mine to question at the moment...

But how can I target that column without the "."?
Chris, I gave this a shot and it didn't clear the Validators, but perhaps I'm closer?


router.post('/companies/update-activity', async (req, res) => {

    try {
        let _data = {
            notes : req.body.notes,
            date : req.body.date,
            company : req.body.company,
            type : req.body.type
        }

        Activity.model.updateOne({ _id: req.body.activityId }, {
            $set: {
                // set up the data you want to persist to the DB
                notes: _data.notes,
                "meta.$.date": _data.date,
            type: _data.type,
            }
        },
      { runValidators: true }, (err) => {
            if (err) {
                console.log(err)
                return res.send(Activity.error('Hmmm. Something went wrong!'))
            }
            res.send(Company.success( { message : "Updated!" } ))
        });

    } catch (err) {
        res.send(Company.error(err))
    }

});

I think you just need:

"meta.date" : _data.date

What error message are you getting (Check the console - the error should be logged there.
Lotta stuff here, Chris:

    at C:\wamp64\www\bSmart\server\services\company.js:635:49
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
Error [ValidationError]: Validation failed: type: Path `type` is required.
    at ValidationError.inspect (C:\wamp64\www\bSmart\node_modules\mongoose\lib\error\validation.js:59:24)
    at formatValue (internal/util/inspect.js:563:31)
    at inspect (internal/util/inspect.js:221:10)
    at formatWithOptions (internal/util/inspect.js:1693:40)
    at Object.Console.<computed> (internal/console/constructor.js:272:10)
    at Object.log (internal/console/constructor.js:282:61)
    at C:\wamp64\www\bSmart\server\routes\companies.js:289:25
    at C:\wamp64\www\bSmart\node_modules\mongoose\lib\model.js:4568:16
    at callback (C:\wamp64\www\bSmart\node_modules\mongoose\lib\query.js:4080:9)
    at C:\wamp64\www\bSmart\node_modules\mongoose\lib\query.js:4304:14
    at _callback (C:\wamp64\www\bSmart\node_modules\mongoose\lib\query.js:3668:16)
    at C:\wamp64\www\bSmart\node_modules\mongoose\lib\helpers\updateValidators.js:223:16
    at C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\async\internal\parallel.js:39:9
    at C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\async\internal\once.js:12:16
    at iteratorCallback (C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\async\eachOf.js:60:13)
    at C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\async\internal\onlyOnce.js:12:16
    at C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\async\internal\parallel.js:36:13
    at schemaPath.doValidate.updateValidator (C:\wamp64\www\bSmart\node_modules\mongoose\lib\helpers\updateValidators.js:165:13)
    at C:\wamp64\www\bSmart\node_modules\mongoose\lib\schematype.js:1037:9
    at processTicksAndRejections (internal/process/task_queues.js:75:11) {
  errors: {
    type: MongooseError [ValidatorError]: Path `type` is required.
        at new ValidatorError (C:\wamp64\www\bSmart\node_modules\mongoose\lib\error\validator.js:29:11)
        at validate (C:\wamp64\www\bSmart\node_modules\mongoose\lib\schematype.js:1034:13)
        at C:\wamp64\www\bSmart\node_modules\mongoose\lib\schematype.js:1088:11
        at Array.forEach (<anonymous>)
        at SchemaString.SchemaType.doValidate (C:\wamp64\www\bSmart\node_modules\mongoose\lib\schematype.js:1043:14)
        at C:\wamp64\www\bSmart\node_modules\mongoose\lib\helpers\updateValidators.js:160:22
        at C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\async\internal\parallel.js:31:39
        at eachOfArrayLike (C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\async\eachOf.js:65:9)
        at exports.default (C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\async\eachOf.js:9:5)
        at _parallel (C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\async\internal\parallel.js:30:5)
        at parallelLimit (C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\async\parallel.js:88:26)
        at C:\wamp64\www\bSmart\node_modules\mongoose\lib\helpers\updateValidators.js:217:5
        at model.Query._updateThunk (C:\wamp64\www\bSmart\node_modules\mongoose\lib\query.js:3677:7)
        at model.Query.<anonymous> (C:\wamp64\www\bSmart\node_modules\mongoose\lib\query.js:3724:23)
        at model.Query._wrappedThunk [as _updateOne] (C:\wamp64\www\bSmart\node_modules\mongoose\lib\helpers\query\wrapThunk.js:16:8)
        at C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\kareem\index.js:369:33
        at processTicksAndRejections (internal/process/task_queues.js:75:11) {
      message: 'Path `type` is required.',
      name: 'ValidatorError',
      properties: [Object],
      kind: 'required',
      path: 'type',
      value: undefined,
      reason: undefined,
      [Symbol(mongoose:validatorError)]: true
    }
  },
  _message: 'Validation failed',
  name: 'ValidationError'


Hey, friend! I'm done for tonite. I'll be back bright and early tomorrow morning. Thanks for all your help!
Hey,

The error you're getting indicates that 'type' isn't being filled in correctly. Earlier, we had undefined for type, and you said you'd fixed that by changing to activityType. It seems we're reverted back to 'type', so that needs fixing.
Morning, Chris!

OK, I fixed the "type" (it should be activityType). Now I'm getting this:

  at C:\wamp64\www\bSmart\server\services\company.js:635:49
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
MongoError: BSON field 'update.updates.collation' is the wrong type 'string', expected type 'object'
    at Connection.<anonymous> (C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\mongodb-core\lib\connection\pool.js:443:61)
    at Connection.emit (events.js:210:5)
    at processMessage (C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\mongodb-core\lib\connection\connection.js:364:10)
    at TLSSocket.<anonymous> (C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\mongodb-core\lib\connection\connection.js:533:15)
    at TLSSocket.emit (events.js:210:5)
    at addChunk (_stream_readable.js:309:12)
    at readableAddChunk (_stream_readable.js:290:11)
    at TLSSocket.Readable.push (_stream_readable.js:224:10)
    at TLSWrap.onStreamRead (internal/stream_base_commons.js:182:23) {
  operationTime: Timestamp { _bsontype: 'Timestamp', low_: 1, high_: 1586864086 },
  ok: 0,
  errmsg: "BSON field 'update.updates.collation' is the wrong type 'string', expected type 'object'",
  code: 14,
  codeName: 'TypeMismatch',
  '$clusterTime': {
    clusterTime: Timestamp { _bsontype: 'Timestamp', low_: 1, high_: 1586864086 },
    signature: { hash: [Binary], keyId: [Long] }
  },
  name: 'MongoError',
  [Symbol(mongoErrorContextSymbol)]: {}


And Chris, one other thing - and I'm just throwing stuff up against a wall, here...

One of the lines in the error references line #635 in the company.js service...

at C:\wamp64\www\bSmart\server\services\company.js:635:49

 Here's that piece (I have it in bold):

getActivity(companyId) {
        return new Promise((resolve, reject) => {
            const Activity = mongoose.model('Activity', ActivitySchema);
            //let pipeline = [
            //            {
            //                $match: {
            //                    account: global._user.account._id,
            //                    company: companyId
            //                }
            //            },
            //            {
            //                $sort: {
            //                    date: -1
            //                }
            //            },
            //            { $limit: 100 },
            //            {
            //                $lookup: {
            //                    from: 'users',
            //                    foreignField: '_id',
            //                    localField: 'user',
            //                    as: 'user'
            //                }
            //            },
            //            { $unwind: '$user' }
            //        ];
            //  Activity.aggregate(pipeline).then((rows) => {
            //  var activity = [];

            //  for (let i in rows) {
            //    // get the icon by type
            //    rows[i].icon = this.getIconByType(rows[i].type);

            //    // add it to the list
            //    activity.push(rows[i]);
            //  }

            //  resolve(activity);
            //}).catch((err) => {
            //  reject(err);
            //});
            let pipeline = [
                {
                    $match: {
                        account: global._user.account._id,
                        company: companyId
                    }
                },
                {
                    $lookup: {
                        from: 'users',
                        foreignField: '_id',
                        localField: 'user',
                        as: 'user'
                    }
                },
                { $unwind: '$user' },
                {
                    $group: {
                        '_id': {
                            year: { $year: "$date" },
                            month: { $month: "$date" },
                            day: { $dayOfMonth: "$date" }
                        },
                        activities: {
                            $push: {
                                _id: '$_id',
                                type: '$type',
                                company: '$company',
                                proposal: '$proposal',
                                user: { _id: '$user._id', 'firstName': '$user.firstName', 'lastName': '$user.lastName' },
                                date: '$date',
                                meta: '$meta'

                            }
                        }
                    }
                },
                { $project: { date: '$_id', activities: 1, _id: 0 } },
                { $sort: { "activities.date": -1 } }
            ];
            Activity.aggregate(pipeline).then((rows) => {
                var activity = [];

                for (let i in rows) {
               for (let j in rows[i].activities) {
                  let today = moment(rows[i].activities[j].date).isSame(moment(), 'day'); // get true/false indicating today
                  rows[i].activities[j].icon = this.getIconByType(rows[i].activities[j].type);                        
                  rows[i].activities[j].prettyDate = today ? "today" : moment(rows[i].activities[j].date).fromNow(); // set prettyDate to 'today' if we have a match, otherwise use moment
                  rows[i].activities[j].readableDate = moment(rows[i].activities[j].date).format('MMM Do - LT');
                  rows[i].activities[j].doableDate = moment(rows[i].activities[j].date).format('m/d/Y');
               }
               /*
               =======================================================
               the above for loop was put in as a replacement for what you see below in order to facilitate "today" rather than 5-6 hours ago
               or whatever might be registered in hours rather than something more readable like "today" (Bruce Gust 4/13/2020)               
               =======================================================
               */
               /*
                    for (let j in rows[i].activities) {
                  rows[i].activities[j].icon = this.getIconByType(rows[i].activities[j].type);                        
                  rows[i].activities[j].prettyDate = moment(rows[i].activities[j].date).fromNow();
                  rows[i].activities[j].readableDate = moment(rows[i].activities[j].date).format('MMM Do - LT');
                  rows[i].activities[j].doableDate = moment(rows[i].activities[j].date).format('m/d/Y');
                    }
               */
                    // add it to the list
                    rows[i].prettyDate = moment.utc(rows[i].date.year + '-' + rows[i].date.month + '-' + rows[i].date.day).local().fromNow();
                    activity.push(rows[i]);

                }
         //console.log(activity.activities);
                resolve(activity);
            }).catch((err) => {
                reject(err);
            });
        });
    }
Not sure what's going on there. Just as a test, can you hard code in a couple of values and see what happens (skip the date for now):

Activity.model.updateOne({ _id: req.body.activityId }, {
    $set: {
        // set up the data you want to persist to the DB
        notes: "Testing the notes",
        type: "call",
    }
}, 

Open in new window

And take a look at your Schema definitions for both Activity and Company - see if the collation has been set on either of them
Not sure why that particular line would through an error - don't even think we're touching that for the update
First, here are the Schemas:

ActivitySchema:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;


const CompanySchema = require('./CompanySchema');
const ProposalSchema = require('./ProposalSchema');
const UserSchema = require('./UserSchema');


const ActivitySchema = new Schema({
    type: {
        type: String,
        required: true,
        enum: ['call', 'visit', 'reminder', 'event', 'Company created', 'Company updated', 'Company deleted', 'Broker info added', 'Broker info updated', 'Contact added', 'Contact updated', 'Address added', 'Address updated', 'Proposal created', 'Proposal updated', 'Converted to company', 'Target selected', 'Target ignored', 'Owner change', 'Proposal assigned', 'File attached', 'File removed', 'User added', 'User deleted', 'Service line added', 'Service line deleted', 'Swimlane added', 'Swimlane removed', 'Custom Fields Updated']
    },
    company: {
        type: Schema.Types.ObjectId,
        required: true,
        ref: 'Company'
    },
    proposal: {
        type: Schema.Types.ObjectId,
        ref: 'Proposal'
    },
    pipeline: {
        type: Schema.Types.ObjectId, //for tracking activity on each pipeline stage.
        ref: 'PipelineColumn'
    },
    user: {
        type: Schema.Types.ObjectId,
        ref: 'User'
    },
    date: {
        type: Date,
        default: Date.now
    },
    meta: {
        type: Schema.Types.Mixed
    },
  account: {
    type: Schema.Types.ObjectId,
    default: () => {
      return global._user.account._id;
    }
    },
    active: { type: Boolean, default: true }
}, { discriminatorKey: 'type', collation: 'activities' });// removed strict:false and added discriminatorKey for meta.start, meta.end, meta.date fields for type=event/call/note/reminder


module.exports = ActivitySchema;

Open in new window


CompanySchema:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = require('./UserSchema');
const CompanyContactSchema = require('./CompanyContactSchema');
const CompanyAddressSchema = require('./CompanyAddressSchema');
const ProspectSchema = require('./ProspectSchema');


var validateEmail = function (email) {
    var re = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
    return re.test(email);
};
const CompanySchema = new Schema({
  name: {type: String,required: true},
  status: {type: String,enum: ['info', 'warning', 'danger', 'success']},
  market: {type:Schema.Types.ObjectId, ref:'Prospect'},
  account: {type: Schema.Types.ObjectId,default: () => {return global._user.account._id;}},
  owner: {type: Schema.Types.ObjectId,ref: 'User'},
  files: [{
    key: {type: String,required: true},
    name: {type: String,required: true},
    url: {type: String,required: true},
    date: {type: Date,required: true,default: Date.now}
  }],
  meta: {
    fundingStatus: {type: String,required: true,enum: ['Unknown', 'Fully', 'Mixed', 'Self'],default: 'Unknown'},
    ftes: {type: Number,default: 0},
    premium: {type: Number,default: 0},
    revenue: {type: Number,default: 0},
    fteGrouping: {type: String,enum: ['Unknown','50-99', '100-499', '500-999', '1000-4999', '> or = 5000']},
    industry: String,
    premiumPerFte: { type: Number, default: 0 },
    brokerCommissionTotal: { type: Number, default: 0 },
    brokerFeesTotal: { type: Number, default: 0 },
    ein: { type: String },
    fax: { type: String },
    companyUrl: { type: String },
    businessDescription:{ type: String },
    SIC: { type: String },
    SICDescription: { type: String },
    source: { type: String }
  },
    brokers: [{
    _id: false,
    name: { type: String },
    normalized: { type: String },
    street: { type: String },
    street1: { type: String },
    street2: { type: String },
    city: {type: String},
        state: {
            type: String,      
        match: /^[A-Z]{2}/,  ///^[a-z]{2}$/ig,
      uppercase: true
    },
      postal: { //5 digit US ZIP code, 5 digit US ZIP code + 4, and 6 digit alphanumeric Canadian Postal Code, Regex matches for : 44240 | 44240-5555 | G3H 6A3
      type: String,      
      trim:true,
      match: /^\d{5}-\d{4}|\d{5}|[A-Z]\d[A-Z] \d[A-Z]\d$/
      },
      commission: { type: Number, default: 0 },
      fees: { type: Number, default: 0 },
      feesText: { type: String }
  }],
  dates: {
    created: {
      type: Date,
      default: Date.now
    },
      updated: Date,
    assigned:Date
    },
    clientMessage: { type: String },
    active: { type: Boolean, required: true, default: true },
    addresses: [
        {
            street: {
                type: String,
                required: true
            },
            city: {
                type: String,
                required: true
            },
            state: {
                type: String,
                required: true,
                trim: true,
                uppercase: true,
                match: /^[A-Z]{2}//*match: /^[A-Z]{2}/g,*/
            },
            postal: {
                type: String,
                required: true,
                trim: true,
                match: /^\d{5}-\d{4}|\d{5}|[A-Z]\d[A-Z] \d[A-Z]\d$/
            },
            county: String,
            country: {
                type: String,
                default: 'US'
            },
            hash: String,
            formatted: String,
            nonGeo: String,
            default: {
                type: Boolean,
                default: false
            },
            active: {
                type: Boolean,
                default: true
            },
            error_message: String
        }
    ],
    contacts: [
        {
            name: {
                type: String,
                required: true
            },
            email: {
                type: String,
                trim: true,
                validate: [validateEmail, 'Please fill a valid email address'],
                match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 'Please fill a valid email address']
            },
            phone: {
                type: String,
                trim: true,
                match: /^\d{3}\-\d{3}\-\d{4}$/  /*match: /^\d{3}\-\d{3}\-\d{4}$/g*/
            },
            default: {
                type: Boolean,
                default: false
            },
            active: {
                type: Boolean,
                default: true
            }
        }
    ],
    deleted: Boolean,
    pipeline: { type: Schema.Types.ObjectId },
    _pipelineDate: { type: Date },
    source: { type: String },
    clientImport: { type: Schema.Types.ObjectId },
    customFields: { type: Schema.Types.Mixed}
});


module.exports = CompanySchema;

Open in new window


BTW: I went looking for "Collations." Is that the feature that accommodates other languages, as far as the way they're sorted?

Collations are a new feature in MongoDB version 3.4. They provide a set of rules to use when comparing strings that comply with the conventions of a particular language, such as Spanish or German. If no collation is specified, the server sorts strings based on a binary comparison. Many languages have specific ordering rules, and collations allow users to build applications that adhere to language-specific comparison rules.
https://api.mongodb.com/python/current/examples/collations.html 

Not even sure why that would be a "thing" in this app.

Also in an effort to extend the scope of the "sanity check," I did this:

router.post('/companies/update-activity', async (req, res) => {

    try {
        let _data = {
            notes : req.body.notes,
            date : req.body.date,
            company : req.body.company,
            type : req.body.activityType
        }

        Activity.model.updateOne({ _id: req.body.activityId }, {
            $set: {
                // set up the data you want to persist to the DB
                notes: _data.notes,
               //"meta.date" : _data.date,
            //type: _data.type

            }
        },
      { runValidators: true }, (err) => {
            if (err) {
                console.log(err)
                return res.send(Activity.error('Hmmm. Something went wrong!'))
            }
            res.send(Company.success( { message : "Updated!" } ))
        });

    } catch (err) {
        res.send(Company.error(err))
    }

});

This is what I got back in the console:

Response from Update Updated! undefined undefined

I'm going to pop the hood to make sure "_data.notes" etc is actually populated with some data, but I wanted that on your screen as well.

The form names are all intact...
<textarea id="formit-notes" name="notes" placeholder="Notes" class="form-control " required=""></textarea>
OK.

That's good progress. The fact that you got a response logged to the console of Updated indicates that the Update didn't error.

However, looking at your scheme for Activity - you don't actually have a notes field, so I don't expect any updates to the DB to have happened. Thinking back, your Activity can have different 'types' so I'm guessing that the meta data will maybe have different properties based on that type. Do you have any other Models / Schemas that are specific to the different types.
You're right about the "notes" field and perhaps that's why the "date" went south as well.

They're both a part of the "meta" field. This is from the "Activity" schema:

    meta: {
        type: Schema.Types.Mixed
    },

Here's a screenshot of the collection so you have a visual:

User generated image
How does that impact the way in which the query is constructed?
ASKER CERTIFIED SOLUTION
Avatar of Chris Stanyon
Chris Stanyon
Flag of United Kingdom of Great Britain and Northern Ireland image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
OK!

Here's the error:

MongoError: BSON field 'update.updates.collation' is the wrong type 'string', expected type 'object'
    at Connection.<anonymous> (C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\mongodb-core\lib\connection\pool.js:443:61)
    at Connection.emit (events.js:210:5)
    at processMessage (C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\mongodb-core\lib\connection\connection.js:364:10)
    at TLSSocket.<anonymous> (C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\mongodb-core\lib\connection\connection.js:533:15)
    at TLSSocket.emit (events.js:210:5)
    at addChunk (_stream_readable.js:309:12)
    at readableAddChunk (_stream_readable.js:290:11)
    at TLSSocket.Readable.push (_stream_readable.js:224:10)
    at TLSWrap.onStreamRead (internal/stream_base_commons.js:182:23) {
  operationTime: Timestamp { _bsontype: 'Timestamp', low_: 2, high_: 1586875658 },
  ok: 0,
  errmsg: "BSON field 'update.updates.collation' is the wrong type 'string', expected type 'object'",
  code: 14,
  codeName: 'TypeMismatch',
  '$clusterTime': {
    clusterTime: Timestamp { _bsontype: 'Timestamp', low_: 2, high_: 1586875658 },
    signature: { hash: [Binary], keyId: [Long] }
  },
  name: 'MongoError',
  [Symbol(mongoErrorContextSymbol)]: {}
}


And here's the code I used:

router.post('/companies/update-activity', async (req, res) => {

    try {
        let _data = {
            notes : req.body.notes,
            date : req.body.date,
            company : req.body.company,
            type : req.body.activityType
        }

        Activity.model.updateOne({ _id: req.body.activityId }, {
         $set: {
         // set up the data you want to persist to the DB
         meta : {
            notes: _data.notes,
            date: _data.date,
            },
         }
        },
      { runValidators: true }, (err) => {
            if (err) {
                console.log(err)
                return res.send(Activity.error('Hmmm. Something went wrong!'))
            }
            res.send(Company.success( { message : "Updated!" } ))
        });


    } catch (err) {
        res.send(Company.error(err))
    }

});


Chris, do I have to convert the date so that it shows up in the database correctly?
Right. One more quick test just so we can confirm that it's a data issue:

Activity.model.updateOne({ _id: req.body.activityId }, {
    $set: {
        // set up the data you want to persist to the DB
        date: _data.date,
    }
},

Open in new window

I know that's updating the wrong date, but I just want to check to make sure we are actually getting an DB Update done. Run that and then check to the DB to make sure we have a successful update. If we do, we can go from there.
Same error:

MongoError: BSON field 'update.updates.collation' is the wrong type 'string', expected type 'object'
    at Connection.<anonymous> (C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\mongodb-core\lib\connection\pool.js:443:61)
    at Connection.emit (events.js:210:5)
    at processMessage (C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\mongodb-core\lib\connection\connection.js:364:10)
    at TLSSocket.<anonymous> (C:\wamp64\www\bSmart\node_modules\mongoose\node_modules\mongodb-core\lib\connection\connection.js:533:15)
    at TLSSocket.emit (events.js:210:5)
    at addChunk (_stream_readable.js:309:12)
    at readableAddChunk (_stream_readable.js:290:11)
    at TLSSocket.Readable.push (_stream_readable.js:224:10)
    at TLSWrap.onStreamRead (internal/stream_base_commons.js:182:23) {
  operationTime: Timestamp { _bsontype: 'Timestamp', low_: 1, high_: 1586877997 },
  ok: 0,
  errmsg: "BSON field 'update.updates.collation' is the wrong type 'string', expected type 'object'",
  code: 14,
  codeName: 'TypeMismatch',
  '$clusterTime': {
    clusterTime: Timestamp { _bsontype: 'Timestamp', low_: 1, high_: 1586877997 },
    signature: { hash: [Binary], keyId: [Long] }
  },
  name: 'MongoError',
  [Symbol(mongoErrorContextSymbol)]: {}
}

Here's the code I ran:

router.post('/companies/update-activity', async (req, res) => {

    try {
        let _data = {
            notes : req.body.notes,
            date : req.body.date,
            company : req.body.company,
            type : req.body.activityType
        }

        Activity.model.updateOne({ _id: req.body.activityId }, {
         $set: {
         // set up the data you want to persist to the DB
         date: _data.date,
         }
        },
      { runValidators: true }, (err) => {
            if (err) {
                console.log(err)
                return res.send(Activity.error('Hmmm. Something went wrong!'))
            }
            res.send(Company.success( { message : "Updated!" } ))
        });


    } catch (err) {
        res.send(Company.error(err))
    }

});
Chris, I am stumped, but I think I've at least got some intelligent questions / observations...

This seems to be the buzzard:

BSON field 'update.updates.collation' is the wrong type 'string', expected type 'object'",

From what I can gather, "collations" are more or less, "rules," yes? How those rules are defined and how they apply, I'm not sure, but I see this at the bottom of the ActivitySchema.js file:

{ discriminatorKey: 'type', collation: 'activities' });// removed strict:false and added discriminatorKey for meta.start, meta.end, meta.date fields for type=event/call/note/reminder

I can't update anything. I can't even update the type of note. Do you have any ideas?


Chris, I eliminated the "collation" from the Activity schema and the error is gone. I asked my supervisor to take a look at it and he couldn't see any good reason to hang on to it, so it's gone.

No errors, but it's still not updating. I'm going to keep at it!
Chris!

I've got everything working! Thanks so much! Now, I need to figure out how to display a "success" alert and refresh the page so the changes can be seen!
Hey Bruce,

Good effort - I head away from the computer for a couple of hours and you've nailed it ... Nice :)

Regards showing a success, you send info back to your AJAX call using the following:

res.send(Company.success( { message : "Updated!" } ))

That object will be received by the success handler ... resp in your case:

onComplete: function (resp, self) {
    alert(resp.message);

Open in new window

If you want to update/ refresh the display, you could also pass back the Activity that's been updated.