feat(d37): society category (Kerala co-op directory taxonomy) on clients

Adds client.category (SQLite migrate + PG migration 014), derived from the name to match
the official Kerala co-op classification: Service Bank / Urban Bank / Vanitha / Housing /
Employees / Agricultural / Marketing / Society (other) / Store / Other. Backfilled by a
portable CASE (most-specific-first); new/edited clients derive it via categoryFromName.
Clients screen gains a Category filter dropdown + column. Live backfill: 135 Service
Bank, 38 Employees, 15 Vanitha, 14 Store, 12 Agricultural, 11 Urban, … typecheck + full
suite (416) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 2 days ago
parent 1c375fdf01
commit 9cb5127faf

@ -105,6 +105,8 @@ export interface Client {
anydesk?: string; os?: string; district?: string; sector?: string anydesk?: string; os?: string; district?: string; sector?: string
/** D35: 'PACS' (co-op society) or 'Store' (retail). */ /** D35: 'PACS' (co-op society) or 'Store' (retail). */
clientType: string clientType: string
/** D37: society category (Service Bank / Vanitha / Housing / Employees / …). */
category: string
hasDbPassword: boolean hasDbPassword: boolean
} }
@ -247,13 +249,14 @@ export interface ClientWrite {
} }
export const getClients = ( export const getClients = (
q?: string, filters?: { district?: string; sector?: string; clientType?: string }, q?: string, filters?: { district?: string; sector?: string; clientType?: string; category?: string },
): Promise<Client[]> => { ): Promise<Client[]> => {
const params = new URLSearchParams() const params = new URLSearchParams()
if (q !== undefined && q !== '') params.set('q', q) if (q !== undefined && q !== '') params.set('q', q)
if (filters?.district !== undefined && filters.district !== '') params.set('district', filters.district) if (filters?.district !== undefined && filters.district !== '') params.set('district', filters.district)
if (filters?.sector !== undefined && filters.sector !== '') params.set('sector', filters.sector) if (filters?.sector !== undefined && filters.sector !== '') params.set('sector', filters.sector)
if (filters?.clientType !== undefined && filters.clientType !== '') params.set('clientType', filters.clientType) if (filters?.clientType !== undefined && filters.clientType !== '') params.set('clientType', filters.clientType)
if (filters?.category !== undefined && filters.category !== '') params.set('category', filters.category)
const qs = params.toString() const qs = params.toString()
return apiFetch<{ clients: Client[] }>(`/clients${qs !== '' ? `?${qs}` : ''}`).then((r) => r.clients) return apiFetch<{ clients: Client[] }>(`/clients${qs !== '' ? `?${qs}` : ''}`).then((r) => r.clients)
} }

