feat(rfid): front end view of the readers and there status

This commit is contained in:
2025-07-10 21:29:01 -05:00
parent 6584b37cb0
commit f7b4de8130
15 changed files with 533 additions and 36 deletions

View File

@@ -1,4 +1,4 @@
import {Printer} from "lucide-react";
import { Printer } from "lucide-react";
import {
SidebarGroup,
SidebarGroupContent,
@@ -7,8 +7,8 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "../../ui/sidebar";
import {hasPageAccess} from "@/utils/userAccess";
import {User} from "@/types/users";
import { hasPageAccess } from "@/utils/userAccess";
import { User } from "@/types/users";
const items = [
{
@@ -19,9 +19,32 @@ const items = [
module: "ocp",
active: true,
},
{
title: "RFID",
moduleName: "prodcution",
description: "RFID stuff",
url: "/rfid",
icon: "Tags",
active: true,
roles: [
"viewer",
"technician",
"supervisor",
"manager",
"admin",
"systemAdmin",
],
subSubModule: [],
},
];
export function ProductionSideBar({user, moduleID}: {user: User | null; moduleID: string}) {
export function ProductionSideBar({
user,
moduleID,
}: {
user: User | null;
moduleID: string;
}) {
return (
<SidebarGroup>
<SidebarGroupLabel>Production</SidebarGroupLabel>
@@ -30,14 +53,15 @@ export function ProductionSideBar({user, moduleID}: {user: User | null; moduleID
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<>
{hasPageAccess(user, item.role, moduleID) && item.active && (
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
)}
{hasPageAccess(user, item.role, moduleID) &&
item.active && (
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
)}
</>
</SidebarMenuItem>
))}

View File

@@ -0,0 +1,28 @@
import { getReaders } from "@/utils/querys/rfid/getReaders";
import { readerColumns } from "@/utils/tableData/rfid/readers/readerColumns";
import { ReaderTable } from "@/utils/tableData/rfid/readers/readerData";
import { useQuery } from "@tanstack/react-query";
export default function RfidPage() {
const { data, isError, isLoading } = useQuery(getReaders());
if (isError) {
return <div>Error</div>;
}
if (isLoading) {
return <div>Loading</div>;
}
return (
<div className="m-2">
<ReaderTable
columns={readerColumns}
data={data.data.sort((a: any, b: any) =>
a.reader.localeCompare(b.reader)
)}
//style={style}
/>
</div>
);
}

View File

@@ -17,6 +17,7 @@ import { Route as EomRouteImport } from './routes/_eom'
import { Route as AuthRouteImport } from './routes/_auth'
import { Route as AdminRouteImport } from './routes/_admin'
import { Route as IndexRouteImport } from './routes/index'
import { Route as RfidIndexRouteImport } from './routes/rfid/index'
import { Route as OcpIndexRouteImport } from './routes/ocp/index'
import { Route as EomEomRouteImport } from './routes/_eom/eom'
import { Route as AuthProfileRouteImport } from './routes/_auth/profile'
@@ -78,6 +79,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const RfidIndexRoute = RfidIndexRouteImport.update({
id: '/rfid/',
path: '/rfid/',
getParentRoute: () => rootRouteImport,
} as any)
const OcpIndexRoute = OcpIndexRouteImport.update({
id: '/ocp/',
path: '/ocp/',
@@ -220,6 +226,7 @@ export interface FileRoutesByFullPath {
'/profile': typeof AuthProfileRoute
'/eom': typeof EomEomRoute
'/ocp': typeof OcpIndexRoute
'/rfid': typeof RfidIndexRoute
'/siloAdjustments/$hist': typeof logisticsSiloAdjustmentsHistRoute
'/article/$av': typeof EomArticleAvRoute
'/barcodegen': typeof logisticsBarcodegenIndexRoute
@@ -250,6 +257,7 @@ export interface FileRoutesByTo {
'/profile': typeof AuthProfileRoute
'/eom': typeof EomEomRoute
'/ocp': typeof OcpIndexRoute
'/rfid': typeof RfidIndexRoute
'/siloAdjustments/$hist': typeof logisticsSiloAdjustmentsHistRoute
'/article/$av': typeof EomArticleAvRoute
'/barcodegen': typeof logisticsBarcodegenIndexRoute
@@ -284,6 +292,7 @@ export interface FileRoutesById {
'/_auth/profile': typeof AuthProfileRoute
'/_eom/eom': typeof EomEomRoute
'/ocp/': typeof OcpIndexRoute
'/rfid/': typeof RfidIndexRoute
'/(logistics)/siloAdjustments/$hist': typeof logisticsSiloAdjustmentsHistRoute
'/_eom/article/$av': typeof EomArticleAvRoute
'/(logistics)/barcodegen/': typeof logisticsBarcodegenIndexRoute
@@ -316,6 +325,7 @@ export interface FileRouteTypes {
| '/profile'
| '/eom'
| '/ocp'
| '/rfid'
| '/siloAdjustments/$hist'
| '/article/$av'
| '/barcodegen'
@@ -346,6 +356,7 @@ export interface FileRouteTypes {
| '/profile'
| '/eom'
| '/ocp'
| '/rfid'
| '/siloAdjustments/$hist'
| '/article/$av'
| '/barcodegen'
@@ -379,6 +390,7 @@ export interface FileRouteTypes {
| '/_auth/profile'
| '/_eom/eom'
| '/ocp/'
| '/rfid/'
| '/(logistics)/siloAdjustments/$hist'
| '/_eom/article/$av'
| '/(logistics)/barcodegen/'
@@ -404,6 +416,7 @@ export interface RootRouteChildren {
RegisterRoute: typeof RegisterRoute
userPasswordChangeRoute: typeof userPasswordChangeRoute
OcpIndexRoute: typeof OcpIndexRoute
RfidIndexRoute: typeof RfidIndexRoute
logisticsSiloAdjustmentsHistRoute: typeof logisticsSiloAdjustmentsHistRoute
logisticsBarcodegenIndexRoute: typeof logisticsBarcodegenIndexRoute
logisticsDmIndexRoute: typeof logisticsDmIndexRoute
@@ -475,6 +488,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/rfid/': {
id: '/rfid/'
path: '/rfid'
fullPath: '/rfid'
preLoaderRoute: typeof RfidIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/ocp/': {
id: '/ocp/'
path: '/ocp'
@@ -694,6 +714,7 @@ const rootRouteChildren: RootRouteChildren = {
RegisterRoute: RegisterRoute,
userPasswordChangeRoute: userPasswordChangeRoute,
OcpIndexRoute: OcpIndexRoute,
RfidIndexRoute: RfidIndexRoute,
logisticsSiloAdjustmentsHistRoute: logisticsSiloAdjustmentsHistRoute,
logisticsBarcodegenIndexRoute: logisticsBarcodegenIndexRoute,
logisticsDmIndexRoute: logisticsDmIndexRoute,

View File

@@ -0,0 +1,14 @@
import RfidPage from "@/components/rfid/RfidPage";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/rfid/")({
component: RouteComponent,
});
function RouteComponent() {
return (
<div>
<RfidPage />
</div>
);
}

View File

@@ -0,0 +1,20 @@
import { queryOptions } from "@tanstack/react-query";
import axios from "axios";
export function getReaders() {
return queryOptions({
queryKey: ["getReaders"],
queryFn: () => fetchReaders(),
//enabled:
staleTime: 1000,
refetchInterval: 60 * 1000,
refetchOnWindowFocus: true,
});
}
const fetchReaders = async () => {
const { data } = await axios.get(`/api/rfid/getreaders`);
// if we are not localhost ignore the devDir setting.
//const url: string = window.location.host.split(":")[0];
return data.data ?? [];
};

View File

@@ -0,0 +1,97 @@
//import { fixTime } from "@/utils/fixTime";
import { ColumnDef } from "@tanstack/react-table";
import { format } from "date-fns-tz";
// This type is used to define the shape of our data.
// You can use a Zod schema here if you want.
export type Readers = {
rfidReader_id: string;
reader: string;
readerIP: string;
lastHeartBeat: string;
lastTrigger: string;
lastTriggerGood: boolean;
active: boolean;
lastTagScanned: string;
goodReads: number;
badReads: number;
totalReads: number;
goodRatio: number;
};
export const readerColumns: ColumnDef<Readers>[] = [
{
accessorKey: "reader",
header: () => <div className="text-left">Name</div>,
},
{
accessorKey: "lastHeartBeat",
header: "Last HeartBeat",
cell: ({ row }) => {
if (row.getValue("lastHeartBeat")) {
const correctDate: any = row.getValue("lastHeartBeat");
const strippedDate = correctDate.replace("Z", ""); // Remove Z
const formattedDate = format(strippedDate, "MM/dd/yyyy HH:mm");
return (
<div className="text-left font-medium">{formattedDate}</div>
);
}
},
},
{
accessorKey: "lastTrigger",
header: "Last Trigger",
cell: ({ row }) => {
if (row.getValue("lastTrigger")) {
const correctDate: any = row.getValue("lastTrigger");
const strippedDate = correctDate.replace("Z", ""); // Remove Z
const formattedDate = format(strippedDate, "MM/dd/yyyy HH:mm");
return (
<div className="text-left font-medium">{formattedDate}</div>
);
}
},
},
{
accessorKey: "lastTriggerGood",
header: "Last Trigger Status",
},
{
accessorKey: "lastTagScanned",
header: "Last Scanned Tag",
},
{
accessorKey: "goodReads",
header: "Total Good Reads",
},
{
accessorKey: "badReads",
header: "Total Bad Reads",
},
{
accessorKey: "totalReads",
header: "Total Reads",
cell: ({ row }) => {
const total =
parseInt(row.getValue("goodReads")) +
parseInt(row.getValue("badReads"));
return <div className="text-left font-medium">{total}</div>;
},
},
{
accessorKey: "goodRatio",
header: "Good Ratio",
cell: ({ row }) => {
const goodRatio =
(parseInt(row.getValue("goodReads")) /
(parseInt(row.getValue("goodReads")) +
parseInt(row.getValue("badReads")))) *
100;
return (
<div className="text-left font-medium">
{isNaN(goodRatio) ? 0 : goodRatio}%
</div>
);
},
},
];

View File

@@ -0,0 +1,174 @@
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
getPaginationRowModel,
} from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ScrollArea } from "@/components/ui/scroll-area";
import { LstCard } from "@/components/extendedUI/LstCard";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
//style: any;
}
export function ReaderTable<TData, TValue>({
columns,
data,
//style,
}: DataTableProps<TData, TValue>) {
const [pagination, setPagination] = useState({
pageIndex: 0, //initial page index
pageSize: 10, //default page size
});
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
state: {
//...
pagination,
},
});
//console.log(parseInt(style.height.replace("px", "")) - 50);
return (
<LstCard>
<div>
<div className="flex flex-row justify-between">
{data.length === 0 ? (
<span>No readers</span>
) : (
<span>Current reader Info</span>
)}
<Select
value={pagination.pageSize.toString()}
onValueChange={(e) =>
setPagination({
...pagination,
pageSize: parseInt(e),
})
}
>
<SelectTrigger className="w-[180px]">
<SelectValue
//id={field.name}
placeholder="Select Page"
defaultValue={10}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Page Size</SelectLabel>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<ScrollArea className="h-96 rounded-md border m-2">
<Table
// style={{
// width: `${parseInt(style.width.replace("px", "")) - 50}px`,
// height: `${parseInt(style.height.replace("px", "")) - 200}px`,
// cursor: "move",
// }}
>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column
.columnDef.header,
header.getContext()
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={
row.getIsSelected() && "selected"
}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No labels.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</ScrollArea>
</div>
<div className="flex items-center justify-end space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</LstCard>
);
}

View File

@@ -1,23 +1,43 @@
import type { Context } from "hono";
import { createLog } from "../../services/logger/logger.js";
export type ReturnRes<T> =
| { success: true; message: string; data: T }
| { success: false; message: string; error: any };
export function returnRes<T>(
success: true,
message: string,
service: string,
user: string,
level: "info" | "error",
data: T
): { success: true; message: string; data: T };
export const returnRes = <T>(
success: boolean,
message: string,
data: T | null = null
): ReturnRes<T> => {
/**
* just a simple return to reduce the typing and make sure we are always consitant with our returns.
*
* data can be an error as well.
*/
return success
? { success, message, data: data as T }
: { success, message, error: data ?? "An unknown error occurred" };
};
export function returnRes<T>(
success: false,
message: string,
service: string,
user: string,
level: "info" | "error",
data?: T
): { success: false; message: string; error: T | string };
export function returnRes<T>(
success: boolean,
message: string,
service: string,
user: string,
level: "info" | "error",
data?: T
) {
createLog(level, user, service, message);
if (success) {
return { success: true, message, data: data as T };
} else {
return {
success: false,
message,
error: data ?? "An unknown error occurred",
};
}
}
// export const returnApi = (c:Context,success: boolean, message: string, data?: any, code: number)=>{
// /**

View File

@@ -12,13 +12,15 @@ export const getAllUsersRoles = async () => {
const { data, error } = await tryCatch(db.select().from(userRoles));
if (error) {
returnRes(
return returnRes(
false,
"auth",
"auth",
"There was an error getting users",
"error",
new Error("No user exists.")
);
}
returnRes(true, "All users.", data);
return { success: true, message: "All users", data };
return returnRes(true, "auth", "auth", "All users.", "info", data);
};

View File

@@ -32,7 +32,10 @@ export const getAllUsers = async () => {
if (error) {
returnRes(
false,
"auth",
"auth",
"There was an error getting users",
"error",
new Error("No user exists.")
);
}

View File

@@ -0,0 +1,21 @@
import { db } from "../../../../database/dbclient.js";
import { rfidReaders } from "../../../../database/schema/rfidReaders.js";
import { returnRes } from "../../../globalUtils/routeDefs/returnRes.js";
import { tryCatch } from "../../../globalUtils/tryCatch.js";
export const getReaders = async () => {
const { data, error } = await tryCatch(db.select().from(rfidReaders));
if (error) {
return returnRes(
false,
"There was an error getting the Readers",
"rfid",
"rfid",
"error",
error
);
}
return returnRes(true, "Current readers", "rfid", "rfid", "info", data);
};

View File

@@ -52,6 +52,7 @@ export const badRead = async (reader: string) => {
lastTrigger: sql`NOW()`,
lastTriggerGood: false,
lastTagScanned: null,
badReads: sql`${rfidReaders.badReads} + 1`,
})
.where(eq(rfidReaders.reader, reader));
createLog(
@@ -85,7 +86,11 @@ export const goodRead = async (reader: string) => {
try {
const goodRead = await db
.update(rfidReaders)
.set({ lastTrigger: sql`NOW()`, lastTriggerGood: true })
.set({
lastTrigger: sql`NOW()`,
lastTriggerGood: true,
goodReads: sql`${rfidReaders.goodReads} + 1`,
})
.where(eq(rfidReaders.reader, reader));
createLog(
"info",

View File

@@ -1,13 +1,21 @@
import {OpenAPIHono} from "@hono/zod-openapi";
import { OpenAPIHono } from "@hono/zod-openapi";
import mgtEvents from "./route/mgtEvents.js";
import tagInfo from "./route/tagInfo.js";
import addReader from "./route/addReader.js";
import updateReader from "./route/updateReader.js";
import manualTrigger from "./route/manualTagRead.js";
import getReaders from "./route/getReaders.js";
const app = new OpenAPIHono();
const routes = [mgtEvents, tagInfo, addReader, updateReader, manualTrigger] as const;
const routes = [
mgtEvents,
tagInfo,
addReader,
updateReader,
manualTrigger,
getReaders,
] as const;
// app.route("/server", modules);
const appRoutes = routes.forEach((route) => {

View File

@@ -0,0 +1,43 @@
import { z, createRoute, OpenAPIHono } from "@hono/zod-openapi";
import { addReader } from "../controller/addReader.js";
import { authMiddleware } from "../../auth/middleware/authMiddleware.js";
import { responses } from "../../../globalUtils/routeDefs/responses.js";
import type { User } from "../../../types/users.js";
import { apiHit } from "../../../globalUtils/apiHits.js";
import { tryCatch } from "../../../globalUtils/tryCatch.js";
import { getReaders } from "../controller/getReaders.js";
const app = new OpenAPIHono();
export const ReaderBody = z.object({
reader: z.string().openapi({ example: "wrapper1" }),
readerIP: z.string().openapi({ example: "192.168.1.52" }),
});
app.openapi(
createRoute({
tags: ["rfid"],
summary: "Get readers",
method: "get",
path: "/getreaders",
responses: responses(),
}),
async (c: any) => {
apiHit(c, { endpoint: "/getreaders" });
const { data, error } = await tryCatch(getReaders());
if (error) {
return c.json({
error,
});
}
return c.json({
data,
});
}
);
export default app;

View File

@@ -8,6 +8,23 @@ import { subModules } from "../../../../database/schema/subModules.js";
import { createLog } from "../../logger/logger.js";
// "view", "technician", "supervisor","manager", "admin", "systemAdmin"
const newSubModules = [
{
name: "RFID",
moduleName: "prodcution",
description: "RFID stuff",
link: "/rfid",
icon: "Tags",
active: true,
roles: [
"viewer",
"technician",
"supervisor",
"manager",
"admin",
"systemAdmin",
],
subSubModule: [],
},
{
name: "Silo Adjustments",
moduleName: "logistics",