feat(scan users): added in the place to add the new scanner users in
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@ node-v24.14.0-x64.msi
|
||||
postgresql-17.9-2-windows-x64.exe
|
||||
VSCodeUserSetup-x64-1.112.0.exe
|
||||
nssm.exe
|
||||
frontend/.tanstack
|
||||
|
||||
# Logs
|
||||
logs
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function AdminSidebar({ session }: any) {
|
||||
title: "Scan users",
|
||||
url: "/admin/scanUsers",
|
||||
icon: UsersRound,
|
||||
role: ["systemAdmin", "admin"],
|
||||
role: ["systemAdmin", "admin", "manager"],
|
||||
module: "admin",
|
||||
active: true,
|
||||
},
|
||||
@@ -79,9 +79,9 @@ export default function AdminSidebar({ session }: any) {
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<>
|
||||
<div key={item.title}>
|
||||
{item.role.includes(session.user.role) && (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link to={item.url} onClick={() => setOpen(false)}>
|
||||
<item.icon />
|
||||
@@ -90,7 +90,7 @@ export default function AdminSidebar({ session }: any) {
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
|
||||
@@ -1,64 +1,67 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Slot } from "radix-ui";
|
||||
import type * as React from "react";
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
icon: "size-8",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm":
|
||||
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button";
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
export { Button, buttonVariants }
|
||||
|
||||
166
frontend/src/components/ui/dialog.tsx
Normal file
166
frontend/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import * as React from "react"
|
||||
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close data-slot="dialog-close" asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2"
|
||||
size="icon-sm"
|
||||
>
|
||||
<XIcon
|
||||
/>
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn(
|
||||
"text-base leading-none font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn(
|
||||
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
25
frontend/src/lib/queries/getScannerIds.ts
Normal file
25
frontend/src/lib/queries/getScannerIds.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
|
||||
export function getScannerIds() {
|
||||
return queryOptions({
|
||||
queryKey: ["getScannerIds"],
|
||||
queryFn: () => fetch(),
|
||||
staleTime: 5000,
|
||||
refetchOnWindowFocus: true,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
}
|
||||
|
||||
const fetch = async () => {
|
||||
if (window.location.hostname === "localhost") {
|
||||
await new Promise((res) => setTimeout(res, 1500));
|
||||
}
|
||||
|
||||
const { data } = await axios.get("/lst/api/mobile/available", {
|
||||
withCredentials: true,
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
return data.data;
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import { Route as AboutRouteImport } from './routes/about'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as DocsIndexRouteImport } from './routes/docs/index'
|
||||
import { Route as DocsSplatRouteImport } from './routes/docs/$'
|
||||
import { Route as AdminUsersRouteImport } from './routes/admin/users'
|
||||
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
|
||||
import { Route as AdminServersRouteImport } from './routes/admin/servers'
|
||||
import { Route as AdminScanUsersRouteImport } from './routes/admin/scanUsers'
|
||||
@@ -43,6 +44,11 @@ const DocsSplatRoute = DocsSplatRouteImport.update({
|
||||
path: '/docs/$',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AdminUsersRoute = AdminUsersRouteImport.update({
|
||||
id: '/admin/users',
|
||||
path: '/admin/users',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AdminSettingsRoute = AdminSettingsRouteImport.update({
|
||||
id: '/admin/settings',
|
||||
path: '/admin/settings',
|
||||
@@ -98,6 +104,7 @@ export interface FileRoutesByFullPath {
|
||||
'/admin/scanUsers': typeof AdminScanUsersRoute
|
||||
'/admin/servers': typeof AdminServersRoute
|
||||
'/admin/settings': typeof AdminSettingsRoute
|
||||
'/admin/users': typeof AdminUsersRoute
|
||||
'/docs/$': typeof DocsSplatRoute
|
||||
'/docs/': typeof DocsIndexRoute
|
||||
'/user/profile': typeof authUserProfileRoute
|
||||
@@ -113,6 +120,7 @@ export interface FileRoutesByTo {
|
||||
'/admin/scanUsers': typeof AdminScanUsersRoute
|
||||
'/admin/servers': typeof AdminServersRoute
|
||||
'/admin/settings': typeof AdminSettingsRoute
|
||||
'/admin/users': typeof AdminUsersRoute
|
||||
'/docs/$': typeof DocsSplatRoute
|
||||
'/docs': typeof DocsIndexRoute
|
||||
'/user/profile': typeof authUserProfileRoute
|
||||
@@ -129,6 +137,7 @@ export interface FileRoutesById {
|
||||
'/admin/scanUsers': typeof AdminScanUsersRoute
|
||||
'/admin/servers': typeof AdminServersRoute
|
||||
'/admin/settings': typeof AdminSettingsRoute
|
||||
'/admin/users': typeof AdminUsersRoute
|
||||
'/docs/$': typeof DocsSplatRoute
|
||||
'/docs/': typeof DocsIndexRoute
|
||||
'/(auth)/user/profile': typeof authUserProfileRoute
|
||||
@@ -146,6 +155,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/scanUsers'
|
||||
| '/admin/servers'
|
||||
| '/admin/settings'
|
||||
| '/admin/users'
|
||||
| '/docs/$'
|
||||
| '/docs/'
|
||||
| '/user/profile'
|
||||
@@ -161,6 +171,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/scanUsers'
|
||||
| '/admin/servers'
|
||||
| '/admin/settings'
|
||||
| '/admin/users'
|
||||
| '/docs/$'
|
||||
| '/docs'
|
||||
| '/user/profile'
|
||||
@@ -176,6 +187,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/scanUsers'
|
||||
| '/admin/servers'
|
||||
| '/admin/settings'
|
||||
| '/admin/users'
|
||||
| '/docs/$'
|
||||
| '/docs/'
|
||||
| '/(auth)/user/profile'
|
||||
@@ -192,6 +204,7 @@ export interface RootRouteChildren {
|
||||
AdminScanUsersRoute: typeof AdminScanUsersRoute
|
||||
AdminServersRoute: typeof AdminServersRoute
|
||||
AdminSettingsRoute: typeof AdminSettingsRoute
|
||||
AdminUsersRoute: typeof AdminUsersRoute
|
||||
DocsSplatRoute: typeof DocsSplatRoute
|
||||
DocsIndexRoute: typeof DocsIndexRoute
|
||||
authUserProfileRoute: typeof authUserProfileRoute
|
||||
@@ -229,6 +242,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof DocsSplatRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/admin/users': {
|
||||
id: '/admin/users'
|
||||
path: '/admin/users'
|
||||
fullPath: '/admin/users'
|
||||
preLoaderRoute: typeof AdminUsersRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/admin/settings': {
|
||||
id: '/admin/settings'
|
||||
path: '/admin/settings'
|
||||
@@ -304,6 +324,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
AdminScanUsersRoute: AdminScanUsersRoute,
|
||||
AdminServersRoute: AdminServersRoute,
|
||||
AdminSettingsRoute: AdminSettingsRoute,
|
||||
AdminUsersRoute: AdminUsersRoute,
|
||||
DocsSplatRoute: DocsSplatRoute,
|
||||
DocsIndexRoute: DocsIndexRoute,
|
||||
authUserProfileRoute: authUserProfileRoute,
|
||||
|
||||
161
frontend/src/routes/admin/-components/NewScanUser.tsx
Normal file
161
frontend/src/routes/admin/-components/NewScanUser.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../../../components/ui/dialog";
|
||||
import { useAppForm } from "../../../lib/formSutff";
|
||||
import { getScannerIds } from "../../../lib/queries/getScannerIds";
|
||||
|
||||
export default function NewScanUser({ refetch }: { refetch: any }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data, refetch: scannerFetch } = useSuspenseQuery(getScannerIds());
|
||||
const form = useAppForm({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
scannerId: "",
|
||||
pinNumber: "",
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
if (value.scannerId === "") {
|
||||
toast.error(
|
||||
"Scanner id is required please select a scanner id before submitting ",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await axios.post(
|
||||
"/lst/api/mobile/auth/user",
|
||||
{
|
||||
name: value.name,
|
||||
pinNumber: value.pinNumber,
|
||||
scannerId: value.scannerId,
|
||||
},
|
||||
{
|
||||
withCredentials: true,
|
||||
timeout: 15000,
|
||||
validateStatus: () => true,
|
||||
},
|
||||
);
|
||||
|
||||
if (data.success) {
|
||||
toast.success(
|
||||
`${value.name}, was just created and can now log into the scanner with PIN: ${value.pinNumber}`,
|
||||
);
|
||||
form.reset();
|
||||
setOpen(false);
|
||||
refetch();
|
||||
}
|
||||
|
||||
if (!data.success) {
|
||||
toast.error(data.message);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const closeModel = (e: boolean) => {
|
||||
setOpen(e);
|
||||
|
||||
if (!e) {
|
||||
form.reset();
|
||||
scannerFetch();
|
||||
}
|
||||
};
|
||||
|
||||
const openForm = () => {
|
||||
setOpen(true);
|
||||
scannerFetch();
|
||||
};
|
||||
|
||||
let n: any = [];
|
||||
if (data) {
|
||||
n = data.map((i: any) => ({
|
||||
label: i.label,
|
||||
value: i.value.toString(),
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={(e) => closeModel(e)} open={open}>
|
||||
<Button onClick={openForm}>Create new user</Button>
|
||||
|
||||
<DialogContent showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Scan user.</DialogTitle>
|
||||
<DialogDescription></DialogDescription>
|
||||
</DialogHeader>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div className="mb-2">
|
||||
<form.AppField name="name">
|
||||
{(field) => (
|
||||
<field.InputField
|
||||
label="Name"
|
||||
inputType="text"
|
||||
required={true}
|
||||
/>
|
||||
)}
|
||||
</form.AppField>
|
||||
</div>
|
||||
|
||||
<div className="w-32">
|
||||
<form.AppField name="scannerId">
|
||||
{(field) => (
|
||||
<field.SelectField
|
||||
label="Scanner Id"
|
||||
placeholder="Select New scanner Id"
|
||||
options={n}
|
||||
/>
|
||||
)}
|
||||
</form.AppField>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div>
|
||||
<form.AppField name="pinNumber">
|
||||
{(field) => (
|
||||
<field.InputField
|
||||
label="Pin Number"
|
||||
inputType="number"
|
||||
required={true}
|
||||
/>
|
||||
)}
|
||||
</form.AppField>
|
||||
</div>
|
||||
<div className="mt-9 ml-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
const { data } = await axios.get("/lst/api/mobile/pin/new");
|
||||
|
||||
form.setFieldValue("pinNumber", data.data[0].pin);
|
||||
}}
|
||||
>
|
||||
New Pin
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end mt-2 ">
|
||||
<form.AppForm>
|
||||
<form.SubmitButton>Submit</form.SubmitButton>
|
||||
</form.AppForm>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,258 @@
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import axios from "axios";
|
||||
import { format } from "date-fns-tz";
|
||||
import { CircleFadingArrowUp, Trash } from "lucide-react";
|
||||
import { Suspense, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Spinner } from "../../components/ui/spinner";
|
||||
import { authClient } from "../../lib/auth-client";
|
||||
import { getScanUsers } from "../../lib/queries/getScanUsers";
|
||||
import EditableCellInput from "../../lib/tableStuff/EditableCellInput";
|
||||
import LstTable from "../../lib/tableStuff/LstTable";
|
||||
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
|
||||
import SkellyTable from "../../lib/tableStuff/SkellyTable";
|
||||
import NewScanUser from "./-components/NewScanUser";
|
||||
|
||||
export const Route = createFileRoute("/admin/scanUsers")({
|
||||
beforeLoad: async ({ location }) => {
|
||||
const { data: session } = await authClient.getSession();
|
||||
const allowedRole = ["systemAdmin", "admin"];
|
||||
|
||||
if (!session?.user) {
|
||||
throw redirect({
|
||||
to: "/",
|
||||
search: {
|
||||
redirect: location.href,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!allowedRole.includes(session.user.role as string)) {
|
||||
throw redirect({
|
||||
to: "/",
|
||||
});
|
||||
}
|
||||
|
||||
return { user: session.user };
|
||||
},
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
const ScanUserTable = () => {
|
||||
const { data } = useSuspenseQuery(getScanUsers());
|
||||
console.log(data);
|
||||
return <div>Hello "/admin/scanUsers"!</div>;
|
||||
const updateSettings = async (
|
||||
id: string,
|
||||
data: Record<string, string | number | boolean | null>,
|
||||
) => {
|
||||
//console.log(id, data);
|
||||
try {
|
||||
const res = await axios.patch(`/lst/api/mobile/auth/user/${id}`, data, {
|
||||
withCredentials: true,
|
||||
timeout: 15000,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
toast.success(`User was just updated`);
|
||||
return res;
|
||||
} catch (err) {
|
||||
toast.error("Error in updating the user");
|
||||
return err;
|
||||
}
|
||||
};
|
||||
|
||||
const ScanUserTable = () => {
|
||||
const { data, refetch } = useSuspenseQuery(getScanUsers());
|
||||
const columnHelper = createColumnHelper<any>();
|
||||
|
||||
const updateSetting = useMutation({
|
||||
mutationFn: ({
|
||||
id,
|
||||
field,
|
||||
value,
|
||||
}: {
|
||||
id: string;
|
||||
field: string;
|
||||
value: string | number | boolean | null;
|
||||
}) => updateSettings(id, { [field]: value }),
|
||||
|
||||
onSuccess: () => {
|
||||
// refetch or update cache
|
||||
refetch();
|
||||
},
|
||||
});
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor("name", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="Name" searchable={true} />
|
||||
),
|
||||
filterFn: "includesString",
|
||||
cell: (i) => i.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("scannerId", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader
|
||||
column={column}
|
||||
title="Scanner ID"
|
||||
searchable={false}
|
||||
/>
|
||||
),
|
||||
filterFn: "includesString",
|
||||
cell: (i) => i.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("pinNumber", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="Pin Number" />
|
||||
),
|
||||
|
||||
filterFn: "includesString",
|
||||
cell: ({ row, getValue }) => (
|
||||
<div className="flex flex-row gap-2">
|
||||
<div>
|
||||
<EditableCellInput
|
||||
value={getValue()}
|
||||
id={row.original.name}
|
||||
field="value"
|
||||
onSubmit={({ id, field, value }) => {
|
||||
updateSetting.mutate({ id, field, value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
const { data } = await axios.get("/lst/api/mobile/pin/new");
|
||||
updateSetting.mutate({
|
||||
id: row.original.id,
|
||||
field: "pinNumber",
|
||||
value: data.data[0].pin,
|
||||
});
|
||||
}}
|
||||
>
|
||||
New Pin
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("lastScan", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="Last Scan" />
|
||||
),
|
||||
cell: (i) => <span>{format(i.getValue(), "M/d/yyyy HH:mm")}</span>,
|
||||
}),
|
||||
columnHelper.accessor("excludedCommand", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader column={column} title="Command id's Not Allowed" />
|
||||
),
|
||||
cell: (i) => {
|
||||
const commands = i.getValue().join();
|
||||
return (
|
||||
<span>{commands === "" ? "All commands allowed" : commands}</span>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("deleteUser", {
|
||||
header: ({ column }) => (
|
||||
<SearchableHeader
|
||||
column={column}
|
||||
title="Delete User"
|
||||
searchable={false}
|
||||
/>
|
||||
),
|
||||
filterFn: "includesString",
|
||||
cell: (i) => {
|
||||
// biome-ignore lint: just removing the lint for now to get this going will maybe fix later
|
||||
const [activeToggle, setActiveToggle] = useState(false);
|
||||
|
||||
const onTrigger = async () => {
|
||||
setActiveToggle(true);
|
||||
|
||||
try {
|
||||
const res = await axios.delete(
|
||||
`/lst/api/mobile/auth/user/${i.row.original.id}`,
|
||||
|
||||
{
|
||||
withCredentials: true,
|
||||
timeout: 5000,
|
||||
validateStatus: () => true,
|
||||
},
|
||||
);
|
||||
|
||||
if (res.data.success) {
|
||||
toast.success(`${i.row.original.name} was deleted.`);
|
||||
refetch();
|
||||
setActiveToggle(false);
|
||||
}
|
||||
|
||||
if (!res.data.success) {
|
||||
toast.error(
|
||||
`${i.row.original.name} encountered an error when trying to delete: ${res.data.message}`,
|
||||
);
|
||||
refetch();
|
||||
setActiveToggle(false);
|
||||
}
|
||||
} catch (error) {
|
||||
setActiveToggle(false);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={activeToggle}
|
||||
onClick={onTrigger}
|
||||
>
|
||||
{activeToggle ? (
|
||||
<span>
|
||||
<Spinner />
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<Trash />
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-end m-2">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<NewScanUser refetch={refetch} />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div>
|
||||
<LstTable data={data} columns={columns} pageSize={50} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// const NewUserForm = ()=>{
|
||||
// const { data, refetch } = useSuspenseQuery(getScanUsers());
|
||||
// }
|
||||
function RouteComponent() {
|
||||
return <ScanUserTable />;
|
||||
//const { data: session } = useSession();
|
||||
return (
|
||||
<Suspense fallback={<SkellyTable />}>
|
||||
<ScanUserTable />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user