Browse Source

Moved to new table api.

Martin Edenhofer 10 years ago
parent
commit
e417001822

+ 6 - 12
app/assets/javascripts/app/controllers/_application_controller_generic.js.coffee

@@ -71,7 +71,7 @@ class App.ControllerGenericEdit extends App.ControllerModal
 
   submit: (e) ->
     e.preventDefault()
-    params = @formParam(e.target) 
+    params = @formParam(e.target)
     @item.load(params)
 
     # validate
@@ -169,20 +169,14 @@ class App.ControllerGenericIndex extends App.Controller
       objects:    objects
       overview:   overview
       attributes: attributes
-      groupBy:    'state'
+      bindRow:
+        events:
+          'click': @edit
     )
 
-    binds = {}
-    for item in attributes
-      if item.dataType
-        if !binds[item.dataType]
-          callback = item.callback || @edit
-          @el.on( 'click', "[data-type=#{item.dataType}]", callback )
-          binds[item.dataType] = true
-
-  edit: (e) =>
+  edit: (id, e) =>
     e.preventDefault()
-    item = $(e.target).item( App[ @genericObject ] )
+    item = App[ @genericObject ].find(id)
 
     if @editCallback
       @editCallback(item)

+ 169 - 116
app/assets/javascripts/app/controllers/_application_controller_table.js.coffee

@@ -4,48 +4,64 @@ class App.ControllerTable extends App.Controller
       @[key] = value
 
     @table = @tableGen(params)
+
     if @el
       @el.append( @table )
 
   ###
 
+    # table simple based on model
+
+    rowClick = -> (id, e)
+      e.preventDefault()
+      console.log('rowClick', id)
+    rowMouseover = -> (id, e)
+      e.preventDefault()
+      console.log('rowMouseover', id)
+    rowMouseout = -> (id, e)
+      e.preventDefault()
+      console.log('rowMouseout', id)
+    rowDblClick = -> (id, e)
+      e.preventDefault()
+      console.log('rowDblClick', id)
+
+    colClick = -> (id, e)
+      e.preventDefault()
+      console.log('colClick', e.target)
+
+    checkboxClick = -> (id, e)
+      e.preventDefault()
+      console.log('checkboxClick', e.target)
+
     new App.ControllerTable(
-      header:   ['Host', 'User', 'Adapter', 'Active']
       overview: ['host', 'user', 'adapter', 'active']
       model:    App.Channel
       objects:  data
+      groupBy:  'group'
       checkbox: false
       radio:    false
+      bindRow:
+        events:
+          'click':      rowClick
+          'mouseover':  rowMouseover
+          'mouseout':   rowMouseout
+          'dblclick':   rowDblClick
+      bindCol:
+        host:
+          events:
+            'click': colEvent
+      bindCheckbox:
+        events:
+          'click':      rowClick
+          'mouseover':  rowMouseover
+          'mouseout':   rowMouseout
+          'dblclick':   rowDblClick
     )
-
-    new App.ControllerTable(
-      overview_extended: [
-        { name: 'number',                 link: true }
-        { name: 'title',                  link: true }
-        { name: 'customer',               class: 'user-popover', data: { id: true } }
-        { name: 'state',                  translate: true }
-        { name: 'priority',               translate: true }
-        { name: 'group' },
-        { name: 'owner',                  class: 'user-popover', data: { id: true } }
-        { name: 'created_at',             callback: @frontendTime }
-        { name: 'last_contact',           callback: @frontendTime }
-        { name: 'last_contact_agent',     callback: @frontendTime }
-        { name: 'last_contact_customer',  callback: @frontendTime }
-        { name: 'first_response',         callback: @frontendTime }
-        { name: 'close_time',             callback: @frontendTime }
-      ],
-      model:    App.Ticket
-      objects:  tickets
-      checkbox: false
-      radio:    false
-    )
-
   ###
 
   tableGen: (data) ->
     overview   = data.overview || data.model.configure_overview || []
     attributes = data.attributes || data.model.configure_attributes || {}
-    header     = data.header
     destroy    = data.model.configure_delete
 
     # check if table is empty
@@ -53,108 +69,132 @@ class App.ControllerTable extends App.Controller
       table = '<p>-' + App.i18n.translateContent( 'none' ) + '-</p>'
       return $(table)
 
-    # define table header
-    if header
-      header_new = []
-      for key in header
-        header_new.push {
-          display: key
-        }
-      header = header_new
-    else if !data.overview_extended
-      header = []
-      for row in overview
-        found = false
-        if attributes
-          for attribute in attributes
-            if row is attribute.name
-              found = true
-              header.push attribute
-            else
-              rowWithoutId = row + '_id'
-              if rowWithoutId is attribute.name
-                found = true
-                header.push attribute
-        if !found
-          header.push {
-            name:    row
-            display: row
-          }
-
-    # collect data of col. types
-    dataTypesForCols = []
-    for row in overview
-
-      if !_.isEmpty(attributes)
-        for attribute in attributes
-          found = false
-          if row is attribute.name
-            found = true
-            dataTypesAttribute = _.clone(attribute)
-          else if row + '_id' is attribute.name
-            found = true
-            dataTypesAttribute = _.clone(attribute)
-            dataTypesAttribute['name'] = row
-          if found
-            dataTypesAttribute['type'] = 'link'
-            if !dataTypesAttribute['dataType']
-              dataTypesAttribute['dataType'] = 'edit'
-            dataTypesForCols.push dataTypesAttribute
-      else
-        dataTypesForCols.push {
-          name: row
-          type: 'link'
-          dataType: 'edit'
-        }
-
-    # extended table format
-    if data.overview_extended
-      if !header
-        header = []
-        for row in data.overview_extended
-          for attribute in attributes
-            if row.name is attribute.name
-              header.push attribute
-            else
-              rowWithoutId = row.name + '_id'
-              if rowWithoutId is attribute.name
-                header.push attribute
-
-      dataTypesForCols = data.overview_extended
-
-    # generate content data
-    for object in data.objects
-
-      # check if info for each col. is already there
-      for row in dataTypesForCols
-
-        # lookup relation
-        if !object[row.name]
-          rowWithoutId = row.name + '_id'
-          for attribute in attributes
-            if rowWithoutId is attribute.name
-              if attribute.relation && App[ attribute.relation ]
-                if App[ attribute.relation ].exists( object[rowWithoutId] )
-                  record = App[ attribute.relation ].find( object[rowWithoutId] )
-                  object[row.name] = record.name
-
-    @log 'debug', 'table', 'header', header, 'overview', dataTypesForCols, 'objects', data.objects
+    # group by
+    if data.groupBy
+
+      # remove group by attribute from header
+      overview = _.filter(
+        overview
+        (item) =>
+          return item if item isnt data.groupBy
+          return
+      )
+
+      # get new order
+      groupObjects = _.groupBy(
+        data.objects
+        (item) =>
+          return '' if !item[data.groupBy]
+          return item[data.groupBy].displayName() if item[data.groupBy].displayName
+          item[data.groupBy]
+      )
+      groupOrder = []
+      for group, value of groupObjects
+        groupOrder.push group
+
+      # sort new groups
+      groupOrder = _.sortBy(
+        groupOrder
+        (item) =>
+          item
+      )
+
+      # create new data array
+      data.objects = []
+      for group in groupOrder
+        data.objects = data.objects.concat groupObjects[group]
+        groupObjects[group] = [] # release old array
+
+    # get header data
+    header = []
+    for item in overview
+      headerFound = false
+      for attribute in attributes
+        if attribute.name is item
+          headerFound = true
+          header.push attribute
+        else
+          rowWithoutId = item + '_id'
+          if attribute.name is rowWithoutId
+            headerFound = true
+            header.push attribute
+
+    # get content
+    @log 'debug', 'table', 'header', header, 'overview', 'objects', data.objects
     table = App.view('generic/table')(
       header:   header
-      overview: dataTypesForCols
       objects:  data.objects
       checkbox: data.checkbox
       radio:    data.radio
       groupBy:  data.groupBy
       destroy:  destroy
+      callbacks: data.callbackAttributes
     )
 
     # convert to jquery object
     table = $(table)
 
