feat: sla business hours
This commit is contained in:
+9
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { evaluateSLAStatus } from '@chatwoot/utils';
|
||||
import { getBusinessHoursConfig } from 'dashboard/helper/slaHelper';
|
||||
|
||||
const props = defineProps({
|
||||
conversation: {
|
||||
@@ -40,9 +41,17 @@ const slaStatusText = computed(() => {
|
||||
});
|
||||
|
||||
const updateSlaStatus = () => {
|
||||
const businessHoursConfig = getBusinessHoursConfig(
|
||||
appliedSLA.value?.slaPolicy,
|
||||
props.conversation?.inbox
|
||||
);
|
||||
|
||||
slaStatus.value = evaluateSLAStatus({
|
||||
appliedSla: convertObjectCamelCaseToSnakeCase(appliedSLA.value || {}),
|
||||
chat: props.conversation,
|
||||
options: businessHoursConfig
|
||||
? { businessHours: businessHoursConfig }
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { evaluateSLAStatus } from '@chatwoot/utils';
|
||||
import { getBusinessHoursConfig } from 'dashboard/helper/slaHelper';
|
||||
import SLAPopoverCard from './SLAPopoverCard.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -58,9 +59,17 @@ const groupClass = computed(() => {
|
||||
});
|
||||
|
||||
const updateSlaStatus = () => {
|
||||
const businessHoursConfig = getBusinessHoursConfig(
|
||||
appliedSLA.value?.sla_policy,
|
||||
props.chat?.inbox
|
||||
);
|
||||
|
||||
slaStatus.value = evaluateSLAStatus({
|
||||
appliedSla: appliedSLA.value,
|
||||
chat: props.chat,
|
||||
options: businessHoursConfig
|
||||
? { businessHours: businessHoursConfig }
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Helper functions for SLA business hours configuration
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extracts business hours configuration from SLA policy and inbox data
|
||||
* Supports both camelCase and snake_case property naming conventions
|
||||
*
|
||||
* @param {Object} slaPolicy - The SLA policy object
|
||||
* @param {Object} inbox - The inbox object with working hours configuration
|
||||
* @returns {Object|null} Business hours configuration for utils package, or null if not applicable
|
||||
*/
|
||||
export const getBusinessHoursConfig = (slaPolicy, inbox) => {
|
||||
// Handle both camelCase and snake_case property names
|
||||
const onlyDuringBusinessHours =
|
||||
slaPolicy?.only_during_business_hours ?? slaPolicy?.onlyDuringBusinessHours;
|
||||
const workingHoursEnabled =
|
||||
inbox?.working_hours_enabled ?? inbox?.workingHoursEnabled;
|
||||
|
||||
if (!onlyDuringBusinessHours || !workingHoursEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert working hours format to match utils expectation
|
||||
const workingHours = {};
|
||||
const dayMap = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
||||
|
||||
// Handle both camelCase and snake_case working hours arrays
|
||||
const inboxWorkingHours = inbox.working_hours ?? inbox.workingHours;
|
||||
|
||||
if (inboxWorkingHours) {
|
||||
inboxWorkingHours.forEach(dayConfig => {
|
||||
// Handle both property naming conventions
|
||||
const dayOfWeek = dayConfig.day_of_week ?? dayConfig.dayOfWeek;
|
||||
const closedAllDay = dayConfig.closed_all_day ?? dayConfig.closedAllDay;
|
||||
const openHour = dayConfig.open_hour ?? dayConfig.openHour;
|
||||
const openMinutes = dayConfig.open_minutes ?? dayConfig.openMinutes;
|
||||
const closeHour = dayConfig.close_hour ?? dayConfig.closeHour;
|
||||
const closeMinutes = dayConfig.close_minutes ?? dayConfig.closeMinutes;
|
||||
|
||||
const dayName = dayMap[dayOfWeek];
|
||||
|
||||
if (closedAllDay) {
|
||||
workingHours[dayName] = null;
|
||||
} else {
|
||||
// Convert to HH:MM format
|
||||
const startHour = String(openHour || 0).padStart(2, '0');
|
||||
const startMin = String(openMinutes || 0).padStart(2, '0');
|
||||
const endHour = String(closeHour || 0).padStart(2, '0');
|
||||
const endMin = String(closeMinutes || 0).padStart(2, '0');
|
||||
|
||||
workingHours[dayName] = {
|
||||
start: `${startHour}:${startMin}`,
|
||||
finish: `${endHour}:${endMin}`,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
working_hours_enabled: workingHoursEnabled,
|
||||
timezone: inbox.timezone || 'UTC',
|
||||
working_hours: workingHours,
|
||||
only_during_business_hours: onlyDuringBusinessHours,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,386 @@
|
||||
import { getBusinessHoursConfig } from '../slaHelper';
|
||||
|
||||
describe('slaHelper', () => {
|
||||
describe('getBusinessHoursConfig', () => {
|
||||
const mockWorkingHours = [
|
||||
{
|
||||
day_of_week: 1, // Monday
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 30,
|
||||
closed_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 2, // Tuesday
|
||||
open_hour: 8,
|
||||
open_minutes: 30,
|
||||
close_hour: 18,
|
||||
close_minutes: 0,
|
||||
closed_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 0, // Sunday
|
||||
closed_all_day: true,
|
||||
},
|
||||
];
|
||||
|
||||
const mockWorkingHoursCamelCase = [
|
||||
{
|
||||
dayOfWeek: 1, // Monday
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 30,
|
||||
closedAllDay: false,
|
||||
},
|
||||
{
|
||||
dayOfWeek: 2, // Tuesday
|
||||
openHour: 8,
|
||||
openMinutes: 30,
|
||||
closeHour: 18,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: false,
|
||||
},
|
||||
{
|
||||
dayOfWeek: 0, // Sunday
|
||||
closedAllDay: true,
|
||||
},
|
||||
];
|
||||
|
||||
describe('when business hours are not required', () => {
|
||||
it('returns null when SLA policy does not require business hours', () => {
|
||||
const slaPolicy = { only_during_business_hours: false };
|
||||
const inbox = {
|
||||
working_hours_enabled: true,
|
||||
working_hours: mockWorkingHours,
|
||||
};
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, inbox);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when inbox does not have working hours enabled', () => {
|
||||
const slaPolicy = { only_during_business_hours: true };
|
||||
const inbox = {
|
||||
working_hours_enabled: false,
|
||||
working_hours: mockWorkingHours,
|
||||
};
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, inbox);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when both conditions are false', () => {
|
||||
const slaPolicy = { only_during_business_hours: false };
|
||||
const inbox = { working_hours_enabled: false };
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, inbox);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when slaPolicy is null', () => {
|
||||
const inbox = {
|
||||
working_hours_enabled: true,
|
||||
working_hours: mockWorkingHours,
|
||||
};
|
||||
|
||||
const result = getBusinessHoursConfig(null, inbox);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when inbox is null', () => {
|
||||
const slaPolicy = { only_during_business_hours: true };
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, null);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when business hours are required - snake_case properties', () => {
|
||||
it('converts working hours correctly with snake_case properties', () => {
|
||||
const slaPolicy = { only_during_business_hours: true };
|
||||
const inbox = {
|
||||
working_hours_enabled: true,
|
||||
working_hours: mockWorkingHours,
|
||||
timezone: 'America/New_York',
|
||||
};
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, inbox);
|
||||
|
||||
expect(result).toEqual({
|
||||
working_hours_enabled: true,
|
||||
timezone: 'America/New_York',
|
||||
working_hours: {
|
||||
sun: null, // Closed all day
|
||||
mon: {
|
||||
start: '09:00',
|
||||
finish: '17:30',
|
||||
},
|
||||
tue: {
|
||||
start: '08:30',
|
||||
finish: '18:00',
|
||||
},
|
||||
},
|
||||
only_during_business_hours: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles missing timezone by defaulting to UTC', () => {
|
||||
const slaPolicy = { only_during_business_hours: true };
|
||||
const inbox = {
|
||||
working_hours_enabled: true,
|
||||
working_hours: mockWorkingHours.slice(0, 1), // Just Monday
|
||||
};
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, inbox);
|
||||
|
||||
expect(result.timezone).toBe('UTC');
|
||||
});
|
||||
|
||||
it('handles zero hours and minutes correctly', () => {
|
||||
const workingHoursWithZeros = [
|
||||
{
|
||||
day_of_week: 1,
|
||||
open_hour: 0,
|
||||
open_minutes: 0,
|
||||
close_hour: 0,
|
||||
close_minutes: 0,
|
||||
closed_all_day: false,
|
||||
},
|
||||
];
|
||||
|
||||
const slaPolicy = { only_during_business_hours: true };
|
||||
const inbox = {
|
||||
working_hours_enabled: true,
|
||||
working_hours: workingHoursWithZeros,
|
||||
};
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, inbox);
|
||||
|
||||
expect(result.working_hours.mon).toEqual({
|
||||
start: '00:00',
|
||||
finish: '00:00',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when business hours are required - camelCase properties', () => {
|
||||
it('converts working hours correctly with camelCase properties', () => {
|
||||
const slaPolicy = { onlyDuringBusinessHours: true };
|
||||
const inbox = {
|
||||
workingHoursEnabled: true,
|
||||
workingHours: mockWorkingHoursCamelCase,
|
||||
timezone: 'America/Los_Angeles',
|
||||
};
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, inbox);
|
||||
|
||||
expect(result).toEqual({
|
||||
working_hours_enabled: true,
|
||||
timezone: 'America/Los_Angeles',
|
||||
working_hours: {
|
||||
sun: null, // Closed all day
|
||||
mon: {
|
||||
start: '09:00',
|
||||
finish: '17:30',
|
||||
},
|
||||
tue: {
|
||||
start: '08:30',
|
||||
finish: '18:00',
|
||||
},
|
||||
},
|
||||
only_during_business_hours: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mixed property naming conventions', () => {
|
||||
it('handles mixed snake_case and camelCase properties', () => {
|
||||
const slaPolicy = {
|
||||
only_during_business_hours: true, // snake_case
|
||||
onlyDuringBusinessHours: false, // This should be ignored due to nullish coalescing
|
||||
};
|
||||
const inbox = {
|
||||
working_hours_enabled: true, // snake_case
|
||||
workingHours: mockWorkingHoursCamelCase, // camelCase
|
||||
timezone: 'UTC',
|
||||
};
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, inbox);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.only_during_business_hours).toBe(true);
|
||||
expect(result.working_hours_enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('prioritizes snake_case over camelCase when both exist', () => {
|
||||
const slaPolicy = {
|
||||
only_during_business_hours: true,
|
||||
onlyDuringBusinessHours: false,
|
||||
};
|
||||
const inbox = {
|
||||
working_hours_enabled: true,
|
||||
workingHoursEnabled: false,
|
||||
working_hours: mockWorkingHours,
|
||||
workingHours: mockWorkingHoursCamelCase,
|
||||
};
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, inbox);
|
||||
|
||||
expect(result.only_during_business_hours).toBe(true);
|
||||
expect(result.working_hours_enabled).toBe(true);
|
||||
// Should use snake_case working_hours
|
||||
expect(result.working_hours.mon.start).toBe('09:00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles empty working hours array', () => {
|
||||
const slaPolicy = { only_during_business_hours: true };
|
||||
const inbox = {
|
||||
working_hours_enabled: true,
|
||||
working_hours: [],
|
||||
};
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, inbox);
|
||||
|
||||
expect(result.working_hours).toEqual({});
|
||||
});
|
||||
|
||||
it('handles missing working hours property', () => {
|
||||
const slaPolicy = { only_during_business_hours: true };
|
||||
const inbox = {
|
||||
working_hours_enabled: true,
|
||||
// No working_hours property
|
||||
};
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, inbox);
|
||||
|
||||
expect(result.working_hours).toEqual({});
|
||||
});
|
||||
|
||||
it('handles undefined values in working hours', () => {
|
||||
const workingHoursWithUndefined = [
|
||||
{
|
||||
day_of_week: 1,
|
||||
open_hour: undefined,
|
||||
open_minutes: undefined,
|
||||
close_hour: undefined,
|
||||
close_minutes: undefined,
|
||||
closed_all_day: false,
|
||||
},
|
||||
];
|
||||
|
||||
const slaPolicy = { only_during_business_hours: true };
|
||||
const inbox = {
|
||||
working_hours_enabled: true,
|
||||
working_hours: workingHoursWithUndefined,
|
||||
};
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, inbox);
|
||||
|
||||
expect(result.working_hours.mon).toEqual({
|
||||
start: '00:00',
|
||||
finish: '00:00',
|
||||
});
|
||||
});
|
||||
|
||||
it('pads single digit hours and minutes correctly', () => {
|
||||
const workingHoursSingleDigit = [
|
||||
{
|
||||
day_of_week: 3, // Wednesday
|
||||
open_hour: 9,
|
||||
open_minutes: 5,
|
||||
close_hour: 5,
|
||||
close_minutes: 0,
|
||||
closed_all_day: false,
|
||||
},
|
||||
];
|
||||
|
||||
const slaPolicy = { only_during_business_hours: true };
|
||||
const inbox = {
|
||||
working_hours_enabled: true,
|
||||
working_hours: workingHoursSingleDigit,
|
||||
};
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, inbox);
|
||||
|
||||
expect(result.working_hours.wed).toEqual({
|
||||
start: '09:05',
|
||||
finish: '05:00',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles all days of the week correctly', () => {
|
||||
const fullWeekWorkingHours = [
|
||||
{ day_of_week: 0, closed_all_day: true }, // Sunday
|
||||
{
|
||||
day_of_week: 1,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
closed_all_day: false,
|
||||
}, // Monday
|
||||
{
|
||||
day_of_week: 2,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
closed_all_day: false,
|
||||
}, // Tuesday
|
||||
{
|
||||
day_of_week: 3,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
closed_all_day: false,
|
||||
}, // Wednesday
|
||||
{
|
||||
day_of_week: 4,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
closed_all_day: false,
|
||||
}, // Thursday
|
||||
{
|
||||
day_of_week: 5,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
closed_all_day: false,
|
||||
}, // Friday
|
||||
{ day_of_week: 6, closed_all_day: true }, // Saturday
|
||||
];
|
||||
|
||||
const slaPolicy = { only_during_business_hours: true };
|
||||
const inbox = {
|
||||
working_hours_enabled: true,
|
||||
working_hours: fullWeekWorkingHours,
|
||||
};
|
||||
|
||||
const result = getBusinessHoursConfig(slaPolicy, inbox);
|
||||
|
||||
expect(result.working_hours).toEqual({
|
||||
sun: null,
|
||||
mon: { start: '09:00', finish: '17:00' },
|
||||
tue: { start: '09:00', finish: '17:00' },
|
||||
wed: { start: '09:00', finish: '17:00' },
|
||||
thu: { start: '09:00', finish: '17:00' },
|
||||
fri: { start: '09:00', finish: '17:00' },
|
||||
sat: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user