Initial commit: Add logistics and order_detail message types
Some checks failed
Lock Threads / action (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled

- Add Logistics component with progress tracking
- Add OrderDetail component for order information
- Support data-driven steps and actions
- Add blue color scale to widget SCSS
- Fix node overflow and progress bar rendering issues
- Add English translations for dashboard components

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Liang XJ
2026-01-26 11:16:56 +08:00
commit 092fb2e083
7646 changed files with 975643 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
require 'rails_helper'
describe V2::Reports::Timeseries::AverageReportBuilder do
subject { described_class.new(account, params) }
let(:account) { create(:account) }
let(:team) { create(:team, account: account) }
let(:inbox) { create(:inbox, account: account) }
let(:label) { create(:label, title: 'spec-billing', account: account) }
let!(:conversation) { create(:conversation, account: account, inbox: inbox, team: team) }
let(:current_time) { '26.10.2020 10:00'.to_datetime }
let(:params) do
{
type: filter_type,
business_hours: business_hours,
timezone_offset: timezone_offset,
group_by: group_by,
metric: metric,
since: (current_time - 1.week).beginning_of_day.to_i.to_s,
until: current_time.end_of_day.to_i.to_s,
id: filter_id
}
end
let(:timezone_offset) { nil }
let(:group_by) { 'day' }
let(:metric) { 'avg_first_response_time' }
let(:business_hours) { false }
let(:filter_type) { :account }
let(:filter_id) { '' }
before do
travel_to current_time
conversation.label_list.add(label.title)
conversation.save!
create(:reporting_event, name: 'first_response', value: 80, value_in_business_hours: 10, account: account, created_at: Time.zone.now,
conversation: conversation, inbox: inbox)
create(:reporting_event, name: 'first_response', value: 100, value_in_business_hours: 20, account: account, created_at: 1.hour.ago)
create(:reporting_event, name: 'first_response', value: 93, value_in_business_hours: 30, account: account, created_at: 1.week.ago)
end
describe '#timeseries' do
context 'when there is no filter applied' do
it 'returns the correct values' do
timeseries_values = subject.timeseries
expect(timeseries_values).to eq(
[
{ count: 1, timestamp: 1_603_065_600, value: 93.0 },
{ count: 0, timestamp: 1_603_152_000, value: 0 },
{ count: 0, timestamp: 1_603_238_400, value: 0 },
{ count: 0, timestamp: 1_603_324_800, value: 0 },
{ count: 0, timestamp: 1_603_411_200, value: 0 },
{ count: 0, timestamp: 1_603_497_600, value: 0 },
{ count: 0, timestamp: 1_603_584_000, value: 0 },
{ count: 2, timestamp: 1_603_670_400, value: 90.0 }
]
)
end
context 'when business hours is provided' do
let(:business_hours) { true }
it 'returns correct timeseries' do
timeseries_values = subject.timeseries
expect(timeseries_values).to eq(
[
{ count: 1, timestamp: 1_603_065_600, value: 30.0 },
{ count: 0, timestamp: 1_603_152_000, value: 0 },
{ count: 0, timestamp: 1_603_238_400, value: 0 },
{ count: 0, timestamp: 1_603_324_800, value: 0 },
{ count: 0, timestamp: 1_603_411_200, value: 0 },
{ count: 0, timestamp: 1_603_497_600, value: 0 },
{ count: 0, timestamp: 1_603_584_000, value: 0 },
{ count: 2, timestamp: 1_603_670_400, value: 15.0 }
]
)
end
end
context 'when group_by is provided' do
let(:group_by) { 'week' }
it 'returns correct timeseries' do
timeseries_values = subject.timeseries
expect(timeseries_values).to eq(
[
{ count: 1, timestamp: (current_time - 1.week).beginning_of_week(:sunday).to_i, value: 93.0 },
{ count: 2, timestamp: current_time.beginning_of_week(:sunday).to_i, value: 90.0 }
]
)
end
end
context 'when timezone offset is provided' do
let(:timezone_offset) { '5.5' }
let(:group_by) { 'week' }
it 'returns correct timeseries' do
timeseries_values = subject.timeseries
expect(timeseries_values).to eq(
[
{ count: 1, timestamp: (current_time - 1.week).in_time_zone('Chennai').beginning_of_week(:sunday).to_i, value: 93.0 },
{ count: 2, timestamp: current_time.in_time_zone('Chennai').beginning_of_week(:sunday).to_i, value: 90.0 }
]
)
end
end
end
context 'when the label filter is applied' do
let(:group_by) { 'week' }
let(:filter_type) { 'label' }
let(:filter_id) { label.id }
it 'returns correct timeseries' do
timeseries_values = subject.timeseries
start_of_the_week = current_time.beginning_of_week(:sunday).to_i
last_week_start_of_the_week = (current_time - 1.week).beginning_of_week(:sunday).to_i
expect(timeseries_values).to eq(
[
{ count: 0, timestamp: last_week_start_of_the_week, value: 0 },
{ count: 1, timestamp: start_of_the_week, value: 80.0 }
]
)
end
end
context 'when the inbox filter is applied' do
let(:group_by) { 'week' }
let(:filter_type) { 'inbox' }
let(:filter_id) { inbox.id }
it 'returns correct timeseries' do
timeseries_values = subject.timeseries
start_of_the_week = current_time.beginning_of_week(:sunday).to_i
last_week_start_of_the_week = (current_time - 1.week).beginning_of_week(:sunday).to_i
expect(timeseries_values).to eq(
[
{ count: 0, timestamp: last_week_start_of_the_week, value: 0 },
{ count: 1, timestamp: start_of_the_week, value: 80.0 }
]
)
end
end
context 'when the team filter is applied' do
let(:group_by) { 'week' }
let(:filter_type) { 'team' }
let(:filter_id) { team.id }
it 'returns correct timeseries' do
timeseries_values = subject.timeseries
start_of_the_week = current_time.beginning_of_week(:sunday).to_i
last_week_start_of_the_week = (current_time - 1.week).beginning_of_week(:sunday).to_i
expect(timeseries_values).to eq(
[
{ count: 0, timestamp: last_week_start_of_the_week, value: 0 },
{ count: 1, timestamp: start_of_the_week, value: 80.0 }
]
)
end
end
end
describe '#aggregate_value' do
context 'when there is no filter applied' do
it 'returns the correct average value' do
expect(subject.aggregate_value).to eq 91.0
end
end
end
end

View File

@@ -0,0 +1,113 @@
require 'rails_helper'
describe V2::Reports::Timeseries::CountReportBuilder do
subject { described_class.new(account, params) }
let(:account) { create(:account) }
let(:account2) { create(:account) }
let(:user) { create(:user, email: 'agent1@example.com') }
let(:inbox) { create(:inbox, account: account) }
let(:inbox2) { create(:inbox, account: account2) }
let(:current_time) { Time.current }
let(:params) do
{
type: 'agent',
metric: 'resolutions_count',
since: (current_time - 1.day).beginning_of_day.to_i.to_s,
until: current_time.end_of_day.to_i.to_s,
id: user.id.to_s
}
end
before do
travel_to current_time
# Add the same user to both accounts
create(:account_user, account: account, user: user)
create(:account_user, account: account2, user: user)
# Create conversations in account1
conversation1 = create(:conversation, account: account, inbox: inbox, assignee: user)
conversation2 = create(:conversation, account: account, inbox: inbox, assignee: user)
# Create conversations in account2
conversation3 = create(:conversation, account: account2, inbox: inbox2, assignee: user)
conversation4 = create(:conversation, account: account2, inbox: inbox2, assignee: user)
# User resolves 2 conversations in account1
create(:reporting_event,
name: 'conversation_resolved',
account: account,
user: user,
conversation: conversation1,
created_at: current_time - 12.hours)
create(:reporting_event,
name: 'conversation_resolved',
account: account,
user: user,
conversation: conversation2,
created_at: current_time - 6.hours)
# Same user resolves 3 conversations in account2 - these should NOT be counted for account1
create(:reporting_event,
name: 'conversation_resolved',
account: account2,
user: user,
conversation: conversation3,
created_at: current_time - 8.hours)
create(:reporting_event,
name: 'conversation_resolved',
account: account2,
user: user,
conversation: conversation4,
created_at: current_time - 4.hours)
# Create another conversation in account2 for testing
conversation5 = create(:conversation, account: account2, inbox: inbox2, assignee: user)
create(:reporting_event,
name: 'conversation_resolved',
account: account2,
user: user,
conversation: conversation5,
created_at: current_time - 2.hours)
end
describe '#aggregate_value' do
it 'returns only resolutions performed by the user in the specified account' do
# User should have 2 resolutions in account1, not 5 (total across both accounts)
expect(subject.aggregate_value).to eq(2)
end
context 'when querying account2' do
subject { described_class.new(account2, params) }
it 'returns only resolutions for account2' do
# User should have 3 resolutions in account2
expect(subject.aggregate_value).to eq(3)
end
end
end
describe '#timeseries' do
it 'filters resolutions by account' do
result = subject.timeseries
# Should only count the 2 resolutions from account1
total_count = result.sum { |r| r[:value] }
expect(total_count).to eq(2)
end
end
describe 'account isolation' do
it 'does not leak data between accounts' do
# If account isolation works correctly, the counts should be different
account1_count = described_class.new(account, params).aggregate_value
account2_count = described_class.new(account2, params).aggregate_value
expect(account1_count).to eq(2)
expect(account2_count).to eq(3)
end
end
end