Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
12603a77
authored
Jul 04, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(redemption): add status filtering and cleanup action
parent
86021d8e
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
272 additions
and
130 deletions
+272
-130
controller/redemption.go
+2
-1
model/redemption.go
+29
-7
model/redemption_test.go
+100
-0
web/default/src/features/redemption-codes/api.ts
+7
-4
web/default/src/features/redemption-codes/components/data-table-bulk-actions.tsx
+14
-94
web/default/src/features/redemption-codes/components/redemptions-primary-buttons.tsx
+69
-8
web/default/src/features/redemption-codes/components/redemptions-table.tsx
+39
-12
web/default/src/features/redemption-codes/constants.ts
+9
-2
web/default/src/features/redemption-codes/types.ts
+1
-0
web/default/src/routes/_authenticated/redemption-codes/index.tsx
+2
-2
No files found.
controller/redemption.go
View file @
12603a77
...
@@ -29,8 +29,9 @@ func GetAllRedemptions(c *gin.Context) {
...
@@ -29,8 +29,9 @@ func GetAllRedemptions(c *gin.Context) {
func
SearchRedemptions
(
c
*
gin
.
Context
)
{
func
SearchRedemptions
(
c
*
gin
.
Context
)
{
keyword
:=
c
.
Query
(
"keyword"
)
keyword
:=
c
.
Query
(
"keyword"
)
status
:=
c
.
Query
(
"status"
)
pageInfo
:=
common
.
GetPageQuery
(
c
)
pageInfo
:=
common
.
GetPageQuery
(
c
)
redemptions
,
total
,
err
:=
model
.
SearchRedemptions
(
keyword
,
pageInfo
.
GetStartIdx
(),
pageInfo
.
GetPageSize
())
redemptions
,
total
,
err
:=
model
.
SearchRedemptions
(
keyword
,
status
,
pageInfo
.
GetStartIdx
(),
pageInfo
.
GetPageSize
())
if
err
!=
nil
{
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
common
.
ApiError
(
c
,
err
)
return
return
...
...
model/redemption.go
View file @
12603a77
...
@@ -60,7 +60,7 @@ func GetAllRedemptions(startIdx int, num int) (redemptions []*Redemption, total
...
@@ -60,7 +60,7 @@ func GetAllRedemptions(startIdx int, num int) (redemptions []*Redemption, total
return
redemptions
,
total
,
nil
return
redemptions
,
total
,
nil
}
}
func
SearchRedemptions
(
keyword
string
,
startIdx
int
,
num
int
)
(
redemptions
[]
*
Redemption
,
total
int64
,
err
error
)
{
func
SearchRedemptions
(
keyword
string
,
sta
tus
string
,
sta
rtIdx
int
,
num
int
)
(
redemptions
[]
*
Redemption
,
total
int64
,
err
error
)
{
tx
:=
DB
.
Begin
()
tx
:=
DB
.
Begin
()
if
tx
.
Error
!=
nil
{
if
tx
.
Error
!=
nil
{
return
nil
,
0
,
tx
.
Error
return
nil
,
0
,
tx
.
Error
...
@@ -71,14 +71,36 @@ func SearchRedemptions(keyword string, startIdx int, num int) (redemptions []*Re
...
@@ -71,14 +71,36 @@ func SearchRedemptions(keyword string, startIdx int, num int) (redemptions []*Re
}
}
}()
}()
// Build query based on keyword type
query
:=
tx
.
Model
(
&
Redemption
{})
query
:=
tx
.
Model
(
&
Redemption
{})
// Only try to convert to ID if the string represents a valid integer
if
keyword
!=
""
{
if
id
,
err
:=
strconv
.
Atoi
(
keyword
);
err
==
nil
{
if
id
,
err
:=
strconv
.
Atoi
(
keyword
);
err
==
nil
{
query
=
query
.
Where
(
"id = ? OR name LIKE ?"
,
id
,
keyword
+
"%"
)
query
=
query
.
Where
(
"id = ? OR name LIKE ?"
,
id
,
keyword
+
"%"
)
}
else
{
}
else
{
query
=
query
.
Where
(
"name LIKE ?"
,
keyword
+
"%"
)
query
=
query
.
Where
(
"name LIKE ?"
,
keyword
+
"%"
)
}
}
if
status
!=
""
{
now
:=
common
.
GetTimestamp
()
switch
status
{
case
"expired"
:
query
=
query
.
Where
(
"status = ? AND expired_time != 0 AND expired_time < ?"
,
common
.
RedemptionCodeStatusEnabled
,
now
,
)
case
strconv
.
Itoa
(
common
.
RedemptionCodeStatusEnabled
)
:
query
=
query
.
Where
(
"status = ? AND (expired_time = 0 OR expired_time >= ?)"
,
common
.
RedemptionCodeStatusEnabled
,
now
,
)
case
strconv
.
Itoa
(
common
.
RedemptionCodeStatusDisabled
)
:
query
=
query
.
Where
(
"status = ?"
,
common
.
RedemptionCodeStatusDisabled
)
case
strconv
.
Itoa
(
common
.
RedemptionCodeStatusUsed
)
:
query
=
query
.
Where
(
"status = ?"
,
common
.
RedemptionCodeStatusUsed
)
}
}
}
// Get total count
// Get total count
...
...
model/redemption_test.go
0 → 100644
View file @
12603a77
package
model
import
(
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func
TestSearchRedemptionsFiltersAndPaginates
(
t
*
testing
.
T
)
{
require
.
NoError
(
t
,
DB
.
AutoMigrate
(
&
Redemption
{}))
require
.
NoError
(
t
,
DB
.
Session
(
&
gorm
.
Session
{
AllowGlobalUpdate
:
true
})
.
Unscoped
()
.
Delete
(
&
Redemption
{})
.
Error
)
t
.
Cleanup
(
func
()
{
require
.
NoError
(
t
,
DB
.
Session
(
&
gorm
.
Session
{
AllowGlobalUpdate
:
true
})
.
Unscoped
()
.
Delete
(
&
Redemption
{})
.
Error
)
})
now
:=
common
.
GetTimestamp
()
redemptions
:=
[]
Redemption
{
{
Id
:
1
,
Name
:
"alpha-active"
,
Key
:
"00000000000000000000000000000001"
,
Status
:
common
.
RedemptionCodeStatusEnabled
,
ExpiredTime
:
0
},
{
Id
:
2
,
Name
:
"alpha-future"
,
Key
:
"00000000000000000000000000000002"
,
Status
:
common
.
RedemptionCodeStatusEnabled
,
ExpiredTime
:
now
+
3600
},
{
Id
:
3
,
Name
:
"alpha-expired"
,
Key
:
"00000000000000000000000000000003"
,
Status
:
common
.
RedemptionCodeStatusEnabled
,
ExpiredTime
:
now
-
10
},
{
Id
:
4
,
Name
:
"beta-disabled"
,
Key
:
"00000000000000000000000000000004"
,
Status
:
common
.
RedemptionCodeStatusDisabled
,
ExpiredTime
:
0
},
{
Id
:
5
,
Name
:
"beta-used"
,
Key
:
"00000000000000000000000000000005"
,
Status
:
common
.
RedemptionCodeStatusUsed
,
ExpiredTime
:
0
},
}
require
.
NoError
(
t
,
DB
.
Create
(
&
redemptions
)
.
Error
)
tests
:=
[]
struct
{
name
string
keyword
string
status
string
startIdx
int
num
int
wantTotal
int64
wantIds
[]
int
}{
{
name
:
"no filters returns all rows"
,
num
:
10
,
wantTotal
:
5
,
wantIds
:
[]
int
{
5
,
4
,
3
,
2
,
1
},
},
{
name
:
"keyword filters by name prefix"
,
keyword
:
"alpha"
,
num
:
10
,
wantTotal
:
3
,
wantIds
:
[]
int
{
3
,
2
,
1
},
},
{
name
:
"enabled status excludes expired rows"
,
status
:
"1"
,
num
:
10
,
wantTotal
:
2
,
wantIds
:
[]
int
{
2
,
1
},
},
{
name
:
"expired status returns enabled expired rows"
,
status
:
"expired"
,
num
:
10
,
wantTotal
:
1
,
wantIds
:
[]
int
{
3
},
},
{
name
:
"disabled status"
,
status
:
"2"
,
num
:
10
,
wantTotal
:
1
,
wantIds
:
[]
int
{
4
},
},
{
name
:
"used status"
,
status
:
"3"
,
num
:
10
,
wantTotal
:
1
,
wantIds
:
[]
int
{
5
},
},
{
name
:
"pagination keeps unpaged total"
,
startIdx
:
1
,
num
:
2
,
wantTotal
:
5
,
wantIds
:
[]
int
{
4
,
3
},
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
rows
,
total
,
err
:=
SearchRedemptions
(
tt
.
keyword
,
tt
.
status
,
tt
.
startIdx
,
tt
.
num
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
tt
.
wantTotal
,
total
)
gotIds
:=
make
([]
int
,
0
,
len
(
rows
))
for
_
,
row
:=
range
rows
{
gotIds
=
append
(
gotIds
,
row
.
Id
)
}
assert
.
Equal
(
t
,
tt
.
wantIds
,
gotIds
)
})
}
}
web/default/src/features/redemption-codes/api.ts
View file @
12603a77
...
@@ -44,10 +44,13 @@ export async function getRedemptions(
...
@@ -44,10 +44,13 @@ export async function getRedemptions(
export
async
function
searchRedemptions
(
export
async
function
searchRedemptions
(
params
:
SearchRedemptionsParams
params
:
SearchRedemptionsParams
):
Promise
<
GetRedemptionsResponse
>
{
):
Promise
<
GetRedemptionsResponse
>
{
const
{
keyword
=
''
,
p
=
1
,
page_size
=
10
}
=
params
const
{
keyword
=
''
,
status
=
''
,
p
=
1
,
page_size
=
10
}
=
params
const
res
=
await
api
.
get
(
const
queryParams
=
new
URLSearchParams
()
`/api/redemption/search?keyword=
${
keyword
}
&p=
${
p
}
&page_size=
${
page_size
}
`
queryParams
.
set
(
'keyword'
,
keyword
)
)
if
(
status
)
queryParams
.
set
(
'status'
,
status
)
queryParams
.
set
(
'p'
,
String
(
p
))
queryParams
.
set
(
'page_size'
,
String
(
page_size
))
const
res
=
await
api
.
get
(
`/api/redemption/search?
${
queryParams
.
toString
()}
`
)
return
res
.
data
return
res
.
data
}
}
...
...
web/default/src/features/redemption-codes/components/data-table-bulk-actions.tsx
View file @
12603a77
...
@@ -16,25 +16,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -16,25 +16,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
{
type
Table
}
from
'@tanstack/react-table'
import
type
{
Table
}
from
'@tanstack/react-table'
import
{
Trash2
}
from
'lucide-react'
import
{
useMemo
}
from
'react'
import
{
useState
,
useMemo
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
toast
}
from
'sonner'
import
{
ConfirmDialog
}
from
'@/components/confirm-dialog'
import
{
CopyButton
}
from
'@/components/copy-button'
import
{
CopyButton
}
from
'@/components/copy-button'
import
{
DataTableBulkActions
as
BulkActionsToolbar
}
from
'@/components/data-table'
import
{
DataTableBulkActions
as
BulkActionsToolbar
}
from
'@/components/data-table'
import
{
Button
}
from
'@/components/ui/button'
import
{
Tooltip
,
TooltipContent
,
TooltipTrigger
,
}
from
'@/components/ui/tooltip'
import
{
deleteInvalidRedemptions
}
from
'../api'
import
type
{
Redemption
}
from
'../types'
import
{
type
Redemption
}
from
'../types'
import
{
useRedemptions
}
from
'./redemptions-provider'
type
DataTableBulkActionsProps
<
TData
>
=
{
type
DataTableBulkActionsProps
<
TData
>
=
{
table
:
Table
<
TData
>
table
:
Table
<
TData
>
...
@@ -44,11 +33,7 @@ export function DataTableBulkActions<TData>({
...
@@ -44,11 +33,7 @@ export function DataTableBulkActions<TData>({
table
,
table
,
}:
DataTableBulkActionsProps
<
TData
>
)
{
}:
DataTableBulkActionsProps
<
TData
>
)
{
const
{
t
}
=
useTranslation
()
const
{
t
}
=
useTranslation
()
const
{
triggerRefresh
}
=
useRedemptions
()
const
selectedRows
=
table
.
getSelectedRowModel
().
rows
const
[
showDeleteInvalidConfirm
,
setShowDeleteInvalidConfirm
]
=
useState
(
false
)
const
[
isDeleting
,
setIsDeleting
]
=
useState
(
false
)
const
selectedRows
=
table
.
getFilteredSelectedRowModel
().
rows
const
contentToCopy
=
useMemo
(()
=>
{
const
contentToCopy
=
useMemo
(()
=>
{
const
selectedCodes
=
selectedRows
.
map
((
row
)
=>
{
const
selectedCodes
=
selectedRows
.
map
((
row
)
=>
{
...
@@ -58,82 +43,17 @@ export function DataTableBulkActions<TData>({
...
@@ -58,82 +43,17 @@ export function DataTableBulkActions<TData>({
return
selectedCodes
.
join
(
'\n'
)
return
selectedCodes
.
join
(
'\n'
)
},
[
selectedRows
])
},
[
selectedRows
])
const
handleDeleteInvalid
=
async
()
=>
{
setIsDeleting
(
true
)
try
{
const
result
=
await
deleteInvalidRedemptions
()
if
(
result
.
success
)
{
const
count
=
result
.
data
||
0
toast
.
success
(
t
(
'Successfully deleted {{count}} invalid redemption codes'
,
{
count
,
})
)
table
.
resetRowSelection
()
triggerRefresh
()
setShowDeleteInvalidConfirm
(
false
)
}
}
finally
{
setIsDeleting
(
false
)
}
}
return
(
return
(
<>
<
BulkActionsToolbar
table=
{
table
}
entityName=
{
t
(
'redemption code'
)
}
>
<
BulkActionsToolbar
table=
{
table
}
entityName=
{
t
(
'redemption code'
)
}
>
<
CopyButton
<
CopyButton
value=
{
contentToCopy
}
value=
{
contentToCopy
}
variant=
'outline'
variant=
'outline'
size=
'icon'
size=
'icon'
className=
'size-8'
className=
'size-8'
tooltip=
{
t
(
'Copy selected codes'
)
}
tooltip=
{
t
(
'Copy selected codes'
)
}
successTooltip=
{
t
(
'Codes copied!'
)
}
successTooltip=
{
t
(
'Codes copied!'
)
}
aria
-
label=
{
t
(
'Copy selected codes'
)
}
aria
-
label=
{
t
(
'Copy selected codes'
)
}
/>
<
Tooltip
>
<
TooltipTrigger
render=
{
<
Button
variant=
'destructive'
size=
'icon'
onClick=
{
()
=>
setShowDeleteInvalidConfirm
(
true
)
}
className=
'size-8'
aria
-
label=
{
t
(
'Delete invalid redemption codes'
)
}
title=
{
t
(
'Delete invalid redemption codes'
)
}
/>
}
>
<
Trash2
/>
<
span
className=
'sr-only'
>
{
t
(
'Delete invalid codes'
)
}
</
span
>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
{
t
(
'Delete invalid codes (used/disabled/expired)'
)
}
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
BulkActionsToolbar
>
<
ConfirmDialog
destructive
open=
{
showDeleteInvalidConfirm
}
onOpenChange=
{
setShowDeleteInvalidConfirm
}
handleConfirm=
{
handleDeleteInvalid
}
isLoading=
{
isDeleting
}
className=
'max-w-md'
title=
{
t
(
'Delete Invalid Redemption Codes?'
)
}
desc=
{
<>
{
t
(
'This will delete all'
)
}
<
strong
>
{
t
(
'used'
)
}
</
strong
>
,
{
' '
}
<
strong
>
{
t
(
'disabled'
)
}
</
strong
>
{
t
(
', and'
)
}
<
strong
>
{
t
(
'expired'
)
}
</
strong
>
{
' '
}
{
t
(
'redemption codes.'
)
}
<
br
/>
{
t
(
'This action cannot be undone.'
)
}
</>
}
confirmText=
{
t
(
'Delete Invalid'
)
}
/>
/>
</>
</
BulkActionsToolbar
>
)
)
}
}
web/default/src/features/redemption-codes/components/redemptions-primary-buttons.tsx
View file @
12603a77
...
@@ -16,22 +16,83 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -16,22 +16,83 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
{
Plus
}
from
'lucide-react'
import
{
Plus
,
Trash2
}
from
'lucide-react'
import
{
useState
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
toast
}
from
'sonner'
import
{
ConfirmDialog
}
from
'@/components/confirm-dialog'
import
{
Button
}
from
'@/components/ui/button'
import
{
Button
}
from
'@/components/ui/button'
import
{
deleteInvalidRedemptions
}
from
'../api'
import
{
ERROR_MESSAGES
}
from
'../constants'
import
{
useRedemptions
}
from
'./redemptions-provider'
import
{
useRedemptions
}
from
'./redemptions-provider'
export
function
RedemptionsPrimaryButtons
()
{
export
function
RedemptionsPrimaryButtons
()
{
const
{
t
}
=
useTranslation
()
const
{
t
}
=
useTranslation
()
const
{
setOpen
}
=
useRedemptions
()
const
{
setOpen
,
triggerRefresh
}
=
useRedemptions
()
const
[
showDeleteInvalidConfirm
,
setShowDeleteInvalidConfirm
]
=
useState
(
false
)
const
[
isDeleting
,
setIsDeleting
]
=
useState
(
false
)
const
handleDeleteInvalid
=
async
()
=>
{
setIsDeleting
(
true
)
try
{
const
result
=
await
deleteInvalidRedemptions
()
if
(
result
.
success
)
{
const
count
=
result
.
data
||
0
toast
.
success
(
t
(
'Successfully deleted {{count}} invalid redemption codes'
,
{
count
,
})
)
triggerRefresh
()
setShowDeleteInvalidConfirm
(
false
)
}
else
{
toast
.
error
(
result
.
message
||
t
(
ERROR_MESSAGES
.
DELETE_INVALID_FAILED
))
}
}
finally
{
setIsDeleting
(
false
)
}
}
return
(
return
(
<
div
className=
'flex gap-2'
>
<>
<
Button
size=
'sm'
onClick=
{
()
=>
setOpen
(
'create'
)
}
>
<
div
className=
'flex flex-wrap gap-2'
>
<
Plus
className=
'h-4 w-4'
/>
<
Button
{
t
(
'Create Code'
)
}
size=
'sm'
</
Button
>
variant=
'outline'
</
div
>
onClick=
{
()
=>
setShowDeleteInvalidConfirm
(
true
)
}
>
<
Trash2
className=
'text-destructive h-4 w-4'
/>
{
t
(
'Delete Invalid'
)
}
</
Button
>
<
Button
size=
'sm'
onClick=
{
()
=>
setOpen
(
'create'
)
}
>
<
Plus
className=
'h-4 w-4'
/>
{
t
(
'Create Code'
)
}
</
Button
>
</
div
>
<
ConfirmDialog
destructive
open=
{
showDeleteInvalidConfirm
}
onOpenChange=
{
setShowDeleteInvalidConfirm
}
handleConfirm=
{
handleDeleteInvalid
}
isLoading=
{
isDeleting
}
className=
'max-w-md'
title=
{
t
(
'Delete Invalid Redemption Codes?'
)
}
desc=
{
<>
{
t
(
'This will delete all'
)
}
<
strong
>
{
t
(
'used'
)
}
</
strong
>
,
{
' '
}
<
strong
>
{
t
(
'disabled'
)
}
</
strong
>
{
t
(
', and'
)
}
<
strong
>
{
t
(
'expired'
)
}
</
strong
>
{
' '
}
{
t
(
'redemption codes.'
)
}
<
br
/>
{
t
(
'This action cannot be undone.'
)
}
</>
}
confirmText=
{
t
(
'Delete Invalid'
)
}
/>
</>
)
)
}
}
web/default/src/features/redemption-codes/components/redemptions-table.tsx
View file @
12603a77
...
@@ -20,6 +20,7 @@ import { useQuery } from '@tanstack/react-query'
...
@@ -20,6 +20,7 @@ import { useQuery } from '@tanstack/react-query'
import
{
getRouteApi
}
from
'@tanstack/react-router'
import
{
getRouteApi
}
from
'@tanstack/react-router'
import
{
useMemo
}
from
'react'
import
{
useMemo
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
toast
}
from
'sonner'
import
{
import
{
DISABLED_ROW_DESKTOP
,
DISABLED_ROW_DESKTOP
,
...
@@ -31,7 +32,11 @@ import { useMediaQuery } from '@/hooks'
...
@@ -31,7 +32,11 @@ import { useMediaQuery } from '@/hooks'
import
{
useTableUrlState
}
from
'@/hooks/use-table-url-state'
import
{
useTableUrlState
}
from
'@/hooks/use-table-url-state'
import
{
getRedemptions
,
searchRedemptions
}
from
'../api'
import
{
getRedemptions
,
searchRedemptions
}
from
'../api'
import
{
REDEMPTION_STATUS
,
getRedemptionStatusOptions
}
from
'../constants'
import
{
ERROR_MESSAGES
,
REDEMPTION_STATUS
,
getRedemptionStatusOptions
,
}
from
'../constants'
import
{
isRedemptionExpired
}
from
'../lib'
import
{
isRedemptionExpired
}
from
'../lib'
import
type
{
Redemption
}
from
'../types'
import
type
{
Redemption
}
from
'../types'
import
{
DataTableBulkActions
}
from
'./data-table-bulk-actions'
import
{
DataTableBulkActions
}
from
'./data-table-bulk-actions'
...
@@ -68,6 +73,11 @@ export function RedemptionsTable() {
...
@@ -68,6 +73,11 @@ export function RedemptionsTable() {
globalFilter
:
{
enabled
:
true
,
key
:
'filter'
},
globalFilter
:
{
enabled
:
true
,
key
:
'filter'
},
columnFilters
:
[{
columnId
:
'status'
,
searchKey
:
'status'
,
type
:
'array'
}],
columnFilters
:
[{
columnId
:
'status'
,
searchKey
:
'status'
,
type
:
'array'
}],
})
})
const
statusFilter
=
(
columnFilters
.
find
((
filter
)
=>
filter
.
id
===
'status'
)?.
value
as
|
string
[]
|
undefined
)
??
[]
const
statusFilterValue
=
statusFilter
[
0
]
??
''
// Fetch data with React Query
// Fetch data with React Query
const
{
data
,
isLoading
,
isFetching
}
=
useQuery
({
const
{
data
,
isLoading
,
isFetching
}
=
useQuery
({
...
@@ -76,18 +86,37 @@ export function RedemptionsTable() {
...
@@ -76,18 +86,37 @@ export function RedemptionsTable() {
pagination
.
pageIndex
+
1
,
pagination
.
pageIndex
+
1
,
pagination
.
pageSize
,
pagination
.
pageSize
,
globalFilter
,
globalFilter
,
statusFilterValue
,
refreshTrigger
,
refreshTrigger
,
],
],
queryFn
:
async
()
=>
{
queryFn
:
async
()
=>
{
const
hasFilter
=
globalFilter
?.
trim
()
const
hasFilter
=
globalFilter
?.
trim
()
const
hasStatusFilter
=
statusFilterValue
!==
''
const
params
=
{
const
params
=
{
p
:
pagination
.
pageIndex
+
1
,
p
:
pagination
.
pageIndex
+
1
,
page_size
:
pagination
.
pageSize
,
page_size
:
pagination
.
pageSize
,
}
}
const
result
=
hasFilter
const
result
=
?
await
searchRedemptions
({
...
params
,
keyword
:
globalFilter
})
hasFilter
||
hasStatusFilter
:
await
getRedemptions
(
params
)
?
await
searchRedemptions
({
...
params
,
keyword
:
globalFilter
,
status
:
statusFilterValue
,
})
:
await
getRedemptions
(
params
)
if
(
!
result
.
success
)
{
toast
.
error
(
result
.
message
||
t
(
hasFilter
||
hasStatusFilter
?
ERROR_MESSAGES
.
SEARCH_FAILED
:
ERROR_MESSAGES
.
LOAD_FAILED
)
)
return
{
items
:
[],
total
:
0
}
}
return
{
return
{
items
:
result
.
data
?.
items
||
[],
items
:
result
.
data
?.
items
||
[],
...
@@ -116,7 +145,8 @@ export function RedemptionsTable() {
...
@@ -116,7 +145,8 @@ export function RedemptionsTable() {
onPaginationChange
,
onPaginationChange
,
onGlobalFilterChange
,
onGlobalFilterChange
,
onColumnFiltersChange
,
onColumnFiltersChange
,
manualPagination
:
!
globalFilter
,
manualPagination
:
true
,
manualFiltering
:
true
,
totalCount
:
data
?.
total
||
0
,
totalCount
:
data
?.
total
||
0
,
ensurePageInRange
,
ensurePageInRange
,
})
})
...
@@ -149,13 +179,10 @@ export function RedemptionsTable() {
...
@@ -149,13 +179,10 @@ export function RedemptionsTable() {
},
},
],
],
}
}
}
}
getRowClassName=
{
(
row
,
{
isMobile
})
=>
getRowClassName=
{
(
row
,
{
isMobile
})
=>
{
isDisabledRedemptionRow
(
row
.
original
)
if
(
!
isDisabledRedemptionRow
(
row
.
original
))
return
undefined
?
isMobile
return
isMobile
?
DISABLED_ROW_MOBILE
:
DISABLED_ROW_DESKTOP
?
DISABLED_ROW_MOBILE
}
}
:
DISABLED_ROW_DESKTOP
:
undefined
}
bulkActions=
{
<
DataTableBulkActions
table=
{
table
}
/>
}
bulkActions=
{
<
DataTableBulkActions
table=
{
table
}
/>
}
/>
/>
)
)
...
...
web/default/src/features/redemption-codes/constants.ts
View file @
12603a77
...
@@ -16,9 +16,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -16,9 +16,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
{
type
TFunction
}
from
'i18next'
import
type
{
TFunction
}
from
'i18next'
import
{
type
StatusBadgeProps
}
from
'@/components/status-badge'
import
type
{
StatusBadgeProps
}
from
'@/components/status-badge'
// ============================================================================
// ============================================================================
// Redemption Status Configuration
// Redemption Status Configuration
...
@@ -63,6 +63,13 @@ export const REDEMPTION_STATUSES: Record<
...
@@ -63,6 +63,13 @@ export const REDEMPTION_STATUSES: Record<
// Note: "Expired" is not a real DB status, it's computed from expired_time
// Note: "Expired" is not a real DB status, it's computed from expired_time
export
const
REDEMPTION_FILTER_EXPIRED
=
'expired'
export
const
REDEMPTION_FILTER_EXPIRED
=
'expired'
export
const
REDEMPTION_FILTER_VALUES
=
[
String
(
REDEMPTION_STATUS
.
ENABLED
),
String
(
REDEMPTION_STATUS
.
DISABLED
),
String
(
REDEMPTION_STATUS
.
USED
),
REDEMPTION_FILTER_EXPIRED
,
]
as
const
export
function
getRedemptionStatusOptions
(
t
:
TFunction
)
{
export
function
getRedemptionStatusOptions
(
t
:
TFunction
)
{
return
[
return
[
...
Object
.
values
(
REDEMPTION_STATUSES
).
map
((
config
)
=>
({
...
Object
.
values
(
REDEMPTION_STATUSES
).
map
((
config
)
=>
({
...
...
web/default/src/features/redemption-codes/types.ts
View file @
12603a77
...
@@ -65,6 +65,7 @@ export interface GetRedemptionsResponse {
...
@@ -65,6 +65,7 @@ export interface GetRedemptionsResponse {
export
interface
SearchRedemptionsParams
{
export
interface
SearchRedemptionsParams
{
keyword
?:
string
keyword
?:
string
status
?:
string
p
?:
number
p
?:
number
page_size
?:
number
page_size
?:
number
}
}
...
...
web/default/src/routes/_authenticated/redemption-codes/index.tsx
View file @
12603a77
...
@@ -20,7 +20,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router'
...
@@ -20,7 +20,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router'
import
z
from
'zod'
import
z
from
'zod'
import
{
Redemptions
}
from
'@/features/redemption-codes'
import
{
Redemptions
}
from
'@/features/redemption-codes'
import
{
REDEMPTION_
STATUS
_VALUES
}
from
'@/features/redemption-codes/constants'
import
{
REDEMPTION_
FILTER
_VALUES
}
from
'@/features/redemption-codes/constants'
import
{
ROLE
}
from
'@/lib/roles'
import
{
ROLE
}
from
'@/lib/roles'
import
{
useAuthStore
}
from
'@/stores/auth-store'
import
{
useAuthStore
}
from
'@/stores/auth-store'
...
@@ -28,7 +28,7 @@ const redemptionsSearchSchema = z.object({
...
@@ -28,7 +28,7 @@ const redemptionsSearchSchema = z.object({
page
:
z
.
number
().
optional
().
catch
(
1
),
page
:
z
.
number
().
optional
().
catch
(
1
),
pageSize
:
z
.
number
().
optional
().
catch
(
10
),
pageSize
:
z
.
number
().
optional
().
catch
(
10
),
filter
:
z
.
string
().
optional
().
catch
(
''
),
filter
:
z
.
string
().
optional
().
catch
(
''
),
status
:
z
.
array
(
z
.
enum
(
REDEMPTION_
STATUS
_VALUES
)).
optional
().
catch
([]),
status
:
z
.
array
(
z
.
enum
(
REDEMPTION_
FILTER
_VALUES
)).
optional
().
catch
([]),
})
})
export
const
Route
=
createFileRoute
(
'/_authenticated/redemption-codes/'
)({
export
const
Route
=
createFileRoute
(
'/_authenticated/redemption-codes/'
)({
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment