diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index 5306aa6bc..1e1f384ff 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -6,13 +6,13 @@
//= require bootstrap-sprockets
//= require jquery.validate.min
//= require additional-methods.min
-//= require jquery-validate-message-zh
//= require bootstrap-notify
//= require jquery.cookie.min
//= require select2
-//= require select2-i18n.zh-CN
//= require jquery.cxselect
+//= require bootstrap-datepicker
+//= require_tree ./i18n
//= require_tree ./admins
// ******** select2 global config ********
diff --git a/app/assets/javascripts/admins/daily_school_statistics/index.js b/app/assets/javascripts/admins/daily_school_statistics/index.js
new file mode 100644
index 000000000..7c3aa2d4f
--- /dev/null
+++ b/app/assets/javascripts/admins/daily_school_statistics/index.js
@@ -0,0 +1,12 @@
+$(document).on('turbolinks:load', function(){
+ if ($('body.admins-daily-school-statistics-index-page').length > 0) {
+ $('.export-action').on('click', function(){
+ var form = $(".daily-school-statistic-list-form .search-form")
+ var exportLink = $(this);
+ var keyword = form.find("input[name='keyword']").val();
+
+ var url = exportLink.data("url").split('?')[0] + "?keyword=" + keyword;
+ window.open(url);
+ });
+ }
+})
\ No newline at end of file
diff --git a/app/assets/javascripts/admins/school_statistics/index.js b/app/assets/javascripts/admins/school_statistics/index.js
new file mode 100644
index 000000000..f6c364e76
--- /dev/null
+++ b/app/assets/javascripts/admins/school_statistics/index.js
@@ -0,0 +1,135 @@
+$(document).on('turbolinks:load', function(){
+ if ($('body.admins-school-statistics-index-page').length > 0) {
+ var searchForm = $(".school-statistic-list-form .search-form");
+ var growFormUrl = searchForm.data('grow-form-url');
+ var contrastFormUrl = searchForm.data('contrast-form-url');
+
+ var dataTypeInput = searchForm.find("input[name='data_type']");
+ var keywordInput = searchForm.find("input[name='keyword']");
+ var contrastBtn = searchForm.find(".contrast-btn");
+ var growBtn = searchForm.find(".grow-btn");
+ var contrastDateContainer = searchForm.find('.contrast-date-container');
+ var growDateContainer = searchForm.find('.grow-date-container');
+
+ // 数据对比日期输入框
+ var beginDateInput = searchForm.find("input[name='begin_date']");
+ var endDateInput = searchForm.find("input[name='end_date']");
+ var otherBeginDateInput = searchForm.find("input[name='other_begin_date']");
+ var otherEndDateInput = searchForm.find("input[name='other_end_date']");
+
+ // 新增数据日期输入框
+ var growBeginDateInput = searchForm.find("input[name='grow_begin_date']");
+ var growEndDateInput = searchForm.find("input[name='grow_end_date']");
+
+ // 数据展示切换: 数据对比、新增数据
+ searchForm.on('click', ".contrast-btn", function(){
+ if(contrastBtn.hasClass("active")) { return }
+ changeDataType("contrast");
+ submitForm();
+ });
+ searchForm.on('click', ".grow-btn", function(){
+ if(growBtn.hasClass("active")) { return }
+ changeDataType("grow");
+ submitForm();
+ });
+
+ // 搜索按钮
+ searchForm.on('click', ".search-btn", function(){
+ console.log('submit');
+ submitForm();
+ });
+
+ $('.school-statistic-list-container').on('change', '.contrast-column-select', function() {
+ searchForm.find("input[name='contrast_column']").val($('.contrast-column-select').val());
+ submitForm();
+ });
+
+ var submitForm = function(){
+ if(!validateFrom()) { return }
+
+ var form = searchForm;
+ var url = dataTypeInput.val() == "contrast" ? contrastFormUrl : growFormUrl;
+
+ $.ajax({
+ url: url,
+ data: form.serialize(),
+ dataType: "script"
+ })
+ };
+
+ var validateFrom = function(){
+ if (dataTypeInput.val() != "contrast") { return true; }
+
+ // 全部为空时,需要展示空数据页
+ if (beginDateInput.val() == "" && endDateInput.val() == "" &&
+ otherBeginDateInput.val() == "" && otherBeginDateInput.val() == "") {
+ return true;
+ }
+
+ if (beginDateInput.val() != "" && endDateInput.val() != "" &&
+ otherBeginDateInput.val() != "" && otherBeginDateInput.val() != "") {
+ return true;
+ }
+
+ return false;
+ };
+
+ var changeDataType = function(dataType){
+ if (dataTypeInput.val() == dataType) { return }
+
+ if (dataType == "contrast") {
+ contrastBtn.addClass("active");
+ growBtn.removeClass("active");
+ dataTypeInput.val('contrast');
+ growDateContainer.hide();
+ contrastDateContainer.show();
+
+ clearGrowDateInput();
+ } else {
+ contrastBtn.removeClass("active");
+ growBtn.addClass("active");
+ dataTypeInput.val('grow');
+ growDateContainer.show();
+ contrastDateContainer.hide();
+
+ clearContrastDateInput();
+ }
+ };
+
+ var clearGrowDateInput = function() {
+ searchForm.find("input[name='grow_begin_date']").val('');
+ searchForm.find("input[name='grow_end_date']").val('');
+ searchForm.find("input[name='grow_date_input']").val('');
+ };
+
+ var clearContrastDateInput = function(){
+ searchForm.find("input[name='begin_date']").val('');
+ searchForm.find("input[name='end_date']").val('');
+ searchForm.find("input[name='other_begin_date']").val('');
+ searchForm.find("input[name='other_end_date']").val('');
+ searchForm.find("input[name='date_input']").val('');
+ searchForm.find("input[name='other_date_input']").val('');
+ };
+
+ var baseOptions = {
+ autoclose: true,
+ language: 'zh-CN',
+ format: 'yyyy-mm-dd',
+ startDate: '2017-04-01',
+ endDate: '-1d'
+ }
+
+ var defineDateRangeSelect = function(element){
+ var options = $.extend({inputs: $(element).find('.start-date, .end-date')}, baseOptions);
+ $(element).datepicker(options);
+
+ $(element).find('.start-date').datepicker().on('changeDate', function(e){
+ $(element).find('.end-date').datepicker('setStartDate', e.date);
+ })
+ };
+
+ defineDateRangeSelect('.grow-date-input-daterange');
+ defineDateRangeSelect('.date-input-daterange');
+ defineDateRangeSelect('.other-date-input-daterange');
+ }
+})
\ No newline at end of file
diff --git a/app/assets/javascripts/admins/sidebar.js b/app/assets/javascripts/admins/sidebar.js
new file mode 100644
index 000000000..432aa700b
--- /dev/null
+++ b/app/assets/javascripts/admins/sidebar.js
@@ -0,0 +1,16 @@
+$(document).on('turbolinks:load', function(){
+ $('#sidebarCollapse').on('click', function () {
+ $(this).toggleClass('active');
+ $('#sidebar').toggleClass('active');
+ $.cookie('admin_sidebar_collapse', $(this).hasClass('active'), {path: '/admins'});
+ });
+
+ var sidebarController = $('#sidebar').data('current-controller');
+ if (sidebarController.length > 0) {
+ $('#sidebar a.active').removeClass('active');
+ $('#sidebar ul.collapse.show').removeClass('show');
+ var activeLi = $('#sidebar a[data-controller="' + sidebarController + '"]');
+ activeLi.addClass('active');
+ activeLi.parent().parent('ul.collapse').addClass('show');
+ }
+});
\ No newline at end of file
diff --git a/app/assets/javascripts/bootstrap-datepicker.js b/app/assets/javascripts/bootstrap-datepicker.js
new file mode 100644
index 000000000..70d91c557
--- /dev/null
+++ b/app/assets/javascripts/bootstrap-datepicker.js
@@ -0,0 +1,2039 @@
+/*!
+ * Datepicker for Bootstrap v1.9.0 (https://github.com/uxsolutions/bootstrap-datepicker)
+ *
+ * Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ */
+
+(function(factory){
+ if (typeof define === 'function' && define.amd) {
+ define(['jquery'], factory);
+ } else if (typeof exports === 'object') {
+ factory(require('jquery'));
+ } else {
+ factory(jQuery);
+ }
+}(function($, undefined){
+ function UTCDate(){
+ return new Date(Date.UTC.apply(Date, arguments));
+ }
+ function UTCToday(){
+ var today = new Date();
+ return UTCDate(today.getFullYear(), today.getMonth(), today.getDate());
+ }
+ function isUTCEquals(date1, date2) {
+ return (
+ date1.getUTCFullYear() === date2.getUTCFullYear() &&
+ date1.getUTCMonth() === date2.getUTCMonth() &&
+ date1.getUTCDate() === date2.getUTCDate()
+ );
+ }
+ function alias(method, deprecationMsg){
+ return function(){
+ if (deprecationMsg !== undefined) {
+ $.fn.datepicker.deprecated(deprecationMsg);
+ }
+
+ return this[method].apply(this, arguments);
+ };
+ }
+ function isValidDate(d) {
+ return d && !isNaN(d.getTime());
+ }
+
+ var DateArray = (function(){
+ var extras = {
+ get: function(i){
+ return this.slice(i)[0];
+ },
+ contains: function(d){
+ // Array.indexOf is not cross-browser;
+ // $.inArray doesn't work with Dates
+ var val = d && d.valueOf();
+ for (var i=0, l=this.length; i < l; i++)
+ // Use date arithmetic to allow dates with different times to match
+ if (0 <= this[i].valueOf() - val && this[i].valueOf() - val < 1000*60*60*24)
+ return i;
+ return -1;
+ },
+ remove: function(i){
+ this.splice(i,1);
+ },
+ replace: function(new_array){
+ if (!new_array)
+ return;
+ if (!$.isArray(new_array))
+ new_array = [new_array];
+ this.clear();
+ this.push.apply(this, new_array);
+ },
+ clear: function(){
+ this.length = 0;
+ },
+ copy: function(){
+ var a = new DateArray();
+ a.replace(this);
+ return a;
+ }
+ };
+
+ return function(){
+ var a = [];
+ a.push.apply(a, arguments);
+ $.extend(a, extras);
+ return a;
+ };
+ })();
+
+
+ // Picker object
+
+ var Datepicker = function(element, options){
+ $.data(element, 'datepicker', this);
+
+ this._events = [];
+ this._secondaryEvents = [];
+
+ this._process_options(options);
+
+ this.dates = new DateArray();
+ this.viewDate = this.o.defaultViewDate;
+ this.focusDate = null;
+
+ this.element = $(element);
+ this.isInput = this.element.is('input');
+ this.inputField = this.isInput ? this.element : this.element.find('input');
+ this.component = this.element.hasClass('date') ? this.element.find('.add-on, .input-group-addon, .input-group-append, .input-group-prepend, .btn') : false;
+ if (this.component && this.component.length === 0)
+ this.component = false;
+ this.isInline = !this.component && this.element.is('div');
+
+ this.picker = $(DPGlobal.template);
+
+ // Checking templates and inserting
+ if (this._check_template(this.o.templates.leftArrow)) {
+ this.picker.find('.prev').html(this.o.templates.leftArrow);
+ }
+
+ if (this._check_template(this.o.templates.rightArrow)) {
+ this.picker.find('.next').html(this.o.templates.rightArrow);
+ }
+
+ this._buildEvents();
+ this._attachEvents();
+
+ if (this.isInline){
+ this.picker.addClass('datepicker-inline').appendTo(this.element);
+ }
+ else {
+ this.picker.addClass('datepicker-dropdown dropdown-menu');
+ }
+
+ if (this.o.rtl){
+ this.picker.addClass('datepicker-rtl');
+ }
+
+ if (this.o.calendarWeeks) {
+ this.picker.find('.datepicker-days .datepicker-switch, thead .datepicker-title, tfoot .today, tfoot .clear')
+ .attr('colspan', function(i, val){
+ return Number(val) + 1;
+ });
+ }
+
+ this._process_options({
+ startDate: this._o.startDate,
+ endDate: this._o.endDate,
+ daysOfWeekDisabled: this.o.daysOfWeekDisabled,
+ daysOfWeekHighlighted: this.o.daysOfWeekHighlighted,
+ datesDisabled: this.o.datesDisabled
+ });
+
+ this._allow_update = false;
+ this.setViewMode(this.o.startView);
+ this._allow_update = true;
+
+ this.fillDow();
+ this.fillMonths();
+
+ this.update();
+
+ if (this.isInline){
+ this.show();
+ }
+ };
+
+ Datepicker.prototype = {
+ constructor: Datepicker,
+
+ _resolveViewName: function(view){
+ $.each(DPGlobal.viewModes, function(i, viewMode){
+ if (view === i || $.inArray(view, viewMode.names) !== -1){
+ view = i;
+ return false;
+ }
+ });
+
+ return view;
+ },
+
+ _resolveDaysOfWeek: function(daysOfWeek){
+ if (!$.isArray(daysOfWeek))
+ daysOfWeek = daysOfWeek.split(/[,\s]*/);
+ return $.map(daysOfWeek, Number);
+ },
+
+ _check_template: function(tmp){
+ try {
+ // If empty
+ if (tmp === undefined || tmp === "") {
+ return false;
+ }
+ // If no html, everything ok
+ if ((tmp.match(/[<>]/g) || []).length <= 0) {
+ return true;
+ }
+ // Checking if html is fine
+ var jDom = $(tmp);
+ return jDom.length > 0;
+ }
+ catch (ex) {
+ return false;
+ }
+ },
+
+ _process_options: function(opts){
+ // Store raw options for reference
+ this._o = $.extend({}, this._o, opts);
+ // Processed options
+ var o = this.o = $.extend({}, this._o);
+
+ // Check if "de-DE" style date is available, if not language should
+ // fallback to 2 letter code eg "de"
+ var lang = o.language;
+ if (!dates[lang]){
+ lang = lang.split('-')[0];
+ if (!dates[lang])
+ lang = defaults.language;
+ }
+ o.language = lang;
+
+ // Retrieve view index from any aliases
+ o.startView = this._resolveViewName(o.startView);
+ o.minViewMode = this._resolveViewName(o.minViewMode);
+ o.maxViewMode = this._resolveViewName(o.maxViewMode);
+
+ // Check view is between min and max
+ o.startView = Math.max(this.o.minViewMode, Math.min(this.o.maxViewMode, o.startView));
+
+ // true, false, or Number > 0
+ if (o.multidate !== true){
+ o.multidate = Number(o.multidate) || false;
+ if (o.multidate !== false)
+ o.multidate = Math.max(0, o.multidate);
+ }
+ o.multidateSeparator = String(o.multidateSeparator);
+
+ o.weekStart %= 7;
+ o.weekEnd = (o.weekStart + 6) % 7;
+
+ var format = DPGlobal.parseFormat(o.format);
+ if (o.startDate !== -Infinity){
+ if (!!o.startDate){
+ if (o.startDate instanceof Date)
+ o.startDate = this._local_to_utc(this._zero_time(o.startDate));
+ else
+ o.startDate = DPGlobal.parseDate(o.startDate, format, o.language, o.assumeNearbyYear);
+ }
+ else {
+ o.startDate = -Infinity;
+ }
+ }
+ if (o.endDate !== Infinity){
+ if (!!o.endDate){
+ if (o.endDate instanceof Date)
+ o.endDate = this._local_to_utc(this._zero_time(o.endDate));
+ else
+ o.endDate = DPGlobal.parseDate(o.endDate, format, o.language, o.assumeNearbyYear);
+ }
+ else {
+ o.endDate = Infinity;
+ }
+ }
+
+ o.daysOfWeekDisabled = this._resolveDaysOfWeek(o.daysOfWeekDisabled||[]);
+ o.daysOfWeekHighlighted = this._resolveDaysOfWeek(o.daysOfWeekHighlighted||[]);
+
+ o.datesDisabled = o.datesDisabled||[];
+ if (!$.isArray(o.datesDisabled)) {
+ o.datesDisabled = o.datesDisabled.split(',');
+ }
+ o.datesDisabled = $.map(o.datesDisabled, function(d){
+ return DPGlobal.parseDate(d, format, o.language, o.assumeNearbyYear);
+ });
+
+ var plc = String(o.orientation).toLowerCase().split(/\s+/g),
+ _plc = o.orientation.toLowerCase();
+ plc = $.grep(plc, function(word){
+ return /^auto|left|right|top|bottom$/.test(word);
+ });
+ o.orientation = {x: 'auto', y: 'auto'};
+ if (!_plc || _plc === 'auto')
+ ; // no action
+ else if (plc.length === 1){
+ switch (plc[0]){
+ case 'top':
+ case 'bottom':
+ o.orientation.y = plc[0];
+ break;
+ case 'left':
+ case 'right':
+ o.orientation.x = plc[0];
+ break;
+ }
+ }
+ else {
+ _plc = $.grep(plc, function(word){
+ return /^left|right$/.test(word);
+ });
+ o.orientation.x = _plc[0] || 'auto';
+
+ _plc = $.grep(plc, function(word){
+ return /^top|bottom$/.test(word);
+ });
+ o.orientation.y = _plc[0] || 'auto';
+ }
+ if (o.defaultViewDate instanceof Date || typeof o.defaultViewDate === 'string') {
+ o.defaultViewDate = DPGlobal.parseDate(o.defaultViewDate, format, o.language, o.assumeNearbyYear);
+ } else if (o.defaultViewDate) {
+ var year = o.defaultViewDate.year || new Date().getFullYear();
+ var month = o.defaultViewDate.month || 0;
+ var day = o.defaultViewDate.day || 1;
+ o.defaultViewDate = UTCDate(year, month, day);
+ } else {
+ o.defaultViewDate = UTCToday();
+ }
+ },
+ _applyEvents: function(evs){
+ for (var i=0, el, ch, ev; i < evs.length; i++){
+ el = evs[i][0];
+ if (evs[i].length === 2){
+ ch = undefined;
+ ev = evs[i][1];
+ } else if (evs[i].length === 3){
+ ch = evs[i][1];
+ ev = evs[i][2];
+ }
+ el.on(ev, ch);
+ }
+ },
+ _unapplyEvents: function(evs){
+ for (var i=0, el, ev, ch; i < evs.length; i++){
+ el = evs[i][0];
+ if (evs[i].length === 2){
+ ch = undefined;
+ ev = evs[i][1];
+ } else if (evs[i].length === 3){
+ ch = evs[i][1];
+ ev = evs[i][2];
+ }
+ el.off(ev, ch);
+ }
+ },
+ _buildEvents: function(){
+ var events = {
+ keyup: $.proxy(function(e){
+ if ($.inArray(e.keyCode, [27, 37, 39, 38, 40, 32, 13, 9]) === -1)
+ this.update();
+ }, this),
+ keydown: $.proxy(this.keydown, this),
+ paste: $.proxy(this.paste, this)
+ };
+
+ if (this.o.showOnFocus === true) {
+ events.focus = $.proxy(this.show, this);
+ }
+
+ if (this.isInput) { // single input
+ this._events = [
+ [this.element, events]
+ ];
+ }
+ // component: input + button
+ else if (this.component && this.inputField.length) {
+ this._events = [
+ // For components that are not readonly, allow keyboard nav
+ [this.inputField, events],
+ [this.component, {
+ click: $.proxy(this.show, this)
+ }]
+ ];
+ }
+ else {
+ this._events = [
+ [this.element, {
+ click: $.proxy(this.show, this),
+ keydown: $.proxy(this.keydown, this)
+ }]
+ ];
+ }
+ this._events.push(
+ // Component: listen for blur on element descendants
+ [this.element, '*', {
+ blur: $.proxy(function(e){
+ this._focused_from = e.target;
+ }, this)
+ }],
+ // Input: listen for blur on element
+ [this.element, {
+ blur: $.proxy(function(e){
+ this._focused_from = e.target;
+ }, this)
+ }]
+ );
+
+ if (this.o.immediateUpdates) {
+ // Trigger input updates immediately on changed year/month
+ this._events.push([this.element, {
+ 'changeYear changeMonth': $.proxy(function(e){
+ this.update(e.date);
+ }, this)
+ }]);
+ }
+
+ this._secondaryEvents = [
+ [this.picker, {
+ click: $.proxy(this.click, this)
+ }],
+ [this.picker, '.prev, .next', {
+ click: $.proxy(this.navArrowsClick, this)
+ }],
+ [this.picker, '.day:not(.disabled)', {
+ click: $.proxy(this.dayCellClick, this)
+ }],
+ [$(window), {
+ resize: $.proxy(this.place, this)
+ }],
+ [$(document), {
+ 'mousedown touchstart': $.proxy(function(e){
+ // Clicked outside the datepicker, hide it
+ if (!(
+ this.element.is(e.target) ||
+ this.element.find(e.target).length ||
+ this.picker.is(e.target) ||
+ this.picker.find(e.target).length ||
+ this.isInline
+ )){
+ this.hide();
+ }
+ }, this)
+ }]
+ ];
+ },
+ _attachEvents: function(){
+ this._detachEvents();
+ this._applyEvents(this._events);
+ },
+ _detachEvents: function(){
+ this._unapplyEvents(this._events);
+ },
+ _attachSecondaryEvents: function(){
+ this._detachSecondaryEvents();
+ this._applyEvents(this._secondaryEvents);
+ },
+ _detachSecondaryEvents: function(){
+ this._unapplyEvents(this._secondaryEvents);
+ },
+ _trigger: function(event, altdate){
+ var date = altdate || this.dates.get(-1),
+ local_date = this._utc_to_local(date);
+
+ this.element.trigger({
+ type: event,
+ date: local_date,
+ viewMode: this.viewMode,
+ dates: $.map(this.dates, this._utc_to_local),
+ format: $.proxy(function(ix, format){
+ if (arguments.length === 0){
+ ix = this.dates.length - 1;
+ format = this.o.format;
+ } else if (typeof ix === 'string'){
+ format = ix;
+ ix = this.dates.length - 1;
+ }
+ format = format || this.o.format;
+ var date = this.dates.get(ix);
+ return DPGlobal.formatDate(date, format, this.o.language);
+ }, this)
+ });
+ },
+
+ show: function(){
+ if (this.inputField.is(':disabled') || (this.inputField.prop('readonly') && this.o.enableOnReadonly === false))
+ return;
+ if (!this.isInline)
+ this.picker.appendTo(this.o.container);
+ this.place();
+ this.picker.show();
+ this._attachSecondaryEvents();
+ this._trigger('show');
+ if ((window.navigator.msMaxTouchPoints || 'ontouchstart' in document) && this.o.disableTouchKeyboard) {
+ $(this.element).blur();
+ }
+ return this;
+ },
+
+ hide: function(){
+ if (this.isInline || !this.picker.is(':visible'))
+ return this;
+ this.focusDate = null;
+ this.picker.hide().detach();
+ this._detachSecondaryEvents();
+ this.setViewMode(this.o.startView);
+
+ if (this.o.forceParse && this.inputField.val())
+ this.setValue();
+ this._trigger('hide');
+ return this;
+ },
+
+ destroy: function(){
+ this.hide();
+ this._detachEvents();
+ this._detachSecondaryEvents();
+ this.picker.remove();
+ delete this.element.data().datepicker;
+ if (!this.isInput){
+ delete this.element.data().date;
+ }
+ return this;
+ },
+
+ paste: function(e){
+ var dateString;
+ if (e.originalEvent.clipboardData && e.originalEvent.clipboardData.types
+ && $.inArray('text/plain', e.originalEvent.clipboardData.types) !== -1) {
+ dateString = e.originalEvent.clipboardData.getData('text/plain');
+ } else if (window.clipboardData) {
+ dateString = window.clipboardData.getData('Text');
+ } else {
+ return;
+ }
+ this.setDate(dateString);
+ this.update();
+ e.preventDefault();
+ },
+
+ _utc_to_local: function(utc){
+ if (!utc) {
+ return utc;
+ }
+
+ var local = new Date(utc.getTime() + (utc.getTimezoneOffset() * 60000));
+
+ if (local.getTimezoneOffset() !== utc.getTimezoneOffset()) {
+ local = new Date(utc.getTime() + (local.getTimezoneOffset() * 60000));
+ }
+
+ return local;
+ },
+ _local_to_utc: function(local){
+ return local && new Date(local.getTime() - (local.getTimezoneOffset()*60000));
+ },
+ _zero_time: function(local){
+ return local && new Date(local.getFullYear(), local.getMonth(), local.getDate());
+ },
+ _zero_utc_time: function(utc){
+ return utc && UTCDate(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate());
+ },
+
+ getDates: function(){
+ return $.map(this.dates, this._utc_to_local);
+ },
+
+ getUTCDates: function(){
+ return $.map(this.dates, function(d){
+ return new Date(d);
+ });
+ },
+
+ getDate: function(){
+ return this._utc_to_local(this.getUTCDate());
+ },
+
+ getUTCDate: function(){
+ var selected_date = this.dates.get(-1);
+ if (selected_date !== undefined) {
+ return new Date(selected_date);
+ } else {
+ return null;
+ }
+ },
+
+ clearDates: function(){
+ this.inputField.val('');
+ this.update();
+ this._trigger('changeDate');
+
+ if (this.o.autoclose) {
+ this.hide();
+ }
+ },
+
+ setDates: function(){
+ var args = $.isArray(arguments[0]) ? arguments[0] : arguments;
+ this.update.apply(this, args);
+ this._trigger('changeDate');
+ this.setValue();
+ return this;
+ },
+
+ setUTCDates: function(){
+ var args = $.isArray(arguments[0]) ? arguments[0] : arguments;
+ this.setDates.apply(this, $.map(args, this._utc_to_local));
+ return this;
+ },
+
+ setDate: alias('setDates'),
+ setUTCDate: alias('setUTCDates'),
+ remove: alias('destroy', 'Method `remove` is deprecated and will be removed in version 2.0. Use `destroy` instead'),
+
+ setValue: function(){
+ var formatted = this.getFormattedDate();
+ this.inputField.val(formatted);
+ return this;
+ },
+
+ getFormattedDate: function(format){
+ if (format === undefined)
+ format = this.o.format;
+
+ var lang = this.o.language;
+ return $.map(this.dates, function(d){
+ return DPGlobal.formatDate(d, format, lang);
+ }).join(this.o.multidateSeparator);
+ },
+
+ getStartDate: function(){
+ return this.o.startDate;
+ },
+
+ setStartDate: function(startDate){
+ this._process_options({startDate: startDate});
+ this.update();
+ this.updateNavArrows();
+ return this;
+ },
+
+ getEndDate: function(){
+ return this.o.endDate;
+ },
+
+ setEndDate: function(endDate){
+ this._process_options({endDate: endDate});
+ this.update();
+ this.updateNavArrows();
+ return this;
+ },
+
+ setDaysOfWeekDisabled: function(daysOfWeekDisabled){
+ this._process_options({daysOfWeekDisabled: daysOfWeekDisabled});
+ this.update();
+ return this;
+ },
+
+ setDaysOfWeekHighlighted: function(daysOfWeekHighlighted){
+ this._process_options({daysOfWeekHighlighted: daysOfWeekHighlighted});
+ this.update();
+ return this;
+ },
+
+ setDatesDisabled: function(datesDisabled){
+ this._process_options({datesDisabled: datesDisabled});
+ this.update();
+ return this;
+ },
+
+ place: function(){
+ if (this.isInline)
+ return this;
+ var calendarWidth = this.picker.outerWidth(),
+ calendarHeight = this.picker.outerHeight(),
+ visualPadding = 10,
+ container = $(this.o.container),
+ windowWidth = container.width(),
+ scrollTop = this.o.container === 'body' ? $(document).scrollTop() : container.scrollTop(),
+ appendOffset = container.offset();
+
+ var parentsZindex = [0];
+ this.element.parents().each(function(){
+ var itemZIndex = $(this).css('z-index');
+ if (itemZIndex !== 'auto' && Number(itemZIndex) !== 0) parentsZindex.push(Number(itemZIndex));
+ });
+ var zIndex = Math.max.apply(Math, parentsZindex) + this.o.zIndexOffset;
+ var offset = this.component ? this.component.parent().offset() : this.element.offset();
+ var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(false);
+ var width = this.component ? this.component.outerWidth(true) : this.element.outerWidth(false);
+ var left = offset.left - appendOffset.left;
+ var top = offset.top - appendOffset.top;
+
+ if (this.o.container !== 'body') {
+ top += scrollTop;
+ }
+
+ this.picker.removeClass(
+ 'datepicker-orient-top datepicker-orient-bottom '+
+ 'datepicker-orient-right datepicker-orient-left'
+ );
+
+ if (this.o.orientation.x !== 'auto'){
+ this.picker.addClass('datepicker-orient-' + this.o.orientation.x);
+ if (this.o.orientation.x === 'right')
+ left -= calendarWidth - width;
+ }
+ // auto x orientation is best-placement: if it crosses a window
+ // edge, fudge it sideways
+ else {
+ if (offset.left < 0) {
+ // component is outside the window on the left side. Move it into visible range
+ this.picker.addClass('datepicker-orient-left');
+ left -= offset.left - visualPadding;
+ } else if (left + calendarWidth > windowWidth) {
+ // the calendar passes the widow right edge. Align it to component right side
+ this.picker.addClass('datepicker-orient-right');
+ left += width - calendarWidth;
+ } else {
+ if (this.o.rtl) {
+ // Default to right
+ this.picker.addClass('datepicker-orient-right');
+ } else {
+ // Default to left
+ this.picker.addClass('datepicker-orient-left');
+ }
+ }
+ }
+
+ // auto y orientation is best-situation: top or bottom, no fudging,
+ // decision based on which shows more of the calendar
+ var yorient = this.o.orientation.y,
+ top_overflow;
+ if (yorient === 'auto'){
+ top_overflow = -scrollTop + top - calendarHeight;
+ yorient = top_overflow < 0 ? 'bottom' : 'top';
+ }
+
+ this.picker.addClass('datepicker-orient-' + yorient);
+ if (yorient === 'top')
+ top -= calendarHeight + parseInt(this.picker.css('padding-top'));
+ else
+ top += height;
+
+ if (this.o.rtl) {
+ var right = windowWidth - (left + width);
+ this.picker.css({
+ top: top,
+ right: right,
+ zIndex: zIndex
+ });
+ } else {
+ this.picker.css({
+ top: top,
+ left: left,
+ zIndex: zIndex
+ });
+ }
+ return this;
+ },
+
+ _allow_update: true,
+ update: function(){
+ if (!this._allow_update)
+ return this;
+
+ var oldDates = this.dates.copy(),
+ dates = [],
+ fromArgs = false;
+ if (arguments.length){
+ $.each(arguments, $.proxy(function(i, date){
+ if (date instanceof Date)
+ date = this._local_to_utc(date);
+ dates.push(date);
+ }, this));
+ fromArgs = true;
+ } else {
+ dates = this.isInput
+ ? this.element.val()
+ : this.element.data('date') || this.inputField.val();
+ if (dates && this.o.multidate)
+ dates = dates.split(this.o.multidateSeparator);
+ else
+ dates = [dates];
+ delete this.element.data().date;
+ }
+
+ dates = $.map(dates, $.proxy(function(date){
+ return DPGlobal.parseDate(date, this.o.format, this.o.language, this.o.assumeNearbyYear);
+ }, this));
+ dates = $.grep(dates, $.proxy(function(date){
+ return (
+ !this.dateWithinRange(date) ||
+ !date
+ );
+ }, this), true);
+ this.dates.replace(dates);
+
+ if (this.o.updateViewDate) {
+ if (this.dates.length)
+ this.viewDate = new Date(this.dates.get(-1));
+ else if (this.viewDate < this.o.startDate)
+ this.viewDate = new Date(this.o.startDate);
+ else if (this.viewDate > this.o.endDate)
+ this.viewDate = new Date(this.o.endDate);
+ else
+ this.viewDate = this.o.defaultViewDate;
+ }
+
+ if (fromArgs){
+ // setting date by clicking
+ this.setValue();
+ this.element.change();
+ }
+ else if (this.dates.length){
+ // setting date by typing
+ if (String(oldDates) !== String(this.dates) && fromArgs) {
+ this._trigger('changeDate');
+ this.element.change();
+ }
+ }
+ if (!this.dates.length && oldDates.length) {
+ this._trigger('clearDate');
+ this.element.change();
+ }
+
+ this.fill();
+ return this;
+ },
+
+ fillDow: function(){
+ if (this.o.showWeekDays) {
+ var dowCnt = this.o.weekStart,
+ html = '
';
+ if (this.o.calendarWeeks){
+ html += ' | ';
+ }
+ while (dowCnt < this.o.weekStart + 7){
+ html += ''+dates[this.o.language].daysMin[(dowCnt++)%7]+' | ';
+ }
+ html += '
';
+ this.picker.find('.datepicker-days thead').append(html);
+ }
+ },
+
+ fillMonths: function(){
+ var localDate = this._utc_to_local(this.viewDate);
+ var html = '';
+ var focused;
+ for (var i = 0; i < 12; i++){
+ focused = localDate && localDate.getMonth() === i ? ' focused' : '';
+ html += '' + dates[this.o.language].monthsShort[i] + '';
+ }
+ this.picker.find('.datepicker-months td').html(html);
+ },
+
+ setRange: function(range){
+ if (!range || !range.length)
+ delete this.range;
+ else
+ this.range = $.map(range, function(d){
+ return d.valueOf();
+ });
+ this.fill();
+ },
+
+ getClassNames: function(date){
+ var cls = [],
+ year = this.viewDate.getUTCFullYear(),
+ month = this.viewDate.getUTCMonth(),
+ today = UTCToday();
+ if (date.getUTCFullYear() < year || (date.getUTCFullYear() === year && date.getUTCMonth() < month)){
+ cls.push('old');
+ } else if (date.getUTCFullYear() > year || (date.getUTCFullYear() === year && date.getUTCMonth() > month)){
+ cls.push('new');
+ }
+ if (this.focusDate && date.valueOf() === this.focusDate.valueOf())
+ cls.push('focused');
+ // Compare internal UTC date with UTC today, not local today
+ if (this.o.todayHighlight && isUTCEquals(date, today)) {
+ cls.push('today');
+ }
+ if (this.dates.contains(date) !== -1)
+ cls.push('active');
+ if (!this.dateWithinRange(date)){
+ cls.push('disabled');
+ }
+ if (this.dateIsDisabled(date)){
+ cls.push('disabled', 'disabled-date');
+ }
+ if ($.inArray(date.getUTCDay(), this.o.daysOfWeekHighlighted) !== -1){
+ cls.push('highlighted');
+ }
+
+ if (this.range){
+ if (date > this.range[0] && date < this.range[this.range.length-1]){
+ cls.push('range');
+ }
+ if ($.inArray(date.valueOf(), this.range) !== -1){
+ cls.push('selected');
+ }
+ if (date.valueOf() === this.range[0]){
+ cls.push('range-start');
+ }
+ if (date.valueOf() === this.range[this.range.length-1]){
+ cls.push('range-end');
+ }
+ }
+ return cls;
+ },
+
+ _fill_yearsView: function(selector, cssClass, factor, year, startYear, endYear, beforeFn){
+ var html = '';
+ var step = factor / 10;
+ var view = this.picker.find(selector);
+ var startVal = Math.floor(year / factor) * factor;
+ var endVal = startVal + step * 9;
+ var focusedVal = Math.floor(this.viewDate.getFullYear() / step) * step;
+ var selected = $.map(this.dates, function(d){
+ return Math.floor(d.getUTCFullYear() / step) * step;
+ });
+
+ var classes, tooltip, before;
+ for (var currVal = startVal - step; currVal <= endVal + step; currVal += step) {
+ classes = [cssClass];
+ tooltip = null;
+
+ if (currVal === startVal - step) {
+ classes.push('old');
+ } else if (currVal === endVal + step) {
+ classes.push('new');
+ }
+ if ($.inArray(currVal, selected) !== -1) {
+ classes.push('active');
+ }
+ if (currVal < startYear || currVal > endYear) {
+ classes.push('disabled');
+ }
+ if (currVal === focusedVal) {
+ classes.push('focused');
+ }
+
+ if (beforeFn !== $.noop) {
+ before = beforeFn(new Date(currVal, 0, 1));
+ if (before === undefined) {
+ before = {};
+ } else if (typeof before === 'boolean') {
+ before = {enabled: before};
+ } else if (typeof before === 'string') {
+ before = {classes: before};
+ }
+ if (before.enabled === false) {
+ classes.push('disabled');
+ }
+ if (before.classes) {
+ classes = classes.concat(before.classes.split(/\s+/));
+ }
+ if (before.tooltip) {
+ tooltip = before.tooltip;
+ }
+ }
+
+ html += '' + currVal + '';
+ }
+
+ view.find('.datepicker-switch').text(startVal + '-' + endVal);
+ view.find('td').html(html);
+ },
+
+ fill: function(){
+ var d = new Date(this.viewDate),
+ year = d.getUTCFullYear(),
+ month = d.getUTCMonth(),
+ startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity,
+ startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity,
+ endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity,
+ endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity,
+ todaytxt = dates[this.o.language].today || dates['en'].today || '',
+ cleartxt = dates[this.o.language].clear || dates['en'].clear || '',
+ titleFormat = dates[this.o.language].titleFormat || dates['en'].titleFormat,
+ todayDate = UTCToday(),
+ titleBtnVisible = (this.o.todayBtn === true || this.o.todayBtn === 'linked') && todayDate >= this.o.startDate && todayDate <= this.o.endDate && !this.weekOfDateIsDisabled(todayDate),
+ tooltip,
+ before;
+ if (isNaN(year) || isNaN(month))
+ return;
+ this.picker.find('.datepicker-days .datepicker-switch')
+ .text(DPGlobal.formatDate(d, titleFormat, this.o.language));
+ this.picker.find('tfoot .today')
+ .text(todaytxt)
+ .css('display', titleBtnVisible ? 'table-cell' : 'none');
+ this.picker.find('tfoot .clear')
+ .text(cleartxt)
+ .css('display', this.o.clearBtn === true ? 'table-cell' : 'none');
+ this.picker.find('thead .datepicker-title')
+ .text(this.o.title)
+ .css('display', typeof this.o.title === 'string' && this.o.title !== '' ? 'table-cell' : 'none');
+ this.updateNavArrows();
+ this.fillMonths();
+ var prevMonth = UTCDate(year, month, 0),
+ day = prevMonth.getUTCDate();
+ prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.weekStart + 7)%7);
+ var nextMonth = new Date(prevMonth);
+ if (prevMonth.getUTCFullYear() < 100){
+ nextMonth.setUTCFullYear(prevMonth.getUTCFullYear());
+ }
+ nextMonth.setUTCDate(nextMonth.getUTCDate() + 42);
+ nextMonth = nextMonth.valueOf();
+ var html = [];
+ var weekDay, clsName;
+ while (prevMonth.valueOf() < nextMonth){
+ weekDay = prevMonth.getUTCDay();
+ if (weekDay === this.o.weekStart){
+ html.push('');
+ if (this.o.calendarWeeks){
+ // ISO 8601: First week contains first thursday.
+ // ISO also states week starts on Monday, but we can be more abstract here.
+ var
+ // Start of current week: based on weekstart/current date
+ ws = new Date(+prevMonth + (this.o.weekStart - weekDay - 7) % 7 * 864e5),
+ // Thursday of this week
+ th = new Date(Number(ws) + (7 + 4 - ws.getUTCDay()) % 7 * 864e5),
+ // First Thursday of year, year from thursday
+ yth = new Date(Number(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay()) % 7 * 864e5),
+ // Calendar week: ms between thursdays, div ms per day, div 7 days
+ calWeek = (th - yth) / 864e5 / 7 + 1;
+ html.push(''+ calWeek +' | ');
+ }
+ }
+ clsName = this.getClassNames(prevMonth);
+ clsName.push('day');
+
+ var content = prevMonth.getUTCDate();
+
+ if (this.o.beforeShowDay !== $.noop){
+ before = this.o.beforeShowDay(this._utc_to_local(prevMonth));
+ if (before === undefined)
+ before = {};
+ else if (typeof before === 'boolean')
+ before = {enabled: before};
+ else if (typeof before === 'string')
+ before = {classes: before};
+ if (before.enabled === false)
+ clsName.push('disabled');
+ if (before.classes)
+ clsName = clsName.concat(before.classes.split(/\s+/));
+ if (before.tooltip)
+ tooltip = before.tooltip;
+ if (before.content)
+ content = before.content;
+ }
+
+ //Check if uniqueSort exists (supported by jquery >=1.12 and >=2.2)
+ //Fallback to unique function for older jquery versions
+ if ($.isFunction($.uniqueSort)) {
+ clsName = $.uniqueSort(clsName);
+ } else {
+ clsName = $.unique(clsName);
+ }
+
+ html.push('' + content + ' | ');
+ tooltip = null;
+ if (weekDay === this.o.weekEnd){
+ html.push('
');
+ }
+ prevMonth.setUTCDate(prevMonth.getUTCDate() + 1);
+ }
+ this.picker.find('.datepicker-days tbody').html(html.join(''));
+
+ var monthsTitle = dates[this.o.language].monthsTitle || dates['en'].monthsTitle || 'Months';
+ var months = this.picker.find('.datepicker-months')
+ .find('.datepicker-switch')
+ .text(this.o.maxViewMode < 2 ? monthsTitle : year)
+ .end()
+ .find('tbody span').removeClass('active');
+
+ $.each(this.dates, function(i, d){
+ if (d.getUTCFullYear() === year)
+ months.eq(d.getUTCMonth()).addClass('active');
+ });
+
+ if (year < startYear || year > endYear){
+ months.addClass('disabled');
+ }
+ if (year === startYear){
+ months.slice(0, startMonth).addClass('disabled');
+ }
+ if (year === endYear){
+ months.slice(endMonth+1).addClass('disabled');
+ }
+
+ if (this.o.beforeShowMonth !== $.noop){
+ var that = this;
+ $.each(months, function(i, month){
+ var moDate = new Date(year, i, 1);
+ var before = that.o.beforeShowMonth(moDate);
+ if (before === undefined)
+ before = {};
+ else if (typeof before === 'boolean')
+ before = {enabled: before};
+ else if (typeof before === 'string')
+ before = {classes: before};
+ if (before.enabled === false && !$(month).hasClass('disabled'))
+ $(month).addClass('disabled');
+ if (before.classes)
+ $(month).addClass(before.classes);
+ if (before.tooltip)
+ $(month).prop('title', before.tooltip);
+ });
+ }
+
+ // Generating decade/years picker
+ this._fill_yearsView(
+ '.datepicker-years',
+ 'year',
+ 10,
+ year,
+ startYear,
+ endYear,
+ this.o.beforeShowYear
+ );
+
+ // Generating century/decades picker
+ this._fill_yearsView(
+ '.datepicker-decades',
+ 'decade',
+ 100,
+ year,
+ startYear,
+ endYear,
+ this.o.beforeShowDecade
+ );
+
+ // Generating millennium/centuries picker
+ this._fill_yearsView(
+ '.datepicker-centuries',
+ 'century',
+ 1000,
+ year,
+ startYear,
+ endYear,
+ this.o.beforeShowCentury
+ );
+ },
+
+ updateNavArrows: function(){
+ if (!this._allow_update)
+ return;
+
+ var d = new Date(this.viewDate),
+ year = d.getUTCFullYear(),
+ month = d.getUTCMonth(),
+ startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity,
+ startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity,
+ endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity,
+ endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity,
+ prevIsDisabled,
+ nextIsDisabled,
+ factor = 1;
+ switch (this.viewMode){
+ case 4:
+ factor *= 10;
+ /* falls through */
+ case 3:
+ factor *= 10;
+ /* falls through */
+ case 2:
+ factor *= 10;
+ /* falls through */
+ case 1:
+ prevIsDisabled = Math.floor(year / factor) * factor <= startYear;
+ nextIsDisabled = Math.floor(year / factor) * factor + factor > endYear;
+ break;
+ case 0:
+ prevIsDisabled = year <= startYear && month <= startMonth;
+ nextIsDisabled = year >= endYear && month >= endMonth;
+ break;
+ }
+
+ this.picker.find('.prev').toggleClass('disabled', prevIsDisabled);
+ this.picker.find('.next').toggleClass('disabled', nextIsDisabled);
+ },
+
+ click: function(e){
+ e.preventDefault();
+ e.stopPropagation();
+
+ var target, dir, day, year, month;
+ target = $(e.target);
+
+ // Clicked on the switch
+ if (target.hasClass('datepicker-switch') && this.viewMode !== this.o.maxViewMode){
+ this.setViewMode(this.viewMode + 1);
+ }
+
+ // Clicked on today button
+ if (target.hasClass('today') && !target.hasClass('day')){
+ this.setViewMode(0);
+ this._setDate(UTCToday(), this.o.todayBtn === 'linked' ? null : 'view');
+ }
+
+ // Clicked on clear button
+ if (target.hasClass('clear')){
+ this.clearDates();
+ }
+
+ if (!target.hasClass('disabled')){
+ // Clicked on a month, year, decade, century
+ if (target.hasClass('month')
+ || target.hasClass('year')
+ || target.hasClass('decade')
+ || target.hasClass('century')) {
+ this.viewDate.setUTCDate(1);
+
+ day = 1;
+ if (this.viewMode === 1){
+ month = target.parent().find('span').index(target);
+ year = this.viewDate.getUTCFullYear();
+ this.viewDate.setUTCMonth(month);
+ } else {
+ month = 0;
+ year = Number(target.text());
+ this.viewDate.setUTCFullYear(year);
+ }
+
+ this._trigger(DPGlobal.viewModes[this.viewMode - 1].e, this.viewDate);
+
+ if (this.viewMode === this.o.minViewMode){
+ this._setDate(UTCDate(year, month, day));
+ } else {
+ this.setViewMode(this.viewMode - 1);
+ this.fill();
+ }
+ }
+ }
+
+ if (this.picker.is(':visible') && this._focused_from){
+ this._focused_from.focus();
+ }
+ delete this._focused_from;
+ },
+
+ dayCellClick: function(e){
+ var $target = $(e.currentTarget);
+ var timestamp = $target.data('date');
+ var date = new Date(timestamp);
+
+ if (this.o.updateViewDate) {
+ if (date.getUTCFullYear() !== this.viewDate.getUTCFullYear()) {
+ this._trigger('changeYear', this.viewDate);
+ }
+
+ if (date.getUTCMonth() !== this.viewDate.getUTCMonth()) {
+ this._trigger('changeMonth', this.viewDate);
+ }
+ }
+ this._setDate(date);
+ },
+
+ // Clicked on prev or next
+ navArrowsClick: function(e){
+ var $target = $(e.currentTarget);
+ var dir = $target.hasClass('prev') ? -1 : 1;
+ if (this.viewMode !== 0){
+ dir *= DPGlobal.viewModes[this.viewMode].navStep * 12;
+ }
+ this.viewDate = this.moveMonth(this.viewDate, dir);
+ this._trigger(DPGlobal.viewModes[this.viewMode].e, this.viewDate);
+ this.fill();
+ },
+
+ _toggle_multidate: function(date){
+ var ix = this.dates.contains(date);
+ if (!date){
+ this.dates.clear();
+ }
+
+ if (ix !== -1){
+ if (this.o.multidate === true || this.o.multidate > 1 || this.o.toggleActive){
+ this.dates.remove(ix);
+ }
+ } else if (this.o.multidate === false) {
+ this.dates.clear();
+ this.dates.push(date);
+ }
+ else {
+ this.dates.push(date);
+ }
+
+ if (typeof this.o.multidate === 'number')
+ while (this.dates.length > this.o.multidate)
+ this.dates.remove(0);
+ },
+
+ _setDate: function(date, which){
+ if (!which || which === 'date')
+ this._toggle_multidate(date && new Date(date));
+ if ((!which && this.o.updateViewDate) || which === 'view')
+ this.viewDate = date && new Date(date);
+
+ this.fill();
+ this.setValue();
+ if (!which || which !== 'view') {
+ this._trigger('changeDate');
+ }
+ this.inputField.trigger('change');
+ if (this.o.autoclose && (!which || which === 'date')){
+ this.hide();
+ }
+ },
+
+ moveDay: function(date, dir){
+ var newDate = new Date(date);
+ newDate.setUTCDate(date.getUTCDate() + dir);
+
+ return newDate;
+ },
+
+ moveWeek: function(date, dir){
+ return this.moveDay(date, dir * 7);
+ },
+
+ moveMonth: function(date, dir){
+ if (!isValidDate(date))
+ return this.o.defaultViewDate;
+ if (!dir)
+ return date;
+ var new_date = new Date(date.valueOf()),
+ day = new_date.getUTCDate(),
+ month = new_date.getUTCMonth(),
+ mag = Math.abs(dir),
+ new_month, test;
+ dir = dir > 0 ? 1 : -1;
+ if (mag === 1){
+ test = dir === -1
+ // If going back one month, make sure month is not current month
+ // (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02)
+ ? function(){
+ return new_date.getUTCMonth() === month;
+ }
+ // If going forward one month, make sure month is as expected
+ // (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02)
+ : function(){
+ return new_date.getUTCMonth() !== new_month;
+ };
+ new_month = month + dir;
+ new_date.setUTCMonth(new_month);
+ // Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11
+ new_month = (new_month + 12) % 12;
+ }
+ else {
+ // For magnitudes >1, move one month at a time...
+ for (var i=0; i < mag; i++)
+ // ...which might decrease the day (eg, Jan 31 to Feb 28, etc)...
+ new_date = this.moveMonth(new_date, dir);
+ // ...then reset the day, keeping it in the new month
+ new_month = new_date.getUTCMonth();
+ new_date.setUTCDate(day);
+ test = function(){
+ return new_month !== new_date.getUTCMonth();
+ };
+ }
+ // Common date-resetting loop -- if date is beyond end of month, make it
+ // end of month
+ while (test()){
+ new_date.setUTCDate(--day);
+ new_date.setUTCMonth(new_month);
+ }
+ return new_date;
+ },
+
+ moveYear: function(date, dir){
+ return this.moveMonth(date, dir*12);
+ },
+
+ moveAvailableDate: function(date, dir, fn){
+ do {
+ date = this[fn](date, dir);
+
+ if (!this.dateWithinRange(date))
+ return false;
+
+ fn = 'moveDay';
+ }
+ while (this.dateIsDisabled(date));
+
+ return date;
+ },
+
+ weekOfDateIsDisabled: function(date){
+ return $.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1;
+ },
+
+ dateIsDisabled: function(date){
+ return (
+ this.weekOfDateIsDisabled(date) ||
+ $.grep(this.o.datesDisabled, function(d){
+ return isUTCEquals(date, d);
+ }).length > 0
+ );
+ },
+
+ dateWithinRange: function(date){
+ return date >= this.o.startDate && date <= this.o.endDate;
+ },
+
+ keydown: function(e){
+ if (!this.picker.is(':visible')){
+ if (e.keyCode === 40 || e.keyCode === 27) { // allow down to re-show picker
+ this.show();
+ e.stopPropagation();
+ }
+ return;
+ }
+ var dateChanged = false,
+ dir, newViewDate,
+ focusDate = this.focusDate || this.viewDate;
+ switch (e.keyCode){
+ case 27: // escape
+ if (this.focusDate){
+ this.focusDate = null;
+ this.viewDate = this.dates.get(-1) || this.viewDate;
+ this.fill();
+ }
+ else
+ this.hide();
+ e.preventDefault();
+ e.stopPropagation();
+ break;
+ case 37: // left
+ case 38: // up
+ case 39: // right
+ case 40: // down
+ if (!this.o.keyboardNavigation || this.o.daysOfWeekDisabled.length === 7)
+ break;
+ dir = e.keyCode === 37 || e.keyCode === 38 ? -1 : 1;
+ if (this.viewMode === 0) {
+ if (e.ctrlKey){
+ newViewDate = this.moveAvailableDate(focusDate, dir, 'moveYear');
+
+ if (newViewDate)
+ this._trigger('changeYear', this.viewDate);
+ } else if (e.shiftKey){
+ newViewDate = this.moveAvailableDate(focusDate, dir, 'moveMonth');
+
+ if (newViewDate)
+ this._trigger('changeMonth', this.viewDate);
+ } else if (e.keyCode === 37 || e.keyCode === 39){
+ newViewDate = this.moveAvailableDate(focusDate, dir, 'moveDay');
+ } else if (!this.weekOfDateIsDisabled(focusDate)){
+ newViewDate = this.moveAvailableDate(focusDate, dir, 'moveWeek');
+ }
+ } else if (this.viewMode === 1) {
+ if (e.keyCode === 38 || e.keyCode === 40) {
+ dir = dir * 4;
+ }
+ newViewDate = this.moveAvailableDate(focusDate, dir, 'moveMonth');
+ } else if (this.viewMode === 2) {
+ if (e.keyCode === 38 || e.keyCode === 40) {
+ dir = dir * 4;
+ }
+ newViewDate = this.moveAvailableDate(focusDate, dir, 'moveYear');
+ }
+ if (newViewDate){
+ this.focusDate = this.viewDate = newViewDate;
+ this.setValue();
+ this.fill();
+ e.preventDefault();
+ }
+ break;
+ case 13: // enter
+ if (!this.o.forceParse)
+ break;
+ focusDate = this.focusDate || this.dates.get(-1) || this.viewDate;
+ if (this.o.keyboardNavigation) {
+ this._toggle_multidate(focusDate);
+ dateChanged = true;
+ }
+ this.focusDate = null;
+ this.viewDate = this.dates.get(-1) || this.viewDate;
+ this.setValue();
+ this.fill();
+ if (this.picker.is(':visible')){
+ e.preventDefault();
+ e.stopPropagation();
+ if (this.o.autoclose)
+ this.hide();
+ }
+ break;
+ case 9: // tab
+ this.focusDate = null;
+ this.viewDate = this.dates.get(-1) || this.viewDate;
+ this.fill();
+ this.hide();
+ break;
+ }
+ if (dateChanged){
+ if (this.dates.length)
+ this._trigger('changeDate');
+ else
+ this._trigger('clearDate');
+ this.inputField.trigger('change');
+ }
+ },
+
+ setViewMode: function(viewMode){
+ this.viewMode = viewMode;
+ this.picker
+ .children('div')
+ .hide()
+ .filter('.datepicker-' + DPGlobal.viewModes[this.viewMode].clsName)
+ .show();
+ this.updateNavArrows();
+ this._trigger('changeViewMode', new Date(this.viewDate));
+ }
+ };
+
+ var DateRangePicker = function(element, options){
+ $.data(element, 'datepicker', this);
+ this.element = $(element);
+ this.inputs = $.map(options.inputs, function(i){
+ return i.jquery ? i[0] : i;
+ });
+ delete options.inputs;
+
+ this.keepEmptyValues = options.keepEmptyValues;
+ delete options.keepEmptyValues;
+
+ datepickerPlugin.call($(this.inputs), options)
+ .on('changeDate', $.proxy(this.dateUpdated, this));
+
+ this.pickers = $.map(this.inputs, function(i){
+ return $.data(i, 'datepicker');
+ });
+ this.updateDates();
+ };
+ DateRangePicker.prototype = {
+ updateDates: function(){
+ this.dates = $.map(this.pickers, function(i){
+ return i.getUTCDate();
+ });
+ this.updateRanges();
+ },
+ updateRanges: function(){
+ var range = $.map(this.dates, function(d){
+ return d.valueOf();
+ });
+ $.each(this.pickers, function(i, p){
+ p.setRange(range);
+ });
+ },
+ clearDates: function(){
+ $.each(this.pickers, function(i, p){
+ p.clearDates();
+ });
+ },
+ dateUpdated: function(e){
+ // `this.updating` is a workaround for preventing infinite recursion
+ // between `changeDate` triggering and `setUTCDate` calling. Until
+ // there is a better mechanism.
+ if (this.updating)
+ return;
+ this.updating = true;
+
+ var dp = $.data(e.target, 'datepicker');
+
+ if (dp === undefined) {
+ return;
+ }
+
+ var new_date = dp.getUTCDate(),
+ keep_empty_values = this.keepEmptyValues,
+ i = $.inArray(e.target, this.inputs),
+ j = i - 1,
+ k = i + 1,
+ l = this.inputs.length;
+ if (i === -1)
+ return;
+
+ $.each(this.pickers, function(i, p){
+ if (!p.getUTCDate() && (p === dp || !keep_empty_values))
+ p.setUTCDate(new_date);
+ });
+
+ if (new_date < this.dates[j]){
+ // Date being moved earlier/left
+ while (j >= 0 && new_date < this.dates[j]){
+ this.pickers[j--].setUTCDate(new_date);
+ }
+ } else if (new_date > this.dates[k]){
+ // Date being moved later/right
+ while (k < l && new_date > this.dates[k]){
+ this.pickers[k++].setUTCDate(new_date);
+ }
+ }
+ this.updateDates();
+
+ delete this.updating;
+ },
+ destroy: function(){
+ $.map(this.pickers, function(p){ p.destroy(); });
+ $(this.inputs).off('changeDate', this.dateUpdated);
+ delete this.element.data().datepicker;
+ },
+ remove: alias('destroy', 'Method `remove` is deprecated and will be removed in version 2.0. Use `destroy` instead')
+ };
+
+ function opts_from_el(el, prefix){
+ // Derive options from element data-attrs
+ var data = $(el).data(),
+ out = {}, inkey,
+ replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])');
+ prefix = new RegExp('^' + prefix.toLowerCase());
+ function re_lower(_,a){
+ return a.toLowerCase();
+ }
+ for (var key in data)
+ if (prefix.test(key)){
+ inkey = key.replace(replace, re_lower);
+ out[inkey] = data[key];
+ }
+ return out;
+ }
+
+ function opts_from_locale(lang){
+ // Derive options from locale plugins
+ var out = {};
+ // Check if "de-DE" style date is available, if not language should
+ // fallback to 2 letter code eg "de"
+ if (!dates[lang]){
+ lang = lang.split('-')[0];
+ if (!dates[lang])
+ return;
+ }
+ var d = dates[lang];
+ $.each(locale_opts, function(i,k){
+ if (k in d)
+ out[k] = d[k];
+ });
+ return out;
+ }
+
+ var old = $.fn.datepicker;
+ var datepickerPlugin = function(option){
+ var args = Array.apply(null, arguments);
+ args.shift();
+ var internal_return;
+ this.each(function(){
+ var $this = $(this),
+ data = $this.data('datepicker'),
+ options = typeof option === 'object' && option;
+ if (!data){
+ var elopts = opts_from_el(this, 'date'),
+ // Preliminary otions
+ xopts = $.extend({}, defaults, elopts, options),
+ locopts = opts_from_locale(xopts.language),
+ // Options priority: js args, data-attrs, locales, defaults
+ opts = $.extend({}, defaults, locopts, elopts, options);
+ if ($this.hasClass('input-daterange') || opts.inputs){
+ $.extend(opts, {
+ inputs: opts.inputs || $this.find('input').toArray()
+ });
+ data = new DateRangePicker(this, opts);
+ }
+ else {
+ data = new Datepicker(this, opts);
+ }
+ $this.data('datepicker', data);
+ }
+ if (typeof option === 'string' && typeof data[option] === 'function'){
+ internal_return = data[option].apply(data, args);
+ }
+ });
+
+ if (
+ internal_return === undefined ||
+ internal_return instanceof Datepicker ||
+ internal_return instanceof DateRangePicker
+ )
+ return this;
+
+ if (this.length > 1)
+ throw new Error('Using only allowed for the collection of a single element (' + option + ' function)');
+ else
+ return internal_return;
+ };
+ $.fn.datepicker = datepickerPlugin;
+
+ var defaults = $.fn.datepicker.defaults = {
+ assumeNearbyYear: false,
+ autoclose: false,
+ beforeShowDay: $.noop,
+ beforeShowMonth: $.noop,
+ beforeShowYear: $.noop,
+ beforeShowDecade: $.noop,
+ beforeShowCentury: $.noop,
+ calendarWeeks: false,
+ clearBtn: false,
+ toggleActive: false,
+ daysOfWeekDisabled: [],
+ daysOfWeekHighlighted: [],
+ datesDisabled: [],
+ endDate: Infinity,
+ forceParse: true,
+ format: 'mm/dd/yyyy',
+ keepEmptyValues: false,
+ keyboardNavigation: true,
+ language: 'en',
+ minViewMode: 0,
+ maxViewMode: 4,
+ multidate: false,
+ multidateSeparator: ',',
+ orientation: "auto",
+ rtl: false,
+ startDate: -Infinity,
+ startView: 0,
+ todayBtn: false,
+ todayHighlight: false,
+ updateViewDate: true,
+ weekStart: 0,
+ disableTouchKeyboard: false,
+ enableOnReadonly: true,
+ showOnFocus: true,
+ zIndexOffset: 10,
+ container: 'body',
+ immediateUpdates: false,
+ title: '',
+ templates: {
+ leftArrow: '«',
+ rightArrow: '»'
+ },
+ showWeekDays: true
+ };
+ var locale_opts = $.fn.datepicker.locale_opts = [
+ 'format',
+ 'rtl',
+ 'weekStart'
+ ];
+ $.fn.datepicker.Constructor = Datepicker;
+ var dates = $.fn.datepicker.dates = {
+ en: {
+ days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
+ daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
+ months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
+ monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
+ today: "Today",
+ clear: "Clear",
+ titleFormat: "MM yyyy"
+ }
+ };
+
+ var DPGlobal = {
+ viewModes: [
+ {
+ names: ['days', 'month'],
+ clsName: 'days',
+ e: 'changeMonth'
+ },
+ {
+ names: ['months', 'year'],
+ clsName: 'months',
+ e: 'changeYear',
+ navStep: 1
+ },
+ {
+ names: ['years', 'decade'],
+ clsName: 'years',
+ e: 'changeDecade',
+ navStep: 10
+ },
+ {
+ names: ['decades', 'century'],
+ clsName: 'decades',
+ e: 'changeCentury',
+ navStep: 100
+ },
+ {
+ names: ['centuries', 'millennium'],
+ clsName: 'centuries',
+ e: 'changeMillennium',
+ navStep: 1000
+ }
+ ],
+ validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g,
+ nonpunctuation: /[^ -\/:-@\u5e74\u6708\u65e5\[-`{-~\t\n\r]+/g,
+ parseFormat: function(format){
+ if (typeof format.toValue === 'function' && typeof format.toDisplay === 'function')
+ return format;
+ // IE treats \0 as a string end in inputs (truncating the value),
+ // so it's a bad format delimiter, anyway
+ var separators = format.replace(this.validParts, '\0').split('\0'),
+ parts = format.match(this.validParts);
+ if (!separators || !separators.length || !parts || parts.length === 0){
+ throw new Error("Invalid date format.");
+ }
+ return {separators: separators, parts: parts};
+ },
+ parseDate: function(date, format, language, assumeNearby){
+ if (!date)
+ return undefined;
+ if (date instanceof Date)
+ return date;
+ if (typeof format === 'string')
+ format = DPGlobal.parseFormat(format);
+ if (format.toValue)
+ return format.toValue(date, format, language);
+ var fn_map = {
+ d: 'moveDay',
+ m: 'moveMonth',
+ w: 'moveWeek',
+ y: 'moveYear'
+ },
+ dateAliases = {
+ yesterday: '-1d',
+ today: '+0d',
+ tomorrow: '+1d'
+ },
+ parts, part, dir, i, fn;
+ if (date in dateAliases){
+ date = dateAliases[date];
+ }
+ if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/i.test(date)){
+ parts = date.match(/([\-+]\d+)([dmwy])/gi);
+ date = new Date();
+ for (i=0; i < parts.length; i++){
+ part = parts[i].match(/([\-+]\d+)([dmwy])/i);
+ dir = Number(part[1]);
+ fn = fn_map[part[2].toLowerCase()];
+ date = Datepicker.prototype[fn](date, dir);
+ }
+ return Datepicker.prototype._zero_utc_time(date);
+ }
+
+ parts = date && date.match(this.nonpunctuation) || [];
+
+ function applyNearbyYear(year, threshold){
+ if (threshold === true)
+ threshold = 10;
+
+ // if year is 2 digits or less, than the user most likely is trying to get a recent century
+ if (year < 100){
+ year += 2000;
+ // if the new year is more than threshold years in advance, use last century
+ if (year > ((new Date()).getFullYear()+threshold)){
+ year -= 100;
+ }
+ }
+
+ return year;
+ }
+
+ var parsed = {},
+ setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'],
+ setters_map = {
+ yyyy: function(d,v){
+ return d.setUTCFullYear(assumeNearby ? applyNearbyYear(v, assumeNearby) : v);
+ },
+ m: function(d,v){
+ if (isNaN(d))
+ return d;
+ v -= 1;
+ while (v < 0) v += 12;
+ v %= 12;
+ d.setUTCMonth(v);
+ while (d.getUTCMonth() !== v)
+ d.setUTCDate(d.getUTCDate()-1);
+ return d;
+ },
+ d: function(d,v){
+ return d.setUTCDate(v);
+ }
+ },
+ val, filtered;
+ setters_map['yy'] = setters_map['yyyy'];
+ setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m'];
+ setters_map['dd'] = setters_map['d'];
+ date = UTCToday();
+ var fparts = format.parts.slice();
+ // Remove noop parts
+ if (parts.length !== fparts.length){
+ fparts = $(fparts).filter(function(i,p){
+ return $.inArray(p, setters_order) !== -1;
+ }).toArray();
+ }
+ // Process remainder
+ function match_part(){
+ var m = this.slice(0, parts[i].length),
+ p = parts[i].slice(0, m.length);
+ return m.toLowerCase() === p.toLowerCase();
+ }
+ if (parts.length === fparts.length){
+ var cnt;
+ for (i=0, cnt = fparts.length; i < cnt; i++){
+ val = parseInt(parts[i], 10);
+ part = fparts[i];
+ if (isNaN(val)){
+ switch (part){
+ case 'MM':
+ filtered = $(dates[language].months).filter(match_part);
+ val = $.inArray(filtered[0], dates[language].months) + 1;
+ break;
+ case 'M':
+ filtered = $(dates[language].monthsShort).filter(match_part);
+ val = $.inArray(filtered[0], dates[language].monthsShort) + 1;
+ break;
+ }
+ }
+ parsed[part] = val;
+ }
+ var _date, s;
+ for (i=0; i < setters_order.length; i++){
+ s = setters_order[i];
+ if (s in parsed && !isNaN(parsed[s])){
+ _date = new Date(date);
+ setters_map[s](_date, parsed[s]);
+ if (!isNaN(_date))
+ date = _date;
+ }
+ }
+ }
+ return date;
+ },
+ formatDate: function(date, format, language){
+ if (!date)
+ return '';
+ if (typeof format === 'string')
+ format = DPGlobal.parseFormat(format);
+ if (format.toDisplay)
+ return format.toDisplay(date, format, language);
+ var val = {
+ d: date.getUTCDate(),
+ D: dates[language].daysShort[date.getUTCDay()],
+ DD: dates[language].days[date.getUTCDay()],
+ m: date.getUTCMonth() + 1,
+ M: dates[language].monthsShort[date.getUTCMonth()],
+ MM: dates[language].months[date.getUTCMonth()],
+ yy: date.getUTCFullYear().toString().substring(2),
+ yyyy: date.getUTCFullYear()
+ };
+ val.dd = (val.d < 10 ? '0' : '') + val.d;
+ val.mm = (val.m < 10 ? '0' : '') + val.m;
+ date = [];
+ var seps = $.extend([], format.separators);
+ for (var i=0, cnt = format.parts.length; i <= cnt; i++){
+ if (seps.length)
+ date.push(seps.shift());
+ date.push(val[format.parts[i]]);
+ }
+ return date.join('');
+ },
+ headTemplate: ''+
+ ''+
+ ' | '+
+ '
'+
+ ''+
+ ''+defaults.templates.leftArrow+' | '+
+ ' | '+
+ ''+defaults.templates.rightArrow+' | '+
+ '
'+
+ '',
+ contTemplate: ' |
',
+ footTemplate: ''+
+ ''+
+ ' | '+
+ '
'+
+ ''+
+ ' | '+
+ '
'+
+ ''
+ };
+ DPGlobal.template = ''+
+ '
'+
+ '
'+
+ DPGlobal.headTemplate+
+ ''+
+ DPGlobal.footTemplate+
+ '
'+
+ '
'+
+ '
'+
+ '
'+
+ DPGlobal.headTemplate+
+ DPGlobal.contTemplate+
+ DPGlobal.footTemplate+
+ '
'+
+ '
'+
+ '
'+
+ '
'+
+ DPGlobal.headTemplate+
+ DPGlobal.contTemplate+
+ DPGlobal.footTemplate+
+ '
'+
+ '
'+
+ '
'+
+ '
'+
+ DPGlobal.headTemplate+
+ DPGlobal.contTemplate+
+ DPGlobal.footTemplate+
+ '
'+
+ '
'+
+ '
'+
+ '
'+
+ DPGlobal.headTemplate+
+ DPGlobal.contTemplate+
+ DPGlobal.footTemplate+
+ '
'+
+ '
'+
+ '
';
+
+ $.fn.datepicker.DPGlobal = DPGlobal;
+
+
+ /* DATEPICKER NO CONFLICT
+ * =================== */
+
+ $.fn.datepicker.noConflict = function(){
+ $.fn.datepicker = old;
+ return this;
+ };
+
+ /* DATEPICKER VERSION
+ * =================== */
+ $.fn.datepicker.version = '1.9.0';
+
+ $.fn.datepicker.deprecated = function(msg){
+ var console = window.console;
+ if (console && console.warn) {
+ console.warn('DEPRECATED: ' + msg);
+ }
+ };
+
+
+ /* DATEPICKER DATA-API
+ * ================== */
+
+ $(document).on(
+ 'focus.datepicker.data-api click.datepicker.data-api',
+ '[data-provide="datepicker"]',
+ function(e){
+ var $this = $(this);
+ if ($this.data('datepicker'))
+ return;
+ e.preventDefault();
+ // component click requires us to explicitly show it
+ datepickerPlugin.call($this, 'show');
+ }
+ );
+ $(function(){
+ datepickerPlugin.call($('[data-provide="datepicker-inline"]'));
+ });
+
+}));
\ No newline at end of file
diff --git a/app/assets/javascripts/i18n/bootstrap-datepicker.zh-CN.min.js b/app/assets/javascripts/i18n/bootstrap-datepicker.zh-CN.min.js
new file mode 100644
index 000000000..8e6920b0c
--- /dev/null
+++ b/app/assets/javascripts/i18n/bootstrap-datepicker.zh-CN.min.js
@@ -0,0 +1 @@
+!function(a){a.fn.datepicker.dates["zh-CN"]={days:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],daysShort:["周日","周一","周二","周三","周四","周五","周六"],daysMin:["日","一","二","三","四","五","六"],months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthsShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],today:"今天",monthsTitle:"选择月份",clear:"清除",format:"yyyy-mm-dd",titleFormat:"yyyy年mm月",weekStart:1}}(jQuery);
\ No newline at end of file
diff --git a/app/assets/javascripts/jquery-validate-message-zh.js b/app/assets/javascripts/i18n/jquery-validate-message-zh.js
similarity index 100%
rename from app/assets/javascripts/jquery-validate-message-zh.js
rename to app/assets/javascripts/i18n/jquery-validate-message-zh.js
diff --git a/app/assets/javascripts/select2-i18n.zh-CN.js b/app/assets/javascripts/i18n/select2-i18n.zh-CN.js
similarity index 100%
rename from app/assets/javascripts/select2-i18n.zh-CN.js
rename to app/assets/javascripts/i18n/select2-i18n.zh-CN.js
diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss
index dd0843946..33d585d83 100644
--- a/app/assets/stylesheets/admin.scss
+++ b/app/assets/stylesheets/admin.scss
@@ -3,6 +3,8 @@
@import "font-awesome";
@import "select2.min";
@import "select2-bootstrap4.min";
+@import "bootstrap-datepicker";
+@import "bootstrap-datepicker.standalone";
@import "admins/*";
diff --git a/app/assets/stylesheets/admins/common.scss b/app/assets/stylesheets/admins/common.scss
index b24d0f8b2..9830df3c7 100644
--- a/app/assets/stylesheets/admins/common.scss
+++ b/app/assets/stylesheets/admins/common.scss
@@ -40,6 +40,7 @@
}
& > td {
+ text-align: center;
height: 300px;
}
}
@@ -72,14 +73,32 @@
/* 搜索表单 */
.search-form-container {
+ display: flex;
margin-bottom: 20px;
.search-form {
+ flex: 1;
+
+ * { font-size: 14px; }
+
select, input {
margin-right: 10px;
font-size: 14px;
}
}
}
+
+ .global-error {
+ color: grey;
+ min-height: 300px;
+
+ &-code {
+ font-size: 80px;
+ }
+
+ &-text {
+ font-size: 24px;
+ }
+ }
}
diff --git a/app/assets/stylesheets/admins/daily-school-statistics.scss b/app/assets/stylesheets/admins/daily-school-statistics.scss
new file mode 100644
index 000000000..d54881b77
--- /dev/null
+++ b/app/assets/stylesheets/admins/daily-school-statistics.scss
@@ -0,0 +1,5 @@
+.admins-daily-school-statistics-index-page {
+ .daily-school-statistic-list-container {
+ text-align: center;
+ }
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/admins/school-statistics.scss b/app/assets/stylesheets/admins/school-statistics.scss
new file mode 100644
index 000000000..2bbcef5a3
--- /dev/null
+++ b/app/assets/stylesheets/admins/school-statistics.scss
@@ -0,0 +1,43 @@
+.admins-school-statistics-index-page {
+ .school-statistic-list-form {
+ .time-select {
+ flex: 1;
+ }
+
+ .type-box {
+ .btn { margin: 0 5px; }
+ }
+
+ .search-input {
+ width: 220px;
+ }
+
+ .contrast-date-container {
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ .school-statistic-list-container {
+ .contrast-column-select {
+ position: absolute;
+ right: 30px;
+ top: 15px;
+ width: 130px;
+ }
+
+ .relative {
+ position: relative;
+ }
+
+ .right-border::after {
+ position: absolute;
+ top: 10px;
+ right: 0;
+ content: '';
+ width: 0;
+ height: 20px;
+ border-right: 1px solid #000;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/bootstrap-datepicker.scss b/app/assets/stylesheets/bootstrap-datepicker.scss
new file mode 100644
index 000000000..c8a35bb38
--- /dev/null
+++ b/app/assets/stylesheets/bootstrap-datepicker.scss
@@ -0,0 +1,477 @@
+/*!
+ * Datepicker for Bootstrap v1.9.0 (https://github.com/uxsolutions/bootstrap-datepicker)
+ *
+ * Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ */
+
+.datepicker {
+ padding: 4px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ direction: ltr;
+}
+.datepicker-inline {
+ width: 220px;
+}
+.datepicker-rtl {
+ direction: rtl;
+}
+.datepicker-rtl.dropdown-menu {
+ left: auto;
+}
+.datepicker-rtl table tr td span {
+ float: right;
+}
+.datepicker-dropdown {
+ top: 0;
+ left: 0;
+}
+.datepicker-dropdown:before {
+ content: '';
+ display: inline-block;
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ border-bottom: 7px solid #999;
+ border-top: 0;
+ border-bottom-color: rgba(0, 0, 0, 0.2);
+ position: absolute;
+}
+.datepicker-dropdown:after {
+ content: '';
+ display: inline-block;
+ border-left: 6px solid transparent;
+ border-right: 6px solid transparent;
+ border-bottom: 6px solid #fff;
+ border-top: 0;
+ position: absolute;
+}
+.datepicker-dropdown.datepicker-orient-left:before {
+ left: 6px;
+}
+.datepicker-dropdown.datepicker-orient-left:after {
+ left: 7px;
+}
+.datepicker-dropdown.datepicker-orient-right:before {
+ right: 6px;
+}
+.datepicker-dropdown.datepicker-orient-right:after {
+ right: 7px;
+}
+.datepicker-dropdown.datepicker-orient-bottom:before {
+ top: -7px;
+}
+.datepicker-dropdown.datepicker-orient-bottom:after {
+ top: -6px;
+}
+.datepicker-dropdown.datepicker-orient-top:before {
+ bottom: -7px;
+ border-bottom: 0;
+ border-top: 7px solid #999;
+}
+.datepicker-dropdown.datepicker-orient-top:after {
+ bottom: -6px;
+ border-bottom: 0;
+ border-top: 6px solid #fff;
+}
+.datepicker table {
+ margin: 0;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.datepicker td,
+.datepicker th {
+ text-align: center;
+ width: 20px;
+ height: 20px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ border: none;
+}
+.table-striped .datepicker table tr td,
+.table-striped .datepicker table tr th {
+ background-color: transparent;
+}
+.datepicker table tr td.day:hover,
+.datepicker table tr td.day.focused {
+ background: #eee;
+ cursor: pointer;
+}
+.datepicker table tr td.old,
+.datepicker table tr td.new {
+ color: #999;
+}
+.datepicker table tr td.disabled,
+.datepicker table tr td.disabled:hover {
+ background: none;
+ color: #999;
+ cursor: default;
+}
+.datepicker table tr td.highlighted {
+ background: #d9edf7;
+ border-radius: 0;
+}
+.datepicker table tr td.today,
+.datepicker table tr td.today:hover,
+.datepicker table tr td.today.disabled,
+.datepicker table tr td.today.disabled:hover {
+ background-color: #fde19a;
+ background-image: -moz-linear-gradient(to bottom, #fdd49a, #fdf59a);
+ background-image: -ms-linear-gradient(to bottom, #fdd49a, #fdf59a);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fdd49a), to(#fdf59a));
+ background-image: -webkit-linear-gradient(to bottom, #fdd49a, #fdf59a);
+ background-image: -o-linear-gradient(to bottom, #fdd49a, #fdf59a);
+ background-image: linear-gradient(to bottom, #fdd49a, #fdf59a);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0);
+ border-color: #fdf59a #fdf59a #fbed50;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+ color: #000;
+}
+.datepicker table tr td.today:hover,
+.datepicker table tr td.today:hover:hover,
+.datepicker table tr td.today.disabled:hover,
+.datepicker table tr td.today.disabled:hover:hover,
+.datepicker table tr td.today:active,
+.datepicker table tr td.today:hover:active,
+.datepicker table tr td.today.disabled:active,
+.datepicker table tr td.today.disabled:hover:active,
+.datepicker table tr td.today.active,
+.datepicker table tr td.today:hover.active,
+.datepicker table tr td.today.disabled.active,
+.datepicker table tr td.today.disabled:hover.active,
+.datepicker table tr td.today.disabled,
+.datepicker table tr td.today:hover.disabled,
+.datepicker table tr td.today.disabled.disabled,
+.datepicker table tr td.today.disabled:hover.disabled,
+.datepicker table tr td.today[disabled],
+.datepicker table tr td.today:hover[disabled],
+.datepicker table tr td.today.disabled[disabled],
+.datepicker table tr td.today.disabled:hover[disabled] {
+ background-color: #fdf59a;
+}
+.datepicker table tr td.today:active,
+.datepicker table tr td.today:hover:active,
+.datepicker table tr td.today.disabled:active,
+.datepicker table tr td.today.disabled:hover:active,
+.datepicker table tr td.today.active,
+.datepicker table tr td.today:hover.active,
+.datepicker table tr td.today.disabled.active,
+.datepicker table tr td.today.disabled:hover.active {
+ background-color: #fbf069 \9;
+}
+.datepicker table tr td.today:hover:hover {
+ color: #000;
+}
+.datepicker table tr td.today.active:hover {
+ color: #fff;
+}
+.datepicker table tr td.range,
+.datepicker table tr td.range:hover,
+.datepicker table tr td.range.disabled,
+.datepicker table tr td.range.disabled:hover {
+ background: #eee;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+.datepicker table tr td.range.today,
+.datepicker table tr td.range.today:hover,
+.datepicker table tr td.range.today.disabled,
+.datepicker table tr td.range.today.disabled:hover {
+ background-color: #f3d17a;
+ background-image: -moz-linear-gradient(to bottom, #f3c17a, #f3e97a);
+ background-image: -ms-linear-gradient(to bottom, #f3c17a, #f3e97a);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f3c17a), to(#f3e97a));
+ background-image: -webkit-linear-gradient(to bottom, #f3c17a, #f3e97a);
+ background-image: -o-linear-gradient(to bottom, #f3c17a, #f3e97a);
+ background-image: linear-gradient(to bottom, #f3c17a, #f3e97a);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0);
+ border-color: #f3e97a #f3e97a #edde34;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+.datepicker table tr td.range.today:hover,
+.datepicker table tr td.range.today:hover:hover,
+.datepicker table tr td.range.today.disabled:hover,
+.datepicker table tr td.range.today.disabled:hover:hover,
+.datepicker table tr td.range.today:active,
+.datepicker table tr td.range.today:hover:active,
+.datepicker table tr td.range.today.disabled:active,
+.datepicker table tr td.range.today.disabled:hover:active,
+.datepicker table tr td.range.today.active,
+.datepicker table tr td.range.today:hover.active,
+.datepicker table tr td.range.today.disabled.active,
+.datepicker table tr td.range.today.disabled:hover.active,
+.datepicker table tr td.range.today.disabled,
+.datepicker table tr td.range.today:hover.disabled,
+.datepicker table tr td.range.today.disabled.disabled,
+.datepicker table tr td.range.today.disabled:hover.disabled,
+.datepicker table tr td.range.today[disabled],
+.datepicker table tr td.range.today:hover[disabled],
+.datepicker table tr td.range.today.disabled[disabled],
+.datepicker table tr td.range.today.disabled:hover[disabled] {
+ background-color: #f3e97a;
+}
+.datepicker table tr td.range.today:active,
+.datepicker table tr td.range.today:hover:active,
+.datepicker table tr td.range.today.disabled:active,
+.datepicker table tr td.range.today.disabled:hover:active,
+.datepicker table tr td.range.today.active,
+.datepicker table tr td.range.today:hover.active,
+.datepicker table tr td.range.today.disabled.active,
+.datepicker table tr td.range.today.disabled:hover.active {
+ background-color: #efe24b \9;
+}
+.datepicker table tr td.selected,
+.datepicker table tr td.selected:hover,
+.datepicker table tr td.selected.disabled,
+.datepicker table tr td.selected.disabled:hover {
+ background-color: #9e9e9e;
+ background-image: -moz-linear-gradient(to bottom, #b3b3b3, #808080);
+ background-image: -ms-linear-gradient(to bottom, #b3b3b3, #808080);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#b3b3b3), to(#808080));
+ background-image: -webkit-linear-gradient(to bottom, #b3b3b3, #808080);
+ background-image: -o-linear-gradient(to bottom, #b3b3b3, #808080);
+ background-image: linear-gradient(to bottom, #b3b3b3, #808080);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0);
+ border-color: #808080 #808080 #595959;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+ color: #fff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker table tr td.selected:hover,
+.datepicker table tr td.selected:hover:hover,
+.datepicker table tr td.selected.disabled:hover,
+.datepicker table tr td.selected.disabled:hover:hover,
+.datepicker table tr td.selected:active,
+.datepicker table tr td.selected:hover:active,
+.datepicker table tr td.selected.disabled:active,
+.datepicker table tr td.selected.disabled:hover:active,
+.datepicker table tr td.selected.active,
+.datepicker table tr td.selected:hover.active,
+.datepicker table tr td.selected.disabled.active,
+.datepicker table tr td.selected.disabled:hover.active,
+.datepicker table tr td.selected.disabled,
+.datepicker table tr td.selected:hover.disabled,
+.datepicker table tr td.selected.disabled.disabled,
+.datepicker table tr td.selected.disabled:hover.disabled,
+.datepicker table tr td.selected[disabled],
+.datepicker table tr td.selected:hover[disabled],
+.datepicker table tr td.selected.disabled[disabled],
+.datepicker table tr td.selected.disabled:hover[disabled] {
+ background-color: #808080;
+}
+.datepicker table tr td.selected:active,
+.datepicker table tr td.selected:hover:active,
+.datepicker table tr td.selected.disabled:active,
+.datepicker table tr td.selected.disabled:hover:active,
+.datepicker table tr td.selected.active,
+.datepicker table tr td.selected:hover.active,
+.datepicker table tr td.selected.disabled.active,
+.datepicker table tr td.selected.disabled:hover.active {
+ background-color: #666666 \9;
+}
+.datepicker table tr td.active,
+.datepicker table tr td.active:hover,
+.datepicker table tr td.active.disabled,
+.datepicker table tr td.active.disabled:hover {
+ background-color: #006dcc;
+ background-image: -moz-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: -ms-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#08c), to(#0044cc));
+ background-image: -webkit-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: -o-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: linear-gradient(to bottom, #08c, #0044cc);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#08c', endColorstr='#0044cc', GradientType=0);
+ border-color: #0044cc #0044cc #002a80;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+ color: #fff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker table tr td.active:hover,
+.datepicker table tr td.active:hover:hover,
+.datepicker table tr td.active.disabled:hover,
+.datepicker table tr td.active.disabled:hover:hover,
+.datepicker table tr td.active:active,
+.datepicker table tr td.active:hover:active,
+.datepicker table tr td.active.disabled:active,
+.datepicker table tr td.active.disabled:hover:active,
+.datepicker table tr td.active.active,
+.datepicker table tr td.active:hover.active,
+.datepicker table tr td.active.disabled.active,
+.datepicker table tr td.active.disabled:hover.active,
+.datepicker table tr td.active.disabled,
+.datepicker table tr td.active:hover.disabled,
+.datepicker table tr td.active.disabled.disabled,
+.datepicker table tr td.active.disabled:hover.disabled,
+.datepicker table tr td.active[disabled],
+.datepicker table tr td.active:hover[disabled],
+.datepicker table tr td.active.disabled[disabled],
+.datepicker table tr td.active.disabled:hover[disabled] {
+ background-color: #0044cc;
+}
+.datepicker table tr td.active:active,
+.datepicker table tr td.active:hover:active,
+.datepicker table tr td.active.disabled:active,
+.datepicker table tr td.active.disabled:hover:active,
+.datepicker table tr td.active.active,
+.datepicker table tr td.active:hover.active,
+.datepicker table tr td.active.disabled.active,
+.datepicker table tr td.active.disabled:hover.active {
+ background-color: #003399 \9;
+}
+.datepicker table tr td span {
+ display: block;
+ width: 23%;
+ height: 54px;
+ line-height: 54px;
+ float: left;
+ margin: 1%;
+ cursor: pointer;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+.datepicker table tr td span:hover,
+.datepicker table tr td span.focused {
+ background: #eee;
+}
+.datepicker table tr td span.disabled,
+.datepicker table tr td span.disabled:hover {
+ background: none;
+ color: #999;
+ cursor: default;
+}
+.datepicker table tr td span.active,
+.datepicker table tr td span.active:hover,
+.datepicker table tr td span.active.disabled,
+.datepicker table tr td span.active.disabled:hover {
+ background-color: #006dcc;
+ background-image: -moz-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: -ms-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#08c), to(#0044cc));
+ background-image: -webkit-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: -o-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: linear-gradient(to bottom, #08c, #0044cc);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#08c', endColorstr='#0044cc', GradientType=0);
+ border-color: #0044cc #0044cc #002a80;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+ color: #fff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker table tr td span.active:hover,
+.datepicker table tr td span.active:hover:hover,
+.datepicker table tr td span.active.disabled:hover,
+.datepicker table tr td span.active.disabled:hover:hover,
+.datepicker table tr td span.active:active,
+.datepicker table tr td span.active:hover:active,
+.datepicker table tr td span.active.disabled:active,
+.datepicker table tr td span.active.disabled:hover:active,
+.datepicker table tr td span.active.active,
+.datepicker table tr td span.active:hover.active,
+.datepicker table tr td span.active.disabled.active,
+.datepicker table tr td span.active.disabled:hover.active,
+.datepicker table tr td span.active.disabled,
+.datepicker table tr td span.active:hover.disabled,
+.datepicker table tr td span.active.disabled.disabled,
+.datepicker table tr td span.active.disabled:hover.disabled,
+.datepicker table tr td span.active[disabled],
+.datepicker table tr td span.active:hover[disabled],
+.datepicker table tr td span.active.disabled[disabled],
+.datepicker table tr td span.active.disabled:hover[disabled] {
+ background-color: #0044cc;
+}
+.datepicker table tr td span.active:active,
+.datepicker table tr td span.active:hover:active,
+.datepicker table tr td span.active.disabled:active,
+.datepicker table tr td span.active.disabled:hover:active,
+.datepicker table tr td span.active.active,
+.datepicker table tr td span.active:hover.active,
+.datepicker table tr td span.active.disabled.active,
+.datepicker table tr td span.active.disabled:hover.active {
+ background-color: #003399 \9;
+}
+.datepicker table tr td span.old,
+.datepicker table tr td span.new {
+ color: #999;
+}
+.datepicker .datepicker-switch {
+ width: 145px;
+}
+.datepicker .datepicker-switch,
+.datepicker .prev,
+.datepicker .next,
+.datepicker tfoot tr th {
+ cursor: pointer;
+}
+.datepicker .datepicker-switch:hover,
+.datepicker .prev:hover,
+.datepicker .next:hover,
+.datepicker tfoot tr th:hover {
+ background: #eee;
+}
+.datepicker .prev.disabled,
+.datepicker .next.disabled {
+ visibility: hidden;
+}
+.datepicker .cw {
+ font-size: 10px;
+ width: 12px;
+ padding: 0 2px 0 5px;
+ vertical-align: middle;
+}
+.input-append.date .add-on,
+.input-prepend.date .add-on {
+ cursor: pointer;
+}
+.input-append.date .add-on i,
+.input-prepend.date .add-on i {
+ margin-top: 3px;
+}
+.input-daterange input {
+ text-align: center;
+}
+.input-daterange input:first-child {
+ -webkit-border-radius: 3px 0 0 3px;
+ -moz-border-radius: 3px 0 0 3px;
+ border-radius: 3px 0 0 3px;
+}
+.input-daterange input:last-child {
+ -webkit-border-radius: 0 3px 3px 0;
+ -moz-border-radius: 0 3px 3px 0;
+ border-radius: 0 3px 3px 0;
+}
+.input-daterange .add-on {
+ display: inline-block;
+ width: auto;
+ min-width: 16px;
+ height: 18px;
+ padding: 4px 5px;
+ font-weight: normal;
+ line-height: 18px;
+ text-align: center;
+ text-shadow: 0 1px 0 #fff;
+ vertical-align: middle;
+ background-color: #eee;
+ border: 1px solid #ccc;
+ margin-left: -5px;
+ margin-right: -5px;
+}
+/*# sourceMappingURL=bootstrap-datepicker.css.map */
\ No newline at end of file
diff --git a/app/assets/stylesheets/bootstrap-datepicker.standalone.scss b/app/assets/stylesheets/bootstrap-datepicker.standalone.scss
new file mode 100644
index 000000000..0e7014851
--- /dev/null
+++ b/app/assets/stylesheets/bootstrap-datepicker.standalone.scss
@@ -0,0 +1,510 @@
+/*!
+ * Datepicker for Bootstrap v1.9.0 (https://github.com/uxsolutions/bootstrap-datepicker)
+ *
+ * Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ */
+
+.datepicker {
+ padding: 4px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ direction: ltr;
+}
+.datepicker-inline {
+ width: 220px;
+}
+.datepicker-rtl {
+ direction: rtl;
+}
+.datepicker-rtl.dropdown-menu {
+ left: auto;
+}
+.datepicker-rtl table tr td span {
+ float: right;
+}
+.datepicker-dropdown {
+ top: 0;
+ left: 0;
+}
+.datepicker-dropdown:before {
+ content: '';
+ display: inline-block;
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ border-bottom: 7px solid #999;
+ border-top: 0;
+ border-bottom-color: rgba(0, 0, 0, 0.2);
+ position: absolute;
+}
+.datepicker-dropdown:after {
+ content: '';
+ display: inline-block;
+ border-left: 6px solid transparent;
+ border-right: 6px solid transparent;
+ border-bottom: 6px solid #fff;
+ border-top: 0;
+ position: absolute;
+}
+.datepicker-dropdown.datepicker-orient-left:before {
+ left: 6px;
+}
+.datepicker-dropdown.datepicker-orient-left:after {
+ left: 7px;
+}
+.datepicker-dropdown.datepicker-orient-right:before {
+ right: 6px;
+}
+.datepicker-dropdown.datepicker-orient-right:after {
+ right: 7px;
+}
+.datepicker-dropdown.datepicker-orient-bottom:before {
+ top: -7px;
+}
+.datepicker-dropdown.datepicker-orient-bottom:after {
+ top: -6px;
+}
+.datepicker-dropdown.datepicker-orient-top:before {
+ bottom: -7px;
+ border-bottom: 0;
+ border-top: 7px solid #999;
+}
+.datepicker-dropdown.datepicker-orient-top:after {
+ bottom: -6px;
+ border-bottom: 0;
+ border-top: 6px solid #fff;
+}
+.datepicker table {
+ margin: 0;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.datepicker td,
+.datepicker th {
+ text-align: center;
+ width: 20px;
+ height: 20px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ border: none;
+}
+.table-striped .datepicker table tr td,
+.table-striped .datepicker table tr th {
+ background-color: transparent;
+}
+.datepicker table tr td.day:hover,
+.datepicker table tr td.day.focused {
+ background: #eee;
+ cursor: pointer;
+}
+.datepicker table tr td.old,
+.datepicker table tr td.new {
+ color: #999;
+}
+.datepicker table tr td.disabled,
+.datepicker table tr td.disabled:hover {
+ background: none;
+ color: #999;
+ cursor: default;
+}
+.datepicker table tr td.highlighted {
+ background: #d9edf7;
+ border-radius: 0;
+}
+.datepicker table tr td.today,
+.datepicker table tr td.today:hover,
+.datepicker table tr td.today.disabled,
+.datepicker table tr td.today.disabled:hover {
+ background-color: #fde19a;
+ background-image: -moz-linear-gradient(to bottom, #fdd49a, #fdf59a);
+ background-image: -ms-linear-gradient(to bottom, #fdd49a, #fdf59a);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fdd49a), to(#fdf59a));
+ background-image: -webkit-linear-gradient(to bottom, #fdd49a, #fdf59a);
+ background-image: -o-linear-gradient(to bottom, #fdd49a, #fdf59a);
+ background-image: linear-gradient(to bottom, #fdd49a, #fdf59a);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0);
+ border-color: #fdf59a #fdf59a #fbed50;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+ color: #000;
+}
+.datepicker table tr td.today:hover,
+.datepicker table tr td.today:hover:hover,
+.datepicker table tr td.today.disabled:hover,
+.datepicker table tr td.today.disabled:hover:hover,
+.datepicker table tr td.today:active,
+.datepicker table tr td.today:hover:active,
+.datepicker table tr td.today.disabled:active,
+.datepicker table tr td.today.disabled:hover:active,
+.datepicker table tr td.today.active,
+.datepicker table tr td.today:hover.active,
+.datepicker table tr td.today.disabled.active,
+.datepicker table tr td.today.disabled:hover.active,
+.datepicker table tr td.today.disabled,
+.datepicker table tr td.today:hover.disabled,
+.datepicker table tr td.today.disabled.disabled,
+.datepicker table tr td.today.disabled:hover.disabled,
+.datepicker table tr td.today[disabled],
+.datepicker table tr td.today:hover[disabled],
+.datepicker table tr td.today.disabled[disabled],
+.datepicker table tr td.today.disabled:hover[disabled] {
+ background-color: #fdf59a;
+}
+.datepicker table tr td.today:active,
+.datepicker table tr td.today:hover:active,
+.datepicker table tr td.today.disabled:active,
+.datepicker table tr td.today.disabled:hover:active,
+.datepicker table tr td.today.active,
+.datepicker table tr td.today:hover.active,
+.datepicker table tr td.today.disabled.active,
+.datepicker table tr td.today.disabled:hover.active {
+ background-color: #fbf069 \9;
+}
+.datepicker table tr td.today:hover:hover {
+ color: #000;
+}
+.datepicker table tr td.today.active:hover {
+ color: #fff;
+}
+.datepicker table tr td.range,
+.datepicker table tr td.range:hover,
+.datepicker table tr td.range.disabled,
+.datepicker table tr td.range.disabled:hover {
+ background: #eee;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+.datepicker table tr td.range.today,
+.datepicker table tr td.range.today:hover,
+.datepicker table tr td.range.today.disabled,
+.datepicker table tr td.range.today.disabled:hover {
+ background-color: #f3d17a;
+ background-image: -moz-linear-gradient(to bottom, #f3c17a, #f3e97a);
+ background-image: -ms-linear-gradient(to bottom, #f3c17a, #f3e97a);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f3c17a), to(#f3e97a));
+ background-image: -webkit-linear-gradient(to bottom, #f3c17a, #f3e97a);
+ background-image: -o-linear-gradient(to bottom, #f3c17a, #f3e97a);
+ background-image: linear-gradient(to bottom, #f3c17a, #f3e97a);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0);
+ border-color: #f3e97a #f3e97a #edde34;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+.datepicker table tr td.range.today:hover,
+.datepicker table tr td.range.today:hover:hover,
+.datepicker table tr td.range.today.disabled:hover,
+.datepicker table tr td.range.today.disabled:hover:hover,
+.datepicker table tr td.range.today:active,
+.datepicker table tr td.range.today:hover:active,
+.datepicker table tr td.range.today.disabled:active,
+.datepicker table tr td.range.today.disabled:hover:active,
+.datepicker table tr td.range.today.active,
+.datepicker table tr td.range.today:hover.active,
+.datepicker table tr td.range.today.disabled.active,
+.datepicker table tr td.range.today.disabled:hover.active,
+.datepicker table tr td.range.today.disabled,
+.datepicker table tr td.range.today:hover.disabled,
+.datepicker table tr td.range.today.disabled.disabled,
+.datepicker table tr td.range.today.disabled:hover.disabled,
+.datepicker table tr td.range.today[disabled],
+.datepicker table tr td.range.today:hover[disabled],
+.datepicker table tr td.range.today.disabled[disabled],
+.datepicker table tr td.range.today.disabled:hover[disabled] {
+ background-color: #f3e97a;
+}
+.datepicker table tr td.range.today:active,
+.datepicker table tr td.range.today:hover:active,
+.datepicker table tr td.range.today.disabled:active,
+.datepicker table tr td.range.today.disabled:hover:active,
+.datepicker table tr td.range.today.active,
+.datepicker table tr td.range.today:hover.active,
+.datepicker table tr td.range.today.disabled.active,
+.datepicker table tr td.range.today.disabled:hover.active {
+ background-color: #efe24b \9;
+}
+.datepicker table tr td.selected,
+.datepicker table tr td.selected:hover,
+.datepicker table tr td.selected.disabled,
+.datepicker table tr td.selected.disabled:hover {
+ background-color: #9e9e9e;
+ background-image: -moz-linear-gradient(to bottom, #b3b3b3, #808080);
+ background-image: -ms-linear-gradient(to bottom, #b3b3b3, #808080);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#b3b3b3), to(#808080));
+ background-image: -webkit-linear-gradient(to bottom, #b3b3b3, #808080);
+ background-image: -o-linear-gradient(to bottom, #b3b3b3, #808080);
+ background-image: linear-gradient(to bottom, #b3b3b3, #808080);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0);
+ border-color: #808080 #808080 #595959;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+ color: #fff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker table tr td.selected:hover,
+.datepicker table tr td.selected:hover:hover,
+.datepicker table tr td.selected.disabled:hover,
+.datepicker table tr td.selected.disabled:hover:hover,
+.datepicker table tr td.selected:active,
+.datepicker table tr td.selected:hover:active,
+.datepicker table tr td.selected.disabled:active,
+.datepicker table tr td.selected.disabled:hover:active,
+.datepicker table tr td.selected.active,
+.datepicker table tr td.selected:hover.active,
+.datepicker table tr td.selected.disabled.active,
+.datepicker table tr td.selected.disabled:hover.active,
+.datepicker table tr td.selected.disabled,
+.datepicker table tr td.selected:hover.disabled,
+.datepicker table tr td.selected.disabled.disabled,
+.datepicker table tr td.selected.disabled:hover.disabled,
+.datepicker table tr td.selected[disabled],
+.datepicker table tr td.selected:hover[disabled],
+.datepicker table tr td.selected.disabled[disabled],
+.datepicker table tr td.selected.disabled:hover[disabled] {
+ background-color: #808080;
+}
+.datepicker table tr td.selected:active,
+.datepicker table tr td.selected:hover:active,
+.datepicker table tr td.selected.disabled:active,
+.datepicker table tr td.selected.disabled:hover:active,
+.datepicker table tr td.selected.active,
+.datepicker table tr td.selected:hover.active,
+.datepicker table tr td.selected.disabled.active,
+.datepicker table tr td.selected.disabled:hover.active {
+ background-color: #666666 \9;
+}
+.datepicker table tr td.active,
+.datepicker table tr td.active:hover,
+.datepicker table tr td.active.disabled,
+.datepicker table tr td.active.disabled:hover {
+ background-color: #006dcc;
+ background-image: -moz-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: -ms-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#08c), to(#0044cc));
+ background-image: -webkit-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: -o-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: linear-gradient(to bottom, #08c, #0044cc);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#08c', endColorstr='#0044cc', GradientType=0);
+ border-color: #0044cc #0044cc #002a80;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+ color: #fff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker table tr td.active:hover,
+.datepicker table tr td.active:hover:hover,
+.datepicker table tr td.active.disabled:hover,
+.datepicker table tr td.active.disabled:hover:hover,
+.datepicker table tr td.active:active,
+.datepicker table tr td.active:hover:active,
+.datepicker table tr td.active.disabled:active,
+.datepicker table tr td.active.disabled:hover:active,
+.datepicker table tr td.active.active,
+.datepicker table tr td.active:hover.active,
+.datepicker table tr td.active.disabled.active,
+.datepicker table tr td.active.disabled:hover.active,
+.datepicker table tr td.active.disabled,
+.datepicker table tr td.active:hover.disabled,
+.datepicker table tr td.active.disabled.disabled,
+.datepicker table tr td.active.disabled:hover.disabled,
+.datepicker table tr td.active[disabled],
+.datepicker table tr td.active:hover[disabled],
+.datepicker table tr td.active.disabled[disabled],
+.datepicker table tr td.active.disabled:hover[disabled] {
+ background-color: #0044cc;
+}
+.datepicker table tr td.active:active,
+.datepicker table tr td.active:hover:active,
+.datepicker table tr td.active.disabled:active,
+.datepicker table tr td.active.disabled:hover:active,
+.datepicker table tr td.active.active,
+.datepicker table tr td.active:hover.active,
+.datepicker table tr td.active.disabled.active,
+.datepicker table tr td.active.disabled:hover.active {
+ background-color: #003399 \9;
+}
+.datepicker table tr td span {
+ display: block;
+ width: 23%;
+ height: 54px;
+ line-height: 54px;
+ float: left;
+ margin: 1%;
+ cursor: pointer;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+.datepicker table tr td span:hover,
+.datepicker table tr td span.focused {
+ background: #eee;
+}
+.datepicker table tr td span.disabled,
+.datepicker table tr td span.disabled:hover {
+ background: none;
+ color: #999;
+ cursor: default;
+}
+.datepicker table tr td span.active,
+.datepicker table tr td span.active:hover,
+.datepicker table tr td span.active.disabled,
+.datepicker table tr td span.active.disabled:hover {
+ background-color: #006dcc;
+ background-image: -moz-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: -ms-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#08c), to(#0044cc));
+ background-image: -webkit-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: -o-linear-gradient(to bottom, #08c, #0044cc);
+ background-image: linear-gradient(to bottom, #08c, #0044cc);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#08c', endColorstr='#0044cc', GradientType=0);
+ border-color: #0044cc #0044cc #002a80;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+ color: #fff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker table tr td span.active:hover,
+.datepicker table tr td span.active:hover:hover,
+.datepicker table tr td span.active.disabled:hover,
+.datepicker table tr td span.active.disabled:hover:hover,
+.datepicker table tr td span.active:active,
+.datepicker table tr td span.active:hover:active,
+.datepicker table tr td span.active.disabled:active,
+.datepicker table tr td span.active.disabled:hover:active,
+.datepicker table tr td span.active.active,
+.datepicker table tr td span.active:hover.active,
+.datepicker table tr td span.active.disabled.active,
+.datepicker table tr td span.active.disabled:hover.active,
+.datepicker table tr td span.active.disabled,
+.datepicker table tr td span.active:hover.disabled,
+.datepicker table tr td span.active.disabled.disabled,
+.datepicker table tr td span.active.disabled:hover.disabled,
+.datepicker table tr td span.active[disabled],
+.datepicker table tr td span.active:hover[disabled],
+.datepicker table tr td span.active.disabled[disabled],
+.datepicker table tr td span.active.disabled:hover[disabled] {
+ background-color: #0044cc;
+}
+.datepicker table tr td span.active:active,
+.datepicker table tr td span.active:hover:active,
+.datepicker table tr td span.active.disabled:active,
+.datepicker table tr td span.active.disabled:hover:active,
+.datepicker table tr td span.active.active,
+.datepicker table tr td span.active:hover.active,
+.datepicker table tr td span.active.disabled.active,
+.datepicker table tr td span.active.disabled:hover.active {
+ background-color: #003399 \9;
+}
+.datepicker table tr td span.old,
+.datepicker table tr td span.new {
+ color: #999;
+}
+.datepicker .datepicker-switch {
+ width: 145px;
+}
+.datepicker .datepicker-switch,
+.datepicker .prev,
+.datepicker .next,
+.datepicker tfoot tr th {
+ cursor: pointer;
+}
+.datepicker .datepicker-switch:hover,
+.datepicker .prev:hover,
+.datepicker .next:hover,
+.datepicker tfoot tr th:hover {
+ background: #eee;
+}
+.datepicker .prev.disabled,
+.datepicker .next.disabled {
+ visibility: hidden;
+}
+.datepicker .cw {
+ font-size: 10px;
+ width: 12px;
+ padding: 0 2px 0 5px;
+ vertical-align: middle;
+}
+.input-append.date .add-on,
+.input-prepend.date .add-on {
+ cursor: pointer;
+}
+.input-append.date .add-on i,
+.input-prepend.date .add-on i {
+ margin-top: 3px;
+}
+.input-daterange input {
+ text-align: center;
+}
+.input-daterange input:first-child {
+ -webkit-border-radius: 3px 0 0 3px;
+ -moz-border-radius: 3px 0 0 3px;
+ border-radius: 3px 0 0 3px;
+}
+.input-daterange input:last-child {
+ -webkit-border-radius: 0 3px 3px 0;
+ -moz-border-radius: 0 3px 3px 0;
+ border-radius: 0 3px 3px 0;
+}
+.input-daterange .add-on {
+ display: inline-block;
+ width: auto;
+ min-width: 16px;
+ height: 20px;
+ padding: 4px 5px;
+ font-weight: normal;
+ line-height: 20px;
+ text-align: center;
+ text-shadow: 0 1px 0 #fff;
+ vertical-align: middle;
+ background-color: #eee;
+ border: 1px solid #ccc;
+ margin-left: -5px;
+ margin-right: -5px;
+}
+.datepicker.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ float: left;
+ display: none;
+ min-width: 160px;
+ list-style: none;
+ background-color: #fff;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+ -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ -webkit-background-clip: padding-box;
+ -moz-background-clip: padding;
+ background-clip: padding-box;
+ *border-right-width: 2px;
+ *border-bottom-width: 2px;
+ color: #333333;
+ font-size: 13px;
+ line-height: 20px;
+}
+.datepicker.dropdown-menu th,
+.datepicker.datepicker-inline th,
+.datepicker.dropdown-menu td,
+.datepicker.datepicker-inline td {
+ padding: 4px 5px;
+}
+/*# sourceMappingURL=bootstrap-datepicker.standalone.css.map */
\ No newline at end of file
diff --git a/app/controllers/admins/daily_school_statistics_controller.rb b/app/controllers/admins/daily_school_statistics_controller.rb
new file mode 100644
index 000000000..eb9c75fbc
--- /dev/null
+++ b/app/controllers/admins/daily_school_statistics_controller.rb
@@ -0,0 +1,36 @@
+class Admins::DailySchoolStatisticsController < Admins::BaseController
+ def index
+ params[:sort_by] = params[:sort_by].presence || :teacher_count
+ params[:sort_direction] = params[:sort_direction].presence || :desc
+
+ total_count, statistics = Admins::SchoolDailyStatisticService.call(params)
+
+ @statistics = paginate statistics, total_count: total_count
+
+ respond_to do |format|
+ format.html { load_statistic_total }
+ format.js
+ end
+ end
+
+ def export
+ params[:per_page] = 10000
+ _count, @schools = Admins::SchoolDailyStatisticService.call(params)
+
+ filename = ['学校统计总表', params[:keyword], Time.zone.now.strftime('%Y%m%d%H%M%S')].join('-') << '.xlsx'
+ render xlsx: 'export', filename: filename
+ end
+
+ private
+
+ def load_statistic_total
+ @teacher_total = User.joins(:user_extension).where(user_extensions: { identity: :teacher }).count
+ @student_total = User.joins(:user_extension).where(user_extensions: { identity: :student }).count
+ @course_total = Course.count
+ @active_course_total = Course.where(is_end: false).count
+ @shixun_homework_total = HomeworkCommon.where(homework_type: 4).count
+ @other_homework_total = HomeworkCommon.where(homework_type: [1, 3]).count
+ @shixun_total = Shixun.count
+ @shixun_evaluate_total = SchoolReport.sum(:shixun_evaluate_count)
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/admins/school_statistics_controller.rb b/app/controllers/admins/school_statistics_controller.rb
new file mode 100644
index 000000000..fdd10c70f
--- /dev/null
+++ b/app/controllers/admins/school_statistics_controller.rb
@@ -0,0 +1,49 @@
+class Admins::SchoolStatisticsController < Admins::BaseController
+ before_action :contrast_column_select_options, only: [:contrast]
+
+ def index
+ params[:data_type] ||= 'grow'
+ params[:sort_by] = params[:sort_by].presence || :teacher_increase_count
+ params[:sort_direction] = params[:sort_direction].presence || :desc
+
+ service = Admins::StatisticSchoolDataGrowService.new(params)
+ @grow_summary = service.grow_summary
+
+ total_count, statistics = service.call
+
+ @statistics = paginate statistics, total_count: total_count
+ end
+
+ def contrast
+ params[:contrast_column] = params[:contrast_column].presence || :teacher_increase_count
+ params[:sort_direction] ||= :desc
+ params[:sort_by] = :percentage
+
+ # 无对比日期时直接返回无数据页面
+ if useless_contrast_date_parameter?
+ @total_count = 0
+ @statistics = paginate([])
+ return
+ end
+
+ total_count, statistics = Admins::StatisticSchoolContrastDataService.call(params)
+
+ @statistics = paginate statistics, total_count: total_count
+ rescue Admins::StatisticSchoolContrastDataService::ParameterError
+ render_unprocessable_entity('参数错误')
+ end
+
+ private
+
+ def useless_contrast_date_parameter?
+ params[:begin_date].blank? && params[:end_date].blank? &&
+ params[:other_begin_date].blank? &¶ms[:other_end_date].blank?
+ end
+
+ def contrast_column_select_options
+ @select_options =
+ Admins::StatisticSchoolContrastDataService::CONTRAST_COLUMN_LIST.map do |column|
+ [I18n.t("school_daily_report.#{column}"), column]
+ end
+ end
+end
diff --git a/app/controllers/concerns/admins/error_rescue_handler.rb b/app/controllers/concerns/admins/error_rescue_handler.rb
index 7d11e1716..b1e29d5ce 100644
--- a/app/controllers/concerns/admins/error_rescue_handler.rb
+++ b/app/controllers/concerns/admins/error_rescue_handler.rb
@@ -3,6 +3,8 @@ module Admins::ErrorRescueHandler
included do
rescue_from Exception, Educoder::TipException do |e|
+ raise e if Rails.env.development?
+
Util.logger_error e
internal_server_error
end
diff --git a/app/controllers/concerns/admins/paginate_helper.rb b/app/controllers/concerns/admins/paginate_helper.rb
index 5e74c2f2f..da7652584 100644
--- a/app/controllers/concerns/admins/paginate_helper.rb
+++ b/app/controllers/concerns/admins/paginate_helper.rb
@@ -1,6 +1,10 @@
module Admins::PaginateHelper
extend ActiveSupport::Concern
+ def offset
+ (page - 1) * per_page
+ end
+
def page
params[:page].to_i <= 0 ? 1 : params[:page].to_i
end
@@ -8,8 +12,13 @@ module Admins::PaginateHelper
def per_page
params[:per_page].to_i <= 0 || params[:per_page].to_i > 100 ? 20 : params[:per_page].to_i
end
+ alias_method :limit, :per_page
- def paginate(relations)
- relations.page(page).per(per_page)
+ def paginate(relations, total_count: nil)
+ if relations.is_a?(Array)
+ Kaminari.paginate_array(relations, limit: limit, offset: offset, total_count: total_count)
+ else
+ relations.page(page).per(per_page)
+ end
end
end
\ No newline at end of file
diff --git a/app/models/school.rb b/app/models/school.rb
index a0c829160..8a28ae4bf 100644
--- a/app/models/school.rb
+++ b/app/models/school.rb
@@ -10,6 +10,9 @@ class School < ApplicationRecord
has_many :ec_major_schools, :dependent => :destroy
has_many :ec_majors, :through => :ec_major_schools
+ has_many :school_daily_reports, dependent: :destroy
+ has_many :courses
+
# 学校管理员
def manager?(user)
ec_school_users.exists?(user_id: user.id)
diff --git a/app/models/school_daily_active_user.rb b/app/models/school_daily_active_user.rb
new file mode 100644
index 000000000..2abe85145
--- /dev/null
+++ b/app/models/school_daily_active_user.rb
@@ -0,0 +1,3 @@
+class SchoolDailyActiveUser < ApplicationRecord
+ belongs_to :school_daily_report
+end
\ No newline at end of file
diff --git a/app/models/school_daily_report.rb b/app/models/school_daily_report.rb
new file mode 100644
index 000000000..364f319ab
--- /dev/null
+++ b/app/models/school_daily_report.rb
@@ -0,0 +1,5 @@
+class SchoolDailyReport < ApplicationRecord
+ belongs_to :school
+
+ has_many :school_daily_active_users, dependent: :delete_all
+end
\ No newline at end of file
diff --git a/app/models/school_report.rb b/app/models/school_report.rb
new file mode 100644
index 000000000..b1183b3ec
--- /dev/null
+++ b/app/models/school_report.rb
@@ -0,0 +1,3 @@
+class SchoolReport < ApplicationRecord
+ belongs_to :school
+end
\ No newline at end of file
diff --git a/app/services/admins/school_daily_statistic_service.rb b/app/services/admins/school_daily_statistic_service.rb
new file mode 100644
index 000000000..64bb97864
--- /dev/null
+++ b/app/services/admins/school_daily_statistic_service.rb
@@ -0,0 +1,123 @@
+class Admins::SchoolDailyStatisticService < ApplicationService
+ include CustomSortable
+
+ attr_reader :params
+
+ sort_columns :student_count, :teacher_count, :homework_count, :other_homework_count,
+ :course_count, :active_course_count, :nearly_course_time, :shixun_count, :shixun_evaluate_count,
+ default_by: :teacher_count, default_direction: :desc
+
+ def initialize(params)
+ @params = params
+ end
+
+ def call
+ schools = School.group('schools.id')
+
+ keyword = params[:keyword].try(:to_s).try(:strip)
+ if keyword.present?
+ schools = schools.where("schools.name LIKE :keyword OR schools.id LIKE :keyword", keyword: "%#{keyword}%")
+ end
+
+ count = schools.count.count
+
+ # 根据排序字段进行查询
+ schools = query_by_sort_column(schools, params[:sort_by])
+ schools = custom_sort(schools, params[:sort_by], params[:sort_direction])
+
+ schools = schools.limit(page_size).offset(offset)
+ # 查询并组装其它数据
+ schools = package_other_data(schools)
+
+ [count, schools]
+ end
+
+ def package_other_data(schools)
+ ids = schools.map(&:id)
+
+ student_map = UserExtension.where(school_id: ids, identity: :student).group(:school_id).count
+ teacher_map = UserExtension.where(school_id: ids, identity: :teacher).group(:school_id).count
+
+ homeworks = HomeworkCommon.joins(:course)
+ shixun_homework_map = homeworks.where(homework_type: 4, courses: { school_id: ids }).group('school_id').count
+ other_homework_map = homeworks.where(homework_type: [1, 3], courses: { school_id: ids }).group('school_id').count
+
+ courses = Course.where(is_delete: 0, school_id: ids).group('school_id')
+ course_map = courses.count
+ nearly_course_time_map = courses.joins(:course_acts).maximum('course_activities.updated_at')
+ active_course_map = courses.where(is_end: false).count
+
+ shixun_map = Shixun.joins(user: :user_extension).where(user_extensions: { identity: :teacher, school_id: ids })
+ .where(fork_from: nil).group('school_id').count
+
+ reports = SchoolReport.where(school_id: ids)
+ evaluate_count_map = reports.each_with_object({}) { |report, obj| obj[report.school_id] = report.shixun_evaluate_count }
+
+ schools.map do |school|
+ {
+ id: school.id,
+ name: school.name,
+ teacher_count: teacher_map[school.id],
+ student_count: student_map[school.id],
+ homework_count: shixun_homework_map[school.id],
+ other_homework_count: other_homework_map[school.id],
+ course_count: course_map[school.id],
+ nearly_course_time: nearly_course_time_map[school.id],
+ active_course_count: active_course_map[school.id],
+ shixun_count: shixun_map.fetch(school.id, 0),
+ shixun_evaluate_count: evaluate_count_map.fetch(school.id, 0)
+ }
+ end
+ end
+
+ private
+ def query_by_sort_column(schools, sort_by_column)
+ base_query_column = 'schools.id, schools.name'
+
+ case sort_by_column.to_s
+ when 'teacher_count' then
+ schools.joins('LEFT JOIN user_extensions ue ON ue.school_id = schools.id AND ue.identity = 0')
+ .select("#{base_query_column}, COUNT(*) teacher_count")
+ when 'student_count' then
+ schools.joins('LEFT JOIN user_extensions ue ON ue.school_id = schools.id AND ue.identity = 1')
+ .select("#{base_query_column}, COUNT(*) student_count")
+ when 'homework_count' then
+ schools.joins('LEFT JOIN courses ON courses.school_id = schools.id')
+ .joins('LEFT JOIN homework_commons hc ON hc.course_id = courses.id AND hc.homework_type = 4')
+ .select("#{base_query_column}, COUNT(*) homework_count")
+ when 'other_homework_count' then
+ schools.joins('LEFT JOIN courses ON courses.school_id = schools.id')
+ .joins('LEFT JOIN homework_commons hc ON hc.course_id = courses.id AND hc.homework_type IN (1, 3)')
+ .select("#{base_query_column}, COUNT(*) other_homework_count")
+ when 'course_count' then
+ schools.joins('LEFT JOIN courses cs ON cs.school_id = schools.id AND cs.is_delete = 0')
+ .select("#{base_query_column}, COUNT(*) course_count")
+ when 'shixun_count' then
+ schools.joins('LEFT JOIN user_extensions ue ON ue.school_id = schools.id AND ue.identity = 0')
+ .joins('LEFT JOIN users ON users.id = ue.user_id')
+ .joins('LEFT JOIN shixuns sx ON sx.user_id = users.id AND sx.fork_from IS NULL')
+ .select("#{base_query_column}, COUNT(*) shixun_count")
+ when 'shixun_evaluate_count' then
+ schools.joins('LEFT JOIN school_reports ON school_reports.school_id = schools.id')
+ .select("#{base_query_column}, shixun_evaluate_count")
+ when 'nearly_course_time' then
+ schools.joins('LEFT JOIN courses cs ON cs.school_id = schools.id AND cs.is_delete = 0')
+ .joins('LEFT JOIN course_activities acs ON acs.course_id = cs.id')
+ .select("#{base_query_column}, MAX(acs.updated_at) nearly_course_time")
+ when 'active_course_count' then
+ schools.joins('LEFT JOIN courses cs ON cs.school_id = schools.id AND cs.is_delete = 0 AND cs.is_end = false')
+ .select("#{base_query_column}, COUNT(*) active_course_count")
+ else
+ schools.joins('LEFT JOIN user_extensions ue ON ue.school_id = schools.id AND ue.identity = 0')
+ .select("#{base_query_column}, COUNT(*) teacher_count")
+ end
+ end
+
+ def page_size
+ params[:per_page] || 20
+ end
+
+ def offset
+ (params[:page].to_i.zero? ? 0 : params[:page].to_i - 1) * page_size
+ end
+end
\ No newline at end of file
diff --git a/app/services/admins/statistic_school_contrast_data_service.rb b/app/services/admins/statistic_school_contrast_data_service.rb
new file mode 100644
index 000000000..0496b1371
--- /dev/null
+++ b/app/services/admins/statistic_school_contrast_data_service.rb
@@ -0,0 +1,80 @@
+class Admins::StatisticSchoolContrastDataService < ApplicationService
+ ParameterError = Class.new(StandardError)
+
+ PAGE_SIZE = 20
+ CONTRAST_COLUMN_LIST = %w(
+ teacher_increase_count student_increase_count course_increase_count
+ shixun_increase_count active_user_count shixun_homework_count shixun_evaluate_count
+ ).freeze
+
+ attr_reader :params, :sort_direction, :contrast_column
+
+ def initialize(params)
+ @params = params
+ @sort_direction = params[:sort_direction].to_s
+ @contrast_column = params[:contrast_column].to_s
+ end
+
+ def call
+ validate_parameter!
+ reports = School.joins(:school_daily_reports).select(select_columns)
+
+ keyword = params[:keyword].try(:to_s).try(:strip)
+ if keyword.present?
+ reports = reports.where("schools.name LIKE :keyword OR schools.id LIKE :keyword", keyword: "%#{keyword}%")
+ end
+
+ count = reports.count('distinct(schools.id)')
+
+ sql = query_report_sql(reports.group('schools.id').to_sql)
+ reports = SchoolDailyReport.find_by_sql(sql)
+
+ [count, reports]
+ end
+
+ private
+ def validate_parameter!
+ if %i[begin_date end_date other_begin_date other_end_date].any? { |key| params[key].blank? }
+ raise ParameterError
+ end
+
+ unless %w(desc asc).include?(sort_direction)
+ raise ParameterError
+ end
+
+ unless CONTRAST_COLUMN_LIST.include?(contrast_column)
+ raise ParameterError
+ end
+ end
+
+ def format_date(date)
+ Time.zone.parse(date).strftime("%Y-%m-%d")
+ end
+
+ def offset
+ (params[:page].to_i.zero? ? 0 : params[:page].to_i - 1) * PAGE_SIZE
+ end
+
+ def select_columns
+ if contrast_column != 'active_user_count'
+ "schools.id school_id, schools.name school_name,"\
+ "(SUM(IF(date BETWEEN '#{format_date(params[:begin_date])}' AND '#{format_date(params[:end_date])}', #{contrast_column}, 0))) total,"\
+ "(SUM(IF(date BETWEEN '#{format_date(params[:other_begin_date])}' AND '#{format_date(params[:other_end_date])}', #{contrast_column}, 0))) other_total"
+ else
+ # 活跃用户对比时处理方法不同
+ relations = SchoolDailyActiveUser.select('COUNT(distinct user_id)').joins(:school_daily_report)
+ .where('school_id = schools.id')
+ total_subquery = relations.where("date BETWEEN '#{format_date(params[:begin_date])}' AND '#{format_date(params[:end_date])}'").to_sql
+ other_total_subquery = relations.where("date BETWEEN '#{format_date(params[:other_begin_date])}' AND '#{format_date(params[:other_end_date])}'").to_sql
+
+ "schools.id school_id, schools.name school_name, (#{total_subquery}) AS total, (#{other_total_subquery}) AS other_total"
+ end
+ end
+
+ def query_report_sql(from_sql)
+ order_by = "(total = 0 AND other_total != 0) #{sort_direction}, (percentage != 0) #{sort_direction}, percentage #{sort_direction}"
+
+ "SELECT reports.*, (other_total - total) increase, (IF(other_total - total = 0, 0.0, round((other_total - total) / IF(total = 0, 1, total), 5))) percentage "\
+ "FROM (#{from_sql}) reports ORDER BY #{order_by} LIMIT #{PAGE_SIZE} OFFSET #{offset}"
+ end
+end
\ No newline at end of file
diff --git a/app/services/admins/statistic_school_data_grow_service.rb b/app/services/admins/statistic_school_data_grow_service.rb
new file mode 100644
index 000000000..8df106666
--- /dev/null
+++ b/app/services/admins/statistic_school_data_grow_service.rb
@@ -0,0 +1,107 @@
+class Admins::StatisticSchoolDataGrowService < ApplicationService
+ include CustomSortable
+
+ PAGE_SIZE = 20
+
+ attr_reader :params
+
+ sort_columns :teacher_increase_count, :student_increase_count,
+ :course_increase_count, :shixun_increase_count, :uniq_active_user_count,
+ :shixun_homework_count, :shixun_evaluate_count,
+ default_by: :teacher_increase_count, default_direction: :desc
+
+ def initialize(params)
+ @params = params
+ end
+
+ def call
+ reports = School.where(nil)
+
+ reports = search_filter(reports)
+
+ count = reports.count
+
+ subquery = SchoolDailyActiveUser.select('COUNT(distinct(user_id))').joins(:school_daily_report)
+ .where(date_condition_sql).where("school_id is not null and school_id = schools.id").to_sql
+ reports = reports.joins("LEFT JOIN school_daily_reports sdr ON sdr.school_id = schools.id AND #{date_condition_sql}")
+ reports = reports.select(
+ 'schools.id school_id, schools.name school_name,'\
+ 'SUM(teacher_increase_count) teacher_increase_count,'\
+ 'SUM(student_increase_count) student_increase_count,'\
+ 'SUM(course_increase_count) course_increase_count,'\
+ 'SUM(shixun_increase_count) shixun_increase_count,'\
+ 'SUM(shixun_homework_count) shixun_homework_count,'\
+ 'SUM(shixun_evaluate_count) shixun_evaluate_count,'\
+ "(#{subquery}) uniq_active_user_count,"\
+ 'SUM(active_user_count) active_user_count').group('schools.id')
+
+ reports = custom_sort(reports, params[:sort_by], params[:sort_direction])
+ reports = reports.order('school_id asc').limit(PAGE_SIZE).offset(offset)
+
+ [count, reports]
+ end
+
+ def grow_summary
+ @_grow_summary ||= begin
+ reports = School.joins("LEFT JOIN school_daily_reports sdr ON sdr.school_id = schools.id")
+ .where(date_condition_sql)
+
+ subquery = SchoolDailyActiveUser.select('COUNT(distinct user_id)')
+ .joins('LEFT JOIN school_daily_reports sdr ON sdr.id = school_daily_active_users.school_daily_report_id')
+ .where(date_condition_sql).to_sql
+ reports = search_filter(reports)
+ reports.select(
+ 'SUM(teacher_increase_count) teacher_increase_count,'\
+ 'SUM(student_increase_count) student_increase_count,'\
+ 'SUM(course_increase_count) course_increase_count,'\
+ 'SUM(shixun_increase_count) shixun_increase_count,'\
+ 'SUM(shixun_homework_count) shixun_homework_count,'\
+ 'SUM(shixun_evaluate_count) shixun_evaluate_count,'\
+ "(#{subquery}) uniq_active_user_count,"\
+ 'SUM(active_user_count) active_user_count'
+ ).first
+ end
+ end
+
+ private
+
+ def search_filter(relations)
+ keyword = params[:keyword].try(:to_s).try(:strip)
+ if keyword.present?
+ relations = relations.where("schools.name LIKE :keyword OR schools.id LIKE :keyword", keyword: "%#{keyword}%")
+ end
+
+ relations
+ end
+
+ def date_condition_sql
+ date = query_date
+ if date.is_a?(Range)
+ "date BETWEEN '#{date.min.strftime('%Y-%m-%d')}' AND '#{date.max.strftime('%Y-%m-%d')}'"
+ else
+ "date = '#{date.strftime('%Y-%m-%d')}'"
+ end
+ end
+
+ def query_date
+ if params[:grow_begin_date].present?
+ begin_time = Time.zone.parse(params[:grow_begin_date])
+ end_date = if params[:grow_end_date].present?
+ Time.zone.parse(params[:grow_end_date])
+ end
+
+ end_date.blank? || end_date == begin_time ? begin_time : begin_time..end_date
+ else
+ yesterday
+ end
+ end
+
+ def yesterday
+ # 每日凌晨5点为节点, 25日凌晨4点、3点、2点等等,未到更新数据时间点,看到的数据是:23日-24日的统计数据
+ (Time.zone.now - 5.hours).beginning_of_day - 1.days
+ end
+
+ def offset
+ (params[:page].to_i.zero? ? 0 : params[:page].to_i - 1) * PAGE_SIZE
+ end
+end
\ No newline at end of file
diff --git a/app/tasks/statistic_school_daily_report_task.rb b/app/tasks/statistic_school_daily_report_task.rb
new file mode 100644
index 000000000..5cd3fda7c
--- /dev/null
+++ b/app/tasks/statistic_school_daily_report_task.rb
@@ -0,0 +1,69 @@
+class StatisticSchoolDailyReportTask
+ def call
+ School.find_each do |school|
+ # 新增教师和学生
+ users = User.joins(:user_extension)
+ .where(user_extensions: { school_id: school.id })
+
+ teacher_count = users.where(created_on: yesterday, user_extensions: { identity: :teacher }).count
+ student_count = users.where(created_on: yesterday, user_extensions: { identity: :student }).count
+
+ # 活跃用户
+ active_user_ids = users.where(last_login_on: yesterday).pluck(:id)
+ active_user_count = active_user_ids.size
+
+
+ # 新增课堂
+ course_count = school.courses.where(created_at: yesterday).count
+
+ # 新增实训
+ shixun_count = Shixun.joins(user: :user_extension)
+ .where(user_extensions: { identity: :teacher, school_id: school.id })
+ .where(created_at: yesterday).count
+ # 新增实训作业数
+ shixun_homework_count = HomeworkCommon.joins(:course).where(courses: { school_id: school.id })
+ .where(homework_type: 4, created_at: yesterday).count
+
+ # 新增实训评测数量
+ shixun_evaluate_count = EvaluateRecord.joins('LEFT JOIN homework_commons_shixuns hcs ON hcs.shixun_id = evaluate_records.shixun_id')
+ .joins('LEFT JOIN homework_commons hc ON hcs.homework_common_id = hc.id AND hc.homework_type = 4')
+ .joins('LEFT JOIN members ON members.user_id = evaluate_records.user_id')
+ .joins('LEFT JOIN courses ON members.course_id = courses.id AND hc.course_id = courses.id')
+ .where(courses: { school_id: school.id })
+ .where(created_at: yesterday).reorder(nil).count
+
+ # 无有效数据时不记录
+ data = [teacher_count, student_count, course_count, shixun_count, active_user_count,
+ shixun_homework_count, shixun_evaluate_count]
+ next if data.all?(&:zero?)
+
+ create_params = {
+ school_id: school.id, school_name: school.name, teacher_increase_count: teacher_count,
+ student_increase_count: student_count, course_increase_count: course_count,
+ shixun_homework_count: shixun_homework_count, shixun_evaluate_count: shixun_evaluate_count,
+ shixun_increase_count: shixun_count, active_user_count: active_user_count, date: current_date
+ }
+ report = SchoolDailyReport.create!(create_params)
+
+ if active_user_ids.present?
+ values = '(' + active_user_ids.join(", #{report.id}),(") + ", #{report.id})"
+ user_sql = "INSERT INTO school_daily_active_users(user_id, school_daily_report_id) VALUES#{values}"
+ SchoolDailyActiveUser.connection.execute(user_sql)
+ end
+ end
+ end
+
+ private
+ def current_date
+ @_current_date ||= Time.zone.now.beginning_of_day - 1.day
+ end
+
+ def yesterday
+ @_yesterday ||= begin
+ # 每日凌晨5点为节点
+ end_time = Time.zone.now.beginning_of_day + 5.hour
+ begin_time = end_time - 1.day
+ begin_time..end_time
+ end
+ end
+end
diff --git a/app/tasks/statistic_school_report_task.rb b/app/tasks/statistic_school_report_task.rb
new file mode 100644
index 000000000..a55eb7a42
--- /dev/null
+++ b/app/tasks/statistic_school_report_task.rb
@@ -0,0 +1,20 @@
+class StatisticSchoolReportTask
+ def call
+ School.find_each do |school|
+ evaluate_count = Game.joins(:challenge)
+ .joins('LEFT JOIN members ON members.user_id = games.user_id')
+ .joins('LEFT JOIN homework_commons_shixuns hcs ON hcs.shixun_id = challenges.shixun_id')
+ .joins('LEFT JOIN homework_commons hc ON hcs.homework_common_id = hc.id AND hc.homework_type = 4')
+ .joins('LEFT JOIN courses ON hc.course_id = courses.id AND members.course_id = courses.id')
+ .where(courses: { school_id: school.id })
+ .sum(:evaluate_count)
+
+ report = SchoolReport.find_or_initialize_by(school_id: school.id)
+
+ report.school_name = school.name
+ report.shixun_evaluate_count = evaluate_count
+
+ report.save
+ end
+ end
+end
diff --git a/app/views/admins/daily_school_statistics/export.xlsx.axlsx b/app/views/admins/daily_school_statistics/export.xlsx.axlsx
new file mode 100644
index 000000000..1757a8fe2
--- /dev/null
+++ b/app/views/admins/daily_school_statistics/export.xlsx.axlsx
@@ -0,0 +1,13 @@
+wb = xlsx_package.workbook
+wb.add_worksheet(name: '统计总表') do |sheet|
+ sheet.add_row %w(ID 单位名称 教师总人数 学生总人数 课堂总数 正在进行课堂数 总实训数 实训评测总数 实训作业总数 其它作业总数 动态时间)
+
+ @schools.each do |school|
+ sheet.add_row([
+ school[:id].to_s, school[:name].to_s, (school[:teacher_count] || 0).to_s, (school[:student_count] || 0).to_s,
+ (school[:course_count] || 0).to_s, (school[:active_course_count] || 0).to_s,
+ (school[:shixun_count] || 0).to_s,(school[:shixun_evaluate_count] || 0).to_s, (school[:homework_count] || 0).to_s,
+ (school[:other_homework_count] || 0).to_s, format_time(school[:nearly_course_time])
+ ])
+ end
+end
\ No newline at end of file
diff --git a/app/views/admins/daily_school_statistics/index.html.erb b/app/views/admins/daily_school_statistics/index.html.erb
new file mode 100644
index 000000000..054e06fc6
--- /dev/null
+++ b/app/views/admins/daily_school_statistics/index.html.erb
@@ -0,0 +1,29 @@
+<% define_admin_breadcrumbs do %>
+ <% add_admin_breadcrumb('统计总表', admins_daily_school_statistics_path) %>
+<% end %>
+
+
+ <%= form_tag(admins_daily_school_statistics_path, method: :get, class: 'form-inline search-form', remote: true) do %>
+ <%= text_field_tag(:keyword, params[:keyword], class: 'form-control col-sm-2 ml-3', placeholder: 'ID/单位名称搜索') %>
+ <%= submit_tag('搜索', class: 'btn btn-primary ml-3') %>
+ <% end %>
+
+ <%#= link_to '导出Excel', export_admins_daily_school_statistics_path(format: :xlsx), class: 'btn btn-outline-primary export-action' %>
+ <%= javascript_void_link '导出Excel', class: 'btn btn-outline-primary export-action', 'data-url': export_admins_daily_school_statistics_path(format: :xlsx) %>
+
+
+
+ 统计总计:
+ 教师总人数<%= @teacher_total %>人,
+ 学生总人数<%= @student_total %>人,
+ 课堂总数<%= @course_total %>个,
+ 正在进行课堂总数<%= @active_course_total %>个,
+ 实训总数<%= @shixun_total %>个,
+ 实训评测总数<%= @shixun_evaluate_total %>个,
+ 实训作业总数<%= @shixun_homework_total %>个,
+ 其它作业总数<%= @other_homework_total %>个
+
+
+
+ <%= render partial: 'admins/daily_school_statistics/shared/list', locals: { statistics: @statistics } %>
+
\ No newline at end of file
diff --git a/app/views/admins/daily_school_statistics/index.js.erb b/app/views/admins/daily_school_statistics/index.js.erb
new file mode 100644
index 000000000..d3e261f64
--- /dev/null
+++ b/app/views/admins/daily_school_statistics/index.js.erb
@@ -0,0 +1 @@
+$(".daily-school-statistic-list-container").html("<%= j(render partial: 'admins/daily_school_statistics/shared/list', locals: { statistics: @statistics }) %>")
\ No newline at end of file
diff --git a/app/views/admins/daily_school_statistics/shared/_list.html.erb b/app/views/admins/daily_school_statistics/shared/_list.html.erb
new file mode 100644
index 000000000..b71be485e
--- /dev/null
+++ b/app/views/admins/daily_school_statistics/shared/_list.html.erb
@@ -0,0 +1,46 @@
+
+
+
+ 单位名称 |
+
+ <%= sort_tag('教师总人数', name: 'teacher_count', path: admins_daily_school_statistics_path) %> |
+ <%= sort_tag('学生总人数', name: 'student_count', path: admins_daily_school_statistics_path) %> |
+ <%= sort_tag('课堂总数', name: 'course_count', path: admins_daily_school_statistics_path) %> |
+ <%= sort_tag('正在进行课堂数', name: 'active_course_count', path: admins_daily_school_statistics_path) %> |
+ <%= sort_tag('实训总数', name: 'shixun_count', path: admins_daily_school_statistics_path) %> |
+
+ <%= sort_tag('实训评测总数', name: 'shixun_evaluate_count', path: admins_daily_school_statistics_path) %>
+
+ |
+ <%= sort_tag('实训作业总数', name: 'homework_count', path: admins_daily_school_statistics_path) %> |
+ <%= sort_tag('其它作业总数', name: 'other_homework_count', path: admins_daily_school_statistics_path) %> |
+ <%= sort_tag('动态时间', name: 'nearly_course_time', path: admins_daily_school_statistics_path) %> |
+
+
+
+ <% if statistics.present? %>
+ <% statistics.each do |statistic| %>
+
+
+ <%= link_to '#' do %>
+ <%= overflow_hidden_span statistic[:name], width: '200px' %>
+ <% end %>
+ |
+ <%= statistic[:teacher_count].to_i %> |
+ <%= statistic[:student_count].to_i %> |
+ <%= statistic[:course_count].to_i %> |
+ <%= statistic[:active_course_count].to_i %> |
+ <%= statistic[:shixun_count].to_i %> |
+ <%= statistic[:shixun_evaluate_count].to_i %> |
+ <%= statistic[:homework_count].to_i %> |
+ <%= statistic[:other_homework_count].to_i %> |
+ <%= statistic[:nearly_course_time]&.strftime('%Y-%m-%d %H:%M') %> |
+
+ <% end %>
+ <% else %>
+ <%= render 'admins/shared/no_data_for_table' %>
+ <% end %>
+
+
+
+<%= render partial: 'admins/shared/paginate', locals: { objects: statistics } %>
\ No newline at end of file
diff --git a/app/views/admins/school_statistics/contrast.js.erb b/app/views/admins/school_statistics/contrast.js.erb
new file mode 100644
index 000000000..bc9916c12
--- /dev/null
+++ b/app/views/admins/school_statistics/contrast.js.erb
@@ -0,0 +1 @@
+$(".school-statistic-list-container").html("<%= j(render partial: 'admins/school_statistics/shared/contrast_list', locals: { statistics: @statistics, select_options: @select_options }) %>")
\ No newline at end of file
diff --git a/app/views/admins/school_statistics/index.html.erb b/app/views/admins/school_statistics/index.html.erb
new file mode 100644
index 000000000..fd0c7cac1
--- /dev/null
+++ b/app/views/admins/school_statistics/index.html.erb
@@ -0,0 +1,49 @@
+<% define_admin_breadcrumbs do %>
+ <% add_admin_breadcrumb('数据变化报表', admins_school_statistics_path) %>
+<% end %>
+
+
+
+
+ <%= render partial: 'admins/school_statistics/shared/list', locals: { statistics: @statistics } %>
+
\ No newline at end of file
diff --git a/app/views/admins/school_statistics/index.js.erb b/app/views/admins/school_statistics/index.js.erb
new file mode 100644
index 000000000..3f5f7334b
--- /dev/null
+++ b/app/views/admins/school_statistics/index.js.erb
@@ -0,0 +1 @@
+$(".school-statistic-list-container").html("<%= j(render partial: 'admins/school_statistics/shared/list', locals: { statistics: @statistics }) %>")
\ No newline at end of file
diff --git a/app/views/admins/school_statistics/shared/_contrast_list.html.erb b/app/views/admins/school_statistics/shared/_contrast_list.html.erb
new file mode 100644
index 000000000..049711ca2
--- /dev/null
+++ b/app/views/admins/school_statistics/shared/_contrast_list.html.erb
@@ -0,0 +1,71 @@
+
+
+ 学校数据统计(<%= I18n.t("school_daily_report.#{params[:contrast_column]}") %>变化统计情况)
+
+ <%= select_tag :contrast_column,
+ options_for_select(select_options, params[:contrast_column]),
+ class: 'form-control contrast-column-select' %>
+
+ <% if statistics.present? %>
+
+ 说明:新增数=时段二-时段一;新增百分比=(新增数/时段一)*100%(保留小数点后五位)
+
+ <% end %>
+
+
+<% if statistics.present? %>
+
+
+
+ 单位名称 |
+ 时段一 <%= "(#{params[:begin_date]} 05:00至#{(Time.zone.parse(params[:end_date]) + 1.days).strftime('%Y-%m-%d')} 05:00)" %> |
+ 时段二 <%= "(#{params[:other_begin_date]} 05:00至#{(Time.zone.parse(params[:other_end_date]) + 1.days).strftime('%Y-%m-%d')} 05:00)" %> |
+
+ <%= sort_tag('变化情况', name: 'percentage', path: contrast_admins_school_statistics_path) %>
+ ( 新 增 数 | 新增百分比)
+ |
+
+
+
+ <% statistics.each do |statistic| %>
+
+
+ <%= link_to statistic.school_name, '' %>
+ |
+ <%= statistic['total'] %> |
+ <%= statistic['other_total'] %> |
+ <%
+ increase = statistic['other_total'] - statistic['total']
+ percentage = statistic['total'].zero? ? increase.to_f * 100 : (increase / statistic['total'].to_f) * 100
+ %>
+ <% if increase > 0 %>
+
+ +<%= increase %>
+ |
+ <% if statistic['total'].zero? %>
+ - |
+ <% else %>
+ +<%= percentage.round(5) %>% |
+ <% end %>
+ <% elsif increase.zero? %>
+
+ <%= increase %>
+ |
+ <%= percentage.round(5) %>% |
+ <% else %>
+
+ <%= increase %>
+ |
+ <%= percentage.round(5) %>% |
+ <% end %>
+
+ <% end %>
+
+
+
+ <%= render partial: 'admins/shared/paginate', locals: { objects: statistics } %>
+<% else %>
+
+ 暂无数据,请选择时间段对比
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/admins/school_statistics/shared/_list.html.erb b/app/views/admins/school_statistics/shared/_list.html.erb
new file mode 100644
index 000000000..72b92c13d
--- /dev/null
+++ b/app/views/admins/school_statistics/shared/_list.html.erb
@@ -0,0 +1,55 @@
+
+ 统计总计:
+ <% if params[:grow_begin_date].present? %>
+ <%= params[:grow_begin_date] %> 05:00至<%= (Time.zone.parse(params[:grow_end_date]) + 1.days).strftime('%Y-%m-%d') %> 05:00
+ <% else %>
+ <%= (Time.current - 5.hour).beginning_of_day.ago(1.days).strftime('%Y-%m-%d') %> 05:00至
+ <%= (Time.current - 5.hour).beginning_of_day.strftime('%Y-%m-%d') %> 05:00,
+ <% end %>
+ 新增教师<%= @grow_summary.teacher_increase_count || 0 %>人,
+ 新增学生<%= @grow_summary.student_increase_count || 0 %>人,
+ 新增课堂<%= @grow_summary.course_increase_count || 0 %>个,
+ 新增实训<%= @grow_summary.shixun_increase_count || 0 %>个,
+ 新增实训作业<%= @grow_summary.shixun_homework_count || 0 %>个,
+ 新增实训评测<%= @grow_summary.shixun_evaluate_count || 0 %>个,
+ 活跃用户
+ <%= @grow_summary.uniq_active_user_count.to_i.zero? ? @grow_summary.active_user_count.to_i : @grow_summary.uniq_active_user_count.to_i %>
+ 个
+
+
+
+
+
+ 单位名称 |
+ <%= sort_tag('新增教师', name: 'teacher_increase_count', path: admins_school_statistics_path) %> |
+ <%= sort_tag('新增学生', name: 'student_increase_count', path: admins_school_statistics_path) %> |
+ <%= sort_tag('新增课堂', name: 'course_increase_count', path: admins_school_statistics_path) %> |
+ <%= sort_tag('新增实训', name: 'shixun_increase_count', path: admins_school_statistics_path) %> |
+ <%= sort_tag('新增实训作业', name: 'shixun_homework_count', path: admins_school_statistics_path) %> |
+ <%= sort_tag('新增实训评测', name: 'shixun_evaluate_count', path: admins_school_statistics_path) %> |
+ <%= sort_tag('活跃用户', name: 'uniq_active_user_count', path: admins_school_statistics_path) %> |
+
+
+
+ <% if statistics.present? %>
+ <% statistics.each do |statistic| %>
+
+
+ <%= link_to statistic.school_name, '' %>
+ |
+ <%= statistic.teacher_increase_count.to_i %> |
+ <%= statistic.student_increase_count.to_i %> |
+ <%= statistic.course_increase_count.to_i %> |
+ <%= statistic.shixun_increase_count.to_i %> |
+ <%= statistic.shixun_homework_count.to_i %> |
+ <%= statistic.shixun_evaluate_count.to_i %> |
+ <%= statistic.uniq_active_user_count.to_i.zero? ? statistic.active_user_count.to_i : statistic.uniq_active_user_count.to_i %> |
+
+ <% end %>
+ <% else %>
+ <%= render 'admins/shared/no_data_for_table' %>
+ <% end %>
+
+
+
+<%= render partial: 'admins/shared/paginate', locals: { objects: statistics } %>
\ No newline at end of file
diff --git a/app/views/admins/shared/403.html.erb b/app/views/admins/shared/403.html.erb
deleted file mode 100644
index 9afbb199c..000000000
--- a/app/views/admins/shared/403.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-403
\ No newline at end of file
diff --git a/app/views/admins/shared/404.html.erb b/app/views/admins/shared/404.html.erb
index c1a02e5d8..8e285cb27 100644
--- a/app/views/admins/shared/404.html.erb
+++ b/app/views/admins/shared/404.html.erb
@@ -1,6 +1,6 @@
-
-
+
\ No newline at end of file
diff --git a/app/views/admins/shared/422.html.erb b/app/views/admins/shared/422.html.erb
index f8c3ff8cc..e56889496 100644
--- a/app/views/admins/shared/422.html.erb
+++ b/app/views/admins/shared/422.html.erb
@@ -1,6 +1,6 @@
-
-
+
+
422
-
<%= @message %>
+
<%= @message %>
\ No newline at end of file
diff --git a/app/views/admins/shared/500.html.erb b/app/views/admins/shared/500.html.erb
index 7cb9c17ff..f053f58ec 100644
--- a/app/views/admins/shared/500.html.erb
+++ b/app/views/admins/shared/500.html.erb
@@ -1,6 +1,6 @@
-
-
+
\ No newline at end of file
diff --git a/app/views/admins/shared/_sidebar.html.erb b/app/views/admins/shared/_sidebar.html.erb
index 7190d235a..eb697faec 100644
--- a/app/views/admins/shared/_sidebar.html.erb
+++ b/app/views/admins/shared/_sidebar.html.erb
@@ -1,5 +1,5 @@
<% sidebar_collapse = request.cookies['admin_sidebar_collapse'].to_s == 'true' %>
-