You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
103 lines
4.1 KiB
TypeScript
103 lines
4.1 KiB
TypeScript
import { uuidv7, validateGstin } from '@sims/domain'
|
|
import { writeAudit } from './audit'
|
|
import type { DB } from './db'
|
|
|
|
/** Client registry — plain functions over the handle (D12 portable-repo pattern). */
|
|
|
|
export interface ClientContact { name: string; phone?: string; email?: string; role?: string }
|
|
|
|
export interface Client {
|
|
id: string; code: string; name: string; gstin?: string; stateCode: string
|
|
address: string; contacts: ClientContact[]
|
|
status: 'lead' | 'active' | 'dormant' | 'lost'; notes: string
|
|
}
|
|
|
|
interface ClientRow {
|
|
id: string; code: string; name: string; gstin: string | null; state_code: string
|
|
address: string; contacts: string; status: string; notes: string
|
|
source: string; created_at: string
|
|
}
|
|
|
|
function toClient(r: ClientRow): Client {
|
|
return {
|
|
id: r.id, code: r.code, name: r.name,
|
|
...(r.gstin !== null ? { gstin: r.gstin } : {}),
|
|
stateCode: r.state_code, address: r.address,
|
|
contacts: JSON.parse(r.contacts) as ClientContact[],
|
|
status: r.status as Client['status'], notes: r.notes,
|
|
}
|
|
}
|
|
|
|
function assertGstin(gstin: string): void {
|
|
const v = validateGstin(gstin)
|
|
if (!v.ok) throw new Error(`Invalid GSTIN (${v.reason ?? 'invalid'}): ${gstin}`)
|
|
}
|
|
|
|
export function listClients(db: DB, q?: string): Client[] {
|
|
let rows: ClientRow[]
|
|
if (q !== undefined && q !== '') {
|
|
const like = `%${q}%`
|
|
rows = db.prepare(
|
|
`SELECT * FROM client WHERE name LIKE ? OR code LIKE ? OR gstin LIKE ? ORDER BY name`,
|
|
).all(like, like, like) as ClientRow[]
|
|
} else {
|
|
rows = db.prepare(`SELECT * FROM client ORDER BY name`).all() as ClientRow[]
|
|
}
|
|
return rows.map(toClient)
|
|
}
|
|
|
|
export function getClient(db: DB, id: string): Client | null {
|
|
const row = db.prepare(`SELECT * FROM client WHERE id=?`).get(id) as ClientRow | undefined
|
|
return row === undefined ? null : toClient(row)
|
|
}
|
|
|
|
export interface ClientInput {
|
|
code?: string; name: string; gstin?: string; stateCode: string; address?: string
|
|
contacts?: ClientContact[]; status?: Client['status']; notes?: string
|
|
}
|
|
|
|
export function createClient(db: DB, userId: string, input: ClientInput): Client {
|
|
if (input.gstin !== undefined) assertGstin(input.gstin)
|
|
const id = uuidv7()
|
|
const n = (db.prepare(`SELECT COUNT(*) AS n FROM client`).get() as { n: number }).n
|
|
const code = input.code ?? `CL${String(n + 1).padStart(4, '0')}`
|
|
db.prepare(
|
|
`INSERT INTO client (id, code, name, gstin, state_code, address, contacts, status, notes, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
).run(
|
|
id, code, input.name, input.gstin ?? null, input.stateCode,
|
|
input.address ?? '', JSON.stringify(input.contacts ?? []),
|
|
input.status ?? 'active', input.notes ?? '', new Date().toISOString(),
|
|
)
|
|
const client = getClient(db, id)!
|
|
writeAudit(db, userId, 'create', 'client', id, undefined, client)
|
|
return client
|
|
}
|
|
|
|
export interface ClientPatch {
|
|
name?: string; gstin?: string | null; stateCode?: string; address?: string
|
|
contacts?: ClientContact[]; status?: Client['status']; notes?: string
|
|
}
|
|
|
|
export function updateClient(db: DB, userId: string, id: string, patch: ClientPatch): Client {
|
|
const before = getClient(db, id)
|
|
if (before === null) throw new Error('Client not found')
|
|
if (patch.gstin !== undefined && patch.gstin !== null) assertGstin(patch.gstin)
|
|
const sets: string[] = []
|
|
const args: unknown[] = []
|
|
if (patch.name !== undefined) { sets.push('name=?'); args.push(patch.name) }
|
|
if (patch.gstin !== undefined) { sets.push('gstin=?'); args.push(patch.gstin) }
|
|
if (patch.stateCode !== undefined) { sets.push('state_code=?'); args.push(patch.stateCode) }
|
|
if (patch.address !== undefined) { sets.push('address=?'); args.push(patch.address) }
|
|
if (patch.contacts !== undefined) { sets.push('contacts=?'); args.push(JSON.stringify(patch.contacts)) }
|
|
if (patch.status !== undefined) { sets.push('status=?'); args.push(patch.status) }
|
|
if (patch.notes !== undefined) { sets.push('notes=?'); args.push(patch.notes) }
|
|
if (sets.length > 0) {
|
|
args.push(id)
|
|
db.prepare(`UPDATE client SET ${sets.join(', ')} WHERE id=?`).run(...args)
|
|
}
|
|
const after = getClient(db, id)!
|
|
writeAudit(db, userId, 'update', 'client', id, before, after)
|
|
return after
|
|
}
|