Add top-level members list, primary member on account, member move, combined create flows
- GET /v1/members with search across all members (includes account name) - POST /members/:id/move with optional accountId (creates new account if omitted) - primary_member_id on account table, auto-set when first member added - isMinor flag on member create (manual override when no DOB provided) - Account search now includes member names - New account form includes primary contact fields, auto-generates name - Members page in sidebar with global search
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as LoginRouteImport } from './routes/login'
|
||||
import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
|
||||
import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index'
|
||||
import { Route as AuthenticatedMembersRouteImport } from './routes/_authenticated/members'
|
||||
import { Route as AuthenticatedAccountsIndexRouteImport } from './routes/_authenticated/accounts/index'
|
||||
import { Route as AuthenticatedAccountsNewRouteImport } from './routes/_authenticated/accounts/new'
|
||||
import { Route as AuthenticatedAccountsAccountIdRouteImport } from './routes/_authenticated/accounts/$accountId'
|
||||
import { Route as AuthenticatedAccountsAccountIdIndexRouteImport } from './routes/_authenticated/accounts/$accountId/index'
|
||||
import { Route as AuthenticatedAccountsAccountIdTaxExemptionsRouteImport } from './routes/_authenticated/accounts/$accountId/tax-exemptions'
|
||||
import { Route as AuthenticatedAccountsAccountIdProcessorLinksRouteImport } from './routes/_authenticated/accounts/$accountId/processor-links'
|
||||
import { Route as AuthenticatedAccountsAccountIdPaymentMethodsRouteImport } from './routes/_authenticated/accounts/$accountId/payment-methods'
|
||||
import { Route as AuthenticatedAccountsAccountIdMembersRouteImport } from './routes/_authenticated/accounts/$accountId/members'
|
||||
|
||||
const LoginRoute = LoginRouteImport.update({
|
||||
id: '/login',
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthenticatedRoute = AuthenticatedRouteImport.update({
|
||||
id: '/_authenticated',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthenticatedIndexRoute = AuthenticatedIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedMembersRoute = AuthenticatedMembersRouteImport.update({
|
||||
id: '/members',
|
||||
path: '/members',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsIndexRoute =
|
||||
AuthenticatedAccountsIndexRouteImport.update({
|
||||
id: '/accounts/',
|
||||
path: '/accounts/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsNewRoute =
|
||||
AuthenticatedAccountsNewRouteImport.update({
|
||||
id: '/accounts/new',
|
||||
path: '/accounts/new',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdRoute =
|
||||
AuthenticatedAccountsAccountIdRouteImport.update({
|
||||
id: '/accounts/$accountId',
|
||||
path: '/accounts/$accountId',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdIndexRoute =
|
||||
AuthenticatedAccountsAccountIdIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdTaxExemptionsRoute =
|
||||
AuthenticatedAccountsAccountIdTaxExemptionsRouteImport.update({
|
||||
id: '/tax-exemptions',
|
||||
path: '/tax-exemptions',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdProcessorLinksRoute =
|
||||
AuthenticatedAccountsAccountIdProcessorLinksRouteImport.update({
|
||||
id: '/processor-links',
|
||||
path: '/processor-links',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdPaymentMethodsRoute =
|
||||
AuthenticatedAccountsAccountIdPaymentMethodsRouteImport.update({
|
||||
id: '/payment-methods',
|
||||
path: '/payment-methods',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdMembersRoute =
|
||||
AuthenticatedAccountsAccountIdMembersRouteImport.update({
|
||||
id: '/members',
|
||||
path: '/members',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/members': typeof AuthenticatedMembersRoute
|
||||
'/accounts/$accountId': typeof AuthenticatedAccountsAccountIdRouteWithChildren
|
||||
'/accounts/new': typeof AuthenticatedAccountsNewRoute
|
||||
'/accounts/': typeof AuthenticatedAccountsIndexRoute
|
||||
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
'/accounts/$accountId/tax-exemptions': typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
|
||||
'/accounts/$accountId/': typeof AuthenticatedAccountsAccountIdIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/login': typeof LoginRoute
|
||||
'/members': typeof AuthenticatedMembersRoute
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/accounts/new': typeof AuthenticatedAccountsNewRoute
|
||||
'/accounts': typeof AuthenticatedAccountsIndexRoute
|
||||
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
'/accounts/$accountId/tax-exemptions': typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
|
||||
'/accounts/$accountId': typeof AuthenticatedAccountsAccountIdIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/_authenticated': typeof AuthenticatedRouteWithChildren
|
||||
'/login': typeof LoginRoute
|
||||
'/_authenticated/members': typeof AuthenticatedMembersRoute
|
||||
'/_authenticated/': typeof AuthenticatedIndexRoute
|
||||
'/_authenticated/accounts/$accountId': typeof AuthenticatedAccountsAccountIdRouteWithChildren
|
||||
'/_authenticated/accounts/new': typeof AuthenticatedAccountsNewRoute
|
||||
'/_authenticated/accounts/': typeof AuthenticatedAccountsIndexRoute
|
||||
'/_authenticated/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/_authenticated/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/_authenticated/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
'/_authenticated/accounts/$accountId/tax-exemptions': typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
|
||||
'/_authenticated/accounts/$accountId/': typeof AuthenticatedAccountsAccountIdIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/login'
|
||||
| '/members'
|
||||
| '/accounts/$accountId'
|
||||
| '/accounts/new'
|
||||
| '/accounts/'
|
||||
| '/accounts/$accountId/members'
|
||||
| '/accounts/$accountId/payment-methods'
|
||||
| '/accounts/$accountId/processor-links'
|
||||
| '/accounts/$accountId/tax-exemptions'
|
||||
| '/accounts/$accountId/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/login'
|
||||
| '/members'
|
||||
| '/'
|
||||
| '/accounts/new'
|
||||
| '/accounts'
|
||||
| '/accounts/$accountId/members'
|
||||
| '/accounts/$accountId/payment-methods'
|
||||
| '/accounts/$accountId/processor-links'
|
||||
| '/accounts/$accountId/tax-exemptions'
|
||||
| '/accounts/$accountId'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/_authenticated'
|
||||
| '/login'
|
||||
| '/_authenticated/members'
|
||||
| '/_authenticated/'
|
||||
| '/_authenticated/accounts/$accountId'
|
||||
| '/_authenticated/accounts/new'
|
||||
| '/_authenticated/accounts/'
|
||||
| '/_authenticated/accounts/$accountId/members'
|
||||
| '/_authenticated/accounts/$accountId/payment-methods'
|
||||
| '/_authenticated/accounts/$accountId/processor-links'
|
||||
| '/_authenticated/accounts/$accountId/tax-exemptions'
|
||||
| '/_authenticated/accounts/$accountId/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
|
||||
LoginRoute: typeof LoginRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/login': {
|
||||
id: '/login'
|
||||
path: '/login'
|
||||
fullPath: '/login'
|
||||
preLoaderRoute: typeof LoginRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_authenticated': {
|
||||
id: '/_authenticated'
|
||||
path: ''
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof AuthenticatedRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_authenticated/': {
|
||||
id: '/_authenticated/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof AuthenticatedIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/members': {
|
||||
id: '/_authenticated/members'
|
||||
path: '/members'
|
||||
fullPath: '/members'
|
||||
preLoaderRoute: typeof AuthenticatedMembersRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/accounts/': {
|
||||
id: '/_authenticated/accounts/'
|
||||
path: '/accounts'
|
||||
fullPath: '/accounts/'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/accounts/new': {
|
||||
id: '/_authenticated/accounts/new'
|
||||
path: '/accounts/new'
|
||||
fullPath: '/accounts/new'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsNewRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId': {
|
||||
id: '/_authenticated/accounts/$accountId'
|
||||
path: '/accounts/$accountId'
|
||||
fullPath: '/accounts/$accountId'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/': {
|
||||
id: '/_authenticated/accounts/$accountId/'
|
||||
path: '/'
|
||||
fullPath: '/accounts/$accountId/'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/tax-exemptions': {
|
||||
id: '/_authenticated/accounts/$accountId/tax-exemptions'
|
||||
path: '/tax-exemptions'
|
||||
fullPath: '/accounts/$accountId/tax-exemptions'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdTaxExemptionsRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/processor-links': {
|
||||
id: '/_authenticated/accounts/$accountId/processor-links'
|
||||
path: '/processor-links'
|
||||
fullPath: '/accounts/$accountId/processor-links'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdProcessorLinksRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/payment-methods': {
|
||||
id: '/_authenticated/accounts/$accountId/payment-methods'
|
||||
path: '/payment-methods'
|
||||
fullPath: '/accounts/$accountId/payment-methods'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdPaymentMethodsRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/members': {
|
||||
id: '/_authenticated/accounts/$accountId/members'
|
||||
path: '/members'
|
||||
fullPath: '/accounts/$accountId/members'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdMembersRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthenticatedAccountsAccountIdRouteChildren {
|
||||
AuthenticatedAccountsAccountIdMembersRoute: typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
AuthenticatedAccountsAccountIdPaymentMethodsRoute: typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
AuthenticatedAccountsAccountIdProcessorLinksRoute: typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
AuthenticatedAccountsAccountIdTaxExemptionsRoute: typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
|
||||
AuthenticatedAccountsAccountIdIndexRoute: typeof AuthenticatedAccountsAccountIdIndexRoute
|
||||
}
|
||||
|
||||
const AuthenticatedAccountsAccountIdRouteChildren: AuthenticatedAccountsAccountIdRouteChildren =
|
||||
{
|
||||
AuthenticatedAccountsAccountIdMembersRoute:
|
||||
AuthenticatedAccountsAccountIdMembersRoute,
|
||||
AuthenticatedAccountsAccountIdPaymentMethodsRoute:
|
||||
AuthenticatedAccountsAccountIdPaymentMethodsRoute,
|
||||
AuthenticatedAccountsAccountIdProcessorLinksRoute:
|
||||
AuthenticatedAccountsAccountIdProcessorLinksRoute,
|
||||
AuthenticatedAccountsAccountIdTaxExemptionsRoute:
|
||||
AuthenticatedAccountsAccountIdTaxExemptionsRoute,
|
||||
AuthenticatedAccountsAccountIdIndexRoute:
|
||||
AuthenticatedAccountsAccountIdIndexRoute,
|
||||
}
|
||||
|
||||
const AuthenticatedAccountsAccountIdRouteWithChildren =
|
||||
AuthenticatedAccountsAccountIdRoute._addFileChildren(
|
||||
AuthenticatedAccountsAccountIdRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticatedRouteChildren {
|
||||
AuthenticatedMembersRoute: typeof AuthenticatedMembersRoute
|
||||
AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute
|
||||
AuthenticatedAccountsAccountIdRoute: typeof AuthenticatedAccountsAccountIdRouteWithChildren
|
||||
AuthenticatedAccountsNewRoute: typeof AuthenticatedAccountsNewRoute
|
||||
AuthenticatedAccountsIndexRoute: typeof AuthenticatedAccountsIndexRoute
|
||||
}
|
||||
|
||||
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
||||
AuthenticatedMembersRoute: AuthenticatedMembersRoute,
|
||||
AuthenticatedIndexRoute: AuthenticatedIndexRoute,
|
||||
AuthenticatedAccountsAccountIdRoute:
|
||||
AuthenticatedAccountsAccountIdRouteWithChildren,
|
||||
AuthenticatedAccountsNewRoute: AuthenticatedAccountsNewRoute,
|
||||
AuthenticatedAccountsIndexRoute: AuthenticatedAccountsIndexRoute,
|
||||
}
|
||||
|
||||
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
|
||||
AuthenticatedRouteChildren,
|
||||
)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
AuthenticatedRoute: AuthenticatedRouteWithChildren,
|
||||
LoginRoute: LoginRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
@@ -3,9 +3,15 @@ import { api } from '@/lib/api-client'
|
||||
import type { Member } from '@/types/account'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
|
||||
interface MemberWithAccount extends Member {
|
||||
accountName: string | null
|
||||
}
|
||||
|
||||
export const memberKeys = {
|
||||
all: (accountId: string) => ['accounts', accountId, 'members'] as const,
|
||||
list: (accountId: string, params: PaginationInput) => [...memberKeys.all(accountId), params] as const,
|
||||
globalAll: ['members'] as const,
|
||||
globalList: (params: PaginationInput) => ['members', 'list', params] as const,
|
||||
}
|
||||
|
||||
export function memberListOptions(accountId: string, params: PaginationInput) {
|
||||
@@ -15,6 +21,13 @@ export function memberListOptions(accountId: string, params: PaginationInput) {
|
||||
})
|
||||
}
|
||||
|
||||
export function globalMemberListOptions(params: PaginationInput) {
|
||||
return queryOptions({
|
||||
queryKey: memberKeys.globalList(params),
|
||||
queryFn: () => api.get<PaginatedResponse<MemberWithAccount>>('/v1/members', params),
|
||||
})
|
||||
}
|
||||
|
||||
export const memberMutations = {
|
||||
create: (accountId: string, data: Record<string, unknown>) =>
|
||||
api.post<Member>(`/v1/accounts/${accountId}/members`, data),
|
||||
@@ -24,4 +37,9 @@ export const memberMutations = {
|
||||
|
||||
delete: (id: string) =>
|
||||
api.del<Member>(`/v1/members/${id}`),
|
||||
|
||||
move: (id: string, accountId?: string) =>
|
||||
api.post<Member>(`/v1/members/${id}/move`, accountId ? { accountId } : {}),
|
||||
}
|
||||
|
||||
export type { MemberWithAccount }
|
||||
|
||||
@@ -2,31 +2,26 @@
|
||||
|
||||
@variant dark (&:is(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
/* Defaults — overridden at runtime by theme store */
|
||||
--color-background: hsl(210 20% 97%);
|
||||
--color-foreground: hsl(222 47% 11%);
|
||||
--color-card: hsl(0 0% 100%);
|
||||
--color-card-foreground: hsl(222 47% 11%);
|
||||
--color-popover: hsl(0 0% 100%);
|
||||
--color-popover-foreground: hsl(222 47% 11%);
|
||||
--color-primary: hsl(215 25% 27%);
|
||||
--color-primary-foreground: hsl(210 40% 98%);
|
||||
--color-secondary: hsl(210 40% 94%);
|
||||
--color-secondary-foreground: hsl(222 47% 11%);
|
||||
--color-muted: hsl(210 40% 94%);
|
||||
--color-muted-foreground: hsl(215 16% 47%);
|
||||
--color-accent: hsl(210 40% 94%);
|
||||
--color-accent-foreground: hsl(222 47% 11%);
|
||||
--color-destructive: hsl(0 72% 51%);
|
||||
--color-destructive-foreground: hsl(210 40% 98%);
|
||||
--color-border: hsl(214 32% 89%);
|
||||
--color-input: hsl(214 32% 89%);
|
||||
--color-ring: hsl(215 25% 27%);
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
@@ -35,9 +30,29 @@
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: hsl(210 20% 97%);
|
||||
--foreground: hsl(222 47% 11%);
|
||||
--card: hsl(0 0% 100%);
|
||||
--card-foreground: hsl(222 47% 11%);
|
||||
--popover: hsl(0 0% 100%);
|
||||
--popover-foreground: hsl(222 47% 11%);
|
||||
--primary: hsl(215 25% 27%);
|
||||
--primary-foreground: hsl(210 40% 98%);
|
||||
--secondary: hsl(210 40% 94%);
|
||||
--secondary-foreground: hsl(222 47% 11%);
|
||||
--muted: hsl(210 40% 94%);
|
||||
--muted-foreground: hsl(215 16% 47%);
|
||||
--accent: hsl(210 40% 94%);
|
||||
--accent-foreground: hsl(222 47% 11%);
|
||||
--destructive: hsl(0 72% 51%);
|
||||
--destructive-foreground: hsl(210 40% 98%);
|
||||
--border: hsl(214 32% 89%);
|
||||
--input: hsl(214 32% 89%);
|
||||
--ring: hsl(215 25% 27%);
|
||||
--sidebar: hsl(210 25% 95%);
|
||||
--sidebar-foreground: hsl(215 16% 47%);
|
||||
--sidebar-primary: hsl(215 25% 27%);
|
||||
@@ -55,6 +70,8 @@ body {
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.login-input {
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { AccountCreateSchema } from '@forte/shared/schemas'
|
||||
import type { AccountCreateInput } from '@forte/shared/schemas'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import type { Account } from '@/types/account'
|
||||
|
||||
interface AccountFormProps {
|
||||
defaultValues?: Partial<Account>
|
||||
onSubmit: (data: AccountCreateInput) => void
|
||||
onSubmit: (data: Record<string, unknown>) => void
|
||||
loading?: boolean
|
||||
includeFirstMember?: boolean
|
||||
}
|
||||
|
||||
export function AccountForm({ defaultValues, onSubmit, loading }: AccountFormProps) {
|
||||
export function AccountForm({ defaultValues, onSubmit, loading, includeFirstMember }: AccountFormProps) {
|
||||
const optionalNameSchema = AccountCreateSchema.extend({ name: AccountCreateSchema.shape.name.optional() })
|
||||
const schema = includeFirstMember ? optionalNameSchema : AccountCreateSchema
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<AccountCreateInput>({
|
||||
resolver: zodResolver(AccountCreateSchema),
|
||||
} = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
name: defaultValues?.name ?? '',
|
||||
email: defaultValues?.email ?? undefined,
|
||||
@@ -36,12 +40,31 @@ export function AccountForm({ defaultValues, onSubmit, loading }: AccountFormPro
|
||||
|
||||
const billingMode = watch('billingMode')
|
||||
|
||||
function handleFormSubmit(accountData: Record<string, unknown>) {
|
||||
if (includeFirstMember) {
|
||||
// Grab member fields from the native form
|
||||
const form = document.getElementById('account-form') as HTMLFormElement
|
||||
const formData = new FormData(form)
|
||||
onSubmit({
|
||||
...accountData,
|
||||
memberFirstName: formData.get('memberFirstName') || undefined,
|
||||
memberLastName: formData.get('memberLastName') || undefined,
|
||||
memberEmail: formData.get('memberEmail') || undefined,
|
||||
memberPhone: formData.get('memberPhone') || undefined,
|
||||
memberDateOfBirth: formData.get('memberDateOfBirth') || undefined,
|
||||
memberIsMinor: formData.get('memberIsMinor') === 'on' || undefined,
|
||||
})
|
||||
} else {
|
||||
onSubmit(accountData)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 max-w-lg">
|
||||
<form id="account-form" onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4 max-w-lg">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Name *</Label>
|
||||
<Input id="name" {...register('name')} />
|
||||
{errors.name && <p className="text-sm text-destructive">{errors.name.message}</p>}
|
||||
<Label htmlFor="name">Account Name {includeFirstMember ? '' : '*'}</Label>
|
||||
<Input id="name" {...register('name')} placeholder={includeFirstMember ? 'Auto-generated from member name if blank' : 'e.g. Smith Family, Lincoln Elementary'} />
|
||||
{errors.name && !includeFirstMember && <p className="text-sm text-destructive">{String(errors.name.message)}</p>}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@@ -88,6 +111,41 @@ export function AccountForm({ defaultValues, onSubmit, loading }: AccountFormPro
|
||||
<Textarea id="notes" {...register('notes')} />
|
||||
</div>
|
||||
|
||||
{includeFirstMember && (
|
||||
<>
|
||||
<Separator />
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">Primary Contact</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="memberFirstName">First Name</Label>
|
||||
<Input id="memberFirstName" name="memberFirstName" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="memberLastName">Last Name</Label>
|
||||
<Input id="memberLastName" name="memberLastName" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="memberEmail">Email</Label>
|
||||
<Input id="memberEmail" name="memberEmail" type="email" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="memberDateOfBirth">Date of Birth</Label>
|
||||
<Input id="memberDateOfBirth" name="memberDateOfBirth" type="date" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="memberPhone">Phone</Label>
|
||||
<Input id="memberPhone" name="memberPhone" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="checkbox" id="memberIsMinor" name="memberIsMinor" className="rounded" />
|
||||
<Label htmlFor="memberIsMinor" className="font-normal">This person is a minor</Label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Saving...' : defaultValues ? 'Update Account' : 'Create Account'}
|
||||
</Button>
|
||||
|
||||
@@ -19,6 +19,7 @@ export function MemberForm({ accountId, defaultValues, onSubmit, loading }: Memb
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<MemberCreateInput>({
|
||||
resolver: zodResolver(MemberCreateSchema),
|
||||
@@ -27,12 +28,15 @@ export function MemberForm({ accountId, defaultValues, onSubmit, loading }: Memb
|
||||
firstName: defaultValues?.firstName ?? '',
|
||||
lastName: defaultValues?.lastName ?? '',
|
||||
dateOfBirth: defaultValues?.dateOfBirth ?? undefined,
|
||||
isMinor: defaultValues?.isMinor ?? undefined,
|
||||
email: defaultValues?.email ?? undefined,
|
||||
phone: defaultValues?.phone ?? undefined,
|
||||
notes: defaultValues?.notes ?? undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const dateOfBirth = watch('dateOfBirth')
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<input type="hidden" {...register('accountId')} />
|
||||
@@ -48,20 +52,26 @@ export function MemberForm({ accountId, defaultValues, onSubmit, loading }: Memb
|
||||
{errors.lastName && <p className="text-sm text-destructive">{errors.lastName.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="memberEmail">Email</Label>
|
||||
<Input id="memberEmail" type="email" {...register('email')} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dateOfBirth">Date of Birth</Label>
|
||||
<Input id="dateOfBirth" type="date" {...register('dateOfBirth')} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="memberEmail">Email</Label>
|
||||
<Input id="memberEmail" type="email" {...register('email')} />
|
||||
<Label htmlFor="memberPhone">Phone</Label>
|
||||
<Input id="memberPhone" {...register('phone')} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="memberPhone">Phone</Label>
|
||||
<Input id="memberPhone" {...register('phone')} />
|
||||
</div>
|
||||
{!dateOfBirth && (
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="checkbox" id="isMinor" {...register('isMinor')} className="rounded" />
|
||||
<Label htmlFor="isMinor" className="font-normal">This person is a minor (under 18)</Label>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="memberNotes">Notes</Label>
|
||||
<Textarea id="memberNotes" {...register('notes')} />
|
||||
|
||||
@@ -337,40 +337,20 @@ export const themes: ThemePreset[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const CSS_VAR_MAP: Record<keyof ThemeColors, string> = {
|
||||
background: '--color-background',
|
||||
foreground: '--color-foreground',
|
||||
card: '--color-card',
|
||||
cardForeground: '--color-card-foreground',
|
||||
popover: '--color-popover',
|
||||
popoverForeground: '--color-popover-foreground',
|
||||
primary: '--color-primary',
|
||||
primaryForeground: '--color-primary-foreground',
|
||||
secondary: '--color-secondary',
|
||||
secondaryForeground: '--color-secondary-foreground',
|
||||
muted: '--color-muted',
|
||||
mutedForeground: '--color-muted-foreground',
|
||||
accent: '--color-accent',
|
||||
accentForeground: '--color-accent-foreground',
|
||||
destructive: '--color-destructive',
|
||||
destructiveForeground: '--color-destructive-foreground',
|
||||
border: '--color-border',
|
||||
input: '--color-input',
|
||||
ring: '--color-ring',
|
||||
sidebar: '--sidebar',
|
||||
sidebarForeground: '--sidebar-foreground',
|
||||
sidebarPrimary: '--sidebar-primary',
|
||||
sidebarPrimaryForeground: '--sidebar-primary-foreground',
|
||||
sidebarAccent: '--sidebar-accent',
|
||||
sidebarAccentForeground: '--sidebar-accent-foreground',
|
||||
sidebarBorder: '--sidebar-border',
|
||||
function camelToKebab(str: string): string {
|
||||
return str.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase())
|
||||
}
|
||||
|
||||
export function applyThemeColors(colors: ThemeColors) {
|
||||
const root = document.documentElement
|
||||
for (const [key, cssVar] of Object.entries(CSS_VAR_MAP)) {
|
||||
const value = colors[key as keyof ThemeColors]
|
||||
root.style.setProperty(cssVar, `hsl(${value})`)
|
||||
for (const [key, value] of Object.entries(colors)) {
|
||||
const prop = `--${camelToKebab(key)}`
|
||||
const hsl = `hsl(${value})`
|
||||
root.style.setProperty(prop, hsl)
|
||||
}
|
||||
// Also set on body for portaled components (dialogs, dropdowns, popovers)
|
||||
for (const [key, value] of Object.entries(colors)) {
|
||||
document.body.style.setProperty(`--${camelToKebab(key)}`, `hsl(${value})`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as LoginRouteImport } from './routes/login'
|
||||
import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
|
||||
import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index'
|
||||
import { Route as AuthenticatedMembersRouteImport } from './routes/_authenticated/members'
|
||||
import { Route as AuthenticatedAccountsIndexRouteImport } from './routes/_authenticated/accounts/index'
|
||||
import { Route as AuthenticatedAccountsNewRouteImport } from './routes/_authenticated/accounts/new'
|
||||
import { Route as AuthenticatedAccountsAccountIdRouteImport } from './routes/_authenticated/accounts/$accountId'
|
||||
@@ -35,6 +36,11 @@ const AuthenticatedIndexRoute = AuthenticatedIndexRouteImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedMembersRoute = AuthenticatedMembersRouteImport.update({
|
||||
id: '/members',
|
||||
path: '/members',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsIndexRoute =
|
||||
AuthenticatedAccountsIndexRouteImport.update({
|
||||
id: '/accounts/',
|
||||
@@ -87,6 +93,7 @@ const AuthenticatedAccountsAccountIdMembersRoute =
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/members': typeof AuthenticatedMembersRoute
|
||||
'/accounts/$accountId': typeof AuthenticatedAccountsAccountIdRouteWithChildren
|
||||
'/accounts/new': typeof AuthenticatedAccountsNewRoute
|
||||
'/accounts/': typeof AuthenticatedAccountsIndexRoute
|
||||
@@ -98,6 +105,7 @@ export interface FileRoutesByFullPath {
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/login': typeof LoginRoute
|
||||
'/members': typeof AuthenticatedMembersRoute
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/accounts/new': typeof AuthenticatedAccountsNewRoute
|
||||
'/accounts': typeof AuthenticatedAccountsIndexRoute
|
||||
@@ -111,6 +119,7 @@ export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/_authenticated': typeof AuthenticatedRouteWithChildren
|
||||
'/login': typeof LoginRoute
|
||||
'/_authenticated/members': typeof AuthenticatedMembersRoute
|
||||
'/_authenticated/': typeof AuthenticatedIndexRoute
|
||||
'/_authenticated/accounts/$accountId': typeof AuthenticatedAccountsAccountIdRouteWithChildren
|
||||
'/_authenticated/accounts/new': typeof AuthenticatedAccountsNewRoute
|
||||
@@ -126,6 +135,7 @@ export interface FileRouteTypes {
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/login'
|
||||
| '/members'
|
||||
| '/accounts/$accountId'
|
||||
| '/accounts/new'
|
||||
| '/accounts/'
|
||||
@@ -137,6 +147,7 @@ export interface FileRouteTypes {
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/login'
|
||||
| '/members'
|
||||
| '/'
|
||||
| '/accounts/new'
|
||||
| '/accounts'
|
||||
@@ -149,6 +160,7 @@ export interface FileRouteTypes {
|
||||
| '__root__'
|
||||
| '/_authenticated'
|
||||
| '/login'
|
||||
| '/_authenticated/members'
|
||||
| '/_authenticated/'
|
||||
| '/_authenticated/accounts/$accountId'
|
||||
| '/_authenticated/accounts/new'
|
||||
@@ -188,6 +200,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticatedIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/members': {
|
||||
id: '/_authenticated/members'
|
||||
path: '/members'
|
||||
fullPath: '/members'
|
||||
preLoaderRoute: typeof AuthenticatedMembersRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/accounts/': {
|
||||
id: '/_authenticated/accounts/'
|
||||
path: '/accounts'
|
||||
@@ -275,6 +294,7 @@ const AuthenticatedAccountsAccountIdRouteWithChildren =
|
||||
)
|
||||
|
||||
interface AuthenticatedRouteChildren {
|
||||
AuthenticatedMembersRoute: typeof AuthenticatedMembersRoute
|
||||
AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute
|
||||
AuthenticatedAccountsAccountIdRoute: typeof AuthenticatedAccountsAccountIdRouteWithChildren
|
||||
AuthenticatedAccountsNewRoute: typeof AuthenticatedAccountsNewRoute
|
||||
@@ -282,6 +302,7 @@ interface AuthenticatedRouteChildren {
|
||||
}
|
||||
|
||||
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
||||
AuthenticatedMembersRoute: AuthenticatedMembersRoute,
|
||||
AuthenticatedIndexRoute: AuthenticatedIndexRoute,
|
||||
AuthenticatedAccountsAccountIdRoute:
|
||||
AuthenticatedAccountsAccountIdRouteWithChildren,
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Users, Sun, Moon, Monitor, LogOut, User, Palette } from 'lucide-react'
|
||||
import { Users, UserRound, Sun, Moon, Monitor, LogOut, User, Palette } from 'lucide-react'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated')({
|
||||
beforeLoad: () => {
|
||||
@@ -61,6 +61,14 @@ function AuthenticatedLayout() {
|
||||
<Users className="h-4 w-4" />
|
||||
Accounts
|
||||
</Link>
|
||||
<Link
|
||||
to="/members"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground' }}
|
||||
>
|
||||
<UserRound className="h-4 w-4" />
|
||||
Members
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="p-3 border-t border-sidebar-border">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { accountMutations, accountKeys } from '@/api/accounts'
|
||||
import { memberMutations } from '@/api/members'
|
||||
import { AccountForm } from '@/components/accounts/account-form'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
@@ -13,7 +14,39 @@ function NewAccountPage() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: accountMutations.create,
|
||||
mutationFn: async (data: Record<string, unknown>) => {
|
||||
// Extract member fields
|
||||
const memberFirstName = data.memberFirstName as string | undefined
|
||||
const memberLastName = data.memberLastName as string | undefined
|
||||
const memberEmail = data.memberEmail as string | undefined
|
||||
const memberPhone = data.memberPhone as string | undefined
|
||||
const memberDateOfBirth = data.memberDateOfBirth as string | undefined
|
||||
const memberIsMinor = data.memberIsMinor as boolean | undefined
|
||||
|
||||
// Create account (without member fields)
|
||||
const { memberFirstName: _, memberLastName: __, memberEmail: ___, memberPhone: ____, memberDateOfBirth: _____, memberIsMinor: ______, ...accountData } = data
|
||||
|
||||
// Auto-generate account name from member if not provided
|
||||
if (!accountData.name && memberFirstName && memberLastName) {
|
||||
accountData.name = `${memberLastName}, ${memberFirstName}`
|
||||
}
|
||||
|
||||
const account = await accountMutations.create(accountData)
|
||||
|
||||
// Create first member if name provided
|
||||
if (memberFirstName && memberLastName) {
|
||||
await memberMutations.create(account.id, {
|
||||
firstName: memberFirstName,
|
||||
lastName: memberLastName,
|
||||
email: memberEmail || undefined,
|
||||
phone: memberPhone || undefined,
|
||||
dateOfBirth: memberDateOfBirth || undefined,
|
||||
isMinor: memberIsMinor,
|
||||
})
|
||||
}
|
||||
|
||||
return account
|
||||
},
|
||||
onSuccess: (account) => {
|
||||
queryClient.invalidateQueries({ queryKey: accountKeys.lists() })
|
||||
toast.success('Account created')
|
||||
@@ -27,7 +60,7 @@ function NewAccountPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold">New Account</h1>
|
||||
<AccountForm onSubmit={mutation.mutate} loading={mutation.isPending} />
|
||||
<AccountForm onSubmit={mutation.mutate} loading={mutation.isPending} includeFirstMember />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
101
packages/admin/src/routes/_authenticated/members.tsx
Normal file
101
packages/admin/src/routes/_authenticated/members.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { globalMemberListOptions, type MemberWithAccount } from '@/api/members'
|
||||
import { usePagination } from '@/hooks/use-pagination'
|
||||
import { DataTable, type Column } from '@/components/shared/data-table'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Search } from 'lucide-react'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/members')({
|
||||
validateSearch: (search: Record<string, unknown>) => ({
|
||||
page: Number(search.page) || 1,
|
||||
limit: Number(search.limit) || 25,
|
||||
q: (search.q as string) || undefined,
|
||||
sort: (search.sort as string) || undefined,
|
||||
order: (search.order as 'asc' | 'desc') || 'asc',
|
||||
}),
|
||||
component: MembersListPage,
|
||||
})
|
||||
|
||||
const memberColumns: Column<MemberWithAccount>[] = [
|
||||
{
|
||||
key: 'last_name',
|
||||
header: 'Name',
|
||||
sortable: true,
|
||||
render: (row) => <span className="font-medium">{row.firstName} {row.lastName}</span>,
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
header: 'Email',
|
||||
sortable: true,
|
||||
render: (row) => row.email ?? <span className="text-muted-foreground">-</span>,
|
||||
},
|
||||
{
|
||||
key: 'phone',
|
||||
header: 'Phone',
|
||||
render: (row) => row.phone ?? <span className="text-muted-foreground">-</span>,
|
||||
},
|
||||
{
|
||||
key: 'accountName',
|
||||
header: 'Account',
|
||||
render: (row) => row.accountName ?? <span className="text-muted-foreground">-</span>,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Status',
|
||||
render: (row) => row.isMinor ? <Badge variant="secondary">Minor</Badge> : <Badge>Adult</Badge>,
|
||||
},
|
||||
]
|
||||
|
||||
function MembersListPage() {
|
||||
const navigate = useNavigate()
|
||||
const { params, setPage, setSearch, setSort } = usePagination()
|
||||
const [searchInput, setSearchInput] = useState(params.q ?? '')
|
||||
|
||||
const { data, isLoading } = useQuery(globalMemberListOptions(params))
|
||||
|
||||
function handleSearchSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setSearch(searchInput)
|
||||
}
|
||||
|
||||
function handleRowClick(member: MemberWithAccount) {
|
||||
navigate({ to: '/accounts/$accountId/members', params: { accountId: member.accountId } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold">Members</h1>
|
||||
|
||||
<form onSubmit={handleSearchSubmit} className="flex gap-2 max-w-sm">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search members..."
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" variant="secondary">Search</Button>
|
||||
</form>
|
||||
|
||||
<DataTable
|
||||
columns={memberColumns}
|
||||
data={data?.data ?? []}
|
||||
loading={isLoading}
|
||||
page={params.page}
|
||||
totalPages={data?.pagination.totalPages ?? 1}
|
||||
total={data?.pagination.total ?? 0}
|
||||
sort={params.sort}
|
||||
order={params.order}
|
||||
onPageChange={setPage}
|
||||
onSort={setSort}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user