chore: dummy reports page

This commit is contained in:
Sojan
2025-05-07 03:47:34 -07:00
parent ebabb69048
commit 2b0c154bc5
4 changed files with 440 additions and 1 deletions
@@ -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>