chore: dummy reports page
This commit is contained in:
@@ -260,11 +260,31 @@
|
||||
},
|
||||
"INBOX_REPORTS": {
|
||||
"HEADER": "Inbox Overview",
|
||||
"VOICE_HEADER": "Voice Channel Overview",
|
||||
"DESCRIPTION": "Quickly view your inbox performance with key metrics like conversations, response times, resolution times, and resolved cases—all in one place. Click an inbox name for more details.",
|
||||
"VOICE_DESCRIPTION": "Track call metrics such as total calls, average duration, waiting times, and more across your voice channels.",
|
||||
"LOADING_CHART": "Loading chart data...",
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"NO_VOICE_INBOXES": "No voice channels found",
|
||||
"CREATE_VOICE_INBOX_PROMPT": "Create a voice channel first to view reports",
|
||||
"DOWNLOAD_INBOX_REPORTS": "Download inbox reports",
|
||||
"DOWNLOAD_VOICE_REPORTS": "Download voice reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Inbox",
|
||||
"VOICE_METRICS": {
|
||||
"TOTAL_CALLS": "Total Calls",
|
||||
"INCOMING_CALLS": "Incoming Calls",
|
||||
"OUTGOING_CALLS": "Outgoing Calls",
|
||||
"MISSED_CALLS": "Missed Calls",
|
||||
"AVG_DURATION": "Average Call Duration",
|
||||
"AVG_WAITING_TIME": "Average Waiting Time",
|
||||
"SUCCESS_RATE": "Call Success Rate"
|
||||
},
|
||||
"VOICE_CHARTS": {
|
||||
"CALLS_BY_TYPE": "Calls by Type",
|
||||
"CALL_DURATION": "Average Call Duration",
|
||||
"WAITING_TIME": "Average Waiting Time",
|
||||
"SUCCESS_RATE": "Call Success Rate"
|
||||
},
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Conversations",
|
||||
|
||||
@@ -1,17 +1,40 @@
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useFunctionGetter } from 'dashboard/composables/store';
|
||||
import { computed, watch } from 'vue';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
|
||||
import WootReports from './components/WootReports.vue';
|
||||
import VoiceInboxReports from './VoiceInboxReports.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const inbox = useFunctionGetter('inboxes/getInboxById', route.params.id);
|
||||
|
||||
// No special parameters needed - only show voice reports for actual voice channels
|
||||
|
||||
// Determine if this is a voice channel
|
||||
const isVoiceChannel = computed(() => {
|
||||
// If inbox data isn't loaded yet, return false
|
||||
if (!inbox.value) return false;
|
||||
|
||||
// Log for debugging purposes
|
||||
console.log('Inbox data for voice check:', inbox.value);
|
||||
console.log('Channel type (camelCase):', inbox.value.channelType);
|
||||
console.log('Expected channel type:', INBOX_TYPES.VOICE);
|
||||
|
||||
// Check against the channelType property (camelCase)
|
||||
return inbox.value.channelType === INBOX_TYPES.VOICE;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VoiceInboxReports
|
||||
v-if="inbox.id && isVoiceChannel"
|
||||
:key="inbox.id"
|
||||
/>
|
||||
<WootReports
|
||||
v-if="inbox.id"
|
||||
v-else-if="inbox.id"
|
||||
:key="inbox.id"
|
||||
type="inbox"
|
||||
getter-key="inboxes/getInboxes"
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useFunctionGetter } from 'dashboard/composables/store';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { format } from 'date-fns';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
import ReportHeader from './components/ReportHeader.vue';
|
||||
import ReportFilters from './components/ReportFilters.vue';
|
||||
import V4Button from 'dashboard/components-next/button/Button.vue';
|
||||
import BarChart from 'shared/components/charts/BarChart.vue';
|
||||
import { GROUP_BY_FILTER, GROUP_BY_OPTIONS } from './constants';
|
||||
|
||||
const route = useRoute();
|
||||
const inbox = useFunctionGetter('inboxes/getInboxById', route.params.id);
|
||||
|
||||
// For this dummy implementation, we'll show dummy data
|
||||
// In a real implementation, this would fetch actual call data from the API
|
||||
console.log('VoiceInboxReports: Using dummy data for demonstration');
|
||||
|
||||
// Dummy data for voice metrics
|
||||
const voiceMetrics = ref({
|
||||
total_calls: { value: 145, trend: 12 },
|
||||
incoming_calls: { value: 98, trend: 8 },
|
||||
outgoing_calls: { value: 47, trend: 15 },
|
||||
average_duration: { value: 248, trend: -5 }, // in seconds
|
||||
average_waiting_time: { value: 32, trend: -10 }, // in seconds
|
||||
missed_calls: { value: 12, trend: -20 },
|
||||
call_success_rate: { value: 92, trend: 5 }, // percentage
|
||||
});
|
||||
|
||||
// Chart data
|
||||
const from = ref(Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60); // 30 days ago
|
||||
const to = ref(Math.floor(Date.now() / 1000));
|
||||
const groupBy = ref(GROUP_BY_FILTER[1]); // Day
|
||||
const groupByfilterItemsList = ref([
|
||||
{ id: 1, groupBy: 'Day' },
|
||||
{ id: 2, groupBy: 'Week' },
|
||||
{ id: 3, groupBy: 'Month' },
|
||||
]);
|
||||
const selectedGroupByFilter = ref(groupByfilterItemsList.value[0]);
|
||||
const businessHours = ref(false);
|
||||
|
||||
// Generate random chart data
|
||||
const generateRandomData = (min, max, count) => {
|
||||
return Array.from({ length: count }, () =>
|
||||
Math.floor(Math.random() * (max - min + 1) + min)
|
||||
);
|
||||
};
|
||||
|
||||
// Generate dates for the last 7 days (default selection)
|
||||
const generateDateLabels = (days = 7) => {
|
||||
const dates = [];
|
||||
const today = new Date();
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(today.getDate() - i);
|
||||
dates.push(format(date, 'dd-MMM'));
|
||||
}
|
||||
|
||||
return dates;
|
||||
};
|
||||
|
||||
// Chart data for calls by type
|
||||
const callsByTypeChart = computed(() => {
|
||||
return {
|
||||
labels: generateDateLabels(),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Incoming Calls',
|
||||
data: generateRandomData(2, 8, 7),
|
||||
backgroundColor: '#3B82F6', // blue
|
||||
barPercentage: 0.6,
|
||||
},
|
||||
{
|
||||
label: 'Outgoing Calls',
|
||||
data: generateRandomData(1, 5, 7),
|
||||
backgroundColor: '#10B981', // green
|
||||
barPercentage: 0.6,
|
||||
},
|
||||
{
|
||||
label: 'Missed Calls',
|
||||
data: generateRandomData(0, 2, 7),
|
||||
backgroundColor: '#EF4444', // red
|
||||
barPercentage: 0.6,
|
||||
},
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// Chart data for call duration
|
||||
const callDurationChart = computed(() => {
|
||||
return {
|
||||
labels: generateDateLabels(),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Average Call Duration (seconds)',
|
||||
data: generateRandomData(180, 320, 7),
|
||||
backgroundColor: '#8B5CF6', // purple
|
||||
barPercentage: 0.6,
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// Chart data for waiting time
|
||||
const waitingTimeChart = computed(() => {
|
||||
return {
|
||||
labels: generateDateLabels(),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Average Waiting Time (seconds)',
|
||||
data: generateRandomData(15, 60, 7),
|
||||
backgroundColor: '#F59E0B', // amber
|
||||
barPercentage: 0.6,
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// Chart data for call success rate
|
||||
const successRateChart = computed(() => {
|
||||
return {
|
||||
labels: generateDateLabels(),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Call Success Rate (%)',
|
||||
data: generateRandomData(75, 100, 7),
|
||||
backgroundColor: '#059669', // emerald
|
||||
barPercentage: 0.6,
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// Chart options
|
||||
const chartOptions = {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
};
|
||||
|
||||
// Dummy handlers
|
||||
const onDateRangeChange = (payload) => {
|
||||
// In a real implementation, this would fetch new data based on date range
|
||||
console.log('Date range changed', payload);
|
||||
};
|
||||
|
||||
const onGroupByFilterChange = (payload) => {
|
||||
selectedGroupByFilter.value = payload;
|
||||
groupBy.value = GROUP_BY_FILTER[payload.id];
|
||||
// In a real implementation, this would fetch new data based on grouping
|
||||
console.log('Group by filter changed', groupBy.value);
|
||||
};
|
||||
|
||||
const onBusinessHoursToggle = (value) => {
|
||||
businessHours.value = value;
|
||||
// In a real implementation, this would fetch new data with business hours filter
|
||||
console.log('Business hours toggle', value);
|
||||
};
|
||||
|
||||
// Format time helper (converts seconds to MM:SS format)
|
||||
const formatSecondsToTime = (seconds) => {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const downloadReports = () => {
|
||||
alert('This would download voice reports in a real implementation.');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ReportHeader
|
||||
:header-title="$t('INBOX_REPORTS.VOICE_HEADER')"
|
||||
has-back-button
|
||||
>
|
||||
<V4Button
|
||||
:label="$t('INBOX_REPORTS.DOWNLOAD_VOICE_REPORTS')"
|
||||
icon="i-ph-download-simple"
|
||||
size="sm"
|
||||
@click="downloadReports"
|
||||
/>
|
||||
</ReportHeader>
|
||||
|
||||
<ReportFilters
|
||||
type="inbox"
|
||||
:filter-items-list="[inbox]"
|
||||
:group-by-filter-items-list="groupByfilterItemsList"
|
||||
:selected-group-by-filter="selectedGroupByFilter"
|
||||
:current-filter="inbox"
|
||||
@date-range-change="onDateRangeChange"
|
||||
@group-by-filter-change="onGroupByFilterChange"
|
||||
@business-hours-toggle="onBusinessHoursToggle"
|
||||
/>
|
||||
|
||||
<!-- Main container -->
|
||||
<div class="py-4">
|
||||
<!-- Metrics Cards -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<!-- Total Calls -->
|
||||
<div class="bg-n-solid-2 p-4 rounded-lg shadow border border-n-container">
|
||||
<div class="text-n-slate-11 text-sm mb-1">{{ $t('INBOX_REPORTS.VOICE_METRICS.TOTAL_CALLS') }}</div>
|
||||
<div class="flex items-end">
|
||||
<span class="text-2xl font-semibold mr-2">{{ voiceMetrics.total_calls.value }}</span>
|
||||
<span class="text-xs" :class="voiceMetrics.total_calls.trend > 0 ? 'text-n-emerald-6' : 'text-n-ruby-6'">
|
||||
{{ voiceMetrics.total_calls.trend > 0 ? '↑' : '↓' }} {{ Math.abs(voiceMetrics.total_calls.trend) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Incoming Calls -->
|
||||
<div class="bg-n-solid-2 p-4 rounded-lg shadow border border-n-container">
|
||||
<div class="text-n-slate-11 text-sm mb-1">{{ $t('INBOX_REPORTS.VOICE_METRICS.INCOMING_CALLS') }}</div>
|
||||
<div class="flex items-end">
|
||||
<span class="text-2xl font-semibold mr-2">{{ voiceMetrics.incoming_calls.value }}</span>
|
||||
<span class="text-xs" :class="voiceMetrics.incoming_calls.trend > 0 ? 'text-n-emerald-6' : 'text-n-ruby-6'">
|
||||
{{ voiceMetrics.incoming_calls.trend > 0 ? '↑' : '↓' }} {{ Math.abs(voiceMetrics.incoming_calls.trend) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outgoing Calls -->
|
||||
<div class="bg-n-solid-2 p-4 rounded-lg shadow border border-n-container">
|
||||
<div class="text-n-slate-11 text-sm mb-1">{{ $t('INBOX_REPORTS.VOICE_METRICS.OUTGOING_CALLS') }}</div>
|
||||
<div class="flex items-end">
|
||||
<span class="text-2xl font-semibold mr-2">{{ voiceMetrics.outgoing_calls.value }}</span>
|
||||
<span class="text-xs" :class="voiceMetrics.outgoing_calls.trend > 0 ? 'text-n-emerald-6' : 'text-n-ruby-6'">
|
||||
{{ voiceMetrics.outgoing_calls.trend > 0 ? '↑' : '↓' }} {{ Math.abs(voiceMetrics.outgoing_calls.trend) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Missed Calls -->
|
||||
<div class="bg-n-solid-2 p-4 rounded-lg shadow border border-n-container">
|
||||
<div class="text-n-slate-11 text-sm mb-1">{{ $t('INBOX_REPORTS.VOICE_METRICS.MISSED_CALLS') }}</div>
|
||||
<div class="flex items-end">
|
||||
<span class="text-2xl font-semibold mr-2">{{ voiceMetrics.missed_calls.value }}</span>
|
||||
<span class="text-xs" :class="voiceMetrics.missed_calls.trend < 0 ? 'text-n-emerald-6' : 'text-n-ruby-6'">
|
||||
{{ voiceMetrics.missed_calls.trend > 0 ? '↑' : '↓' }} {{ Math.abs(voiceMetrics.missed_calls.trend) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- More Metrics -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||
<!-- Average Call Duration -->
|
||||
<div class="bg-n-solid-2 p-4 rounded-lg shadow border border-n-container">
|
||||
<div class="text-n-slate-11 text-sm mb-1">{{ $t('INBOX_REPORTS.VOICE_METRICS.AVG_DURATION') }}</div>
|
||||
<div class="flex items-end">
|
||||
<span class="text-2xl font-semibold mr-2">{{ formatSecondsToTime(voiceMetrics.average_duration.value) }}</span>
|
||||
<span class="text-xs" :class="voiceMetrics.average_duration.trend > 0 ? 'text-n-emerald-6' : 'text-n-ruby-6'">
|
||||
{{ voiceMetrics.average_duration.trend > 0 ? '↑' : '↓' }} {{ Math.abs(voiceMetrics.average_duration.trend) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Average Waiting Time -->
|
||||
<div class="bg-n-solid-2 p-4 rounded-lg shadow border border-n-container">
|
||||
<div class="text-n-slate-11 text-sm mb-1">{{ $t('INBOX_REPORTS.VOICE_METRICS.AVG_WAITING_TIME') }}</div>
|
||||
<div class="flex items-end">
|
||||
<span class="text-2xl font-semibold mr-2">{{ formatSecondsToTime(voiceMetrics.average_waiting_time.value) }}</span>
|
||||
<span class="text-xs" :class="voiceMetrics.average_waiting_time.trend < 0 ? 'text-n-emerald-6' : 'text-n-ruby-6'">
|
||||
{{ voiceMetrics.average_waiting_time.trend > 0 ? '↑' : '↓' }} {{ Math.abs(voiceMetrics.average_waiting_time.trend) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Call Success Rate -->
|
||||
<div class="bg-n-solid-2 p-4 rounded-lg shadow border border-n-container">
|
||||
<div class="text-n-slate-11 text-sm mb-1">{{ $t('INBOX_REPORTS.VOICE_METRICS.SUCCESS_RATE') }}</div>
|
||||
<div class="flex items-end">
|
||||
<span class="text-2xl font-semibold mr-2">{{ voiceMetrics.call_success_rate.value }}%</span>
|
||||
<span class="text-xs" :class="voiceMetrics.call_success_rate.trend > 0 ? 'text-n-emerald-6' : 'text-n-ruby-6'">
|
||||
{{ voiceMetrics.call_success_rate.trend > 0 ? '↑' : '↓' }} {{ Math.abs(voiceMetrics.call_success_rate.trend) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<!-- Calls by Type -->
|
||||
<div class="p-4 bg-n-solid-2 rounded-lg shadow border border-n-container">
|
||||
<h3 class="text-md font-semibold mb-3">{{ $t('INBOX_REPORTS.VOICE_CHARTS.CALLS_BY_TYPE') }}</h3>
|
||||
<div class="h-72">
|
||||
<BarChart
|
||||
:collection="callsByTypeChart"
|
||||
:chart-options="chartOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Average Call Duration -->
|
||||
<div class="p-4 bg-n-solid-2 rounded-lg shadow border border-n-container">
|
||||
<h3 class="text-md font-semibold mb-3">{{ $t('INBOX_REPORTS.VOICE_CHARTS.CALL_DURATION') }}</h3>
|
||||
<div class="h-72">
|
||||
<BarChart
|
||||
:collection="callDurationChart"
|
||||
:chart-options="chartOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Waiting Time -->
|
||||
<div class="p-4 bg-n-solid-2 rounded-lg shadow border border-n-container">
|
||||
<h3 class="text-md font-semibold mb-3">{{ $t('INBOX_REPORTS.VOICE_CHARTS.WAITING_TIME') }}</h3>
|
||||
<div class="h-72">
|
||||
<BarChart
|
||||
:collection="waitingTimeChart"
|
||||
:chart-options="chartOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Call Success Rate -->
|
||||
<div class="p-4 bg-n-solid-2 rounded-lg shadow border border-n-container">
|
||||
<h3 class="text-md font-semibold mb-3">{{ $t('INBOX_REPORTS.VOICE_CHARTS.SUCCESS_RATE') }}</h3>
|
||||
<div class="h-72">
|
||||
<BarChart
|
||||
:collection="successRateChart"
|
||||
:chart-options="chartOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useStore } from 'vuex';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
|
||||
import ReportHeader from './components/ReportHeader.vue';
|
||||
import SummaryReportLink from './components/SummaryReportLink.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
const store = useStore();
|
||||
const inboxes = computed(() => store.getters['inboxes/getInboxes']);
|
||||
const isLoading = computed(() => store.getters['inboxes/getUIFlags'].isFetching);
|
||||
|
||||
// Filter only voice inboxes
|
||||
const voiceInboxes = computed(() => {
|
||||
return inboxes.value.filter(inbox => inbox.channel_type === INBOX_TYPES.VOICE);
|
||||
});
|
||||
|
||||
// For debugging
|
||||
console.log('All inboxes:', inboxes.value);
|
||||
console.log('Voice inboxes:', voiceInboxes.value);
|
||||
|
||||
const fetchInboxes = async () => {
|
||||
await store.dispatch('inboxes/get');
|
||||
};
|
||||
|
||||
fetchInboxes();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ReportHeader
|
||||
:header-title="$t('INBOX_REPORTS.VOICE_HEADER')"
|
||||
:header-description="$t('INBOX_REPORTS.VOICE_DESCRIPTION')"
|
||||
/>
|
||||
|
||||
<div v-if="isLoading" class="w-full py-20">
|
||||
<Spinner class="mx-auto" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="voiceInboxes.length === 0" class="flex flex-col items-center justify-center py-10">
|
||||
<div class="text-n-slate-8 text-lg">{{ $t('INBOX_REPORTS.NO_VOICE_INBOXES') }}</div>
|
||||
<div class="text-n-slate-5 text-sm mt-2">{{ $t('INBOX_REPORTS.CREATE_VOICE_INBOX_PROMPT') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-6">
|
||||
<SummaryReportLink
|
||||
v-for="inbox in voiceInboxes"
|
||||
:key="inbox.id"
|
||||
:name="inbox.name"
|
||||
:id="inbox.id"
|
||||
type="inbox"
|
||||
:avatar-path="'/assets/images/channels/voice.png'"
|
||||
:icon-class="'i-ph-phone-fill'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user