user and roles functional
This commit is contained in:
parent
86ff92511e
commit
302866dc58
29 changed files with 2190 additions and 78 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
apps/backend/src/trpc/pharmanager/v1/roles.ts
Normal file
14
apps/backend/src/trpc/pharmanager/v1/roles.ts
Normal file
|
|
@ -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()),
|
||||
});
|
||||
72
apps/backend/src/trpc/pharmanager/v1/staffManagement.ts
Normal file
72
apps/backend/src/trpc/pharmanager/v1/staffManagement.ts
Normal file
|
|
@ -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 };
|
||||
}),
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
157
apps/pharmanager/src/components/ui/Select.tsx
Normal file
157
apps/pharmanager/src/components/ui/Select.tsx
Normal file
|
|
@ -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<HTMLSelectElement, SelectProps>(
|
||||
(
|
||||
{
|
||||
options,
|
||||
multiple,
|
||||
placeholder,
|
||||
value: controlledValue,
|
||||
onChangeValue,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(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 (
|
||||
<div ref={containerRef} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className={cn(
|
||||
"w-full px-3.5 py-2.5 border border-slate-200 rounded-md text-sm text-left bg-white",
|
||||
"focus:outline-none focus:ring-[3px] focus:ring-blue-100 focus:border-blue-600",
|
||||
"flex items-center justify-between gap-2 min-h-[42px]",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap gap-1 flex-1">
|
||||
{selectedLabels.length > 0 ? (
|
||||
selectedLabels.map((label) => (
|
||||
<span
|
||||
key={label}
|
||||
className="inline-flex items-center gap-0.5 px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-xs font-medium"
|
||||
>
|
||||
{label}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const opt = options.find(
|
||||
(o) => o.label === label,
|
||||
);
|
||||
if (opt) toggle(opt.value);
|
||||
}}
|
||||
className="ml-0.5 hover:bg-blue-100 rounded-full"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-slate-400">
|
||||
{placeholder || "Select..."}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronsUpDown className="w-4 h-4 text-slate-400 shrink-0" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute top-full left-0 right-0 z-50 mt-1 bg-white border border-slate-200 rounded-md shadow-lg max-h-52 overflow-y-auto p-2">
|
||||
<div className="space-y-1">
|
||||
{options.map((opt) => (
|
||||
<Checkbox
|
||||
key={opt.value}
|
||||
checked={selected.includes(opt.value)}
|
||||
onChange={() => toggle(opt.value)}
|
||||
label={opt.label}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<select
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"w-full px-3.5 py-2.5 border border-slate-200 rounded-md text-sm text-slate-900 bg-white focus:outline-none focus:ring-[3px] focus:ring-blue-100 focus:border-blue-600",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{placeholder && <option value="">{placeholder}</option>}
|
||||
{options.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Select.displayName = "Select";
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ if (!rootElement) throw new Error("Root element not found");
|
|||
if (!rootElement.innerHTML) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<TrpcProvider baseUrl="http://localhost:3001">
|
||||
<TrpcProvider>
|
||||
<RouterProvider router={router} />
|
||||
</TrpcProvider>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-900 mb-1.5">Category</label>
|
||||
<select
|
||||
<Select
|
||||
{...register("category")}
|
||||
className="w-full px-3.5 py-2.5 border border-slate-200 rounded-md text-sm text-slate-900 bg-white focus:outline-none focus:ring-[3px] focus:ring-blue-100 focus:border-blue-600"
|
||||
>
|
||||
{CATEGORIES.map((c) => (
|
||||
<option key={c} value={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
options={CATEGORIES.map((c) => ({ value: c, label: c }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showUnitsPerStrip && (
|
||||
|
|
@ -236,29 +232,21 @@ function AddProductPage() {
|
|||
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||
Product Unit <span className="text-red-600">*</span>
|
||||
</label>
|
||||
<select
|
||||
<Select
|
||||
{...register("unit_name")}
|
||||
className={`w-full px-3.5 py-2.5 border rounded-md text-sm text-slate-900 bg-white focus:outline-none focus:ring-[3px] focus:ring-blue-100 ${errors.unit_name ? "border-red-600" : "border-slate-200 focus:border-blue-600"}`}
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
{(unitList ?? []).map((u) => (
|
||||
<option key={u.id} value={u.name}>{u.name}</option>
|
||||
))}
|
||||
</select>
|
||||
options={(unitList ?? []).map((u) => ({ value: u.name, label: u.name }))}
|
||||
placeholder="Select..."
|
||||
/>
|
||||
{errors.unit_name && <p className="text-sm text-red-600 mt-1">{errors.unit_name.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className={showUnitsPerStrip ? "" : "col-span-full"}>
|
||||
<label className="block text-sm font-medium text-slate-900 mb-1.5">Distributor</label>
|
||||
<select
|
||||
<Select
|
||||
{...register("distributor_id")}
|
||||
className="w-full px-3.5 py-2.5 border border-slate-200 rounded-md text-sm text-slate-900 bg-white focus:outline-none focus:ring-[3px] focus:ring-blue-100 focus:border-blue-600"
|
||||
>
|
||||
<option value="">None</option>
|
||||
{(distributorList ?? []).map((d) => (
|
||||
<option key={d.id} value={d.id}>{d.agency}</option>
|
||||
))}
|
||||
</select>
|
||||
options={(distributorList ?? []).map((d) => ({ value: String(d.id), label: d.agency }))}
|
||||
placeholder="None"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Composition */}
|
||||
|
|
@ -294,15 +282,11 @@ function AddProductPage() {
|
|||
</div>
|
||||
<div className="w-28">
|
||||
<label className="block text-xs font-medium text-slate-600 mb-1">Unit</label>
|
||||
<select
|
||||
<Select
|
||||
{...register(`compositions.${index}.unit_name`)}
|
||||
className={`w-full px-3 py-2 border rounded-md text-sm text-slate-900 bg-white focus:outline-none focus:ring-[3px] focus:ring-blue-100 ${errors.compositions?.[index]?.unit_name ? "border-red-600" : "border-slate-200 focus:border-blue-600"}`}
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
{(unitList ?? []).map((u) => (
|
||||
<option key={u.id} value={u.name}>{u.name}</option>
|
||||
))}
|
||||
</select>
|
||||
options={(unitList ?? []).map((u) => ({ value: u.name, label: u.name }))}
|
||||
placeholder="Select..."
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/staff")({
|
||||
component: StaffPage,
|
||||
staticData: {
|
||||
title: "Staff",
|
||||
subtitle: "Staff management with roles & permissions",
|
||||
},
|
||||
component: () => <Outlet />,
|
||||
});
|
||||
|
||||
function StaffPage() {
|
||||
return <div>Staff</div>;
|
||||
}
|
||||
|
|
|
|||
140
apps/pharmanager/src/routes/staff/$id.tsx
Normal file
140
apps/pharmanager/src/routes/staff/$id.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Pencil, Trash2, Users, Shield, Calendar, Mail, Phone, User } from "lucide-react";
|
||||
import { Button, DetailRow, BackLink, EmptyState } from "#/components/ui";
|
||||
import { useGetStaffById, useRemoveStaff, trpc } from "shared-react";
|
||||
|
||||
function fmtDate(d: string): string {
|
||||
const date = new Date(d + "T00:00:00");
|
||||
return date.toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric" });
|
||||
}
|
||||
|
||||
function roleBadgeClass(name: string): string {
|
||||
const l = name.toLowerCase();
|
||||
if (l.includes("admin")) return "bg-red-50 text-red-600";
|
||||
if (l.includes("pharmacist")) return "bg-blue-50 text-blue-600";
|
||||
if (l.includes("cashier")) return "bg-emerald-50 text-emerald-600";
|
||||
if (l.includes("manager")) return "bg-amber-50 text-amber-600";
|
||||
return "bg-slate-100 text-slate-600";
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/staff/$id")({
|
||||
component: StaffDetailsPage,
|
||||
staticData: {
|
||||
title: "Staff Details",
|
||||
subtitle: "Staff member information",
|
||||
},
|
||||
});
|
||||
|
||||
function StaffDetailsPage() {
|
||||
const { id } = Route.useParams();
|
||||
const staffId = Number(id);
|
||||
const { data: staff, isLoading, error } = useGetStaffById(staffId);
|
||||
const removeMutation = useRemoveStaff();
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
function handleDelete() {
|
||||
if (!staff) return;
|
||||
if (!confirm(`Delete ${staff.name}?`)) return;
|
||||
removeMutation.mutate(
|
||||
{ id: staff.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
utils.staffManagement.list.invalidate();
|
||||
window.location.href = "/staff";
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) return <div className="text-sm text-slate-600 py-8">Loading staff details...</div>;
|
||||
if (error || !staff) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={Users}
|
||||
title="Staff not found"
|
||||
description="The staff member you're looking for doesn't exist."
|
||||
actionLabel="Back to Staff"
|
||||
actionTo="/staff"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const initials = staff.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BackLink to="/staff" label="Staff" />
|
||||
|
||||
<div className="flex items-center gap-5 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-6 mb-5 max-w-[800px]">
|
||||
<div className="w-16 h-16 rounded-full bg-blue-600 flex items-center justify-center text-white font-semibold text-[22px] shrink-0">
|
||||
{initials}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-slate-900">{staff.name}</h2>
|
||||
<div className="flex flex-wrap gap-2 mt-1.5">
|
||||
{staff.roles.length > 0 ? (
|
||||
staff.roles.map((r) => (
|
||||
<span
|
||||
key={r.id}
|
||||
className={`inline-block px-3 py-0.5 rounded-full text-[13px] font-medium ${roleBadgeClass(r.name)}`}
|
||||
>
|
||||
{r.name}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-sm text-slate-400">No role assigned</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="primary">
|
||||
<Pencil className="w-[15px] h-[15px]" />
|
||||
Edit Staff
|
||||
</Button>
|
||||
<Button variant="danger" onClick={handleDelete}>
|
||||
<Trash2 className="w-[15px] h-[15px]" />
|
||||
Delete Staff
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 max-w-[800px]">
|
||||
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] p-5">
|
||||
<h3 className="flex items-center gap-1.5 text-xs font-semibold text-slate-600 uppercase tracking-wider mb-3">
|
||||
<User className="w-[14px] h-[14px]" />
|
||||
Personal Info
|
||||
</h3>
|
||||
<DetailRow label="Username" value={staff.username} />
|
||||
<DetailRow label="Email" value={staff.email || "—"} />
|
||||
<DetailRow label="Mobile" value={staff.mobile || "—"} last />
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] p-5">
|
||||
<h3 className="flex items-center gap-1.5 text-xs font-semibold text-slate-600 uppercase tracking-wider mb-3">
|
||||
<Shield className="w-[14px] h-[14px]" />
|
||||
Roles
|
||||
</h3>
|
||||
<DetailRow
|
||||
label="Roles"
|
||||
value={staff.roles.length > 0 ? staff.roles.map((r) => r.name).join(", ") : "No roles"}
|
||||
last
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] p-5 col-span-full">
|
||||
<h3 className="flex items-center gap-1.5 text-xs font-semibold text-slate-600 uppercase tracking-wider mb-3">
|
||||
<Calendar className="w-[14px] h-[14px]" />
|
||||
Employment
|
||||
</h3>
|
||||
<DetailRow label="Date Joined" value={fmtDate(staff.added_on)} />
|
||||
<DetailRow label="Password Reset Needed" value={staff.is_password_reset_needed ? "Yes" : "No"} last />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
192
apps/pharmanager/src/routes/staff/add.tsx
Normal file
192
apps/pharmanager/src/routes/staff/add.tsx
Normal file
|
|
@ -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<typeof formSchema>;
|
||||
|
||||
export const Route = createFileRoute("/staff/add")({
|
||||
component: AddStaffPage,
|
||||
validateSearch: (search: Record<string, unknown>) => ({
|
||||
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<FormValues>({
|
||||
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 (
|
||||
<div>
|
||||
<BackLink to="/staff" label="Staff" />
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] p-8 max-w-2xl">
|
||||
<h2 className="text-xl font-semibold mb-1">{isEditing ? "Edit Staff" : "Add Staff"}</h2>
|
||||
<p className="text-sm text-slate-600 mb-7">
|
||||
{isEditing ? "Update staff member details." : "Add a new staff member to the pharmacy."}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<div className="col-span-full">
|
||||
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||
Full Name <span className="text-red-600">*</span>
|
||||
</label>
|
||||
<Input {...register("name")} variant={errors.name ? "error" : "default"} placeholder="e.g. John Doe" />
|
||||
{errors.name && <p className="text-sm text-red-600 mt-1">{errors.name.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||
Username <span className="text-red-600">*</span>
|
||||
</label>
|
||||
<Input {...register("username")} variant={errors.username ? "error" : "default"} placeholder="johndoe" />
|
||||
{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 <span className="text-red-600">*</span>
|
||||
</label>
|
||||
<Input type="password" {...register("password")} variant={errors.password ? "error" : "default"} placeholder={isEditing ? "Leave blank to keep current" : "Enter password"} />
|
||||
{errors.password && <p className="text-sm text-red-600 mt-1">{errors.password.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-900 mb-1.5">Email</label>
|
||||
<Input {...register("email", { setValueAs: (v: string) => v || null })} placeholder="johndoe@example.com" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-900 mb-1.5">Mobile</label>
|
||||
<Input {...register("mobile", { setValueAs: (v: string) => v || null })} placeholder="9876543210" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||
Date Joined <span className="text-red-600">*</span>
|
||||
</label>
|
||||
<Input type="date" {...register("added_on")} variant={errors.added_on ? "error" : "default"} />
|
||||
{errors.added_on && <p className="text-sm text-red-600 mt-1">{errors.added_on.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="col-span-full">
|
||||
<label className="block text-sm font-medium text-slate-900 mb-2">Roles</label>
|
||||
<Select
|
||||
multiple
|
||||
value={selectedRoleIds.map(String)}
|
||||
onChangeValue={toggleRoles}
|
||||
options={(roleList ?? []).map((r) => ({ value: String(r.id), label: r.name }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-full flex justify-end gap-3 pt-5 mt-2 border-t border-slate-200">
|
||||
<Link to="/staff" className={buttonVariants({ variant: "outline" })}>Cancel</Link>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
<Plus className="w-[15px] h-[15px]" />
|
||||
{mutation.isPending ? "Saving..." : isEditing ? "Update Staff" : "Add Staff"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mutation.error && (
|
||||
<p className="text-sm text-red-600 mt-4">Failed to {isEditing ? "update" : "create"} staff. Please try again.</p>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
171
apps/pharmanager/src/routes/staff/index.tsx
Normal file
171
apps/pharmanager/src/routes/staff/index.tsx
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { useState, useMemo, useCallback } from "react";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { Pencil, Trash2, Users } from "lucide-react";
|
||||
import { GridTable } from "#/components/GridTable";
|
||||
import type { GridTableColumn } from "#/components/GridTable";
|
||||
import { Button, SearchToolbar } from "#/components/ui";
|
||||
import { useListStaff, useRemoveStaff, trpc } from "shared-react";
|
||||
|
||||
interface StaffRow {
|
||||
id: number;
|
||||
name: string;
|
||||
username: string;
|
||||
mobile: string | null;
|
||||
added_on: string;
|
||||
roles: { id: number; name: string }[];
|
||||
}
|
||||
|
||||
function fmtDate(d: string): string {
|
||||
const date = new Date(d + "T00:00:00");
|
||||
return date.toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric" });
|
||||
}
|
||||
|
||||
function roleBadgeClass(name: string): string {
|
||||
const l = name.toLowerCase();
|
||||
if (l.includes("admin")) return "bg-red-50 text-red-600";
|
||||
if (l.includes("pharmacist")) return "bg-blue-50 text-blue-600";
|
||||
if (l.includes("cashier")) return "bg-emerald-50 text-emerald-600";
|
||||
if (l.includes("manager")) return "bg-amber-50 text-amber-600";
|
||||
return "bg-slate-100 text-slate-600";
|
||||
}
|
||||
|
||||
function makeColumns(
|
||||
onDelete: (row: StaffRow) => void,
|
||||
): GridTableColumn<StaffRow>[] {
|
||||
return [
|
||||
{
|
||||
id: "name",
|
||||
header: "Staff Name",
|
||||
cell: ({ row }) => (
|
||||
<Link
|
||||
to="/staff/$id"
|
||||
params={{ id: row.id.toString() }}
|
||||
className="font-semibold text-blue-600 hover:underline"
|
||||
>
|
||||
{row.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "mobile",
|
||||
header: "Mobile",
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-slate-600 font-mono">
|
||||
{row.mobile || "—"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "role",
|
||||
header: "Role",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{row.roles.length > 0 ? (
|
||||
row.roles.map((r) => (
|
||||
<span
|
||||
key={r.id}
|
||||
className={`inline-block px-2.5 py-0.5 rounded-full text-xs font-medium ${roleBadgeClass(r.name)}`}
|
||||
>
|
||||
{r.name}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-sm text-slate-400">—</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "added_on",
|
||||
header: "Date Joined",
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-slate-600 whitespace-nowrap">
|
||||
{fmtDate(row.added_on)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
size: 90,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Link to="/staff/add" search={{ id: row.id }}>
|
||||
<Button variant="ghost-blue" size="icon" aria-label={`Edit ${row.name}`} type="button">
|
||||
<Pencil className="w-[15px] h-[15px]" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Button variant="ghost-red" size="icon" aria-label={`Delete ${row.name}`} onClick={() => onDelete(row)}>
|
||||
<Trash2 className="w-[15px] h-[15px]" />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/staff/")({
|
||||
component: StaffIndexPage,
|
||||
staticData: {
|
||||
title: "Staff",
|
||||
subtitle: "Manage pharmacy staff members",
|
||||
},
|
||||
});
|
||||
|
||||
function StaffIndexPage() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const { data: staffList, isLoading, error } = useListStaff();
|
||||
const removeMutation = useRemoveStaff();
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(row: StaffRow) => {
|
||||
if (!confirm(`Delete ${row.name}?`)) return;
|
||||
removeMutation.mutate(
|
||||
{ id: row.id },
|
||||
{ onSuccess: () => utils.staffManagement.list.invalidate() },
|
||||
);
|
||||
},
|
||||
[removeMutation, utils],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => makeColumns(handleDelete), [handleDelete]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = searchQuery.toLowerCase().trim();
|
||||
if (!q) return staffList ?? [];
|
||||
return (staffList ?? []).filter((s) => {
|
||||
const text = `${s.name} ${s.mobile || ""} ${s.roles.map((r) => r.name).join(" ")}`;
|
||||
return text.toLowerCase().includes(q);
|
||||
});
|
||||
}, [searchQuery, staffList]);
|
||||
|
||||
if (isLoading) return <div className="text-sm text-slate-600 py-8">Loading staff...</div>;
|
||||
if (error) return <div className="text-sm text-red-600 py-8">Failed to load staff.</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SearchToolbar
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
placeholder="Search by name, mobile, or role..."
|
||||
addLink="/staff/add"
|
||||
addLabel="Add Staff"
|
||||
/>
|
||||
|
||||
<GridTable
|
||||
columns={columns}
|
||||
data={filtered}
|
||||
emptyState={
|
||||
<div className="flex flex-col items-center py-16 px-6 text-slate-600">
|
||||
<Users className="w-12 h-12 mb-4 opacity-40" />
|
||||
<h3 className="text-base font-semibold text-slate-900 mb-1.5">No staff found</h3>
|
||||
<p className="text-sm">
|
||||
{searchQuery ? "No staff match your search." : "Add your first staff member to get started."}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import {
|
|||
useListDistributors,
|
||||
trpc,
|
||||
} from "shared-react";
|
||||
import { Button, Input, Checkbox, buttonVariants, Combobox } from "#/components/ui";
|
||||
import { Button, Input, Checkbox, Select, buttonVariants, Combobox } from "#/components/ui";
|
||||
import { CreateStockBatchInput } from "@repo/shared";
|
||||
|
||||
const formSchema = CreateStockBatchInput.extend({
|
||||
|
|
@ -193,28 +193,20 @@ function AddStockPage() {
|
|||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-900 mb-1.5">Rack</label>
|
||||
<select
|
||||
<Select
|
||||
{...register("rack_id")}
|
||||
className="w-full px-3.5 py-2.5 border border-slate-200 rounded-md text-sm text-slate-900 bg-white focus:outline-none focus:ring-[3px] focus:ring-blue-100 focus:border-blue-600"
|
||||
>
|
||||
<option value="">— Select a rack —</option>
|
||||
{(racks ?? []).map((r) => (
|
||||
<option key={r.id} value={r.id}>{r.name}</option>
|
||||
))}
|
||||
</select>
|
||||
options={(racks ?? []).map((r) => ({ value: String(r.id), label: r.name }))}
|
||||
placeholder="— Select a rack —"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-900 mb-1.5">Distributor</label>
|
||||
<select
|
||||
<Select
|
||||
{...register("distributor_id")}
|
||||
className="w-full px-3.5 py-2.5 border border-slate-200 rounded-md text-sm text-slate-900 bg-white focus:outline-none focus:ring-[3px] focus:ring-blue-100 focus:border-blue-600"
|
||||
>
|
||||
<option value="">— Select a distributor —</option>
|
||||
{(distributorList ?? []).map((d) => (
|
||||
<option key={d.id} value={d.id}>{d.agency}</option>
|
||||
))}
|
||||
</select>
|
||||
options={(distributorList ?? []).map((d) => ({ value: String(d.id), label: d.agency }))}
|
||||
placeholder="— Select a distributor —"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-full">
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
1026
packages/data-manager-sqlite/drizzle/meta/0006_snapshot.json
Normal file
1026
packages/data-manager-sqlite/drizzle/meta/0006_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
48
packages/data-manager-sqlite/src/permissions.ts
Normal file
48
packages/data-manager-sqlite/src/permissions.ts
Normal file
|
|
@ -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<Permission[]>
|
||||
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 }
|
||||
}
|
||||
50
packages/data-manager-sqlite/src/roles.ts
Normal file
50
packages/data-manager-sqlite/src/roles.ts
Normal file
|
|
@ -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<Role[]>
|
||||
getRoleById: (id: number) => Promise<Role | null>
|
||||
createRole: (input: { name: string }) => Promise<Role>
|
||||
updateRole: (id: number, input: { name: string }) => Promise<Role | null>
|
||||
deleteRole: (id: number) => Promise<boolean>
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
|
@ -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),
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -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<Staff | null>
|
||||
getStaffByUsername: (username: string) => Promise<Staff | null>
|
||||
createStaff: (input: { name: string; username: string; email?: string | null; mobile?: string | null; added_on: string; password: string; is_password_reset_needed?: boolean }) => Promise<Staff>
|
||||
getStaffByEnterprise: (enterpriseId: number) => Promise<Staff[]>
|
||||
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<Staff>
|
||||
updateStaff: (id: number, input: { name?: string; username?: string; email?: string | null; mobile?: string | null; is_password_reset_needed?: boolean; role_ids?: number[] }) => Promise<Staff | null>
|
||||
updateStaffRoles: (staffId: number, roleIds: number[]) => void
|
||||
deleteStaff: (id: number) => Promise<boolean>
|
||||
verifyPassword: (staff: Staff, password: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
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<Staff, 'roles'> {
|
||||
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<string, unknown> = {}
|
||||
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) {
|
||||
|
|
|
|||
48
packages/data-manager-sqlite/src/staffRoles.ts
Normal file
48
packages/data-manager-sqlite/src/staffRoles.ts
Normal file
|
|
@ -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 }
|
||||
}
|
||||
5
packages/shared-react/src/hooks/roles.ts
Normal file
5
packages/shared-react/src/hooks/roles.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { trpc } from "../trpc";
|
||||
|
||||
export function useListRoles() {
|
||||
return trpc.roles.list.useQuery();
|
||||
}
|
||||
21
packages/shared-react/src/hooks/staffManagement.ts
Normal file
21
packages/shared-react/src/hooks/staffManagement.ts
Normal file
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue