From 302866dc580fc9239ffabb67b6deb9fb31aad692 Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Sun, 24 May 2026 00:03:44 +0530 Subject: [PATCH] user and roles functional --- apps/backend/src/lib/data-manager.ts | 5 + apps/backend/src/trpc/pharmanager/v1/roles.ts | 14 + .../trpc/pharmanager/v1/staffManagement.ts | 72 ++ apps/backend/src/trpc/router.ts | 4 + apps/pharmanager/src/components/ui/Select.tsx | 157 +++ apps/pharmanager/src/components/ui/index.ts | 2 + apps/pharmanager/src/main.tsx | 2 +- apps/pharmanager/src/routeTree.gen.ts | 81 +- apps/pharmanager/src/routes/products/add.tsx | 48 +- apps/pharmanager/src/routes/staff.tsx | 12 +- apps/pharmanager/src/routes/staff/$id.tsx | 140 +++ apps/pharmanager/src/routes/staff/add.tsx | 192 +++ apps/pharmanager/src/routes/staff/index.tsx | 171 +++ apps/pharmanager/src/routes/stock/add.tsx | 26 +- .../drizzle/0006_nappy_sir_ram.sql | 3 + .../drizzle/meta/0006_snapshot.json | 1026 +++++++++++++++++ .../drizzle/meta/_journal.json | 7 + .../src/enterpriseStaff.ts | 6 + packages/data-manager-sqlite/src/index.ts | 15 + .../data-manager-sqlite/src/permissions.ts | 48 + packages/data-manager-sqlite/src/roles.ts | 50 + .../src/schema/enterpriseStaff.ts | 6 +- .../src/schema/rolePermissions.ts | 6 +- .../src/schema/staffRoles.ts | 6 +- packages/data-manager-sqlite/src/staff.ts | 93 +- .../data-manager-sqlite/src/staffRoles.ts | 48 + packages/shared-react/src/hooks/roles.ts | 5 + .../shared-react/src/hooks/staffManagement.ts | 21 + packages/shared-react/src/index.ts | 2 + 29 files changed, 2190 insertions(+), 78 deletions(-) create mode 100644 apps/backend/src/trpc/pharmanager/v1/roles.ts create mode 100644 apps/backend/src/trpc/pharmanager/v1/staffManagement.ts create mode 100644 apps/pharmanager/src/components/ui/Select.tsx create mode 100644 apps/pharmanager/src/routes/staff/$id.tsx create mode 100644 apps/pharmanager/src/routes/staff/add.tsx create mode 100644 apps/pharmanager/src/routes/staff/index.tsx create mode 100644 packages/data-manager-sqlite/drizzle/0006_nappy_sir_ram.sql create mode 100644 packages/data-manager-sqlite/drizzle/meta/0006_snapshot.json create mode 100644 packages/data-manager-sqlite/src/permissions.ts create mode 100644 packages/data-manager-sqlite/src/roles.ts create mode 100644 packages/data-manager-sqlite/src/staffRoles.ts create mode 100644 packages/shared-react/src/hooks/roles.ts create mode 100644 packages/shared-react/src/hooks/staffManagement.ts diff --git a/apps/backend/src/lib/data-manager.ts b/apps/backend/src/lib/data-manager.ts index 79b8265..4a8e0c0 100644 --- a/apps/backend/src/lib/data-manager.ts +++ b/apps/backend/src/lib/data-manager.ts @@ -8,6 +8,7 @@ import { createEnterpriseRepo, createStaffRepo, createEnterpriseStaffRepo, + createRolesRepo, type StorageSpacesRepo, type DistributorsRepo, type ProductsRepo, @@ -17,6 +18,7 @@ import { type EnterpriseRepo, type StaffRepo, type EnterpriseStaffRepo, + type RolesRepo, } from "data-manager-sqlite"; export class DataManager { @@ -29,6 +31,7 @@ export class DataManager { readonly enterprises: EnterpriseRepo; readonly staff: StaffRepo; readonly enterpriseStaff: EnterpriseStaffRepo; + readonly roles: RolesRepo; constructor() { const { repo: storageSpacesRepo } = createStorageSpacesRepo(); @@ -40,6 +43,7 @@ export class DataManager { const { repo: enterpriseRepo } = createEnterpriseRepo(); const { repo: staffRepo } = createStaffRepo(); const { repo: enterpriseStaffRepo } = createEnterpriseStaffRepo(); + const { repo: rolesRepo } = createRolesRepo(); this.storageSpaces = storageSpacesRepo; this.distributors = distributorsRepo; @@ -50,5 +54,6 @@ export class DataManager { this.enterprises = enterpriseRepo; this.staff = staffRepo; this.enterpriseStaff = enterpriseStaffRepo; + this.roles = rolesRepo; } } diff --git a/apps/backend/src/trpc/pharmanager/v1/roles.ts b/apps/backend/src/trpc/pharmanager/v1/roles.ts new file mode 100644 index 0000000..ab72161 --- /dev/null +++ b/apps/backend/src/trpc/pharmanager/v1/roles.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; +import { protectedProcedure, router } from "../../init"; +import { dataManager } from "../../../lib/data-manager-instance"; + +export const RoleSchema = z.object({ + id: z.number().int(), + name: z.string(), +}); + +export const rolesRouter = router({ + list: protectedProcedure + .output(z.array(RoleSchema)) + .query(() => dataManager.roles.listRoles()), +}); diff --git a/apps/backend/src/trpc/pharmanager/v1/staffManagement.ts b/apps/backend/src/trpc/pharmanager/v1/staffManagement.ts new file mode 100644 index 0000000..e896cb2 --- /dev/null +++ b/apps/backend/src/trpc/pharmanager/v1/staffManagement.ts @@ -0,0 +1,72 @@ +import { z } from "zod"; +import { protectedProcedure, router } from "../../init"; +import { dataManager } from "../../../lib/data-manager-instance"; + +export const StaffRoleSchema = z.object({ id: z.number().int(), name: z.string() }); + +export const StaffWithRolesSchema = z.object({ + id: z.number().int(), + name: z.string(), + username: z.string(), + email: z.string().nullable(), + mobile: z.string().nullable(), + added_on: z.string(), + is_password_reset_needed: z.boolean(), + roles: z.array(StaffRoleSchema), +}); + +export const CreateStaffInput = z.object({ + name: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + email: z.string().nullable().optional(), + mobile: z.string().nullable().optional(), + added_on: z.string(), + role_ids: z.array(z.number().int()).default([]), +}); + +export const UpdateStaffInput = z.object({ + id: z.number().int(), + name: z.string().min(1).optional(), + username: z.string().min(1).optional(), + email: z.string().nullable().optional(), + mobile: z.string().nullable().optional(), + is_password_reset_needed: z.boolean().optional(), + role_ids: z.array(z.number().int()).optional(), +}); + +export const staffManagementRouter = router({ + list: protectedProcedure + .output(z.array(StaffWithRolesSchema)) + .query(({ ctx }) => + dataManager.staff.getStaffByEnterprise(ctx.staff.enterpriseId), + ), + + byId: protectedProcedure + .input(z.object({ id: z.number().int() })) + .output(StaffWithRolesSchema.nullable()) + .query(({ input }) => dataManager.staff.getStaffById(input.id)), + + create: protectedProcedure + .input(CreateStaffInput) + .output(StaffWithRolesSchema) + .mutation(({ ctx, input }) => + dataManager.staff.createStaff(input, ctx.staff.enterpriseId), + ), + + update: protectedProcedure + .input(UpdateStaffInput) + .output(StaffWithRolesSchema.nullable()) + .mutation(({ input }) => { + const { id, ...patch } = input; + return dataManager.staff.updateStaff(id, patch); + }), + + remove: protectedProcedure + .input(z.object({ id: z.number().int() })) + .output(z.object({ ok: z.boolean() })) + .mutation(async ({ input }) => { + const ok = await dataManager.staff.deleteStaff(input.id); + return { ok }; + }), +}); diff --git a/apps/backend/src/trpc/router.ts b/apps/backend/src/trpc/router.ts index 261accf..09bc678 100644 --- a/apps/backend/src/trpc/router.ts +++ b/apps/backend/src/trpc/router.ts @@ -6,6 +6,8 @@ import { drugInfoRouter } from "./pharmanager/v1/drugInfo"; import { unitsRouter } from "./pharmanager/v1/units"; import { stockRouter } from "./pharmanager/v1/stock"; import { authRouter } from "./pharmanager/v1/auth"; +import { staffManagementRouter } from "./pharmanager/v1/staffManagement"; +import { rolesRouter } from "./pharmanager/v1/roles"; export const appRouter = router({ storage: storageRouter, @@ -15,6 +17,8 @@ export const appRouter = router({ units: unitsRouter, stock: stockRouter, auth: authRouter, + staffManagement: staffManagementRouter, + roles: rolesRouter, }); export type AppRouter = typeof appRouter; diff --git a/apps/pharmanager/src/components/ui/Select.tsx b/apps/pharmanager/src/components/ui/Select.tsx new file mode 100644 index 0000000..9f496d6 --- /dev/null +++ b/apps/pharmanager/src/components/ui/Select.tsx @@ -0,0 +1,157 @@ +import { + forwardRef, + useState, + useRef, + useEffect, + type ComponentPropsWithoutRef, +} from "react"; +import { ChevronsUpDown, X } from "lucide-react"; +import { cn } from "#/lib/cn"; +import { Checkbox } from "./Checkbox"; + +export interface SelectOption { + value: string; + label: string; +} + +interface SelectBaseProps { + options: SelectOption[]; + multiple?: boolean; + placeholder?: string; + value?: string | string[]; + onChangeValue?: (value: string | string[]) => void; +} + +export type SelectProps = Omit< + ComponentPropsWithoutRef<"select">, + "multiple" +> & + SelectBaseProps; + +export const Select = forwardRef( + ( + { + options, + multiple, + placeholder, + value: controlledValue, + onChangeValue, + className, + ...props + }, + ref, + ) => { + const [open, setOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if ( + containerRef.current && + !containerRef.current.contains(e.target as Node) + ) { + setOpen(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => + document.removeEventListener("mousedown", handleClickOutside); + }, []); + + if (multiple) { + const selected = Array.isArray(controlledValue) + ? controlledValue + : []; + const selectedLabels = options + .filter((o) => selected.includes(o.value)) + .map((o) => o.label); + + function toggle(val: string) { + const next = selected.includes(val) + ? selected.filter((v) => v !== val) + : [...selected, val]; + onChangeValue?.(next); + } + + return ( +
+ + + )) + ) : ( + + {placeholder || "Select..."} + + )} +
+ + + + {open && ( +
+
+ {options.map((opt) => ( + toggle(opt.value)} + label={opt.label} + /> + ))} +
+
+ )} + + ); + } + + return ( + + ); + }, +); + +Select.displayName = "Select"; diff --git a/apps/pharmanager/src/components/ui/index.ts b/apps/pharmanager/src/components/ui/index.ts index 2e1f09c..f23c126 100644 --- a/apps/pharmanager/src/components/ui/index.ts +++ b/apps/pharmanager/src/components/ui/index.ts @@ -10,3 +10,5 @@ export { BackLink } from "./BackLink"; export { FormField } from "./FormField"; export { EmptyState } from "./EmptyState"; export { SearchToolbar } from "./SearchToolbar"; +export { Select } from "./Select"; +export type { SelectOption } from "./Select"; diff --git a/apps/pharmanager/src/main.tsx b/apps/pharmanager/src/main.tsx index 22265f0..c70630d 100644 --- a/apps/pharmanager/src/main.tsx +++ b/apps/pharmanager/src/main.tsx @@ -21,7 +21,7 @@ if (!rootElement) throw new Error("Root element not found"); if (!rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); root.render( - + , ); diff --git a/apps/pharmanager/src/routeTree.gen.ts b/apps/pharmanager/src/routeTree.gen.ts index 36933f7..b8ed1a5 100644 --- a/apps/pharmanager/src/routeTree.gen.ts +++ b/apps/pharmanager/src/routeTree.gen.ts @@ -21,12 +21,15 @@ import { Route as BillingRouteImport } from './routes/billing' import { Route as IndexRouteImport } from './routes/index' import { Route as StorageIndexRouteImport } from './routes/storage/index' import { Route as StockIndexRouteImport } from './routes/stock/index' +import { Route as StaffIndexRouteImport } from './routes/staff/index' import { Route as ProductsIndexRouteImport } from './routes/products/index' import { Route as DistributorsIndexRouteImport } from './routes/distributors/index' import { Route as StorageAddRouteImport } from './routes/storage/add' 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 StaffAddRouteImport } from './routes/staff/add' +import { Route as StaffIdRouteImport } from './routes/staff/$id' import { Route as ProductsAddRouteImport } from './routes/products/add' import { Route as ProductsIdRouteImport } from './routes/products/$id' import { Route as DistributorsAddRouteImport } from './routes/distributors/add' @@ -92,6 +95,11 @@ const StockIndexRoute = StockIndexRouteImport.update({ path: '/', getParentRoute: () => StockRoute, } as any) +const StaffIndexRoute = StaffIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => StaffRoute, +} as any) const ProductsIndexRoute = ProductsIndexRouteImport.update({ id: '/', path: '/', @@ -122,6 +130,16 @@ const StockIdRoute = StockIdRouteImport.update({ path: '/$id', getParentRoute: () => StockRoute, } as any) +const StaffAddRoute = StaffAddRouteImport.update({ + id: '/add', + path: '/add', + getParentRoute: () => StaffRoute, +} as any) +const StaffIdRoute = StaffIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => StaffRoute, +} as any) const ProductsAddRoute = ProductsAddRouteImport.update({ id: '/add', path: '/add', @@ -151,19 +169,22 @@ export interface FileRoutesByFullPath { '/login': typeof LoginRoute '/products': typeof ProductsRouteWithChildren '/profile': typeof ProfileRoute - '/staff': typeof StaffRoute + '/staff': typeof StaffRouteWithChildren '/stock': typeof StockRouteWithChildren '/storage': typeof StorageRouteWithChildren '/distributors/$id': typeof DistributorsIdRoute '/distributors/add': typeof DistributorsAddRoute '/products/$id': typeof ProductsIdRoute '/products/add': typeof ProductsAddRoute + '/staff/$id': typeof StaffIdRoute + '/staff/add': typeof StaffAddRoute '/stock/$id': typeof StockIdRoute '/stock/add': typeof StockAddRoute '/storage/$id': typeof StorageIdRoute '/storage/add': typeof StorageAddRoute '/distributors/': typeof DistributorsIndexRoute '/products/': typeof ProductsIndexRoute + '/staff/': typeof StaffIndexRoute '/stock/': typeof StockIndexRoute '/storage/': typeof StorageIndexRoute } @@ -173,17 +194,19 @@ export interface FileRoutesByTo { '/customers': typeof CustomersRoute '/login': typeof LoginRoute '/profile': typeof ProfileRoute - '/staff': typeof StaffRoute '/distributors/$id': typeof DistributorsIdRoute '/distributors/add': typeof DistributorsAddRoute '/products/$id': typeof ProductsIdRoute '/products/add': typeof ProductsAddRoute + '/staff/$id': typeof StaffIdRoute + '/staff/add': typeof StaffAddRoute '/stock/$id': typeof StockIdRoute '/stock/add': typeof StockAddRoute '/storage/$id': typeof StorageIdRoute '/storage/add': typeof StorageAddRoute '/distributors': typeof DistributorsIndexRoute '/products': typeof ProductsIndexRoute + '/staff': typeof StaffIndexRoute '/stock': typeof StockIndexRoute '/storage': typeof StorageIndexRoute } @@ -196,19 +219,22 @@ export interface FileRoutesById { '/login': typeof LoginRoute '/products': typeof ProductsRouteWithChildren '/profile': typeof ProfileRoute - '/staff': typeof StaffRoute + '/staff': typeof StaffRouteWithChildren '/stock': typeof StockRouteWithChildren '/storage': typeof StorageRouteWithChildren '/distributors/$id': typeof DistributorsIdRoute '/distributors/add': typeof DistributorsAddRoute '/products/$id': typeof ProductsIdRoute '/products/add': typeof ProductsAddRoute + '/staff/$id': typeof StaffIdRoute + '/staff/add': typeof StaffAddRoute '/stock/$id': typeof StockIdRoute '/stock/add': typeof StockAddRoute '/storage/$id': typeof StorageIdRoute '/storage/add': typeof StorageAddRoute '/distributors/': typeof DistributorsIndexRoute '/products/': typeof ProductsIndexRoute + '/staff/': typeof StaffIndexRoute '/stock/': typeof StockIndexRoute '/storage/': typeof StorageIndexRoute } @@ -229,12 +255,15 @@ export interface FileRouteTypes { | '/distributors/add' | '/products/$id' | '/products/add' + | '/staff/$id' + | '/staff/add' | '/stock/$id' | '/stock/add' | '/storage/$id' | '/storage/add' | '/distributors/' | '/products/' + | '/staff/' | '/stock/' | '/storage/' fileRoutesByTo: FileRoutesByTo @@ -244,17 +273,19 @@ export interface FileRouteTypes { | '/customers' | '/login' | '/profile' - | '/staff' | '/distributors/$id' | '/distributors/add' | '/products/$id' | '/products/add' + | '/staff/$id' + | '/staff/add' | '/stock/$id' | '/stock/add' | '/storage/$id' | '/storage/add' | '/distributors' | '/products' + | '/staff' | '/stock' | '/storage' id: @@ -273,12 +304,15 @@ export interface FileRouteTypes { | '/distributors/add' | '/products/$id' | '/products/add' + | '/staff/$id' + | '/staff/add' | '/stock/$id' | '/stock/add' | '/storage/$id' | '/storage/add' | '/distributors/' | '/products/' + | '/staff/' | '/stock/' | '/storage/' fileRoutesById: FileRoutesById @@ -291,7 +325,7 @@ export interface RootRouteChildren { LoginRoute: typeof LoginRoute ProductsRoute: typeof ProductsRouteWithChildren ProfileRoute: typeof ProfileRoute - StaffRoute: typeof StaffRoute + StaffRoute: typeof StaffRouteWithChildren StockRoute: typeof StockRouteWithChildren StorageRoute: typeof StorageRouteWithChildren } @@ -382,6 +416,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof StockIndexRouteImport parentRoute: typeof StockRoute } + '/staff/': { + id: '/staff/' + path: '/' + fullPath: '/staff/' + preLoaderRoute: typeof StaffIndexRouteImport + parentRoute: typeof StaffRoute + } '/products/': { id: '/products/' path: '/' @@ -424,6 +465,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof StockIdRouteImport parentRoute: typeof StockRoute } + '/staff/add': { + id: '/staff/add' + path: '/add' + fullPath: '/staff/add' + preLoaderRoute: typeof StaffAddRouteImport + parentRoute: typeof StaffRoute + } + '/staff/$id': { + id: '/staff/$id' + path: '/$id' + fullPath: '/staff/$id' + preLoaderRoute: typeof StaffIdRouteImport + parentRoute: typeof StaffRoute + } '/products/add': { id: '/products/add' path: '/add' @@ -487,6 +542,20 @@ const ProductsRouteWithChildren = ProductsRoute._addFileChildren( ProductsRouteChildren, ) +interface StaffRouteChildren { + StaffIdRoute: typeof StaffIdRoute + StaffAddRoute: typeof StaffAddRoute + StaffIndexRoute: typeof StaffIndexRoute +} + +const StaffRouteChildren: StaffRouteChildren = { + StaffIdRoute: StaffIdRoute, + StaffAddRoute: StaffAddRoute, + StaffIndexRoute: StaffIndexRoute, +} + +const StaffRouteWithChildren = StaffRoute._addFileChildren(StaffRouteChildren) + interface StockRouteChildren { StockIdRoute: typeof StockIdRoute StockAddRoute: typeof StockAddRoute @@ -524,7 +593,7 @@ const rootRouteChildren: RootRouteChildren = { LoginRoute: LoginRoute, ProductsRoute: ProductsRouteWithChildren, ProfileRoute: ProfileRoute, - StaffRoute: StaffRoute, + StaffRoute: StaffRouteWithChildren, StockRoute: StockRouteWithChildren, StorageRoute: StorageRouteWithChildren, } diff --git a/apps/pharmanager/src/routes/products/add.tsx b/apps/pharmanager/src/routes/products/add.tsx index 18e0475..ed98d44 100644 --- a/apps/pharmanager/src/routes/products/add.tsx +++ b/apps/pharmanager/src/routes/products/add.tsx @@ -13,7 +13,7 @@ import { useListDistributors, trpc, } from "shared-react"; -import { Button, Input, Checkbox, buttonVariants } from "#/components/ui"; +import { Button, Input, Checkbox, Select, buttonVariants } from "#/components/ui"; import { CreateProductInput } from "@repo/shared"; const formSchema = CreateProductInput.extend({ @@ -206,14 +206,10 @@ function AddProductPage() {
- + options={CATEGORIES.map((c) => ({ value: c, label: c }))} + />
{showUnitsPerStrip && ( @@ -236,29 +232,21 @@ function AddProductPage() { - + options={(unitList ?? []).map((u) => ({ value: u.name, label: u.name }))} + placeholder="Select..." + /> {errors.unit_name &&

{errors.unit_name.message}

}
- + options={(distributorList ?? []).map((d) => ({ value: String(d.id), label: d.agency }))} + placeholder="None" + />
{/* Composition */} @@ -294,15 +282,11 @@ function AddProductPage() {
- + options={(unitList ?? []).map((u) => ({ value: u.name, label: u.name }))} + placeholder="Select..." + />
+ + + + +
+
+

+ + Personal Info +

+ + + +
+ +
+

+ + Roles +

+ 0 ? staff.roles.map((r) => r.name).join(", ") : "No roles"} + last + /> +
+ +
+

+ + Employment +

+ + +
+
+ + ); +} diff --git a/apps/pharmanager/src/routes/staff/add.tsx b/apps/pharmanager/src/routes/staff/add.tsx new file mode 100644 index 0000000..7887f97 --- /dev/null +++ b/apps/pharmanager/src/routes/staff/add.tsx @@ -0,0 +1,192 @@ +import { useEffect } from "react"; +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; +import { Plus } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { + useCreateStaff, + useUpdateStaff, + useGetStaffById, + useListRoles, + trpc, +} from "shared-react"; +import { Button, Input, BackLink, Select, buttonVariants } from "#/components/ui"; + +const formSchema = z.object({ + name: z.string().min(1, "Name is required"), + username: z.string().min(1, "Username is required"), + password: z.string().min(1, "Password is required"), + email: z.string().nullable().optional(), + mobile: z.string().nullable().optional(), + added_on: z.string().min(1, "Date is required"), + role_ids: z.array(z.coerce.number().int()).default([]), +}); + +type FormValues = z.infer; + +export const Route = createFileRoute("/staff/add")({ + component: AddStaffPage, + validateSearch: (search: Record) => ({ + id: search.id ? Number(search.id) : undefined, + }), + staticData: { + title: "Add Staff", + subtitle: "Add a new staff member to the pharmacy", + }, +}); + +function AddStaffPage() { + const navigate = useNavigate(); + const { id: editId } = Route.useSearch(); + const createMutation = useCreateStaff(); + const updateMutation = useUpdateStaff(); + const { data: existingStaff } = useGetStaffById(editId ?? 0); + const utils = trpc.useUtils(); + const { data: roleList } = useListRoles(); + + const isEditing = typeof editId === "number" && editId > 0; + + const { + register, + handleSubmit, + reset, + watch, + setValue, + formState: { errors }, + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + username: "", + password: "", + email: null, + mobile: null, + added_on: new Date().toISOString().slice(0, 10), + role_ids: [], + }, + }); + + useEffect(() => { + if (isEditing && existingStaff) { + reset({ + name: existingStaff.name, + username: existingStaff.username, + password: "", + email: existingStaff.email, + mobile: existingStaff.mobile, + added_on: existingStaff.added_on, + role_ids: existingStaff.roles.map((r) => r.id), + }); + } + }, [isEditing, existingStaff, reset]); + + const selectedRoleIds: number[] = watch("role_ids") || []; + + function toggleRoles(newIds: string[]) { + setValue("role_ids", newIds.map(Number)); + } + + function onSubmit(values: FormValues) { + if (isEditing) { + updateMutation.mutate( + { id: editId!, ...values, password: values.password || undefined }, + { + onSuccess: () => { + utils.staffManagement.list.invalidate(); + utils.staffManagement.byId.invalidate({ id: editId! }); + navigate({ to: "/staff" }); + }, + }, + ); + } else { + createMutation.mutate(values, { + onSuccess: () => { + utils.staffManagement.list.invalidate(); + navigate({ to: "/staff" }); + }, + }); + } + } + + const mutation = isEditing ? updateMutation : createMutation; + + return ( +
+ + +
+

{isEditing ? "Edit Staff" : "Add Staff"}

+

+ {isEditing ? "Update staff member details." : "Add a new staff member to the pharmacy."} +

+ +
+
+ + + {errors.name &&

{errors.name.message}

} +
+ +
+ + + {errors.username &&

{errors.username.message}

} +
+ +
+ + + {errors.password &&

{errors.password.message}

} +
+ +
+ + v || null })} placeholder="johndoe@example.com" /> +
+ +
+ + v || null })} placeholder="9876543210" /> +
+ +
+ + + {errors.added_on &&

{errors.added_on.message}

} +
+ +
+ + - - {(racks ?? []).map((r) => ( - - ))} - + options={(racks ?? []).map((r) => ({ value: String(r.id), label: r.name }))} + placeholder="— Select a rack —" + />
- + options={(distributorList ?? []).map((d) => ({ value: String(d.id), label: d.agency }))} + placeholder="— Select a distributor —" + />
diff --git a/packages/data-manager-sqlite/drizzle/0006_nappy_sir_ram.sql b/packages/data-manager-sqlite/drizzle/0006_nappy_sir_ram.sql new file mode 100644 index 0000000..eb977ac --- /dev/null +++ b/packages/data-manager-sqlite/drizzle/0006_nappy_sir_ram.sql @@ -0,0 +1,3 @@ +CREATE UNIQUE INDEX `role_permissions_role_id_permission_id_unique` ON `role_permissions` (`role_id`,`permission_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `staff_roles_staff_id_role_id_unique` ON `staff_roles` (`staff_id`,`role_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `enterprise_staff_staff_id_enterprise_id_unique` ON `enterprise_staff` (`staff_id`,`enterprise_id`); \ No newline at end of file diff --git a/packages/data-manager-sqlite/drizzle/meta/0006_snapshot.json b/packages/data-manager-sqlite/drizzle/meta/0006_snapshot.json new file mode 100644 index 0000000..c8539a1 --- /dev/null +++ b/packages/data-manager-sqlite/drizzle/meta/0006_snapshot.json @@ -0,0 +1,1026 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "737f8f6d-432c-489e-84d0-54f906a6faca", + "prevId": "4ebc4f18-552b-459a-abef-dc2dfe600cd2", + "tables": { + "storage_spaces": { + "name": "storage_spaces", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aliases": { + "name": "aliases", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "image_urls": { + "name": "image_urls", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "storage_spaces_enterprise_idx": { + "name": "storage_spaces_enterprise_idx", + "columns": [ + "enterprise_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "storage_spaces_enterprise_id_enterprises_id_fk": { + "name": "storage_spaces_enterprise_id_enterprises_id_fk", + "tableFrom": "storage_spaces", + "tableTo": "enterprises", + "columnsFrom": [ + "enterprise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distributors": { + "name": "distributors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "agency": { + "name": "agency", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contact": { + "name": "contact", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mobile": { + "name": "mobile", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "distributors_enterprise_idx": { + "name": "distributors_enterprise_idx", + "columns": [ + "enterprise_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "distributors_enterprise_id_enterprises_id_fk": { + "name": "distributors_enterprise_id_enterprises_id_fk", + "tableFrom": "distributors", + "tableTo": "enterprises", + "columnsFrom": [ + "enterprise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "products": { + "name": "products", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distributor_id": { + "name": "distributor_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit_id": { + "name": "unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "procured_price": { + "name": "procured_price", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mrp": { + "name": "mrp", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "selling_price": { + "name": "selling_price", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reorder_level": { + "name": "reorder_level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "units_per_strip": { + "name": "units_per_strip", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hide_product_from_public": { + "name": "hide_product_from_public", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "hide_price_from_public": { + "name": "hide_price_from_public", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "products_enterprise_idx": { + "name": "products_enterprise_idx", + "columns": [ + "enterprise_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "products_distributor_id_distributors_id_fk": { + "name": "products_distributor_id_distributors_id_fk", + "tableFrom": "products", + "tableTo": "distributors", + "columnsFrom": [ + "distributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "products_unit_id_units_id_fk": { + "name": "products_unit_id_units_id_fk", + "tableFrom": "products", + "tableTo": "units", + "columnsFrom": [ + "unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "products_enterprise_id_enterprises_id_fk": { + "name": "products_enterprise_id_enterprises_id_fk", + "tableFrom": "products", + "tableTo": "enterprises", + "columnsFrom": [ + "enterprise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "drug_info": { + "name": "drug_info", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "drug_info_name_unique": { + "name": "drug_info_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "units": { + "name": "units", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "units_name_unique": { + "name": "units_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "product_compositions": { + "name": "product_compositions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "drug_info_id": { + "name": "drug_info_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unit_id": { + "name": "unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "product_compositions_product_id_products_id_fk": { + "name": "product_compositions_product_id_products_id_fk", + "tableFrom": "product_compositions", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "product_compositions_drug_info_id_drug_info_id_fk": { + "name": "product_compositions_drug_info_id_drug_info_id_fk", + "tableFrom": "product_compositions", + "tableTo": "drug_info", + "columnsFrom": [ + "drug_info_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "product_compositions_unit_id_units_id_fk": { + "name": "product_compositions_unit_id_units_id_fk", + "tableFrom": "product_compositions", + "tableTo": "units", + "columnsFrom": [ + "unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stock_batches": { + "name": "stock_batches", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "arrived": { + "name": "arrived", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "batch_no": { + "name": "batch_no", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mfg": { + "name": "mfg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiry": { + "name": "expiry", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rack_id": { + "name": "rack_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distributor_id": { + "name": "distributor_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "stock_batches_enterprise_idx": { + "name": "stock_batches_enterprise_idx", + "columns": [ + "enterprise_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "stock_batches_product_id_products_id_fk": { + "name": "stock_batches_product_id_products_id_fk", + "tableFrom": "stock_batches", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "stock_batches_rack_id_storage_spaces_id_fk": { + "name": "stock_batches_rack_id_storage_spaces_id_fk", + "tableFrom": "stock_batches", + "tableTo": "storage_spaces", + "columnsFrom": [ + "rack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "stock_batches_distributor_id_distributors_id_fk": { + "name": "stock_batches_distributor_id_distributors_id_fk", + "tableFrom": "stock_batches", + "tableTo": "distributors", + "columnsFrom": [ + "distributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "stock_batches_enterprise_id_enterprises_id_fk": { + "name": "stock_batches_enterprise_id_enterprises_id_fk", + "tableFrom": "stock_batches", + "tableTo": "enterprises", + "columnsFrom": [ + "enterprise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "enterprises": { + "name": "enterprises", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_name": { + "name": "owner_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mobile": { + "name": "mobile", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "staff": { + "name": "staff", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mobile": { + "name": "mobile", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "added_on": { + "name": "added_on", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_password_reset_needed": { + "name": "is_password_reset_needed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "staff_username_unique": { + "name": "staff_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "permissions": { + "name": "permissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "permissions_name_unique": { + "name": "permissions_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "role_permissions": { + "name": "role_permissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_id": { + "name": "permission_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "role_permissions_role_id_permission_id_unique": { + "name": "role_permissions_role_id_permission_id_unique", + "columns": [ + "role_id", + "permission_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "role_permissions_role_id_roles_id_fk": { + "name": "role_permissions_role_id_roles_id_fk", + "tableFrom": "role_permissions", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "role_permissions_permission_id_permissions_id_fk": { + "name": "role_permissions_permission_id_permissions_id_fk", + "tableFrom": "role_permissions", + "tableTo": "permissions", + "columnsFrom": [ + "permission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "staff_roles": { + "name": "staff_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "staff_id": { + "name": "staff_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "staff_roles_staff_id_role_id_unique": { + "name": "staff_roles_staff_id_role_id_unique", + "columns": [ + "staff_id", + "role_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "staff_roles_staff_id_staff_id_fk": { + "name": "staff_roles_staff_id_staff_id_fk", + "tableFrom": "staff_roles", + "tableTo": "staff", + "columnsFrom": [ + "staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "staff_roles_role_id_roles_id_fk": { + "name": "staff_roles_role_id_roles_id_fk", + "tableFrom": "staff_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "enterprise_staff": { + "name": "enterprise_staff", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "staff_id": { + "name": "staff_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "enterprise_staff_staff_id_enterprise_id_unique": { + "name": "enterprise_staff_staff_id_enterprise_id_unique", + "columns": [ + "staff_id", + "enterprise_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "enterprise_staff_staff_id_staff_id_fk": { + "name": "enterprise_staff_staff_id_staff_id_fk", + "tableFrom": "enterprise_staff", + "tableTo": "staff", + "columnsFrom": [ + "staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "enterprise_staff_enterprise_id_enterprises_id_fk": { + "name": "enterprise_staff_enterprise_id_enterprises_id_fk", + "tableFrom": "enterprise_staff", + "tableTo": "enterprises", + "columnsFrom": [ + "enterprise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/data-manager-sqlite/drizzle/meta/_journal.json b/packages/data-manager-sqlite/drizzle/meta/_journal.json index 63a801c..700fc61 100644 --- a/packages/data-manager-sqlite/drizzle/meta/_journal.json +++ b/packages/data-manager-sqlite/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1779546674621, "tag": "0005_yielding_silver_fox", "breakpoints": true + }, + { + "idx": 6, + "version": "6", + "when": 1779551911536, + "tag": "0006_nappy_sir_ram", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/data-manager-sqlite/src/enterpriseStaff.ts b/packages/data-manager-sqlite/src/enterpriseStaff.ts index 671ff00..62eb42a 100644 --- a/packages/data-manager-sqlite/src/enterpriseStaff.ts +++ b/packages/data-manager-sqlite/src/enterpriseStaff.ts @@ -11,6 +11,7 @@ export type EnterpriseStaff = { export type EnterpriseStaffRepo = { getByStaffId: (staffId: number) => EnterpriseStaff | null + listByEnterprise: (enterpriseId: number) => EnterpriseStaff[] create: (input: { staff_id: number; enterprise_id: number }) => EnterpriseStaff } @@ -29,6 +30,11 @@ export function createEnterpriseStaffRepo(): { repo: EnterpriseStaffRepo } { return row ? toEnterpriseStaff(row) : null }, + listByEnterprise(enterpriseId) { + const rows = db.select().from(enterpriseStaff).where(eq(enterpriseStaff.enterpriseId, enterpriseId)).all() + return rows.map(toEnterpriseStaff) + }, + create(input) { const created = db.insert(enterpriseStaff).values({ staffId: input.staff_id, diff --git a/packages/data-manager-sqlite/src/index.ts b/packages/data-manager-sqlite/src/index.ts index 893bcb8..17abf00 100644 --- a/packages/data-manager-sqlite/src/index.ts +++ b/packages/data-manager-sqlite/src/index.ts @@ -62,3 +62,18 @@ export { type EnterpriseStaff, type EnterpriseStaffRepo, } from './enterpriseStaff' +export { + createRolesRepo, + type Role, + type RolesRepo, +} from './roles' +export { + createPermissionsRepo, + type Permission, + type PermissionsRepo, +} from './permissions' +export { + createStaffRolesRepo, + type StaffRole, + type StaffRolesRepo, +} from './staffRoles' diff --git a/packages/data-manager-sqlite/src/permissions.ts b/packages/data-manager-sqlite/src/permissions.ts new file mode 100644 index 0000000..b0dc58e --- /dev/null +++ b/packages/data-manager-sqlite/src/permissions.ts @@ -0,0 +1,48 @@ +import { eq } from 'drizzle-orm' + +import { db } from './db-instance' +import { permissions } from './schema/permissions' +import { rolePermissions } from './schema/rolePermissions' + +export type Permission = { + id: number + name: string +} + +export type PermissionsRepo = { + listPermissions: () => Promise + getRolePermissions: (roleId: number) => Permission[] + setRolePermissions: (roleId: number, permIds: number[]) => void +} + +function toPermission(row: typeof permissions.$inferSelect): Permission { + return { id: row.id, name: row.name } +} + +export function createPermissionsRepo(): { repo: PermissionsRepo } { + const repo: PermissionsRepo = { + listPermissions() { + const rows = db.select().from(permissions).all() + return Promise.resolve(rows.map(toPermission)) + }, + getRolePermissions(roleId) { + const rows = db + .select({ + id: permissions.id, + name: permissions.name, + }) + .from(rolePermissions) + .innerJoin(permissions, eq(rolePermissions.permissionId, permissions.id)) + .where(eq(rolePermissions.roleId, roleId)) + .all() + return rows.map((r) => ({ id: r.id, name: r.name })) + }, + setRolePermissions(roleId, permIds) { + db.delete(rolePermissions).where(eq(rolePermissions.roleId, roleId)).run() + for (const permId of permIds) { + db.insert(rolePermissions).values({ roleId, permissionId: permId }).run() + } + }, + } + return { repo } +} diff --git a/packages/data-manager-sqlite/src/roles.ts b/packages/data-manager-sqlite/src/roles.ts new file mode 100644 index 0000000..e7569da --- /dev/null +++ b/packages/data-manager-sqlite/src/roles.ts @@ -0,0 +1,50 @@ +import { eq } from 'drizzle-orm' + +import { db } from './db-instance' +import { roles } from './schema/roles' + +export type Role = { + id: number + name: string +} + +export type RolesRepo = { + listRoles: () => Promise + getRoleById: (id: number) => Promise + createRole: (input: { name: string }) => Promise + updateRole: (id: number, input: { name: string }) => Promise + deleteRole: (id: number) => Promise +} + +function toRole(row: typeof roles.$inferSelect): Role { + return { id: row.id, name: row.name } +} + +export function createRolesRepo(): { repo: RolesRepo } { + const repo: RolesRepo = { + listRoles() { + const rows = db.select().from(roles).all() + return Promise.resolve(rows.map(toRole)) + }, + getRoleById(id) { + const row = db.select().from(roles).where(eq(roles.id, id)).get() + return Promise.resolve(row ? toRole(row) : null) + }, + createRole(input) { + const created = db.insert(roles).values(input).returning().get() + return Promise.resolve(toRole(created)) + }, + updateRole(id, input) { + const existing = db.select().from(roles).where(eq(roles.id, id)).get() + if (!existing) return Promise.resolve(null) + db.update(roles).set({ name: input.name }).where(eq(roles.id, id)).run() + const updated = db.select().from(roles).where(eq(roles.id, id)).get()! + return Promise.resolve(toRole(updated)) + }, + deleteRole(id) { + const deleted = db.delete(roles).where(eq(roles.id, id)).returning({ id: roles.id }).get() + return Promise.resolve(Boolean(deleted)) + }, + } + return { repo } +} diff --git a/packages/data-manager-sqlite/src/schema/enterpriseStaff.ts b/packages/data-manager-sqlite/src/schema/enterpriseStaff.ts index be31134..4b45b9b 100644 --- a/packages/data-manager-sqlite/src/schema/enterpriseStaff.ts +++ b/packages/data-manager-sqlite/src/schema/enterpriseStaff.ts @@ -1,4 +1,4 @@ -import { integer, sqliteTable } from 'drizzle-orm/sqlite-core' +import { integer, sqliteTable, unique } from 'drizzle-orm/sqlite-core' import { staff } from './staff' import { enterprises } from './enterprises' @@ -6,4 +6,6 @@ export const enterpriseStaff = sqliteTable('enterprise_staff', { id: integer('id').primaryKey({ autoIncrement: true }), staffId: integer('staff_id').notNull().references(() => staff.id), enterpriseId: integer('enterprise_id').notNull().references(() => enterprises.id), -}) +}, (table) => ({ + uniquePair: unique().on(table.staffId, table.enterpriseId), +})) diff --git a/packages/data-manager-sqlite/src/schema/rolePermissions.ts b/packages/data-manager-sqlite/src/schema/rolePermissions.ts index b92b154..3c9c7ed 100644 --- a/packages/data-manager-sqlite/src/schema/rolePermissions.ts +++ b/packages/data-manager-sqlite/src/schema/rolePermissions.ts @@ -1,4 +1,4 @@ -import { integer, sqliteTable } from 'drizzle-orm/sqlite-core' +import { integer, sqliteTable, unique } from 'drizzle-orm/sqlite-core' import { roles } from './roles' import { permissions } from './permissions' @@ -6,4 +6,6 @@ export const rolePermissions = sqliteTable('role_permissions', { id: integer('id').primaryKey({ autoIncrement: true }), roleId: integer('role_id').notNull().references(() => roles.id), permissionId: integer('permission_id').notNull().references(() => permissions.id), -}) +}, (table) => ({ + uniquePair: unique().on(table.roleId, table.permissionId), +})) diff --git a/packages/data-manager-sqlite/src/schema/staffRoles.ts b/packages/data-manager-sqlite/src/schema/staffRoles.ts index 3ef94ae..49c8ef8 100644 --- a/packages/data-manager-sqlite/src/schema/staffRoles.ts +++ b/packages/data-manager-sqlite/src/schema/staffRoles.ts @@ -1,4 +1,4 @@ -import { integer, sqliteTable } from 'drizzle-orm/sqlite-core' +import { integer, sqliteTable, unique } from 'drizzle-orm/sqlite-core' import { staff } from './staff' import { roles } from './roles' @@ -6,4 +6,6 @@ export const staffRoles = sqliteTable('staff_roles', { id: integer('id').primaryKey({ autoIncrement: true }), staffId: integer('staff_id').notNull().references(() => staff.id), roleId: integer('role_id').notNull().references(() => roles.id), -}) +}, (table) => ({ + uniquePair: unique().on(table.staffId, table.roleId), +})) diff --git a/packages/data-manager-sqlite/src/staff.ts b/packages/data-manager-sqlite/src/staff.ts index 56484f9..7929393 100644 --- a/packages/data-manager-sqlite/src/staff.ts +++ b/packages/data-manager-sqlite/src/staff.ts @@ -3,6 +3,9 @@ import bcrypt from 'bcryptjs' import { db } from './db-instance' import { staff } from './schema/staff' +import { staffRoles } from './schema/staffRoles' +import { roles } from './schema/roles' +import { enterpriseStaff } from './schema/enterpriseStaff' export type Staff = { id: number @@ -13,16 +16,31 @@ export type Staff = { added_on: string password: string is_password_reset_needed: boolean + roles: { id: number; name: string }[] } export type StaffRepo = { getStaffById: (id: number) => Promise getStaffByUsername: (username: string) => Promise - createStaff: (input: { name: string; username: string; email?: string | null; mobile?: string | null; added_on: string; password: string; is_password_reset_needed?: boolean }) => Promise + getStaffByEnterprise: (enterpriseId: number) => Promise + createStaff: (input: { name: string; username: string; email?: string | null; mobile?: string | null; added_on: string; password: string; is_password_reset_needed?: boolean; role_ids?: number[] }, enterpriseId: number) => Promise + updateStaff: (id: number, input: { name?: string; username?: string; email?: string | null; mobile?: string | null; is_password_reset_needed?: boolean; role_ids?: number[] }) => Promise + updateStaffRoles: (staffId: number, roleIds: number[]) => void + deleteStaff: (id: number) => Promise verifyPassword: (staff: Staff, password: string) => Promise } -function toStaff(row: typeof staff.$inferSelect): Staff { +function getRolesForStaff(staffId: number): { id: number; name: string }[] { + const rows = db + .select({ roleId: roles.id, roleName: roles.name }) + .from(staffRoles) + .innerJoin(roles, eq(staffRoles.roleId, roles.id)) + .where(eq(staffRoles.staffId, staffId)) + .all() + return rows.map((r) => ({ id: r.roleId, name: r.roleName })) +} + +function toStaff(row: typeof staff.$inferSelect): Omit { return { id: row.id, name: row.name, @@ -39,15 +57,29 @@ export function createStaffRepo(): { repo: StaffRepo } { const repo: StaffRepo = { getStaffById(id) { const row = db.select().from(staff).where(eq(staff.id, id)).get() - return Promise.resolve(row ? toStaff(row) : null) + if (!row) return Promise.resolve(null) + return Promise.resolve({ ...toStaff(row), roles: getRolesForStaff(id) }) }, getStaffByUsername(username) { const row = db.select().from(staff).where(eq(staff.username, username)).get() - return Promise.resolve(row ? toStaff(row) : null) + if (!row) return Promise.resolve(null) + return Promise.resolve({ ...toStaff(row), roles: getRolesForStaff(row.id) }) }, - createStaff(input) { + getStaffByEnterprise(enterpriseId) { + const rows = db + .select({ staff: staff }) + .from(enterpriseStaff) + .innerJoin(staff, eq(enterpriseStaff.staffId, staff.id)) + .where(eq(enterpriseStaff.enterpriseId, enterpriseId)) + .all() + return Promise.resolve( + rows.map((r) => ({ ...toStaff(r.staff), roles: getRolesForStaff(r.staff.id) })), + ) + }, + + createStaff(input, enterpriseId) { const hashed = bcrypt.hashSync(input.password, 10) const created = db.insert(staff).values({ name: input.name, @@ -58,7 +90,56 @@ export function createStaffRepo(): { repo: StaffRepo } { password: hashed, isPasswordResetNeeded: input.is_password_reset_needed ?? true, }).returning().get() - return Promise.resolve(toStaff(created)) + + db.insert(enterpriseStaff).values({ staffId: created.id, enterpriseId }).run() + + if (input.role_ids && input.role_ids.length > 0) { + for (const roleId of input.role_ids) { + db.insert(staffRoles).values({ staffId: created.id, roleId }).run() + } + } + + return Promise.resolve({ ...toStaff(created), roles: getRolesForStaff(created.id) }) + }, + + updateStaff(id, input) { + const existing = db.select().from(staff).where(eq(staff.id, id)).get() + if (!existing) return Promise.resolve(null) + + const setData: Record = {} + if (input.name !== undefined) setData.name = input.name + if (input.username !== undefined) setData.username = input.username + if (input.email !== undefined) setData.email = input.email ?? null + if (input.mobile !== undefined) setData.mobile = input.mobile ?? null + if (input.is_password_reset_needed !== undefined) setData.isPasswordResetNeeded = input.is_password_reset_needed + + if (Object.keys(setData).length > 0) { + db.update(staff).set(setData).where(eq(staff.id, id)).run() + } + + if (input.role_ids !== undefined) { + db.delete(staffRoles).where(eq(staffRoles.staffId, id)).run() + for (const roleId of input.role_ids) { + db.insert(staffRoles).values({ staffId: id, roleId }).run() + } + } + + const updated = db.select().from(staff).where(eq(staff.id, id)).get()! + return Promise.resolve({ ...toStaff(updated), roles: getRolesForStaff(id) }) + }, + + updateStaffRoles(staffId, roleIds) { + db.delete(staffRoles).where(eq(staffRoles.staffId, staffId)).run() + for (const roleId of roleIds) { + db.insert(staffRoles).values({ staffId, roleId }).run() + } + }, + + deleteStaff(id) { + db.delete(staffRoles).where(eq(staffRoles.staffId, id)).run() + db.delete(enterpriseStaff).where(eq(enterpriseStaff.staffId, id)).run() + const deleted = db.delete(staff).where(eq(staff.id, id)).returning({ id: staff.id }).get() + return Promise.resolve(Boolean(deleted)) }, async verifyPassword(s, password) { diff --git a/packages/data-manager-sqlite/src/staffRoles.ts b/packages/data-manager-sqlite/src/staffRoles.ts new file mode 100644 index 0000000..3c34337 --- /dev/null +++ b/packages/data-manager-sqlite/src/staffRoles.ts @@ -0,0 +1,48 @@ +import { eq } from 'drizzle-orm' + +import { db } from './db-instance' +import { staffRoles } from './schema/staffRoles' +import { roles } from './schema/roles' + +export type StaffRole = { + id: number + staff_id: number + role: { id: number; name: string } +} + +export type StaffRolesRepo = { + getByStaffId: (staffId: number) => StaffRole[] + setStaffRoles: (staffId: number, roleIds: number[]) => void +} + +export function createStaffRolesRepo(): { repo: StaffRolesRepo } { + const repo: StaffRolesRepo = { + getByStaffId(staffId) { + const rows = db + .select({ + id: staffRoles.id, + staffId: staffRoles.staffId, + roleId: roles.id, + roleName: roles.name, + }) + .from(staffRoles) + .innerJoin(roles, eq(staffRoles.roleId, roles.id)) + .where(eq(staffRoles.staffId, staffId)) + .all() + return rows.map((r) => ({ + id: r.id, + staff_id: r.staffId, + role: { id: r.roleId, name: r.roleName }, + })) + }, + + setStaffRoles(staffId, roleIds) { + db.delete(staffRoles).where(eq(staffRoles.staffId, staffId)).run() + for (const roleId of roleIds) { + db.insert(staffRoles).values({ staffId, roleId }).run() + } + }, + } + + return { repo } +} diff --git a/packages/shared-react/src/hooks/roles.ts b/packages/shared-react/src/hooks/roles.ts new file mode 100644 index 0000000..fcba59e --- /dev/null +++ b/packages/shared-react/src/hooks/roles.ts @@ -0,0 +1,5 @@ +import { trpc } from "../trpc"; + +export function useListRoles() { + return trpc.roles.list.useQuery(); +} diff --git a/packages/shared-react/src/hooks/staffManagement.ts b/packages/shared-react/src/hooks/staffManagement.ts new file mode 100644 index 0000000..2565f83 --- /dev/null +++ b/packages/shared-react/src/hooks/staffManagement.ts @@ -0,0 +1,21 @@ +import { trpc } from "../trpc"; + +export function useListStaff() { + return trpc.staffManagement.list.useQuery(); +} + +export function useGetStaffById(id: number) { + return trpc.staffManagement.byId.useQuery({ id }); +} + +export function useCreateStaff() { + return trpc.staffManagement.create.useMutation(); +} + +export function useUpdateStaff() { + return trpc.staffManagement.update.useMutation(); +} + +export function useRemoveStaff() { + return trpc.staffManagement.remove.useMutation(); +} diff --git a/packages/shared-react/src/index.ts b/packages/shared-react/src/index.ts index 8ebfab9..a8d0c32 100644 --- a/packages/shared-react/src/index.ts +++ b/packages/shared-react/src/index.ts @@ -7,6 +7,8 @@ export * from './hooks/products' export * from './hooks/drugInfo' export * from './hooks/units' export * from './hooks/stockBatches' +export * from './hooks/staffManagement' +export * from './hooks/roles' export { trpc } from './trpc' export { useAuthStore, useWhoAmI, useLogin } from './auth' export type { AuthStaff, AuthEnterprise } from './auth'