Implement Pagination for Form and Process Management

- Added pagination functionality to the form management and process management pages, allowing users to navigate through forms and processes more efficiently.
- Introduced controls for items per page selection and pagination navigation, enhancing user experience and accessibility of data.
- Updated computed properties to handle paginated data and maintain accurate pagination state across filters and searches.
- Ensured that pagination resets when filters change, providing a consistent and intuitive user interface.
This commit is contained in:
Md Afiq Iskandar 2025-08-05 17:38:08 +08:00
parent 0c93e93460
commit 77e3b8601f
3 changed files with 376 additions and 66 deletions

View File

@ -398,65 +398,6 @@ onMounted(() => {
</template> </template>
</rs-card> </rs-card>
</div> </div>
<!-- Development Roadmap -->
<rs-card>
<template #header>
<div class="flex items-center">
<Icon name="material-symbols:road" class="w-5 h-5 mr-2 text-purple-500" />
Development Roadmap & System Guide
</div>
</template>
<template #body>
<div class="space-y-6">
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 class="font-semibold text-blue-900 mb-2">🚀 Getting Started with Corrad BPM</h3>
<p class="text-blue-800 text-sm mb-3">
Follow these steps to start developing business processes in the system:
</p>
<ol class="list-decimal list-inside space-y-1 text-sm text-blue-800">
<li><strong>Design Forms:</strong> Use Form Builder to create data collection forms</li>
<li><strong>Create Processes:</strong> Use Process Builder to design workflow logic</li>
<li><strong>Configure Integrations:</strong> Set up API calls and business rules</li>
<li><strong>Test & Deploy:</strong> Test process execution and publish to production</li>
</ol>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div
v-for="phase in developmentRoadmap"
:key="phase.phase"
class="border border-gray-200 rounded-lg p-4"
>
<div class="flex items-center justify-between mb-3">
<h4 class="font-semibold text-gray-900">{{ phase.phase }}</h4>
<RsBadge :class="getStatusColor(phase.status)" size="sm">
{{ phase.status.replace('-', ' ') }}
</RsBadge>
</div>
<ul class="space-y-2">
<li
v-for="item in phase.items"
:key="item"
class="flex items-start text-sm text-gray-600"
>
<Icon
:name="phase.status === 'completed' ? 'material-symbols:check-circle' :
phase.status === 'in-progress' ? 'material-symbols:radio-button-partial' :
'material-symbols:radio-button-unchecked'"
:class="phase.status === 'completed' ? 'text-green-500' :
phase.status === 'in-progress' ? 'text-blue-500' :
'text-gray-400'"
class="w-4 h-4 mr-2 mt-0.5 flex-shrink-0"
/>
{{ item }}
</li>
</ul>
</div>
</div>
</div>
</template>
</rs-card>
</div> </div>
</template> </template>

View File

@ -125,7 +125,7 @@
<!-- Forms Grid --> <!-- Forms Grid -->
<div v-else-if="filteredForms.length > 0" class="grid gap-4"> <div v-else-if="filteredForms.length > 0" class="grid gap-4">
<div <div
v-for="form in filteredForms" v-for="form in paginatedForms"
:key="form.id" :key="form.id"
class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
> >
@ -204,8 +204,126 @@
</div> </div>
</div> </div>
<!-- Pagination -->
<div v-if="filteredForms.length > 0 && totalPages > 1" class="mt-6 bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
<!-- Pagination Info -->
<div class="text-sm text-gray-700">
Showing {{ paginationInfo.start }}-{{ paginationInfo.end }} of {{ paginationInfo.total }} forms
</div>
<!-- Items per page selector -->
<div class="flex items-center gap-2">
<span class="text-sm text-gray-700">Items per page:</span>
<FormKit
:model-value="itemsPerPage"
@update:model-value="changeItemsPerPage"
type="select"
:options="[
{ label: '5', value: 5 },
{ label: '10', value: 10 },
{ label: '20', value: 20 },
{ label: '50', value: 50 }
]"
:classes="{
outer: 'mb-0',
input: 'px-2 py-1 border border-gray-300 rounded text-sm bg-white min-w-16'
}"
/>
</div>
<!-- Pagination Controls -->
<div class="flex items-center gap-1">
<!-- Previous Button -->
<button
@click="prevPage"
:disabled="!hasPrevPage"
class="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<Icon name="material-symbols:chevron-left" class="w-4 h-4" />
</button>
<!-- Page Numbers -->
<template v-if="totalPages <= 7">
<!-- Show all pages if 7 or fewer -->
<button
v-for="page in totalPages"
:key="page"
@click="goToPage(page)"
:class="[
'px-3 py-2 text-sm border rounded-lg transition-colors',
page === currentPage
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
]"
>
{{ page }}
</button>
</template>
<template v-else>
<!-- Show first page -->
<button
@click="goToPage(1)"
:class="[
'px-3 py-2 text-sm border rounded-lg transition-colors',
1 === currentPage
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
]"
>
1
</button>
<!-- Show ellipsis if needed -->
<span v-if="currentPage > 3" class="px-2 text-gray-500">...</span>
<!-- Show pages around current page -->
<button
v-for="page in [currentPage - 1, currentPage, currentPage + 1].filter(p => p > 1 && p < totalPages)"
:key="page"
@click="goToPage(page)"
:class="[
'px-3 py-2 text-sm border rounded-lg transition-colors',
page === currentPage
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
]"
>
{{ page }}
</button>
<!-- Show ellipsis if needed -->
<span v-if="currentPage < totalPages - 2" class="px-2 text-gray-500">...</span>
<!-- Show last page -->
<button
v-if="totalPages > 1"
@click="goToPage(totalPages)"
:class="[
'px-3 py-2 text-sm border rounded-lg transition-colors',
totalPages === currentPage
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
]"
>
{{ totalPages }}
</button>
</template>
<!-- Next Button -->
<button
@click="nextPage"
:disabled="!hasNextPage"
class="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<Icon name="material-symbols:chevron-right" class="w-4 h-4" />
</button>
</div>
</div>
</div>
<!-- Empty State --> <!-- Empty State -->
<div v-else class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center"> <div v-else-if="filteredForms.length === 0" class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
<Icon name="material-symbols:description-outline" class="w-16 h-16 text-gray-300 mx-auto mb-4" /> <Icon name="material-symbols:description-outline" class="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 class="text-lg font-medium text-gray-900 mb-2"> <h3 class="text-lg font-medium text-gray-900 mb-2">
{{ hasActiveFilters ? 'No forms match your filters' : 'No forms found' }} {{ hasActiveFilters ? 'No forms match your filters' : 'No forms found' }}
@ -437,6 +555,11 @@ const jsonContent = ref('');
const jsonValidationMessage = ref(''); const jsonValidationMessage = ref('');
const jsonIsValid = ref(false); const jsonIsValid = ref(false);
// Pagination state
const currentPage = ref(1);
const itemsPerPage = ref(5); // Set to 5 to test pagination
const totalItems = ref(0);
// Computed properties for grouping // Computed properties for grouping
const categoryOptions = computed(() => { const categoryOptions = computed(() => {
const categories = [...new Set(formStore.savedForms.map(form => form.category).filter(Boolean))]; const categories = [...new Set(formStore.savedForms.map(form => form.category).filter(Boolean))];
@ -458,7 +581,7 @@ const hasActiveFilters = computed(() => {
return searchQuery.value || selectedCategory.value || selectedGroup.value; return searchQuery.value || selectedCategory.value || selectedGroup.value;
}); });
// Filtered forms // Filtered forms (all forms after applying filters)
const filteredForms = computed(() => { const filteredForms = computed(() => {
let filtered = formStore.savedForms; let filtered = formStore.savedForms;
@ -482,9 +605,42 @@ const filteredForms = computed(() => {
filtered = filtered.filter(form => form.group === selectedGroup.value); filtered = filtered.filter(form => form.group === selectedGroup.value);
} }
// Update total items for pagination
totalItems.value = filtered.length;
return filtered; return filtered;
}); });
// Paginated forms (forms for current page)
const paginatedForms = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage.value;
const end = start + itemsPerPage.value;
return filteredForms.value.slice(start, end);
});
// Pagination computed properties
const totalPages = computed(() => {
return Math.ceil(totalItems.value / itemsPerPage.value);
});
const hasNextPage = computed(() => {
return currentPage.value < totalPages.value;
});
const hasPrevPage = computed(() => {
return currentPage.value > 1;
});
const paginationInfo = computed(() => {
const start = totalItems.value === 0 ? 0 : (currentPage.value - 1) * itemsPerPage.value + 1;
const end = Math.min(currentPage.value * itemsPerPage.value, totalItems.value);
return {
start,
end,
total: totalItems.value
};
});
// Format date for display // Format date for display
const formatDate = (isoString) => { const formatDate = (isoString) => {
if (!isoString) return ''; if (!isoString) return '';
@ -623,6 +779,8 @@ onMounted(async () => {
// Watch for changes in search and filters // Watch for changes in search and filters
watch([searchQuery, selectedCategory, selectedGroup], () => { watch([searchQuery, selectedCategory, selectedGroup], () => {
// Reset to first page when filters change
currentPage.value = 1;
// Debounce the search to avoid too many API calls // Debounce the search to avoid too many API calls
clearTimeout(searchTimeout); clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => { searchTimeout = setTimeout(() => {
@ -637,9 +795,34 @@ const clearFilters = () => {
searchQuery.value = ''; searchQuery.value = '';
selectedCategory.value = ''; selectedCategory.value = '';
selectedGroup.value = ''; selectedGroup.value = '';
currentPage.value = 1; // Reset to first page
// loadForms will be called automatically by the watcher // loadForms will be called automatically by the watcher
}; };
// Pagination methods
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page;
}
};
const nextPage = () => {
if (hasNextPage.value) {
currentPage.value++;
}
};
const prevPage = () => {
if (hasPrevPage.value) {
currentPage.value--;
}
};
const changeItemsPerPage = (newItemsPerPage) => {
itemsPerPage.value = newItemsPerPage;
currentPage.value = 1; // Reset to first page when changing items per page
};
// Helper function to get category color // Helper function to get category color
const getCategoryColor = (category) => { const getCategoryColor = (category) => {
const colors = { const colors = {

View File

@ -29,6 +29,11 @@ const sortBy = ref('processCreatedDate');
const sortOrder = ref('desc'); const sortOrder = ref('desc');
const currentView = ref('dashboard'); // 'dashboard', 'list', 'analytics' const currentView = ref('dashboard'); // 'dashboard', 'list', 'analytics'
// Pagination state
const currentPage = ref(1);
const itemsPerPage = ref(5); // Set to 5 to test pagination
const totalItems = ref(0);
// Dashboard metrics and data // Dashboard metrics and data
const dashboardMetrics = ref({ const dashboardMetrics = ref({
totalProcesses: 0, totalProcesses: 0,
@ -66,9 +71,44 @@ const categoryOptions = [
{ value: 'Procurement', label: 'Procurement' } { value: 'Procurement', label: 'Procurement' }
]; ];
// Filtered processes // Filtered processes (all processes after applying filters)
const filteredProcesses = computed(() => { const filteredProcesses = computed(() => {
return processStore.processes; let filtered = processStore.processes;
// Update total items for pagination
totalItems.value = filtered.length;
return filtered;
});
// Paginated processes (processes for current page)
const paginatedProcesses = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage.value;
const end = start + itemsPerPage.value;
return filteredProcesses.value.slice(start, end);
});
// Pagination computed properties
const totalPages = computed(() => {
return Math.ceil(totalItems.value / itemsPerPage.value);
});
const hasNextPage = computed(() => {
return currentPage.value < totalPages.value;
});
const hasPrevPage = computed(() => {
return currentPage.value > 1;
});
const paginationInfo = computed(() => {
const start = totalItems.value === 0 ? 0 : (currentPage.value - 1) * itemsPerPage.value + 1;
const end = Math.min(currentPage.value * itemsPerPage.value, totalItems.value);
return {
start,
end,
total: totalItems.value
};
}); });
// Load dashboard summary data from API // Load dashboard summary data from API
@ -220,6 +260,8 @@ const loadProcesses = async () => {
// Watch for changes in filters and reload processes // Watch for changes in filters and reload processes
watch([searchQuery, statusFilter, categoryFilter], () => { watch([searchQuery, statusFilter, categoryFilter], () => {
// Reset to first page when filters change
currentPage.value = 1;
clearTimeout(searchTimeout); clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => { searchTimeout = setTimeout(() => {
loadProcesses(); loadProcesses();
@ -349,6 +391,31 @@ const clearFilters = () => {
searchQuery.value = ''; searchQuery.value = '';
statusFilter.value = ''; statusFilter.value = '';
categoryFilter.value = ''; categoryFilter.value = '';
currentPage.value = 1; // Reset to first page
};
// Pagination methods
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page;
}
};
const nextPage = () => {
if (hasNextPage.value) {
currentPage.value++;
}
};
const prevPage = () => {
if (hasPrevPage.value) {
currentPage.value--;
}
};
const changeItemsPerPage = (newItemsPerPage) => {
itemsPerPage.value = newItemsPerPage;
currentPage.value = 1; // Reset to first page when changing items per page
}; };
// Load processes on component mount // Load processes on component mount
@ -653,8 +720,9 @@ const copyWorkflowLink = async (processId) => {
<!-- Processes Grid --> <!-- Processes Grid -->
<div v-else-if="filteredProcesses.length > 0" class="grid gap-4"> <div v-else-if="filteredProcesses.length > 0" class="grid gap-4">
<div <div
v-for="process in filteredProcesses" v-for="process in paginatedProcesses"
:key="process.id" :key="process.id"
class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
> >
@ -774,8 +842,126 @@ const copyWorkflowLink = async (processId) => {
</div> </div>
</div> </div>
<!-- Pagination -->
<div v-if="filteredProcesses.length > 0 && totalPages > 1" class="mt-6 bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
<!-- Pagination Info -->
<div class="text-sm text-gray-700">
Showing {{ paginationInfo.start }}-{{ paginationInfo.end }} of {{ paginationInfo.total }} processes
</div>
<!-- Items per page selector -->
<div class="flex items-center gap-2">
<span class="text-sm text-gray-700">Items per page:</span>
<FormKit
:model-value="itemsPerPage"
@update:model-value="changeItemsPerPage"
type="select"
:options="[
{ label: '5', value: 5 },
{ label: '10', value: 10 },
{ label: '20', value: 20 },
{ label: '50', value: 50 }
]"
:classes="{
outer: 'mb-0',
input: 'px-2 py-1 border border-gray-300 rounded text-sm bg-white min-w-16'
}"
/>
</div>
<!-- Pagination Controls -->
<div class="flex items-center gap-1">
<!-- Previous Button -->
<button
@click="prevPage"
:disabled="!hasPrevPage"
class="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<Icon name="material-symbols:chevron-left" class="w-4 h-4" />
</button>
<!-- Page Numbers -->
<template v-if="totalPages <= 7">
<!-- Show all pages if 7 or fewer -->
<button
v-for="page in totalPages"
:key="page"
@click="goToPage(page)"
:class="[
'px-3 py-2 text-sm border rounded-lg transition-colors',
page === currentPage
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
]"
>
{{ page }}
</button>
</template>
<template v-else>
<!-- Show first page -->
<button
@click="goToPage(1)"
:class="[
'px-3 py-2 text-sm border rounded-lg transition-colors',
1 === currentPage
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
]"
>
1
</button>
<!-- Show ellipsis if needed -->
<span v-if="currentPage > 3" class="px-2 text-gray-500">...</span>
<!-- Show pages around current page -->
<button
v-for="page in [currentPage - 1, currentPage, currentPage + 1].filter(p => p > 1 && p < totalPages)"
:key="page"
@click="goToPage(page)"
:class="[
'px-3 py-2 text-sm border rounded-lg transition-colors',
page === currentPage
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
]"
>
{{ page }}
</button>
<!-- Show ellipsis if needed -->
<span v-if="currentPage < totalPages - 2" class="px-2 text-gray-500">...</span>
<!-- Show last page -->
<button
v-if="totalPages > 1"
@click="goToPage(totalPages)"
:class="[
'px-3 py-2 text-sm border rounded-lg transition-colors',
totalPages === currentPage
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
]"
>
{{ totalPages }}
</button>
</template>
<!-- Next Button -->
<button
@click="nextPage"
:disabled="!hasNextPage"
class="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<Icon name="material-symbols:chevron-right" class="w-4 h-4" />
</button>
</div>
</div>
</div>
<!-- Empty State --> <!-- Empty State -->
<div v-else class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center"> <div v-else-if="filteredProcesses.length === 0" 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" /> <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"> <h3 class="text-lg font-medium text-gray-900 mb-2">
{{ (searchQuery || statusFilter || categoryFilter) ? 'No processes match your filters' : 'No processes found' }} {{ (searchQuery || statusFilter || categoryFilter) ? 'No processes match your filters' : 'No processes found' }}