PageRenderTime 64ms CodeModel.GetById 31ms RepoModel.GetById 1ms app.codeStats 0ms

/beancounter/static/js/entries.js

https://bitbucket.org/xamox/beancounter
JavaScript | 476 lines | 373 code | 63 blank | 40 comment | 23 complexity | 0ed179ed3946a3422ed249be4325087d MD5 | raw file
Possible License(s): 0BSD
  1. // vim: fdm=marker
  2. /*
  3. * Copyright (c) 2011-2012 Audrius KaĹžukauskas
  4. *
  5. * This file is part of BeanCounter and is released under
  6. * the ISC license, see LICENSE for more details.
  7. */
  8. /*jslint indent: 4, browser: true, nomen: true, sloppy: true */
  9. /*global $, _, Backbone, B, DATA */
  10. B.Entries = {};
  11. // Entry resource URL.
  12. B.Entries.URL = '/api/entries';
  13. B.Entries.Model = Backbone.Model.extend({ // {{{
  14. defaults: {
  15. amount: '0.00',
  16. note: '',
  17. tags: [],
  18. checked: false
  19. },
  20. validate: function (attrs) {
  21. if (attrs.hasOwnProperty('amount')) {
  22. var amount = $.trim(attrs.amount);
  23. if (!amount) {
  24. return 'Amount is required';
  25. }
  26. if (!amount.match(/^\d+\.?\d{0,2}$/)) {
  27. return 'Amount should be in format #.##';
  28. }
  29. }
  30. },
  31. toJSON: function () {
  32. var attrs = _.clone(this.attributes);
  33. delete attrs.checked;
  34. if (attrs.amount) {
  35. attrs.amount = parseFloat(attrs.amount).toFixed(2);
  36. }
  37. return attrs;
  38. },
  39. toggleChecked: function () {
  40. this.set({checked: !this.get('checked')});
  41. },
  42. removeTags: function () {
  43. var ids, tags;
  44. ids = B.tags.pluck('id');
  45. tags = _.filter(this.get('tags'), function (tag) {
  46. return _.include(ids, tag);
  47. });
  48. this.set({tags: tags});
  49. }
  50. }); // }}}
  51. B.Entries.Collection = B.Collection.extend({ // {{{
  52. model: B.Entries.Model,
  53. url: B.Entries.URL,
  54. initialize: function () {
  55. // Set current month.
  56. this.currMonth = $.datepick.parseDate('yyyy-mm-dd', DATA.currDate);
  57. },
  58. parse: function (resp) {
  59. this.totalAmount = resp.totalAmount;
  60. return resp.entries;
  61. },
  62. // Update collection with entries for the current month.
  63. update: function () {
  64. this.fetch({data: {
  65. total: 1,
  66. month: $.datepick.formatDate('yyyy-mm', this.currMonth)
  67. }});
  68. },
  69. // Update collection with entries for the next month.
  70. nextMonth: function () {
  71. $.datepick.add(this.currMonth, 1, 'm');
  72. this.update();
  73. },
  74. // Update collection with entries for the previous month.
  75. previousMonth: function () {
  76. $.datepick.add(this.currMonth, -1, 'm');
  77. this.update();
  78. },
  79. setMonth: function (date) {
  80. if (!this.inCurrMonth(date)) {
  81. this.currMonth = date;
  82. this.update();
  83. }
  84. },
  85. // Check if given date belongs to current month.
  86. inCurrMonth: function (date) {
  87. if (_.isString(date)) {
  88. date = $.datepick.parseDate('yyyy-mm-dd', date);
  89. }
  90. return this.currMonth.getFullYear() === date.getFullYear() &&
  91. this.currMonth.getMonth() === date.getMonth();
  92. },
  93. setTotalAmount: function (total) {
  94. this.totalAmount = total;
  95. this.trigger('change:total');
  96. }
  97. }); // }}}
  98. B.Entries.Views = {};
  99. B.Entries.Views.Row = B.Views.Row.extend({ // {{{
  100. // Cache template function.
  101. template: _.template($('#entry-row-template').html()),
  102. initialize: function () {
  103. B.Views.Row.prototype.initialize.call(this);
  104. B.tags.on('change:name', this.renderTags, this);
  105. B.tags.on('remove:multiple', this.model.removeTags, this.model);
  106. this.model.on('change:tags', this.renderTags, this);
  107. },
  108. remove: function () {
  109. B.tags.off('change:name', this.renderTags);
  110. B.tags.off('remove:multiple', this.model.removeTags);
  111. this.model.off('change:tags', this.renderTags);
  112. B.Views.Row.prototype.remove.call(this);
  113. },
  114. render: function () {
  115. B.Views.Row.prototype.render.call(this);
  116. this.renderTags();
  117. return this;
  118. },
  119. renderTags: function () {
  120. var tags = this.model.get('tags').map(function (id) {
  121. return B.tags.get(id).get('name');
  122. }).sort();
  123. this.$('td.tags div')
  124. .text(tags.join(', '))
  125. // Firefox ignores line ends in title attribute.
  126. .attr('title', tags.join(', \n'));
  127. },
  128. formFactory: function () {
  129. return B.Entries.Views.EditForm;
  130. }
  131. }); // }}}
  132. // Base view for entry forms.
  133. B.Entries.Views.Form = B.Views.Form.extend({ // {{{
  134. // Cache template function.
  135. template: _.template($('#entry-form-template').html()),
  136. render: function () {
  137. var entry, tags, dateSettings = {};
  138. if (this.model) {
  139. entry = this.model.toJSON();
  140. } else {
  141. entry = {amount: '', date: '', note: '', tags: []};
  142. // Set datepicker's default date to collection's current month.
  143. if (!this.collection.inCurrMonth($.datepick.today())) {
  144. dateSettings.defaultDate = new Date(this.collection.currMonth);
  145. dateSettings.defaultDate.setDate(1);
  146. }
  147. }
  148. this.$el.html(this.template(entry));
  149. this.$('input[name=date]').datepick(dateSettings);
  150. // Initialize multiselect widget for tags.
  151. tags = B.tags.chain().map(function (tag) {
  152. return [
  153. tag.id,
  154. tag.get('name'),
  155. _.include(entry.tags, tag.id)
  156. ];
  157. }).sortBy(function (item) {
  158. return item[1];
  159. }).value();
  160. this.$('div.multiselect-tags').multiselect(tags, {
  161. notSelectedText: '(no tags selected)',
  162. noItemsText: '(no tags)'
  163. });
  164. B.tags.on('add', this.updateMultiselect, this);
  165. B.tags.on('change:name', this.updateMultiselect, this);
  166. B.tags.on('remove:multiple', this.updateMultiselect, this);
  167. return this;
  168. },
  169. getValues: function () {
  170. var values = B.Views.Form.prototype.getValues.call(this);
  171. values.tags = this.$('div.multiselect-tags').multiselect('getChecked');
  172. return values;
  173. },
  174. updateMultiselect: function () {
  175. var tags = B.tags.chain().map(function (tag) {
  176. return [tag.id, tag.get('name')];
  177. }).sortBy(function (item) {
  178. return item[1];
  179. }).value();
  180. this.$('div.multiselect-tags').multiselect('update', tags);
  181. }
  182. }); // }}}
  183. // Add entry form view.
  184. B.Entries.Views.AddForm = B.Entries.Views.Form.extend({ // {{{
  185. remove: function () {
  186. B.tags.off('add', this.updateMultiselect);
  187. B.tags.off('change:name', this.updateMultiselect);
  188. B.tags.off('remove:multiple', this.updateMultiselect);
  189. this.$('div.multiselect-tags').multiselect('destroy');
  190. this.$el.remove();
  191. this.trigger('edit:end');
  192. },
  193. saveChanges: function () {
  194. var model, func,
  195. collection = this.collection,
  196. entry = this.getValues();
  197. // Check if date belongs to the current month. If not, don't add model
  198. // to the collection.
  199. if (collection.inCurrMonth(entry.date)) {
  200. func = _.bind(collection.create, collection);
  201. } else {
  202. model = new B.Entries.Model();
  203. model.urlRoot = B.Entries.URL;
  204. func = _.bind(model.save, model);
  205. }
  206. // Request total amount for current month.
  207. if (collection.inCurrMonth(entry.date)) {
  208. entry.totalForMonth = $.datepick.formatDate(collection.currMonth);
  209. }
  210. func(entry, {
  211. wait: true,
  212. success: _.bind(function (model, resp) {
  213. // Update total amount for current month.
  214. if (model.has('totalAmount')) {
  215. collection.setTotalAmount(model.get('totalAmount'));
  216. // Remove temporary attributes.
  217. model.unset('totalAmount', {silent: true});
  218. model.unset('totalForMonth', {silent: true});
  219. }
  220. this.remove();
  221. $.sticky('Added new entry');
  222. }, this),
  223. error: function (model, resp) {
  224. $.sticky(resp, {category: 'error'});
  225. }
  226. });
  227. }
  228. }); // }}}
  229. // Edit entry form view.
  230. B.Entries.Views.EditForm = B.Entries.Views.Form.extend({ // {{{
  231. initialize: function () {
  232. // Uncheck entry before editing.
  233. this.model.set({checked: false});
  234. this.visible = true;
  235. },
  236. remove: function () {
  237. if (this.visible) {
  238. var row = new B.Entries.Views.Row({model: this.model});
  239. this.$el.after(row.render().el);
  240. }
  241. B.tags.off('add', this.updateMultiselect);
  242. B.tags.off('change:name', this.updateMultiselect);
  243. B.tags.off('remove:multiple', this.updateMultiselect);
  244. this.$('div.multiselect-tags').multiselect('destroy');
  245. this.$el.remove();
  246. this.model.trigger('edit:end');
  247. },
  248. saveChanges: function () {
  249. var collection = this.model.collection,
  250. entry = this.getValues();
  251. // Check if date belongs to the current month. If not, remove model
  252. // from the collection.
  253. if (!collection.inCurrMonth(entry.date)) {
  254. this.visible = false;
  255. }
  256. // Request total amount for current month.
  257. entry.totalForMonth = $.datepick.formatDate(collection.currMonth);
  258. this.model.save(entry, {
  259. wait: true,
  260. success: _.bind(function (model, resp) {
  261. // Update total amount for current month.
  262. collection.setTotalAmount(model.get('totalAmount'));
  263. this.remove();
  264. if (this.visible) {
  265. // Remove temporary attributes.
  266. model.unset('totalAmount', {silent: true});
  267. model.unset('totalForMonth', {silent: true});
  268. model.trigger('sort');
  269. } else {
  270. collection.remove(model);
  271. }
  272. $.sticky('Changed an entry');
  273. }, this),
  274. error: _.bind(function (model, resp) {
  275. this.visible = true;
  276. if (typeof resp === 'object') {
  277. resp = JSON.parse(resp.responseText).error;
  278. }
  279. $.sticky(resp, {category: 'error'});
  280. }, this)
  281. });
  282. }
  283. }); // }}}
  284. B.Entries.Views.Table = B.Views.Table.extend({ // {{{
  285. rowFactory: function () {
  286. return B.Entries.Views.Row;
  287. },
  288. initialize: function () {
  289. B.Views.Table.prototype.initialize.call(this);
  290. this.setElement($('#entries table'));
  291. this.collection.on('change:total', function () {
  292. this.$('td.total-amount').text(this.collection.totalAmount);
  293. this.$('tfoot tr').show();
  294. }, this);
  295. },
  296. render: function () {
  297. this.$('caption')
  298. .text($.datepick.formatDate('MM yyyy', this.collection.currMonth));
  299. if (this.collection.isEmpty()) {
  300. this.$('tfoot tr').hide();
  301. } else {
  302. this.$('td.total-amount').text(this.collection.totalAmount);
  303. this.$('tfoot tr').show();
  304. }
  305. return B.Views.Table.prototype.render.call(this);
  306. },
  307. showEmptyNotice: function () {
  308. B.Views.Table.prototype.showEmptyNotice.call(this);
  309. if (this.collection.isEmpty()) {
  310. this.$('tfoot tr').hide();
  311. }
  312. }
  313. }); // }}}
  314. B.Entries.Views.Tab = Backbone.View.extend({ // {{{
  315. events: {
  316. 'click #show-add-entry': 'showAddForm',
  317. 'click #delete-entries': 'deleteEntries',
  318. 'click #next-month': 'nextMonth',
  319. 'click #previous-month': 'previousMonth',
  320. 'click #current-month': 'currentMonth'
  321. },
  322. initialize: function () {
  323. this.setElement($('#entries'));
  324. this.collection = new B.Entries.Collection();
  325. this.table = new B.Entries.Views.Table({
  326. collection: this.collection
  327. });
  328. // Populate collection with entries.
  329. this.collection.totalAmount = DATA.totalAmount;
  330. this.collection.reset(DATA.entries);
  331. // Make functions for enabling/disabling input elements.
  332. this.enable = _.bind(this.setDisabled, this, false);
  333. this.disable = _.bind(this.setDisabled, this, true);
  334. this.collection.on('change:checked', this.toggleDeleteEntries, this);
  335. this.collection.on('change:checked', this.toggleCheckAll, this);
  336. this.collection.on('edit:start', this.disable);
  337. this.collection.on('edit:end', this.enable);
  338. this.collection.on('reset', this.enable);
  339. },
  340. // Disable input elements while displaying add/edit form.
  341. setDisabled: function (disable) {
  342. this.$('nav button:not(#delete-entries)').disabled(disable);
  343. this.$('tr > :first-child > input[type=checkbox]')
  344. .prop('disabled', disable);
  345. this.$('tbody div.edit').prop('disabled', disable);
  346. if (!disable) {
  347. disable = _.isEmpty(this.collection.checked());
  348. }
  349. this.$('#delete-entries').disabled(disable);
  350. },
  351. showAddForm: function () {
  352. var form = new B.Entries.Views.AddForm({
  353. collection: this.collection
  354. });
  355. form.on('edit:end', function () {
  356. if (this.collection.isEmpty()) {
  357. this.$('tr.empty-msg').show();
  358. }
  359. this.enable();
  360. }, this);
  361. this.disable();
  362. this.$('tr.empty-msg').hide();
  363. this.$('tbody').prepend(form.render().el);
  364. form.focus();
  365. },
  366. toggleCheckAll: function () {
  367. this.$('input.check-all').prop('checked',
  368. this.collection.length === this.collection.checked().length);
  369. },
  370. toggleDeleteEntries: function () {
  371. this.$('#delete-entries')
  372. .disabled(_.isEmpty(this.collection.checked()));
  373. },
  374. deleteEntries: function () {
  375. var checkedIds = this.collection.checkedIds();
  376. $.ajax({
  377. type: 'DELETE',
  378. contentType: 'application/json',
  379. dataType: 'json',
  380. data: JSON.stringify({
  381. ids: checkedIds,
  382. // Request total amount for current month.
  383. totalForMonth: $.datepick.formatDate(this.collection.currMonth)
  384. }),
  385. url: B.Entries.URL,
  386. success: _.bind(function (resp) {
  387. var count = checkedIds.length;
  388. this.$('#delete-entries').disabled(true);
  389. this.$('input.check-all').prop('checked', false);
  390. // Update total amount for current month.
  391. this.collection.setTotalAmount(resp.totalAmount);
  392. this.collection.remove(checkedIds);
  393. $.sticky('Removed ' + count +
  394. (count > 1 ? ' entries' : ' entry'));
  395. }, this),
  396. error: function (xhr, textStatus, errorThrown) {
  397. // TODO: notify about error.
  398. }
  399. });
  400. },
  401. nextMonth: function () {
  402. this.collection.nextMonth();
  403. this.router.navigateEntries();
  404. },
  405. previousMonth: function () {
  406. this.collection.previousMonth();
  407. this.router.navigateEntries();
  408. },
  409. currentMonth: function () {
  410. this.collection.setMonth($.datepick.today());
  411. this.router.navigateEntries();
  412. }
  413. }); // }}}