|
|
@@ -74,6 +74,7 @@ wn.views.GridReport = Class.extend({ |
|
|
|
init: function(opts) { |
|
|
|
this.filter_inputs = {}; |
|
|
|
this.preset_checks = []; |
|
|
|
this.tree_grid = {show: false}; |
|
|
|
$.extend(this, opts); |
|
|
|
|
|
|
|
this.wrapper = $('<div>').appendTo(this.parent); |
|
|
@@ -133,17 +134,25 @@ wn.views.GridReport = Class.extend({ |
|
|
|
me.init_filter_values(); |
|
|
|
me.set_route(); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
}, |
|
|
|
make_waiting: function() { |
|
|
|
this.waiting = $('<div class="well" style="width: 63%; margin: 30px auto;">\ |
|
|
|
<p style="text-align: center;">Loading Report...</p>\ |
|
|
|
<div class="progress progress-striped active">\ |
|
|
|
<div class="bar" style="width: 10%"></div></div>') |
|
|
|
.appendTo(this.wrapper); |
|
|
|
|
|
|
|
init_filter_values: function() { |
|
|
|
var me = this; |
|
|
|
$.each(this.filter_inputs, function(key, filter) { |
|
|
|
var opts = filter.get(0).opts; |
|
|
|
if(sys_defaults[key]) { |
|
|
|
filter.val(sys_defaults[key]); |
|
|
|
} else if(opts.fieldtype=='Select') { |
|
|
|
filter.get(0).selectedIndex = 0; |
|
|
|
} else if(opts.fieldtype=='Data') { |
|
|
|
filter.val(""); |
|
|
|
} |
|
|
|
}) |
|
|
|
if(this.filter_inputs.from_date) |
|
|
|
this.filter_inputs.from_date.val(dateutil.str_to_user(sys_defaults.year_start_date)); |
|
|
|
if(this.filter_inputs.to_date) |
|
|
|
this.filter_inputs.to_date.val(dateutil.str_to_user(sys_defaults.year_end_date)); |
|
|
|
}, |
|
|
|
|
|
|
|
make_filters: function() { |
|
|
|
var me = this; |
|
|
|
$.each(this.filters, function(i, v) { |
|
|
@@ -178,6 +187,14 @@ wn.views.GridReport = Class.extend({ |
|
|
|
me.filter_inputs[v.fieldname] = input; |
|
|
|
}); |
|
|
|
}, |
|
|
|
make_waiting: function() { |
|
|
|
this.waiting = $('<div class="well" style="width: 63%; margin: 30px auto;">\ |
|
|
|
<p style="text-align: center;">Loading Report...</p>\ |
|
|
|
<div class="progress progress-striped active">\ |
|
|
|
<div class="bar" style="width: 10%"></div></div>') |
|
|
|
.appendTo(this.wrapper); |
|
|
|
|
|
|
|
}, |
|
|
|
load_filter_values: function() { |
|
|
|
var me = this; |
|
|
|
$.each(this.filter_inputs, function(i, f) { |
|
|
@@ -191,6 +208,12 @@ wn.views.GridReport = Class.extend({ |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
if(this.filter_inputs.from_date && this.filter_inputs.to_date && (this.to_date < this.from_date)) { |
|
|
|
msgprint("From Date must be before To Date"); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
make_name_map: function(data, key) { |
|
|
|
var map = {}; |
|
|
@@ -200,6 +223,16 @@ wn.views.GridReport = Class.extend({ |
|
|
|
}) |
|
|
|
return map; |
|
|
|
}, |
|
|
|
|
|
|
|
reset_item_values: function(item) { |
|
|
|
var me = this; |
|
|
|
$.each(this.columns, function(i, col) { |
|
|
|
if (col.formatter==me.currency_formatter) { |
|
|
|
item[col.id] = 0; |
|
|
|
} |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
import_slickgrid: function() { |
|
|
|
wn.require('js/lib/slickgrid/slick.grid.css'); |
|
|
|
wn.require('js/lib/slickgrid/slick-default-theme.css'); |
|
|
@@ -207,7 +240,8 @@ wn.views.GridReport = Class.extend({ |
|
|
|
wn.require('js/lib/slickgrid/slick.core.js'); |
|
|
|
wn.require('js/lib/slickgrid/slick.grid.js'); |
|
|
|
wn.require('js/lib/slickgrid/slick.dataview.js'); |
|
|
|
wn.dom.set_style('.slick-cell { font-size: 12px; }'); |
|
|
|
wn.dom.set_style('.slick-cell { font-size: 12px; }'); |
|
|
|
if(this.tree_grid.show) wn.require("js/app/tree_grid.css"); |
|
|
|
}, |
|
|
|
refresh: function() { |
|
|
|
this.waiting.toggle(false); |
|
|
@@ -219,7 +253,10 @@ wn.views.GridReport = Class.extend({ |
|
|
|
this.setup_dataview_columns(); |
|
|
|
this.apply_link_formatters(); |
|
|
|
this.prepare_data(); |
|
|
|
this.prepare_data_view(); |
|
|
|
// plot might need prepared data |
|
|
|
this.wrapper.find(".processing").toggle(true); |
|
|
|
this.wrapper.find(".processing").delay(2000).fadeOut(300); |
|
|
|
this.render(); |
|
|
|
this.render_plot(); |
|
|
|
}, |
|
|
@@ -231,11 +268,13 @@ wn.views.GridReport = Class.extend({ |
|
|
|
make: function() { |
|
|
|
|
|
|
|
// plot wrapper |
|
|
|
$('<div class="plot" style="margin-bottom: 15px; display: none; \ |
|
|
|
this.plot_area = $('<div class="plot" style="margin-bottom: 15px; display: none; \ |
|
|
|
height: 300px; width: 100%;"></div>').appendTo(this.wrapper); |
|
|
|
|
|
|
|
// print / export |
|
|
|
$('<div style="text-align: right;"> \ |
|
|
|
<div class="processing" style="background-color: #fec; display: none; float: left; margin: 2px"> \ |
|
|
|
Updated! </div>\ |
|
|
|
<a href="#" class="grid-report-print"><i class="icon icon-print"></i> Print</a> \ |
|
|
|
<span style="color: #aaa; margin: 0px 10px;"> | </span> \ |
|
|
|
<a href="#" class="grid-report-export"><i class="icon icon-download-alt"></i> Export</a> \ |
|
|
@@ -286,6 +325,10 @@ wn.views.GridReport = Class.extend({ |
|
|
|
+ '=' + encodeURIComponent(val); |
|
|
|
}).join('&')) |
|
|
|
}, |
|
|
|
options: { |
|
|
|
editable: false, |
|
|
|
enableColumnReorder: false |
|
|
|
}, |
|
|
|
render: function() { |
|
|
|
// new slick grid |
|
|
|
this.grid = new Slick.Grid("#"+this.id, this.dataView, this.dataview_columns, this.options); |
|
|
@@ -302,14 +345,15 @@ wn.views.GridReport = Class.extend({ |
|
|
|
me.grid.render(); |
|
|
|
}); |
|
|
|
|
|
|
|
this.add_grid_events && this.add_grid_events(); |
|
|
|
this.tree_grid.show && this.add_tree_grid_events(); |
|
|
|
}, |
|
|
|
prepare_data_view: function(items) { |
|
|
|
prepare_data_view: function() { |
|
|
|
// initialize the model |
|
|
|
this.dataView = new Slick.Data.DataView({ inlineFilters: true }); |
|
|
|
this.dataView.beginUpdate(); |
|
|
|
this.dataView.setItems(items); |
|
|
|
this.dataView.setItems(this.data); |
|
|
|
if(this.dataview_filter) this.dataView.setFilter(this.dataview_filter); |
|
|
|
if(this.tree_grid.show) this.dataView.setFilter(this.tree_dataview_filter); |
|
|
|
this.dataView.endUpdate(); |
|
|
|
}, |
|
|
|
export: function() { |
|
|
@@ -342,80 +386,73 @@ wn.views.GridReport = Class.extend({ |
|
|
|
|
|
|
|
return false; |
|
|
|
}, |
|
|
|
render_plot: function() { |
|
|
|
var plot_data = this.get_plot_data ? this.get_plot_data() : null; |
|
|
|
if(!plot_data) { |
|
|
|
this.wrapper.find('.plot').toggle(false); |
|
|
|
return; |
|
|
|
} |
|
|
|
wn.require('js/lib/flot/jquery.flot.js'); |
|
|
|
|
|
|
|
this.plot = $.plot(this.wrapper.find('.plot').toggle(true), plot_data, |
|
|
|
this.get_plot_options()); |
|
|
|
|
|
|
|
this.setup_plot_hover(); |
|
|
|
}, |
|
|
|
setup_plot_hover: function() { |
|
|
|
add_tree_grid_events: function() { |
|
|
|
var me = this; |
|
|
|
this.tooltip_id = wn.dom.set_unique_id(); |
|
|
|
function showTooltip(x, y, contents) { |
|
|
|
$('<div id="' + me.tooltip_id + '">' + contents + '</div>').css( { |
|
|
|
position: 'absolute', |
|
|
|
display: 'none', |
|
|
|
top: y + 5, |
|
|
|
left: x + 5, |
|
|
|
border: '1px solid #fdd', |
|
|
|
padding: '2px', |
|
|
|
'background-color': '#fee', |
|
|
|
opacity: 0.80 |
|
|
|
}).appendTo("body").fadeIn(200); |
|
|
|
} |
|
|
|
|
|
|
|
this.previousPoint = null; |
|
|
|
this.wrapper.find('.plot').bind("plothover", function (event, pos, item) { |
|
|
|
if (item) { |
|
|
|
if (me.previousPoint != item.dataIndex) { |
|
|
|
me.previousPoint = item.dataIndex; |
|
|
|
this.grid.onClick.subscribe(function (e, args) { |
|
|
|
if ($(e.target).hasClass("toggle")) { |
|
|
|
var item = me.dataView.getItem(args.row); |
|
|
|
if (item) { |
|
|
|
if (!item._collapsed) { |
|
|
|
item._collapsed = true; |
|
|
|
} else { |
|
|
|
item._collapsed = false; |
|
|
|
} |
|
|
|
|
|
|
|
$("#" + me.tooltip_id).remove(); |
|
|
|
showTooltip(item.pageX, item.pageY, |
|
|
|
me.get_tooltip_text(item.series.label, item.datapoint[0], item.datapoint[1])); |
|
|
|
me.dataView.updateItem(item.id, item); |
|
|
|
} |
|
|
|
e.stopImmediatePropagation(); |
|
|
|
} |
|
|
|
else { |
|
|
|
$("#" + me.tooltip_id).remove(); |
|
|
|
me.previousPoint = null; |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
}, |
|
|
|
get_tooltip_text: function(label, x, y) { |
|
|
|
var date = dateutil.obj_to_user(new Date(x)); |
|
|
|
var value = fmt_money(y); |
|
|
|
return value + " on " + date; |
|
|
|
}); |
|
|
|
}, |
|
|
|
get_view_data: function() { |
|
|
|
var res = []; |
|
|
|
var col_map = $.map(this.columns, function(v) { return v.field; }); |
|
|
|
|
|
|
|
for (var i=0, len=this.dataView.getLength(); i<len; i++) { |
|
|
|
var d = this.dataView.getItem(i); |
|
|
|
var row = []; |
|
|
|
$.each(col_map, function(i, col) { |
|
|
|
var val = d[col]; |
|
|
|
if(val===null || val===undefined) { |
|
|
|
val = "" |
|
|
|
} |
|
|
|
row.push(val); |
|
|
|
}) |
|
|
|
tree_formatter: function (row, cell, value, columnDef, dataContext) { |
|
|
|
var me = wn.cur_grid_report; |
|
|
|
value = value.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); |
|
|
|
var data = me.data; |
|
|
|
var spacer = "<span style='display:inline-block;height:1px;width:" + |
|
|
|
(15 * dataContext["indent"]) + "px'></span>"; |
|
|
|
var idx = me.dataView.getIdxById(dataContext.id); |
|
|
|
var link = me.tree_grid.formatter(dataContext); |
|
|
|
|
|
|
|
if(columnDef.doctype) { |
|
|
|
link += me.get_link_open_icon(columnDef.doctype, value); |
|
|
|
} |
|
|
|
|
|
|
|
res.push(row); |
|
|
|
if (data[idx + 1] && data[idx + 1].indent > data[idx].indent) { |
|
|
|
if (dataContext._collapsed) { |
|
|
|
return spacer + " <span class='toggle expand'></span> " + link; |
|
|
|
} else { |
|
|
|
return spacer + " <span class='toggle collapse'></span> " + link; |
|
|
|
} |
|
|
|
} else { |
|
|
|
return spacer + " <span class='toggle'></span> " + link; |
|
|
|
} |
|
|
|
return res; |
|
|
|
}, |
|
|
|
options: { |
|
|
|
editable: false, |
|
|
|
enableColumnReorder: false |
|
|
|
tree_dataview_filter: function(item) { |
|
|
|
var me = wn.cur_grid_report; |
|
|
|
if(!me.apply_filters(item)) return false; |
|
|
|
|
|
|
|
var parent = item[me.tree_grid.parent_field]; |
|
|
|
while (parent) { |
|
|
|
if (me.item_by_name[parent]._collapsed) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
parent = me.parent_map[parent]; |
|
|
|
} |
|
|
|
return true; |
|
|
|
}, |
|
|
|
set_indent: function() { |
|
|
|
var me = this; |
|
|
|
$.each(this.data, function(i, d) { |
|
|
|
var indent = 0; |
|
|
|
var parent = me.parent_map[d.name]; |
|
|
|
if(parent) { |
|
|
|
while(parent) { |
|
|
|
indent++; |
|
|
|
parent = me.parent_map[parent]; |
|
|
|
} |
|
|
|
} |
|
|
|
d.indent = indent; |
|
|
|
}); |
|
|
|
}, |
|
|
|
apply_filters: function(item) { |
|
|
|
// generic filter: apply filter functiions |
|
|
@@ -427,11 +464,6 @@ wn.views.GridReport = Class.extend({ |
|
|
|
if(!this.apply_filter(item, i)) return false; |
|
|
|
} |
|
|
|
|
|
|
|
// hand over to additional filters (if applicable) |
|
|
|
if(this.custom_dataview_filter) { |
|
|
|
return this.custom_dataview_filter(item); |
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
}, |
|
|
|
apply_filter: function(item, fieldname) { |
|
|
@@ -448,12 +480,12 @@ wn.views.GridReport = Class.extend({ |
|
|
|
if(!me.show_zero) { |
|
|
|
for(var i=0, j=me.columns.length; i<j; i++) { |
|
|
|
var col = me.columns[i]; |
|
|
|
if(col.formatter==me.currency_formatter) { |
|
|
|
if(col.formatter==me.currency_formatter && !col.hidden) { |
|
|
|
if(flt(item[col.field]) > 0.001 || flt(item[col.field]) < -0.001) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
return true; |
|
|
@@ -501,6 +533,8 @@ wn.views.GridReport = Class.extend({ |
|
|
|
// link_formatter must have |
|
|
|
// filter_input, open_btn (true / false), doctype (will be eval'd) |
|
|
|
if(!value) return ""; |
|
|
|
|
|
|
|
var me = wn.cur_grid_report; |
|
|
|
|
|
|
|
if(dataContext._show) { |
|
|
|
return repl('<span style="%(_style)s">%(value)s</span>', { |
|
|
@@ -510,7 +544,7 @@ wn.views.GridReport = Class.extend({ |
|
|
|
} |
|
|
|
|
|
|
|
// make link to add a filter |
|
|
|
var link_formatter = wn.cur_grid_report.dataview_columns[cell].link_formatter; |
|
|
|
var link_formatter = me.dataview_columns[cell].link_formatter; |
|
|
|
var html = repl('<a href="#" \ |
|
|
|
onclick="wn.cur_grid_report.filter_inputs.%(col_name)s.val(\'%(value)s\'); \ |
|
|
|
wn.cur_grid_report.set_route(); return false;">\ |
|
|
@@ -522,16 +556,173 @@ wn.views.GridReport = Class.extend({ |
|
|
|
|
|
|
|
// make icon to open form |
|
|
|
if(link_formatter.open_btn) { |
|
|
|
html += repl(' <i class="icon icon-share" style="cursor: pointer;"\ |
|
|
|
onclick="wn.set_route(\'Form\', \'%(doctype)s\', \'%(value)s\');">\ |
|
|
|
</i>', { |
|
|
|
value: value, |
|
|
|
doctype: eval(link_formatter.doctype) |
|
|
|
}); |
|
|
|
html += me.get_link_open_icon(eval(link_formatter.doctype), value); |
|
|
|
} |
|
|
|
return html; |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
}, |
|
|
|
get_link_open_icon: function(doctype, name) { |
|
|
|
return repl(' <i class="icon icon-share" style="cursor: pointer;"\ |
|
|
|
onclick="wn.set_route(\'Form\', \'%(doctype)s\', \'%(name)s\');">\ |
|
|
|
</i>', { |
|
|
|
name: name, |
|
|
|
doctype: doctype |
|
|
|
}); |
|
|
|
}, |
|
|
|
make_date_range_columns: function() { |
|
|
|
this.columns = []; |
|
|
|
|
|
|
|
var me = this; |
|
|
|
var range = this.filter_inputs.range.val(); |
|
|
|
this.from_date = dateutil.user_to_str(this.filter_inputs.from_date.val()); |
|
|
|
this.to_date = dateutil.user_to_str(this.filter_inputs.to_date.val()); |
|
|
|
var date_diff = dateutil.get_diff(this.to_date, this.from_date); |
|
|
|
|
|
|
|
me.column_map = {}; |
|
|
|
|
|
|
|
var add_column = function(date) { |
|
|
|
me.columns.push({ |
|
|
|
id: date, |
|
|
|
name: dateutil.str_to_user(date), |
|
|
|
field: date, |
|
|
|
formatter: me.currency_formatter, |
|
|
|
width: 100 |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
var build_columns = function(condition) { |
|
|
|
// add column for each date range |
|
|
|
for(var i=0; i < date_diff; i++) { |
|
|
|
var date = dateutil.add_days(me.from_date, i); |
|
|
|
if(!condition) condition = function() { return true; } |
|
|
|
|
|
|
|
if(condition(date)) add_column(date); |
|
|
|
me.last_date = date; |
|
|
|
|
|
|
|
if(me.columns.length) { |
|
|
|
me.column_map[date] = me.columns[me.columns.length-1]; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// make columns for all date ranges |
|
|
|
if(range=='Daily') { |
|
|
|
build_columns(); |
|
|
|
} else if(range=='Weekly') { |
|
|
|
build_columns(function(date) { |
|
|
|
if(!me.last_date) return true; |
|
|
|
return !(dateutil.get_diff(date, me.from_date) % 7) |
|
|
|
}); |
|
|
|
} else if(range=='Monthly') { |
|
|
|
build_columns(function(date) { |
|
|
|
if(!me.last_date) return true; |
|
|
|
return dateutil.str_to_obj(me.last_date).getMonth() != dateutil.str_to_obj(date).getMonth() |
|
|
|
}); |
|
|
|
} else if(range=='Quarterly') { |
|
|
|
build_columns(function(date) { |
|
|
|
if(!me.last_date) return true; |
|
|
|
return dateutil.str_to_obj(date).getDate()==1 && in_list([0,3,6,9], dateutil.str_to_obj(date).getMonth()) |
|
|
|
}); |
|
|
|
} else if(range=='Yearly') { |
|
|
|
build_columns(function(date) { |
|
|
|
if(!me.last_date) return true; |
|
|
|
return $.map(wn.report_dump.data['Fiscal Year'], function(v) { |
|
|
|
return date==v.year_start_date ? true : null; |
|
|
|
}).length; |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// set label as last date of period |
|
|
|
$.each(this.columns, function(i, col) { |
|
|
|
col.name = me.columns[i+1] |
|
|
|
? dateutil.str_to_user(dateutil.add_days(me.columns[i+1].id, -1)) |
|
|
|
: dateutil.str_to_user(me.to_date); |
|
|
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
wn.views.GridReportWithPlot = wn.views.GridReport.extend({ |
|
|
|
render_plot: function() { |
|
|
|
var plot_data = this.get_plot_data ? this.get_plot_data() : null; |
|
|
|
if(!plot_data) { |
|
|
|
this.plot_area.toggle(false); |
|
|
|
return; |
|
|
|
} |
|
|
|
wn.require('js/lib/flot/jquery.flot.js'); |
|
|
|
|
|
|
|
this.plot = $.plot(this.plot_area.toggle(true), plot_data, |
|
|
|
this.get_plot_options()); |
|
|
|
|
|
|
|
this.setup_plot_hover(); |
|
|
|
}, |
|
|
|
setup_plot_check: function() { |
|
|
|
var me = this; |
|
|
|
me.wrapper.bind('make', function() { |
|
|
|
me.wrapper.on("click", ".plot-check", function() { |
|
|
|
var checked = $(this).attr("checked"); |
|
|
|
me.item_by_name[$(this).attr("data-id")].checked = checked ? true : false; |
|
|
|
me.render_plot(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}, |
|
|
|
setup_plot_hover: function() { |
|
|
|
var me = this; |
|
|
|
this.tooltip_id = wn.dom.set_unique_id(); |
|
|
|
function showTooltip(x, y, contents) { |
|
|
|
$('<div id="' + me.tooltip_id + '">' + contents + '</div>').css( { |
|
|
|
position: 'absolute', |
|
|
|
display: 'none', |
|
|
|
top: y + 5, |
|
|
|
left: x + 5, |
|
|
|
border: '1px solid #fdd', |
|
|
|
padding: '2px', |
|
|
|
'background-color': '#fee', |
|
|
|
opacity: 0.80 |
|
|
|
}).appendTo("body").fadeIn(200); |
|
|
|
} |
|
|
|
|
|
|
|
this.previousPoint = null; |
|
|
|
this.wrapper.find('.plot').bind("plothover", function (event, pos, item) { |
|
|
|
if (item) { |
|
|
|
if (me.previousPoint != item.dataIndex) { |
|
|
|
me.previousPoint = item.dataIndex; |
|
|
|
|
|
|
|
$("#" + me.tooltip_id).remove(); |
|
|
|
showTooltip(item.pageX, item.pageY, |
|
|
|
me.get_tooltip_text(item.series.label, item.datapoint[0], item.datapoint[1])); |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
$("#" + me.tooltip_id).remove(); |
|
|
|
me.previousPoint = null; |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
}, |
|
|
|
get_tooltip_text: function(label, x, y) { |
|
|
|
var date = dateutil.obj_to_user(new Date(x)); |
|
|
|
var value = fmt_money(y); |
|
|
|
return value + " on " + date; |
|
|
|
}, |
|
|
|
get_view_data: function() { |
|
|
|
var res = []; |
|
|
|
var col_map = $.map(this.columns, function(v) { return v.field; }); |
|
|
|
|
|
|
|
for (var i=0, len=this.dataView.getLength(); i<len; i++) { |
|
|
|
var d = this.dataView.getItem(i); |
|
|
|
var row = []; |
|
|
|
$.each(col_map, function(i, col) { |
|
|
|
var val = d[col]; |
|
|
|
if(val===null || val===undefined) { |
|
|
|
val = "" |
|
|
|
} |
|
|
|
row.push(val); |
|
|
|
}) |
|
|
|
|
|
|
|
res.push(row); |
|
|
|
} |
|
|
|
return res; |
|
|
|
}, |
|
|
|
}) |