Md Afiq Iskandar 35a0bd412e Refactor Navigation and Enhance Form Builder UI
- 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.
2025-07-11 14:50:42 +08:00

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>