From 32449b45f3c76c3f8596ac218c953c493b09ad9c Mon Sep 17 00:00:00 2001
From: shafi54 <108669266+shafi-aviz@users.noreply.github.com>
Date: Sun, 24 May 2026 15:12:43 +0530
Subject: [PATCH] billing functional
---
apps/backend/src/lib/data-manager.ts | 10 +
.../src/trpc/pharmanager/v1/billing.ts | 85 +
.../src/trpc/pharmanager/v1/customers.ts | 67 +
apps/backend/src/trpc/router.ts | 4 +
apps/pharmanager/src/routeTree.gen.ts | 144 +-
apps/pharmanager/src/routes/billing.tsx | 12 +-
apps/pharmanager/src/routes/billing/$id.tsx | 122 ++
apps/pharmanager/src/routes/billing/index.tsx | 512 ++++++
apps/pharmanager/src/routes/customers.tsx | 12 +-
apps/pharmanager/src/routes/customers/$id.tsx | 60 +
apps/pharmanager/src/routes/customers/add.tsx | 89 ++
.../src/routes/customers/index.tsx | 114 ++
.../drizzle/0007_yellow_venom.sql | 47 +
.../drizzle/meta/0007_snapshot.json | 1380 +++++++++++++++++
.../drizzle/meta/_journal.json | 7 +
packages/data-manager-sqlite/src/bills.ts | 170 ++
packages/data-manager-sqlite/src/customers.ts | 99 ++
packages/data-manager-sqlite/src/index.ts | 14 +
.../src/schema/billItems.ts | 21 +
.../data-manager-sqlite/src/schema/bills.ts | 19 +
.../src/schema/customers.ts | 10 +
.../data-manager-sqlite/src/schema/index.ts | 3 +
packages/shared-react/src/hooks/billing.ts | 13 +
packages/shared-react/src/hooks/customers.ts | 25 +
packages/shared-react/src/index.ts | 2 +
25 files changed, 3009 insertions(+), 32 deletions(-)
create mode 100644 apps/backend/src/trpc/pharmanager/v1/billing.ts
create mode 100644 apps/backend/src/trpc/pharmanager/v1/customers.ts
create mode 100644 apps/pharmanager/src/routes/billing/$id.tsx
create mode 100644 apps/pharmanager/src/routes/billing/index.tsx
create mode 100644 apps/pharmanager/src/routes/customers/$id.tsx
create mode 100644 apps/pharmanager/src/routes/customers/add.tsx
create mode 100644 apps/pharmanager/src/routes/customers/index.tsx
create mode 100644 packages/data-manager-sqlite/drizzle/0007_yellow_venom.sql
create mode 100644 packages/data-manager-sqlite/drizzle/meta/0007_snapshot.json
create mode 100644 packages/data-manager-sqlite/src/bills.ts
create mode 100644 packages/data-manager-sqlite/src/customers.ts
create mode 100644 packages/data-manager-sqlite/src/schema/billItems.ts
create mode 100644 packages/data-manager-sqlite/src/schema/bills.ts
create mode 100644 packages/data-manager-sqlite/src/schema/customers.ts
create mode 100644 packages/shared-react/src/hooks/billing.ts
create mode 100644 packages/shared-react/src/hooks/customers.ts
diff --git a/apps/backend/src/lib/data-manager.ts b/apps/backend/src/lib/data-manager.ts
index 4a8e0c0..3ea2edf 100644
--- a/apps/backend/src/lib/data-manager.ts
+++ b/apps/backend/src/lib/data-manager.ts
@@ -9,6 +9,8 @@ import {
createStaffRepo,
createEnterpriseStaffRepo,
createRolesRepo,
+ createCustomersRepo,
+ createBillsRepo,
type StorageSpacesRepo,
type DistributorsRepo,
type ProductsRepo,
@@ -19,6 +21,8 @@ import {
type StaffRepo,
type EnterpriseStaffRepo,
type RolesRepo,
+ type CustomersRepo,
+ type BillsRepo,
} from "data-manager-sqlite";
export class DataManager {
@@ -32,6 +36,8 @@ export class DataManager {
readonly staff: StaffRepo;
readonly enterpriseStaff: EnterpriseStaffRepo;
readonly roles: RolesRepo;
+ readonly customers: CustomersRepo;
+ readonly bills: BillsRepo;
constructor() {
const { repo: storageSpacesRepo } = createStorageSpacesRepo();
@@ -44,6 +50,8 @@ export class DataManager {
const { repo: staffRepo } = createStaffRepo();
const { repo: enterpriseStaffRepo } = createEnterpriseStaffRepo();
const { repo: rolesRepo } = createRolesRepo();
+ const { repo: customersRepo } = createCustomersRepo();
+ const { repo: billsRepo } = createBillsRepo();
this.storageSpaces = storageSpacesRepo;
this.distributors = distributorsRepo;
@@ -55,5 +63,7 @@ export class DataManager {
this.staff = staffRepo;
this.enterpriseStaff = enterpriseStaffRepo;
this.roles = rolesRepo;
+ this.customers = customersRepo;
+ this.bills = billsRepo;
}
}
diff --git a/apps/backend/src/trpc/pharmanager/v1/billing.ts b/apps/backend/src/trpc/pharmanager/v1/billing.ts
new file mode 100644
index 0000000..097b085
--- /dev/null
+++ b/apps/backend/src/trpc/pharmanager/v1/billing.ts
@@ -0,0 +1,85 @@
+import { z } from "zod";
+import { protectedProcedure, router } from "../../init";
+import { dataManager } from "../../../lib/data-manager-instance";
+
+export const BillItemSchema = z.object({
+ id: z.number().int(),
+ product_id: z.number().int(),
+ product_name: z.string(),
+ brand: z.string().nullable(),
+ batch_id: z.number().int().nullable(),
+ strips: z.number().int(),
+ loose: z.number().int(),
+ qty: z.number().int(),
+ original_price: z.number(),
+ selling_price: z.number(),
+ total: z.number(),
+});
+
+export const BillSchema = z.object({
+ id: z.number().int(),
+ bill_no: z.string(),
+ customer_mobile: z.string(),
+ customer_name: z.string().nullable(),
+ subtotal: z.number(),
+ tax: z.number(),
+ tax_rate: z.number(),
+ total: z.number(),
+ discount: z.number(),
+ discount_percent: z.number().int(),
+ generated_by: z.number().int(),
+ created_at: z.string(),
+ items: z.array(BillItemSchema),
+});
+
+const BillItemInputSchema = z.object({
+ product_id: z.number().int(),
+ product_name: z.string(),
+ brand: z.string().nullable().optional(),
+ batch_id: z.number().int().nullable().optional(),
+ strips: z.number().int(),
+ loose: z.number().int(),
+ qty: z.number().int(),
+ original_price: z.number(),
+ selling_price: z.number(),
+ total: z.number(),
+});
+
+export const CreateBillInput = z.object({
+ bill_no: z.string(),
+ customer_mobile: z.string(),
+ customer_name: z.string().nullable().optional(),
+ subtotal: z.number(),
+ tax: z.number(),
+ tax_rate: z.number(),
+ total: z.number(),
+ discount: z.number().default(0),
+ discount_percent: z.number().int().default(0),
+ created_at: z.string(),
+ items: z.array(BillItemInputSchema).min(1),
+});
+
+export const billingRouter = router({
+ list: protectedProcedure
+ .output(z.array(BillSchema))
+ .query(({ ctx }) =>
+ dataManager.bills.getBills(ctx.staff.enterpriseId),
+ ),
+
+ byId: protectedProcedure
+ .input(z.object({ id: z.number().int() }))
+ .output(BillSchema.nullable())
+ .query(({ ctx, input }) =>
+ dataManager.bills.getBillById(input.id, ctx.staff.enterpriseId),
+ ),
+
+ create: protectedProcedure
+ .input(CreateBillInput)
+ .output(BillSchema)
+ .mutation(({ ctx, input }) =>
+ dataManager.bills.createBill(
+ { ...input, generated_by: ctx.staff.staffId },
+ ctx.staff.enterpriseId,
+ ),
+ ),
+});
diff --git a/apps/backend/src/trpc/pharmanager/v1/customers.ts b/apps/backend/src/trpc/pharmanager/v1/customers.ts
new file mode 100644
index 0000000..a95a826
--- /dev/null
+++ b/apps/backend/src/trpc/pharmanager/v1/customers.ts
@@ -0,0 +1,67 @@
+import { z } from "zod";
+import { protectedProcedure, router } from "../../init";
+import { dataManager } from "../../../lib/data-manager-instance";
+
+export const CustomerSchema = z.object({
+ id: z.number().int(),
+ mobile: z.string(),
+ name: z.string().nullable(),
+ added_on: z.string(),
+});
+
+export const CreateCustomerInput = z.object({
+ mobile: z.string().min(1),
+ name: z.string().nullable().optional(),
+ added_on: z.string(),
+});
+
+export const UpdateCustomerInput = z.object({
+ id: z.number().int(),
+ mobile: z.string().min(1).optional(),
+ name: z.string().nullable().optional(),
+});
+
+export const customersRouter = router({
+ list: protectedProcedure
+ .output(z.array(CustomerSchema))
+ .query(({ ctx }) =>
+ dataManager.customers.listCustomers(ctx.staff.enterpriseId),
+ ),
+
+ search: protectedProcedure
+ .input(z.object({ query: z.string() }))
+ .output(z.array(CustomerSchema))
+ .query(({ ctx, input }) =>
+ dataManager.customers.searchCustomers(input.query, ctx.staff.enterpriseId),
+ ),
+
+ byId: protectedProcedure
+ .input(z.object({ id: z.number().int() }))
+ .output(CustomerSchema.nullable())
+ .query(({ ctx, input }) =>
+ dataManager.customers.getCustomerById(input.id, ctx.staff.enterpriseId),
+ ),
+
+ create: protectedProcedure
+ .input(CreateCustomerInput)
+ .output(CustomerSchema)
+ .mutation(({ ctx, input }) =>
+ dataManager.customers.createCustomer(input, ctx.staff.enterpriseId),
+ ),
+
+ update: protectedProcedure
+ .input(UpdateCustomerInput)
+ .output(CustomerSchema.nullable())
+ .mutation(({ ctx, input }) => {
+ const { id, ...patch } = input;
+ return dataManager.customers.updateCustomer(id, patch, ctx.staff.enterpriseId);
+ }),
+
+ remove: protectedProcedure
+ .input(z.object({ id: z.number().int() }))
+ .output(z.object({ ok: z.boolean() }))
+ .mutation(async ({ ctx, input }) => {
+ const ok = await dataManager.customers.deleteCustomer(input.id, ctx.staff.enterpriseId);
+ return { ok };
+ }),
+});
diff --git a/apps/backend/src/trpc/router.ts b/apps/backend/src/trpc/router.ts
index 09bc678..b5abf87 100644
--- a/apps/backend/src/trpc/router.ts
+++ b/apps/backend/src/trpc/router.ts
@@ -8,6 +8,8 @@ import { stockRouter } from "./pharmanager/v1/stock";
import { authRouter } from "./pharmanager/v1/auth";
import { staffManagementRouter } from "./pharmanager/v1/staffManagement";
import { rolesRouter } from "./pharmanager/v1/roles";
+import { billingRouter } from "./pharmanager/v1/billing";
+import { customersRouter } from "./pharmanager/v1/customers";
export const appRouter = router({
storage: storageRouter,
@@ -19,6 +21,8 @@ export const appRouter = router({
auth: authRouter,
staffManagement: staffManagementRouter,
roles: rolesRouter,
+ billing: billingRouter,
+ customers: customersRouter,
});
export type AppRouter = typeof appRouter;
diff --git a/apps/pharmanager/src/routeTree.gen.ts b/apps/pharmanager/src/routeTree.gen.ts
index b8ed1a5..3b989c2 100644
--- a/apps/pharmanager/src/routeTree.gen.ts
+++ b/apps/pharmanager/src/routeTree.gen.ts
@@ -24,6 +24,8 @@ import { Route as StockIndexRouteImport } from './routes/stock/index'
import { Route as StaffIndexRouteImport } from './routes/staff/index'
import { Route as ProductsIndexRouteImport } from './routes/products/index'
import { Route as DistributorsIndexRouteImport } from './routes/distributors/index'
+import { Route as CustomersIndexRouteImport } from './routes/customers/index'
+import { Route as BillingIndexRouteImport } from './routes/billing/index'
import { Route as StorageAddRouteImport } from './routes/storage/add'
import { Route as StorageIdRouteImport } from './routes/storage/$id'
import { Route as StockAddRouteImport } from './routes/stock/add'
@@ -34,6 +36,9 @@ import { Route as ProductsAddRouteImport } from './routes/products/add'
import { Route as ProductsIdRouteImport } from './routes/products/$id'
import { Route as DistributorsAddRouteImport } from './routes/distributors/add'
import { Route as DistributorsIdRouteImport } from './routes/distributors/$id'
+import { Route as CustomersAddRouteImport } from './routes/customers/add'
+import { Route as CustomersIdRouteImport } from './routes/customers/$id'
+import { Route as BillingIdRouteImport } from './routes/billing/$id'
const StorageRoute = StorageRouteImport.update({
id: '/storage',
@@ -110,6 +115,16 @@ const DistributorsIndexRoute = DistributorsIndexRouteImport.update({
path: '/',
getParentRoute: () => DistributorsRoute,
} as any)
+const CustomersIndexRoute = CustomersIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => CustomersRoute,
+} as any)
+const BillingIndexRoute = BillingIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => BillingRoute,
+} as any)
const StorageAddRoute = StorageAddRouteImport.update({
id: '/add',
path: '/add',
@@ -160,11 +175,26 @@ const DistributorsIdRoute = DistributorsIdRouteImport.update({
path: '/$id',
getParentRoute: () => DistributorsRoute,
} as any)
+const CustomersAddRoute = CustomersAddRouteImport.update({
+ id: '/add',
+ path: '/add',
+ getParentRoute: () => CustomersRoute,
+} as any)
+const CustomersIdRoute = CustomersIdRouteImport.update({
+ id: '/$id',
+ path: '/$id',
+ getParentRoute: () => CustomersRoute,
+} as any)
+const BillingIdRoute = BillingIdRouteImport.update({
+ id: '/$id',
+ path: '/$id',
+ getParentRoute: () => BillingRoute,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
- '/billing': typeof BillingRoute
- '/customers': typeof CustomersRoute
+ '/billing': typeof BillingRouteWithChildren
+ '/customers': typeof CustomersRouteWithChildren
'/distributors': typeof DistributorsRouteWithChildren
'/login': typeof LoginRoute
'/products': typeof ProductsRouteWithChildren
@@ -172,6 +202,9 @@ export interface FileRoutesByFullPath {
'/staff': typeof StaffRouteWithChildren
'/stock': typeof StockRouteWithChildren
'/storage': typeof StorageRouteWithChildren
+ '/billing/$id': typeof BillingIdRoute
+ '/customers/$id': typeof CustomersIdRoute
+ '/customers/add': typeof CustomersAddRoute
'/distributors/$id': typeof DistributorsIdRoute
'/distributors/add': typeof DistributorsAddRoute
'/products/$id': typeof ProductsIdRoute
@@ -182,6 +215,8 @@ export interface FileRoutesByFullPath {
'/stock/add': typeof StockAddRoute
'/storage/$id': typeof StorageIdRoute
'/storage/add': typeof StorageAddRoute
+ '/billing/': typeof BillingIndexRoute
+ '/customers/': typeof CustomersIndexRoute
'/distributors/': typeof DistributorsIndexRoute
'/products/': typeof ProductsIndexRoute
'/staff/': typeof StaffIndexRoute
@@ -190,10 +225,11 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
- '/billing': typeof BillingRoute
- '/customers': typeof CustomersRoute
'/login': typeof LoginRoute
'/profile': typeof ProfileRoute
+ '/billing/$id': typeof BillingIdRoute
+ '/customers/$id': typeof CustomersIdRoute
+ '/customers/add': typeof CustomersAddRoute
'/distributors/$id': typeof DistributorsIdRoute
'/distributors/add': typeof DistributorsAddRoute
'/products/$id': typeof ProductsIdRoute
@@ -204,6 +240,8 @@ export interface FileRoutesByTo {
'/stock/add': typeof StockAddRoute
'/storage/$id': typeof StorageIdRoute
'/storage/add': typeof StorageAddRoute
+ '/billing': typeof BillingIndexRoute
+ '/customers': typeof CustomersIndexRoute
'/distributors': typeof DistributorsIndexRoute
'/products': typeof ProductsIndexRoute
'/staff': typeof StaffIndexRoute
@@ -213,8 +251,8 @@ export interface FileRoutesByTo {
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
- '/billing': typeof BillingRoute
- '/customers': typeof CustomersRoute
+ '/billing': typeof BillingRouteWithChildren
+ '/customers': typeof CustomersRouteWithChildren
'/distributors': typeof DistributorsRouteWithChildren
'/login': typeof LoginRoute
'/products': typeof ProductsRouteWithChildren
@@ -222,6 +260,9 @@ export interface FileRoutesById {
'/staff': typeof StaffRouteWithChildren
'/stock': typeof StockRouteWithChildren
'/storage': typeof StorageRouteWithChildren
+ '/billing/$id': typeof BillingIdRoute
+ '/customers/$id': typeof CustomersIdRoute
+ '/customers/add': typeof CustomersAddRoute
'/distributors/$id': typeof DistributorsIdRoute
'/distributors/add': typeof DistributorsAddRoute
'/products/$id': typeof ProductsIdRoute
@@ -232,6 +273,8 @@ export interface FileRoutesById {
'/stock/add': typeof StockAddRoute
'/storage/$id': typeof StorageIdRoute
'/storage/add': typeof StorageAddRoute
+ '/billing/': typeof BillingIndexRoute
+ '/customers/': typeof CustomersIndexRoute
'/distributors/': typeof DistributorsIndexRoute
'/products/': typeof ProductsIndexRoute
'/staff/': typeof StaffIndexRoute
@@ -251,6 +294,9 @@ export interface FileRouteTypes {
| '/staff'
| '/stock'
| '/storage'
+ | '/billing/$id'
+ | '/customers/$id'
+ | '/customers/add'
| '/distributors/$id'
| '/distributors/add'
| '/products/$id'
@@ -261,6 +307,8 @@ export interface FileRouteTypes {
| '/stock/add'
| '/storage/$id'
| '/storage/add'
+ | '/billing/'
+ | '/customers/'
| '/distributors/'
| '/products/'
| '/staff/'
@@ -269,10 +317,11 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
- | '/billing'
- | '/customers'
| '/login'
| '/profile'
+ | '/billing/$id'
+ | '/customers/$id'
+ | '/customers/add'
| '/distributors/$id'
| '/distributors/add'
| '/products/$id'
@@ -283,6 +332,8 @@ export interface FileRouteTypes {
| '/stock/add'
| '/storage/$id'
| '/storage/add'
+ | '/billing'
+ | '/customers'
| '/distributors'
| '/products'
| '/staff'
@@ -300,6 +351,9 @@ export interface FileRouteTypes {
| '/staff'
| '/stock'
| '/storage'
+ | '/billing/$id'
+ | '/customers/$id'
+ | '/customers/add'
| '/distributors/$id'
| '/distributors/add'
| '/products/$id'
@@ -310,6 +364,8 @@ export interface FileRouteTypes {
| '/stock/add'
| '/storage/$id'
| '/storage/add'
+ | '/billing/'
+ | '/customers/'
| '/distributors/'
| '/products/'
| '/staff/'
@@ -319,8 +375,8 @@ export interface FileRouteTypes {
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
- BillingRoute: typeof BillingRoute
- CustomersRoute: typeof CustomersRoute
+ BillingRoute: typeof BillingRouteWithChildren
+ CustomersRoute: typeof CustomersRouteWithChildren
DistributorsRoute: typeof DistributorsRouteWithChildren
LoginRoute: typeof LoginRoute
ProductsRoute: typeof ProductsRouteWithChildren
@@ -437,6 +493,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DistributorsIndexRouteImport
parentRoute: typeof DistributorsRoute
}
+ '/customers/': {
+ id: '/customers/'
+ path: '/'
+ fullPath: '/customers/'
+ preLoaderRoute: typeof CustomersIndexRouteImport
+ parentRoute: typeof CustomersRoute
+ }
+ '/billing/': {
+ id: '/billing/'
+ path: '/'
+ fullPath: '/billing/'
+ preLoaderRoute: typeof BillingIndexRouteImport
+ parentRoute: typeof BillingRoute
+ }
'/storage/add': {
id: '/storage/add'
path: '/add'
@@ -507,9 +577,59 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DistributorsIdRouteImport
parentRoute: typeof DistributorsRoute
}
+ '/customers/add': {
+ id: '/customers/add'
+ path: '/add'
+ fullPath: '/customers/add'
+ preLoaderRoute: typeof CustomersAddRouteImport
+ parentRoute: typeof CustomersRoute
+ }
+ '/customers/$id': {
+ id: '/customers/$id'
+ path: '/$id'
+ fullPath: '/customers/$id'
+ preLoaderRoute: typeof CustomersIdRouteImport
+ parentRoute: typeof CustomersRoute
+ }
+ '/billing/$id': {
+ id: '/billing/$id'
+ path: '/$id'
+ fullPath: '/billing/$id'
+ preLoaderRoute: typeof BillingIdRouteImport
+ parentRoute: typeof BillingRoute
+ }
}
}
+interface BillingRouteChildren {
+ BillingIdRoute: typeof BillingIdRoute
+ BillingIndexRoute: typeof BillingIndexRoute
+}
+
+const BillingRouteChildren: BillingRouteChildren = {
+ BillingIdRoute: BillingIdRoute,
+ BillingIndexRoute: BillingIndexRoute,
+}
+
+const BillingRouteWithChildren =
+ BillingRoute._addFileChildren(BillingRouteChildren)
+
+interface CustomersRouteChildren {
+ CustomersIdRoute: typeof CustomersIdRoute
+ CustomersAddRoute: typeof CustomersAddRoute
+ CustomersIndexRoute: typeof CustomersIndexRoute
+}
+
+const CustomersRouteChildren: CustomersRouteChildren = {
+ CustomersIdRoute: CustomersIdRoute,
+ CustomersAddRoute: CustomersAddRoute,
+ CustomersIndexRoute: CustomersIndexRoute,
+}
+
+const CustomersRouteWithChildren = CustomersRoute._addFileChildren(
+ CustomersRouteChildren,
+)
+
interface DistributorsRouteChildren {
DistributorsIdRoute: typeof DistributorsIdRoute
DistributorsAddRoute: typeof DistributorsAddRoute
@@ -587,8 +707,8 @@ const StorageRouteWithChildren =
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
- BillingRoute: BillingRoute,
- CustomersRoute: CustomersRoute,
+ BillingRoute: BillingRouteWithChildren,
+ CustomersRoute: CustomersRouteWithChildren,
DistributorsRoute: DistributorsRouteWithChildren,
LoginRoute: LoginRoute,
ProductsRoute: ProductsRouteWithChildren,
diff --git a/apps/pharmanager/src/routes/billing.tsx b/apps/pharmanager/src/routes/billing.tsx
index 27bb4c6..fb1e6c3 100644
--- a/apps/pharmanager/src/routes/billing.tsx
+++ b/apps/pharmanager/src/routes/billing.tsx
@@ -1,13 +1,5 @@
-import { createFileRoute } from "@tanstack/react-router";
+import { Outlet, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/billing")({
- component: BillingPage,
- staticData: {
- title: "Billing",
- subtitle: "Invoices, payments, revenue tracking",
- },
+ component: () => ,
});
-
-function BillingPage() {
- return
Billing
;
-}
diff --git a/apps/pharmanager/src/routes/billing/$id.tsx b/apps/pharmanager/src/routes/billing/$id.tsx
new file mode 100644
index 0000000..2da352e
--- /dev/null
+++ b/apps/pharmanager/src/routes/billing/$id.tsx
@@ -0,0 +1,122 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { ReceiptText, User, Clock, DollarSign } from "lucide-react";
+import { DetailRow, BackLink, EmptyState } from "#/components/ui";
+import { useGetBillById } from "shared-react";
+
+export const Route = createFileRoute("/billing/$id")({
+ component: BillDetailsPage,
+ staticData: {
+ title: "Bill Details",
+ subtitle: "Loading...",
+ },
+});
+
+function BillDetailsPage() {
+ const { id } = Route.useParams();
+ const billId = Number(id);
+ const { data: bill, isLoading } = useGetBillById(billId);
+
+ if (isLoading) return Loading bill...
;
+ if (!bill) {
+ return (
+
+ );
+ }
+
+ const d = new Date(bill.created_at);
+ const dateStr = d.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit" });
+
+ return (
+
+
+
+
+
+
{bill.bill_no}
+
{dateStr}
+
+
+ {bill.customer_name ? `${bill.customer_name} (${bill.customer_mobile})` : bill.customer_mobile}
+
+
+
+
₹{bill.total.toFixed(2)}
+
Completed
+
+
+
+
+
+
+ Timings & Info
+
+
+ Generated{dateStr}
+ Items{bill.items.length}
+
+
+
+
+
+ Payment Summary
+
+
+ Subtotal₹{bill.subtotal.toFixed(2)}
+ Tax ({bill.tax_rate}%)₹{bill.tax.toFixed(2)}
+ Discount{bill.discount_percent}%
+ Total
+ ₹{bill.total.toFixed(2)}
+
+
+
+
+
+
+ Bill Items
+
+
+
+
+
+ | Product |
+ Strips |
+ Loose |
+ Price |
+ Total |
+
+
+
+ {bill.items.map((item) => (
+
+ |
+ {item.product_name}
+ {item.brand && {item.brand} }
+ |
+ {item.strips} |
+ {item.loose} |
+
+ {item.original_price !== item.selling_price ? (
+
+ ₹{item.original_price.toFixed(2)}
+ ₹{item.selling_price.toFixed(2)}
+
+ ) : (
+ ₹{item.selling_price.toFixed(2)}
+ )}
+ |
+ ₹{item.total.toFixed(2)} |
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/apps/pharmanager/src/routes/billing/index.tsx b/apps/pharmanager/src/routes/billing/index.tsx
new file mode 100644
index 0000000..08b9b16
--- /dev/null
+++ b/apps/pharmanager/src/routes/billing/index.tsx
@@ -0,0 +1,512 @@
+import { useState, useMemo, useRef, useEffect } from "react";
+import { createFileRoute, Link } from "@tanstack/react-router";
+import { Plus, Trash2, ReceiptText } from "lucide-react";
+import { Button, Input, Combobox, buttonVariants } from "#/components/ui";
+import {
+ useListBills,
+ useCreateBill,
+ useSearchCustomers,
+ useCreateCustomer,
+ useListProducts,
+ useListStockBatches,
+ trpc,
+} from "shared-react";
+
+export const Route = createFileRoute("/billing/")({
+ component: BillingPage,
+ staticData: {
+ title: "Billing",
+ subtitle: "Invoice generation & payment management",
+ },
+});
+
+interface BillItemForm {
+ product_id: number;
+ product_name: string;
+ brand: string;
+ batch_id: number | null;
+ strips: number;
+ loose: number;
+ qty: number;
+ original_price: number;
+ selling_price: number;
+ total: number;
+ batchOptions: { id: number; batch_no: string; quantity: number; is_default: boolean }[];
+}
+
+function BillingPage() {
+ const [tab, setTab] = useState<"new" | "history">("new");
+ const [customerMobile, setCustomerMobile] = useState("");
+ const [customerName, setCustomerName] = useState("");
+ const [customerSearch, setCustomerSearch] = useState("");
+ const [showCustomerDropdown, setShowCustomerDropdown] = useState(false);
+ const [items, setItems] = useState([]);
+
+ const { data: bills, isLoading: billsLoading } = useListBills();
+ const { data: customerResults } = useSearchCustomers(customerSearch);
+ const { data: products } = useListProducts();
+ const { data: batches } = useListStockBatches();
+ const createBillMutation = useCreateBill();
+ const createCustomerMutation = useCreateCustomer();
+ const utils = trpc.useUtils();
+
+ const custRef = useRef(null);
+
+ useEffect(() => {
+ function handleClickOutside(e: MouseEvent) {
+ if (custRef.current && !custRef.current.contains(e.target as Node)) {
+ setShowCustomerDropdown(false);
+ }
+ }
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => document.removeEventListener("mousedown", handleClickOutside);
+ }, []);
+
+ const customerSearchResults = useMemo(() => {
+ if (!customerSearch) return [];
+ return (customerResults ?? []).filter(
+ (c) => c.mobile.includes(customerSearch) || (c.name || "").toLowerCase().includes(customerSearch.toLowerCase()),
+ );
+ }, [customerSearch, customerResults]);
+
+ function selectCustomer(c: { mobile: string; name: string | null }) {
+ setCustomerMobile(c.mobile);
+ setCustomerName(c.name || "");
+ setShowCustomerDropdown(false);
+ setCustomerSearch("");
+ }
+
+ function selectAnonymous() {
+ setCustomerMobile("Anonymous");
+ setCustomerName("");
+ setShowCustomerDropdown(false);
+ setCustomerSearch("");
+ }
+
+ function addCustomer() {
+ const mobile = customerSearch.trim();
+ if (!mobile) return;
+ createCustomerMutation.mutate(
+ { mobile, name: null, added_on: new Date().toISOString().slice(0, 10) },
+ {
+ onSuccess: (c) => {
+ setCustomerMobile(c.mobile);
+ setCustomerName(c.name || "");
+ setShowCustomerDropdown(false);
+ setCustomerSearch("");
+ },
+ },
+ );
+ }
+
+ function addItem(product?: (typeof products)[number]) {
+ if (product) {
+ const productBatches = (batches ?? []).filter((b) => b.product.id === product.id);
+ const defaultBatch = productBatches.find((b) => b.is_default);
+ setItems((prev) => [
+ ...prev,
+ {
+ product_id: product.id,
+ product_name: product.name,
+ brand: product.brand,
+ batch_id: defaultBatch?.id ?? null,
+ strips: product.units_per_strip ? 1 : 0,
+ loose: 0,
+ qty: product.units_per_strip ? product.units_per_strip * 1 : 1,
+ original_price: product.selling_price,
+ selling_price: product.selling_price,
+ total: product.units_per_strip ? product.units_per_strip * 1 * product.selling_price : product.selling_price,
+ batchOptions: productBatches.map((b) => ({
+ id: b.id,
+ batch_no: b.batch_no,
+ quantity: b.quantity,
+ is_default: b.is_default,
+ })),
+ },
+ ]);
+ } else {
+ setItems((prev) => [
+ ...prev,
+ {
+ product_id: 0,
+ product_name: "",
+ brand: "",
+ batch_id: null,
+ strips: 0,
+ loose: 0,
+ qty: 0,
+ original_price: 0,
+ selling_price: 0,
+ total: 0,
+ batchOptions: [],
+ },
+ ]);
+ }
+ }
+
+ function removeItem(idx: number) {
+ setItems((prev) => prev.filter((_, i) => i !== idx));
+ }
+
+ function updateItem(idx: number, field: keyof BillItemForm, value: string | number | null) {
+ setItems((prev) =>
+ prev.map((item, i) => {
+ if (i !== idx) return item;
+
+ let updated = { ...item, [field]: value };
+
+ if (field === "batch_id") {
+ const batch = item.batchOptions.find((b) => b.id === Number(value));
+ if (batch) updated.qty = updated.strips * (products?.find((p) => p.id === item.product_id)?.units_per_strip ?? 1) + updated.loose;
+ }
+
+ if (field === "selling_price") {
+ const ups = products?.find((p) => p.id === item.product_id)?.units_per_strip ?? 1;
+ updated.qty = updated.strips * ups + updated.loose;
+ updated.total = Number(updated.qty) * Number(updated.selling_price);
+ }
+
+ if (field === "strips" || field === "loose") {
+ const ups = products?.find((p) => p.id === item.product_id)?.units_per_strip ?? 1;
+ updated.qty = Number(updated.strips) * ups + Number(updated.loose);
+ updated.total = Number(updated.qty) * Number(updated.selling_price);
+ }
+
+ return updated;
+ }),
+ );
+ }
+
+ function applyProductSearch(idx: number, product: (typeof products)[number]) {
+ const productBatches = (batches ?? []).filter((b) => b.product.id === product.id);
+ const defaultBatch = productBatches.find((b) => b.is_default);
+ setItems((prev) =>
+ prev.map((item, i) => {
+ if (i !== idx) return item;
+ const ups = product.units_per_strip ?? 1;
+ const strips = product.units_per_strip ? 1 : 0;
+ const qty = strips * ups;
+ return {
+ ...item,
+ product_id: product.id,
+ product_name: product.name,
+ brand: product.brand,
+ batch_id: defaultBatch?.id ?? null,
+ strips,
+ loose: 0,
+ qty,
+ original_price: product.selling_price,
+ selling_price: product.selling_price,
+ total: qty * product.selling_price,
+ batchOptions: productBatches.map((b) => ({
+ id: b.id,
+ batch_no: b.batch_no,
+ quantity: b.quantity,
+ is_default: b.is_default,
+ })),
+ };
+ }),
+ );
+ }
+
+ const subtotal = items.reduce((s, i) => s + i.total, 0);
+ const tax = subtotal * 0.18;
+ const total = subtotal + tax;
+
+ function handleGenerateBill() {
+ if (!customerMobile) return;
+ const validItems = items.filter((i) => i.product_id > 0 && i.qty > 0);
+ if (validItems.length === 0) return;
+
+ createBillMutation.mutate(
+ {
+ bill_no: "BIL-" + Date.now().toString(36).toUpperCase(),
+ customer_mobile: customerMobile,
+ customer_name: customerName || null,
+ subtotal,
+ tax,
+ tax_rate: 18,
+ total,
+ discount: 0,
+ discount_percent: 0,
+ created_at: new Date().toISOString(),
+ items: validItems.map((i) => ({
+ product_id: i.product_id,
+ product_name: i.product_name,
+ brand: i.brand || null,
+ batch_id: i.batch_id,
+ strips: i.strips,
+ loose: i.loose,
+ qty: i.qty,
+ original_price: i.original_price,
+ selling_price: i.selling_price,
+ total: i.total,
+ })),
+ },
+ {
+ onSuccess: () => {
+ utils.billing.list.invalidate();
+ setCustomerMobile("");
+ setCustomerName("");
+ setItems([]);
+ setTab("history");
+ },
+ },
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ {tab === "new" && (
+
+ {/* Customer */}
+
+
+
+ Customer
+
+
+
+
{
+ setCustomerSearch(e.target.value);
+ setShowCustomerDropdown(true);
+ }}
+ onFocus={() => setShowCustomerDropdown(true)}
+ placeholder="Search by name or mobile..."
+ />
+ {showCustomerDropdown && (
+
+
+ Unknown / Anonymous
+
+ {customerSearchResults.map((c) => (
+
selectCustomer(c)}
+ >
+ {c.name || c.mobile}
+ {c.name && {c.mobile}}
+
+ ))}
+ {customerSearch && (
+
+ )}
+
+ )}
+
+ {customerMobile && (
+
+ {customerName || customerMobile}
+
+ )}
+
+
+
+ {/* Items */}
+
+
+
+ Items
+
+
+
+
+
+ | Product |
+ Batch |
+ Strips |
+ Loose |
+ Price |
+ Total |
+ |
+
+
+
+ {items.map((item, idx) => (
+
+
+ {item.product_id > 0 ? (
+
+ {item.product_name}
+ {item.brand}
+
+ ) : (
+ {
+ const product = (products ?? []).find((p) => String(p.id) === val);
+ if (product) applyProductSearch(idx, product);
+ }}
+ options={(products ?? []).map((p) => ({
+ value: String(p.id),
+ label: `${p.name} — ${p.brand} — ₹${p.selling_price.toFixed(2)}`,
+ }))}
+ placeholder="Search product..."
+ emptyMessage="No products found."
+ />
+ )}
+ |
+
+ {item.product_id > 0 && (
+
+ )}
+ |
+
+ updateItem(idx, "strips", Number(e.target.value))}
+ className="w-full px-2 py-1.5 border border-slate-200 rounded text-xs text-center"
+ min={0}
+ disabled={!products?.find((p) => p.id === item.product_id)?.units_per_strip}
+ />
+ |
+
+ updateItem(idx, "loose", Number(e.target.value))}
+ className="w-full px-2 py-1.5 border border-slate-200 rounded text-xs text-center"
+ min={0}
+ />
+ |
+
+ updateItem(idx, "selling_price", Number(e.target.value))}
+ className="w-full px-2 py-1.5 border border-slate-200 rounded text-xs text-right"
+ min={0}
+ step={0.01}
+ />
+ |
+ ₹{item.total.toFixed(2)} |
+
+
+ |
+
+ ))}
+
+
+
+
+
+
+ {/* Summary */}
+
+
+ Bill Summary
+
+
+
+ Subtotal
+ ₹{subtotal.toFixed(2)}
+
+
+ Tax (18% GST)
+ ₹{tax.toFixed(2)}
+
+
+ Total
+ ₹{total.toFixed(2)}
+
+
+
+
+
+
+
+ )}
+
+ {tab === "history" && (
+
+ {billsLoading &&
Loading bills...
}
+ {(bills ?? []).length === 0 && !billsLoading && (
+
+
+
No bills yet
+
Generate your first bill to see it here.
+
+ )}
+
+ {(bills ?? []).map((bill) => {
+ const d = new Date(bill.created_at);
+ const dateStr = d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit" });
+ return (
+
+
+
{bill.bill_no}
+
{bill.customer_name || bill.customer_mobile}
+
{dateStr}
+
+
+
₹{bill.total.toFixed(2)}
+
{bill.items.length} item{bill.items.length > 1 ? "s" : ""}
+
Completed
+
+
+ );
+ })}
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/pharmanager/src/routes/customers.tsx b/apps/pharmanager/src/routes/customers.tsx
index c689d72..4ad5ecd 100644
--- a/apps/pharmanager/src/routes/customers.tsx
+++ b/apps/pharmanager/src/routes/customers.tsx
@@ -1,13 +1,5 @@
-import { createFileRoute } from "@tanstack/react-router";
+import { Outlet, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/customers")({
- component: CustomersPage,
- staticData: {
- title: "Customers",
- subtitle: "Customer management with purchase history",
- },
+ component: () => ,
});
-
-function CustomersPage() {
- return Customers
;
-}
diff --git a/apps/pharmanager/src/routes/customers/$id.tsx b/apps/pharmanager/src/routes/customers/$id.tsx
new file mode 100644
index 0000000..b87d117
--- /dev/null
+++ b/apps/pharmanager/src/routes/customers/$id.tsx
@@ -0,0 +1,60 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { Pencil, Trash2, Users, Phone, Calendar } from "lucide-react";
+import { Button, DetailRow, BackLink, EmptyState } from "#/components/ui";
+import { useGetCustomerById, useRemoveCustomer, trpc } from "shared-react";
+
+function fmtDate(d: string): string {
+ const date = new Date(d + "T00:00:00");
+ return date.toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric" });
+}
+
+export const Route = createFileRoute("/customers/$id")({
+ component: CustomerDetailsPage,
+ staticData: { title: "Customer Details", subtitle: "Customer information" },
+});
+
+function CustomerDetailsPage() {
+ const { id } = Route.useParams();
+ const customerId = Number(id);
+ const { data: customer, isLoading, error } = useGetCustomerById(customerId);
+ const removeMutation = useRemoveCustomer();
+ const utils = trpc.useUtils();
+
+ function handleDelete() {
+ if (!customer) return;
+ if (!confirm(`Delete ${customer.name || customer.mobile}?`)) return;
+ removeMutation.mutate({ id: customer.id }, { onSuccess: () => { utils.customers.list.invalidate(); window.location.href = "/customers"; } });
+ }
+
+ if (isLoading) return Loading...
;
+ if (error || !customer) return ;
+
+ return (
+
+
+
+
+ {(customer.name || customer.mobile)[0].toUpperCase()}
+
+
+
{customer.name || "Unnamed"}
+
{customer.mobile}
+
+
+
+
+
+ );
+}
diff --git a/apps/pharmanager/src/routes/customers/add.tsx b/apps/pharmanager/src/routes/customers/add.tsx
new file mode 100644
index 0000000..0622488
--- /dev/null
+++ b/apps/pharmanager/src/routes/customers/add.tsx
@@ -0,0 +1,89 @@
+import { useEffect } from "react";
+import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
+import { Plus } from "lucide-react";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { useCreateCustomer, useUpdateCustomer, useGetCustomerById, trpc } from "shared-react";
+import { Button, Input, BackLink, buttonVariants } from "#/components/ui";
+
+const formSchema = z.object({
+ mobile: z.string().min(1, "Mobile is required"),
+ name: z.string().nullable().optional(),
+ added_on: z.string().min(1, "Date is required"),
+});
+
+type FormValues = z.infer;
+
+export const Route = createFileRoute("/customers/add")({
+ component: AddCustomerPage,
+ validateSearch: (search: Record) => ({ id: search.id ? Number(search.id) : undefined }),
+ staticData: { title: "Add Customer", subtitle: "Register a new customer" },
+});
+
+function AddCustomerPage() {
+ const navigate = useNavigate();
+ const { id: editId } = Route.useSearch();
+ const createMutation = useCreateCustomer();
+ const updateMutation = useUpdateCustomer();
+ const { data: existingCustomer } = useGetCustomerById(editId ?? 0);
+ const utils = trpc.useUtils();
+ const isEditing = typeof editId === "number" && editId > 0;
+
+ const { register, handleSubmit, reset, formState: { errors } } = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: { mobile: "", name: null, added_on: new Date().toISOString().slice(0, 10) },
+ });
+
+ useEffect(() => {
+ if (isEditing && existingCustomer) {
+ reset({ mobile: existingCustomer.mobile, name: existingCustomer.name, added_on: existingCustomer.added_on });
+ }
+ }, [isEditing, existingCustomer, reset]);
+
+ function onSubmit(values: FormValues) {
+ if (isEditing) {
+ updateMutation.mutate(
+ { id: editId!, ...values },
+ { onSuccess: () => { utils.customers.list.invalidate(); utils.customers.byId.invalidate({ id: editId! }); navigate({ to: "/customers" }); } },
+ );
+ } else {
+ createMutation.mutate(values, { onSuccess: () => { utils.customers.list.invalidate(); navigate({ to: "/customers" }); } });
+ }
+ }
+
+ const mutation = isEditing ? updateMutation : createMutation;
+
+ return (
+
+ );
+}
diff --git a/apps/pharmanager/src/routes/customers/index.tsx b/apps/pharmanager/src/routes/customers/index.tsx
new file mode 100644
index 0000000..d3fb1f8
--- /dev/null
+++ b/apps/pharmanager/src/routes/customers/index.tsx
@@ -0,0 +1,114 @@
+import { useState, useMemo, useCallback } from "react";
+import { createFileRoute, Link } from "@tanstack/react-router";
+import { Pencil, Trash2, Users } from "lucide-react";
+import { GridTable } from "#/components/GridTable";
+import type { GridTableColumn } from "#/components/GridTable";
+import { Button, SearchToolbar } from "#/components/ui";
+import { useListCustomers, useRemoveCustomer, trpc } from "shared-react";
+
+interface CustomerRow {
+ id: number;
+ mobile: string;
+ name: string | null;
+ added_on: string;
+}
+
+function fmtDate(d: string): string {
+ const date = new Date(d + "T00:00:00");
+ return date.toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric" });
+}
+
+function makeColumns(onDelete: (row: CustomerRow) => void): GridTableColumn[] {
+ return [
+ {
+ id: "name",
+ header: "Name",
+ cell: ({ row }) => (
+
+ {row.name || "—"}
+
+ ),
+ },
+ {
+ id: "mobile",
+ header: "Mobile",
+ cell: ({ row }) => {row.mobile},
+ },
+ {
+ id: "added_on",
+ header: "Registered",
+ cell: ({ row }) => {fmtDate(row.added_on)},
+ },
+ {
+ id: "actions",
+ header: "Actions",
+ size: 90,
+ cell: ({ row }) => (
+
+
+
+
+
+
+ ),
+ },
+ ];
+}
+
+export const Route = createFileRoute("/customers/")({
+ component: CustomersIndexPage,
+ staticData: {
+ title: "Customers",
+ subtitle: "Customer management with purchase history",
+ },
+});
+
+function CustomersIndexPage() {
+ const [searchQuery, setSearchQuery] = useState("");
+ const { data: customerList, isLoading, error } = useListCustomers();
+ const removeMutation = useRemoveCustomer();
+ const utils = trpc.useUtils();
+
+ const handleDelete = useCallback(
+ (row: CustomerRow) => {
+ if (!confirm(`Delete ${row.name || row.mobile}?`)) return;
+ removeMutation.mutate({ id: row.id }, { onSuccess: () => utils.customers.list.invalidate() });
+ },
+ [removeMutation, utils],
+ );
+
+ const columns = useMemo(() => makeColumns(handleDelete), [handleDelete]);
+
+ const filtered = useMemo(() => {
+ const q = searchQuery.toLowerCase().trim();
+ if (!q) return customerList ?? [];
+ return (customerList ?? []).filter((c) => {
+ const text = `${c.name || ""} ${c.mobile}`;
+ return text.toLowerCase().includes(q);
+ });
+ }, [searchQuery, customerList]);
+
+ if (isLoading) return Loading customers...
;
+ if (error) return Failed to load customers.
;
+
+ return (
+
+
+
+
+ No customers found
+ {searchQuery ? "No customers match your search." : "Add your first customer to get started."}
+
+ }
+ />
+
+ );
+}
diff --git a/packages/data-manager-sqlite/drizzle/0007_yellow_venom.sql b/packages/data-manager-sqlite/drizzle/0007_yellow_venom.sql
new file mode 100644
index 0000000..043cd58
--- /dev/null
+++ b/packages/data-manager-sqlite/drizzle/0007_yellow_venom.sql
@@ -0,0 +1,47 @@
+CREATE TABLE `customers` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `mobile` text NOT NULL,
+ `name` text,
+ `added_on` text NOT NULL,
+ `enterprise_id` integer NOT NULL,
+ FOREIGN KEY (`enterprise_id`) REFERENCES `enterprises`(`id`) ON UPDATE no action ON DELETE no action
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `customers_mobile_unique` ON `customers` (`mobile`);--> statement-breakpoint
+CREATE TABLE `bills` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `bill_no` text NOT NULL,
+ `customer_mobile` text NOT NULL,
+ `customer_name` text,
+ `subtotal` real NOT NULL,
+ `tax` real NOT NULL,
+ `tax_rate` real DEFAULT 18 NOT NULL,
+ `total` real NOT NULL,
+ `discount` real DEFAULT 0 NOT NULL,
+ `discount_percent` integer DEFAULT 0 NOT NULL,
+ `generated_by` integer NOT NULL,
+ `created_at` text NOT NULL,
+ `enterprise_id` integer NOT NULL,
+ FOREIGN KEY (`generated_by`) REFERENCES `staff`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`enterprise_id`) REFERENCES `enterprises`(`id`) ON UPDATE no action ON DELETE no action
+);
+--> statement-breakpoint
+CREATE TABLE `bill_items` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `bill_id` integer NOT NULL,
+ `product_id` integer NOT NULL,
+ `batch_id` integer,
+ `product_name` text NOT NULL,
+ `brand` text,
+ `strips` integer DEFAULT 0 NOT NULL,
+ `loose` integer DEFAULT 0 NOT NULL,
+ `qty` integer NOT NULL,
+ `original_price` real NOT NULL,
+ `selling_price` real NOT NULL,
+ `total` real NOT NULL,
+ `enterprise_id` integer NOT NULL,
+ FOREIGN KEY (`bill_id`) REFERENCES `bills`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`product_id`) REFERENCES `products`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`batch_id`) REFERENCES `stock_batches`(`id`) ON UPDATE no action ON DELETE no action,
+ FOREIGN KEY (`enterprise_id`) REFERENCES `enterprises`(`id`) ON UPDATE no action ON DELETE no action
+);
diff --git a/packages/data-manager-sqlite/drizzle/meta/0007_snapshot.json b/packages/data-manager-sqlite/drizzle/meta/0007_snapshot.json
new file mode 100644
index 0000000..bb23884
--- /dev/null
+++ b/packages/data-manager-sqlite/drizzle/meta/0007_snapshot.json
@@ -0,0 +1,1380 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "2eafc73e-0dde-444b-bf3b-a27420a05f2b",
+ "prevId": "737f8f6d-432c-489e-84d0-54f906a6faca",
+ "tables": {
+ "storage_spaces": {
+ "name": "storage_spaces",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "aliases": {
+ "name": "aliases",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'[]'"
+ },
+ "image_urls": {
+ "name": "image_urls",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'[]'"
+ },
+ "enterprise_id": {
+ "name": "enterprise_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "storage_spaces_enterprise_idx": {
+ "name": "storage_spaces_enterprise_idx",
+ "columns": [
+ "enterprise_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "storage_spaces_enterprise_id_enterprises_id_fk": {
+ "name": "storage_spaces_enterprise_id_enterprises_id_fk",
+ "tableFrom": "storage_spaces",
+ "tableTo": "enterprises",
+ "columnsFrom": [
+ "enterprise_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "distributors": {
+ "name": "distributors",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "agency": {
+ "name": "agency",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "contact": {
+ "name": "contact",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "mobile": {
+ "name": "mobile",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "address": {
+ "name": "address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "enterprise_id": {
+ "name": "enterprise_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "distributors_enterprise_idx": {
+ "name": "distributors_enterprise_idx",
+ "columns": [
+ "enterprise_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "distributors_enterprise_id_enterprises_id_fk": {
+ "name": "distributors_enterprise_id_enterprises_id_fk",
+ "tableFrom": "distributors",
+ "tableTo": "enterprises",
+ "columnsFrom": [
+ "enterprise_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "products": {
+ "name": "products",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "brand": {
+ "name": "brand",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "category": {
+ "name": "category",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "distributor_id": {
+ "name": "distributor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "unit_id": {
+ "name": "unit_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "procured_price": {
+ "name": "procured_price",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "mrp": {
+ "name": "mrp",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "selling_price": {
+ "name": "selling_price",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "reorder_level": {
+ "name": "reorder_level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "units_per_strip": {
+ "name": "units_per_strip",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "hide_product_from_public": {
+ "name": "hide_product_from_public",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "hide_price_from_public": {
+ "name": "hide_price_from_public",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "enterprise_id": {
+ "name": "enterprise_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "products_enterprise_idx": {
+ "name": "products_enterprise_idx",
+ "columns": [
+ "enterprise_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "products_distributor_id_distributors_id_fk": {
+ "name": "products_distributor_id_distributors_id_fk",
+ "tableFrom": "products",
+ "tableTo": "distributors",
+ "columnsFrom": [
+ "distributor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "products_unit_id_units_id_fk": {
+ "name": "products_unit_id_units_id_fk",
+ "tableFrom": "products",
+ "tableTo": "units",
+ "columnsFrom": [
+ "unit_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "products_enterprise_id_enterprises_id_fk": {
+ "name": "products_enterprise_id_enterprises_id_fk",
+ "tableFrom": "products",
+ "tableTo": "enterprises",
+ "columnsFrom": [
+ "enterprise_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "drug_info": {
+ "name": "drug_info",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "drug_info_name_unique": {
+ "name": "drug_info_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "units": {
+ "name": "units",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "units_name_unique": {
+ "name": "units_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "product_compositions": {
+ "name": "product_compositions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "product_id": {
+ "name": "product_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "drug_info_id": {
+ "name": "drug_info_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "unit_id": {
+ "name": "unit_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "product_compositions_product_id_products_id_fk": {
+ "name": "product_compositions_product_id_products_id_fk",
+ "tableFrom": "product_compositions",
+ "tableTo": "products",
+ "columnsFrom": [
+ "product_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "product_compositions_drug_info_id_drug_info_id_fk": {
+ "name": "product_compositions_drug_info_id_drug_info_id_fk",
+ "tableFrom": "product_compositions",
+ "tableTo": "drug_info",
+ "columnsFrom": [
+ "drug_info_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "product_compositions_unit_id_units_id_fk": {
+ "name": "product_compositions_unit_id_units_id_fk",
+ "tableFrom": "product_compositions",
+ "tableTo": "units",
+ "columnsFrom": [
+ "unit_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "stock_batches": {
+ "name": "stock_batches",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "product_id": {
+ "name": "product_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "arrived": {
+ "name": "arrived",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "batch_no": {
+ "name": "batch_no",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "mfg": {
+ "name": "mfg",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expiry": {
+ "name": "expiry",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "rack_id": {
+ "name": "rack_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "distributor_id": {
+ "name": "distributor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "enterprise_id": {
+ "name": "enterprise_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "stock_batches_enterprise_idx": {
+ "name": "stock_batches_enterprise_idx",
+ "columns": [
+ "enterprise_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "stock_batches_product_id_products_id_fk": {
+ "name": "stock_batches_product_id_products_id_fk",
+ "tableFrom": "stock_batches",
+ "tableTo": "products",
+ "columnsFrom": [
+ "product_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "stock_batches_rack_id_storage_spaces_id_fk": {
+ "name": "stock_batches_rack_id_storage_spaces_id_fk",
+ "tableFrom": "stock_batches",
+ "tableTo": "storage_spaces",
+ "columnsFrom": [
+ "rack_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "stock_batches_distributor_id_distributors_id_fk": {
+ "name": "stock_batches_distributor_id_distributors_id_fk",
+ "tableFrom": "stock_batches",
+ "tableTo": "distributors",
+ "columnsFrom": [
+ "distributor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "stock_batches_enterprise_id_enterprises_id_fk": {
+ "name": "stock_batches_enterprise_id_enterprises_id_fk",
+ "tableFrom": "stock_batches",
+ "tableTo": "enterprises",
+ "columnsFrom": [
+ "enterprise_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "enterprises": {
+ "name": "enterprises",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_name": {
+ "name": "owner_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "mobile": {
+ "name": "mobile",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "address": {
+ "name": "address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "staff": {
+ "name": "staff",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "mobile": {
+ "name": "mobile",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "added_on": {
+ "name": "added_on",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "is_password_reset_needed": {
+ "name": "is_password_reset_needed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ }
+ },
+ "indexes": {
+ "staff_username_unique": {
+ "name": "staff_username_unique",
+ "columns": [
+ "username"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "roles": {
+ "name": "roles",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "roles_name_unique": {
+ "name": "roles_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "permissions": {
+ "name": "permissions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "permissions_name_unique": {
+ "name": "permissions_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "role_permissions": {
+ "name": "role_permissions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission_id": {
+ "name": "permission_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "role_permissions_role_id_permission_id_unique": {
+ "name": "role_permissions_role_id_permission_id_unique",
+ "columns": [
+ "role_id",
+ "permission_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "role_permissions_role_id_roles_id_fk": {
+ "name": "role_permissions_role_id_roles_id_fk",
+ "tableFrom": "role_permissions",
+ "tableTo": "roles",
+ "columnsFrom": [
+ "role_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "role_permissions_permission_id_permissions_id_fk": {
+ "name": "role_permissions_permission_id_permissions_id_fk",
+ "tableFrom": "role_permissions",
+ "tableTo": "permissions",
+ "columnsFrom": [
+ "permission_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "staff_roles": {
+ "name": "staff_roles",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "staff_id": {
+ "name": "staff_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "staff_roles_staff_id_role_id_unique": {
+ "name": "staff_roles_staff_id_role_id_unique",
+ "columns": [
+ "staff_id",
+ "role_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "staff_roles_staff_id_staff_id_fk": {
+ "name": "staff_roles_staff_id_staff_id_fk",
+ "tableFrom": "staff_roles",
+ "tableTo": "staff",
+ "columnsFrom": [
+ "staff_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "staff_roles_role_id_roles_id_fk": {
+ "name": "staff_roles_role_id_roles_id_fk",
+ "tableFrom": "staff_roles",
+ "tableTo": "roles",
+ "columnsFrom": [
+ "role_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "enterprise_staff": {
+ "name": "enterprise_staff",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "staff_id": {
+ "name": "staff_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enterprise_id": {
+ "name": "enterprise_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "enterprise_staff_staff_id_enterprise_id_unique": {
+ "name": "enterprise_staff_staff_id_enterprise_id_unique",
+ "columns": [
+ "staff_id",
+ "enterprise_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "enterprise_staff_staff_id_staff_id_fk": {
+ "name": "enterprise_staff_staff_id_staff_id_fk",
+ "tableFrom": "enterprise_staff",
+ "tableTo": "staff",
+ "columnsFrom": [
+ "staff_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "enterprise_staff_enterprise_id_enterprises_id_fk": {
+ "name": "enterprise_staff_enterprise_id_enterprises_id_fk",
+ "tableFrom": "enterprise_staff",
+ "tableTo": "enterprises",
+ "columnsFrom": [
+ "enterprise_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "customers": {
+ "name": "customers",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "mobile": {
+ "name": "mobile",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "added_on": {
+ "name": "added_on",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enterprise_id": {
+ "name": "enterprise_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "customers_mobile_unique": {
+ "name": "customers_mobile_unique",
+ "columns": [
+ "mobile"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "customers_enterprise_id_enterprises_id_fk": {
+ "name": "customers_enterprise_id_enterprises_id_fk",
+ "tableFrom": "customers",
+ "tableTo": "enterprises",
+ "columnsFrom": [
+ "enterprise_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "bills": {
+ "name": "bills",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "bill_no": {
+ "name": "bill_no",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "customer_mobile": {
+ "name": "customer_mobile",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "customer_name": {
+ "name": "customer_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "subtotal": {
+ "name": "subtotal",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "tax": {
+ "name": "tax",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "tax_rate": {
+ "name": "tax_rate",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 18
+ },
+ "total": {
+ "name": "total",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "discount": {
+ "name": "discount",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "discount_percent": {
+ "name": "discount_percent",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "generated_by": {
+ "name": "generated_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enterprise_id": {
+ "name": "enterprise_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "bills_generated_by_staff_id_fk": {
+ "name": "bills_generated_by_staff_id_fk",
+ "tableFrom": "bills",
+ "tableTo": "staff",
+ "columnsFrom": [
+ "generated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "bills_enterprise_id_enterprises_id_fk": {
+ "name": "bills_enterprise_id_enterprises_id_fk",
+ "tableFrom": "bills",
+ "tableTo": "enterprises",
+ "columnsFrom": [
+ "enterprise_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "bill_items": {
+ "name": "bill_items",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "bill_id": {
+ "name": "bill_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "product_id": {
+ "name": "product_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "batch_id": {
+ "name": "batch_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "product_name": {
+ "name": "product_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "brand": {
+ "name": "brand",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "strips": {
+ "name": "strips",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "loose": {
+ "name": "loose",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "qty": {
+ "name": "qty",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "original_price": {
+ "name": "original_price",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "selling_price": {
+ "name": "selling_price",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "total": {
+ "name": "total",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "enterprise_id": {
+ "name": "enterprise_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "bill_items_bill_id_bills_id_fk": {
+ "name": "bill_items_bill_id_bills_id_fk",
+ "tableFrom": "bill_items",
+ "tableTo": "bills",
+ "columnsFrom": [
+ "bill_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "bill_items_product_id_products_id_fk": {
+ "name": "bill_items_product_id_products_id_fk",
+ "tableFrom": "bill_items",
+ "tableTo": "products",
+ "columnsFrom": [
+ "product_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "bill_items_batch_id_stock_batches_id_fk": {
+ "name": "bill_items_batch_id_stock_batches_id_fk",
+ "tableFrom": "bill_items",
+ "tableTo": "stock_batches",
+ "columnsFrom": [
+ "batch_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "bill_items_enterprise_id_enterprises_id_fk": {
+ "name": "bill_items_enterprise_id_enterprises_id_fk",
+ "tableFrom": "bill_items",
+ "tableTo": "enterprises",
+ "columnsFrom": [
+ "enterprise_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/packages/data-manager-sqlite/drizzle/meta/_journal.json b/packages/data-manager-sqlite/drizzle/meta/_journal.json
index 700fc61..0b333f2 100644
--- a/packages/data-manager-sqlite/drizzle/meta/_journal.json
+++ b/packages/data-manager-sqlite/drizzle/meta/_journal.json
@@ -50,6 +50,13 @@
"when": 1779551911536,
"tag": "0006_nappy_sir_ram",
"breakpoints": true
+ },
+ {
+ "idx": 7,
+ "version": "6",
+ "when": 1779561725701,
+ "tag": "0007_yellow_venom",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/data-manager-sqlite/src/bills.ts b/packages/data-manager-sqlite/src/bills.ts
new file mode 100644
index 0000000..2ae41a5
--- /dev/null
+++ b/packages/data-manager-sqlite/src/bills.ts
@@ -0,0 +1,170 @@
+import { eq } from 'drizzle-orm'
+
+import { db, sqlite } from './db-instance'
+import { bills } from './schema/bills'
+import { billItems } from './schema/billItems'
+import { stockBatches } from './schema/stockBatches'
+
+export type BillItem = {
+ id: number
+ product_id: number
+ product_name: string
+ brand: string | null
+ batch_id: number | null
+ strips: number
+ loose: number
+ qty: number
+ original_price: number
+ selling_price: number
+ total: number
+}
+
+export type Bill = {
+ id: number
+ bill_no: string
+ customer_mobile: string
+ customer_name: string | null
+ subtotal: number
+ tax: number
+ tax_rate: number
+ total: number
+ discount: number
+ discount_percent: number
+ generated_by: number
+ created_at: string
+ items: BillItem[]
+}
+
+export type BillsRepo = {
+ getBills: (enterpriseId: number) => Promise
+ getBillById: (id: number, enterpriseId: number) => Promise
+ createBill: (input: {
+ bill_no: string
+ customer_mobile: string
+ customer_name?: string | null
+ subtotal: number
+ tax: number
+ tax_rate: number
+ total: number
+ discount: number
+ discount_percent: number
+ generated_by: number
+ created_at: string
+ items: {
+ product_id: number
+ product_name: string
+ brand?: string | null
+ batch_id?: number | null
+ strips: number
+ loose: number
+ qty: number
+ original_price: number
+ selling_price: number
+ total: number
+ }[]
+ }, enterpriseId: number) => Promise
+}
+
+function toBill(row: typeof bills.$inferSelect): Omit {
+ return {
+ id: row.id,
+ bill_no: row.billNo,
+ customer_mobile: row.customerMobile,
+ customer_name: row.customerName,
+ subtotal: row.subtotal,
+ tax: row.tax,
+ tax_rate: row.taxRate,
+ total: row.total,
+ discount: row.discount,
+ discount_percent: row.discountPercent,
+ generated_by: row.generatedBy,
+ created_at: row.createdAt,
+ }
+}
+
+function getItemsForBill(billId: number): BillItem[] {
+ const rows = db.select().from(billItems).where(eq(billItems.billId, billId)).all()
+ return rows.map((r) => ({
+ id: r.id,
+ product_id: r.productId,
+ product_name: r.productName,
+ brand: r.brand,
+ batch_id: r.batchId,
+ strips: r.strips,
+ loose: r.loose,
+ qty: r.qty,
+ original_price: r.originalPrice,
+ selling_price: r.sellingPrice,
+ total: r.total,
+ }))
+}
+
+function deductStock(batchId: number | null, qty: number) {
+ if (!batchId) return
+ const batch = db.select().from(stockBatches).where(eq(stockBatches.id, batchId)).get()
+ if (batch && batch.quantity >= qty) {
+ db.update(stockBatches)
+ .set({ quantity: batch.quantity - qty })
+ .where(eq(stockBatches.id, batchId))
+ .run()
+ }
+}
+
+export function createBillsRepo(): { repo: BillsRepo } {
+ const repo: BillsRepo = {
+ getBills(enterpriseId) {
+ const rows = db.select().from(bills).where(eq(bills.enterpriseId, enterpriseId)).all()
+ return Promise.resolve(rows.map((r) => ({ ...toBill(r), items: getItemsForBill(r.id) })))
+ },
+
+ getBillById(id, enterpriseId) {
+ const row = db.select().from(bills).where(eq(bills.id, id)).get()
+ if (!row || row.enterpriseId !== enterpriseId) return Promise.resolve(null)
+ return Promise.resolve({ ...toBill(row), items: getItemsForBill(row.id) })
+ },
+
+ createBill(input, enterpriseId) {
+ const result = sqlite.transaction(() => {
+ const created = db.insert(bills).values({
+ billNo: input.bill_no,
+ customerMobile: input.customer_mobile,
+ customerName: input.customer_name ?? null,
+ subtotal: input.subtotal,
+ tax: input.tax,
+ taxRate: input.tax_rate,
+ total: input.total,
+ discount: input.discount,
+ discountPercent: input.discount_percent,
+ generatedBy: input.generated_by,
+ createdAt: input.created_at,
+ enterpriseId,
+ }).returning().get()
+
+ for (const item of input.items) {
+ db.insert(billItems).values({
+ billId: created.id,
+ productId: item.product_id,
+ productName: item.product_name,
+ brand: item.brand ?? null,
+ batchId: item.batch_id ?? null,
+ strips: item.strips,
+ loose: item.loose,
+ qty: item.qty,
+ originalPrice: item.original_price,
+ sellingPrice: item.selling_price,
+ total: item.total,
+ enterpriseId,
+ }).run()
+
+ deductStock(item.batch_id ?? null, item.qty)
+ }
+
+ return created
+ })()
+
+ return Promise.resolve({ ...toBill(result), items: getItemsForBill(result.id) })
+ },
+ }
+
+ return { repo }
+}
diff --git a/packages/data-manager-sqlite/src/customers.ts b/packages/data-manager-sqlite/src/customers.ts
new file mode 100644
index 0000000..cb0ff20
--- /dev/null
+++ b/packages/data-manager-sqlite/src/customers.ts
@@ -0,0 +1,99 @@
+import { eq, or, like } from 'drizzle-orm'
+
+import { db } from './db-instance'
+import { customers } from './schema/customers'
+
+export type Customer = {
+ id: number
+ mobile: string
+ name: string | null
+ added_on: string
+}
+
+export type CustomersRepo = {
+ listCustomers: (enterpriseId: number) => Promise
+ searchCustomers: (query: string, enterpriseId: number) => Promise
+ getCustomerById: (id: number, enterpriseId: number) => Promise
+ getCustomerByMobile: (mobile: string, enterpriseId: number) => Promise
+ createCustomer: (input: { mobile: string; name?: string | null; added_on: string }, enterpriseId: number) => Promise
+ updateCustomer: (id: number, input: { mobile?: string; name?: string | null }, enterpriseId: number) => Promise
+ deleteCustomer: (id: number, enterpriseId: number) => Promise
+}
+
+function toCustomer(row: typeof customers.$inferSelect): Customer {
+ return { id: row.id, mobile: row.mobile, name: row.name, added_on: row.addedOn }
+}
+
+export function createCustomersRepo(): { repo: CustomersRepo } {
+ const repo: CustomersRepo = {
+ listCustomers(enterpriseId) {
+ const rows = db.select().from(customers).where(eq(customers.enterpriseId, enterpriseId)).all()
+ return Promise.resolve(rows.map(toCustomer))
+ },
+
+ searchCustomers(query, enterpriseId) {
+ const q = `%${query}%`
+ const rows = db
+ .select()
+ .from(customers)
+ .where(
+ or(
+ like(customers.mobile, q),
+ like(customers.name, q),
+ ),
+ )
+ .all()
+ return Promise.resolve(rows.filter(r => r.enterpriseId === enterpriseId).map(toCustomer))
+ },
+
+ getCustomerById(id, enterpriseId) {
+ const row = db.select().from(customers).where(eq(customers.id, id)).get()
+ if (!row || row.enterpriseId !== enterpriseId) return Promise.resolve(null)
+ return Promise.resolve(toCustomer(row))
+ },
+
+ getCustomerByMobile(mobile, enterpriseId) {
+ const row = db
+ .select()
+ .from(customers)
+ .where(eq(customers.mobile, mobile))
+ .get()
+ if (!row || row.enterpriseId !== enterpriseId) return Promise.resolve(null)
+ return Promise.resolve(toCustomer(row))
+ },
+
+ createCustomer(input, enterpriseId) {
+ const created = db.insert(customers).values({
+ mobile: input.mobile,
+ name: input.name ?? null,
+ addedOn: input.added_on,
+ enterpriseId,
+ }).returning().get()
+ return Promise.resolve(toCustomer(created))
+ },
+
+ updateCustomer(id, input, enterpriseId) {
+ const existing = db.select().from(customers).where(eq(customers.id, id)).get()
+ if (!existing || existing.enterpriseId !== enterpriseId) return Promise.resolve(null)
+
+ const setData: Record = {}
+ if (input.mobile !== undefined) setData.mobile = input.mobile
+ if (input.name !== undefined) setData.name = input.name ?? null
+
+ if (Object.keys(setData).length > 0) {
+ db.update(customers).set(setData).where(eq(customers.id, id)).run()
+ }
+ const updated = db.select().from(customers).where(eq(customers.id, id)).get()!
+ return Promise.resolve(toCustomer(updated))
+ },
+
+ deleteCustomer(id, enterpriseId) {
+ const existing = db.select().from(customers).where(eq(customers.id, id)).get()
+ if (!existing || existing.enterpriseId !== enterpriseId) return Promise.resolve(false)
+ db.delete(customers).where(eq(customers.id, id)).run()
+ return Promise.resolve(true)
+ },
+ }
+
+ return { repo }
+}
diff --git a/packages/data-manager-sqlite/src/index.ts b/packages/data-manager-sqlite/src/index.ts
index 17abf00..de8c918 100644
--- a/packages/data-manager-sqlite/src/index.ts
+++ b/packages/data-manager-sqlite/src/index.ts
@@ -77,3 +77,17 @@ export {
type StaffRole,
type StaffRolesRepo,
} from './staffRoles'
+export {
+ createCustomersRepo,
+ type Customer,
+ type CustomersRepo,
+} from './customers'
+export {
+ createBillsRepo,
+ type Bill,
+ type BillItem,
+ type BillsRepo,
+} from './bills'
+export { customers } from './schema/customers'
+export { bills } from './schema/bills'
+export { billItems } from './schema/billItems'
diff --git a/packages/data-manager-sqlite/src/schema/billItems.ts b/packages/data-manager-sqlite/src/schema/billItems.ts
new file mode 100644
index 0000000..9103b90
--- /dev/null
+++ b/packages/data-manager-sqlite/src/schema/billItems.ts
@@ -0,0 +1,21 @@
+import { integer, sqliteTable, text, real } from 'drizzle-orm/sqlite-core'
+import { bills } from './bills'
+import { products } from './products'
+import { stockBatches } from './stockBatches'
+import { enterprises } from './enterprises'
+
+export const billItems = sqliteTable('bill_items', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ billId: integer('bill_id').notNull().references(() => bills.id),
+ productId: integer('product_id').notNull().references(() => products.id),
+ batchId: integer('batch_id').references(() => stockBatches.id),
+ productName: text('product_name').notNull(),
+ brand: text('brand'),
+ strips: integer('strips').notNull().default(0),
+ loose: integer('loose').notNull().default(0),
+ qty: integer('qty').notNull(),
+ originalPrice: real('original_price').notNull(),
+ sellingPrice: real('selling_price').notNull(),
+ total: real('total').notNull(),
+ enterpriseId: integer('enterprise_id').notNull().references(() => enterprises.id),
+})
diff --git a/packages/data-manager-sqlite/src/schema/bills.ts b/packages/data-manager-sqlite/src/schema/bills.ts
new file mode 100644
index 0000000..0f2a69c
--- /dev/null
+++ b/packages/data-manager-sqlite/src/schema/bills.ts
@@ -0,0 +1,19 @@
+import { integer, sqliteTable, text, real } from 'drizzle-orm/sqlite-core'
+import { staff } from './staff'
+import { enterprises } from './enterprises'
+
+export const bills = sqliteTable('bills', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ billNo: text('bill_no').notNull(),
+ customerMobile: text('customer_mobile').notNull(),
+ customerName: text('customer_name'),
+ subtotal: real('subtotal').notNull(),
+ tax: real('tax').notNull(),
+ taxRate: real('tax_rate').notNull().default(18),
+ total: real('total').notNull(),
+ discount: real('discount').notNull().default(0),
+ discountPercent: integer('discount_percent').notNull().default(0),
+ generatedBy: integer('generated_by').notNull().references(() => staff.id),
+ createdAt: text('created_at').notNull(),
+ enterpriseId: integer('enterprise_id').notNull().references(() => enterprises.id),
+})
diff --git a/packages/data-manager-sqlite/src/schema/customers.ts b/packages/data-manager-sqlite/src/schema/customers.ts
new file mode 100644
index 0000000..4b08b1f
--- /dev/null
+++ b/packages/data-manager-sqlite/src/schema/customers.ts
@@ -0,0 +1,10 @@
+import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
+import { enterprises } from './enterprises'
+
+export const customers = sqliteTable('customers', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ mobile: text('mobile').notNull().unique(),
+ name: text('name'),
+ addedOn: text('added_on').notNull(),
+ enterpriseId: integer('enterprise_id').notNull().references(() => enterprises.id),
+})
diff --git a/packages/data-manager-sqlite/src/schema/index.ts b/packages/data-manager-sqlite/src/schema/index.ts
index e5b409b..5bf5520 100644
--- a/packages/data-manager-sqlite/src/schema/index.ts
+++ b/packages/data-manager-sqlite/src/schema/index.ts
@@ -12,3 +12,6 @@ export * from './permissions'
export * from './rolePermissions'
export * from './staffRoles'
export * from './enterpriseStaff'
+export * from './customers'
+export * from './bills'
+export * from './billItems'
diff --git a/packages/shared-react/src/hooks/billing.ts b/packages/shared-react/src/hooks/billing.ts
new file mode 100644
index 0000000..c78ee8c
--- /dev/null
+++ b/packages/shared-react/src/hooks/billing.ts
@@ -0,0 +1,13 @@
+import { trpc } from "../trpc";
+
+export function useListBills() {
+ return trpc.billing.list.useQuery();
+}
+
+export function useGetBillById(id: number) {
+ return trpc.billing.byId.useQuery({ id });
+}
+
+export function useCreateBill() {
+ return trpc.billing.create.useMutation();
+}
diff --git a/packages/shared-react/src/hooks/customers.ts b/packages/shared-react/src/hooks/customers.ts
new file mode 100644
index 0000000..faf719e
--- /dev/null
+++ b/packages/shared-react/src/hooks/customers.ts
@@ -0,0 +1,25 @@
+import { trpc } from "../trpc";
+
+export function useListCustomers() {
+ return trpc.customers.list.useQuery();
+}
+
+export function useSearchCustomers(query: string) {
+ return trpc.customers.search.useQuery({ query }, { enabled: query.length > 0 });
+}
+
+export function useGetCustomerById(id: number) {
+ return trpc.customers.byId.useQuery({ id });
+}
+
+export function useCreateCustomer() {
+ return trpc.customers.create.useMutation();
+}
+
+export function useUpdateCustomer() {
+ return trpc.customers.update.useMutation();
+}
+
+export function useRemoveCustomer() {
+ return trpc.customers.remove.useMutation();
+}
diff --git a/packages/shared-react/src/index.ts b/packages/shared-react/src/index.ts
index a8d0c32..7140826 100644
--- a/packages/shared-react/src/index.ts
+++ b/packages/shared-react/src/index.ts
@@ -9,6 +9,8 @@ export * from './hooks/units'
export * from './hooks/stockBatches'
export * from './hooks/staffManagement'
export * from './hooks/roles'
+export * from './hooks/billing'
+export * from './hooks/customers'
export { trpc } from './trpc'
export { useAuthStore, useWhoAmI, useLogin } from './auth'
export type { AuthStaff, AuthEnterprise } from './auth'