From 8ea3b8dffbaa5de399a9c8ecab811dfa0383f57d Mon Sep 17 00:00:00 2001 From: Ryan Moon Date: Sat, 28 Mar 2026 09:15:27 -0500 Subject: [PATCH] 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. --- .../src/routes/_authenticated/members.tsx | 15 +++++++++-- packages/admin/src/types/account.ts | 2 ++ .../src/db/migrations/0009_member_number.sql | 2 ++ .../src/db/migrations/meta/_journal.json | 7 ++++++ packages/backend/src/db/schema/accounts.ts | 1 + .../backend/src/services/account.service.ts | 25 +++++++++++++++++++ 6 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 packages/backend/src/db/migrations/0009_member_number.sql diff --git a/packages/admin/src/routes/_authenticated/members.tsx b/packages/admin/src/routes/_authenticated/members.tsx index f58adfd..187affb 100644 --- a/packages/admin/src/routes/_authenticated/members.tsx +++ b/packages/admin/src/routes/_authenticated/members.tsx @@ -7,7 +7,7 @@ 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' +import { Search, Plus } from 'lucide-react' export const Route = createFileRoute('/_authenticated/members')({ validateSearch: (search: Record) => ({ @@ -21,6 +21,11 @@ export const Route = createFileRoute('/_authenticated/members')({ }) const memberColumns: Column[] = [ + { + key: 'memberNumber', + header: '#', + render: (row) => {row.memberNumber ?? '-'}, + }, { key: 'last_name', header: 'Name', @@ -68,7 +73,13 @@ function MembersListPage() { return (
-

Members

+
+

Members

+ +
diff --git a/packages/admin/src/types/account.ts b/packages/admin/src/types/account.ts index ee4febb..0d8f0b3 100644 --- a/packages/admin/src/types/account.ts +++ b/packages/admin/src/types/account.ts @@ -3,6 +3,7 @@ export interface Account { companyId: string accountNumber: string | null name: string + primaryMemberId: string | null email: string | null phone: string | null address: { @@ -25,6 +26,7 @@ export interface Member { id: string accountId: string companyId: string + memberNumber: string | null firstName: string lastName: string dateOfBirth: string | null diff --git a/packages/backend/src/db/migrations/0009_member_number.sql b/packages/backend/src/db/migrations/0009_member_number.sql new file mode 100644 index 0000000..2619045 --- /dev/null +++ b/packages/backend/src/db/migrations/0009_member_number.sql @@ -0,0 +1,2 @@ +-- Add member_number to member table +ALTER TABLE "member" ADD COLUMN "member_number" varchar(50); diff --git a/packages/backend/src/db/migrations/meta/_journal.json b/packages/backend/src/db/migrations/meta/_journal.json index cfd9606..e26a400 100644 --- a/packages/backend/src/db/migrations/meta/_journal.json +++ b/packages/backend/src/db/migrations/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1774702800000, "tag": "0008_member_primary_account", "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1774703400000, + "tag": "0009_member_number", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/backend/src/db/schema/accounts.ts b/packages/backend/src/db/schema/accounts.ts index 522c346..b5b0970 100644 --- a/packages/backend/src/db/schema/accounts.ts +++ b/packages/backend/src/db/schema/accounts.ts @@ -49,6 +49,7 @@ export const members = pgTable('member', { companyId: uuid('company_id') .notNull() .references(() => companies.id), + memberNumber: varchar('member_number', { length: 50 }), firstName: varchar('first_name', { length: 100 }).notNull(), lastName: varchar('last_name', { length: 100 }).notNull(), dateOfBirth: date('date_of_birth'), diff --git a/packages/backend/src/services/account.service.ts b/packages/backend/src/services/account.service.ts index eaf2ca4..c64ea91 100644 --- a/packages/backend/src/services/account.service.ts +++ b/packages/backend/src/services/account.service.ts @@ -26,12 +26,35 @@ import { paginatedResponse, } 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 { + 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 = { async create(db: PostgresJsDatabase, companyId: string, input: AccountCreateInput) { + const accountNumber = await generateUniqueNumber(db, accounts, accounts.accountNumber, companyId, accounts.companyId) + const [account] = await db .insert(accounts) .values({ companyId, + accountNumber, name: input.name, email: input.email, phone: input.phone, @@ -137,11 +160,13 @@ export const MemberService = { ) { // isMinor: explicit flag wins, else derive from DOB, else 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 .insert(members) .values({ companyId, + memberNumber, accountId: input.accountId, firstName: input.firstName, lastName: input.lastName,