563 lines
17 KiB
Vue
563 lines
17 KiB
Vue
<script setup>
|
|
import LineChart from '@core/libs/chartjs/components/LineChart';
|
|
import { endOfMonth, format, startOfMonth, subDays, subMonths } from 'date-fns';
|
|
import { computed, onBeforeMount, ref, watch } from 'vue';
|
|
import { useTheme } from 'vuetify';
|
|
import { useStore } from 'vuex';
|
|
|
|
const vuetifyTheme = useTheme();
|
|
const store = useStore();
|
|
const selectedPeriod = ref('this_month');
|
|
const date = ref('');
|
|
const userData = useCookie('userData');
|
|
const showDateRange = computed(() => selectedPeriod.value === 'custom');
|
|
|
|
const periodOptions = ref([
|
|
{ abbreviation: 'today', name: 'Today' },
|
|
{ abbreviation: 'last7_days', name: 'Last 7 Days' },
|
|
{ abbreviation: 'last30_days', name: 'Last 30 Days' },
|
|
{ abbreviation: 'this_month', name: 'This Month' },
|
|
{ abbreviation: 'last_month', name: 'Last Month' },
|
|
{ abbreviation: 'this_quarter', name: 'Current Quarter' },
|
|
{ abbreviation: 'last_quarter', name: 'Last Quarter' },
|
|
{ abbreviation: 'this_year', name: 'This Year' },
|
|
{ abbreviation: 'last_year', name: 'Last Year' },
|
|
{ abbreviation: 'last365_days', name: 'Last 365 Days' },
|
|
{ abbreviation: 'custom', name: 'Custom' },
|
|
]);
|
|
|
|
const setPeriod = (period) => {
|
|
selectedPeriod.value = period;
|
|
setDateRange();
|
|
};
|
|
|
|
const setDateRange = () => {
|
|
const today = new Date();
|
|
let start, end;
|
|
|
|
switch (selectedPeriod.value) {
|
|
case 'today':
|
|
start = end = today;
|
|
break;
|
|
case 'last7_days':
|
|
start = subDays(today, 7);
|
|
end = today;
|
|
break;
|
|
case 'last30_days':
|
|
start = subDays(today, 30);
|
|
end = today;
|
|
break;
|
|
case 'this_month':
|
|
start = startOfMonth(today);
|
|
end = endOfMonth(today);
|
|
break;
|
|
case 'last_month':
|
|
start = startOfMonth(subMonths(today, 1));
|
|
end = endOfMonth(subMonths(today, 1));
|
|
break;
|
|
case 'this_quarter':
|
|
end = startOfMonth(subMonths(today, today.getMonth() % 3));
|
|
start = endOfMonth(subMonths(today, today.getMonth() % 3 + 2));
|
|
break;
|
|
case 'last_quarter':
|
|
end = startOfMonth(subMonths(today, today.getMonth() % 3 + 3));
|
|
start = endOfMonth(subMonths(today, today.getMonth() % 3 + 5));
|
|
break;
|
|
case 'this_year':
|
|
start = new Date(today.getFullYear(), 0, 1);
|
|
end = new Date(today.getFullYear(), 11, 31);
|
|
break;
|
|
case 'last_year':
|
|
start = new Date(today.getFullYear() - 1, 0, 1);
|
|
end = new Date(today.getFullYear() - 1, 11, 31);
|
|
break;
|
|
case 'last365_days':
|
|
start = subDays(today, 365);
|
|
end = today;
|
|
break;
|
|
case 'custom':
|
|
return;
|
|
default:
|
|
start = end = today;
|
|
}
|
|
|
|
date.value = [format(start, 'yyyy-MM-dd'), format(end, 'yyyy-MM-dd')];
|
|
};
|
|
|
|
onBeforeMount(async () => {
|
|
setDateRange();
|
|
const [startDate, endDate] = date.value;
|
|
|
|
await store.dispatch('getAdminDashboardData', {
|
|
start_date: startDate,
|
|
end_date: endDate,
|
|
});
|
|
});
|
|
|
|
watch(selectedPeriod, async () => {
|
|
setDateRange();
|
|
if (selectedPeriod.value !== 'custom') {
|
|
const [startDate, endDate] = date.value;
|
|
await store.dispatch('getAdminDashboardData', {
|
|
start_date: startDate,
|
|
end_date: endDate,
|
|
});
|
|
}
|
|
});
|
|
|
|
const getButtonColor = (buttonPeriod) => {
|
|
return selectedPeriod.value === buttonPeriod ? 'primary' : 'secondary';
|
|
};
|
|
|
|
const changeDateRange = async () => {
|
|
console.log('changed date', date.value, 'type:', typeof date.value);
|
|
|
|
try {
|
|
const dateString = typeof date.value === 'string' ? date.value : '';
|
|
const [startDate, endDate] = dateString.split(" to ");
|
|
|
|
if (startDate && endDate) {
|
|
await store.dispatch('getAdminDashboardData', {
|
|
start_date: startDate,
|
|
end_date: endDate,
|
|
});
|
|
} else {
|
|
console.warn('Invalid date range');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error processing date range:', error);
|
|
}
|
|
};
|
|
|
|
const assignmentData = computed(() => [
|
|
{ title: 'Patients', amount: store.getters.getDashboardData.totals.total_patints, color: 'primary' },
|
|
{ title: 'Orders', amount: store.getters.getDashboardData.totals.total_orders, color: 'success' },
|
|
{ title: 'Amount', amount: store.getters.getDashboardData.totals.total_amount ? formatAmount(store.getters.getDashboardData.totals.total_amount) : '$0.00', color: 'secondary' },
|
|
{ title: 'Products Sold', amount: store.getters.getDashboardData.totals.total_products_sold, color: 'error' },
|
|
]);
|
|
|
|
const recentActivity = [
|
|
{ title: 'Patient created by Provider', subtitle: '31/Jul/2024 00:33 39.50.134.183' },
|
|
{ type: 'divider', inset: true },
|
|
{ title: 'Order Created By Patient William', subtitle: '31/Jul/2024 00:19 39.50.134.183' },
|
|
{ type: 'divider', inset: true },
|
|
];
|
|
|
|
const OrdersData = ref([]);
|
|
const ordersHeaders = [
|
|
{ title: 'Date', key: 'date' },
|
|
{ title: '#Order', key: 'order_id' },
|
|
{ title: 'Patient', key: 'patient_name' },
|
|
{ title: 'Amount', key: 'amount' },
|
|
];
|
|
const completedMeetingsHeaders = [
|
|
{ title: 'Date', key: 'appointment_date' },
|
|
{ title: 'Time', key: 'appointment_time' },
|
|
{ title: '#Order', key: 'order_id' },
|
|
{ title: 'Patient', key: 'patient_name' },
|
|
{ title: 'Provider', key: 'provider_name' },
|
|
{ title: 'Start Time', key: 'start_time' },
|
|
{ title: 'End Time', key: 'end_time' },
|
|
{ title: 'Duration', key: 'duration' },
|
|
];
|
|
const productsHeaders = [
|
|
{ title: 'ID', key: 'product_id' },
|
|
{ title: 'Name', key: 'product_name' },
|
|
{ title: 'Amount', key: 'total_amount' },
|
|
{ title: 'Orders', key: 'total_orders' },
|
|
];
|
|
|
|
function changeFormat(dateFormat) {
|
|
const dateParts = dateFormat.split('-');
|
|
const year = parseInt(dateParts[0]);
|
|
const month = parseInt(dateParts[1]);
|
|
const day = parseInt(dateParts[2]);
|
|
const date = new Date(year, month - 1, day);
|
|
return `${month}-${day}-${date.getFullYear()}`;
|
|
};
|
|
|
|
const formatDateTime = (dateStr) => {
|
|
const [date, time] = dateStr.split(' ');
|
|
const [year, month, day] = date.split('-');
|
|
const [hours, minutes] = time.split(':');
|
|
return `${month}-${day}-${year} ${hours}:${minutes}`;
|
|
};
|
|
|
|
const formattedDuration = (startTime, endTime) => {
|
|
const start = new Date(startTime);
|
|
const end = new Date(endTime);
|
|
const diffInSeconds = Math.floor((end - start) / 1000); // Difference in seconds
|
|
|
|
const hours = Math.floor(diffInSeconds / 3600);
|
|
const minutes = Math.floor((diffInSeconds % 3600) / 60);
|
|
const seconds = diffInSeconds % 60;
|
|
|
|
if (hours > 0) {
|
|
return `${hours}h ${minutes}m ${seconds}s`;
|
|
} else if (minutes > 0) {
|
|
return `${minutes}m ${seconds}s`;
|
|
} else {
|
|
return `${seconds}s`;
|
|
}
|
|
};
|
|
const formatAmount = (amount) => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD'
|
|
}).format(amount);
|
|
};
|
|
const data = ref({
|
|
labels: [],
|
|
datasets: [
|
|
{
|
|
fill: false,
|
|
tension: 0,
|
|
pointRadius: 4,
|
|
label: 'Meetings',
|
|
pointHoverRadius: 6,
|
|
pointStyle: 'circle',
|
|
borderColor: 'green',
|
|
backgroundColor: 'green',
|
|
pointHoverBorderWidth: 5,
|
|
pointHoverBorderColor: 'white',
|
|
pointBorderColor: 'transparent',
|
|
pointHoverBackgroundColor: 'primary',
|
|
data: [],
|
|
yAxisID: 'y-axis-meetings',
|
|
},
|
|
{
|
|
fill: false,
|
|
tension: 0,
|
|
label: 'Sales',
|
|
pointRadius: 4,
|
|
pointHoverRadius: 6,
|
|
pointStyle: 'circle',
|
|
borderColor: 'orange',
|
|
backgroundColor: 'orange',
|
|
pointHoverBorderWidth: 5,
|
|
pointHoverBorderColor: 'white',
|
|
pointBorderColor: 'transparent',
|
|
pointHoverBackgroundColor: 'warning',
|
|
data: [],
|
|
yAxisID: 'y-axis-sales',
|
|
},
|
|
],
|
|
});
|
|
|
|
const updateChartData = (newData) => {
|
|
const isSingleDate = newData.graph_data.dates.length === 1;
|
|
let labels = newData.graph_data.dates;
|
|
let meetingsData = newData.graph_data.data.total_meetings;
|
|
let salesData = newData.graph_data.data.total_sales;
|
|
|
|
if (isSingleDate) {
|
|
labels = [labels[0], labels[0]];
|
|
meetingsData = [meetingsData[0], meetingsData[0]];
|
|
salesData = [salesData[0], salesData[0]];
|
|
}
|
|
|
|
data.value = {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
...data.value.datasets[0],
|
|
data: meetingsData,
|
|
},
|
|
{
|
|
...data.value.datasets[1],
|
|
data: salesData,
|
|
},
|
|
],
|
|
};
|
|
};
|
|
const formatOrderId = (id) => {
|
|
if (id >= 1 && id <= 9) {
|
|
return id.toString().padStart(4, '0');
|
|
} else if (id >= 10 && id <= 99) {
|
|
return id.toString().padStart(4, '0');
|
|
} else if (id >= 100 && id <= 999) {
|
|
return id.toString().padStart(4, '0');
|
|
} else {
|
|
return id; // or handle cases for IDs outside these ranges
|
|
}
|
|
}
|
|
watch(() => store.getters.getDashboardData, updateChartData, { immediate: true });
|
|
|
|
const chartConfig = computed(() => {
|
|
const maxMeetings = Math.max(...data.value.datasets[0].data, 1);
|
|
const maxSales = Math.max(...data.value.datasets[1].data, 1);
|
|
const isSingleDate = store.getters.getDashboardData.graph_data.dates.length === 1;
|
|
|
|
return {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
'y-axis-meetings': {
|
|
type: 'linear',
|
|
position: 'left',
|
|
beginAtZero: true,
|
|
min: 0,
|
|
max: maxMeetings * 2,
|
|
ticks: {
|
|
stepSize: Math.max(1, Math.floor(maxMeetings / 4)),
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: 'Meetings',
|
|
},
|
|
},
|
|
'y-axis-sales': {
|
|
type: 'linear',
|
|
position: 'right',
|
|
beginAtZero: true,
|
|
min: 0,
|
|
max: maxSales * 1.5,
|
|
ticks: {
|
|
callback: (value) => `$${value.toLocaleString()}`,
|
|
stepSize: Math.max(1, Math.floor(maxSales / 4)),
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: 'Sales',
|
|
},
|
|
},
|
|
x: {
|
|
ticks: {
|
|
autoSkip: true,
|
|
maxTicksLimit: isSingleDate ? 2 : 10,
|
|
maxRotation: 0,
|
|
minRotation: 0,
|
|
},
|
|
},
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
position: 'top',
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: (context) => {
|
|
let label = context.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
if (context.parsed.y !== null) {
|
|
label += context.dataset.yAxisID === 'y-axis-sales'
|
|
? `$${context.parsed.y.toLocaleString()}`
|
|
: context.parsed.y;
|
|
}
|
|
return label;
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<VRow class="match-height d-flex" v-if="$can('read', 'Dashboard Filters')">
|
|
<VCol cols="12" :md="selectedPeriod === 'custom' ? '9' : '12'"
|
|
class="d-flex align-center justify-end flex-wrap pull-right">
|
|
<VBtn class="mx-2" :color="getButtonColor('today')" @click="setPeriod('today')">Day</VBtn>
|
|
<VBtn class="mx-2" :color="getButtonColor('last7_days')" @click="setPeriod('last7_days')">Week</VBtn>
|
|
<VBtn class="mx-2" :color="getButtonColor('this_month')" @click="setPeriod('this_month')">Month</VBtn>
|
|
<VSelect v-model="selectedPeriod" label="Filter By" density="comfortable" item-title="name"
|
|
item-value="abbreviation" :items="periodOptions" class="medium-width" />
|
|
</VCol>
|
|
<VCol cols="12" :md="selectedPeriod === 'custom' ? '3' : ''" v-if="selectedPeriod === 'custom'">
|
|
<AppDateTimePicker v-model="date" label="Date Range" :config="{ mode: 'range' }" @change="changeDateRange()"
|
|
class="ml-4 date-range-picker" />
|
|
</VCol>
|
|
</VRow>
|
|
<VRow>
|
|
<VCol cols="12" md="4">
|
|
<VCard title="Welcome! Glad to see you." class="main-cards">
|
|
<VCardText>
|
|
<h1 class="mb-2 mt-10">{{ userData.fullName || userData.username }}</h1>
|
|
<p>Here are your company's most recent statistics:</p>
|
|
</VCardText>
|
|
<VCardItem>
|
|
</VCardItem>
|
|
<VCardText>
|
|
<VList class="card-list mb-0">
|
|
<VListItem v-for="assignment in assignmentData" :key="assignment.title" class="pb-2">
|
|
<template #title>
|
|
<div class="text-h6 me-4 mb-0 text-truncate">
|
|
{{ assignment.title }}
|
|
</div>
|
|
</template>
|
|
<template #append>
|
|
<VBtn :color="assignment.color" class="rounded" size="small" width="100">
|
|
{{ assignment.amount }}
|
|
</VBtn>
|
|
</template>
|
|
</VListItem>
|
|
</VList>
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
<VCol cols="12" md="8">
|
|
<VCard title="Overview" class="main-cards">
|
|
<VCardText>
|
|
<!-- <ApexChart /> -->
|
|
<LineChart :chart-options="chartConfig" :height="400" :chart-data="data" />
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
</VRow>
|
|
<VRow>
|
|
<VCol cols="12" md="6">
|
|
<VCard title="Recent Activity" class="activity-card">
|
|
<VCardText>
|
|
<template v-if="store.getters.getDashboardData.patient_reg_activity.length">
|
|
<v-list v-for="recentAct in store.getters.getDashboardData.patient_reg_activity" :key="recentAct.id"
|
|
lines="two" style="border-bottom: 1px solid silver;padding-bottom: 0px;">
|
|
<v-list-item :title="recentAct.activity" :subtitle="changeFormat(recentAct.created_at)"
|
|
style="padding-bottom: 0px;"></v-list-item>
|
|
</v-list>
|
|
</template>
|
|
<template v-else>
|
|
<div style="display: flex; justify-content: center; align-items: center; height: 200px;">
|
|
No Record
|
|
</div>
|
|
</template>
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
|
|
|
|
<VCol cols="12" md="6">
|
|
<VCard title="Recent Orders" class="recent-cards ">
|
|
<VCardText>
|
|
<VDataTable :headers="ordersHeaders" :items="store.getters.getDashboardData.orders_data" :items-per-page="5"
|
|
class="text-no-wrap">
|
|
<template #item.order_id="{ item }">
|
|
<RouterLink :to="{ name: 'admin-order-detail', params: { id: item.order_id } }">
|
|
<span class="text-h6 text-primary">{{ formatOrderId(item.order_id) }}</span>
|
|
</RouterLink>
|
|
</template>
|
|
<template #item.date="{ item }">
|
|
<span class="text-h6">{{ changeFormat(item.date) }}</span>
|
|
</template>
|
|
<template #item.amount="{ item }">
|
|
<span class="text-h6">{{ formatAmount(item.amount) }}</span>
|
|
</template>
|
|
</VDataTable>
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
</VRow>
|
|
<VRow>
|
|
<VCol cols="12" md="6">
|
|
<VCard title="Completed Appointments" class="recent-cards ">
|
|
<VCardText>
|
|
<VDataTable :headers="completedMeetingsHeaders" :items="store.getters.getDashboardData.completed_meetings"
|
|
:items-per-page="5" class="text-no-wrap">
|
|
<template #item.appointment_date="{ item }">
|
|
<span class="text-h6">{{ changeFormat(item.appointment_date) }}</span>
|
|
</template>
|
|
<template #item.order_id="{ item }">
|
|
<span class="text-h6">{{ item.order_id }}</span>
|
|
</template>
|
|
<template #item.start_time="{ item }">
|
|
<span class="text-h6">{{ formatDateTime(item.start_time) }}</span>
|
|
</template>
|
|
<template #item.end_time="{ item }">
|
|
<span class="text-h6">{{ formatDateTime(item.end_time) }}</span>
|
|
</template>
|
|
<template #item.duration="{ item }">
|
|
<span class="text-h6">{{ formattedDuration(item.start_time, item.end_time) }}</span>
|
|
</template>
|
|
</VDataTable>
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
<VCol cols="12" md="6">
|
|
<VCard title="Products" class="recent-cards">
|
|
<VCardText>
|
|
<VDataTable :headers="productsHeaders" :items="store.getters.getDashboardData.products" :items-per-page="5"
|
|
class="text-no-wrap">
|
|
<template #item.product_id="{ item }">
|
|
<span class="text-h6">{{ item.product_id }}</span>
|
|
</template>
|
|
<template #item.total_amount="{ item }">
|
|
<span class="text-h6">{{ formatAmount(item.total_amount) }}</span>
|
|
</template>
|
|
</VDataTable>
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
</VRow>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
@use "@core-scss/template/libs/apex-chart.scss";
|
|
|
|
.activity-card {
|
|
min-height: 480px;
|
|
max-height: 480px;
|
|
overflow-y: scroll !important;
|
|
}
|
|
|
|
.main-cards {
|
|
min-height: 508px;
|
|
}
|
|
|
|
.recent-cards {
|
|
min-height: 480px;
|
|
}
|
|
|
|
.medium-width {
|
|
min-width: 200px;
|
|
max-width: 250px;
|
|
}
|
|
|
|
.date-range-picker {
|
|
max-width: 300px;
|
|
}
|
|
|
|
.ml-4 {
|
|
margin-left: 8px;
|
|
}
|
|
|
|
.d-flex {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.mx-2 {
|
|
margin-left: 8px;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.v-btn {
|
|
min-width: 80px;
|
|
text-transform: none;
|
|
}
|
|
|
|
.v-autocomplete {
|
|
min-width: 200px;
|
|
max-width: 250px;
|
|
}
|
|
|
|
.v-autocomplete__content {
|
|
max-width: 250px;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.v-autocomplete {
|
|
min-width: 100px;
|
|
}
|
|
|
|
.v-btn {
|
|
min-width: 60px;
|
|
}
|
|
|
|
.date-range-picker {
|
|
max-width: 100%;
|
|
}
|
|
}
|
|
</style>
|