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 +

+
+ + + + + + + + + + + + {bill.items.map((item) => ( + + + + + + + + ))} + +
ProductStripsLoosePriceTotal
+
{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 && ( +
+ + Add New Customer +
+ )} +
+ )} +
+ {customerMobile && ( + + {customerName || customerMobile} + + )} +
+
+ + {/* Items */} +
+

+ + Items +

+
+ + + + + + + + + + + + + {items.map((item, idx) => ( + + + + + + + + + + ))} + +
ProductBatchStripsLoosePriceTotal +
+ {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}

+
+
+ + +
+
+
+
+

Contact

+ +
+
+

Details

+ +
+
+
+ ); +} 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 ( +
+ +
+

{isEditing ? "Edit Customer" : "New Customer"}

+

{isEditing ? "Update customer details." : "Register a new customer."}

+
+
+ + + {errors.mobile &&

{errors.mobile.message}

} +
+
+ + v || null })} placeholder="John Doe" /> +
+
+ + + {errors.added_on &&

{errors.added_on.message}

} +
+
+
+ Cancel + +
+ {mutation.error &&

Failed to {isEditing ? "update" : "create"} customer.

} +
+
+ ); +} 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'