diff --git a/Gemfile b/Gemfile index dad5b951d..4f56f0973 100644 --- a/Gemfile +++ b/Gemfile @@ -1,9 +1,9 @@ -source 'https://rubygems.org' +source 'http://rubygems.org' gem 'rails', '3.2.2' # Bundle edge Rails instead: -# gem 'rails', :git => 'git://github.com/rails/rails.git' +#gem 'rails', :git => 'git://github.com/rails/rails.git' gem 'sqlite3' @@ -12,24 +12,34 @@ gem 'json' # Gems used only for assets and not required # in production environments by default. group :assets do - gem 'sass-rails', '~> 3.2.3' - gem 'coffee-rails', '~> 3.2.1' - - # See https://github.com/sstephenson/execjs#readme for more supported runtimes - # gem 'therubyracer' - - gem 'uglifier', '>= 1.0.3' + gem 'sass-rails', '~> 3.2.4' + gem 'coffee-rails', '~> 3.2.2' + gem 'uglifier', '>= 1.2.3' end gem 'jquery-rails' +# Optional support for eco templates +gem 'eco' + +gem "omniauth" +gem "omniauth-twitter" +gem "omniauth-facebook" +gem "omniauth-linkedin" + +gem "twitter" +gem "koala" +gem "mail" + +gem "mime-types" + +gem 'delayed_job_active_record' +gem "daemons" + # To use ActiveModel has_secure_password # gem 'bcrypt-ruby', '~> 3.0.0' -# To use Jbuilder templates for JSON -# gem 'jbuilder' - -# Use unicorn as the app server +# Use unicorn as the web server # gem 'unicorn' # Deploy with Capistrano @@ -37,3 +47,4 @@ gem 'jquery-rails' # To use debugger # gem 'ruby-debug' + diff --git a/app/assets/images/close.png b/app/assets/images/close.png new file mode 100644 index 000000000..81a24d89c Binary files /dev/null and b/app/assets/images/close.png differ diff --git a/app/assets/images/glyphicons-halflings.png b/app/assets/images/glyphicons-halflings.png new file mode 100644 index 000000000..92d4445df Binary files /dev/null and b/app/assets/images/glyphicons-halflings.png differ diff --git a/app/assets/javascripts/app/controllers/.gitkeep b/app/assets/javascripts/app/controllers/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/app/assets/javascripts/app/controllers/_application_controller.js.coffee b/app/assets/javascripts/app/controllers/_application_controller.js.coffee new file mode 100644 index 000000000..b93a692b9 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_application_controller.js.coffee @@ -0,0 +1,698 @@ +class App.Controller extends Spine.Controller + + # add @title methode to set title + title: (name) -> + $('html head title').html( Config.product_name + ' - ' + name ) + + # add @notify methode to create notification + notify: (data) -> + Spine.trigger 'notify', data + + # add @navupdate methode to update navigation + navupdate: (url) -> + Spine.trigger 'navupdate', url + +# # extend delegateEvents to unbind and undelegate +# delegateEvents: -> +# +# # here unbind and undelegate while @el +# @el.unbind() +# @el.undelegate() +# +# for key, method of @events +# unless typeof(method) is 'function' +# method = @proxy(@[method]) +# +# match = key.match(@eventSplitter) +# eventName = match[1] +# selector = match[2] +# +# if selector is '' +# @el.bind(eventName, method) +# else +# @el.delegate(selector, eventName, method) + + formGen: (data) -> + form = $('
') + fieldset = $('
') + fieldset.appendTo(form) + autofocus = 1; + if data.autofocus isnt undefined + autofocus = data.autofocus + + attributes = clone( data.model.configure_attributes || [] ) + for attribute in attributes + + if !attribute.readonly && ( !data.required || data.required && attribute[data.required] ) + + # set autofocus + if autofocus is 1 + attribute.autofocus = 'autofocus' + autofocus = 0 + + # set required option + if !attribute.null + attribute.required = 'required' + else + attribute.required = '' + + # set multible option + if attribute.multiple + attribute.multiple = 'multiple' + else + attribute.multiple = '' + + # set autocapitalize option + if attribute.autocapitalize is undefined || attribute.autocapitalize + attribute.autocapitalize = '' + else + attribute.autocapitalize = 'autocapitalize="off"' + + # set value + if data.params + if attribute.name of data.params + attribute.value = data.params[attribute.name] + + # set default value + else + if 'default' of attribute + @log 'default', attribute.default + attribute.value = attribute.default + else + attribute.value = '' + + # add item + item = $( @formGenItem(attribute, data.model.className) ) + item.appendTo(fieldset) + + # if password, add confirm password item + if attribute.type is 'password' + + attribute.display = attribute.display + ' (confirm)' + attribute.name = attribute.name + '_confirm'; + + item = $( @formGenItem(attribute, data.model.className) ) + item.appendTo(fieldset) + + # return form + return form.html() + + formGenItem: (attribute, classname) -> + + # create item id + attribute.id = classname + '_' + attribute.name + + # build options list based on config + selection = [] + if attribute.options + if attribute.nulloption + attribute.options[''] = '-' + for key of attribute.options + selection.push { + name: attribute.options[key], + value: key, + } + + # build options list based on relation + attribute.options = selection || [] + if attribute.relation && App[attribute.relation] + attribute.options = [] + if attribute.nulloption + attribute.options[''] = '-' + attribute.options.push { + name: '-', + value: '', + } + + list = [] + if attribute.filter && attribute.filter[attribute.name] + filter = attribute.filter[attribute.name] + + # check all records + for record in App[attribute.relation].all() + + # check all filter attributes + for key of filter + + # check all filter values as array + for value in filter[key] + + # if it's matching, use it for selection + if record[key] is value + list.push record + else + list = App[attribute.relation].all() + + list.forEach( (item) => + if item.active + name = '???' + if item.name + name = item.name + else if item.firstname + name = item.firstname + if item.lastname + if name + name = name + ' ' + name = name + item.lastname + + attribute.options.push { + name: name, + value: item.id, + note: item.note, + } + ) + + # finde selected/checked item of list + if attribute.options + for record in attribute.options + if typeof attribute.value is 'string' || typeof attribute.value is 'number' || typeof attribute.value is 'boolean' + + # if name or value is matching + if record.value.toString() is attribute.value.toString() || record.name.toString() is attribute.value.toString() + record.selected = 'selected' + record.checked = 'checked' +# if record.name.toString() is attribute.value.toString() +# record.selected = 'selected' +# record.checked = 'checked' + if ( attribute.value && record.value && _.include(attribute.value, record.value) ) || ( attribute.value && record.name && _.include(attribute.value, record.name) ) + record.selected = 'selected' + record.checked = 'checked' + + # boolean + if attribute.tag is 'boolean' + + # build options list + attribute.options = [ + { name: 'active', value: true } + { name: 'inactive', value: false } + ] || [] + + # finde selected item of list + for record in attribute.options + if record.value is attribute.value + record.selected = 'selected' + + # return item + item = App.view('generic/select')( attribute: attribute ) + + # select + else if attribute.tag is 'select' + item = App.view('generic/select')( attribute: attribute ) + + # checkbox + else if attribute.tag is 'checkbox' + item = App.view('generic/checkbox')( attribute: attribute ) + + # radio + else if attribute.tag is 'radio' + item = App.view('generic/radio')( attribute: attribute ) + + # textarea + else if attribute.tag is 'textarea' + item = App.view('generic/textarea')( attribute: attribute ) + + # autocompletion + else if attribute.tag is 'autocompletion' + item = App.view('generic/autocompletion')( attribute: attribute ) + + a = -> +# if attribute.relation && App[attribute.relation] +# @log '1312312333333333333', App[attribute.relation] +# @log '1231231231', '#' + attribute.id + '_autocompletion' + @local_attribute = '#' + attribute.id + @local_attribute_full = '#' + attribute.id + '_autocompletion' + @callback = attribute.callback + + b = (event, key) => +# @log 'zzzz', event, item, key, @local_attribute + $(@local_attribute).val(key) + if @callback + @callback( user_id: key ) + ### + $(@local_attribute_full).tagsInput( + autocomplete_url: '/user_search', + height: '30px', + width: '530px', + auto: { + source: '/user_search', + minLength: 2, + select: ( event, ui ) => + @log 'selected', event, ui + b(event, ui.item.id) + } + ) + ### + $(@local_attribute_full).autocomplete( + source: '/user_search', + minLength: 2, + select: ( event, ui ) => + @log 'selected', event, ui + b(event, ui.item.id) + ) + + @delay(a, 800) + + # input + else + item = App.view('generic/input')( attribute: attribute ) + + return App.view('generic/attribute')( + attribute: attribute, + item: item, + ) + + # get all params of the form + formParam: (form, errors) -> + param = {} + + # find form based on sub elements + if $(form).children()[0] + form = $(form).children().parents('form') + + # find form based on parents next + else if $(form).parents('form')[0] + form = $(form).parents('form') + + # find form based on parents next , not really good! + else if $(form).parents().find('form')[0] + form = $(form).parents().find('form') + else + @log 'ERROR, no form found!', form + + for key in form.serializeArray() + if param[key.name] + if typeof param[key.name] is 'string' + param[key.name] = [ param[key.name], key.value] + else + param[key.name].push key.value + else + param[key.name] = key.value + + @log 'formParam', form, param + return param + + formDisable: (form) -> + @log 'disable...', $(form.target).parent() + $(form.target).parent().find('[type="submit"]').attr('disabled', true) + $(form.target).parent().find('[type="reset"]').attr('disabled', true) + + formEnable: (form) -> + @log 'enable...', $(form).parent() + $(form).parent().find('[type="submit"]').attr('disabled', false) + $(form).parent().find('[type="reset"]').attr('disabled', false) + + table: (data) -> + overview = data.overview || data.model.configure_overview || [] + attributes = data.attributes || data.model.configure_attributes || [] + + # define normal header + header = [] + for row in overview + for attribute in attributes + if row is attribute.name + header.push(attribute.display) + else + rowWithoutId = row + '_id' + if rowWithoutId is attribute.name + header.push(attribute.display) + + data_types = [] + for row in overview + data_types.push { + name: row, + link: 1, + } + + # extended table format + if data.overview_extended + header = [] + for row in data.overview_extended + for attribute in attributes + if row.name is attribute.name + header.push(attribute.display) + else + rowWithoutId = row.name + '_id' + if rowWithoutId is attribute.name + header.push(attribute.display) + + data_types = data.overview_extended + + # generate content data + objects = clone( data.objects ) + for object in objects + for row in data_types + + # check if data is a object + if typeof object[row.name] is 'object' + if !object[row.name] + object[row.name] = { + name: '-', + } + + # if no content exists, try firstname/lastname + if !object[row.name]['name'] + if object[row.name]['firstname'] || object[row.name]['lastname'] + object[row.name]['name'] = (object[row.name]['firstname'] || '') + ' ' + (object[row.name]['lastname'] || '') + + # if it isnt a object, create one + else if typeof object[row.name] isnt 'object' + object[row.name] = { + name: object[row.name], + } + + # fallback if it's something else + else + object[row.name] = { + name: '????', + } + + # execute callback on content + if row.callback + object[row.name]['name'] = row.callback(object[row.name]['name']) + +# @log 'table', 'header', header, 'overview', data_types, 'objects', objects + table = App.view('generic/table')( + header: header, + overview: data_types, + objects: objects, + checkbox: data.checkbox, + ) +# @log 'ttt', $(table).find('span') +# $(table).find('span').bind('click', -> +# console.log('----------click---------') +# ) + + # convert to jquery object + table = $(table) + + # enable checkbox bulk selection + if data.checkbox + table.delegate('[name="bulk_all"]', 'click', (e) -> + if $(e.target).attr('checked') + $(e.target).parents().find('[name="bulk"]').attr('checked', true); + else + $(e.target).parents().find('[name="bulk"]').attr('checked', false); + ) + + return table + + ticketTableAttributes: (attributes) => + all_attributes = [ + { name: 'number', link: true }, + { name: 'title', link: true }, + { name: 'customer', class: 'user-data', data: { id: true } }, + { name: 'ticket_state' }, + { name: 'ticket_priority' }, + { name: 'group' }, + { name: 'owner', class: 'user-data', data: { id: true } }, + { name: 'created_at', callback: @humanTime }, + { name: 'last_contact', callback: @humanTime }, + { name: 'last_contact_agent', callback: @humanTime }, + { name: 'last_contact_customer', callback: @humanTime }, + { name: 'first_response', callback: @humanTime }, + { name: 'close_time', callback: @humanTime }, + ] + shown_all_attributes = [] + for all_attribute in all_attributes + for attribute in attributes + if all_attribute['name'] is attribute + shown_all_attributes.push all_attribute + break + return shown_all_attributes + + validateForm: (data) -> + + # remove all errors + $(data.form).parents().find('.error').removeClass('error') + $(data.form).parents().find('.help-inline').html('') + + # show new errors + for key, msg of data.errors + $(data.form).parents().find('[name*="' + key + '"]').parents('div .control-group').addClass('error') + $(data.form).parents().find('[name*="' + key + '"]').parent().find('.help-inline').html(msg); + + # set autofocus + $(data.form).parents().find('.error').find('input, textarea').first().focus() + +# # enable form again +# if $(data.form).parents().find('.error').html() +# @formEnable(data.form) + +# redirectToLogin: (data) -> +# + + # human readable file size + humanFileSize: (size) => + if size > ( 1024 * 1024 ) + size = Math.round( size / ( 1024 * 1024 ) ) + ' MBytes' + else if size > 1024 + size = Math.round( size / 1024 ) + ' KBytes' + else + size = size + ' Bytes' + return size + + # human readable time + humanTime: (time) => + current = new Date() + created = new Date(time) + diff = (current - created) / 1000 + if diff >= 86400 + unit = Math.round( (diff / 86400) ) + if unit > 1 + return unit + ' days' + else + return unit + ' day' + if diff >= 3600 + unit = Math.round( (diff / 3600) ) + if unit > 1 + return unit + ' hours' + else + return unit + ' hour' + if diff <= 3600 + unit = Math.round( (diff / 60) ) + if unit > 1 + return unit + ' minutes' + else + return unit + ' minute' + + userInfo: (data) => + # start customer info controller + new App.UserInfo( + el: data.el || $('#customer_info'), + user_id: data.user_id, + ) + + authenticate: -> + console.log 'authenticate', window.Session + return true if window.Session['id'] + + # redirect to login + @navigate '#login' + return false + + clone = (obj) -> + if not obj? or typeof obj isnt 'object' + return obj + + newInstance = new obj.constructor() + + for key of obj + newInstance[key] = clone obj[key] + + return newInstance + + userPopups: (position = 'right') -> + # show user popup + $('.user-data').popover( + delay: { show: 500, hide: 1200 }, +# placement: 'bottom', + placement: position, + title: (e) => + user_id = $(e).data('id') + user = App.User.find(user_id) + (user.firstname || '') + ' ' + (user.lastname || '') + content: (e) => + user_id = $(e).data('id') + user = App.User.find(user_id) + + # get display data + data = [] + for item in App.User.configure_attributes + if user[item.name] + if item.name isnt 'firstname' + if item.name isnt 'lastname' + if item.info #&& ( @user[item.name] || item.name isnt 'note' ) + data.push item + + # insert data + App.view('user_info_small')( + user: user, + data: data, + ) + ) + + userTicketPopups: (data) -> + # get data + @tickets = {} + ajax = new App.Ajax + ajax.ajax( + type: 'GET', + url: '/ticket_customer', + data: { + customer_id: data.user_id, + } + processData: true, + success: (data, status, xhr) => + @tickets = data.tickets + ) + + if !data.position + data.position = 'left' + + # show user popup + $(data.selector).popover( + delay: { show: 500, hide: 5200 }, + placement: data.position, + title: (e) => + $(e).find('[title="*"]').val() + + content: (e) => + type = $(e).filter('[data-type]').data('type') + data = @tickets[type] || [] + + for ticket in data + + # set human time + ticket.humanTime = @humanTime(ticket.created_at) + + # insert data + App.view('user_ticket_info_small')( + tickets: data, + ) + ) + + loadCollection: (params) -> + + # users + if params.type == 'User' + for user of params.data + if user && !user.image + user.image = 'http://placehold.it/48x48' + App.User.refresh( params.data[user], options: { clear: true } ) + + # tickets + else if params.type == 'Ticket' + for ticket in params.data + # priority + ticket.ticket_priority = App.TicketPriority.find(ticket.ticket_priority_id) + + # state + ticket.ticket_state = App.TicketState.find(ticket.ticket_state_id) + + # group + ticket.group = App.Group.find(ticket.group_id) + + # customer + if ticket.customer_id and App.User.exists(ticket.customer_id) + user = App.User.find(ticket.customer_id) + ticket.customer = user + + # owner + if ticket.owner_id and App.User.exists(ticket.owner_id) + user = App.User.find(ticket.owner_id) + ticket.owner = user + + # load collection + App.Ticket.refresh( ticket, options: { clear: true } ) + + # articles + else if params.type == 'TicketArticle' + for article in params.data + + # add user + article.created_by = App.User.find(article.created_by_id) + + # set human time + article.humanTime = @humanTime(article.created_at) + + # add possible actions + article.article_type = App.TicketArticleType.find( article.ticket_article_type_id ) + article.article_sender = App.TicketArticleSender.find( article.ticket_article_sender_id ) + + App.TicketArticle.refresh( article, options: { clear: true } ) + + # history + else if params.type == 'History' + for histroy in params.data + + # add user + histroy.created_by = App.User.find(histroy.created_by_id) + + # set human time + histroy.humanTime = @humanTime(histroy.created_at) + + # add possible actions + if histroy.history_attribute_id + histroy.attribute = App.HistoryAttribute.find( histroy.history_attribute_id ) + if histroy.history_type_id + histroy.type = App.HistoryType.find( histroy.history_type_id ) + if histroy.history_object_id + histroy.object = App.HistoryObject.find( histroy.history_object_id ) + + App.History.refresh( histroy, options: { clear: true } ) + + # all the rest + else + for object in params.data + App[params.type].refresh( object, options: { clear: true } ) + + +class App.ControllerModal extends App.Controller + className: 'modal hide fade', + tag: 'div', + + events: + 'submit form': 'submit', + 'click .submit': 'submit', + 'click .cancel': 'modalHide', + 'click .close': 'modalHide', + + constructor: (options) -> + + # do not use @el, because it's inserted by js + if options + delete options.el + + # callbacks +# @callback = {} +# if options.success +# @callback.success = options.success +# if options.error +# @callback.error = options.error + + super(options) + + modalShow: (params) => + @el.modal({ + backdrop: true, + keyboard: true, + show: true + }) + @el.bind('hidden', => + + # navigate back to home page + if @pageData && @pageData.home + @navigate @pageData.home + + # navigate back + if params && params.navigateBack + window.history.back() + + # remove modal from dom + $('.modal').remove(); + ) + + modalHide: (e) => + if e + e.preventDefault() + @el.modal('hide') diff --git a/app/assets/javascripts/app/controllers/_application_controller_generic.js.coffee b/app/assets/javascripts/app/controllers/_application_controller_generic.js.coffee new file mode 100644 index 000000000..ac2467f6c --- /dev/null +++ b/app/assets/javascripts/app/controllers/_application_controller_generic.js.coffee @@ -0,0 +1,230 @@ +$ = jQuery.sub() +$.fn.item = (genericObject) -> + elementID = $(@).data('id') + elementID or= $(@).parents('[data-id]').data('id') + genericObject.find(elementID) + +class App.ControllerGenericNew extends App.ControllerModal + constructor: (params) -> + super + @render() + + render: -> + @log 'ren new', @el + @html App.view('generic/admin/new')( + form: @formGen( model: @genericObject ), + head: 'New ' + @pageData.object + ) + @modalShow() + + submit: (e) -> + @log 'submit' + e.preventDefault() + params = @formParam(e.target) + ### + for num in [1..199] + user = new User + params.login = 'login_c' + num + user.updateAttributes(params) + return false + ### + object = new @genericObject + object.load(params) + + # validate + errors = object.validate( form: true ) + if errors + @log 'error new', errors + @validateForm( form: e.target, errors: errors ) + return false + + # save object + object.save( + success: => + @modalHide() + error: => + @log 'errors' + @modalHide() + ) + +class App.ControllerGenericEdit extends App.ControllerModal + constructor: (params) -> + super + @log 'ControllerGenericEditWindow', params + + # fetch item on demand + if @genericObject.exists(params.id) + @item = @genericObject.find(params.id) + @render() + else + @genericObject.bind 'refresh', => + @log 'changed....' + @item = @genericObject.find(params.id) + @render() + @genericObject.unbind 'refresh' + @genericObject.fetch( id: params.id) + + render: -> + @html App.view('generic/admin/edit')( + form: @formGen( model: @genericObject, params: @item ), + head: 'Edit ' + @pageData.object + ) + @modalShow() + + submit: (e) -> + e.preventDefault() + params = @formParam(e.target) + @item.load(params) + + # validate + errors = @item.validate( form: true ) + if errors + @log 'error new', errors + @validateForm( form: e.target, errors: errors ) + return false + + @log 'save....' + # save object + @item.save( + success: => + @modalHide() + error: => + @log 'errors' + @modalHide() + ) + +class App.ControllerGenericIndex extends App.Controller + events: + 'click [data-type=edit]': 'edit' + 'click [data-type=destroy]': 'destroy' + 'click [data-type=new]': 'new' + + constructor: -> + super + + # set controller to active + Config['ActiveController'] = @pageData.navupdate + + + # set title + @title @pageData.title + + # set nav bar + @navupdate @pageData.navupdate + + # bind render after a change is done + @genericObject.bind 'refresh change', @render + @genericObject.bind 'ajaxError', (rec, msg) => + @log 'ajax notice', msg.status + if msg.status is 401 + @log 'ajax error', rec, msg, msg.status +# @navigate @pageData.navupdate +# alert('relogin') + @navigate 'login' + + # execute fetch, if needed + if !@genericObject.count() || true +# if !@genericObject.count() + + # prerender without content + @render() + + # fetch all + @genericObject.fetch() + else + @render() + + render: => + + return if Config['ActiveController'] isnt @pageData.navupdate + + objects = @genericObject.all() + + # remove ignored items from collection + if @ignoreObjectIDs + objects = _.filter(objects, (item) -> + return if item.id is 1 + return item + ) + + @html App.view('generic/admin/index')( + head: @pageData.objects, + notes: @pageData.notes, + buttons: @pageData.buttons, + menus: @pageData.menus, + ) + + # append content table + table = @table( + model: @genericObject, + objects: objects, + ) + @el.find('.table-overview').append(table) + + edit: (e) => + e.preventDefault() + item = $(e.target).item(@genericObject) + new App.ControllerGenericEdit( + id: item.id, + pageData: @pageData, + genericObject: @genericObject + ) + + destroy: (e) -> + item = $(e.target).item(@genericObject) + item.destroy() if confirm('Sure?') + + new: (e) -> + e.preventDefault() + new App.ControllerGenericNew( + pageData: @pageData, + genericObject: @genericObject + ) + +class App.ControllerLevel2 extends App.Controller + events: + 'click [data-toggle="tabnav"]': 'toggle', + + constructor: -> + super + + return if !@authenticate() + + render: -> + @log 'ttt', @target, @ + # set title + @title @page.title + @navupdate @page.nav + + @html App.view('generic/admin_level2/index')( + page: @page, + menus: @menu, + type: @type, + target: @target, + ) + for menu in @menu + @el.find('.nav-tab-content').append('') + if menu.controller + params = menu.params || {} + params.el = @el.find( '#' + menu.target ) + new menu.controller( params ) + + @el.find('.tabbable').addClass('hide') + if @target + @el.find( '#' + @target ).removeClass('hide') + else + @el.find('.tabbable:first').removeClass('hide') + + @el.find('[data-toggle="tabnav"]:first').addClass('active') + + + toggle: (e) -> + return true if @toggleable is false + e.preventDefault() + target = $(e.target).data('target') + $(e.target).parents('ul').find('li').removeClass('active') + $(e.target).parents('li').addClass('active') + @el.find('.tabbable').addClass('hide') + @el.find('#' + target).removeClass('hide') +# window.scrollTo(0,0) + \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/_channel/chat.js.coffee b/app/assets/javascripts/app/controllers/_channel/chat.js.coffee new file mode 100644 index 000000000..b3448039b --- /dev/null +++ b/app/assets/javascripts/app/controllers/_channel/chat.js.coffee @@ -0,0 +1,18 @@ +$ = jQuery.sub() + +class App.ChannelChat extends App.Controller + events: + 'click [data-toggle="tabnav"]': 'toggle', + + constructor: -> + super + + # render page + @render() + + render: -> + + @html App.view('channel/chat')( + head: 'some header' + ) + diff --git a/app/assets/javascripts/app/controllers/_channel/email.js.coffee b/app/assets/javascripts/app/controllers/_channel/email.js.coffee new file mode 100644 index 000000000..733d2df4d --- /dev/null +++ b/app/assets/javascripts/app/controllers/_channel/email.js.coffee @@ -0,0 +1,18 @@ +$ = jQuery.sub() + +class App.ChannelEmail extends App.Controller + events: + 'click [data-toggle="tabnav"]': 'toggle', + + constructor: -> + super + + # render page + @render() + + render: -> + + @html App.view('channel/email')( + head: 'some header' + ) + diff --git a/app/assets/javascripts/app/controllers/_channel/facebook.js.coffee b/app/assets/javascripts/app/controllers/_channel/facebook.js.coffee new file mode 100644 index 000000000..a0ab7151f --- /dev/null +++ b/app/assets/javascripts/app/controllers/_channel/facebook.js.coffee @@ -0,0 +1,18 @@ +$ = jQuery.sub() + +class App.ChannelFacebook extends App.Controller + events: + 'click [data-toggle="tabnav"]': 'toggle', + + constructor: -> + super + + # render page + @render() + + render: -> + + @html App.view('channel/facebook')( + head: 'some header' + ) + diff --git a/app/assets/javascripts/app/controllers/_channel/twitter.js.coffee b/app/assets/javascripts/app/controllers/_channel/twitter.js.coffee new file mode 100644 index 000000000..7c220ec25 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_channel/twitter.js.coffee @@ -0,0 +1,18 @@ +$ = jQuery.sub() + +class App.ChannelTwitter extends App.Controller + events: + 'click [data-toggle="tabnav"]': 'toggle', + + constructor: -> + super + + # render page + @render() + + render: -> + + @html App.view('channel/twitter')( + head: 'some header' + ) + diff --git a/app/assets/javascripts/app/controllers/_channel/web.js.coffee b/app/assets/javascripts/app/controllers/_channel/web.js.coffee new file mode 100644 index 000000000..7688ecbfb --- /dev/null +++ b/app/assets/javascripts/app/controllers/_channel/web.js.coffee @@ -0,0 +1,18 @@ +$ = jQuery.sub() + +class App.ChannelWeb extends App.Controller + events: + 'click [data-toggle="tabnav"]': 'toggle', + + constructor: -> + super + + # render page + @render() + + render: -> + + @html App.view('channel/web')( + head: 'some header' + ) + diff --git a/app/assets/javascripts/app/controllers/_dashboard/activity_stream.js.coffee b/app/assets/javascripts/app/controllers/_dashboard/activity_stream.js.coffee new file mode 100644 index 000000000..55fd75835 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_dashboard/activity_stream.js.coffee @@ -0,0 +1,61 @@ +$ = jQuery.sub() + +class App.DashboardActivityStream extends App.Controller + events: + 'click [data-type=edit]': 'zoom' + + constructor: -> + super +# @log 'aaaa', @el + + @items = [] + + # get data + @ajax = new App.Ajax + @ajax.ajax( + type: 'GET', + url: '/activity_stream', + data: { + limit: 10, + } + processData: true, +# data: JSON.stringify( view: @view ), + success: (data, status, xhr) => + @items = data.activity_stream + + # load user collection + @loadCollection( type: 'User', data: data.users ) + + # load ticket collection + @loadCollection( type: 'Ticket', data: data.tickets ) + + @render() + ) + + + render: -> + + # load user data + for item in @items + item.created_by = App.User.find(item.created_by_id) + + # load ticket data + for item in @items + item.ticket = App.Ticket.find(item.o_id) + + html = App.view('dashboard/activity_stream')( + head: 'Activity Stream', + items: @items + ) + html = $(html) + + @html html + + # start user popups + @userPopups('left') + + zoom: (e) => + e.preventDefault() + id = $(e.target).parents('[data-id]').data('id') + @log 'goto zoom!' + @navigate 'ticket/zoom/' + id diff --git a/app/assets/javascripts/app/controllers/_dashboard/recent_viewed.js.coffee b/app/assets/javascripts/app/controllers/_dashboard/recent_viewed.js.coffee new file mode 100644 index 000000000..cb12c9ab9 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_dashboard/recent_viewed.js.coffee @@ -0,0 +1,63 @@ +$ = jQuery.sub() + +class App.DashboardRecentViewed extends App.Controller + events: + 'click [data-type=edit]': 'zoom' + + constructor: -> + super +# @log 'aaaa', @el + + @items = [] + + # get data + @ajax = new App.Ajax + @ajax.ajax( + type: 'GET', + url: '/recent_viewed', + data: { + limit: 5, + } + processData: true, +# data: JSON.stringify( view: @view ), + success: (data, status, xhr) => + @items = data.recent_viewed + + # load user collection + @loadCollection( type: 'User', data: data.users ) + + # load ticket collection + @loadCollection( type: 'Ticket', data: data.tickets ) + + @render() + ) + + + render: -> + + # load user data + for item in @items +# @log 'load', item.created_by_id + item.created_by = App.User.find(item.created_by_id) + + # load ticket data + for item in @items +# @log 'load', item.o_id + item.ticket = App.Ticket.find(item.o_id) + + html = App.view('dashboard/recent_viewed')( + head: 'Recent Viewed', + items: @items + ) + html = $(html) + + @html html + + # start user popups + @userPopups('left') + + zoom: (e) => + e.preventDefault() + id = $(e.target).parents('[data-id]').data('id') + @log 'goto zoom!' + @navigate 'ticket/zoom/' + id diff --git a/app/assets/javascripts/app/controllers/_dashboard/ticket.js.coffee b/app/assets/javascripts/app/controllers/_dashboard/ticket.js.coffee new file mode 100644 index 000000000..20a65c5ba --- /dev/null +++ b/app/assets/javascripts/app/controllers/_dashboard/ticket.js.coffee @@ -0,0 +1,235 @@ +$ = jQuery.sub() + +class App.DashboardTicket extends App.Controller + events: + 'click [data-type=edit]': 'zoom' + 'click [data-type=settings]': 'settings' + 'click [data-type=page]': 'page' + + constructor: -> + super + @tickets = [] + @tickets_count = 0 + @start_page = 1 + @navupdate '#' + + @fetch() + + fetch: -> + + # get data + @ajax = new App.Ajax + @ajax.ajax( + type: 'GET', + url: '/ticket_overviews', + data: { + view: @view, + view_mode: 'd', + start_page: @start_page, + } + processData: true, +# data: JSON.stringify( view: @view ), + success: (data, status, xhr) => + + # get meta data + @overview = data.overview + App.Overview.refresh( @overview, options: { clear: true } ) + + App.Overview.unbind('local:rerender') + App.Overview.bind 'local:rerender', (record) => + @log 'rerender...', record + @render() + + App.Overview.unbind('local:refetch') + App.Overview.bind 'local:refetch', (record) => + @log 'refetch...', record + @fetch() + + # load user collection + @loadCollection( type: 'User', data: data.users ) + + # load ticket collection + @loadCollection( type: 'Ticket', data: data.tickets ) + + @tickets = data.tickets + @tickets_count = data.tickets_count + + @render() + ) + + render: -> + + pages_total = parseInt( ( @tickets_count / @overview.view.d.per_page ) + 0.99999 ) || 1 + html = App.view('dashboard/ticket')( + overview: @overview, + pages_total: pages_total, + start_page: @start_page, + ) + html = $(html) + html.find('li').removeClass('active') + html.find("[data-id=\"#{@start_page}\"]").parents('li').addClass('active') + + + shown_all_attributes = @ticketTableAttributes( App.Overview.find(@overview.id).view.d.overview ) + table = @table( + overview_extended: shown_all_attributes, + model: App.Ticket, + objects: @tickets, + checkbox: false, + ) + + if _.isEmpty(@tickets) + table = '' +# table = '-none-' + + # append content table + html.find('.table-overview').append(table) + @html html + + # start user popups + @userPopups() + + zoom: (e) => + e.preventDefault() + id = $(e.target).parents('[data-id]').data('id') + @log 'goto zoom!' + @navigate 'ticket/zoom/' + id + + settings: (e) => + e.preventDefault() + new Settings( + overview: App.Overview.find(@overview.id) + ) + + page: (e) => + e.preventDefault() + id = $(e.target).data('id') + @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: 'ticket_article_type_id', display: 'Type', tag: 'select', multiple: false, null: true, relation: 'TicketArticleType', default: '9', class: 'medium', item_class: 'keepleft' }, +# { name: 'internal', display: 'Visability', tag: 'radio', default: false, null: true, options: { true: 'internal', false: 'public' }, class: 'medium', item_class: 'keepleft' }, + { + 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: 'keepleft', + }, + { + name: 'attributes', + display: 'Attributes', + tag: 'checkbox', + default: @overview.view.d.overview, + null: false, + options: { + number: 'Number', + title: 'Title', + customer: 'Customer', + ticket_state: 'State', + ticket_priority: 'Priority', + group: 'Group', + owner: 'Owner', + created_at: 'Alter', + last_contact: 'Last Contact', + last_contact_agent: 'Last Contact Agent', + last_contact_customer: 'Last Contact Customer', + first_response: 'First Response', + close_time: 'Close Time', + }, + class: 'medium', +# item_class: 'keepleft', + }, + { + name: 'order_by', + display: 'Order', + tag: 'select', + default: @overview.order.by, + null: false, + options: { + number: 'Number', + title: 'Title', + customer: 'Customer', + ticket_state: 'State', + ticket_priority: 'Priority', + group: 'Group', + owner: 'Owner', + created_at: 'Alter', + last_contact: 'Last Contact', + last_contact_agent: 'Last Contact Agent', + last_contact_customer: 'Last Contact Customer', + first_response: 'First Response', + close_time: 'Close Time', + }, + class: 'medium', + }, + { + name: 'order_by_direction', + display: 'Direction', + tag: 'select', + default: @overview.order.direction, + null: false, + options: { + ASC: 'up', + DESC: 'down', + }, + class: 'medium', + }, + ] + form = @formGen( model: { configure_attributes: @configure_attributes_article } ) + + @el.find('.setting').append(form) + + @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']['overview'] = params['attributes'] + + @overview.save( + success: => + if @reload_needed + @overview.trigger('local:refetch') + else + @overview.trigger('local:rerender') + ) + + @modalHide() diff --git a/app/assets/javascripts/app/controllers/_settings/area.js.coffee b/app/assets/javascripts/app/controllers/_settings/area.js.coffee new file mode 100644 index 000000000..1978f10c7 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_settings/area.js.coffee @@ -0,0 +1,70 @@ +$ = jQuery.sub() + +class App.SettingsArea extends App.Controller + constructor: -> + super + + # check authentication + return if !@authenticate() + + App.Setting.bind 'refresh change', @render + App.Setting.fetch() + + render: => + settings = App.Setting.all() + + html = $('
') + for setting in settings + if setting.area is @area + item = new App.SettingsAreaItem( setting: setting ) + html.append( item.el ) + + @html html + +class App.SettingsAreaItem extends App.Controller + events: + 'submit form': 'update', + + constructor: -> + super + @render() + + render: => + # defaults + for item in @setting.options['form'] + if typeof @setting.state.value is 'object' + item['default'] = @setting.state.value[item.name] + else + item['default'] = @setting.state.value + + # form + @configure_attributes = @setting.options['form'] + form = @formGen( model: { configure_attributes: @configure_attributes, className: '' }, autofocus: false ) + + # item + @html App.view('settings/item')( + setting: @setting, + form: form, + ) + + update: (e) => + e.preventDefault() + params = @formParam(e.target) + @log 'submit', @setting, params, e.target + if typeof @setting.state.value is 'object' + state = { + value: params + } + else + state = { + value: params[@setting.name] + } + + @setting['state'] = state + @setting.save( + success: => + + # login check + auth = new App.Auth + auth.loginCheck() + ) diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create.js.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create.js.coffee new file mode 100644 index 000000000..3c6b40808 --- /dev/null +++ b/app/assets/javascripts/app/controllers/agent_ticket_create.js.coffee @@ -0,0 +1,185 @@ +$ = jQuery.sub() + +class Index extends App.Controller + events: + 'click .customer_new': 'user_new' + 'submit form': 'submit', + 'click .submit': 'submit', + 'click .cancel': 'cancel', + + constructor: -> + super + + # check authentication + return if !@authenticate() + + # set title + @title 'New Ticket' +# @render() + @fetch() + @navupdate '#ticket_create' + + @edit_form = undefined + + fetch: () -> + # get data + @ajax = new App.Ajax + @ajax.ajax( + type: 'GET', + url: '/ticket_create', + data: { +# view: @view + } + processData: true, + success: (data, status, xhr) => + + # get edit form attributes + @edit_form = data.edit_form + + # load user collection + @loadCollection( type: 'User', data: data.users ) + + # render page + @render() + ) + + render: -> + + configure_attributes = [ + { name: 'customer_id', display: 'From', tag: 'autocompletion', type: 'text', limit: 100, null: false, relation: 'User', class: 'span7', autocapitalize: false, help: 'Select the customer of the Ticket or create one.', link: '»', callback: @userInfo }, + { name: 'group_id', display: 'Group', tag: 'select', multiple: false, null: false, filter: @edit_form, nulloption: true, relation: 'Group', class: 'span7', }, + { name: 'owner_id', display: 'Owner', tag: 'select', multiple: false, null: true, filter: @edit_form, nulloption: true, relation: 'User', class: 'span7', }, + { name: 'subject', display: 'Subject', tag: 'input', type: 'text', limit: 100, null: false, class: 'span7', }, + { name: 'body', display: 'Text', tag: 'textarea', rows: 6, limit: 100, null: false, class: 'span7', }, + { name: 'ticket_state_id', display: 'State', tag: 'select', multiple: false, null: false, filter: @edit_form, relation: 'TicketState', default: 'new', class: 'medium' }, + { name: 'ticket_priority_id', display: 'Priority', tag: 'select', multiple: false, null: false, filter: @edit_form, relation: 'TicketPriority', default: '2 normal', class: 'medium' }, + ] + @html App.view('agent_ticket_create')( + head: 'New Ticket', + form: @formGen( model: { configure_attributes: configure_attributes, className: 'create' } ), + ) + +# @modalShow( +# navigateBack: true +# ) + + user_new: (e) => + e.preventDefault() + new UserNew() + + cancel: -> + @render() + + submit: (e) -> + e.preventDefault() + + # get params + params = @formParam(e.target) + + # fillup params + if !params.title + params.title = params.subject + + # create ticket + object = new App.Ticket + @log 'updateAttributes', params + object.load(params) + + # validate form + errors = object.validate() + + # show errors in form + if errors + @log 'error new', errors + @validateForm( form: e.target, errors: errors ) + + # save ticket, create article + else + + # disable form + @formDisable(e) + + object.save( + success: (r) => + + # find sender_id + sender = App.TicketArticleSender.findByAttribute("name", "Customer") + type = App.TicketArticleType.findByAttribute("name", "phone") + + # create article + article = new App.TicketArticle + article.load( + from: 'some guy', + to: 'some group', + subject: params.subject, + body: params.body, + ticket_id: r.id, + ticket_article_type_id: type.id, + ticket_article_sender_id: sender.id, + ) + article.save() +# console.log('params', params) + + # notify UI + @notify + type: 'success', + msg: 'Ticket ' + r.number + ' created!' + + # create new create screen + @render() + + error: => + @log 'save failed!' + ) + + +class UserNew extends App.ControllerModal + constructor: -> + super + @render() + + render: -> + + @html App.view('agent_user_create')( + head: 'New User', + form: @formGen( model: App.User, required: 'quick' ), + ) + @modalShow() + + submit: (e) -> + @log 'submit' + e.preventDefault() + params = @formParam(e.target) + + # if no login is given, use emails as fallback + if !params.login && params.email + params.login = params.email + + user = new App.User + + # find role_id + role = App.Role.findByAttribute("name", "Customer") + params.role_ids = role.id + @log 'updateAttributes', params + user.load(params) + + errors = user.validate() + if errors + @log 'error new', errors + @validateForm( form: e.target, errors: errors ) + return + + # save user + user.save( + success: (r) => + @modalHide() + $('#create_customer_id').val(r.id) + $('#create_customer_id_autocompletion').val(r.firstname) + + # start customer info controller + @userInfo( user_id: r.id ) + error: => + @modalHide() + ) + +Config.Routes['ticket_create'] = Index \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee b/app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee new file mode 100644 index 000000000..9610282bc --- /dev/null +++ b/app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee @@ -0,0 +1,399 @@ +$ = jQuery.sub() + +class Index extends App.Controller + events: + 'click [data-type=edit]': 'zoom' + 'click [data-type=settings]': 'settings' + 'click [data-type=view-mode]': 'view_mode' + 'click [data-type=page]': 'page' + + constructor: -> + super + + # check authentication + return if !@authenticate() + + @log 'view:', @view + @view_mode = localStorage.getItem( "mode:#{@view}" ) || 's' + + # set title + @title '' + @navupdate '#ticket_view' + + @tickets = [] + @tickets_count = 0 + @start_page = 1 + @meta = {} + @bulk = {} + +# @render() + @fetch() + + fetch: -> + + # get data + @ajax = new App.Ajax + @ajax.ajax( + type: 'GET', + url: '/ticket_overviews', + data: { + view: @view, + view_mode: @view_mode, + start_page: @start_page, + } + processData: true, + success: (data, status, xhr) => + + # get meta data + @overview = data.overview + App.Overview.refresh( @overview, options: { clear: true } ) + + App.Overview.unbind('local:rerender') + App.Overview.bind 'local:rerender', (record) => + @log 'rerender...', record + @render() + + App.Overview.unbind('local:refetch') + App.Overview.bind 'local:refetch', (record) => + @log 'refetch...', record + @fetch() + + # set page title + @title @overview.meta.name + + # load user collection + @loadCollection( type: 'User', data: data.users ) + + # load ticket collection + @loadCollection( type: 'Ticket', data: data.tickets ) + + # remember ticket order + @tickets = data.tickets + + # remember ticket count + @tickets_count = data.tickets_count + + + # remeber bulk attributes + @bulk = data.bulk + + # render page + @render() + ) + + # refresh/load default collections +# for key, value of data.default_collections +# App[key].refresh( value, options: { clear: true } ) + + render: -> + + pages_total = parseInt( ( @tickets_count / @overview.view[@view_mode].per_page ) + 0.99999 ) || 1 + + # render init page + view_modes = [ + { + name: 'S', + type: 's', + class: 'active' if @view_mode is 's', + }, + { + name: 'M', + type: 'm', + class: 'active' if @view_mode is 'm', + } + ] + html = App.view('agent_ticket_view')( + overview: @overview, + view_modes: view_modes, + pages_total: pages_total, + start_page: @start_page, + checkbox: true, + ) + html = $(html) +# html.find('li').removeClass('active') + html.find("[data-id=\"#{@start_page}\"]").parents('li').addClass('active') + @html html + + # create table/overview + table = '' + if @view_mode is 'm' + table = App.view('agent_ticket_view/detail')( + overview: @overview, + objects: @tickets, + checkbox: true + ) + table = $(table) + table.delegate('[name="bulk_all"]', 'click', (e) -> + if $(e.target).attr('checked') + $(e.target).parents().find('[name="bulk"]').attr('checked', true); + else + $(e.target).parents().find('[name="bulk"]').attr('checked', false); + ) + else + shown_all_attributes = @ticketTableAttributes( App.Overview.find(@overview.id).view.s.overview ) + table = @table( + overview_extended: shown_all_attributes, + model: App.Ticket, + objects: @tickets, + checkbox: true, + ) + + # append content table + @el.find('.table-overview').append(table) + + # start user popups + @userPopups() + + # start bulk action observ + @el.find('.bulk-action').append( @bulk_form() ) + + # 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 + + # hide + @el.find('.bulk-action').addClass('hide') + else + + # show + @el.find('.bulk-action').removeClass('hide') + ) + + page: (e) => + e.preventDefault() + id = $(e.target).data('id') + @start_page = id + @fetch() + + view_mode: (e) => + e.preventDefault() + @start_page = 1 + id = $(e.target).data('mode') + @view_mode = id + localStorage.setItem( "mode:#{@view}", id ) + @fetch() + @render() + + bulk_form: => + @configure_attributes_ticket = [ + { name: 'ticket_state_id', display: 'State', tag: 'select', multiple: false, null: true, relation: 'TicketState', nulloption: true, default: '', class: 'span2', item_class: 'keepleft' }, + { name: 'ticket_priority_id', display: 'Priority', tag: 'select', multiple: false, null: true, relation: 'TicketPriority', nulloption: true, default: '', class: 'span2', item_class: 'keepleft' }, + { name: 'group_id', display: 'Group', tag: 'select', multiple: false, null: true, relation: 'Group', nulloption: true, class: 'span2', item_class: 'keepleft' }, + { name: 'owner_id', display: 'Owner', tag: 'select', multiple: false, null: true, relation: 'User', filter: @bulk, nulloption: true, class: 'span2', item_class: 'keepleft' }, + ] + form_ticket = @formGen( model: { configure_attributes: @configure_attributes_ticket, className: 'create' } ) + @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: 'cc', display: 'Cc', tag: 'input', type: 'text', limit: 100, null: true, class: 'span7', item_class: 'hide' }, + { name: 'subject', display: 'Subject', tag: 'input', type: 'text', limit: 100, null: true, class: 'span7', item_class: 'hide' }, + { name: 'in_reply_to', display: 'In Reply to', tag: 'input', type: 'text', limit: 100, null: true, class: 'span7', item_class: 'hide' }, + { name: 'body', display: 'Text', tag: 'textarea', rows: 4, limit: 100, null: true, class: 'span7', }, + { name: 'ticket_article_type_id', display: 'Type', tag: 'select', multiple: false, null: true, relation: 'TicketArticleType', default: '9', class: 'medium', item_class: 'keepleft' }, + { name: 'internal', display: 'Visability', tag: 'radio', default: false, null: true, options: { true: 'internal', false: 'public' }, class: 'medium', item_class: 'keepleft' }, +# { name: 'ticket_article_sender_id', display: 'Sender', tag: 'select', multiple: false, null: true, relation: 'TicketArticleSender', default: '', class: 'medium' }, + ] + form_article = @formGen( model: { configure_attributes: @configure_attributes_article } ) + + # render init page + html = App.view('agent_ticket_view/bulk')( + meta: @overview.meta, + checkbox: true + form_ticket: form_ticket, +# form_article: form_article, + ) + html = $(html) +# html.delegate('.bulk-action-form', 'submit', (e) => + html.bind('submit', (e) => + e.preventDefault() + @bulk_submit(e) + ) + return html + + bulk_submit: (e) => + @bulk_count = @el.find('.table-overview').find('[name="bulk"]:checked').length + @bulk_count_index = 0 + @el.find('.table-overview').find('[name="bulk"]:checked').each( (index, element) => + @log '@bulk_count_index', @bulk_count, @bulk_count_index + ticket_id = $(element).val() + ticket = App.Ticket.find(ticket_id) + params = @formParam(e.target) + + # update ticket + ticket_update = {} + for item of params + if params[item] != '' + ticket_update[item] = params[item] + +# @log 'update', params, ticket_update, ticket + + ticket.load(ticket_update) + ticket.save( + success: (r) => + @bulk_count_index++ + + # refresh view after all tickets are proceeded + if @bulk_count_index == @bulk_count + + @tickets = [] + + # rebuild navbar with updated ticket count of overviews + Spine.trigger 'navupdate_remote' + + # fetch overview data again + @fetch() + ) + ) + + zoom: (e) => + e.preventDefault() + id = $(e.target).parents('[data-id]').data('id') + @navigate 'ticket/zoom/' + id + + settings: (e) => + e.preventDefault() + new Settings( + overview: App.Overview.find(@overview.id), + view_mode: @view_mode, + ) + +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: 'ticket_article_type_id', display: 'Type', tag: 'select', multiple: false, null: true, relation: 'TicketArticleType', default: '9', class: 'medium', item_class: 'keepleft' }, +# { name: 'internal', display: 'Visability', tag: 'radio', default: false, null: true, options: { true: 'internal', false: 'public' }, class: 'medium', item_class: 'keepleft' }, + { + 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: 'keepleft', + }, + { + name: 'attributes', + display: 'Attributes', + tag: 'checkbox', + default: @overview.view[@view_mode].overview, + null: false, + options: { +# true: 'internal', +# false: 'public', + number: 'Number', + title: 'Title', + customer: 'Customer', + ticket_state: 'State', + ticket_priority: 'Priority', + group: 'Group', + owner: 'Owner', + created_at: 'Alter', + last_contact: 'Last Contact', + last_contact_agent: 'Last Contact Agent', + last_contact_customer: 'Last Contact Customer', + first_response: 'First Response', + close_time: 'Close Time', + }, + class: 'medium', + }, + { + name: 'order_by', + display: 'Order', + tag: 'select', + default: @overview.order.by, + null: false, + options: { + number: 'Number', + title: 'Title', + customer: 'Customer', + ticket_state: 'State', + ticket_priority: 'Priority', + group: 'Group', + owner: 'Owner', + created_at: 'Alter', + last_contact: 'Last Contact', + last_contact_agent: 'Last Contact Agent', + last_contact_customer: 'Last Contact Customer', + first_response: 'First Response', + close_time: 'Close Time', + }, + class: 'medium', + }, + { + name: 'order_by_direction', + display: 'Direction', + tag: 'select', + default: @overview.order.direction, + null: false, + options: { + ASC: 'up', + DESC: 'down', + }, + class: 'medium', + }, +# { +# name: 'condition', +# display: 'Conditions', +# tag: 'select', +# multiple: false, +# null: false, +# relation: 'TicketArticleType', +# default: '9', +# class: 'medium', +# item_class: 'keepleft', +# }, + ] + form = @formGen( model: { configure_attributes: @configure_attributes_article } ) + + @el.find('.setting').append(form) + + @modalShow() + + submit: (e) => + e.preventDefault() + params = @formParam(e.target) + + # check if refetch is needed + @reload_needed = 0 + if @overview.view[@view_mode]['per_page'] isnt params['per_page'] + @overview.view[@view_mode]['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[@view_mode]['overview'] = params['attributes'] + + @overview.save( + success: => + if @reload_needed + @overview.trigger('local:refetch') + else + @overview.trigger('local:rerender') + ) + @modalHide() + +Config.Routes['ticket/view/:view'] = Index diff --git a/app/assets/javascripts/app/controllers/agent_ticket_zoom.js.coffee b/app/assets/javascripts/app/controllers/agent_ticket_zoom.js.coffee new file mode 100644 index 000000000..0601517fc --- /dev/null +++ b/app/assets/javascripts/app/controllers/agent_ticket_zoom.js.coffee @@ -0,0 +1,381 @@ +$ = jQuery.sub() + +class Index extends App.Controller + events: + 'click .submit': 'update', + 'click [data-type=reply]': 'reply', + 'click [data-type=reply-all]': 'replyall', + 'click [data-type=public]': 'public_internal', + 'click [data-type=internal]': 'public_internal', + 'click [data-type=history]': 'history_view', + 'change [name="ticket_article_type_id"]': 'form_update', + + constructor: (params) -> + super + @log 'zoom', params + + # check authentication + return if !@authenticate() + + @navupdate '#' + + @edit_form = undefined +# @render() + @ticket_id = params.ticket_id + @fetch(@ticket_id) + + fetch: (ticket_id) -> + # get data + @ajax = new App.Ajax + @ajax.ajax( + type: 'GET', + url: '/ticket_full/' + ticket_id, + data: { + view: @view + } + processData: true, + success: (data, status, xhr) => + # reset old indexes + @ticket = undefined + @articles = undefined + + # get edit form attributes + @edit_form = data.edit_form + + # load user collection + @loadCollection( type: 'User', data: data.users ) + + # load ticket collection + @loadCollection( type: 'Ticket', data: [data.ticket] ) + + # load article collections + @loadCollection( type: 'TicketArticle', data: data.articles || [] ) + + # render page + @render() + ) + + render: => + + if !App.Ticket.exists(@ticket_id) + return + + # get data + if !@ticket + @ticket = App.Ticket.find(@ticket_id) + if !@articles + @articles = [] + for article_id in @ticket.article_ids + @articles.push App.TicketArticle.find(article_id) + + # check attachments + for article in @articles + if article.attachments + for attachment in article.attachments + attachment.size = @humanFileSize(attachment.size) + + # define actions + for article in @articles + actions = [] + if article.internal is true + actions = [ + { + name: 'set to public', + type: 'public', + } + ] + else + actions = [ + { + name: 'set to internal', + type: 'internal', + } + ] + if article.article_type.name is 'note' +# actions.push [] + else + if article.article_sender.name is 'Customer' + actions.push { + name: 'reply', + type: 'reply', + } + actions.push { + name: 'reply all', + type: 'reply-all', + } + article.actions = actions + + # set title + @title 'Ticket Zoom ' + @ticket.number + + @configure_attributes_ticket = [ + { name: 'ticket_state_id', display: 'State', tag: 'select', multiple: false, null: true, relation: 'TicketState', default: 'new', class: 'span2', item_class: 'keepleft' }, + { name: 'ticket_priority_id', display: 'Priority', tag: 'select', multiple: false, null: true, relation: 'TicketPriority', default: '2 normal', class: 'span2', item_class: 'keepleft' }, + { name: 'group_id', display: 'Group', tag: 'select', multiple: false, null: true, relation: 'Group', class: 'span2', item_class: 'keepleft' }, + { name: 'owner_id', display: 'Owner', tag: 'select', multiple: false, null: true, relation: 'User', filter: @edit_form, nulloption: true, class: 'span2', item_class: 'keepleft' }, + ] + form_ticket = @formGen( model: { configure_attributes: @configure_attributes_ticket, className: 'create' }, params: @ticket ) + @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: 'cc', display: 'Cc', tag: 'input', type: 'text', limit: 100, null: true, class: 'span7', item_class: 'hide' }, + { name: 'subject', display: 'Subject', tag: 'input', type: 'text', limit: 100, null: true, class: 'span7', item_class: 'hide' }, + { name: 'in_reply_to', display: 'In Reply to', tag: 'input', type: 'text', limit: 100, null: true, class: 'span7', item_class: 'hide' }, + { name: 'body', display: 'Text', tag: 'textarea', rows: 5, limit: 100, null: true, class: 'span7', }, + { name: 'ticket_article_type_id', display: 'Type', tag: 'select', multiple: false, null: true, relation: 'TicketArticleType', default: '9', class: 'medium', item_class: 'keepleft' }, + { name: 'internal', display: 'Visability', tag: 'radio', default: false, null: true, options: { true: 'internal', false: 'public' }, class: 'medium', item_class: 'keepleft' }, +# { name: 'ticket_article_sender_id', display: 'Sender', tag: 'select', multiple: false, null: true, relation: 'TicketArticleSender', default: '', class: 'medium' }, + ] + form_article = @formGen( model: { configure_attributes: @configure_attributes_article } ) + + @html App.view('agent_ticket_zoom')( + ticket: @ticket, + articles: @articles, + form_ticket: form_ticket, + form_article: form_article, + ) + + @userPopups() + + # start customer info controller + new App.UserInfo( + el: @el.find('#customer_info'), + user_id: @ticket.customer_id, + ticket: @ticket, + ) + + @delay(@u, 200) + + u: => + uploader = new qq.FileUploader( + element: document.getElementById('file-uploader'), + action: 'ticket_attachment_new', + params: { + form: 'TicketZoom', + form_id: @ticket.id, + }, + debug: false + ) + + history_view: (e) -> + e.preventDefault() + new History( ticket_id: @ticket_id ) + + public_internal: (e) -> + e.preventDefault() + article_id = $(e.target).parents('[data-id]').data('id') + + # storage update + article = App.TicketArticle.find(article_id) + internal = true + if article.internal == true + internal = false + + article.updateAttributes( + internal: internal + ) + + # runtime update + for article in @articles + if article_id is article.id + article['internal'] = internal + + @render() + + form_update: (e) -> + ticket_article_type_id = $(e.target).find('option:selected').val() + @log 'eeee', e, ticket_article_type_id + article_type = App.TicketArticleType.find( ticket_article_type_id ) + @form_update_execute(article_type) + + form_update_execute: (article_type) => + if article_type.name is 'twitter status' + + # hide to + @el.find('[name="to"]').parents('.control-group').addClass('hide') + @el.find('[name="cc"]').parents('.control-group').addClass('hide') + @el.find('[name="subject"]').parents('.control-group').addClass('hide') + + else if article_type.name is 'twitter direct-message' + + # show + @el.find('[name="to"]').parents('.control-group').removeClass('hide') + @el.find('[name="cc"]').parents('.control-group').addClass('hide') + @el.find('[name="subject"]').parents('.control-group').addClass('hide') + + else if article_type.name is 'note' + + # hide to + @el.find('[name="to"]').parents('.control-group').addClass('hide') + @el.find('[name="cc"]').parents('.control-group').addClass('hide') + @el.find('[name="subject"]').parents('.control-group').addClass('hide') + + else if article_type.name is 'email' + + # show + @el.find('[name="to"]').parents('.control-group').removeClass('hide') + @el.find('[name="cc"]').parents('.control-group').removeClass('hide') +# @el.find('[name="subject"]').parents('.control-group').removeClass('hide') + + reply: (e) => + e.preventDefault() + article_id = $(e.target).parents('[data-id]').data('id') + article = App.TicketArticle.find( article_id ) + article_type = App.TicketArticleType.find( article.ticket_article_type_id ) + customer = App.User.find( article.created_by_id ) + + @log 'reply', e, article_type + + # update form + @form_update_execute(article_type) + + # preselect article type + @el.find('[name="ticket_article_type_id"]').find('option:selected').removeAttr('selected') + @el.find('[name="ticket_article_type_id"]').find('[value="' + article_type.id + '"]').attr('selected',true) + + # empty form + @el.find('[name="to"]').val('') + @el.find('[name="cc"]').val('') + @el.find('[name="subject"]').val('') + @el.find('[name="in_reply_to"]').val('') + + if article.message_id + @el.find('[name="in_reply_to"]').val(article.message_id) + + if article_type.name is 'twitter status' + + # set to in body + to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid + @log 'c', customer + @el.find('[name="body"]').val('@' + to) + + else if article_type.name is 'twitter direct-message' + + # show to + to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid + @el.find('[name="to"]').val(to) + + else if article_type.name is 'email' + @el.find('[name="to"]').val(article.from) +# @log 'reply ', article, @el.find('[name="to"]') + + update: (e) => + e.preventDefault() + params = @formParam(e.target) + @log 'update', params, @ticket + + # update ticket + ticket_update = {} + for item in @configure_attributes_ticket + ticket_update[item.name] = params[item.name] + + # check owner assignment + if !ticket_update['owner_id'] + ticket_update['owner_id'] = 1 + + @ticket.load( ticket_update ) + @log 'update ticket', ticket_update, @ticket + + # disable form + @formDisable(e) + + @ticket.save( + success: (r) => + + # create article + if params['body'] + article = new App.TicketArticle + params.from = window.Session['firstname'] + ' ' + window.Session['lastname'] + params.ticket_id = @ticket.id + + # find sender_id + sender = App.TicketArticleSender.findByAttribute("name", "Agent") + params.ticket_article_sender_id = sender.id + @log 'updateAttributes', params, sender, sender.id + article.load(params) + article.save( + success: (r) => + @fetch(@ticket.id) + ) + else + @fetch(@ticket.id) + ) + +# errors = article.validate() +# @log 'error new', errors +# @validateForm( form: e.target, errors: errors ) + return false + + +class History extends App.ControllerModal + constructor: -> + super + @fetch(@ticket_id) + + fetch: (@ticket_id) -> + # get data + @ajax = new App.Ajax + @ajax.ajax( + type: 'GET', + url: '/ticket_history/' + ticket_id, + data: { +# view: @view + } +# processData: true, + success: (data, status, xhr) => + # remember ticket + @ticket = data.ticket + + # load user collection + @loadCollection( type: 'User', data: data.users ) + + # load ticket collection + @loadCollection( type: 'Ticket', data: [data.ticket] ) + + # load history_type collections + @loadCollection( type: 'HistoryType', data: data.history_types ) + + # load history_object collections + @loadCollection( type: 'HistoryObject', data: data.history_objects ) + + # load history_attributes collections + @loadCollection( type: 'HistoryAttribute', data: data.history_attributes ) + + # load history collections + App.History.deleteAll() + @loadCollection( type: 'History', data: data.history ) + + # render page + @render() + ) + + render: -> + + + # create table/overview + table = @table( + overview_extended: [ + { name: 'type', }, + { name: 'attribute', }, + { name: 'value_from', }, + { name: 'value_to', }, + { name: 'created_by', class: 'user-data', data: { id: 1 } }, + { name: 'created_at', callback: @humanTime }, + ], + model: App.History, + objects: App.History.all(), + ) + + + @html App.view('agent_ticket_history')( +# head: 'New User', +# form: @formGen( model: App.User, required: 'quick' ), + ) + @el.find('.table_history').append(table) + + @modalShow() + + @userPopups() + +Config.Routes['ticket/zoom/:ticket_id'] = Index +Config.Routes['ticket/zoom/:ticket_id/:article_id'] = Index \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/channel.js.coffee b/app/assets/javascripts/app/controllers/channel.js.coffee new file mode 100644 index 000000000..f773e90a1 --- /dev/null +++ b/app/assets/javascripts/app/controllers/channel.js.coffee @@ -0,0 +1,26 @@ +$ = jQuery.sub() + +class Index extends App.ControllerLevel2 + toggleable: true + + menu: [ + { name: 'Web', 'target': 'web', controller: App.ChannelWeb }, + { name: 'Mail', 'target': 'email', controller: App.ChannelEmail }, + { name: 'Chat', 'target': 'chat', controller: App.ChannelChat }, + { name: 'Twitter', 'target': 'twitter', controller: App.ChannelTwitter }, + { name: 'Facebook', 'target': 'facebook', controller: App.ChannelFacebook }, + ] + page: { + title: 'Channels', + sub_title: 'Management' + nav: '#channels', + } + + constructor: -> + super + + # render page + @render() + +Config.Routes['channels'] = Index + diff --git a/app/assets/javascripts/app/controllers/dashboard.js.coffee b/app/assets/javascripts/app/controllers/dashboard.js.coffee new file mode 100644 index 000000000..51e2d12b3 --- /dev/null +++ b/app/assets/javascripts/app/controllers/dashboard.js.coffee @@ -0,0 +1,60 @@ +$ = jQuery.sub() + +class Index extends App.Controller + + constructor: -> + super + + # check authentication + return if !@authenticate() + + # set title + @title 'Dashboard' + @navupdate '#/' + + @plugins = { + main: { + my_assigned: { + controller: App.DashboardTicket, + params: { + view: 'my_assigned', + }, + }, + all_unassigned: { + controller: App.DashboardTicket, + params: { + view: 'all_unassigned', + }, + }, + }, + side: { + activity_stream: { + controller: App.DashboardActivityStream, + }, + recent_viewed: { + controller: App.DashboardRecentViewed, + } + } + } + + # render page + @render() + + render: -> + + @html App.view('dashboard')( + head: 'Dashboard' + ) + + for area, plugins of @plugins + for name, plugin of plugins + target = area + '_' + name + @el.find('.' + area + '-overviews').append('
') + if plugin.controller + params = plugin.params || {} + params.el = @el.find( '#' + target ) + new plugin.controller( params ) + + +Config.Routes[''] = Index +Config.Routes['/'] = Index diff --git a/app/assets/javascripts/app/controllers/getting_started.js.coffee b/app/assets/javascripts/app/controllers/getting_started.js.coffee new file mode 100644 index 000000000..f9b4398a7 --- /dev/null +++ b/app/assets/javascripts/app/controllers/getting_started.js.coffee @@ -0,0 +1,74 @@ +$ = jQuery.sub() + +class Index extends App.Controller + className: 'container getstarted' + + events: + 'submit form': 'submit', + 'click .submit': 'submit', + + constructor: -> + super + + # check authentication + return if !@authenticate() + + # set title + @title 'Get Started' + + @render() + + @navupdate '#get_started' + + render: -> + @html App.view('getting_started')( + form: @formGen( model: App.User, required: 'invite_agent' ), + ) + + cancel: -> + @log 'cancel....' + @navigate 'login' + + submit: (e) -> + @log 'submit' + e.preventDefault() + @params = @formParam(e.target) + + # if no login is given, use emails as fallback + if !@params.login && @params.email + @params.login = @params.email + + # find agent role + role = App.Role.findByAttribute("name", "Agent") + @params.role_ids = role.id + + # set invite flag + @params.invite = true + + @log 'updateAttributes', @params + user = new App.User + user.load(@params) + + errors = user.validate() + if errors + @log 'error new', errors + @validateForm( form: e.target, errors: errors ) + return false + + # save user + user.save( + success: (r) => + # send email + + # clear form + @render() +# error: => +# @modalHide() + ) + +Config.Routes['getting_started'] = Index + +#class App.GetStarted extends App.Router +# routes: +# 'getting_started': Index +#Config.Controller.push App.GetStarted; \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/groups.js.coffee b/app/assets/javascripts/app/controllers/groups.js.coffee new file mode 100644 index 000000000..69e433906 --- /dev/null +++ b/app/assets/javascripts/app/controllers/groups.js.coffee @@ -0,0 +1,37 @@ +$ = jQuery.sub() + +class Index extends App.Controller + constructor: -> + super + + # check authentication + return if !@authenticate() + + new App.ControllerGenericIndex( + el: @el, + id: @id, + genericObject: App.Group, + pageData: { + title: 'Groups', + home: 'groups', + object: 'Group', + objects: 'Groups', + navupdate: '#groups', + notes: [ + 'Groups are ...' + ], + buttons: [ + { name: 'New Group', 'data-type': 'new', class: 'primary' }, + ], + }, + ) + + +Config.Routes['groups'] = Index + +#class App.Groups extends App.Router +# routes: +# 'groups/new': New +# 'groups/:id/edit': Edit +# 'groups': Index +#Config.Controller.push App.Groups \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/login.js.coffee b/app/assets/javascripts/app/controllers/login.js.coffee new file mode 100644 index 000000000..62613a4dc --- /dev/null +++ b/app/assets/javascripts/app/controllers/login.js.coffee @@ -0,0 +1,97 @@ +$ = jQuery.sub() +Note = App.Note + +$.fn.item = -> + elementID = $(@).data('id') + elementID or= $(@).parents('[data-id]').data('id') + Note.find(elementID) + +class Index extends App.Controller + events: + 'submit #login': 'login', + 'click #register': 'register' + + constructor: -> + super + @title 'Sign in' + @render() + @navupdate '#login' + + render: (data = {}) -> + @html App.view('login')(item: data) + if $(@el).find('[name="username"]').val() + $(@el).find('[name="username"]').focus() + + login: (e) -> + e.preventDefault() + e.stopPropagation(); + + @log 'submit', $(e.target) + @username = $(e.target).find('[name="username"]').val() + @password = $(e.target).find('[name="password"]').val() +# @log @username, @password + + # session create with login/password + auth = new App.Auth + auth.login( + data: { + username: @username, + password: @password, + }, + success: @success + error: @error, + ) + + success: (data, status, xhr) => + @log 'login:success', data + + # set avatar + if !data.session.image + data.session.image = 'http://placehold.it/48x48' + + # update config + for key, value of data.config + window.Config[key] = value + + # store user data + for key, value of data.session + window.Session[key] = value + + # refresh default collections + for key, value of data.default_collections + App[key].refresh( value, options: { clear: true } ) + + Spine.trigger 'navrebuild', data.session + + # add notify + Spine.trigger 'notify:removeall' + Spine.trigger 'notify', { + type: 'success', + msg: 'Login successfully! Have a nice day!', + } + + # redirect to # + @navigate '#/' + + error: (xhr, statusText, error) => + console.log 'login:error' + + # add notify + Spine.trigger 'notify:removeall' + Spine.trigger 'notify', { + type: 'warning', + msg: 'Wrong Username and Password combination.', + } + + # rerender login page + @render( + msg: 'Wrong Username and Password combination.', + username: @username + ) + +Config.Routes['login'] = Index + +#class App.Login extends App.Router +# routes: +# 'login': Index +#Config.Controller.push App.Login \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/logout.js.coffee b/app/assets/javascripts/app/controllers/logout.js.coffee new file mode 100644 index 000000000..6852a877b --- /dev/null +++ b/app/assets/javascripts/app/controllers/logout.js.coffee @@ -0,0 +1,30 @@ +$ = jQuery.sub() + +class Index extends Spine.Controller + + constructor: -> + super + + @signout() + + signout: -> + + # remove remote session + auth = new App.Auth + auth.logout() + + # remoce local session + @log 'Session', window.Session + window.Session = {} + @log 'Session', window.Session + Spine.trigger 'navrebuild' + + # redirect to login + @navigate 'login' + +Config.Routes['logout'] = Index + +#class App.Logout extends App.Router +# routes: +# 'logout': Index +#Config.Controller.push App.Logout \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/navigation.js.coffee b/app/assets/javascripts/app/controllers/navigation.js.coffee new file mode 100644 index 000000000..c877725be --- /dev/null +++ b/app/assets/javascripts/app/controllers/navigation.js.coffee @@ -0,0 +1,169 @@ +$ = jQuery.sub() +Note = App.Note + +$.fn.item = -> + elementID = $(@).data('id') + elementID or= $(@).parents('[data-id]').data('id') + Note.find(elementID) + +class App.Navigation extends Spine.Controller + events: + 'focusin [data-type=edit]': 'edit_in' + + constructor: -> + super + @log 'nav...' + @render() + + Spine.bind 'navupdate', (data) => + @update(arguments[0]) + + Spine.bind 'navrebuild', (user) => + @log 'navbarrebuild', user + @render(user) + + Spine.bind 'navupdate_remote', (user) => + @log 'navupdate_remote' + @sync + + # rerender if new overview data is there + @delay( @sync, 1800 ) + + render: (user) -> +# @log 'nav render', Config.NavBar +# @log '111', _.keys(Config.NavBar) + navbar = _.values(Config.NavBar) + + level1 = [] + dropdown = {} + + for item in navbar + if !item.parent + match = 0 + if !window.Session['roles'] + match = _.include(item.role, 'Anybody') + if window.Session['roles'] + window.Session['roles'].forEach( (role) => + if !match + match = _.include(item.role, role.name) + ) + + if match + level1.push item + + for item in navbar + if item.parent && !dropdown[ item.parent ] + dropdown[ item.parent ] = [] + + # find all childs and order + for itemSub in navbar + if itemSub.parent is item.parent + match = 0 + if !window.Session['roles'] + match = _.include(itemSub.role, 'Anybody') + if window.Session['roles'] + window.Session['roles'].forEach( (role) => + if !match + match = _.include(itemSub.role, role.name) + ) + + if match + dropdown[ item.parent ].push itemSub + + # find parent + for itemLevel1 in level1 + if itemLevel1.target is item.parent + sub = @getOrder(dropdown[ item.parent ]) + itemLevel1.child = sub + + nav = @getOrder(level1) + @html App.view('navigation')( + navbar: nav, + user: user, + ) + + getOrder: (data) -> + newlist = {} + for item in data + # check if same prio already exists + @addPrioCount newlist, item + + newlist[ item['prio'] ] = item; + + # get keys for sort order + keys = _.keys(newlist) + inorder = keys.sort(@sortit) + + # create new array with prio sort order + inordervalue = [] + for num in inorder + inordervalue.push newlist[ num ] + return inordervalue + + sortit: (a,b) -> + return(a-b) + + addPrioCount: (newlist, item) -> + if newlist[ item['prio'] ] + item['prio']++ + if newlist[ item['prio'] ] + @addPrioCount newlist, item + + update: (url) => + @el.find('li').removeClass('active') +# if url isnt '#' + @el.find("[href=\"#{url}\"]").parents('li').addClass('active') +# @el.find("[href*=\"#{url}\"]").parents('li').addClass('active') + + sync: => + + @ticket_overview() + + # auto save + every = (ms, cb) -> setInterval cb, ms + + # clear auto save + clearInterval(@intervalID) if @intervalID + + # request new data + @intervalID = every 40000, () => + @ticket_overview() + + # get data + ticket_overview: => + + # do no load and rerender if sub-menu is open + open = @el.find('.open').val() + if open isnt undefined + return + + # do no load and rerender if user is not logged in + if !window.Session['id'] + return + + @ajax = new App.Ajax + @ajax.ajax( + type: 'GET', + url: '/ticket_overviews', + data: {}, + processData: true, + success: (data, status, xhr) => + + # remove old views + for key of Config.NavBar + if Config.NavBar[key].parent is '#ticket/view' + delete Config.NavBar[key] + + # add new views + for item in data + Config.NavBar['TicketOverview' + item.url] = { + prio: item.prio, + parent: '#ticket/view', + name: item.name + ' (' + item.count + ')', + target: '#ticket/view/' + item.url, + role: ['Agent'], + } + + # rebuild navbar + Spine.trigger 'navrebuild', window.Session + ) diff --git a/app/assets/javascripts/app/controllers/network.js.coffee b/app/assets/javascripts/app/controllers/network.js.coffee new file mode 100644 index 000000000..bc09a0f41 --- /dev/null +++ b/app/assets/javascripts/app/controllers/network.js.coffee @@ -0,0 +1,71 @@ +$ = jQuery.sub() +Note = App.Note + +$.fn.item = -> + elementID = $(@).data('id') + elementID or= $(@).parents('[data-id]').data('id') + Note.find(elementID) + +class Index extends App.Controller + events: + 'click [data-type=network-new]': 'network_new' + 'click [data-type=network-edit]': 'network_edit' + 'click [data-type=network-destroy]': 'network_destory' + 'click [data-type=network-category-new]': 'network_category_new' + 'click [data-type=network-category-edit]': 'network_category_edit' + 'click [data-type=network-category-destroy]': 'network_category_destroy' + + constructor: -> + super + + # set title + @title 'Network' + @render() + @navupdate '#network' + + render: -> + networks = App.Network.all() + network_categories = App.NetworkCategory.all() + for network in networks + @log 'f', network for network in networks + + for network_category in network_categories + @log 'fc', network_category + + @html App.view('network')( + networks: App.Network.all(), + ) + + network_new: (e) -> + e.preventDefault() + new App.ControllerGenericNewWindow( + pageData: { + object: 'Network', + }, + genericObject: App.Network, + success: => + @render() + ) + + network_edit: (e) -> + e.preventDefault() + @id = $(e.target).parents('[data-id]').data('id') + new App.ControllerGenericEditWindow( + id: @id, + pageData: { + object: 'Network', + }, + genericObject: App.Network, + success: => + @render() + ) + + network_destory: (e) -> + e.preventDefault() + id = $(e.target).parents('[data-id]').data('id') + item = App.Network.find(id) + item.destroy() if confirm('Sure?') + @render() + + +Config.Routes['network'] = Index diff --git a/app/assets/javascripts/app/controllers/notify.js.coffee b/app/assets/javascripts/app/controllers/notify.js.coffee new file mode 100644 index 000000000..4376ac0c8 --- /dev/null +++ b/app/assets/javascripts/app/controllers/notify.js.coffee @@ -0,0 +1,44 @@ +$ = jQuery.sub() +#Post = App.Post + +class App.Notify extends Spine.Controller + events: + 'click .alert': 'destroy' + + className: 'container' + + constructor: -> + super + + Spine.bind 'notify', (data) => +# @log 'bind notify', data + @[data.type] data.msg + + Spine.bind 'notify:removeall', => + @log 'notify:removeall', @ + @destroyAll() + + info: (data) -> + @render( text: arguments[0], type: 'alert-info' ) + + warning: (data) -> + @render( text: arguments[0], type: 'alert-warning' ) + + error: (data) -> + @render( text: arguments[0], type: 'alert-error' ) + + success: (data) -> + @render( text: arguments[0], type: 'alert-success' ) + + render: (data) -> + notify = App.view('notify')(data: data) + @append( notify ) +# notify.html('') + + destroy: (e) -> + e.preventDefault() + $(e.target).parents('.alert').remove(); + + destroyAll: -> + $(@el).find('.alert').remove(); + diff --git a/app/assets/javascripts/app/controllers/organizations.js.coffee b/app/assets/javascripts/app/controllers/organizations.js.coffee new file mode 100644 index 000000000..572f148d5 --- /dev/null +++ b/app/assets/javascripts/app/controllers/organizations.js.coffee @@ -0,0 +1,65 @@ +$ = jQuery.sub() + +class Index extends App.Controller + constructor: -> + super + + # check authentication + return if !@authenticate() + + new App.ControllerGenericIndex( + el: @el, + id: @id, + genericObject: App.Organization, + pageData: { + title: 'Organizations', + home: 'organizations', + object: 'Organization', + objects: 'Organizations', + navupdate: '#organizations', + notes: [ + 'Organizations are for any person in the system. Agents (Owners, Resposbiles, ...) and Customers.' + ], + buttons: [ + { name: 'New Organization', 'data-type': 'new', class: 'primary' }, + ], + }, + ) + +#Config.Routes['organizations/new'] = New +#Config.Routes['organizations/:id/edit'] = Edit +Config.Routes['organizations'] = Index + + +Config.NavBar['Admin'] = { prio: 10000, parent: '', name: 'Manage', target: '#admin', role: ['Admin'] } +Config.NavBar['AdminUser'] = { prio: 1000, parent: '#admin', name: 'Users', target: '#users', role: ['Admin'] } +Config.NavBar['AdminGroup'] = { prio: 1500, parent: '#admin', name: 'Groups', target: '#groups', role: ['Admin'] } +Config.NavBar['AdminOrganization'] = { prio: 2000, parent: '#admin', name: 'Organizations', target: '#organizations', role: ['Admin'] } +Config.NavBar['AdminChannels'] = { prio: 2500, parent: '#admin', name: 'Channels', target: '#channels', role: ['Admin'] } +Config.NavBar['AdminTrigger'] = { prio: 3000, parent: '#admin', name: 'Trigger', target: '#trigger', role: ['Admin'] } +Config.NavBar['AdminScheduler'] = { prio: 3500, parent: '#admin', name: 'Scheduler', target: '#scheduler', role: ['Admin'] } + + +Config.NavBar['Note'] = { prio: 1500, parent: '', name: 'Notes', target: '#notes', role: ['Notes'] } +#Config.NavBar['Post'] = { prio: 1600, parent: '', name: 'Posts', target: '#posts', role: ['Agent'] } + +Config.NavBar['Setting'] = { prio: 20000, parent: '', name: 'Settings', target: '#settings', role: ['Admin'] } +Config.NavBar['SettingSystem'] = { prio: 1400, parent: '#settings', name: 'System', target: '#settings/system', role: ['Admin'] } +Config.NavBar['SettingSecurity'] = { prio: 1500, parent: '#settings', name: 'Security', target: '#settings/security', role: ['Admin'] } +Config.NavBar['SettingTicket'] = { prio: 1600, parent: '#settings', name: 'Ticket', target: '#settings/ticket', role: ['Admin'] } +Config.NavBar['SettingObject'] = { prio: 1700, parent: '#settings', name: 'Objects', target: '#settings/objects', role: ['Admin'] } + +Config.NavBar['Packages'] = { prio: 1800, parent: '#settings', name: 'Packages', target: '#packages', role: ['Admin'] } + + +Config.NavBar['TicketOverview'] = { prio: 1000, parent: '', name: 'Overviews', target: '#ticket/view', role: ['Agent'] } +#Config.NavBar[''] = { prio: 1000, parent: '#ticket/view', name: 'My assigned Tickets (51)', target: '#ticket/view/my_assigned', role: ['Agent'] } +#Config.NavBar[''] = { prio: 1000, parent: '#ticket/view', name: 'Unassigned Tickets (133)', target: '#ticket/view/all_unassigned', role: ['Agent'] } +#Config.NavBar[''] = { prio: 1000, parent: '#ticket/view', name: 'Escalated Tickets (0)', target: '#ticket/view/all_escalated', role: ['Agent'] } +#Config.NavBar[''] = { prio: 1000, parent: '#ticket/view', name: 'My Pending reached Tickets (2)', target: '#ticket/view/my_pending_reached', role: ['Agent'] } + + +#Config.NavBar['Network'] = { prio: 1500, parent: '', name: 'Networking', target: '#network', role: ['Anybody', 'Customer', 'Agent'] } +#Config.NavBar[''] = { prio: 1600, parent: '', name: 'anybody+agent', target: '#aa', role: ['Anybody', 'Agent'] } +#Config.NavBar[''] = { prio: 1600, parent: '', name: 'Anybody', target: '#anybody', role: ['Anybody'] } +Config.NavBar['CustomerTickets'] = { prio: 1600, parent: '', name: 'Tickets', target: '#customer_tickets', role: ['Customer'] } diff --git a/app/assets/javascripts/app/controllers/profile.js.coffee b/app/assets/javascripts/app/controllers/profile.js.coffee new file mode 100644 index 000000000..2211f0ffa --- /dev/null +++ b/app/assets/javascripts/app/controllers/profile.js.coffee @@ -0,0 +1,32 @@ +$ = jQuery.sub() +Note = App.Note + +$.fn.item = -> + elementID = $(@).data('id') + elementID or= $(@).parents('[data-id]').data('id') + Note.find(elementID) + +class Index extends App.Controller + events: + 'focusin [data-type=edit]': 'edit_in' + + constructor: -> + super + + # set title + @title 'Profile' + + @render() + + @navupdate '#profile' + + + render: -> + @html App.view('profile')() + +Config.Routes['profile'] = Index + +#class App.Profile extends App.Router +# routes: +# 'profile': Index +#Config.Controller.push App.Profile \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/scheduler.js.coffee b/app/assets/javascripts/app/controllers/scheduler.js.coffee new file mode 100644 index 000000000..59f0d973a --- /dev/null +++ b/app/assets/javascripts/app/controllers/scheduler.js.coffee @@ -0,0 +1,23 @@ +$ = jQuery.sub() + +class Index extends App.Controller + constructor: -> + super + + # check authentication + return if !@authenticate() + + # set title + @title 'Scheduler' + @navupdate '#scheduler' + + # render page + @render() + + render: -> + + @html App.view('scheduler')( + head: 'some header' + ) +Config.Routes['scheduler'] = Index + diff --git a/app/assets/javascripts/app/controllers/settings.js.coffee b/app/assets/javascripts/app/controllers/settings.js.coffee new file mode 100644 index 000000000..8e32f73ec --- /dev/null +++ b/app/assets/javascripts/app/controllers/settings.js.coffee @@ -0,0 +1,55 @@ +$ = jQuery.sub() + +class Index extends App.ControllerLevel2 + toggleable: false + toggleable: true + + constructor: -> + super + + # system + if @type is 'system' + @menu = [ + { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'System::Base' } }, + # { name: 'Language', 'target': 'language', controller: App.SettingsSystem, params: { area: 'System::Language' } }, + # { name: 'Log', 'target': 'log', controller: App.SettingsSystem, params: { area: 'System::Log' } }, + { name: 'Storage', 'target': 'storage', controller: App.SettingsArea, params: { area: 'System::Storage' } }, + ] + @page = { + title: 'System', + sub_title: 'Settings' + nav: '#settings/system', + } + + # security + if @type is 'security' + @menu = [ + { name: 'Authentication', 'target': 'auth', controller: App.SettingsArea, params: { area: 'Security::Authentication' } }, + { name: 'Password', 'target': 'password', controller: App.SettingsArea, params: { area: 'Security::Password' } }, +# { name: 'Session', 'target': 'session', controller: '' }, + ] + @page = { + title: 'Security', + sub_title: 'Settings' + nav: '#settings/security', + } + + # ticket + if @type is 'ticket' + @menu = [ + { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Ticket::Base' } }, + { name: 'Number', 'target': 'number', controller: App.SettingsArea, params: { area: 'Ticket::Number' } }, + { name: 'Sender Format', 'target': 'sender-format', controller: App.SettingsArea, params: { area: 'Ticket::SenderFormat' } }, + ] + @page = { + title: 'Ticket', + sub_title: 'Settings' + nav: '#settings/ticket', + } + + # render page + @render() + +Config.Routes['settings/:type/:target'] = Index +Config.Routes['settings/:type'] = Index + diff --git a/app/assets/javascripts/app/controllers/signup.js.coffee b/app/assets/javascripts/app/controllers/signup.js.coffee new file mode 100644 index 000000000..73e99dea8 --- /dev/null +++ b/app/assets/javascripts/app/controllers/signup.js.coffee @@ -0,0 +1,121 @@ +$ = jQuery.sub() +User = App.User + +class Index extends App.Controller + className: 'container signup' + + events: + 'submit form': 'submit', + 'click .submit': 'submit', + 'click .cancel': 'cancel', + + constructor: -> + super + + # set title + @title 'Sign up' + + @render() + + @navupdate '#signup' + + + render: -> + + # set password as required + for item in User.configure_attributes + if item.name is 'password' + item.null = false + + @html App.view('signup')( + form: @formGen( model: User, required: 'signup' ), + ) + + cancel: -> + @log 'cancel....' + @navigate 'login' + + submit: (e) -> + @log 'submit' + e.preventDefault() + @params = @formParam(e.target) + ### + for num in [1..199] + user = new User + params.login = 'login_c' + num + user.updateAttributes(params) + return false + ### + + # if no login is given, use emails as fallback + if !@params.login && @params.email + @params.login = @params.email + +# role = App.Role.findByAttribute("name", "Customer") +# @params.role_ids = role.id +# @params.role_ids = 3 + @params.role_ids = [] + @log 'updateAttributes', @params + user = new User + user.load(@params) + + errors = user.validate() + if errors + @log 'error new', errors + @validateForm( form: e.target, errors: errors ) + return false + + # save user + user.save( + success: (r) => + auth = new App.Auth + auth.login( + data: { + username: @params.login, + password: @params.password, + }, + success: @success + error: @error, + ) +# error: => +# @modalHide() + ) + + success: (data, status, xhr) => + @log 'login:success', data + + # login check + auth = new App.Auth + auth.loginCheck() + + # add notify + Spine.trigger 'notify:removeall' + @notify + type: 'success', + msg: 'Thanks for joining. Email sent to "' + @params.email + '". Please verify your email address.' + + # redirect to # + @navigate '#' + + error: (xhr, statusText, error) => + console.log 'login:error' + + # add notify + Spine.trigger 'notify:removeall' + Spine.trigger 'notify', { + type: 'warning', + msg: 'Wrong Username and Password combination.', + } + + # rerender login page + @render( + msg: 'Wrong Username and Password combination.', + username: @username + ) + +Config.Routes['signup'] = Index + +#class App.SignUp extends App.Router +# routes: +# 'signup': Index +#Config.Controller.push App.SignUp \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/trigger.js.coffee b/app/assets/javascripts/app/controllers/trigger.js.coffee new file mode 100644 index 000000000..612cf01a6 --- /dev/null +++ b/app/assets/javascripts/app/controllers/trigger.js.coffee @@ -0,0 +1,34 @@ +$ = jQuery.sub() + +class Index extends App.Controller + constructor: -> + super + + # check authentication + return if !@authenticate() + + # set title + @title 'Triggers' + @navupdate '#trigger' + + # render page + @render() + + render: -> + + @html App.view('trigger')( + head: 'some header' + ) +Config.Routes['trigger'] = Index + +#class App.Triggers extends App.Router +# routes: +# 'triggers/web': New +# 'triggers/email': New +# 'triggers/twitter': New +# 'triggers/facebook': New +# 'triggers/new': New +# 'triggers/:id/edit': Edit +# 'triggers': Index +# +#Config.Controller.push App.Triggers \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/user_info.js.coffee b/app/assets/javascripts/app/controllers/user_info.js.coffee new file mode 100644 index 000000000..042103a12 --- /dev/null +++ b/app/assets/javascripts/app/controllers/user_info.js.coffee @@ -0,0 +1,68 @@ +$ = jQuery.sub() + +class App.UserInfo extends App.Controller + events: + 'focusout [data-type=edit]': 'update', + + constructor: -> + super + + # fetch item on demand + fetch_needed = 1 + if App.User.exists(@user_id) + @user = App.User.find(@user_id) + @log 'exists', @user + fetch_needed = 0 + @render() + + if fetch_needed + @reload(@user_id) + + reload: (user_id) => + App.User.bind 'refresh', => + @log 'loading....', user_id + @user = App.User.find(user_id) + @render() + App.User.unbind 'refresh' + App.User.fetch( id: user_id ) + + render: -> + + # define links to linked accounts + if @user['accounts'] + for account of @user['accounts'] + if account == 'twitter' + @user['accounts'][account]['link'] = 'http://twitter.com/' + @user['accounts'][account]['username'] + if account == 'facebook' + @user['accounts'][account]['link'] = 'https://www.facebook.com/profile.php?id=' + @user['accounts'][account]['uid'] + + # set default image url + if !@user.image + @user.image = 'http://placehold.it/48x48' + + # get display data + data = [] + for item in App.User.configure_attributes + if item.name isnt 'firstname' + if item.name isnt 'lastname' + if item.info #&& ( @user[item.name] || item.name isnt 'note' ) + data.push item + + # insert data + @html App.view('user_info')( + user: @user, + data: data, + ) + + @userTicketPopups( + selector: '.user-tickets', + user_id: @user.id, + ) + + update: (e) => + + # update changes + note = $(e.target).parent().find('[data-type=edit]').val() + if @user.note isnt note + @user.updateAttributes( note: note ) + @log 'update', e, note, @user diff --git a/app/assets/javascripts/app/controllers/users.js.coffee b/app/assets/javascripts/app/controllers/users.js.coffee new file mode 100644 index 000000000..22170ac7b --- /dev/null +++ b/app/assets/javascripts/app/controllers/users.js.coffee @@ -0,0 +1,39 @@ +$ = jQuery.sub() + +class Index extends App.Controller + constructor: -> + super + + # check authentication + return if !@authenticate() + + new App.ControllerGenericIndex( + el: @el, + id: @id, + genericObject: App.User, + ignoreObjectIDs: [1], + pageData: { + title: 'Users', + home: 'users', + object: 'User', + objects: 'Users', + navupdate: '#users', + notes: [ + 'Users are for any person in the system. Agents (Owners, Resposbiles, ...) and Customers.' + ], + buttons: [ +# { name: 'List', 'data-type': '', class: 'active' }, + { name: 'New User', 'data-type': 'new', class: 'primary' }, + ], + } + ) + +Config.Routes['users'] = Index + +#class App.Users extends App.Router +# routes: +# 'users/new': New +# 'users/:id/edit': Edit +# 'users': Index +# +#Config.Controller.push App.Users diff --git a/app/assets/javascripts/app/index.js.coffee b/app/assets/javascripts/app/index.js.coffee new file mode 100644 index 000000000..6f137d4f1 --- /dev/null +++ b/app/assets/javascripts/app/index.js.coffee @@ -0,0 +1,195 @@ +#s#= require json2 +#= require jquery + +#= require ./lib/spine/spine.coffee +#= require ./lib/spine/ajax.coffee +#= require ./lib/spine/route.coffee +#not_userd= require ./lib/spine/manager.coffee + +#= require ./lib/bootstrap-dropdown.js +#= require ./lib/bootstrap-tooltip.js +#= require ./lib/bootstrap-popover.js +#= require ./lib/bootstrap-modal.js +#= require ./lib/bootstrap-tab.js + +#= require ./lib/underscore.coffee +#= require ./lib/ba-linkify.js +#= require ./lib/ui/jquery.ui.core.js +#= require ./lib/ui/jquery.ui.widget.js +#= require ./lib/ui/jquery.ui.position.js +#= require ./lib/ui/jquery.ui.autocomplete.js +#not_used= require_tree ./lib/uis +#not_used= require ./lib/jquery.autocomplete.js +#= require ./lib/jquery.tagsinput.js +#= require ./lib/fileuploader.js + +#not_used= require_tree ./lib +#= require_self +#= require_tree ./models +#= require_tree ./controllers +#= require_tree ./views + +class App extends Spine.Controller + @view: (name) -> + JST["app/views/#{name}"] + +### +class App.Config extends Spine.Module + constructor: -> + super + @config = {} + + set: (key, value) => + @config[key] = value + + get: (key) => + @config[key] + + append: (key, value) => + if !@config[key] + @config[key] = [] + + @config[key].push = value + + +Config2 = new App.Config +Config2.set( 'a', 123) +console.log '1112222', Config2.get( 'a') +### + +class App.Ajax + defaults: + contentType: 'application/json' + dataType: 'json' + processData: false + headers: {'X-Requested-With': 'XMLHttpRequest'} + cache: false + + ajax: (params, defaults) -> + $.ajax($.extend({}, @defaults, defaults, params)) + +class App.Auth extends App.Ajax + constructor: -> + console.log 'auth' + + login: (params) -> + console.log 'login(...)', params + @ajax( +# params, + type: 'POST', + url: '/signin', + data: JSON.stringify(params.data), + success: params.success, + error: params.error, + ) + + loginCheck: -> + console.log 'loginCheck(...)' + @ajax( + async: false, + type: 'GET', + url: '/signshow', + success: (data, status, xhr) => + console.log 'logincheck:success', data + + # set avatar + if !data.session.image + data.session.image = 'http://placehold.it/48x48' + + # update config + for key, value of data.config + window.Config[key] = value + + # store user data + for key, value of data.session + window.Session[key] = value + + # refresh/load default collections + for key, value of data.default_collections + App[key].refresh( value, options: { clear: true } ) + + # rebuild navbar with new navbar items + Spine.trigger 'navrebuild', data.session + + # rebuild navbar with updated ticket count of overviews + Spine.trigger 'navupdate_remote' + + + error: (xhr, statusText, error) => + console.log 'loginCheck:error'#, error, statusText, xhr.statusCode + + # empty session + window.Session = {} + + ) + + logout: -> + console.log 'logout(...)' + @ajax( + type: 'DELETE', + url: '/signout', + ) + +class App.Run extends Spine.Controller + constructor: -> + super + @log 'RUN app'#, @ + @el = $('#app') + + # start navigation controller + new App.Navigation( el: @el.find('#navigation') ); + + # check if session already exists/try to get session data from server + auth = new App.Auth + auth.loginCheck() + + # start notify controller + new App.Notify( el: @el.find('#notify') ); + + # start content + new App.Content( el: @el.find('#content') ); + + +#class App.Content extends Spine.Stack +class App.Content extends Spine.Controller + className: 'container' + + constructor: -> +# @controllers = {} +# @routes = {} +# @default = '/' +# for route, controller of Config.Routes +## @log 'route,controller', route, controller +# @controllers[route] = controller +# @routes[route] = route + + super + @log 'RUN content'#, @ + + for route, callback of Config.Routes +# @log 'route,controller', route#, controller + do (route, callback) => + @route(route, (params) -> +# @log 'routing...', route + Config['ActiveController'] = route + + # unbind in controller area + @el.unbind() + @el.undelegate() + + params.el = @el + params.auth = @auth + new callback( params ) + + # scroll to top +# window.scrollTo(0,0) + ) + +# for name, object of Config.Controller +## @log 'new', object, @el +# new object( el: @el, auth: @auth ) + + Spine.Route.setup() + +window.App = App + diff --git a/app/assets/javascripts/app/lib/.gitkeep b/app/assets/javascripts/app/lib/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/app/assets/javascripts/app/lib/ba-linkify.js b/app/assets/javascripts/app/lib/ba-linkify.js new file mode 100644 index 000000000..293dd67af --- /dev/null +++ b/app/assets/javascripts/app/lib/ba-linkify.js @@ -0,0 +1,179 @@ +/*! + * linkify - v0.3 - 6/27/2009 + * http://benalman.com/code/test/js-linkify/ + * + * Copyright (c) 2009 "Cowboy" Ben Alman + * Licensed under the MIT license + * http://benalman.com/about/license/ + * + * Some regexps adapted from http://userscripts.org/scripts/review/7122 + */ + +// Turn text into linkified html. +// +// var html = linkify( text, options ); +// +// options: +// +// callback (Function) - default: undefined - if defined, this will be called +// for each link- or non-link-chunk with two arguments, text and href. If the +// chunk is non-link, href will be omitted. +// +// punct_regexp (RegExp | Boolean) - a RegExp that can be used to trim trailing +// punctuation from links, instead of the default. +// +// This is a work in progress, please let me know if (and how) it fails! + +window.linkify = (function(){ + var + SCHEME = "[a-z\\d.-]+://", + IPV4 = "(?:(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.){3}(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])", + HOSTNAME = "(?:(?:[^\\s!@#$%^&*()_=+[\\]{}\\\\|;:'\",.<>/?]+)\\.)+", + TLD = "(?:ac|ad|aero|ae|af|ag|ai|al|am|an|ao|aq|arpa|ar|asia|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|biz|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|cat|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|coop|com|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|edu|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|info|int|in|io|iq|ir|is|it|je|jm|jobs|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mil|mk|ml|mm|mn|mobi|mo|mp|mq|mr|ms|mt|museum|mu|mv|mw|mx|my|mz|name|na|nc|net|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pro|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tel|tf|tg|th|tj|tk|tl|tm|tn|to|tp|travel|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|xn--0zwm56d|xn--11b5bs3a9aj6g|xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--g6w251d|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--jxalpdlp|xn--kgbechtv|xn--zckzah|ye|yt|yu|za|zm|zw)", + HOST_OR_IP = "(?:" + HOSTNAME + TLD + "|" + IPV4 + ")", + PATH = "(?:[;/][^#?<>\\s]*)?", + QUERY_FRAG = "(?:\\?[^#<>\\s]*)?(?:#[^<>\\s]*)?", + URI1 = "\\b" + SCHEME + "[^<>\\s]+", + URI2 = "\\b" + HOST_OR_IP + PATH + QUERY_FRAG + "(?!\\w)", + + MAILTO = "mailto:", + EMAIL = "(?:" + MAILTO + ")?[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@" + HOST_OR_IP + QUERY_FRAG + "(?!\\w)", + + URI_RE = new RegExp( "(?:" + URI1 + "|" + URI2 + "|" + EMAIL + ")", "ig" ), + SCHEME_RE = new RegExp( "^" + SCHEME, "i" ), + + quotes = { + "'": "`", + '>': '<', + ')': '(', + ']': '[', + '}': '{', + '»': '«', + '›': '‹' + }, + + default_options = { + callback: function( text, href ) { +// return href ? '' + text + '<\/a>' : text; + return href ? '' + text + '<\/a>' : text; + }, + punct_regexp: /(?:[!?.,:;'"]|(?:&|&)(?:lt|gt|quot|apos|raquo|laquo|rsaquo|lsaquo);)$/ + }; + + return function( txt, options ) { + options = options || {}; + + // me + txt = txt + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + // me + // Temp variables. + var arr, + i, + link, + href, + + // Output HTML. + html = '', + + // Store text / link parts, in order, for re-combination. + parts = [], + + // Used for keeping track of indices in the text. + idx_prev, + idx_last, + idx, + link_last, + + // Used for trimming trailing punctuation and quotes from links. + matches_begin, + matches_end, + quote_begin, + quote_end; + + // Initialize options. + for ( i in default_options ) { + if ( options[ i ] === undefined ) { + options[ i ] = default_options[ i ]; + } + } + + // Find links. + while ( arr = URI_RE.exec( txt ) ) { + + link = arr[0]; + idx_last = URI_RE.lastIndex; + idx = idx_last - link.length; + + // Not a link if preceded by certain characters. + if ( /[\/:]/.test( txt.charAt( idx - 1 ) ) ) { + continue; + } + + // Trim trailing punctuation. + do { + // If no changes are made, we don't want to loop forever! + link_last = link; + + quote_end = link.substr( -1 ) + quote_begin = quotes[ quote_end ]; + + // Ending quote character? + if ( quote_begin ) { + matches_begin = link.match( new RegExp( '\\' + quote_begin + '(?!$)', 'g' ) ); + matches_end = link.match( new RegExp( '\\' + quote_end, 'g' ) ); + + // If quotes are unbalanced, remove trailing quote character. + if ( ( matches_begin ? matches_begin.length : 0 ) < ( matches_end ? matches_end.length : 0 ) ) { + link = link.substr( 0, link.length - 1 ); + idx_last--; + } + } + + // Ending non-quote punctuation character? + if ( options.punct_regexp ) { + link = link.replace( options.punct_regexp, function(a){ + idx_last -= a.length; + return ''; + }); + } + } while ( link.length && link !== link_last ); + + href = link; + + // Add appropriate protocol to naked links. + if ( !SCHEME_RE.test( href ) ) { + href = ( href.indexOf( '@' ) !== -1 ? ( !href.indexOf( MAILTO ) ? '' : MAILTO ) + : !href.indexOf( 'irc.' ) ? 'irc://' + : !href.indexOf( 'ftp.' ) ? 'ftp://' + : 'http://' ) + + href; + } + + // Push preceding non-link text onto the array. + if ( idx_prev != idx ) { + parts.push([ txt.slice( idx_prev, idx ) ]); + idx_prev = idx_last; + } + + // Push massaged link onto the array + parts.push([ link, href ]); + }; + + // Push remaining non-link text onto the array. + parts.push([ txt.substr( idx_prev ) ]); + + // Process the array items. + for ( i = 0; i < parts.length; i++ ) { + html += options.callback.apply( window, parts[i] ); + } + + // In case of catastrophic failure, return the original text; + return html || txt; + }; + +})(); \ No newline at end of file diff --git a/app/assets/javascripts/app/lib/bootstrap-dropdown.js b/app/assets/javascripts/app/lib/bootstrap-dropdown.js new file mode 100644 index 000000000..48d3ce0f8 --- /dev/null +++ b/app/assets/javascripts/app/lib/bootstrap-dropdown.js @@ -0,0 +1,92 @@ +/* ============================================================ + * bootstrap-dropdown.js v2.0.0 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function( $ ){ + + "use strict" + + /* DROPDOWN CLASS DEFINITION + * ========================= */ + + var toggle = '[data-toggle="dropdown"]' + , Dropdown = function ( element ) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function () { + $el.parent().removeClass('open') + }) + } + + Dropdown.prototype = { + + constructor: Dropdown + + , toggle: function ( e ) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + , isActive + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + $parent.length || ($parent = $this.parent()) + + isActive = $parent.hasClass('open') + + clearMenus() + !isActive && $parent.toggleClass('open') + + return false + } + + } + + function clearMenus() { + $(toggle).parent().removeClass('open') + } + + + /* DROPDOWN PLUGIN DEFINITION + * ========================== */ + + $.fn.dropdown = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('dropdown') + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + /* APPLY TO STANDARD DROPDOWN ELEMENTS + * =================================== */ + + $(function () { + $('html').on('click.dropdown.data-api', clearMenus) + $('body').on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle) + }) + +}( window.jQuery ) diff --git a/app/assets/javascripts/app/lib/bootstrap-modal.js b/app/assets/javascripts/app/lib/bootstrap-modal.js new file mode 100644 index 000000000..ba64368b2 --- /dev/null +++ b/app/assets/javascripts/app/lib/bootstrap-modal.js @@ -0,0 +1,209 @@ +/* ========================================================= + * bootstrap-modal.js v2.0.0 + * http://twitter.github.com/bootstrap/javascript.html#modals + * ========================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + +!function( $ ){ + + "use strict" + + /* MODAL CLASS DEFINITION + * ====================== */ + + var Modal = function ( content, options ) { + this.options = $.extend({}, $.fn.modal.defaults, options) + this.$element = $(content) + .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) + } + + Modal.prototype = { + + constructor: Modal + + , toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + + if (this.isShown) return + + $('body').addClass('modal-open') + + this.isShown = true + this.$element.trigger('show') + + escape.call(this) + backdrop.call(this, function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + !that.$element.parent().length && that.$element.appendTo(document.body) //don't move modals dom position + + that.$element + .show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element.addClass('in') + + transition ? + that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : + that.$element.trigger('shown') + + }) + } + + , hide: function ( e ) { + e && e.preventDefault() + + if (!this.isShown) return + + var that = this + this.isShown = false + + $('body').removeClass('modal-open') + + escape.call(this) + + this.$element + .trigger('hide') + .removeClass('in') + + $.support.transition && this.$element.hasClass('fade') ? + hideWithTransition.call(this) : + hideModal.call(this) + } + + } + + + /* MODAL PRIVATE METHODS + * ===================== */ + + function hideWithTransition() { + var that = this + , timeout = setTimeout(function () { + that.$element.off($.support.transition.end) + hideModal.call(that) + }, 500) + + this.$element.one($.support.transition.end, function () { + clearTimeout(timeout) + hideModal.call(that) + }) + } + + function hideModal( that ) { + this.$element + .hide() + .trigger('hidden') + + backdrop.call(this) + } + + function backdrop( callback ) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('