Add auto-generated account numbers and member numbers

6-digit random numbers generated on create, unique per company. Member
number column added to member table. Both displayed in UI tables.
This commit is contained in:
Ryan Moon
2026-03-28 09:15:27 -05:00
parent 572af05a3f
commit 8ea3b8dffb
6 changed files with 50 additions and 2 deletions

View File

@@ -7,7 +7,7 @@ import { DataTable, type Column } from '@/components/shared/data-table'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Search } from 'lucide-react' import { Search, Plus } from 'lucide-react'
export const Route = createFileRoute('/_authenticated/members')({ export const Route = createFileRoute('/_authenticated/members')({
validateSearch: (search: Record<string, unknown>) => ({ validateSearch: (search: Record<string, unknown>) => ({
@@ -21,6 +21,11 @@ export const Route = createFileRoute('/_authenticated/members')({
}) })
const memberColumns: Column<MemberWithAccount>[] = [ const memberColumns: Column<MemberWithAccount>[] = [
{
key: 'memberNumber',
header: '#',
render: (row) => <span className="font-mono text-sm text-muted-foreground">{row.memberNumber ?? '-'}</span>,
},
{ {
key: 'last_name', key: 'last_name',
header: 'Name', header: 'Name',
@@ -68,7 +73,13 @@ function MembersListPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<h1 className="text-2xl font-bold">Members</h1> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Members</h1>
<Button onClick={() => navigate({ to: '/accounts/new' })}>
<Plus className="mr-2 h-4 w-4" />
New Member
</Button>
</div>
<form onSubmit={handleSearchSubmit} className="flex gap-2 max-w-sm"> <form onSubmit={handleSearchSubmit} className="flex gap-2 max-w-sm">
<div className="relative flex-1"> <div className="relative flex-1">

View File

@@ -3,6 +3,7 @@ export interface Account {
companyId: string companyId: string
accountNumber: string | null accountNumber: string | null
name: string name: string
primaryMemberId: string | null
email: string | null email: string | null
phone: string | null phone: string | null
address: { address: {
@@ -25,6 +26,7 @@ export interface Member {
id: string id: string
accountId: string accountId: string
companyId: string companyId: string
memberNumber: string | null
firstName: string firstName: string
lastName: string lastName: string
dateOfBirth: string | null dateOfBirth: string | null

View File

@@ -0,0 +1,2 @@
-- Add member_number to member table
ALTER TABLE "member" ADD COLUMN "member_number" varchar(50);

View File

@@ -64,6 +64,13 @@
"when": 1774702800000, "when": 1774702800000,
"tag": "0008_member_primary_account", "tag": "0008_member_primary_account",
"breakpoints": true "breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1774703400000,
"tag": "0009_member_number",
"breakpoints": true
} }
] ]
} }

View File

@@ -49,6 +49,7 @@ export const members = pgTable('member', {
companyId: uuid('company_id') companyId: uuid('company_id')
.notNull() .notNull()
.references(() => companies.id), .references(() => companies.id),
memberNumber: varchar('member_number', { length: 50 }),
firstName: varchar('first_name', { length: 100 }).notNull(), firstName: varchar('first_name', { length: 100 }).notNull(),
lastName: varchar('last_name', { length: 100 }).notNull(), lastName: varchar('last_name', { length: 100 }).notNull(),
dateOfBirth: date('date_of_birth'), dateOfBirth: date('date_of_birth'),

View File

@@ -26,12 +26,35 @@ import {
paginatedResponse, paginatedResponse,
} from '../utils/pagination.js' } from '../utils/pagination.js'
async function generateUniqueNumber(
db: PostgresJsDatabase,
table: typeof accounts | typeof members,
column: typeof accounts.accountNumber | typeof members.memberNumber,
companyId: string,
companyIdColumn: typeof accounts.companyId,
): Promise<string> {
for (let attempt = 0; attempt < 10; attempt++) {
const num = String(Math.floor(100000 + Math.random() * 900000))
const [existing] = await db
.select({ id: table.id })
.from(table)
.where(and(eq(companyIdColumn, companyId), eq(column, num)))
.limit(1)
if (!existing) return num
}
// Fallback to 8 digits if 6-digit space is crowded
return String(Math.floor(10000000 + Math.random() * 90000000))
}
export const AccountService = { export const AccountService = {
async create(db: PostgresJsDatabase, companyId: string, input: AccountCreateInput) { async create(db: PostgresJsDatabase, companyId: string, input: AccountCreateInput) {
const accountNumber = await generateUniqueNumber(db, accounts, accounts.accountNumber, companyId, accounts.companyId)
const [account] = await db const [account] = await db
.insert(accounts) .insert(accounts)
.values({ .values({
companyId, companyId,
accountNumber,
name: input.name, name: input.name,
email: input.email, email: input.email,
phone: input.phone, phone: input.phone,
@@ -137,11 +160,13 @@ export const MemberService = {
) { ) {
// isMinor: explicit flag wins, else derive from DOB, else false // isMinor: explicit flag wins, else derive from DOB, else false
const minor = input.isMinor ?? (input.dateOfBirth ? isMinor(input.dateOfBirth) : false) const minor = input.isMinor ?? (input.dateOfBirth ? isMinor(input.dateOfBirth) : false)
const memberNumber = await generateUniqueNumber(db, members, members.memberNumber, companyId, members.companyId)
const [member] = await db const [member] = await db
.insert(members) .insert(members)
.values({ .values({
companyId, companyId,
memberNumber,
accountId: input.accountId, accountId: input.accountId,
firstName: input.firstName, firstName: input.firstName,
lastName: input.lastName, lastName: input.lastName,