diff --git a/apps/admin/src/components/ui/pagination.tsx b/apps/admin/src/components/ui/pagination.tsx
new file mode 100644
index 0000000..c972b32
--- /dev/null
+++ b/apps/admin/src/components/ui/pagination.tsx
@@ -0,0 +1,97 @@
+import { ChevronLeft, ChevronRight } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { cn } from '@/lib/utils';
+
+type PaginationControlsProps = {
+ page: number;
+ pageSize: number;
+ totalCount: number;
+ onPageChange: (page: number) => void;
+ onPageSizeChange?: (pageSize: number) => void;
+ pageSizeOptions?: number[];
+ showPageSize?: boolean;
+ className?: string;
+};
+
+export function PaginationControls({
+ page,
+ pageSize,
+ totalCount,
+ onPageChange,
+ onPageSizeChange,
+ pageSizeOptions = [10, 25, 50],
+ showPageSize = true,
+ className,
+}: PaginationControlsProps) {
+ const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
+ const safePage = Math.min(Math.max(page, 1), totalPages);
+ const start = totalCount === 0 ? 0 : (safePage - 1) * pageSize + 1;
+ const end = totalCount === 0 ? 0 : Math.min(safePage * pageSize, totalCount);
+
+ return (
+
+
+ {totalCount === 0
+ ? 'No results'
+ : `Showing ${start} to ${end} of ${totalCount}`}
+
+
+ {showPageSize && onPageSizeChange && (
+
+ Rows
+
+
+ )}
+
+
+
+ Page {safePage} of {totalPages}
+
+
+
+
+
+ );
+}
diff --git a/apps/admin/src/hooks/useAuditedSupabase.ts b/apps/admin/src/hooks/useAuditedSupabase.ts
index fa14327..c24329d 100644
--- a/apps/admin/src/hooks/useAuditedSupabase.ts
+++ b/apps/admin/src/hooks/useAuditedSupabase.ts
@@ -86,10 +86,10 @@ export const auditedSupabase = {
/**
* Update a single record with audit logging
*/
- async update(
+ async update>(
table: string,
id: string,
- updates: Partial
+ updates: Partial | Record
): Promise> {
// Fetch old values before update
let oldValues: Record | null = null;
diff --git a/apps/admin/src/hooks/useEntityForm.ts b/apps/admin/src/hooks/useEntityForm.ts
index 3c4c1fc..421d6e3 100644
--- a/apps/admin/src/hooks/useEntityForm.ts
+++ b/apps/admin/src/hooks/useEntityForm.ts
@@ -43,8 +43,8 @@ import { useState, useCallback } from 'react';
* ```
*/
export function useEntityForm<
- TFormData extends Record,
- TEntity extends Record = TFormData
+ TFormData extends object,
+ TEntity extends object = TFormData
>(
initialFormData: TFormData,
/**
diff --git a/apps/admin/src/pages/AuditLogsPage.tsx b/apps/admin/src/pages/AuditLogsPage.tsx
index 0a5b5b2..8866cec 100644
--- a/apps/admin/src/pages/AuditLogsPage.tsx
+++ b/apps/admin/src/pages/AuditLogsPage.tsx
@@ -1,7 +1,8 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import { supabase } from '@/config';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
+import { PaginationControls } from '@/components/ui/pagination';
import {
Table,
TableBody,
@@ -34,28 +35,42 @@ const actionColors: Record = {
export function AuditLogsPage() {
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(true);
+ const [page, setPage] = useState(1);
+ const [pageSize, setPageSize] = useState(50);
+ const [totalCount, setTotalCount] = useState(0);
+
+ const fetchLogs = useCallback(async () => {
+ setLoading(true);
+ try {
+ const from = (page - 1) * pageSize;
+ const to = from + pageSize - 1;
+
+ const { data, error, count } = await supabase
+ .from('audit_logs')
+ .select('*', { count: 'exact' })
+ .order('created_at', { ascending: false })
+ .range(from, to);
+
+ if (error) throw error;
+ setLogs(data || []);
+ setTotalCount(count || 0);
+ } catch (error) {
+ console.error('Failed to load audit logs:', error);
+ } finally {
+ setLoading(false);
+ }
+ }, [page, pageSize]);
useEffect(() => {
- const fetchLogs = async () => {
- setLoading(true);
- try {
- const { data, error } = await supabase
- .from('audit_logs')
- .select('*')
- .order('created_at', { ascending: false })
- .limit(100);
-
- if (error) throw error;
- setLogs(data || []);
- } catch (error) {
- console.error('Failed to load audit logs:', error);
- } finally {
- setLoading(false);
- }
- };
-
fetchLogs();
- }, []);
+ }, [fetchLogs]);
+
+ useEffect(() => {
+ const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
+ if (page > totalPages) {
+ setPage(totalPages);
+ }
+ }, [page, pageSize, totalCount]);
if (loading) {
return (
@@ -141,6 +156,19 @@ export function AuditLogsPage() {
+
+ {logs.length > 0 && (
+ {
+ setPage(1);
+ setPageSize(size);
+ }}
+ />
+ )}
);
}
diff --git a/apps/admin/src/pages/FeedbackPage.tsx b/apps/admin/src/pages/FeedbackPage.tsx
index 0b57f92..c9f571f 100644
--- a/apps/admin/src/pages/FeedbackPage.tsx
+++ b/apps/admin/src/pages/FeedbackPage.tsx
@@ -3,7 +3,9 @@ import { supabase } from '@/config';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
-import { Skeleton } from '@/components/ui/skeleton';
+import { Skeleton } from '@/components/ui/skeleton';
+import { PaginationControls } from '@/components/ui/pagination';
+
import { Textarea } from '@/components/ui/textarea';
import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription';
import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator';
@@ -84,9 +86,12 @@ const statusConfig: Record([]);
const [loading, setLoading] = useState(true);
- const [search, setSearch] = useState('');
- const [typeFilter, setTypeFilter] = useState('all');
- const [statusFilter, setStatusFilter] = useState('all');
+ const [search, setSearch] = useState('');
+ const [typeFilter, setTypeFilter] = useState('all');
+ const [statusFilter, setStatusFilter] = useState('all');
+ const [page, setPage] = useState(1);
+ const [pageSize, setPageSize] = useState(25);
+
// Detail Dialog State
const [selectedFeedback, setSelectedFeedback] = useState(null);
@@ -136,9 +141,14 @@ export function FeedbackPage() {
},
});
- useEffect(() => {
- fetchFeedback();
- }, [fetchFeedback]);
+ useEffect(() => {
+ fetchFeedback();
+ }, [fetchFeedback]);
+
+ useEffect(() => {
+ setPage(1);
+ }, [search, typeFilter, statusFilter]);
+
const handleOpenDetail = (item: Feedback) => {
setSelectedFeedback(item);
@@ -173,19 +183,33 @@ export function FeedbackPage() {
}
};
- const filteredFeedback = feedback.filter(item => {
- if (typeFilter !== 'all' && item.type !== typeFilter) return false;
- if (statusFilter !== 'all' && item.status !== statusFilter) return false;
- if (!search) return true;
-
- const query = search.toLowerCase();
- return (
- item.title.toLowerCase().includes(query) ||
- item.description.toLowerCase().includes(query) ||
- item.profile?.name?.toLowerCase().includes(query) ||
- item.profile?.email?.toLowerCase().includes(query)
- );
- });
+ const filteredFeedback = feedback.filter(item => {
+ if (typeFilter !== 'all' && item.type !== typeFilter) return false;
+ if (statusFilter !== 'all' && item.status !== statusFilter) return false;
+ if (!search) return true;
+
+ const query = search.toLowerCase();
+ return (
+ item.title.toLowerCase().includes(query) ||
+ item.description.toLowerCase().includes(query) ||
+ item.profile?.name?.toLowerCase().includes(query) ||
+ item.profile?.email?.toLowerCase().includes(query)
+ );
+ });
+
+ const totalCount = filteredFeedback.length;
+ const paginatedFeedback = filteredFeedback.slice(
+ (page - 1) * pageSize,
+ page * pageSize
+ );
+
+ useEffect(() => {
+ const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
+ if (page > totalPages) {
+ setPage(totalPages);
+ }
+ }, [page, pageSize, totalCount]);
+
const getTypeBadge = (type: FeedbackType) => {
const config = feedbackTypeConfig[type];
@@ -267,67 +291,80 @@ export function FeedbackPage() {
{/* Table */}
-
-
-
-
- Type
- Title
- User
- Status
- Created
- Actions
-
-
-
- {loading ? (
-
-
-
-
-
- ) : filteredFeedback.length === 0 ? (
-
-
- {search || typeFilter !== 'all' || statusFilter !== 'all'
- ? 'No feedback matches your filters.'
- : 'No feedback submissions yet.'}
-
-
- ) : (
- filteredFeedback.map((item) => (
- handleOpenDetail(item)}>
- {getTypeBadge(item.type)}
-
-
-
{item.title}
-
- {item.description.slice(0, 80)}
- {item.description.length > 80 && '...'}
-
-
-
-
-
-
{item.profile?.name || 'Unknown'}
-
{item.profile?.email}
-
-
- {getStatusBadge(item.status)}
-
- {format(new Date(item.created_at), 'MMM d, yyyy')}
-
-
-
-
-
- ))
- )}
-
-
-
+
+
+
+
+ Type
+ Title
+ User
+ Status
+ Created
+ Actions
+
+
+
+ {loading ? (
+
+
+
+
+
+ ) : filteredFeedback.length === 0 ? (
+
+
+ {search || typeFilter !== 'all' || statusFilter !== 'all'
+ ? 'No feedback matches your filters.'
+ : 'No feedback submissions yet.'}
+
+
+ ) : (
+ paginatedFeedback.map((item) => (
+ handleOpenDetail(item)}>
+ {getTypeBadge(item.type)}
+
+
+
{item.title}
+
+ {item.description.slice(0, 80)}
+ {item.description.length > 80 && '...'}
+
+
+
+
+
+
{item.profile?.name || 'Unknown'}
+
{item.profile?.email}
+
+
+ {getStatusBadge(item.status)}
+
+ {format(new Date(item.created_at), 'MMM d, yyyy')}
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+ {filteredFeedback.length > 0 && (
+ {
+ setPage(1);
+ setPageSize(size);
+ }}
+ />
+ )}
{/* Detail Dialog */}