/beancounter/static/js/entries.js
JavaScript | 476 lines | 373 code | 63 blank | 40 comment | 23 complexity | 0ed179ed3946a3422ed249be4325087d MD5 | raw file
Possible License(s): 0BSD
- // vim: fdm=marker
- /*
- * Copyright (c) 2011-2012 Audrius KaĹžukauskas
- *
- * This file is part of BeanCounter and is released under
- * the ISC license, see LICENSE for more details.
- */
- /*jslint indent: 4, browser: true, nomen: true, sloppy: true */
- /*global $, _, Backbone, B, DATA */
- B.Entries = {};
- // Entry resource URL.
- B.Entries.URL = '/api/entries';
- B.Entries.Model = Backbone.Model.extend({ // {{{
- defaults: {
- amount: '0.00',
- note: '',
- tags: [],
- checked: false
- },
- validate: function (attrs) {
- if (attrs.hasOwnProperty('amount')) {
- var amount = $.trim(attrs.amount);
- if (!amount) {
- return 'Amount is required';
- }
- if (!amount.match(/^\d+\.?\d{0,2}$/)) {
- return 'Amount should be in format #.##';
- }
- }
- },
- toJSON: function () {
- var attrs = _.clone(this.attributes);
- delete attrs.checked;
- if (attrs.amount) {
- attrs.amount = parseFloat(attrs.amount).toFixed(2);
- }
- return attrs;
- },
- toggleChecked: function () {
- this.set({checked: !this.get('checked')});
- },
- removeTags: function () {
- var ids, tags;
- ids = B.tags.pluck('id');
- tags = _.filter(this.get('tags'), function (tag) {
- return _.include(ids, tag);
- });
- this.set({tags: tags});
- }
- }); // }}}
- B.Entries.Collection = B.Collection.extend({ // {{{
- model: B.Entries.Model,
- url: B.Entries.URL,
- initialize: function () {
- // Set current month.
- this.currMonth = $.datepick.parseDate('yyyy-mm-dd', DATA.currDate);
- },
- parse: function (resp) {
- this.totalAmount = resp.totalAmount;
- return resp.entries;
- },
- // Update collection with entries for the current month.
- update: function () {
- this.fetch({data: {
- total: 1,
- month: $.datepick.formatDate('yyyy-mm', this.currMonth)
- }});
- },
- // Update collection with entries for the next month.
- nextMonth: function () {
- $.datepick.add(this.currMonth, 1, 'm');
- this.update();
- },
- // Update collection with entries for the previous month.
- previousMonth: function () {
- $.datepick.add(this.currMonth, -1, 'm');
- this.update();
- },
- setMonth: function (date) {
- if (!this.inCurrMonth(date)) {
- this.currMonth = date;
- this.update();
- }
- },
- // Check if given date belongs to current month.
- inCurrMonth: function (date) {
- if (_.isString(date)) {
- date = $.datepick.parseDate('yyyy-mm-dd', date);
- }
- return this.currMonth.getFullYear() === date.getFullYear() &&
- this.currMonth.getMonth() === date.getMonth();
- },
- setTotalAmount: function (total) {
- this.totalAmount = total;
- this.trigger('change:total');
- }
- }); // }}}
- B.Entries.Views = {};
- B.Entries.Views.Row = B.Views.Row.extend({ // {{{
- // Cache template function.
- template: _.template($('#entry-row-template').html()),
- initialize: function () {
- B.Views.Row.prototype.initialize.call(this);
- B.tags.on('change:name', this.renderTags, this);
- B.tags.on('remove:multiple', this.model.removeTags, this.model);
- this.model.on('change:tags', this.renderTags, this);
- },
- remove: function () {
- B.tags.off('change:name', this.renderTags);
- B.tags.off('remove:multiple', this.model.removeTags);
- this.model.off('change:tags', this.renderTags);
- B.Views.Row.prototype.remove.call(this);
- },
- render: function () {
- B.Views.Row.prototype.render.call(this);
- this.renderTags();
- return this;
- },
- renderTags: function () {
- var tags = this.model.get('tags').map(function (id) {
- return B.tags.get(id).get('name');
- }).sort();
- this.$('td.tags div')
- .text(tags.join(', '))
- // Firefox ignores line ends in title attribute.
- .attr('title', tags.join(', \n'));
- },
- formFactory: function () {
- return B.Entries.Views.EditForm;
- }
- }); // }}}
- // Base view for entry forms.
- B.Entries.Views.Form = B.Views.Form.extend({ // {{{
- // Cache template function.
- template: _.template($('#entry-form-template').html()),
- render: function () {
- var entry, tags, dateSettings = {};
- if (this.model) {
- entry = this.model.toJSON();
- } else {
- entry = {amount: '', date: '', note: '', tags: []};
- // Set datepicker's default date to collection's current month.
- if (!this.collection.inCurrMonth($.datepick.today())) {
- dateSettings.defaultDate = new Date(this.collection.currMonth);
- dateSettings.defaultDate.setDate(1);
- }
- }
- this.$el.html(this.template(entry));
- this.$('input[name=date]').datepick(dateSettings);
- // Initialize multiselect widget for tags.
- tags = B.tags.chain().map(function (tag) {
- return [
- tag.id,
- tag.get('name'),
- _.include(entry.tags, tag.id)
- ];
- }).sortBy(function (item) {
- return item[1];
- }).value();
- this.$('div.multiselect-tags').multiselect(tags, {
- notSelectedText: '(no tags selected)',
- noItemsText: '(no tags)'
- });
- B.tags.on('add', this.updateMultiselect, this);
- B.tags.on('change:name', this.updateMultiselect, this);
- B.tags.on('remove:multiple', this.updateMultiselect, this);
- return this;
- },
- getValues: function () {
- var values = B.Views.Form.prototype.getValues.call(this);
- values.tags = this.$('div.multiselect-tags').multiselect('getChecked');
- return values;
- },
- updateMultiselect: function () {
- var tags = B.tags.chain().map(function (tag) {
- return [tag.id, tag.get('name')];
- }).sortBy(function (item) {
- return item[1];
- }).value();
- this.$('div.multiselect-tags').multiselect('update', tags);
- }
- }); // }}}
- // Add entry form view.
- B.Entries.Views.AddForm = B.Entries.Views.Form.extend({ // {{{
- remove: function () {
- B.tags.off('add', this.updateMultiselect);
- B.tags.off('change:name', this.updateMultiselect);
- B.tags.off('remove:multiple', this.updateMultiselect);
- this.$('div.multiselect-tags').multiselect('destroy');
- this.$el.remove();
- this.trigger('edit:end');
- },
- saveChanges: function () {
- var model, func,
- collection = this.collection,
- entry = this.getValues();
- // Check if date belongs to the current month. If not, don't add model
- // to the collection.
- if (collection.inCurrMonth(entry.date)) {
- func = _.bind(collection.create, collection);
- } else {
- model = new B.Entries.Model();
- model.urlRoot = B.Entries.URL;
- func = _.bind(model.save, model);
- }
- // Request total amount for current month.
- if (collection.inCurrMonth(entry.date)) {
- entry.totalForMonth = $.datepick.formatDate(collection.currMonth);
- }
- func(entry, {
- wait: true,
- success: _.bind(function (model, resp) {
- // Update total amount for current month.
- if (model.has('totalAmount')) {
- collection.setTotalAmount(model.get('totalAmount'));
- // Remove temporary attributes.
- model.unset('totalAmount', {silent: true});
- model.unset('totalForMonth', {silent: true});
- }
- this.remove();
- $.sticky('Added new entry');
- }, this),
- error: function (model, resp) {
- $.sticky(resp, {category: 'error'});
- }
- });
- }
- }); // }}}
- // Edit entry form view.
- B.Entries.Views.EditForm = B.Entries.Views.Form.extend({ // {{{
- initialize: function () {
- // Uncheck entry before editing.
- this.model.set({checked: false});
- this.visible = true;
- },
- remove: function () {
- if (this.visible) {
- var row = new B.Entries.Views.Row({model: this.model});
- this.$el.after(row.render().el);
- }
- B.tags.off('add', this.updateMultiselect);
- B.tags.off('change:name', this.updateMultiselect);
- B.tags.off('remove:multiple', this.updateMultiselect);
- this.$('div.multiselect-tags').multiselect('destroy');
- this.$el.remove();
- this.model.trigger('edit:end');
- },
- saveChanges: function () {
- var collection = this.model.collection,
- entry = this.getValues();
- // Check if date belongs to the current month. If not, remove model
- // from the collection.
- if (!collection.inCurrMonth(entry.date)) {
- this.visible = false;
- }
- // Request total amount for current month.
- entry.totalForMonth = $.datepick.formatDate(collection.currMonth);
- this.model.save(entry, {
- wait: true,
- success: _.bind(function (model, resp) {
- // Update total amount for current month.
- collection.setTotalAmount(model.get('totalAmount'));
- this.remove();
- if (this.visible) {
- // Remove temporary attributes.
- model.unset('totalAmount', {silent: true});
- model.unset('totalForMonth', {silent: true});
- model.trigger('sort');
- } else {
- collection.remove(model);
- }
- $.sticky('Changed an entry');
- }, this),
- error: _.bind(function (model, resp) {
- this.visible = true;
- if (typeof resp === 'object') {
- resp = JSON.parse(resp.responseText).error;
- }
- $.sticky(resp, {category: 'error'});
- }, this)
- });
- }
- }); // }}}
- B.Entries.Views.Table = B.Views.Table.extend({ // {{{
- rowFactory: function () {
- return B.Entries.Views.Row;
- },
- initialize: function () {
- B.Views.Table.prototype.initialize.call(this);
- this.setElement($('#entries table'));
- this.collection.on('change:total', function () {
- this.$('td.total-amount').text(this.collection.totalAmount);
- this.$('tfoot tr').show();
- }, this);
- },
- render: function () {
- this.$('caption')
- .text($.datepick.formatDate('MM yyyy', this.collection.currMonth));
- if (this.collection.isEmpty()) {
- this.$('tfoot tr').hide();
- } else {
- this.$('td.total-amount').text(this.collection.totalAmount);
- this.$('tfoot tr').show();
- }
- return B.Views.Table.prototype.render.call(this);
- },
- showEmptyNotice: function () {
- B.Views.Table.prototype.showEmptyNotice.call(this);
- if (this.collection.isEmpty()) {
- this.$('tfoot tr').hide();
- }
- }
- }); // }}}
- B.Entries.Views.Tab = Backbone.View.extend({ // {{{
- events: {
- 'click #show-add-entry': 'showAddForm',
- 'click #delete-entries': 'deleteEntries',
- 'click #next-month': 'nextMonth',
- 'click #previous-month': 'previousMonth',
- 'click #current-month': 'currentMonth'
- },
- initialize: function () {
- this.setElement($('#entries'));
- this.collection = new B.Entries.Collection();
- this.table = new B.Entries.Views.Table({
- collection: this.collection
- });
- // Populate collection with entries.
- this.collection.totalAmount = DATA.totalAmount;
- this.collection.reset(DATA.entries);
- // Make functions for enabling/disabling input elements.
- this.enable = _.bind(this.setDisabled, this, false);
- this.disable = _.bind(this.setDisabled, this, true);
- this.collection.on('change:checked', this.toggleDeleteEntries, this);
- this.collection.on('change:checked', this.toggleCheckAll, this);
- this.collection.on('edit:start', this.disable);
- this.collection.on('edit:end', this.enable);
- this.collection.on('reset', this.enable);
- },
- // Disable input elements while displaying add/edit form.
- setDisabled: function (disable) {
- this.$('nav button:not(#delete-entries)').disabled(disable);
- this.$('tr > :first-child > input[type=checkbox]')
- .prop('disabled', disable);
- this.$('tbody div.edit').prop('disabled', disable);
- if (!disable) {
- disable = _.isEmpty(this.collection.checked());
- }
- this.$('#delete-entries').disabled(disable);
- },
- showAddForm: function () {
- var form = new B.Entries.Views.AddForm({
- collection: this.collection
- });
- form.on('edit:end', function () {
- if (this.collection.isEmpty()) {
- this.$('tr.empty-msg').show();
- }
- this.enable();
- }, this);
- this.disable();
- this.$('tr.empty-msg').hide();
- this.$('tbody').prepend(form.render().el);
- form.focus();
- },
- toggleCheckAll: function () {
- this.$('input.check-all').prop('checked',
- this.collection.length === this.collection.checked().length);
- },
- toggleDeleteEntries: function () {
- this.$('#delete-entries')
- .disabled(_.isEmpty(this.collection.checked()));
- },
- deleteEntries: function () {
- var checkedIds = this.collection.checkedIds();
- $.ajax({
- type: 'DELETE',
- contentType: 'application/json',
- dataType: 'json',
- data: JSON.stringify({
- ids: checkedIds,
- // Request total amount for current month.
- totalForMonth: $.datepick.formatDate(this.collection.currMonth)
- }),
- url: B.Entries.URL,
- success: _.bind(function (resp) {
- var count = checkedIds.length;
- this.$('#delete-entries').disabled(true);
- this.$('input.check-all').prop('checked', false);
- // Update total amount for current month.
- this.collection.setTotalAmount(resp.totalAmount);
- this.collection.remove(checkedIds);
- $.sticky('Removed ' + count +
- (count > 1 ? ' entries' : ' entry'));
- }, this),
- error: function (xhr, textStatus, errorThrown) {
- // TODO: notify about error.
- }
- });
- },
- nextMonth: function () {
- this.collection.nextMonth();
- this.router.navigateEntries();
- },
- previousMonth: function () {
- this.collection.previousMonth();
- this.router.navigateEntries();
- },
- currentMonth: function () {
- this.collection.setMonth($.datepick.today());
- this.router.navigateEntries();
- }
- }); // }}}