Files
chatwoot/app/javascript/widget/components/Availability/AvailabilityText.vue
Sivin Varghese 6ca38e10e9 feat: Migrate availability mixins to composable and helper (#11596)
# Pull Request Template

## Description

**This PR includes:**

* Refactored two legacy mixins (`availability.js`,
`nextAvailability.js`) into a Vue 3 composable (`useAvailability`),
helper module and component based rendering logic.
* Fixed an issue where the widget wouldn't load if business hours were
enabled but all days were unchecked.
* Fixed translation issue
[[#11280](https://github.com/chatwoot/chatwoot/issues/11280)](https://github.com/chatwoot/chatwoot/issues/11280).
* Reduced code complexity and size.
* Added test coverage for both the composable and helper functions.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Loom video

https://www.loom.com/share/2bc3ed694b4349419505e275d14d0b98?sid=22d585e4-0dc7-4242-bcb6-e3edc16e3aee

### Story
<img width="995" height="442" alt="image"
src="https://github.com/user-attachments/assets/d6340738-07db-41d5-86fa-a8ecf734cc70"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules


Fixes https://github.com/chatwoot/chatwoot/issues/12012

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2025-08-22 00:43:34 +05:30

179 lines
4.4 KiB
Vue

<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { getTime } from 'dashboard/routes/dashboard/settings/inbox/helpers/businessHour.js';
import { findNextAvailableSlotDetails } from 'widget/helpers/availabilityHelpers';
const props = defineProps({
time: {
type: Date,
required: true,
},
utcOffset: {
type: String,
required: true,
},
workingHours: {
type: Array,
required: true,
},
workingHoursEnabled: {
type: Boolean,
required: true,
},
replyTime: {
type: String,
default: 'in_a_few_minutes',
},
isOnline: {
type: Boolean,
required: true,
},
isInWorkingHours: {
type: Boolean,
required: true,
},
});
const MINUTE_ROUNDING_INTERVAL = 5;
const HOUR_THRESHOLD_FOR_EXACT_TIME = 3;
const MINUTES_IN_HOUR = 60;
const { t } = useI18n();
const dayNames = computed(() => [
t('DAY_NAMES.SUNDAY'),
t('DAY_NAMES.MONDAY'),
t('DAY_NAMES.TUESDAY'),
t('DAY_NAMES.WEDNESDAY'),
t('DAY_NAMES.THURSDAY'),
t('DAY_NAMES.FRIDAY'),
t('DAY_NAMES.SATURDAY'),
]);
// Check if all days in working hours are closed
const allDayClosed = computed(() => {
if (!props.workingHours.length) return false;
return props.workingHours.every(slot => slot.closedAllDay);
});
const replyTimeMessage = computed(() => {
const replyTimeKey = `REPLY_TIME.${props.replyTime.toUpperCase()}`;
return t(replyTimeKey);
});
const nextSlot = computed(() => {
if (
!props.workingHoursEnabled ||
allDayClosed.value ||
(props.isInWorkingHours && props.isOnline)
) {
return null;
}
const slot = findNextAvailableSlotDetails(
props.time,
props.utcOffset,
props.workingHours
);
if (!slot) return null;
return {
...slot,
hoursUntilOpen: Math.floor(slot.minutesUntilOpen / MINUTES_IN_HOUR),
remainingMinutes: slot.minutesUntilOpen % MINUTES_IN_HOUR,
};
});
const roundedMinutesUntilOpen = computed(() => {
if (!nextSlot.value) return 0;
return (
Math.ceil(nextSlot.value.remainingMinutes / MINUTE_ROUNDING_INTERVAL) *
MINUTE_ROUNDING_INTERVAL
);
});
const adjustedHoursUntilOpen = computed(() => {
if (!nextSlot.value) return 0;
return nextSlot.value.remainingMinutes > 0
? nextSlot.value.hoursUntilOpen + 1
: nextSlot.value.hoursUntilOpen;
});
const formattedOpeningTime = computed(() => {
if (!nextSlot.value) return '';
return getTime(
nextSlot.value.config.openHour || 0,
nextSlot.value.config.openMinutes || 0
);
});
</script>
<template>
<span>
<!-- 1. If currently in working hours, show reply time -->
<template v-if="isInWorkingHours">
{{ replyTimeMessage }}
</template>
<!-- 2. Else, if working hours are disabled, show based on online status -->
<template v-else-if="!workingHoursEnabled">
{{
isOnline
? replyTimeMessage
: t('TEAM_AVAILABILITY.BACK_AS_SOON_AS_POSSIBLE')
}}
</template>
<!-- 3. Else (not in working hours, but working hours ARE enabled) -->
<!-- Check if all configured slots are 'closedAllDay' -->
<template v-else-if="allDayClosed">
{{ t('TEAM_AVAILABILITY.BACK_AS_SOON_AS_POSSIBLE') }}
</template>
<!-- 4. Else (not in WH, WH enabled, not allDayClosed), calculate next slot -->
<template v-else-if="!nextSlot">
{{ t('REPLY_TIME.BACK_IN_SOME_TIME') }}
</template>
<!-- Tomorrow -->
<template v-else-if="nextSlot.daysUntilOpen === 1">
{{ t('REPLY_TIME.BACK_TOMORROW') }}
</template>
<!-- Multiple days away (eg: on Monday) -->
<template v-else-if="nextSlot.daysUntilOpen > 1">
{{
t('REPLY_TIME.BACK_ON_DAY', {
day: dayNames[nextSlot.config.dayOfWeek],
})
}}
</template>
<!-- Same day - less than 1 hour (eg: in 5 minutes) -->
<template v-else-if="nextSlot.hoursUntilOpen === 0">
{{
t('REPLY_TIME.BACK_IN_MINUTES', {
time: `${roundedMinutesUntilOpen}`,
})
}}
</template>
<!-- Same day - less than 3 hours (eg: in 2 hours) -->
<template
v-else-if="nextSlot.hoursUntilOpen < HOUR_THRESHOLD_FOR_EXACT_TIME"
>
{{ t('REPLY_TIME.BACK_IN_HOURS', adjustedHoursUntilOpen) }}
</template>
<!-- Same day - 3+ hours away (eg: at 10:00 AM) -->
<template v-else>
{{
t('REPLY_TIME.BACK_AT_TIME', {
time: formattedOpeningTime,
})
}}
</template>
</span>
</template>