From 9abdf6c050be464ebf1494a848c4747a8c3fd5a1 Mon Sep 17 00:00:00 2001 From: Ryan Moon Date: Sat, 28 Mar 2026 07:45:52 -0500 Subject: [PATCH] Add accounts UI with list, create, edit, detail tabs for all sub-entities Accounts list with paginated table, search, sort. Account detail page with tabs for members, payment methods, tax exemptions, and processor links. All sub-entities have create/edit dialogs and delete actions. Forms use shared Zod schemas via react-hook-form. --- packages/admin/src/api/accounts.ts | 37 ++++ packages/admin/src/api/members.ts | 27 +++ packages/admin/src/api/payment-methods.ts | 25 +++ packages/admin/src/api/processor-links.ts | 25 +++ packages/admin/src/api/tax-exemptions.ts | 28 +++ .../src/components/accounts/account-form.tsx | 96 ++++++++++ .../src/components/accounts/account-table.tsx | 42 ++++ .../src/components/accounts/member-form.tsx | 74 +++++++ .../accounts/payment-method-form.tsx | 84 ++++++++ .../accounts/processor-link-form.tsx | 63 ++++++ .../accounts/tax-exemption-form.tsx | 61 ++++++ .../src/components/shared/data-table.tsx | 143 ++++++++++++++ packages/admin/src/hooks/use-pagination.ts | 43 +++++ packages/admin/src/routeTree.gen.ts | 180 +++++++++++++++++- .../_authenticated/accounts/$accountId.tsx | 71 +++++++ .../accounts/$accountId/index.tsx | 39 ++++ .../accounts/$accountId/members.tsx | 128 +++++++++++++ .../accounts/$accountId/payment-methods.tsx | 117 ++++++++++++ .../accounts/$accountId/processor-links.tsx | 96 ++++++++++ .../accounts/$accountId/tax-exemptions.tsx | 128 +++++++++++++ .../routes/_authenticated/accounts/index.tsx | 72 ++++++- .../routes/_authenticated/accounts/new.tsx | 33 ++++ packages/admin/src/types/account.ts | 82 ++++++++ 23 files changed, 1688 insertions(+), 6 deletions(-) create mode 100644 packages/admin/src/api/accounts.ts create mode 100644 packages/admin/src/api/members.ts create mode 100644 packages/admin/src/api/payment-methods.ts create mode 100644 packages/admin/src/api/processor-links.ts create mode 100644 packages/admin/src/api/tax-exemptions.ts create mode 100644 packages/admin/src/components/accounts/account-form.tsx create mode 100644 packages/admin/src/components/accounts/account-table.tsx create mode 100644 packages/admin/src/components/accounts/member-form.tsx create mode 100644 packages/admin/src/components/accounts/payment-method-form.tsx create mode 100644 packages/admin/src/components/accounts/processor-link-form.tsx create mode 100644 packages/admin/src/components/accounts/tax-exemption-form.tsx create mode 100644 packages/admin/src/components/shared/data-table.tsx create mode 100644 packages/admin/src/hooks/use-pagination.ts create mode 100644 packages/admin/src/routes/_authenticated/accounts/$accountId.tsx create mode 100644 packages/admin/src/routes/_authenticated/accounts/$accountId/index.tsx create mode 100644 packages/admin/src/routes/_authenticated/accounts/$accountId/members.tsx create mode 100644 packages/admin/src/routes/_authenticated/accounts/$accountId/payment-methods.tsx create mode 100644 packages/admin/src/routes/_authenticated/accounts/$accountId/processor-links.tsx create mode 100644 packages/admin/src/routes/_authenticated/accounts/$accountId/tax-exemptions.tsx create mode 100644 packages/admin/src/routes/_authenticated/accounts/new.tsx create mode 100644 packages/admin/src/types/account.ts diff --git a/packages/admin/src/api/accounts.ts b/packages/admin/src/api/accounts.ts new file mode 100644 index 0000000..eb15919 --- /dev/null +++ b/packages/admin/src/api/accounts.ts @@ -0,0 +1,37 @@ +import { queryOptions } from '@tanstack/react-query' +import { api } from '@/lib/api-client' +import type { Account } from '@/types/account' +import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas' + +export const accountKeys = { + all: ['accounts'] as const, + lists: () => [...accountKeys.all, 'list'] as const, + list: (params: PaginationInput) => [...accountKeys.all, 'list', params] as const, + details: () => [...accountKeys.all, 'detail'] as const, + detail: (id: string) => [...accountKeys.all, 'detail', id] as const, +} + +export function accountListOptions(params: PaginationInput) { + return queryOptions({ + queryKey: accountKeys.list(params), + queryFn: () => api.get>('/v1/accounts', params), + }) +} + +export function accountDetailOptions(id: string) { + return queryOptions({ + queryKey: accountKeys.detail(id), + queryFn: () => api.get(`/v1/accounts/${id}`), + }) +} + +export const accountMutations = { + create: (data: Record) => + api.post('/v1/accounts', data), + + update: (id: string, data: Record) => + api.patch(`/v1/accounts/${id}`, data), + + delete: (id: string) => + api.del(`/v1/accounts/${id}`), +} diff --git a/packages/admin/src/api/members.ts b/packages/admin/src/api/members.ts new file mode 100644 index 0000000..0626ce5 --- /dev/null +++ b/packages/admin/src/api/members.ts @@ -0,0 +1,27 @@ +import { queryOptions } from '@tanstack/react-query' +import { api } from '@/lib/api-client' +import type { Member } from '@/types/account' +import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas' + +export const memberKeys = { + all: (accountId: string) => ['accounts', accountId, 'members'] as const, + list: (accountId: string, params: PaginationInput) => [...memberKeys.all(accountId), params] as const, +} + +export function memberListOptions(accountId: string, params: PaginationInput) { + return queryOptions({ + queryKey: memberKeys.list(accountId, params), + queryFn: () => api.get>(`/v1/accounts/${accountId}/members`, params), + }) +} + +export const memberMutations = { + create: (accountId: string, data: Record) => + api.post(`/v1/accounts/${accountId}/members`, data), + + update: (id: string, data: Record) => + api.patch(`/v1/members/${id}`, data), + + delete: (id: string) => + api.del(`/v1/members/${id}`), +} diff --git a/packages/admin/src/api/payment-methods.ts b/packages/admin/src/api/payment-methods.ts new file mode 100644 index 0000000..218cc96 --- /dev/null +++ b/packages/admin/src/api/payment-methods.ts @@ -0,0 +1,25 @@ +import { queryOptions } from '@tanstack/react-query' +import { api } from '@/lib/api-client' +import type { PaymentMethod } from '@/types/account' + +export const paymentMethodKeys = { + all: (accountId: string) => ['accounts', accountId, 'payment-methods'] as const, +} + +export function paymentMethodListOptions(accountId: string) { + return queryOptions({ + queryKey: paymentMethodKeys.all(accountId), + queryFn: () => api.get<{ data: PaymentMethod[] }>(`/v1/accounts/${accountId}/payment-methods`), + }) +} + +export const paymentMethodMutations = { + create: (accountId: string, data: Record) => + api.post(`/v1/accounts/${accountId}/payment-methods`, data), + + update: (id: string, data: Record) => + api.patch(`/v1/payment-methods/${id}`, data), + + delete: (id: string) => + api.del(`/v1/payment-methods/${id}`), +} diff --git a/packages/admin/src/api/processor-links.ts b/packages/admin/src/api/processor-links.ts new file mode 100644 index 0000000..316ea56 --- /dev/null +++ b/packages/admin/src/api/processor-links.ts @@ -0,0 +1,25 @@ +import { queryOptions } from '@tanstack/react-query' +import { api } from '@/lib/api-client' +import type { ProcessorLink } from '@/types/account' + +export const processorLinkKeys = { + all: (accountId: string) => ['accounts', accountId, 'processor-links'] as const, +} + +export function processorLinkListOptions(accountId: string) { + return queryOptions({ + queryKey: processorLinkKeys.all(accountId), + queryFn: () => api.get<{ data: ProcessorLink[] }>(`/v1/accounts/${accountId}/processor-links`), + }) +} + +export const processorLinkMutations = { + create: (accountId: string, data: Record) => + api.post(`/v1/accounts/${accountId}/processor-links`, data), + + update: (id: string, data: Record) => + api.patch(`/v1/processor-links/${id}`, data), + + delete: (id: string) => + api.del(`/v1/processor-links/${id}`), +} diff --git a/packages/admin/src/api/tax-exemptions.ts b/packages/admin/src/api/tax-exemptions.ts new file mode 100644 index 0000000..713a98c --- /dev/null +++ b/packages/admin/src/api/tax-exemptions.ts @@ -0,0 +1,28 @@ +import { queryOptions } from '@tanstack/react-query' +import { api } from '@/lib/api-client' +import type { TaxExemption } from '@/types/account' + +export const taxExemptionKeys = { + all: (accountId: string) => ['accounts', accountId, 'tax-exemptions'] as const, +} + +export function taxExemptionListOptions(accountId: string) { + return queryOptions({ + queryKey: taxExemptionKeys.all(accountId), + queryFn: () => api.get<{ data: TaxExemption[] }>(`/v1/accounts/${accountId}/tax-exemptions`), + }) +} + +export const taxExemptionMutations = { + create: (accountId: string, data: Record) => + api.post(`/v1/accounts/${accountId}/tax-exemptions`, data), + + update: (id: string, data: Record) => + api.patch(`/v1/tax-exemptions/${id}`, data), + + approve: (id: string) => + api.post(`/v1/tax-exemptions/${id}/approve`, {}), + + revoke: (id: string, reason: string) => + api.post(`/v1/tax-exemptions/${id}/revoke`, { reason }), +} diff --git a/packages/admin/src/components/accounts/account-form.tsx b/packages/admin/src/components/accounts/account-form.tsx new file mode 100644 index 0000000..72f4132 --- /dev/null +++ b/packages/admin/src/components/accounts/account-form.tsx @@ -0,0 +1,96 @@ +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 type { Account } from '@/types/account' + +interface AccountFormProps { + defaultValues?: Partial + onSubmit: (data: AccountCreateInput) => void + loading?: boolean +} + +export function AccountForm({ defaultValues, onSubmit, loading }: AccountFormProps) { + const { + register, + handleSubmit, + setValue, + watch, + formState: { errors }, + } = useForm({ + resolver: zodResolver(AccountCreateSchema), + defaultValues: { + name: defaultValues?.name ?? '', + email: defaultValues?.email ?? undefined, + phone: defaultValues?.phone ?? undefined, + billingMode: defaultValues?.billingMode ?? 'consolidated', + notes: defaultValues?.notes ?? undefined, + address: defaultValues?.address ?? undefined, + }, + }) + + const billingMode = watch('billingMode') + + return ( +
+
+ + + {errors.name &&

{errors.name.message}

} +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ +
+ + +
+ + +
+
+
+ +
+ +