Merge branch 'develop' into feat/CW-5624
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useCaptain } from 'dashboard/composables/useCaptain';
|
||||
import { format } from 'date-fns';
|
||||
import sessionStorage from 'shared/helpers/sessionStorage';
|
||||
|
||||
import BillingMeter from './components/BillingMeter.vue';
|
||||
import BillingCard from './components/BillingCard.vue';
|
||||
@@ -13,7 +15,8 @@ import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import ButtonV4 from 'next/button/Button.vue';
|
||||
|
||||
const { currentAccount } = useAccount();
|
||||
const router = useRouter();
|
||||
const { currentAccount, isOnChatwootCloud } = useAccount();
|
||||
const {
|
||||
captainEnabled,
|
||||
captainLimits,
|
||||
@@ -24,6 +27,12 @@ const {
|
||||
|
||||
const uiFlags = useMapGetter('accounts/getUIFlags');
|
||||
const store = useStore();
|
||||
|
||||
const BILLING_REFRESH_ATTEMPTED = 'billing_refresh_attempted';
|
||||
|
||||
// State for handling refresh attempts and loading
|
||||
const isWaitingForBilling = ref(false);
|
||||
|
||||
const customAttributes = computed(() => {
|
||||
return currentAccount.value.custom_attributes || {};
|
||||
});
|
||||
@@ -61,11 +70,45 @@ const hasABillingPlan = computed(() => {
|
||||
|
||||
const fetchAccountDetails = async () => {
|
||||
if (!hasABillingPlan.value) {
|
||||
store.dispatch('accounts/subscription');
|
||||
await store.dispatch('accounts/subscription');
|
||||
fetchLimits();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBillingPageLogic = async () => {
|
||||
// If self-hosted, redirect to dashboard
|
||||
if (!isOnChatwootCloud.value) {
|
||||
router.push({ name: 'home' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we've already attempted a refresh for billing setup
|
||||
const billingRefreshAttempted = sessionStorage.get(BILLING_REFRESH_ATTEMPTED);
|
||||
|
||||
// If cloud user, fetch account details first
|
||||
await fetchAccountDetails();
|
||||
|
||||
// If still no billing plan after fetch
|
||||
if (!hasABillingPlan.value) {
|
||||
// If we haven't attempted refresh yet, do it once
|
||||
if (!billingRefreshAttempted) {
|
||||
isWaitingForBilling.value = true;
|
||||
sessionStorage.set(BILLING_REFRESH_ATTEMPTED, true);
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 5000);
|
||||
} else {
|
||||
// We've already tried refreshing, so just show the no billing message
|
||||
// Clear the flag for future visits
|
||||
sessionStorage.remove(BILLING_REFRESH_ATTEMPTED);
|
||||
}
|
||||
} else {
|
||||
// Billing plan found, clear any existing refresh flag
|
||||
sessionStorage.remove(BILLING_REFRESH_ATTEMPTED);
|
||||
}
|
||||
};
|
||||
|
||||
const onClickBillingPortal = () => {
|
||||
store.dispatch('accounts/checkout');
|
||||
};
|
||||
@@ -76,14 +119,18 @@ const onToggleChatWindow = () => {
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(fetchAccountDetails);
|
||||
onMounted(handleBillingPageLogic);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.isFetchingItem"
|
||||
:loading-message="$t('ATTRIBUTES_MGMT.LOADING')"
|
||||
:no-records-found="!hasABillingPlan"
|
||||
:is-loading="uiFlags.isFetchingItem || isWaitingForBilling"
|
||||
:loading-message="
|
||||
isWaitingForBilling
|
||||
? $t('BILLING_SETTINGS.NO_BILLING_USER')
|
||||
: $t('ATTRIBUTES_MGMT.LOADING')
|
||||
"
|
||||
:no-records-found="!hasABillingPlan && !isWaitingForBilling"
|
||||
:no-records-message="$t('BILLING_SETTINGS.NO_BILLING_USER')"
|
||||
>
|
||||
<template #header>
|
||||
|
||||
@@ -96,11 +96,12 @@ export default {
|
||||
return parse(this.toTime, 'hh:mm a', new Date());
|
||||
},
|
||||
totalHours() {
|
||||
if (this.timeSlot.openAllDay) {
|
||||
return 24;
|
||||
}
|
||||
const totalHours = differenceInMinutes(this.toDate, this.fromDate) / 60;
|
||||
return totalHours;
|
||||
if (this.timeSlot.openAllDay) return '24h';
|
||||
|
||||
const totalMinutes = differenceInMinutes(this.toDate, this.fromDate);
|
||||
const [h, m] = [Math.floor(totalMinutes / 60), totalMinutes % 60];
|
||||
|
||||
return [h && `${h}h`, m && `${m}m`].filter(Boolean).join(' ') || '0m';
|
||||
},
|
||||
hasError() {
|
||||
return !this.timeSlot.valid;
|
||||
@@ -211,7 +212,7 @@ export default {
|
||||
v-if="isDayEnabled && !hasError"
|
||||
class="label bg-n-brand/10 dark:bg-n-brand/30 text-n-blue-text text-xs inline-block px-2 py-1 rounded-lg cursor-default whitespace-nowrap"
|
||||
>
|
||||
{{ totalHours }} {{ $t('INBOX_MGMT.BUSINESS_HOURS.DAY.HOURS') }}
|
||||
{{ totalHours }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,6 +53,7 @@ export const generateTimeSlots = (step = 15) => {
|
||||
Generates a list of time strings from 12:00 AM to next 24 hours. Each new string
|
||||
will be generated by adding `step` minutes to the previous one.
|
||||
The list is generated by starting with a random day and adding step minutes till end of the same day.
|
||||
Always includes 11:59 PM as the final slot to complete the day.
|
||||
*/
|
||||
const date = new Date(1970, 1, 1);
|
||||
const slots = [];
|
||||
@@ -66,6 +67,13 @@ export const generateTimeSlots = (step = 15) => {
|
||||
);
|
||||
date.setMinutes(date.getMinutes() + step);
|
||||
}
|
||||
|
||||
// Always add 11:59 PM as the final slot if it's not already included
|
||||
const lastSlot = '11:59 PM';
|
||||
if (!slots.includes(lastSlot)) {
|
||||
slots.push(lastSlot);
|
||||
}
|
||||
|
||||
return slots;
|
||||
};
|
||||
|
||||
|
||||
+55
-3
@@ -7,10 +7,19 @@ import {
|
||||
} from '../businessHour';
|
||||
|
||||
describe('#generateTimeSlots', () => {
|
||||
it('returns correct number of time slots', () => {
|
||||
expect(generateTimeSlots(15).length).toStrictEqual((60 / 15) * 24);
|
||||
it('returns correct number of time slots for 15-minute intervals', () => {
|
||||
const slots = generateTimeSlots(15);
|
||||
// 24 hours * 4 slots per hour + 1 for 11:59 PM = 97 slots
|
||||
expect(slots.length).toStrictEqual(97);
|
||||
});
|
||||
it('returns correct time slots', () => {
|
||||
|
||||
it('returns correct number of time slots for 30-minute intervals', () => {
|
||||
const slots = generateTimeSlots(30);
|
||||
// 24 hours * 2 slots per hour + 1 for 11:59 PM = 49 slots
|
||||
expect(slots.length).toStrictEqual(49);
|
||||
});
|
||||
|
||||
it('returns correct time slots for 4-hour intervals', () => {
|
||||
expect(generateTimeSlots(240)).toStrictEqual([
|
||||
'12:00 AM',
|
||||
'04:00 AM',
|
||||
@@ -18,8 +27,51 @@ describe('#generateTimeSlots', () => {
|
||||
'12:00 PM',
|
||||
'04:00 PM',
|
||||
'08:00 PM',
|
||||
'11:59 PM',
|
||||
]);
|
||||
});
|
||||
|
||||
it('always starts with 12:00 AM', () => {
|
||||
expect(generateTimeSlots(15)[0]).toStrictEqual('12:00 AM');
|
||||
expect(generateTimeSlots(30)[0]).toStrictEqual('12:00 AM');
|
||||
expect(generateTimeSlots(60)[0]).toStrictEqual('12:00 AM');
|
||||
});
|
||||
|
||||
it('always ends with 11:59 PM', () => {
|
||||
const slots15 = generateTimeSlots(15);
|
||||
const slots30 = generateTimeSlots(30);
|
||||
const slots60 = generateTimeSlots(60);
|
||||
|
||||
expect(slots15[slots15.length - 1]).toStrictEqual('11:59 PM');
|
||||
expect(slots30[slots30.length - 1]).toStrictEqual('11:59 PM');
|
||||
expect(slots60[slots60.length - 1]).toStrictEqual('11:59 PM');
|
||||
});
|
||||
|
||||
it('includes 11:59 PM even when it would not be in regular intervals', () => {
|
||||
const slots = generateTimeSlots(30);
|
||||
expect(slots).toContain('11:59 PM');
|
||||
expect(slots).toContain('11:30 PM'); // Regular interval
|
||||
});
|
||||
|
||||
it('does not duplicate 11:59 PM if it already exists in regular intervals', () => {
|
||||
// Test with a step that would naturally include 11:59 PM
|
||||
const slots = generateTimeSlots(1); // 1-minute intervals
|
||||
const count11_59 = slots.filter(slot => slot === '11:59 PM').length;
|
||||
expect(count11_59).toStrictEqual(1);
|
||||
});
|
||||
|
||||
it('generates correct time format', () => {
|
||||
const slots = generateTimeSlots(60);
|
||||
expect(slots).toContain('01:00 AM');
|
||||
expect(slots).toContain('12:00 PM');
|
||||
expect(slots).toContain('01:00 PM');
|
||||
expect(slots).toContain('11:00 PM');
|
||||
});
|
||||
|
||||
it('handles edge case with very large step', () => {
|
||||
const slots = generateTimeSlots(1440); // 24 hours
|
||||
expect(slots).toStrictEqual(['12:00 AM', '11:59 PM']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getTime', () => {
|
||||
|
||||
Reference in New Issue
Block a user