Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 
 
 

447 рядки
13 KiB

  1. // Copyright (c) 2012 Web Notes Technologies Pvt Ltd (http://erpnext.com)
  2. //
  3. // MIT License (MIT)
  4. //
  5. // Permission is hereby granted, free of charge, to any person obtaining a
  6. // copy of this software and associated documentation files (the "Software"),
  7. // to deal in the Software without restriction, including without limitation
  8. // the rights to use, copy, modify, merge, publish, distribute, sublicense,
  9. // and/or sell copies of the Software, and to permit persons to whom the
  10. // Software is furnished to do so, subject to the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be included in
  13. // all copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  16. // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
  17. // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  18. // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
  19. // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
  20. // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  21. //
  22. wn.provide('wn.views.doclistview');
  23. wn.provide('wn.doclistviews');
  24. wn.views.doclistview.pages = {};
  25. wn.views.doclistview.show = function(doctype) {
  26. var pagename = doctype + ' List';
  27. var doctype = get_label_doctype(doctype);
  28. wn.model.with_doctype(doctype, function() {
  29. var page = wn.views.doclistview.pages[pagename];
  30. if(!page) {
  31. var page = wn.container.add_page(pagename);
  32. page.doclistview = new wn.views.DocListView(doctype, page);
  33. wn.views.doclistview.pages[pagename] = page;
  34. }
  35. document.title = page.doclistview.label;
  36. wn.container.change_to(pagename);
  37. })
  38. }
  39. wn.views.DocListView = wn.ui.Listing.extend({
  40. init: function(doctype, page) {
  41. this.doctype = doctype;
  42. this.$page = $(page);
  43. this.label = get_doctype_label(doctype);
  44. this.label = (this.label.toLowerCase().substr(-4) == 'list') ?
  45. this.label : (this.label + ' List');
  46. this.make_page();
  47. this.setup();
  48. },
  49. make_page: function() {
  50. var me = this;
  51. this.$page.html(repl('<div class="layout-wrapper layout-wrapper-background">\
  52. <div class="appframe-area"></div>\
  53. <div class="layout-main-section">\
  54. <h1>%(label)s</h1>\
  55. <hr>\
  56. <div class="wnlist-area"><div class="help">Loading...</div></div>\
  57. </div>\
  58. <div class="layout-side-section">\
  59. <div class="stat-wrapper show-docstatus hide">\
  60. <h4>Show</h4>\
  61. <div><input data-docstatus="0" type="checkbox" checked="checked" /> Drafts</div>\
  62. <div><input data-docstatus="1" type="checkbox" checked="checked" /> Submitted</div>\
  63. <div><input data-docstatus="2" type="checkbox" /> Cancelled</div>\
  64. </div>\
  65. </div>\
  66. <div style="clear: both"></div>\
  67. </div>', {label: this.label}));
  68. this.appframe = new wn.views.AppFrame(this.$page.find('.appframe-area'));
  69. wn.views.breadcrumbs($('<span>').appendTo(this.appframe.$titlebar), locals.DocType[this.doctype].module);
  70. },
  71. setup: function() {
  72. var me = this;
  73. me.can_delete = wn.model.can_delete(me.doctype);
  74. me.meta = locals.DocType[me.doctype];
  75. me.$page.find('.wnlist-area').empty(),
  76. me.setup_docstatus_filter();
  77. me.setup_listview();
  78. me.init_list();
  79. me.init_stats();
  80. me.add_delete_option();
  81. },
  82. setup_docstatus_filter: function() {
  83. var me = this;
  84. this.can_submit = $.map(locals.DocPerm, function(d) {
  85. if(d.parent==me.meta.name && d.submit) return 1
  86. else return null;
  87. }).length;
  88. if(this.can_submit) {
  89. this.$page.find('.show-docstatus').removeClass('hide');
  90. this.$page.find('.show-docstatus input').click(function() {
  91. me.run();
  92. })
  93. }
  94. },
  95. setup_listview: function() {
  96. if(this.meta.__listjs) {
  97. eval(this.meta.__listjs);
  98. this.listview = new wn.doclistviews[this.doctype](this);
  99. } else {
  100. this.listview = new wn.views.ListView(this);
  101. }
  102. this.listview.parent = this;
  103. },
  104. init_list: function() {
  105. // init list
  106. this.make({
  107. method: 'webnotes.widgets.doclistview.get',
  108. get_args: this.get_args,
  109. parent: this.$page.find('.wnlist-area'),
  110. start: 0,
  111. page_length: 20,
  112. show_filters: true,
  113. show_grid: true,
  114. new_doctype: this.doctype,
  115. allow_delete: true,
  116. no_result_message: this.make_no_result(),
  117. columns: this.listview.fields
  118. });
  119. this.run();
  120. },
  121. make_no_result: function() {
  122. return repl('<div class="well"><p>No %(doctype_label)s found</p>\
  123. %(description)s\
  124. <hr>\
  125. <p><button class="btn btn-info btn-small"\
  126. onclick="wn.set_route(\'Form\', \'%(doctype)s\', \'New %(doctype)s\');"\
  127. >Make a new %(doctype_label)s</button>\
  128. </p></div>', {
  129. doctype_label: get_doctype_label(this.doctype),
  130. doctype: this.doctype,
  131. description: wn.markdown(locals.DocType[this.doctype].description || '')
  132. });
  133. },
  134. render_row: function(row, data) {
  135. data.doctype = this.doctype;
  136. this.listview.render(row, data, this);
  137. },
  138. get_query_fields: function() {
  139. return this.listview.fields;
  140. },
  141. get_args: function() {
  142. return {
  143. doctype: this.doctype,
  144. fields: this.get_query_fields(),
  145. filters: this.filter_list.get_filters(),
  146. docstatus: this.can_submit ? $.map(this.$page.find('.show-docstatus :checked'),
  147. function(inp) { return $(inp).attr('data-docstatus') }) : []
  148. }
  149. },
  150. add_delete_option: function() {
  151. var me = this;
  152. if(this.can_delete) {
  153. this.add_button('<a class="btn btn-small btn-delete">\
  154. <i class="icon-remove"></i> Delete</a>',
  155. function() { me.delete_items(); },'.btn-filter')
  156. }
  157. },
  158. delete_items: function() {
  159. var me = this;
  160. var dl = $.map(me.$page.find('.list-delete:checked'), function(e) {
  161. return $(e).data('name');
  162. });
  163. if(!dl.length)
  164. return;
  165. if(!confirm('This is PERMANENT action and you cannot undo. Continue?')) {
  166. return;
  167. }
  168. me.set_working(true);
  169. wn.call({
  170. method: 'webnotes.widgets.doclistview.delete_items',
  171. args: {
  172. items: dl,
  173. doctype: me.doctype
  174. },
  175. callback: function() {
  176. me.set_working(false);
  177. me.refresh();
  178. }
  179. })
  180. },
  181. init_stats: function() {
  182. var me = this
  183. wn.call({
  184. method: 'webnotes.widgets.doclistview.get_stats',
  185. args: {
  186. stats: me.listview.stats,
  187. doctype: me.doctype
  188. },
  189. callback: function(r) {
  190. // This gives a predictable stats order
  191. $.each(me.listview.stats, function(i, v) {
  192. me.render_stat(v, r.message[v]);
  193. });
  194. // This doesn't give a predictable stats order
  195. /*
  196. $.each(r.message, function(field, stat) {
  197. me.render_stat(field, stat);
  198. });*/
  199. }
  200. });
  201. },
  202. render_stat: function(field, stat) {
  203. var me = this;
  204. if(!stat || !stat.length) {
  205. if(field=='_user_tags') {
  206. this.$page.find('.layout-side-section')
  207. .append('<div class="stat-wrapper"><h4>Tags</h4>\
  208. <div class="help small"><i>No records tagged.</i><br><br> \
  209. To add a tag, open the document and click on \
  210. "Add Tag" on the sidebar</div></div>');
  211. }
  212. return;
  213. }
  214. var label = fields[this.doctype][field] ? fields[this.doctype][field].label : field;
  215. if(label=='_user_tags') label = 'Tags';
  216. // grid
  217. var $w = $('<div class="stat-wrapper">\
  218. <h4>'+ label +'</h4>\
  219. <div class="stat-grid">\
  220. </div>\
  221. </div>');
  222. // sort items
  223. stat = stat.sort(function(a, b) { return b[1] - a[1] });
  224. var sum = 0;
  225. $.each(stat, function(i,v) { sum = sum + v[1]; })
  226. // render items
  227. $.each(stat, function(i, v) {
  228. me.render_stat_item(i, v, sum, field).appendTo($w.find('.stat-grid'));
  229. });
  230. $w.appendTo(this.$page.find('.layout-side-section'));
  231. },
  232. render_stat_item: function(i, v, max, field) {
  233. var me = this;
  234. var args = {}
  235. args.label = v[0];
  236. args.width = flt(v[1]) / max * 100;
  237. args.count = v[1];
  238. args.field = field;
  239. $item = $(repl('<div class="stat-item">\
  240. <div class="stat-bar" style="width: %(width)s%"></div>\
  241. <div class="stat-label">\
  242. <a href="#" data-label="%(label)s" data-field="%(field)s">\
  243. %(label)s</a> \
  244. (%(count)s)</div>\
  245. </div>', args));
  246. this.setup_stat_item_click($item);
  247. return $item;
  248. },
  249. setup_stat_item_click: function($item) {
  250. var me = this;
  251. $item.find('a').click(function() {
  252. var fieldname = $(this).attr('data-field');
  253. var label = $(this).attr('data-label');
  254. me.set_filter(fieldname, label);
  255. return false;
  256. });
  257. },
  258. set_filter: function(fieldname, label) {
  259. var filter = this.filter_list.get_filter(fieldname);
  260. if(filter) {
  261. var v = filter.field.get_value();
  262. if(v.indexOf(label)!=-1) {
  263. // already set
  264. return false;
  265. } else {
  266. // second filter set for this field
  267. if(fieldname=='_user_tags') {
  268. // and for tags
  269. this.filter_list.add_filter(fieldname, 'like', '%' + label);
  270. } else {
  271. // or for rest using "in"
  272. filter.set_values(fieldname, 'in', v + ', ' + label);
  273. }
  274. }
  275. } else {
  276. // no filter for this item,
  277. // setup one
  278. if(fieldname=='_user_tags') {
  279. this.filter_list.add_filter(fieldname, 'like', '%' + label);
  280. } else {
  281. this.filter_list.add_filter(fieldname, '=', label);
  282. }
  283. }
  284. this.run();
  285. }
  286. });
  287. wn.views.ListView = Class.extend({
  288. init: function(doclistview) {
  289. this.doclistview = doclistview;
  290. this.doctype = doclistview.doctype;
  291. var t = "`tab"+this.doctype+"`.";
  292. this.fields = [t + 'name', t + 'owner', t + 'docstatus',
  293. t + '_user_tags', t + 'modified'];
  294. this.stats = ['_user_tags'];
  295. if(!this.doclistview.can_delete) {
  296. this.columns = $.map(this.columns, function(v, i) { if(v.content!='check') return v });
  297. }
  298. },
  299. columns: [
  300. {width: '3%', content:'check'},
  301. {width: '4%', content:'avatar'},
  302. {width: '3%', content:'docstatus', css: {"text-align": "center"}},
  303. {width: '35%', content:'name'},
  304. {width: '40%', content:'tags', css: {'color':'#aaa'}},
  305. {width: '15%', content:'modified', css: {'text-align': 'right', 'color':'#777'}}
  306. ],
  307. render_column: function(data, parent, opts) {
  308. var me = this;
  309. // style
  310. if(opts.css) {
  311. $.each(opts.css, function(k, v) { $(parent).css(k, v)});
  312. }
  313. // multiple content
  314. if(opts.content.indexOf && opts.content.indexOf('+')!=-1) {
  315. $.map(opts.content.split('+'), function(v) {
  316. me.render_column(data, parent, {content:v});
  317. });
  318. return;
  319. }
  320. // content
  321. if(typeof opts.content=='function') {
  322. opts.content(parent, data);
  323. }
  324. else if(opts.content=='name') {
  325. $(parent).append(repl('<a href="#!Form/%(doctype)s/%(name)s">%(name)s</a>', data));
  326. }
  327. else if(opts.content=='avatar') {
  328. $(parent).append(repl('<span class="avatar-small"><img src="%(avatar)s" \
  329. title="%(fullname)s"/></span>',
  330. data));
  331. }
  332. else if(opts.content=='check') {
  333. $(parent).append('<input class="list-delete" type="checkbox">');
  334. $(parent).find('input').data('name', data.name);
  335. }
  336. else if(opts.content=='docstatus') {
  337. $(parent).append(repl('<span class="docstatus"><i class="%(docstatus_icon)s" \
  338. title="%(docstatus_title)s"></i></span>', data));
  339. }
  340. else if(opts.content=='tags') {
  341. this.add_user_tags(parent, data);
  342. }
  343. else if(opts.content=='modified') {
  344. $(parent).append(data.when);
  345. }
  346. else if(opts.type=='bar-graph') {
  347. args = {
  348. percent: data[opts.content],
  349. fully_delivered: (data[opts.content] > 99 ? 'bar-complete' : ''),
  350. label: opts.label
  351. }
  352. $(parent).append(repl('<span class="bar-outer" style="width: 30px; float: right" \
  353. title="%(percent)s% %(label)s">\
  354. <span class="bar-inner %(fully_delivered)s" \
  355. style="width: %(percent)s%;"></span>\
  356. </span>', args));
  357. }
  358. else if(data[opts.content]) {
  359. $(parent).append(' ' + data[opts.content]);
  360. }
  361. },
  362. render: function(row, data) {
  363. var me = this;
  364. this.prepare_data(data);
  365. rowhtml = '';
  366. // make table
  367. $.each(this.columns, function(i, v) {
  368. rowhtml += repl('<td style="width: %(width)s"></td>', v);
  369. });
  370. var tr = $(row).html('<table><tbody><tr>' + rowhtml + '</tr></tbody></table>').find('tr').get(0);
  371. // render cells
  372. $.each(this.columns, function(i, v) {
  373. me.render_column(data, tr.cells[i], v);
  374. });
  375. },
  376. prepare_data: function(data) {
  377. data.fullname = wn.user_info(data.owner).fullname;
  378. data.avatar = wn.user_info(data.owner).image;
  379. // when
  380. data.when = dateutil.str_to_user(data.modified).split(' ')[0];
  381. var diff = dateutil.get_diff(dateutil.get_today(), data.modified.split(' ')[0]);
  382. if(diff==0) {
  383. data.when = 'Today'
  384. }
  385. if(diff == 1) {
  386. data.when = 'Yesterday'
  387. }
  388. if(diff == 2) {
  389. data.when = '2 days ago'
  390. }
  391. // docstatus
  392. if(data.docstatus==0 || data.docstatus==null) {
  393. data.docstatus_icon = 'icon-pencil';
  394. data.docstatus_title = 'Editable';
  395. } else if(data.docstatus==1) {
  396. data.docstatus_icon = 'icon-lock';
  397. data.docstatus_title = 'Submitted';
  398. } else if(data.docstatus==2) {
  399. data.docstatus_icon = 'icon-remove';
  400. data.docstatus_title = 'Cancelled';
  401. }
  402. },
  403. add_user_tags: function(parent, data) {
  404. var me = this;
  405. if(data._user_tags) {
  406. $.each(data._user_tags.split(','), function(i, t) {
  407. if(t) {
  408. $('<span class="label label-info" style="cursor: pointer">'
  409. + strip(t) + '</span>')
  410. .click(function() {
  411. me.doclistview.set_filter('_user_tags', $(this).text())
  412. })
  413. .appendTo(parent);
  414. }
  415. });
  416. }
  417. }
  418. })