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:
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add member_number to member table
|
||||||
|
ALTER TABLE "member" ADD COLUMN "member_number" varchar(50);
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user