- Simplified navigation structure by consolidating child routes for Process and Form Designers, improving clarity and accessibility. - Updated Form Builder and Process Builder header styles for a more modern look, including logo and title adjustments. - Enhanced button styles and layout in the Form Builder and Process Management pages for better user interaction. - Introduced new dashboard metrics and recent activity sections in Process Management, providing users with quick insights into their processes. - Improved overall responsiveness and visual consistency across the application.
405 lines
14 KiB
Vue
405 lines
14 KiB
Vue
<script setup>
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useProcessBuilderStore } from '~/stores/processBuilder';
|
|
|
|
// Define page meta
|
|
definePageMeta({
|
|
title: "Process Analytics",
|
|
description: "Analytics for process execution",
|
|
layout: "empty",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
});
|
|
|
|
// Initialize dependencies
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const processStore = useProcessBuilderStore();
|
|
|
|
// State
|
|
const loading = ref(false);
|
|
const currentProcess = ref(null);
|
|
const currentTab = ref('overview'); // 'overview', 'journey'
|
|
|
|
// Mock data for analytics (would come from API in real implementation)
|
|
const analyticsData = ref({
|
|
totalCases: 156,
|
|
activeCases: 23,
|
|
completedCases: 128,
|
|
averageCompletionTime: '4.2 days',
|
|
successRate: 89.7,
|
|
journeySteps: [
|
|
{
|
|
name: 'Process Start',
|
|
type: 'start',
|
|
completed: 156,
|
|
total: 156,
|
|
avgTime: '0 min'
|
|
},
|
|
{
|
|
name: 'Initial Request Form',
|
|
type: 'form',
|
|
completed: 142,
|
|
total: 156,
|
|
avgTime: '2.3 hours',
|
|
issues: 'Form validation errors (12 cases)'
|
|
},
|
|
{
|
|
name: 'Approval Gateway',
|
|
type: 'gateway',
|
|
completed: 142,
|
|
total: 142,
|
|
avgTime: '5 min'
|
|
},
|
|
{
|
|
name: 'Manager Approval',
|
|
type: 'form',
|
|
completed: 51,
|
|
total: 53,
|
|
avgTime: '1.2 days',
|
|
issues: 'Manager unavailable (2 cases)'
|
|
},
|
|
{
|
|
name: 'Final Confirmation',
|
|
type: 'form',
|
|
completed: 128,
|
|
total: 138,
|
|
avgTime: '15 min',
|
|
issues: 'Email notification failures (6 cases)'
|
|
},
|
|
{
|
|
name: 'Process Complete',
|
|
type: 'end',
|
|
completed: 128,
|
|
total: 128,
|
|
avgTime: '0 min'
|
|
}
|
|
],
|
|
topUsers: [
|
|
{ name: 'John Doe', completed: 10, department: 'Finance' },
|
|
{ name: 'Jane Smith', completed: 8, department: 'HR' },
|
|
{ name: 'Mike Johnson', completed: 13, department: 'Operations' }
|
|
]
|
|
});
|
|
|
|
// Tab options
|
|
const tabOptions = [
|
|
{ id: 'overview', label: 'Overview', icon: 'material-symbols:dashboard-outline' },
|
|
{ id: 'journey', label: 'Process Journey', icon: 'material-symbols:route' }
|
|
];
|
|
|
|
// Computed properties
|
|
const processId = computed(() => route.params.id);
|
|
|
|
const completionRate = computed(() => {
|
|
return ((analyticsData.value.completedCases / analyticsData.value.totalCases) * 100).toFixed(1);
|
|
});
|
|
|
|
// Helper functions
|
|
const formatDate = (isoString) => {
|
|
if (!isoString) return '';
|
|
const date = new Date(isoString);
|
|
return new Intl.DateTimeFormat('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
}).format(date);
|
|
};
|
|
|
|
const getNodeIcon = (nodeType) => {
|
|
switch (nodeType) {
|
|
case 'start': return 'material-symbols:play-circle-outline';
|
|
case 'form': return 'material-symbols:description-outline';
|
|
case 'gateway': return 'material-symbols:alt-route';
|
|
case 'end': return 'material-symbols:check-circle-outline';
|
|
default: return 'material-symbols:circle-outline';
|
|
}
|
|
};
|
|
|
|
const getNodeColor = (nodeType) => {
|
|
switch (nodeType) {
|
|
case 'start': return 'text-green-600';
|
|
case 'form': return 'text-blue-600';
|
|
case 'gateway': return 'text-purple-600';
|
|
case 'end': return 'text-green-600';
|
|
default: return 'text-gray-600';
|
|
}
|
|
};
|
|
|
|
const getCompletionPercentage = (step) => {
|
|
return ((step.completed / step.total) * 100).toFixed(0);
|
|
};
|
|
|
|
// Navigation functions
|
|
const goBack = () => {
|
|
router.push('/process-builder/manage');
|
|
};
|
|
|
|
const editProcess = () => {
|
|
router.push(`/process-builder?id=${processId.value}`);
|
|
};
|
|
|
|
// Load process data
|
|
const loadProcessData = async () => {
|
|
loading.value = true;
|
|
try {
|
|
// In real implementation, load process details from API
|
|
// For now, using mock data
|
|
currentProcess.value = {
|
|
id: processId.value,
|
|
name: 'Purchase Order Approval Process',
|
|
description: 'Streamlined purchase order approval workflow with automatic routing based on order amount.',
|
|
status: 'published',
|
|
category: 'Finance',
|
|
version: 3,
|
|
createdAt: '2024-01-01T00:00:00Z',
|
|
updatedAt: '2024-01-10T15:30:00Z',
|
|
creator: {
|
|
name: 'John Doe',
|
|
email: 'john.doe@company.com'
|
|
}
|
|
};
|
|
} catch (error) {
|
|
console.error('Error loading process data:', error);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// Load data on mount
|
|
onMounted(() => {
|
|
loadProcessData();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex flex-col h-screen bg-gray-50">
|
|
<!-- Header Bar -->
|
|
<header class="bg-white border-b border-gray-200 px-6 py-4 shadow-sm">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<Icon
|
|
@click="goBack"
|
|
name="ph:arrow-circle-left-duotone"
|
|
class="cursor-pointer w-6 h-6 hover:text-gray-600 text-gray-500"
|
|
/>
|
|
<div>
|
|
<h1 class="text-xl font-semibold text-gray-900">
|
|
{{ currentProcess?.name || 'Process Analytics' }}
|
|
</h1>
|
|
<p class="text-sm text-gray-500">
|
|
Analytics and journey visualization
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
<RsButton @click="editProcess" variant="secondary" size="sm">
|
|
<Icon name="material-symbols:edit" class="mr-1" />
|
|
Edit Process
|
|
</RsButton>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Tab Navigation -->
|
|
<div class="bg-white border-b border-gray-200 px-6">
|
|
<nav class="flex space-x-8">
|
|
<button
|
|
v-for="tab in tabOptions"
|
|
:key="tab.id"
|
|
@click="currentTab = tab.id"
|
|
:class="[
|
|
'flex items-center py-4 px-1 border-b-2 font-medium text-sm',
|
|
currentTab === tab.id
|
|
? 'border-blue-500 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
]"
|
|
>
|
|
<Icon :name="tab.icon" class="mr-2" />
|
|
{{ tab.label }}
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div class="flex-1 overflow-auto p-6">
|
|
<!-- Loading State -->
|
|
<div v-if="loading" class="flex justify-center items-center h-full">
|
|
<div class="text-center">
|
|
<Icon name="material-symbols:progress-activity" class="w-8 h-8 animate-spin text-blue-500 mx-auto mb-2" />
|
|
<p class="text-gray-500">Loading analytics...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Overview Tab -->
|
|
<div v-else-if="currentTab === 'overview'" class="space-y-6">
|
|
<!-- Key Metrics -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-600">Total Cases</p>
|
|
<p class="text-3xl font-bold text-gray-900">{{ analyticsData.totalCases }}</p>
|
|
<p class="text-sm text-blue-600 mt-1">
|
|
{{ analyticsData.activeCases }} active
|
|
</p>
|
|
</div>
|
|
<div class="p-3 bg-blue-50 rounded-lg">
|
|
<Icon name="material-symbols:cases-outline" class="w-6 h-6 text-blue-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-600">Completion Rate</p>
|
|
<p class="text-3xl font-bold text-gray-900">{{ completionRate }}%</p>
|
|
<p class="text-sm text-green-600 mt-1">
|
|
{{ analyticsData.completedCases }} completed
|
|
</p>
|
|
</div>
|
|
<div class="p-3 bg-green-50 rounded-lg">
|
|
<Icon name="material-symbols:check-circle-outline" class="w-6 h-6 text-green-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-600">Avg. Completion</p>
|
|
<p class="text-3xl font-bold text-gray-900">{{ analyticsData.averageCompletionTime }}</p>
|
|
</div>
|
|
<div class="p-3 bg-purple-50 rounded-lg">
|
|
<Icon name="material-symbols:timer-outline" class="w-6 h-6 text-purple-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-600">Success Rate</p>
|
|
<p class="text-3xl font-bold text-gray-900">{{ analyticsData.successRate }}%</p>
|
|
</div>
|
|
<div class="p-3 bg-orange-50 rounded-lg">
|
|
<Icon name="material-symbols:trending-up" class="w-6 h-6 text-orange-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Users -->
|
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Top Users</h3>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Department</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Completed Cases</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
<tr v-for="(user, index) in analyticsData.topUsers" :key="index">
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm font-medium text-gray-900">{{ user.name }}</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-gray-500">{{ user.department }}</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-gray-900">{{ user.completed }}</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Journey Tab -->
|
|
<div v-else-if="currentTab === 'journey'" class="space-y-6">
|
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-6">Process Journey Flow</h3>
|
|
|
|
<!-- Journey Timeline -->
|
|
<div class="space-y-6">
|
|
<div
|
|
v-for="(step, index) in analyticsData.journeySteps"
|
|
:key="index"
|
|
class="relative"
|
|
>
|
|
<!-- Connection Line -->
|
|
<div
|
|
v-if="index < analyticsData.journeySteps.length - 1"
|
|
class="absolute left-6 top-12 w-0.5 h-12 bg-gray-200"
|
|
></div>
|
|
|
|
<!-- Node Card -->
|
|
<div class="flex items-start gap-4">
|
|
<!-- Node Icon -->
|
|
<div :class="['p-3 rounded-lg bg-white border-2 border-white shadow-sm', getNodeColor(step.type)]">
|
|
<Icon :name="getNodeIcon(step.type)" class="w-6 h-6" />
|
|
</div>
|
|
|
|
<!-- Node Content -->
|
|
<div class="flex-1 bg-gray-50 rounded-lg p-4">
|
|
<div class="flex items-start justify-between mb-3">
|
|
<div>
|
|
<h4 class="font-semibold text-gray-900">{{ step.name }}</h4>
|
|
<p class="text-sm text-gray-600 capitalize">{{ step.type }}</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="text-sm font-medium text-gray-900">
|
|
{{ step.completed }}/{{ step.total }} completed
|
|
</p>
|
|
<p class="text-sm text-gray-500">
|
|
{{ getCompletionPercentage(step) }}% success
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Progress Bar -->
|
|
<div class="mb-3">
|
|
<div class="w-full bg-gray-200 rounded-full h-2">
|
|
<div
|
|
class="bg-blue-600 h-2 rounded-full"
|
|
:style="`width: ${getCompletionPercentage(step)}%`"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Node Statistics -->
|
|
<div class="grid grid-cols-2 gap-4 mb-3">
|
|
<div>
|
|
<p class="text-xs text-gray-500">Average Time</p>
|
|
<p class="font-medium text-gray-900">{{ step.avgTime }}</p>
|
|
</div>
|
|
<div v-if="step.issues">
|
|
<p class="text-xs text-gray-500">Issues</p>
|
|
<p class="font-medium text-red-600">{{ step.issues }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Simple transitions */
|
|
.transition-colors {
|
|
transition-property: color, background-color, border-color;
|
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
transition-duration: 150ms;
|
|
}
|
|
</style> |