feat: sla business hours

This commit is contained in:
Muhsin
2025-12-16 13:01:10 +05:30
parent bb8bafe3dc
commit 473dc978e0
6 changed files with 804 additions and 3 deletions
@@ -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,
});
});
});
});
});