diff --git a/src/actions/get-credit-transactions.ts b/src/actions/get-credit-transactions.ts index ec40ae5..2e5b052 100644 --- a/src/actions/get-credit-transactions.ts +++ b/src/actions/get-credit-transactions.ts @@ -44,17 +44,30 @@ export const getCreditTransactionsAction = userActionClient const { pageIndex, pageSize, search, sorting } = parsedInput; const currentUser = (ctx as { user: User }).user; - // search by type, amount, paymentId, description, and restrict to current user + // Search logic: text fields use ilike, and if search is a number, also search amount fields + const searchConditions = []; + if (search) { + // Always search text fields + searchConditions.push( + ilike(creditTransaction.type, `%${search}%`), + ilike(creditTransaction.paymentId, `%${search}%`), + ilike(creditTransaction.description, `%${search}%`) + ); + + // If search is a valid number, also search numeric fields + const numericSearch = Number.parseInt(search, 10); + if (!Number.isNaN(numericSearch)) { + searchConditions.push( + eq(creditTransaction.amount, numericSearch), + eq(creditTransaction.remainingAmount, numericSearch) + ); + } + } + const where = search ? and( eq(creditTransaction.userId, currentUser.id), - or( - ilike(creditTransaction.type, `%${search}%`), - ilike(creditTransaction.amount, `%${search}%`), - ilike(creditTransaction.remainingAmount, `%${search}%`), - ilike(creditTransaction.paymentId, `%${search}%`), - ilike(creditTransaction.description, `%${search}%`) - ) + or(...searchConditions) ) : eq(creditTransaction.userId, currentUser.id); diff --git a/src/components/admin/users-table.tsx b/src/components/admin/users-table.tsx index baf21ee..2485f49 100644 --- a/src/components/admin/users-table.tsx +++ b/src/components/admin/users-table.tsx @@ -515,7 +515,7 @@ export function UsersTable({ - {[5, 10, 20, 30, 40, 50].map((pageSize) => ( + {[10, 20, 30, 40, 50].map((pageSize) => ( {pageSize} diff --git a/src/components/settings/credits/credit-transactions-table.tsx b/src/components/settings/credits/credit-transactions-table.tsx index 0e1fd51..39fedfa 100644 --- a/src/components/settings/credits/credit-transactions-table.tsx +++ b/src/components/settings/credits/credit-transactions-table.tsx @@ -76,6 +76,7 @@ import { useState } from 'react'; import { toast } from 'sonner'; import { Badge } from '../../ui/badge'; import { Label } from '../../ui/label'; +import { Skeleton } from '../../ui/skeleton'; // Define the credit transaction interface export interface CreditTransaction { @@ -152,12 +153,27 @@ function DataTableColumnHeader({ ); } +function TableRowSkeleton({ columns }: { columns: number }) { + return ( + + {Array.from({ length: columns }).map((_, index) => ( + +
+ +
+
+ ))} +
+ ); +} + interface CreditTransactionsTableProps { data: CreditTransaction[]; total: number; pageIndex: number; pageSize: number; search: string; + sorting?: SortingState; loading?: boolean; onSearch: (search: string) => void; onPageChange: (page: number) => void; @@ -171,6 +187,7 @@ export function CreditTransactionsTable({ pageIndex, pageSize, search, + sorting = [{ id: 'createdAt', desc: true }], loading, onSearch, onPageChange, @@ -179,9 +196,6 @@ export function CreditTransactionsTable({ }: CreditTransactionsTableProps) { const t = useTranslations('Dashboard.settings.credits.transactions'); const tTable = useTranslations('Common.table'); - const [sorting, setSorting] = useState([ - { id: 'createdAt', desc: true }, - ]); const [columnFilters, setColumnFilters] = useState([]); const [columnVisibility, setColumnVisibility] = useState({}); @@ -449,7 +463,6 @@ export function CreditTransactionsTable({ }, onSortingChange: (updater) => { const next = typeof updater === 'function' ? updater(sorting) : updater; - setSorting(next); onSortingChange?.(next); }, onColumnFiltersChange: setColumnFilters, @@ -538,7 +551,12 @@ export function CreditTransactionsTable({ ))} - {table.getRowModel().rows?.length ? ( + {loading ? ( + // Show skeleton rows while loading + Array.from({ length: pageSize }).map((_, index) => ( + + )) + ) : table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( - {loading ? tTable('loading') : tTable('noResults')} + {tTable('noResults')} )} diff --git a/src/components/settings/credits/credit-transactions.tsx b/src/components/settings/credits/credit-transactions.tsx index 608f0c9..d9b0351 100644 --- a/src/components/settings/credits/credit-transactions.tsx +++ b/src/components/settings/credits/credit-transactions.tsx @@ -4,22 +4,36 @@ import { CreditTransactionsTable } from '@/components/settings/credits/credit-tr import { useCreditTransactions } from '@/hooks/use-credits'; import type { SortingState } from '@tanstack/react-table'; import { useTranslations } from 'next-intl'; -import { useState } from 'react'; +import { + parseAsIndex, + parseAsInteger, + parseAsString, + useQueryStates, +} from 'nuqs'; +import { useMemo } from 'react'; /** * Credit transactions component */ export function CreditTransactions() { const t = useTranslations('Dashboard.settings.credits.transactions'); - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(10); - const [search, setSearch] = useState(''); - const [sorting, setSorting] = useState([ - { id: 'createdAt', desc: true }, - ]); + + const [{ page, pageSize, search, sortId, sortDesc }, setQueryStates] = + useQueryStates({ + page: parseAsIndex.withDefault(0), // 0-based internally, 1-based in URL + pageSize: parseAsInteger.withDefault(10), + search: parseAsString.withDefault(''), + sortId: parseAsString.withDefault('createdAt'), + sortDesc: parseAsInteger.withDefault(1), + }); + + const sorting: SortingState = useMemo( + () => [{ id: sortId, desc: Boolean(sortDesc) }], + [sortId, sortDesc] + ); const { data, isLoading } = useCreditTransactions( - pageIndex, + page, pageSize, search, sorting @@ -29,14 +43,24 @@ export function CreditTransactions() { setQueryStates({ search: newSearch, page: 0 })} + onPageChange={(newPageIndex) => setQueryStates({ page: newPageIndex })} + onPageSizeChange={(newPageSize) => + setQueryStates({ pageSize: newPageSize, page: 0 }) + } + onSortingChange={(newSorting) => { + if (newSorting.length > 0) { + setQueryStates({ + sortId: newSorting[0].id, + sortDesc: newSorting[0].desc ? 1 : 0, + }); + } + }} /> ); } diff --git a/src/components/settings/credits/credits-page-client.tsx b/src/components/settings/credits/credits-page-client.tsx index dc5172f..34cedb8 100644 --- a/src/components/settings/credits/credits-page-client.tsx +++ b/src/components/settings/credits/credits-page-client.tsx @@ -5,6 +5,7 @@ import { CreditTransactions } from '@/components/settings/credits/credit-transac import CreditsBalanceCard from '@/components/settings/credits/credits-balance-card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useTranslations } from 'next-intl'; +import { parseAsStringLiteral, useQueryState } from 'nuqs'; /** * Credits page client, show credit balance and transactions @@ -12,9 +13,24 @@ import { useTranslations } from 'next-intl'; export default function CreditsPageClient() { const t = useTranslations('Dashboard.settings.credits'); + const [activeTab, setActiveTab] = useQueryState( + 'tab', + parseAsStringLiteral(['balance', 'transactions']).withDefault('balance') + ); + + const handleTabChange = (value: string) => { + if (value === 'balance' || value === 'transactions') { + setActiveTab(value); + } + }; + return (
- + {t('tabs.balance')}