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.
This commit is contained in:
parent
8919ac819c
commit
35a0bd412e
407
components/ProcessJourneyTimeline.vue
Normal file
407
components/ProcessJourneyTimeline.vue
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
journeyData: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
showMetrics: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
showIssues: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
showBranches: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
interactive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'medium', // 'small', 'medium', 'large'
|
||||||
|
validator: (value) => ['small', 'medium', 'large'].includes(value)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits(['nodeClick', 'issueClick']);
|
||||||
|
|
||||||
|
// State
|
||||||
|
const selectedNodeId = ref(null);
|
||||||
|
const hoveredNodeId = ref(null);
|
||||||
|
|
||||||
|
// Computed properties
|
||||||
|
const sizeClasses = computed(() => {
|
||||||
|
switch (props.size) {
|
||||||
|
case 'small':
|
||||||
|
return {
|
||||||
|
container: 'text-sm',
|
||||||
|
icon: 'w-4 h-4',
|
||||||
|
iconContainer: 'p-2',
|
||||||
|
card: 'p-3',
|
||||||
|
spacing: 'space-y-4'
|
||||||
|
};
|
||||||
|
case 'large':
|
||||||
|
return {
|
||||||
|
container: 'text-base',
|
||||||
|
icon: 'w-8 h-8',
|
||||||
|
iconContainer: 'p-4',
|
||||||
|
card: 'p-6',
|
||||||
|
spacing: 'space-y-10'
|
||||||
|
};
|
||||||
|
default: // medium
|
||||||
|
return {
|
||||||
|
container: 'text-sm',
|
||||||
|
icon: 'w-6 h-6',
|
||||||
|
iconContainer: 'p-3',
|
||||||
|
card: 'p-4',
|
||||||
|
spacing: 'space-y-8'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const getNodeIcon = (nodeType) => {
|
||||||
|
switch (nodeType) {
|
||||||
|
case 'start': return 'material-symbols:play-circle-outline';
|
||||||
|
case 'form': return 'material-symbols:description-outline';
|
||||||
|
case 'task': return 'material-symbols:task-outline';
|
||||||
|
case 'gateway': return 'material-symbols:alt-route';
|
||||||
|
case 'exclusive-gateway': return 'material-symbols:call-split';
|
||||||
|
case 'parallel-gateway': return 'material-symbols:merge';
|
||||||
|
case 'merge': return 'material-symbols:merge';
|
||||||
|
case 'api': return 'material-symbols:api';
|
||||||
|
case 'script': return 'material-symbols:code';
|
||||||
|
case 'timer': return 'material-symbols:timer-outline';
|
||||||
|
case 'message': return 'material-symbols:mail-outline';
|
||||||
|
case 'end': return 'material-symbols:check-circle-outline';
|
||||||
|
default: return 'material-symbols:circle-outline';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNodeColor = (nodeType, dropoffRate = 0) => {
|
||||||
|
// Color based on dropoff rate for critical assessment
|
||||||
|
if (dropoffRate > 15) return 'text-red-600 bg-red-50 border-red-200';
|
||||||
|
if (dropoffRate > 10) return 'text-orange-600 bg-orange-50 border-orange-200';
|
||||||
|
if (dropoffRate > 5) return 'text-yellow-600 bg-yellow-50 border-yellow-200';
|
||||||
|
|
||||||
|
// Default colors by node type
|
||||||
|
switch (nodeType) {
|
||||||
|
case 'start': return 'text-green-600 bg-green-50 border-green-200';
|
||||||
|
case 'form': return 'text-blue-600 bg-blue-50 border-blue-200';
|
||||||
|
case 'task': return 'text-indigo-600 bg-indigo-50 border-indigo-200';
|
||||||
|
case 'gateway': return 'text-purple-600 bg-purple-50 border-purple-200';
|
||||||
|
case 'exclusive-gateway': return 'text-purple-600 bg-purple-50 border-purple-200';
|
||||||
|
case 'parallel-gateway': return 'text-orange-600 bg-orange-50 border-orange-200';
|
||||||
|
case 'merge': return 'text-orange-600 bg-orange-50 border-orange-200';
|
||||||
|
case 'api': return 'text-cyan-600 bg-cyan-50 border-cyan-200';
|
||||||
|
case 'script': return 'text-slate-600 bg-slate-50 border-slate-200';
|
||||||
|
case 'timer': return 'text-amber-600 bg-amber-50 border-amber-200';
|
||||||
|
case 'message': return 'text-teal-600 bg-teal-50 border-teal-200';
|
||||||
|
case 'end': return 'text-green-600 bg-green-50 border-green-200';
|
||||||
|
default: return 'text-gray-600 bg-gray-50 border-gray-200';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDropoffColor = (rate) => {
|
||||||
|
if (rate === 0) return 'text-green-600';
|
||||||
|
if (rate < 5) return 'text-yellow-600';
|
||||||
|
if (rate < 10) return 'text-orange-600';
|
||||||
|
return 'text-red-600';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCompletionRate = (node) => {
|
||||||
|
if (node.totalEntered === 0) return 0;
|
||||||
|
return ((node.totalCompleted / node.totalEntered) * 100).toFixed(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDuration = (timeString) => {
|
||||||
|
if (!timeString) return 'N/A';
|
||||||
|
|
||||||
|
// Parse various time formats
|
||||||
|
if (timeString.includes('minute')) {
|
||||||
|
const minutes = parseInt(timeString);
|
||||||
|
if (minutes < 60) return `${minutes}m`;
|
||||||
|
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeString.includes('hour')) {
|
||||||
|
const hours = parseFloat(timeString);
|
||||||
|
return `${hours}h`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeString.includes('day')) {
|
||||||
|
const days = parseFloat(timeString);
|
||||||
|
if (days < 1) {
|
||||||
|
const hours = Math.round(days * 24);
|
||||||
|
return `${hours}h`;
|
||||||
|
}
|
||||||
|
return `${days}d`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeString;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIssueIcon = (issue) => {
|
||||||
|
if (issue.toLowerCase().includes('validation')) return 'material-symbols:error-outline';
|
||||||
|
if (issue.toLowerCase().includes('timeout')) return 'material-symbols:timer-off';
|
||||||
|
if (issue.toLowerCase().includes('unavailable')) return 'material-symbols:person-off';
|
||||||
|
if (issue.toLowerCase().includes('notification')) return 'material-symbols:notifications-off';
|
||||||
|
if (issue.toLowerCase().includes('system')) return 'material-symbols:warning-outline';
|
||||||
|
if (issue.toLowerCase().includes('session')) return 'material-symbols:logout';
|
||||||
|
return 'material-symbols:info-outline';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodeClick = (node) => {
|
||||||
|
if (props.interactive) {
|
||||||
|
selectedNodeId.value = selectedNodeId.value === node.nodeId ? null : node.nodeId;
|
||||||
|
emit('nodeClick', node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIssueClick = (issue, node) => {
|
||||||
|
if (props.interactive) {
|
||||||
|
emit('issueClick', { issue, node });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="['process-journey-timeline', sizeClasses.container]">
|
||||||
|
<!-- Journey Flow -->
|
||||||
|
<div :class="['journey-flow', sizeClasses.spacing]">
|
||||||
|
<div
|
||||||
|
v-for="(node, index) in journeyData"
|
||||||
|
:key="node.nodeId"
|
||||||
|
class="relative"
|
||||||
|
@mouseover="hoveredNodeId = node.nodeId"
|
||||||
|
@mouseleave="hoveredNodeId = null"
|
||||||
|
>
|
||||||
|
<!-- Connection Line -->
|
||||||
|
<div
|
||||||
|
v-if="index < journeyData.length - 1"
|
||||||
|
class="absolute left-6 w-0.5 bg-gray-200 z-0"
|
||||||
|
:class="[
|
||||||
|
size === 'small' ? 'top-8 h-8' :
|
||||||
|
size === 'large' ? 'top-16 h-24' :
|
||||||
|
'top-12 h-16'
|
||||||
|
]"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Node Container -->
|
||||||
|
<div
|
||||||
|
class="relative z-10 flex items-start gap-4"
|
||||||
|
:class="[
|
||||||
|
interactive && 'cursor-pointer',
|
||||||
|
selectedNodeId === node.nodeId && 'ring-2 ring-blue-500 ring-offset-2 rounded-lg'
|
||||||
|
]"
|
||||||
|
@click="handleNodeClick(node)"
|
||||||
|
>
|
||||||
|
<!-- Node Icon -->
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'rounded-lg border-2 border-white shadow-sm transition-all duration-200 flex-shrink-0',
|
||||||
|
sizeClasses.iconContainer,
|
||||||
|
getNodeColor(node.nodeType, node.dropoffRate),
|
||||||
|
hoveredNodeId === node.nodeId && 'scale-110 shadow-md'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
:name="getNodeIcon(node.nodeType)"
|
||||||
|
:class="sizeClasses.icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Node Content -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<!-- Node Header -->
|
||||||
|
<div class="flex items-start justify-between mb-2">
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<h4 class="font-semibold text-gray-900 truncate">{{ node.nodeName }}</h4>
|
||||||
|
<p class="text-xs text-gray-600 capitalize">{{ node.nodeType.replace('-', ' ') }} node</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="showMetrics" class="text-right ml-4 flex-shrink-0">
|
||||||
|
<p class="text-xs font-medium text-gray-900">
|
||||||
|
{{ node.totalCompleted }}/{{ node.totalEntered }}
|
||||||
|
</p>
|
||||||
|
<p :class="['text-xs font-medium', getDropoffColor(node.dropoffRate)]">
|
||||||
|
{{ node.dropoffRate }}% loss
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<div v-if="showMetrics" class="mb-3">
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
class="h-2 rounded-full transition-all duration-300"
|
||||||
|
:class="[
|
||||||
|
node.dropoffRate > 10 ? 'bg-red-500' :
|
||||||
|
node.dropoffRate > 5 ? 'bg-orange-500' :
|
||||||
|
'bg-green-500'
|
||||||
|
]"
|
||||||
|
:style="`width: ${getCompletionRate(node)}%`"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
||||||
|
<span>{{ getCompletionRate(node) }}% success</span>
|
||||||
|
<span>{{ formatDuration(node.averageTime) }} avg</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Node Metrics Grid -->
|
||||||
|
<div v-if="showMetrics" :class="['grid gap-2 mb-3', size === 'small' ? 'grid-cols-2' : 'grid-cols-4']">
|
||||||
|
<div class="bg-gray-50 rounded p-2">
|
||||||
|
<p class="text-xs text-gray-500">Entered</p>
|
||||||
|
<p class="font-medium text-gray-900">{{ node.totalEntered.toLocaleString() }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 rounded p-2">
|
||||||
|
<p class="text-xs text-gray-500">Completed</p>
|
||||||
|
<p class="font-medium text-gray-900">{{ node.totalCompleted.toLocaleString() }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="size !== 'small'" class="bg-gray-50 rounded p-2">
|
||||||
|
<p class="text-xs text-gray-500">Avg. Time</p>
|
||||||
|
<p class="font-medium text-gray-900">{{ formatDuration(node.averageTime) }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="size !== 'small'" class="bg-gray-50 rounded p-2">
|
||||||
|
<p class="text-xs text-gray-500">Success Rate</p>
|
||||||
|
<p class="font-medium text-gray-900">{{ getCompletionRate(node) }}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gateway Branches -->
|
||||||
|
<div v-if="showBranches && node.branches && node.branches.length > 0" class="mb-3">
|
||||||
|
<p class="text-xs text-gray-500 mb-2 font-medium">Decision Branches</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="branch in node.branches"
|
||||||
|
:key="branch.condition"
|
||||||
|
class="flex items-center justify-between bg-blue-50 rounded-lg p-2 border border-blue-100"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||||
|
<Icon name="material-symbols:arrow-forward" class="w-3 h-3 text-blue-600 flex-shrink-0" />
|
||||||
|
<span class="text-sm text-blue-900 truncate">{{ branch.condition }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 ml-3 flex-shrink-0">
|
||||||
|
<span class="text-sm font-medium text-blue-900">{{ branch.taken }}</span>
|
||||||
|
<span class="text-xs text-blue-700 bg-blue-100 px-2 py-1 rounded">
|
||||||
|
{{ branch.percentage }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Issues Section -->
|
||||||
|
<div v-if="showIssues && node.issues && node.issues.length > 0" class="bg-red-50 border border-red-200 rounded-lg p-3">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<Icon name="material-symbols:warning" class="w-4 h-4 text-red-600" />
|
||||||
|
<p class="text-xs font-medium text-red-800">Issues Identified</p>
|
||||||
|
</div>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li
|
||||||
|
v-for="(issue, issueIndex) in node.issues"
|
||||||
|
:key="issueIndex"
|
||||||
|
class="flex items-start gap-2 group"
|
||||||
|
:class="interactive && 'cursor-pointer hover:bg-red-100 rounded p-1 -m-1'"
|
||||||
|
@click="handleIssueClick(issue, node)"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
:name="getIssueIcon(issue)"
|
||||||
|
class="w-3 h-3 text-red-600 mt-0.5 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-red-700 flex-1">{{ issue }}</span>
|
||||||
|
<Icon
|
||||||
|
v-if="interactive"
|
||||||
|
name="material-symbols:chevron-right"
|
||||||
|
class="w-3 h-3 text-red-500 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Success Indicator for nodes with no issues -->
|
||||||
|
<div v-else-if="showIssues && node.dropoffRate === 0" class="bg-green-50 border border-green-200 rounded-lg p-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Icon name="material-symbols:check-circle" class="w-4 h-4 text-green-600" />
|
||||||
|
<p class="text-xs font-medium text-green-800">No issues detected</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Legend -->
|
||||||
|
<div v-if="size !== 'small'" class="mt-6 bg-gray-50 rounded-lg p-4">
|
||||||
|
<h5 class="text-xs font-medium text-gray-900 mb-3">Legend</h5>
|
||||||
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 text-xs">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-3 h-3 bg-green-500 rounded"></div>
|
||||||
|
<span class="text-gray-700">Good (0-5% loss)</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-3 h-3 bg-yellow-500 rounded"></div>
|
||||||
|
<span class="text-gray-700">Warning (5-10% loss)</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-3 h-3 bg-orange-500 rounded"></div>
|
||||||
|
<span class="text-gray-700">Concern (10-15% loss)</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-3 h-3 bg-red-500 rounded"></div>
|
||||||
|
<span class="text-gray-700">Critical (>15% loss)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.process-journey-timeline {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-flow {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth transitions for all interactive elements */
|
||||||
|
.transition-all {
|
||||||
|
transition-property: all;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effects */
|
||||||
|
.group:hover .group-hover\:opacity-100 {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar for overflow content */
|
||||||
|
.journey-flow::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-flow::-webkit-scrollbar-track {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-flow::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-flow::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
</style>
|
@ -16,49 +16,15 @@ export default [
|
|||||||
header: "Design & Build",
|
header: "Design & Build",
|
||||||
description: "Create and design your workflows and forms",
|
description: "Create and design your workflows and forms",
|
||||||
child: [
|
child: [
|
||||||
{
|
|
||||||
title: "Process Designer",
|
|
||||||
icon: "material-symbols:account-tree",
|
|
||||||
child: [
|
|
||||||
{
|
|
||||||
title: "Visual Builder",
|
|
||||||
path: "/process-builder",
|
|
||||||
child: [],
|
|
||||||
meta: {
|
|
||||||
description: "Design workflows with drag-and-drop interface"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Manage Processes",
|
|
||||||
path: "/process-builder/manage",
|
|
||||||
child: [],
|
|
||||||
meta: {
|
|
||||||
description: "View and manage all your processes"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Form Designer",
|
title: "Form Designer",
|
||||||
icon: "material-symbols:dynamic-form",
|
icon: "material-symbols:dynamic-form",
|
||||||
child: [
|
path: "/form-builder/manage",
|
||||||
{
|
},
|
||||||
title: "Form Builder",
|
{
|
||||||
path: "/form-builder",
|
title: "Process Designer",
|
||||||
child: [],
|
icon: "material-symbols:account-tree",
|
||||||
meta: {
|
path: "/process-builder/manage",
|
||||||
description: "Create dynamic forms with advanced components"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Manage Forms",
|
|
||||||
path: "/form-builder/manage",
|
|
||||||
child: [],
|
|
||||||
meta: {
|
|
||||||
description: "View and manage all your forms"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -75,16 +41,16 @@ export default [
|
|||||||
path: "/execution",
|
path: "/execution",
|
||||||
child: [],
|
child: [],
|
||||||
meta: {
|
meta: {
|
||||||
description: "Monitor active processes and performance metrics"
|
description: "Monitor active processes and performance metrics",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Start New Process",
|
title: "Start New Process",
|
||||||
path: "/execution/new-case",
|
path: "/execution/new-case",
|
||||||
child: [],
|
child: [],
|
||||||
meta: {
|
meta: {
|
||||||
description: "Initiate a new process instance"
|
description: "Initiate a new process instance",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -97,16 +63,16 @@ export default [
|
|||||||
path: "/execution/inbox",
|
path: "/execution/inbox",
|
||||||
child: [],
|
child: [],
|
||||||
meta: {
|
meta: {
|
||||||
description: "View and complete assigned tasks"
|
description: "View and complete assigned tasks",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Process History",
|
title: "Process History",
|
||||||
path: "/execution/history",
|
path: "/execution/history",
|
||||||
child: [],
|
child: [],
|
||||||
meta: {
|
meta: {
|
||||||
description: "Review completed processes and audit trails"
|
description: "Review completed processes and audit trails",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -2,22 +2,28 @@
|
|||||||
<div class="flex flex-col h-screen bg-gray-50">
|
<div class="flex flex-col h-screen bg-gray-50">
|
||||||
<!-- Header Bar -->
|
<!-- Header Bar -->
|
||||||
<header
|
<header
|
||||||
class="bg-gray-800 px-6 py-3 flex items-center justify-between text-white shadow-lg border-b border-gray-700"
|
class="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between shadow-sm"
|
||||||
>
|
>
|
||||||
<!-- Left section - Logo and navigation -->
|
<!-- Left section - Logo and navigation -->
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<Icon
|
<Icon
|
||||||
@click="navigateTo('/', { external: true })"
|
@click="navigateTo('/', { external: true })"
|
||||||
name="ph:arrow-circle-left-duotone"
|
name="ph:arrow-circle-left-duotone"
|
||||||
class="cursor-pointer w-6 h-6 hover:text-gray-300 transition-colors"
|
class="cursor-pointer w-6 h-6 hover:text-gray-600 text-gray-500"
|
||||||
/>
|
/>
|
||||||
<img
|
<div class="flex items-center gap-3">
|
||||||
src="@/assets/img/logo/logo-word-white.svg"
|
<img
|
||||||
alt="Corrad Logo"
|
src="@/assets/img/logo/logo-word-black.svg"
|
||||||
class="h-7"
|
alt="Corrad Logo"
|
||||||
/>
|
class="h-8"
|
||||||
<div v-if="isPreview" class="bg-blue-500 text-white text-xs px-3 py-1 rounded-full ml-2 font-medium">
|
/>
|
||||||
Preview Mode
|
<div class="border-l border-gray-300 pl-3">
|
||||||
|
<h1 class="text-xl font-semibold text-gray-900">Form Builder</h1>
|
||||||
|
<p class="text-sm text-gray-500">Create and manage form layouts</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="isPreview" class="bg-blue-500 text-white text-xs px-3 py-1 rounded-full ml-2 font-medium">
|
||||||
|
Preview Mode
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -41,25 +47,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right section - Actions -->
|
<!-- Right section - Actions -->
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center">
|
||||||
<!-- Primary Action Group -->
|
<!-- Primary Action Group -->
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center mr-1 md:mr-2 border-r border-gray-200 pr-1 md:pr-2">
|
||||||
<RsButton v-if="!isPreview" @click="handleSave" variant="primary" size="sm">
|
<RsButton v-if="!isPreview" @click="handleSave" variant="primary" size="sm" class="mr-1 md:mr-2">
|
||||||
<Icon name="material-symbols:save" class="mr-1.5" />
|
<Icon name="material-symbols:save" class="mr-0 md:mr-1" />
|
||||||
Save
|
<span class="hidden md:inline">Save</span>
|
||||||
</RsButton>
|
</RsButton>
|
||||||
|
|
||||||
<RsButton @click="togglePreview" :variant="isPreview ? 'primary' : 'secondary'" size="sm">
|
<RsButton @click="togglePreview" :variant="isPreview ? 'primary' : 'secondary'" size="sm">
|
||||||
<Icon :name="isPreview ? 'material-symbols:edit' : 'material-symbols:preview'" class="mr-1.5" />
|
<Icon :name="isPreview ? 'material-symbols:edit' : 'material-symbols:preview'" class="mr-0 md:mr-1" />
|
||||||
{{ isPreview ? 'Edit' : 'Preview' }}
|
<span class="hidden md:inline">{{ isPreview ? 'Edit' : 'Preview' }}</span>
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Secondary Action Group -->
|
<!-- Secondary Action Group -->
|
||||||
<div v-if="!isPreview" class="flex items-center space-x-2">
|
<div v-if="!isPreview" class="mr-1 md:mr-2 border-r border-gray-200 pr-1 md:pr-2 hidden md:block">
|
||||||
<RsButton @click="showTemplatesModal = true" variant="secondary" size="sm">
|
<RsButton @click="showTemplatesModal = true" variant="secondary" size="sm" class="mr-1 md:mr-2">
|
||||||
<Icon name="material-symbols:description-outline" class="mr-1.5" />
|
<Icon name="material-symbols:description-outline" class="mr-0 md:mr-1" />
|
||||||
Templates
|
<span class="hidden lg:inline">Templates</span>
|
||||||
</RsButton>
|
</RsButton>
|
||||||
|
|
||||||
<!-- Form History button - only show if form is saved -->
|
<!-- Form History button - only show if form is saved -->
|
||||||
@ -69,18 +75,18 @@
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols:history" class="mr-1.5" />
|
<Icon name="material-symbols:history" class="mr-0 md:mr-1" />
|
||||||
History
|
<span class="hidden lg:inline">History</span>
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- More Actions Dropdown -->
|
<!-- More Actions Dropdown -->
|
||||||
<div v-if="!isPreview" class="relative">
|
<div v-if="!isPreview" class="relative">
|
||||||
<RsButton @click="showDropdown = !showDropdown" variant="tertiary" size="sm" class="flex items-center">
|
<RsButton @click="showDropdown = !showDropdown" variant="tertiary" size="sm" class="flex items-center">
|
||||||
<Icon name="material-symbols:more-vert" class="w-5 h-5" />
|
<Icon name="material-symbols:more-vert" class="w-4 h-4 md:w-5 md:h-5" />
|
||||||
</RsButton>
|
</RsButton>
|
||||||
|
|
||||||
<div v-if="showDropdown" class="dropdown-menu absolute right-0 mt-2 bg-white rounded-lg shadow-lg py-2 z-10 w-48 text-gray-800 border border-gray-200">
|
<div v-if="showDropdown" class="dropdown-menu absolute right-0 mt-2 bg-white rounded shadow-lg py-1 z-10 w-48 text-gray-800">
|
||||||
<button @click="showFormSettings = true; showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-50 flex items-center transition-colors">
|
<button @click="showFormSettings = true; showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-50 flex items-center transition-colors">
|
||||||
<Icon name="material-symbols:code" class="mr-3 w-4 h-4 text-gray-500" />
|
<Icon name="material-symbols:code" class="mr-3 w-4 h-4 text-gray-500" />
|
||||||
<span>Form Settings</span>
|
<span>Form Settings</span>
|
||||||
|
@ -2,26 +2,28 @@
|
|||||||
<div class="flex flex-col h-screen bg-gray-50">
|
<div class="flex flex-col h-screen bg-gray-50">
|
||||||
<!-- Header Bar -->
|
<!-- Header Bar -->
|
||||||
<header
|
<header
|
||||||
class="bg-gray-800 px-4 py-4 flex items-center justify-between text-white shadow-md"
|
class="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between shadow-sm"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-4">
|
||||||
<Icon
|
<Icon
|
||||||
@click="goToDashboard"
|
@click="goToDashboard"
|
||||||
name="ph:arrow-circle-left-duotone"
|
name="ph:arrow-circle-left-duotone"
|
||||||
class="cursor-pointer w-6 h-6 hover:text-gray-300"
|
class="cursor-pointer w-6 h-6 hover:text-gray-600 text-gray-500"
|
||||||
/>
|
/>
|
||||||
<img
|
<div class="flex items-center gap-3">
|
||||||
src="@/assets/img/logo/logo-word-white.svg"
|
<img
|
||||||
alt="Corrad Logo"
|
src="@/assets/img/logo/logo-word-black.svg"
|
||||||
class="h-7"
|
alt="Corrad Logo"
|
||||||
/>
|
class="h-8"
|
||||||
</div>
|
/>
|
||||||
|
<div class="border-l border-gray-300 pl-3">
|
||||||
<div class="flex items-center gap-3">
|
<h1 class="text-xl font-semibold text-gray-900">Form Management</h1>
|
||||||
<h1 class="text-xl font-semibold text-white">Form Management</h1>
|
<p class="text-sm text-gray-500">Manage and organize your forms</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center">
|
||||||
<RsButton @click="createNewForm" variant="primary" size="sm">
|
<RsButton @click="createNewForm" variant="primary" size="sm">
|
||||||
<Icon name="material-symbols:add" class="mr-1" />
|
<Icon name="material-symbols:add" class="mr-1" />
|
||||||
Create New Form
|
Create New Form
|
||||||
|
405
pages/process-builder/analytics/[id].vue
Normal file
405
pages/process-builder/analytics/[id].vue
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
<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>
|
@ -2087,27 +2087,33 @@ const sendToBack = () => {
|
|||||||
<div class="process-builder flex flex-col h-screen bg-white">
|
<div class="process-builder flex flex-col h-screen bg-white">
|
||||||
<!-- Header Bar -->
|
<!-- Header Bar -->
|
||||||
<header
|
<header
|
||||||
class="bg-gray-800 px-2 md:px-4 py-2 md:py-4 flex items-center justify-between text-white shadow-md"
|
class="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between shadow-sm"
|
||||||
>
|
>
|
||||||
<!-- Left section - Logo and navigation -->
|
<!-- Left section - Logo and navigation -->
|
||||||
<div class="flex items-center gap-2 md:gap-3">
|
<div class="flex items-center gap-4">
|
||||||
<Icon
|
<Icon
|
||||||
@click="confirmNavigation('/process-builder/manage')"
|
@click="confirmNavigation('/process-builder/manage')"
|
||||||
name="ph:arrow-circle-left-duotone"
|
name="ph:arrow-circle-left-duotone"
|
||||||
class="cursor-pointer w-5 h-5 md:w-6 md:h-6"
|
class="cursor-pointer w-6 h-6 hover:text-gray-600 text-gray-500"
|
||||||
/>
|
|
||||||
<img
|
|
||||||
src="@/assets/img/logo/logo-word-white.svg"
|
|
||||||
alt="Corrad Logo"
|
|
||||||
class="h-5 md:h-7 hidden sm:block"
|
|
||||||
/>
|
/>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<img
|
||||||
|
src="@/assets/img/logo/logo-word-black.svg"
|
||||||
|
alt="Corrad Logo"
|
||||||
|
class="h-8"
|
||||||
|
/>
|
||||||
|
<div class="border-l border-gray-300 pl-3">
|
||||||
|
<h1 class="text-xl font-semibold text-gray-900">Process Builder</h1>
|
||||||
|
<p class="text-sm text-gray-500">Create business processes with drag and drop</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Mobile panel toggles -->
|
<!-- Mobile panel toggles -->
|
||||||
<div v-if="hasCurrentProcess && (isMobile || isTablet)" class="flex items-center gap-1 ml-2">
|
<div v-if="hasCurrentProcess && (isMobile || isTablet)" class="flex items-center gap-1 ml-2">
|
||||||
<RsButton
|
<RsButton
|
||||||
@click="toggleLeftPanel"
|
@click="toggleLeftPanel"
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
size="sm"
|
size="sm"
|
||||||
:class="{ 'bg-gray-700': showLeftPanel }"
|
:class="{ 'bg-gray-100': showLeftPanel }"
|
||||||
class="p-1"
|
class="p-1"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols:widgets" class="w-4 h-4" />
|
<Icon name="material-symbols:widgets" class="w-4 h-4" />
|
||||||
@ -2116,7 +2122,7 @@ const sendToBack = () => {
|
|||||||
@click="toggleRightPanel"
|
@click="toggleRightPanel"
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
size="sm"
|
size="sm"
|
||||||
:class="{ 'bg-gray-700': showRightPanel }"
|
:class="{ 'bg-gray-100': showRightPanel }"
|
||||||
class="p-1"
|
class="p-1"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols:tune" class="w-4 h-4" />
|
<Icon name="material-symbols:tune" class="w-4 h-4" />
|
||||||
@ -2145,7 +2151,7 @@ const sendToBack = () => {
|
|||||||
<!-- Right section - Actions -->
|
<!-- Right section - Actions -->
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<!-- Primary actions -->
|
<!-- Primary actions -->
|
||||||
<div class="flex items-center mr-1 md:mr-2 border-r border-gray-600 pr-1 md:pr-2">
|
<div class="flex items-center mr-1 md:mr-2 border-r border-gray-200 pr-1 md:pr-2">
|
||||||
<RsButton @click="saveProcess" variant="primary" size="sm" class="mr-1 md:mr-2" :disabled="!hasCurrentProcess || isSaving">
|
<RsButton @click="saveProcess" variant="primary" size="sm" class="mr-1 md:mr-2" :disabled="!hasCurrentProcess || isSaving">
|
||||||
<Icon v-if="isSaving" name="material-symbols:progress-activity" class="mr-0 md:mr-1 animate-spin" />
|
<Icon v-if="isSaving" name="material-symbols:progress-activity" class="mr-0 md:mr-1 animate-spin" />
|
||||||
<Icon v-else name="material-symbols:save" class="mr-0 md:mr-1" />
|
<Icon v-else name="material-symbols:save" class="mr-0 md:mr-1" />
|
||||||
@ -2154,7 +2160,7 @@ const sendToBack = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Templates button - Hidden on mobile -->
|
<!-- Templates button - Hidden on mobile -->
|
||||||
<div class="mr-1 md:mr-2 border-r border-gray-600 pr-1 md:pr-2 hidden md:block">
|
<div class="mr-1 md:mr-2 border-r border-gray-200 pr-1 md:pr-2 hidden md:block">
|
||||||
<RsButton @click="showTemplatesModal = true" variant="secondary" size="sm" :disabled="!hasCurrentProcess">
|
<RsButton @click="showTemplatesModal = true" variant="secondary" size="sm" :disabled="!hasCurrentProcess">
|
||||||
<Icon name="material-symbols:description-outline" class="mr-0 md:mr-1" />
|
<Icon name="material-symbols:description-outline" class="mr-0 md:mr-1" />
|
||||||
<span class="hidden lg:inline">Templates</span>
|
<span class="hidden lg:inline">Templates</span>
|
||||||
@ -2162,7 +2168,7 @@ const sendToBack = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Process History button - Hidden on mobile, only show if process is saved -->
|
<!-- Process History button - Hidden on mobile, only show if process is saved -->
|
||||||
<div v-if="processStore.currentProcess && processStore.currentProcess.id" class="mr-1 md:mr-2 border-r border-gray-600 pr-1 md:pr-2 hidden md:block">
|
<div v-if="processStore.currentProcess && processStore.currentProcess.id" class="mr-1 md:mr-2 border-r border-gray-200 pr-1 md:pr-2 hidden md:block">
|
||||||
<RsButton @click="showProcessHistoryModal = true" variant="secondary" size="sm">
|
<RsButton @click="showProcessHistoryModal = true" variant="secondary" size="sm">
|
||||||
<Icon name="material-symbols:history" class="mr-0 md:mr-1" />
|
<Icon name="material-symbols:history" class="mr-0 md:mr-1" />
|
||||||
<span class="hidden lg:inline">History</span>
|
<span class="hidden lg:inline">History</span>
|
||||||
|
@ -5,8 +5,8 @@ import { useRouter } from 'vue-router';
|
|||||||
|
|
||||||
// Define page meta
|
// Define page meta
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
title: "Process Management",
|
title: "Process Management Dashboard",
|
||||||
description: "Manage your business processes",
|
description: "Manage and monitor your business processes",
|
||||||
layout: "empty",
|
layout: "empty",
|
||||||
middleware: ["auth"],
|
middleware: ["auth"],
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
@ -22,10 +22,31 @@ const loading = ref(false);
|
|||||||
const showDeleteConfirm = ref(false);
|
const showDeleteConfirm = ref(false);
|
||||||
const processToDelete = ref(null);
|
const processToDelete = ref(null);
|
||||||
const statusFilter = ref('');
|
const statusFilter = ref('');
|
||||||
|
const categoryFilter = ref('');
|
||||||
const sortBy = ref('processCreatedDate');
|
const sortBy = ref('processCreatedDate');
|
||||||
const sortOrder = ref('desc');
|
const sortOrder = ref('desc');
|
||||||
|
const currentView = ref('dashboard'); // 'dashboard', 'list', 'analytics'
|
||||||
|
|
||||||
// Status options for filtering
|
// Dashboard metrics and data
|
||||||
|
const dashboardMetrics = ref({
|
||||||
|
totalProcesses: 0,
|
||||||
|
publishedProcesses: 0,
|
||||||
|
draftProcesses: 0,
|
||||||
|
totalCases: 0,
|
||||||
|
activeCases: 0,
|
||||||
|
completedCases: 0,
|
||||||
|
totalTasks: 0,
|
||||||
|
pendingTasks: 0,
|
||||||
|
averageCompletionTime: 0,
|
||||||
|
successRate: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const recentActivity = ref([]);
|
||||||
|
const processChartData = ref([]);
|
||||||
|
const caseChartData = ref([]);
|
||||||
|
const dashboardLoading = ref(false);
|
||||||
|
|
||||||
|
// Status and category options
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{ value: '', label: 'All Status' },
|
{ value: '', label: 'All Status' },
|
||||||
{ value: 'draft', label: 'Draft' },
|
{ value: 'draft', label: 'Draft' },
|
||||||
@ -34,15 +55,87 @@ const statusOptions = [
|
|||||||
{ value: 'deleted', label: 'Deleted' }
|
{ value: 'deleted', label: 'Deleted' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Filtered processes - now just returns the processes from store since filtering happens on backend
|
const categoryOptions = [
|
||||||
|
{ value: '', label: 'All Categories' },
|
||||||
|
{ value: 'HR', label: 'Human Resources' },
|
||||||
|
{ value: 'Finance', label: 'Finance' },
|
||||||
|
{ value: 'IT', label: 'Information Technology' },
|
||||||
|
{ value: 'Operations', label: 'Operations' },
|
||||||
|
{ value: 'Procurement', label: 'Procurement' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Filtered processes
|
||||||
const filteredProcesses = computed(() => {
|
const filteredProcesses = computed(() => {
|
||||||
return processStore.processes;
|
return processStore.processes;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load dashboard summary data from API
|
||||||
|
const loadDashboardData = async () => {
|
||||||
|
dashboardLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await $fetch('/api/process/dashboard/summary');
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
// Update metrics
|
||||||
|
dashboardMetrics.value = {
|
||||||
|
totalProcesses: data.overview.totalProcesses,
|
||||||
|
publishedProcesses: data.overview.publishedProcesses,
|
||||||
|
draftProcesses: data.overview.draftProcesses,
|
||||||
|
totalCases: data.overview.totalCases,
|
||||||
|
activeCases: data.overview.activeCases,
|
||||||
|
completedCases: data.overview.completedCases,
|
||||||
|
totalTasks: data.overview.totalTasks,
|
||||||
|
pendingTasks: data.overview.pendingTasks || data.overview.totalTasks,
|
||||||
|
averageCompletionTime: data.overview.averageCompletionTime,
|
||||||
|
successRate: data.overview.successRate
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update chart data
|
||||||
|
processChartData.value = data.charts.processDistribution;
|
||||||
|
caseChartData.value = data.charts.caseStatus;
|
||||||
|
|
||||||
|
// Update recent activity
|
||||||
|
recentActivity.value = data.recentActivity;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading dashboard data:', error);
|
||||||
|
// Fallback to basic metrics from processes
|
||||||
|
updateDashboardMetricsFromProcesses();
|
||||||
|
} finally {
|
||||||
|
dashboardLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fallback method to update metrics from processes data
|
||||||
|
const updateDashboardMetricsFromProcesses = () => {
|
||||||
|
const processes = processStore.processes;
|
||||||
|
|
||||||
|
dashboardMetrics.value = {
|
||||||
|
totalProcesses: processes.length,
|
||||||
|
publishedProcesses: processes.filter(p => p.status === 'published').length,
|
||||||
|
draftProcesses: processes.filter(p => p.status === 'draft').length,
|
||||||
|
totalCases: 0,
|
||||||
|
activeCases: 0,
|
||||||
|
completedCases: 0,
|
||||||
|
totalTasks: 0,
|
||||||
|
pendingTasks: 0,
|
||||||
|
averageCompletionTime: 0,
|
||||||
|
successRate: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
processChartData.value = [
|
||||||
|
{ label: 'Draft', value: processes.filter(p => p.status === 'draft').length, color: '#F59E0B' },
|
||||||
|
{ label: 'Published', value: processes.filter(p => p.status === 'published').length, color: '#10B981' },
|
||||||
|
{ label: 'Archived', value: processes.filter(p => p.status === 'archived').length, color: '#6B7280' },
|
||||||
|
{ label: 'Deleted', value: processes.filter(p => p.status === 'deleted').length, color: '#EF4444' }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
// Format date for display
|
// Format date for display
|
||||||
const formatDate = (isoString) => {
|
const formatDate = (isoString) => {
|
||||||
if (!isoString) return '';
|
if (!isoString) return '';
|
||||||
|
|
||||||
const date = new Date(isoString);
|
const date = new Date(isoString);
|
||||||
return new Intl.DateTimeFormat('en-US', {
|
return new Intl.DateTimeFormat('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@ -53,6 +146,21 @@ const formatDate = (isoString) => {
|
|||||||
}).format(date);
|
}).format(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Format relative time
|
||||||
|
const formatRelativeTime = (isoString) => {
|
||||||
|
if (!isoString) return '';
|
||||||
|
const date = new Date(isoString);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now - date;
|
||||||
|
const diffMinutes = Math.floor(diffMs / 60000);
|
||||||
|
const diffHours = Math.floor(diffMinutes / 60);
|
||||||
|
const diffDays = Math.floor(diffHours / 24);
|
||||||
|
|
||||||
|
if (diffMinutes < 60) return `${diffMinutes}m ago`;
|
||||||
|
if (diffHours < 24) return `${diffHours}h ago`;
|
||||||
|
return `${diffDays}d ago`;
|
||||||
|
};
|
||||||
|
|
||||||
// Get status badge variant
|
// Get status badge variant
|
||||||
const getStatusVariant = (status) => {
|
const getStatusVariant = (status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@ -64,6 +172,16 @@ const getStatusVariant = (status) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get activity icon
|
||||||
|
const getActivityIcon = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'process_created': return 'material-symbols:add-circle-outline';
|
||||||
|
case 'process_published': return 'material-symbols:publish';
|
||||||
|
case 'case_completed': return 'material-symbols:check-circle-outline';
|
||||||
|
default: return 'material-symbols:info';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Load processes from API with filters
|
// Load processes from API with filters
|
||||||
const loadProcesses = async () => {
|
const loadProcesses = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@ -73,27 +191,33 @@ const loadProcesses = async () => {
|
|||||||
sortOrder: sortOrder.value
|
sortOrder: sortOrder.value
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add filters if they have values
|
|
||||||
if (statusFilter.value) {
|
if (statusFilter.value) {
|
||||||
options.status = statusFilter.value;
|
options.status = statusFilter.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (categoryFilter.value) {
|
||||||
|
options.category = categoryFilter.value;
|
||||||
|
}
|
||||||
|
|
||||||
if (searchQuery.value.trim()) {
|
if (searchQuery.value.trim()) {
|
||||||
options.search = searchQuery.value.trim();
|
options.search = searchQuery.value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
await processStore.fetchProcesses(options);
|
await processStore.fetchProcesses(options);
|
||||||
|
if (currentView.value === 'dashboard') {
|
||||||
|
await loadDashboardData();
|
||||||
|
} else {
|
||||||
|
updateDashboardMetricsFromProcesses();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading processes:', error);
|
console.error('Error loading processes:', error);
|
||||||
// TODO: Show error notification
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watch for changes in filters and reload processes
|
// Watch for changes in filters and reload processes
|
||||||
watch([searchQuery, statusFilter], () => {
|
watch([searchQuery, statusFilter, categoryFilter], () => {
|
||||||
// Debounce the search to avoid too many API calls
|
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
searchTimeout = setTimeout(() => {
|
searchTimeout = setTimeout(() => {
|
||||||
loadProcesses();
|
loadProcesses();
|
||||||
@ -102,131 +226,106 @@ watch([searchQuery, statusFilter], () => {
|
|||||||
|
|
||||||
let searchTimeout = null;
|
let searchTimeout = null;
|
||||||
|
|
||||||
// Edit a process
|
// Navigation functions
|
||||||
|
const switchView = (view) => {
|
||||||
|
currentView.value = view;
|
||||||
|
};
|
||||||
|
|
||||||
const editProcess = async (processId) => {
|
const editProcess = async (processId) => {
|
||||||
try {
|
try {
|
||||||
// Navigate to process builder with the process ID as a query parameter
|
|
||||||
router.push(`/process-builder?id=${processId}`);
|
router.push(`/process-builder?id=${processId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error navigating to process:', error);
|
console.error('Error navigating to process:', error);
|
||||||
// TODO: Show error notification
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Duplicate a process
|
const viewProcessAnalytics = (processId) => {
|
||||||
|
router.push(`/process-builder/analytics/${processId}`);
|
||||||
|
};
|
||||||
|
|
||||||
const duplicateProcess = async (process) => {
|
const duplicateProcess = async (process) => {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const newName = `${process.name} (Copy)`;
|
const newName = `${process.name} (Copy)`;
|
||||||
await processStore.duplicateProcess(process.id, newName);
|
await processStore.duplicateProcess(process.id, newName);
|
||||||
|
|
||||||
// Refresh the process list
|
|
||||||
await loadProcesses();
|
await loadProcesses();
|
||||||
|
|
||||||
// TODO: Show success notification
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error duplicating process:', error);
|
console.error('Error duplicating process:', error);
|
||||||
// TODO: Show error notification
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete a process
|
|
||||||
const deleteProcess = async () => {
|
const deleteProcess = async () => {
|
||||||
if (!processToDelete.value) return;
|
if (!processToDelete.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await processStore.deleteProcess(processToDelete.value);
|
await processStore.deleteProcess(processToDelete.value);
|
||||||
|
|
||||||
// Refresh the process list
|
|
||||||
await loadProcesses();
|
await loadProcesses();
|
||||||
|
|
||||||
showDeleteConfirm.value = false;
|
showDeleteConfirm.value = false;
|
||||||
processToDelete.value = null;
|
processToDelete.value = null;
|
||||||
|
|
||||||
// TODO: Show success notification
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting process:', error);
|
console.error('Error deleting process:', error);
|
||||||
// TODO: Show error notification
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Publish a process
|
|
||||||
const publishProcess = async (processId) => {
|
const publishProcess = async (processId) => {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await processStore.publishProcess(processId);
|
await processStore.publishProcess(processId);
|
||||||
|
|
||||||
// Refresh the process list
|
|
||||||
await loadProcesses();
|
await loadProcesses();
|
||||||
|
|
||||||
// TODO: Show success notification
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error publishing process:', error);
|
console.error('Error publishing process:', error);
|
||||||
// TODO: Show error notification
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Restore a deleted process
|
|
||||||
const restoreProcess = async (processId) => {
|
const restoreProcess = async (processId) => {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await processStore.restoreProcess(processId);
|
await processStore.restoreProcess(processId);
|
||||||
|
|
||||||
// Refresh the process list
|
|
||||||
await loadProcesses();
|
await loadProcesses();
|
||||||
|
|
||||||
// TODO: Show success notification
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error restoring process:', error);
|
console.error('Error restoring process:', error);
|
||||||
// TODO: Show error notification
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show delete confirmation
|
|
||||||
const confirmDelete = (processId) => {
|
const confirmDelete = (processId) => {
|
||||||
processToDelete.value = processId;
|
processToDelete.value = processId;
|
||||||
showDeleteConfirm.value = true;
|
showDeleteConfirm.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close delete confirmation
|
|
||||||
const cancelDelete = () => {
|
const cancelDelete = () => {
|
||||||
showDeleteConfirm.value = false;
|
showDeleteConfirm.value = false;
|
||||||
processToDelete.value = null;
|
processToDelete.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a new process
|
|
||||||
const createNewProcess = () => {
|
const createNewProcess = () => {
|
||||||
// Clear current process to start fresh
|
|
||||||
processStore.clearCurrentProcess();
|
processStore.clearCurrentProcess();
|
||||||
router.push('/process-builder');
|
router.push('/process-builder');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Go back to builder
|
|
||||||
const goToBuilder = () => {
|
const goToBuilder = () => {
|
||||||
router.push('/');
|
router.push('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
searchQuery.value = '';
|
||||||
|
statusFilter.value = '';
|
||||||
|
categoryFilter.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
// Load processes on component mount
|
// Load processes on component mount
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadProcesses();
|
await loadProcesses();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear all filters
|
|
||||||
const clearFilters = () => {
|
|
||||||
searchQuery.value = '';
|
|
||||||
statusFilter.value = '';
|
|
||||||
// loadProcesses will be called automatically by the watcher
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clean up the search timeout on component unmount
|
// Clean up the search timeout on component unmount
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
@ -236,38 +335,208 @@ onUnmounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-screen bg-gray-50">
|
<div class="flex flex-col h-screen bg-gray-50">
|
||||||
<!-- Header Bar -->
|
<!-- Header Bar -->
|
||||||
<header
|
<header class="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between shadow-sm">
|
||||||
class="bg-gray-800 px-4 py-4 flex items-center justify-between text-white shadow-md"
|
<div class="flex items-center gap-4">
|
||||||
>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<Icon
|
<Icon
|
||||||
@click="goToBuilder"
|
@click="goToBuilder"
|
||||||
name="ph:arrow-circle-left-duotone"
|
name="ph:arrow-circle-left-duotone"
|
||||||
class="cursor-pointer w-6 h-6 hover:text-gray-300"
|
class="cursor-pointer w-6 h-6 hover:text-gray-600 text-gray-500"
|
||||||
/>
|
/>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
<img
|
<img
|
||||||
src="@/assets/img/logo/logo-word-white.svg"
|
src="@/assets/img/logo/logo-word-black.svg"
|
||||||
alt="Corrad Logo"
|
alt="Corrad Logo"
|
||||||
class="h-7"
|
class="h-8"
|
||||||
/>
|
/>
|
||||||
|
<div class="border-l border-gray-300 pl-3">
|
||||||
|
<h1 class="text-xl font-semibold text-gray-900">Process Management</h1>
|
||||||
|
<p class="text-sm text-gray-500">Monitor and manage your business processes</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<h1 class="text-xl font-semibold text-white">Process Management</h1>
|
<!-- View Toggle -->
|
||||||
|
<div class="flex bg-gray-100 rounded-lg p-1">
|
||||||
|
<button
|
||||||
|
@click="switchView('dashboard')"
|
||||||
|
:class="[
|
||||||
|
'px-3 py-1 text-sm rounded-md transition-colors',
|
||||||
|
currentView === 'dashboard'
|
||||||
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:dashboard-outline" class="mr-1" />
|
||||||
|
Dashboard
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="switchView('list')"
|
||||||
|
:class="[
|
||||||
|
'px-3 py-1 text-sm rounded-md transition-colors',
|
||||||
|
currentView === 'list'
|
||||||
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:list" class="mr-1" />
|
||||||
|
List View
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<RsButton @click="createNewProcess" variant="primary" size="sm">
|
<RsButton @click="createNewProcess" variant="primary" size="sm">
|
||||||
<Icon name="material-symbols:add" class="mr-1" />
|
<Icon name="material-symbols:add" class="mr-1" />
|
||||||
Create New Process
|
Create Process
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
<!-- Main Content Area -->
|
||||||
<div class="flex-1 p-6 overflow-auto">
|
<div class="flex-1 overflow-auto">
|
||||||
|
<!-- Dashboard View -->
|
||||||
|
<div v-if="currentView === 'dashboard'" class="p-6 space-y-6">
|
||||||
|
<!-- Metrics Cards -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<!-- Total Processes -->
|
||||||
|
<div class="bg-white rounded-xl 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 Processes</p>
|
||||||
|
<p class="text-3xl font-bold text-gray-900">{{ dashboardMetrics.totalProcesses }}</p>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">
|
||||||
|
{{ dashboardMetrics.publishedProcesses }} published
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-blue-50 rounded-lg">
|
||||||
|
<Icon name="material-symbols:shape-line-outline-sharp" class="w-6 h-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Cases -->
|
||||||
|
<div class="bg-white rounded-xl 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">Active Cases</p>
|
||||||
|
<p class="text-3xl font-bold text-gray-900">{{ dashboardMetrics.activeCases }}</p>
|
||||||
|
<p class="text-sm text-green-600 mt-1">
|
||||||
|
{{ (dashboardMetrics.activeCases + dashboardMetrics.completedCases) > 0 ? `${dashboardMetrics.completedCases} completed` : 'No cases' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-green-50 rounded-lg">
|
||||||
|
<Icon name="material-symbols:play-circle-outline" class="w-6 h-6 text-green-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Completion Rate -->
|
||||||
|
<div class="bg-white rounded-xl 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">{{ dashboardMetrics.successRate }}%</p>
|
||||||
|
<p class="text-sm text-blue-600 mt-1">
|
||||||
|
{{ dashboardMetrics.completedCases }} completed
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-purple-50 rounded-lg">
|
||||||
|
<Icon name="material-symbols:check-circle-outline" class="w-6 h-6 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Avg. Completion Time -->
|
||||||
|
<div class="bg-white rounded-xl 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">{{ dashboardMetrics.averageCompletionTime }}{{ dashboardMetrics.averageCompletionTime > 0 ? 'h' : '' }}</p>
|
||||||
|
<p class="text-sm text-orange-600 mt-1">
|
||||||
|
{{ dashboardMetrics.pendingTasks }} tasks pending
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-orange-50 rounded-lg">
|
||||||
|
<Icon name="material-symbols:timer-outline" class="w-6 h-6 text-orange-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Charts and Recent Activity -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- Process Distribution Chart -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Process Distribution</h3>
|
||||||
|
<div class="h-64 flex items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<Icon name="material-symbols:pie-chart-outline" class="w-16 h-16 text-gray-400 mx-auto mb-2" />
|
||||||
|
<p class="text-gray-500">Chart will be implemented</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Case Status Chart -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Case Status</h3>
|
||||||
|
<div class="h-64 flex items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<Icon name="material-symbols:donut-small" class="w-16 h-16 text-gray-400 mx-auto mb-2" />
|
||||||
|
<p class="text-gray-500">Chart will be implemented</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Activity -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Recent Activity</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-for="activity in recentActivity"
|
||||||
|
:key="activity.id"
|
||||||
|
class="flex items-start gap-3"
|
||||||
|
>
|
||||||
|
<div class="p-2 bg-gray-50 rounded-lg">
|
||||||
|
<Icon
|
||||||
|
:name="getActivityIcon(activity.type)"
|
||||||
|
class="w-4 h-4 text-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm text-gray-900">{{ activity.message }}</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
by {{ activity.user }} • {{ formatRelativeTime(activity.timestamp) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Quick Actions</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<RsButton @click="createNewProcess" variant="primary" class="justify-center">
|
||||||
|
<Icon name="material-symbols:add" class="mr-2" />
|
||||||
|
Create New Process
|
||||||
|
</RsButton>
|
||||||
|
<RsButton @click="switchView('list')" variant="secondary" class="justify-center">
|
||||||
|
<Icon name="material-symbols:list" class="mr-2" />
|
||||||
|
View All Processes
|
||||||
|
</RsButton>
|
||||||
|
<RsButton @click="router.push('/execution')" variant="tertiary" class="justify-center">
|
||||||
|
<Icon name="material-symbols:play-arrow" class="mr-2" />
|
||||||
|
Execute Processes
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- List View -->
|
||||||
|
<div v-else-if="currentView === 'list'" class="p-6">
|
||||||
<!-- Filters and Search -->
|
<!-- Filters and Search -->
|
||||||
<div class="mb-6 flex flex-col sm:flex-row gap-4">
|
<div class="mb-6 bg-white rounded-xl shadow-sm border border-gray-200 p-4">
|
||||||
|
<div class="flex flex-col lg:flex-row gap-4">
|
||||||
<div class="flex-1 max-w-md">
|
<div class="flex-1 max-w-md">
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
@ -277,7 +546,7 @@ onUnmounted(() => {
|
|||||||
outer: 'mb-0',
|
outer: 'mb-0',
|
||||||
wrapper: 'relative',
|
wrapper: 'relative',
|
||||||
inner: 'relative',
|
inner: 'relative',
|
||||||
input: 'w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
|
input: 'w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
|
||||||
prefixIcon: 'absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 pointer-events-none'
|
prefixIcon: 'absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 pointer-events-none'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
@ -295,21 +564,41 @@ onUnmounted(() => {
|
|||||||
placeholder="Filter by status"
|
placeholder="Filter by status"
|
||||||
:classes="{
|
:classes="{
|
||||||
outer: 'mb-0',
|
outer: 'mb-0',
|
||||||
input: 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white'
|
input: 'w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-w-48">
|
||||||
|
<FormKit
|
||||||
|
v-model="categoryFilter"
|
||||||
|
type="select"
|
||||||
|
:options="categoryOptions"
|
||||||
|
placeholder="Filter by category"
|
||||||
|
:classes="{
|
||||||
|
outer: 'mb-0',
|
||||||
|
input: 'w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white'
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<RsButton @click="clearFilters" variant="secondary" size="sm" :disabled="loading" v-if="searchQuery || statusFilter">
|
<RsButton
|
||||||
|
@click="clearFilters"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
:disabled="loading"
|
||||||
|
v-if="searchQuery || statusFilter || categoryFilter"
|
||||||
|
>
|
||||||
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
||||||
Clear Filters
|
Clear
|
||||||
</RsButton>
|
</RsButton>
|
||||||
|
|
||||||
<RsButton @click="loadProcesses" variant="tertiary" size="sm" :disabled="loading">
|
<RsButton @click="loadProcesses" variant="tertiary" size="sm" :disabled="loading">
|
||||||
<Icon name="material-symbols:refresh" class="mr-1" />
|
<Icon name="material-symbols:refresh" class="mr-1" />
|
||||||
Refresh
|
Refresh
|
||||||
</RsButton>
|
</RsButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -321,86 +610,58 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Processes Table -->
|
<!-- Processes Grid -->
|
||||||
<div v-else class="bg-white shadow-md rounded-lg overflow-hidden">
|
<div v-else-if="filteredProcesses.length > 0" class="grid gap-4">
|
||||||
<table class="w-full table-auto">
|
<div
|
||||||
<thead class="bg-gray-50 border-b">
|
v-for="process in filteredProcesses"
|
||||||
<tr>
|
:key="process.id"
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
|
>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
<div class="flex items-start justify-between">
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Category</th>
|
<div class="flex-1">
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Updated</th>
|
<h3 class="text-lg font-semibold text-gray-900">{{ process.name }}</h3>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
<RsBadge :variant="getStatusVariant(process.status)">
|
||||||
</tr>
|
{{ process.status || 'draft' }}
|
||||||
</thead>
|
</RsBadge>
|
||||||
<tbody class="divide-y divide-gray-200">
|
</div>
|
||||||
<tr v-if="filteredProcesses.length === 0">
|
|
||||||
<td colspan="7" class="px-6 py-12 text-center text-gray-500">
|
<p class="text-gray-600 mb-3">
|
||||||
<div class="flex flex-col items-center">
|
{{ process.description || 'No description available' }}
|
||||||
<Icon name="material-symbols:folder-open-outline" class="w-12 h-12 text-gray-300 mb-2" />
|
</p>
|
||||||
<p class="text-lg font-medium mb-1">
|
|
||||||
{{ (searchQuery || statusFilter) ? 'No processes match your filters' : 'No processes found' }}
|
<div class="flex items-center gap-6 text-sm text-gray-500">
|
||||||
</p>
|
<div class="flex items-center gap-1">
|
||||||
<p class="text-sm mb-2">
|
<Icon name="material-symbols:folder-outline" class="w-4 h-4" />
|
||||||
{{ (searchQuery || statusFilter) ? 'Try adjusting your search or filter criteria' : 'Create your first process to get started' }}
|
{{ process.category || 'Uncategorized' }}
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Show current filters if any -->
|
|
||||||
<div v-if="searchQuery || statusFilter" class="text-xs text-gray-400 mb-4 space-y-1">
|
|
||||||
<div v-if="searchQuery">Search: "{{ searchQuery }}"</div>
|
|
||||||
<div v-if="statusFilter">Status: {{ statusOptions.find(opt => opt.value === statusFilter)?.label }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
<div class="flex gap-2">
|
<Icon name="material-symbols:person-outline" class="w-4 h-4" />
|
||||||
<RsButton v-if="searchQuery || statusFilter" @click="clearFilters" variant="secondary" size="sm">
|
{{ process.creator?.userFullName || process.creator?.userUsername || 'Unknown' }}
|
||||||
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
</div>
|
||||||
Clear Filters
|
<div class="flex items-center gap-1">
|
||||||
</RsButton>
|
<Icon name="material-symbols:schedule" class="w-4 h-4" />
|
||||||
<RsButton v-if="!searchQuery && !statusFilter" @click="createNewProcess" variant="primary" size="sm">
|
{{ formatDate(process.updatedAt) }}
|
||||||
<Icon name="material-symbols:add" class="mr-1" />
|
|
||||||
Create New Process
|
|
||||||
</RsButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-for="process in filteredProcesses" :key="process.id" class="hover:bg-gray-50">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="font-medium text-gray-900">{{ process.name }}</div>
|
|
||||||
<div v-if="process.creator" class="text-sm text-gray-500">
|
|
||||||
by {{ process.creator.userFullName || process.creator.userUsername }}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4">
|
<div class="flex items-center gap-2 ml-4">
|
||||||
<div class="text-sm text-gray-700 max-w-xs truncate">
|
<!-- Analytics Button -->
|
||||||
{{ process.description || 'No description' }}
|
<button
|
||||||
</div>
|
@click="viewProcessAnalytics(process.id)"
|
||||||
</td>
|
class="p-2 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors"
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
title="View Analytics"
|
||||||
<RsBadge :variant="getStatusVariant(process.status)">
|
:disabled="loading"
|
||||||
{{ process.status || 'draft' }}
|
>
|
||||||
</RsBadge>
|
<Icon name="material-symbols:analytics" class="text-lg" />
|
||||||
</td>
|
</button>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="text-sm text-gray-700">
|
|
||||||
{{ process.category || '-' }}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="text-sm text-gray-500">{{ formatDate(process.createdAt) }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="text-sm text-gray-500">{{ formatDate(process.updatedAt) }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
||||||
<div class="flex space-x-2 justify-end">
|
|
||||||
<!-- Show restore button for deleted processes -->
|
<!-- Show restore button for deleted processes -->
|
||||||
<button
|
<button
|
||||||
v-if="process.status === 'deleted'"
|
v-if="process.status === 'deleted'"
|
||||||
@click="restoreProcess(process.id)"
|
@click="restoreProcess(process.id)"
|
||||||
class="p-1 text-green-600 hover:text-green-900 hover:bg-green-50 rounded"
|
class="p-2 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors"
|
||||||
title="Restore Process"
|
title="Restore Process"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
>
|
>
|
||||||
@ -411,7 +672,7 @@ onUnmounted(() => {
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<button
|
<button
|
||||||
@click="editProcess(process.id)"
|
@click="editProcess(process.id)"
|
||||||
class="p-1 text-blue-600 hover:text-blue-900 hover:bg-blue-50 rounded"
|
class="p-2 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors"
|
||||||
title="Edit Process"
|
title="Edit Process"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
>
|
>
|
||||||
@ -421,7 +682,7 @@ onUnmounted(() => {
|
|||||||
<button
|
<button
|
||||||
v-if="process.status === 'draft'"
|
v-if="process.status === 'draft'"
|
||||||
@click="publishProcess(process.id)"
|
@click="publishProcess(process.id)"
|
||||||
class="p-1 text-green-600 hover:text-green-900 hover:bg-green-50 rounded"
|
class="p-2 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors"
|
||||||
title="Publish Process"
|
title="Publish Process"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
>
|
>
|
||||||
@ -430,7 +691,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
@click="duplicateProcess(process)"
|
@click="duplicateProcess(process)"
|
||||||
class="p-1 text-indigo-600 hover:text-indigo-900 hover:bg-indigo-50 rounded"
|
class="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded-lg transition-colors"
|
||||||
title="Duplicate Process"
|
title="Duplicate Process"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
>
|
>
|
||||||
@ -439,7 +700,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
@click="confirmDelete(process.id)"
|
@click="confirmDelete(process.id)"
|
||||||
class="p-1 text-red-600 hover:text-red-900 hover:bg-red-50 rounded"
|
class="p-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
title="Delete Process"
|
title="Delete Process"
|
||||||
:disabled="loading || process.status === 'published'"
|
:disabled="loading || process.status === 'published'"
|
||||||
>
|
>
|
||||||
@ -447,15 +708,43 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-else class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
||||||
|
<Icon name="material-symbols:folder-open-outline" class="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-2">
|
||||||
|
{{ (searchQuery || statusFilter || categoryFilter) ? 'No processes match your filters' : 'No processes found' }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600 mb-6">
|
||||||
|
{{ (searchQuery || statusFilter || categoryFilter) ? 'Try adjusting your search or filter criteria' : 'Create your first process to get started' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Show current filters if any -->
|
||||||
|
<div v-if="searchQuery || statusFilter || categoryFilter" class="text-sm text-gray-400 mb-6 space-y-1">
|
||||||
|
<div v-if="searchQuery">Search: "{{ searchQuery }}"</div>
|
||||||
|
<div v-if="statusFilter">Status: {{ statusOptions.find(opt => opt.value === statusFilter)?.label }}</div>
|
||||||
|
<div v-if="categoryFilter">Category: {{ categoryOptions.find(opt => opt.value === categoryFilter)?.label }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center gap-3">
|
||||||
|
<RsButton v-if="searchQuery || statusFilter || categoryFilter" @click="clearFilters" variant="secondary">
|
||||||
|
<Icon name="material-symbols:filter-alt-off" class="mr-2" />
|
||||||
|
Clear Filters
|
||||||
|
</RsButton>
|
||||||
|
<RsButton v-if="!searchQuery && !statusFilter && !categoryFilter" @click="createNewProcess" variant="primary">
|
||||||
|
<Icon name="material-symbols:add" class="mr-2" />
|
||||||
|
Create New Process
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Global Loading Overlay -->
|
<!-- Global Loading Overlay -->
|
||||||
<div v-if="loading && processStore.processes.length > 0" class="fixed inset-0 bg-black bg-opacity-20 flex items-center justify-center z-50">
|
<div v-if="loading && processStore.processes.length > 0" class="fixed inset-0 bg-black bg-opacity-20 flex items-center justify-center z-50">
|
||||||
<div class="bg-white rounded-lg p-6 flex items-center space-x-3">
|
<div class="bg-white rounded-lg p-6 flex items-center space-x-3 shadow-lg">
|
||||||
<Icon name="material-symbols:progress-activity" class="w-6 h-6 animate-spin text-blue-500" />
|
<Icon name="material-symbols:progress-activity" class="w-6 h-6 animate-spin text-blue-500" />
|
||||||
<span class="text-gray-700">Processing...</span>
|
<span class="text-gray-700">Processing...</span>
|
||||||
</div>
|
</div>
|
||||||
@ -494,13 +783,26 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Custom styles for better UX */
|
/* Custom transitions and animations */
|
||||||
.hover\:bg-gray-50:hover {
|
.transition-colors {
|
||||||
transition: background-color 0.15s ease-in-out;
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-shadow {
|
||||||
|
transition-property: box-shadow;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Glassmorphism effect for cards */
|
||||||
|
.bg-white {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
349
server/api/process/dashboard/summary.get.js
Normal file
349
server/api/process/dashboard/summary.get.js
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
// Initialize Prisma client
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
// Get query parameters for filtering (optional)
|
||||||
|
const query = getQuery(event);
|
||||||
|
const {
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
category,
|
||||||
|
status
|
||||||
|
} = query;
|
||||||
|
|
||||||
|
// Build date filter
|
||||||
|
const dateFilter = {};
|
||||||
|
if (startDate && endDate) {
|
||||||
|
dateFilter.processCreatedDate = {
|
||||||
|
gte: new Date(startDate),
|
||||||
|
lte: new Date(endDate)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build process filter
|
||||||
|
const processFilter = {
|
||||||
|
...dateFilter
|
||||||
|
};
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
processFilter.processCategory = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
processFilter.processStatus = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get process statistics
|
||||||
|
const [
|
||||||
|
totalProcesses,
|
||||||
|
publishedProcesses,
|
||||||
|
draftProcesses,
|
||||||
|
archivedProcesses,
|
||||||
|
deletedProcesses,
|
||||||
|
processCategories
|
||||||
|
] = await Promise.all([
|
||||||
|
// Total processes
|
||||||
|
prisma.process.count({
|
||||||
|
where: processFilter
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Published processes
|
||||||
|
prisma.process.count({
|
||||||
|
where: {
|
||||||
|
...processFilter,
|
||||||
|
processStatus: 'published'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Draft processes
|
||||||
|
prisma.process.count({
|
||||||
|
where: {
|
||||||
|
...processFilter,
|
||||||
|
processStatus: 'draft'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Archived processes
|
||||||
|
prisma.process.count({
|
||||||
|
where: {
|
||||||
|
...processFilter,
|
||||||
|
processStatus: 'archived'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Deleted processes
|
||||||
|
prisma.process.count({
|
||||||
|
where: {
|
||||||
|
...processFilter,
|
||||||
|
processStatus: 'deleted'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Process categories distribution
|
||||||
|
prisma.process.groupBy({
|
||||||
|
by: ['processCategory'],
|
||||||
|
where: processFilter,
|
||||||
|
_count: {
|
||||||
|
processCategory: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get case instance statistics
|
||||||
|
const [
|
||||||
|
totalCases,
|
||||||
|
activeCases,
|
||||||
|
completedCases,
|
||||||
|
casesByStatus
|
||||||
|
] = await Promise.all([
|
||||||
|
// Total case instances
|
||||||
|
prisma.caseInstance.count(),
|
||||||
|
|
||||||
|
// Active cases
|
||||||
|
prisma.caseInstance.count({
|
||||||
|
where: {
|
||||||
|
caseStatus: 'active'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Completed cases
|
||||||
|
prisma.caseInstance.count({
|
||||||
|
where: {
|
||||||
|
caseStatus: 'completed'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Cases by status
|
||||||
|
prisma.caseInstance.groupBy({
|
||||||
|
by: ['caseStatus'],
|
||||||
|
_count: {
|
||||||
|
caseStatus: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get task statistics
|
||||||
|
const [
|
||||||
|
totalTasks,
|
||||||
|
pendingTasks,
|
||||||
|
completedTasks,
|
||||||
|
overdueTasks
|
||||||
|
] = await Promise.all([
|
||||||
|
// Total tasks
|
||||||
|
prisma.task.count(),
|
||||||
|
|
||||||
|
// Pending tasks
|
||||||
|
prisma.task.count({
|
||||||
|
where: {
|
||||||
|
taskStatus: 'pending'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Completed tasks
|
||||||
|
prisma.task.count({
|
||||||
|
where: {
|
||||||
|
taskStatus: 'completed'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Overdue tasks (pending tasks created more than 3 days ago)
|
||||||
|
prisma.task.count({
|
||||||
|
where: {
|
||||||
|
taskStatus: 'pending',
|
||||||
|
taskCreatedDate: {
|
||||||
|
lt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000) // 3 days ago
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Calculate average completion time for completed cases
|
||||||
|
const completedCasesWithTime = await prisma.caseInstance.findMany({
|
||||||
|
where: {
|
||||||
|
caseStatus: 'completed',
|
||||||
|
caseCompletedDate: { not: null }
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
caseCreatedDate: true,
|
||||||
|
caseCompletedDate: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let averageCompletionTime = 0;
|
||||||
|
if (completedCasesWithTime.length > 0) {
|
||||||
|
const totalCompletionTime = completedCasesWithTime.reduce((sum, case_) => {
|
||||||
|
const diffMs = new Date(case_.caseCompletedDate) - new Date(case_.caseCreatedDate);
|
||||||
|
const diffHours = diffMs / (1000 * 60 * 60);
|
||||||
|
return sum + diffHours;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
averageCompletionTime = Math.round((totalCompletionTime / completedCasesWithTime.length) * 10) / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate success rate (completed cases / total cases * 100)
|
||||||
|
const successRate = totalCases > 0 ? Math.round((completedCases / totalCases) * 1000) / 10 : 0;
|
||||||
|
|
||||||
|
// Get recent activity (latest case instances and process updates)
|
||||||
|
const recentCases = await prisma.caseInstance.findMany({
|
||||||
|
take: 5,
|
||||||
|
orderBy: {
|
||||||
|
caseCreatedDate: 'desc'
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
process: {
|
||||||
|
select: {
|
||||||
|
processName: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startedBy: {
|
||||||
|
select: {
|
||||||
|
userFullName: true,
|
||||||
|
userUsername: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const recentProcessUpdates = await prisma.process.findMany({
|
||||||
|
take: 5,
|
||||||
|
orderBy: {
|
||||||
|
processModifiedDate: 'desc'
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
processModifiedDate: { not: null }
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
creator: {
|
||||||
|
select: {
|
||||||
|
userFullName: true,
|
||||||
|
userUsername: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format recent activity
|
||||||
|
const recentActivity = [];
|
||||||
|
|
||||||
|
// Add recent cases
|
||||||
|
recentCases.forEach(case_ => {
|
||||||
|
recentActivity.push({
|
||||||
|
id: `case-${case_.caseID}`,
|
||||||
|
type: case_.caseStatus === 'completed' ? 'case_completed' : 'case_started',
|
||||||
|
message: `${case_.process.processName} case ${case_.caseStatus === 'completed' ? 'completed' : 'started'}`,
|
||||||
|
user: case_.startedBy?.userFullName || case_.startedBy?.userUsername || 'Unknown',
|
||||||
|
timestamp: case_.caseStatus === 'completed' ? case_.caseCompletedDate : case_.caseCreatedDate
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add recent process updates
|
||||||
|
recentProcessUpdates.forEach(process => {
|
||||||
|
recentActivity.push({
|
||||||
|
id: `process-${process.processID}`,
|
||||||
|
type: process.processStatus === 'published' ? 'process_published' : 'process_updated',
|
||||||
|
message: `${process.processName} process ${process.processStatus === 'published' ? 'published' : 'updated'}`,
|
||||||
|
user: process.creator?.userFullName || process.creator?.userUsername || 'Unknown',
|
||||||
|
timestamp: process.processModifiedDate
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort recent activity by timestamp and take the latest 10
|
||||||
|
recentActivity.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
||||||
|
const latestActivity = recentActivity.slice(0, 10);
|
||||||
|
|
||||||
|
// Get monthly statistics for the last 6 months
|
||||||
|
const sixMonthsAgo = new Date();
|
||||||
|
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
|
||||||
|
|
||||||
|
const monthlyCases = await prisma.caseInstance.groupBy({
|
||||||
|
by: ['caseCreatedDate'],
|
||||||
|
where: {
|
||||||
|
caseCreatedDate: {
|
||||||
|
gte: sixMonthsAgo
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
caseID: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process monthly data
|
||||||
|
const monthlyStats = [];
|
||||||
|
for (let i = 5; i >= 0; i--) {
|
||||||
|
const date = new Date();
|
||||||
|
date.setMonth(date.getMonth() - i);
|
||||||
|
const monthKey = date.toISOString().substring(0, 7); // YYYY-MM format
|
||||||
|
|
||||||
|
const monthData = monthlyCases.filter(item =>
|
||||||
|
item.caseCreatedDate.toISOString().substring(0, 7) === monthKey
|
||||||
|
);
|
||||||
|
|
||||||
|
monthlyStats.push({
|
||||||
|
month: date.toLocaleString('default', { month: 'short' }),
|
||||||
|
year: date.getFullYear(),
|
||||||
|
cases: monthData.length,
|
||||||
|
completed: 0 // Would need additional query to get completed cases for the month
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare response
|
||||||
|
const summary = {
|
||||||
|
overview: {
|
||||||
|
totalProcesses,
|
||||||
|
publishedProcesses,
|
||||||
|
draftProcesses,
|
||||||
|
archivedProcesses,
|
||||||
|
deletedProcesses,
|
||||||
|
totalCases,
|
||||||
|
activeCases,
|
||||||
|
completedCases,
|
||||||
|
totalTasks,
|
||||||
|
pendingTasks,
|
||||||
|
completedTasks,
|
||||||
|
overdueTasks,
|
||||||
|
averageCompletionTime,
|
||||||
|
successRate
|
||||||
|
},
|
||||||
|
charts: {
|
||||||
|
processDistribution: [
|
||||||
|
{ label: 'Published', value: publishedProcesses, color: '#10B981' },
|
||||||
|
{ label: 'Draft', value: draftProcesses, color: '#F59E0B' },
|
||||||
|
{ label: 'Archived', value: archivedProcesses, color: '#6B7280' },
|
||||||
|
{ label: 'Deleted', value: deletedProcesses, color: '#EF4444' }
|
||||||
|
],
|
||||||
|
caseStatus: casesByStatus.map(item => ({
|
||||||
|
label: item.caseStatus,
|
||||||
|
value: item._count.caseStatus,
|
||||||
|
color: item.caseStatus === 'active' ? '#3B82F6' :
|
||||||
|
item.caseStatus === 'completed' ? '#10B981' : '#6B7280'
|
||||||
|
})),
|
||||||
|
categoryDistribution: processCategories.map(item => ({
|
||||||
|
label: item.processCategory || 'Uncategorized',
|
||||||
|
value: item._count.processCategory
|
||||||
|
})),
|
||||||
|
monthlyTrend: monthlyStats
|
||||||
|
},
|
||||||
|
recentActivity: latestActivity,
|
||||||
|
lastUpdated: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: summary
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching dashboard summary:', error);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to fetch dashboard summary',
|
||||||
|
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user