stock screens functional
This commit is contained in:
parent
15cf205bd7
commit
7c8f847e5a
27 changed files with 2102 additions and 37 deletions
|
|
@ -4,11 +4,13 @@ import {
|
||||||
createProductsRepo,
|
createProductsRepo,
|
||||||
createDrugInfoRepo,
|
createDrugInfoRepo,
|
||||||
createUnitsRepo,
|
createUnitsRepo,
|
||||||
|
createStockBatchesRepo,
|
||||||
type StorageSpacesRepo,
|
type StorageSpacesRepo,
|
||||||
type DistributorsRepo,
|
type DistributorsRepo,
|
||||||
type ProductsRepo,
|
type ProductsRepo,
|
||||||
type DrugInfoRepo,
|
type DrugInfoRepo,
|
||||||
type UnitsRepo,
|
type UnitsRepo,
|
||||||
|
type StockBatchesRepo,
|
||||||
} from "data-manager-sqlite";
|
} from "data-manager-sqlite";
|
||||||
|
|
||||||
export class DataManager {
|
export class DataManager {
|
||||||
|
|
@ -17,6 +19,7 @@ export class DataManager {
|
||||||
readonly products: ProductsRepo;
|
readonly products: ProductsRepo;
|
||||||
readonly drugInfo: DrugInfoRepo;
|
readonly drugInfo: DrugInfoRepo;
|
||||||
readonly units: UnitsRepo;
|
readonly units: UnitsRepo;
|
||||||
|
readonly stockBatches: StockBatchesRepo;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const { repo: storageSpacesRepo } = createStorageSpacesRepo();
|
const { repo: storageSpacesRepo } = createStorageSpacesRepo();
|
||||||
|
|
@ -24,11 +27,13 @@ export class DataManager {
|
||||||
const { repo: productsRepo } = createProductsRepo();
|
const { repo: productsRepo } = createProductsRepo();
|
||||||
const { repo: drugInfoRepo } = createDrugInfoRepo();
|
const { repo: drugInfoRepo } = createDrugInfoRepo();
|
||||||
const { repo: unitsRepo } = createUnitsRepo();
|
const { repo: unitsRepo } = createUnitsRepo();
|
||||||
|
const { repo: stockBatchesRepo } = createStockBatchesRepo();
|
||||||
|
|
||||||
this.storageSpaces = storageSpacesRepo;
|
this.storageSpaces = storageSpacesRepo;
|
||||||
this.distributors = distributorsRepo;
|
this.distributors = distributorsRepo;
|
||||||
this.products = productsRepo;
|
this.products = productsRepo;
|
||||||
this.drugInfo = drugInfoRepo;
|
this.drugInfo = drugInfoRepo;
|
||||||
this.units = unitsRepo;
|
this.units = unitsRepo;
|
||||||
|
this.stockBatches = stockBatchesRepo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
apps/backend/src/trpc/pharmanager/v1/stock.ts
Normal file
66
apps/backend/src/trpc/pharmanager/v1/stock.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
import { t } from "../../init";
|
||||||
|
import { dataManager } from "../../../lib/data-manager-instance";
|
||||||
|
|
||||||
|
export const StockBatchSchema = z.object({
|
||||||
|
id: z.number().int(),
|
||||||
|
product: z.object({ id: z.number().int(), name: z.string(), brand: z.string() }),
|
||||||
|
arrived: z.string(),
|
||||||
|
batch_no: z.string(),
|
||||||
|
mfg: z.string(),
|
||||||
|
expiry: z.string(),
|
||||||
|
rack: z.object({ id: z.number().int(), name: z.string() }).nullable(),
|
||||||
|
distributor: z.object({ id: z.number().int(), agency: z.string() }).nullable(),
|
||||||
|
is_default: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { shape } = StockBatchSchema;
|
||||||
|
|
||||||
|
export const CreateStockBatchInput = z.object({
|
||||||
|
product_id: shape.product.shape.id,
|
||||||
|
arrived: shape.arrived.min(1),
|
||||||
|
batch_no: shape.batch_no.min(1),
|
||||||
|
mfg: shape.mfg.min(1),
|
||||||
|
expiry: shape.expiry.min(1),
|
||||||
|
rack_id: z.number().int().nullable().optional(),
|
||||||
|
distributor_id: z.number().int().nullable().optional(),
|
||||||
|
is_default: shape.is_default.default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateStockBatchInput = z
|
||||||
|
.object({ id: z.number().int() })
|
||||||
|
.merge(CreateStockBatchInput.partial());
|
||||||
|
|
||||||
|
export type StockBatch = z.infer<typeof StockBatchSchema>;
|
||||||
|
|
||||||
|
export const stockRouter = t.router({
|
||||||
|
list: t.procedure
|
||||||
|
.output(z.array(StockBatchSchema))
|
||||||
|
.query(() => dataManager.stockBatches.getStockBatches()),
|
||||||
|
|
||||||
|
byId: t.procedure
|
||||||
|
.input(z.object({ id: z.number().int() }))
|
||||||
|
.output(StockBatchSchema.nullable())
|
||||||
|
.query(({ input }) => dataManager.stockBatches.getStockBatchById(input.id)),
|
||||||
|
|
||||||
|
create: t.procedure
|
||||||
|
.input(CreateStockBatchInput)
|
||||||
|
.output(StockBatchSchema)
|
||||||
|
.mutation(({ input }) => dataManager.stockBatches.createStockBatch(input)),
|
||||||
|
|
||||||
|
update: t.procedure
|
||||||
|
.input(UpdateStockBatchInput)
|
||||||
|
.output(StockBatchSchema.nullable())
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
const { id, ...patch } = input;
|
||||||
|
return dataManager.stockBatches.updateStockBatch(id, patch);
|
||||||
|
}),
|
||||||
|
|
||||||
|
remove: t.procedure
|
||||||
|
.input(z.object({ id: z.number().int() }))
|
||||||
|
.output(z.object({ ok: z.boolean() }))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const ok = await dataManager.stockBatches.deleteStockBatch(input.id);
|
||||||
|
return { ok };
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
@ -4,6 +4,7 @@ import { distributorRouter } from "./pharmanager/v1/distributor";
|
||||||
import { productRouter } from "./pharmanager/v1/product";
|
import { productRouter } from "./pharmanager/v1/product";
|
||||||
import { drugInfoRouter } from "./pharmanager/v1/drugInfo";
|
import { drugInfoRouter } from "./pharmanager/v1/drugInfo";
|
||||||
import { unitsRouter } from "./pharmanager/v1/units";
|
import { unitsRouter } from "./pharmanager/v1/units";
|
||||||
|
import { stockRouter } from "./pharmanager/v1/stock";
|
||||||
|
|
||||||
export const appRouter = t.router({
|
export const appRouter = t.router({
|
||||||
storage: storageRouter,
|
storage: storageRouter,
|
||||||
|
|
@ -11,6 +12,7 @@ export const appRouter = t.router({
|
||||||
product: productRouter,
|
product: productRouter,
|
||||||
drugInfo: drugInfoRouter,
|
drugInfo: drugInfoRouter,
|
||||||
units: unitsRouter,
|
units: unitsRouter,
|
||||||
|
stock: stockRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,13 @@ import { Route as CustomersRouteImport } from './routes/customers'
|
||||||
import { Route as BillingRouteImport } from './routes/billing'
|
import { Route as BillingRouteImport } from './routes/billing'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as StorageIndexRouteImport } from './routes/storage/index'
|
import { Route as StorageIndexRouteImport } from './routes/storage/index'
|
||||||
|
import { Route as StockIndexRouteImport } from './routes/stock/index'
|
||||||
import { Route as ProductsIndexRouteImport } from './routes/products/index'
|
import { Route as ProductsIndexRouteImport } from './routes/products/index'
|
||||||
import { Route as DistributorsIndexRouteImport } from './routes/distributors/index'
|
import { Route as DistributorsIndexRouteImport } from './routes/distributors/index'
|
||||||
import { Route as StorageAddRouteImport } from './routes/storage/add'
|
import { Route as StorageAddRouteImport } from './routes/storage/add'
|
||||||
import { Route as StorageIdRouteImport } from './routes/storage/$id'
|
import { Route as StorageIdRouteImport } from './routes/storage/$id'
|
||||||
|
import { Route as StockAddRouteImport } from './routes/stock/add'
|
||||||
|
import { Route as StockIdRouteImport } from './routes/stock/$id'
|
||||||
import { Route as ProductsAddRouteImport } from './routes/products/add'
|
import { Route as ProductsAddRouteImport } from './routes/products/add'
|
||||||
import { Route as ProductsIdRouteImport } from './routes/products/$id'
|
import { Route as ProductsIdRouteImport } from './routes/products/$id'
|
||||||
import { Route as DistributorsAddRouteImport } from './routes/distributors/add'
|
import { Route as DistributorsAddRouteImport } from './routes/distributors/add'
|
||||||
|
|
@ -78,6 +81,11 @@ const StorageIndexRoute = StorageIndexRouteImport.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => StorageRoute,
|
getParentRoute: () => StorageRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const StockIndexRoute = StockIndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => StockRoute,
|
||||||
|
} as any)
|
||||||
const ProductsIndexRoute = ProductsIndexRouteImport.update({
|
const ProductsIndexRoute = ProductsIndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|
@ -98,6 +106,16 @@ const StorageIdRoute = StorageIdRouteImport.update({
|
||||||
path: '/$id',
|
path: '/$id',
|
||||||
getParentRoute: () => StorageRoute,
|
getParentRoute: () => StorageRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const StockAddRoute = StockAddRouteImport.update({
|
||||||
|
id: '/add',
|
||||||
|
path: '/add',
|
||||||
|
getParentRoute: () => StockRoute,
|
||||||
|
} as any)
|
||||||
|
const StockIdRoute = StockIdRouteImport.update({
|
||||||
|
id: '/$id',
|
||||||
|
path: '/$id',
|
||||||
|
getParentRoute: () => StockRoute,
|
||||||
|
} as any)
|
||||||
const ProductsAddRoute = ProductsAddRouteImport.update({
|
const ProductsAddRoute = ProductsAddRouteImport.update({
|
||||||
id: '/add',
|
id: '/add',
|
||||||
path: '/add',
|
path: '/add',
|
||||||
|
|
@ -127,16 +145,19 @@ export interface FileRoutesByFullPath {
|
||||||
'/products': typeof ProductsRouteWithChildren
|
'/products': typeof ProductsRouteWithChildren
|
||||||
'/profile': typeof ProfileRoute
|
'/profile': typeof ProfileRoute
|
||||||
'/staff': typeof StaffRoute
|
'/staff': typeof StaffRoute
|
||||||
'/stock': typeof StockRoute
|
'/stock': typeof StockRouteWithChildren
|
||||||
'/storage': typeof StorageRouteWithChildren
|
'/storage': typeof StorageRouteWithChildren
|
||||||
'/distributors/$id': typeof DistributorsIdRoute
|
'/distributors/$id': typeof DistributorsIdRoute
|
||||||
'/distributors/add': typeof DistributorsAddRoute
|
'/distributors/add': typeof DistributorsAddRoute
|
||||||
'/products/$id': typeof ProductsIdRoute
|
'/products/$id': typeof ProductsIdRoute
|
||||||
'/products/add': typeof ProductsAddRoute
|
'/products/add': typeof ProductsAddRoute
|
||||||
|
'/stock/$id': typeof StockIdRoute
|
||||||
|
'/stock/add': typeof StockAddRoute
|
||||||
'/storage/$id': typeof StorageIdRoute
|
'/storage/$id': typeof StorageIdRoute
|
||||||
'/storage/add': typeof StorageAddRoute
|
'/storage/add': typeof StorageAddRoute
|
||||||
'/distributors/': typeof DistributorsIndexRoute
|
'/distributors/': typeof DistributorsIndexRoute
|
||||||
'/products/': typeof ProductsIndexRoute
|
'/products/': typeof ProductsIndexRoute
|
||||||
|
'/stock/': typeof StockIndexRoute
|
||||||
'/storage/': typeof StorageIndexRoute
|
'/storage/': typeof StorageIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
|
|
@ -145,15 +166,17 @@ export interface FileRoutesByTo {
|
||||||
'/customers': typeof CustomersRoute
|
'/customers': typeof CustomersRoute
|
||||||
'/profile': typeof ProfileRoute
|
'/profile': typeof ProfileRoute
|
||||||
'/staff': typeof StaffRoute
|
'/staff': typeof StaffRoute
|
||||||
'/stock': typeof StockRoute
|
|
||||||
'/distributors/$id': typeof DistributorsIdRoute
|
'/distributors/$id': typeof DistributorsIdRoute
|
||||||
'/distributors/add': typeof DistributorsAddRoute
|
'/distributors/add': typeof DistributorsAddRoute
|
||||||
'/products/$id': typeof ProductsIdRoute
|
'/products/$id': typeof ProductsIdRoute
|
||||||
'/products/add': typeof ProductsAddRoute
|
'/products/add': typeof ProductsAddRoute
|
||||||
|
'/stock/$id': typeof StockIdRoute
|
||||||
|
'/stock/add': typeof StockAddRoute
|
||||||
'/storage/$id': typeof StorageIdRoute
|
'/storage/$id': typeof StorageIdRoute
|
||||||
'/storage/add': typeof StorageAddRoute
|
'/storage/add': typeof StorageAddRoute
|
||||||
'/distributors': typeof DistributorsIndexRoute
|
'/distributors': typeof DistributorsIndexRoute
|
||||||
'/products': typeof ProductsIndexRoute
|
'/products': typeof ProductsIndexRoute
|
||||||
|
'/stock': typeof StockIndexRoute
|
||||||
'/storage': typeof StorageIndexRoute
|
'/storage': typeof StorageIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
|
|
@ -165,16 +188,19 @@ export interface FileRoutesById {
|
||||||
'/products': typeof ProductsRouteWithChildren
|
'/products': typeof ProductsRouteWithChildren
|
||||||
'/profile': typeof ProfileRoute
|
'/profile': typeof ProfileRoute
|
||||||
'/staff': typeof StaffRoute
|
'/staff': typeof StaffRoute
|
||||||
'/stock': typeof StockRoute
|
'/stock': typeof StockRouteWithChildren
|
||||||
'/storage': typeof StorageRouteWithChildren
|
'/storage': typeof StorageRouteWithChildren
|
||||||
'/distributors/$id': typeof DistributorsIdRoute
|
'/distributors/$id': typeof DistributorsIdRoute
|
||||||
'/distributors/add': typeof DistributorsAddRoute
|
'/distributors/add': typeof DistributorsAddRoute
|
||||||
'/products/$id': typeof ProductsIdRoute
|
'/products/$id': typeof ProductsIdRoute
|
||||||
'/products/add': typeof ProductsAddRoute
|
'/products/add': typeof ProductsAddRoute
|
||||||
|
'/stock/$id': typeof StockIdRoute
|
||||||
|
'/stock/add': typeof StockAddRoute
|
||||||
'/storage/$id': typeof StorageIdRoute
|
'/storage/$id': typeof StorageIdRoute
|
||||||
'/storage/add': typeof StorageAddRoute
|
'/storage/add': typeof StorageAddRoute
|
||||||
'/distributors/': typeof DistributorsIndexRoute
|
'/distributors/': typeof DistributorsIndexRoute
|
||||||
'/products/': typeof ProductsIndexRoute
|
'/products/': typeof ProductsIndexRoute
|
||||||
|
'/stock/': typeof StockIndexRoute
|
||||||
'/storage/': typeof StorageIndexRoute
|
'/storage/': typeof StorageIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
|
|
@ -193,10 +219,13 @@ export interface FileRouteTypes {
|
||||||
| '/distributors/add'
|
| '/distributors/add'
|
||||||
| '/products/$id'
|
| '/products/$id'
|
||||||
| '/products/add'
|
| '/products/add'
|
||||||
|
| '/stock/$id'
|
||||||
|
| '/stock/add'
|
||||||
| '/storage/$id'
|
| '/storage/$id'
|
||||||
| '/storage/add'
|
| '/storage/add'
|
||||||
| '/distributors/'
|
| '/distributors/'
|
||||||
| '/products/'
|
| '/products/'
|
||||||
|
| '/stock/'
|
||||||
| '/storage/'
|
| '/storage/'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
|
|
@ -205,15 +234,17 @@ export interface FileRouteTypes {
|
||||||
| '/customers'
|
| '/customers'
|
||||||
| '/profile'
|
| '/profile'
|
||||||
| '/staff'
|
| '/staff'
|
||||||
| '/stock'
|
|
||||||
| '/distributors/$id'
|
| '/distributors/$id'
|
||||||
| '/distributors/add'
|
| '/distributors/add'
|
||||||
| '/products/$id'
|
| '/products/$id'
|
||||||
| '/products/add'
|
| '/products/add'
|
||||||
|
| '/stock/$id'
|
||||||
|
| '/stock/add'
|
||||||
| '/storage/$id'
|
| '/storage/$id'
|
||||||
| '/storage/add'
|
| '/storage/add'
|
||||||
| '/distributors'
|
| '/distributors'
|
||||||
| '/products'
|
| '/products'
|
||||||
|
| '/stock'
|
||||||
| '/storage'
|
| '/storage'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
|
|
@ -230,10 +261,13 @@ export interface FileRouteTypes {
|
||||||
| '/distributors/add'
|
| '/distributors/add'
|
||||||
| '/products/$id'
|
| '/products/$id'
|
||||||
| '/products/add'
|
| '/products/add'
|
||||||
|
| '/stock/$id'
|
||||||
|
| '/stock/add'
|
||||||
| '/storage/$id'
|
| '/storage/$id'
|
||||||
| '/storage/add'
|
| '/storage/add'
|
||||||
| '/distributors/'
|
| '/distributors/'
|
||||||
| '/products/'
|
| '/products/'
|
||||||
|
| '/stock/'
|
||||||
| '/storage/'
|
| '/storage/'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +279,7 @@ export interface RootRouteChildren {
|
||||||
ProductsRoute: typeof ProductsRouteWithChildren
|
ProductsRoute: typeof ProductsRouteWithChildren
|
||||||
ProfileRoute: typeof ProfileRoute
|
ProfileRoute: typeof ProfileRoute
|
||||||
StaffRoute: typeof StaffRoute
|
StaffRoute: typeof StaffRoute
|
||||||
StockRoute: typeof StockRoute
|
StockRoute: typeof StockRouteWithChildren
|
||||||
StorageRoute: typeof StorageRouteWithChildren
|
StorageRoute: typeof StorageRouteWithChildren
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,6 +355,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof StorageIndexRouteImport
|
preLoaderRoute: typeof StorageIndexRouteImport
|
||||||
parentRoute: typeof StorageRoute
|
parentRoute: typeof StorageRoute
|
||||||
}
|
}
|
||||||
|
'/stock/': {
|
||||||
|
id: '/stock/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/stock/'
|
||||||
|
preLoaderRoute: typeof StockIndexRouteImport
|
||||||
|
parentRoute: typeof StockRoute
|
||||||
|
}
|
||||||
'/products/': {
|
'/products/': {
|
||||||
id: '/products/'
|
id: '/products/'
|
||||||
path: '/'
|
path: '/'
|
||||||
|
|
@ -349,6 +390,20 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof StorageIdRouteImport
|
preLoaderRoute: typeof StorageIdRouteImport
|
||||||
parentRoute: typeof StorageRoute
|
parentRoute: typeof StorageRoute
|
||||||
}
|
}
|
||||||
|
'/stock/add': {
|
||||||
|
id: '/stock/add'
|
||||||
|
path: '/add'
|
||||||
|
fullPath: '/stock/add'
|
||||||
|
preLoaderRoute: typeof StockAddRouteImport
|
||||||
|
parentRoute: typeof StockRoute
|
||||||
|
}
|
||||||
|
'/stock/$id': {
|
||||||
|
id: '/stock/$id'
|
||||||
|
path: '/$id'
|
||||||
|
fullPath: '/stock/$id'
|
||||||
|
preLoaderRoute: typeof StockIdRouteImport
|
||||||
|
parentRoute: typeof StockRoute
|
||||||
|
}
|
||||||
'/products/add': {
|
'/products/add': {
|
||||||
id: '/products/add'
|
id: '/products/add'
|
||||||
path: '/add'
|
path: '/add'
|
||||||
|
|
@ -412,6 +467,20 @@ const ProductsRouteWithChildren = ProductsRoute._addFileChildren(
|
||||||
ProductsRouteChildren,
|
ProductsRouteChildren,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
interface StockRouteChildren {
|
||||||
|
StockIdRoute: typeof StockIdRoute
|
||||||
|
StockAddRoute: typeof StockAddRoute
|
||||||
|
StockIndexRoute: typeof StockIndexRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const StockRouteChildren: StockRouteChildren = {
|
||||||
|
StockIdRoute: StockIdRoute,
|
||||||
|
StockAddRoute: StockAddRoute,
|
||||||
|
StockIndexRoute: StockIndexRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const StockRouteWithChildren = StockRoute._addFileChildren(StockRouteChildren)
|
||||||
|
|
||||||
interface StorageRouteChildren {
|
interface StorageRouteChildren {
|
||||||
StorageIdRoute: typeof StorageIdRoute
|
StorageIdRoute: typeof StorageIdRoute
|
||||||
StorageAddRoute: typeof StorageAddRoute
|
StorageAddRoute: typeof StorageAddRoute
|
||||||
|
|
@ -435,7 +504,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||||
ProductsRoute: ProductsRouteWithChildren,
|
ProductsRoute: ProductsRouteWithChildren,
|
||||||
ProfileRoute: ProfileRoute,
|
ProfileRoute: ProfileRoute,
|
||||||
StaffRoute: StaffRoute,
|
StaffRoute: StaffRoute,
|
||||||
StockRoute: StockRoute,
|
StockRoute: StockRouteWithChildren,
|
||||||
StorageRoute: StorageRouteWithChildren,
|
StorageRoute: StorageRouteWithChildren,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,9 @@ function ProductDetailsPage() {
|
||||||
const marginVal = (product.selling_price - product.procured_price).toFixed(2);
|
const marginVal = (product.selling_price - product.procured_price).toFixed(2);
|
||||||
|
|
||||||
const stockClass =
|
const stockClass =
|
||||||
product.quantity <= product.reorder_level
|
product.size <= product.reorder_level
|
||||||
? "text-red-600 font-semibold"
|
? "text-red-600 font-semibold"
|
||||||
: product.quantity <= product.reorder_level * 2
|
: product.size <= product.reorder_level * 2
|
||||||
? "text-amber-600"
|
? "text-amber-600"
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
|
@ -96,7 +96,7 @@ function ProductDetailsPage() {
|
||||||
<span className="text-xs font-semibold text-slate-600 uppercase tracking-wider">Inventory</span>
|
<span className="text-xs font-semibold text-slate-600 uppercase tracking-wider">Inventory</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-5">
|
<div className="p-5">
|
||||||
<DetailRow label="Current Stock" value={`${product.quantity} units`} valueClass={stockClass} />
|
<DetailRow label="Current Stock" value={`${product.size} units`} valueClass={stockClass} />
|
||||||
<DetailRow label="Reorder Level" value={`${product.reorder_level} units`} last />
|
<DetailRow label="Reorder Level" value={`${product.reorder_level} units`} last />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ const formSchema = CreateProductInput.extend({
|
||||||
procured_price: z.coerce.number().min(0, "Must be ≥ 0"),
|
procured_price: z.coerce.number().min(0, "Must be ≥ 0"),
|
||||||
mrp: z.coerce.number().min(0, "Must be ≥ 0"),
|
mrp: z.coerce.number().min(0, "Must be ≥ 0"),
|
||||||
selling_price: z.coerce.number().min(0, "Must be ≥ 0"),
|
selling_price: z.coerce.number().min(0, "Must be ≥ 0"),
|
||||||
quantity: z.coerce.number().int().default(0),
|
size: z.coerce.number().int().default(0),
|
||||||
reorder_level: z.coerce.number().int().default(0),
|
reorder_level: z.coerce.number().int().default(0),
|
||||||
units_per_strip: z.coerce.number().int().nullable().optional(),
|
units_per_strip: z.coerce.number().int().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
@ -85,7 +85,7 @@ function AddProductPage() {
|
||||||
procured_price: 0,
|
procured_price: 0,
|
||||||
mrp: 0,
|
mrp: 0,
|
||||||
selling_price: 0,
|
selling_price: 0,
|
||||||
quantity: 0,
|
size: 0,
|
||||||
reorder_level: 0,
|
reorder_level: 0,
|
||||||
units_per_strip: null,
|
units_per_strip: null,
|
||||||
hide_product_from_public: false,
|
hide_product_from_public: false,
|
||||||
|
|
@ -120,7 +120,7 @@ function AddProductPage() {
|
||||||
procured_price: existingProduct.procured_price,
|
procured_price: existingProduct.procured_price,
|
||||||
mrp: existingProduct.mrp,
|
mrp: existingProduct.mrp,
|
||||||
selling_price: existingProduct.selling_price,
|
selling_price: existingProduct.selling_price,
|
||||||
quantity: existingProduct.quantity,
|
size: existingProduct.size,
|
||||||
reorder_level: existingProduct.reorder_level,
|
reorder_level: existingProduct.reorder_level,
|
||||||
units_per_strip: existingProduct.units_per_strip,
|
units_per_strip: existingProduct.units_per_strip,
|
||||||
hide_product_from_public: existingProduct.hide_product_from_public,
|
hide_product_from_public: existingProduct.hide_product_from_public,
|
||||||
|
|
@ -227,9 +227,9 @@ function AddProductPage() {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||||
Quantity <span className="text-red-600">*</span>
|
Size <span className="text-red-600">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input type="number" {...register("quantity")} placeholder="0" />
|
<Input type="number" {...register("size")} placeholder="0" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ interface ProductRow {
|
||||||
selling_price: number;
|
selling_price: number;
|
||||||
mrp: number;
|
mrp: number;
|
||||||
procured_price: number;
|
procured_price: number;
|
||||||
quantity: number;
|
size: number;
|
||||||
reorder_level: number;
|
reorder_level: number;
|
||||||
distributor: { id: number; name: string } | null;
|
distributor: { id: number; name: string } | null;
|
||||||
unit: { id: number; name: string };
|
unit: { id: number; name: string };
|
||||||
|
|
@ -76,18 +76,18 @@ function makeColumns(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "quantity",
|
id: "size",
|
||||||
header: "Quantity",
|
header: "Size",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const cls =
|
const cls =
|
||||||
row.quantity <= row.reorder_level
|
row.size <= row.reorder_level
|
||||||
? "text-red-600"
|
? "text-red-600"
|
||||||
: row.quantity <= row.reorder_level * 2
|
: row.size <= row.reorder_level * 2
|
||||||
? "text-amber-600"
|
? "text-amber-600"
|
||||||
: "text-emerald-600";
|
: "text-emerald-600";
|
||||||
return (
|
return (
|
||||||
<span className={`font-semibold text-[13px] ${cls} whitespace-nowrap`}>
|
<span className={`font-semibold text-[13px] ${cls} whitespace-nowrap`}>
|
||||||
{row.quantity} {row.unit.name}
|
{row.size} {row.unit.name}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createFileRoute("/stock")({
|
export const Route = createFileRoute("/stock")({
|
||||||
component: StockPage,
|
component: () => <Outlet />,
|
||||||
staticData: {
|
|
||||||
title: "Stock",
|
|
||||||
subtitle: "Inventory levels, low-stock alerts, purchase orders",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function StockPage() {
|
|
||||||
return <div>Stock</div>;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
157
apps/pharmanager/src/routes/stock/$id.tsx
Normal file
157
apps/pharmanager/src/routes/stock/$id.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
|
import { ArrowLeft, Pencil, Trash2, Package, Calendar, MapPin, Truck } from "lucide-react";
|
||||||
|
import { Button } from "#/components/ui";
|
||||||
|
import { useGetStockBatchById, useRemoveStockBatch, trpc } from "shared-react";
|
||||||
|
|
||||||
|
function daysUntil(expiry: string): number {
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
const exp = new Date(expiry + "T00:00:00");
|
||||||
|
return Math.ceil((exp.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
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("/stock/$id")({
|
||||||
|
component: StockDetailsPage,
|
||||||
|
staticData: {
|
||||||
|
title: "Stock Batch Details",
|
||||||
|
subtitle: "Batch information",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function StockDetailsPage() {
|
||||||
|
const { id } = Route.useParams();
|
||||||
|
const batchId = Number(id);
|
||||||
|
const { data: batch, isLoading, error } = useGetStockBatchById(batchId);
|
||||||
|
const removeMutation = useRemoveStockBatch();
|
||||||
|
const utils = trpc.useUtils();
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
if (!batch) return;
|
||||||
|
if (!confirm(`Delete batch ${batch.batch_no}? This cannot be undone.`)) return;
|
||||||
|
removeMutation.mutate(
|
||||||
|
{ id: batch.id },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.stock.list.invalidate();
|
||||||
|
window.location.href = "/stock";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) return <div className="text-sm text-slate-600 py-8">Loading batch details...</div>;
|
||||||
|
if (error || !batch) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center py-16 px-6 text-slate-600">
|
||||||
|
<Package className="w-12 h-12 mb-4 opacity-40" />
|
||||||
|
<h3 className="text-base font-semibold text-slate-900 mb-1.5">Batch not found</h3>
|
||||||
|
<p className="text-sm mb-4">The batch you're looking for doesn't exist.</p>
|
||||||
|
<Link to="/stock" className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors">
|
||||||
|
Back to Stock
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = daysUntil(batch.expiry);
|
||||||
|
const dayCls = days < 0 ? "expired" : days <= 30 ? "critical" : days <= 90 ? "warning" : "healthy";
|
||||||
|
const dayLabel = days < 0 ? "Expired" : days <= 30 ? "Expiring soon" : days <= 90 ? "Approaching expiry" : "Healthy";
|
||||||
|
const dayBg = dayCls === "healthy" ? "bg-emerald-50 text-emerald-600" : dayCls === "warning" ? "bg-amber-50 text-amber-600" : "bg-red-50 text-red-600";
|
||||||
|
const dateCls = days < 0 ? "text-red-600 font-semibold" : days <= 30 ? "text-amber-600 font-semibold" : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Link to="/stock" className="inline-flex items-center gap-1.5 text-sm text-blue-600 hover:underline mb-5">
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
Back to Stock
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6 max-w-[800px]">
|
||||||
|
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
|
||||||
|
<div className="px-5 py-4 border-b border-slate-200 flex items-center gap-2">
|
||||||
|
<Package className="w-[14px] h-[14px] text-slate-600" />
|
||||||
|
<span className="text-xs font-semibold text-slate-600 uppercase tracking-wider">Product Information</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-5">
|
||||||
|
<DetailRow label="Product" value={batch.product.name} />
|
||||||
|
<DetailRow label="Brand" value={batch.product.brand} last />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
|
||||||
|
<div className="px-5 py-4 border-b border-slate-200 flex items-center gap-2 justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Calendar className="w-[14px] h-[14px] text-slate-600" />
|
||||||
|
<span className="text-xs font-semibold text-slate-600 uppercase tracking-wider">Batch Timeline</span>
|
||||||
|
</div>
|
||||||
|
{batch.is_default && (
|
||||||
|
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-semibold bg-amber-50 text-amber-600">
|
||||||
|
★ Default
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="p-5">
|
||||||
|
<DetailRow label="Arrived Date" value={fmtDate(batch.arrived)} />
|
||||||
|
<DetailRow label="Batch No" value={batch.batch_no} valueClass="font-mono text-[13px]" />
|
||||||
|
<DetailRow label="Quantity" value={String(batch.quantity)} />
|
||||||
|
<DetailRow label="Manufacture Date" value={fmtDate(batch.mfg)} />
|
||||||
|
<DetailRow label="Expiry Date" value={fmtDate(batch.expiry)} valueClass={dateCls} />
|
||||||
|
<div className="flex justify-between items-center py-2.5 text-sm">
|
||||||
|
<span className="text-slate-600 text-[13px]">Shelf Life Status</span>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-[32px] font-bold leading-none">{Math.abs(days)}</div>
|
||||||
|
<div className="text-xs text-slate-500">{days < 0 ? "days past expiry" : "days remaining"}</div>
|
||||||
|
<span className={`inline-block mt-1 px-2.5 py-0.5 rounded-full text-[11px] font-semibold ${dayBg}`}>{dayLabel}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
|
||||||
|
<div className="px-5 py-4 border-b border-slate-200 flex items-center gap-2">
|
||||||
|
<MapPin className="w-[14px] h-[14px] text-slate-600" />
|
||||||
|
<span className="text-xs font-semibold text-slate-600 uppercase tracking-wider">Location</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-5">
|
||||||
|
<div className="text-base font-semibold">{batch.rack?.name || "— Not assigned"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
|
||||||
|
<div className="px-5 py-4 border-b border-slate-200 flex items-center gap-2">
|
||||||
|
<Truck className="w-[14px] h-[14px] text-slate-600" />
|
||||||
|
<span className="text-xs font-semibold text-slate-600 uppercase tracking-wider">Supplier</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-5">
|
||||||
|
<div className="text-base font-semibold">{batch.distributor?.agency || "— Not recorded"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2.5 py-5 mt-2 max-w-[800px]">
|
||||||
|
<Button variant="primary">
|
||||||
|
<Pencil className="w-[15px] h-[15px]" />
|
||||||
|
Edit Batch
|
||||||
|
</Button>
|
||||||
|
<Button variant="danger" onClick={handleDelete}>
|
||||||
|
<Trash2 className="w-[15px] h-[15px]" />
|
||||||
|
Delete Batch
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailRow({ label, value, valueClass = "", last = false }: { label: string; value: string; valueClass?: string; last?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div className={`flex justify-between items-center py-2.5 text-sm ${last ? "" : "border-b border-slate-200"}`}>
|
||||||
|
<span className="text-slate-600 text-[13px]">{label}</span>
|
||||||
|
<span className={`font-medium text-right ${valueClass}`}>{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
247
apps/pharmanager/src/routes/stock/add.tsx
Normal file
247
apps/pharmanager/src/routes/stock/add.tsx
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
|
||||||
|
import { ArrowLeft, Plus } from "lucide-react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
useCreateStockBatch,
|
||||||
|
useUpdateStockBatch,
|
||||||
|
useGetStockBatchById,
|
||||||
|
useListProducts,
|
||||||
|
useListStorage,
|
||||||
|
useListDistributors,
|
||||||
|
trpc,
|
||||||
|
} from "shared-react";
|
||||||
|
import { Button, Input, buttonVariants } from "#/components/ui";
|
||||||
|
import { CreateStockBatchInput } from "@repo/shared";
|
||||||
|
|
||||||
|
const formSchema = CreateStockBatchInput.extend({
|
||||||
|
product_id: z.coerce.number().int().min(1, "Select a product"),
|
||||||
|
rack_id: z.coerce.number().int().nullable().optional(),
|
||||||
|
distributor_id: z.coerce.number().int().nullable().optional(),
|
||||||
|
quantity: z.coerce.number().int().min(1, "Quantity is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/stock/add")({
|
||||||
|
component: AddStockPage,
|
||||||
|
validateSearch: (search: Record<string, unknown>) => ({
|
||||||
|
id: search.id ? Number(search.id) : undefined,
|
||||||
|
}),
|
||||||
|
staticData: {
|
||||||
|
title: "Add Stock Batch",
|
||||||
|
subtitle: "Register a new inventory batch",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function AddStockPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { id: editId } = Route.useSearch();
|
||||||
|
const createMutation = useCreateStockBatch();
|
||||||
|
const updateMutation = useUpdateStockBatch();
|
||||||
|
const { data: existingBatch } = useGetStockBatchById(editId ?? 0);
|
||||||
|
const utils = trpc.useUtils();
|
||||||
|
const { data: products } = useListProducts();
|
||||||
|
const { data: racks } = useListStorage();
|
||||||
|
const { data: distributorList } = useListDistributors();
|
||||||
|
|
||||||
|
const isEditing = typeof editId === "number" && editId > 0;
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
product_id: 0,
|
||||||
|
arrived: "",
|
||||||
|
batch_no: "",
|
||||||
|
mfg: "",
|
||||||
|
expiry: "",
|
||||||
|
rack_id: null,
|
||||||
|
distributor_id: null,
|
||||||
|
quantity: 0,
|
||||||
|
is_default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEditing && existingBatch) {
|
||||||
|
reset({
|
||||||
|
product_id: existingBatch.product.id,
|
||||||
|
arrived: existingBatch.arrived,
|
||||||
|
batch_no: existingBatch.batch_no,
|
||||||
|
mfg: existingBatch.mfg,
|
||||||
|
expiry: existingBatch.expiry,
|
||||||
|
rack_id: existingBatch.rack?.id ?? null,
|
||||||
|
distributor_id: existingBatch.distributor?.id ?? null,
|
||||||
|
quantity: existingBatch.quantity,
|
||||||
|
is_default: existingBatch.is_default,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isEditing, existingBatch, reset]);
|
||||||
|
|
||||||
|
function onSubmit(values: FormValues) {
|
||||||
|
if (isEditing) {
|
||||||
|
updateMutation.mutate(
|
||||||
|
{ id: editId!, ...values },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.stock.list.invalidate();
|
||||||
|
utils.stock.byId.invalidate({ id: editId! });
|
||||||
|
navigate({ to: "/stock" });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
createMutation.mutate(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.stock.list.invalidate();
|
||||||
|
navigate({ to: "/stock" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutation = isEditing ? updateMutation : createMutation;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
to="/stock"
|
||||||
|
className="inline-flex items-center gap-1.5 text-sm text-blue-600 hover:underline mb-5"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
Back to Stock
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] p-8 max-w-2xl"
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-semibold mb-1">
|
||||||
|
{isEditing ? "Edit Stock Entry" : "New Stock Entry"}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-slate-600 mb-7">
|
||||||
|
{isEditing ? "Update batch details." : "Record a new batch with manufacture and expiry dates."}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-5">
|
||||||
|
<div className="col-span-full">
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||||
|
Product <span className="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
{...register("product_id")}
|
||||||
|
className={`w-full px-3.5 py-2.5 border rounded-md text-sm text-slate-900 bg-white focus:outline-none focus:ring-[3px] focus:ring-blue-100 ${errors.product_id ? "border-red-600" : "border-slate-200 focus:border-blue-600"}`}
|
||||||
|
>
|
||||||
|
<option value={0}>Search and select a product...</option>
|
||||||
|
{(products ?? []).map((p) => (
|
||||||
|
<option key={p.id} value={p.id}>
|
||||||
|
{p.name} — {p.brand}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{errors.product_id && <p className="text-sm text-red-600 mt-1">{errors.product_id.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||||
|
Arrived Date <span className="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<Input type="date" {...register("arrived")} variant={errors.arrived ? "error" : "default"} />
|
||||||
|
{errors.arrived && <p className="text-sm text-red-600 mt-1">{errors.arrived.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||||
|
Batch No <span className="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<Input {...register("batch_no")} variant={errors.batch_no ? "error" : "default"} placeholder="e.g. BCH-PCM-003" />
|
||||||
|
{errors.batch_no && <p className="text-sm text-red-600 mt-1">{errors.batch_no.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||||
|
Quantity <span className="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<Input type="number" {...register("quantity")} placeholder="0" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||||
|
Manufacture Date <span className="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<Input type="date" {...register("mfg")} variant={errors.mfg ? "error" : "default"} />
|
||||||
|
{errors.mfg && <p className="text-sm text-red-600 mt-1">{errors.mfg.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||||
|
Expiry Date <span className="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<Input type="date" {...register("expiry")} variant={errors.expiry ? "error" : "default"} />
|
||||||
|
{errors.expiry && <p className="text-sm text-red-600 mt-1">{errors.expiry.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">Rack</label>
|
||||||
|
<select
|
||||||
|
{...register("rack_id")}
|
||||||
|
className="w-full px-3.5 py-2.5 border border-slate-200 rounded-md text-sm text-slate-900 bg-white focus:outline-none focus:ring-[3px] focus:ring-blue-100 focus:border-blue-600"
|
||||||
|
>
|
||||||
|
<option value="">— Select a rack —</option>
|
||||||
|
{(racks ?? []).map((r) => (
|
||||||
|
<option key={r.id} value={r.id}>{r.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">Distributor</label>
|
||||||
|
<select
|
||||||
|
{...register("distributor_id")}
|
||||||
|
className="w-full px-3.5 py-2.5 border border-slate-200 rounded-md text-sm text-slate-900 bg-white focus:outline-none focus:ring-[3px] focus:ring-blue-100 focus:border-blue-600"
|
||||||
|
>
|
||||||
|
<option value="">— Select a distributor —</option>
|
||||||
|
{(distributorList ?? []).map((d) => (
|
||||||
|
<option key={d.id} value={d.id}>{d.agency}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-full">
|
||||||
|
<label className="flex items-center gap-2.5 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
{...register("is_default")}
|
||||||
|
className="w-[18px] h-[18px] accent-blue-600 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium text-slate-900">
|
||||||
|
Set as default batch — stock will be deducted from this batch when billing
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-full flex justify-end gap-3 pt-5 mt-2 border-t border-slate-200">
|
||||||
|
<Link to="/stock" className={buttonVariants({ variant: "outline" })}>
|
||||||
|
Cancel
|
||||||
|
</Link>
|
||||||
|
<Button type="submit" disabled={mutation.isPending}>
|
||||||
|
<Plus className="w-[15px] h-[15px]" />
|
||||||
|
{mutation.isPending ? "Saving..." : isEditing ? "Update Stock" : "Add to Stock"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mutation.error && (
|
||||||
|
<p className="text-sm text-red-600 mt-4">Failed to {isEditing ? "update" : "create"} batch. Please try again.</p>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
221
apps/pharmanager/src/routes/stock/index.tsx
Normal file
221
apps/pharmanager/src/routes/stock/index.tsx
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
import { useState, useMemo, useCallback } from "react";
|
||||||
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
|
import { Search, Plus, Pencil, Trash2, Package, Star } from "lucide-react";
|
||||||
|
import { GridTable } from "#/components/GridTable";
|
||||||
|
import type { GridTableColumn } from "#/components/GridTable";
|
||||||
|
import { Button, buttonVariants } from "#/components/ui";
|
||||||
|
import { useListStockBatches, useRemoveStockBatch, trpc } from "shared-react";
|
||||||
|
|
||||||
|
interface StockRow {
|
||||||
|
id: number;
|
||||||
|
product: { id: number; name: string; brand: string };
|
||||||
|
arrived: string;
|
||||||
|
batch_no: string;
|
||||||
|
mfg: string;
|
||||||
|
expiry: string;
|
||||||
|
rack: { id: number; name: string } | null;
|
||||||
|
distributor: { id: number; agency: string } | null;
|
||||||
|
quantity: number;
|
||||||
|
is_default: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function daysUntil(expiry: string): number {
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
const exp = new Date(expiry + "T00:00:00");
|
||||||
|
return Math.ceil((exp.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 daysBadge(days: number) {
|
||||||
|
if (days < 0) return <span className="inline-block px-2 py-0.5 rounded-full text-[11px] font-semibold bg-red-50 text-red-600 line-through">Expired</span>;
|
||||||
|
if (days <= 30) return <span className="inline-block px-2 py-0.5 rounded-full text-[11px] font-semibold bg-red-50 text-red-600">{days} days</span>;
|
||||||
|
if (days <= 90) return <span className="inline-block px-2 py-0.5 rounded-full text-[11px] font-semibold bg-amber-50 text-amber-600">{days} days</span>;
|
||||||
|
return <span className="inline-block px-2 py-0.5 rounded-full text-[11px] font-semibold bg-emerald-50 text-emerald-600">{days} days</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeColumns(
|
||||||
|
onDelete: (row: StockRow) => void,
|
||||||
|
): GridTableColumn<StockRow>[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "product",
|
||||||
|
header: "Product",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-[13px]">{row.product.name}</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-px">{row.product.brand}</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "arrived",
|
||||||
|
header: "Arrived Date",
|
||||||
|
cell: ({ row }) => <span className="text-[13px] whitespace-nowrap">{fmtDate(row.arrived)}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "batch_no",
|
||||||
|
header: "Batch No",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-xs font-mono whitespace-nowrap">{row.batch_no}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "quantity",
|
||||||
|
header: "Qty",
|
||||||
|
size: 60,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="font-semibold text-[13px]">{row.quantity}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "is_default",
|
||||||
|
header: "Default",
|
||||||
|
size: 60,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span title={row.is_default ? "Default batch" : ""}>
|
||||||
|
{row.is_default ? (
|
||||||
|
<Star className="w-4 h-4 text-amber-500 fill-amber-500" />
|
||||||
|
) : (
|
||||||
|
<Star className="w-4 h-4 text-slate-300" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mfg",
|
||||||
|
header: "Manufacture Date",
|
||||||
|
cell: ({ row }) => <span className="text-[13px] whitespace-nowrap">{fmtDate(row.mfg)}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "expiry",
|
||||||
|
header: "Expiry Date",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const days = daysUntil(row.expiry);
|
||||||
|
return (
|
||||||
|
<span className={`text-[13px] whitespace-nowrap ${days < 0 ? "text-red-600 font-medium" : days <= 30 ? "text-amber-600 font-medium" : ""}`}>
|
||||||
|
{fmtDate(row.expiry)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "days",
|
||||||
|
header: "Days Remaining",
|
||||||
|
cell: ({ row }) => daysBadge(daysUntil(row.expiry)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "rack",
|
||||||
|
header: "Rack",
|
||||||
|
cell: ({ row }) => <span className="text-[13px]">{row.rack?.name || "—"}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "distributor",
|
||||||
|
header: "Distributor",
|
||||||
|
cell: ({ row }) => <span className="text-[13px] text-slate-600">{row.distributor?.agency || "—"}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: "Actions",
|
||||||
|
size: 90,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
<Link
|
||||||
|
to="/stock/$id"
|
||||||
|
params={{ id: row.id.toString() }}
|
||||||
|
className="text-[10px] font-medium text-blue-600 hover:underline whitespace-nowrap"
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Link>
|
||||||
|
<Link to="/stock/add" search={{ id: row.id }}>
|
||||||
|
<Button variant="ghost-blue" size="icon" aria-label={`Edit ${row.batch_no}`} type="button">
|
||||||
|
<Pencil className="w-[15px] h-[15px]" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button variant="ghost-red" size="icon" aria-label={`Delete ${row.batch_no}`} onClick={() => onDelete(row)}>
|
||||||
|
<Trash2 className="w-[15px] h-[15px]" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/stock/")({
|
||||||
|
component: StockIndexPage,
|
||||||
|
staticData: {
|
||||||
|
title: "Stock Batches",
|
||||||
|
subtitle: "Track inventory batches by expiry and rack location",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function StockIndexPage() {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const { data: batches, isLoading, error } = useListStockBatches();
|
||||||
|
const removeMutation = useRemoveStockBatch();
|
||||||
|
const utils = trpc.useUtils();
|
||||||
|
|
||||||
|
const handleDelete = useCallback(
|
||||||
|
(row: StockRow) => {
|
||||||
|
if (!confirm(`Delete batch ${row.batch_no}?`)) return;
|
||||||
|
removeMutation.mutate(
|
||||||
|
{ id: row.id },
|
||||||
|
{ onSuccess: () => utils.stock.list.invalidate() },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[removeMutation, utils],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(() => makeColumns(handleDelete), [handleDelete]);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const q = searchQuery.toLowerCase().trim();
|
||||||
|
if (!q) return batches ?? [];
|
||||||
|
return (batches ?? []).filter((b) => {
|
||||||
|
const text = `${b.product.name} ${b.product.brand} ${b.batch_no} ${b.rack?.name || ""} ${b.distributor?.agency || ""}`;
|
||||||
|
return text.toLowerCase().includes(q);
|
||||||
|
});
|
||||||
|
}, [searchQuery, batches]);
|
||||||
|
|
||||||
|
if (isLoading) return <div className="text-sm text-slate-600 py-8">Loading stock batches...</div>;
|
||||||
|
if (error) return <div className="text-sm text-red-600 py-8">Failed to load stock batches.</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3 mb-5 flex-wrap">
|
||||||
|
<div className="flex items-center gap-2 flex-1 min-w-[200px] max-w-[480px] px-3.5 py-2 bg-white rounded-md border border-slate-200 transition-all duration-200 focus-within:border-blue-600 focus-within:shadow-[0_0_0_3px_rgba(37,99,235,0.1)]">
|
||||||
|
<Search className="w-4 h-4 text-slate-600 shrink-0" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Search by product, brand, batch, or rack..."
|
||||||
|
className="bg-transparent border-none outline-none text-sm text-slate-900 w-full placeholder:text-slate-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Link to="/stock/add" className={buttonVariants({ variant: "primary" })}>
|
||||||
|
<Plus className="w-[15px] h-[15px]" />
|
||||||
|
Add Stock
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GridTable
|
||||||
|
columns={columns}
|
||||||
|
data={filtered}
|
||||||
|
emptyState={
|
||||||
|
<div className="flex flex-col items-center py-16 px-6 text-slate-600">
|
||||||
|
<Package className="w-12 h-12 mb-4 opacity-40" />
|
||||||
|
<h3 className="text-base font-semibold text-slate-900 mb-1.5">No stock batches found</h3>
|
||||||
|
<p className="text-sm">
|
||||||
|
{searchQuery ? "No batches match your search." : "Add your first stock batch to get started."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
packages/data-manager-sqlite/drizzle/0003_useful_ultron.sql
Normal file
14
packages/data-manager-sqlite/drizzle/0003_useful_ultron.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
CREATE TABLE `stock_batches` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`product_id` integer NOT NULL,
|
||||||
|
`arrived` text NOT NULL,
|
||||||
|
`batch_no` text NOT NULL,
|
||||||
|
`mfg` text NOT NULL,
|
||||||
|
`expiry` text NOT NULL,
|
||||||
|
`rack_id` integer,
|
||||||
|
`distributor_id` integer,
|
||||||
|
`is_default` integer DEFAULT false NOT NULL,
|
||||||
|
FOREIGN KEY (`product_id`) REFERENCES `products`(`id`) ON UPDATE no action ON DELETE no action,
|
||||||
|
FOREIGN KEY (`rack_id`) REFERENCES `storage_spaces`(`id`) ON UPDATE no action ON DELETE no action,
|
||||||
|
FOREIGN KEY (`distributor_id`) REFERENCES `distributors`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE `products` RENAME COLUMN "quantity" TO "size";--> statement-breakpoint
|
||||||
|
ALTER TABLE `stock_batches` ADD `quantity` integer DEFAULT 0 NOT NULL;
|
||||||
512
packages/data-manager-sqlite/drizzle/meta/0003_snapshot.json
Normal file
512
packages/data-manager-sqlite/drizzle/meta/0003_snapshot.json
Normal file
|
|
@ -0,0 +1,512 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "7e0de346-d773-4526-85aa-96c7c9652ac9",
|
||||||
|
"prevId": "e65ed66a-0a3c-4338-8ba7-90ca587a7c07",
|
||||||
|
"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": "'[]'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"quantity": {
|
||||||
|
"name": "quantity",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"is_default": {
|
||||||
|
"name": "is_default",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
522
packages/data-manager-sqlite/drizzle/meta/0004_snapshot.json
Normal file
522
packages/data-manager-sqlite/drizzle/meta/0004_snapshot.json
Normal file
|
|
@ -0,0 +1,522 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "dfa18399-c3f0-4ef0-896b-632b38f67c1f",
|
||||||
|
"prevId": "7e0de346-d773-4526-85aa-96c7c9652ac9",
|
||||||
|
"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": "'[]'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {
|
||||||
|
"\"products\".\"quantity\"": "\"products\".\"size\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,20 @@
|
||||||
"when": 1779530472486,
|
"when": 1779530472486,
|
||||||
"tag": "0002_sour_praxagora",
|
"tag": "0002_sour_praxagora",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1779532150299,
|
||||||
|
"tag": "0003_useful_ultron",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1779533139096,
|
||||||
|
"tag": "0004_ambiguous_captain_america",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -34,3 +34,9 @@ export {
|
||||||
type Unit,
|
type Unit,
|
||||||
type UnitsRepo,
|
type UnitsRepo,
|
||||||
} from './units'
|
} from './units'
|
||||||
|
export {
|
||||||
|
createStockBatchesRepo,
|
||||||
|
type StockBatch,
|
||||||
|
type StockBatchesRepo,
|
||||||
|
} from './stockBatches'
|
||||||
|
export { stockBatches } from './schema/stockBatches'
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ interface ProductFields {
|
||||||
procured_price: number
|
procured_price: number
|
||||||
mrp: number
|
mrp: number
|
||||||
selling_price: number
|
selling_price: number
|
||||||
quantity: number
|
size: number
|
||||||
reorder_level: number
|
reorder_level: number
|
||||||
units_per_strip: number | null
|
units_per_strip: number | null
|
||||||
hide_product_from_public: boolean
|
hide_product_from_public: boolean
|
||||||
|
|
@ -92,7 +92,7 @@ function toProduct(
|
||||||
procured_price: row.procuredPrice,
|
procured_price: row.procuredPrice,
|
||||||
mrp: row.mrp,
|
mrp: row.mrp,
|
||||||
selling_price: row.sellingPrice,
|
selling_price: row.sellingPrice,
|
||||||
quantity: row.quantity,
|
size: row.size,
|
||||||
reorder_level: row.reorderLevel,
|
reorder_level: row.reorderLevel,
|
||||||
units_per_strip: row.unitsPerStrip,
|
units_per_strip: row.unitsPerStrip,
|
||||||
hide_product_from_public: row.hideProductFromPublic,
|
hide_product_from_public: row.hideProductFromPublic,
|
||||||
|
|
@ -191,7 +191,7 @@ export function createProductsRepo(): { repo: ProductsRepo } {
|
||||||
procuredPrice: input.procured_price,
|
procuredPrice: input.procured_price,
|
||||||
mrp: input.mrp,
|
mrp: input.mrp,
|
||||||
sellingPrice: input.selling_price,
|
sellingPrice: input.selling_price,
|
||||||
quantity: input.quantity,
|
size: input.size,
|
||||||
reorderLevel: input.reorder_level,
|
reorderLevel: input.reorder_level,
|
||||||
unitsPerStrip: input.units_per_strip ?? null,
|
unitsPerStrip: input.units_per_strip ?? null,
|
||||||
hideProductFromPublic: input.hide_product_from_public ?? false,
|
hideProductFromPublic: input.hide_product_from_public ?? false,
|
||||||
|
|
@ -229,7 +229,7 @@ export function createProductsRepo(): { repo: ProductsRepo } {
|
||||||
if (patch.procured_price !== undefined) setData.procuredPrice = patch.procured_price
|
if (patch.procured_price !== undefined) setData.procuredPrice = patch.procured_price
|
||||||
if (patch.mrp !== undefined) setData.mrp = patch.mrp
|
if (patch.mrp !== undefined) setData.mrp = patch.mrp
|
||||||
if (patch.selling_price !== undefined) setData.sellingPrice = patch.selling_price
|
if (patch.selling_price !== undefined) setData.sellingPrice = patch.selling_price
|
||||||
if (patch.quantity !== undefined) setData.quantity = patch.quantity
|
if (patch.size !== undefined) setData.size = patch.size
|
||||||
if (patch.reorder_level !== undefined) setData.reorderLevel = patch.reorder_level
|
if (patch.reorder_level !== undefined) setData.reorderLevel = patch.reorder_level
|
||||||
if (patch.units_per_strip !== undefined) setData.unitsPerStrip = patch.units_per_strip ?? null
|
if (patch.units_per_strip !== undefined) setData.unitsPerStrip = patch.units_per_strip ?? null
|
||||||
if (patch.hide_product_from_public !== undefined) setData.hideProductFromPublic = patch.hide_product_from_public
|
if (patch.hide_product_from_public !== undefined) setData.hideProductFromPublic = patch.hide_product_from_public
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,4 @@ export * from './products'
|
||||||
export * from './drugInfo'
|
export * from './drugInfo'
|
||||||
export * from './units'
|
export * from './units'
|
||||||
export * from './productCompositions'
|
export * from './productCompositions'
|
||||||
|
export * from './stockBatches'
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export const products = sqliteTable('products', {
|
||||||
procuredPrice: real('procured_price').notNull(),
|
procuredPrice: real('procured_price').notNull(),
|
||||||
mrp: real('mrp').notNull(),
|
mrp: real('mrp').notNull(),
|
||||||
sellingPrice: real('selling_price').notNull(),
|
sellingPrice: real('selling_price').notNull(),
|
||||||
quantity: integer('quantity').notNull().default(0),
|
size: integer('size').notNull().default(0),
|
||||||
reorderLevel: integer('reorder_level').notNull().default(0),
|
reorderLevel: integer('reorder_level').notNull().default(0),
|
||||||
unitsPerStrip: integer('units_per_strip'),
|
unitsPerStrip: integer('units_per_strip'),
|
||||||
hideProductFromPublic: integer('hide_product_from_public', { mode: 'boolean' }).notNull().default(false),
|
hideProductFromPublic: integer('hide_product_from_public', { mode: 'boolean' }).notNull().default(false),
|
||||||
|
|
|
||||||
17
packages/data-manager-sqlite/src/schema/stockBatches.ts
Normal file
17
packages/data-manager-sqlite/src/schema/stockBatches.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||||
|
import { products } from './products'
|
||||||
|
import { storageSpaces } from './storageSpacesSchema'
|
||||||
|
import { distributors } from './distributors'
|
||||||
|
|
||||||
|
export const stockBatches = sqliteTable('stock_batches', {
|
||||||
|
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||||
|
productId: integer('product_id').notNull().references(() => products.id),
|
||||||
|
arrived: text('arrived').notNull(),
|
||||||
|
batchNo: text('batch_no').notNull(),
|
||||||
|
mfg: text('mfg').notNull(),
|
||||||
|
expiry: text('expiry').notNull(),
|
||||||
|
rackId: integer('rack_id').references(() => storageSpaces.id),
|
||||||
|
distributorId: integer('distributor_id').references(() => distributors.id),
|
||||||
|
quantity: integer('quantity').notNull().default(0),
|
||||||
|
isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false),
|
||||||
|
})
|
||||||
161
packages/data-manager-sqlite/src/stockBatches.ts
Normal file
161
packages/data-manager-sqlite/src/stockBatches.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
|
||||||
|
import { db, sqlite } from './db-instance'
|
||||||
|
import { stockBatches } from './schema/stockBatches'
|
||||||
|
import { products } from './schema/products'
|
||||||
|
import { storageSpaces } from './schema/storageSpacesSchema'
|
||||||
|
import { distributors } from './schema/distributors'
|
||||||
|
|
||||||
|
export type StockBatch = {
|
||||||
|
id: number
|
||||||
|
product: { id: number; name: string; brand: string }
|
||||||
|
arrived: string
|
||||||
|
batch_no: string
|
||||||
|
mfg: string
|
||||||
|
expiry: string
|
||||||
|
rack: { id: number; name: string } | null
|
||||||
|
distributor: { id: number; agency: string } | null
|
||||||
|
quantity: number
|
||||||
|
is_default: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateStockBatchInput = {
|
||||||
|
product_id: number
|
||||||
|
arrived: string
|
||||||
|
batch_no: string
|
||||||
|
mfg: string
|
||||||
|
expiry: string
|
||||||
|
rack_id?: number | null
|
||||||
|
distributor_id?: number | null
|
||||||
|
quantity: number
|
||||||
|
is_default?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateStockBatchPatch = Partial<CreateStockBatchInput>
|
||||||
|
|
||||||
|
export type StockBatchesRepo = {
|
||||||
|
getStockBatches: () => Promise<StockBatch[]>
|
||||||
|
getStockBatchById: (id: number) => Promise<StockBatch | null>
|
||||||
|
createStockBatch: (input: CreateStockBatchInput) => Promise<StockBatch>
|
||||||
|
updateStockBatch: (id: number, patch: UpdateStockBatchPatch) => Promise<StockBatch | null>
|
||||||
|
deleteStockBatch: (id: number) => Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchProduct(productId: number): { id: number; name: string; brand: string } | null {
|
||||||
|
const p = db.select().from(products).where(eq(products.id, productId)).get()
|
||||||
|
return p ? { id: p.id, name: p.name, brand: p.brand } : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchRack(rackId: number | null): { id: number; name: string } | null {
|
||||||
|
if (!rackId) return null
|
||||||
|
const r = db.select().from(storageSpaces).where(eq(storageSpaces.id, rackId)).get()
|
||||||
|
return r ? { id: r.id, name: r.name } : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchDistributor(distributorId: number | null): { id: number; agency: string } | null {
|
||||||
|
if (!distributorId) return null
|
||||||
|
const d = db.select().from(distributors).where(eq(distributors.id, distributorId)).get()
|
||||||
|
return d ? { id: d.id, agency: d.agency } : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function toStockBatch(row: typeof stockBatches.$inferSelect): StockBatch {
|
||||||
|
const product = fetchProduct(row.productId)
|
||||||
|
const rack = fetchRack(row.rackId)
|
||||||
|
const distributor = fetchDistributor(row.distributorId)
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
product: product ?? { id: row.productId, name: 'Unknown', brand: '' },
|
||||||
|
arrived: row.arrived,
|
||||||
|
batch_no: row.batchNo,
|
||||||
|
mfg: row.mfg,
|
||||||
|
expiry: row.expiry,
|
||||||
|
rack,
|
||||||
|
distributor,
|
||||||
|
quantity: row.quantity,
|
||||||
|
is_default: row.isDefault,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createStockBatchesRepo(): { repo: StockBatchesRepo } {
|
||||||
|
const repo: StockBatchesRepo = {
|
||||||
|
getStockBatches() {
|
||||||
|
const rows = db.select().from(stockBatches).all()
|
||||||
|
return Promise.resolve(rows.map(toStockBatch))
|
||||||
|
},
|
||||||
|
|
||||||
|
getStockBatchById(id) {
|
||||||
|
const row = db.select().from(stockBatches).where(eq(stockBatches.id, id)).get()
|
||||||
|
return Promise.resolve(row ? toStockBatch(row) : null)
|
||||||
|
},
|
||||||
|
|
||||||
|
createStockBatch(input) {
|
||||||
|
const result = sqlite.transaction(() => {
|
||||||
|
if (input.is_default) {
|
||||||
|
db.update(stockBatches)
|
||||||
|
.set({ isDefault: false })
|
||||||
|
.where(eq(stockBatches.productId, input.product_id))
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = db
|
||||||
|
.insert(stockBatches)
|
||||||
|
.values({
|
||||||
|
productId: input.product_id,
|
||||||
|
arrived: input.arrived,
|
||||||
|
batchNo: input.batch_no,
|
||||||
|
mfg: input.mfg,
|
||||||
|
expiry: input.expiry,
|
||||||
|
rackId: input.rack_id ?? null,
|
||||||
|
distributorId: input.distributor_id ?? null,
|
||||||
|
quantity: input.quantity ?? 0,
|
||||||
|
isDefault: input.is_default ?? false,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.get()
|
||||||
|
|
||||||
|
return created
|
||||||
|
})()
|
||||||
|
|
||||||
|
return Promise.resolve(toStockBatch(result))
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStockBatch(id, patch) {
|
||||||
|
const existing = db.select().from(stockBatches).where(eq(stockBatches.id, id)).get()
|
||||||
|
if (!existing) return Promise.resolve(null)
|
||||||
|
|
||||||
|
sqlite.transaction(() => {
|
||||||
|
if (patch.is_default) {
|
||||||
|
db.update(stockBatches)
|
||||||
|
.set({ isDefault: false })
|
||||||
|
.where(eq(stockBatches.productId, patch.product_id ?? existing.productId))
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setData: Record<string, unknown> = {}
|
||||||
|
if (patch.product_id !== undefined) setData.productId = patch.product_id
|
||||||
|
if (patch.arrived !== undefined) setData.arrived = patch.arrived
|
||||||
|
if (patch.batch_no !== undefined) setData.batchNo = patch.batch_no
|
||||||
|
if (patch.mfg !== undefined) setData.mfg = patch.mfg
|
||||||
|
if (patch.expiry !== undefined) setData.expiry = patch.expiry
|
||||||
|
if (patch.rack_id !== undefined) setData.rackId = patch.rack_id ?? null
|
||||||
|
if (patch.distributor_id !== undefined) setData.distributorId = patch.distributor_id ?? null
|
||||||
|
if (patch.quantity !== undefined) setData.quantity = patch.quantity
|
||||||
|
if (patch.is_default !== undefined) setData.isDefault = patch.is_default
|
||||||
|
|
||||||
|
if (Object.keys(setData).length > 0) {
|
||||||
|
db.update(stockBatches).set(setData).where(eq(stockBatches.id, id)).run()
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
const updated = db.select().from(stockBatches).where(eq(stockBatches.id, id)).get()!
|
||||||
|
return Promise.resolve(toStockBatch(updated))
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteStockBatch(id) {
|
||||||
|
const deleted = db.delete(stockBatches).where(eq(stockBatches.id, id)).returning({ id: stockBatches.id }).get()
|
||||||
|
return Promise.resolve(Boolean(deleted))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return { repo }
|
||||||
|
}
|
||||||
21
packages/shared-react/src/hooks/stockBatches.ts
Normal file
21
packages/shared-react/src/hooks/stockBatches.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { trpc } from "../trpc";
|
||||||
|
|
||||||
|
export function useListStockBatches() {
|
||||||
|
return trpc.stock.list.useQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetStockBatchById(id: number) {
|
||||||
|
return trpc.stock.byId.useQuery({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateStockBatch() {
|
||||||
|
return trpc.stock.create.useMutation();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateStockBatch() {
|
||||||
|
return trpc.stock.update.useMutation();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRemoveStockBatch() {
|
||||||
|
return trpc.stock.remove.useMutation();
|
||||||
|
}
|
||||||
|
|
@ -6,4 +6,5 @@ export * from './hooks/distributors'
|
||||||
export * from './hooks/products'
|
export * from './hooks/products'
|
||||||
export * from './hooks/drugInfo'
|
export * from './hooks/drugInfo'
|
||||||
export * from './hooks/units'
|
export * from './hooks/units'
|
||||||
|
export * from './hooks/stockBatches'
|
||||||
export { trpc } from './trpc'
|
export { trpc } from './trpc'
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,4 @@ export type {
|
||||||
|
|
||||||
// Shared schemas
|
// Shared schemas
|
||||||
export * from './schemas/product'
|
export * from './schemas/product'
|
||||||
|
export * from './schemas/stock'
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export const ProductSchema = z.object({
|
||||||
procured_price: z.number(),
|
procured_price: z.number(),
|
||||||
mrp: z.number(),
|
mrp: z.number(),
|
||||||
selling_price: z.number(),
|
selling_price: z.number(),
|
||||||
quantity: z.number().int(),
|
size: z.number().int(),
|
||||||
reorder_level: z.number().int(),
|
reorder_level: z.number().int(),
|
||||||
units_per_strip: z.number().int().nullable(),
|
units_per_strip: z.number().int().nullable(),
|
||||||
hide_product_from_public: z.boolean(),
|
hide_product_from_public: z.boolean(),
|
||||||
|
|
@ -42,7 +42,7 @@ export const CreateProductInput = z.object({
|
||||||
procured_price: shape.procured_price.min(0),
|
procured_price: shape.procured_price.min(0),
|
||||||
mrp: shape.mrp.min(0),
|
mrp: shape.mrp.min(0),
|
||||||
selling_price: shape.selling_price.min(0),
|
selling_price: shape.selling_price.min(0),
|
||||||
quantity: shape.quantity.default(0),
|
size: shape.size.default(0),
|
||||||
reorder_level: shape.reorder_level.default(0),
|
reorder_level: shape.reorder_level.default(0),
|
||||||
units_per_strip: shape.units_per_strip.optional(),
|
units_per_strip: shape.units_per_strip.optional(),
|
||||||
hide_product_from_public: shape.hide_product_from_public.default(false),
|
hide_product_from_public: shape.hide_product_from_public.default(false),
|
||||||
|
|
|
||||||
34
packages/shared/src/schemas/stock.ts
Normal file
34
packages/shared/src/schemas/stock.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const StockBatchSchema = z.object({
|
||||||
|
id: z.number().int(),
|
||||||
|
product: z.object({ id: z.number().int(), name: z.string(), brand: z.string() }),
|
||||||
|
arrived: z.string(),
|
||||||
|
batch_no: z.string(),
|
||||||
|
mfg: z.string(),
|
||||||
|
expiry: z.string(),
|
||||||
|
rack: z.object({ id: z.number().int(), name: z.string() }).nullable(),
|
||||||
|
distributor: z.object({ id: z.number().int(), agency: z.string() }).nullable(),
|
||||||
|
quantity: z.number().int(),
|
||||||
|
is_default: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { shape } = StockBatchSchema;
|
||||||
|
|
||||||
|
export const CreateStockBatchInput = z.object({
|
||||||
|
product_id: shape.product.shape.id,
|
||||||
|
arrived: shape.arrived.min(1),
|
||||||
|
batch_no: shape.batch_no.min(1),
|
||||||
|
mfg: shape.mfg.min(1),
|
||||||
|
expiry: shape.expiry.min(1),
|
||||||
|
rack_id: z.number().int().nullable().optional(),
|
||||||
|
distributor_id: z.number().int().nullable().optional(),
|
||||||
|
quantity: z.number().int().min(1),
|
||||||
|
is_default: shape.is_default.default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateStockBatchInput = z
|
||||||
|
.object({ id: z.number().int() })
|
||||||
|
.merge(CreateStockBatchInput.partial());
|
||||||
|
|
||||||
|
export type StockBatch = z.infer<typeof StockBatchSchema>;
|
||||||
Loading…
Add table
Reference in a new issue