EDMS/components/dms/workflows/DMSAccessRequestTracker.vue
2025-05-31 14:58:52 +08:00

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>