Add paginated users/roles, user status, frontend permissions, profile pictures, identifier file storage
- Users page: paginated, searchable, sortable with inline roles (no N+1) - Roles page: paginated, searchable, sortable + /roles/all for dropdowns - User is_active field with migration, PATCH toggle, auth check (disabled=401) - Frontend permission checks: auth store loads permissions, sidebar/buttons conditional - Profile pictures via file storage for users and members, avatar component - Identifier images use file storage API instead of base64 - Fix TypeScript errors across admin UI - 64 API tests passing (10 new)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { MemberIdentifier } from '@/types/account'
|
||||
import type { PaginatedResponse } from '@forte/shared/schemas'
|
||||
|
||||
export const identifierKeys = {
|
||||
all: (memberId: string) => ['members', memberId, 'identifiers'] as const,
|
||||
@@ -9,7 +10,7 @@ export const identifierKeys = {
|
||||
export function identifierListOptions(memberId: string) {
|
||||
return queryOptions({
|
||||
queryKey: identifierKeys.all(memberId),
|
||||
queryFn: () => api.get<{ data: MemberIdentifier[] }>(`/v1/members/${memberId}/identifiers`),
|
||||
queryFn: () => api.get<PaginatedResponse<MemberIdentifier>>(`/v1/members/${memberId}/identifiers`, { page: 1, limit: 100, order: 'asc' }),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { PaymentMethod } from '@/types/account'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
|
||||
export const paymentMethodKeys = {
|
||||
all: (accountId: string) => ['accounts', accountId, 'payment-methods'] as const,
|
||||
list: (accountId: string, params: PaginationInput) => [...paymentMethodKeys.all(accountId), params] as const,
|
||||
}
|
||||
|
||||
export function paymentMethodListOptions(accountId: string) {
|
||||
export function paymentMethodListOptions(accountId: string, params: PaginationInput) {
|
||||
return queryOptions({
|
||||
queryKey: paymentMethodKeys.all(accountId),
|
||||
queryFn: () => api.get<{ data: PaymentMethod[] }>(`/v1/accounts/${accountId}/payment-methods`),
|
||||
queryKey: paymentMethodKeys.list(accountId, params),
|
||||
queryFn: () => api.get<PaginatedResponse<PaymentMethod>>(`/v1/accounts/${accountId}/payment-methods`, params),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { ProcessorLink } from '@/types/account'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
|
||||
export const processorLinkKeys = {
|
||||
all: (accountId: string) => ['accounts', accountId, 'processor-links'] as const,
|
||||
list: (accountId: string, params: PaginationInput) => [...processorLinkKeys.all(accountId), params] as const,
|
||||
}
|
||||
|
||||
export function processorLinkListOptions(accountId: string) {
|
||||
export function processorLinkListOptions(accountId: string, params: PaginationInput) {
|
||||
return queryOptions({
|
||||
queryKey: processorLinkKeys.all(accountId),
|
||||
queryFn: () => api.get<{ data: ProcessorLink[] }>(`/v1/accounts/${accountId}/processor-links`),
|
||||
queryKey: processorLinkKeys.list(accountId, params),
|
||||
queryFn: () => api.get<PaginatedResponse<ProcessorLink>>(`/v1/accounts/${accountId}/processor-links`, params),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { Permission, Role } from '@/types/rbac'
|
||||
import type { PaginationInput, PaginatedResponse } from '@forte/shared/schemas'
|
||||
|
||||
export const rbacKeys = {
|
||||
permissions: ['permissions'] as const,
|
||||
roles: ['roles'] as const,
|
||||
roleList: (params: PaginationInput) => ['roles', 'list', params] as const,
|
||||
role: (id: string) => ['roles', id] as const,
|
||||
userRoles: (userId: string) => ['users', userId, 'roles'] as const,
|
||||
myPermissions: ['me', 'permissions'] as const,
|
||||
@@ -17,10 +19,19 @@ export function permissionListOptions() {
|
||||
})
|
||||
}
|
||||
|
||||
/** All roles (for dropdowns, selectors) */
|
||||
export function roleListOptions() {
|
||||
return queryOptions({
|
||||
queryKey: rbacKeys.roles,
|
||||
queryFn: () => api.get<{ data: Role[] }>('/v1/roles'),
|
||||
queryFn: () => api.get<{ data: Role[] }>('/v1/roles/all'),
|
||||
})
|
||||
}
|
||||
|
||||
/** Paginated roles (for the roles list page) */
|
||||
export function rolePageOptions(params: PaginationInput) {
|
||||
return queryOptions({
|
||||
queryKey: rbacKeys.roleList(params),
|
||||
queryFn: () => api.get<PaginatedResponse<Role>>('/v1/roles', params as Record<string, unknown>),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { TaxExemption } from '@/types/account'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
|
||||
export const taxExemptionKeys = {
|
||||
all: (accountId: string) => ['accounts', accountId, 'tax-exemptions'] as const,
|
||||
list: (accountId: string, params: PaginationInput) => [...taxExemptionKeys.all(accountId), params] as const,
|
||||
}
|
||||
|
||||
export function taxExemptionListOptions(accountId: string) {
|
||||
export function taxExemptionListOptions(accountId: string, params: PaginationInput) {
|
||||
return queryOptions({
|
||||
queryKey: taxExemptionKeys.all(accountId),
|
||||
queryFn: () => api.get<{ data: TaxExemption[] }>(`/v1/accounts/${accountId}/tax-exemptions`),
|
||||
queryKey: taxExemptionKeys.list(accountId, params),
|
||||
queryFn: () => api.get<PaginatedResponse<TaxExemption>>(`/v1/accounts/${accountId}/tax-exemptions`, params),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { PaginationInput, PaginatedResponse } from '@forte/shared/schemas'
|
||||
|
||||
export interface UserRole {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
isSystem: boolean
|
||||
}
|
||||
|
||||
export interface UserRecord {
|
||||
id: string
|
||||
@@ -7,16 +15,31 @@ export interface UserRecord {
|
||||
firstName: string
|
||||
lastName: string
|
||||
role: string
|
||||
isActive: boolean
|
||||
createdAt: string
|
||||
roles: UserRole[]
|
||||
}
|
||||
|
||||
export const userKeys = {
|
||||
list: (params: PaginationInput) => ['users', params] as const,
|
||||
roles: (userId: string) => ['users', userId, 'roles'] as const,
|
||||
}
|
||||
|
||||
export function userListOptions(params: PaginationInput) {
|
||||
return queryOptions({
|
||||
queryKey: userKeys.list(params),
|
||||
queryFn: () => api.get<PaginatedResponse<UserRecord>>('/v1/users', params as Record<string, unknown>),
|
||||
})
|
||||
}
|
||||
|
||||
export function userRolesOptions(userId: string) {
|
||||
return queryOptions({
|
||||
queryKey: userKeys.roles(userId),
|
||||
queryFn: () => api.get<{ data: { id: string; name: string; slug: string; isSystem: boolean }[] }>(`/v1/users/${userId}/roles`),
|
||||
queryFn: () => api.get<{ data: UserRole[] }>(`/v1/users/${userId}/roles`),
|
||||
})
|
||||
}
|
||||
|
||||
export const userMutations = {
|
||||
toggleStatus: (userId: string, isActive: boolean) =>
|
||||
api.patch<{ id: string; isActive: boolean }>(`/v1/users/${userId}/status`, { isActive }),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user