@ -87,6 +87,12 @@ export const DOC_TYPE_LABEL: Record<DocType, string> = {
QUOTATION: 'Quotation', PROFORMA: 'Proforma', INVOICE: 'Invoice', CREDIT_NOTE: 'Credit note', RECEIPT: 'Receipt', QUOTATION: 'Quotation', PROFORMA: 'Proforma', INVOICE: 'Invoice', CREDIT_NOTE: 'Credit note', RECEIPT: 'Receipt',
} }
/** Society categories (Kerala co-op directory taxonomy, D37) — the filter dropdown list. */
const CATEGORIES = [
'Service Bank', 'Urban Bank', 'Vanitha', 'Housing', 'Employees',
'Agricultural', 'Marketing', 'Bank (other)', 'Society (other)', 'Store', 'Other',
] as const
/** Client registry — search, list, inline create; row click opens the Client 360°. */ /** Client registry — search, list, inline create; row click opens the Client 360°. */
export function Clients() { export function Clients() {
const nav = useNavigate() const nav = useNavigate()
@ -95,11 +101,16 @@ export function Clients() {
const [district, setDistrict] = useState('') const [district, setDistrict] = useState('')
const [sector, setSector] = useState('') const [sector, setSector] = useState('')
const [typeFilter, setTypeFilter] = useState('all') // D35: PACS / Store const [typeFilter, setTypeFilter] = useState('all') // D35: PACS / Store
const [catFilter, setCatFilter] = useState('') // D37: society category
// Debounced so typing in the search box doesn't fire a server request per keystroke. // Debounced so typing in the search box doesn't fire a server request per keystroke.
const qDebounced = useDebounced(q) const qDebounced = useDebounced(q)
const { data, error, reload } = useData( const { data, error, reload } = useData(
() => getClients(qDebounced, { district, sector, ...(typeFilter !== 'all' ? { clientType: typeFilter } : {}) }), () => getClients(qDebounced, {
[qDebounced, district, sector, typeFilter], district, sector,
...(typeFilter !== 'all' ? { clientType: typeFilter } : {}),
...(catFilter !== '' ? { category: catFilter } : {}),
}),
[qDebounced, district, sector, typeFilter, catFilter],
) )
const [sp, setSp] = useSearchParams() const [sp, setSp] = useSearchParams()
const toast = useToast() const toast = useToast()
@ -175,6 +186,11 @@ export function Clients() {
{ key: 'Store', label: 'Store' }, { key: 'Store', label: 'Store' },
]} ]}
/> />
<select className="wf" style={{ maxWidth: 190 }} aria-label="Category"
value={catFilter} onChange={(e) => setCatFilter(e.target.value)}>
<option value="">All categories</option>
{CATEGORIES.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
<span className="spacer" /> <span className="spacer" />
{isOwner && (selecting ? ( {isOwner && (selecting ? (
<> <>
@ -200,7 +216,7 @@ export function Clients() {
columns={[ columns={[
...(selecting ? [{ key: 'sel', label: '' }] : []), ...(selecting ? [{ key: 'sel', label: '' }] : []),
{ key: 'code', label: 'Code', mono: true }, { key: 'name', label: 'Name' }, { key: 'code', label: 'Code', mono: true }, { key: 'name', label: 'Name' },
{ key: 'type', label: 'Type' }, { key: 'type', label: 'Type' }, { key: 'category', label: 'Category' },
{ key: 'status', label: 'Status' }, { key: 'state', label: 'State' }, { key: 'status', label: 'Status' }, { key: 'state', label: 'State' },
{ key: 'contact', label: 'Contact' }, { key: 'contact', label: 'Contact' },
]} ]}
@ -216,6 +232,7 @@ export function Clients() {
} : {}), } : {}),
code: c.code, name: c.name, code: c.code, name: c.name,
type: <Badge tone={c.clientType === 'Store' ? 'violet' : undefined}>{c.clientType}</Badge>, type: <Badge tone={c.clientType === 'Store' ? 'violet' : undefined}>{c.clientType}</Badge>,
category: <span style={{ fontSize: 12.5 }}>{c.category}</span>,
status: <Badge tone={CLIENT_TONE[c.status]}>{c.status}</Badge>, status: <Badge tone={CLIENT_TONE[c.status]}>{c.status}</Badge>,
state: c.stateCode, state: c.stateCode,
contact: c.contacts[0]?.email ?? c.contacts[0]?.phone ?? '—', contact: c.contacts[0]?.email ?? c.contacts[0]?.phone ?? '—',

@ -283,6 +283,7 @@ export function apiRouter(
...(typeof req.query['district'] === 'string' ? { district: req.query['district'] } : {}), ...(typeof req.query['district'] === 'string' ? { district: req.query['district'] } : {}),
...(typeof req.query['sector'] === 'string' ? { sector: req.query['sector'] } : {}), ...(typeof req.query['sector'] === 'string' ? { sector: req.query['sector'] } : {}),
...(typeof req.query['clientType'] === 'string' ? { clientType: req.query['clientType'] } : {}), ...(typeof req.query['clientType'] === 'string' ? { clientType: req.query['clientType'] } : {}),
...(typeof req.query['category'] === 'string' ? { category: req.query['category'] } : {}),
} }
res.json({ ok: true, clients: await listClients(db, q, filters) }) res.json({ ok: true, clients: await listClients(db, q, filters) })
} catch (err) { } catch (err) {
@ -863,7 +864,7 @@ export function apiRouter(
const client: Client = picked ?? { const client: Client = picked ?? {
id: '', code: '—', name: '— pick a client —', id: '', code: '—', name: '— pick a client —',
stateCode: company['company.state_code'] ?? '32', stateCode: company['company.state_code'] ?? '32',
address: '', contacts: [], status: 'lead', notes: '', clientType: 'PACS', hasDbPassword: false, address: '', contacts: [], status: 'lead', notes: '', clientType: 'PACS', category: 'Other', hasDbPassword: false,
} }
const t = prepared.totals const t = prepared.totals
const doc: Doc = { const doc: Doc = {

@ -106,6 +106,26 @@ export class SqliteDb implements DB {
* the SQLite SCHEMA below and by the Postgres migration set. Columns chosen: foreign keys * the SQLite SCHEMA below and by the Postgres migration set. Columns chosen: foreign keys
* used in joins/filters, and the status/date/renewal columns the scans and boards read. * used in joins/filters, and the status/date/renewal columns the scans and boards read.
*/ */
/**
* D37: derive the society category from the client name, matching the Kerala co-op directory
* taxonomy. Order matters most specific first (a "VANITHA … SOCIETY" is Vanitha, not the
* generic Society). Portable SQL (UPPER + LIKE) so SQLite and Postgres backfill identically.
*/
export const CATEGORY_BACKFILL_SQL = `
UPDATE client SET category = CASE
WHEN client_type = 'Store' THEN 'Store'
WHEN UPPER(name) LIKE '%VANITHA%' THEN 'Vanitha'
WHEN UPPER(name) LIKE '%HOUSING%' THEN 'Housing'
WHEN UPPER(name) LIKE '%URBAN%' THEN 'Urban Bank'
WHEN UPPER(name) LIKE '%EMPLOYEE%' OR UPPER(name) LIKE '%TEACHER%' OR UPPER(name) LIKE '%STAFF%' THEN 'Employees'
WHEN UPPER(name) LIKE '%MARKETING%' THEN 'Marketing'
WHEN UPPER(name) LIKE '%AGRICULTUR%' OR UPPER(name) LIKE '%RURAL DEVELOPMENT%' OR UPPER(name) LIKE '%RUBBER%' THEN 'Agricultural'
WHEN UPPER(name) LIKE '%SERVICE CO-OPERATIVE%' OR UPPER(name) LIKE '%SERVICE BANK%' OR UPPER(name) LIKE '%SCB%' THEN 'Service Bank'
WHEN UPPER(name) LIKE '%BANK%' THEN 'Bank (other)'
WHEN UPPER(name) LIKE '%SOCIET%' OR UPPER(name) LIKE '%CO-OPERATIVE%' OR UPPER(name) LIKE '%AICOS%' THEN 'Society (other)'
ELSE 'Other'
END`
export const INDEX_DDL = ` export const INDEX_DDL = `
CREATE INDEX IF NOT EXISTS ix_document_client ON document (client_id); CREATE INDEX IF NOT EXISTS ix_document_client ON document (client_id);
CREATE INDEX IF NOT EXISTS ix_document_type_status ON document (doc_type, status); CREATE INDEX IF NOT EXISTS ix_document_type_status ON document (doc_type, status);
@ -179,6 +199,8 @@ CREATE TABLE IF NOT EXISTS client (
anydesk TEXT, os TEXT, district TEXT, sector TEXT, db_password_enc TEXT, anydesk TEXT, os TEXT, district TEXT, sector TEXT, db_password_enc TEXT,
-- D35: 'PACS' (co-op society) vs 'Store' (retail); backfilled by name, editable per client. -- D35: 'PACS' (co-op society) vs 'Store' (retail); backfilled by name, editable per client.
client_type TEXT NOT NULL DEFAULT 'PACS', client_type TEXT NOT NULL DEFAULT 'PACS',
-- D37: society category (Service Bank / Vanitha / Housing / Employees / ); derived from name.
category TEXT NOT NULL DEFAULT 'Other',
notes TEXT NOT NULL DEFAULT '', source TEXT NOT NULL DEFAULT 'hq', created_at TEXT NOT NULL notes TEXT NOT NULL DEFAULT '', source TEXT NOT NULL DEFAULT 'hq', created_at TEXT NOT NULL
); );
CREATE TABLE IF NOT EXISTS module ( CREATE TABLE IF NOT EXISTS module (
@ -460,6 +482,12 @@ function migrate(db: SqliteRaw): void {
db.exec(`ALTER TABLE client ADD COLUMN client_type TEXT NOT NULL DEFAULT 'PACS'`) db.exec(`ALTER TABLE client ADD COLUMN client_type TEXT NOT NULL DEFAULT 'PACS'`)
db.exec(`UPDATE client SET client_type='Store' WHERE UPPER(TRIM(name)) LIKE '%STORE' OR UPPER(TRIM(name)) LIKE '%STORES'`) db.exec(`UPDATE client SET client_type='Store' WHERE UPPER(TRIM(name)) LIKE '%STORE' OR UPPER(TRIM(name)) LIKE '%STORES'`)
} }
// D37: society category matching the Kerala co-op directory taxonomy — derived from the
// name (Service Bank / Urban Bank / Vanitha / Housing / Employees / Agricultural / …).
if (!clientCols.some((c) => c.name === 'category')) {
db.exec(`ALTER TABLE client ADD COLUMN category TEXT NOT NULL DEFAULT 'Other'`)
db.exec(CATEGORY_BACKFILL_SQL)
}
// …and the same fields on the import staging table so the cutover carries them. // …and the same fields on the import staging table so the cutover carries them.
const stgClientCols = db.prepare(`PRAGMA table_info(stg_client)`).all() as { name: string }[] const stgClientCols = db.prepare(`PRAGMA table_info(stg_client)`).all() as { name: string }[]
for (const col of ['anydesk', 'os', 'district', 'sector']) { for (const col of ['anydesk', 'os', 'district', 'sector']) {

@ -330,6 +330,26 @@ CREATE TABLE IF NOT EXISTS login_event (
staff_id text, username_tried text NOT NULL, outcome text NOT NULL, staff_id text, username_tried text NOT NULL, outcome text NOT NULL,
ip text NOT NULL DEFAULT '', user_agent text NOT NULL DEFAULT '' ip text NOT NULL DEFAULT '', user_agent text NOT NULL DEFAULT ''
); );
`,
},
{
// D37: society category (directory taxonomy), backfilled from the name.
id: '014-client-category',
sql: `
ALTER TABLE client ADD COLUMN IF NOT EXISTS category text NOT NULL DEFAULT 'Other';
UPDATE client SET category = CASE
WHEN client_type = 'Store' THEN 'Store'
WHEN UPPER(name) LIKE '%VANITHA%' THEN 'Vanitha'
WHEN UPPER(name) LIKE '%HOUSING%' THEN 'Housing'
WHEN UPPER(name) LIKE '%URBAN%' THEN 'Urban Bank'
WHEN UPPER(name) LIKE '%EMPLOYEE%' OR UPPER(name) LIKE '%TEACHER%' OR UPPER(name) LIKE '%STAFF%' THEN 'Employees'
WHEN UPPER(name) LIKE '%MARKETING%' THEN 'Marketing'
WHEN UPPER(name) LIKE '%AGRICULTUR%' OR UPPER(name) LIKE '%RURAL DEVELOPMENT%' OR UPPER(name) LIKE '%RUBBER%' THEN 'Agricultural'
WHEN UPPER(name) LIKE '%SERVICE CO-OPERATIVE%' OR UPPER(name) LIKE '%SERVICE BANK%' OR UPPER(name) LIKE '%SCB%' THEN 'Service Bank'
WHEN UPPER(name) LIKE '%BANK%' THEN 'Bank (other)'
WHEN UPPER(name) LIKE '%SOCIET%' OR UPPER(name) LIKE '%CO-OPERATIVE%' OR UPPER(name) LIKE '%AICOS%' THEN 'Society (other)'
ELSE 'Other'
END;
`, `,
}, },
] ]

@ -18,6 +18,8 @@ export interface Client {
anydesk?: string; os?: string; district?: string; sector?: string anydesk?: string; os?: string; district?: string; sector?: string
/** D35: 'PACS' (co-op society) or 'Store' (retail). */ /** D35: 'PACS' (co-op society) or 'Store' (retail). */
clientType: string clientType: string
/** D37: society category (Service Bank / Vanitha / Housing / Employees / …). */
category: string
/** True when an encrypted DB password is stored (the UI shows •••• + Reveal). */ /** True when an encrypted DB password is stored (the UI shows •••• + Reveal). */
hasDbPassword: boolean hasDbPassword: boolean
} }
@ -26,11 +28,27 @@ interface ClientRow {
id: string; code: string; name: string; gstin: string | null; state_code: string id: string; code: string; name: string; gstin: string | null; state_code: string
address: string; contacts: string; status: string; owner_id: string | null; notes: string address: string; contacts: string; status: string; owner_id: string | null; notes: string
anydesk: string | null; os: string | null; district: string | null; sector: string | null anydesk: string | null; os: string | null; district: string | null; sector: string | null
client_type: string | null client_type: string | null; category: string | null
db_password_enc: string | null db_password_enc: string | null
source: string; created_at: string source: string; created_at: string
} }
/** Derive the society category from a name (same taxonomy as the DB backfill; most specific first). */
export function categoryFromName(name: string): string {
const n = name.toUpperCase()
if (/STORE[S]?$/.test(name.trim().toUpperCase())) return 'Store'
if (n.includes('VANITHA')) return 'Vanitha'
if (n.includes('HOUSING')) return 'Housing'
if (n.includes('URBAN')) return 'Urban Bank'
if (/EMPLOYEE|TEACHER|STAFF/.test(n)) return 'Employees'
if (n.includes('MARKETING')) return 'Marketing'
if (/AGRICULTUR|RURAL DEVELOPMENT|RUBBER/.test(n)) return 'Agricultural'
if (/SERVICE CO-OPERATIVE|SERVICE BANK|SCB/.test(n)) return 'Service Bank'
if (n.includes('BANK')) return 'Bank (other)'
if (/SOCIET|CO-OPERATIVE|AICOS/.test(n)) return 'Society (other)'
return 'Other'
}
function toClient(r: ClientRow): Client { function toClient(r: ClientRow): Client {
return { return {
id: r.id, code: r.code, name: r.name, id: r.id, code: r.code, name: r.name,
@ -44,6 +62,7 @@ function toClient(r: ClientRow): Client {
...(r.district !== null ? { district: r.district } : {}), ...(r.district !== null ? { district: r.district } : {}),
...(r.sector !== null ? { sector: r.sector } : {}), ...(r.sector !== null ? { sector: r.sector } : {}),
clientType: r.client_type ?? 'PACS', clientType: r.client_type ?? 'PACS',
category: r.category ?? 'Other',
hasDbPassword: r.db_password_enc !== null, // the ciphertext itself never leaves the repo hasDbPassword: r.db_password_enc !== null, // the ciphertext itself never leaves the repo
} }
} }
@ -54,7 +73,7 @@ function assertGstin(gstin: string): void {
} }
export async function listClients( export async function listClients(
db: DB, q?: string, filters: { district?: string; sector?: string; clientType?: string } = {}, db: DB, q?: string, filters: { district?: string; sector?: string; clientType?: string; category?: string } = {},
): Promise<Client[]> { ): Promise<Client[]> {
let where = ' WHERE 1=1' let where = ' WHERE 1=1'
const args: unknown[] = [] const args: unknown[] = []
@ -77,6 +96,9 @@ export async function listClients(
if (filters.clientType !== undefined && filters.clientType !== '') { if (filters.clientType !== undefined && filters.clientType !== '') {
where += ` AND client_type=?`; args.push(filters.clientType) where += ` AND client_type=?`; args.push(filters.clientType)
} }
if (filters.category !== undefined && filters.category !== '') {
where += ` AND category=?`; args.push(filters.category)
}
const rows = await db.all<ClientRow>(`SELECT * FROM client${where} ORDER BY name`, ...args) const rows = await db.all<ClientRow>(`SELECT * FROM client${where} ORDER BY name`, ...args)
return rows.map(toClient) return rows.map(toClient)
} }
@ -89,7 +111,7 @@ export async function getClient(db: DB, id: string): Promise<Client | null> {
export interface ClientInput { export interface ClientInput {
code?: string; name: string; gstin?: string; stateCode: string; address?: string code?: string; name: string; gstin?: string; stateCode: string; address?: string
contacts?: ClientContact[]; status?: Client['status']; notes?: string contacts?: ClientContact[]; status?: Client['status']; notes?: string
anydesk?: string; os?: string; district?: string; sector?: string; clientType?: string anydesk?: string; os?: string; district?: string; sector?: string; clientType?: string; category?: string
} }
/** New clients default their type from the name (…STORE → Store), like the backfill. */ /** New clients default their type from the name (…STORE → Store), like the backfill. */
@ -104,13 +126,14 @@ export async function createClient(db: DB, userId: string, input: ClientInput):
const code = input.code ?? `CL${String(n + 1).padStart(4, '0')}` const code = input.code ?? `CL${String(n + 1).padStart(4, '0')}`
await db.run( await db.run(
`INSERT INTO client (id, code, name, gstin, state_code, address, contacts, status, `INSERT INTO client (id, code, name, gstin, state_code, address, contacts, status,
anydesk, os, district, sector, client_type, notes, created_at) anydesk, os, district, sector, client_type, category, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
id, code, input.name, input.gstin ?? null, input.stateCode, id, code, input.name, input.gstin ?? null, input.stateCode,
input.address ?? '', JSON.stringify(input.contacts ?? []), input.address ?? '', JSON.stringify(input.contacts ?? []),
input.status ?? 'active', input.status ?? 'active',
input.anydesk ?? null, input.os ?? null, input.district ?? null, input.sector ?? null, input.anydesk ?? null, input.os ?? null, input.district ?? null, input.sector ?? null,
input.clientType ?? typeFromName(input.name), input.clientType ?? typeFromName(input.name),
input.category ?? categoryFromName(input.name),
input.notes ?? '', new Date().toISOString(), input.notes ?? '', new Date().toISOString(),
) )
const client = (await getClient(db, id))! const client = (await getClient(db, id))!
@ -123,7 +146,7 @@ export interface ClientPatch {
contacts?: ClientContact[]; status?: Client['status']; notes?: string contacts?: ClientContact[]; status?: Client['status']; notes?: string
/** '' clears the field (stored NULL). */ /** '' clears the field (stored NULL). */
anydesk?: string; os?: string; district?: string; sector?: string anydesk?: string; os?: string; district?: string; sector?: string
clientType?: string clientType?: string; category?: string
} }
export async function updateClient(db: DB, userId: string, id: string, patch: ClientPatch): Promise<Client> { export async function updateClient(db: DB, userId: string, id: string, patch: ClientPatch): Promise<Client> {
@ -140,6 +163,7 @@ export async function updateClient(db: DB, userId: string, id: string, patch: Cl
if (patch.status !== undefined) { sets.push('status=?'); args.push(patch.status) } if (patch.status !== undefined) { sets.push('status=?'); args.push(patch.status) }
if (patch.notes !== undefined) { sets.push('notes=?'); args.push(patch.notes) } if (patch.notes !== undefined) { sets.push('notes=?'); args.push(patch.notes) }
if (patch.clientType !== undefined) { sets.push('client_type=?'); args.push(patch.clientType) } if (patch.clientType !== undefined) { sets.push('client_type=?'); args.push(patch.clientType) }
if (patch.category !== undefined) { sets.push('category=?'); args.push(patch.category) }
const optText = (col: string, v: string | undefined) => { const optText = (col: string, v: string | undefined) => {
if (v !== undefined) { sets.push(`${col}=?`); args.push(v.trim() === '' ? null : v.trim()) } if (v !== undefined) { sets.push(`${col}=?`); args.push(v.trim() === '' ? null : v.trim()) }
} }

@ -413,7 +413,7 @@ export function documentHtmlSample(company: Record<string, string>, opts: { cont
const client: Client = { const client: Client = {
id: 'sample', code: 'SAMPLE', name: 'Sample Client Pvt Ltd', gstin: '32ABCDE1234F1Z5', id: 'sample', code: 'SAMPLE', name: 'Sample Client Pvt Ltd', gstin: '32ABCDE1234F1Z5',
stateCode, address: '1st Floor, MG Road, Kochi', contacts: [], status: 'active', notes: '', stateCode, address: '1st Floor, MG Road, Kochi', contacts: [], status: 'active', notes: '',
clientType: 'PACS', hasDbPassword: false, clientType: 'PACS', category: 'Other', hasDbPassword: false,
} }
const line: BillLine = { const line: BillLine = {
itemId: 'sample', name: 'POS Billing — Yearly', hsn: '998313', qty: 1, unitCode: 'NOS', itemId: 'sample', name: 'POS Billing — Yearly', hsn: '998313', qty: 1, unitCode: 'NOS',

Loading…
Cancel
Save