functional auth
This commit is contained in:
parent
51bfb1513a
commit
dfea6814e9
17 changed files with 483 additions and 143 deletions
|
|
@ -33,11 +33,14 @@ app.all("/trpc/*", async (c) => {
|
||||||
createContext: () => createContext({ req: c.req.raw, resHeaders }),
|
createContext: () => createContext({ req: c.req.raw, resHeaders }),
|
||||||
});
|
});
|
||||||
|
|
||||||
resHeaders.forEach((value, key) => {
|
const merged = new Headers(response.headers);
|
||||||
response.headers.set(key, value);
|
resHeaders.forEach((value, key) => merged.set(key, value));
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
return new Response(response.body, {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: merged,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export { app };
|
export { app };
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { t } from "../../init";
|
import { protectedProcedure, router } from "../../init";
|
||||||
import { dataManager } from "../../../lib/data-manager-instance";
|
import { dataManager } from "../../../lib/data-manager-instance";
|
||||||
|
|
||||||
export const DistributorSchema = z.object({
|
export const DistributorSchema = z.object({
|
||||||
|
|
@ -25,39 +25,54 @@ export const UpdateDistributorInput = z
|
||||||
|
|
||||||
export type Distributor = z.infer<typeof DistributorSchema>;
|
export type Distributor = z.infer<typeof DistributorSchema>;
|
||||||
|
|
||||||
export const distributorRouter = t.router({
|
export const distributorRouter = router({
|
||||||
list: t.procedure
|
list: protectedProcedure
|
||||||
.output(z.array(DistributorSchema))
|
.output(z.array(DistributorSchema))
|
||||||
.query(() => dataManager.distributors.getDistributors()),
|
.query(({ ctx }) =>
|
||||||
|
dataManager.distributors.getDistributors(ctx.staff.enterpriseId),
|
||||||
byId: t.procedure
|
|
||||||
.input(z.object({ id: z.number().int() }))
|
|
||||||
.output(DistributorSchema.nullable())
|
|
||||||
.query(({ input }) =>
|
|
||||||
dataManager.distributors.getDistributorById(input.id),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
create: t.procedure
|
byId: protectedProcedure
|
||||||
|
.input(z.object({ id: z.number().int() }))
|
||||||
|
.output(DistributorSchema.nullable())
|
||||||
|
.query(({ ctx, input }) =>
|
||||||
|
dataManager.distributors.getDistributorById(
|
||||||
|
input.id,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
create: protectedProcedure
|
||||||
.input(CreateDistributorInput)
|
.input(CreateDistributorInput)
|
||||||
.output(DistributorSchema)
|
.output(DistributorSchema)
|
||||||
.mutation(({ input }) =>
|
.mutation(({ ctx, input }) =>
|
||||||
dataManager.distributors.createDistributor(input),
|
dataManager.distributors.createDistributor(
|
||||||
|
input,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
update: t.procedure
|
update: protectedProcedure
|
||||||
.input(UpdateDistributorInput)
|
.input(UpdateDistributorInput)
|
||||||
.output(DistributorSchema.nullable())
|
.output(DistributorSchema.nullable())
|
||||||
.mutation(({ input }) => {
|
.mutation(({ ctx, input }) => {
|
||||||
const { id, ...patch } = input;
|
const { id, ...patch } = input;
|
||||||
return dataManager.distributors.updateDistributor(id, patch);
|
return dataManager.distributors.updateDistributor(
|
||||||
|
id,
|
||||||
|
patch,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
remove: t.procedure
|
remove: protectedProcedure
|
||||||
.input(z.object({ id: z.number().int() }))
|
.input(z.object({ id: z.number().int() }))
|
||||||
.output(z.object({ ok: z.boolean() }))
|
.output(z.object({ ok: z.boolean() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const ok =
|
const ok =
|
||||||
await dataManager.distributors.deleteDistributor(input.id);
|
await dataManager.distributors.deleteDistributor(
|
||||||
|
input.id,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
);
|
||||||
return { ok };
|
return { ok };
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { t } from "../../init";
|
import { protectedProcedure, router } from "../../init";
|
||||||
import { dataManager } from "../../../lib/data-manager-instance";
|
import { dataManager } from "../../../lib/data-manager-instance";
|
||||||
import {
|
import {
|
||||||
ProductSchema,
|
ProductSchema,
|
||||||
|
|
@ -7,34 +7,53 @@ import {
|
||||||
UpdateProductInput,
|
UpdateProductInput,
|
||||||
} from "@repo/shared";
|
} from "@repo/shared";
|
||||||
|
|
||||||
export const productRouter = t.router({
|
export const productRouter = router({
|
||||||
list: t.procedure
|
list: protectedProcedure
|
||||||
.output(z.array(ProductSchema))
|
.output(z.array(ProductSchema))
|
||||||
.query(() => dataManager.products.getProducts()),
|
.query(({ ctx }) =>
|
||||||
|
dataManager.products.getProducts(ctx.staff.enterpriseId),
|
||||||
|
),
|
||||||
|
|
||||||
byId: t.procedure
|
byId: protectedProcedure
|
||||||
.input(z.object({ id: z.number().int() }))
|
.input(z.object({ id: z.number().int() }))
|
||||||
.output(ProductSchema.nullable())
|
.output(ProductSchema.nullable())
|
||||||
.query(({ input }) => dataManager.products.getProductById(input.id)),
|
.query(({ ctx, input }) =>
|
||||||
|
dataManager.products.getProductById(
|
||||||
|
input.id,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
create: t.procedure
|
create: protectedProcedure
|
||||||
.input(CreateProductInput)
|
.input(CreateProductInput)
|
||||||
.output(ProductSchema)
|
.output(ProductSchema)
|
||||||
.mutation(({ input }) => dataManager.products.createProduct(input)),
|
.mutation(({ ctx, input }) =>
|
||||||
|
dataManager.products.createProduct(
|
||||||
|
input,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
update: t.procedure
|
update: protectedProcedure
|
||||||
.input(UpdateProductInput)
|
.input(UpdateProductInput)
|
||||||
.output(ProductSchema.nullable())
|
.output(ProductSchema.nullable())
|
||||||
.mutation(({ input }) => {
|
.mutation(({ ctx, input }) => {
|
||||||
const { id, ...patch } = input;
|
const { id, ...patch } = input;
|
||||||
return dataManager.products.updateProduct(id, patch);
|
return dataManager.products.updateProduct(
|
||||||
|
id,
|
||||||
|
patch,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
remove: t.procedure
|
remove: protectedProcedure
|
||||||
.input(z.object({ id: z.number().int() }))
|
.input(z.object({ id: z.number().int() }))
|
||||||
.output(z.object({ ok: z.boolean() }))
|
.output(z.object({ ok: z.boolean() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const ok = await dataManager.products.deleteProduct(input.id);
|
const ok = await dataManager.products.deleteProduct(
|
||||||
|
input.id,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
);
|
||||||
return { ok };
|
return { ok };
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { t } from "../../init";
|
import { protectedProcedure, router } from "../../init";
|
||||||
import { dataManager } from "../../../lib/data-manager-instance";
|
import { dataManager } from "../../../lib/data-manager-instance";
|
||||||
|
|
||||||
export const StockBatchSchema = z.object({
|
export const StockBatchSchema = z.object({
|
||||||
|
|
@ -11,6 +11,7 @@ export const StockBatchSchema = z.object({
|
||||||
expiry: z.string(),
|
expiry: z.string(),
|
||||||
rack: z.object({ id: z.number().int(), name: z.string() }).nullable(),
|
rack: z.object({ id: z.number().int(), name: z.string() }).nullable(),
|
||||||
distributor: z.object({ id: z.number().int(), agency: z.string() }).nullable(),
|
distributor: z.object({ id: z.number().int(), agency: z.string() }).nullable(),
|
||||||
|
quantity: z.number().int(),
|
||||||
is_default: z.boolean(),
|
is_default: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ export const CreateStockBatchInput = z.object({
|
||||||
expiry: shape.expiry.min(1),
|
expiry: shape.expiry.min(1),
|
||||||
rack_id: z.number().int().nullable().optional(),
|
rack_id: z.number().int().nullable().optional(),
|
||||||
distributor_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),
|
is_default: shape.is_default.default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -33,34 +35,53 @@ export const UpdateStockBatchInput = z
|
||||||
|
|
||||||
export type StockBatch = z.infer<typeof StockBatchSchema>;
|
export type StockBatch = z.infer<typeof StockBatchSchema>;
|
||||||
|
|
||||||
export const stockRouter = t.router({
|
export const stockRouter = router({
|
||||||
list: t.procedure
|
list: protectedProcedure
|
||||||
.output(z.array(StockBatchSchema))
|
.output(z.array(StockBatchSchema))
|
||||||
.query(() => dataManager.stockBatches.getStockBatches()),
|
.query(({ ctx }) =>
|
||||||
|
dataManager.stockBatches.getStockBatches(ctx.staff.enterpriseId),
|
||||||
|
),
|
||||||
|
|
||||||
byId: t.procedure
|
byId: protectedProcedure
|
||||||
.input(z.object({ id: z.number().int() }))
|
.input(z.object({ id: z.number().int() }))
|
||||||
.output(StockBatchSchema.nullable())
|
.output(StockBatchSchema.nullable())
|
||||||
.query(({ input }) => dataManager.stockBatches.getStockBatchById(input.id)),
|
.query(({ ctx, input }) =>
|
||||||
|
dataManager.stockBatches.getStockBatchById(
|
||||||
|
input.id,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
create: t.procedure
|
create: protectedProcedure
|
||||||
.input(CreateStockBatchInput)
|
.input(CreateStockBatchInput)
|
||||||
.output(StockBatchSchema)
|
.output(StockBatchSchema)
|
||||||
.mutation(({ input }) => dataManager.stockBatches.createStockBatch(input)),
|
.mutation(({ ctx, input }) =>
|
||||||
|
dataManager.stockBatches.createStockBatch(
|
||||||
|
input,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
update: t.procedure
|
update: protectedProcedure
|
||||||
.input(UpdateStockBatchInput)
|
.input(UpdateStockBatchInput)
|
||||||
.output(StockBatchSchema.nullable())
|
.output(StockBatchSchema.nullable())
|
||||||
.mutation(({ input }) => {
|
.mutation(({ ctx, input }) => {
|
||||||
const { id, ...patch } = input;
|
const { id, ...patch } = input;
|
||||||
return dataManager.stockBatches.updateStockBatch(id, patch);
|
return dataManager.stockBatches.updateStockBatch(
|
||||||
|
id,
|
||||||
|
patch,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
remove: t.procedure
|
remove: protectedProcedure
|
||||||
.input(z.object({ id: z.number().int() }))
|
.input(z.object({ id: z.number().int() }))
|
||||||
.output(z.object({ ok: z.boolean() }))
|
.output(z.object({ ok: z.boolean() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const ok = await dataManager.stockBatches.deleteStockBatch(input.id);
|
const ok = await dataManager.stockBatches.deleteStockBatch(
|
||||||
|
input.id,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
);
|
||||||
return { ok };
|
return { ok };
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {t} from '../../init'
|
import { protectedProcedure, router } from "../../init";
|
||||||
import { dataManager } from "../../../lib/data-manager-instance";
|
import { dataManager } from "../../../lib/data-manager-instance";
|
||||||
|
|
||||||
export const StorageSpaceSchema = z.object({
|
export const StorageSpaceSchema = z.object({
|
||||||
|
|
@ -25,39 +25,54 @@ export const UpdateStorageInput = z
|
||||||
|
|
||||||
export type StorageSpace = z.infer<typeof StorageSpaceSchema>;
|
export type StorageSpace = z.infer<typeof StorageSpaceSchema>;
|
||||||
|
|
||||||
export const storageRouter = t.router({
|
export const storageRouter = router({
|
||||||
list: t.procedure
|
list: protectedProcedure
|
||||||
.output(z.array(StorageSpaceSchema))
|
.output(z.array(StorageSpaceSchema))
|
||||||
.query(() => dataManager.storageSpaces.getStorageSpaces()),
|
.query(({ ctx }) =>
|
||||||
|
dataManager.storageSpaces.getStorageSpaces(ctx.staff.enterpriseId),
|
||||||
byId: t.procedure
|
|
||||||
.input(z.object({ id: z.number().int() }))
|
|
||||||
.output(StorageSpaceSchema.nullable())
|
|
||||||
.query(({ input }) =>
|
|
||||||
dataManager.storageSpaces.getStorageSpaceById(input.id),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
create: t.procedure
|
byId: protectedProcedure
|
||||||
|
.input(z.object({ id: z.number().int() }))
|
||||||
|
.output(StorageSpaceSchema.nullable())
|
||||||
|
.query(({ ctx, input }) =>
|
||||||
|
dataManager.storageSpaces.getStorageSpaceById(
|
||||||
|
input.id,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
create: protectedProcedure
|
||||||
.input(CreateStorageInput)
|
.input(CreateStorageInput)
|
||||||
.output(StorageSpaceSchema)
|
.output(StorageSpaceSchema)
|
||||||
.mutation(({ input }) =>
|
.mutation(({ ctx, input }) =>
|
||||||
dataManager.storageSpaces.createStorageSpace(input),
|
dataManager.storageSpaces.createStorageSpace(
|
||||||
|
input,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
update: t.procedure
|
update: protectedProcedure
|
||||||
.input(UpdateStorageInput)
|
.input(UpdateStorageInput)
|
||||||
.output(StorageSpaceSchema.nullable())
|
.output(StorageSpaceSchema.nullable())
|
||||||
.mutation(({ input }) => {
|
.mutation(({ ctx, input }) => {
|
||||||
const { id, ...patch } = input;
|
const { id, ...patch } = input;
|
||||||
return dataManager.storageSpaces.updateStorageSpace(id, patch);
|
return dataManager.storageSpaces.updateStorageSpace(
|
||||||
|
id,
|
||||||
|
patch,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
remove: t.procedure
|
remove: protectedProcedure
|
||||||
.input(z.object({ id: z.number().int() }))
|
.input(z.object({ id: z.number().int() }))
|
||||||
.output(z.object({ ok: z.boolean() }))
|
.output(z.object({ ok: z.boolean() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const ok =
|
const ok =
|
||||||
await dataManager.storageSpaces.deleteStorageSpace(input.id);
|
await dataManager.storageSpaces.deleteStorageSpace(
|
||||||
|
input.id,
|
||||||
|
ctx.staff.enterpriseId,
|
||||||
|
);
|
||||||
return { ok };
|
return { ok };
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { Route as StockRouteImport } from './routes/stock'
|
||||||
import { Route as StaffRouteImport } from './routes/staff'
|
import { Route as StaffRouteImport } from './routes/staff'
|
||||||
import { Route as ProfileRouteImport } from './routes/profile'
|
import { Route as ProfileRouteImport } from './routes/profile'
|
||||||
import { Route as ProductsRouteImport } from './routes/products'
|
import { Route as ProductsRouteImport } from './routes/products'
|
||||||
|
import { Route as LoginRouteImport } from './routes/login'
|
||||||
import { Route as DistributorsRouteImport } from './routes/distributors'
|
import { Route as DistributorsRouteImport } from './routes/distributors'
|
||||||
import { Route as CustomersRouteImport } from './routes/customers'
|
import { Route as CustomersRouteImport } from './routes/customers'
|
||||||
import { Route as BillingRouteImport } from './routes/billing'
|
import { Route as BillingRouteImport } from './routes/billing'
|
||||||
|
|
@ -56,6 +57,11 @@ const ProductsRoute = ProductsRouteImport.update({
|
||||||
path: '/products',
|
path: '/products',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const LoginRoute = LoginRouteImport.update({
|
||||||
|
id: '/login',
|
||||||
|
path: '/login',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const DistributorsRoute = DistributorsRouteImport.update({
|
const DistributorsRoute = DistributorsRouteImport.update({
|
||||||
id: '/distributors',
|
id: '/distributors',
|
||||||
path: '/distributors',
|
path: '/distributors',
|
||||||
|
|
@ -142,6 +148,7 @@ export interface FileRoutesByFullPath {
|
||||||
'/billing': typeof BillingRoute
|
'/billing': typeof BillingRoute
|
||||||
'/customers': typeof CustomersRoute
|
'/customers': typeof CustomersRoute
|
||||||
'/distributors': typeof DistributorsRouteWithChildren
|
'/distributors': typeof DistributorsRouteWithChildren
|
||||||
|
'/login': typeof LoginRoute
|
||||||
'/products': typeof ProductsRouteWithChildren
|
'/products': typeof ProductsRouteWithChildren
|
||||||
'/profile': typeof ProfileRoute
|
'/profile': typeof ProfileRoute
|
||||||
'/staff': typeof StaffRoute
|
'/staff': typeof StaffRoute
|
||||||
|
|
@ -164,6 +171,7 @@ export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/billing': typeof BillingRoute
|
'/billing': typeof BillingRoute
|
||||||
'/customers': typeof CustomersRoute
|
'/customers': typeof CustomersRoute
|
||||||
|
'/login': typeof LoginRoute
|
||||||
'/profile': typeof ProfileRoute
|
'/profile': typeof ProfileRoute
|
||||||
'/staff': typeof StaffRoute
|
'/staff': typeof StaffRoute
|
||||||
'/distributors/$id': typeof DistributorsIdRoute
|
'/distributors/$id': typeof DistributorsIdRoute
|
||||||
|
|
@ -185,6 +193,7 @@ export interface FileRoutesById {
|
||||||
'/billing': typeof BillingRoute
|
'/billing': typeof BillingRoute
|
||||||
'/customers': typeof CustomersRoute
|
'/customers': typeof CustomersRoute
|
||||||
'/distributors': typeof DistributorsRouteWithChildren
|
'/distributors': typeof DistributorsRouteWithChildren
|
||||||
|
'/login': typeof LoginRoute
|
||||||
'/products': typeof ProductsRouteWithChildren
|
'/products': typeof ProductsRouteWithChildren
|
||||||
'/profile': typeof ProfileRoute
|
'/profile': typeof ProfileRoute
|
||||||
'/staff': typeof StaffRoute
|
'/staff': typeof StaffRoute
|
||||||
|
|
@ -210,6 +219,7 @@ export interface FileRouteTypes {
|
||||||
| '/billing'
|
| '/billing'
|
||||||
| '/customers'
|
| '/customers'
|
||||||
| '/distributors'
|
| '/distributors'
|
||||||
|
| '/login'
|
||||||
| '/products'
|
| '/products'
|
||||||
| '/profile'
|
| '/profile'
|
||||||
| '/staff'
|
| '/staff'
|
||||||
|
|
@ -232,6 +242,7 @@ export interface FileRouteTypes {
|
||||||
| '/'
|
| '/'
|
||||||
| '/billing'
|
| '/billing'
|
||||||
| '/customers'
|
| '/customers'
|
||||||
|
| '/login'
|
||||||
| '/profile'
|
| '/profile'
|
||||||
| '/staff'
|
| '/staff'
|
||||||
| '/distributors/$id'
|
| '/distributors/$id'
|
||||||
|
|
@ -252,6 +263,7 @@ export interface FileRouteTypes {
|
||||||
| '/billing'
|
| '/billing'
|
||||||
| '/customers'
|
| '/customers'
|
||||||
| '/distributors'
|
| '/distributors'
|
||||||
|
| '/login'
|
||||||
| '/products'
|
| '/products'
|
||||||
| '/profile'
|
| '/profile'
|
||||||
| '/staff'
|
| '/staff'
|
||||||
|
|
@ -276,6 +288,7 @@ export interface RootRouteChildren {
|
||||||
BillingRoute: typeof BillingRoute
|
BillingRoute: typeof BillingRoute
|
||||||
CustomersRoute: typeof CustomersRoute
|
CustomersRoute: typeof CustomersRoute
|
||||||
DistributorsRoute: typeof DistributorsRouteWithChildren
|
DistributorsRoute: typeof DistributorsRouteWithChildren
|
||||||
|
LoginRoute: typeof LoginRoute
|
||||||
ProductsRoute: typeof ProductsRouteWithChildren
|
ProductsRoute: typeof ProductsRouteWithChildren
|
||||||
ProfileRoute: typeof ProfileRoute
|
ProfileRoute: typeof ProfileRoute
|
||||||
StaffRoute: typeof StaffRoute
|
StaffRoute: typeof StaffRoute
|
||||||
|
|
@ -320,6 +333,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof ProductsRouteImport
|
preLoaderRoute: typeof ProductsRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/login': {
|
||||||
|
id: '/login'
|
||||||
|
path: '/login'
|
||||||
|
fullPath: '/login'
|
||||||
|
preLoaderRoute: typeof LoginRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/distributors': {
|
'/distributors': {
|
||||||
id: '/distributors'
|
id: '/distributors'
|
||||||
path: '/distributors'
|
path: '/distributors'
|
||||||
|
|
@ -501,6 +521,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||||
BillingRoute: BillingRoute,
|
BillingRoute: BillingRoute,
|
||||||
CustomersRoute: CustomersRoute,
|
CustomersRoute: CustomersRoute,
|
||||||
DistributorsRoute: DistributorsRouteWithChildren,
|
DistributorsRoute: DistributorsRouteWithChildren,
|
||||||
|
LoginRoute: LoginRoute,
|
||||||
ProductsRoute: ProductsRouteWithChildren,
|
ProductsRoute: ProductsRouteWithChildren,
|
||||||
ProfileRoute: ProfileRoute,
|
ProfileRoute: ProfileRoute,
|
||||||
StaffRoute: StaffRoute,
|
StaffRoute: StaffRoute,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
import { Outlet, createRootRoute } from "@tanstack/react-router";
|
import { useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Outlet,
|
||||||
|
createRootRoute,
|
||||||
|
useNavigate,
|
||||||
|
useRouterState,
|
||||||
|
} from "@tanstack/react-router";
|
||||||
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||||
import { TanStackDevtools } from "@tanstack/react-devtools";
|
import { TanStackDevtools } from "@tanstack/react-devtools";
|
||||||
import { AppLayout } from "#/components/AppLayout";
|
import { AppLayout } from "#/components/AppLayout";
|
||||||
|
import { AuthGate, useAuthStore } from "shared-react";
|
||||||
|
|
||||||
import "../styles.css";
|
import "../styles.css";
|
||||||
|
|
||||||
|
|
@ -9,12 +16,46 @@ export const Route = createRootRoute({
|
||||||
component: RootComponent,
|
component: RootComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function AuthGuard() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
||||||
|
const isLoading = useAuthStore((s) => s.isLoading);
|
||||||
|
const staff = useAuthStore((s) => s.staff);
|
||||||
|
const isLoginPage = pathname === "/login";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && !staff && !isLoginPage) {
|
||||||
|
navigate({ to: "/login" });
|
||||||
|
}
|
||||||
|
}, [isLoading, staff, isLoginPage, navigate]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<p className="text-sm text-slate-600">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!staff && !isLoginPage) return null;
|
||||||
|
|
||||||
|
if (isLoginPage) return <Outlet />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<Outlet />
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function RootComponent() {
|
function RootComponent() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppLayout>
|
<AuthGate>
|
||||||
<Outlet />
|
<AuthGuard />
|
||||||
</AppLayout>
|
</AuthGate>
|
||||||
<TanStackDevtools
|
<TanStackDevtools
|
||||||
config={{
|
config={{
|
||||||
position: "bottom-right",
|
position: "bottom-right",
|
||||||
|
|
|
||||||
117
apps/pharmanager/src/routes/login.tsx
Normal file
117
apps/pharmanager/src/routes/login.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useLogin, useAuthStore } from "shared-react";
|
||||||
|
import { Button, Input } from "#/components/ui";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
username: z.string().min(1, "Username is required"),
|
||||||
|
password: z.string().min(1, "Password is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/login")({
|
||||||
|
component: LoginPage,
|
||||||
|
staticData: {
|
||||||
|
title: "Login",
|
||||||
|
subtitle: "Sign in to your account",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function LoginPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const loginMutation = useLogin();
|
||||||
|
const setUser = useAuthStore((s) => s.setUser);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSubmit(values: FormValues) {
|
||||||
|
loginMutation.mutate(values, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setUser(data.staff, {
|
||||||
|
id: data.enterprise_id,
|
||||||
|
name: "",
|
||||||
|
type: "",
|
||||||
|
});
|
||||||
|
navigate({ to: "/" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<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 w-full max-w-sm"
|
||||||
|
>
|
||||||
|
<div className="mb-6">
|
||||||
|
<h2 className="text-xl font-semibold text-slate-900 mb-1">
|
||||||
|
Sign In
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-slate-600">
|
||||||
|
Enter your credentials to continue
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...register("username")}
|
||||||
|
variant={errors.username ? "error" : "default"}
|
||||||
|
placeholder="Enter your username"
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
{errors.username && (
|
||||||
|
<p className="text-sm text-red-600 mt-1">
|
||||||
|
{errors.username.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
{...register("password")}
|
||||||
|
variant={errors.password ? "error" : "default"}
|
||||||
|
placeholder="Enter your password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
{errors.password && (
|
||||||
|
<p className="text-sm text-red-600 mt-1">
|
||||||
|
{errors.password.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loginMutation.error && (
|
||||||
|
<p className="text-sm text-red-600 mt-4">
|
||||||
|
Invalid username or password
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full mt-6"
|
||||||
|
disabled={loginMutation.isPending}
|
||||||
|
>
|
||||||
|
{loginMutation.isPending ? "Signing in..." : "Sign In"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -15,8 +15,8 @@ sqlite.run("INSERT OR IGNORE INTO units (name) VALUES ('piece')")
|
||||||
sqlite.run("INSERT OR IGNORE INTO enterprises (id, name, type, owner_name, mobile) VALUES (1, 'Main Pharmacy', 'Pharmacy', 'Admin User', '0000000000')")
|
sqlite.run("INSERT OR IGNORE INTO enterprises (id, name, type, owner_name, mobile) VALUES (1, 'Main Pharmacy', 'Pharmacy', 'Admin User', '0000000000')")
|
||||||
|
|
||||||
// Seed staff (admin user)
|
// Seed staff (admin user)
|
||||||
const today = new Date().toISOString().split('T')[0]
|
const today = new Date().toISOString().slice(0, 10)
|
||||||
const adminHash = bcrypt.hashSync('admin123', 10)
|
const adminHash = bcrypt.hashSync('admin123', 10) || ''
|
||||||
sqlite.run(
|
sqlite.run(
|
||||||
"INSERT OR IGNORE INTO staff (id, name, username, password, added_on, is_password_reset_needed) VALUES (1, 'Admin', 'admin', ?, ?, 1)",
|
"INSERT OR IGNORE INTO staff (id, name, username, password, added_on, is_password_reset_needed) VALUES (1, 'Admin', 'admin', ?, ?, 1)",
|
||||||
[adminHash, today],
|
[adminHash, today],
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq, and } from 'drizzle-orm'
|
||||||
|
|
||||||
import { db } from './db-instance'
|
import { db } from './db-instance'
|
||||||
import { distributors } from './schema/distributors'
|
import { distributors } from './schema/distributors'
|
||||||
|
|
@ -21,11 +21,11 @@ export type CreateDistributorInput = {
|
||||||
export type UpdateDistributorPatch = Partial<CreateDistributorInput>
|
export type UpdateDistributorPatch = Partial<CreateDistributorInput>
|
||||||
|
|
||||||
export type DistributorsRepo = {
|
export type DistributorsRepo = {
|
||||||
getDistributors: () => Promise<Distributor[]>
|
getDistributors: (enterpriseId: number) => Promise<Distributor[]>
|
||||||
getDistributorById: (id: number) => Promise<Distributor | null>
|
getDistributorById: (id: number, enterpriseId: number) => Promise<Distributor | null>
|
||||||
createDistributor: (input: CreateDistributorInput) => Promise<Distributor>
|
createDistributor: (input: CreateDistributorInput, enterpriseId: number) => Promise<Distributor>
|
||||||
updateDistributor: (id: number, patch: UpdateDistributorPatch) => Promise<Distributor | null>
|
updateDistributor: (id: number, patch: UpdateDistributorPatch, enterpriseId: number) => Promise<Distributor | null>
|
||||||
deleteDistributor: (id: number) => Promise<boolean>
|
deleteDistributor: (id: number, enterpriseId: number) => Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDistributor(row: {
|
function toDistributor(row: {
|
||||||
|
|
@ -48,21 +48,25 @@ export function createDistributorsRepo(): {
|
||||||
repo: DistributorsRepo
|
repo: DistributorsRepo
|
||||||
} {
|
} {
|
||||||
const repo: DistributorsRepo = {
|
const repo: DistributorsRepo = {
|
||||||
getDistributors() {
|
getDistributors(enterpriseId) {
|
||||||
const rows = db.select().from(distributors).all()
|
const rows = db
|
||||||
|
.select()
|
||||||
|
.from(distributors)
|
||||||
|
.where(eq(distributors.enterpriseId, enterpriseId))
|
||||||
|
.all()
|
||||||
return Promise.resolve(rows.map(toDistributor))
|
return Promise.resolve(rows.map(toDistributor))
|
||||||
},
|
},
|
||||||
|
|
||||||
getDistributorById(id) {
|
getDistributorById(id, enterpriseId) {
|
||||||
const row = db
|
const row = db
|
||||||
.select()
|
.select()
|
||||||
.from(distributors)
|
.from(distributors)
|
||||||
.where(eq(distributors.id, id))
|
.where(and(eq(distributors.id, id), eq(distributors.enterpriseId, enterpriseId)))
|
||||||
.get()
|
.get()
|
||||||
return Promise.resolve(row ? toDistributor(row) : null)
|
return Promise.resolve(row ? toDistributor(row) : null)
|
||||||
},
|
},
|
||||||
|
|
||||||
createDistributor(input) {
|
createDistributor(input, enterpriseId) {
|
||||||
const created = db
|
const created = db
|
||||||
.insert(distributors)
|
.insert(distributors)
|
||||||
.values({
|
.values({
|
||||||
|
|
@ -70,13 +74,14 @@ export function createDistributorsRepo(): {
|
||||||
contact: input.contact,
|
contact: input.contact,
|
||||||
mobile: input.mobile,
|
mobile: input.mobile,
|
||||||
address: input.address ?? null,
|
address: input.address ?? null,
|
||||||
|
enterpriseId,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.get()
|
.get()
|
||||||
return Promise.resolve(toDistributor(created))
|
return Promise.resolve(toDistributor(created))
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDistributor(id, patch) {
|
updateDistributor(id, patch, enterpriseId) {
|
||||||
const updated = db
|
const updated = db
|
||||||
.update(distributors)
|
.update(distributors)
|
||||||
.set({
|
.set({
|
||||||
|
|
@ -85,16 +90,16 @@ export function createDistributorsRepo(): {
|
||||||
...(patch.mobile !== undefined ? { mobile: patch.mobile } : {}),
|
...(patch.mobile !== undefined ? { mobile: patch.mobile } : {}),
|
||||||
...(patch.address !== undefined ? { address: patch.address } : {}),
|
...(patch.address !== undefined ? { address: patch.address } : {}),
|
||||||
})
|
})
|
||||||
.where(eq(distributors.id, id))
|
.where(and(eq(distributors.id, id), eq(distributors.enterpriseId, enterpriseId)))
|
||||||
.returning()
|
.returning()
|
||||||
.get()
|
.get()
|
||||||
return Promise.resolve(updated ? toDistributor(updated) : null)
|
return Promise.resolve(updated ? toDistributor(updated) : null)
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteDistributor(id) {
|
deleteDistributor(id, enterpriseId) {
|
||||||
const deleted = db
|
const deleted = db
|
||||||
.delete(distributors)
|
.delete(distributors)
|
||||||
.where(eq(distributors.id, id))
|
.where(and(eq(distributors.id, id), eq(distributors.enterpriseId, enterpriseId)))
|
||||||
.returning({ id: distributors.id })
|
.returning({ id: distributors.id })
|
||||||
.get()
|
.get()
|
||||||
return Promise.resolve(Boolean(deleted))
|
return Promise.resolve(Boolean(deleted))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq, and } from 'drizzle-orm'
|
||||||
import type { Database } from 'bun:sqlite'
|
|
||||||
|
|
||||||
import { db, sqlite } from './db-instance'
|
import { db, sqlite } from './db-instance'
|
||||||
import { products } from './schema/products'
|
import { products } from './schema/products'
|
||||||
|
|
@ -56,11 +55,11 @@ export type CreateProductInput =
|
||||||
export type UpdateProductPatch = Partial<CreateProductInput>
|
export type UpdateProductPatch = Partial<CreateProductInput>
|
||||||
|
|
||||||
export type ProductsRepo = {
|
export type ProductsRepo = {
|
||||||
getProducts: () => Promise<Product[]>
|
getProducts: (enterpriseId: number) => Promise<Product[]>
|
||||||
getProductById: (id: number) => Promise<Product | null>
|
getProductById: (id: number, enterpriseId: number) => Promise<Product | null>
|
||||||
createProduct: (input: CreateProductInput) => Promise<Product>
|
createProduct: (input: CreateProductInput, enterpriseId: number) => Promise<Product>
|
||||||
updateProduct: (id: number, patch: UpdateProductPatch) => Promise<Product | null>
|
updateProduct: (id: number, patch: UpdateProductPatch, enterpriseId: number) => Promise<Product | null>
|
||||||
deleteProduct: (id: number) => Promise<boolean>
|
deleteProduct: (id: number, enterpriseId: number) => Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrCreateDrug(name: string): number {
|
function getOrCreateDrug(name: string): number {
|
||||||
|
|
@ -151,8 +150,8 @@ function setCompositions(productId: number, comps: CompositionInput[]) {
|
||||||
|
|
||||||
export function createProductsRepo(): { repo: ProductsRepo } {
|
export function createProductsRepo(): { repo: ProductsRepo } {
|
||||||
const repo: ProductsRepo = {
|
const repo: ProductsRepo = {
|
||||||
getProducts() {
|
getProducts(enterpriseId) {
|
||||||
const rows = db.select().from(products).all()
|
const rows = db.select().from(products).where(eq(products.enterpriseId, enterpriseId)).all()
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
rows.map((r) => {
|
rows.map((r) => {
|
||||||
const distributor = fetchDistributor(r.distributorId)
|
const distributor = fetchDistributor(r.distributorId)
|
||||||
|
|
@ -165,8 +164,8 @@ export function createProductsRepo(): { repo: ProductsRepo } {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
getProductById(id) {
|
getProductById(id, enterpriseId) {
|
||||||
const row = db.select().from(products).where(eq(products.id, id)).get()
|
const row = db.select().from(products).where(and(eq(products.id, id), eq(products.enterpriseId, enterpriseId))).get()
|
||||||
if (!row) return Promise.resolve(null)
|
if (!row) return Promise.resolve(null)
|
||||||
const distributor = fetchDistributor(row.distributorId)
|
const distributor = fetchDistributor(row.distributorId)
|
||||||
const unit = fetchUnit(row.unitId)
|
const unit = fetchUnit(row.unitId)
|
||||||
|
|
@ -176,7 +175,7 @@ export function createProductsRepo(): { repo: ProductsRepo } {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
createProduct(input) {
|
createProduct(input, enterpriseId) {
|
||||||
const result = sqlite.transaction(() => {
|
const result = sqlite.transaction(() => {
|
||||||
const unitId = getOrCreateUnit(input.unit_name)
|
const unitId = getOrCreateUnit(input.unit_name)
|
||||||
|
|
||||||
|
|
@ -196,6 +195,7 @@ export function createProductsRepo(): { repo: ProductsRepo } {
|
||||||
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,
|
||||||
hidePriceFromPublic: input.hide_price_from_public ?? false,
|
hidePriceFromPublic: input.hide_price_from_public ?? false,
|
||||||
|
enterpriseId,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.get()
|
.get()
|
||||||
|
|
@ -214,8 +214,8 @@ export function createProductsRepo(): { repo: ProductsRepo } {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProduct(id, patch) {
|
updateProduct(id, patch, enterpriseId) {
|
||||||
const existing = db.select().from(products).where(eq(products.id, id)).get()
|
const existing = db.select().from(products).where(and(eq(products.id, id), eq(products.enterpriseId, enterpriseId))).get()
|
||||||
if (!existing) return Promise.resolve(null)
|
if (!existing) return Promise.resolve(null)
|
||||||
|
|
||||||
sqlite.transaction(() => {
|
sqlite.transaction(() => {
|
||||||
|
|
@ -236,7 +236,7 @@ export function createProductsRepo(): { repo: ProductsRepo } {
|
||||||
if (patch.hide_price_from_public !== undefined) setData.hidePriceFromPublic = patch.hide_price_from_public
|
if (patch.hide_price_from_public !== undefined) setData.hidePriceFromPublic = patch.hide_price_from_public
|
||||||
|
|
||||||
if (Object.keys(setData).length > 0) {
|
if (Object.keys(setData).length > 0) {
|
||||||
db.update(products).set(setData).where(eq(products.id, id)).run()
|
db.update(products).set(setData).where(and(eq(products.id, id), eq(products.enterpriseId, enterpriseId))).run()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patch.compositions) {
|
if (patch.compositions) {
|
||||||
|
|
@ -244,7 +244,7 @@ export function createProductsRepo(): { repo: ProductsRepo } {
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const updated = db.select().from(products).where(eq(products.id, id)).get()!
|
const updated = db.select().from(products).where(and(eq(products.id, id), eq(products.enterpriseId, enterpriseId))).get()!
|
||||||
const distributor = fetchDistributor(updated.distributorId)
|
const distributor = fetchDistributor(updated.distributorId)
|
||||||
const unit = fetchUnit(updated.unitId)
|
const unit = fetchUnit(updated.unitId)
|
||||||
|
|
||||||
|
|
@ -254,9 +254,9 @@ export function createProductsRepo(): { repo: ProductsRepo } {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteProduct(id) {
|
deleteProduct(id, enterpriseId) {
|
||||||
db.delete(productCompositions).where(eq(productCompositions.productId, id)).run()
|
db.delete(productCompositions).where(eq(productCompositions.productId, id)).run()
|
||||||
const deleted = db.delete(products).where(eq(products.id, id)).returning({ id: products.id }).get()
|
const deleted = db.delete(products).where(and(eq(products.id, id), eq(products.enterpriseId, enterpriseId))).returning({ id: products.id }).get()
|
||||||
return Promise.resolve(Boolean(deleted))
|
return Promise.resolve(Boolean(deleted))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq, and } from 'drizzle-orm'
|
||||||
|
|
||||||
import { db, sqlite } from './db-instance'
|
import { db, sqlite } from './db-instance'
|
||||||
import { stockBatches } from './schema/stockBatches'
|
import { stockBatches } from './schema/stockBatches'
|
||||||
|
|
@ -34,11 +34,11 @@ export type CreateStockBatchInput = {
|
||||||
export type UpdateStockBatchPatch = Partial<CreateStockBatchInput>
|
export type UpdateStockBatchPatch = Partial<CreateStockBatchInput>
|
||||||
|
|
||||||
export type StockBatchesRepo = {
|
export type StockBatchesRepo = {
|
||||||
getStockBatches: () => Promise<StockBatch[]>
|
getStockBatches: (enterpriseId: number) => Promise<StockBatch[]>
|
||||||
getStockBatchById: (id: number) => Promise<StockBatch | null>
|
getStockBatchById: (id: number, enterpriseId: number) => Promise<StockBatch | null>
|
||||||
createStockBatch: (input: CreateStockBatchInput) => Promise<StockBatch>
|
createStockBatch: (input: CreateStockBatchInput, enterpriseId: number) => Promise<StockBatch>
|
||||||
updateStockBatch: (id: number, patch: UpdateStockBatchPatch) => Promise<StockBatch | null>
|
updateStockBatch: (id: number, patch: UpdateStockBatchPatch, enterpriseId: number) => Promise<StockBatch | null>
|
||||||
deleteStockBatch: (id: number) => Promise<boolean>
|
deleteStockBatch: (id: number, enterpriseId: number) => Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchProduct(productId: number): { id: number; name: string; brand: string } | null {
|
function fetchProduct(productId: number): { id: number; name: string; brand: string } | null {
|
||||||
|
|
@ -78,22 +78,22 @@ function toStockBatch(row: typeof stockBatches.$inferSelect): StockBatch {
|
||||||
|
|
||||||
export function createStockBatchesRepo(): { repo: StockBatchesRepo } {
|
export function createStockBatchesRepo(): { repo: StockBatchesRepo } {
|
||||||
const repo: StockBatchesRepo = {
|
const repo: StockBatchesRepo = {
|
||||||
getStockBatches() {
|
getStockBatches(enterpriseId) {
|
||||||
const rows = db.select().from(stockBatches).all()
|
const rows = db.select().from(stockBatches).where(eq(stockBatches.enterpriseId, enterpriseId)).all()
|
||||||
return Promise.resolve(rows.map(toStockBatch))
|
return Promise.resolve(rows.map(toStockBatch))
|
||||||
},
|
},
|
||||||
|
|
||||||
getStockBatchById(id) {
|
getStockBatchById(id, enterpriseId) {
|
||||||
const row = db.select().from(stockBatches).where(eq(stockBatches.id, id)).get()
|
const row = db.select().from(stockBatches).where(and(eq(stockBatches.id, id), eq(stockBatches.enterpriseId, enterpriseId))).get()
|
||||||
return Promise.resolve(row ? toStockBatch(row) : null)
|
return Promise.resolve(row ? toStockBatch(row) : null)
|
||||||
},
|
},
|
||||||
|
|
||||||
createStockBatch(input) {
|
createStockBatch(input, enterpriseId) {
|
||||||
const result = sqlite.transaction(() => {
|
const result = sqlite.transaction(() => {
|
||||||
if (input.is_default) {
|
if (input.is_default) {
|
||||||
db.update(stockBatches)
|
db.update(stockBatches)
|
||||||
.set({ isDefault: false })
|
.set({ isDefault: false })
|
||||||
.where(eq(stockBatches.productId, input.product_id))
|
.where(and(eq(stockBatches.productId, input.product_id), eq(stockBatches.enterpriseId, enterpriseId)))
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,6 +109,7 @@ export function createStockBatchesRepo(): { repo: StockBatchesRepo } {
|
||||||
distributorId: input.distributor_id ?? null,
|
distributorId: input.distributor_id ?? null,
|
||||||
quantity: input.quantity ?? 0,
|
quantity: input.quantity ?? 0,
|
||||||
isDefault: input.is_default ?? false,
|
isDefault: input.is_default ?? false,
|
||||||
|
enterpriseId,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.get()
|
.get()
|
||||||
|
|
@ -119,15 +120,15 @@ export function createStockBatchesRepo(): { repo: StockBatchesRepo } {
|
||||||
return Promise.resolve(toStockBatch(result))
|
return Promise.resolve(toStockBatch(result))
|
||||||
},
|
},
|
||||||
|
|
||||||
updateStockBatch(id, patch) {
|
updateStockBatch(id, patch, enterpriseId) {
|
||||||
const existing = db.select().from(stockBatches).where(eq(stockBatches.id, id)).get()
|
const existing = db.select().from(stockBatches).where(and(eq(stockBatches.id, id), eq(stockBatches.enterpriseId, enterpriseId))).get()
|
||||||
if (!existing) return Promise.resolve(null)
|
if (!existing) return Promise.resolve(null)
|
||||||
|
|
||||||
sqlite.transaction(() => {
|
sqlite.transaction(() => {
|
||||||
if (patch.is_default) {
|
if (patch.is_default) {
|
||||||
db.update(stockBatches)
|
db.update(stockBatches)
|
||||||
.set({ isDefault: false })
|
.set({ isDefault: false })
|
||||||
.where(eq(stockBatches.productId, patch.product_id ?? existing.productId))
|
.where(and(eq(stockBatches.productId, patch.product_id ?? existing.productId), eq(stockBatches.enterpriseId, enterpriseId)))
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,16 +144,16 @@ export function createStockBatchesRepo(): { repo: StockBatchesRepo } {
|
||||||
if (patch.is_default !== undefined) setData.isDefault = patch.is_default
|
if (patch.is_default !== undefined) setData.isDefault = patch.is_default
|
||||||
|
|
||||||
if (Object.keys(setData).length > 0) {
|
if (Object.keys(setData).length > 0) {
|
||||||
db.update(stockBatches).set(setData).where(eq(stockBatches.id, id)).run()
|
db.update(stockBatches).set(setData).where(and(eq(stockBatches.id, id), eq(stockBatches.enterpriseId, enterpriseId))).run()
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const updated = db.select().from(stockBatches).where(eq(stockBatches.id, id)).get()!
|
const updated = db.select().from(stockBatches).where(and(eq(stockBatches.id, id), eq(stockBatches.enterpriseId, enterpriseId))).get()!
|
||||||
return Promise.resolve(toStockBatch(updated))
|
return Promise.resolve(toStockBatch(updated))
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteStockBatch(id) {
|
deleteStockBatch(id, enterpriseId) {
|
||||||
const deleted = db.delete(stockBatches).where(eq(stockBatches.id, id)).returning({ id: stockBatches.id }).get()
|
const deleted = db.delete(stockBatches).where(and(eq(stockBatches.id, id), eq(stockBatches.enterpriseId, enterpriseId))).returning({ id: stockBatches.id }).get()
|
||||||
return Promise.resolve(Boolean(deleted))
|
return Promise.resolve(Boolean(deleted))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq, and } from 'drizzle-orm'
|
||||||
|
|
||||||
import { db } from './db-instance'
|
import { db } from './db-instance'
|
||||||
import { storageSpaces } from './schema/storageSpacesSchema'
|
import { storageSpaces } from './schema/storageSpacesSchema'
|
||||||
|
|
@ -22,11 +22,11 @@ export type CreateStorageSpaceInput = {
|
||||||
export type UpdateStorageSpacePatch = Partial<CreateStorageSpaceInput>
|
export type UpdateStorageSpacePatch = Partial<CreateStorageSpaceInput>
|
||||||
|
|
||||||
export type StorageSpacesRepo = {
|
export type StorageSpacesRepo = {
|
||||||
getStorageSpaces: () => Promise<StorageSpace[]>
|
getStorageSpaces: (enterpriseId: number) => Promise<StorageSpace[]>
|
||||||
getStorageSpaceById: (id: number) => Promise<StorageSpace | null>
|
getStorageSpaceById: (id: number, enterpriseId: number) => Promise<StorageSpace | null>
|
||||||
createStorageSpace: (input: CreateStorageSpaceInput) => Promise<StorageSpace>
|
createStorageSpace: (input: CreateStorageSpaceInput, enterpriseId: number) => Promise<StorageSpace>
|
||||||
updateStorageSpace: (id: number, patch: UpdateStorageSpacePatch) => Promise<StorageSpace | null>
|
updateStorageSpace: (id: number, patch: UpdateStorageSpacePatch, enterpriseId: number) => Promise<StorageSpace | null>
|
||||||
deleteStorageSpace: (id: number) => Promise<boolean>
|
deleteStorageSpace: (id: number, enterpriseId: number) => Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
function toStorageSpace(row: {
|
function toStorageSpace(row: {
|
||||||
|
|
@ -50,21 +50,25 @@ export function createStorageSpacesRepo(): {
|
||||||
} {
|
} {
|
||||||
|
|
||||||
const repo: StorageSpacesRepo = {
|
const repo: StorageSpacesRepo = {
|
||||||
getStorageSpaces() {
|
getStorageSpaces(enterpriseId) {
|
||||||
const rows = db.select().from(storageSpaces).all()
|
const rows = db
|
||||||
|
.select()
|
||||||
|
.from(storageSpaces)
|
||||||
|
.where(eq(storageSpaces.enterpriseId, enterpriseId))
|
||||||
|
.all()
|
||||||
return Promise.resolve(rows.map(toStorageSpace))
|
return Promise.resolve(rows.map(toStorageSpace))
|
||||||
},
|
},
|
||||||
|
|
||||||
getStorageSpaceById(id) {
|
getStorageSpaceById(id, enterpriseId) {
|
||||||
const row = db
|
const row = db
|
||||||
.select()
|
.select()
|
||||||
.from(storageSpaces)
|
.from(storageSpaces)
|
||||||
.where(eq(storageSpaces.id, id))
|
.where(and(eq(storageSpaces.id, id), eq(storageSpaces.enterpriseId, enterpriseId)))
|
||||||
.get()
|
.get()
|
||||||
return Promise.resolve(row ? toStorageSpace(row) : null)
|
return Promise.resolve(row ? toStorageSpace(row) : null)
|
||||||
},
|
},
|
||||||
|
|
||||||
createStorageSpace(input) {
|
createStorageSpace(input, enterpriseId) {
|
||||||
const created = db
|
const created = db
|
||||||
.insert(storageSpaces)
|
.insert(storageSpaces)
|
||||||
.values({
|
.values({
|
||||||
|
|
@ -72,13 +76,14 @@ export function createStorageSpacesRepo(): {
|
||||||
description: input.description ?? null,
|
description: input.description ?? null,
|
||||||
aliases: serializeStringArrJson(input.aliases),
|
aliases: serializeStringArrJson(input.aliases),
|
||||||
imageUrls: serializeStringArrJson(input.image_urls),
|
imageUrls: serializeStringArrJson(input.image_urls),
|
||||||
|
enterpriseId,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.get()
|
.get()
|
||||||
return Promise.resolve(toStorageSpace(created))
|
return Promise.resolve(toStorageSpace(created))
|
||||||
},
|
},
|
||||||
|
|
||||||
updateStorageSpace(id, patch) {
|
updateStorageSpace(id, patch, enterpriseId) {
|
||||||
const updated = db
|
const updated = db
|
||||||
.update(storageSpaces)
|
.update(storageSpaces)
|
||||||
.set({
|
.set({
|
||||||
|
|
@ -91,16 +96,16 @@ export function createStorageSpacesRepo(): {
|
||||||
? { imageUrls: serializeStringArrJson(patch.image_urls) }
|
? { imageUrls: serializeStringArrJson(patch.image_urls) }
|
||||||
: {}),
|
: {}),
|
||||||
})
|
})
|
||||||
.where(eq(storageSpaces.id, id))
|
.where(and(eq(storageSpaces.id, id), eq(storageSpaces.enterpriseId, enterpriseId)))
|
||||||
.returning()
|
.returning()
|
||||||
.get()
|
.get()
|
||||||
return Promise.resolve(updated ? toStorageSpace(updated) : null)
|
return Promise.resolve(updated ? toStorageSpace(updated) : null)
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteStorageSpace(id) {
|
deleteStorageSpace(id, enterpriseId) {
|
||||||
const deleted = db
|
const deleted = db
|
||||||
.delete(storageSpaces)
|
.delete(storageSpaces)
|
||||||
.where(eq(storageSpaces.id, id))
|
.where(and(eq(storageSpaces.id, id), eq(storageSpaces.enterpriseId, enterpriseId)))
|
||||||
.returning({ id: storageSpaces.id })
|
.returning({ id: storageSpaces.id })
|
||||||
.get()
|
.get()
|
||||||
return Promise.resolve(Boolean(deleted))
|
return Promise.resolve(Boolean(deleted))
|
||||||
|
|
|
||||||
24
packages/shared-react/src/auth-gate.tsx
Normal file
24
packages/shared-react/src/auth-gate.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { useEffect, type ReactNode } from "react";
|
||||||
|
import { useWhoAmI, useAuthStore } from "./auth";
|
||||||
|
|
||||||
|
export function AuthGate({ children }: { children: ReactNode }) {
|
||||||
|
const { data, isLoading } = useWhoAmI();
|
||||||
|
const setUser = useAuthStore((s) => s.setUser);
|
||||||
|
const setLoading = useAuthStore((s) => s.setLoading);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(isLoading);
|
||||||
|
}, [isLoading, setLoading]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading) {
|
||||||
|
if (data) {
|
||||||
|
setUser(data.staff, data.enterprise);
|
||||||
|
} else {
|
||||||
|
setUser(null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data, isLoading, setUser]);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
48
packages/shared-react/src/auth.ts
Normal file
48
packages/shared-react/src/auth.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { trpc } from "./trpc";
|
||||||
|
|
||||||
|
export interface AuthStaff {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
username: string;
|
||||||
|
isPasswordResetNeeded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthEnterprise {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
staff: AuthStaff | null;
|
||||||
|
enterprise: AuthEnterprise | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
setUser: (
|
||||||
|
staff: AuthStaff | null,
|
||||||
|
enterprise: AuthEnterprise | null,
|
||||||
|
) => void;
|
||||||
|
setLoading: (loading: boolean) => void;
|
||||||
|
logout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthState>((set) => ({
|
||||||
|
staff: null,
|
||||||
|
enterprise: null,
|
||||||
|
isLoading: true,
|
||||||
|
setUser: (staff, enterprise) =>
|
||||||
|
set({ staff, enterprise, isLoading: false }),
|
||||||
|
setLoading: (isLoading) => set({ isLoading }),
|
||||||
|
logout: () => set({ staff: null, enterprise: null, isLoading: false }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function useWhoAmI() {
|
||||||
|
return trpc.auth.whoAmI.useQuery(undefined, {
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLogin() {
|
||||||
|
return trpc.auth.login.useMutation();
|
||||||
|
}
|
||||||
|
|
@ -8,3 +8,6 @@ export * from './hooks/drugInfo'
|
||||||
export * from './hooks/units'
|
export * from './hooks/units'
|
||||||
export * from './hooks/stockBatches'
|
export * from './hooks/stockBatches'
|
||||||
export { trpc } from './trpc'
|
export { trpc } from './trpc'
|
||||||
|
export { useAuthStore, useWhoAmI, useLogin } from './auth'
|
||||||
|
export type { AuthStaff, AuthEnterprise } from './auth'
|
||||||
|
export { AuthGate } from './auth-gate'
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ export function createTrpcClient(baseUrl: string) {
|
||||||
links: [
|
links: [
|
||||||
httpBatchLink({
|
httpBatchLink({
|
||||||
url: `${baseUrl}/trpc`,
|
url: `${baseUrl}/trpc`,
|
||||||
|
fetch: (url, options) =>
|
||||||
|
fetch(url, { ...options, credentials: "include" }),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue