- Fixes SLA breach computation to respect the "Only during business hours" setting - Backend now pre-computes SLA deadlines, simplifying frontend logic ## How it works Before: SLA deadlines were calculated using wall-clock time, ignoring business hours. After: When an SLA policy has "Only during business hours" enabled and the inbox has working hours configured, the deadline is calculated by adding threshold time only during business hours. **How you check if a conversation has a SLA hit or miss?** <img width="474" height="510" alt="Screenshot 2026-01-28 at 7 06 53 PM" src="https://github.com/user-attachments/assets/54ec8581-18b8-45c6-a356-de8c778ea78d" /> **Example:** - Conversation created: Friday 4:30 PM - FRT threshold: 1 hour - Business hours: Mon-Fri 9 AM - 5 PM | | Breach time | |--|--| | Before | Friday 5:30 PM | | After | Monday 9:30 AM | ## Test plan - [x] Create an SLA policy with "Only during business hours" enabled - [x] Configure inbox with business hours (e.g., Mon-Fri 9-5) - [x] Conversation created during business hours - Create a conversation on Wednesday 10:00 AM UTC - Expected: FRT deadline shows Wednesday 12:00 PM UTC (2 business hours later) - [x] Conversation created before business hours - Create a conversation on Wednesday 7:00 AM UTC - Expected: FRT deadline shows Wednesday 11:00 AM UTC (counting starts at 9 AM) - [x] Conversation created after business hours - Create a conversation on Wednesday 6:00 PM UTC - Expected: FRT deadline shows Thursday 11:00 AM UTC (counting starts next day 9 AM) - [x] Conversation created on weekend - Create a conversation on Saturday 10:00 AM UTC - Expected: FRT deadline shows Monday 11:00 AM UTC (skips weekend) - [x] Threshold spans weekend - Create a conversation on Friday 4:00 PM UTC with 2-hour FRT - Expected: FRT deadline shows Monday 10:00 AM UTC (1h Friday + 1h Monday) - [x] SLA without business hours - Create an SLA policy with only_during_business_hours: false - Create a conversation on Friday 4:00 PM UTC with 2-hour FRT - Expected: FRT deadline shows Friday 6:00 PM UTC (wall-clock time) - [x] All Day marked as closed_all_day - Create a conversation on Tuesday 4:00 PM UTC with 2-hour FRT - Expected: FRT deadline shows Thursday 10:00 AM UTC - [x] All Day marked as open_all_day - Create a conversation on Saturday 10:00 AM UTC with 2-hour FRT - Expected: FRT deadline shows Saturday 12:00 PM UTC - [x] UI displays correct countdown - Verify conversation card shows correct SLA timer - Verify timer shows flame icon when breached - Verify timer shows alarm icon when within threshold - Time updates automatically when time passes - [x] Verify the breach with a different timezone than your local timezone --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sojan Jose <sojan@pepalo.com> Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
103 lines
3.6 KiB
Vue
103 lines
3.6 KiB
Vue
<script setup>
|
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
|
import { evaluateSLAStatus } from 'dashboard/helper/slaHelper';
|
|
|
|
const props = defineProps({
|
|
conversation: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
});
|
|
|
|
const REFRESH_INTERVAL = 60000;
|
|
|
|
const timer = ref(null);
|
|
const slaStatus = ref({
|
|
threshold: null,
|
|
isSlaMissed: false,
|
|
type: null,
|
|
icon: null,
|
|
});
|
|
|
|
const appliedSLA = computed(() => props.conversation?.appliedSla);
|
|
const slaEvents = computed(() => props.conversation?.slaEvents);
|
|
const isSlaMissed = computed(() => slaStatus.value?.isSlaMissed);
|
|
|
|
const hasSlaThreshold = computed(() => {
|
|
return slaStatus.value?.threshold && appliedSLA.value?.id;
|
|
});
|
|
|
|
const slaStatusText = computed(() => {
|
|
return slaStatus.value?.type?.toUpperCase();
|
|
});
|
|
|
|
const updateSlaStatus = () => {
|
|
slaStatus.value = evaluateSLAStatus({
|
|
appliedSla: appliedSLA.value || {},
|
|
chat: props.conversation,
|
|
slaEvents: slaEvents.value || [],
|
|
});
|
|
};
|
|
|
|
const createTimer = () => {
|
|
timer.value = setTimeout(() => {
|
|
updateSlaStatus();
|
|
createTimer();
|
|
}, REFRESH_INTERVAL);
|
|
};
|
|
|
|
onMounted(() => {
|
|
updateSlaStatus();
|
|
createTimer();
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (timer.value) {
|
|
clearTimeout(timer.value);
|
|
}
|
|
});
|
|
|
|
watch(() => props.conversation, updateSlaStatus);
|
|
|
|
// This expose is to provide context to the parent component, so that it can decided weather
|
|
// a new row has to be added to the conversation card or not
|
|
// SLACardLabel > CardMessagePreviewWithMeta > ConversationCard
|
|
//
|
|
// We need to do this becuase each SLA card has it's own SLA timer
|
|
// and it's just convenient to have this logic in the SLACardLabel component
|
|
// However this is a bit hacky, and we should change this in the future
|
|
//
|
|
// TODO: A better implementation would be to have the timer as a shared composable, just like the provider pattern
|
|
// we use across the next components. Have the calculation be done on the top ConversationCard component
|
|
// and then the value be injected to the SLACardLabel component
|
|
defineExpose({
|
|
hasSlaThreshold,
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex items-center min-w-fit gap-0.5 h-6">
|
|
<div class="flex items-center justify-center size-4">
|
|
<svg
|
|
width="10"
|
|
height="13"
|
|
viewBox="0 0 10 13"
|
|
fill="none"
|
|
:class="isSlaMissed ? 'fill-n-ruby-10' : 'fill-n-slate-9'"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
d="M4.55091 12.412C7.44524 12.412 9.37939 10.4571 9.37939 7.51446C9.37939 2.63072 5.21405 0.599854 2.36808 0.599854C1.81546 0.599854 1.45626 0.800176 1.45626 1.1801C1.45626 1.32516 1.52534 1.48404 1.64277 1.62219C2.27828 2.38204 2.92069 3.27314 2.93451 4.36455C2.93451 4.5925 2.9276 4.78592 2.76181 5.08295L3.05194 5.03459C2.81017 4.21949 2.18848 3.63234 1.5806 3.63234C1.32501 3.63234 1.15232 3.81884 1.15232 4.09514C1.15232 4.23331 1.19377 4.56488 1.19377 4.79974C1.19377 5.95332 0.26123 6.69935 0.26123 8.67495C0.26123 10.92 1.97434 12.412 4.55091 12.412ZM4.68906 10.8923C3.65982 10.8923 2.96905 10.2637 2.96905 9.33119C2.96905 8.3572 3.66672 8.01181 3.75652 7.38322C3.76344 7.32796 3.79107 7.31414 3.83251 7.34867C4.08809 7.57663 4.24697 7.85293 4.37822 8.1776C4.67525 7.77696 4.81341 6.9204 4.73051 6.0293C4.72361 5.97404 4.75814 5.94642 4.80649 5.96713C6.02916 6.53357 6.65085 7.74241 6.65085 8.82693C6.65085 9.92527 6.00844 10.8923 4.68906 10.8923Z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
|
|
<span
|
|
class="text-sm truncate"
|
|
:class="isSlaMissed ? 'text-n-ruby-11' : 'text-n-slate-11'"
|
|
>
|
|
{{ `${slaStatusText}: ${slaStatus.threshold}` }}
|
|
</span>
|
|
</div>
|
|
</template>
|