+    cursorMap =
+      click:    'pointer'
+      dblclick: 'pointer'
+      #mouseover: 'alias'
+
+    # bind col.
+    if data.bindCol
+      for name, item of data.bindCol
+        if item.events
+          position = 0
+          if data.checkbox
+            position += 1
+          hit      = false
+
+          for headerName in header
+            if !hit
+              position += 1
+            if headerName.name is name || headerName.name is "#{name}_id"
+              hit = true
+
+          if hit
+            for event, callback of item.events
+              do (table, event, callback) =>
+                if cursorMap[event]
+                  table.find("tbody > tr > td:nth-child(#{position}) > span").css( 'cursor', cursorMap[event] )
+                table.on( event, "tbody > tr > td:nth-child(#{position}) > span",
+                  (e) =>
+                    e.stopPropagation()
+                    id = $(e.target).parents('tr').data('id')
+                    callback(id, e)
+                )
+
+    # bind row
+    if data.bindRow
+      if data.bindRow.events
+        for event, callback of data.bindRow.events
+          do (table, event, callback) =>
+            if cursorMap[event]
+              table.find('tbody > tr').css( 'cursor', cursorMap[event] )
+            table.on( event, 'tbody > tr',
+              (e) =>
+                id = $(e.target).parents('tr').data('id')
+                callback(id, e)
+            )
+
+    # bind bindCheckbox
+    if data.bindCheckbox
+      if data.bindCheckbox.events
+        for event, callback of data.bindCheckbox.events
+          do (table, event, callback) =>
+            table.delegate('input[name="bulk"]', event, (e) ->
+              e.stopPropagation()
+              id      = $(e.target).parents('tr').data('id')
+              checked = $(e.target).prop('checked')
+              callback(id, checked, e)
+            )
+
     # bind on delete dialog
     if data.model && destroy
       table.delegate('[data-type="destroy"]', 'click', (e) ->
+        e.stopPropagation()
         e.preventDefault()
         itemId = $(e.target).parents('tr').data('id')
         item   = data.model.find(itemId)
@@ -165,11 +205,24 @@ class App.ControllerTable extends App.Controller
 
     # enable checkbox bulk selection
     if data.checkbox
-      table.delegate('[name="bulk_all"]', 'click', (e) ->
+      table.delegate('input[name="bulk_all"]', 'click', (e) ->
+        e.stopPropagation()
         if $(e.target).prop('checked')
-          $(e.target).parents().find('[name="bulk"]').prop( 'checked', true );
+          $(e.target).parents('table').find('[name="bulk"]').each( ->
+            if !$(@).prop('checked')
+              #$(@).prop('checked', true)
+              $(@).trigger('click')
+          )
         else
-          $(e.target).parents().find('[name="bulk"]').prop( 'checked', false );
+          $(e.target).parents('table').find('[name="bulk"]').each( ->
+            if $(@).prop('checked')
+              #$(@).prop('checked', false)
+              $(@).trigger('click')
+          )
       )
 
-    return table
+    time = =>
+      @frontendTimeUpdate()
+    @delay(time, 80)
+
+    table

+ 36 - 72
app/assets/javascripts/app/controllers/_channel/email.js.coffee

@@ -1,8 +1,3 @@
-$.fn.item = (genericObject) ->
-  elementID   = $(@).data('id')
-  elementID or= $(@).parents('[data-id]').data('id')
-  genericObject.find(elementID)
-
 class App.ChannelEmail extends App.ControllerTabs
   constructor: ->
     super
@@ -46,7 +41,6 @@ class App.ChannelEmail extends App.ControllerTabs
 class App.ChannelEmailFilter extends App.Controller
   events:
     'click [data-type=new]':  'new'
-    'click [data-type=edit]': 'edit'
 
   constructor: ->
     super
@@ -59,9 +53,12 @@ class App.ChannelEmailFilter extends App.Controller
     template = $( '<div><div class="overview"></div><a data-type="new" class="btn btn-default">' + App.i18n.translateContent('New') + '</a></div>' )
 
     new App.ControllerTable(
-      el:       template.find('.overview'),
-      model:    App.PostmasterFilter,
-      objects:  data,
+      el:       template.find('.overview')
+      model:    App.PostmasterFilter
+      objects:  data
+      bindRow:
+        events:
+          'click': @edit
     )
     @html template
 
@@ -69,10 +66,9 @@ class App.ChannelEmailFilter extends App.Controller
     e.preventDefault()
     new App.ChannelEmailFilterEdit( {} )
 
-  edit: (e) =>
+  edit: (id, e) =>
     e.preventDefault()
-    item = $(e.target).item( App.PostmasterFilter )
-    new App.ChannelEmailFilterEdit( object: item )
+    new App.ChannelEmailFilterEdit( object: App.PostmasterFilter.find(id) )
 
 class App.ChannelEmailFilterEdit extends App.ControllerModal
   constructor: ->
@@ -135,7 +131,6 @@ class App.ChannelEmailFilterEdit extends App.ControllerModal
 class App.ChannelEmailAddress extends App.Controller
   events:
     'click [data-type=new]':  'new'
-    'click [data-type=edit]': 'edit'
 
   constructor: ->
     super
@@ -151,6 +146,9 @@ class App.ChannelEmailAddress extends App.Controller
       el:       template.find('.overview')
       model:    App.EmailAddress
       objects:  data
+      bindRow:
+        events:
+          'click': @edit
     )
 
     @html template
@@ -159,9 +157,9 @@ class App.ChannelEmailAddress extends App.Controller
     e.preventDefault()
     new App.ChannelEmailAddressEdit( {} )
 
-  edit: (e) =>
+  edit: (id, e) =>
     e.preventDefault()
-    item = $(e.target).item( App.EmailAddress )
+    item = App.EmailAddress.find(id)
     new App.ChannelEmailAddressEdit( object: item )
 
 class App.ChannelEmailAddressEdit extends App.ControllerModal
@@ -223,7 +221,6 @@ class App.ChannelEmailAddressEdit extends App.ControllerModal
 class App.ChannelEmailSignature extends App.Controller
   events:
     'click [data-type=new]':  'new'
-    'click [data-type=edit]': 'edit'
 
   constructor: ->
     super
@@ -238,17 +235,19 @@ class App.ChannelEmailSignature extends App.Controller
       el:       template.find('.overview')
       model:    App.Signature
       objects:  data
+      bindRow:
+        events:
+          'click': @edit
     )
