diff --git a/app/assets/javascripts/app/controllers/report.js.coffee b/app/assets/javascripts/app/controllers/report.js.coffee
new file mode 100644
index 000000000..a38475642
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/report.js.coffee
@@ -0,0 +1,488 @@
+class Index extends App.ControllerContent
+ constructor: ->
+ super
+
+ # check authentication
+ return if !@authenticate()
+
+ @title 'Reporting'
+ @navupdate '#report'
+ @startLoading()
+ @ajax(
+ type: 'GET',
+ url: @apiPath + '/reports/config',
+ processData: true,
+ success: (data) =>
+ @stopLoading()
+ @config = data.config
+ App.Collection.load( type: 'ReportProfile', data: data.profiles )
+ @render()
+ )
+
+ getParams: =>
+ return @params if @params
+
+ @params = {}
+ @params.timeRange = 'year'
+ current = new Date()
+ currentDay = current.getDate()
+ currentMonth = current.getMonth() + 1
+ currentYear = current.getFullYear()
+ currentWeek = current.getWeek()
+ @params.day = currentDay
+ @params.month = currentMonth
+ @params.week = currentWeek
+ @params.year = currentYear
+ if !@params.metric
+ for key, config of @config.metric
+ if config.default
+ @params.metric = config.name
+ if !@params.backendSelected
+ @params.backendSelected = {}
+ for key, config of @config.metric
+ for backend in config.backend
+ if backend.selected
+ @params.backendSelected[backend.name] = true
+ if !@params.profileSelected
+ @params.profileSelected = {}
+ for profile in App.ReportProfile.all()
+ if _.isEmpty( @params.profileSelected )
+ @params.profileSelected[ profile.id ] = true
+ @params
+
+ render: (data = {}) =>
+
+ @params = @getParams()
+
+ @html App.view('report/main')(
+ params: @params
+ )
+
+ new TimeRangePicker(
+ el: @el.find('.js-timeRangePicker')
+ params: @params
+ ui: @
+ )
+
+ new TimePicker(
+ el: @el.find('.js-timePicker')
+ params: @params
+ ui: @
+ )
+
+ new Sidebar(
+ el: @el.find('.sidebar')
+ config: @config
+ params: @params
+ )
+
+ new Graph(
+ el: @el
+ config: @config
+ params: @params
+ ui: @
+ )
+
+class Graph extends App.ControllerContent
+ constructor: ->
+ super
+
+ # rerender view
+ @bind 'ui:report:rerender', =>
+ @render()
+
+ @render()
+
+ render: =>
+
+ update = (data) =>
+ @draw(data.data)
+ t = new Date
+ @el.find('#download-chart').html(t.toString())
+ new Download(
+ el: @el.find('.js-dataDownload')
+ config: @config
+ params: @params
+ ui: @ui
+ )
+
+ url = @apiPath + '/reports/generate'
+ interval = 60000
+ if @params.timeRange is 'year'
+ interval = 30000
+ if @params.timeRange is 'month'
+ interval = 20000
+ if @params.timeRange is 'week'
+ interval = 20000
+ if @params.timeRange is 'day'
+ interval = 20000
+ if @params.timeRange is 'realtime'
+ interval = 10000
+
+ @ajax(
+ type: 'POST'
+ url: url
+ data: JSON.stringify(
+ metric: @params.metric
+ year: @params.year
+ month: @params.month
+ week: @params.week
+ day: @params.day
+ timeRange: @params.timeRange
+ profiles: @params.profileSelected
+ backends: @params.backendSelected
+ )
+ processData: true
+ success: (data) =>
+ update(data)
+ @delay( @render, interval, 'report-update', 'page' )
+ )
+
+ draw: (data) =>
+ @log('draw', data)
+ $('#placeholder').empty()
+
+ # create xaxis
+ xaxis = []
+ if @params.timeRange is 'realtime'
+ for minute in [0..59]
+ xaxis.push [minute, '']
+ else if @params.timeRange is 'day'
+ for hour in [0..23]
+ xaxis.push [hour, hour]
+ else if @params.timeRange is 'month'
+ for day in [1..31]
+ xaxis.push [day, day]
+ else if @params.timeRange is 'week'
+ xaxis = [[1, 'Mon'], [2, 'Tue'], [3, 'Wed'], [4, 'Thr'], [5, 'Fri'], [6, 'Sat'], [7, 'Sun'] ]
+ else
+ xaxis = [[1, 'Jan'], [2, 'Feb'], [3, 'Mar'], [4, 'Apr'], [5, 'Mai'], [6, 'Jun'], [7, 'Jul'], [8, 'Aug'], [9, 'Sep'], [10, 'Oct'], [11, 'Nov'], [12, 'Dec']]
+
+ dataPlot = []
+ for key, value of data
+ dataPlot.push {
+ data: value
+ label: key
+ }
+ # plot
+ $.plot( $('#placeholder'), dataPlot, {
+ yaxis: { min: 0 },
+ xaxis: { ticks: xaxis }
+ } )
+
+
+class Download extends App.Controller
+ events:
+ 'click .js-dataDownloadBackendSelector': 'tableUpdate'
+
+ constructor: (data) ->
+
+ # unbind existing click binds
+ data.el.unbind('click .js-dataDownloadBackendSelector')
+
+ super
+ @render()
+
+ render: ->
+
+ reports = []
+ $('.js-backendSelector:checked').each( (index, element) ->
+ if $(element).hasClass('download')
+ value = $(element).val()
+ reports.push value
+ )
+
+ profiles = []
+ for key, value of @params.profileSelected
+ profiles.push App.ReportProfile.find(key)
+ console.log('reports', reports, 'profiles', profiles, @config.metric, @params.metric, @config.metric[@params.metric])
+ @html App.view('report/download_header')(
+ reports: reports
+ profiles: profiles
+ metric: @config.metric[@params.metric]
+ )
+
+ @profileSelected = ''
+ @backendSelected = ''
+ active = false
+ @el.find('.js-dataDownloadBackendSelector').each( (index, element) ->
+ if $(element).parent().hasClass('active')
+ active = true
+ )
+ if !active
+ @el.find('.js-dataDownloadBackendSelector').first().parent().addClass('active')
+
+ @el.find('.js-dataDownloadBackendSelector').each( (index, element) =>
+ if $(element).parent().hasClass('active')
+ @profileSelected = $(element).data('profile')
+ @backendSelected = $(element).data('report')
+ )
+
+ @tableUpdate()
+
+ tableUpdate: (e) =>
+ if e
+ e.preventDefault()
+ @el.find('.js-dataDownloadBackendSelector').parent().removeClass('active')
+ $(e.target).parent().addClass('active')
+ @profileSelected = $(e.target).data('profile')
+ @backendSelected = $(e.target).data('backend')
+
+ table = (tickets, count) =>
+ url = '#ticket/zoom/'
+ if App.Config.get('import_mode')
+ url = App.Config.get('import_otrs_endpoint') + '/index.pl?Action=AgentTicketZoom;TicketID='
+ if _.isEmpty(tickets)
+ @el.find('js-dataDownloadTable').html('')
+ else
+ html = App.view('report/download_list')(
+ tickets: tickets
+ count: count
+ url: url
+ download: @apiPath + '/reports/csvforset/' + name
+ )
+ @el.find('js-dataDownloadTable').html(html)
+
+ @startLoading()
+ @ajax(
+ type: 'POST'
+ url: @apiPath + '/reports/sets'
+ data: JSON.stringify(
+ metric: @params.metric
+ year: @params.year
+ month: @params.month
+ week: @params.week
+ day: @params.day
+ timeRange: @params.timeRange
+ profiles: @params.profileSelected
+
+ )
+ processData: true
+ success: (data) =>
+ @stopLoading()
+
+ # load ticket collection / do not save in localStorage
+ App.Collection.load( type: 'TicketReport', data: data.tickets, localStorage: true )
+ ticket_collection = []
+
+ if data.tickets
+ for record in data.tickets
+ ticket = App.TicketReport.fullLocal( record.id )
+ ticket_collection.push ticket
+
+ table( ticket_collection, data.count )
+ )
+
+class TimeRangePicker extends App.Controller
+ events:
+ 'click .js-timeRange': 'select'
+
+ constructor: ->
+ super
+
+ # rerender view
+ @bind 'ui:report:rerender', =>
+ @render()
+
+ @render()
+
+ render: =>
+ @html App.view('report/time_range_picker')()
+
+ # select time slot
+ @el.find('.js-timeRange').removeClass('active')
+ @el.find('.js-timeRange[data-type="' + @ui.params.timeRange + '"]').addClass('active')
+
+ select: (e) =>
+ console.log('TS click')
+ e.preventDefault()
+ @ui.params.timeRange = $(e.target).data('type')
+ console.log 'SLOT', @ui.params.timeRange
+ App.Event.trigger( 'ui:report:rerender' )
+
+
+class TimePicker extends App.Controller
+ events:
+ 'click .js-timePickerDay': 'selectTimeDay'
+ 'click .js-timePickerYear': 'selectTimeYear'
+ 'click .js-timePickerMonth': 'selectTimeMonth'
+ 'click .js-timePickerWeek': 'selectTimeWeek'
+
+ constructor: ->
+ super
+
+ @_timeSlotPicker()
+
+ # rerender view
+ @bind 'ui:report:rerender', =>
+ @render()
+
+ @render()
+
+ render: =>
+ @html App.view('report/time_picker')(
+ params: @ui.params
+ timeRangeDay: @timeRangeDay
+ timeRangeMonth: @timeRangeMonth
+ timeRangeWeek: @timeRangeWeek
+ timeRangeYear: @timeRangeYear
+ )
+
+ # select time slot
+ @el.find('.time-slot').removeClass('active')
+ @el.find('.time-slot[data-type="' + @ui.params.timeRange + '"]').addClass('active')
+
+ selectTimeDay: (e) =>
+ e.preventDefault()
+ @ui.params.day = $(e.target).data('type')
+ $(e.target).parent().parent().find('li').removeClass('active')
+ $(e.target).parent().addClass('active')
+ App.Event.trigger( 'ui:report:rerender' )
+
+ selectTimeMonth: (e) =>
+ e.preventDefault()
+ @ui.params.month = $(e.target).data('type')
+ $(e.target).parent().parent().find('li').removeClass('active')
+ $(e.target).parent().addClass('active')
+ App.Event.trigger( 'ui:report:rerender' )
+
+ selectTimeWeek: (e) =>
+ e.preventDefault()
+ @ui.params.week = $(e.target).data('type')
+ $(e.target).parent().parent().find('li').removeClass('active')
+ $(e.target).parent().addClass('active')
+ App.Event.trigger( 'ui:report:rerender' )
+
+ selectTimeYear: (e) =>
+ e.preventDefault()
+ @ui.params.year = $(e.target).data('type')
+ $(e.target).parent().parent().find('li').removeClass('active')
+ $(e.target).parent().addClass('active')
+ App.Event.trigger( 'ui:report:rerender' )
+
+ _timeSlotPicker: ->
+ @timeRangeYear = []
+ year = new Date().getFullYear()
+ for item in [year-2..year]
+ record = {
+ display: item
+ value: item
+ }
+ @timeRangeYear.push record
+
+ @timeRangeMonth = [
+ {
+ display: 'Jan'
+ value: 1
+ },
+ {
+ display: 'Feb'
+ value: 2
+ },
+ {
+ display: 'Mar'
+ value: 3
+ },
+ {
+ display: 'Apr'
+ value: 4,
+ },
+ {
+ display: 'Mai'
+ value: 5,
+ },
+ {
+ display: 'Jun'
+ value: 6,
+ },
+ {
+ display: 'Jul'
+ value: 7,
+ },
+ {
+ display: 'Aug'
+ value: 8,
+ },
+ {
+ display: 'Sep'
+ value: 9,
+ },
+ {
+ display: 'Oct'
+ value: 10,
+ },
+ {
+ display: 'Nov'
+ value: 11,
+ },
+ {
+ display: 'Dec'
+ value: 12,
+ },
+ ]
+
+ @timeRangeWeek = []
+ for item in [1..52]
+ record = {
+ display: item
+ value: item
+ }
+ @timeRangeWeek.push record
+
+ @timeRangeDay = []
+ for item in [1..31]
+ record = {
+ display: item
+ value: item
+ }
+ @timeRangeDay.push record
+
+
+class Sidebar extends App.Controller
+ events:
+ 'click .js-profileSelector': 'selectProfile'
+ 'click .js-backendSelector': 'selectBackend'
+ 'click .panel-title': 'selectMetric'
+
+ constructor: ->
+ super
+ @render()
+
+ render: =>
+
+ metrics = @config.metric
+ profiles = App.ReportProfile.all()
+ console.log('Si', @params)
+ @html App.view('report/sidebar')(
+ metrics: metrics
+ params: @params
+ profiles: profiles
+ )
+
+ selectMetric: (e) =>
+ return if $(e.target).closest('.panel').find('.collapse.in').get(0)
+ metric = $(e.target).closest('.panel').data('metric')
+ return if @params.metric is metric
+ @params.metric = metric
+ App.Event.trigger( 'ui:report:rerender' )
+
+ selectProfile: (e) =>
+ profile_id = $(e.target).val()
+ active = $(e.target).prop('checked')
+ if active
+ @params.profileSelected[profile_id] = true
+ else
+ delete @params.profileSelected[profile_id]
+ App.Event.trigger( 'ui:report:rerender' )
+
+ selectBackend: (e) =>
+ backend = $(e.target).val()
+ active = $(e.target).prop('checked')
+ if active
+ @params.backendSelected[backend] = true
+ else
+ delete @params.backendSelected[backend]
+ App.Event.trigger( 'ui:report:rerender' )
+
+App.Config.set( 'report', Index, 'Routes' )
+App.Config.set( 'Reporting', { prio: 8000, parent: '', name: 'Reporing', translate: true, target: '#report', icon: 'report', role: ['Admin'] }, 'NavBarRight' )
\ No newline at end of file
diff --git a/app/assets/javascripts/app/controllers/report_profile.js.coffee b/app/assets/javascripts/app/controllers/report_profile.js.coffee
new file mode 100644
index 000000000..aa00ec31f
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/report_profile.js.coffee
@@ -0,0 +1,27 @@
+class Index extends App.ControllerContent
+ constructor: ->
+ super
+
+ # check authentication
+ return if !@authenticate()
+
+ new App.ControllerGenericIndex(
+ el: @el
+ id: @id
+ genericObject: 'ReportProfile'
+ pageData:
+ title: 'Report Profile'
+ home: 'report_profiles'
+ object: 'Report Profile'
+ objects: 'Report Profiles'
+ navupdate: '#report_profiles'
+ notes: [
+# 'Report Profile are ...'
+ ]
+ buttons: [
+ { name: 'New Profile', 'data-type': 'new', class: 'primary' }
+ ]
+ container: @el.closest('.content')
+ )
+
+App.Config.set( 'ReportProfile', { prio: 8000, name: 'Report Profiles', parent: '#manage', target: '#manage/report_profiles', controller: Index, role: ['Admin'] }, 'NavBarAdmin' )
diff --git a/app/assets/javascripts/app/models/report_profile.js.coffee b/app/assets/javascripts/app/models/report_profile.js.coffee
new file mode 100644
index 000000000..a21fa0458
--- /dev/null
+++ b/app/assets/javascripts/app/models/report_profile.js.coffee
@@ -0,0 +1,14 @@
+class App.ReportProfile extends App.Model
+ @configure 'ReportProfile', 'name', 'condition', 'active'
+ @extend Spine.Model.Ajax
+ @url: @apiPath + '/report_profiles'
+ @configure_attributes = [
+ { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
+ { name: 'condition', display: 'Filter', tag: 'ticket_selector', null: true },
+ { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
+ { name: 'active', display: 'Active', tag: 'active', default: true },
+ ]
+ @configure_delete = true
+ @configure_overview = [
+ 'name',
+ ]
diff --git a/app/assets/javascripts/app/views/report/download_header.jst.eco b/app/assets/javascripts/app/views/report/download_header.jst.eco
new file mode 100644
index 000000000..2c06034c9
--- /dev/null
+++ b/app/assets/javascripts/app/views/report/download_header.jst.eco
@@ -0,0 +1,14 @@
+
+
+
+ <% for profile in @profiles: %>
+ <% for backend in @metric.backend: %>
+ <% if backend.dataDownload: %>
+ <%= @T(backend.display) %>
+ <% end %>
+ <% end %>
+ <% end %>
+
+
+
+
\ No newline at end of file
diff --git a/app/assets/javascripts/app/views/report/download_list.jst.eco b/app/assets/javascripts/app/views/report/download_list.jst.eco
new file mode 100644
index 000000000..8fcbda5c7
--- /dev/null
+++ b/app/assets/javascripts/app/views/report/download_list.jst.eco
@@ -0,0 +1,26 @@
+<%- @T('%s records', @count) %>
+
+
+
+
+
+
+ <%- @T('Number') %>
+ <%- @T('Title') %>
+ <%- @T('State') %>
+ <%- @T('Queue') %>
+ <%- @T('Created') %>
+
+
+
+<% for ticket in @tickets: %>
+
+ <%- @P(ticket, 'number') %>
+ <%- @P(ticket, 'title') %>
+ <%- @P(ticket, 'state') %>
+ <%- @P(ticket, 'group') %>
+ <%- @P(ticket, 'created_at') %>
+
+<% end %>
+
+
diff --git a/app/assets/javascripts/app/views/report/main.jst.eco b/app/assets/javascripts/app/views/report/main.jst.eco
new file mode 100644
index 000000000..d1c5bc958
--- /dev/null
+++ b/app/assets/javascripts/app/views/report/main.jst.eco
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/app/views/report/sidebar.jst.eco b/app/assets/javascripts/app/views/report/sidebar.jst.eco
new file mode 100644
index 000000000..fed8f34bb
--- /dev/null
+++ b/app/assets/javascripts/app/views/report/sidebar.jst.eco
@@ -0,0 +1,33 @@
+
+ <% for key, metric of @metrics: %>
+
+ <% end %>
+
+
+<%- @T('Profiles') %>
+
diff --git a/app/assets/javascripts/app/views/report/time_picker.jst.eco b/app/assets/javascripts/app/views/report/time_picker.jst.eco
new file mode 100644
index 000000000..707fc5220
--- /dev/null
+++ b/app/assets/javascripts/app/views/report/time_picker.jst.eco
@@ -0,0 +1,36 @@
+
+ <% if @params.timeRange is 'day': %>
+
+ <% for item in @timeRangeDay: %>
+ <%= item.display %>
+ <% end %>
+
+
+ <% end %>
+
+ <% if @params.timeRange is 'day' || @params.timeRange is 'month': %>
+
+ <% for item in @timeRangeMonth: %>
+ <%= item.display %>
+ <% end %>
+
+
+ <% end %>
+
+ <% if @params.timeRange is 'week': %>
+
+ <% for item in @timeRangeWeek: %>
+ <%= item.display %>
+ <% end %>
+
+
+ <% end %>
+
+ <% if @params.timeRange isnt 'realtime': %>
+
+ <% for item in @timeRangeYear: %>
+ <%= item.display %>
+ <% end %>
+
+ <% end %>
+
\ No newline at end of file
diff --git a/app/assets/javascripts/app/views/report/time_range_picker.jst.eco b/app/assets/javascripts/app/views/report/time_range_picker.jst.eco
new file mode 100644
index 000000000..6102afc5c
--- /dev/null
+++ b/app/assets/javascripts/app/views/report/time_range_picker.jst.eco
@@ -0,0 +1,7 @@
+
+ <%- @T('Year') %>
+ <%- @T('Month') %>
+ <%- @T('Week') %>
+ <%- @T('Day') %>
+ <%- @T('Realtime') %>
+
\ No newline at end of file
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index eebf8fbe7..a1aa3874e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -151,7 +151,7 @@ class ApplicationController < ActionController::Base
def authentication_check_only(auth_param)
- logger.debug 'authentication_check'
+ #logger.debug 'authentication_check'
#logger.debug params.inspect
#logger.debug session.inspect
#logger.debug cookies.inspect
diff --git a/app/controllers/report_profiles_controller.rb b/app/controllers/report_profiles_controller.rb
new file mode 100644
index 000000000..58f574e18
--- /dev/null
+++ b/app/controllers/report_profiles_controller.rb
@@ -0,0 +1,141 @@
+class ReportProfilesController < ApplicationController
+ before_action :authentication_check
+
+=begin
+
+Format:
+JSON
+
+Example:
+{
+ "id":1,
+ "name":"some report_profile",
+ "condition":{"c_a":1,"c_b":2},
+ "updated_at":"2012-09-14T17:51:53Z",
+ "created_at":"2012-09-14T17:51:53Z",
+ "updated_by_id":2.
+ "created_by_id":2,
+}
+
+=end
+
+=begin
+
+Resource:
+GET /api/report_profiles.json
+
+Response:
+[
+ {
+ "id": 1,
+ "name": "some_name1",
+ ...
+ },
+ {
+ "id": 2,
+ "name": "some_name2",
+ ...
+ }
+]
+
+Test:
+curl http://localhost/api/report_profiles.json -v -u #{login}:#{password}
+
+=end
+
+ def index
+ model_index_render(Report::Profile, params)
+ end
+
+=begin
+
+Resource:
+GET /api/report_profiles/#{id}.json
+
+Response:
+{
+ "id": 1,
+ "name": "name_1",
+ ...
+}
+
+Test:
+curl http://localhost/api/report_profiles/#{id}.json -v -u #{login}:#{password}
+
+=end
+
+ def show
+ model_show_render(Report::Profile, params)
+ end
+
+=begin
+
+Resource:
+POST /api/report_profiles.json
+
+Payload:
+{
+ "name":"some report_profile",
+ "condition":{"c_a":1,"c_b":2},
+}
+
+Response:
+{
+ "id": 1,
+ "name": "some_name",
+ ...
+}
+
+Test:
+curl http://localhost/api/report_profiles.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"name": "some_name","active": true, "note": "some note"}'
+
+=end
+
+ def create
+ model_create_render(Report::Profile, params)
+ end
+
+=begin
+
+Resource:
+PUT /api/report_profiles/{id}.json
+
+Payload:
+{
+ "name":"some report_profile",
+ "condition":{"c_a":1,"c_b":2},
+}
+
+Response:
+{
+ "id": 1,
+ "name": "some_name",
+ ...
+}
+
+Test:
+curl http://localhost/api/report_profiles.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X PUT -d '{"name": "some_name","active": true, "note": "some note"}'
+
+=end
+
+ def update
+ model_update_render(Report::Profile, params)
+ end
+
+=begin
+
+Resource:
+DELETE /api/report_profiles/{id}.json
+
+Response:
+{}
+
+Test:
+curl http://localhost/api/report_profiles.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X DELETE
+
+=end
+
+ def destroy
+ model_destory_render(Report::Profile, params)
+ end
+end
diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb
new file mode 100644
index 000000000..52eaa5e72
--- /dev/null
+++ b/app/controllers/reports_controller.rb
@@ -0,0 +1,152 @@
+# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
+
+class ReportsController < ApplicationController
+ before_action :authentication_check
+
+ # GET /api/reports/config
+ def config
+ return if deny_if_not_role('Report')
+ render json: {
+ config: Report.config,
+ profiles: Report::Profile.list,
+ }
+ end
+
+ # GET /api/reports/generate
+ def generate
+ return if deny_if_not_role('Report')
+
+ #{"metric"=>"count", "year"=>2015, "month"=>10, "week"=>43, "day"=>20, "timeSlot"=>"year", "report"=>{"metric"=>"count", "year"=>2015, "month"=>10, "week"=>43, "day"=>20, "timeSlot"=>"year"}}
+ if params[:timeRange] == 'realtime'
+ start = (Time.zone.now - 60.minutes).iso8601
+ stop = Time.zone.now.iso8601
+ created = aggs(start, stop, 'minute', 'created_at')
+ closed = aggs(start, stop, 'minute', 'close_time')
+ elsif params[:timeRange] == 'day'
+ start = Date.parse("#{params[:year]}-#{params[:month]}-#{params[:day]}").iso8601
+ start = "#{start}T00:00:00Z"
+ stop = "#{start}T23:59:59Z"
+ created = aggs(start, stop, 'hour', 'created_at')
+ closed = aggs(start, stop, 'hour', 'close_time')
+ elsif params[:timeRange] == 'week'
+ start = Date.commercial(params[:year], params[:week]).iso8601
+ stop = Date.parse(start).end_of_week
+ created = aggs(start, stop, 'week', 'created_at')
+ closed = aggs(start, stop, 'week', 'close_time')
+ elsif params[:timeRange] == 'month'
+ start = Date.parse("#{params[:year]}-#{params[:month]}-01}").iso8601
+ stop = Date.parse(start).end_of_month
+ created = aggs(start, stop, 'day', 'created_at')
+ closed = aggs(start, stop, 'day', 'close_time')
+ else
+ start = "#{params[:year]}-01-01"
+ stop = "#{params[:year]}-12-31"
+ created = aggs(start, stop, 'month', 'created_at')
+ closed = aggs(start, stop, 'month', 'close_time')
+ end
+ render json: {
+ data: {
+ created: created,
+ closed: closed,
+ }
+ }
+ end
+
+ # GET /api/reports/sets
+ def sets
+ return if deny_if_not_role('Report')
+
+ #{"metric"=>"count", "year"=>2015, "month"=>10, "week"=>43, "day"=>20, "timeSlot"=>"year", "report"=>{"metric"=>"count", "year"=>2015, "month"=>10, "week"=>43, "day"=>20, "timeSlot"=>"year"}}
+ if params[:timeRange] == 'realtime'
+ start = (Time.zone.now - 60.minutes).iso8601
+ stop = Time.zone.now.iso8601
+ elsif params[:timeRange] == 'day'
+ start = Date.parse("#{params[:year]}-#{params[:month]}-#{params[:day]}").iso8601
+ start = "#{start}T00:00:00Z"
+ stop = "#{start}T23:59:59Z"
+ elsif params[:timeRange] == 'week'
+ start = Date.commercial(params[:year], params[:week]).iso8601
+ stop = Date.parse(start).end_of_week
+ elsif params[:timeRange] == 'month'
+ start = Date.parse("#{params[:year]}-#{params[:month]}-01}").iso8601
+ stop = Date.parse(start).end_of_month
+ else
+ start = "#{params[:year]}-01-01"
+ stop = "#{params[:year]}-12-31"
+ end
+
+ # get data
+
+ render json: {
+ data: {
+ start: start,
+ stop: stop,
+ }
+ }
+ end
+
+ def aggs(range_start, range_end, interval, field)
+ result = SearchIndexBackend.aggs(
+ {
+ },
+ [range_start, range_end, field, interval],
+ ['Ticket'],
+ )
+ data = []
+ if interval == 'month'
+ start = Date.parse(range_start)
+ stop_interval = 12
+ elsif interval == 'week'
+ start = Date.parse(range_start)
+ stop_interval = 7
+ elsif interval == 'day'
+ start = Date.parse(range_start)
+ stop_interval = 31
+ elsif interval == 'hour'
+ start = Time.zone.parse(range_start)
+ stop_interval = 24
+ elsif interval == 'minute'
+ start = Time.zone.parse(range_start)
+ stop_interval = 60
+ end
+ (1..stop_interval).each {|counter|
+ match = false
+ result['aggregations']['time_buckets']['buckets'].each {|item|
+ if interval == 'minute'
+ start_string = start.iso8601.sub(/:\d\d.+?$/, '')
+ else
+ start_string = start.iso8601.sub(/:\d\d:\d\d.+?$/, '')
+ end
+ next if !item['doc_count']
+ next if item['key_as_string'] !~ /#{start_string}/
+ match = true
+ data.push [counter, item['doc_count']]
+ if interval == 'month'
+ start = start.next_month
+ elsif interval == 'week'
+ start = start.next_week
+ elsif interval == 'day'
+ start = start.next_day
+ elsif interval == 'hour'
+ start = start + 1.hour
+ elsif interval == 'minute'
+ start = start + 1.minute
+ end
+ }
+ next if match
+ data.push [counter, 0]
+ if interval == 'month'
+ start = start.next_month
+ elsif interval == 'week'
+ start = start.next_week
+ elsif interval == 'day'
+ start = start + 1.day
+ elsif interval == 'hour'
+ start = start + 1.hour
+ elsif interval == 'minute'
+ start = start + 1.minute
+ end
+ }
+ data
+ end
+end
diff --git a/app/models/report.rb b/app/models/report.rb
new file mode 100644
index 000000000..22cb72ff2
--- /dev/null
+++ b/app/models/report.rb
@@ -0,0 +1,278 @@
+class Report
+
+ def self.config
+ config = {}
+ config[:metric] = {}
+
+ config[:metric][:count] = {
+ name: 'count',
+ display: 'Ticket Count',
+ default: true,
+ prio: 10_000,
+ }
+ backend = [
+ {
+ name: 'created',
+ display: 'Created',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'closed',
+ display: 'Closed',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'backlog',
+ display: 'Backlog',
+ selected: true,
+ dataDownload: false,
+ },
+ {
+ name: 'first_solution',
+ display: 'First Solution',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'reopen',
+ display: 'Re-Open',
+ selected: false,
+ dataDownload: true,
+ },
+ {
+ name: 'movedin',
+ display: 'Moved in',
+ selected: false,
+ dataDownload: true,
+ },
+ {
+ name: 'movedout',
+ display: 'Moved out',
+ selected: false,
+ dataDownload: true,
+ },
+ {
+ name: 'sla_in',
+ display: 'SLA in',
+ selected: false,
+ dataDownload: true,
+ },
+ {
+ name: 'sla_out',
+ display: 'SLA out',
+ selected: false,
+ dataDownload: true,
+ },
+ ]
+ config[:metric][:count][:backend] = backend
+
+ config[:metric][:create_channels] = {
+ name: 'create_channels',
+ display: 'Create Channels',
+ prio: 9000,
+ }
+ backend = [
+ {
+ name: 'phone_in',
+ display: 'Phone (in)',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'phone_out',
+ display: 'Phone (out)',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'email_in',
+ display: 'Email (in)',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'email_out',
+ display: 'Email (out)',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'web_in',
+ display: 'Web (in)',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'twitter_in',
+ display: 'Twitter (in)',
+ selected: true,
+ dataDownload: true,
+ },
+ ]
+ config[:metric][:create_channels][:backend] = backend
+
+ config[:metric][:times] = {
+ name: 'times',
+ display: 'Times',
+ prio: 8000,
+ }
+ backend = [
+ {
+ name: 'first_response_average',
+ display: 'First Response average',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'first_response_max',
+ display: 'First Response max',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'first_response_min',
+ display: 'First Response min',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'solution_time_average',
+ display: 'Solution Time average',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'solution_time_max',
+ display: 'Solution Time max',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'solution_time_min',
+ display: 'Solution Time min',
+ selected: true,
+ dataDownload: true,
+ },
+ ]
+ config[:metric][:times][:backend] = backend
+
+ config[:metric][:communication] = {
+ name: 'communication',
+ display: 'Communication',
+ prio: 7000,
+ }
+ backend = [
+ {
+ name: 'phone_in',
+ display: 'Phone (in)',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'phone_out',
+ display: 'Phone (out)',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'email_in',
+ display: 'Email (in)',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'email_out',
+ display: 'Email (out)',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'web_in',
+ display: 'Web (in)',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'twitter_in',
+ display: 'Twitter (in)',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'twitter_out',
+ display: 'Twitter (out)',
+ selected: true,
+ dataDownload: true,
+ },
+ ]
+ config[:metric][:communication][:backend] = backend
+
+ config[:metric][:sla] = {
+ name: 'sla',
+ display: 'SLAs',
+ prio: 6000,
+ }
+ backend = [
+ {
+ name: 'sla_out_1',
+ display: 'SLA (out) - <1h',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'sla_out_2',
+ display: 'SLA (out) - <2h',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'sla_out_4',
+ display: 'SLA (out) - <4h',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'sla_out_8',
+ display: 'SLA (out) - <8h',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'sla_in_1',
+ display: 'SLA (in) - <1h',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'sla_in_2',
+ display: 'SLA (in) - <2h',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'sla_in_4',
+ display: 'SLA (in) - <4h',
+ selected: true,
+ dataDownload: true,
+ },
+ {
+ name: 'sla_in_8',
+ display: 'SLA (in) - <8h',
+ selected: true,
+ dataDownload: true,
+ },
+ ]
+ config[:metric][:sla][:backend] = backend
+
+ config[:metric].each {|metric_key, metric_value|
+ metric_value[:backend].each {|metric_backend|
+ metric_backend[:name] = "#{metric_key}::#{metric_backend[:name]}"
+ }
+ }
+
+ config
+ end
+
+end
diff --git a/app/models/report/profile.rb b/app/models/report/profile.rb
new file mode 100644
index 000000000..a5770ccf7
--- /dev/null
+++ b/app/models/report/profile.rb
@@ -0,0 +1,10 @@
+class Report::Profile < ApplicationModel
+ self.table_name = 'report_profiles'
+ validates :name, presence: true
+ store :condition
+
+ def self.list
+ where(active: true)
+ end
+
+end
diff --git a/config/routes/report.rb b/config/routes/report.rb
new file mode 100644
index 000000000..0dfa453bf
--- /dev/null
+++ b/config/routes/report.rb
@@ -0,0 +1,16 @@
+Zammad::Application.routes.draw do
+ api_path = Rails.configuration.api_path
+
+ # reports
+ match api_path + '/reports/config', to: 'reports#config', via: :get
+ match api_path + '/reports/generate', to: 'reports#generate', via: :post
+ match api_path + '/reports/sets', to: 'reports#sets', via: :post
+
+ # report_profiles
+ match api_path + '/report_profiles', to: 'report_profiles#index', via: :get
+ match api_path + '/report_profiles/:id', to: 'report_profiles#show', via: :get
+ match api_path + '/report_profiles', to: 'report_profiles#create', via: :post
+ match api_path + '/report_profiles/:id', to: 'report_profiles#update', via: :put
+ match api_path + '/report_profiles/:id', to: 'report_profiles#destroy', via: :delete
+
+end
diff --git a/db/migrate/20151019000001_create_report.rb b/db/migrate/20151019000001_create_report.rb
new file mode 100644
index 000000000..a72935f60
--- /dev/null
+++ b/db/migrate/20151019000001_create_report.rb
@@ -0,0 +1,63 @@
+class CreateReport < ActiveRecord::Migration
+ def up
+ create_table :report_profiles do |t|
+ t.column :name, :string, limit: 150, null: true
+ t.column :condition, :string, limit: 6000, null: true
+ t.column :active, :boolean, null: false, default: true
+ t.column :updated_by_id, :integer, null: false
+ t.column :created_by_id, :integer, null: false
+ t.timestamps null: false
+ end
+ add_index :report_profiles, [:name], unique: true
+
+ Report::Profile.create_if_not_exists(
+ name: '-all-',
+ condition: {},
+ active: true,
+ updated_by_id: 1,
+ created_by_id: 1,
+ )
+
+ Role.create_if_not_exists( name: 'Report', created_by_id: 1, updated_by_id: 1 )
+ Translation.create_if_not_exists(
+ locale: 'de-de',
+ source: 'Ticket Count',
+ target: 'Ticket Anzahl',
+ updated_by_id: 1,
+ created_by_id: 1,
+ )
+ Translation.create_if_not_exists(
+ locale: 'de-de',
+ source: 'Ticket Count',
+ target: 'Ticket Anzahl',
+ updated_by_id: 1,
+ created_by_id: 1,
+ )
+ Translation.create_if_not_exists(
+ locale: 'de-de',
+ source: 'Create Channels',
+ target: 'Erstellkanäle',
+ updated_by_id: 1,
+ created_by_id: 1,
+ )
+ Translation.create_if_not_exists(
+ locale: 'de-de',
+ source: 'Times',
+ target: 'Zeiten',
+ updated_by_id: 1,
+ created_by_id: 1,
+ )
+ Translation.create_if_not_exists(
+ locale: 'de-de',
+ source: 'Communication',
+ target: 'Kommunikation',
+ updated_by_id: 1,
+ created_by_id: 1,
+ )
+
+ end
+
+ def down
+ drop_table :report_profiles
+ end
+end
diff --git a/lib/search_index_backend.rb b/lib/search_index_backend.rb
index b7c84bdd7..ef9a31551 100644
--- a/lib/search_index_backend.rb
+++ b/lib/search_index_backend.rb
@@ -44,7 +44,7 @@ create/update/delete index
end
Rails.logger.info "# curl -X PUT \"#{url}\" \\"
- #Rails.logger.info "-d '#{data[:data].to_json}'"
+ Rails.logger.debug "-d '#{data[:data].to_json}'"
response = UserAgent.put(
url,
@@ -76,7 +76,7 @@ add new object to search index
return if !url
Rails.logger.info "# curl -X POST \"#{url}\" \\"
- #Rails.logger.info "-d '#{data.to_json}'"
+ Rails.logger.debug "-d '#{data.to_json}'"
response = UserAgent.post(
url,
@@ -119,7 +119,7 @@ remove whole data from index
password: Setting.get('es_password'),
}
)
- #Rails.logger.info "# #{response.code.to_s}"
+ Rails.logger.info "# #{response.code}"
return true if response.success?
#Rails.logger.info "NOTICE: can't drop index: " + response.inspect
false
@@ -194,7 +194,7 @@ return search result
data['query']['bool']['must'].push condition
Rails.logger.info "# curl -X POST \"#{url}\" \\"
- #Rails.logger.info " -d'#{data.to_json}'"
+ Rails.logger.debug " -d'#{data.to_json}'"
response = UserAgent.get(
url,
@@ -232,6 +232,124 @@ return search result
=begin
+return aggregation result
+
+ result = SearchIndexBackend.aggs(
+ {
+ title: 'test',
+ state_id: 4,
+ },
+ ['2014-10-19', '2015-10-19', 'created_at', 'month'],
+ ['Ticket'],
+ )
+
+ # year, quarter, month, week, day, hour, minute, second
+
+ result = {
+ hits:{
+ total:4819,
+ },
+ aggregations:{
+ time_buckets:{
+ buckets:[
+ {
+ key_as_string:"2014-10-01T00:00:00.000Z",
+ key:1412121600000,
+ doc_count:420
+ },
+ {
+ key_as_string:"2014-11-01T00:00:00.000Z",
+ key:1414800000000,
+ doc_count:561
+ },
+ ...
+ ]
+ }
+ }
+ }
+
+=end
+
+ def self.aggs(query, range, index = nil)
+
+ url = build_url()
+ return if !url
+ if index
+ if index.class == Array
+ url += "/#{index.join(',')}/_search"
+ else
+ url += "/#{index}/_search"
+ end
+ else
+ url += '/_search'
+ end
+
+ and_data = []
+ if query && !query.empty?
+ bool = {
+ bool: {
+ must: {
+ term: query,
+ },
+ },
+ }
+ and_data.push bool
+ end
+ range_data = {}
+ range_data[range[2]] = {
+ from: range[0],
+ to: range[1],
+ }
+ range_data_and = {
+ range: range_data,
+ }
+ and_data.push range_data_and
+
+ data = {
+ query: {
+ filtered: {
+ filter: {
+ and: and_data,
+ }
+ }
+ },
+ size: 0,
+ aggs: {
+ time_buckets: {
+ date_histogram: {
+ field: range[2],
+ interval: range[3],
+ }
+ }
+ }
+ }
+
+ Rails.logger.info "# curl -X POST \"#{url}\" \\"
+ Rails.logger.debug " -d'#{data.to_json}'"
+
+ response = UserAgent.get(
+ url,
+ data,
+ {
+ json: true,
+ open_timeout: 5,
+ read_timeout: 14,
+ user: Setting.get('es_user'),
+ password: Setting.get('es_password'),
+ }
+ )
+
+ Rails.logger.info "# #{response.code}"
+ if !response.success?
+ Rails.logger.error "ERROR: #{response.inspect}"
+ return []
+ end
+ Rails.logger.debug response.data.to_json
+ response.data
+ end
+
+=begin
+
return true if backend is configured
result = SearchIndexBackend.enabled?