generated from corrad-software/corrad-af-2024
435 lines
18 KiB
Vue
435 lines
18 KiB
Vue
<script setup>
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import { useDmsStore } from '~/stores/dms';
|
|
|
|
// Store
|
|
const dmsStore = useDmsStore();
|
|
|
|
// Props
|
|
const props = defineProps({
|
|
userId: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
timeRange: {
|
|
type: String,
|
|
default: '30days' // 7days, 30days, 90days, 365days
|
|
},
|
|
showPersonal: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
showDepartmental: {
|
|
type: Boolean,
|
|
default: true
|
|
}
|
|
});
|
|
|
|
// Component state
|
|
const isLoading = ref(true);
|
|
const accessRequests = ref([]);
|
|
const departmentRequests = ref([]);
|
|
const selectedTimeRange = ref(props.timeRange);
|
|
|
|
// Time ranges
|
|
const timeRangeOptions = [
|
|
{ id: '7days', label: 'Last 7 Days' },
|
|
{ id: '30days', label: 'Last 30 Days' },
|
|
{ id: '90days', label: 'Last 90 Days' },
|
|
{ id: '365days', label: 'Last Year' }
|
|
];
|
|
|
|
// Methods
|
|
const loadRequestData = async () => {
|
|
isLoading.value = true;
|
|
try {
|
|
// In a real implementation, this would come from an API
|
|
const [userRequests, deptRequests] = await Promise.all([
|
|
dmsStore.getAccessRequests(props.userId),
|
|
dmsStore.getDepartmentAccessRequests()
|
|
]);
|
|
|
|
accessRequests.value = userRequests;
|
|
departmentRequests.value = deptRequests;
|
|
} catch (error) {
|
|
console.error('Failed to load access request data:', error);
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
// Update time range
|
|
const updateTimeRange = (range) => {
|
|
selectedTimeRange.value = range;
|
|
loadRequestData();
|
|
};
|
|
|
|
// Format time duration in hours
|
|
const formatDuration = (hours) => {
|
|
if (hours < 1) {
|
|
return `${Math.round(hours * 60)} minutes`;
|
|
} else if (hours < 24) {
|
|
return `${Math.round(hours)} hours`;
|
|
} else {
|
|
return `${Math.round(hours / 24)} days`;
|
|
}
|
|
};
|
|
|
|
// Format percentage
|
|
const formatPercentage = (value) => {
|
|
return `${Math.round(value * 100)}%`;
|
|
};
|
|
|
|
// Date formatters
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return '';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
};
|
|
|
|
// Computed metrics
|
|
const personalMetrics = computed(() => {
|
|
// Filter requests by the selected time range
|
|
const requests = accessRequests.value.filter(req => {
|
|
const requestDate = new Date(req.requestDate);
|
|
const now = new Date();
|
|
const daysAgo = (now - requestDate) / (1000 * 60 * 60 * 24);
|
|
|
|
if (selectedTimeRange.value === '7days') return daysAgo <= 7;
|
|
if (selectedTimeRange.value === '30days') return daysAgo <= 30;
|
|
if (selectedTimeRange.value === '90days') return daysAgo <= 90;
|
|
if (selectedTimeRange.value === '365days') return daysAgo <= 365;
|
|
|
|
return true;
|
|
});
|
|
|
|
// Extract only resolved requests (approved or rejected)
|
|
const resolvedRequests = requests.filter(req =>
|
|
req.status === 'approved' || req.status === 'rejected'
|
|
);
|
|
|
|
// Total requests
|
|
const totalRequests = requests.length;
|
|
const pendingRequests = requests.filter(req => req.status === 'pending').length;
|
|
const approvedRequests = requests.filter(req => req.status === 'approved').length;
|
|
const rejectedRequests = requests.filter(req => req.status === 'rejected').length;
|
|
|
|
// Calculate response times
|
|
let totalResponseTime = 0;
|
|
let fastestResponse = Infinity;
|
|
let slowestResponse = 0;
|
|
|
|
resolvedRequests.forEach(req => {
|
|
const requestDate = new Date(req.requestDate);
|
|
const responseDate = new Date(req.responseDate);
|
|
const responseTime = (responseDate - requestDate) / (1000 * 60 * 60); // in hours
|
|
|
|
totalResponseTime += responseTime;
|
|
fastestResponse = Math.min(fastestResponse, responseTime);
|
|
slowestResponse = Math.max(slowestResponse, responseTime);
|
|
});
|
|
|
|
// Calculate averages
|
|
const avgResponseTime = resolvedRequests.length > 0
|
|
? totalResponseTime / resolvedRequests.length
|
|
: 0;
|
|
|
|
// Calculate overdue
|
|
const overdueRequests = requests.filter(req => {
|
|
if (req.status !== 'pending') return false;
|
|
|
|
const deadline = new Date(req.targetResolutionTime);
|
|
const now = new Date();
|
|
return now > deadline;
|
|
}).length;
|
|
|
|
const overduePercentage = totalRequests > 0
|
|
? overdueRequests / totalRequests
|
|
: 0;
|
|
|
|
return {
|
|
totalRequests,
|
|
pendingRequests,
|
|
approvedRequests,
|
|
rejectedRequests,
|
|
avgResponseTime,
|
|
fastestResponse: fastestResponse === Infinity ? 0 : fastestResponse,
|
|
slowestResponse,
|
|
overdueRequests,
|
|
overduePercentage
|
|
};
|
|
});
|
|
|
|
const departmentMetrics = computed(() => {
|
|
// Same calculations but for department requests
|
|
const requests = departmentRequests.value.filter(req => {
|
|
const requestDate = new Date(req.requestDate);
|
|
const now = new Date();
|
|
const daysAgo = (now - requestDate) / (1000 * 60 * 60 * 24);
|
|
|
|
if (selectedTimeRange.value === '7days') return daysAgo <= 7;
|
|
if (selectedTimeRange.value === '30days') return daysAgo <= 30;
|
|
if (selectedTimeRange.value === '90days') return daysAgo <= 90;
|
|
if (selectedTimeRange.value === '365days') return daysAgo <= 365;
|
|
|
|
return true;
|
|
});
|
|
|
|
// Extract only resolved requests (approved or rejected)
|
|
const resolvedRequests = requests.filter(req =>
|
|
req.status === 'approved' || req.status === 'rejected'
|
|
);
|
|
|
|
// Total requests
|
|
const totalRequests = requests.length;
|
|
const pendingRequests = requests.filter(req => req.status === 'pending').length;
|
|
const approvedRequests = requests.filter(req => req.status === 'approved').length;
|
|
const rejectedRequests = requests.filter(req => req.status === 'rejected').length;
|
|
|
|
// Calculate response times
|
|
let totalResponseTime = 0;
|
|
let fastestResponse = Infinity;
|
|
let slowestResponse = 0;
|
|
|
|
resolvedRequests.forEach(req => {
|
|
const requestDate = new Date(req.requestDate);
|
|
const responseDate = new Date(req.responseDate);
|
|
const responseTime = (responseDate - requestDate) / (1000 * 60 * 60); // in hours
|
|
|
|
totalResponseTime += responseTime;
|
|
fastestResponse = Math.min(fastestResponse, responseTime);
|
|
slowestResponse = Math.max(slowestResponse, responseTime);
|
|
});
|
|
|
|
// Calculate averages
|
|
const avgResponseTime = resolvedRequests.length > 0
|
|
? totalResponseTime / resolvedRequests.length
|
|
: 0;
|
|
|
|
// Calculate overdue
|
|
const overdueRequests = requests.filter(req => {
|
|
if (req.status !== 'pending') return false;
|
|
|
|
const deadline = new Date(req.targetResolutionTime);
|
|
const now = new Date();
|
|
return now > deadline;
|
|
}).length;
|
|
|
|
const overduePercentage = totalRequests > 0
|
|
? overdueRequests / totalRequests
|
|
: 0;
|
|
|
|
return {
|
|
totalRequests,
|
|
pendingRequests,
|
|
approvedRequests,
|
|
rejectedRequests,
|
|
avgResponseTime,
|
|
fastestResponse: fastestResponse === Infinity ? 0 : fastestResponse,
|
|
slowestResponse,
|
|
overdueRequests,
|
|
overduePercentage
|
|
};
|
|
});
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
loadRequestData();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="dms-access-request-tracker">
|
|
<!-- Header with time range selector -->
|
|
<div class="flex flex-wrap items-center justify-between mb-4 gap-3">
|
|
<div>
|
|
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Access Request Metrics</h2>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-2">
|
|
<!-- Time range selector -->
|
|
<div class="inline-flex rounded-md shadow-sm">
|
|
<button
|
|
v-for="option in timeRangeOptions"
|
|
:key="option.id"
|
|
@click="updateTimeRange(option.id)"
|
|
class="relative inline-flex items-center px-3 py-1.5 text-sm font-medium border transition-colors"
|
|
:class="[
|
|
selectedTimeRange === option.id
|
|
? 'bg-blue-100 border-blue-300 text-blue-800 dark:bg-blue-900/20 dark:border-blue-700 dark:text-blue-300 z-10'
|
|
: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300',
|
|
option.id === '7days' ? 'rounded-l-md' : '',
|
|
option.id === '365days' ? 'rounded-r-md' : ''
|
|
]"
|
|
>
|
|
{{ option.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Refresh button -->
|
|
<button
|
|
@click="loadRequestData"
|
|
class="inline-flex items-center px-2 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
<span class="ml-1">Refresh</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading state -->
|
|
<div v-if="isLoading" class="flex items-center justify-center py-12">
|
|
<div class="text-center">
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
|
<p class="text-gray-600 dark:text-gray-400">Loading metrics...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- KPI Dashboard -->
|
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<!-- Personal Metrics -->
|
|
<div v-if="props.showPersonal" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
|
<div class="px-6 py-4 bg-blue-50 dark:bg-blue-900/10 border-b border-blue-200 dark:border-blue-800">
|
|
<h3 class="text-md font-medium text-blue-900 dark:text-blue-100">Personal Metrics</h3>
|
|
<p class="text-sm text-blue-700 dark:text-blue-300">Your access request performance</p>
|
|
</div>
|
|
|
|
<div class="p-6">
|
|
<!-- Status Metrics -->
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
|
|
<div class="bg-gray-50 dark:bg-gray-900/10 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
<div class="text-xl font-bold text-gray-900 dark:text-gray-100">{{ personalMetrics.totalRequests }}</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Total Requests</div>
|
|
</div>
|
|
|
|
<div class="bg-yellow-50 dark:bg-yellow-900/10 p-4 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
|
<div class="text-xl font-bold text-yellow-700 dark:text-yellow-300">{{ personalMetrics.pendingRequests }}</div>
|
|
<div class="text-sm text-yellow-600 dark:text-yellow-400">Pending</div>
|
|
</div>
|
|
|
|
<div class="bg-green-50 dark:bg-green-900/10 p-4 rounded-lg border border-green-200 dark:border-green-800">
|
|
<div class="text-xl font-bold text-green-700 dark:text-green-300">{{ personalMetrics.approvedRequests }}</div>
|
|
<div class="text-sm text-green-600 dark:text-green-400">Approved</div>
|
|
</div>
|
|
|
|
<div class="bg-red-50 dark:bg-red-900/10 p-4 rounded-lg border border-red-200 dark:border-red-800">
|
|
<div class="text-xl font-bold text-red-700 dark:text-red-300">{{ personalMetrics.rejectedRequests }}</div>
|
|
<div class="text-sm text-red-600 dark:text-red-400">Rejected</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Response Time Metrics -->
|
|
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Response Time Metrics</h4>
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
|
<div class="bg-indigo-50 dark:bg-indigo-900/10 p-4 rounded-lg border border-indigo-200 dark:border-indigo-800">
|
|
<div class="text-xl font-bold text-indigo-700 dark:text-indigo-300">{{ formatDuration(personalMetrics.avgResponseTime) }}</div>
|
|
<div class="text-sm text-indigo-600 dark:text-indigo-400">Average Response</div>
|
|
</div>
|
|
|
|
<div class="bg-green-50 dark:bg-green-900/10 p-4 rounded-lg border border-green-200 dark:border-green-800">
|
|
<div class="text-xl font-bold text-green-700 dark:text-green-300">{{ formatDuration(personalMetrics.fastestResponse) }}</div>
|
|
<div class="text-sm text-green-600 dark:text-green-400">Fastest Response</div>
|
|
</div>
|
|
|
|
<div class="bg-red-50 dark:bg-red-900/10 p-4 rounded-lg border border-red-200 dark:border-red-800">
|
|
<div class="text-xl font-bold text-red-700 dark:text-red-300">{{ formatDuration(personalMetrics.slowestResponse) }}</div>
|
|
<div class="text-sm text-red-600 dark:text-red-400">Slowest Response</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Overdue Metrics -->
|
|
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Overdue Metrics</h4>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div class="bg-orange-50 dark:bg-orange-900/10 p-4 rounded-lg border border-orange-200 dark:border-orange-800">
|
|
<div class="text-xl font-bold text-orange-700 dark:text-orange-300">{{ personalMetrics.overdueRequests }}</div>
|
|
<div class="text-sm text-orange-600 dark:text-orange-400">Overdue Requests</div>
|
|
</div>
|
|
|
|
<div class="bg-orange-50 dark:bg-orange-900/10 p-4 rounded-lg border border-orange-200 dark:border-orange-800">
|
|
<div class="text-xl font-bold text-orange-700 dark:text-orange-300">{{ formatPercentage(personalMetrics.overduePercentage) }}</div>
|
|
<div class="text-sm text-orange-600 dark:text-orange-400">Overdue Percentage</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Department Metrics -->
|
|
<div v-if="props.showDepartmental" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
|
<div class="px-6 py-4 bg-purple-50 dark:bg-purple-900/10 border-b border-purple-200 dark:border-purple-800">
|
|
<h3 class="text-md font-medium text-purple-900 dark:text-purple-100">Department Metrics</h3>
|
|
<p class="text-sm text-purple-700 dark:text-purple-300">Department-wide performance</p>
|
|
</div>
|
|
|
|
<div class="p-6">
|
|
<!-- Status Metrics -->
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
|
|
<div class="bg-gray-50 dark:bg-gray-900/10 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
<div class="text-xl font-bold text-gray-900 dark:text-gray-100">{{ departmentMetrics.totalRequests }}</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Total Requests</div>
|
|
</div>
|
|
|
|
<div class="bg-yellow-50 dark:bg-yellow-900/10 p-4 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
|
<div class="text-xl font-bold text-yellow-700 dark:text-yellow-300">{{ departmentMetrics.pendingRequests }}</div>
|
|
<div class="text-sm text-yellow-600 dark:text-yellow-400">Pending</div>
|
|
</div>
|
|
|
|
<div class="bg-green-50 dark:bg-green-900/10 p-4 rounded-lg border border-green-200 dark:border-green-800">
|
|
<div class="text-xl font-bold text-green-700 dark:text-green-300">{{ departmentMetrics.approvedRequests }}</div>
|
|
<div class="text-sm text-green-600 dark:text-green-400">Approved</div>
|
|
</div>
|
|
|
|
<div class="bg-red-50 dark:bg-red-900/10 p-4 rounded-lg border border-red-200 dark:border-red-800">
|
|
<div class="text-xl font-bold text-red-700 dark:text-red-300">{{ departmentMetrics.rejectedRequests }}</div>
|
|
<div class="text-sm text-red-600 dark:text-red-400">Rejected</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Response Time Metrics -->
|
|
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Response Time Metrics</h4>
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
|
<div class="bg-indigo-50 dark:bg-indigo-900/10 p-4 rounded-lg border border-indigo-200 dark:border-indigo-800">
|
|
<div class="text-xl font-bold text-indigo-700 dark:text-indigo-300">{{ formatDuration(departmentMetrics.avgResponseTime) }}</div>
|
|
<div class="text-sm text-indigo-600 dark:text-indigo-400">Average Response</div>
|
|
</div>
|
|
|
|
<div class="bg-green-50 dark:bg-green-900/10 p-4 rounded-lg border border-green-200 dark:border-green-800">
|
|
<div class="text-xl font-bold text-green-700 dark:text-green-300">{{ formatDuration(departmentMetrics.fastestResponse) }}</div>
|
|
<div class="text-sm text-green-600 dark:text-green-400">Fastest Response</div>
|
|
</div>
|
|
|
|
<div class="bg-red-50 dark:bg-red-900/10 p-4 rounded-lg border border-red-200 dark:border-red-800">
|
|
<div class="text-xl font-bold text-red-700 dark:text-red-300">{{ formatDuration(departmentMetrics.slowestResponse) }}</div>
|
|
<div class="text-sm text-red-600 dark:text-red-400">Slowest Response</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Overdue Metrics -->
|
|
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">Overdue Metrics</h4>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div class="bg-orange-50 dark:bg-orange-900/10 p-4 rounded-lg border border-orange-200 dark:border-orange-800">
|
|
<div class="text-xl font-bold text-orange-700 dark:text-orange-300">{{ departmentMetrics.overdueRequests }}</div>
|
|
<div class="text-sm text-orange-600 dark:text-orange-400">Overdue Requests</div>
|
|
</div>
|
|
|
|
<div class="bg-orange-50 dark:bg-orange-900/10 p-4 rounded-lg border border-orange-200 dark:border-orange-800">
|
|
<div class="text-xl font-bold text-orange-700 dark:text-orange-300">{{ formatPercentage(departmentMetrics.overduePercentage) }}</div>
|
|
<div class="text-sm text-orange-600 dark:text-orange-400">Overdue Percentage</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.dms-access-request-tracker {
|
|
width: 100%;
|
|
}
|
|
</style> |