Vendor Chatwoot Vue 3 frontend into frontend/
Copy the Chatwoot (v4.14.0) frontend runnable subset into frontend/ for customization: - app/javascript/ (Vue SPA: dashboard, widget, sdk, portal, superadmin) - app/views/ (ERB templates for vite-plugin-ruby entrypoint resolution) - app/helpers/, app/assets/ (Rails view helpers, static assets) - enterprise/ (Enterprise edition frontend overlay) - config/vite.json, vite.config.ts, bin/vite (Vite-Rails toolchain) - package.json, pnpm-lock.yaml, tailwind/postcss/eslint configs - Gemfile, Gemfile.lock (vite_rails gem for bin/vite binstub) Excluded Rails backend: controllers, models, services, jobs, mailers, policies, db, lib, spec, public, node_modules. Update references to the new frontend location: - .gitignore: exclude frontend build artifacts (node_modules, tmp, packs), keep frontend/bin/ and frontend/vendor/ via negation - backend/scripts/parity_frontend_smoke.sh: CHATWOOT_DIR default reference/chatwoot -> ../frontend - backend/scripts/parity_frontend_browser_smoke.mjs: same default update - AGENTS.md: add frontend section with Rails/Vite coupling notes - README: architecture tree includes frontend/
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
module Api::BaseHelper
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
module Api::V1::AgentsHelper
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
module Api::V1::CannedResponsesHelper
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
module Api::V1::ConversationsHelper
|
||||
end
|
||||
@@ -0,0 +1,123 @@
|
||||
module Api::V1::InboxesHelper
|
||||
def inbox_name(channel)
|
||||
return channel.try(:bot_name) if channel.is_a?(Channel::Telegram)
|
||||
|
||||
permitted_params[:name]
|
||||
end
|
||||
|
||||
def validate_email_channel(attributes)
|
||||
channel_data = permitted_params(attributes)[:channel]
|
||||
|
||||
validate_imap(channel_data)
|
||||
validate_smtp(channel_data)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_imap(channel_data)
|
||||
return unless channel_data.key?('imap_enabled') && channel_data[:imap_enabled]
|
||||
|
||||
# Validate the user-selected auth mechanism before opening the connection.
|
||||
authentication = Imap::Authentication.validate_user_configurable!(channel_data[:imap_authentication])
|
||||
|
||||
# Use the same auth adapter as the fetch service so LOGIN uses the IMAP LOGIN command,
|
||||
# not SASL AUTH=LOGIN.
|
||||
check_imap_connection(channel_data, authentication)
|
||||
end
|
||||
|
||||
def validate_smtp(channel_data)
|
||||
return unless channel_data.key?('smtp_enabled') && channel_data[:smtp_enabled]
|
||||
|
||||
smtp = Net::SMTP.new(channel_data[:smtp_address], channel_data[:smtp_port])
|
||||
|
||||
set_smtp_encryption(channel_data, smtp)
|
||||
check_smtp_connection(channel_data, smtp)
|
||||
end
|
||||
|
||||
def check_imap_connection(channel_data, authentication)
|
||||
imap = open_imap_connection(channel_data, authentication)
|
||||
rescue SocketError => e
|
||||
raise StandardError, I18n.t('errors.inboxes.imap.socket_error')
|
||||
rescue Net::IMAP::NoResponseError => e
|
||||
raise StandardError, I18n.t('errors.inboxes.imap.no_response_error')
|
||||
rescue Errno::EHOSTUNREACH => e
|
||||
raise StandardError, I18n.t('errors.inboxes.imap.host_unreachable_error')
|
||||
rescue Net::OpenTimeout => e
|
||||
raise StandardError,
|
||||
I18n.t('errors.inboxes.imap.connection_timed_out_error', address: channel_data[:imap_address], port: channel_data[:imap_port])
|
||||
rescue Net::IMAP::Error => e
|
||||
raise StandardError, I18n.t('errors.inboxes.imap.connection_closed_error')
|
||||
rescue StandardError => e
|
||||
raise StandardError, e.message
|
||||
ensure
|
||||
imap.disconnect if imap.present? && !imap.disconnected?
|
||||
Rails.logger.error "[Api::V1::InboxesHelper] check_imap_connection failed with #{e.message}" if e.present?
|
||||
end
|
||||
|
||||
def open_imap_connection(channel_data, authentication)
|
||||
imap = build_imap_connection(channel_data)
|
||||
Imap::Authentication.authenticate!(imap, authentication, channel_data[:imap_login], channel_data[:imap_password])
|
||||
imap
|
||||
end
|
||||
|
||||
def build_imap_connection(channel_data)
|
||||
Net::IMAP.new(channel_data[:imap_address], port: channel_data[:imap_port], ssl: channel_data[:imap_enable_ssl])
|
||||
end
|
||||
|
||||
def check_smtp_connection(channel_data, smtp)
|
||||
smtp.open_timeout = 10
|
||||
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password],
|
||||
channel_data[:smtp_authentication]&.to_sym || :login)
|
||||
smtp.finish
|
||||
rescue Net::SMTPAuthenticationError
|
||||
raise StandardError, I18n.t('errors.inboxes.smtp.authentication_error')
|
||||
rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Net::OpenTimeout
|
||||
raise StandardError, I18n.t('errors.inboxes.smtp.connection_error')
|
||||
rescue OpenSSL::SSL::SSLError
|
||||
raise StandardError, I18n.t('errors.inboxes.smtp.ssl_error')
|
||||
rescue Net::SMTPServerBusy, Net::SMTPSyntaxError, Net::SMTPFatalError
|
||||
raise StandardError, I18n.t('errors.inboxes.smtp.smtp_error')
|
||||
rescue StandardError => e
|
||||
raise StandardError, e.message
|
||||
end
|
||||
|
||||
def set_smtp_encryption(channel_data, smtp)
|
||||
if channel_data[:smtp_enable_ssl_tls]
|
||||
set_smtp_ssl_method(smtp, :enable_tls, channel_data[:smtp_openssl_verify_mode])
|
||||
elsif channel_data[:smtp_enable_starttls_auto]
|
||||
set_smtp_ssl_method(smtp, :enable_starttls_auto, channel_data[:smtp_openssl_verify_mode])
|
||||
end
|
||||
end
|
||||
|
||||
def set_smtp_ssl_method(smtp, method, openssl_verify_mode)
|
||||
return unless smtp.respond_to?(method)
|
||||
|
||||
context = enable_openssl_mode(openssl_verify_mode) if openssl_verify_mode
|
||||
context ? smtp.send(method, context) : smtp.send(method)
|
||||
end
|
||||
|
||||
def enable_openssl_mode(smtp_openssl_verify_mode)
|
||||
openssl_verify_mode = "OpenSSL::SSL::VERIFY_#{smtp_openssl_verify_mode.upcase}".constantize if smtp_openssl_verify_mode.is_a?(String)
|
||||
context = Net::SMTP.default_ssl_context
|
||||
context.verify_mode = openssl_verify_mode
|
||||
context
|
||||
end
|
||||
|
||||
def account_channels_method
|
||||
{
|
||||
'web_widget' => Current.account.web_widgets,
|
||||
'api' => Current.account.api_channels,
|
||||
'email' => Current.account.email_channels,
|
||||
'line' => Current.account.line_channels,
|
||||
'telegram' => Current.account.telegram_channels,
|
||||
'whatsapp' => Current.account.whatsapp_channels,
|
||||
'sms' => Current.account.sms_channels
|
||||
}[permitted_params[:channel][:type]]
|
||||
end
|
||||
|
||||
def validate_limit
|
||||
return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes]
|
||||
|
||||
render_payment_required('Account limit exceeded. Upgrade to a higher plan')
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
module Api::V1::Widget::MessagesHelper
|
||||
end
|
||||
@@ -0,0 +1,104 @@
|
||||
module Api::V2::Accounts::HeatmapHelper
|
||||
def generate_conversations_heatmap_report
|
||||
timezone_data = generate_heatmap_data_for_timezone(params[:timezone_offset])
|
||||
|
||||
group_traffic_data(timezone_data)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def group_traffic_data(data)
|
||||
# start with an empty array
|
||||
result_arr = []
|
||||
|
||||
# pick all the unique dates from the data in ascending order
|
||||
dates = data.pluck(:date).uniq.sort
|
||||
|
||||
# add the dates as the first row, leave an empty cell for the hour column
|
||||
# e.g. ['Start of the hour', '2023-01-01', '2023-1-02', '2023-01-03']
|
||||
result_arr << (['Start of the hour'] + dates)
|
||||
|
||||
# group the data by hour, we do not need to sort it, because the data is already sorted
|
||||
# given it starts from the beginning of the day
|
||||
# here each hour is a key, and the value is an array of all the items for that hour at each date
|
||||
# e.g. hour = 1
|
||||
# value = [{date: 2023-01-01, value: 1}, {date: 2023-01-02, value: 1}, {date: 2023-01-03, value: 1}, ...]
|
||||
data.group_by { |d| d[:hour] }.each do |hour, items|
|
||||
# create a new row for each hour
|
||||
row = [format('%02d:00', hour)]
|
||||
|
||||
# group the items by date, so we can easily access the value for each date
|
||||
# grouped values will be a hasg with the date as the key, and the value as the value
|
||||
# e.g. { '2023-01-01' => [{date: 2023-01-01, value: 1}], '2023-01-02' => [{date: 2023-01-02, value: 1}], ... }
|
||||
grouped_values = items.group_by { |d| d[:date] }
|
||||
|
||||
# now for each unique date we have, we can access the value for that date and append it to the array
|
||||
dates.each do |date|
|
||||
row << (grouped_values[date][0][:value] if grouped_values[date].is_a?(Array))
|
||||
end
|
||||
|
||||
# row will look like ['22:00', 0, 0, 1, 4, 6, 7, 4]
|
||||
# add the row to the result array
|
||||
|
||||
result_arr << row
|
||||
end
|
||||
|
||||
# return the resultant array
|
||||
# the result looks like this
|
||||
# [
|
||||
# ['Start of the hour', '2023-01-01', '2023-1-02', '2023-01-03'],
|
||||
# ['00:00', 0, 0, 0],
|
||||
# ['01:00', 0, 0, 0],
|
||||
# ['02:00', 0, 0, 0],
|
||||
# ['03:00', 0, 0, 0],
|
||||
# ['04:00', 0, 0, 0],
|
||||
# ]
|
||||
result_arr
|
||||
end
|
||||
|
||||
def generate_heatmap_data_for_timezone(offset)
|
||||
timezone = ActiveSupport::TimeZone[offset]&.name
|
||||
timezone_today = DateTime.now.in_time_zone(timezone).beginning_of_day
|
||||
|
||||
timezone_data_raw = generate_heatmap_data(timezone_today, offset)
|
||||
|
||||
transform_data(timezone_data_raw, false)
|
||||
end
|
||||
|
||||
def generate_heatmap_data(date, offset)
|
||||
report_params = {
|
||||
type: :account,
|
||||
group_by: 'hour',
|
||||
metric: 'conversations_count',
|
||||
business_hours: false
|
||||
}
|
||||
|
||||
V2::ReportBuilder.new(Current.account, report_params.merge({
|
||||
since: since_timestamp(date),
|
||||
until: until_timestamp(date),
|
||||
timezone_offset: offset
|
||||
})).build
|
||||
end
|
||||
|
||||
def transform_data(data, zone_transform)
|
||||
# rubocop:disable Rails/TimeZone
|
||||
data.map do |d|
|
||||
date = zone_transform ? Time.zone.at(d[:timestamp]) : Time.at(d[:timestamp])
|
||||
{
|
||||
date: date.to_date.to_s,
|
||||
hour: date.hour,
|
||||
value: d[:value]
|
||||
}
|
||||
end
|
||||
# rubocop:enable Rails/TimeZone
|
||||
end
|
||||
|
||||
def since_timestamp(date)
|
||||
number_of_days = params[:days_before].present? ? params[:days_before].to_i.days : 6.days
|
||||
(date - number_of_days).to_i.to_s
|
||||
end
|
||||
|
||||
def until_timestamp(date)
|
||||
date.to_i.to_s
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,93 @@
|
||||
module Api::V2::Accounts::ReportsHelper
|
||||
def generate_agents_report
|
||||
reports = V2::Reports::AgentSummaryBuilder.new(
|
||||
account: Current.account,
|
||||
params: build_params(type: :agent)
|
||||
).build
|
||||
|
||||
Current.account.users.map do |agent|
|
||||
report = reports.find { |r| r[:id] == agent.id }
|
||||
[agent.name] + generate_readable_report_metrics(report)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_inboxes_report
|
||||
reports = V2::Reports::InboxSummaryBuilder.new(
|
||||
account: Current.account,
|
||||
params: build_params(type: :inbox)
|
||||
).build
|
||||
|
||||
Current.account.inboxes.map do |inbox|
|
||||
report = reports.find { |r| r[:id] == inbox.id }
|
||||
[inbox.name, inbox.channel&.name] + generate_readable_report_metrics(report)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_teams_report
|
||||
reports = V2::Reports::TeamSummaryBuilder.new(
|
||||
account: Current.account,
|
||||
params: build_params(type: :team)
|
||||
).build
|
||||
|
||||
Current.account.teams.map do |team|
|
||||
report = reports.find { |r| r[:id] == team.id }
|
||||
[team.name] + generate_readable_report_metrics(report)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_labels_report
|
||||
reports = V2::Reports::LabelSummaryBuilder.new(
|
||||
account: Current.account,
|
||||
params: build_params({})
|
||||
).build
|
||||
|
||||
reports.map do |report|
|
||||
[report[:name]] + generate_readable_report_metrics(report)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_conversations_report
|
||||
builder = V2::Reports::Conversations::MetricBuilder.new(Current.account, build_params(type: :account))
|
||||
summary = builder.summary
|
||||
|
||||
[generate_conversation_report_metrics(summary)]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_params(base_params)
|
||||
base_params.merge(
|
||||
{
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def report_builder(report_params)
|
||||
V2::ReportBuilder.new(Current.account, build_params(report_params))
|
||||
end
|
||||
|
||||
def generate_readable_report_metrics(report)
|
||||
[
|
||||
report[:conversations_count],
|
||||
Reports::TimeFormatPresenter.new(report[:avg_first_response_time]).format,
|
||||
Reports::TimeFormatPresenter.new(report[:avg_resolution_time]).format,
|
||||
Reports::TimeFormatPresenter.new(report[:avg_reply_time]).format,
|
||||
report[:resolved_conversations_count]
|
||||
]
|
||||
end
|
||||
|
||||
def generate_conversation_report_metrics(summary)
|
||||
[
|
||||
summary[:conversations_count],
|
||||
summary[:incoming_messages_count],
|
||||
summary[:outgoing_messages_count],
|
||||
Reports::TimeFormatPresenter.new(summary[:avg_first_response_time]).format,
|
||||
Reports::TimeFormatPresenter.new(summary[:avg_resolution_time]).format,
|
||||
summary[:resolutions_count],
|
||||
Reports::TimeFormatPresenter.new(summary[:reply_time]).format
|
||||
]
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user