Initial commit: Add logistics and order_detail message types
Some checks failed
Lock Threads / action (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled
Some checks failed
Lock Threads / action (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled
- Add Logistics component with progress tracking - Add OrderDetail component for order information - Support data-driven steps and actions - Add blue color scale to widget SCSS - Fix node overflow and progress bar rendering issues - Add English translations for dashboard components Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import { computed, toRef } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
import GroupedAvatars from 'widget/components/GroupedAvatars.vue';
|
||||
import AvailabilityText from './AvailabilityText.vue';
|
||||
import { useAvailability } from 'widget/composables/useAvailability';
|
||||
|
||||
const props = defineProps({
|
||||
agents: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showAvatars: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
textClasses: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const availableMessage = useMapGetter('appConfig/getAvailableMessage');
|
||||
const unavailableMessage = useMapGetter('appConfig/getUnavailableMessage');
|
||||
|
||||
// Pass toRef(props, 'agents') instead of props.agents to maintain reactivity
|
||||
// when the parent component's agents prop updates (e.g., after API response)
|
||||
const {
|
||||
currentTime,
|
||||
hasOnlineAgents,
|
||||
isOnline,
|
||||
inboxConfig,
|
||||
isInWorkingHours,
|
||||
} = useAvailability(toRef(props, 'agents'));
|
||||
|
||||
const workingHours = computed(() => inboxConfig.value.workingHours || []);
|
||||
const workingHoursEnabled = computed(
|
||||
() => inboxConfig.value.workingHoursEnabled || false
|
||||
);
|
||||
const utcOffset = computed(
|
||||
() => inboxConfig.value.utcOffset || inboxConfig.value.timezone || 'UTC'
|
||||
);
|
||||
const replyTime = computed(
|
||||
() => inboxConfig.value.replyTime || 'in_a_few_minutes'
|
||||
);
|
||||
|
||||
// If online or in working hours
|
||||
const isAvailable = computed(
|
||||
() => isOnline.value || (workingHoursEnabled.value && isInWorkingHours.value)
|
||||
);
|
||||
|
||||
const headerText = computed(() =>
|
||||
isAvailable.value
|
||||
? availableMessage.value || t('TEAM_AVAILABILITY.ONLINE')
|
||||
: unavailableMessage.value || t('TEAM_AVAILABILITY.OFFLINE')
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div v-if="showHeader" class="font-medium text-n-slate-12">
|
||||
{{ headerText }}
|
||||
</div>
|
||||
|
||||
<AvailabilityText
|
||||
:time="currentTime"
|
||||
:utc-offset="utcOffset"
|
||||
:working-hours="workingHours"
|
||||
:working-hours-enabled="workingHoursEnabled"
|
||||
:has-online-agents="hasOnlineAgents"
|
||||
:reply-time="replyTime"
|
||||
:is-online="isOnline"
|
||||
:is-in-working-hours="isInWorkingHours"
|
||||
:class="textClasses"
|
||||
class="text-n-slate-11"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<GroupedAvatars v-if="showAvatars && isOnline" :users="agents" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,217 @@
|
||||
<script setup>
|
||||
import AvailabilityText from './AvailabilityText.vue';
|
||||
|
||||
// Base time for consistent testing: Monday, July 15, 2024, 10:00:00 UTC
|
||||
const baseTime = new Date('2024-07-15T10:00:00.000Z');
|
||||
const utcOffset = '+00:00'; // UTC
|
||||
|
||||
const defaultProps = {
|
||||
time: baseTime,
|
||||
utcOffset,
|
||||
workingHours: [
|
||||
{
|
||||
dayOfWeek: 0,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: false,
|
||||
}, // Sunday
|
||||
{
|
||||
dayOfWeek: 1,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: false,
|
||||
}, // Monday (current day)
|
||||
{
|
||||
dayOfWeek: 2,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: false,
|
||||
}, // Tuesday
|
||||
{
|
||||
dayOfWeek: 3,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: false,
|
||||
}, // Wednesday
|
||||
{
|
||||
dayOfWeek: 4,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: false,
|
||||
}, // Thursday
|
||||
{
|
||||
dayOfWeek: 5,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: false,
|
||||
}, // Friday
|
||||
{
|
||||
dayOfWeek: 6,
|
||||
openHour: 9,
|
||||
openMinutes: 0,
|
||||
closeHour: 17,
|
||||
closeMinutes: 0,
|
||||
closedAllDay: true,
|
||||
}, // Saturday (closed)
|
||||
],
|
||||
workingHoursEnabled: true,
|
||||
replyTime: 'in_a_few_minutes',
|
||||
isOnline: true,
|
||||
isInWorkingHours: true,
|
||||
};
|
||||
|
||||
const createVariant = (
|
||||
title,
|
||||
propsOverride = {},
|
||||
isOnlineOverride = null,
|
||||
isInWorkingHoursOverride = null
|
||||
) => {
|
||||
const props = { ...defaultProps, ...propsOverride };
|
||||
if (isOnlineOverride !== null) props.isOnline = isOnlineOverride;
|
||||
if (isInWorkingHoursOverride !== null)
|
||||
props.isInWorkingHours = isInWorkingHoursOverride;
|
||||
|
||||
// Adjust time for specific scenarios
|
||||
if (title.includes('Back Tomorrow')) {
|
||||
// Set time to just after closing on Monday to trigger 'Back Tomorrow' (Tuesday)
|
||||
props.time = new Date('2024-07-15T17:01:00.000Z');
|
||||
props.isInWorkingHours = false;
|
||||
props.isOnline = false;
|
||||
}
|
||||
if (title.includes('Back Multiple Days Away')) {
|
||||
// Set time to Friday evening to trigger 'Back on Sunday' (as Saturday is closed)
|
||||
props.time = new Date('2024-07-19T18:00:00.000Z');
|
||||
props.isInWorkingHours = false;
|
||||
props.isOnline = false;
|
||||
}
|
||||
if (title.includes('Back Same Day - In Minutes')) {
|
||||
// Monday 16:50, next slot is 17:00 (in 10 minutes)
|
||||
// To make this specific, let's assume the next slot is within the hour
|
||||
// For this, we need to be outside working hours but a slot is available soon.
|
||||
// Let's say current time is 8:50 AM, office opens at 9:00 AM.
|
||||
props.time = new Date('2024-07-15T08:50:00.000Z');
|
||||
props.isInWorkingHours = false;
|
||||
props.isOnline = false;
|
||||
}
|
||||
if (title.includes('Back Same Day - In Hours')) {
|
||||
// Monday 07:30 AM, office opens at 9:00 AM (in 1.5 hours, rounds to 2 hours)
|
||||
props.time = new Date('2024-07-15T07:30:00.000Z');
|
||||
props.isInWorkingHours = false;
|
||||
props.isOnline = false;
|
||||
}
|
||||
if (title.includes('in 1 hour')) {
|
||||
// Monday 08:00 AM, office opens at 9:00 AM (exactly in 1 hour)
|
||||
// At exactly 1 hour difference, remainingMinutes = 0
|
||||
props.time = new Date('2024-07-15T08:00:00.000Z');
|
||||
props.isInWorkingHours = false;
|
||||
props.isOnline = false;
|
||||
}
|
||||
if (title.includes('Back Same Day - At Time')) {
|
||||
// Monday 05:00 AM, office opens at 9:00 AM (at 9:00 AM)
|
||||
props.time = new Date('2024-07-15T05:00:00.000Z');
|
||||
props.isInWorkingHours = false;
|
||||
props.isOnline = false;
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
props,
|
||||
};
|
||||
};
|
||||
|
||||
const variants = [
|
||||
createVariant(
|
||||
'Working Hours Disabled - Online',
|
||||
{ workingHoursEnabled: false },
|
||||
true,
|
||||
true
|
||||
),
|
||||
createVariant(
|
||||
'Working Hours Disabled - Offline',
|
||||
{ workingHoursEnabled: false },
|
||||
false,
|
||||
false
|
||||
),
|
||||
createVariant(
|
||||
'All Day Closed - Offline',
|
||||
{
|
||||
workingHours: defaultProps.workingHours.map(wh => ({
|
||||
...wh,
|
||||
closedAllDay: true,
|
||||
})),
|
||||
},
|
||||
false,
|
||||
false
|
||||
),
|
||||
createVariant('Online and In Working Hours', {}, true, true),
|
||||
createVariant(
|
||||
'No Next Slot Available (e.g., all future slots closed or empty workingHours)',
|
||||
{ workingHours: [] }, // No working hours defined
|
||||
false,
|
||||
false
|
||||
),
|
||||
createVariant(
|
||||
'Back Tomorrow',
|
||||
{},
|
||||
false,
|
||||
false // Time will be adjusted by createVariant
|
||||
),
|
||||
createVariant('Back Multiple Days Away (e.g., on Sunday)', {}, false, false),
|
||||
createVariant(
|
||||
'Back Same Day - In Minutes (e.g., in 10 minutes)',
|
||||
{},
|
||||
false,
|
||||
false
|
||||
),
|
||||
createVariant(
|
||||
'Back Same Day - In Hours (e.g., in 2 hours)',
|
||||
{},
|
||||
false,
|
||||
false
|
||||
),
|
||||
createVariant(
|
||||
'Back Same Day - Exactly an Hour (e.g., in 1 hour)',
|
||||
{},
|
||||
false,
|
||||
false
|
||||
),
|
||||
createVariant(
|
||||
'Back Same Day - At Time (e.g., at 09:00 AM)',
|
||||
{},
|
||||
false,
|
||||
false
|
||||
),
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Widget/Components/Availability/AvailabilityText"
|
||||
:layout="{ type: 'grid', width: 300 }"
|
||||
>
|
||||
<Variant v-for="(variant, i) in variants" :key="i" :title="variant.title">
|
||||
<AvailabilityText
|
||||
:time="variant.props.time"
|
||||
:utc-offset="variant.props.utcOffset"
|
||||
:working-hours="variant.props.workingHours"
|
||||
:working-hours-enabled="variant.props.workingHoursEnabled"
|
||||
:reply-time="variant.props.replyTime"
|
||||
:is-online="variant.props.isOnline"
|
||||
:is-in-working-hours="variant.props.isInWorkingHours"
|
||||
class="text-n-slate-11"
|
||||
/>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,178 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user