Initial Redmine tooling and local plugin forks
@@ -0,0 +1,90 @@
|
||||
pipeline:
|
||||
tests:
|
||||
image: redmineup/redmine_contacts_helpdesk
|
||||
commands:
|
||||
- service postgresql start && service mysql start && sleep 5
|
||||
- export PATH=~/.rbenv/shims:$PATH
|
||||
- export CODEPATH=`pwd`
|
||||
- /root/run_for.sh redmine_contacts_helpdesk ${RUBY_VER} ${DB} ${REDMINE} redmine_contacts+${LICENSE}
|
||||
matrix:
|
||||
include:
|
||||
- RUBY_VER: ruby-2.2.6
|
||||
LICENSE: pro
|
||||
DB: mysql
|
||||
REDMINE: redmine-3.3
|
||||
- RUBY_VER: ruby-2.2.6
|
||||
LICENSE: light
|
||||
DB: mysql
|
||||
REDMINE: redmine-3.3
|
||||
- RUBY_VER: ruby-2.2.6
|
||||
LICENSE: pro
|
||||
DB: pg
|
||||
REDMINE: redmine-3.3
|
||||
- RUBY_VER: ruby-2.2.6
|
||||
LICENSE: light
|
||||
DB: pg
|
||||
REDMINE: redmine-3.3
|
||||
- RUBY_VER: ruby-2.2.6
|
||||
LICENSE: pro
|
||||
DB: mysql
|
||||
REDMINE: redmine-3.0
|
||||
- RUBY_VER: ruby-1.8.7
|
||||
DB: pg
|
||||
LICENSE: pro
|
||||
REDMINE: redmine-2.3
|
||||
- RUBY_VER: ruby-1.8.7
|
||||
DB: pg
|
||||
LICENSE: pro
|
||||
REDMINE: redmine-2.6
|
||||
- RUBY_VER: ruby-1.8.7
|
||||
DB: mysql
|
||||
LICENSE: pro
|
||||
REDMINE: redmine-2.3
|
||||
- RUBY_VER: ruby-1.8.7
|
||||
DB: mysql
|
||||
LICENSE: pro
|
||||
REDMINE: redmine-2.6
|
||||
- RUBY_VER: ruby-1.9.3
|
||||
DB: pg
|
||||
LICENSE: pro
|
||||
REDMINE: redmine-2.3
|
||||
- RUBY_VER: ruby-1.9.3
|
||||
DB: pg
|
||||
LICENSE: pro
|
||||
REDMINE: redmine-2.6
|
||||
- RUBY_VER: ruby-1.9.3
|
||||
DB: pg
|
||||
LICENSE: pro
|
||||
REDMINE: redmine-3.3
|
||||
- RUBY_VER: ruby-1.9.3
|
||||
DB: mysql
|
||||
LICENSE: pro
|
||||
REDMINE: redmine-3.3
|
||||
- RUBY_VER: ruby-1.9.3
|
||||
DB: pg
|
||||
LICENSE: light
|
||||
REDMINE: redmine-3.3
|
||||
- RUBY_VER: ruby-1.8.7
|
||||
DB: pg
|
||||
LICENSE: light
|
||||
REDMINE: redmine-2.3
|
||||
- RUBY_VER: ruby-1.8.7
|
||||
DB: pg
|
||||
LICENSE: light
|
||||
REDMINE: redmine-2.6
|
||||
# - RUBY_VER: ruby-2.4.1
|
||||
# DB: mysql
|
||||
# LICENSE: pro
|
||||
# REDMINE: redmine-trunk
|
||||
# - RUBY_VER: ruby-2.4.1
|
||||
# DB: mysql
|
||||
# LICENSE: light
|
||||
# REDMINE: redmine-trunk
|
||||
# - RUBY_VER: ruby-2.4.1
|
||||
# DB: pg
|
||||
# LICENSE: pro
|
||||
# REDMINE: redmine-trunk
|
||||
# - RUBY_VER: ruby-2.2.6
|
||||
# DB: mysql
|
||||
# LICENSE: pro
|
||||
# REDMINE: redmine-trunk
|
||||
@@ -0,0 +1,26 @@
|
||||
# Local Change Log
|
||||
|
||||
This RedmineUP helpdesk plugin is maintained as local legacy code for the
|
||||
installed Redmine 3.4.4 environment. Keep entries focused on local behavior,
|
||||
rollback archives, and LAN test status.
|
||||
|
||||
## 2026-04-21 - Helpdesk Search Read API And Outbox Coverage
|
||||
|
||||
- Purpose: make helpdesk ticket and message identity first-class for external
|
||||
operational search and indexing.
|
||||
- Rollback archives:
|
||||
- `dist/redmine_contacts-4.1.2-local-before-helpdesk-search-20260421T215548Z.tar.gz`
|
||||
- `dist/redmine_contacts_helpdesk-3.0.9-local-before-helpdesk-search-20260421T215548Z.tar.gz`
|
||||
- Touched behavior:
|
||||
- Added read-only JSON endpoints under `helpdesk_search/*`.
|
||||
- Extended the existing `view_helpdesk_tickets` permission to cover those
|
||||
endpoints.
|
||||
- Added event outbox callbacks for `HelpdeskTicket` and `JournalMessage` in
|
||||
the local `redmine_event_outbox` plugin.
|
||||
- Payload policy:
|
||||
- Includes ids, direction, message id, source, and non-body address metadata.
|
||||
- Does not include full email bodies, private notes, attachment data, or BCC
|
||||
addresses.
|
||||
- LAN test result: pending. Validate by creating/updating a controlled helpdesk
|
||||
ticket and journal message, then checking `event_outbox_events` and the
|
||||
`helpdesk_search/*` JSON endpoints on the LAN Redmine copy.
|
||||
@@ -0,0 +1,106 @@
|
||||
class CannedResponsesController < ApplicationController
|
||||
unloadable
|
||||
|
||||
before_filter :find_canned_response, :except => [:new, :create, :index]
|
||||
before_filter :find_optional_project, :only => [:new, :create, :add, :destroy]
|
||||
before_filter :find_issue, :only => [:add]
|
||||
before_filter :require_admin, :only => [:index]
|
||||
|
||||
accept_api_auth :index
|
||||
|
||||
def index
|
||||
case params[:format]
|
||||
when 'xml', 'json'
|
||||
@offset, @limit = api_offset_and_limit
|
||||
else
|
||||
@limit = per_page_option
|
||||
end
|
||||
|
||||
scope = CannedResponse.visible
|
||||
scope = scope.in_project_or_public(@project) if @project
|
||||
|
||||
@canned_response_count = scope.count
|
||||
@canned_response_pages = Paginator.new @canned_response_count, @limit, params['page']
|
||||
@offset ||= @canned_response_pages.offset
|
||||
@canned_responses = scope.limit(@limit).offset(@offset).order("#{CannedResponse.table_name}.name")
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
end
|
||||
end
|
||||
|
||||
def add
|
||||
@content = HelpdeskMailer.apply_macro(@canned_response.content, @issue.customer, @issue, User.current)
|
||||
end
|
||||
|
||||
def new
|
||||
@canned_response = CannedResponse.new
|
||||
@canned_response.user = User.current
|
||||
@canned_response.project = @project
|
||||
@canned_response.is_public = false unless User.current.allowed_to?(:manage_public_canned_responses, @project) || User.current.admin?
|
||||
end
|
||||
|
||||
def create
|
||||
@canned_response = CannedResponse.new(params[:canned_response])
|
||||
@canned_response.user = User.current
|
||||
@canned_response.project = params[:canned_response_is_for_all] ? nil : @project
|
||||
@canned_response.is_public = false unless User.current.allowed_to?(:manage_public_canned_responses, @project) || User.current.admin?
|
||||
|
||||
if @canned_response.save
|
||||
flash[:notice] = l(:notice_successful_create)
|
||||
redirect_to_project_or_global
|
||||
else
|
||||
render :action => 'new', :layout => !request.xhr?
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@canned_response.attributes = params[:canned_response]
|
||||
@canned_response.project = nil if params[:canned_response_is_for_all]
|
||||
@canned_response.is_public = false unless User.current.allowed_to?(:manage_public_canned_responses, @project) || User.current.admin?
|
||||
|
||||
if @canned_response.save
|
||||
flash[:notice] = l(:notice_successful_update)
|
||||
redirect_to_project_or_global
|
||||
else
|
||||
render :action => 'edit'
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@canned_response.destroy
|
||||
redirect_to_project_or_global
|
||||
end
|
||||
|
||||
private
|
||||
def redirect_to_project_or_global
|
||||
redirect_to @project ? settings_project_path(@project, :tab => 'helpdesk_canned_responses') : path_to_global_setting
|
||||
end
|
||||
|
||||
def path_to_global_setting
|
||||
{
|
||||
:action =>"plugin",
|
||||
:id => "redmine_contacts_helpdesk",
|
||||
:controller => "settings",
|
||||
:tab => 'canned_responses'
|
||||
}
|
||||
end
|
||||
|
||||
def find_issue
|
||||
@issue = Issue.find(params[:issue_id])
|
||||
@project = @issue.project
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def find_canned_response
|
||||
@canned_response = CannedResponse.find(params[:id])
|
||||
@project = @canned_response.project
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,244 @@
|
||||
class HelpdeskController < ApplicationController
|
||||
unloadable
|
||||
|
||||
before_filter :find_project, :authorize, :except => [:email_note, :update_customer_email]
|
||||
|
||||
accept_api_auth :email_note, :create_ticket
|
||||
|
||||
def save_settings
|
||||
if request.put?
|
||||
set_settings
|
||||
flash[:notice] = l(:notice_successful_update)
|
||||
end
|
||||
|
||||
redirect_to :controller => 'projects', :action => 'settings', :tab => params[:tab] || 'helpdesk', :id => @project
|
||||
end
|
||||
|
||||
def show_original
|
||||
@attachment = Attachment.find(params[:id])
|
||||
email = Mail.read(@attachment.diskfile)
|
||||
part = email.text_part || email.html_part || email
|
||||
body_charset = Mail::RubyVer.pick_encoding(part.charset).to_s rescue part.charset
|
||||
plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, body_charset)
|
||||
headers = email.header.fields.map{|f| "#{f.name}: #{Mail::Encodings.unquote_and_convert_to(f.value, 'utf-8')}"}.join("\n")
|
||||
@content = headers + "\n\n" + plain_text_body
|
||||
|
||||
render "attachments/file"
|
||||
end
|
||||
|
||||
def delete_spam
|
||||
if User.current.allowed_to?(:delete_issues, @project) && User.current.allowed_to?(:delete_contacts, @project)
|
||||
begin
|
||||
@issue = Issue.find(params[:issue_id])
|
||||
@customer = @issue.customer
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
ContactsSetting["helpdesk_blacklist", @project.id] = (ContactsSetting["helpdesk_blacklist", @project.id].split("\n") | [@issue.customer.primary_email.strip]).join("\n")
|
||||
@customer.tickets.map(&:destroy)
|
||||
@customer.destroy
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_default(:controller => 'issues', :action => 'index', :project_id => @project) }
|
||||
format.api { render_api_ok }
|
||||
end
|
||||
|
||||
else
|
||||
deny_access
|
||||
end
|
||||
end
|
||||
|
||||
def email_note
|
||||
raise Exception, "Param 'message' should be set" unless params[:message]
|
||||
@issue = Issue.find(params[:message][:issue_id])
|
||||
|
||||
raise Exception, "Issue with ID: #{params[:message][:issue_id].to_i} should be present and relate to customer" if @issue.nil? || @issue.customer.nil?
|
||||
|
||||
|
||||
@journal = @issue.init_journal(User.current)
|
||||
@issue.status_id = params[:message][:status_id] if params[:message][:status_id].blank? && IssueStatus.find_by_id(params[:message][:status_id])
|
||||
@journal.notes = params[:message][:content]
|
||||
@issue.save!
|
||||
|
||||
contact = @issue.customer
|
||||
|
||||
HelpdeskMailer.with_activated_perform_deliveries do
|
||||
if HelpdeskMailer.issue_response(contact, @journal, params).deliver
|
||||
|
||||
@journal_message = JournalMessage.create(:from_address => "",
|
||||
:to_address => contact.primary_email.downcase,
|
||||
:is_incoming => false,
|
||||
:message_date => Time.now,
|
||||
:contact => contact,
|
||||
:journal => @journal)
|
||||
end
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.api { render :action => 'show', :status => :created }
|
||||
end
|
||||
|
||||
rescue Exception => e
|
||||
respond_to do |format|
|
||||
format.api do
|
||||
@error_messages = [e.message]
|
||||
render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_ticket
|
||||
raise Exception, "Param 'ticket' should be set" if params[:ticket].blank?
|
||||
@issue = Issue.new
|
||||
@issue.project = @project
|
||||
@issue.author ||= User.current
|
||||
@issue.safe_attributes = params[:ticket][:issue]
|
||||
raise Exception, "Contact should have email address" unless params[:ticket][:contact] || params[:ticket][:contact][:email]
|
||||
|
||||
@contact = Contact.find_by_emails([params[:ticket][:contact][:email]]).first
|
||||
@contact ||= Contact.new(params[:ticket][:contact])
|
||||
@contact.projects << @project
|
||||
|
||||
helpdesk_ticket = HelpdeskTicket.new(:from_address => @contact.primary_email,
|
||||
:to_address => '',
|
||||
:ticket_date => Time.now,
|
||||
:customer => @contact,
|
||||
:is_incoming => true,
|
||||
:issue => @issue,
|
||||
:source => HelpdeskTicket::HELPDESK_WEB_SOURCE)
|
||||
|
||||
@issue.helpdesk_ticket = helpdesk_ticket
|
||||
@issue.assigned_to = @contact.find_assigned_user(@project, @issue.assigned_to)
|
||||
@issue.save_attachments(params[:attachments] || (params[:ticket][:issue] && params[:ticket][:issue][:uploads]))
|
||||
if @issue.save
|
||||
HelpdeskMailer.auto_answer(@contact, @issue).deliver if HelpdeskSettings["helpdesk_send_notification", @project].to_i > 0
|
||||
|
||||
respond_to do |format|
|
||||
format.api { redirect_on_create(params) }
|
||||
end
|
||||
else
|
||||
raise Exception, "Can't create issue: #{@issue.errors.full_messages}"
|
||||
end
|
||||
|
||||
rescue Exception => e
|
||||
respond_to do |format|
|
||||
format.api do
|
||||
@error_messages = [e.message]
|
||||
HelpdeskLogger.error "API Create Ticket Error: #{e.message}" if HelpdeskLogger
|
||||
render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_mail
|
||||
set_settings
|
||||
|
||||
msg_count = HelpdeskMailer.check_project(@project.id)
|
||||
|
||||
respond_to do |format|
|
||||
format.js do
|
||||
@message = "<div class='flash notice'> #{l(:label_helpdesk_get_mail_success, :count => msg_count)} </div>"
|
||||
flash.discard
|
||||
end
|
||||
format.html {redirect_to :back}
|
||||
end
|
||||
rescue Exception => e
|
||||
respond_to do |format|
|
||||
format.js do
|
||||
@message = "<div class='flash error'> Error: #{e.message} </div>"
|
||||
Rails.logger.error "Helpdesk MailHandler Error: #{e.message}" if Rails.logger && Rails.logger.error
|
||||
flash.discard
|
||||
end
|
||||
format.html {redirect_to :back}
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def update_customer_email
|
||||
@journal = Journal.find(params[:journal_id])
|
||||
@issue = @journal.journalized
|
||||
@project = @issue.project
|
||||
@display = HelpdeskSettings[:send_note_by_default, @project] ? 'inline' : 'none'
|
||||
if @journal.is_incoming?
|
||||
@contact = @journal.contact
|
||||
@email = @journal.journal_message.from_address
|
||||
from_address = HelpdeskSettings['helpdesk_answer_from', @issue.project].blank? ? Setting.mail_from : HelpdeskSettings['helpdesk_answer_from', @issue.project]
|
||||
@cc_emails = (@issue.helpdesk_ticket.cc_addresses + @journal.journal_message.cc_address.split(',') - [@email, from_address]).uniq
|
||||
else
|
||||
@contact = @issue.helpdesk_ticket.last_reply_customer
|
||||
@email = @issue.helpdesk_ticket.default_to_address
|
||||
@cc_emails = @issue.helpdesk_ticket.cc_addresses - [@email]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_project
|
||||
project_id = params[:project_id] || (params[:ticket] && params[:ticket][:issue] && params[:ticket][:issue][:project_id])
|
||||
@project = Project.find(project_id)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def set_settings
|
||||
set_settings_param("helpdesk_answer_from")
|
||||
set_settings_param("helpdesk_send_notification")
|
||||
set_settings_param("helpdesk_is_not_create_contacts")
|
||||
set_settings_param("helpdesk_created_contact_tag")
|
||||
set_settings_param("helpdesk_blacklist")
|
||||
set_settings_param("helpdesk_emails_header")
|
||||
set_settings_param("helpdesk_answer_subject")
|
||||
set_settings_param("helpdesk_first_answer_subject")
|
||||
set_settings_param("helpdesk_first_answer_template")
|
||||
set_settings_param("helpdesk_emails_footer")
|
||||
set_settings_param("helpdesk_answered_status")
|
||||
set_settings_param("helpdesk_reopen_status")
|
||||
set_settings_param("helpdesk_tracker")
|
||||
set_settings_param("helpdesk_assigned_to")
|
||||
set_settings_param("helpdesk_lifetime")
|
||||
|
||||
set_settings_param(:helpdesk_protocol)
|
||||
set_settings_param(:helpdesk_host)
|
||||
set_settings_param(:helpdesk_port)
|
||||
set_settings_param(:helpdesk_password)
|
||||
set_settings_param(:helpdesk_username)
|
||||
|
||||
set_settings_param(:helpdesk_use_ssl)
|
||||
set_settings_param(:helpdesk_imap_folder)
|
||||
set_settings_param(:helpdesk_move_on_success)
|
||||
set_settings_param(:helpdesk_move_on_failure)
|
||||
set_settings_param(:helpdesk_apop)
|
||||
set_settings_param(:helpdesk_delete_unprocessed)
|
||||
|
||||
set_settings_param(:helpdesk_smtp_use_default_settings)
|
||||
set_settings_param(:helpdesk_smtp_server)
|
||||
set_settings_param(:helpdesk_smtp_domain)
|
||||
set_settings_param(:helpdesk_smtp_port)
|
||||
set_settings_param(:helpdesk_smtp_authentication)
|
||||
set_settings_param(:helpdesk_smtp_username)
|
||||
set_settings_param(:helpdesk_smtp_password)
|
||||
set_settings_param(:helpdesk_smtp_tls)
|
||||
set_settings_param(:helpdesk_smtp_ssl)
|
||||
|
||||
end
|
||||
|
||||
def set_settings_param(param)
|
||||
if param == :helpdesk_password || param == :helpdesk_smtp_password
|
||||
ContactsSetting[param, @project.id] = params[param] if params[param] && !params[param].blank?
|
||||
else
|
||||
ContactsSetting[param, @project.id] = params[param] if params[param]
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_on_create(options)
|
||||
if options[:redirect_on_success].to_s.match('^(http|https):\/\/')
|
||||
redirect_to options[:redirect_on_success].to_s
|
||||
else
|
||||
render :text => "Issue #{@issue.id} created", :status => :created, :location => issue_url(@issue)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
class HelpdeskMailerController < ActionController::Base
|
||||
unloadable
|
||||
before_filter :check_credential
|
||||
|
||||
# Submits an incoming email to ContactsMailer
|
||||
def index
|
||||
options = params.dup
|
||||
if options[:issue].present?
|
||||
project = Project.find_by_identifier(options[:issue][:project])
|
||||
options = HelpdeskMailer.get_issue_options(options, project.id) if project
|
||||
end
|
||||
email = options.delete(:email)
|
||||
if HelpdeskMailer.receive(email, options)
|
||||
render :nothing => true, :status => :created
|
||||
else
|
||||
render :nothing => true, :status => :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def get_mail
|
||||
msg_count = 0
|
||||
errors = []
|
||||
Project.active.has_module(:contacts_helpdesk).each do |project|
|
||||
begin
|
||||
msg_count += HelpdeskMailer.check_project(project.id)
|
||||
rescue Exception => e
|
||||
errors << e.message
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
render :status => :ok, :text => {:count => msg_count, :errors => errors}.to_json
|
||||
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_credential
|
||||
User.current = nil
|
||||
unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
|
||||
render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
class HelpdeskReportsController < ApplicationController
|
||||
unloadable
|
||||
menu_item :issues
|
||||
|
||||
helper :helpdesk
|
||||
helper :queries
|
||||
include QueriesHelper
|
||||
|
||||
before_filter :find_optional_project, :authorize_global
|
||||
|
||||
def show
|
||||
retrieve_reports_query
|
||||
@collector = HelpdeskDataCollectorManager.new(@report).collect_data(@query)
|
||||
return render_404 unless @collector
|
||||
respond_to do |format|
|
||||
format.html
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def retrieve_reports_query
|
||||
@report = params[:report] || 'first_response_time'
|
||||
report_query_class = @report == 'first_response_time' ? HelpdeskReportsFirstResponseQuery : HelpdeskReportsBusiestTimeQuery
|
||||
if params[:set_filter] || session[:helpdesk_reports_query].nil? || session[:helpdesk_reports_query][:project_id] != (@project ? @project.id : nil)
|
||||
@query = report_query_class.new(:name => '_', :project => @project)
|
||||
params.merge!('f' => ['message_date'], 'op' => { 'message_date' => 'm' }) if params['f'].nil? || params['f'].all?(&:blank?)
|
||||
@query.build_from_params(params)
|
||||
@query[:filters] = { 'message_date' => { :operator => 'm', :values => [''] } } unless @query[:filters]
|
||||
session[:helpdesk_reports_query] = { :project_id => @query.project_id, :filters => @query.filters || {} }
|
||||
else
|
||||
@query = report_query_class.new(:name => '_', :project => @project, :filters => session[:helpdesk_reports_query][:filters] || {})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,201 @@
|
||||
class HelpdeskSearchController < ApplicationController
|
||||
unloadable
|
||||
|
||||
accept_api_auth :ticket_by_issue, :issues_by_contact, :messages_by_issue, :contact_timeline
|
||||
|
||||
before_filter :require_login
|
||||
|
||||
def usage
|
||||
# Human/browser probes often stop at /helpdesk_search/issues. Return a
|
||||
# machine-readable usage response instead of letting Rails raise a route
|
||||
# exception that looks like an application failure in production.log.
|
||||
render :json => {
|
||||
:helpdesk_search => {
|
||||
:ticket_by_issue => '/helpdesk_search/issues/:issue_id/ticket',
|
||||
:ticket_by_issue_alias => '/helpdesk_search/issues/:issue_id',
|
||||
:issues_by_contact => '/helpdesk_search/contacts/:contact_id/issues',
|
||||
:messages_by_issue => '/helpdesk_search/issues/:issue_id/messages',
|
||||
:contact_timeline => '/helpdesk_search/contacts/:contact_id/timeline'
|
||||
}
|
||||
}, :status => :bad_request
|
||||
end
|
||||
|
||||
def ticket_by_issue
|
||||
issue = Issue.find(params[:issue_id])
|
||||
return unless authorize_helpdesk_project!(issue.project)
|
||||
|
||||
ticket = HelpdeskTicket.where(:issue_id => issue.id).first
|
||||
unless ticket
|
||||
render_404
|
||||
return
|
||||
end
|
||||
|
||||
render :json => {:helpdesk_ticket => serialize_ticket(ticket)}
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def issues_by_contact
|
||||
contact = Contact.find(params[:contact_id])
|
||||
tickets = HelpdeskTicket.
|
||||
includes(:issue => [:project, :status, :tracker, :assigned_to]).
|
||||
where(:contact_id => contact.id).
|
||||
order("#{HelpdeskTicket.table_name}.ticket_date DESC").
|
||||
limit(api_limit)
|
||||
|
||||
render :json => {
|
||||
:contact_id => contact.id,
|
||||
:issues => tickets.map { |ticket| serialize_ticket_issue(ticket) }.compact
|
||||
}
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def messages_by_issue
|
||||
issue = Issue.find(params[:issue_id])
|
||||
return unless authorize_helpdesk_project!(issue.project)
|
||||
|
||||
messages = JournalMessage.
|
||||
includes(:contact, :journal).
|
||||
joins(:journal).
|
||||
where(:journals => {:journalized_type => 'Issue', :journalized_id => issue.id}).
|
||||
order("#{JournalMessage.table_name}.message_date ASC").
|
||||
limit(api_limit)
|
||||
|
||||
render :json => {
|
||||
:issue_id => issue.id,
|
||||
:journal_messages => messages.map { |message| serialize_journal_message(message) }
|
||||
}
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def contact_timeline
|
||||
contact = Contact.find(params[:contact_id])
|
||||
tickets = HelpdeskTicket.
|
||||
includes(:issue => [:project, :status, :tracker]).
|
||||
where(:contact_id => contact.id).
|
||||
order("#{HelpdeskTicket.table_name}.ticket_date DESC").
|
||||
limit(api_limit)
|
||||
|
||||
messages = JournalMessage.
|
||||
includes(:contact, :journal).
|
||||
where(:contact_id => contact.id).
|
||||
order("#{JournalMessage.table_name}.message_date DESC").
|
||||
limit(api_limit)
|
||||
|
||||
events = []
|
||||
tickets.each do |ticket|
|
||||
next unless visible_issue?(ticket.issue)
|
||||
events << serialize_ticket(ticket).merge(:type => 'helpdesk_ticket', :date => iso8601(ticket.ticket_date))
|
||||
end
|
||||
|
||||
messages.each do |message|
|
||||
issue = message_issue(message)
|
||||
next unless visible_issue?(issue)
|
||||
events << serialize_journal_message(message).merge(:type => 'journal_message', :date => iso8601(message.message_date))
|
||||
end
|
||||
|
||||
events.sort_by! { |event| event[:date].to_s }
|
||||
events.reverse!
|
||||
|
||||
render :json => {
|
||||
:contact_id => contact.id,
|
||||
:timeline => events.first(api_limit)
|
||||
}
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_helpdesk_project!(project)
|
||||
# Search endpoints expose customer/email context, so they reuse the native
|
||||
# per-project helpdesk visibility permission instead of adding a new role.
|
||||
return true if project && User.current.allowed_to?(:view_helpdesk_tickets, project)
|
||||
deny_access
|
||||
false
|
||||
end
|
||||
|
||||
def visible_issue?(issue)
|
||||
issue && issue.project && User.current.allowed_to?(:view_helpdesk_tickets, issue.project)
|
||||
end
|
||||
|
||||
def api_limit
|
||||
requested_limit = params[:limit].present? ? params[:limit].to_i : 100
|
||||
[[requested_limit, 1].max, 200].min
|
||||
end
|
||||
|
||||
def serialize_ticket_issue(ticket)
|
||||
issue = ticket.issue
|
||||
return nil unless visible_issue?(issue)
|
||||
|
||||
serialize_ticket(ticket).merge(
|
||||
:issue => {
|
||||
:id => issue.id,
|
||||
:project_id => issue.project_id,
|
||||
:tracker_id => issue.tracker_id,
|
||||
:tracker_name => issue.tracker.try(:name),
|
||||
:status_id => issue.status_id,
|
||||
:status_name => issue.status.try(:name),
|
||||
:assigned_to_id => issue.assigned_to_id,
|
||||
:assigned_to_name => issue.assigned_to.try(:name),
|
||||
:subject => issue.subject,
|
||||
:created_on => iso8601(issue.created_on),
|
||||
:updated_on => iso8601(issue.updated_on)
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def serialize_ticket(ticket)
|
||||
{
|
||||
:id => ticket.id,
|
||||
:issue_id => ticket.issue_id,
|
||||
:contact_id => ticket.contact_id,
|
||||
:message_id => ticket.message_id,
|
||||
:source => ticket.source,
|
||||
:is_incoming => ticket.is_incoming?,
|
||||
:from_address => ticket.from_address,
|
||||
:to_address => ticket.to_address,
|
||||
:cc_address => ticket.cc_address,
|
||||
:ticket_date => iso8601(ticket.ticket_date)
|
||||
}
|
||||
end
|
||||
|
||||
def serialize_journal_message(message)
|
||||
journal = message.journal
|
||||
issue = message_issue(message)
|
||||
|
||||
# Keep this API metadata-only. The external indexer can fetch journal.notes
|
||||
# with its own read policy; this endpoint should not leak message bodies,
|
||||
# private notes, attachments, or BCC addresses by accident.
|
||||
{
|
||||
:id => message.id,
|
||||
:journal_id => message.journal_id,
|
||||
:issue_id => issue.try(:id),
|
||||
:project_id => issue.try(:project_id),
|
||||
:contact_id => message.contact_id,
|
||||
:message_id => message.message_id,
|
||||
:source => message.source,
|
||||
:is_incoming => message.is_incoming?,
|
||||
:from_address => message.from_address,
|
||||
:to_address => message.to_address,
|
||||
:cc_address => message.cc_address,
|
||||
:has_bcc_address => message.bcc_address.present?,
|
||||
:message_date => iso8601(message.message_date),
|
||||
:journal_user_id => journal.try(:user_id),
|
||||
:journal_private_notes => journal.try(:private_notes?),
|
||||
:journal_has_notes => journal.try(:notes).present?
|
||||
}
|
||||
end
|
||||
|
||||
def message_issue(message)
|
||||
journal = message.journal
|
||||
return nil unless journal
|
||||
journal.try(:issue) || journal.try(:journalized)
|
||||
end
|
||||
|
||||
def iso8601(value)
|
||||
value.try(:utc).try(:iso8601)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
class HelpdeskTicketsController < ApplicationController
|
||||
unloadable
|
||||
|
||||
before_filter :find_issue, :except => :destroy
|
||||
before_filter :find_helpdesk_ticket, :only => :destroy
|
||||
before_filter :authorize
|
||||
|
||||
helper :helpdesk
|
||||
|
||||
def edit
|
||||
@show_form = "true"
|
||||
respond_to do |format|
|
||||
format.js
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@helpdesk_ticket.attributes = params[:helpdesk_ticket]
|
||||
@helpdesk_ticket.cc_address = params[:helpdesk_ticket][:cc_address].reject(&:empty?).join(',') if params[:helpdesk_ticket][:cc_address]
|
||||
@helpdesk_ticket.issue = @issue
|
||||
@helpdesk_ticket.from_address = @helpdesk_ticket.customer.primary_email if @helpdesk_ticket.customer
|
||||
|
||||
if @helpdesk_ticket.save
|
||||
flash[:notice] = l(:notice_successful_update)
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_default({:controller => 'issues', :action => 'show', :id => @issue}) }
|
||||
format.api { render_api_ok }
|
||||
end
|
||||
else
|
||||
flash[:error] = @helpdesk_ticket.errors.full_messages.flatten.join("\n")
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_default({:controller => 'issues', :action => 'show', :id => @issue}) }
|
||||
format.api { render_validation_errors(@helpdesk_ticket) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @helpdesk_ticket.destroy
|
||||
flash[:notice] = l(:notice_successful_delete)
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_default({:controller => 'issues', :action => 'show', :id => @issue}) }
|
||||
format.api { render_api_ok }
|
||||
end
|
||||
else
|
||||
flash[:error] = l(:notice_unsuccessful_save)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def find_helpdesk_ticket
|
||||
@helpdesk_ticket = HelpdeskTicket.find(params[:id])
|
||||
@issue = @helpdesk_ticket.issue
|
||||
@project = @issue.project if @issue
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def find_issue
|
||||
@issue = Issue.find(params[:issue_id])
|
||||
@project = @issue.project
|
||||
@helpdesk_ticket = @issue.helpdesk_ticket || HelpdeskTicket.new(:ticket_date => Time.now, :issue => @issue)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
class HelpdeskVotesController < ApplicationController
|
||||
unloadable
|
||||
layout 'public_tickets'
|
||||
skip_before_filter :check_if_login_required
|
||||
before_filter :find_ticket, :authorize_ticket
|
||||
before_filter :fill_data
|
||||
|
||||
helper :issues
|
||||
|
||||
def vote
|
||||
@ticket.update_vote(params[:vote], params[:vote_comment]) if params[:vote]
|
||||
end
|
||||
|
||||
def fast_vote
|
||||
if RedmineHelpdesk.vote_comment_allow?
|
||||
@ticket.vote = params[:vote] if params[:vote]
|
||||
render :action => "show"
|
||||
else
|
||||
@ticket.update_vote(params[:vote]) if params[:vote]
|
||||
render :action => "vote"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_ticket
|
||||
@ticket = HelpdeskTicket.find(params[:id])
|
||||
@issue = @ticket.issue
|
||||
@project = @issue.project
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def authorize_ticket(action = params[:action])
|
||||
allow = true
|
||||
allow &&= (@ticket.token == params[:hash]) && RedmineHelpdesk.vote_allow?
|
||||
allow &&= !@issue.is_private
|
||||
render_404 unless allow
|
||||
end
|
||||
|
||||
def fill_data
|
||||
@previous_tickets = @ticket.customer.tickets.where(:is_private => false).includes([:status, :helpdesk_ticket]).order_by_status
|
||||
@total_spent_hours = @previous_tickets.map.sum(&:total_spent_hours)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,206 @@
|
||||
class HelpdeskWidgetController < ApplicationController
|
||||
unloadable
|
||||
layout false
|
||||
helper :custom_fields
|
||||
protect_from_forgery :except => [:widget, :load_form, :load_custom_fields, :avatar, :create_ticket, :iframe]
|
||||
skip_before_filter :check_if_login_required, :only => [:widget, :load_form, :load_custom_fields, :avatar, :create_ticket, :iframe]
|
||||
|
||||
before_filter :prepare_data, :only => [:load_custom_fields, :create_ticket]
|
||||
after_filter :set_access_control_header
|
||||
|
||||
def load_form
|
||||
render :json => schema.to_json
|
||||
end
|
||||
|
||||
def load_custom_fields
|
||||
@issue = @project.issues.build(:tracker => @tracker) if @tracker
|
||||
@enabled_cf = HelpdeskSettings["helpdesk_widget_available_custom_fields", nil]
|
||||
end
|
||||
|
||||
def avatar
|
||||
user = User.where(:login => params[:login]).first
|
||||
return render :nothing => true, :status => 404 unless user
|
||||
if user.try(:avatar).nil?
|
||||
avatar_thumb, avatar_type = gravatar_avatar(user) if Setting.gravatar_enabled?
|
||||
else
|
||||
avatar_thumb, avatar_type = local_avatar(user.avatar)
|
||||
end
|
||||
return render :nothing => true, :status => 404 unless avatar_thumb
|
||||
send_avatar(avatar_thumb, avatar_type)
|
||||
end
|
||||
|
||||
def send_avatar(avatar_thumb, avatar_type)
|
||||
send_file avatar_thumb, :filename => (request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(params[:login]) : params[:login]),
|
||||
:type => avatar_type,
|
||||
:disposition => 'inline'
|
||||
end
|
||||
|
||||
def gravatar_avatar(user)
|
||||
email = user.mail if user.respond_to?(:mail)
|
||||
email = user.to_s[/<(.+?)>/, 1] unless email
|
||||
return [nil, nil] unless email
|
||||
default = Setting.gravatar_default ? CGI::escape(Setting.gravatar_default) : ''
|
||||
temp_file = Tempfile.new([user.login, '.jpeg'])
|
||||
temp_file.binmode
|
||||
open("http://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email)}?rating=PG&size=54&default=#{default}") do |url_file|
|
||||
temp_file.write(url_file.read)
|
||||
end
|
||||
temp_file.rewind
|
||||
[temp_file, 'image/jpeg']
|
||||
end
|
||||
|
||||
def local_avatar(user_avatar)
|
||||
return nil unless user_avatar.readable? || user_avatar.thumbnailable?
|
||||
if (defined?(RedmineContacts::Thumbnail) == 'constant') && Redmine::Thumbnail.convert_available?
|
||||
target = File.join(user_avatar.class.thumbnails_storage_path, "#{user_avatar.id}_#{user_avatar.digest}_54x54.thumb")
|
||||
thumbnail = RedmineContacts::Thumbnail.generate(user_avatar.diskfile, target, '54x54')
|
||||
elsif Redmine::Thumbnail.convert_available?
|
||||
thumbnail = user_avatar.thumbnail(:size => '54x54')
|
||||
else
|
||||
thumbnail = user_avatar.diskfile
|
||||
end
|
||||
[thumbnail, detect_content_type(user_avatar)]
|
||||
end
|
||||
|
||||
def create_ticket
|
||||
@issue = prepare_issue
|
||||
@issue.helpdesk_ticket = prepare_helpdesk_ticket
|
||||
result =
|
||||
if valid_email? && @issue.save
|
||||
save_attachment(@issue)
|
||||
HelpdeskMailer.auto_answer(@issue.helpdesk_ticket.customer, @issue).deliver if HelpdeskSettings["helpdesk_send_notification", @project].to_i > 0
|
||||
{ :result => true, :errors => [] }
|
||||
else
|
||||
{ :result => false, :errors => prepared_errors }
|
||||
end
|
||||
render :json => result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prepare_data
|
||||
@project = Project.find(params[:project_id])
|
||||
@tracker = @project.trackers.where(:id => params[:tracker_id]).first
|
||||
end
|
||||
|
||||
def schema
|
||||
if HelpdeskSettings["helpdesk_widget_enable", nil].to_i > 0
|
||||
projects = Project.has_module('contacts_helpdesk').where(:id => HelpdeskSettings[:helpdesk_widget_available_projects, nil])
|
||||
else
|
||||
projects = []
|
||||
end
|
||||
data_schema = {}
|
||||
data_schema[:projects] = Hash[projects.map { |project| [project.name.capitalize, project.id] }]
|
||||
data_schema[:projects_data] = {}
|
||||
projects.each do |project|
|
||||
data_schema[:projects_data][project.id] = {}
|
||||
if HelpdeskSettings["helpdesk_tracker", project] && HelpdeskSettings["helpdesk_tracker", project] != 'all'
|
||||
data_schema[:projects_data][project.id][:trackers] = Hash[Tracker.where(id: HelpdeskSettings["helpdesk_tracker", project])
|
||||
.map { |tracker| [tracker.name, tracker.id] }]
|
||||
else
|
||||
data_schema[:projects_data][project.id][:trackers] = Hash[project.trackers.map { |tracker| [tracker.name, tracker.id] }]
|
||||
end
|
||||
end
|
||||
data_schema[:custom_fields] = Hash[IssueCustomField.where(id: HelpdeskSettings["helpdesk_widget_available_custom_fields", nil])
|
||||
.map { |custom_field| [custom_field.name, custom_field.id] }]
|
||||
data_schema[:avatar] = HelpdeskSettings[:helpdesk_widget_avatar_login, nil]
|
||||
data_schema
|
||||
end
|
||||
|
||||
def prepared_errors
|
||||
errors_hash = @issue.errors.dup
|
||||
# Username
|
||||
if errors_hash[:'helpdesk_ticket.customer.first_name'].present?
|
||||
@issue.errors.delete(:'helpdesk_ticket.customer.first_name')
|
||||
@issue.errors[:username] = errors_hash[:'helpdesk_ticket.customer.first_name'].collect { |error| ['Username', error].join(' ') }
|
||||
end
|
||||
|
||||
# Subject
|
||||
if errors_hash[:subject].present?
|
||||
errors = errors_hash[:subject].collect { |error| ['Subject', error].join(' ') }
|
||||
@issue.errors[:subject].clear
|
||||
@issue.errors[:subject] = errors
|
||||
end
|
||||
|
||||
# Description
|
||||
if params[:issue][:description].empty?
|
||||
@issue.errors[:description] = I18n.t(:label_helpdesk_widget_ticket_error_description)
|
||||
end
|
||||
|
||||
# Nested objects
|
||||
if errors_hash[:'helpdesk_ticket.customer.projects'].present?
|
||||
@issue.errors.delete(:'helpdesk_ticket.customer.projects')
|
||||
end
|
||||
@issue.errors
|
||||
end
|
||||
|
||||
def prepare_issue
|
||||
redmine_user = User.where(id: params[:redmine_user]).first
|
||||
author = redmine_user.present? && redmine_user.allowed_to?(:edit_helpdesk_tickets, @project) ? redmine_user : User.anonymous
|
||||
issue = @project.issues.build(:tracker => @tracker, :author => author)
|
||||
issue.safe_attributes = params[:issue].deep_dup
|
||||
issue.assigned_to = widget_contact.find_assigned_user(@project, HelpdeskSettings["helpdesk_assigned_to", @project])
|
||||
issue
|
||||
end
|
||||
|
||||
def prepare_helpdesk_ticket
|
||||
HelpdeskTicket.new(:from_address => params[:email],
|
||||
:ticket_date => Time.now,
|
||||
:customer => widget_contact,
|
||||
:issue => @issue,
|
||||
:source => HelpdeskTicket::HELPDESK_WEB_SOURCE)
|
||||
end
|
||||
|
||||
def save_attachment(issue)
|
||||
return unless params[:attachment].present?
|
||||
attachment_hash = split_base64(params[:attachment])
|
||||
attachment = Attachment.new(file: Base64.decode64(attachment_hash[:data]))
|
||||
attachment.filename = params[:attachment_name] || [Redmine::Utils.random_hex(16), attachment_hash[:extension]].join('.')
|
||||
attachment.content_type = attachment_hash[:type]
|
||||
attachment.author = User.anonymous
|
||||
issue.attachments << attachment
|
||||
issue.save
|
||||
end
|
||||
|
||||
def split_base64(uri)
|
||||
matcher = uri.match(/^data:(.*?)\;(.*?),(.*)$/)
|
||||
{ type: matcher[1],
|
||||
encoder: matcher[2],
|
||||
data: matcher[3],
|
||||
extension: matcher[1].split('/')[1] }
|
||||
end
|
||||
|
||||
def widget_contact
|
||||
return @widget_contact if @widget_contact
|
||||
contacts = Contact.find_by_emails([params[:email]])
|
||||
return @widget_contact = contacts.first if contacts.any?
|
||||
@widget_contact = Contact.new(:email => params[:email])
|
||||
@widget_contact.first_name, @widget_contact.last_name = params[:username].split(' ')
|
||||
@widget_contact.projects << @project
|
||||
@widget_contact
|
||||
end
|
||||
|
||||
def set_access_control_header
|
||||
headers['Access-Control-Allow-Origin'] = '*'
|
||||
headers['X-Frame-Options'] = '*'
|
||||
end
|
||||
|
||||
def valid_email?
|
||||
if params[:email].empty?
|
||||
@issue.errors[:email] = 'Email cannot be empty'
|
||||
return false
|
||||
elsif params[:email].match(/\A([\w\.\+\-]+)@([\w\-]+\.)+([\w]{2,})\z/i).nil?
|
||||
@issue.errors[:email] = 'Email is incorrect'
|
||||
return false
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def detect_content_type(attachment)
|
||||
content_type = attachment.content_type
|
||||
if content_type.blank?
|
||||
content_type = Redmine::MimeType.of(attachment.filename)
|
||||
end
|
||||
content_type.to_s
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,89 @@
|
||||
class MailFetcherController < ApplicationController
|
||||
unloadable
|
||||
require 'timeout'
|
||||
|
||||
before_filter :check_credential
|
||||
|
||||
def receive_imap
|
||||
imap_options = {:host => params['host'],
|
||||
:port => params['port'],
|
||||
:ssl => params['ssl'],
|
||||
:username => params['username'],
|
||||
:password => params['password'],
|
||||
:folder => params['folder'],
|
||||
:move_on_success => params['move_on_success'],
|
||||
:move_on_failure => params['move_on_failure']}
|
||||
|
||||
options = { :issue => {} }
|
||||
%w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = params[a] if params[a] }
|
||||
options[:allow_override] = params['allow_override'] if params['allow_override']
|
||||
options[:unknown_user] = params['unknown_user'] if params['unknown_user']
|
||||
options[:no_permission_check] = params['no_permission_check'] if params['no_permission_check']
|
||||
|
||||
begin
|
||||
Timeout::timeout(15){ Redmine::IMAP.check(imap_options, options) }
|
||||
rescue Exception => e
|
||||
@error_messages = [e.message]
|
||||
end
|
||||
|
||||
|
||||
if @error_messages.blank?
|
||||
respond_to do |format|
|
||||
format.html { render :nothing => true, :status => :ok }
|
||||
format.api { render_api_ok }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html { render :text => @error_messages, :status => :unprocessable_entity, :layout => nil }
|
||||
format.api { render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil}
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def receive_pop3
|
||||
pop_options = {:host => params['host'],
|
||||
:port => params['port'],
|
||||
:apop => params['apop'],
|
||||
:username => params['username'],
|
||||
:password => params['password'],
|
||||
:delete_unprocessed => params['delete_unprocessed']}
|
||||
|
||||
options = { :issue => {} }
|
||||
%w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = params[a] if params[a] }
|
||||
options[:allow_override] = params['allow_override'] if params['allow_override']
|
||||
options[:unknown_user] = params['unknown_user'] if params['unknown_user']
|
||||
options[:no_permission_check] = params['no_permission_check'] if params['no_permission_check']
|
||||
|
||||
begin
|
||||
Timeout::timeout(15){ Redmine::POP3.check(pop_options, options) }
|
||||
rescue Exception => e
|
||||
@error_messages = [e.message]
|
||||
end
|
||||
|
||||
|
||||
if @error_messages.blank?
|
||||
respond_to do |format|
|
||||
format.html { render :nothing => true, :status => :ok }
|
||||
format.api { render_api_ok }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html { render :text => @error_messages, :status => :unprocessable_entity, :layout => nil }
|
||||
format.api { render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_credential
|
||||
User.current = nil
|
||||
unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
|
||||
render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
class PublicTicketsController < ApplicationController
|
||||
unloadable
|
||||
layout 'public_tickets'
|
||||
|
||||
skip_before_filter :check_if_login_required
|
||||
before_filter :find_ticket, :authorize_ticket
|
||||
|
||||
helper :issues
|
||||
helper :attachments
|
||||
helper :journals
|
||||
helper :custom_fields
|
||||
|
||||
def show
|
||||
@previous_tickets = @ticket.customer.tickets.where(:is_private => false).includes([:status, :helpdesk_ticket]).order_by_status
|
||||
|
||||
@total_spent_hours = @previous_tickets.map.sum(&:total_spent_hours)
|
||||
|
||||
@journals = @issue.journals.includes(:user).
|
||||
includes(:details).
|
||||
order("#{Journal.table_name}.created_on ASC").
|
||||
where(:private_notes => false).
|
||||
where("EXISTS (SELECT * FROM #{JournalMessage.table_name} WHERE #{JournalMessage.table_name}.journal_id = #{Journal.table_name}.id)")
|
||||
@journals = @journals.each_with_index {|j,i| j.indice = i+1}.to_a
|
||||
@journals.reverse! if User.current.wants_comments_in_reverse_order?
|
||||
@journal = @issue.journals.new
|
||||
|
||||
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
|
||||
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
|
||||
@priorities = IssuePriority.active
|
||||
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
|
||||
prepend_view_path "app/views/issues"
|
||||
|
||||
end
|
||||
|
||||
def add_comment
|
||||
@journal = @issue.journals.new(params[:journal])
|
||||
@issue.status_id = HelpdeskSettings["helpdesk_reopen_status", @issue.project_id] unless HelpdeskSettings["helpdesk_reopen_status", @issue.project_id].blank?
|
||||
@journal.user = User.current
|
||||
@journal.journal_message = JournalMessage.new(:from_address => @ticket.customer_email,
|
||||
:contact => @ticket.customer,
|
||||
:journal => @journal,
|
||||
:is_incoming => true,
|
||||
:message_date => Time.now)
|
||||
if @issue.save
|
||||
flash[:notice] = l(:notice_successful_create)
|
||||
@journal.save
|
||||
end
|
||||
redirect_back_or_default(public_ticket_path(@ticket, @ticket.token))
|
||||
end
|
||||
|
||||
def render_404(options={})
|
||||
@message = l(:notice_file_not_found)
|
||||
respond_to do |format|
|
||||
format.html {
|
||||
render :template => 'common/error', :status => 404
|
||||
}
|
||||
format.any { head 404 }
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def find_ticket
|
||||
@ticket = HelpdeskTicket.find(params[:id])
|
||||
@issue = @ticket.issue
|
||||
@project = @issue.project
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def authorize_ticket(action = params[:action])
|
||||
allow = true
|
||||
allow &&= RedmineHelpdesk.public_comments? if (action.to_s == "add_comment")
|
||||
allow &&= (@ticket.token == params[:hash]) && RedmineHelpdesk.public_tickets?
|
||||
allow &&= !@issue.is_private
|
||||
render_404 unless allow
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,83 @@
|
||||
module HelpdeskHelper
|
||||
def helpdesk_ticket_source_icon(helpdesk_ticket)
|
||||
case helpdesk_ticket.source
|
||||
when HelpdeskTicket::HELPDESK_EMAIL_SOURCE
|
||||
"icon-email"
|
||||
when HelpdeskTicket::HELPDESK_PHONE_SOURCE
|
||||
"icon-call"
|
||||
when HelpdeskTicket::HELPDESK_WEB_SOURCE
|
||||
"icon-web"
|
||||
when HelpdeskTicket::HELPDESK_TWITTER_SOURCE
|
||||
"icon-twitter"
|
||||
else
|
||||
"icon-helpdesk"
|
||||
end
|
||||
end
|
||||
|
||||
def helpdesk_tickets_source_for_select
|
||||
[[l(:label_helpdesk_tickets_email), HelpdeskTicket::HELPDESK_EMAIL_SOURCE.to_s],
|
||||
[l(:label_helpdesk_tickets_phone), HelpdeskTicket::HELPDESK_PHONE_SOURCE.to_s],
|
||||
[l(:label_helpdesk_tickets_web), HelpdeskTicket::HELPDESK_WEB_SOURCE.to_s],
|
||||
[l(:label_helpdesk_tickets_conversation), HelpdeskTicket::HELPDESK_CONVERSATION_SOURCE.to_s]
|
||||
]
|
||||
end
|
||||
|
||||
def helpdesk_send_as_for_select
|
||||
[[l(:label_helpdesk_not_send), ''],
|
||||
[l(:label_helpdesk_send_as_notification), HelpdeskTicket::SEND_AS_NOTIFICATION.to_s],
|
||||
[l(:label_helpdesk_send_as_message), HelpdeskTicket::SEND_AS_MESSAGE.to_s]
|
||||
]
|
||||
end
|
||||
|
||||
def show_customer_vote(vote, comment)
|
||||
case vote
|
||||
when 2
|
||||
generate_vote_link(vote, 'icon-awesome', comment)
|
||||
when 1
|
||||
generate_vote_link(vote, 'icon-justok', comment)
|
||||
when 0
|
||||
generate_vote_link(vote, 'icon-notgood', comment)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_vote_link(vote, vote_class, title)
|
||||
"<div class='icon #{ vote_class }' title='#{ title }'>#{ HelpdeskTicket.vote_message(vote) }</div>".html_safe
|
||||
end
|
||||
|
||||
def render_helpdesk_chart(report_name, issues_scope)
|
||||
render :partial => 'helpdesk_reports/chart', :locals => { :report => report_name, :issues_scope => issues_scope }
|
||||
end
|
||||
|
||||
def helpdesk_time_label(seconds)
|
||||
hours, minutes = seconds.divmod(60).first.divmod(60)
|
||||
"#{hours}<span>#{l(:label_helpdesk_hour)}</span> #{minutes}<span>#{l(:label_helpdesk_minute)}</span>".html_safe
|
||||
end
|
||||
|
||||
def slim_helpdesk_time_label(seconds)
|
||||
hours, minutes = seconds.divmod(60).first.divmod(60)
|
||||
"#{hours}#{l(:label_helpdesk_hour)} #{minutes}#{l(:label_helpdesk_minute)}".html_safe
|
||||
end
|
||||
|
||||
def progress_in_percents(value)
|
||||
return '0%'.html_safe if value.zero?
|
||||
"<span class='caret #{value > 0 ? 'pos' : 'neg'}'></span>#{value}%".html_safe
|
||||
end
|
||||
|
||||
def mirror_progress_in_percents(value)
|
||||
return '0%'.html_safe if value.zero?
|
||||
"<span class='caret #{value < 0 ? 'mirror_pos' : 'mirror_neg'}'></span>#{value}%".html_safe
|
||||
end
|
||||
|
||||
def process_deviation(before, now, time = true)
|
||||
["#{l(:label_helpdesk_report_previous)}: #{time ? slim_helpdesk_time_label(before) : before}",
|
||||
"#{l(:label_helpdesk_report_deviation)}: #{time ? slim_helpdesk_time_label(calculate_deviation(before, now)) : calculate_deviation(before, now)}"].join("\n").html_safe
|
||||
end
|
||||
|
||||
def calculate_deviation(before, now)
|
||||
before > now ? before - now : now - before
|
||||
end
|
||||
|
||||
def helpdesk_reply_link
|
||||
link_to l(:label_helpdesk_reply), edit_issue_path(@issue), :onclick => 'showWithSendAndScrollTo("update", "issue_notes"); return false;', :class => 'icon icon-reply'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
# encoding: utf-8
|
||||
# include RedCloth
|
||||
|
||||
module HelpdeskMailerHelper
|
||||
def textile(text)
|
||||
Redmine::WikiFormatting.to_html(Setting.text_formatting, text)
|
||||
end
|
||||
|
||||
def message_sender(email)
|
||||
sender = email.reply_to.try(:first) || email.from_addrs.try(:first)
|
||||
sender.to_s.strip
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
module PublicTicketsHelper
|
||||
include HelpdeskHelper
|
||||
|
||||
def authoring_public(journal, options={})
|
||||
if journal.journal_message && journal.journal_message.from_address
|
||||
l(options[:label] || :label_added_time_by, :author => mail_to(journal.journal_message.contact_email), :age => ticket_time_tag(journal.created_on)).html_safe
|
||||
else
|
||||
l(options[:label] || :label_added_time_by, :author => journal.user.name, :age => ticket_time_tag(journal.created_on)).html_safe
|
||||
end
|
||||
end
|
||||
|
||||
def ticket_time_tag(time)
|
||||
text = distance_of_time_in_words(Time.now, time)
|
||||
content_tag('acronym', text, :title => format_time(time))
|
||||
end
|
||||
|
||||
def link_to_attachments_with_hash(container, options = {})
|
||||
options.assert_valid_keys(:author, :thumbnails)
|
||||
|
||||
if container.attachments.any?
|
||||
options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
|
||||
render :partial => 'attachment_links',
|
||||
:locals => {:attachments => container.attachments, :options => options, :thumbnails => (options[:thumbnails] && Setting.thumbnails_enabled?)}
|
||||
end
|
||||
end
|
||||
|
||||
def link_to_attachment_with_hash(attachment, options={})
|
||||
text = options.delete(:text) || attachment.filename
|
||||
route_method = options.delete(:download) ? :hashed_download_named_attachment_path : :hashed_named_attachment_path
|
||||
html_options = options.slice!(:only_path)
|
||||
url = send(route_method, attachment, @ticket.id, @ticket.token, attachment.filename, options)
|
||||
link_to text, url, html_options
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
class CannedResponse < ActiveRecord::Base
|
||||
unloadable
|
||||
attr_accessible :name, :content, :is_public
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :user
|
||||
|
||||
validates_presence_of :name, :content
|
||||
validates_length_of :name, :maximum => 255
|
||||
|
||||
scope :visible, lambda {|*args|
|
||||
user = args.shift || User.current
|
||||
base = Project.allowed_to_condition(user, :view_helpdesk_tickets, *args)
|
||||
user_id = user.logged? ? user.id : 0
|
||||
|
||||
eager_load(:project).where("(#{CannedResponse.table_name}.project_id IS NULL OR (#{base})) AND (#{CannedResponse.table_name}.is_public = ? OR #{CannedResponse.table_name}.user_id = ?)", true, user_id)
|
||||
}
|
||||
|
||||
scope :in_project_or_public, lambda {|project|
|
||||
where("(#{CannedResponse.table_name}.project_id IS NULL) OR #{CannedResponse.table_name}.project_id = ?", project)
|
||||
}
|
||||
|
||||
# Returns true if the query is visible to +user+ or the current user.
|
||||
def visible?(user=User.current)
|
||||
(project.nil? || user.allowed_to?(:view_helpdesk_tickets, project)) && (self.is_public? || self.user_id == user.id)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,148 @@
|
||||
class HelpdeskDataCollectorBusiestTime
|
||||
MAX_WEIGHT = 200
|
||||
|
||||
RESPONSE_INTERVALS = {'6_8h' => [6, 8],
|
||||
'8_10h' => [8, 10],
|
||||
'10_12h' => [10, 12],
|
||||
'12_14h' => [12, 14],
|
||||
'14_16h' => [14, 16],
|
||||
'16_18h' => [16, 18],
|
||||
'18_20h' => [18, 20],
|
||||
'20_22h' => [20, 22],
|
||||
'22_0h' => [22, 0],
|
||||
'0_2h' => [0, 2],
|
||||
'2_4h' => [2, 4],
|
||||
'4_6h' => [4, 6]}
|
||||
|
||||
def columns
|
||||
@columns ||= collect_columns
|
||||
end
|
||||
|
||||
def issue_weight
|
||||
@issue_weight ||= (MAX_WEIGHT.to_f / columns.map { |column| column[:issues_count] }.sort.last).ceil
|
||||
end
|
||||
|
||||
# New tickets
|
||||
def new_issues_count
|
||||
return @new_issues_count if @new_issues_count
|
||||
condition = @query.send('sql_for_field', nil, @query.filters['message_date'][:operator], nil, 'issues', 'created_on')
|
||||
@new_issues_count ||= @issues.where(condition).count
|
||||
end
|
||||
|
||||
def previous_new_issues_count
|
||||
return @previous_new_issues_count if @previous_new_issues_count
|
||||
condition = previous_query.send('sql_for_field', nil, previous_query.filters['message_date'][:operator], nil, 'issues', 'created_on')
|
||||
@previous_new_issues_count ||= @previous_issues.where(condition).count
|
||||
end
|
||||
|
||||
def new_issue_count_progress
|
||||
return 0 if previous_new_issues_count.zero?
|
||||
calculate_progress(previous_new_issues_count, new_issues_count)
|
||||
end
|
||||
|
||||
# New contacts
|
||||
def contacts_count
|
||||
contacts.count
|
||||
end
|
||||
|
||||
def previous_contacts_count
|
||||
previous_contacts.count
|
||||
end
|
||||
|
||||
def total_contacts_count_progress
|
||||
return 0 if previous_contacts_count.zero?
|
||||
calculate_progress(previous_contacts_count, contacts_count)
|
||||
end
|
||||
|
||||
# Total incoming
|
||||
def issues_count
|
||||
@issues_count ||= @issues.count + @journal_messages.count
|
||||
end
|
||||
|
||||
def previous_issues_count
|
||||
@previous_issues_count ||= @previous_issues.count + @previous_journal_messages.count
|
||||
end
|
||||
|
||||
def issue_count_progress
|
||||
return 0 if previous_issues_count.zero?
|
||||
calculate_progress(previous_issues_count, issues_count)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize(query)
|
||||
@query = query
|
||||
@issues = with_created_issues(@query)
|
||||
@journal_messages = JournalMessage.where(:is_incoming => true).
|
||||
where(@query.send('sql_for_field', nil, @query.filters['message_date'][:operator], nil, 'journal_messages', 'message_date'))
|
||||
@previous_issues = with_created_issues(previous_query)
|
||||
@previous_journal_messages = JournalMessage.where(:is_incoming => true).
|
||||
where(previous_query.send('sql_for_field', nil, @query.filters['message_date'][:operator], nil, 'journal_messages', 'message_date'))
|
||||
end
|
||||
|
||||
def collect_columns
|
||||
columns = []
|
||||
RESPONSE_INTERVALS.each do |interval_name, interval_hours|
|
||||
interval_objects_count = find_incoming_objects_count(interval_hours)
|
||||
columns << { :name => interval_name, :issues_count => interval_objects_count,
|
||||
:issues_percent => ((interval_objects_count.to_f / issues_count.to_f) * 100).round(2) }
|
||||
end
|
||||
columns
|
||||
end
|
||||
|
||||
def find_incoming_objects_count(interval)
|
||||
interval_start = interval.first
|
||||
interval_end = interval.last - 1 < 0 ? 23 : interval.last - 1
|
||||
interval_issues = @issues.each.select do |issue|
|
||||
issue_time = timezone ? issue.created_on.in_time_zone(timezone) : issue.created_on.localtime
|
||||
interval_start <= issue_time.hour && issue_time.hour <= interval_end
|
||||
end
|
||||
interval_messages = @journal_messages.each.select do |message|
|
||||
message_time = timezone ? message.message_date.in_time_zone(timezone) : message.message_date.localtime
|
||||
interval_start <= message_time.hour && message_time.hour <= interval_end
|
||||
end
|
||||
interval_issues.count + interval_messages.count
|
||||
end
|
||||
|
||||
def timezone
|
||||
@timezone ||= User.current.time_zone
|
||||
end
|
||||
|
||||
def previous_query
|
||||
return if @query[:filters].nil? || @query[:filters]['message_date'].nil? || @query[:filters]['message_date'][:operator].nil?
|
||||
return @previous_query if @previous_query
|
||||
|
||||
previous_operator = ['pre_', @query[:filters]['message_date'][:operator]].join
|
||||
previous_filters = @query[:filters].merge('message_date' => { :operator => previous_operator, :values => [Date.today.to_s] })
|
||||
@previous_query = HelpdeskReportsBusiestTimeQuery.new(:name => '_', :project => @query.project, :filters => previous_filters)
|
||||
@previous_query
|
||||
end
|
||||
|
||||
def with_created_issues(query)
|
||||
condition = query.send('sql_for_field', nil, query.filters['message_date'][:operator], nil, 'issues', 'created_on')
|
||||
created_ids = Issue.joins(:project).visible.where(:project_id => query.project).where(condition).pluck(:id)
|
||||
Issue.where(:id => query.issues.pluck(:id) | created_ids).joins(:helpdesk_ticket)
|
||||
end
|
||||
|
||||
def contacts
|
||||
return @contacts if @contacts
|
||||
condition = @query.send('sql_for_field', nil, @query.filters['message_date'][:operator], nil, 'contacts', 'created_on')
|
||||
@contacts = Contact.where(:id => @issues.joins(:customer).map(&:customer).map(&:id).uniq).where(condition)
|
||||
end
|
||||
|
||||
def previous_contacts
|
||||
return @previous_contacts if @previous_contacts
|
||||
condition = @query.send('sql_for_field', nil, @previous_query.filters['message_date'][:operator], nil, 'contacts', 'created_on')
|
||||
@previous_contacts = Contact.where(:id => @previous_issues.joins(:customer).map(&:customer).map(&:id).uniq).where(condition)
|
||||
end
|
||||
|
||||
def calculate_progress(before, now)
|
||||
progress =
|
||||
if before.to_f > now.to_f
|
||||
100 - (now.to_f * 100 / before.to_f)
|
||||
else
|
||||
(100 - (before.to_f * 100 / now.to_f)) * -1
|
||||
end
|
||||
progress.round
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,178 @@
|
||||
class HelpdeskDataCollectorFirstResponse
|
||||
MAX_WEIGHT = 200
|
||||
|
||||
RESPONSE_INTERVALS = { '0_1h' => [0, 1],
|
||||
'1_2h' => [1, 2],
|
||||
'2_4h' => [2, 4],
|
||||
'4_8h' => [4, 8],
|
||||
'8_12h' => [8, 12],
|
||||
'12_24h' => [12, 24],
|
||||
'24_48h' => [24, 48],
|
||||
'48_0h' => [48, 0] }
|
||||
attr_reader :issues
|
||||
attr_reader :previous_issues
|
||||
|
||||
def columns
|
||||
@columns ||= collect_columns
|
||||
end
|
||||
|
||||
def issue_weight
|
||||
@issue_weight ||= (MAX_WEIGHT.to_f / columns.map { |column| column[:issues_count] }.sort.last).ceil
|
||||
end
|
||||
|
||||
# First response time
|
||||
def average_response_time
|
||||
@average_response_time ||= median(HelpdeskTicket.where(:issue_id => issues.pluck(:id)).pluck(:first_response_time))
|
||||
end
|
||||
|
||||
def previous_average_response_time
|
||||
return 0 if previous_issues_count.zero?
|
||||
@previous_average_response_time ||= median(HelpdeskTicket.where(:issue_id => previous_issues.pluck(:id)).pluck(:first_response_time))
|
||||
end
|
||||
|
||||
def average_response_time_progress
|
||||
return 0 if previous_issues_count.zero?
|
||||
calculate_progress(previous_average_response_time, average_response_time)
|
||||
end
|
||||
|
||||
# Time to close
|
||||
def average_close_time
|
||||
return @average_close_time if @average_close_time
|
||||
closed_issue_ids = issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id)
|
||||
return 0 if closed_issue_ids.empty?
|
||||
@average_close_time = median(HelpdeskTicket.where(:issue_id => closed_issue_ids).pluck(:resolve_time))
|
||||
end
|
||||
|
||||
def previous_average_close_time
|
||||
return @previous_average_close_time if @previous_average_close_time.present?
|
||||
closed_issue_ids = previous_issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id)
|
||||
return 0 if closed_issue_ids.empty?
|
||||
@previous_average_close_time ||= median(HelpdeskTicket.where(:issue_id => closed_issue_ids).pluck(:resolve_time))
|
||||
end
|
||||
|
||||
def average_close_time_progress
|
||||
closed_issue_ids = previous_issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id)
|
||||
return 0 if closed_issue_ids.empty?
|
||||
calculate_progress(previous_average_close_time, average_close_time)
|
||||
end
|
||||
|
||||
# Average responses count
|
||||
def average_response_count
|
||||
return @average_response_count if @average_response_count
|
||||
closed_issue_ids = issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id)
|
||||
return @average_response_count = 0 if closed_issue_ids.empty?
|
||||
journal_ids = JournalMessage.joins(:journal).where("journals.journalized_type = 'Issue'").
|
||||
where(journal_message_date_condition(@query)).
|
||||
where("journals.journalized_id IN (#{closed_issue_ids.join(',')})").pluck(:journal_id)
|
||||
@average_response_count = median(Journal.where(:id => journal_ids).group(:journalized_id).count(:id).values)
|
||||
end
|
||||
|
||||
def previous_average_response_count
|
||||
return 0 if previous_issues_count.zero?
|
||||
return @previous_average_response_count if @previous_average_response_count
|
||||
closed_issue_ids = previous_issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id)
|
||||
return @previous_average_response_count = 0 if closed_issue_ids.empty?
|
||||
journal_ids = JournalMessage.joins(:journal).where("journals.journalized_type = 'Issue'").
|
||||
where(journal_message_date_condition(previous_query)).
|
||||
where("journals.journalized_id IN (#{closed_issue_ids.join(',')})").pluck(:journal_id)
|
||||
|
||||
@previous_average_response_count = median(Journal.where(:id => journal_ids).group(:journalized_id).count(:id).values)
|
||||
end
|
||||
|
||||
def average_response_count_progress
|
||||
return 0 if previous_average_response_count.zero?
|
||||
calculate_progress(previous_average_response_count, average_response_count)
|
||||
end
|
||||
|
||||
# Total replies
|
||||
def total_response_count
|
||||
return @total_response_count if @total_response_count
|
||||
@total_response_count = JournalMessage.joins(:journal).where("journals.journalized_type = 'Issue'").
|
||||
where(journal_message_date_condition(@query)).
|
||||
where("journals.journalized_id IN (#{issues.pluck(:id).join(',')})").
|
||||
count
|
||||
end
|
||||
|
||||
def previous_total_response_count
|
||||
return 0 if previous_issues_count.zero?
|
||||
return @previous_total_response_count if @previous_total_response_count
|
||||
@previous_total_response_count ||= JournalMessage.joins(:journal).where("journals.journalized_type = 'Issue'").
|
||||
where(journal_message_date_condition(previous_query)).
|
||||
where("journals.journalized_id IN (#{previous_issues.pluck(:id).join(',')})").
|
||||
count
|
||||
end
|
||||
|
||||
def total_response_count_progress
|
||||
return 0 if previous_total_response_count.zero?
|
||||
calculate_progress(previous_total_response_count, total_response_count)
|
||||
end
|
||||
|
||||
def issues_count
|
||||
@issues_count ||= issues.count
|
||||
end
|
||||
|
||||
def previous_issues_count
|
||||
@previous_issues_count ||= previous_issues.count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize(query)
|
||||
@query = query
|
||||
@issues = @query.issues.joins(:helpdesk_ticket).where('helpdesk_tickets.first_response_time > 0').uniq
|
||||
@previous_issues = previous_query.issues.joins(:helpdesk_ticket).where('helpdesk_tickets.first_response_time > 0').uniq
|
||||
end
|
||||
|
||||
def collect_columns
|
||||
columns = []
|
||||
RESPONSE_INTERVALS.each do |interval_name, interval_hours|
|
||||
interval_issues_count = find_issues_count(interval_hours)
|
||||
columns << { :name => interval_name, :issues_count => interval_issues_count,
|
||||
:issues_percent => ((interval_issues_count.to_f / issues_count.to_f) * 100).round(2) }
|
||||
end
|
||||
columns
|
||||
end
|
||||
|
||||
def find_issues_count(interval)
|
||||
interval_start = (interval.first.hours + 1).to_i
|
||||
interval_end = interval.last.hours.to_i
|
||||
interval_issues =
|
||||
if interval.last > 0
|
||||
issues.joins(:helpdesk_ticket).where('helpdesk_tickets.first_response_time BETWEEN ? AND ?', interval_start, interval_end)
|
||||
else
|
||||
issues.joins(:helpdesk_ticket).where('helpdesk_tickets.first_response_time > ?', interval_start)
|
||||
end
|
||||
interval_issues.count
|
||||
end
|
||||
|
||||
def previous_query
|
||||
return if @query[:filters].nil? || @query[:filters]['message_date'].nil? || @query[:filters]['message_date'][:operator].nil?
|
||||
return @previous_query if @previous_query
|
||||
|
||||
previous_operator = ['pre_', @query[:filters]['message_date'][:operator]].join
|
||||
previous_filters = @query[:filters].merge('message_date' => { :operator => previous_operator, :values => [Date.today.to_s] })
|
||||
@previous_query = HelpdeskReportsFirstResponseQuery.new(:name => '_', :project => @query.project, :filters => previous_filters)
|
||||
@previous_query
|
||||
end
|
||||
|
||||
def journal_message_date_condition(query)
|
||||
query.send('sql_for_field', nil, query.filters['message_date'][:operator], nil, 'journal_messages', 'message_date')
|
||||
end
|
||||
|
||||
def median(array)
|
||||
return 0 if array.compact.empty?
|
||||
range = array.sort.reverse
|
||||
middle = range.count / 2
|
||||
(range.count % 2).zero? ? (range[middle - 1] + range[middle]) / 2 : range[middle]
|
||||
end
|
||||
|
||||
def calculate_progress(before, now)
|
||||
progress =
|
||||
if before.to_f > now.to_f
|
||||
100 - (now.to_f * 100 / before.to_f)
|
||||
else
|
||||
(100 - (before.to_f * 100 / now.to_f)) * -1
|
||||
end
|
||||
progress.round
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
class HelpdeskDataCollectorManager
|
||||
def initialize(report)
|
||||
@report = report
|
||||
end
|
||||
|
||||
def collect_data(query)
|
||||
case @report
|
||||
when 'first_response_time'
|
||||
HelpdeskDataCollectorFirstResponse.new(query)
|
||||
when 'busiest_time_of_day'
|
||||
HelpdeskDataCollectorBusiestTime.new(query)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,823 @@
|
||||
require "digest/md5"
|
||||
|
||||
class HelpdeskMailer < MailHandler
|
||||
include HelpdeskMailerHelper
|
||||
include AbstractController::Callbacks
|
||||
after_filter :set_delivery_options
|
||||
|
||||
attr_reader :contact, :user, :email, :project
|
||||
|
||||
def self.default_url_options
|
||||
{ :host => Setting.host_name, :protocol => Setting.protocol }
|
||||
end
|
||||
|
||||
def self.with_activated_perform_deliveries
|
||||
perform_delivery_state = ActionMailer::Base.perform_deliveries
|
||||
ActionMailer::Base.perform_deliveries = true
|
||||
yield
|
||||
ensure
|
||||
ActionMailer::Base.perform_deliveries = perform_delivery_state
|
||||
end
|
||||
|
||||
def issue_response(contact, journal, options={})
|
||||
@project = journal.issue.project
|
||||
to_address = options[:to_address] || (journal.journal_message && journal.journal_message.to_address) || contact.primary_email
|
||||
cc_address = options[:cc_address] || (journal.journal_message && journal.journal_message.cc_address)
|
||||
bcc_address = options[:bcc_address] || (journal.journal_message && journal.journal_message.bcc_address)
|
||||
from_address = options[:from_address] || (!HelpdeskSettings["helpdesk_answer_from", journal.issue.project].blank? && HelpdeskSettings["helpdesk_answer_from", journal.issue.project] )|| Setting.mail_from
|
||||
in_reply_to = options[:in_reply_to] || ((journal.issue.helpdesk_ticket.blank? || journal.issue.helpdesk_ticket.message_id.blank?) ? '' : "<#{journal.issue.helpdesk_ticket.message_id}>")
|
||||
|
||||
headers['X-Redmine-Ticket-ID'] = journal.issue.id.to_s
|
||||
@email_header = self.class.apply_macro(HelpdeskSettings["helpdesk_emails_header", journal.issue.project], contact, journal.issue, journal.user) unless HelpdeskSettings["helpdesk_emails_header", journal.issue.project].blank?
|
||||
@email_footer = self.class.apply_macro(HelpdeskSettings["helpdesk_emails_footer", journal.issue.project], contact, journal.issue, journal.user) unless HelpdeskSettings["helpdesk_emails_footer", journal.issue.project].blank?
|
||||
@email_body = self.class.apply_macro(journal.notes, contact, journal.issue, journal.user)
|
||||
@email_body = attachment_macro(@email_body, journal.issue)
|
||||
|
||||
raise MissingInformation.new(l(:text_helpdesk_to_address_cant_be_blank)) if to_address.blank?
|
||||
raise MissingInformation.new(l(:text_helpdesk_message_body_cant_be_blank)) if @email_body.blank?
|
||||
raise MissingInformation.new(l(:text_helpdesk_from_address_cant_be_blank)) if from_address.blank?
|
||||
|
||||
subject_macro = self.class.apply_macro(HelpdeskSettings["helpdesk_answer_subject", journal.issue.project], contact, journal.issue)
|
||||
# subject_macro += " - [##{journal.issue.id}]" if !subject_macro.blank? && !subject_macro.include?("##{journal.issue.id}]")
|
||||
@email_stylesheet = HelpdeskSettings[:helpdesk_helpdesk_css, journal.issue.project].to_s.html_safe
|
||||
|
||||
extract_attachments(journal)
|
||||
|
||||
if journal.details.blank? && journal.private_notes? && journal.notes.present?
|
||||
details_journal = Journal.where('id != ?', journal.id).where(:created_on => journal.created_on).first
|
||||
extract_attachments(details_journal) if details_journal
|
||||
end
|
||||
|
||||
mail :from => self.class.apply_from_macro(from_address.to_s, journal.user),
|
||||
:to => to_address.to_s,
|
||||
:cc => cc_address.to_s,
|
||||
:bcc => bcc_address.to_s,
|
||||
:in_reply_to => in_reply_to.to_s,
|
||||
:subject => (subject_macro.blank? ? journal.issue.subject + " [#{journal.issue.tracker} ##{journal.issue.id}]" : subject_macro) do |format|
|
||||
format.text { render 'email_layout' }
|
||||
format.html { render 'email_layout' } unless RedmineHelpdesk.settings["plain_text_mail"]
|
||||
end
|
||||
end
|
||||
|
||||
def extract_attachments(journal)
|
||||
journal.details.where(:property => 'attachment').each do |attachment_journal|
|
||||
if attach = Attachment.where(:id => attachment_journal.prop_key).first
|
||||
attachments[attach.filename] = File.open(attach.diskfile, 'rb') { |io| io.read }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def auto_answer(contact, issue)
|
||||
@project = issue.project
|
||||
|
||||
headers['X-Redmine-Ticket-ID'] = issue.id.to_s
|
||||
headers['X-Auto-Response-Suppress'] = 'oof'
|
||||
|
||||
confirmation_body = self.class.apply_macro(HelpdeskSettings["helpdesk_first_answer_template", issue.project_id], contact, issue)
|
||||
|
||||
@email_stylesheet = HelpdeskSettings[:helpdesk_helpdesk_css, issue.project_id].to_s.html_safe
|
||||
@email_body = confirmation_body
|
||||
from_address = HelpdeskSettings["helpdesk_answer_from", issue.project].blank? ? Setting.mail_from : HelpdeskSettings["helpdesk_answer_from", issue.project]
|
||||
|
||||
mail :from => self.class.apply_from_macro(from_address.to_s, nil),
|
||||
:to => contact.primary_email,
|
||||
:cc => issue.helpdesk_ticket.try(:cc_address),
|
||||
:subject => self.class.apply_macro(HelpdeskSettings["helpdesk_first_answer_subject", issue.project_id], contact, issue) || "Helpdesk auto answer [Case ##{issue.id}]",
|
||||
:in_reply_to => issue.helpdesk_ticket.try(:message_id) do |format|
|
||||
format.text { render 'email_layout'}
|
||||
format.html { render 'email_layout' } unless RedmineHelpdesk.settings["plain_text_mail"]
|
||||
end
|
||||
|
||||
logger.info "##{issue.id}: Sending confirmation to #{contact.primary_email}" if logger
|
||||
end
|
||||
|
||||
def initial_message(contact, issue, params)
|
||||
@project = issue.project
|
||||
|
||||
headers['X-Redmine-Ticket-ID'] = issue.id.to_s
|
||||
@email_header = self.class.apply_macro(HelpdeskSettings["helpdesk_emails_header", issue.project], contact, issue, issue.author) unless HelpdeskSettings["helpdesk_emails_header", issue.project].blank?
|
||||
@email_footer = self.class.apply_macro(HelpdeskSettings["helpdesk_emails_footer", issue.project], contact, issue, issue.author) unless HelpdeskSettings["helpdesk_emails_footer", issue.project].blank?
|
||||
@email_body = self.class.apply_macro(issue.description, contact, issue, issue.author)
|
||||
|
||||
@email_body = attachment_macro(@email_body, issue)
|
||||
|
||||
raise MissingInformation.new("Contact #{contact.name} should have mail") if contact.email.blank?
|
||||
raise MissingInformation.new("Message shouldn't be blank") if @email_body.blank?
|
||||
|
||||
@email_stylesheet = HelpdeskSettings[:helpdesk_helpdesk_css, issue.project].to_s.html_safe
|
||||
|
||||
params[:attachments].each_value do |mail_attachment|
|
||||
if file = mail_attachment['file']
|
||||
file.rewind if file
|
||||
attachments[file.original_filename] = file.read
|
||||
file.rewind if file
|
||||
elsif token = mail_attachment['token']
|
||||
if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
|
||||
attachment_id, attachment_digest = $1, $2
|
||||
if a = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
|
||||
attachments[a.filename] = File.open(a.diskfile, 'rb'){|io| io.read}
|
||||
end
|
||||
end
|
||||
end
|
||||
end unless params[:attachments].blank?
|
||||
|
||||
to_address = (params[:helpdesk] && !params[:helpdesk][:to_address].blank?) ? params[:helpdesk][:to_address] : contact.primary_email
|
||||
from_address = HelpdeskSettings["helpdesk_answer_from", issue.project].blank? ? Setting.mail_from : HelpdeskSettings["helpdesk_answer_from", issue.project]
|
||||
|
||||
logger.error "##{issue.id}: From address couldn't be black" if from_address.blank? && logger
|
||||
|
||||
mail :from => self.class.apply_from_macro(from_address.to_s, nil),
|
||||
:to => to_address,
|
||||
:subject => self.class.apply_macro(HelpdeskSettings["helpdesk_first_answer_subject", issue.project_id], contact, issue) || issue.subject do |format|
|
||||
format.text { render 'email_layout' }
|
||||
format.html { render 'email_layout' } unless RedmineHelpdesk.settings["plain_text_mail"]
|
||||
end
|
||||
end
|
||||
|
||||
# Receive email methods
|
||||
|
||||
def self.receive(raw_email, options={})
|
||||
@@helpdesk_mailer_options = options.dup
|
||||
raw_email.force_encoding('ASCII-8BIT') if raw_email.respond_to?(:force_encoding)
|
||||
email = Mail.new(raw_email)
|
||||
new.receive(email)
|
||||
end
|
||||
|
||||
# Processes incoming emails
|
||||
# Returns the created object (eg. an issue, a message) or false
|
||||
def receive(email)
|
||||
@email = email
|
||||
if !target_project.module_enabled?(:contacts) || !target_project.module_enabled?(:issue_tracking)
|
||||
logger.error "#{email && email.message_id}: Contacts and issues modules should be enable for #{target_project.name} project" if logger
|
||||
return false
|
||||
end
|
||||
|
||||
@@helpdesk_mailer_options = HelpdeskMailer.get_issue_options(@@helpdesk_mailer_options, target_project.id)
|
||||
sender_email = message_sender(email)
|
||||
# Ignore emails received from the application emission address to avoid hell cycles
|
||||
if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
|
||||
logger.info "#{email && email.message_id}: Ignoring email from Redmine emission address [#{sender_email}]" if logger
|
||||
return false
|
||||
end
|
||||
|
||||
return false unless handle_ignored(email)
|
||||
|
||||
if !check_blacklist?(email)
|
||||
logger.info "#{email && email.message_id}: Email #{sender_email} ignored because in blacklist" if logger
|
||||
return false
|
||||
end
|
||||
|
||||
@user = User.find_by_mail(sender_email) || User.anonymous
|
||||
@contact = contact_from_email(email)
|
||||
|
||||
User.current = @user
|
||||
if @contact
|
||||
logger.info "#{email && email.message_id}: [#{@contact.name}] contact created/founded" if logger
|
||||
else
|
||||
logger.error "#{email && email.message_id}: could not create/found contact for [#{sender_email}]" if logger
|
||||
return false
|
||||
end
|
||||
|
||||
dispatch
|
||||
end
|
||||
|
||||
def self.check_project(project_id)
|
||||
msg_count = 0
|
||||
unless Project.find_by_id(project_id).blank? || HelpdeskSettings[:helpdesk_protocol, project_id].blank?
|
||||
|
||||
mail_options, options = self.get_mail_options(project_id)
|
||||
|
||||
case mail_options[:protocol]
|
||||
when "pop3" then
|
||||
msg_count = RedmineContacts::Mailer.check_pop3(self, mail_options, options)
|
||||
when "imap" then
|
||||
msg_count = RedmineContacts::Mailer.check_imap(self, mail_options, options)
|
||||
end
|
||||
end
|
||||
|
||||
if project = Project.find_by_id(project_id)
|
||||
HelpdeskTicket.autoclose(project)
|
||||
end
|
||||
|
||||
msg_count
|
||||
end
|
||||
|
||||
def self.get_mail_options(project_id)
|
||||
case HelpdeskSettings[:helpdesk_protocol, project_id]
|
||||
when "gmail"
|
||||
protocol = "imap"
|
||||
host = "imap.gmail.com"
|
||||
port = "993"
|
||||
ssl = "1"
|
||||
when "yahoo"
|
||||
protocol = "imap"
|
||||
host = "imap.mail.yahoo.com"
|
||||
port = "993"
|
||||
ssl = "1"
|
||||
when "yandex"
|
||||
protocol = "imap"
|
||||
host = "imap.yandex.ru"
|
||||
port = "993"
|
||||
ssl = "1"
|
||||
else
|
||||
protocol = HelpdeskSettings[:helpdesk_protocol, project_id]
|
||||
host = HelpdeskSettings[:helpdesk_host, project_id]
|
||||
port = HelpdeskSettings[:helpdesk_port, project_id]
|
||||
ssl = HelpdeskSettings[:helpdesk_use_ssl, project_id] != "1" ? nil : "1"
|
||||
end
|
||||
|
||||
mail_options = {:protocol => protocol,
|
||||
:host => host,
|
||||
:port => port,
|
||||
:ssl => ssl,
|
||||
:apop => HelpdeskSettings[:helpdesk_apop, project_id],
|
||||
:username => HelpdeskSettings[:helpdesk_username, project_id],
|
||||
:password => HelpdeskSettings[:helpdesk_password, project_id],
|
||||
:folder => HelpdeskSettings[:helpdesk_imap_folder, project_id],
|
||||
:move_on_success => HelpdeskSettings[:helpdesk_move_on_success, project_id],
|
||||
:move_on_failure => HelpdeskSettings[:helpdesk_move_on_failure, project_id],
|
||||
:delete_unprocessed => HelpdeskSettings[:helpdesk_delete_unprocessed, project_id].to_i > 0
|
||||
}
|
||||
options = get_issue_options({}, project_id)
|
||||
[mail_options, options]
|
||||
end
|
||||
|
||||
def self.get_issue_options(options, project_id)
|
||||
options = { :issue => {} } unless options[:issue]
|
||||
options[:issue][:project_id] = project_id
|
||||
options[:issue][:status_id] = HelpdeskSettings[:helpdesk_new_status, project_id] unless options[:issue][:status_id]
|
||||
options[:issue][:assigned_to_id] = HelpdeskSettings["helpdesk_assigned_to", project_id] unless options[:issue][:assigned_to_id]
|
||||
options[:issue][:tracker_id] = HelpdeskSettings["helpdesk_tracker", project_id] unless options[:issue][:tracker_id]
|
||||
options[:issue][:priority_id] = HelpdeskSettings[:helpdesk_issue_priority, project_id] unless options[:issue][:priority_id]
|
||||
options[:issue][:due_date] = HelpdeskSettings[:helpdesk_issue_due_date, project_id] unless options[:issue][:due_date]
|
||||
options[:issue][:reopen_status_id] = HelpdeskSettings["helpdesk_reopen_status", project_id] unless options[:issue][:reopen_status_id]
|
||||
options
|
||||
end
|
||||
|
||||
def attachment_macro(text, issue)
|
||||
text.scan(/\{\{send_file\(([^%\}]+)\)\}\}/).flatten.each do |file_name|
|
||||
attachment = file_name.match(/^(\d)+$/) ? Attachment.where(:id => file_name).first : issue.attachments.where(:filename => file_name).first
|
||||
attachments[attachment.filename] = File.open(attachment.diskfile, 'rb'){|io| io.read} if attachment
|
||||
end
|
||||
text.gsub(/\{\{send_file\(([^%\}]+)\)\}\}/, '')
|
||||
end
|
||||
|
||||
def self.apply_from_macro(text, journal_user = nil)
|
||||
return text unless text =~ /\A\{%.+%\}.*?<.+@.+\..{2,}>\z/
|
||||
text = text[/<.*>/] if journal_user.nil? || journal_user == User.anonymous
|
||||
text = text.gsub(/\{%response.author%\}/, journal_user.name) if text =~ /\{%response.author%\}/
|
||||
text = text.gsub(/\{%response.author.first_name%\}/, journal_user.firstname) if text =~ /\{%response.author.first_name%\}/
|
||||
text
|
||||
end
|
||||
|
||||
def self.apply_macro(text, contact, issue, journal_user=nil)
|
||||
return '' if text.blank?
|
||||
text = text.gsub(/%%NAME%%|\{%contact.first_name%\}/, contact.first_name)
|
||||
text = text.gsub(/%%FULL_NAME%%|\{%contact.name%\}/, contact.name)
|
||||
text = text.gsub(/%%COMPANY%%|\{%contact.company%\}/, contact.company) if contact.company
|
||||
text = text.gsub(/%%LAST_NAME%%|\{%contact.last_name%\}/, contact.last_name.blank? ? "" : contact.last_name)
|
||||
text = text.gsub(/%%MIDDLE_NAME%%|\{%contact.middle_name%\}/, contact.middle_name.blank? ? "" : contact.middle_name)
|
||||
text = text.gsub(/\{%contact.email%\}/, contact.primary_email.to_s)
|
||||
text = text.gsub(/%%DATE%%|\{%date%\}/, ApplicationHelper.format_date(Date.today))
|
||||
text = text.gsub(/%%ASSIGNEE%%|\{%ticket.assigned_to%\}/, issue.assigned_to.blank? ? "" : issue.assigned_to.name)
|
||||
text = text.gsub(/%%ISSUE_ID%%|\{%ticket.id%\}/, issue.id.to_s) if issue.id
|
||||
text = text.gsub(/%%ISSUE_TRACKER%%|\{%ticket.tracker%\}/, issue.tracker.name) if issue.tracker
|
||||
text = text.gsub(/%%QUOTED_ISSUE_DESCRIPTION%%|\{%ticket.quoted_description%\}/, issue.description.gsub(/^/, "> ")) if issue.description
|
||||
text = text.gsub(/%%PROJECT%%|\{%ticket.project%\}/, issue.project.name) if issue.project
|
||||
text = text.gsub(/%%SUBJECT%%|\{%ticket.subject%\}/, issue.subject) if issue.subject
|
||||
text = text.gsub(/%%NOTE_AUTHOR%%|\{%response.author%\}/, journal_user.name) if journal_user
|
||||
text = text.gsub(/%%NOTE_AUTHOR.FIRST_NAME%%|\{%response.author.first_name%\}/, journal_user.firstname) if journal_user
|
||||
text = text.gsub(/%%NOTE_AUTHOR.LAST_NAME%%|\{%response.author.last_name%\}/, journal_user.lastname) if journal_user
|
||||
text = text.gsub(/\{%ticket.status%\}/, issue.status.name) if issue.status
|
||||
text = text.gsub(/\{%ticket.priority%\}/, issue.priority.name) if issue.priority
|
||||
text = text.gsub(/\{%ticket.estimated_hours%\}/, issue.estimated_hours ? issue.estimated_hours.to_s : "")
|
||||
text = text.gsub(/\{%ticket.done_ratio%\}/, issue.done_ratio.to_s) if issue.done_ratio
|
||||
text = text.gsub(/\{%ticket.closed_on%\}/, issue.closed_on ? ApplicationHelper.format_date(issue.closed_on) : "") if issue.respond_to?(:closed_on)
|
||||
text = text.gsub(/\{%ticket.due_date%\}/, issue.due_date ? ApplicationHelper.format_date(issue.due_date) : "")
|
||||
text = text.gsub(/\{%ticket.start_date%\}/, issue.start_date ? ApplicationHelper.format_date(issue.start_date) : "")
|
||||
text = text.gsub(/\{%ticket.public_url%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.public_ticket_path(issue.helpdesk_ticket.id, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.public_url%\}/) && issue.helpdesk_ticket
|
||||
if RedmineHelpdesk.vote_allow?
|
||||
text = text.gsub(/\{%ticket.voting%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.helpdesk_votes_show_path(issue.helpdesk_ticket.id, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.voting%\}/)
|
||||
text = text.gsub(/\{%ticket.voting.good%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.helpdesk_votes_fast_vote_path(issue.helpdesk_ticket.id, 2, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.voting.good%\}/)
|
||||
text = text.gsub(/\{%ticket.voting.okay%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.helpdesk_votes_fast_vote_path(issue.helpdesk_ticket.id, 1, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.voting.okay%\}/)
|
||||
text = text.gsub(/\{%ticket.voting.bad%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.helpdesk_votes_fast_vote_path(issue.helpdesk_ticket.id, 0, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.voting.bad%\}/)
|
||||
end
|
||||
|
||||
if text =~ /\{%ticket.history%\}/
|
||||
ticket_history = ''
|
||||
issue.journals.eager_load(:journal_message).map(&:journal_message).compact.each do |journal_message|
|
||||
message_author = "*#{l(:label_crm_added_by)} #{journal_message.is_incoming? ? journal_message.from_address : journal_message.journal.user.name}, #{format_time(journal_message.message_date)}*"
|
||||
ticket_history = (message_author + "\n" + journal_message.journal.notes + "\n" + ticket_history).gsub(/^/, "> ")
|
||||
end
|
||||
text = text.gsub(/\{%ticket.history%\}/, ticket_history)
|
||||
end
|
||||
|
||||
issue.custom_field_values.each do |value|
|
||||
text = text.gsub(/%%#{value.custom_field.name}%%/, value.value.to_s)
|
||||
end
|
||||
|
||||
contact.custom_field_values.each do |value|
|
||||
text = text.gsub(/%%#{value.custom_field.name}%%/, value.value.to_s)
|
||||
end if contact.respond_to?("custom_field_values")
|
||||
|
||||
journal_user.custom_field_values.each do |value|
|
||||
text = text.gsub(/\{%response.author.custom_field: #{value.custom_field.name}%\}/, value.value.to_s)
|
||||
end if journal_user
|
||||
|
||||
text
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dispatch
|
||||
m = email.subject && email.subject.match(ISSUE_REPLY_SUBJECT_RE)
|
||||
journal_message = !email.in_reply_to.blank? && JournalMessage.find_by_message_id(email.in_reply_to)
|
||||
helpdesk_ticket = !email.in_reply_to.blank? && HelpdeskTicket.find_by_message_id(email.in_reply_to)
|
||||
if journal_message && journal_message.journal && journal_message.journal.issue
|
||||
receive_issue_reply(journal_message.journal.issue.id)
|
||||
elsif helpdesk_ticket && helpdesk_ticket.issue
|
||||
receive_issue_reply(helpdesk_ticket.issue.id)
|
||||
elsif m && Issue.exists?(m[1].to_i)
|
||||
receive_issue_reply(m[1].to_i)
|
||||
else
|
||||
dispatch_to_default
|
||||
end
|
||||
rescue MissingInformation => e
|
||||
logger.error "#{email && email.message_id}: missing information from #{user}: #{e.message}" if logger
|
||||
false
|
||||
rescue UnauthorizedAction => e
|
||||
logger.error "#{email && email.message_id}: unauthorized attempt from #{user}" if logger
|
||||
false
|
||||
rescue Exception => e
|
||||
# TODO: send a email to the user
|
||||
logger.error "#{email && email.message_id}: dispatch error #{e.message}" if logger
|
||||
false
|
||||
end
|
||||
|
||||
def dispatch_to_default
|
||||
receive_issue
|
||||
end
|
||||
|
||||
def target_project
|
||||
@target_project ||= Project.find_by_identifier(get_keyword(:project) || get_keyword(:project_id))
|
||||
@target_project ||= Project.find_by_id(get_keyword(:project_id)) if @target_project.nil?
|
||||
raise MissingInformation.new('Unable to determine @target_project project') if @target_project.nil?
|
||||
@target_project
|
||||
end
|
||||
|
||||
def helpdesk_issue_attributes_from_keywords(issue)
|
||||
# assigned_to = ((k = get_keyword(:assigned_to_id, :override => true)) && User.find_by_id(k)) || ((k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k))
|
||||
assigned_to = ((k = get_keyword(:assigned_to_id, :override => true)) && (User.find_by_id(k) || Group.find_by_id(k))) || ((k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k))
|
||||
|
||||
attrs = {
|
||||
'status_id' => ((k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id) ) || ((k = get_keyword(:status_id)) && IssueStatus.find_by_id(k).try(:id)),
|
||||
'priority_id' => ((k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id)) || ((k = get_keyword(:priority_id)) && IssuePriority.find_by_id(k).try(:id)),
|
||||
'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
|
||||
'assigned_to_id' => assigned_to.try(:id),
|
||||
'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.named(k).first.try(:id),
|
||||
'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
|
||||
'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
|
||||
'estimated_hours' => get_keyword(:estimated_hours, :override => true),
|
||||
'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
|
||||
}.delete_if {|k, v| v.blank? }
|
||||
|
||||
attrs
|
||||
end
|
||||
|
||||
def calculated_tracker_id(issue)
|
||||
issue_tracker_id = ((k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id)) ||
|
||||
((k = get_keyword(:tracker_id)) && issue.project.trackers.find_by_id(k).try(:id))
|
||||
issue_tracker_id = issue.project.trackers.first.try(:id) unless issue_tracker_id
|
||||
issue_tracker_id
|
||||
end
|
||||
|
||||
# Creates a new issue
|
||||
def receive_issue
|
||||
project = target_project
|
||||
issue = Issue.new
|
||||
issue.author = user
|
||||
issue.project = project
|
||||
issue.safe_attributes = helpdesk_issue_attributes_from_keywords(issue)
|
||||
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
|
||||
issue.tracker_id = calculated_tracker_id(issue)
|
||||
issue.subject = cleaned_up_subject(email)
|
||||
issue.subject = '(no subject)' if issue.subject.blank?
|
||||
issue.description = escaped_cleaned_up_text_body
|
||||
issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
|
||||
|
||||
helpdesk_ticket = HelpdeskTicket.new(:from_address => message_sender(email).downcase.to_s.slice(0, 255),
|
||||
:to_address => email.to_addrs.join(',').downcase.to_s.slice(0, 255),
|
||||
:cc_address => email.cc_addrs.join(',').downcase.to_s.slice(0, 255),
|
||||
:ticket_date => email.date || Time.now,
|
||||
:message_id => email.message_id.to_s.slice(0, 255),
|
||||
:is_incoming => true,
|
||||
:customer => contact,
|
||||
:issue => issue,
|
||||
:source => HelpdeskTicket::HELPDESK_EMAIL_SOURCE)
|
||||
|
||||
issue.helpdesk_ticket = helpdesk_ticket
|
||||
issue.contacts << cc_contacts if HelpdeskSettings["helpdesk_save_cc", target_project.id].to_i > 0
|
||||
issue.assigned_to = @contact.find_assigned_user(project, issue.assigned_to_id)
|
||||
|
||||
|
||||
save_email_as_attachment(helpdesk_ticket)
|
||||
add_attachments(issue)
|
||||
|
||||
Redmine::Hook.call_hook(:helpdesk_mailer_receive_issue_before_save, { :issue => issue, :contact => contact, :helpdesk_ticket => helpdesk_ticket, :email => email})
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
issue.save!(:validate => false)
|
||||
ContactNote.create(:content => "*#{issue.subject}* [#{issue.tracker.name} - ##{issue.id}]\n\n" + issue.description,
|
||||
:type_id => Note.note_types[:email],
|
||||
:source => contact,
|
||||
:author_id => issue.author_id) if HelpdeskSettings["helpdesk_add_contact_notes", project]
|
||||
begin
|
||||
notification = HelpdeskMailer.auto_answer(contact, issue).deliver if HelpdeskSettings["helpdesk_send_notification", project].to_i > 0
|
||||
logger.info "#{email && email.message_id}: notification was sent to #{notification.to_addrs.first}" if logger && notification
|
||||
rescue Exception => e
|
||||
logger.error "#{email && email.message_id}: notification was not sent #{e.message}" if logger
|
||||
false
|
||||
end
|
||||
|
||||
logger.info "#{email && email.message_id}: issue ##{issue.id} created by #{user} for #{contact.name}" if logger
|
||||
issue
|
||||
end #transaction
|
||||
|
||||
end
|
||||
|
||||
# Adds a note to an existing issue
|
||||
def receive_issue_reply(issue_id)
|
||||
issue = Issue.find_by_id(issue_id)
|
||||
return unless issue
|
||||
# if lifetime expaired create new issue
|
||||
if (HelpdeskSettings["helpdesk_lifetime", target_project].to_i > 0) && issue.journals && issue.journals.last && ((Date.today) - issue.journals.last.created_on.to_date > HelpdeskSettings["helpdesk_lifetime", target_project].to_i)
|
||||
email.subject = email.subject.to_s.gsub(ISSUE_REPLY_SUBJECT_RE, '')
|
||||
return receive_issue
|
||||
end
|
||||
journal = issue.init_journal(user)
|
||||
journal.notes = escaped_cleaned_up_text_body
|
||||
|
||||
journal_message = JournalMessage.create(:from_address => message_sender(email).downcase,
|
||||
:to_address => email.to_addrs.join(',').downcase,
|
||||
:bcc_address => email.bcc_addrs.join(',').downcase,
|
||||
:cc_address => email.cc_addrs.join(',').downcase,
|
||||
:message_id => email.message_id,
|
||||
:is_incoming => true,
|
||||
:message_date => email.date || Time.now,
|
||||
:contact => contact,
|
||||
:journal => journal)
|
||||
|
||||
issue.contacts << cc_contacts if HelpdeskSettings["helpdesk_save_cc", target_project.id].to_i > 0
|
||||
|
||||
add_attachments(issue)
|
||||
|
||||
save_email_as_attachment(journal_message, "reply-#{DateTime.now.strftime('%d.%m.%y-%H.%M.%S')}.eml")
|
||||
|
||||
if reopen_status_id = ((k = @@helpdesk_mailer_options[:reopen_status]) && IssueStatus.named(k).first.try(:id) ) || ((k = get_keyword(:reopen_status_id)) && IssueStatus.find_by_id(k).try(:id))
|
||||
issue.status_id = reopen_status_id
|
||||
end
|
||||
|
||||
issue.save!
|
||||
logger.info "#{email && email.message_id}: issue ##{issue.id} updated by #{user}" if logger
|
||||
journal
|
||||
end
|
||||
|
||||
# Reply will be added to the issue
|
||||
def receive_journal_reply(journal_id)
|
||||
journal = Journal.find_by_id(journal_id)
|
||||
if journal && journal.journalized_type == 'Issue'
|
||||
receive_issue_reply(journal.journalized_id)
|
||||
end
|
||||
end
|
||||
|
||||
def add_attachments(obj)
|
||||
fwd_attachments = email.parts.map { |p|
|
||||
if p.content_type =~ /message\/rfc822/
|
||||
Mail.new(p.body).attachments
|
||||
elsif p.parts.empty?
|
||||
p if p.attachment?
|
||||
else
|
||||
p.attachments
|
||||
end
|
||||
}.flatten.compact
|
||||
|
||||
email_attachments = fwd_attachments | email.attachments
|
||||
|
||||
unless email_attachments.blank?
|
||||
email_attachments.each do |attachment|
|
||||
if RUBY_VERSION < '1.9'
|
||||
attachment_filename = (attachment[:content_type].filename rescue nil) ||
|
||||
(attachment[:content_disposition].filename rescue nil) ||
|
||||
(attachment[:content_location].location rescue nil) ||
|
||||
"attachment"
|
||||
attachment_filename = Mail::Encodings.unquote_and_convert_to(attachment_filename, 'utf-8') rescue 'unprocessable_filename'
|
||||
attachment_filename = helpdesk_to_utf8(attachment_filename)
|
||||
else
|
||||
attachment_filename = helpdesk_to_utf8(attachment.filename, 'binary')
|
||||
end
|
||||
|
||||
new_attachment = Attachment.new(:container => obj,
|
||||
:file => (attachment.decoded rescue nil) || (attachment.decode_body rescue nil) || attachment.raw_source,
|
||||
:filename => attachment_filename,
|
||||
:author => user,
|
||||
:content_type => attachment.mime_type)
|
||||
if obj.attachments.where(:digest => attachment_digest(attachment.body.to_s)).empty? && accept_attachment?(new_attachment)
|
||||
obj.attachments << new_attachment
|
||||
logger.info "#{email && email.message_id}: attachment #{attachment_filename} added to ticket: '#{obj.subject}'" if logger
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_keyword(attr, options = {})
|
||||
@keywords ||= {}
|
||||
if !@keywords.has_key?(attr)
|
||||
if (options[:override] || attr_overridable?(attr)) &&
|
||||
v = extract_keyword!(escaped_cleaned_up_text_body, attr, options[:format])
|
||||
@keywords[attr] = v
|
||||
elsif !@@helpdesk_mailer_options[:issue][attr].blank?
|
||||
@keywords[attr] = @@helpdesk_mailer_options[:issue][attr]
|
||||
end
|
||||
end
|
||||
@keywords[attr]
|
||||
end
|
||||
|
||||
def attr_overridable?(attr)
|
||||
@@helpdesk_mailer_options[:allow_override].present? &&
|
||||
@@helpdesk_mailer_options[:allow_override].include?(attr.to_s)
|
||||
end
|
||||
|
||||
def find_user_from_keyword(keyword)
|
||||
user ||= User.find_by_mail(keyword)
|
||||
user ||= User.find_by_login(keyword)
|
||||
if user.nil? && keyword.match(/ /)
|
||||
firstname, lastname = *(keyword.split) # "First Last Throwaway"
|
||||
user ||= User.find_by_firstname_and_lastname(firstname, lastname)
|
||||
end
|
||||
user
|
||||
end
|
||||
|
||||
def check_blacklist?(email)
|
||||
return true if HelpdeskSettings["helpdesk_blacklist", target_project].blank?
|
||||
addr = email.from_addrs.first.to_s.strip
|
||||
from_addr = addr # (addr && !addr.spec.blank?) ? addr.spec : email.header["from"].inspect.match(/[-A-z0-9.]+@[-A-z0-9.]+/).to_s
|
||||
cond = "(" + HelpdeskSettings["helpdesk_blacklist", target_project].split("\n").map{|u| u.strip unless u.blank?}.compact.join('|') + ")"
|
||||
!from_addr.match(cond)
|
||||
end
|
||||
|
||||
def new_contact_from_attributes(email_address, fullname=nil)
|
||||
contact = Contact.new
|
||||
|
||||
# Truncating the email address would result in an invalid format
|
||||
contact.email = email_address
|
||||
names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
|
||||
contact.first_name = names.shift.slice(0, 255)
|
||||
contact.last_name = names.join(' ').slice(0, 255)
|
||||
contact.company = email_address.downcase.slice(0, 255)
|
||||
contact.last_name = '-' if contact.last_name.blank?
|
||||
|
||||
if contact.last_name =~ %r(\((.*)\))
|
||||
contact.last_name, contact.company = $`, $1
|
||||
end
|
||||
|
||||
if contact.first_name =~ /,$/
|
||||
contact.first_name = contact.last_name
|
||||
contact.last_name = $` # everything before the match
|
||||
end
|
||||
|
||||
contact.projects << target_project
|
||||
contact.tag_list = HelpdeskSettings["helpdesk_created_contact_tag", target_project] if HelpdeskSettings["helpdesk_created_contact_tag", target_project]
|
||||
|
||||
contact
|
||||
end
|
||||
|
||||
def cc_contacts
|
||||
email[:cc].to_s
|
||||
email.cc_addrs.each_with_index.map do |cc_addr, index|
|
||||
cc_name = email[:cc].display_names[index]
|
||||
create_contact_from_address(cc_addr, cc_name)
|
||||
end.compact
|
||||
end
|
||||
|
||||
def create_contact_from_address(addr, name)
|
||||
contacts = Contact.find_by_emails([addr])
|
||||
unless contacts.blank?
|
||||
contact = contacts.first
|
||||
if contact.projects.blank? || HelpdeskSettings[:helpdesk_add_contact_to_project, target_project].to_i > 0
|
||||
contact.projects << target_project
|
||||
contact.save!
|
||||
end
|
||||
|
||||
return contact
|
||||
end
|
||||
|
||||
if HelpdeskSettings["helpdesk_is_not_create_contacts", target_project].to_i > 0
|
||||
logger.error "#{email && email.message_id}: can't find contact with email: #{addr} in whitelist. Not create new contacts option enable" if logger
|
||||
nil
|
||||
else
|
||||
contact = new_contact_from_attributes(addr, name)
|
||||
if contact.save(:validate => false)
|
||||
contact
|
||||
else
|
||||
logger.error "Helpdeks MailHandler: failed to create Contact: #{contact.errors.full_messages}" if logger
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Get or create contact for the +email+ sender
|
||||
def contact_from_email(email)
|
||||
# from = email.header['from'].to_s
|
||||
# debugger
|
||||
from = cleaned_up_from_address
|
||||
addr, name = from, nil
|
||||
if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
|
||||
addr, name = m[2], m[1]
|
||||
end
|
||||
if addr.present?
|
||||
create_contact_from_address(addr, name)
|
||||
else
|
||||
logger.error "#{email && email.message_id}: failed to create Contact: no FROM address found" if logger
|
||||
nil
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Returns a Hash of issue custom field values extracted from keywords in the email body
|
||||
def custom_field_values_from_keywords(customized)
|
||||
customized.custom_field_values.inject({}) do |h, v|
|
||||
if value = get_keyword(v.custom_field.name, :override => true)
|
||||
h[v.custom_field.id.to_s] = value
|
||||
end
|
||||
h
|
||||
end
|
||||
end
|
||||
|
||||
def save_email_as_attachment(container, filename="message.eml")
|
||||
Attachment.create(:container => container,
|
||||
:file => email.raw_source.to_s,
|
||||
:author => user,
|
||||
:filename => filename,
|
||||
:content_type => "message/rfc822")
|
||||
end
|
||||
|
||||
def plain_text_body
|
||||
return @plain_text_body unless @plain_text_body.nil?
|
||||
part = email.text_part || email.html_part
|
||||
unless part
|
||||
return @plain_text_body = '' if email.parts.present? && email.parts.all? { |part| part.attachment? }
|
||||
part = email
|
||||
end
|
||||
|
||||
is_html = email.text_part.blank?
|
||||
part_charset = Mail::RubyVer.pick_encoding(part.charset).to_s rescue part.charset
|
||||
@plain_text_body = helpdesk_to_utf8(part.body.decoded, part_charset)
|
||||
|
||||
# strip html tags and remove doctype directive
|
||||
@plain_text_body.gsub! %r{^[ ]+}, ''
|
||||
if is_html && RedmineHelpdesk.strip_tags?
|
||||
@plain_text_body.gsub! %r{<head>(?:.|\n|\r)+?<\/head>}, ""
|
||||
@plain_text_body.gsub! %r{<\/(li|ol|ul|h1|h2|h3|h4)>}, "\r\n"
|
||||
@plain_text_body.gsub! %r{<\/(p|div|pre)>}, "\r\n\r\n"
|
||||
@plain_text_body.gsub! %r{<li>}, " - "
|
||||
@plain_text_body.gsub! %r{<br[^>]*>}, "\r\n"
|
||||
@plain_text_body = strip_tags(@plain_text_body.strip)
|
||||
@plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
|
||||
end
|
||||
@plain_text_body.strip
|
||||
|
||||
rescue Exception => e
|
||||
logger.error "#{email && email.message_id}: Message body processing error - #{e.message}" if logger
|
||||
@plain_text_body = '(Unprocessable message body)'
|
||||
end
|
||||
|
||||
def cleaned_up_subject(email)
|
||||
return "" if email[:subject].blank?
|
||||
subject = decode_subject(email[:subject].value)
|
||||
subject = helpdesk_to_utf8(subject)
|
||||
subject.strip[0,255]
|
||||
rescue Exception => e
|
||||
logger.error "#{email && email.message_id}: Message subject processing error - #{e.message}" if logger
|
||||
'(Unprocessable subject)'
|
||||
end
|
||||
|
||||
def cleaned_up_from_address
|
||||
from = email.header['reply-to'].to_s.present? ? email.header['reply-to'] : email.header['from']
|
||||
from.to_s.strip[0, 255]
|
||||
end
|
||||
|
||||
def logger
|
||||
HelpdeskLogger
|
||||
end
|
||||
|
||||
def helpdesk_to_utf8(str, encoding="UTF-8")
|
||||
return str if str.nil?
|
||||
if str.respond_to?(:force_encoding)
|
||||
begin
|
||||
cleaned = str.force_encoding('UTF-8')
|
||||
cleaned = cleaned.encode("UTF-8", encoding) if encoding.upcase == 'ISO-2022-JP'
|
||||
unless cleaned.valid_encoding?
|
||||
cleaned = str.encode('UTF-8', encoding, :invalid => :replace, :undef => :replace, :replace => '').chars.select{|i| i.valid_encoding?}.join
|
||||
end
|
||||
str = cleaned
|
||||
rescue EncodingError
|
||||
str.encode!( 'UTF-8', :invalid => :replace, :undef => :replace )
|
||||
end
|
||||
elsif RUBY_PLATFORM == 'java'
|
||||
begin
|
||||
ic = Iconv.new('UTF-8', encoding + '//IGNORE')
|
||||
str = ic.iconv(str)
|
||||
rescue
|
||||
str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
|
||||
end
|
||||
else
|
||||
ic = Iconv.new('UTF-8', encoding + '//IGNORE')
|
||||
txtar = ""
|
||||
begin
|
||||
txtar += ic.iconv(str)
|
||||
rescue Iconv::IllegalSequence
|
||||
txtar += $!.success
|
||||
str = '?' + $!.failed[1,$!.failed.length]
|
||||
retry
|
||||
rescue
|
||||
txtar += $!.success
|
||||
end
|
||||
str = txtar
|
||||
end
|
||||
str
|
||||
end
|
||||
|
||||
def attachment_digest(file_source)
|
||||
encoder = Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch? ? Digest::SHA256.new : Digest::MD5.new
|
||||
encoder.update(file_source)
|
||||
encoder.hexdigest
|
||||
end
|
||||
|
||||
def set_delivery_options
|
||||
return false if HelpdeskSettings[:helpdesk_smtp_use_default_settings, project.id].to_i == 0
|
||||
message.delivery_method(:smtp)
|
||||
settings = {:address => HelpdeskSettings[:helpdesk_smtp_server, project.id],
|
||||
:port => HelpdeskSettings[:helpdesk_smtp_port, project.id] || 25,
|
||||
:domain => HelpdeskSettings[:helpdesk_smtp_domain, project.id],
|
||||
:enable_starttls_auto => true,
|
||||
:ssl => HelpdeskSettings[:helpdesk_smtp_ssl, project.id].to_i > 0 &&
|
||||
HelpdeskSettings[:helpdesk_smtp_tls, project.id].to_i == 0}
|
||||
authentication = HelpdeskSettings[:helpdesk_smtp_authentication, project.id]
|
||||
unless authentication.blank?
|
||||
settings.merge!(:authentication => authentication,
|
||||
:user_name => HelpdeskSettings[:helpdesk_smtp_username, project.id],
|
||||
:password => HelpdeskSettings[:helpdesk_smtp_password, project.id])
|
||||
end
|
||||
message.delivery_method.settings.merge!(settings)
|
||||
end
|
||||
|
||||
def decode_subject(str)
|
||||
# Optimization: If there's no encoded-words in the string, just return it
|
||||
return str unless str.index("=?")
|
||||
|
||||
str = str.gsub(/\?=(\s*)=\?/, '?=????=?') # Replace whitespaces between 'encoded-word's on special symbols
|
||||
str.split('????').map do |text|
|
||||
if text.index('=?') .nil?
|
||||
text
|
||||
else
|
||||
text.gsub!(/[\r\n]/, '')
|
||||
text.scan(/\=\?.+?\?[qQbB]\?.+?\?\=/).map do |part|
|
||||
if part.index(/\=\?.+\?[Bb]\?.+\?\=/m)
|
||||
part.gsub(/\=\?.+\?[Bb]\?.+\?=/m) { |substr| Mail::Encodings.b_value_decode(substr) }
|
||||
elsif part.index(/\=\?.+\?[Qq]\?.+\?\=/m)
|
||||
part.gsub(/\=\?.+\?[Qq]\?.+\?\=/m) { |substr| Mail::Encodings.q_value_decode(substr) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end.join('')
|
||||
end
|
||||
|
||||
def self.ignored_helpdesk_headers
|
||||
helpdesk_headers = {
|
||||
'X-Auto-Response-Suppress' => /\A(all|AutoReply|oof)/
|
||||
}
|
||||
ignored_emails_headers.merge(helpdesk_headers)
|
||||
end
|
||||
|
||||
def handle_ignored(email)
|
||||
self.class.ignored_helpdesk_headers.each do |key, ignored_value|
|
||||
value = email.header[key]
|
||||
if value
|
||||
value = value.to_s.downcase
|
||||
if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
|
||||
if logger
|
||||
logger.info "#{email && email.message_id}: ignoring email with #{key}:#{value} header"
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
def escaped_cleaned_up_text_body
|
||||
text_body = cleaned_up_text_body
|
||||
return text_body unless (ActiveRecord::Base.connection.adapter_name =~ /mysql/i).present?
|
||||
text_body.gsub(/./) { |c| c.bytesize == 4 ? ' ' : c }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
class HelpdeskReportsBusiestTimeQuery < HelpdeskReportsQuery
|
||||
def sql_for_staff_field(_field, operator, value)
|
||||
issue_table = Issue.table_name
|
||||
compare = operator == '=' ? 'IN' : 'NOT IN'
|
||||
staff_ids = value.join(',')
|
||||
"#{issue_table}.id IN(SELECT #{issue_table}.id FROM #{issue_table} WHERE (#{issue_table}.assigned_to_id #{compare} (#{staff_ids})))"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collect_answered_users
|
||||
return [] unless project
|
||||
user_ids = Issue.joins(:project).
|
||||
visible.uniq.
|
||||
pluck(:assigned_to_id).compact
|
||||
User.where(:id => user_ids)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
class HelpdeskReportsFirstResponseQuery < HelpdeskReportsQuery
|
||||
def sql_for_staff_field(_field, operator, value)
|
||||
issue_table = Issue.table_name
|
||||
journal_table = Journal.table_name
|
||||
compare = operator == '=' ? 'IN' : 'NOT IN'
|
||||
staff_ids = value.join(',')
|
||||
"#{issue_table}.id IN(SELECT #{issue_table}.id FROM #{issue_table} INNER JOIN #{journal_table} ON #{journal_table}.journalized_id = #{issue_table}.id AND #{journal_table}.journalized_type = 'Issue' WHERE (#{journal_table}.user_id #{compare} (#{staff_ids})))"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collect_answered_users
|
||||
return [] unless project
|
||||
user_ids = Issue.joins(:project).
|
||||
joins(:journals).
|
||||
joins(:journals => :journal_message).
|
||||
visible.uniq.
|
||||
pluck(:assigned_to_id).compact
|
||||
User.where(:id => user_ids)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,94 @@
|
||||
class HelpdeskReportsQuery < Query
|
||||
self.queried_class = JournalMessage
|
||||
operators_by_filter_type[:time_interval] = ['t', 'ld', 'w', 'l2w', 'm', 'lm', 'y']
|
||||
|
||||
def initialize_available_filters
|
||||
add_available_filter 'message_date', :type => :time_interval, :name => l(:label_helpdesk_filter_time_interval)
|
||||
author_values = collect_answered_users.collect { |user| [user.name, user.id.to_s] }
|
||||
add_available_filter 'staff', :type => :list, :name => l(:field_assigned_to), :values => author_values
|
||||
end
|
||||
|
||||
def build_from_params(params)
|
||||
if params[:fields] || params[:f]
|
||||
self.filters = {}
|
||||
add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
|
||||
else
|
||||
available_filters.keys.each do |field|
|
||||
add_short_filter(field, params[field]) if params[field]
|
||||
end
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
def issues(options = {})
|
||||
scope = issue_scope.eager_load((options[:include] || []).uniq).
|
||||
where(options[:conditions]).
|
||||
limit(options[:limit]).
|
||||
offset(options[:offset])
|
||||
scope
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid.new(e.message)
|
||||
end
|
||||
|
||||
def sql_for_staff_field(_field, operator, value)
|
||||
issue_table = Issue.table_name
|
||||
journal_table = Journal.table_name
|
||||
compare = operator == '=' ? 'IN' : 'NOT IN'
|
||||
staff_ids = value.join(',')
|
||||
"#{issue_table}.id IN(SELECT #{issue_table}.id FROM #{issue_table} INNER JOIN #{journal_table} ON #{journal_table}.journalized_id = #{issue_table}.id AND #{journal_table}.journalized_type = 'Issue' WHERE (#{journal_table}.user_id #{compare} (#{staff_ids})))"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collect_answered_users
|
||||
return [] unless project
|
||||
user_ids = Issue.joins(:project).
|
||||
joins(:journals).
|
||||
joins(:journals => :journal_message).
|
||||
visible.uniq.
|
||||
pluck(:assigned_to_id).compact
|
||||
User.where(:id => user_ids)
|
||||
end
|
||||
|
||||
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter = false)
|
||||
sql = ''
|
||||
first_day_of_week = l(:general_first_day_of_week).to_i
|
||||
date = Date.today
|
||||
day_of_week = date.cwday
|
||||
case operator
|
||||
when 'pre_t'
|
||||
sql = date_clause_selector(db_table, db_field, -1, -1, is_custom_filter)
|
||||
when 'pre_ld'
|
||||
sql = date_clause_selector(db_table, db_field, -2, -2, is_custom_filter)
|
||||
when 'pre_w'
|
||||
days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
|
||||
sql = date_clause_selector(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
|
||||
when 'pre_l2w'
|
||||
days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
|
||||
sql = date_clause_selector(db_table, db_field, - days_ago - 28, - days_ago - 14 - 1, is_custom_filter)
|
||||
when 'pre_m'
|
||||
sql = date_clause_selector_for_date(db_table, db_field, (date - 1.month).beginning_of_month, (date - 1.month).end_of_month, is_custom_filter)
|
||||
when 'pre_lm'
|
||||
sql = date_clause_selector_for_date(db_table, db_field, (date - 2.months).beginning_of_month, (date - 2.months).end_of_month, is_custom_filter)
|
||||
when 'pre_y'
|
||||
sql = date_clause_selector_for_date(db_table, db_field, (date - 1.year).beginning_of_year, (date - 1.year).end_of_year, is_custom_filter)
|
||||
end
|
||||
sql = super(field, operator, value, db_table, db_field, is_custom_filter) if sql.blank?
|
||||
|
||||
sql
|
||||
end
|
||||
|
||||
def date_clause_selector(table, field, from, to, is_custom_filter)
|
||||
return date_clause(table, field, (from ? Date.today + from : nil), (to ? Date.today + to : nil)) if Redmine::VERSION.to_s < '3.0'
|
||||
date_clause(table, field, (from ? Date.today + from : nil), (to ? Date.today + to : nil), is_custom_filter)
|
||||
end
|
||||
|
||||
def date_clause_selector_for_date(table, field, date_from, date_to, is_custom_filter)
|
||||
return date_clause(table, field, date_from, date_to) if Redmine::VERSION.to_s < '3.0'
|
||||
date_clause(table, field, date_from, date_to, is_custom_filter)
|
||||
end
|
||||
|
||||
def issue_scope
|
||||
Issue.visible.joins(:project, :journals => :journal_message).where(statement).uniq
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,268 @@
|
||||
class HelpdeskTicket < ActiveRecord::Base
|
||||
HELPDESK_EMAIL_SOURCE = 0
|
||||
HELPDESK_WEB_SOURCE = 1
|
||||
HELPDESK_PHONE_SOURCE = 2
|
||||
HELPDESK_TWITTER_SOURCE = 3
|
||||
HELPDESK_CONVERSATION_SOURCE = 4
|
||||
|
||||
SEND_AS_NOTIFICATION = 1
|
||||
SEND_AS_MESSAGE = 2
|
||||
|
||||
attr_accessible :vote, :vote_comment,:from_address,
|
||||
:to_address, :cc_address, :ticket_date,
|
||||
:message_id, :is_incoming, :customer, :issue, :source, :contact_id, :ticket_time
|
||||
|
||||
attr_accessor :ticket_time
|
||||
|
||||
unloadable
|
||||
belongs_to :customer, :class_name => 'Contact', :foreign_key => 'contact_id'
|
||||
belongs_to :issue
|
||||
has_one :message_file, :class_name => "Attachment", :as => :container, :dependent => :destroy
|
||||
|
||||
acts_as_attachable :view_permission => :view_issues,
|
||||
:delete_permission => :edit_issues
|
||||
|
||||
|
||||
if ActiveRecord::VERSION::MAJOR >= 4
|
||||
acts_as_activity_provider :type => 'helpdesk_tickets',
|
||||
:permission => :view_helpdesk_tickets,
|
||||
:timestamp => "#{table_name}.ticket_date",
|
||||
:author_key => "#{Issue.table_name}.author_id",
|
||||
:scope => eager_load(:issue => :project)
|
||||
else
|
||||
acts_as_activity_provider :type => 'helpdesk_tickets',
|
||||
:permission => :view_helpdesk_tickets,
|
||||
:timestamp => "#{table_name}.ticket_date",
|
||||
:author_key => "#{Issue.table_name}.author_id",
|
||||
:find_options => {:include => {:issue => :project}}
|
||||
end
|
||||
|
||||
acts_as_event :datetime => :ticket_date,
|
||||
:project_key => "#{Project.table_name}.id",
|
||||
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue_id}},
|
||||
:type => Proc.new {|o| 'icon icon-email' + (o.issue.closed? ? ' closed' : '') if o.issue },
|
||||
:title => Proc.new {|o| "##{o.issue.id} (#{o.issue.status}): #{o.issue.subject}" if o.issue },
|
||||
:author => Proc.new {|o| o.customer},
|
||||
:description => Proc.new{|o| o.issue.description if o.issue}
|
||||
|
||||
accepts_nested_attributes_for :customer
|
||||
|
||||
after_create :set_ticket_private
|
||||
before_save :calculate_metrics
|
||||
validates_presence_of :customer, :ticket_date
|
||||
|
||||
def initialize(attributes=nil, *args)
|
||||
super
|
||||
if new_record?
|
||||
# set default values for new records only
|
||||
self.ticket_date ||= Time.now
|
||||
self.source ||= HelpdeskTicket::HELPDESK_EMAIL_SOURCE
|
||||
end
|
||||
end
|
||||
|
||||
def ticket_time
|
||||
self.ticket_date.to_s(:time) unless self.ticket_date.blank?
|
||||
end
|
||||
|
||||
def ticket_time=(val)
|
||||
if !self.ticket_date.blank? && val.to_s.gsub(/\s/, "").match(/^(\d{1,2}):(\d{1,2})$/)
|
||||
timezone = ticket_date.try(:time_zone).try(:name) || Time.zone.name
|
||||
self.ticket_date = ActiveSupport::TimeZone.new(timezone).local_to_utc(self.ticket_date.utc).
|
||||
in_time_zone(timezone).
|
||||
change({:hour => $1.to_i % 24, :min => $2.to_i % 60})
|
||||
end
|
||||
end
|
||||
|
||||
def recalculate_events
|
||||
unless issue.closed?
|
||||
close_journal_id = nil
|
||||
end
|
||||
end
|
||||
|
||||
def available_addresses
|
||||
@available_addresses ||= ([self.default_to_address] | self.customer.emails.map{|e| e} | [self.from_address.blank? ? nil : self.from_address.downcase.strip]).compact.uniq if self.customer
|
||||
end
|
||||
|
||||
def default_to_address
|
||||
return last_response_address if last_journal_message && last_journal_message.is_incoming?
|
||||
address = self.from_address.blank? ? "" : self.from_address.downcase.strip
|
||||
self.customer.emails.include?(address) ? address : self.customer.primary_email
|
||||
end
|
||||
|
||||
def last_reply_customer
|
||||
return customer unless default_to_address
|
||||
customer.primary_email == default_to_address ? customer : Contact.find_by_emails([default_to_address]).first
|
||||
end
|
||||
|
||||
def cc_addresses
|
||||
@cc_addresses = ((self.issue.contacts ? self.issue.contacts.map(&:primary_email) : []) | cc_address.to_s.split(',')).compact.uniq
|
||||
end
|
||||
|
||||
def project
|
||||
issue.project if issue
|
||||
end
|
||||
|
||||
def author
|
||||
issue.author if issue
|
||||
end
|
||||
|
||||
def customer_name
|
||||
customer.name if customer
|
||||
end
|
||||
|
||||
def responses
|
||||
@responses ||= JournalMessage.
|
||||
joins(:journal).
|
||||
where(:journals => {:journalized_id => self.issue_id}).
|
||||
order("#{JournalMessage.table_name}.message_date ASC")
|
||||
end
|
||||
|
||||
def reaction_date
|
||||
@reaction_date ||= self.issue.journals.
|
||||
joins(:journal_message).
|
||||
where("#{JournalMessage.table_name}.journal_id IS NULL OR #{JournalMessage.table_name}.is_incoming = ?", false).
|
||||
order("#{Journal.table_name}.created_on ASC").
|
||||
first.
|
||||
try(:created_on).try(:utc)
|
||||
end
|
||||
|
||||
def response_addresses
|
||||
responses.where(:is_incoming => true).map { |response| response.from_address }.uniq
|
||||
end
|
||||
|
||||
def first_response_date
|
||||
@first_response_date ||= responses.select {|r| !r.is_incoming? }.first.try(:message_date).try(:utc)
|
||||
end
|
||||
|
||||
def last_response_time
|
||||
@last_response_time ||= last_journal_message && last_journal_message.is_incoming? && !self.issue.closed? ? last_journal_message.message_date.utc : nil
|
||||
end
|
||||
|
||||
def last_response_address
|
||||
response_addresses.last
|
||||
end
|
||||
|
||||
def last_agent_response
|
||||
@last_agent_response ||= responses.select { |r| !r.is_incoming? }.last
|
||||
end
|
||||
|
||||
def last_journal_message
|
||||
@last_journal_message ||= responses.last
|
||||
end
|
||||
|
||||
def last_customer_response
|
||||
@last_customer_response ||= responses.select { |r| r.is_incoming? }.last
|
||||
end
|
||||
|
||||
def average_response_time
|
||||
|
||||
end
|
||||
|
||||
def ticket_source_name
|
||||
case self.source
|
||||
when HelpdeskTicket::HELPDESK_EMAIL_SOURCE then l(:label_helpdesk_tickets_email)
|
||||
when HelpdeskTicket::HELPDESK_PHONE_SOURCE then l(:label_helpdesk_tickets_phone)
|
||||
when HelpdeskTicket::HELPDESK_WEB_SOURCE then l(:label_helpdesk_tickets_web)
|
||||
when HelpdeskTicket::HELPDESK_TWITTER_SOURCE then l(:label_helpdesk_tickets_twitter)
|
||||
when HelpdeskTicket::HELPDESK_CONVERSATION_SOURCE then l(:label_helpdesk_tickets_conversation)
|
||||
else ""
|
||||
end
|
||||
end
|
||||
|
||||
def ticket_source_icon
|
||||
case self.source
|
||||
when HelpdeskTicket::HELPDESK_EMAIL_SOURCE then "icon-email"
|
||||
when HelpdeskTicket::HELPDESK_PHONE_SOURCE then "icon-call"
|
||||
when HelpdeskTicket::HELPDESK_WEB_SOURCE then "icon-web"
|
||||
when HelpdeskTicket::HELPDESK_TWITTER_SOURCE then "icon-twitter"
|
||||
else "icon-helpdesk"
|
||||
end
|
||||
end
|
||||
|
||||
def content
|
||||
issue.description if issue
|
||||
end
|
||||
|
||||
def customer_email
|
||||
customer.primary_email if customer
|
||||
end
|
||||
|
||||
def last_message
|
||||
@last_message ||= JournalMessage.eager_load(:journal => :issue).where(:issues => {:id => issue.id}).order("#{Journal.table_name}.created_on ASC").last || self
|
||||
end
|
||||
|
||||
def last_message_date
|
||||
last_message.is_a?(HelpdeskTicket) ? self.ticket_date : last_message.message_date if last_message
|
||||
end
|
||||
|
||||
def ticket_date
|
||||
return nil if super.blank?
|
||||
zone = User.current.time_zone
|
||||
zone ? super.in_time_zone(zone) : (super.utc? ? super.localtime : super)
|
||||
end
|
||||
|
||||
def token
|
||||
Digest::MD5.hexdigest("#{issue.id}:#{self.ticket_date.utc}:#{Rails.application.config.secret_token}")
|
||||
end
|
||||
|
||||
def calculate_metrics
|
||||
self.reaction_time = reaction_date - ticket_date.utc if reaction_date && ticket_date
|
||||
self.first_response_time = first_response_date - ticket_date.utc if first_response_date && ticket_date
|
||||
self.resolve_time = self.issue.closed? ? self.issue.closed_on - ticket_date.utc : nil if ticket_date && self.issue.closed_on && last_agent_response
|
||||
self.last_agent_response_at = last_agent_response.message_date if last_agent_response
|
||||
self.last_customer_response_at = last_customer_response.message_date if last_customer_response
|
||||
end
|
||||
|
||||
def self.vote_message(vote)
|
||||
case vote.to_i
|
||||
when 0
|
||||
l(:label_helpdesk_mark_notgood)
|
||||
when 1
|
||||
l(:label_helpdesk_mark_justok)
|
||||
when 2
|
||||
l(:label_helpdesk_mark_awesome)
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
def update_vote(new_vote, comment = nil)
|
||||
old_vote = vote
|
||||
old_vote_comment = vote_comment
|
||||
if update_attributes(:vote => new_vote, :vote_comment => comment )
|
||||
if old_vote != vote || old_vote_comment != vote_comment
|
||||
journal = Journal.new(:journalized => issue, :user => User.current)
|
||||
journal.details << JournalDetail.new(:property => 'attr',
|
||||
:prop_key => 'vote',
|
||||
:old_value => old_vote,
|
||||
:value => vote) if old_vote != vote
|
||||
journal.details << JournalDetail.new(:property => 'attr',
|
||||
:prop_key => 'vote_comment',
|
||||
:old_value => old_vote_comment,
|
||||
:value => vote_comment) if old_vote_comment != vote_comment
|
||||
journal.save
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.autoclose(project)
|
||||
return unless RedmineHelpdesk.autoclose_tickets_after > 0
|
||||
issues = Issue.includes(:helpdesk_ticket).where(:project_id => project.id).
|
||||
where(:status_id => RedmineHelpdesk.autoclose_from_status).
|
||||
where('created_on < ?', Time.now - RedmineHelpdesk.autoclose_time_interval)
|
||||
issues.find_each do |issue|
|
||||
issue.init_journal(User.anonymous)
|
||||
issue.current_journal.notes = I18n.t('label_helpdesk_autoclosed_ticket')
|
||||
issue.status_id = RedmineHelpdesk.autoclose_to_status
|
||||
issue.save
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_ticket_private
|
||||
return unless RedmineHelpdesk.settings["helpdesk_assign_contact_user"].to_i > 0
|
||||
issue.assign_attributes(:is_private => true) if RedmineHelpdesk.settings["helpdesk_create_private_tickets"].to_i > 0
|
||||
issue.save unless issue.new_record?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
class JournalMessage < ActiveRecord::Base
|
||||
unloadable
|
||||
belongs_to :contact
|
||||
belongs_to :journal
|
||||
has_one :message_file, :class_name => "Attachment", :as => :container, :dependent => :destroy
|
||||
|
||||
attr_accessible :source, :from_address, :to_address, :bcc_address, :cc_address, :message_id, :is_incoming, :message_date, :contact, :journal
|
||||
|
||||
acts_as_attachable :view_permission => :view_issues,
|
||||
:delete_permission => :edit_issues
|
||||
|
||||
if ActiveRecord::VERSION::MAJOR >= 4
|
||||
acts_as_activity_provider :type => 'helpdesk_tickets',
|
||||
:permission => :view_helpdesk_tickets,
|
||||
:timestamp => "#{table_name}.message_date",
|
||||
:author_key => "#{Journal.table_name}.user_id",
|
||||
:scope => eager_load({:journal => [{:issue => [:project, :tracker]}, :details, :user]}, :contact)
|
||||
else
|
||||
acts_as_activity_provider :type => 'helpdesk_tickets',
|
||||
:permission => :view_helpdesk_tickets,
|
||||
:timestamp => "#{table_name}.message_date",
|
||||
:author_key => "#{Journal.table_name}.user_id",
|
||||
:find_options => {:include => [{:journal => [{:issue => [:project, :tracker]}, :details, :user]}, :contact]}
|
||||
end
|
||||
|
||||
acts_as_event :title => Proc.new {|o| "#{o.journal.issue.tracker} ##{o.journal.issue.id}: #{o.journal.issue.subject}" if o.journal && o.journal.issue},
|
||||
:datetime => :message_date,
|
||||
:group => :helpdesk_ticket,
|
||||
:project_key => "#{Project.table_name}.id",
|
||||
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.journal.issue.id, :anchor => "change-#{o.id}"} if o.journal},
|
||||
:type => Proc.new {|o| ('icon' + (o.is_incoming? ? " icon-email" : " icon-email-to")) },
|
||||
:author => Proc.new {|o| o.is_incoming? ? o.contact : o.journal.user },
|
||||
:description => Proc.new{|o| o.journal.notes if o.journal}
|
||||
|
||||
validates_presence_of :contact, :journal, :message_date
|
||||
|
||||
def project
|
||||
journal.project
|
||||
end
|
||||
|
||||
def contact_name
|
||||
contact.name
|
||||
end
|
||||
|
||||
def contact_email
|
||||
contact.emails.first
|
||||
end
|
||||
|
||||
def helpdesk_ticket
|
||||
journal.issue.helpdesk_ticket
|
||||
end
|
||||
|
||||
def content
|
||||
journal.notes
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
<%= back_url_hidden_field_tag %>
|
||||
<%= error_messages_for 'canned_response' %>
|
||||
|
||||
<div class="box tabular">
|
||||
<p><%= f.text_field :name, :size => 80, :required => true %></p>
|
||||
<% if User.current.admin? || User.current.allowed_to?(:manage_public_canned_responses, @project) %>
|
||||
<% if @canned_response.user %>
|
||||
<p>
|
||||
<label><%= l(:field_author) %></label>
|
||||
<%= @canned_response.user.name %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<p>
|
||||
<%= f.check_box :is_public,
|
||||
:label => l(:field_is_public),
|
||||
:onchange => (User.current.admin? ? nil : 'if (this.checked) {$("#canned_response_is_for_all").removeAttr("checked"); $("#canned_response_is_for_all").attr("disabled", true);} else {$("#canned_response_is_for_all").removeAttr("disabled");}') %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<p><label for="canned_response_is_for_all"><%=l(:field_is_for_all)%></label>
|
||||
<%= check_box_tag 'canned_response_is_for_all', 1, @canned_response.project.nil?,
|
||||
:disabled => (!@canned_response.new_record? && (@canned_response.project.nil? || (@canned_response.is_public? && !User.current.admin?))) %></p>
|
||||
|
||||
<p><%= f.text_area :content, :required => true, :class => 'wiki-edit', :rows => 5 %>
|
||||
<em class="info"><%= l(:text_helpdesk_answer_macros, :macro => HelpdeskSettings::MACRO_LIST.map{|m| link_to m, "#", :class => "mail-macro"}.join(', ')).html_safe %></em>
|
||||
<%= wikitoolbar_for 'canned_response_content' %>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
$(".info a.mail-macro").bind("click", function() {
|
||||
$('#canned_response_content').insertAtCaret($(this).html());
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<% content_for :header_tags do %>
|
||||
<%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,37 @@
|
||||
<div class="contextual">
|
||||
<%= link_to l(:label_helpdesk_new_canned_response), {:controller => "canned_responses", :action => 'new'}, :class => 'icon icon-add' %>
|
||||
</div>
|
||||
<h3><%= l(:label_helpdesk_canned_response_plural) %></h3>
|
||||
|
||||
<% if @canned_responses.any? %>
|
||||
<table class="list">
|
||||
<thead><tr>
|
||||
<th><%= l(:field_name) %></th>
|
||||
<th><%= l(:field_content) %></th>
|
||||
<th><%= l(:field_is_public) %></th>
|
||||
<th><%= l(:field_author) %></th>
|
||||
<th><%= l(:field_project) %></th>
|
||||
<th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<% @canned_responses.each do |canned_response| %>
|
||||
<tr class="<%= cycle 'odd', 'even' %>">
|
||||
<td class="name"><%= canned_response.name %></td>
|
||||
<td class="name"><em class="info"><%= canned_response.content.gsub(/$/, ' ').truncate(250) %></em></td>
|
||||
<td class="tick"><%= checked_image canned_response.is_public? %></td>
|
||||
<td class="project"><%= canned_response.user.try(:name) %></td>
|
||||
<td class="project"><%= canned_response.project ? canned_response.project.name : l(:field_is_for_all) %></td>
|
||||
<td class="buttons">
|
||||
<%= link_to l(:button_edit), edit_canned_response_path(canned_response), :class => 'icon icon-edit' %>
|
||||
<%= delete_link canned_response_path(canned_response, :project_id => canned_response.project) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% if @canned_response_pages %>
|
||||
<p class="pagination"><%= pagination_links_full @canned_response_pages %></p>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% end %>
|
||||
@@ -0,0 +1,43 @@
|
||||
(function($) {
|
||||
|
||||
$.fn.insertAtCaret = function (myValue) {
|
||||
|
||||
return this.each(function() {
|
||||
|
||||
//IE support
|
||||
if (document.selection) {
|
||||
|
||||
this.focus();
|
||||
sel = document.selection.createRange();
|
||||
sel.text = myValue;
|
||||
this.focus();
|
||||
|
||||
} else if (this.selectionStart || this.selectionStart == '0') {
|
||||
|
||||
//MOZILLA / NETSCAPE support
|
||||
var startPos = this.selectionStart;
|
||||
var endPos = this.selectionEnd;
|
||||
var scrollTop = this.scrollTop;
|
||||
this.value = this.value.substring(0, startPos)+ myValue+ this.value.substring(endPos,this.value.length);
|
||||
this.focus();
|
||||
this.selectionStart = startPos + myValue.length;
|
||||
this.selectionEnd = startPos + myValue.length;
|
||||
this.scrollTop = scrollTop;
|
||||
|
||||
} else {
|
||||
|
||||
this.value += myValue;
|
||||
this.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
|
||||
$('#issue_notes').insertAtCaret("<%= raw escape_javascript(@content) %>")
|
||||
|
||||
$('#helpdesk_canned_response').val("");
|
||||
|
||||
if ($('#cke_issue_notes').length > 0) {
|
||||
CKEDITOR.instances['issue_notes'].insertHtml("<%= raw escape_javascript(@content) %>");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<h2><%=l(:label_helpdesk_canned_response)%></h2>
|
||||
|
||||
<%= labelled_form_for :canned_response, @canned_response, :url => { :action => 'update', :project_id => @project } do |f| %>
|
||||
<%= render :partial => 'canned_responses/form', :locals => { :f => f } %>
|
||||
<%= submit_tag l(:button_save) %>
|
||||
<% end %>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render :partial => 'index' %>
|
||||
@@ -0,0 +1,6 @@
|
||||
<h2><%=l(:label_helpdesk_new_canned_response)%></h2>
|
||||
|
||||
<%= labelled_form_for :canned_response, @canned_response, :url => { :action => 'create', :project_id => @project } do |f| %>
|
||||
<%= render :partial => 'canned_responses/form', :locals => { :f => f } %>
|
||||
<%= submit_tag l(:button_create) %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,54 @@
|
||||
<% tickets_scope = @contact.all_tickets.visible.order_by_status %>
|
||||
|
||||
<% tickets = tickets_scope %>
|
||||
<div id="helpdesk_tickets" class="contact-issues">
|
||||
<div class="contextual">
|
||||
<%= link_to l(:label_helpdesk_ticket_new), {:controller => 'issues',
|
||||
:action => 'new',
|
||||
:customer_id => @contact,
|
||||
:tracker_id => HelpdeskSettings["helpdesk_tracker", @project.id],
|
||||
:project_id => @project} if User.current.allowed_to?(:add_issues, @project) && User.current.allowed_to?(:send_response, @project) && HelpdeskSettings["helpdesk_tracker", @project.id] %>
|
||||
</div>
|
||||
|
||||
<h3><%= link_to(l(:label_helpdesk_ticket_plural), {:controller => 'issues',
|
||||
:action => 'index',
|
||||
:set_filter => 1,
|
||||
:customer => [@contact.id],
|
||||
:status_id => "*",
|
||||
:c => ["project", "tracker", "status", "subject", "customer", "customer_company", "last_message"],
|
||||
:sort => 'priority:desc,updated_on:desc'}) %> </h3>
|
||||
|
||||
<% if tickets && tickets.any? %>
|
||||
<%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do %>
|
||||
<table class="list tickets">
|
||||
<tbody>
|
||||
<% for ticket in tickets %>
|
||||
<tr id="ticket-<%= h(ticket.id) %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= ticket.css_classes %>">
|
||||
<td>
|
||||
<%= check_box_tag("ids[]", ticket.id, false, :style => 'display:none;', :id => nil) %>
|
||||
<span class="icon <%= ticket.helpdesk_ticket.ticket_source_icon %>"></span>
|
||||
</td>
|
||||
<td class="subject">
|
||||
<%= link_to "##{ticket.id} - #{truncate(ticket.subject, :length => 60)} (#{ticket.status})", issue_path(ticket), :class => ticket.css_classes %>
|
||||
</td>
|
||||
<% if @contact.is_company %>
|
||||
<td class="customer"><%= contact_tag(ticket.customer, :type => 'plain') %></td>
|
||||
<% end %>
|
||||
<td class="last_message"><small>
|
||||
<%= ticket.description.truncate(250) %>
|
||||
</small></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
<% if Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch? %>
|
||||
<%= context_menu %>
|
||||
<% else %>
|
||||
<%= context_menu issues_context_menu_path %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
<% if @contact && User.current.allowed_to?(:view_helpdesk_tickets, @project) && User.current.allowed_to?(:add_issues, @project) && HelpdeskSettings["helpdesk_tracker", @project.id] %>
|
||||
<li><%= context_menu_link l(:label_helpdesk_ticket_new), {:controller => 'issues',
|
||||
:action => 'new',
|
||||
:customer_id => @contact,
|
||||
:tracker_id => HelpdeskSettings["helpdesk_tracker", @project.id],
|
||||
:project_id => @project,
|
||||
:back_url => @back},
|
||||
:method => :get,
|
||||
:class => 'icon-support' %>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<div class="contextual">
|
||||
<% if !@query.new_record? && @query.editable_by?(User.current) %>
|
||||
<%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %>
|
||||
<%= delete_link query_path(@query) %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<h2><%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %></h2>
|
||||
<% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %>
|
||||
|
||||
<%= form_tag({ :controller => 'issues', :action => 'index', :project_id => @project },
|
||||
:method => :get, :id => 'query_form') do %>
|
||||
<%= hidden_field_tag 'set_filter', '1' %>
|
||||
<div id="query_form_content" class="hide-when-print">
|
||||
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
|
||||
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
|
||||
<div style="<%= @query.new_record? ? "" : "display: none;" %>">
|
||||
<%= render :partial => 'queries/filters', :locals => {:query => @query} %>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="collapsible collapsed">
|
||||
<legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
|
||||
<div style="display: none;">
|
||||
<table>
|
||||
<tr style="display: none;">
|
||||
<td><%= l(:field_column_names) %></td>
|
||||
<td><%= render :partial => 'queries/columns', :locals => {:query => @query} %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for='group_by'><%= l(:field_group_by) %></label></td>
|
||||
<td><%= select_tag('group_by',
|
||||
options_for_select(
|
||||
[[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]},
|
||||
@query.group_by)
|
||||
) %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for='sort'><%= l(:label_sort) %></label></td>
|
||||
<td><%= select_tag('sort',
|
||||
options_for_select(
|
||||
[[]] + @query.available_columns.select(&:sortable?).collect {|c| [c.caption, "#{c.name.to_s}:desc,id:desc"]},
|
||||
params[:sort])
|
||||
) %></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<p class="buttons hide-when-print">
|
||||
|
||||
<%= link_to_function l(:button_apply), 'submit_query_form("query_form")', :class => 'icon icon-checked' %>
|
||||
<%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %>
|
||||
<% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
|
||||
<%= link_to_function l(:button_save),
|
||||
"$('#query_form').attr('action', '#{ @project ? new_project_query_path(@project) : new_query_path }'); submit_query_form('query_form')",
|
||||
:class => 'icon icon-save' %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<%= error_messages_for 'query' %>
|
||||
<% if @query.valid? %>
|
||||
<% if @issues.empty? %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% else %>
|
||||
<%= render :partial => 'helpdesk/list', :locals => {:issues => @issues, :query => @query} %>
|
||||
<span class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></span>
|
||||
<% end %>
|
||||
|
||||
<% end %>
|
||||
|
||||
<% content_for :sidebar do %>
|
||||
<%= render :partial => 'issues/sidebar' %>
|
||||
<% end %>
|
||||
|
||||
<% content_for :header_tags do %>
|
||||
<%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %>
|
||||
<%= stylesheet_link_tag :helpdesk, :plugin => 'redmine_contacts_helpdesk' %>
|
||||
<% end %>
|
||||
|
||||
<%= context_menu issues_context_menu_path %>
|
||||
@@ -0,0 +1,66 @@
|
||||
<%= form_tag({}) do -%>
|
||||
<%= hidden_field_tag 'back_url', url_for(params) %>
|
||||
<%= hidden_field_tag 'project_id', @project.id if @project %>
|
||||
|
||||
<table class="contacts tickets index">
|
||||
<tbody>
|
||||
<% previous_group = false %>
|
||||
<% @issues.each do |issue| %>
|
||||
<% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
|
||||
<% reset_cycle %>
|
||||
<tr class="group open">
|
||||
<td colspan="<%= @query.columns.size + 2 %>">
|
||||
<span class="expander" onclick="toggleRowGroup(this);"> </span>
|
||||
<%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
|
||||
<%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %>
|
||||
</td>
|
||||
</tr>
|
||||
<% previous_group = group %>
|
||||
<% end %>
|
||||
<tr class="hascontextmenu">
|
||||
<td class="checkbox">
|
||||
<%= check_box_tag("ids[]", issue.id, false, :id => nil) %>
|
||||
</td>
|
||||
<% if Setting.gravatar_enabled? %>
|
||||
<td class="avatar">
|
||||
<% if issue.customer %>
|
||||
<%= link_to avatar_to(issue.customer, :size => "32"), {:controller => 'contacts', :action => 'show', :project_id => @project, :id => issue.customer.id}, :id => "avatar" %>
|
||||
<% else %>
|
||||
<%= avatar(issue.author, :size => "32x32", :height => 32, :width => 32) %>
|
||||
<% end %>
|
||||
</td>
|
||||
<% end %>
|
||||
<td class="name ticket-name">
|
||||
<h1 class="ticket_name"><%= link_to "#{issue.subject}", {:controller => :issues, :action => :show, :id => issue.id} %> <span id="ticket-id">#<%= issue.id %></span></h1>
|
||||
<p class="ticket-description" >
|
||||
<small><%= issue.description.gsub("(\n|\r)", "").strip.truncate(100) unless issue.description.blank? %></small>
|
||||
</p>
|
||||
<p class="contact-info">
|
||||
<%= issue.customer ? "#{content_tag('span', '', :class => "icon icon-email", :title => l(:label_note_type_email))} #{l(:label_helpdesk_from)}: #{link_to_source(issue.customer)}, ".html_safe : "#{l(:label_helpdesk_from)}: #{link_to_user issue.author}, ".html_safe %>
|
||||
<%= l(:label_updated_time, time_tag(issue.updated_on)).html_safe %>
|
||||
</p>
|
||||
</td>
|
||||
<td class="status">
|
||||
<%= content_tag(:span, issue.status.name, :class => "deal-status ticket-status tags status-#{issue.status.id}") %>
|
||||
</td>
|
||||
<td class="info ticket-info">
|
||||
<% if issue.assigned_to %>
|
||||
<div class="ticket-sum"><%= l(:field_assigned_to) %>: <strong><%= link_to_user issue.assigned_to %><%# "#{issue.currency} " if issue.currency %><%# issue_price(issue.amount) %></strong>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="ticket-priority"><%= l(:field_priority) %>: <strong><%= issue.priority.name %><%# "#{issue.currency} " if issue.currency %><%# issue_price(issue.amount) %></strong>
|
||||
</div>
|
||||
<% if issue.due_date %>
|
||||
<div class="ticket-due-date"><%= l(:field_due_date) %>: <strong><%= format_date issue.due_date %><%# "#{issue.currency} " if issue.currency %><%# issue_price(issue.amount) %></strong>
|
||||
</div>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
$('#test_connection_messages').html('<%= escape_javascript @message.html_safe %>')
|
||||
@@ -0,0 +1,7 @@
|
||||
api.message do
|
||||
api.journal_id @journal.id
|
||||
api.content @journal.notes
|
||||
api.to_address @journal_message.to_address
|
||||
api.message_date format_date(@journal_message.message_date)
|
||||
api.customer(:id => @issue.customer.id, :name => @issue.customer.name) unless @issue.customer.nil?
|
||||
end
|
||||
@@ -0,0 +1,332 @@
|
||||
<%= render :partial => 'issues/action_menu' %>
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
$(document).ready(function() {
|
||||
toggleSendMail($('#is_send_mail').get(0));
|
||||
});
|
||||
|
||||
function toggleSendMail(element) {
|
||||
if (element.checked) {
|
||||
$('#journal_contacts').show();
|
||||
$('#helpdesk_cc').show();
|
||||
<% unless HelpdeskSettings["helpdesk_emails_footer", @project].blank? %>
|
||||
$('#email_footer').insertAfter($('#is_send_mail').parents().eq(1).find('.jstEditor .wiki-edit'));
|
||||
$('#email_footer').show();
|
||||
<% end %>
|
||||
<% unless HelpdeskSettings["helpdesk_emails_header", @project].blank? %>
|
||||
$('#email_header').insertBefore($('#is_send_mail').parents().eq(1).find('.jstElements'));
|
||||
$('#email_header').show();
|
||||
<% end %>
|
||||
$('#issue_status_id').val("<%= HelpdeskSettings["helpdesk_answered_status", @project] %>");
|
||||
|
||||
} else {
|
||||
$('#journal_contacts').hide();
|
||||
$('#helpdesk_cc').hide();
|
||||
<% unless HelpdeskSettings["helpdesk_emails_footer", @project].blank? %>
|
||||
$('#email_footer').hide();;
|
||||
<% end %>
|
||||
<% unless HelpdeskSettings["helpdesk_emails_header", @project].blank? %>
|
||||
$('#email_header').hide();;
|
||||
<% end %>
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<% unless HelpdeskSettings["helpdesk_emails_header", @project].blank? %>
|
||||
<div id="email_header" style="display: none;" class="email-template">
|
||||
<%= textilizable(HelpdeskMailer.apply_macro(HelpdeskSettings["helpdesk_emails_header", @project], @issue.contact, @issue, User.current)).html_safe %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% unless HelpdeskSettings["helpdesk_emails_footer", @project].blank? %>
|
||||
<div id="email_footer" style="display: none;" class="email-template">
|
||||
<%= textilizable(HelpdeskMailer.apply_macro(HelpdeskSettings["helpdesk_emails_footer", @project], @issue.contact, @issue, User.current)).html_safe %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
<h2><%= issue_heading(@issue) %></h2>
|
||||
|
||||
<div class="<%= @issue.css_classes %> details">
|
||||
<% if @prev_issue_id || @next_issue_id %>
|
||||
<div class="next-prev-links contextual">
|
||||
<%= link_to_if @prev_issue_id,
|
||||
"\xc2\xab #{l(:label_previous)}",
|
||||
(@prev_issue_id ? issue_path(@prev_issue_id) : nil),
|
||||
:title => "##{@prev_issue_id}" %> |
|
||||
<% if @issue && @issue %>
|
||||
<span class="position"><%= l(:label_item_position, :position => @issue, :count => @issue) %></span> |
|
||||
<% end %>
|
||||
<%= link_to_if @next_issue_id,
|
||||
"#{l(:label_next)} \xc2\xbb",
|
||||
(@next_issue_id ? issue_path(@next_issue_id) : nil),
|
||||
:title => "##{@next_issue_id}" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="subject">
|
||||
<%= render_issue_subject_with_tree(@issue) %>
|
||||
</div>
|
||||
<p class="author icon icon-email">
|
||||
<%= l(:label_added_time_by, :author => @issue.author.instance_of?(AnonymousUser) ? link_to_source(@issue.contacts.first) : link_to_user(@issue.author), :age => time_tag(@issue.created_on)).html_safe %>
|
||||
<% if @issue.created_on != @issue.updated_on %>
|
||||
<%= l(:label_updated_time, time_tag(@issue.updated_on)).html_safe %>.
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<% if @issue.description? || @issue.attachments.any? -%>
|
||||
<hr />
|
||||
<% if @issue.description? %>
|
||||
<div class="contextual">
|
||||
<%= link_to l(:button_quote),
|
||||
{:controller => 'journals', :action => 'new', :id => @issue},
|
||||
:remote => true,
|
||||
:method => 'post',
|
||||
:class => 'icon icon-comment' if authorize_for('issues', 'edit') %>
|
||||
</div>
|
||||
|
||||
<div class="wiki">
|
||||
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= link_to_attachments @issue, :thumbnails => true %>
|
||||
<% end -%>
|
||||
|
||||
</div>
|
||||
|
||||
<% if @journals.present? %>
|
||||
<div id="ticket-history">
|
||||
<h3><%=l(:label_history)%></h3>
|
||||
<% reply_links = authorize_for('issues', 'edit') -%>
|
||||
<% for journal in @journals.select{|j| !j.notes.blank? } %>
|
||||
<div id="change-<%= journal.id %>" class="<%= journal.css_classes %> ticket-note">
|
||||
<% if journal.is_incoming? %>
|
||||
<%= link_to avatar_to(journal.contacts.first, :size => "32"), {:controller => 'contacts', :action => 'show', :project_id => @project, :id => journal.contacts.first.id}, :id => "avatar", :class => "ticket-avatar gravatar" unless journal.contacts.blank? %>
|
||||
<% else %>
|
||||
<%= avatar(journal.user, :size => "32x32", :height => 32, :width => 32, :class => "ticket-avatar gravatar") %>
|
||||
<% end %>
|
||||
|
||||
<div id="note-<%= journal.indice %>" class="ticket-note-content">
|
||||
<h4>
|
||||
<%= link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
|
||||
{ :controller => 'journals', :action => 'edit', :id => journal, :format => 'js' },
|
||||
:title => l(:button_edit),
|
||||
:class => "journal-link") if reply_links %>
|
||||
<%= link_to(image_tag('comment.png'),
|
||||
{:controller => 'journals', :action => 'new', :id => @issue, :journal_id => journal},
|
||||
:remote => true,
|
||||
:method => 'post',
|
||||
:title => l(:button_quote),
|
||||
:class => "journal-link") %>
|
||||
<% if journal.contacts && journal.contacts.any? && User.current.allowed_to?(:view_helpdesk_tickets, @project) %>
|
||||
|
||||
<span class="icon <%= journal.is_incoming? ? 'icon-email' : 'icon-email-to' %>">
|
||||
|
||||
<% if journal.is_incoming? %>
|
||||
<%= "#{link_to_source journal.contacts.first} (#{journal.journal_messages.first.email})".html_safe unless journal.contacts.blank? %>
|
||||
<% if journal.journal_messages.first.attachments.any? %>
|
||||
<% attachment = journal.journal_messages.first.attachments.first %>
|
||||
<span class="attachment" style="white-space: nowrap;display: inline-block;">
|
||||
<%= link_to_attachment attachment, :text => l(:label_helpdesk_original), :class => 'icon icon-attachment' -%>
|
||||
<%= h(" - #{attachment.description}") unless attachment.description.blank? %>
|
||||
<span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
|
||||
<%= link_to_if_authorized image_tag('magnifier.png', :plugin => "redmine_contacts_helpdesk"),
|
||||
:controller => 'helpdesk', :action => 'show_original',
|
||||
:id => attachment, :project_id => @project %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to_user journal.user %>
|
||||
<span class="sent-to">
|
||||
<%= l(:label_sent_to) %>
|
||||
<% journal.journal_messages.each do |journal_message| %>
|
||||
<span class="contact" style="white-space: nowrap;display: inline-block;">
|
||||
<%= link_to_source(journal_message.contact) %>
|
||||
(<%= journal_message.email %>)
|
||||
</span>
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
- <%= format_time(@issue.updated_on).html_safe %>.
|
||||
</span>
|
||||
<%# authoring journal.created_on, journal.user, :label => :label_updated_time_by %>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<% end %>
|
||||
</h4>
|
||||
|
||||
<div class="wiki editable" id="journal-<%= journal.id %>-notes">
|
||||
<%= textilizable(journal, :notes) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% heads_for_wiki_formatter if User.current.allowed_to?(:edit_issue_notes, @issue.project) || User.current.allowed_to?(:edit_own_issue_notes, @issue.project) %>
|
||||
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
<div style="clear: both;"></div>
|
||||
<%= render :partial => 'issues/action_menu' %>
|
||||
|
||||
<div style="clear: both;"></div>
|
||||
<% if authorize_for('issues', 'edit') %>
|
||||
<div id="update" style="display:none;">
|
||||
<h3><%= l(:button_update) %></h3>
|
||||
<%# labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
|
||||
<%= form_tag({:controller => "helpdesk_tickets", :action => "update"}, :id => 'issue-form', :multipart => true, :method => :put) do |f| %>
|
||||
<%= error_messages_for 'issue' %>
|
||||
<%= render :partial => 'issues/conflict' if @conflict %>
|
||||
<div class="box">
|
||||
|
||||
<p>
|
||||
<%= label_tag :is_send_mail, l(:label_is_send_mail), :class => "icon icon-email-to", :style => "" %>
|
||||
<%= check_box_tag 'is_send_mail', 1, HelpdeskSettings["send_note_by_default", @project], :onclick => "toggleSendMail(this);" %>
|
||||
|
||||
<span id="journal_contacts" style="display: none;">
|
||||
<% @issue.contacts.each do |contact| %>
|
||||
<%= contact_tag(contact) %>
|
||||
(<%= contact.emails.first %>)
|
||||
<% end %>
|
||||
<div id="helpdesk_cc" style="display: none;">
|
||||
<p>
|
||||
<%= label_tag :email_cc, l(:label_email_cc) %>
|
||||
<%= text_field_tag :email_cc, '', :size => "80%" %>
|
||||
</p>
|
||||
<p>
|
||||
<%= label_tag :email_bcc, l(:label_email_bcc) %>
|
||||
<%= text_field_tag :email_bcc, '', :size => "80%" %>
|
||||
</p>
|
||||
</div>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
|
||||
<%= wikitoolbar_for 'notes' %>
|
||||
|
||||
<p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form', :locals => {:container => @issue} %></p>
|
||||
</div>
|
||||
|
||||
<%# f.hidden_field :lock_version %>
|
||||
<%# hidden_field_tag 'last_journal_id', params[:last_journal_id] || @issue.last_journal_id %>
|
||||
<%= submit_tag l(:button_submit) %>
|
||||
<%= preview_link preview_edit_issue_path(:project_id => @project, :id => @issue), 'issue-form' %>
|
||||
<% end %>
|
||||
|
||||
<div id="preview" class="wiki"></div>
|
||||
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% other_formats_links do |f| %>
|
||||
<%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
|
||||
<%= f.link_to 'PDF' %>
|
||||
<% end %>
|
||||
|
||||
<% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
|
||||
|
||||
<% content_for :sidebar do %>
|
||||
<div id="ticket_attributes">
|
||||
<% if authorize_for('issues', 'edit') %>
|
||||
<div class="contextual">
|
||||
<%= link_to l(:button_update), :onclick => '#' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<h3><%= l(:label_helpdesk_ticket_attributes) %></h3>
|
||||
<table class="attributes">
|
||||
<%= issue_fields_rows do |rows|
|
||||
rows.left l(:field_status), h(@issue.status.name), :class => 'status'
|
||||
rows.left l(:field_priority), h(@issue.priority.name), :class => 'priority'
|
||||
|
||||
unless @issue.disabled_core_fields.include?('assigned_to_id')
|
||||
rows.left l(:field_assigned_to), avatar(@issue.assigned_to, :size => "14").to_s.html_safe + (@issue.assigned_to ? link_to_user(@issue.assigned_to) : "-"), :class => 'assigned-to'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('category_id') || @issue.category.blank?
|
||||
rows.left l(:field_category), h(@issue.category ? @issue.category.name : "-"), :class => 'category'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('fixed_version_id') || @issue.fixed_version.blank?
|
||||
rows.left l(:field_fixed_version), (@issue.fixed_version ? link_to_version(@issue.fixed_version) : "-"), :class => 'fixed-version'
|
||||
end
|
||||
|
||||
unless @issue.disabled_core_fields.include?('start_date') || @issue.start_date.blank?
|
||||
rows.left l(:field_start_date), format_date(@issue.start_date), :class => 'start-date'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('due_date') || @issue.due_date.blank?
|
||||
rows.left l(:field_due_date), format_date(@issue.due_date), :class => 'due-date'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('done_ratio')
|
||||
rows.left l(:field_done_ratio), progress_bar(@issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%"), :class => 'progress'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('estimated_hours')
|
||||
unless @issue.estimated_hours.nil?
|
||||
rows.left l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours'
|
||||
end
|
||||
end
|
||||
if User.current.allowed_to?(:view_time_entries, @project) && @issue.total_spent_hours > 0
|
||||
rows.left l(:label_spent_time), (@issue.total_spent_hours > 0 ? (link_to l_hours(@issue.total_spent_hours), {:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}) : "-"), :class => 'spent-time'
|
||||
end
|
||||
end %>
|
||||
<%= render_custom_fields_rows(@issue) %>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="contacts_previous_issues">
|
||||
<style type="text/css">
|
||||
#contacts_previous_issues ul {margin: 0; padding: 0;}
|
||||
#contacts_previous_issues li {list-style-type:none; margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
|
||||
</style>
|
||||
|
||||
|
||||
<% if RedmineHelpdesk.settings[:show_contact_card] %>
|
||||
<h3><%= l(:label_helpdesk_contact) %></h3>
|
||||
<% @issue.contacts.each do |contact| %>
|
||||
<span class="small-card">
|
||||
<%= render :partial => 'contacts/contact_card', :object => contact %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if (issues_count = Issue.count(:include => :contacts, :conditions => ["#{Contact.table_name}.id IN (#{@issue.contact_ids.join(', ')})"]) - 1) > 0 %>
|
||||
<h3><%= "#{l(:label_helpdesk_contact_activity)} (#{issues_count})" %> </h3>
|
||||
|
||||
<ul>
|
||||
<% (Issue.visible.find(:all, :include => :contacts, :conditions => ["#{Contact.table_name}.id IN (#{@issue.contact_ids.join(', ')})"], :order => "#{Issue.table_name}.status_id, #{Issue.table_name}.due_date DESC, #{Issue.table_name}.updated_on DESC", :limit => RedmineHelpdesk.settings[:last_message_count].to_i > 0 ? RedmineHelpdesk.settings[:last_message_count].to_i : 11) - [@issue]).each do |issue| %>
|
||||
<li>
|
||||
<%= link_to_issue(issue, :truncate => 60, :project => (@project != issue.project)) %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<div class="contextual">
|
||||
<%= link_to l(:label_issue_view_all), {:controller => 'issues',
|
||||
:action => 'index',
|
||||
:set_filter => 1,
|
||||
:f => [:contacts, :status_id],
|
||||
:v => {:contacts => @issue.contact_ids},
|
||||
:op => {:contacts => '=', :status_id => '*'}} %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
|
||||
<% if User.current.allowed_to?(:add_issue_watchers, @project) ||
|
||||
(@issue.watchers.present? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
|
||||
<div id="watchers">
|
||||
<%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% content_for :header_tags do %>
|
||||
<%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %>
|
||||
<%= stylesheet_link_tag :helpdesk, :plugin => 'redmine_contacts_helpdesk' %>
|
||||
<%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
|
||||
<% end %>
|
||||
|
||||
<%= context_menu issues_context_menu_path %>
|
||||
@@ -0,0 +1,9 @@
|
||||
$('#customer_to_email').html('<%= escape_javascript(render :partial => "issues/customer_to_email", :locals => {:contact => @contact, :contact_email => @email}) %>')
|
||||
|
||||
$("#helpdesk_to").val("<%= @email %>");
|
||||
$("#helpdesk_to").attr("value", "<%= @email %>")
|
||||
$("#helpdesk_to").trigger('change');
|
||||
$("#helpdesk_cc").val("<%= @cc_emails.join(',') %>")
|
||||
$("#helpdesk_cc").trigger('change');
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
$('#cusomer_profile_and_issues').html('<%= escape_javascript(render :partial => "issues/helpdesk_customer_profile") %>')
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
|
||||
"http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<style type="text/css" media="screen">
|
||||
<%= @email_stylesheet %>
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="email_header">
|
||||
<%= textile(@email_header.to_s).html_safe unless @email_header.blank? %>
|
||||
</div>
|
||||
<div class="wrapper" id="email_body">
|
||||
<%= textile(@email_body.to_s).html_safe %>
|
||||
</div>
|
||||
<div id="email_footer">
|
||||
<%= textile(@email_footer.to_s).html_safe unless @email_footer.blank? %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
<%= @email_header %>
|
||||
<%= @email_body %>
|
||||
<%= @email_footer %>
|
||||
@@ -0,0 +1,25 @@
|
||||
<tr class="metrics">
|
||||
<td colspan="<%= @collector.columns.count / 2 %>">
|
||||
<p><%= l(:label_helpdesk_busiest_time_of_day_new_tickets) %></p>
|
||||
<div class="num"><%= @collector.new_issues_count %></div>
|
||||
<div class="change" title="<%= process_deviation(@collector.previous_new_issues_count, @collector.new_issues_count, false) %>">
|
||||
<%= progress_in_percents(-@collector.new_issue_count_progress) %>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="<%= @collector.columns.count / 2 %>">
|
||||
<p><%= l(:label_helpdesk_busiest_time_of_day_new_contacts) %></p>
|
||||
<div class="num"><%= @collector.contacts_count %></div>
|
||||
<div class="change" title="<%= process_deviation(@collector.previous_contacts_count, @collector.contacts_count, false) %>">
|
||||
<%= progress_in_percents(-@collector.total_contacts_count_progress) %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="metrics">
|
||||
<td colspan="<%= @collector.columns.count / 2 %>">
|
||||
<p><%= l(:label_helpdesk_busiest_time_of_day_total_incoming) %></p>
|
||||
<div class="num"><%= @collector.issues_count %></div>
|
||||
<div class="change" title="<%= process_deviation(@collector.previous_issues_count, @collector.issues_count, false) %>">
|
||||
<%= progress_in_percents(-@collector.issue_count_progress) %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -0,0 +1,33 @@
|
||||
<% if @collector.issues_count.zero? %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% else %>
|
||||
<div class="helpdesk_chart">
|
||||
<table class="chart_table">
|
||||
<tr class="header">
|
||||
<% @collector.columns.each do |column| %>
|
||||
<td class="column_data">
|
||||
<p class="issues_count"><%= column[:issues_count] %></p>
|
||||
<p><%= [column[:issues_percent], '%'].join %></p>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<tr class="main_block">
|
||||
<% @collector.columns.each do |column| %>
|
||||
<td class="column_data">
|
||||
<% if column[:issues_count] > 0 %>
|
||||
<div class="percents" style='height: <%= (column[:issues_count] * @collector.issue_weight).ceil %>px'></div>
|
||||
<% end %>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<tr class="footer">
|
||||
<% @collector.columns.each do |column| %>
|
||||
<td class="column_data">
|
||||
<%= l("label_helpdesk_#{@report}_interval_#{column[:name]}") %>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<%= render :partial => "#{@report}_metrics" %>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,33 @@
|
||||
<tr class="metrics">
|
||||
<td colspan="<%= @collector.columns.count / 2 %>">
|
||||
<p><%= l(:label_helpdesk_average_first_response_time) %></p>
|
||||
<div class="num"><%= helpdesk_time_label(@collector.average_response_time) %></div>
|
||||
<div class="change" title="<%= process_deviation(@collector.previous_average_response_time, @collector.average_response_time) %>">
|
||||
<%= mirror_progress_in_percents(@collector.average_response_time_progress) %>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="<%= @collector.columns.count / 2 %>">
|
||||
<p><%= l(:label_helpdesk_average_time_to_close) %></p>
|
||||
<div class="num"><%= helpdesk_time_label(@collector.average_close_time) %></div>
|
||||
<div class="change" title="<%= process_deviation(@collector.previous_average_close_time, @collector.average_close_time) %>">
|
||||
<%= mirror_progress_in_percents(@collector.average_close_time_progress) %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="metrics">
|
||||
<td colspan="<%= @collector.columns.count / 2 %>">
|
||||
<p><%= l(:label_helpdesk_average_responses_count) %></p>
|
||||
<div class="num"><%= @collector.average_response_count %></div>
|
||||
<div class="change" title="<%= process_deviation(@collector.previous_average_response_count, @collector.average_response_count, false) %>">
|
||||
<%= progress_in_percents(@collector.average_response_count_progress) %>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="<%= @collector.columns.count / 2 %>">
|
||||
<p><%= l(:label_helpdesk_total_replies) %></p>
|
||||
<div class="num"><%= @collector.total_response_count %></div>
|
||||
<div class="change" title="<%= process_deviation(@collector.previous_total_response_count, @collector.total_response_count, false) %>">
|
||||
<%= progress_in_percents(-@collector.total_response_count_progress) %>
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -0,0 +1,29 @@
|
||||
<h2><%= l("label_helpdesk_report_names_#{@report}") %></h2>
|
||||
<% html_title(l("label_helpdesk_report_names_#{@report}")) %>
|
||||
|
||||
|
||||
<%= form_tag({ :controller => 'helpdesk_reports', :action => 'show', :project_id => @project },
|
||||
:method => :get, :id => 'query_form') do %>
|
||||
<div id="query_form_with_buttons" class="hide-when-print">
|
||||
<%= hidden_field_tag 'set_filter', '1' %>
|
||||
<div id="query_form_content">
|
||||
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
|
||||
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
|
||||
<div style="<%= @query.new_record? ? "" : "display: none;" %>">
|
||||
<%= render :partial => 'queries/filters', :locals => {:query => @query} %>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<p class="buttons">
|
||||
<%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %>
|
||||
<%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %>
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= error_messages_for 'query' %>
|
||||
<%= render :partial => 'chart' %>
|
||||
|
||||
<% content_for :sidebar do %>
|
||||
<%= render :partial => 'issues/helpdesk_reports' %>
|
||||
<% end %>
|
||||
@@ -0,0 +1 @@
|
||||
<h2>HelpdeskTicketsController#destroy</h2>
|
||||
@@ -0,0 +1,23 @@
|
||||
$('#cusomer_profile_and_issues').html('<%= escape_javascript(render :partial => "issues/helpdesk_customer_profile") %>')
|
||||
$('#helpdesk_ticket_cc_address').select2({
|
||||
ajax: {
|
||||
url: '<%= auto_complete_contacts_path(:project_id => (ContactsSetting.cross_project_contacts? ? nil : @project), :is_company => nil) %>',
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
data: function (params) {
|
||||
return { q: params.term };
|
||||
},
|
||||
processResults: function (data, params) {
|
||||
return { results: $.grep(data, function(elem){ elem.id = elem.email; return elem }) };
|
||||
},
|
||||
cache: true
|
||||
},
|
||||
tags: true,
|
||||
placeholder: ' ',
|
||||
minimumInputLength: 1,
|
||||
width: '100%',
|
||||
templateResult: ccEmailTagResult,
|
||||
templateSelection: ccEmailTagSelection,
|
||||
}).on('select2:open', function (e) {
|
||||
$('#helpdesk_ticket_cc_address').closest('p').find('.select2-search__field').val(' ').trigger($.Event('input', { which: 13 })).val('');
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<h2>HelpdeskTicketsController#update</h2>
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="vote_form">
|
||||
<h2><%= l(:label_helpdesk_mark) %></h2>
|
||||
<%= form_tag helpdesk_votes_vote_path(:id => @ticket.id, :hash => @ticket.token) do %>
|
||||
<p>
|
||||
<%= label_tag :vote_2, nil, :class => 'vote-value' do %>
|
||||
<%= radio_button_tag('vote', 2, @ticket.vote == 2 || @ticket.vote == nil ? true : false) %>
|
||||
<span class="icon icon-awesome"><%= t(:label_helpdesk_mark_awesome) %></span>
|
||||
<% end %>
|
||||
|
||||
<%= label_tag :vote_1, nil, :class => 'vote-value' do %>
|
||||
<%= radio_button_tag('vote', 1, @ticket.vote == 1 ? true : false) %>
|
||||
<span class="icon icon-justok"><%= t(:label_helpdesk_mark_justok) %></span>
|
||||
<% end %>
|
||||
|
||||
<%= label_tag :vote_0, nil, :class => 'vote-value' do %>
|
||||
<%= radio_button_tag('vote', 0, @ticket.vote == 0 ? true : false) %>
|
||||
<span class="icon icon-notgood"><%= t(:label_helpdesk_mark_notgood) %></span>
|
||||
<% end %>
|
||||
</p>
|
||||
<%- if RedmineHelpdesk.vote_comment_allow? %>
|
||||
<%= text_area_tag('vote_comment', nil, { :size => '60x12', :placeholder => t(:label_helpdesk_vote_comment_placeholder) }) %>
|
||||
<% end %>
|
||||
|
||||
<div class='submit'>
|
||||
<%= submit_tag(t(:label_helpdesk_submit)) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="vote_form">
|
||||
<h2><%= t(:label_helpdesk_vote_thank) %></h2>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<%= avatar(@user, :size => 54, :id => 'avatar') %>
|
||||
@@ -0,0 +1,226 @@
|
||||
function getXmlHttp(){
|
||||
var xmlhttp;
|
||||
try {
|
||||
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
|
||||
} catch (e) {
|
||||
try {
|
||||
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
|
||||
} catch (E) {
|
||||
xmlhttp = false;
|
||||
}
|
||||
}
|
||||
if (!xmlhttp && typeof XMLHttpRequest!='undefined') {
|
||||
xmlhttp = new XMLHttpRequest();
|
||||
}
|
||||
return xmlhttp;
|
||||
}
|
||||
|
||||
function serialize(form){
|
||||
var boundary = String(Math.random()).slice(2);
|
||||
var boundaryMiddle = '--' + boundary + '\r\n';
|
||||
var boundaryLast = '--' + boundary + '--\r\n'
|
||||
var cont_start = 'Content-Disposition: form-data; name="';
|
||||
var cont_middle = '"\r\n\r\n';
|
||||
var cont_end = '\r\n';
|
||||
var field = '';
|
||||
var body = ['\r\n'];
|
||||
if (typeof form == 'object' && form.nodeName == "FORM") {
|
||||
for (index = form.elements.length - 1; index >= 0; index--) {
|
||||
field = form.elements[index];
|
||||
if (field.type == 'select-multiple') {
|
||||
for (option = form.elements[index].options.length - 1; option >= 0; option--) {
|
||||
if (field.options[option].selected) { body.push(cont_start + field.name + cont_middle + field.options[option].value + cont_end); }
|
||||
}
|
||||
} else {
|
||||
if (field.type != 'submit' && field.type != 'file' && field.type != 'button') {
|
||||
if ((field.type != 'checkbox' && field.type != 'radio') || field.checked) {
|
||||
body.push(cont_start + field.name + cont_middle + field.value + cont_end);
|
||||
}
|
||||
} else {
|
||||
if (field.type == 'file'){
|
||||
if (field.files.length > 0) {
|
||||
body.push(cont_start + field.name + cont_middle + field.attributes['data-value'] + cont_end);
|
||||
body.push(cont_start + field.name + '_name' + cont_middle + field.files[0].name + cont_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [boundary, body.join(boundaryMiddle) + boundaryLast];
|
||||
}
|
||||
|
||||
function translation(field){
|
||||
return RedmineHelpdeskIframe.configuration['translation'] ? RedmineHelpdeskIframe.configuration['translation'][field] : null;
|
||||
}
|
||||
|
||||
function ticketCreated(){
|
||||
success_div = document.createElement('div');
|
||||
success_div.id = 'submit_button';
|
||||
success_div.className = 'success-message';
|
||||
success_div.style.textAlign = 'center';
|
||||
success_div.style.margin = '15%';
|
||||
success_div.style.font = '20px Arial';
|
||||
success_div.innerHTML = translation('createSuccessLabel') || '<%= t(:label_helpdesk_widget_ticket_created) %>';
|
||||
|
||||
success_desc_div = document.createElement('div');
|
||||
success_desc_div.style.textAlign = 'center';
|
||||
success_desc_div.style.margin = '5%';
|
||||
success_desc_div.style.font = '14px Arial';
|
||||
success_desc_div.innerHTML = translation('createSuccessDescription');
|
||||
|
||||
document.getElementById('widget_form').innerHTML = '';
|
||||
document.getElementById('widget_form').appendChild(success_div);
|
||||
document.getElementById('widget_form').appendChild(success_desc_div);
|
||||
}
|
||||
|
||||
function ticketErrors(errors){
|
||||
errors_div = document.createElement('div');
|
||||
errors_div.id = 'ticket-error-details';
|
||||
errors_div.className = 'ticket-error-details';
|
||||
|
||||
error_p = document.createElement('div');
|
||||
error_p.innerHTML = translation('createErrorLabel') || '<%= t(:label_helpdesk_widget_ticket_errors) %>';
|
||||
errors_div.appendChild(error_p);
|
||||
|
||||
errors_link = document.createElement('a');
|
||||
errors_link.id = 'ticket-errors-link';
|
||||
errors_link.href = 'javascript:void(0)';
|
||||
errors_link.style.paddingLeft = '10px';
|
||||
errors_link.addEventListener('click', function(){ toggleErrorsList() });
|
||||
errors_link.innerHTML = '<%= t(:label_helpdesk_widget_ticket_error_details) %>';
|
||||
error_p.appendChild(errors_link);
|
||||
|
||||
ul = document.createElement('ul');
|
||||
ul.id = 'ticket-errors';
|
||||
ul.className = 'ticket-errors';
|
||||
ul.style.display = 'none';
|
||||
errors_div.appendChild(ul);
|
||||
|
||||
for (var key in errors) {
|
||||
if (key != 'base') {
|
||||
processErrorForField(ul, key, errors[key])
|
||||
} else {
|
||||
errors[key].forEach(function(error_text) {
|
||||
processErrorForCustomField(ul, key, error_text);
|
||||
});
|
||||
}
|
||||
}
|
||||
document.getElementById('flash').appendChild(errors_div);
|
||||
}
|
||||
|
||||
function createErrorLi(target, text){
|
||||
li = document.createElement('li');
|
||||
li.id = 'ticket-error';
|
||||
li.className = 'ticket-error';
|
||||
li.innerHTML = text;
|
||||
target.appendChild(li);
|
||||
}
|
||||
|
||||
function markFieldAsError(element){
|
||||
element.style.border = '';
|
||||
element.classList.add('error_field');
|
||||
element.addEventListener('keyup', checkFieldContent);
|
||||
}
|
||||
|
||||
function markRequireFieldsAsError(){
|
||||
fields = document.querySelectorAll("[data-require='true'] > input, [data-require='true'] > select, [data-require='true'] > textarea, .required-field");
|
||||
required_fields = Array.from(fields);
|
||||
var respose = false;
|
||||
required_fields.forEach(function(field) {
|
||||
if (field.value.length == 0) {
|
||||
markFieldAsError(field);
|
||||
respose = true;
|
||||
}
|
||||
});
|
||||
return respose;
|
||||
}
|
||||
|
||||
function unmarkFieldsAsError(){
|
||||
error_fields = Array.from(document.getElementsByClassName('error_field'));
|
||||
error_fields.forEach(function(field) {
|
||||
field.classList.remove('error_field');
|
||||
field.removeEventListener('keyup', checkFieldContent);
|
||||
});
|
||||
}
|
||||
|
||||
function checkFieldContent(){
|
||||
if (this.value.length > 0) {
|
||||
this.style.border = '1px solid #d9d9d9';
|
||||
} else {
|
||||
this.style.border = '1px solid red';
|
||||
}
|
||||
}
|
||||
|
||||
function processErrorForField(ul, key, error_text) {
|
||||
createErrorLi(ul, error_text);
|
||||
field = document.getElementById(key);
|
||||
if (field != null) { markFieldAsError(field); }
|
||||
}
|
||||
|
||||
function processErrorForCustomField(ul, key, error_text) {
|
||||
checkCustomFieldsOnError(error_text);
|
||||
createErrorLi(ul, error_text);
|
||||
}
|
||||
|
||||
function checkCustomFieldsOnError(error_text){
|
||||
custom_fields = Array.from(document.getElementsByClassName('custom_field'));
|
||||
custom_fields.forEach(function(cfield) {
|
||||
cfield_regex = new RegExp(cfield.attributes['data-error-key'].value);
|
||||
if (cfield_regex.test(error_text)){
|
||||
markFieldAsError(cfield.getElementsByTagName('input')[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleErrorsList(){
|
||||
errors_list = document.getElementById('ticket-errors');
|
||||
if (errors_list == null) { return true }
|
||||
if (errors_list && errors_list.style.display == 'block') {
|
||||
errors_list.style.display = 'none';
|
||||
} else {
|
||||
errors_list.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function processResponse(response){
|
||||
if (response['result']) {
|
||||
ticketCreated();
|
||||
parent.postMessage(JSON.stringify({ reload: true }), "*");
|
||||
} else {
|
||||
ticketErrors(response['errors']);
|
||||
}
|
||||
var formSubmitBtn = document.getElementById('form-submit-btn');
|
||||
if (formSubmitBtn){
|
||||
formSubmitBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function needReloadProjectData(){
|
||||
parent.postMessage(JSON.stringify({ project_reload: true }), "*");
|
||||
}
|
||||
|
||||
function submitTicketForm(){
|
||||
document.getElementById('flash').innerHTML = '';
|
||||
unmarkFieldsAsError();
|
||||
if (markRequireFieldsAsError()){ return false; }
|
||||
|
||||
var base_url = '<%= Setting.protocol %>://<%= Setting.host_name %>'
|
||||
var xmlhttp = getXmlHttp();
|
||||
var serialize_result = serialize(document.getElementById('widget_form'));
|
||||
var boundary = serialize_result[0];
|
||||
var form_params = serialize_result[1];
|
||||
document.getElementById('form-submit-btn').disabled = true;
|
||||
xmlhttp.open('POST', base_url + '/helpdesk_widget/create_ticket.js', true);
|
||||
xmlhttp.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (xmlhttp.readyState == 4) {
|
||||
if (xmlhttp.status == 200 || xmlhttp.status == 304) {
|
||||
processResponse(JSON.parse(xmlhttp.responseText));
|
||||
} else {
|
||||
processResponse({result: false, errors: []});
|
||||
}
|
||||
}
|
||||
};
|
||||
xmlhttp.send(form_params);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<% @issue.editable_custom_field_values.each do |value| %>
|
||||
<% if @enabled_cf && @enabled_cf.include?(value.custom_field_id.to_s) %>
|
||||
<% required = value.custom_field.is_required? %>
|
||||
<p class="custom_field" data-error-key="<%= value.custom_field.name %>" data-require="<%= required %>"><%= custom_field_tag_with_label :issue, value, :required => required %></p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,135 @@
|
||||
#widget_form {
|
||||
padding: 10px;
|
||||
font-family: helvetica, arial, sans-serif;
|
||||
}
|
||||
|
||||
#widget_form .form-control,
|
||||
#widget_form .custom_fields input,
|
||||
#widget_form .custom_fields select,
|
||||
#widget_form .custom_fields textarea {
|
||||
background: 0 0;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 0;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
color: #333;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
height: 36px;
|
||||
line-height: 16px;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 12px;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
#widget_form select.form-control,
|
||||
#widget_form .custom_fields select {
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAAAXNSR0IArs4c6QAAATNJREFUKBW1kc1KxDAUhW/CxM2M4EO4m7UwrzDDDBUXRRDxPbpw4cL3EHFhYcTSEh/BhevufAhhOptJabwnmhIjI+JPIDS5P1/OPSX64yW01o/GmNMkSZ5/wy6KYl8pdS0F0UQNBk8Mnv4UiF4wwJLW2qUQYo+srTiRMZTj316YMEMvGI4FgK6qjIS44KDk5N1qvT5L07T5Cpvn+Wh3OLziviMGddx3PpvPL3s1D2U5s1LeOLVE9caYw22+wq8dpe75wTHDXkTXnUwXCw0B0qtAwLTtAd9r3mN4gkd83n8RQw41vGv0eBhqeiAuULRqmgnGdp5IWQa+Or94itJNAWu4Np6iHxnAYH3y1eUivzhmg563kjgQ3iNfWfhHv8Jaf96m0Ofp/QfcgrZp2+N4xL7wvw6vkGme5fEw/bwAAAAASUVORK5CYII=) 97% 14px no-repeat;
|
||||
background-size: 12px 9px;
|
||||
}
|
||||
|
||||
#widget_form .custom_fields textarea,
|
||||
#widget_form textarea.form-control {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
|
||||
#widget_form .custom_fields select[multiple] {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
#widget_form .title {
|
||||
color: #333;
|
||||
padding-bottom: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#widget_form .custom_fields label > span {
|
||||
color: #333;
|
||||
padding-bottom: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#widget_form #flash {
|
||||
color: red;
|
||||
padding-bottom: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#widget_form input.error_field,
|
||||
#widget_form select.error_field,
|
||||
#widget_form textarea.error_field,
|
||||
#widget_form .custom_fields input.error_field,
|
||||
#widget_form .custom_fields select.error_field,
|
||||
#widget_form .custom_fields textarea.error_field {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
#widget_form .submit_button{
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
#widget_form .attach_div{
|
||||
position: relative;
|
||||
float: right;
|
||||
margin: 10px;
|
||||
min-width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#widget_form .attach_link{
|
||||
position: relative;
|
||||
font: 14px Arial;
|
||||
color: #3699ca;
|
||||
}
|
||||
|
||||
#widget_form .attach_field{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -80px;
|
||||
width: 160px;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#widget_form .btn {
|
||||
float: right;
|
||||
background: #3699ca;
|
||||
color: #fff;
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
line-height: 30px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border: 0px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
-ms-appearance: none;
|
||||
-o-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
#widget_from .attach {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
margin: 10px 16px 0 0;
|
||||
max-width: 210px;
|
||||
text-decoration: underline;
|
||||
color: #2996cc;
|
||||
}
|
||||
|
||||
#helpdesk_widget.open {
|
||||
border-radius: 0 100% 100% !important;
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
-o-transform: rotate(45deg);
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
function getXmlHttp(){
|
||||
var xmlhttp;
|
||||
try {
|
||||
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
|
||||
} catch (e) {
|
||||
try {
|
||||
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
|
||||
} catch (E) {
|
||||
xmlhttp = false;
|
||||
}
|
||||
}
|
||||
if (!xmlhttp && typeof XMLHttpRequest!='undefined') {
|
||||
xmlhttp = new XMLHttpRequest();
|
||||
}
|
||||
return xmlhttp;
|
||||
}
|
||||
|
||||
var RedmineHelpdeskWidget = {
|
||||
widget: document.getElementById('helpdesk_widget'),
|
||||
widget_button: null,
|
||||
width: 400,
|
||||
height: 500,
|
||||
margin: 20,
|
||||
iframe: null,
|
||||
form: null,
|
||||
schema: null,
|
||||
reload: false,
|
||||
configuration: {},
|
||||
attachment: null,
|
||||
base_url: '<%= Setting.protocol %>://<%= Setting.host_name %>',
|
||||
config: function(configuration){
|
||||
this.configuration = configuration;
|
||||
this.apply_config();
|
||||
},
|
||||
apply_config: function(){
|
||||
if (this.configuration['color']) {
|
||||
this.widget_button.style.backgroundColor = this.configuration['color'];
|
||||
}
|
||||
switch (this.configuration['position']) {
|
||||
case 'topLeft':
|
||||
this.widget.style.top = '20px';
|
||||
this.widget.style.left = '20px';
|
||||
break;
|
||||
case 'topRight':
|
||||
this.widget.style.top = '20px';
|
||||
this.widget.style.right = '20px';
|
||||
break;
|
||||
case 'bottomLeft':
|
||||
this.widget.style.bottom = '20px';
|
||||
this.widget.style.left = '20px';
|
||||
break;
|
||||
case 'bottomRight':
|
||||
this.widget.style.bottom = '20px';
|
||||
this.widget.style.right = '20px';
|
||||
break;
|
||||
default:
|
||||
widget.style.bottom = '20px';
|
||||
widget.style.right = '20px';
|
||||
}
|
||||
},
|
||||
translation: function(field){
|
||||
return this.configuration['translation'] && this.configuration['translation'][field] ? this.configuration['translation'][field] : null;
|
||||
},
|
||||
identify: function(field){
|
||||
return this.configuration['identify'] && this.configuration['identify'][field] ? this.configuration['identify'][field] : null
|
||||
},
|
||||
load: function() {
|
||||
this.widget.addEventListener('click', function(){ RedmineHelpdeskWidget.toggle() });
|
||||
this.create_widget_button();
|
||||
this.decorate_widget_button();
|
||||
this.create_iframe();
|
||||
this.decorate_iframe();
|
||||
this.load_schema();
|
||||
this.created = true;
|
||||
},
|
||||
load_schema: function() {
|
||||
var xmlhttp = getXmlHttp();
|
||||
xmlhttp.open('GET', this.base_url + '/helpdesk_widget/load_form.json', true);
|
||||
xmlhttp.responseType = 'json';
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (xmlhttp.readyState == 4) {
|
||||
if (xmlhttp.status == 200 || xmlhttp.status == 304) {
|
||||
RedmineHelpdeskWidget.schema = xmlhttp.response;
|
||||
RedmineHelpdeskWidget.fill_form();
|
||||
} else {
|
||||
RedmineHelpdeskWidget.schema = {};
|
||||
}
|
||||
}
|
||||
};
|
||||
xmlhttp.send(null);
|
||||
},
|
||||
create_widget_button: function(){
|
||||
button = document.createElement('div');
|
||||
button.id = 'widget_button';
|
||||
button.className = 'widget_button';
|
||||
button.innerHTML = this.configuration['icon'] || '?';
|
||||
button.setAttribute('name', 'helpdesk_widget_button');
|
||||
button.style.backgroundColor = '#7E8387';
|
||||
button.style.backgroundSize = '15px 15px';
|
||||
button.style.cursor = 'pointer';
|
||||
button.style.color = 'white';
|
||||
button.style.textAlign = 'center';
|
||||
button.style.fontSize = '32px';
|
||||
button.style.verticalAlign = 'middle';
|
||||
button.style.lineHeight = '54px';
|
||||
button.style.borderRadius = '30px';
|
||||
button.style.boxShadow = 'rgba(0, 0, 0, 0.258824) 0px 2px 5px 0px';
|
||||
button.style.display = 'none';
|
||||
button.style.webkitTransition = "transform 0.2s ease";
|
||||
this.widget_button = button;
|
||||
this.widget.appendChild(button);
|
||||
},
|
||||
decorate_widget_button: function(){
|
||||
widget = this.widget;
|
||||
widget.style.position = 'fixed';
|
||||
widget.style.bottom = '20px';
|
||||
widget.style.right = '20px';
|
||||
widget.style.width = '54px';
|
||||
widget.style.height = '54px';
|
||||
widget.style.zIndex = 9999;
|
||||
},
|
||||
create_iframe: function(){
|
||||
this.iframe = document.createElement('iframe');
|
||||
this.widget.appendChild(this.iframe);
|
||||
},
|
||||
decorate_iframe: function(){
|
||||
iframe = this.iframe;
|
||||
iframe.setAttribute('id', 'helpdesk_ticket_container');
|
||||
iframe.setAttribute('width', this.width);
|
||||
iframe.setAttribute('height', 0);
|
||||
iframe.setAttribute('frameborder', 0);
|
||||
iframe.style.visibility = 'hidden';
|
||||
iframe.style.position = 'absolute';
|
||||
iframe.style.opacity = '0';
|
||||
iframe.style.width = this.width;
|
||||
iframe.style.backgroundColor = 'white';
|
||||
iframe.style.webkitTransition = "opacity 0.2s ease";
|
||||
iframe.style.boxShadow = 'rgba(0, 0, 0, 0.258824) 0px 1px 4px 0px';
|
||||
iframe.setAttribute('name', 'helpdesk_widget_iframe');
|
||||
},
|
||||
fill_form: function(){
|
||||
if (Object.keys(this.schema.projects).length > 0) {
|
||||
this.apply_avatar();
|
||||
this.create_form();
|
||||
this.create_form_title();
|
||||
this.create_error_flash();
|
||||
|
||||
if (this.identify('redmineUserID')) {
|
||||
this.create_form_hidden(this.form, 'redmine_user', 'redmine_user', 'form-control', this.identify('redmineUserID'));
|
||||
}
|
||||
if (this.identify('nameValue')) {
|
||||
this.create_form_hidden(this.form, 'username', 'username', 'form-control', this.identify('nameValue'));
|
||||
} else {
|
||||
this.create_form_text(this.form, 'username', 'username', this.translation('nameLabel') || '<%= t(:label_helpdesk_widget_name) %>', 'form-control', this.identify('nameValue'), true);
|
||||
}
|
||||
if (this.identify('emailValue')) {
|
||||
this.create_form_hidden(this.form, 'email', 'email', 'form-control', this.identify('emailValue'));
|
||||
} else {
|
||||
this.create_form_text(this.form, 'email', 'email' , this.translation('emailLabel') || '<%= t(:label_helpdesk_widget_email) %>', 'form-control', this.identify('emailValue'), true);
|
||||
}
|
||||
if (this.identify('subjectValue')) {
|
||||
this.create_form_hidden(this.form, 'subject', 'issue[subject]', 'form-control', this.identify('subjectValue'));
|
||||
} else {
|
||||
this.create_form_text(this.form, 'subject', 'issue[subject]' , this.translation('subjectLabel') || '<%= t(:label_helpdesk_widget_subject) %>', 'form-control', this.identify('subjectValue'), true);
|
||||
}
|
||||
this.create_projects_selector();
|
||||
this.create_form_area(this.form, 'description', 'issue[description]' , this.translation('descriptionLabel') || '<%= t(:label_helpdesk_widget_description) %>', 'form-control', true);
|
||||
|
||||
var project_id = null;
|
||||
var tracker_id = null;
|
||||
if (RedmineHelpdeskWidget.configuration['identify']){
|
||||
project_id = RedmineHelpdeskWidget.schema.projects[RedmineHelpdeskWidget.configuration['identify']['projectValue']];
|
||||
if (project_id) {
|
||||
tracker_id = RedmineHelpdeskWidget.schema.projects_data[project_id].trackers[RedmineHelpdeskWidget.configuration['identify']['trackerValue']];
|
||||
}
|
||||
}
|
||||
|
||||
this.load_project_data(project_id || this.schema.projects[Object.keys(this.schema.projects)[0]], tracker_id);
|
||||
this.iframe.contentWindow.document.body.appendChild(this.form);
|
||||
this.append_stylesheets();
|
||||
this.append_scripts();
|
||||
this.create_message_listener();
|
||||
} else {
|
||||
this.widget.style.display = 'none';
|
||||
}
|
||||
},
|
||||
apply_avatar: function(){
|
||||
button = document.getElementById('widget_button');
|
||||
avatar = RedmineHelpdeskWidget.configuration['user_avatar'];
|
||||
if (avatar && avatar.length > 0) {
|
||||
var xmlhttp = getXmlHttp();
|
||||
xmlhttp.open('GET', RedmineHelpdeskWidget.base_url + '/helpdesk_widget/avatar/' + avatar, true);
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (xmlhttp.readyState == 4) {
|
||||
if (xmlhttp.status == 200 || xmlhttp.status == 304) {
|
||||
button.style.backgroundSize = 'cover';
|
||||
button.style.backgroundImage = 'url(' + RedmineHelpdeskWidget.base_url + '/helpdesk_widget/avatar/' + avatar + ')' ;
|
||||
button.style.border = '2px solid';
|
||||
button.innerHTML = ' ';
|
||||
} else {
|
||||
button.style.backgroundSize = '15px 15px';
|
||||
button.innerHTML = '?';
|
||||
}
|
||||
button.style.display = 'block';
|
||||
button.style.lineHeight = '50px';
|
||||
}
|
||||
};
|
||||
xmlhttp.send(null);
|
||||
} else {
|
||||
button.style.lineHeight = '54px';
|
||||
button.style.backgroundSize = '15px 15px';
|
||||
button.innerHTML = '?';
|
||||
button.style.display = 'block';
|
||||
}
|
||||
},
|
||||
append_stylesheets: function(){
|
||||
|
||||
if (this.configuration['styles']) {
|
||||
styles_css = document.createElement('style');
|
||||
styles_css.innerHTML = this.configuration['styles'];
|
||||
styles_css.type = "text/css";
|
||||
}
|
||||
widget_css = document.createElement('link');
|
||||
widget_css.href = this.base_url + '/helpdesk_widget/widget.css';
|
||||
widget_css.rel = "stylesheet";
|
||||
widget_css.type = "text/css";
|
||||
this.iframe.contentWindow.document.head.appendChild(widget_css);
|
||||
if (this.configuration['styles']) {
|
||||
this.iframe.contentWindow.document.head.appendChild(styles_css);
|
||||
}
|
||||
},
|
||||
append_scripts: function(){
|
||||
script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = this.base_url + '/helpdesk_widget/iframe.js';
|
||||
this.iframe.contentWindow.document.head.appendChild(script);
|
||||
|
||||
config_script = document.createElement('script');
|
||||
config_script.innerHTML = "var RedmineHelpdeskIframe = {configuration: "+ JSON.stringify(this.configuration) +"}";
|
||||
this.iframe.contentWindow.document.head.appendChild(config_script);
|
||||
},
|
||||
create_form: function(){
|
||||
this.form = document.createElement('form');
|
||||
this.form.action = this.base_url + '/helpdesk_widget/create_ticket';
|
||||
this.form.acceptCharset = 'UTF-8';
|
||||
this.form.method = 'post';
|
||||
this.form.id = 'widget_form';
|
||||
this.form.setAttribute('onSubmit', 'submitTicketForm(); return false;');
|
||||
this.form.style.marginBottom = 0;
|
||||
},
|
||||
create_form_title: function(){
|
||||
if (this.configuration['title']) {
|
||||
title_div = document.createElement('div');
|
||||
title_div.id = 'title';
|
||||
title_div.className = 'title';
|
||||
title_div.innerHTML = this.configuration['title'];
|
||||
this.form.appendChild(title_div);
|
||||
}
|
||||
},
|
||||
create_error_flash: function(){
|
||||
flash_div = document.createElement('div');
|
||||
flash_div.id = 'flash';
|
||||
flash_div.className = 'flash';
|
||||
this.form.appendChild(flash_div);
|
||||
},
|
||||
create_projects_selector: function(){
|
||||
var project_id = null;
|
||||
if (RedmineHelpdeskWidget.configuration['identify']){
|
||||
project_id = RedmineHelpdeskWidget.schema.projects[RedmineHelpdeskWidget.configuration['identify']['projectValue']];
|
||||
}
|
||||
if (project_id) {
|
||||
this.create_form_hidden(this.form, 'project_id', 'project_id', 'form-control projects', project_id);
|
||||
} else {
|
||||
this.create_form_select(this.form, 'project_id', 'project_id', RedmineHelpdeskWidget.schema.projects, project_id, 'form-control projects');
|
||||
}
|
||||
},
|
||||
load_project_data: function(project_id, tracker_id){
|
||||
container_div = this.form.getElementsByClassName('container')[0]
|
||||
if (container_div) { container_div.remove() };
|
||||
|
||||
container_div = document.createElement('div');
|
||||
container_div.id = 'container';
|
||||
container_div.className = 'container';
|
||||
|
||||
custom_div = document.createElement('div');
|
||||
custom_div.id = 'custom_fields';
|
||||
custom_div.className = 'custom_fields';
|
||||
|
||||
submit_div = document.createElement('div');
|
||||
submit_div.id = 'submit_button';
|
||||
submit_div.className = 'submit_button';
|
||||
|
||||
container_div.appendChild(custom_div);
|
||||
container_div.appendChild(submit_div);
|
||||
|
||||
if (RedmineHelpdeskWidget.configuration['identify'] && RedmineHelpdeskWidget.schema.projects_data[project_id].trackers[RedmineHelpdeskWidget.configuration['identify']['trackerValue']]){
|
||||
tracker_id = RedmineHelpdeskWidget.schema.projects_data[project_id].trackers[RedmineHelpdeskWidget.configuration['identify']['trackerValue']]
|
||||
this.create_form_hidden(custom_div, 'tracker_id', 'tracker_id', 'form-control trackers', tracker_id);
|
||||
} else {
|
||||
this.create_form_select(custom_div, 'tracker_id', 'tracker_id', this.schema.projects_data[project_id].trackers, tracker_id, 'form-control trackers');
|
||||
tracker_id = custom_div.getElementsByClassName('trackers')[0].value;
|
||||
}
|
||||
this.load_custom_fields(custom_div, project_id, tracker_id);
|
||||
|
||||
this.create_form_submit(submit_div, this.translation('createButtonLabel') || '<%= l(:label_helpdesk_widget_create_ticket) %>');
|
||||
this.create_attch_link(submit_div);
|
||||
|
||||
this.form.appendChild(container_div);
|
||||
},
|
||||
reload_project_data: function(){
|
||||
project_id = this.form.getElementsByClassName('projects')[0].value;
|
||||
tracker_id = container_div.getElementsByClassName('trackers')[0].value;
|
||||
|
||||
this.load_project_data(project_id, tracker_id);
|
||||
this.positionate_iframe();
|
||||
},
|
||||
create_form_select: function(target, field_id, field_name, values, selected, field_class){
|
||||
|
||||
if (Object.keys(values).length == 1) {
|
||||
field = document.createElement('input');
|
||||
field.type = 'hidden';
|
||||
field.id = field_id;
|
||||
field.name = field_name;
|
||||
field.className = field_class;
|
||||
field.value = values[Object.keys(values)[0]];
|
||||
} else {
|
||||
field = document.createElement('select');
|
||||
field.id = field_id;
|
||||
field.name = field_name;
|
||||
field.className = field_class;
|
||||
for (var project in values) {
|
||||
option = document.createElement('option');
|
||||
option.value = values[project]
|
||||
if(values[project] == selected) { option.selected = 'selected'; }
|
||||
option.innerHTML = project;
|
||||
field.appendChild(option);
|
||||
}
|
||||
}
|
||||
field.setAttribute('onChange', 'needReloadProjectData();');
|
||||
target.appendChild(field);
|
||||
},
|
||||
create_form_hidden: function(target, field_id, field_name, field_class, value){
|
||||
field = document.createElement('input');
|
||||
field.type = 'hidden';
|
||||
field.id = field_id;
|
||||
field.name = field_name;
|
||||
field.value = value;
|
||||
field.className = field_class;
|
||||
target.appendChild(field);
|
||||
},
|
||||
create_form_text: function(target, field_id, field_name, field_placeholder, field_class, value, required){
|
||||
field = document.createElement('input');
|
||||
field.type = 'text';
|
||||
field.id = field_id;
|
||||
field.name = field_name;
|
||||
field.value = value;
|
||||
field.placeholder = field_placeholder;
|
||||
field.className = required ? field_class + ' required-field' : field_class;
|
||||
target.appendChild(field);
|
||||
},
|
||||
create_form_area: function(target, field_id, field_name, field_placeholder, field_class, required){
|
||||
field = document.createElement('textarea');
|
||||
field.cols = 55;
|
||||
field.rows = 10;
|
||||
field.id = field_id;
|
||||
field.name = field_name;
|
||||
field.placeholder = field_placeholder;
|
||||
field.className = required ? field_class + ' required-field' : field_class;
|
||||
target.appendChild(field);
|
||||
},
|
||||
create_form_submit: function(target, label){
|
||||
field = document.createElement('input');
|
||||
field.id = 'form-submit-btn';
|
||||
field.type = 'submit';
|
||||
field.name = 'submit';
|
||||
field.className = 'btn';
|
||||
field.value = label;
|
||||
field.title = this.translation('buttomLabel') || '';
|
||||
if (RedmineHelpdeskWidget.configuration['color']) {
|
||||
field.style.background = RedmineHelpdeskWidget.configuration['color'];
|
||||
}
|
||||
target.appendChild(field);
|
||||
},
|
||||
create_attch_link: function(target){
|
||||
if (this.configuration['attachment'] != false ) {
|
||||
attach_div = document.createElement('div');
|
||||
attach_div.className = 'attach_div';
|
||||
|
||||
attach_link = document.createElement('a');
|
||||
attach_link.className = 'attach_link';
|
||||
attach_link.href = 'javascript:void(0)';
|
||||
attach_link.innerHTML = this.translation('attachmentLinkLabel') || 'Attach a file';
|
||||
attach_div.appendChild(attach_link);
|
||||
|
||||
attach_field = document.createElement('input');
|
||||
attach_field.type = 'file';
|
||||
attach_field.id = 'attachment';
|
||||
attach_field.className = 'attach_field';
|
||||
attach_field.name = 'attachment';
|
||||
attach_field.attributes['data-max-size'] = <%= Setting[:attachment_max_size].to_i * 1024 %>;
|
||||
attach_field.addEventListener('change', function(){ RedmineHelpdeskWidget.upload_file() });
|
||||
attach_div.appendChild(attach_field);
|
||||
this.attachment = attach_field;
|
||||
|
||||
target.appendChild(attach_div);
|
||||
}
|
||||
},
|
||||
upload_file: function(){
|
||||
if (this.attachment.attributes['data-max-size'] > this.attachment.files[0].size) {
|
||||
this.read_file(this.attachment.files[0], function(e){
|
||||
attach_field = RedmineHelpdeskWidget.form.getElementsByClassName('attach_field')[0]
|
||||
attach_field.attributes['data-value'] = e.target.result;
|
||||
displayed_name = (attach_field.files[0].name.length <= 20) ? attach_field.files[0].name : attach_field.files[0].name.substring(0, 20) + '...';
|
||||
RedmineHelpdeskWidget.form.getElementsByClassName('attach_link')[0].innerHTML = displayed_name;
|
||||
});
|
||||
} else {
|
||||
this.attachment.attributes['data-value'] = '';
|
||||
RedmineHelpdeskWidget.form.getElementsByClassName('attach_link')[0].innerHTML = '<%= t(:label_helpdesk_widget_file_large) %>';
|
||||
}
|
||||
},
|
||||
read_file: function(file, callback){
|
||||
var reader = new FileReader();
|
||||
reader.onload = callback
|
||||
reader.readAsDataURL(file);
|
||||
},
|
||||
load_custom_fields: function(target, project_id, tracker_id){
|
||||
var xmlhttp = getXmlHttp();
|
||||
var params = 'project_id=' + encodeURIComponent(project_id) + '&tracker_id=' + encodeURIComponent(tracker_id);
|
||||
custom_div = document.createElement('div');
|
||||
xmlhttp.open('GET', this.base_url + '/helpdesk_widget/load_custom_fields?' + params, true);
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (xmlhttp.readyState == 4) {
|
||||
if (xmlhttp.status == 200 || xmlhttp.status == 304) {
|
||||
custom_div.innerHTML = xmlhttp.responseText;
|
||||
target.appendChild(custom_div);
|
||||
RedmineHelpdeskWidget.set_custom_values();
|
||||
RedmineHelpdeskWidget.positionate_iframe();
|
||||
}
|
||||
}
|
||||
};
|
||||
xmlhttp.send(null);
|
||||
},
|
||||
set_custom_values: function(){
|
||||
if (this.configuration['identify'] && this.configuration['identify']['customFieldValues']){
|
||||
for(var cf in this.configuration['identify']['customFieldValues']) {
|
||||
custom_field = this.form.querySelector('#issue_custom_field_values_' + this.schema.custom_fields[cf])
|
||||
if (custom_field){
|
||||
switch (custom_field.tagName){
|
||||
case 'INPUT':
|
||||
custom_field.type = 'hidden';
|
||||
custom_field.value = this.configuration['identify']['customFieldValues'][cf];
|
||||
this.form.querySelector("[data-error-key='" + cf + "']").style.display = 'none';
|
||||
break;
|
||||
case 'SELECT':
|
||||
options = custom_field.options;
|
||||
for(var option, index = 0; option = options[index]; index++) {
|
||||
if(option.value == this.configuration['identify']['customFieldValues'][cf]) {
|
||||
this.create_form_hidden(custom_field.parentElement, custom_field.id, custom_field.name, custom_field.classList.toString(), this.configuration['identify']['customFieldValues'][cf]);
|
||||
custom_field.remove();
|
||||
this.form.querySelector("[data-error-key='" + cf + "']").style.display = 'none';
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
positionate_iframe: function(){
|
||||
widget_height = this.form.offsetHeight > this.height ? this.height : this.form.offsetHeight;
|
||||
this.iframe.setAttribute('height', this.margin + widget_height);
|
||||
switch (this.configuration['position']) {
|
||||
case 'topLeft':
|
||||
this.iframe.style.top = (this.margin + this.widget_button.offsetWidth) + 'px';
|
||||
iframe.style.left = (this.margin - this.widget_button.offsetWidth / 2) + 'px';
|
||||
break;
|
||||
case 'topRight':
|
||||
this.iframe.style.top = (this.margin + this.widget_button.offsetWidth) + 'px';
|
||||
iframe.style.left = (this.margin + this.widget_button.offsetWidth - this.width - 20) + 'px';
|
||||
break;
|
||||
case 'bottomLeft':
|
||||
this.iframe.style.top = (- this.margin * 2 - widget_height) + 'px';
|
||||
iframe.style.left = (this.margin - this.widget_button.offsetWidth / 2) + 'px';
|
||||
break;
|
||||
case 'bottomRight':
|
||||
this.iframe.style.top = (- this.margin * 2 - widget_height) + 'px';
|
||||
iframe.style.left = (this.margin + this.widget_button.offsetWidth - this.width - 20) + 'px';
|
||||
break;
|
||||
default:
|
||||
this.iframe.style.top = (- this.margin * 2 - widget_height) + 'px';
|
||||
iframe.style.left = (this.margin + this.widget_button.offsetWidth - this.width - 20) + 'px';
|
||||
}
|
||||
},
|
||||
create_message_listener: function(){
|
||||
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
|
||||
var eventer = window[eventMethod];
|
||||
var messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message";
|
||||
|
||||
eventer(messageEvent,function(e) {
|
||||
data = JSON.parse(e.data);
|
||||
if (data['reload'] == true) {
|
||||
RedmineHelpdeskWidget.reload = true;
|
||||
}
|
||||
if (data['project_reload'] == true) {
|
||||
RedmineHelpdeskWidget.reload_project_data();
|
||||
}
|
||||
},false);
|
||||
},
|
||||
reload_form: function(){
|
||||
this.iframe.remove();
|
||||
this.create_iframe();
|
||||
this.fill_form();
|
||||
this.decorate_iframe();
|
||||
this.reload = false;
|
||||
},
|
||||
show: function() {
|
||||
this.iframe.style.visibility = 'visible';
|
||||
this.iframe.style.opacity = '1';
|
||||
this.positionate_iframe();
|
||||
switch (this.configuration['position']) {
|
||||
case 'topLeft':
|
||||
case 'topRight':
|
||||
this.widget_button.style.borderRadius = '50% 50% 0%';
|
||||
break;
|
||||
case 'bottomLeft':
|
||||
case 'bottomRight':
|
||||
this.widget_button.style.borderRadius = '0 100% 100%';
|
||||
break;
|
||||
default:
|
||||
this.widget_button.style.borderRadius = '0 100% 100%';
|
||||
}
|
||||
this.widget_button.style.webkitTransform = 'rotate(45deg)';
|
||||
this.widget_button.style.mozTransform = 'rotate(45deg)';
|
||||
this.widget_button.style.msTransform = 'rotate(45deg)';
|
||||
this.widget_button.style.oTransform = 'rotate(45deg)';
|
||||
},
|
||||
hide: function() {
|
||||
if (this.reload == true) {
|
||||
this.reload_form();
|
||||
}
|
||||
body = this.iframe.contentWindow.document.body;
|
||||
this.iframe.style.visibility = 'hidden';
|
||||
this.iframe.style.opacity = '0';
|
||||
this.widget_button.style.borderRadius = '30px';
|
||||
this.widget_button.style.webkitTransform = '';
|
||||
this.widget_button.style.mozTransform = '';
|
||||
this.widget_button.style.msTransform = '';
|
||||
this.widget_button.style.oTransform = '';
|
||||
},
|
||||
toggle: function() {
|
||||
(this.iframe.style.visibility == 'visible') ? this.hide() : this.show();
|
||||
}
|
||||
}
|
||||
|
||||
RedmineHelpdeskWidget.load();
|
||||
@@ -0,0 +1,2 @@
|
||||
<%= contact_tag(contact, :id => "customer_send_tag") %>
|
||||
(<%= contact_email %>)
|
||||
@@ -0,0 +1,87 @@
|
||||
<% if !@issue.blank? && User.current.allowed_to?(:view_helpdesk_tickets, @project) %>
|
||||
<span id="cusomer_profile_and_issues">
|
||||
<div id="customer_profile">
|
||||
<div class="contextual">
|
||||
<%= link_to l(:button_update),
|
||||
{:controller => 'helpdesk_tickets',
|
||||
:action => 'edit',
|
||||
:issue_id => @issue},
|
||||
:remote => true if User.current.allowed_to?(:edit_helpdesk_tickets, @project) %>
|
||||
|
||||
</div>
|
||||
<h3><%= l(:label_helpdesk_contact) %></h3>
|
||||
<% unless !(@show_form == "true") %>
|
||||
<%= form_for @helpdesk_ticket, :url => {:controller => 'helpdesk_tickets',
|
||||
:action => 'update',
|
||||
:issue_id => @issue},
|
||||
:html => {:id => 'ticket_data_form',
|
||||
:method => :put} do |f| %>
|
||||
|
||||
<% unless @helpdesk_ticket.new_record? %>
|
||||
<div class="contextual">
|
||||
<%= link_to image_tag('link_break.png'),
|
||||
{:controller => 'helpdesk_tickets', :action => 'destroy', :id => @helpdesk_ticket},
|
||||
:method => :delete,
|
||||
:data => {:confirm => l(:text_are_you_sure)},
|
||||
:title => l(:label_relation_delete) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<p class="contact_auto_complete"><%= label_tag :helpdesk_ticket_contact_id, l(:label_helpdesk_contact)%><br>
|
||||
<%= select_contact_tag('helpdesk_ticket[contact_id]', @helpdesk_ticket.customer, :is_select => Contact.visible.by_project(ContactsSetting.cross_project_contacts? ? nil : @project).count < 50, :include_blank => false, :add_contact => true, :display_field => @helpdesk_ticket.customer.blank?) %>
|
||||
</p>
|
||||
|
||||
<p><%= label_tag :helpdesk_ticket_source, l(:label_helpdesk_ticket_source)%><br>
|
||||
<%= f.select :source, helpdesk_tickets_source_for_select %></p>
|
||||
|
||||
<p><%= f.text_field :ticket_date, :size => 12, :required => true, :value => @helpdesk_ticket.ticket_date.to_date, :label => l(:label_helpdesk_ticket_date) %> <%= f.text_field :ticket_time, :value => @helpdesk_ticket.ticket_date.to_s(:time), :size => 5 %><%= calendar_for('helpdesk_ticket_ticket_date') %> </p>
|
||||
|
||||
<p>
|
||||
<%= label_tag :helpdesk_ticket_cc_address, l(:label_helpdesk_cc_address) %><br>
|
||||
<% @cc_address = @helpdesk_ticket.cc_address.try(:split, ',') || [] %>
|
||||
<%= f.select :cc_address, options_for_select(@cc_address.map { |email|[email, email] }, @cc_address), {}, {:multiple => true } %>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
<%= submit_tag l(:button_update) %>
|
||||
<%= link_to l(:button_cancel), {}, :onclick => "$('#ticket_data_form').hide(); return false;" %>
|
||||
|
||||
<% end %>
|
||||
|
||||
<% end %>
|
||||
<span class="small-card">
|
||||
<%= render :partial => 'contacts/contact_card', :object => @issue.customer if @issue.customer %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<% if @issue.customer && (customer_issues = @issue.customer.all_tickets.preload(:status, :tracker, :helpdesk_ticket).visible.order_by_status.to_a).count - 1 > 0 %>
|
||||
<div id="customer_previous_issues">
|
||||
<div class="contextual">
|
||||
<%= link_to l(:label_helpdesk_all) + " (#{customer_issues.count})", {:controller => 'issues',
|
||||
:action => 'index',
|
||||
:set_filter => 1,
|
||||
:f => [:customer, :status_id],
|
||||
:v => {:customer => [@issue.customer.id]},
|
||||
:op => {:customer => '=', :status_id => '*'}} %>
|
||||
</div>
|
||||
|
||||
<h3><%= l(:label_helpdesk_contact_activity) %> </h3>
|
||||
|
||||
<ul>
|
||||
<% (customer_issues.first(5)).each do |issue| %>
|
||||
<li title="<%= "#{issue.tracker} (#{issue.status})" if issue.tracker && issue.status %>" >
|
||||
<span class="icon <%= helpdesk_ticket_source_icon(issue.helpdesk_ticket) %>"></span>
|
||||
<span class="ticket-title <%= 'selected' if @issue == issue %>">
|
||||
<%= link_to_issue(issue, :truncate => 60, :project => (@project != issue.project), :tracker => false) %>
|
||||
</span>
|
||||
<span class="ticket-meta">
|
||||
<%= format_time(issue.created_on) %>
|
||||
<%= "- #{issue.assigned_to.name}" if issue.assigned_to %>
|
||||
</span>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
</span>
|
||||
<% end %>
|
||||
@@ -0,0 +1,6 @@
|
||||
<% if @project && @project.module_enabled?(:contacts_helpdesk)%>
|
||||
<h3><%= l(:label_helpdesk_reports) %></h3>
|
||||
<%= link_to l(:label_helpdesk_first_response_time), project_helpdesk_reports_path(@project, :report => 'first_response_time') %>
|
||||
<br>
|
||||
<%= link_to l(:label_helpdesk_busiest_time_of_day), project_helpdesk_reports_path(@project, :report => 'busiest_time_of_day') %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,154 @@
|
||||
<% if authorize_for(:issues, :send_helpdesk_response) && @issue.customer && @issue.customer.primary_email %>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
function emailTagResult (opt) {
|
||||
if (opt.name){
|
||||
var formated_tag = $('<span>' + opt.avatar + ' ' + opt.text + '</span>');;
|
||||
} else {
|
||||
var formated_tag = opt.text;
|
||||
}
|
||||
return formated_tag
|
||||
};
|
||||
function emailTagSelection (opt) {
|
||||
if (opt.name){
|
||||
var formated_tag = opt.name + ' <' + opt.email + '>';
|
||||
} else {
|
||||
var formated_tag = opt.text;
|
||||
}
|
||||
return formated_tag
|
||||
};
|
||||
$(document).ready(function() {
|
||||
toggleSendMail($('#helpdesk_is_send_mail').get(0));
|
||||
var select2_fields = ['#helpdesk_to', '#helpdesk_cc', '#helpdesk_bcc'];
|
||||
$.each(select2_fields, function(index, field){
|
||||
$(field).select2({
|
||||
ajax: {
|
||||
url: '<%= auto_complete_contacts_path(:project_id => (ContactsSetting.cross_project_contacts? ? nil : @project), :is_company => nil) %>',
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
data: function (params) {
|
||||
return { q: params.term };
|
||||
},
|
||||
processResults: function (data, params) {
|
||||
return { results: $.grep(data, function(elem){ elem.id = elem.email; return elem }) };
|
||||
},
|
||||
cache: true
|
||||
},
|
||||
tags: true,
|
||||
placeholder: ' ',
|
||||
minimumInputLength: 1,
|
||||
width: '60%',
|
||||
templateResult: emailTagResult,
|
||||
templateSelection: emailTagSelection,
|
||||
}).on('select2:open', function (e) {
|
||||
$(field).closest('.cc-list-edit').find('.select2-search__field').val(' ').trigger($.Event('input', { which: 13 })).val('');
|
||||
});
|
||||
});
|
||||
|
||||
$('#email_footer').insertAfter($('#helpdesk_is_send_mail').parents().eq(1).find('.jstEditor'));
|
||||
$('#email_header').insertBefore($('#helpdesk_is_send_mail').parents().eq(1).find('>legend'));
|
||||
$('#email_canned_responses').insertBefore($('#helpdesk_is_send_mail').parents().eq(1).find('>legend'));
|
||||
});
|
||||
|
||||
var issue_status = $('#issue_status_id').val();
|
||||
|
||||
function toggleSendMail(element) {
|
||||
if (element.checked) {
|
||||
$('.email-template').show();
|
||||
<% unless HelpdeskSettings["helpdesk_answered_status", @project].blank? %>
|
||||
issue_status = $('#issue_status_id').val();
|
||||
$('#issue_status_id').val("<%= HelpdeskSettings["helpdesk_answered_status", @project] %>");
|
||||
<% end %>
|
||||
<% if @issue.assigned_to.blank? %>
|
||||
$('#issue_assigned_to_id').val("<%= User.current.id %>");
|
||||
<% end %>
|
||||
|
||||
} else {
|
||||
$('.email-template').hide();
|
||||
$('#cc_list_edit').hide();
|
||||
$('#helpdesk_is_cc').val("");
|
||||
<% unless HelpdeskSettings["helpdesk_answered_status", @project].blank? %>
|
||||
$('#issue_status_id').val(issue_status);
|
||||
<% end %>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function updateCannedResposeFrom(url, value) {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'post',
|
||||
data: {id: value}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<% canned_responses = CannedResponse.visible.in_project_or_public(@project) %>
|
||||
<% if canned_responses.any? %>
|
||||
<span id="email_canned_responses" style="display: none; float:right;" class="email-template">
|
||||
<%= select_tag 'helpdesk[canned_response]', options_for_select([[ "--- #{l(:label_helpdesk_canned_response_plural)} ---", '' ]] + canned_responses.order("#{CannedResponse.table_name}.name").map{|cr| [cr.name, cr.id]}), :onchange => "updateCannedResposeFrom('#{escape_javascript add_canned_responses_path(:project_id => @project, :issue_id => @issue, :format => 'js')}', $(this).val())" %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<% unless HelpdeskSettings["helpdesk_emails_header", @project].blank? %>
|
||||
<div id="email_header" style="display: none;" class="email-template">
|
||||
<%= textilizable(HelpdeskMailer.apply_macro(HelpdeskSettings["helpdesk_emails_header", @project], @issue.customer, @issue, User.current)).html_safe %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% unless HelpdeskSettings["helpdesk_emails_footer", @project].blank? %>
|
||||
<div id="email_footer" style="display: none;" class="email-template">
|
||||
<%= textilizable(HelpdeskMailer.apply_macro(HelpdeskSettings["helpdesk_emails_footer", @project], @issue.customer, @issue, User.current)).html_safe %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<p id="helpdesk_send_response">
|
||||
<%= check_box_tag 'helpdesk[is_send_mail]', 1, HelpdeskSettings["send_note_by_default", @project], :onclick => "toggleSendMail(this);" %>
|
||||
<%= label_tag :helpdesk_is_send_mail, l(:label_is_send_mail), :class => "icon icon-email-to", :style => "" %>
|
||||
|
||||
<span id="journal_contacts" style="display: none;" class="email-template">
|
||||
<span id="customer_to_email" class="email-template">
|
||||
<%= render :partial => "issues/customer_to_email", :locals => {:contact => Contact.where(:email => @issue.helpdesk_ticket.default_to_address).first || @issue.customer, :contact_email => @issue.helpdesk_ticket.default_to_address } %>
|
||||
</span>
|
||||
<a href="#" class="inline-edit email-template" onclick="$('#customer_to_email').hide(); $(this).hide(); $('#cc_list_edit').show(); return false;"><img alt="Edit" src="/images/edit.png" style="vertical-align:middle;" ></a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div id="cc_list_edit" style="display:none;">
|
||||
<% contact_emails = ((@issue.helpdesk_ticket.cc_address.to_s.split(',') + @issue.helpdesk_ticket.response_addresses).uniq.map { |email| [email, email] } + (@issue.contacts + [@issue.customer]).uniq.map{ |contact| [contact.email_name, contact.primary_email] }).uniq(&:last) %>
|
||||
<%= hidden_field_tag 'helpdesk[is_cc]' %>
|
||||
<p class="cc-list-edit">
|
||||
<span class="is-cc">
|
||||
<%= label_tag l(:label_helpdesk_to) %>
|
||||
</span>
|
||||
<%= select_tag 'journal_message[to_address]', options_for_select(contact_emails, @issue.helpdesk_ticket.default_to_address), :id => "helpdesk_to", :multiple => true %>
|
||||
</p>
|
||||
<p class="cc-list-edit">
|
||||
<span class="is-cc">
|
||||
<%= label_tag l(:label_helpdesk_cc) %>
|
||||
</span>
|
||||
<%= select_tag 'journal_message[cc_address]', options_for_select(contact_emails, @issue.customer.primary_email != @issue.helpdesk_ticket.default_to_address ? @issue.customer.primary_email : @issue.helpdesk_ticket.cc_addresses), :id => "helpdesk_cc", :multiple => true %>
|
||||
</p>
|
||||
<div style="clear: both;"></div>
|
||||
<p class="cc-list-edit">
|
||||
<span class="is-cc">
|
||||
<%= label_tag l(:label_helpdesk_bcc) %>
|
||||
</span>
|
||||
<%= select_tag 'journal_message[bcc_address]', nil, :id => "helpdesk_bcc", :multiple => true %>
|
||||
</p>
|
||||
<div style="clear: both;"></div>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
|
||||
<% content_for :header_tags do %>
|
||||
<%= javascript_include_tag :redmine_helpdesk, :plugin => 'redmine_contacts_helpdesk' %>
|
||||
<%= stylesheet_link_tag :helpdesk, :plugin => 'redmine_contacts_helpdesk' %>
|
||||
<% end %>
|
||||
|
||||
<% if authorize_for(:issues, :send_helpdesk_response) && @issue.customer && @issue.customer.primary_email %>
|
||||
<%= javascript_tag do %>
|
||||
$('#content .contextual:first a:first').before('<%= helpdesk_reply_link %>')
|
||||
$('#content .contextual:last a:first').before('<%= helpdesk_reply_link %>')
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,56 @@
|
||||
<% if User.current.allowed_to?(:view_helpdesk_tickets, @project) && @issue.is_ticket? %>
|
||||
<div id="ticket_data">
|
||||
<div class="contextual">
|
||||
<%= link_to l(:label_helpdesk_public_link), public_ticket_path(@issue.helpdesk_ticket, :hash => @issue.helpdesk_ticket.token), :class => "icon icon-public-link" if RedmineHelpdesk.public_tickets? && !@issue.is_private %>
|
||||
<%# link_to l(:label_helpdesk_spam),
|
||||
{:controller => 'helpdesk',
|
||||
:action => 'delete_spam',
|
||||
:project_id => @project,
|
||||
:issue_id => @issue},
|
||||
:method => :delete,
|
||||
:data => {:confirm => l(:text_are_you_sure)},
|
||||
:class => "icon icon-email-spam" if @issue.helpdesk_ticket.source == HelpdeskTicket::HELPDESK_EMAIL_SOURCE && @issue.customer.primary_email && User.current.allowed_to?(:send_response, @project) && User.current.allowed_to?(:delete_issues, @project) && User.current.allowed_to?(:delete_contacts, @project) %>
|
||||
</div>
|
||||
<span class="icon <%= helpdesk_ticket_source_icon(@issue.helpdesk_ticket) %>", title="<%= l(:label_helpdesk_to_address) %>: <%= @issue.helpdesk_ticket.to_address %>">
|
||||
<%= @issue.helpdesk_ticket.is_incoming? ? l(:label_helpdesk_from) : l(:label_sent_to) %>
|
||||
</span>
|
||||
<span class="ticket_customer" style="white-space: nowrap;display: inline-block;">
|
||||
<%= contact_tag(@issue.customer, :type => "plain") %>
|
||||
(<%= @issue.helpdesk_ticket.from_address %>)
|
||||
</span>
|
||||
<% if attachment = @issue.helpdesk_ticket.message_file %>
|
||||
<span class="attachment" style="white-space: nowrap;display: inline-block;">
|
||||
<%= link_to_attachment attachment, :text => l(:label_helpdesk_original), :download => true, :class => 'icon icon-attachment' -%>
|
||||
<%= h(" - #{attachment.description}") unless attachment.description.blank? %>
|
||||
<span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
|
||||
<%= link_to_if_authorized image_tag('magnifier.png', :plugin => "redmine_contacts_helpdesk"),
|
||||
:controller => 'helpdesk', :action => 'show_original',
|
||||
:id => attachment, :project_id => @project %>
|
||||
</span>
|
||||
<% end %>
|
||||
<span class="helpdesk-message-date"><%= format_time(@issue.helpdesk_ticket.ticket_date) %></span>
|
||||
<br>
|
||||
<% unless @issue.helpdesk_ticket.cc_address.blank? %>
|
||||
<span class="helpdesk-message-date"><%= l(:label_helpdesk_cc) %>: <%= @issue.helpdesk_ticket.cc_address.split(',').join(', ') %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if Redmine::VERSION.to_s > '3.2' %>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$(".attributes").first().prepend($("#ticket_data"));
|
||||
$("#ticket_data").after("<hr>");
|
||||
});
|
||||
</script>
|
||||
<% else %>
|
||||
<hr />
|
||||
<% end %>
|
||||
|
||||
<%= issue_fields_rows do |rows|
|
||||
rows.left l(:label_helpdesk_ticket_reaction_time), distance_of_time_in_words(@issue.helpdesk_ticket.reaction_time, 0, :include_seconds => true) if @issue.helpdesk_ticket.reaction_time
|
||||
rows.left l(:label_helpdesk_ticket_resolve_time), distance_of_time_in_words(@issue.helpdesk_ticket.resolve_time, 0, :include_seconds => true) if @issue.helpdesk_ticket.resolve_time
|
||||
rows.right l(:label_helpdesk_contact_vote), show_customer_vote(@issue.helpdesk_ticket.vote, @issue.helpdesk_ticket.vote_comment) if @issue.helpdesk_ticket.vote && @issue.helpdesk_ticket.vote >= 0
|
||||
rows.right l(:label_helpdesk_ticket_first_response_time), distance_of_time_in_words(@issue.helpdesk_ticket.first_response_time, 0, :include_seconds => true) if @issue.helpdesk_ticket.first_response_time
|
||||
rows.right l(:label_helpdesk_ticket_last_response_time), distance_of_time_in_words(@issue.helpdesk_ticket.last_response_time, Time.now.utc) if @issue.helpdesk_ticket.last_response_time
|
||||
end %>
|
||||
|
||||
<% end %>
|
||||
@@ -0,0 +1,26 @@
|
||||
<% if @issue.new_record? && !@copy_from && User.current.allowed_to?(:edit_helpdesk_tickets, @project) && (@issue.tracker_id.to_s == HelpdeskSettings["helpdesk_tracker", @project.id] || HelpdeskSettings["helpdesk_tracker", @project.id] == 'all') %>
|
||||
<div class="email-template">
|
||||
<% @issue.build_helpdesk_ticket if @issue.helpdesk_ticket.blank? %>
|
||||
<%= form.fields_for :helpdesk_ticket do |f| %>
|
||||
<div class="splitcontentleft">
|
||||
<p><%= f.label_for_field("issue_helpdesk_ticket_attributes_contact_id_selected_contact", :label => l(:label_helpdesk_contact), :required => true) %>
|
||||
<%= select_contact_tag('issue[helpdesk_ticket_attributes][contact_id]', @issue.helpdesk_ticket.try(:customer), :is_select => Contact.visible.by_project(ContactsSetting.cross_project_contacts? ? nil : @project).count < 50, :include_blank => true, :add_contact => true, :display_field => @issue.helpdesk_ticket.try(:customer).blank?) %>
|
||||
</p>
|
||||
|
||||
<p class="required">
|
||||
<%= f.text_field :ticket_date, :label => l(:label_helpdesk_ticket_date), :size => 12, :value => @issue.helpdesk_ticket.ticket_date.to_date %>
|
||||
<%= f.text_field :ticket_time, :size => 5, :no_label => true, :value => @issue.helpdesk_ticket.ticket_date.to_s(:time) %><%= calendar_for('issue_helpdesk_ticket_attributes_ticket_date') %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="splitcontentright">
|
||||
<p><%= f.select :source, helpdesk_tickets_source_for_select, :label => l(:label_helpdesk_ticket_source) %></p>
|
||||
<p><%= label_tag :helpdesk_send_as, l(:label_helpdesk_send_as)%>
|
||||
<%= select_tag :helpdesk_send_as, options_for_select(helpdesk_send_as_for_select, params[:helpdesk_send_as]) %> </p>
|
||||
</div>
|
||||
|
||||
<div style="clear:both;"></div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
@@ -0,0 +1,41 @@
|
||||
<% if User.current.allowed_to?(:view_helpdesk_tickets, @project) && @issue.journal_messages && journal_message = @issue.journal_messages.detect{|j| j.journal_id == journal.id} %>
|
||||
<p class="journal_message">
|
||||
<span class="icon <%= journal_message.is_incoming? ? 'icon-email' : 'icon-email-to' %>">
|
||||
<%= journal_message.is_incoming? ? l(:label_received_from) : l(:label_sent_to) %>
|
||||
</span>
|
||||
<span class="contact" style="white-space: nowrap;display: inline-block;">
|
||||
<% if journal_message.is_incoming? %>
|
||||
<%= "#{contact_tag(journal_message.contact)} (#{journal_message.from_address})".html_safe %>
|
||||
<% else %>
|
||||
<%= journal_message.contact.emails.include?(journal_message.to_address) ? "#{contact_tag(journal_message.contact)} (#{journal_message.to_address})".html_safe : journal_message.to_address %>
|
||||
<% end %>
|
||||
</span>
|
||||
|
||||
<% if attachment = journal_message.message_file %>
|
||||
<span class="attachment" style="white-space: nowrap;display: inline-block;">
|
||||
<%= link_to_attachment attachment, :text => l(:label_helpdesk_original), :class => 'icon icon-attachment' -%>
|
||||
<%= h(" - #{attachment.description}") unless attachment.description.blank? %>
|
||||
<span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
|
||||
<%= link_to_if_authorized image_tag('magnifier.png', :plugin => "redmine_contacts_helpdesk"),
|
||||
:controller => 'helpdesk', :action => 'show_original',
|
||||
:id => attachment, :project_id => @project %>
|
||||
</span>
|
||||
<% end %>
|
||||
<span class="helpdesk-message-date"><%= format_time(journal_message.message_date) %></span>
|
||||
|
||||
<% unless journal_message.bcc_address.blank? && journal_message.cc_address.blank? %>
|
||||
<br>
|
||||
<span class="heldesk_cc helpdesk-message-date">
|
||||
<%= "#{l(:label_helpdesk_cc)}: #{journal_message.cc_address}" unless journal_message.cc_address.blank? %><%= ", #{l(:label_helpdesk_bcc)}: #{journal_message.bcc_address}" unless journal_message.bcc_address.blank? %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<% if false && journal_message.is_incoming? %>
|
||||
<span class="actions" style="float: right;">
|
||||
<%= link_to "Split", "", :class => "icon icon-split" %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<%= csrf_meta_tag %>
|
||||
<%= favicon %>
|
||||
<%= stylesheet_link_tag 'jquery/jquery-ui-1.9.2', 'application', :media => 'all' %>
|
||||
<%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
|
||||
<%= javascript_heads %>
|
||||
<%= heads_for_theme %>
|
||||
<%= call_hook :view_layouts_base_html_head %>
|
||||
<!-- page specific tags -->
|
||||
<%= yield :header_tags -%>
|
||||
</head>
|
||||
<body class="<%=h body_css_classes %>">
|
||||
<div id="wrapper">
|
||||
<div id="wrapper2">
|
||||
<div id="wrapper3">
|
||||
<div id="header">
|
||||
<h1><%= RedmineHelpdesk.public_title.blank? ? Setting.app_title : RedmineHelpdesk.public_title %></h1>
|
||||
</div>
|
||||
|
||||
<div id="main" class="<%= sidebar_content? ? '' : 'nosidebar' %>">
|
||||
<div id="sidebar">
|
||||
<%= yield :sidebar %>
|
||||
<%= view_layouts_base_sidebar_hook_response %>
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<%= render_flash_messages %>
|
||||
<%= yield %>
|
||||
<%= call_hook :view_layouts_base_content %>
|
||||
<div style="clear:both;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
|
||||
<div id="ajax-modal" style="display:none;"></div>
|
||||
|
||||
<div id="footer">
|
||||
<div class="bgl"><div class="bgr">
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<%= call_hook :view_layouts_base_body_bottom %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
<% tickets_scope = Issue.visible.open.joins(:helpdesk_ticket).where(:assigned_to_id => User.current.id) %>
|
||||
|
||||
<% tickets = tickets_scope.limit(10) %>
|
||||
|
||||
<h3><%= l(:my_helpdesk_tickets) %> (<%= tickets_scope.count %>)</h3>
|
||||
|
||||
<% if tickets && tickets.any? %>
|
||||
<%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do %>
|
||||
<table class="list tickets">
|
||||
<thead><tr>
|
||||
<th><%=l(:field_subject)%></th>
|
||||
<th><%=l(:field_project)%></th>
|
||||
<th><%=l(:label_helpdesk_contact)%></th>
|
||||
<th><%=l(:label_helpdesk_last_message)%></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<% for ticket in tickets %>
|
||||
<tr id="ticket-<%= h(ticket.id) %>" class="issue hascontextmenu <%= cycle('odd', 'even') %> <%= ticket.css_classes %>">
|
||||
<td class="subject">
|
||||
<%= check_box_tag("ids[]", ticket.id, false, :style => 'display:none;', :id => nil) %>
|
||||
<span class="icon <%= ticket.helpdesk_ticket.ticket_source_icon %>"></span>
|
||||
<%= link_to "##{ticket.id} - #{truncate(ticket.subject, :length => 60)}", issue_path(ticket) %> (<%=h ticket.status %>)
|
||||
</td>
|
||||
<td class="project"><%= link_to_project(ticket.project) %></td>
|
||||
<td class="customer"><%= contact_tag(ticket.customer) + (ticket.customer_company.blank? ? "" : " (#{ticket.customer_company})") if ticket.customer %></td>
|
||||
<td class="last_message"><small>
|
||||
<%= (ticket.last_message.blank? ? ticket.description : ticket.last_message).truncate(250) %>
|
||||
</small></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% end %>
|
||||
|
||||
<% if tickets.length > 0 %>
|
||||
<p class="small"><%= link_to l(:label_helpdesk_view_all_tickets), :controller => 'issues',
|
||||
:action => 'index',
|
||||
:set_filter => 1,
|
||||
:assigned_to_id => 'me',
|
||||
:customer => "*",
|
||||
:status_id => "o",
|
||||
:c => ["project", "tracker", "status", "subject", "customer", "customer_company", "last_message"],
|
||||
# :op => {:assigned_to_id => "=", :customer => "*", :status_id => "o"},
|
||||
|
||||
:sort => 'priority:desc,updated_on:desc' %></p>
|
||||
<% end %>
|
||||
|
||||
<% content_for :header_tags do %>
|
||||
<%= auto_discovery_link_tag(:atom,
|
||||
{:controller => 'issues', :action => 'index', :set_filter => 1,
|
||||
:assigned_to_id => 'me', :format => 'atom', :key => User.current.rss_key},
|
||||
{:title => l(:label_assigned_to_me_issues)}) %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,10 @@
|
||||
<% if User.current.allowed_to?(:view_helpdesk_tickets, @project) %>
|
||||
<% if tickets = HelpdeskTicket.includes(:issue => [:project]).where(:projects => {:id => @project}) %>
|
||||
<% customers = Contact.includes(:tickets => :project).where(:projects => {:id => @project}) %>
|
||||
<h3><%= l(:label_helpdesk_ticket_plural) %></h3>
|
||||
<p><span class="icon icon-helpdesk"><%= l(:text_helpdesk_ticket_count, :count => tickets.count) %></span></p>
|
||||
<p><span class="icon icon-company-contact"><%= l(:text_helpdesk_customer_count, :count => customers.count) %> </span></p>
|
||||
<p><%# link_to(l(:label_report), {:controller => "helpdesk_reports", :action => "tickets_report", :project_id => @project}) %></p>
|
||||
<%= call_hook(:view_projects_show_helpdesk_sidebar_bottom, :project => @project) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,43 @@
|
||||
<%= error_messages_for 'helpdesk_settings' %>
|
||||
|
||||
<% if @project.module_enabled?(:contacts) && @project.module_enabled?(:issue_tracking) %>
|
||||
|
||||
<% if canned_responses = CannedResponse.visible.in_project_or_public(@project).order("#{CannedResponse.table_name}.name") %>
|
||||
<table class="list">
|
||||
<thead><tr>
|
||||
<th><%= l(:field_name) %></th>
|
||||
<th><%= l(:field_content) %></th>
|
||||
<th><%= l(:field_is_public) %></th>
|
||||
<th><%= l(:field_is_for_all) %></th>
|
||||
<th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<% canned_responses.each do |canned_response| %>
|
||||
<tr class="<%= cycle 'odd', 'even' %>">
|
||||
<td class="name"><%= canned_response.name %></td>
|
||||
<td class="name"><em class="info"><%= canned_response.content.gsub(/$/, ' ').truncate(250) %></em></td>
|
||||
<td class="tick"><%= checked_image canned_response.is_public? %></td>
|
||||
<td class="tick"><%= checked_image canned_response.project.blank? %></td>
|
||||
<td class="buttons">
|
||||
<% if User.current.allowed_to?(:manage_canned_responses, @project) %>
|
||||
<%= link_to l(:button_edit), edit_canned_response_path(canned_response), :class => 'icon icon-edit' %>
|
||||
<%= delete_link canned_response_path(canned_response, :project_id => @project) %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_no_data) %></p>
|
||||
<% end %>
|
||||
|
||||
<p><%= link_to l(:label_helpdesk_new_canned_response), new_project_canned_response_path(@project), :class => 'icon icon-add' if User.current.allowed_to?(:manage_canned_responses, @project) %></p>
|
||||
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_helpdesk_enable_modules) %></p>
|
||||
<% end %>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<div class="box" >
|
||||
|
||||
<p>
|
||||
<label><%= l(:field_mail_from) %></label>
|
||||
<%= text_field_tag "helpdesk_answer_from", HelpdeskSettings["helpdesk_answer_from", @project.id], :size => "60", :placeholder => RedmineHelpdesk.settings["helpdesk_answer_from"] %>
|
||||
<em class="info"><%= l(:text_helpdesk_answer_macros, :macro => HelpdeskSettings::FROM_MACRO_LIST.join(', ')) %></em>
|
||||
</p>
|
||||
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_answered_status) %></label>
|
||||
<%= select_tag "helpdesk_answered_status", ("<option value=\"\">#{l(:label_no_change_option)}</option>" + options_for_select(IssueStatus.all.collect {|p| [p.name, p.id.to_s]}, HelpdeskSettings["helpdesk_answered_status", @project.id])).html_safe %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_reopen_status) %></label>
|
||||
<%= select_tag "helpdesk_reopen_status", ("<option value=\"\">#{l(:label_no_change_option)}</option>" + options_for_select(IssueStatus.all.collect {|p| [p.name, p.id.to_s]}, HelpdeskSettings["helpdesk_reopen_status", @project.id])).html_safe %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_tracker) %></label>
|
||||
<%= select_tag "helpdesk_tracker", options_for_select([[l(:label_all), "all"]] + @project.trackers.collect {|t| [t.name, t.id.to_s]}, HelpdeskSettings["helpdesk_tracker", @project.id]), :include_blank => true %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_assigned_to) %></label>
|
||||
<%= select_tag "helpdesk_assigned_to", ("<option value=\"\">#{l(:label_no_change_option)}</option>" + options_for_select(@project.assignable_users.collect {|t| [t.name, t.id.to_s]}, HelpdeskSettings["helpdesk_assigned_to", @project.id])).html_safe %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_lifetime) %></label>
|
||||
<%= text_field_tag "helpdesk_lifetime", HelpdeskSettings["helpdesk_lifetime", @project.id], :size => "5" %> <%= l(:label_day_plural) %>
|
||||
</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_not_create_contacts) %></label>
|
||||
|
||||
<%= hidden_field_tag("helpdesk_is_not_create_contacts", 0) %>
|
||||
<%= check_box_tag "helpdesk_is_not_create_contacts", 1, HelpdeskSettings["helpdesk_is_not_create_contacts", @project.id].to_i > 0, :onclick => '$("#add_tags").toggle();' %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_blacklist) %></label>
|
||||
<%= text_area_tag "helpdesk_blacklist", HelpdeskSettings["helpdesk_blacklist", @project.id].blank? ? '' : HelpdeskSettings["helpdesk_blacklist", @project.id].split("\n").map{|u| u.strip}.join("\n"), :rows => 10 %>
|
||||
<br /><em class="info"><%= l(:text_custom_field_possible_values_info) %></em> </p>
|
||||
|
||||
|
||||
|
||||
<div id="add_tags" class="contacts-tags-edit" <%= "style=\"display: none;\"" if HelpdeskSettings["helpdesk_is_not_create_contacts", @project.id].to_i > 0 %>>
|
||||
<p>
|
||||
<label><%= l(:field_created_contact_tags) %></label>
|
||||
<%= text_field_tag "helpdesk_created_contact_tag", HelpdeskSettings["helpdesk_created_contact_tag", @project.id], :size => 10, :class => 'hol' %><%= tagsedit_for('#helpdesk_created_contact_tag', Contact.available_tags(:project => @project).map(&:name).join("\',\'").html_safe ) %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,184 @@
|
||||
<fieldset class="box tabular"><legend><%= l(:label_helpdesk_incoming_mail_server) %></legend>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
function changeServerSettings(element) {
|
||||
$('#helpdesk_use_ssl_field').show();
|
||||
|
||||
if (element.value == 'pop3') {
|
||||
$('#server_settings').show();
|
||||
$('#imap_settings').hide();
|
||||
$('#pop3_settings').show();
|
||||
$('#host_settings').show();
|
||||
}
|
||||
|
||||
if (element.value == 'imap') {
|
||||
$('#server_settings').show();
|
||||
$('#pop3_settings').hide();
|
||||
$('#imap_settings').show();
|
||||
$('#host_settings').show();
|
||||
}
|
||||
|
||||
if (element.value == '') {
|
||||
$('#server_settings').hide();
|
||||
}
|
||||
|
||||
if (element.value == 'gmail' || element.value == 'yahoo' || element.value == 'yandex' ) {
|
||||
$('#server_settings').show();
|
||||
$('#host_settings').hide();
|
||||
$('#imap_settings').show();
|
||||
$('#pop3_settings').hide();
|
||||
$('#helpdesk_use_ssl_field').hide();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_protocol) %></label>
|
||||
<%= select_tag :helpdesk_protocol, options_for_select([['', ""], ["pop3", "pop3"], ["imap", "imap"], ["Gmail", "gmail"], ["Yahoo", "yahoo"], ["Yandex", "yandex"]] , HelpdeskSettings[:helpdesk_protocol, @project.id]), :onchange => "changeServerSettings(this)" %>
|
||||
</p>
|
||||
|
||||
<span id="server_settings" <%= "style=\"display: none;\"".html_safe if HelpdeskSettings[:helpdesk_protocol, @project.id].blank? %>>
|
||||
|
||||
<span id="host_settings" <%= "style=\"display: none;\"".html_safe if ["gmail","yahoo", "yandex"].include?(HelpdeskSettings[:helpdesk_protocol, @project.id]) %>>
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_host) %></label>
|
||||
<%= text_field_tag :helpdesk_host, HelpdeskSettings[:helpdesk_host, @project.id] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_port) %></label>
|
||||
<%= text_field_tag :helpdesk_port, HelpdeskSettings[:helpdesk_port, @project.id] %>
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_username) %></label>
|
||||
<%= text_field_tag :helpdesk_username, HelpdeskSettings[:helpdesk_username, @project.id] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_password) %></label>
|
||||
<%= link_to_function image_tag('edit.png'), '$(this).hide(); $("#helpdesk_password_field").show()' unless HelpdeskSettings[:helpdesk_username, @project.id].blank? %>
|
||||
<%= content_tag 'span', :id => "helpdesk_password_field", :style => (HelpdeskSettings[:helpdesk_username, @project.id].blank? ? nil : 'display:none') do %>
|
||||
<%= password_field_tag :helpdesk_password, '' %>
|
||||
<% end %>
|
||||
</p>
|
||||
<p <%= "style=\"display: none;\"".html_safe if ["gmail", "yahoo", "yandex"].include?(HelpdeskSettings[:helpdesk_protocol, @project.id]) %> id="helpdesk_use_ssl_field">
|
||||
<label><%= l(:label_helpdesk_ssl) %></label>
|
||||
|
||||
<%= hidden_field_tag(:helpdesk_use_ssl, 0) %>
|
||||
<%= check_box_tag :helpdesk_use_ssl, 1, HelpdeskSettings[:helpdesk_use_ssl, @project.id].to_i > 0 %>
|
||||
</p>
|
||||
|
||||
<span id="imap_settings" <%= "style=\"display: none;\"".html_safe if !["gmail", "yahoo", "yandex", "imap"].include?(HelpdeskSettings[:helpdesk_protocol, @project.id]) %>>
|
||||
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_imap_folder) %></label>
|
||||
<%= text_field_tag :helpdesk_imap_folder, HelpdeskSettings[:helpdesk_imap_folder, @project.id] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_move_on_success) %></label>
|
||||
<%= text_field_tag :helpdesk_move_on_success, HelpdeskSettings[:helpdesk_move_on_success, @project.id] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_move_on_failure) %></label>
|
||||
<%= text_field_tag :helpdesk_move_on_failure, HelpdeskSettings[:helpdesk_move_on_failure, @project.id] %>
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<span id="pop3_settings" <%= "style=\"display: none;\"".html_safe if HelpdeskSettings[:helpdesk_protocol, @project.id] != "pop3" %>>
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_apop) %></label>
|
||||
|
||||
<%= hidden_field_tag(:helpdesk_apop, 0) %>
|
||||
<%= check_box_tag :helpdesk_apop, 1, HelpdeskSettings[:helpdesk_apop, @project.id].to_i > 0 %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_delete_unprocessed) %></label>
|
||||
|
||||
<%= hidden_field_tag(:helpdesk_delete_unprocessed, 0) %>
|
||||
<%= check_box_tag :helpdesk_delete_unprocessed, 1, HelpdeskSettings[:helpdesk_delete_unprocessed, @project.id].to_i > 0 %>
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<div id="test_connection_messages">
|
||||
</div>
|
||||
|
||||
<%= link_to l(:label_helpdesk_get_mail),
|
||||
{},
|
||||
:remote => true,
|
||||
:onclick => "updateCustomForm('#{url_for(:controller => 'helpdesk', :action => 'get_mail', :project_id => @project)}', $('#helpdesk_settings'))" %>
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
</span> <!-- Server settings -->
|
||||
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="box tabular"><legend><%= l(:label_helpdesk_outgoing_mail_server) %> (experimental)</legend>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_smtp_use_default_settings) %></label>
|
||||
<%= hidden_field_tag(:helpdesk_smtp_use_default_settings, 1) %>
|
||||
<%= check_box_tag :helpdesk_smtp_use_default_settings, 0, HelpdeskSettings[:helpdesk_smtp_use_default_settings, @project.id].to_i == 0, :onchange => "$('.smtp-settings').toggle(); return false;" %>
|
||||
</p>
|
||||
|
||||
<span class="smtp-settings" <%= "style=\"display: none;\"".html_safe unless HelpdeskSettings[:helpdesk_smtp_use_default_settings, @project.id].to_i > 0 %>>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_smtp_server) %></label>
|
||||
<%= text_field_tag :helpdesk_smtp_server, HelpdeskSettings[:helpdesk_smtp_server, @project.id] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_port) %></label>
|
||||
<%= text_field_tag :helpdesk_smtp_port, HelpdeskSettings[:helpdesk_smtp_port, @project.id] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_smtp_domain) %></label>
|
||||
<%= text_field_tag :helpdesk_smtp_domain, HelpdeskSettings[:helpdesk_smtp_domain, @project.id] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_authentication) %></label>
|
||||
<%= select_tag :helpdesk_smtp_authentication, options_for_select([[l(:label_helpdesk_authentication_plain), "plain"], [l(:label_helpdesk_authentication_login), "login"], [l(:label_helpdesk_authentication_cram_md5), "cram_md5"]] , HelpdeskSettings[:helpdesk_smtp_authentication, @project.id]) %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_username) %></label>
|
||||
<%= text_field_tag :helpdesk_smtp_username, HelpdeskSettings[:helpdesk_smtp_username, @project.id] %>
|
||||
</p>
|
||||
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_password) %></label>
|
||||
<%= link_to_function image_tag('edit.png'), '$(this).hide(); $("#helpdesk_smtp_password_field").show()' unless HelpdeskSettings[:helpdesk_smtp_username, @project.id].blank? %>
|
||||
<%= content_tag 'span', :id => "helpdesk_smtp_password_field", :style => (HelpdeskSettings[:helpdesk_smtp_username, @project.id].blank? ? nil : 'display:none') do %>
|
||||
<%= password_field_tag :helpdesk_smtp_password, '' %>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_ssl) %></label>
|
||||
<%= hidden_field_tag(:helpdesk_smtp_ssl, 0) %>
|
||||
<%= check_box_tag :helpdesk_smtp_ssl, 1, HelpdeskSettings[:helpdesk_smtp_ssl, @project.id].to_i > 0 %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_smtp_tls) %></label>
|
||||
<%= hidden_field_tag(:helpdesk_smtp_tls, 0) %>
|
||||
<%= check_box_tag :helpdesk_smtp_tls, 1, HelpdeskSettings[:helpdesk_smtp_tls, @project.id].to_i > 0 %>
|
||||
</p>
|
||||
|
||||
</span> <!-- Server settings -->
|
||||
|
||||
</fieldset>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<%= error_messages_for 'helpdesk_settings' %>
|
||||
|
||||
<% errors = [] %>
|
||||
<% errors << l(:label_helpdesk_enable_modules) unless @project.module_enabled?(:contacts) && @project.module_enabled?(:issue_tracking) %>
|
||||
|
||||
<% if errors.empty? %>
|
||||
<%= form_tag({:controller => :helpdesk, :action => :save_settings, :project_id => @project, :tab => 'helpdesk'}, :method => :put, :class => "tabular", :multipart => true, :id => 'helpdesk_settings') do %>
|
||||
|
||||
<div class="splitcontentleft">
|
||||
<h3><%=l(:label_helpdesk)%></h3>
|
||||
<%= render :partial => 'projects/settings/helpdesk_general' %>
|
||||
</div>
|
||||
|
||||
<div class="splitcontentright">
|
||||
<h3><%=l(:label_helpdesk_server_settings)%></h3>
|
||||
<%= render :partial => 'projects/settings/helpdesk_server' %>
|
||||
</div>
|
||||
|
||||
<div style="clear:both;"></div>
|
||||
|
||||
<%= submit_tag l(:button_save) %>
|
||||
|
||||
<% end %>
|
||||
|
||||
<% else %>
|
||||
<p class="nodata"><%= errors.join("<br/>").html_safe %></p>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<%= error_messages_for 'helpdesk_settings' %>
|
||||
|
||||
<% if @project.module_enabled?(:contacts) && @project.module_enabled?(:issue_tracking) %>
|
||||
<%= form_tag({:controller => :helpdesk, :action => :save_settings, :project_id => @project, :tab => 'helpdesk_template'}, :method => :put, :class => "tabular", :multipart => true, :id => 'helpdesk_template') do %>
|
||||
|
||||
<fieldset class="box tabular"><legend><%= l(:label_helpdesk_answer_template) %></legend>
|
||||
|
||||
<p>
|
||||
<label><%= l(:field_subject) %></label>
|
||||
<%= text_field_tag "helpdesk_answer_subject", HelpdeskSettings["helpdesk_answer_subject", @project.id], :style => "width:100%" %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:setting_emails_header) %></label>
|
||||
<%= text_area_tag "helpdesk_emails_header", HelpdeskSettings["helpdesk_emails_header", @project.id], :class => 'wiki-edit', :rows => 5 %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:setting_emails_footer) %></label>
|
||||
<%= text_area_tag "helpdesk_emails_footer", HelpdeskSettings["helpdesk_emails_footer", @project.id], :class => 'wiki-edit', :rows => 5 %>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
|
||||
<fieldset class="box" style="background-color: #FFD;"><legend><%= l(:label_helpdesk_auto_answer_template) %></legend>
|
||||
<p>
|
||||
<label><%= l(:label_send_auto_answer) %></label>
|
||||
|
||||
<%= hidden_field_tag("helpdesk_send_notification", 0) %>
|
||||
<%= check_box_tag "helpdesk_send_notification", 1, ContactsSetting["helpdesk_send_notification", @project.id].to_i > 0 %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:field_subject) %></label>
|
||||
<%= text_field_tag "helpdesk_first_answer_subject", HelpdeskSettings["helpdesk_first_answer_subject", @project.id], :style => "width:100%" %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_first_answer_template) %></label>
|
||||
<%= text_area_tag "helpdesk_first_answer_template", HelpdeskSettings["helpdesk_first_answer_template", @project.id], :class => 'wiki-edit', :rows => 15 %>
|
||||
<%= wikitoolbar_for 'helpdesk_first_answer_template' %>
|
||||
</p>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<div> <em class="info"><%= l(:text_helpdesk_answer_macros, :macro => HelpdeskSettings::MACRO_LIST.join(', ')) %></em></div>
|
||||
|
||||
<br/>
|
||||
|
||||
<%= submit_tag l(:button_save) %>
|
||||
|
||||
<% end %>
|
||||
|
||||
<% else %>
|
||||
<p class="nodata"><%= l(:label_helpdesk_enable_modules) %></p>
|
||||
<% end %>
|
||||
|
||||
|
||||
<% content_for :header_tags do %>
|
||||
<%= javascript_include_tag :"tag-it", :plugin => 'redmine_contacts' %>
|
||||
<%= stylesheet_link_tag :"jquery.tagit.css", :plugin => 'redmine_contacts' %>
|
||||
<%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %>
|
||||
<%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,8 @@
|
||||
<%= labelled_form_for @journal, :url => public_ticket_add_comment_path(:id => @ticket.id, :hash => @ticket.token) , :html => {:id => 'add_comment_form', :multipart => true} do |f| %>
|
||||
<%= error_messages_for 'journal', 'journal_message' %>
|
||||
<div class="box">
|
||||
<%= f.text_area 'notes', :cols => 60, :rows => 10, :class => 'wiki-edit' %>
|
||||
<%= wikitoolbar_for 'journal_notes' %>
|
||||
</div>
|
||||
<%= submit_tag l(:button_submit) %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="attachments">
|
||||
<% for attachment in attachments %>
|
||||
<p><%= link_to_attachment_with_hash attachment, :class => 'icon icon-attachment', :download => true -%>
|
||||
<% if attachment.is_text? %>
|
||||
<%= link_to image_tag('magnifier.png'),
|
||||
:controller => 'attachments', :action => 'show',
|
||||
:id => attachment, :filename => attachment.filename %>
|
||||
<% end %>
|
||||
<%= h(" - #{attachment.description}") unless attachment.description.blank? %>
|
||||
<span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<% for journal in journals %>
|
||||
<div id="change-<%= journal.id %>" class="<%= journal.css_classes %> <%= journal.journal_message.is_incoming? ? 'incoming' : 'outgoing' %>">
|
||||
<div id="note-<%= journal.indice %>">
|
||||
<h4><%= link_to "##{journal.indice}", {:anchor => "note-#{journal.indice}"}, :class => "journal-link" %>
|
||||
<%= authoring_public journal, :label => :label_updated_time_by %></h4>
|
||||
<div class="wiki" id="journal-<%= journal.id %>-notes">
|
||||
<%= textilizable(journal, :notes) unless journal.notes.blank? %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,19 @@
|
||||
<% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
|
||||
|
||||
<% if (RedmineHelpdesk.public_spent_time? && @total_spent_hours.to_i > 0) || !@previous_tickets.blank? %>
|
||||
<% content_for :sidebar do %>
|
||||
<% if RedmineHelpdesk.public_spent_time? && @total_spent_hours.to_i > 0 %>
|
||||
<h3><%= l(:label_spent_time) %></h3>
|
||||
<p><span class="icon icon-time"><%= l_hours(@total_spent_hours) %></span></p>
|
||||
<% end %>
|
||||
|
||||
<% unless @previous_tickets.empty? %>
|
||||
<h3>
|
||||
<%= l(:label_helpdesk_previous_tickets) %>
|
||||
</h3>
|
||||
<% @previous_tickets.each do |previous_ticket| %>
|
||||
<p><%= link_to "##{previous_ticket.id} - #{previous_ticket.subject} (#{previous_ticket.status.name})", public_ticket_path(previous_ticket.helpdesk_ticket, :hash => previous_ticket.helpdesk_ticket.token), :class => previous_ticket.css_classes %></p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,79 @@
|
||||
<h2><%= issue_heading(@issue) %></h2>
|
||||
|
||||
<div class="<%= @issue.css_classes %> details">
|
||||
<%= avatar(@ticket.from_address, :size => "50") %>
|
||||
|
||||
<div class="subject">
|
||||
<h3><%= @issue.subject %></h3>
|
||||
</div>
|
||||
<p class="author">
|
||||
<%= l(:label_added_time_by, :author => mail_to(@ticket.from_address), :age => content_tag('acronym', distance_of_time_in_words(Time.now, @issue.created_on), :title => format_time(@issue.created_on))).html_safe %>.
|
||||
<%# authoring @issue.created_on, mail_to(@ticket.from_address) %>
|
||||
<% if @issue.created_on != @issue.updated_on %>
|
||||
<%= l(:label_updated_time, ticket_time_tag(@issue.updated_on)).html_safe %>.
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<<%= Redmine::VERSION.to_s > '3.2' ? 'div' : 'table' %> class="attributes">
|
||||
<%= issue_fields_rows do |rows|
|
||||
rows.left l(:field_status), h(@issue.status.name), :class => 'status'
|
||||
|
||||
unless @issue.disabled_core_fields.include?('assigned_to_id')
|
||||
rows.left l(:field_assigned_to), (@issue.assigned_to ? @issue.assigned_to.name : "-"), :class => 'assigned-to'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('done_ratio')
|
||||
rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%"), :class => 'progress'
|
||||
end
|
||||
if RedmineHelpdesk.public_spent_time?
|
||||
unless @issue.disabled_core_fields.include?('estimated_hours')
|
||||
if RedmineHelpdesk.public_spent_time? && !@issue.estimated_hours.blank?
|
||||
rows.right l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours'
|
||||
end
|
||||
end
|
||||
rows.right l(:label_spent_time), (@issue.total_spent_hours > 0 ? l_hours(@issue.total_spent_hours) : "-"), :class => 'spent-time'
|
||||
end
|
||||
end %>
|
||||
<%# render_custom_fields_rows(@issue) %>
|
||||
</<%= Redmine::VERSION.to_s > '3.2' ? 'div' : 'table' %>>
|
||||
|
||||
<% if @issue.description? || @issue.attachments.any? -%>
|
||||
<hr />
|
||||
<% if @issue.description? %>
|
||||
|
||||
<p><strong><%=l(:field_description)%></strong></p>
|
||||
<div class="wiki">
|
||||
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @issue.attachments.any? %>
|
||||
<fieldset class="collapsible collapsed">
|
||||
<legend onclick="toggleFieldset(this);"><%= l(:label_attachment_plural) %></legend>
|
||||
<div style="display: none;">
|
||||
<%= link_to_attachments_with_hash @issue, :thumbnails => true %>
|
||||
</div>
|
||||
</fieldset>
|
||||
<% end %>
|
||||
<% end -%>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<% if @journals.present? %>
|
||||
<div id="history" class="ticket-history">
|
||||
<h3><%=l(:label_history)%></h3>
|
||||
<%= render :partial => 'public_tickets/history', :locals => { :issue => @issue, :journals => @journals } %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div style="clear: both;"></div>
|
||||
|
||||
<% if RedmineHelpdesk.public_comments? %>
|
||||
<p><%= toggle_link l(:label_comment_add), "update", :focus => "journal_notes" %></p>
|
||||
<div id="update" style="display:none;">
|
||||
<h3><%= l(:label_comment_add) %></h3>
|
||||
<%= render :partial => 'add_comment' %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render :partial => 'sidebar_content' %>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<% helpdesk_tabs = [
|
||||
{:name => 'general', :partial => 'settings/helpdesk_general', :label => :label_helpdesk_general},
|
||||
{:name => 'public', :partial => 'settings/helpdesk_public', :label => :label_helpdesk_settings_public},
|
||||
{:name => 'templates', :partial => 'settings/helpdesk_template', :label => :label_helpdesk_template},
|
||||
{:name => 'votes', :partial => 'settings/helpdesk_vote', :label => :label_helpdesk_vote},
|
||||
{:name => 'canned_responses', :partial => 'settings/helpdesk_canned_responses', :label => :label_helpdesk_canned_response_plural},
|
||||
{:name => 'widget', :partial => 'settings/helpdesk_widget', :label => :label_helpdesk_widget}
|
||||
] %>
|
||||
|
||||
<% helpdesk_tabs.push({:name => 'hidden', :partial => 'settings/helpdesk_hidden', :label => :label_crm_contacts_hidden}) if params[:hidden] %>
|
||||
|
||||
<%= render_tabs helpdesk_tabs %>
|
||||
|
||||
<% html_title(l(:label_settings), l(:label_helpdesk)) -%>
|
||||
|
||||
<% content_for(:header_tags) do %>
|
||||
<%= javascript_include_tag :redmine_helpdesk, :plugin => 'redmine_contacts_helpdesk' %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,2 @@
|
||||
<% @canned_responses = CannedResponse.all %>
|
||||
<%= render :partial => 'canned_responses/index' %>
|
||||
@@ -0,0 +1,50 @@
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_from_address) %></label>
|
||||
<%= text_field_tag 'settings[helpdesk_answer_from]', @settings["helpdesk_answer_from"], :size => "98%" %>
|
||||
<em class="info"><%= l(:text_helpdesk_answer_macros, :macro => HelpdeskSettings::FROM_MACRO_LIST.join(', ')) %></em>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_save_cc) %></label>
|
||||
<%= check_box_tag 'settings[helpdesk_save_cc]', 1, @settings["helpdesk_save_cc"] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_send_note_by_default) %></label>
|
||||
<%= check_box_tag 'settings[send_note_by_default]', 1, @settings["send_note_by_default"] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_add_contact_notes) %></label>
|
||||
<%= check_box_tag 'settings[helpdesk_add_contact_notes]', 1, @settings["helpdesk_add_contact_notes"] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_assign_contact_user) %></label>
|
||||
<%= check_box_tag 'settings[helpdesk_assign_contact_user]', 1, @settings["helpdesk_assign_contact_user"], :class => 'assign_contact_user' %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label class='parent'><%= l(:label_helpdesk_create_private_tickets) %></label>
|
||||
<%= check_box_tag 'settings[helpdesk_create_private_tickets]', 1, @settings["helpdesk_create_private_tickets"], :disabled => true, :class => 'private_tikets' %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label class='parent'><%= l(:label_helpdesk_autoclose_tickets_after) %></label>
|
||||
<%= select_tag 'settings[helpdesk_autoclose_tickets_after]', options_for_select((1..24).map{ |h| [h, h] }, @settings["helpdesk_autoclose_tickets_after"]),
|
||||
:onchange => "toggleStatusesForAutoclose(this); return false", :include_blank => true %>
|
||||
<%= radio_button_tag 'settings[helpdesk_autoclose_tickets_time_unit]', 'day', RedmineHelpdesk.autoclose_time_unit_is?('day') || RedmineHelpdesk.autoclose_time_unit.nil? %>
|
||||
<%= l(:label_helpdesk_days) %>
|
||||
<%= radio_button_tag 'settings[helpdesk_autoclose_tickets_time_unit]', 'hour', RedmineHelpdesk.autoclose_time_unit_is?('hour') %>
|
||||
<%= l(:label_helpdesk_hours) %>
|
||||
</p>
|
||||
<div id="statuses_autoclose" style="display:<%= !@settings["helpdesk_autoclose_tickets_after"].blank? ? 'block' : 'none' %>">
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_autoclose_from_status) %></label>
|
||||
<%= select_tag 'settings[helpdesk_autoclose_from_status]', options_for_select(IssueStatus.all.map{|st| [st.name, st.id]}, @settings["helpdesk_autoclose_from_status"]), :style => "width:150px" %>
|
||||
</p>
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_autoclose_to_status) %></label>
|
||||
<%= select_tag 'settings[helpdesk_autoclose_to_status]', options_for_select(IssueStatus.all.map{|st| [st.name, st.id]}, @settings["helpdesk_autoclose_to_status"]), :style => "width:150px" %>
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<p>
|
||||
<label><%= l(:setting_plain_text_mail) %></label>
|
||||
<%= check_box_tag 'settings[plain_text_mail]', 1, @settings["plain_text_mail"] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label>Do not strip HTML tags</label>
|
||||
<%= check_box_tag 'settings[helpdesk_do_not_strip_tags]', 1, @settings["helpdesk_do_not_strip_tags"] %>
|
||||
</p>
|
||||
@@ -0,0 +1,20 @@
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_public_tickets) %></label>
|
||||
<%= check_box_tag 'settings[helpdesk_public_tickets]', 1, @settings["helpdesk_public_tickets"] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_public_show_spent_time) %></label>
|
||||
<%= check_box_tag 'settings[helpdesk_public_show_spent_time]', 1, @settings["helpdesk_public_show_spent_time"] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_public_comments) %></label>
|
||||
<%= check_box_tag 'settings[helpdesk_public_comments]', 1, @settings["helpdesk_public_comments"] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_public_title) %></label>
|
||||
<%= text_field_tag 'settings[helpdesk_public_title]', @settings["helpdesk_public_title"], :size => "98%" %>
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<fieldset class="box tabular"><legend><%= l(:label_helpdesk_answer_template) %></legend>
|
||||
|
||||
<p>
|
||||
<label><%= l(:field_subject) %></label>
|
||||
<%= text_field_tag 'settings[helpdesk_answer_subject]', @settings["helpdesk_answer_subject"], :style => "width:100%" %>
|
||||
</p>
|
||||
|
||||
|
||||
<p>
|
||||
<label><%= l(:setting_emails_header) %></label>
|
||||
<%= text_area_tag 'settings[helpdesk_emails_header]', @settings["helpdesk_emails_header"], :class => 'wiki-edit', :rows => 5 %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:setting_emails_footer) %></label>
|
||||
<%= text_area_tag 'settings[helpdesk_emails_footer]', @settings["helpdesk_emails_footer"], :class => 'wiki-edit', :rows => 5 %>
|
||||
</p>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="box" style="background-color: #FFD;"><legend><%= l(:label_helpdesk_auto_answer_template) %></legend>
|
||||
|
||||
<p>
|
||||
<label><%= l(:field_subject) %></label>
|
||||
<%= text_field_tag 'settings[helpdesk_first_answer_subject]', @settings["helpdesk_first_answer_subject"], :style => "width:100%" %>
|
||||
</p>
|
||||
|
||||
<%= text_area_tag 'settings[helpdesk_first_answer_template]', @settings["helpdesk_first_answer_template"], :class => 'wiki-edit', :rows => 15 %>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="box"><legend><%= l(:label_helpdesk_css) %></legend>
|
||||
<%= text_area_tag 'settings[helpdesk_helpdesk_css]', @settings["helpdesk_helpdesk_css"], :class => 'wiki-edit', :rows => 10 %>
|
||||
</fieldset>
|
||||
|
||||
<em class="info"><%= l(:text_helpdesk_answer_macros, :macro => HelpdeskSettings::MACRO_LIST.join(', ')) %></em>
|
||||
|
||||
<% if params[:show_hidden] %>
|
||||
<fieldset id="hidden_settings">
|
||||
<p>
|
||||
<label>Show excerpt issues list</label>
|
||||
<%= check_box_tag 'settings[show_excerpt_tickets_list]', 1, @settings["show_excerpt_tickets_list"] %>
|
||||
</p>
|
||||
</fieldset>
|
||||
<% end %>
|
||||
@@ -0,0 +1,9 @@
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_vote_settings) %></label>
|
||||
<%= check_box_tag 'settings[helpdesk_vote_accept]', 1, @settings["helpdesk_vote_accept"] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_vote_comment_settings) %></label>
|
||||
<%= check_box_tag 'settings[helpdesk_vote_comment_accept]', 1, @settings["helpdesk_vote_comment_accept"] %>
|
||||
</p>
|
||||
@@ -0,0 +1,44 @@
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_widget_enable) %></label>
|
||||
<%= check_box_tag 'settings[helpdesk_widget_enable]', 1, @settings["helpdesk_widget_enable"] %>
|
||||
</p>
|
||||
|
||||
<% if @settings["helpdesk_widget_enable"].to_i > 0 %>
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_widget_available_projects) %></label>
|
||||
<% @helpdesk_projects = Project.visible.has_module('contacts_helpdesk') %>
|
||||
<% if @helpdesk_projects.count > 0 %>
|
||||
<% @helpdesk_projects.each do |project| %>
|
||||
<%= check_box_tag 'settings[helpdesk_widget_available_projects][]', project.id, @settings["helpdesk_widget_available_projects"].try(:include?, project.id.to_s) %>
|
||||
<span><%= project.name %></span>
|
||||
<br>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<span class="error-text"><%= l(:label_helpdesk_widget_no_available_projects) %></span>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= l(:label_helpdesk_widget_custom_fields) %></label>
|
||||
<% IssueCustomField.visible.each do |cf| %>
|
||||
<%= check_box_tag 'settings[helpdesk_widget_available_custom_fields][]', cf.id, @settings["helpdesk_widget_available_custom_fields"].try(:include?, cf.id.to_s) %>
|
||||
<span><%= cf.name %></span>
|
||||
<br>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<h4><%= l(:label_helpdesk_widget_activation_message) %></h4>
|
||||
<pre style="background-color: #ddd;">
|
||||
<code class="html syntaxhl">
|
||||
<%= Redmine::SyntaxHighlighting.highlight_by_language(
|
||||
"<span>
|
||||
<div id=\"helpdesk_widget\"></div>
|
||||
<script type=\"text/javascript\" src=\"#{Setting.protocol}://#{Setting.host_name}/helpdesk_widget/widget.js\"></script>
|
||||
</span>", "html").html_safe %>
|
||||
</code>
|
||||
</pre>
|
||||
<span>
|
||||
<div id="helpdesk_widget"></div>
|
||||
<script type="text/javascript" src="<%= "#{Setting.protocol}://#{Setting.host_name}" %>/helpdesk_widget/widget.js"></script>
|
||||
</span>
|
||||
<% end %>
|
||||
|
After Width: | Height: | Size: 677 B |
|
After Width: | Height: | Size: 795 B |
|
After Width: | Height: | Size: 438 B |
|
After Width: | Height: | Size: 439 B |
|
After Width: | Height: | Size: 792 B |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 754 B |
|
After Width: | Height: | Size: 766 B |
|
After Width: | Height: | Size: 615 B |
|
After Width: | Height: | Size: 794 B |
|
After Width: | Height: | Size: 565 B |
|
After Width: | Height: | Size: 837 B |
|
After Width: | Height: | Size: 743 B |
|
After Width: | Height: | Size: 923 B |
@@ -0,0 +1,59 @@
|
||||
function togglePrivateTicketsOnChange() {
|
||||
var checked = $(this).is(':checked');
|
||||
$('.private_tikets').attr('disabled', !checked);
|
||||
}
|
||||
|
||||
function togglePrivateTicketsInit() {
|
||||
$('.assign_contact_user').each(togglePrivateTicketsOnChange);
|
||||
}
|
||||
|
||||
function toggleStatusesForAutoclose(node) {
|
||||
if ($(node).val() == ''){
|
||||
$('#statuses_autoclose').hide();
|
||||
} else {
|
||||
$('#statuses_autoclose').show();
|
||||
}
|
||||
}
|
||||
|
||||
function ccEmailTagResult (opt) {
|
||||
if (opt.name){
|
||||
var formated_tag = $('<span>' + opt.avatar + ' ' + opt.text + '</span>');;
|
||||
} else {
|
||||
var formated_tag = opt.text;
|
||||
}
|
||||
return formated_tag
|
||||
};
|
||||
|
||||
function ccEmailTagSelection (opt) {
|
||||
if (opt.name){
|
||||
var formated_tag = opt.name + ' <' + opt.email + '>';
|
||||
} else {
|
||||
var formated_tag = opt.text;
|
||||
}
|
||||
return formated_tag
|
||||
};
|
||||
|
||||
function showWithSendAndScrollTo(id, focus) {
|
||||
showAndScrollTo(id, focus);
|
||||
send_mail_input = $('#helpdesk_is_send_mail');
|
||||
send_mail_input.prop( "checked", true );
|
||||
send_mail_input.checked = true;
|
||||
toggleSendMail(send_mail_input);
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
$('#content').on('change', '.assign_contact_user', togglePrivateTicketsOnChange);
|
||||
togglePrivateTicketsInit();
|
||||
|
||||
$('#history .contextual a[title="Quote"]').click(function(){
|
||||
if ($('#ticket_data').length > 0) {
|
||||
var journal_id = this.href.match(/journal_id=(\d*)/)[1];
|
||||
console.log(journal_id);
|
||||
$.ajax({
|
||||
method: 'GET',
|
||||
url: '/helpdesk/update_customer_email',
|
||||
data: { journal_id: journal_id }
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,370 @@
|
||||
|
||||
span.attachment img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
div#contacts_previous_issues {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/**********************************************************************/
|
||||
/* TICKETS DATA SHOW ISSUE
|
||||
/**********************************************************************/
|
||||
|
||||
span.helpdesk-message-date {
|
||||
font-size: 0.9em;
|
||||
color: #888;
|
||||
}
|
||||
/**********************************************************************/
|
||||
/* TICKETS DATA FORM
|
||||
/**********************************************************************/
|
||||
|
||||
form#ticket_data_form {
|
||||
background:
|
||||
#ffffff;
|
||||
display: block;
|
||||
padding: 6px;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid
|
||||
#d7d7d7;
|
||||
}
|
||||
|
||||
.contact_auto_complete > span {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
/**********************************************************************/
|
||||
/* TICKETS EXCERPT LIST
|
||||
/**********************************************************************/
|
||||
table.list.issues td.helpdesk_ticket {white-space: normal; text-align: left;}
|
||||
table.list.issues td.helpdesk_ticket .gravatar {float: left;}
|
||||
table.list.issues td.helpdesk_ticket .ticket-data {margin-left: 45px;}
|
||||
table.list.issues td.helpdesk_ticket .ticket-data .ticket-name {margin: 0px;font-weight: bold;}
|
||||
table.list.issues td.helpdesk_ticket .ticket-status {float: left;}
|
||||
table.list.issues td.last_message {vertical-align: top}
|
||||
|
||||
/**********************************************************************/
|
||||
/* PUBCLIV TICKETS
|
||||
/**********************************************************************/
|
||||
div.ticket-history div.journal {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
div.ticket-history div.journal.incoming,
|
||||
div.ticket-history div.journal.outgoing {
|
||||
padding: 6px;
|
||||
margin-bottom: 6px;
|
||||
border: 1px solid #d7d7d7;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.ticket-history div.journal.incoming {
|
||||
background: #ececec;
|
||||
margin-right: 30px;
|
||||
margin-left: 4px;
|
||||
|
||||
}
|
||||
div.ticket-history div.journal.outgoing {
|
||||
background: #f1faff;
|
||||
margin-left: 30px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
div.ticket-history div.journal.incoming:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left:-7px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #ececec;
|
||||
border-left:1px solid #d7d7d7;
|
||||
border-bottom:1px solid #d7d7d7;
|
||||
-moz-transform:rotate(45deg);
|
||||
-webkit-transform:rotate(45deg);
|
||||
}
|
||||
|
||||
div.ticket-history div.journal.outgoing:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right:-7px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #f1faff;
|
||||
border-right:1px solid #d7d7d7;
|
||||
border-bottom:1px solid #d7d7d7;
|
||||
-moz-transform:rotate(-45deg);
|
||||
-webkit-transform:rotate(-45deg);
|
||||
}
|
||||
|
||||
/**********************************************************************/
|
||||
/* TICKET DATA
|
||||
/**********************************************************************/
|
||||
span.ticket-status {
|
||||
padding: 3px 4px;
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
margin-right: 4px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
div#ticket_data > div.contextual {
|
||||
margin-top: initial;
|
||||
}
|
||||
|
||||
form.new_issue .email-template {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
span.ticket-status.status-1 {background-color: #CD5C5C;}
|
||||
span.ticket-status.status-2 {background-color: deepSkyBlue;}
|
||||
span.ticket-status.status-3 {background-color: green;}
|
||||
span.ticket-status.status-4 {background-color: #FF8C00;}
|
||||
span.ticket-status.status-5 {background-color: #AAA;}
|
||||
span.ticket-status.status-6 {background-color: #8A2BE2;}
|
||||
|
||||
/**********************************************************************/
|
||||
/* SHOW TICKET
|
||||
/**********************************************************************/
|
||||
|
||||
#ticket-history .ticket-avatar {float: left;}
|
||||
#ticket-history .ticket-note-content {margin-left: 50px;}
|
||||
#ticket-history .ticket-note {border-top: 1px solid #d7d7d7; padding-top: 8px;}
|
||||
|
||||
/**********************************************************************/
|
||||
/* SEND RESPONSE
|
||||
/**********************************************************************/
|
||||
|
||||
.cc-list-edit .is-cc {
|
||||
float: left;
|
||||
min-width: 25px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.cc-list-edit {
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.cc-list-edit .select2 {
|
||||
margin-left: 60px;
|
||||
width: 90% !important;
|
||||
-moz-border-radius: 0px;
|
||||
-webkit-border-radius: 0px;
|
||||
-khtml-border-radius: 0px;
|
||||
border-radius: 0px;
|
||||
background: white;
|
||||
padding: 0px;
|
||||
}
|
||||
.cc-list-edit .select2 .select2-selection__choice {
|
||||
background-color: #D7E7F9;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.cc-list-edit ul.tagit li.tagit-choice .tagit-close .text-icon:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.cc-list-edit ul.tagit li.tagit-new input {
|
||||
font-size: 11px;
|
||||
background: white;
|
||||
margin-bottom: 2px;
|
||||
margin-left: 2px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.cc-list-edit ul.tagit li.tagit-new {
|
||||
padding: 0px;
|
||||
}*/
|
||||
|
||||
p#helpdesk_send_response {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
|
||||
/**********************************************************************/
|
||||
/* ISSUES LIST
|
||||
/**********************************************************************/
|
||||
|
||||
tr.issue td.customer, tr.issue td.customer_company {
|
||||
white-space: normal;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table.list.issue td.last_message img.gravatar{
|
||||
vertical-align: middle;
|
||||
margin: 0 4px 2px 0;
|
||||
}
|
||||
|
||||
div.email-template {
|
||||
background-color: #FFD;
|
||||
border: 1px solid #E4E4E4;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
tr.issue td.last_message {
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
tr.issue td.last_message_date {
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
tr.issue.context-menu-selection td.last_message span.description {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/**********************************************************************/
|
||||
/* ICONS
|
||||
/**********************************************************************/
|
||||
|
||||
#admin-menu a.helpdesk { background-image: url(../images/support.png)}
|
||||
.icon-public-link { background-image: url(../../../images/external.png);padding-left: 14px;}
|
||||
.icon-email-spam { background-image: url(../images/email_error.png); }
|
||||
.icon-email-to { background-image: url(../images/email_go.png); }
|
||||
.icon-email-from { background-image: url(../images/email_from.png); }
|
||||
.icon-helpdesk { background-image: url(../images/user_comment.png); }
|
||||
.icon-split { background-image: url(../images/arrow_divide.png); }
|
||||
.icon-web { background-image: url(../images/world.png); }
|
||||
.icon-support { background-image: url(../images/support.png); }
|
||||
.icon-reply { background-image: url(../images/reply.png); }
|
||||
|
||||
/**********************************************************************/
|
||||
/* VOTE
|
||||
/**********************************************************************/
|
||||
|
||||
.vote_form {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.submit, #vote_comment {
|
||||
margin-top: 5px;
|
||||
width: 40%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.vote-value .icon {
|
||||
padding-left: 20px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.icon-awesome { background-image: url(../images/awesome.png); }
|
||||
.icon-justok { background-image: url(../images/just_ok.png); }
|
||||
.icon-notgood { background-image: url(../images/not_good.png); }
|
||||
|
||||
.error-text { color: red; }
|
||||
|
||||
/**********************************************************************/
|
||||
/* CHARTS
|
||||
/**********************************************************************/
|
||||
|
||||
.helpdesk_chart { text-align: center; width: 100%; }
|
||||
|
||||
.center { text-align: center; }
|
||||
.chart_table { margin: auto; border-collapse:collapse; text-align: center }
|
||||
.chart_table .header { height: 50px; background-color: #eee }
|
||||
.chart_table .header .column_data { border: 1px solid #c0c0c0; padding: 0px; }
|
||||
.chart_table .header .column_data .issues_count { font-weight: bold; }
|
||||
.chart_table .main_block { height: 200px }
|
||||
.chart_table .main_block .column_data { vertical-align: bottom; width: 80px; border: 1px solid #c0c0c0; padding: 0px; }
|
||||
.chart_table .main_block .column_data .percents { background-color: #BAE0BA; }
|
||||
.chart_table .footer { height: 50px }
|
||||
.chart_table .footer .column_data { font-weight: bold; padding: 0px; }
|
||||
|
||||
/* Metrics*/
|
||||
|
||||
table.metrics {
|
||||
border-collapse: separate;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.metrics.box {
|
||||
background-color: #eee;
|
||||
width: 250px;
|
||||
margin: 15px;
|
||||
border: 1px solid #c0c0c0;
|
||||
display: inline-block;
|
||||
padding: 20px 5px;
|
||||
}
|
||||
|
||||
.metrics td {
|
||||
padding: 25px 0;
|
||||
border-top: 1px solid #dbdde1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.metrics .num {
|
||||
color: #444;
|
||||
font-size: 29px;
|
||||
margin-right: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.metrics .change {
|
||||
display: inline-block;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metrics p {
|
||||
color: #999;
|
||||
margin: 2px;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.change>.caret {
|
||||
border-width: 6px;
|
||||
display: block;
|
||||
margin: 0 auto 4px
|
||||
}
|
||||
|
||||
.change>.neg {
|
||||
border-top-color: #ed5a5a
|
||||
}
|
||||
|
||||
.change>.pos {
|
||||
border-top: 0;
|
||||
border-bottom: 6px solid #43ac6d
|
||||
}
|
||||
|
||||
.change>.mirror_pos {
|
||||
border-top: 0;
|
||||
border-bottom: 6px solid #ed5a5a
|
||||
}
|
||||
|
||||
.change>.mirror_neg {
|
||||
border-top-color: #43ac6d
|
||||
}
|
||||
|
||||
.caret {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
vertical-align: top;
|
||||
border-top: 4px solid #2b2b2b;
|
||||
border-right: 4px solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
content: ""
|
||||
}
|
||||
|
||||
.metrics .num>span {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#customer_previous_issues ul {margin: 0; padding: 0;}
|
||||
#customer_previous_issues ul li {position: relative; margin-bottom: 10px}
|
||||
#customer_previous_issues ul li .ticket-meta {color: #888; font-size: 0.9em;display: block}
|
||||