Browse Source

Next features for mini reporting.

Martin Edenhofer 9 years ago
parent
commit
b9a5de081e

+ 51 - 34
app/assets/javascripts/app/controllers/report.js.coffee

@@ -87,8 +87,6 @@ class Graph extends App.ControllerContent
   constructor: ->
     super
 
-    return
-
     # rerender view
     @bind 'ui:report:rerender', =>
       @render()
@@ -98,7 +96,14 @@ class Graph extends App.ControllerContent
   render: =>
 
     update = (data) =>
-      @draw(data.data)
+
+      # show only selected lines
+      dataNew = {}
+      for key, value of data.data
+        if @params.backendSelected[key] is true
+          dataNew[key] = value
+
+      @draw(dataNew)
       t = new Date
       @el.find('#download-chart').html(t.toString())
       new Download(
@@ -109,19 +114,20 @@ class Graph extends App.ControllerContent
       )
 
     url = @apiPath + '/reports/generate'
-    interval = 60000
+    interval = 5 * 60000
     if @params.timeRange is 'year'
-      interval = 30000
+      interval = 5 * 60000
     if @params.timeRange is 'month'
-      interval = 20000
+      interval = 60000
     if @params.timeRange is 'week'
-      interval = 20000
+      interval = 40000
     if @params.timeRange is 'day'
       interval = 20000
     if @params.timeRange is 'realtime'
       interval = 10000
 
     @ajax(
+      id: 'report_graph'
       type: 'POST'
       url:  url
       data: JSON.stringify(
@@ -137,7 +143,7 @@ class Graph extends App.ControllerContent
       processData: true
       success: (data) =>
         update(data)
-        @delay( @render, interval, 'report-update', 'page' )
+        @delay(@render, interval, 'report-update', 'page')
     )
 
   draw: (data) =>
@@ -162,10 +168,22 @@ class Graph extends App.ControllerContent
 
     dataPlot = []
     for key, value of data
+      realname = key
+      if @config.metric[@params.metric]
+        for backend in @config.metric[@params.metric].backend
+          if backend.name is key
+            realname = backend.display
+      content = []
+      count = 0
+      for i in xaxis
+        content.push [count, value[count]]
+        count += 1
+
       dataPlot.push {
-        data: value
-        label: key
+        data: content
+        label: App.i18n.translateInline(realname)
       }
+
     # plot
     $.plot( $('#placeholder'), dataPlot, {
       yaxis: { min: 0 },
@@ -190,12 +208,10 @@ class Download extends App.Controller
     reports = []
 
     # select first backend, if no backend is selected
-    @backendSelected = undefined
     if @config.metric[@params.metric]
       for backend in @config.metric[@params.metric].backend
-        console.log('bac', backend)
-        if backend.dataDownload && !@backendSelected
-          @backendSelected = backend.name
+        if backend.dataDownload && !@params.downloadBackendSelected
+          @params.downloadBackendSelected = backend.name
 
     # get used profiles
     profiles = []
@@ -206,10 +222,10 @@ class Download extends App.Controller
         profiles.push App.ReportProfile.find(key)
 
     @html App.view('report/download_header')(
-      reports:         reports
-      profiles:        profiles
-      backendSelected: @backendSelected
-      metric:          @config.metric[@params.metric]
+      reports:                 reports
+      profiles:                profiles
+      downloadBackendSelected: @params.downloadBackendSelected
+      metric:                  @config.metric[@params.metric]
     )
 
     @tableUpdate()
@@ -219,8 +235,8 @@ class Download extends App.Controller
       e.preventDefault()
       @el.find('.js-dataDownloadBackendSelector').parent().removeClass('active')
       $(e.target).parent().addClass('active')
-      @profileSelectedId = $(e.target).data('profile-id')
-      @backendSelected   = $(e.target).data('backend')
+      @profileSelectedId       = $(e.target).data('profile-id')
+      @params.downloadBackendSelected = $(e.target).data('backend')
 
     table = (tickets, count) =>
       url = '#ticket/zoom/'
@@ -239,17 +255,19 @@ class Download extends App.Controller
 
     @startLoading()
     @ajax(
+      id: 'report_download'
       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
-        profile_id: @profileSelectedId
-        backend:    @backendSelected
+        metric:                  @params.metric
+        year:                    @params.year
+        month:                   @params.month
+        week:                    @params.week
+        day:                     @params.day
+        timeRange:               @params.timeRange
+        profiles:                @params.profileSelected
+        backends:                @params.backendSelected
+        downloadBackendSelected: @params.downloadBackendSelected
       )
       processData: true
       success: (data) =>
@@ -433,7 +451,7 @@ class Sidebar extends App.Controller
   events:
     'click .js-profileSelector': 'selectProfile'
     'click .js-backendSelector': 'selectBackend'
-    'click .panel-heading':        'selectMetric'
+    'click .panel-heading':      'selectMetric'
 
   constructor: ->
     super
@@ -459,11 +477,10 @@ class Sidebar extends App.Controller
 
   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]
+    console.log('llll', profile_id)
+    for key, value of @params.profileSelected
+      delete @params.profileSelected[key]
+    @params.profileSelected[profile_id] = true
     App.Event.trigger( 'ui:report:rerender' )
 
   selectBackend: (e) =>

+ 1 - 1
app/assets/javascripts/app/views/report/download_header.jst.eco

@@ -4,7 +4,7 @@
     <% for profile in @profiles: %>
       <% for backend in @metric.backend: %>
         <% if backend.dataDownload: %>
-        <li <% if backend.name is @backendSelected: %>class="is-active active"<% end %>><a href="#" class="js-dataDownloadBackendSelector" data-toggle="tab" data-profile-id="<%= profile.id %>" data-backend="<%= backend.name %>"><%= @T(backend.display) %></a></li>
+        <li <% if backend.name is @downloadBackendSelected: %>class="is-active active"<% end %>><a href="#" class="js-dataDownloadBackendSelector" data-toggle="tab" data-profile-id="<%= profile.id %>" data-backend="<%= backend.name %>"><%= @T(backend.display) %></a></li>
         <% end %>
       <% end %>
     <% end %>

+ 1 - 1
app/assets/javascripts/app/views/report/download_list.jst.eco

@@ -1,5 +1,5 @@
 <i><%- @T('%s records', @count) %></i>
-  <a href="<%- @download %>" target="_blank" data-type="attachment" id="downloadsetascsv"><%- @Icon('download') %></a>
+  <a href="<%- @download %>" target="_blank" data-type="attachment"><%- @Icon('download') %></a>
 <table class="table table-striped table-hover">
   <thead>
     <tr>

+ 2 - 10
app/assets/javascripts/app/views/report/main.jst.eco

@@ -1,6 +1,6 @@
 <div class="main flex">
 
-  <div class="page-header page-header--center">
+  <div class="page-header">
     <div class="page-header-title">
       <h1><%- @T( 'Reporting' ) %> <small></small></h1>
     </div>
@@ -12,17 +12,9 @@
     <div class="page-main">
       <div class="js-timeRangePicker"></div>
       <div class="page-content">
-        
+
         <div id="placeholder" class="" style="height:350px;"></div>
         <span class=" muted" id="download-chart" style="font-size: 8px;"></span>
-      <!--
-      <a href="<%-@download%>" target="_blank" data-type="attachment" class="pull-right" id="download-chart">
-      <i class="icon-download" title="<%- @Ti('Download') %>"></i>
-      </a>
-      -->
-      <!--
-            <div id="overview" style="margin-left:50px;margin-top:20px;width:400px;height:50px"></div>
-      -->
 
         <div class="js-timePicker"></div>
         <div class="js-dataDownload"></div>

+ 1 - 1
app/assets/javascripts/app/views/report/sidebar.jst.eco

@@ -31,7 +31,7 @@
   <% for profile in @profiles: %>
     <li>
       <label class="inline-label radio-replacement">
-        <input class="js-profileSelector" type="radio" value="<%= profile.id %>"<%- ' checked' if @params.profileSelected[profile.id] %>>
+        <input class="js-profileSelector" type="radio" name="profile_id" value="<%= profile.id %>"<%- ' checked' if @params.profileSelected[profile.id] %>>
         <%- @Icon('radio', 'icon-unchecked') %>
         <%- @Icon('radio-checked', 'icon-checked') %>
         <span class="label-text"><%= profile.name %></span>

+ 109 - 116
app/controllers/reports_controller.rb

@@ -16,39 +16,44 @@ class ReportsController < ApplicationController
   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'
-      date = Date.parse("#{params[:year]}-#{params[:month]}-#{params[:day]}").to_s
-      start = "#{date}T00:00:00Z"
-      stop = "#{date}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
+    get_params = params_all
+    return if !get_params
+
+    result = {}
+    get_params[:metric][:backend].each {|backend|
+      condition = get_params[:profile].condition
+      if backend[:condition]
+        backend[:condition].merge(condition)
+      else
+        backend[:condition] = condition
+      end
+      next if !backend[:adapter]
+      result[backend[:name]] = backend[:adapter].aggs(
+        range_start: get_params[:start],
+        range_end:   get_params[:stop],
+        interval:    get_params[:range],
+        selector:    backend[:condition],
+        params:      backend[:params],
+      )
+    }
+
+    #created = aggs(start, stop, range, 'created_at', profile.condition)
+    #closed = aggs(start, stop, range, 'close_time', profile.condition)
+    #first_solution =
+    #reopend = backend(start, stop, range, Report::TicketReopened, profile.condition)
+
+    # add backlog
+    #backlogs = []
+    #position = -1
+    #created.each {|_not_used|
+    # position += 1
+    #  diff = created[position][1] - closed[position][1]
+    #  backlog = [position+1, diff]
+    #  backlogs.push backlog
+    #}
+
     render json: {
-      data: {
-        created: created,
-        closed: closed,
-      }
+      data: result
     }
   end
 
@@ -56,109 +61,97 @@ class ReportsController < ApplicationController
   def sets
     return if deny_if_not_role('Report')
 
+    get_params = params_all
+    return if !get_params
+
+    if !params[:downloadBackendSelected]
+      render json: {
+        error: 'No such downloadBackendSelected param',
+      }, status: :unprocessable_entity
+      return
+    end
+
+    # get data
+    result = {}
+    get_params[:metric][:backend].each {|backend|
+      next if params[:downloadBackendSelected] != backend[:name]
+      condition = get_params[:profile].condition
+      if backend[:condition]
+        backend[:condition].merge(condition)
+      else
+        backend[:condition] = condition
+      end
+      next if !backend[:adapter]
+      result = backend[:adapter].items(
+        range_start: get_params[:start],
+        range_end:   get_params[:stop],
+        interval:    get_params[:range],
+        selector:    backend[:condition],
+        params:      backend[:params],
+      )
+    }
+    render json: result
+  end
+
+  def params_all
+    profile = nil
+    if !params[:profiles]
+      render json: {
+        error: 'No such profiles param',
+      }, status: :unprocessable_entity
+      return
+    end
+    params[:profiles].each {|profile_id, active|
+      next if !active
+      profile = Report::Profile.find(profile_id)
+    }
+    if !profile
+      render json: {
+        error: 'No such active profile',
+      }, status: :unprocessable_entity
+      return
+    end
+
+    config = Report.config
+    if !config || !config[:metric] || !config[:metric][params[:metric].to_sym]
+      render json: {
+        error: "No such metric #{params[:metric]}"
+      }, status: :unprocessable_entity
+      return
+    end
+    metric = config[:metric][params[:metric].to_sym]
+
     #{"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
+      range = 'minute'
     elsif params[:timeRange] == 'day'
       date = Date.parse("#{params[:year]}-#{params[:month]}-#{params[:day]}").to_s
       start = "#{date}T00:00:00Z"
       stop = "#{date}T23:59:59Z"
+      range = 'hour'
     elsif params[:timeRange] == 'week'
       start = Date.commercial(params[:year], params[:week]).iso8601
       stop = Date.parse(start).end_of_week
+      range = 'week'
     elsif params[:timeRange] == 'month'
       start = Date.parse("#{params[:year]}-#{params[:month]}-01}").iso8601
       stop = Date.parse(start).end_of_month
+      range = 'day'
     else
       start = "#{params[:year]}-01-01"
       stop = "#{params[:year]}-12-31"
+      range = 'month'
     end
-
-    # get data
-    ticket_ids = []
-    assets = {}
-    Ticket.select('id').all.each {|ticket_part|
-      ticket = Ticket.lookup(id: ticket_part.id)
-      assets = ticket.assets(assets)
-      ticket_ids.push ticket_part.id
-    }
-    count = Ticket.count
-    render json: {
-      ticket_ids: ticket_ids,
-      assets: assets,
-      count: count,
+    {
+      profile: profile,
+      metric: metric,
+      config: config,
+      start: start,
+      stop: stop,
+      range: range,
     }
   end
 
-  def aggs(range_start, range_end, interval, field)
-    interval_es = interval
-    if interval == 'week'
-      interval_es = 'day'
-    end
-    result = SearchIndexBackend.aggs(
-      {
-      },
-      [range_start, range_end, field, interval_es],
-      ['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'
-          item['key_as_string'] = item['key_as_string'].sub(/:\d\d.\d\d\dZ$/, '')
-          start_string = start.iso8601.sub(/:\d\dZ$/, '')
-        else
-          start_string = start.iso8601.sub(/:\d\d:\d\d.+?$/, '')
-        end
-        next if !item['doc_count']
-        next if item['key_as_string'] !~ /#{start_string}/
-        next if match
-        match = true
-        data.push [counter, item['doc_count']]
-        if interval == 'month'
-          start = start.next_month
-        elsif interval == 'week'
-          start = start.next_day
-        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_day
-      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

+ 87 - 21
app/models/report.rb

@@ -16,12 +16,16 @@ class Report
         display: 'Created',
         selected: true,
         dataDownload: true,
+        adapter: Report::TicketGenericTime,
+        params: { field: 'created_at' },
       },
       {
         name: 'closed',
         display: 'Closed',
         selected: true,
         dataDownload: true,
+        adapter: Report::TicketGenericTime,
+        params: { field: 'close_time' },
       },
       {
         name: 'backlog',
@@ -32,39 +36,45 @@ class Report
       {
         name: 'first_solution',
         display: 'First Solution',
-        selected: true,
+        selected: false,
         dataDownload: true,
+        adapter: Report::TicketFirstSolution,
       },
       {
-        name: 'reopen',
+        name: 'reopened',
         display: 'Re-Open',
         selected: false,
         dataDownload: true,
+        adapter: Report::TicketReopened,
       },
       {
         name: 'movedin',
         display: 'Moved in',
         selected: false,
         dataDownload: true,
+        adapter: Report::TicketMoved,
+        params: { type: 'in' },
       },
       {
         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,
-      },
+        adapter: Report::TicketMoved,
+        params: { type: 'out' },
+      },
+      #{
+      #  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
 
@@ -79,24 +89,80 @@ class Report
         display: 'Phone (in)',
         selected: true,
         dataDownload: true,
+        adapter: Report::TicketGenericTime,
+        params: {
+          field: 'created_at',
+          selector: {
+            'create_article_type_id' => {
+              'operator' => 'is',
+              'value' => Ticket::Article::Type.lookup(name: 'phone').id,
+            },
+            'create_article_sender_id' => {
+              'operator' => 'is',
+              'value' => Ticket::Article::Sender.lookup(name: 'Customer').id,
+            },
+          },
+        },
       },
       {
         name: 'phone_out',
         display: 'Phone (out)',
         selected: true,
         dataDownload: true,
+        adapter: Report::TicketGenericTime,
+        params: {
+          field: 'created_at',
+          selector: {
+            'create_article_type_id' => {
+              'operator' => 'is',
+              'value' => Ticket::Article::Type.lookup(name: 'phone').id,
+            },
+            'create_article_sender_id' => {
+              'operator' => 'is',
+              'value' => Ticket::Article::Sender.lookup(name: 'Agent').id,
+            },
+          }
+        },
       },
       {
         name: 'email_in',
         display: 'Email (in)',
         selected: true,
         dataDownload: true,
+        adapter: Report::TicketGenericTime,
+        params: {
+          field: 'created_at',
+          selector: {
+            'create_article_type_id' => {
+              'operator' => 'is',
+              'value' => Ticket::Article::Type.lookup(name: 'email').id,
+            },
+            'create_article_sender_id' => {
+              'operator' => 'is',
+              'value' => Ticket::Article::Sender.lookup(name: 'Customer').id,
+            },
+          },
+        },
       },
       {
         name: 'email_out',
         display: 'Email (out)',
         selected: true,
         dataDownload: true,
+        adapter: Report::TicketGenericTime,
+        params: {
+          field: 'created_at',
+          selector: {
+            'create_article_type_id' => {
+              'operator' => 'is',
+              'value' => Ticket::Article::Type.lookup(name: 'email').id,
+            },
+            'create_article_sender_id' => {
+              'operator' => 'is',
+              'value' => Ticket::Article::Sender.lookup(name: 'Agent').id,
+            },
+          },
+        },
       },
       {
         name: 'web_in',
@@ -218,19 +284,19 @@ class Report
       {
         name: 'sla_out_1',
         display: 'SLA (out) - <1h',
-        selected: true,
+        selected: false,
         dataDownload: true,
       },
       {
         name: 'sla_out_2',
         display: 'SLA (out) - <2h',
-        selected: true,
+        selected: false,
         dataDownload: true,
       },
       {
         name: 'sla_out_4',
         display: 'SLA (out) - <4h',
-        selected: true,
+        selected: false,
         dataDownload: true,
       },
       {
@@ -248,19 +314,19 @@ class Report
       {
         name: 'sla_in_2',
         display: 'SLA (in) - <2h',
-        selected: true,
+        selected: false,
         dataDownload: true,
       },
       {
         name: 'sla_in_4',
         display: 'SLA (in) - <4h',
-        selected: true,
+        selected: false,
         dataDownload: true,
       },
       {
         name: 'sla_in_8',
         display: 'SLA (in) - <8h',
-        selected: true,
+        selected: false,
         dataDownload: true,
       },
     ]

+ 389 - 0
lib/report/base.rb

@@ -0,0 +1,389 @@
+class Report::Base
+
+  # :object
+  # :type created|updated
+  # :attribute
+  # :value_from
+  # :value_to
+  # :start
+  # :end
+  # :selector
+  def self.history_count(params)
+
+    history_object = History::Object.lookup( name: params[:object] )
+
+    query, bind_params, tables = Ticket.selector2sql(params[:selector])
+
+    count = 0
+    ticket_ids = []
+
+    # created
+    if params[:type] == 'created'
+      history_type = History::Type.lookup( name: 'created' )
+      return History.select('histories.o_id').joins('INNER JOIN tickets ON tickets.id = histories.o_id')
+        .where(
+          'histories.created_at >= ? AND histories.created_at <= ? AND histories.history_object_id = ? AND histories.history_type_id = ?', params[:start], params[:end], history_object.id, history_type.id
+        )
+        .where(query, *bind_params).joins(tables).count
+    end
+
+    # updated
+    if params[:type] == 'updated'
+      history_type      = History::Type.lookup( name: 'updated' )
+      history_attribute = History::Attribute.lookup( name: params[:attribute] )
+      if !history_attribute || !history_type
+        count = 0
+      else
+        if params[:id_not_from] && params[:id_to]
+          return History.select('histories.o_id').joins('INNER JOIN tickets ON tickets.id = histories.o_id')
+            .where(query, *bind_params).joins(tables)
+            .where(
+              'histories.created_at >= ? AND histories.created_at <= ? AND histories.history_object_id = ? AND histories.history_type_id = ? AND histories.history_attribute_id IN (?) AND histories.id_from NOT IN (?) AND histories.id_to IN (?)',
+              params[:start],
+              params[:end],
+              history_object.id,
+              history_type.id,
+              history_attribute.id,
+              params[:id_not_from],
+              params[:id_to],
+            ).count
+        elsif params[:id_from] && params[:id_not_to]
+          return History.select('histories.o_id').joins('INNER JOIN tickets ON tickets.id = histories.o_id')
+            .where(query, *bind_params).joins(tables)
+            .where(
+              'histories.created_at >= ? AND histories.created_at <= ? AND histories.history_object_id = ? AND histories.history_type_id = ? AND histories.history_attribute_id IN (?) AND histories.id_from IN (?) AND histories.id_to NOT IN (?)',
+              params[:start],
+              params[:end],
+              history_object.id,
+              history_type.id,
+              history_attribute.id,
+              params[:id_from],
+              params[:id_not_to],
+            ).count
+        elsif params[:value_from] && params[:value_not_to]
+          return History.joins('INNER JOIN tickets ON tickets.id = histories.o_id')
+            .where(query, *bind_params).joins(tables)
+            .where(
+              'histories.created_at >= ? AND histories.created_at <= ? AND histories.history_object_id = ? AND histories.history_type_id = ? AND histories.history_attribute_id IN (?) AND histories.value_from IN (?) AND histories.value_to NOT IN (?)',
+              params[:start],
+              params[:end],
+              history_object.id,
+              history_type.id,
+              history_attribute.id,
+              params[:value_from],
+              params[:value_not_to],
+            ).count
+        elsif params[:value_to]
+          return History.select('histories.o_id').joins('INNER JOIN tickets ON tickets.id = histories.o_id')
+            .where(query, *bind_params).joins(tables)
+            .where(
+              'histories.created_at >= ? AND histories.created_at <= ? AND histories.history_object_id = ? AND histories.history_type_id = ? AND histories.history_attribute_id IN (?) AND histories.value_to IN (?)',
+              params[:start],
+              params[:end],
+              history_object.id,
+              history_type.id,
+              history_attribute.id,
+              params[:value_to],
+            ).count
+        elsif params[:id_to]
+          return History.select('histories.o_id').joins('INNER JOIN tickets ON tickets.id = histories.o_id')
+            .where(query, *bind_params).joins(tables)
+            .where(
+              'histories.created_at >= ? AND histories.created_at <= ? AND histories.history_object_id = ? AND histories.history_type_id = ? AND histories.history_attribute_id IN (?) AND histories.id_to IN (?)',
+              params[:start],
+              params[:end],
+              history_object.id,
+              history_type.id,
+              history_attribute.id,
+              params[:id_to],
+            ).count
+        else
+          fail "UNKOWN params (#{params.inspect})!"
+        end
+      end
+    end
+    fail "UNKOWN :type (#{params[:type]})!"
+  end
+
+  # :object
+  # :type created|updated
+  # :attribute
+  # :value_from
+  # :value_to
+  # :start
+  # :end
+  # :condition
+  def self.history(data)
+
+    history_object = History::Object.lookup( name: data[:object] )
+
+    query, bind_params, tables = Ticket.selector2sql(data[:selector])
+
+    count = 0
+    ticket_ids = []
+
+    # created
+    if data[:type] == 'created'
+      history_type = History::Type.lookup( name: 'created' )
+      histories = History.select('histories.o_id').joins('INNER JOIN tickets ON tickets.id = histories.o_id')
+                  .where(
+                    'histories.created_at >= ? AND histories.created_at <= ? AND histories.history_object_id = ? AND histories.history_type_id = ?', data[:start], data[:end], history_object.id, history_type.id
+                  )
+                  .where(query, *bind_params).joins(tables)
+      histories.each {|history|
+        count += 1
+        ticket_ids.push history.o_id
+      }
+      return {
+        count: count,
+        ticket_ids: ticket_ids,
+      }
+    end
+
+    # updated
+    if data[:type] == 'updated'
+      history_type      = History::Type.lookup( name: 'updated' )
+      history_attribute = History::Attribute.lookup( name: data[:attribute] )
+      if !history_attribute || !history_type
+        count = 0
+      else
+        if data[:id_not_from] && data[:id_to]
+          histories = History.select('histories.o_id').joins('INNER JOIN tickets ON tickets.id = histories.o_id')
+                      .where(query, *bind_params).joins(tables)
+                      .where(
+                        'histories.created_at >= ? AND histories.created_at <= ? AND histories.history_object_id = ? AND histories.history_type_id = ? AND histories.history_attribute_id IN (?) AND histories.id_from NOT IN (?) AND histories.id_to IN (?)',
+                        data[:start],
+                        data[:end],
+                        history_object.id,
+                        history_type.id,
+                        history_attribute.id,
+                        data[:id_not_from],
+                        data[:id_to],
+                      )
+        elsif data[:id_from] && data[:id_not_to]
+          histories = History.select('histories.o_id').joins('INNER JOIN tickets ON tickets.id = histories.o_id')
+                      .where(query, *bind_params).joins(tables)
+                      .where(
+                        'histories.created_at >= ? AND histories.created_at <= ? AND histories.history_object_id = ? AND histories.history_type_id = ? AND histories.history_attribute_id IN (?) AND histories.id_from IN (?) AND histories.id_to NOT IN (?)',
+                        data[:start],
+                        data[:end],
+                        history_object.id,
+                        history_type.id,
+                        history_attribute.id,
+                        data[:id_from],
+                        data[:id_not_to],
+                      )
+        elsif data[:value_from] && data[:value_not_to]
+          histories = History.joins('INNER JOIN tickets ON tickets.id = histories.o_id')
+                      .where(query, *bind_params).joins(tables)
+                      .where(
+                        'histories.created_at >= ? AND histories.created_at <= ? AND histories.history_object_id = ? AND histories.history_type_id = ? AND histories.history_attribute_id IN (?) AND histories.value_from IN (?) AND histories.value_to NOT IN (?)',
+                        data[:start],
+                        data[:end],
+                        history_object.id,
+                        history_type.id,
+                        history_attribute.id,
+                        data[:value_from],
+                        data[:value_not_to],
+                      )
+        elsif data[:value_to]
+          histories = History.select('histories.o_id').joins('INNER JOIN tickets ON tickets.id = histories.o_id')
+                      .where(query, *bind_params).joins(tables)
+                      .where(
+                        'histories.created_at >= ? AND histories.created_at <= ? AND histories.history_object_id = ? AND histories.history_type_id = ? AND histories.history_attribute_id IN (?) AND histories.value_to IN (?)',
+                        data[:start],
+                        data[:end],
+                        history_object.id,
+                        history_type.id,
+                        history_attribute.id,
+                        data[:value_to],
+                      )
+        elsif data[:id_to]
+          histories = History.select('histories.o_id').joins('INNER JOIN tickets ON tickets.id = histories.o_id')
+                      .where(query, *bind_params).joins(tables)
+                      .where(
+                        'histories.created_at >= ? AND histories.created_at <= ? AND histories.history_object_id = ? AND histories.history_type_id = ? AND histories.history_attribute_id IN (?) AND histories.id_to IN (?)',
+                        data[:start],
+                        data[:end],
+                        history_object.id,
+                        history_type.id,
+                        history_attribute.id,
+                        data[:id_to],
+                      )
+        end
+        histories.each {|history|
+          count += 1
+          ticket_ids.push history.o_id
+        }
+      end
+      return {
+        count: count,
+        ticket_ids: ticket_ids,
+      }
+    end
+    fail "UNKOWN :type (#{data[:type]})!"
+  end
+
+  # :sender
+  # :type
+  # :start
+  # :end
+  # :condition
+  def self.article_type_and_sender(data)
+    query, bind_params, tables = Ticket.selector2sql(data[:condition])
+    sender = Ticket::Article::Sender.lookup( name: data[:sender] )
+    type   = Ticket::Article::Type.lookup( name: data[:type] )
+    articles = Ticket::Article.joins('INNER JOIN tickets ON tickets.id = ticket_articles.ticket_id')
+               .where(query, *bind_params).joins(tables)
+               .where(
+                 'ticket_articles.created_at >= ? AND ticket_articles.created_at <= ? AND ticket_articles.type_id = ? AND ticket_articles.sender_id = ?',
+                 data[:start],
+                 data[:end],
+                 type.id,
+                 sender.id,
+               ).count
+    {
+      count: articles,
+    }
+  end
+
+  # :type
+  # :start
+  # :end
+  # :condition
+  def self.create_channel(data)
+    query, bind_params, tables = Ticket.selector2sql(data[:condition])
+    article_type = Ticket::Article::Type.lookup( name: data[:type] )
+    tickets = Ticket.select('tickets.id')
+              .where( 'tickets.created_at >= ? AND tickets.created_at <= ? AND tickets.create_article_type_id = ?', data[:start], data[:end], article_type.id )
+              .where(query, *bind_params).joins(tables)
+    count = 0
+    ticket_ids = []
+    tickets.each {|ticket|
+      count += 1
+      ticket_ids.push ticket.id
+    }
+    {
+      count: count,
+      ticket_ids: ticket_ids,
+    }
+  end
+
+  # :type
+  # :start
+  # :end
+  # :condition
+  def self.time_average(data)
+    query, bind_params, tables = Ticket.selector2sql(data[:condition])
+    ticket_list = Ticket.where( 'tickets.created_at >= ? AND tickets.created_at <= ?', data[:start], data[:end] )
+                  .where(query, *bind_params).joins(tables)
+    tickets = 0
+    time_total = 0
+    ticket_list.each {|ticket|
+      timestamp = ticket[ data[:type].to_sym ]
+      next if !timestamp
+      #          puts 'FR:' + first_response.to_s
+      #          puts 'CT:' + ticket.created_at.to_s
+      diff = timestamp - ticket.created_at
+      #puts 'DIFF:' + diff.to_s
+      time_total = time_total + diff
+      tickets += 1
+    }
+    if time_total == 0 || tickets == 0
+      tickets = -0.001
+    else
+      tickets = time_total / tickets / 60
+      tickets = tickets.to_i
+    end
+    {
+      count: tickets,
+    }
+  end
+
+  # :type
+  # :start
+  # :end
+  # :condition
+  def self.time_min(data)
+    query, bind_params, tables = Ticket.selector2sql(data[:condition])
+    ticket_list = Ticket.where( 'tickets.created_at >= ? AND tickets.created_at <= ?', data[:start], data[:end] )
+                  .where(query, *bind_params).joins(tables)
+    tickets = 0
+    time_min = 0
+    ticket_ids = []
+    ticket_list.each {|ticket|
+      timestamp = ticket[ data[:type].to_sym ]
+      next if !timestamp
+      ticket_ids.push ticket.id
+      #          puts 'FR:' + first_response.to_s
+      #          puts 'CT:' + ticket.created_at.to_s
+      diff = timestamp - ticket.created_at
+      #puts 'DIFF:' + diff.to_s
+      if !time_min
+        time_min = diff
+      end
+      if diff < time_min
+        time_min = diff
+      end
+    }
+    if time_min == 0
+      tickets = -0.001
+    else
+      tickets = (time_min / 60).to_i
+    end
+    {
+      count: tickets,
+      ticket_ids: ticket_ids,
+    }
+  end
+
+  # :type
+  # :start
+  # :end
+  # :condition
+  def self.time_max(data)
+    query, bind_params, tables = Ticket.selector2sql(data[:condition])
+    ticket_list = Ticket.where( 'tickets.created_at >= ? AND tickets.created_at <= ?', data[:start], data[:end] )
+                  .where(query, *bind_params).joins(tables)
+    tickets = 0
+    time_max = 0
+    ticket_ids = []
+    ticket_list.each {|ticket|
+      timestamp = ticket[ data[:type].to_sym ]
+      next if !timestamp
+      ticket_ids.push ticket.id
+      #        puts "#{data[:type].to_s} - #{timestamp} - #{ticket.inspect}"
+      #          puts 'FR:' + ticket.first_response.to_s
+      #          puts 'CT:' + ticket.created_at.to_s
+      diff = timestamp - ticket.created_at
+      #puts 'DIFF:' + diff.to_s
+      if !time_max
+        time_max = diff
+      end
+      if diff > time_max
+        time_max = diff
+      end
+    }
+    if time_max == 0
+      tickets = -0.001
+    else
+      tickets = (time_max / 60).to_i
+    end
+    {
+      count: tickets,
+      ticket_ids: ticket_ids,
+    }
+  end
+
+  def self.ticket_condition(ticket_id, condition)
+    ticket = Ticket.lookup( id: ticket_id )
+    match = true
+    condition.each {|key, value|
+      if ticket[key.to_sym] != value
+        return false
+      end
+    }
+    true
+  end
+
+end

+ 118 - 0
lib/report/ticket_first_solution.rb

@@ -0,0 +1,118 @@
+class Report::TicketFirstSolution
+
+=begin
+
+  result = Report::TicketFirstSolution.aggs(
+    range_start: '2015-01-01T00:00:00Z',
+    range_end:   '2015-12-31T23:59:59Z',
+    interval:    'month', # quarter, month, week, day, hour, minute, second
+    selector:    selector, # ticket selector to get only a collection of tickets
+  )
+
+returns
+
+  [4,5,1,5,0,51,5,56,7,4]
+
+=end
+
+  def self.aggs(params)
+    interval = params[:interval]
+    if params[:interval] == 'week'
+      interval = 'day'
+    end
+
+    result = []
+    if params[:interval] == 'month'
+      start = Date.parse(params[:range_start])
+      stop_interval = 12
+    elsif params[:interval] == 'week'
+      start = Date.parse(params[:range_start])
+      stop_interval = 7
+    elsif params[:interval] == 'day'
+      start = Date.parse(params[:range_start])
+      stop_interval = 31
+    elsif params[:interval] == 'hour'
+      start = Time.zone.parse(params[:range_start])
+      stop_interval = 24
+    elsif params[:interval] == 'minute'
+      start = Time.zone.parse(params[:range_start])
+      stop_interval = 60
+    end
+    (1..stop_interval).each {|_counter|
+      if params[:interval] == 'month'
+        stop = start.next_month
+      elsif params[:interval] == 'week'
+        stop = start.next_day
+      elsif params[:interval] == 'day'
+        stop = start.next_day
+      elsif params[:interval] == 'hour'
+        stop = start + 1.hour
+      elsif params[:interval] == 'minute'
+        stop = start + 1.minute
+      end
+      query, bind_params, tables = Ticket.selector2sql(params[:selector])
+      ticket_list = Ticket.select('tickets.id, tickets.close_time, tickets.created_at').where(
+        'tickets.close_time IS NOT NULL AND tickets.close_time >= ? AND tickets.close_time < ?',
+        start,
+        stop,
+      ).where(query, *bind_params).joins(tables)
+      count = 0
+      ticket_list.each {|ticket|
+        closed_at  = ticket.close_time
+        created_at = ticket.created_at
+        if (closed_at - (60 * 15) ) < created_at
+          count += 1
+        end
+      }
+      result.push count
+      start = stop
+    }
+    result
+  end
+
+=begin
+
+  result = Report::TicketFirstSolution.items(
+    range_start: '2015-01-01T00:00:00Z',
+    range_end:   '2015-12-31T23:59:59Z',
+    selector:    selector, # ticket selector to get only a collection of tickets
+  )
+
+returns
+
+  {
+    count: 123,
+    ticket_ids: [4,5,1,5,0,51,5,56,7,4],
+    assets: assets,
+  }
+
+=end
+
+  def self.items(params)
+    query, bind_params, tables = Ticket.selector2sql(params[:selector])
+    ticket_list = Ticket.select('tickets.id, tickets.close_time, tickets.created_at').where(
+      'tickets.close_time IS NOT NULL AND tickets.close_time >= ? AND tickets.close_time < ?',
+      params[:range_start],
+      params[:range_end],
+    ).where(query, *bind_params).joins(tables)
+    count = 0
+    assets = {}
+    ticket_ids = []
+    ticket_list.each {|ticket|
+      closed_at  = ticket.close_time
+      created_at = ticket.created_at
+      if (closed_at - (60 * 15) ) < created_at
+        count += 1
+        ticket_ids.push ticket.id
+      end
+      ticket_full = Ticket.find(ticket.id)
+      assets = ticket_full.assets(assets)
+    }
+    {
+      count: count,
+      ticket_ids: ticket_ids,
+      assets: assets,
+    }
+  end
+
+end

+ 131 - 0
lib/report/ticket_generic_time.rb

@@ -0,0 +1,131 @@
+class Report::TicketGenericTime
+
+=begin
+  selector = {}
+  result = Report::TicketGenericTime.aggs(
+    range_start: '2015-01-01T00:00:00Z',
+    range_end:   '2015-12-31T23:59:59Z',
+    interval:    'month', # year, quarter, month, week, day, hour, minute, second
+    selector:    selector, # ticket selector to get only a collection of tickets
+    params:      { field: 'created_at' },
+  )
+
+returns
+
+  [4,5,1,5,0,51,5,56,7,4]
+
+=end
+
+  def self.aggs(params)
+    interval_es = params[:interval]
+    if params[:interval] == 'week'
+      interval_es = 'day'
+    end
+
+    aggs_interval = {
+      from: params[:range_start],
+      to: params[:range_end],
+      interval: interval_es, # year, quarter, month, week, day, hour, minute, second
+      field: params[:params][:field],
+    }
+
+    result_es = SearchIndexBackend.selectors(['Ticket'], params[:selector], nil, nil, aggs_interval)
+
+    if params[:interval] == 'month'
+      start = Date.parse(params[:range_start])
+      stop_interval = 12
+    elsif params[:interval] == 'week'
+      start = Date.parse(params[:range_start])
+      stop_interval = 7
+    elsif params[:interval] == 'day'
+      start = Date.parse(params[:range_start])
+      stop_interval = 31
+    elsif params[:interval] == 'hour'
+      start = Time.zone.parse(params[:range_start])
+      stop_interval = 24
+    elsif params[:interval] == 'minute'
+      start = Time.zone.parse(params[:range_start])
+      stop_interval = 60
+    end
+    result = []
+    (1..stop_interval).each {|_counter|
+      match = false
+      result_es['aggregations']['time_buckets']['buckets'].each {|item|
+        if params[:interval] == 'minute'
+          item['key_as_string'] = item['key_as_string'].sub(/:\d\d.\d\d\dZ$/, '')
+          start_string = start.iso8601.sub(/:\d\dZ$/, '')
+        else
+          start_string = start.iso8601.sub(/:\d\d:\d\d.+?$/, '')
+        end
+        next if !item['doc_count']
+        next if item['key_as_string'] !~ /#{start_string}/
+        next if match
+        match = true
+        result.push item['doc_count']
+        if params[:interval] == 'month'
+          start = start.next_month
+        elsif params[:interval] == 'week'
+          start = start.next_day
+        elsif params[:interval] == 'day'
+          start = start.next_day
+        elsif params[:interval] == 'hour'
+          start = start + 1.hour
+        elsif params[:interval] == 'minute'
+          start = start + 1.minute
+        end
+      }
+      next if match
+      result.push 0
+      if params[:interval] == 'month'
+        start = start.next_month
+      elsif params[:interval] == 'week'
+        start = start.next_day
+      elsif params[:interval] == 'day'
+        start = start + 1.day
+      elsif params[:interval] == 'hour'
+        start = start + 1.hour
+      elsif params[:interval] == 'minute'
+        start = start + 1.minute
+      end
+    }
+    result
+  end
+
+=begin
+
+  result = Report::TicketGenericTime.items(
+    range_start: '2015-01-01T00:00:00Z',
+    range_end:   '2015-12-31T23:59:59Z',
+    selector:    selector, # ticket selector to get only a collection of tickets
+    params:      { field: 'created_at' },
+  )
+
+returns
+
+  {
+    count: 123,
+    ticket_ids: [4,5,1,5,0,51,5,56,7,4],
+    assets: assets,
+  }
+
+=end
+
+  def self.items(params)
+
+    aggs_interval = {
+      from: params[:range_start],
+      to: params[:range_end],
+      field: params[:params][:field],
+    }
+
+    result = SearchIndexBackend.selectors(['Ticket'], params[:selector], nil, nil, aggs_interval)
+    assets = {}
+    result[:ticket_ids].each {|ticket_id|
+      ticket_full = Ticket.find(ticket_id)
+      assets = ticket_full.assets(assets)
+    }
+    result[:assets] = assets
+    result
+  end
+
+end

Some files were not shown because too many files changed in this diff