-
     @html template
 
   new: (e) =>
     e.preventDefault()
     new App.ChannelEmailSignatureEdit( {} )
 
-  edit: (e) =>
+  edit: (id, e) =>
     e.preventDefault()
-    item = $(e.target).item( App.Signature )
+    item = App.Signature.find(id)
     new App.ChannelEmailSignatureEdit( object: item )
 
 class App.ChannelEmailSignatureEdit extends App.ControllerModal
@@ -310,31 +309,23 @@ class App.ChannelEmailSignatureEdit extends App.ControllerModal
 class App.ChannelEmailInbound extends App.Controller
   events:
     'click [data-type=new]':  'new'
-    'click [data-type=edit]': 'edit'
 
   constructor: ->
     super
-
     App.Channel.subscribe( @render, initFetch: true )
 
   render: =>
-    channels = App.Channel.all()
-
-    data = []
-    for channel in channels
-      if channel.area is 'Email::Inbound'
-        channel.host = channel.options['host']
-        channel.user = channel.options['user']
-        data.push channel
+    channels = App.Channel.search( filter: { area: 'Email::Inbound' } )
 
     template = $( '<div><div class="overview"></div><a data-type="new" class="btn btn-default">' + App.i18n.translateContent('New') + '</a></div>' )
 
     new App.ControllerTable(
-      el:       template.find('.overview'),
-      header:   ['Host', 'User', 'Adapter', 'Active'],
-      overview: ['host', 'user', 'adapter', 'active'],
-      model:    App.Channel,
-      objects:  data,
+      el:       template.find('.overview')
+      model:    App.Channel
+      objects:  channels
+      bindRow:
+        events:
+          'click': @edit
     )
     @html template
 
@@ -342,9 +333,9 @@ class App.ChannelEmailInbound extends App.Controller
     e.preventDefault()
     new App.ChannelEmailInboundEdit( {} )
 
-  edit: (e) =>
+  edit: (id, e) =>
     e.preventDefault()
-    item = $(e.target).item( App.Channel )
+    item = App.Channel.find(id)
     new App.ChannelEmailInboundEdit( object: item )
 
 
@@ -354,29 +345,13 @@ class App.ChannelEmailInboundEdit extends App.ControllerModal
     @render(@object)
 
   render: (data = {}) ->
-
-    if !data['options']
-      data['options']        = {}
-      data['options']['ssl'] = true
-      data['active']         = true
-
-    configure_attributes = [
-      { name: 'adapter',  display: 'Type',     tag: 'select',   multiple: false, null: false, options: { IMAP: 'IMAP', POP3: 'POP3' } , class: 'span4', default: data['adapter'] },
-      { name: 'host',     display: 'Host',     tag: 'input',    type: 'text', limit: 120, null: false, class: 'span4', autocapitalize: false, default: (data['options']&&data['options']['host']) },
-      { name: 'user',     display: 'User',     tag: 'input',    type: 'text', limit: 120, null: false, class: 'span4', autocapitalize: false, default: (data['options']&&data['options']['user']) },
-      { name: 'password', display: 'Password', tag: 'input',    type: 'password', limit: 120, null: false, class: 'span4', autocapitalize: false, default: (data['options']&&data['options']['password']) },
-      { name: 'ssl',      display: 'SSL',      tag: 'select',   multiple: false, null: false, options: { true: 'yes', false: 'no' }, translate: true, class: 'span4', default: (data['options']&&data['options']['ssl']) },
-      { name: 'folder',   display: 'Folder',   tag: 'input',    type: 'text', limit: 120, null: true, class: 'span4', autocapitalize: false, default: (data['options']&&data['options']['folder']) },
-      { name: 'group_id', display: 'Group',    tag: 'select',   multiple: false, null: false, filter: @edit_form, nulloption: false, relation: 'Group', class: 'span4', default: data['group_id']  },
-      { name: 'active',   display: 'Active',   tag: 'select',   multiple: false, null: false, options: { true: 'yes', false: 'no' } , translate: true, class: 'span4', default: data['active'] },
-    ]
     if @object
       @html App.view('generic/admin/edit')(
         head: 'Email Channel'
       )
       @form = new App.ControllerForm(
         el: @el.find('#object_edit')
-        model: { configure_attributes: configure_attributes, className: '' }
+        model: App.Channel
         autofocus: true
       )
     else
@@ -385,7 +360,7 @@ class App.ChannelEmailInboundEdit extends App.ControllerModal
       )
       @form = new App.ControllerForm(
         el: @el.find('#object_new')
-        model: { configure_attributes: configure_attributes, className: '' }
+        model: App.Channel
         autofocus: true
       )
     @modalShow()
@@ -395,21 +370,10 @@ class App.ChannelEmailInboundEdit extends App.ControllerModal
 
     # get params
     params = @formParam(e.target)
+    params['area'] = 'Email::Inbound'
 
     object = @object || new App.Channel
-    object.load(
-      area:    'Email::Inbound'
-      adapter:  params['adapter']
-      group_id: params['group_id']
-      options: {
-        host:     params['host']
-        user:     params['user']
-        password: params['password']
-        ssl:      params['ssl']
-        folder:   params['folder']
-      },
-      active: params['active']
-    )
+    object.load(params)
 
     # validate form
     errors = @form.validate( params )
