- 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.
808 lines
29 KiB
Vue
808 lines
29 KiB
Vue
<script setup>
|
|
import { ref, computed, onMounted, watch, onUnmounted } from 'vue';
|
|
import { useProcessBuilderStore } from '~/stores/processBuilder';
|
|
import { useRouter } from 'vue-router';
|
|
|
|
// Define page meta
|
|
definePageMeta({
|
|
title: "Process Management Dashboard",
|
|
description: "Manage and monitor your business processes",
|
|
layout: "empty",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
});
|
|
|
|
// Initialize the store and router
|
|
const processStore = useProcessBuilderStore();
|
|
const router = useRouter();
|
|
|
|
// State
|
|
const searchQuery = ref('');
|
|
const loading = ref(false);
|
|
const showDeleteConfirm = ref(false);
|
|
const processToDelete = ref(null);
|
|
const statusFilter = ref('');
|
|
const categoryFilter = ref('');
|
|
const sortBy = ref('processCreatedDate');
|
|
const sortOrder = ref('desc');
|
|
const currentView = ref('dashboard'); // 'dashboard', 'list', 'analytics'
|
|
|
|
// 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 = [
|
|
{ value: '', label: 'All Status' },
|
|
{ value: 'draft', label: 'Draft' },
|
|
{ value: 'published', label: 'Published' },
|
|
{ value: 'archived', label: 'Archived' },
|
|
{ value: 'deleted', label: 'Deleted' }
|
|
];
|
|
|
|
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(() => {
|
|
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
|
|
const formatDate = (isoString) => {
|
|
if (!isoString) return '';
|
|
const date = new Date(isoString);
|
|
return new Intl.DateTimeFormat('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: 'numeric',
|
|
minute: 'numeric'
|
|
}).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
|
|
const getStatusVariant = (status) => {
|
|
switch (status) {
|
|
case 'published': return 'success';
|
|
case 'draft': return 'warning';
|
|
case 'archived': return 'secondary';
|
|
case 'deleted': return 'danger';
|
|
default: return 'primary';
|
|
}
|
|
};
|
|
|
|
// 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
|
|
const loadProcesses = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const options = {
|
|
sortBy: sortBy.value,
|
|
sortOrder: sortOrder.value
|
|
};
|
|
|
|
if (statusFilter.value) {
|
|
options.status = statusFilter.value;
|
|
}
|
|
|
|
if (categoryFilter.value) {
|
|
options.category = categoryFilter.value;
|
|
}
|
|
|
|
if (searchQuery.value.trim()) {
|
|
options.search = searchQuery.value.trim();
|
|
}
|
|
|
|
await processStore.fetchProcesses(options);
|
|
if (currentView.value === 'dashboard') {
|
|
await loadDashboardData();
|
|
} else {
|
|
updateDashboardMetricsFromProcesses();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading processes:', error);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// Watch for changes in filters and reload processes
|
|
watch([searchQuery, statusFilter, categoryFilter], () => {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
loadProcesses();
|
|
}, 500);
|
|
});
|
|
|
|
let searchTimeout = null;
|
|
|
|
// Navigation functions
|
|
const switchView = (view) => {
|
|
currentView.value = view;
|
|
};
|
|
|
|
const editProcess = async (processId) => {
|
|
try {
|
|
router.push(`/process-builder?id=${processId}`);
|
|
} catch (error) {
|
|
console.error('Error navigating to process:', error);
|
|
}
|
|
};
|
|
|
|
const viewProcessAnalytics = (processId) => {
|
|
router.push(`/process-builder/analytics/${processId}`);
|
|
};
|
|
|
|
const duplicateProcess = async (process) => {
|
|
try {
|
|
loading.value = true;
|
|
const newName = `${process.name} (Copy)`;
|
|
await processStore.duplicateProcess(process.id, newName);
|
|
await loadProcesses();
|
|
} catch (error) {
|
|
console.error('Error duplicating process:', error);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const deleteProcess = async () => {
|
|
if (!processToDelete.value) return;
|
|
|
|
try {
|
|
loading.value = true;
|
|
await processStore.deleteProcess(processToDelete.value);
|
|
await loadProcesses();
|
|
showDeleteConfirm.value = false;
|
|
processToDelete.value = null;
|
|
} catch (error) {
|
|
console.error('Error deleting process:', error);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const publishProcess = async (processId) => {
|
|
try {
|
|
loading.value = true;
|
|
await processStore.publishProcess(processId);
|
|
await loadProcesses();
|
|
} catch (error) {
|
|
console.error('Error publishing process:', error);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const restoreProcess = async (processId) => {
|
|
try {
|
|
loading.value = true;
|
|
await processStore.restoreProcess(processId);
|
|
await loadProcesses();
|
|
} catch (error) {
|
|
console.error('Error restoring process:', error);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const confirmDelete = (processId) => {
|
|
processToDelete.value = processId;
|
|
showDeleteConfirm.value = true;
|
|
};
|
|
|
|
const cancelDelete = () => {
|
|
showDeleteConfirm.value = false;
|
|
processToDelete.value = null;
|
|
};
|
|
|
|
const createNewProcess = () => {
|
|
processStore.clearCurrentProcess();
|
|
router.push('/process-builder');
|
|
};
|
|
|
|
const goToBuilder = () => {
|
|
router.push('/');
|
|
};
|
|
|
|
const clearFilters = () => {
|
|
searchQuery.value = '';
|
|
statusFilter.value = '';
|
|
categoryFilter.value = '';
|
|
};
|
|
|
|
// Load processes on component mount
|
|
onMounted(async () => {
|
|
await loadProcesses();
|
|
});
|
|
|
|
// Clean up the search timeout on component unmount
|
|
onUnmounted(() => {
|
|
clearTimeout(searchTimeout);
|
|
});
|
|
</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 flex items-center justify-between shadow-sm">
|
|
<div class="flex items-center gap-4">
|
|
<Icon
|
|
@click="goToBuilder"
|
|
name="ph:arrow-circle-left-duotone"
|
|
class="cursor-pointer w-6 h-6 hover:text-gray-600 text-gray-500"
|
|
/>
|
|
<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 Management</h1>
|
|
<p class="text-sm text-gray-500">Monitor and manage your business processes</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
<!-- 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>
|
|
|
|
<RsButton @click="createNewProcess" variant="primary" size="sm">
|
|
<Icon name="material-symbols:add" class="mr-1" />
|
|
Create Process
|
|
</RsButton>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Content Area -->
|
|
<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 -->
|
|
<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">
|
|
<FormKit
|
|
v-model="searchQuery"
|
|
type="text"
|
|
placeholder="Search processes..."
|
|
:classes="{
|
|
outer: 'mb-0',
|
|
wrapper: 'relative',
|
|
inner: 'relative',
|
|
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'
|
|
}"
|
|
>
|
|
<template #prefixIcon>
|
|
<Icon name="material-symbols:search" class="text-lg" />
|
|
</template>
|
|
</FormKit>
|
|
</div>
|
|
|
|
<div class="min-w-48">
|
|
<FormKit
|
|
v-model="statusFilter"
|
|
type="select"
|
|
:options="statusOptions"
|
|
placeholder="Filter by status"
|
|
: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 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 class="flex gap-2">
|
|
<RsButton
|
|
@click="clearFilters"
|
|
variant="secondary"
|
|
size="sm"
|
|
:disabled="loading"
|
|
v-if="searchQuery || statusFilter || categoryFilter"
|
|
>
|
|
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
|
Clear
|
|
</RsButton>
|
|
|
|
<RsButton @click="loadProcesses" variant="tertiary" size="sm" :disabled="loading">
|
|
<Icon name="material-symbols:refresh" class="mr-1" />
|
|
Refresh
|
|
</RsButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="loading && processStore.processes.length === 0" class="flex justify-center items-center py-12">
|
|
<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 processes...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Processes Grid -->
|
|
<div v-else-if="filteredProcesses.length > 0" class="grid gap-4">
|
|
<div
|
|
v-for="process in filteredProcesses"
|
|
:key="process.id"
|
|
class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
|
>
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<h3 class="text-lg font-semibold text-gray-900">{{ process.name }}</h3>
|
|
<RsBadge :variant="getStatusVariant(process.status)">
|
|
{{ process.status || 'draft' }}
|
|
</RsBadge>
|
|
</div>
|
|
|
|
<p class="text-gray-600 mb-3">
|
|
{{ process.description || 'No description available' }}
|
|
</p>
|
|
|
|
<div class="flex items-center gap-6 text-sm text-gray-500">
|
|
<div class="flex items-center gap-1">
|
|
<Icon name="material-symbols:folder-outline" class="w-4 h-4" />
|
|
{{ process.category || 'Uncategorized' }}
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
<Icon name="material-symbols:person-outline" class="w-4 h-4" />
|
|
{{ process.creator?.userFullName || process.creator?.userUsername || 'Unknown' }}
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
<Icon name="material-symbols:schedule" class="w-4 h-4" />
|
|
{{ formatDate(process.updatedAt) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 ml-4">
|
|
<!-- Analytics Button -->
|
|
<button
|
|
@click="viewProcessAnalytics(process.id)"
|
|
class="p-2 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors"
|
|
title="View Analytics"
|
|
:disabled="loading"
|
|
>
|
|
<Icon name="material-symbols:analytics" class="text-lg" />
|
|
</button>
|
|
|
|
<!-- Show restore button for deleted processes -->
|
|
<button
|
|
v-if="process.status === 'deleted'"
|
|
@click="restoreProcess(process.id)"
|
|
class="p-2 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors"
|
|
title="Restore Process"
|
|
:disabled="loading"
|
|
>
|
|
<Icon name="material-symbols:restore" class="text-lg" />
|
|
</button>
|
|
|
|
<!-- Regular action buttons for non-deleted processes -->
|
|
<template v-else>
|
|
<button
|
|
@click="editProcess(process.id)"
|
|
class="p-2 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors"
|
|
title="Edit Process"
|
|
:disabled="loading"
|
|
>
|
|
<Icon name="material-symbols:edit" class="text-lg" />
|
|
</button>
|
|
|
|
<button
|
|
v-if="process.status === 'draft'"
|
|
@click="publishProcess(process.id)"
|
|
class="p-2 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors"
|
|
title="Publish Process"
|
|
:disabled="loading"
|
|
>
|
|
<Icon name="material-symbols:publish" class="text-lg" />
|
|
</button>
|
|
|
|
<button
|
|
@click="duplicateProcess(process)"
|
|
class="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded-lg transition-colors"
|
|
title="Duplicate Process"
|
|
:disabled="loading"
|
|
>
|
|
<Icon name="material-symbols:content-copy" class="text-lg" />
|
|
</button>
|
|
|
|
<button
|
|
@click="confirmDelete(process.id)"
|
|
class="p-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-lg transition-colors"
|
|
title="Delete Process"
|
|
:disabled="loading || process.status === 'published'"
|
|
>
|
|
<Icon name="material-symbols:delete" class="text-lg" />
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- 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 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" />
|
|
<span class="text-gray-700">Processing...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete confirmation dialog -->
|
|
<RsModal v-model="showDeleteConfirm" title="Move Process to Trash" size="md" position="center">
|
|
<div class="p-4">
|
|
<div class="flex items-center mb-4">
|
|
<Icon name="material-symbols:delete-outline" class="text-orange-500 w-8 h-8 mr-3 flex-shrink-0" />
|
|
<div>
|
|
<p class="text-gray-600 font-medium mb-1">Move to Trash</p>
|
|
<p class="text-gray-600 text-sm">
|
|
This process will be moved to trash but not permanently deleted. You can restore it later from the "Deleted" filter if needed.
|
|
</p>
|
|
<p class="text-gray-500 text-xs mt-2">
|
|
Note: Published processes cannot be deleted and must be unpublished first.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<template #footer>
|
|
<div class="flex justify-end gap-2">
|
|
<RsButton @click="cancelDelete" variant="tertiary" :disabled="loading">
|
|
Cancel
|
|
</RsButton>
|
|
<RsButton @click="deleteProcess" variant="warning" :disabled="loading">
|
|
<Icon v-if="loading" name="material-symbols:progress-activity" class="w-4 h-4 animate-spin mr-1" />
|
|
Move to Trash
|
|
</RsButton>
|
|
</div>
|
|
</template>
|
|
</RsModal>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Custom transitions and animations */
|
|
.transition-colors {
|
|
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 {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* Glassmorphism effect for cards */
|
|
.bg-white {
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
</style> |