@@ -463,7 +427,7 @@ class App.ChannelEmailOutbound extends App.Controller
             channel_used = channel
 
     configure_attributes = [
-      { name: 'adapter', display: 'Send Mails via', tag: 'select', multiple: false, null: false, options: adapters , class: 'span4', default: adapter_used },
+      { name: 'adapter', display: 'Send Mails via', tag: 'select', multiple: false, null: false, options: adapters , default: adapter_used },
     ]
     new App.ControllerForm(
       el: @el.find('#form-email-adapter'),
@@ -476,10 +440,10 @@ class App.ChannelEmailOutbound extends App.Controller
 
     if adapter_used is 'SMTP'
       configure_attributes = [
-        { name: 'host',     display: 'Host',     tag: 'input',    type: 'text', limit: 120, null: false, class: 'span4', autocapitalize: false, default: (channel_used['options']&&channel_used['options']['host']) },
-        { name: 'user',     display: 'User',     tag: 'input',    type: 'text', limit: 120, null: true, class: 'span4', autocapitalize: false, default: (channel_used['options']&&channel_used['options']['user']) },
-        { name: 'password', display: 'Password', tag: 'input',    type: 'password', limit: 120, null: true, class: 'span4', autocapitalize: false, default: (channel_used['options']&&channel_used['options']['password']) },
-        { name: 'ssl',      display: 'SSL',      tag: 'select',   multiple: false, null: false, options: { true: 'yes', false: 'no' } , class: 'span4', translate: true, default: (channel_used['options']&&channel_used['options']['ssl']) },
+        { name: 'host',     display: 'Host',     tag: 'input',    type: 'text', limit: 120, null: false, autocapitalize: false, default: (channel_used['options']&&channel_used['options']['host']) },
+        { name: 'user',     display: 'User',     tag: 'input',    type: 'text', limit: 120, null: true, autocapitalize: false, default: (channel_used['options']&&channel_used['options']['user']) },
+        { name: 'password', display: 'Password', tag: 'input',    type: 'password', limit: 120, null: true, autocapitalize: false, default: (channel_used['options']&&channel_used['options']['password']) },
+        { name: 'ssl',      display: 'SSL',      tag: 'select',   multiple: false, null: false, options: { true: 'yes', false: 'no' } , translate: true, default: (channel_used['options']&&channel_used['options']['ssl']) },
         { name: 'port',     display: 'Port',     tag: 'input',    type: 'text', limit: 5, null: false, class: 'span1', autocapitalize: false, default: ((channel_used['options']&&channel_used['options']['port']) || 25) },
       ]
       @form = new App.ControllerForm(

+ 39 - 148
app/assets/javascripts/app/controllers/_dashboard/ticket.js.coffee

@@ -1,6 +1,5 @@
 class App.DashboardTicket extends App.Controller
   events:
-    'click [data-type=edit]':     'zoom'
     'click [data-type=settings]': 'settings'
     'click [data-type=page]':     'page'
 
@@ -18,11 +17,11 @@ class App.DashboardTicket extends App.Controller
     # render
     @fetch()
 
-  fetch: =>
+  fetch: (force) =>
 
     # use cache of first page
     cache = App.Store.get( @key )
-    if cache
+    if !force && cache
       @load( cache )
 
     # init fetch via ajax, all other updates on time via websockets
@@ -55,12 +54,13 @@ class App.DashboardTicket extends App.Controller
     App.Overview.unbind('local:rerender')
     App.Overview.bind 'local:rerender', (record) =>
       @log 'notice', 'rerender...', record
+      data.overview = record
       @render(data)
 
     App.Overview.unbind('local:refetch')
     App.Overview.bind 'local:refetch', (record) =>
       @log 'notice', 'refetch...', record
-      @fetch()
+      @fetch(true)
 
     @render( data )
 
@@ -72,8 +72,8 @@ class App.DashboardTicket extends App.Controller
     @overview      = data.overview
     @tickets_count = data.tickets_count
     @ticket_ids    = data.ticket_ids
-    # FIXME 10
-    pages_total =  parseInt( ( @tickets_count / 10 ) + 0.99999 ) || 1
+    per_page    = @overview.view.per_page || 10
+    pages_total =  parseInt( ( @tickets_count / per_page ) + 0.99999 ) || 1
     html = App.view('dashboard/ticket')(
       overview:    @overview,
       pages_total: pages_total,
@@ -92,13 +92,39 @@ class App.DashboardTicket extends App.Controller
       if @ticket_ids[ i - 1 ]
         @tickets_in_table.push App.Ticket.retrieve( @ticket_ids[ i - 1 ] )
 
-    shown_all_attributes = @ticketTableAttributes( App.Overview.find(@overview.id).view.d )
+    openTicket = (id,e) =>
+      ticket = App.Ticket.retrieve(id)
+      @navigate ticket.uiUrl()
+    callbackTicketTitleAdd = (value, object, attribute, attributes, refObject) =>
+      attribute.title = object.title
+    callbackLinkToTicket = (value, object, attribute, attributes, refObject) =>
+      attribute.link = object.uiUrl()
+    callbackResetLink = (value, object, attribute, attributes, refObject) =>
+      attribute.link = undefined
+    callbackUserPopover = (value, object, attribute, attributes, refObject) =>
+      attribute.class = 'user-popover'
+      attribute.data =
+        id: refObject.id
+
     new App.ControllerTable(
+      overview:          @overview.view.d
       el:                html.find('.table-overview'),
-      overview_extended: shown_all_attributes,
-      model:             App.Ticket,
+      model:             App.Ticket
       objects:           @tickets_in_table,
-      checkbox:          false,
+      checkbox:          false
+      groupBy:           @overview.group_by
+      bindRow:
+        events:
+          'click': openTicket
+      callbackAttributes:
+        customer_id:
+          [ callbackResetLink, callbackUserPopover ]
+        owner_id:
+          [ callbackResetLink, callbackUserPopover ]
+        title:
+          [ callbackLinkToTicket, callbackTicketTitleAdd ]
+        number:
+          [ callbackLinkToTicket, callbackTicketTitleAdd ]
     )
 
     @html html
@@ -122,8 +148,9 @@ class App.DashboardTicket extends App.Controller
 
   settings: (e) =>
     e.preventDefault()
-    new Settings(
-      overview: App.Overview.find(@overview.id)
+    new App.OverviewSettings(
+      overview_id: @overview.id
+      view_mode:   'd'
     )
 
   page: (e) =>
@@ -132,139 +159,3 @@ class App.DashboardTicket extends App.Controller
     @start_page = id
     @fetch()
 
-class Settings extends App.ControllerModal
-  constructor: ->
-    super
-    @render()
-
-  render: ->
-
-    @html App.view('dashboard/ticket_settings')(
-      overview: @overview,
-    )
-    @configure_attributes_article = [
-#      { name: 'from',      display: 'From',     tag: 'input',    type: 'text', limit: 100, null: false, class: 'span8',  },
-#      { name: 'to',        display: 'To',          tag: 'input',    type: 'text', limit: 100, null: true, class: 'span7', item_class: 'hide' },
-#      { name: 'type_id',   display: 'Type',        tag: 'select',   multiple: false, null: true, relation: 'TicketArticleType', default: '9', class: 'medium', item_class: 'pull-left' },
-#      { name: 'internal',  display: 'Visibility',  tag: 'radio',  default: false,  null: true, options: { true: 'internal', false: 'public' }, class: 'medium', item_class: 'pull-left' },
-      {
-        name:     'per_page',
-        display:  'Items per page',
-        tag:      'select',
-        multiple: false,
-        null:     false,
-#        default: @overview.view.d.per_page,
-        options: {
-          5: 5,
-          10: 10,
-          15: 15,
-          20: 20,
-        },
-        class: 'medium',
-#        item_class: 'pull-left',
-      },
-      {
-        name:    'attributes',
-        display: 'Attributes',
-        tag:     'checkbox',
-        default: @overview.view.d,
-        null:    false,
-        translate:  true
-        options: {
-          number:                 'Number'
-          title:                  'Title'
-          customer:               'Customer'
-          state:                  'State'
-          priority:               'Priority'
-          group:                  'Group'
-          owner:                  'Owner'
-          created_at:             'Age'
-          last_contact:           'Last Contact'
-          last_contact_agent:     'Last Contact Agent'
-          last_contact_customer:  'Last Contact Customer'
-          first_response:         'First Response'
-          close_time:             'Close Time'
-          escalation_time:        'Escalation in'
-          article_count:          'Article Count'
-        },
-        class:      'medium',
-#        item_class: 'pull-left',
-      },
-      {
-        name:    'order_by',
-        display: 'Order',
-        tag:     'select',
-        default: @overview.order.by,
-        null:    false,
-        translate:  true
-        options: {
-          number:                 'Number'
-          title:                  'Title'
-          customer:               'Customer'
-          state:                  'State'
-          priority:               'Priority'
-          group:                  'Group'
-          owner:                  'Owner'
-          created_at:             'Age'
-          last_contact:           'Last Contact'
-          last_contact_agent:     'Last Contact Agent'
-          last_contact_customer:  'Last Contact Customer'
-          first_response:         'First Response'
-          close_time:             'Close Time'
-          escalation_time:        'Escalation in'
-          article_count:          'Article Count'
-        },
-        class:      'medium',
-      },
-      {
-        name:    'order_by_direction',
-        display: 'Direction',
-        tag:     'select',
-        default: @overview.order.direction,
-        null:    false,
-        translate: true
-        options: {
-          ASC:   'up',
-          DESC:  'down',
-        },
-        class:      'medium',
-      },
-    ]
-
-    new App.ControllerForm(
-      el: @el.find('#form-setting'),
-      model: { configure_attributes: @configure_attributes_article },
-      autofocus: false,
-    )
-
-    @modalShow()
-
-  submit: (e) =>
-    e.preventDefault()
-    params = @formParam(e.target)
-
-    # check if refetch is needed
-    @reload_needed = 0
-    if @overview.view['d']['per_page'] isnt params['per_page']
-      @overview.view['d']['per_page'] = params['per_page']
-      @reload_needed = 1
-
-    if @overview.order['by'] isnt params['order_by']
-      @overview.order['by'] = params['order_by']
-      @reload_needed = 1
-
-    if @overview.order['direction'] isnt params['order_by_direction']
-      @overview.order['direction'] = params['order_by_direction']
-      @reload_needed = 1
-
-    @overview.view['d'] = params['attributes']
-
-    @overview.save(
-      done: =>
-        if @reload_needed
-          @overview.trigger('local:refetch')
-        else
-          @overview.trigger('local:rerender')
-    )
-
-    @modalHide()

+ 215 - 152
app/assets/javascripts/app/controllers/ticket_overview.js.coffee

@@ -1,7 +1,6 @@
 class Index extends App.Controller
   constructor: ->
     super
-
     @render()
 
   render: ->
@@ -55,11 +54,11 @@ class Table extends App.ControllerContent
     # render
     @fetch()
 
-  fetch: =>
+  fetch: (force) =>
 
     # use cache of first page
     cache = App.Store.get( @key )
-    if cache
+    if !force && cache
       @load(cache)
 
     # init fetch via ajax, all other updates on time via websockets
@@ -106,7 +105,7 @@ class Table extends App.ControllerContent
     App.Overview.unbind('local:refetch')
     App.Overview.bind 'local:refetch', (record) =>
       @log 'notice', 'refetch...', record
-      @fetch()
+      @fetch(true)
 
     @ticket_list_show = []
     for ticket_id in @ticket_ids
@@ -123,6 +122,8 @@ class Table extends App.ControllerContent
 
   render: ->
 
+
+
     # if customer and no ticket exists, show the following message only
     if !@ticket_list_show[0] && @isRole('Customer')
       @html App.view('customer_not_ticket_exists')()
@@ -131,6 +132,7 @@ class Table extends App.ControllerContent
     @selected = @bulkGetSelected()
 
     # set page title
+    @overview = App.Overview.find( @overview.id )
     @title @overview.name
 
     # render init page
@@ -182,27 +184,51 @@ class Table extends App.ControllerContent
       )
       @el.find('.table-overview').append(table)
     else
-      shown_all_attributes = @ticketTableAttributes( App.Overview.find( @overview.id ).view.s )
-      groupBy = undefined
-      if @overview.group_by
-        group_by =
-          name: @overview.group_by
-          id:   @overview.group_by + '_id'
-
-        # remove group by attribute from show attributes list
-        shown_all_attributes = _.filter(
-          shown_all_attributes
-          (item) =>
-            return item if item.name isnt @overview.group_by
-            return
-        )
+      openTicket = (id,e) =>
+        ticket = App.Ticket.retrieve(id)
+        @navigate ticket.uiUrl()
+      callbackTicketTitleAdd = (value, object, attribute, attributes, refObject) =>
+        attribute.title = object.title
+      callbackLinkToTicket = (value, object, attribute, attributes, refObject) =>
+        attribute.link = object.uiUrl()
+      callbackResetLink = (value, object, attribute, attributes, refObject) =>
+        attribute.link = undefined
+      callbackUserPopover = (value, object, attribute, attributes, refObject) =>
+        attribute.class = 'user-popover'
+        attribute.data =
+          id: refObject.id
+      callbackCheckbox = (id, checked, e) =>
+        if @el.find('table').find('input[name="bulk"]:checked').length == 0
+          @el.find('.bulk-action').addClass('hide')
+        else
+          @el.find('.bulk-action').removeClass('hide')
+
       new App.ControllerTable(
-        el:                @el.find('.table-overview')
-        overview_extended: shown_all_attributes
-        model:             App.Ticket
-        objects:           @ticket_list_show
-        checkbox:          checkbox
-        groupBy:           group_by
+        overview:     @overview.view.s
+        el:           @el.find('.table-overview')
+        model:        App.Ticket
+        objects:      @ticket_list_show
+        checkbox:     checkbox
+        groupBy:      @overview.group_by
+        bindRow:
+          events:
+            'click':  openTicket
+        #bindCol:
+        #  customer_id:
+        #    events:
+        #      'mouseover': popOver
+        callbackAttributes:
+          customer_id:
+            [ callbackResetLink, callbackUserPopover ]
+          owner_id:
+            [ callbackResetLink, callbackUserPopover ]
+          title:
+            [ callbackLinkToTicket, callbackTicketTitleAdd ]
+          number:
+            [ callbackLinkToTicket, callbackTicketTitleAdd ]
+        bindCheckbox:
+          events:
+            'click':  callbackCheckbox
       )
 
     @bulkSetSelected( @selected )
@@ -215,12 +241,13 @@ class Table extends App.ControllerContent
 
     # start bulk action observ
     @el.find('.bulk-action').append( @bulk_form() )
-    if @el.find('.table-overview').find('[name="bulk"]:checked').length isnt 0
+    if @el.find('.table-overview').find('input[name="bulk"]:checked').length isnt 0
         @el.find('.bulk-action').removeClass('hide')
 
     # show/hide bulk action
-    @el.find('.table-overview').delegate('[name="bulk"], [name="bulk_all"]', 'click', (e) =>
-      if @el.find('.table-overview').find('[name="bulk"]:checked').length == 0
+    @el.find('.table-overview').delegate('input[name="bulk"], input[name="bulk_all"]', 'click', (e) =>
+      console.log('YES')
+      if @el.find('.table-overview').find('input[name="bulk"]:checked').length == 0
 
         # hide
         @el.find('.bulk-action').addClass('hide')
@@ -324,6 +351,7 @@ class Table extends App.ControllerContent
             @fetch()
       )
     )
+    @el.find('.table-overview').find('[name="bulk"]:checked').prop('checked', false)
     App.Event.trigger 'notify', {
       type: 'success'
       msg: App.i18n.translateContent('Bulk-Action executed!')
@@ -343,14 +371,15 @@ class Table extends App.ControllerContent
 
   settings: (e) =>
     e.preventDefault()
-    new Settings(
-      overview: App.Overview.find(@overview.id),
-      view_mode: @view_mode,
+    new App.OverviewSettings(
+      overview_id: @overview.id
+      view_mode:   @view_mode
     )
 
-class Settings extends App.ControllerModal
+class App.OverviewSettings extends App.ControllerModal
   constructor: ->
     super
+    @overview = App.Overview.find(@overview_id)
     @render()
 
   render: ->
@@ -358,119 +387,154 @@ class Settings extends App.ControllerModal
     @html App.view('dashboard/ticket_settings')(
       overview: @overview,
     )
-    @configure_attributes_article = [
-#      { name: 'from',        display: 'From',     tag: 'input',    type: 'text', limit: 100, null: false, class: 'span8',  },
-#      { name: 'to',          display: 'To',          tag: 'input',    type: 'text', limit: 100, null: true, class: 'span7', item_class: 'hide' },
-#      { name: 'type_id',     display: 'Type',        tag: 'select',   multiple: false, null: true, relation: 'TicketArticleType', default: '9', class: 'medium', item_class: 'pull-left' },
-#      { name: 'internal',    display: 'Visibility',  tag: 'radio',  default: false,  null: true, options: { true: 'internal', false: 'public' }, class: 'medium', item_class: 'pull-left' },
-      {
-        name:     'per_page'
-        display:  'Items per page'
-        tag:      'select'
-        multiple: false
-        null:     false
-#        default: @overview.view[@view_mode].per_page
-        options:
-          15: 15
-          20: 20
-          25: 25
-          30: 30
-          35: 35
-        class: 'medium'
-#        item_class: 'pull-left'
-      },
-      {
-        name:    'attributes'
-        display: 'Attributes'
-        tag:     'checkbox'
-        default: @overview.view[@view_mode]
-        null:    false
-        translate: true
-        options:
-#          true:  'internal'
-#          false: 'public'
-          number:                 'Number'
-          title:                  'Title'
-          customer:               'Customer'
-          state:                  'State'
-          priority:               'Priority'
-          group:                  'Group'
-          owner:                  'Owner'
-          created_at:             'Age'
-          last_contact:           'Last Contact'
-          last_contact_agent:     'Last Contact Agent'
-          last_contact_customer:  'Last Contact Customer'
-          first_response:         'First Response'
-          close_time:             'Close Time'
-          escalation_time:        'Escalation in'
-          article_count:          'Article Count'
-        class:      'medium'
-      },
-      {
-        name:    'order_by'
-        display: 'Order'
-        tag:     'select'
-        default: @overview.order.by
-        null:    false
-        translate: true
-        options:
-          number:                 'Number'
-          title:                  'Title'
-          customer:               'Customer'
-          state:                  'State'
-          priority:               'Priority'
-          group:                  'Group'
-          owner:                  'Owner'
-          created_at:             'Age'
-          last_contact:           'Last Contact'
-          last_contact_agent:     'Last Contact Agent'
-          last_contact_customer:  'Last Contact Customer'
-          first_response:         'First Response'
-          close_time:             'Close Time'
-          escalation_time:        'Escalation in'
-          article_count:          'Article Count'
-        class:   'medium'
-      },
-      {
-        name:    'order_by_direction'
-        display: 'Direction'
-        tag:     'select'
-        default: @overview.order.direction
-        null:    false
-        translate: true
-        options:
-          ASC:   'up'
-          DESC:  'down'
-        class:   'medium'
-      },
-      {
-        name:    'group_by'
-        display: 'Group by'
-        tag:     'select'
-        default: @overview.group_by
-        null:    true
-        nulloption: true
-        translate:  true
-        options:
-          customer:               'Customer'
-          state:           'State'
-          priority:        'Priority'
-          group:                  'Group'
-          owner:                  'Owner'
-        class:   'medium'
-      },
-#      {
-#        name: 'condition',
-#        display: 'Conditions',
-#        tag: 'select',
-#        multiple: false,
-#        null: false,
-#        relation: 'TicketArticleType',
-#        default: '9',
-#        class: 'medium',
-#        item_class: 'pull-left',
-#      },
-    ]
+    @configure_attributes_article = []
+    if @view_mode is 'd'
+      @configure_attributes_article.push({
+        name:     'view::per_page',
+        display:  'Items per page',
+        tag:      'select',
+        multiple: false,
+        null:     false,
+        default: @overview.view.per_page
+        options: {
+          5: ' 5'
+          10: '10'
+          15: '15'
+          20: '20'
+          25: '25'
+        },
+        class: 'medium',
+      })
+    @configure_attributes_article.push({
+      name:    "view::#{@view_mode}"
+      display: 'Attributes'
+      tag:     'checkbox'
+      default: @overview.view[@view_mode]
+      null:    false
+      translate: true
+      options: [
+        {
+          value:  'number'
+          name:   'Number'
+        },
+        {
+          value:  'title'
+          name:   'Title'
+        },
+        {
+          value:  'customer'
+          name:   'Customer'
+        },
+        {
+          value:  'organization'
+          name:   'Organization'
+        },
+        {
+          value:  'state'
+          name:   'State'
+        },
+        {
+          value:  'priority'
+          name:   'Priority'
+        },
+        {
+          value:  'group'
+          name:   'Group'
+        },
+        {
+          value:  'owner'
+          name:   'Owner'
+        },
+        {
+          value:  'created_at'
+          name:   'Age'
+        },
+        {
+          value:  'last_contact'
+          name:   'Last Contact'
+        },
+        {
+          value:  'last_contact_agent'
+          name:   'Last Contact Agent'
+        },
+        {
+          value:  'last_contact_customer'
+          name:   'Last Contact Customer'
+        },
+        {
+          value:  'first_response'
+          name:   'First Response'
+        },
+        {
+          value:  'close_time'
+          name:   'Close Time'
+        },
+        {
+          value:  'escalation_time'
+          name:   'Escalation in'
+        },
+        {
+          value:  'article_count'
+          name:   'Article Count'
+        },
+      ]
+      class:      'medium'
+    },
+    {
+      name:    'order::by'
+      display: 'Order'
+      tag:     'select'
+      default: @overview.order.by
+      null:    false
+      translate: true
+      options:
+        number:                 'Number'
+        title:                  'Title'
+        customer:               'Customer'
+        organization:           'Organization'
+        state:                  'State'
+        priority:               'Priority'
+        group:                  'Group'
+        owner:                  'Owner'
+        created_at:             'Age'
+        last_contact:           'Last Contact'
+        last_contact_agent:     'Last Contact Agent'
+        last_contact_customer:  'Last Contact Customer'
+        first_response:         'First Response'
+        close_time:             'Close Time'
+        escalation_time:        'Escalation in'
+        article_count:          'Article Count'
+      class:   'medium'
+    },
+    {
+      name:    'order::direction'
+      display: 'Direction'
+      tag:     'select'
+      default: @overview.order.direction
+      null:    false
+      translate: true
+      options:
+        ASC:   'up'
+        DESC:  'down'
+      class:   'medium'
+    },
+    {
+      name:    'group_by'
+      display: 'Group by'
+      tag:     'select'
+      default: @overview.group_by
+      null:    true
+      nulloption: true
+      translate:  true
+      options:
+        customer:       'Customer'
+        organization:   'Organization'
+        state:          'State'
+        priority:       'Priority'
+        group:          'Group'
+        owner:          'Owner'
+      class:   'medium'
+    })
 
     new App.ControllerForm(
       el:        @el.find('#form-setting')
@@ -486,19 +550,18 @@ class Settings extends App.ControllerModal
 
     # check if refetch is needed
     @reload_needed = 0
-    if @overview.order['by'] isnt params['order_by']
-      @overview.order['by'] = params['order_by']
+    if @overview.order.by isnt params.order.by
+      @overview.order.by = params.order.by
       @reload_needed = 1
 
-    if @overview.order['direction'] isnt params['order_by_direction']
-      @overview.order['direction'] = params['order_by_direction']
+    if @overview.order.direction isnt params.order.direction
+      @overview.order.direction = params.order.direction
       @reload_needed = 1
 
-    if @overview['group_by'] isnt params['group_by']
-      @overview['group_by'] = params['group_by']
-      @reload_needed = 1
+    for key, value of params.view
+      @overview.view[key] = value
 
-    @overview.view[@view_mode] = params['attributes']
+    @overview.group_by = params.group_by
 
     @overview.save(
       done: =>

+ 3 - 0
app/assets/javascripts/app/lib/app_post/i18n.js.coffee

@@ -144,7 +144,10 @@ class _i18nSingleton extends Spine.Module
   translate: ( string, args... ) =>
 
     # return '' on undefined
+    if typeof string is 'boolean'
+      string = string.toString()
     return '' if string is undefined
+    return '' if string is ''
 
     # return translation
     if @map[string] isnt undefined

+ 23 - 7
app/assets/javascripts/app/models/_application_model.js.coffee

@@ -16,6 +16,9 @@ class App.Model extends Spine.Model
   uiUrl: ->
     '#'
 
+  translate: ->
+    App[ @constructor.className ].configure_translate
+
   objectDisplayName: ->
     @constructor.className
 
@@ -67,11 +70,24 @@ class App.Model extends Spine.Model
       if !attribute.readonly
 
         # check required // if null is defined && null is false
-        if 'null' of attribute && !attribute[null] 
+        if 'null' of attribute && !attribute[null]
+
+          # check :: fields
+          parts = attribute.name.split '::'
+          if parts[0] && !parts[1]
+
+            # key exists not in hash || value is '' || value is undefined
+            if !( attribute.name of data['params'] ) || data['params'][attribute.name] is '' || data['params'][attribute.name] is undefined
+              errors[attribute.name] = 'is required'
 
-          # key exists not in hash || value is '' || value is undefined 
-          if !( attribute.name of data['params'] ) || data['params'][attribute.name] is '' || data['params'][attribute.name] is undefined
-            errors[attribute.name] = 'is required'
+          else if parts[0] && parts[1] && !parts[2]
+
+            # key exists not in hash || value is '' || value is undefined
+            if !data.params[parts[0]] || !( parts[1] of data.params[parts[0]] ) || data.params[parts[0]][parts[1]] is '' || data.params[parts[0]][parts[1]] is undefined
+              errors[attribute.name] = 'is required'
+
+          else
+            throw "can't parse '#{attribute.name}'"
 
         # check confirm password
         if attribute.type is 'password' && data['params'][attribute.name] && "#{attribute.name}_confirm" of data['params']
@@ -82,7 +98,9 @@ class App.Model extends Spine.Model
             errors["#{attribute.name}_confirm"] = ''
 
     # return error object
-    return errors if !_.isEmpty(errors)
+    if !_.isEmpty(errors)
+      console.log 'error', 'validation vailed', errors
+      return errors
 
     # return no errors
     return
@@ -327,5 +345,3 @@ class App.Model extends Spine.Model
       return
     )
     collection
-
-  

+ 22 - 1
app/assets/javascripts/app/models/channel.js.coffee

@@ -2,4 +2,25 @@ class App.Channel extends App.Model
   @configure 'Channel', 'adapter', 'area', 'options', 'group_id', 'active', 'updated_at'
   @extend Spine.Model.Ajax
   @url: @apiPath + '/channels'
-  @configure_delete = true
+  @configure_delete = true
+
+  @configure_attributes = [
+    { name: 'adapter',            display: 'Type',     tag: 'select',   multiple: false, null: false, options: { IMAP: 'IMAP', POP3: 'POP3' } },
+    { name: 'options::host',      display: 'Host',     tag: 'input',    type: 'text', limit: 120, null: false, autocapitalize: false },
+    { name: 'options::user',      display: 'User',     tag: 'input',    type: 'text', limit: 120, null: false, autocapitalize: false },
+    { name: 'options::password',  display: 'Password', tag: 'input',    type: 'password', limit: 120, null: false, autocapitalize: false },
+    { name: 'options::ssl',       display: 'SSL',      tag: 'select',   multiple: false, null: false, options: { true: 'yes', false: 'no' }, translate: true, default: true},
+    { name: 'options::folder',    display: 'Folder',   tag: 'input',    type: 'text', limit: 120, null: true, autocapitalize: false },
+    { name: 'group_id',           display: 'Group',    tag: 'select',   multiple: false, null: false, nulloption: true, relation: 'Group'  },
+    { name: 'active',             display: 'Active',   tag: 'select',   multiple: false, null: false, options: { true: 'yes', false: 'no' }, translate: true, default: true },
+  ]
+  @configure_overview = [
+    'adapter', 'options::host', 'options::user', 'group'
+  ]
+
+  @_fillUp: (data) ->
+
+    # group
+    data.group = App.Group.find( data.group_id )
+
+    data

+ 58 - 15
app/assets/javascripts/app/models/overview.js.coffee

@@ -18,21 +18,64 @@ class App.Overview extends App.Model
       default: ['number', 'title', 'state', 'created_at']
       null:    false
       translate: true
-      options:
-        number:                 'Number'
-        title:                  'Title'
-        customer:               'Customer'
-        state:                  'State'
-        priority:               'Priority'
-        group:                  'Group'
-        owner:                  'Owner'
-        created_at:             'Age'
-        last_contact:           'Last Contact'
-        last_contact_agent:     'Last Contact Agent'
-        last_contact_customer:  'Last Contact Customer'
-        first_response:         'First Response'
-        close_time:             'Close Time'
-        article_count:          'Article Count'
+      options: [
+        {
+          value:  'number'
+          name:   'Number'
+        },
+        {
+          value:  'title'
+          name:   'Title'
+        },
+        {
+          value:  'customer'
+          name:   'Customer'
+        },
+        {
+          value:  'state'
+          name:   'State'
+        },
+        {
+          value:  'priority'
+          name:   'Priority'
+        },
+        {
+          value:  'group'
+          name:   'Group'
+        },
+        {
+          value:  'owner'
+          name:   'Owner'
+        },
+        {
+          value:  'created_at'
+          name:   'Age'
+        },
+        {
+          value:  'last_contact'
+          name:   'Last Contact'
+        },
+        {
+          value:  'last_contact_agent'
+          name:   'Last Contact Agent'
+        },
+        {
+          value:  'last_contact_customer'
+          name:   'Last Contact Customer'
+        },
+        {
+          value:  'first_response'
+          name:   'First Response'
+        },
+        {
+          value:  'close_time'
+          name:   'Close Time'
+        },
+        {
+          value:  'article_count'
+          name:   'Article Count'
+        },
+      ]
       class:      'medium'
     },
 

+ 16 - 7
app/assets/javascripts/app/models/ticket.js.coffee

@@ -5,19 +5,21 @@ class App.Ticket extends App.Model
   @configure_attributes = [
       { name: 'number',                display: '#',        tag: 'input',    type: 'text', limit: 100, null: true, read_only: true,  style: 'width: 8%'  },
       { name: 'customer_id',           display: 'Customer', tag: 'input',    type: 'text', limit: 100, null: false, class: 'span8', autocapitalize: false, help: 'Select the customer of the Ticket or create one.', link: '<a href="" class="customer_new">&raquo;</a>' },
+      { name: 'organization_id',       display: 'Organization', tagreadonly: 1 },
       { name: 'group_id',              display: 'Group',    tag: 'select',   multiple: false, limit: 100, null: false, class: 'span8', relation: 'Group', style: 'width: 10%' },
       { name: 'owner_id',              display: 'Owner',    tag: 'select',   multiple: false, limit: 100, null: true, class: 'span8', relation: 'User', style: 'width: 12%' },
       { name: 'title',                 display: 'Title',    tag: 'input',    type: 'text', limit: 100, null: false, class: 'span8' },
       { name: 'state_id',              display: 'State',    tag: 'select',   multiple: false, null: false, relation: 'TicketState', default: 'new', class: 'medium', style: 'width: 12%' },
       { name: 'priority_id',           display: 'Priority', tag: 'select',   multiple: false, null: false, relation: 'TicketPriority', default: '2 normal', class: 'medium', style: 'width: 12%' },
-      { name: 'created_at',            display: 'Created',  tag: 'time', style: 'width: 12%' },
-      { name: 'last_contact',          display: 'Last contact',            tag: 'time', null: true, style: 'width: 12%' },
-      { name: 'last_contact_agent',    display: 'Last contact (Agent)',    tag: 'time', null: true, style: 'width: 12%' },
-      { name: 'last_contact_customer', display: 'Last contact (Customer)', tag: 'time', null: true, style: 'width: 12%' },
-      { name: 'first_response',        display: 'First response',          tag: 'time', null: true, style: 'width: 12%' },
-      { name: 'close_time',            display: 'Close time',              tag: 'time', null: true, style: 'width: 12%' },
-      { name: 'escalation_time',       display: 'Escalation in',           tag: 'time', null: true, style: 'width: 12%' },
+      { name: 'last_contact',          display: 'Last contact',            type: 'time', null: true, style: 'width: 12%' },
+      { name: 'last_contact_agent',    display: 'Last contact (Agent)',    type: 'time', null: true, style: 'width: 12%' },
+      { name: 'last_contact_customer', display: 'Last contact (Customer)', type: 'time', null: true, style: 'width: 12%' },
+      { name: 'first_response',        display: 'First response',          type: 'time', null: true, style: 'width: 12%' },
+      { name: 'close_time',            display: 'Close time',              type: 'time', null: true, style: 'width: 12%' },
+      { name: 'escalation_time',       display: 'Escalation in',           type: 'time', null: true, style: 'width: 12%', class: 'escalation' },
       { name: 'article_count',         display: 'Article#',  style: 'width: 12%' },
+      { name: 'created_at',            display: 'Created', type: 'time', style: 'width: 12%', readonly: 1 },
+      { name: 'updated_at',            display: 'Updated', type: 'time', style: 'width: 12%', readonly: 1 },
     ]
 
   uiUrl: ->
@@ -41,6 +43,13 @@ class App.Ticket extends App.Model
       else
         data.customer = App.User.find( data.customer_id )
 
+    # organization_id
+    if data.organization_id
+      if !App.Organization.exists( data.organization_id )
+        console.error("Can't find user for data.organization_id #{data.organization_id} for ticket #{data.id}")
+      else
+        data.organization = App.Organization.find( data.organization_id )
+
     # owner
     if data.owner_id
       if !App.User.exists( data.owner_id )

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