diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a28b80..ee8a7d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -67,6 +67,7 @@ "preseed", "prodlabels", "prolink", + "Skelly", "trycatch" ], "gitea.token": "8456def90e1c651a761a8711763d6ef225d6b2db", diff --git a/backend/rfid/daytonConfig copy.json b/backend/rfid/daytonConfig copy.json new file mode 100644 index 0000000..d78594d --- /dev/null +++ b/backend/rfid/daytonConfig copy.json @@ -0,0 +1,188 @@ +{ + "GPIO-LED": { + "GPODefaults": { + "1": "HIGH", + "2": "HIGH", + "3": "HIGH", + "4": "HIGH" + }, + "LEDDefaults": { + "3": "GREEN" + }, + "TAG_READ": [ + { + "pin": 1, + "state": "HIGH", + "type": "GPO" + } + ] + }, + "READER-GATEWAY": { + "batching": [ + { + "maxPayloadSizePerReport": 256000, + "reportingInterval": 2000 + } + ], + "endpointConfig": { + "data": { + "event": { + "connections": [ + { + "additionalOptions": { + "batching": { + "maxPayloadSizePerReport": 256000, + "reportingInterval": 2000 + }, + "retention": { + "maxEventRetentionTimeInMin": 500, + "maxNumEvents": 150000, + "throttle": 100 + } + }, + "description": "", + "name": "LST", + "options": { + "URL": "https://usday1prod.alpla.net/lst/old/api/rfid/taginfo/line3.4", + "security": { + "CACertificateFileLocation": "", + "authenticationOptions": { + "privateKeyFileLocation": "/readerconfig/ssl/server.key", + "publicKeyFileLocation": "/readerconfig/ssl/server.crt" + }, + "authenticationType": "NONE", + "verifyHost": false, + "verifyPeer": false + } + }, + "type": "httpPost" + } + ] + } + } + }, + "managementEventConfig": { + "errors": { + "antenna": false, + "cpu": { + "reportIntervalInSec": 1800, + "threshold": 90 + }, + "database": true, + "flash": { + "reportIntervalInSec": 1800, + "threshold": 90 + }, + "ntp": true, + "radio": true, + "radio_control": true, + "ram": { + "reportIntervalInSec": 1800, + "threshold": 90 + }, + "reader_gateway": true, + "userApp": { + "reportIntervalInSec": 1800, + "threshold": 120 + } + }, + "gpiEvents": true, + "gpoEvents": true, + "heartbeat": { + "fields": { + "radio_control": [ + "ANTENNAS", + "RADIO_ACTIVITY", + "RADIO_CONNECTION", + "CPU", + "RAM", + "UPTIME", + "NUM_ERRORS", + "NUM_WARNINGS", + "NUM_TAG_READS", + "NUM_TAG_READS_PER_ANTENNA", + "NUM_DATA_MESSAGES_TXED", + "NUM_RADIO_PACKETS_RXED" + ], + "reader_gateway": [ + "NUM_DATA_MESSAGES_RXED", + "NUM_MANAGEMENT_EVENTS_TXED", + "NUM_DATA_MESSAGES_TXED", + "NUM_DATA_MESSAGES_RETAINED", + "NUM_DATA_MESSAGES_DROPPED", + "CPU", + "RAM", + "UPTIME", + "NUM_ERRORS", + "NUM_WARNINGS", + "INTERFACE_CONNECTION_STATUS", + "NOLOCKQ_DEPTH" + ], + "system": [ + "CPU", + "FLASH", + "NTP", + "RAM", + "SYSTEMTIME", + "TEMPERATURE", + "UPTIME", + "GPO", + "GPI", + "POWER_NEGOTIATION", + "POWER_SOURCE", + "MAC_ADDRESS", + "HOSTNAME" + ], + "userDefined": null, + "userapps": [ + "STATUS", + "CPU", + "RAM", + "UPTIME", + "NUM_DATA_MESSAGES_RXED", + "NUM_DATA_MESSAGES_TXED", + "INCOMING_DATA_BUFFER_PERCENTAGE_REMAINING", + "OUTGOING_DATA_BUFFER_PERCENTAGE_REMAINING" + ] + }, + "interval": 60 + }, + "userappEvents": true, + "warnings": { + "cpu": { + "reportIntervalInSec": 1800, + "threshold": 80 + }, + "database": true, + "flash": { + "reportIntervalInSec": 1800, + "threshold": 80 + }, + "ntp": true, + "radio_api": true, + "radio_control": true, + "ram": { + "reportIntervalInSec": 1800, + "threshold": 80 + }, + "reader_gateway": true, + "temperature": { + "ambient": 75, + "pa": 105 + }, + "userApp": { + "reportIntervalInSec": 1800, + "threshold": 60 + } + } + }, + "retention": [ + { + "maxEventRetentionTimeInMin": 500, + "maxNumEvents": 150000, + "throttle": 100 + } + ] + }, + "xml": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +} \ No newline at end of file diff --git a/backend/rfid/daytonConfig.json b/backend/rfid/daytonConfig.json new file mode 100644 index 0000000..a3fc209 --- /dev/null +++ b/backend/rfid/daytonConfig.json @@ -0,0 +1,206 @@ +{ + "GPIO-LED": { + "GPODefaults": { + "1": "HIGH", + "2": "HIGH", + "3": "HIGH", + "4": "HIGH" + }, + "LEDDefaults": { + "3": "GREEN" + }, + "TAG_READ": [ + { + "pin": 1, + "state": "HIGH", + "type": "GPO" + } + ] + }, + "READER-GATEWAY": { + "batching": [ + { + "maxPayloadSizePerReport": 256000, + "reportingInterval": 2000 + } + ], + "endpointConfig": { + "data": { + "event": { + "connections": [ + { + "additionalOptions": { + "retention": { + "maxEventRetentionTimeInMin": 500, + "maxNumEvents": 150000, + "throttle": 100 + } + }, + "description": "", + "name": "lst", + "options": { + "URL": "http://usday1vms006:3100/api/rfid/taginfo/wrapper1", + "security": { + "CACertificateFileLocation": "", + "authenticationOptions": {}, + "authenticationType": "NONE", + "verifyHost": false, + "verifyPeer": false + } + }, + "type": "httpPost" + }, + { + "additionalOptions": { + "retention": { + "maxEventRetentionTimeInMin": 500, + "maxNumEvents": 150000, + "throttle": 100 + } + }, + "description": "", + "name": "mgt", + "options": { + "URL": "http://usday1vms006:3100/api/rfid/mgtevents/wrapper1", + "security": { + "CACertificateFileLocation": "", + "authenticationOptions": {}, + "authenticationType": "NONE", + "verifyHost": false, + "verifyPeer": false + } + }, + "type": "httpPost" + } + ] + } + } + }, + "interfaces": { + "tagDataInterface1": "lst", + "managementEventsInterface": "mgt" + }, + "managementEventConfig": { + "errors": { + "antenna": false, + "cpu": { + "reportIntervalInSec": 1800, + "threshold": 90 + }, + "database": true, + "flash": { + "reportIntervalInSec": 1800, + "threshold": 90 + }, + "ntp": true, + "radio": true, + "radio_control": true, + "ram": { + "reportIntervalInSec": 1800, + "threshold": 90 + }, + "reader_gateway": true, + "userApp": { + "reportIntervalInSec": 1800, + "threshold": 120 + } + }, + "gpiEvents": true, + "gpoEvents": true, + "heartbeat": { + "fields": { + "radio_control": [ + "ANTENNAS", + "RADIO_ACTIVITY", + "RADIO_CONNECTION", + "CPU", + "RAM", + "UPTIME", + "NUM_ERRORS", + "NUM_WARNINGS", + "NUM_TAG_READS", + "NUM_TAG_READS_PER_ANTENNA", + "NUM_DATA_MESSAGES_TXED", + "NUM_RADIO_PACKETS_RXED" + ], + "reader_gateway": [ + "NUM_DATA_MESSAGES_RXED", + "NUM_MANAGEMENT_EVENTS_TXED", + "NUM_DATA_MESSAGES_TXED", + "NUM_DATA_MESSAGES_RETAINED", + "NUM_DATA_MESSAGES_DROPPED", + "CPU", + "RAM", + "UPTIME", + "NUM_ERRORS", + "NUM_WARNINGS", + "INTERFACE_CONNECTION_STATUS", + "NOLOCKQ_DEPTH" + ], + "system": [ + "CPU", + "FLASH", + "NTP", + "RAM", + "SYSTEMTIME", + "TEMPERATURE", + "UPTIME", + "GPO", + "GPI", + "POWER_NEGOTIATION", + "POWER_SOURCE", + "MAC_ADDRESS", + "HOSTNAME" + ], + "userDefined": null, + "userapps": [ + "STATUS", + "CPU", + "RAM", + "UPTIME", + "NUM_DATA_MESSAGES_RXED", + "NUM_DATA_MESSAGES_TXED", + "INCOMING_DATA_BUFFER_PERCENTAGE_REMAINING", + "OUTGOING_DATA_BUFFER_PERCENTAGE_REMAINING" + ] + }, + "interval": 60 + }, + "userappEvents": true, + "warnings": { + "cpu": { + "reportIntervalInSec": 1800, + "threshold": 80 + }, + "database": true, + "flash": { + "reportIntervalInSec": 1800, + "threshold": 80 + }, + "ntp": true, + "radio_api": true, + "radio_control": true, + "ram": { + "reportIntervalInSec": 1800, + "threshold": 80 + }, + "reader_gateway": true, + "temperature": { + "ambient": 75, + "pa": 105 + }, + "userApp": { + "reportIntervalInSec": 1800, + "threshold": 60 + } + } + }, + "retention": [ + { + "maxEventRetentionTimeInMin": 500, + "maxNumEvents": 150000, + "throttle": 100 + } + ] + } +} \ No newline at end of file diff --git a/backend/socket.io/roomDefinitions.socket.ts b/backend/socket.io/roomDefinitions.socket.ts index 340ecca..11482af 100644 --- a/backend/socket.io/roomDefinitions.socket.ts +++ b/backend/socket.io/roomDefinitions.socket.ts @@ -8,8 +8,8 @@ type RoomDefinition = { }; export const protectedRooms: any = { - logs: { requiresAuth: true, role: "admin" }, - admin: { requiresAuth: true, role: "admin" }, + logs: { requiresAuth: true, role: ["admin", "systemAdmin"] }, + admin: { requiresAuth: true, role: ["admin", "systemAdmin"] }, }; export const roomDefinition: Record = { diff --git a/backend/socket.io/serverSetup.ts b/backend/socket.io/serverSetup.ts index dba1ef3..0d6a995 100644 --- a/backend/socket.io/serverSetup.ts +++ b/backend/socket.io/serverSetup.ts @@ -13,9 +13,9 @@ import { createRoomEmitter, preseedRoom } from "./roomService.socket.js"; //const __dirname = dirname(__filename); const log = createLogger({ module: "socket.io", subModule: "setup" }); +import { auth } from "../utils/auth.utils.js"; //import type { Session, User } from "better-auth"; // adjust if needed import { protectedRooms } from "./roomDefinitions.socket.js"; -import { auth } from "../utils/auth.utils.js"; // declare module "socket.io" { // interface Socket { @@ -88,7 +88,12 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => { }); } - if (config?.role && s.user?.role !== config.role) { + const roles = Array.isArray(config.role) ? config.role : [config.role]; + + console.log(roles, s.user.role); + + //if (config?.role && s.user?.role !== config.role) { + if (config?.role && !roles.includes(s.user?.role)) { return s.emit("room-error", { room: rn, message: `Not authorized to be in room: ${rn}`, diff --git a/brunoApi/environments/lstv3.bru b/brunoApi/environments/lstv3.bru index ea6d279..b05fb6c 100644 --- a/brunoApi/environments/lstv3.bru +++ b/brunoApi/environments/lstv3.bru @@ -1,3 +1,7 @@ vars { url: http://localhost:3000/lst + readerIp: 10.44.14.215 } +vars:secret [ + token +] diff --git a/brunoApi/rfidReaders/folder.bru b/brunoApi/rfidReaders/folder.bru new file mode 100644 index 0000000..1a5b040 --- /dev/null +++ b/brunoApi/rfidReaders/folder.bru @@ -0,0 +1,8 @@ +meta { + name: rfidReaders + seq: 8 +} + +auth { + mode: inherit +} diff --git a/brunoApi/rfidReaders/reader.bru b/brunoApi/rfidReaders/reader.bru new file mode 100644 index 0000000..e443cf9 --- /dev/null +++ b/brunoApi/rfidReaders/reader.bru @@ -0,0 +1,20 @@ +meta { + name: reader + type: http + seq: 2 +} + +post { + url: https://usday1prod.alpla.net/lst/old/api/rfid/mgtevents/line3.1 + body: json + auth: inherit +} + +body:json { + {} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/brunoApi/rfidReaders/readerSpecific/Config.bru b/brunoApi/rfidReaders/readerSpecific/Config.bru new file mode 100644 index 0000000..633b494 --- /dev/null +++ b/brunoApi/rfidReaders/readerSpecific/Config.bru @@ -0,0 +1,20 @@ +meta { + name: Config + type: http + seq: 2 +} + +get { + url: https://{{readerIp}}/cloud/config + body: none + auth: bearer +} + +auth:bearer { + token: {{token}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/brunoApi/rfidReaders/readerSpecific/Login.bru b/brunoApi/rfidReaders/readerSpecific/Login.bru new file mode 100644 index 0000000..4ca6e21 --- /dev/null +++ b/brunoApi/rfidReaders/readerSpecific/Login.bru @@ -0,0 +1,32 @@ +meta { + name: Login + type: http + seq: 1 +} + +get { + url: https://{{readerIp}}/cloud/localRestLogin + body: none + auth: basic +} + +auth:basic { + username: admin + password: Zebra123! +} + +script:post-response { + const body = res.getBody(); + + if (body.message) { + bru.setEnvVar("token", body.message); + } else { + bru.setEnvVar("token", "error"); + } + +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/brunoApi/rfidReaders/readerSpecific/Update Config.bru b/brunoApi/rfidReaders/readerSpecific/Update Config.bru new file mode 100644 index 0000000..9f4ecae --- /dev/null +++ b/brunoApi/rfidReaders/readerSpecific/Update Config.bru @@ -0,0 +1,237 @@ +meta { + name: Update Config + type: http + seq: 3 +} + +put { + url: https://{{readerIp}}/cloud/config + body: json + auth: bearer +} + +headers { + Content-Type: application/json +} + +auth:bearer { + token: {{token}} +} + +body:json { + { + "GPIO-LED": { + "GPODefaults": { + "1": "HIGH", + "2": "HIGH", + "3": "HIGH", + "4": "HIGH" + }, + "LEDDefaults": { + "3": "GREEN" + }, + "TAG_READ": [ + { + "pin": 1, + "state": "HIGH", + "type": "GPO" + } + ] + }, + "READER-GATEWAY": { + "batching": [ + { + "maxPayloadSizePerReport": 256000, + "reportingInterval": 2000 + }, + { + "maxPayloadSizePerReport": 256000, + "reportingInterval": 2000 + } + ], + "endpointConfig": { + "data": { + "event": { + "connections": [ + { + "additionalOptions": { + "retention": { + "maxEventRetentionTimeInMin": 500, + "maxNumEvents": 150000, + "throttle": 100 + } + }, + "description": "", + "name": "LST", + "options": { + "URL": "https://usday1prod.alpla.net/lst/old/api/rfid/taginfo/line3.4", + "security": { + "CACertificateFileLocation": "", + "authenticationOptions": {}, + "authenticationType": "NONE", + "verifyHost": false, + "verifyPeer": false + } + }, + "type": "httpPost" + }, + { + "additionalOptions": { + "retention": { + "maxEventRetentionTimeInMin": 500, + "maxNumEvents": 150000, + "throttle": 100 + } + }, + "description": "", + "name": "mgt", + "options": { + "URL": "https://usday1prod.alpla.net/lst/old/api/rfid/mgtevents/line3.4", + "security": { + "CACertificateFileLocation": "", + "authenticationOptions": {}, + "authenticationType": "NONE", + "verifyHost": false, + "verifyPeer": false + } + }, + "type": "httpPost" + } + ] + } + } + }, + "managementEventConfig": { + "errors": { + "antenna": false, + "cpu": { + "reportIntervalInSec": 1800, + "threshold": 90 + }, + "database": true, + "flash": { + "reportIntervalInSec": 1800, + "threshold": 90 + }, + "ntp": true, + "radio": true, + "radio_control": true, + "ram": { + "reportIntervalInSec": 1800, + "threshold": 90 + }, + "reader_gateway": true, + "userApp": { + "reportIntervalInSec": 1800, + "threshold": 120 + } + }, + "gpiEvents": true, + "gpoEvents": true, + "heartbeat": { + "fields": { + "radio_control": [ + "ANTENNAS", + "RADIO_ACTIVITY", + "RADIO_CONNECTION", + "CPU", + "RAM", + "UPTIME", + "NUM_ERRORS", + "NUM_WARNINGS", + "NUM_TAG_READS", + "NUM_TAG_READS_PER_ANTENNA", + "NUM_DATA_MESSAGES_TXED", + "NUM_RADIO_PACKETS_RXED" + ], + "reader_gateway": [ + "NUM_DATA_MESSAGES_RXED", + "NUM_MANAGEMENT_EVENTS_TXED", + "NUM_DATA_MESSAGES_TXED", + "NUM_DATA_MESSAGES_RETAINED", + "NUM_DATA_MESSAGES_DROPPED", + "CPU", + "RAM", + "UPTIME", + "NUM_ERRORS", + "NUM_WARNINGS", + "INTERFACE_CONNECTION_STATUS", + "NOLOCKQ_DEPTH" + ], + "system": [ + "CPU", + "FLASH", + "NTP", + "RAM", + "SYSTEMTIME", + "TEMPERATURE", + "UPTIME", + "GPO", + "GPI", + "POWER_NEGOTIATION", + "POWER_SOURCE", + "MAC_ADDRESS", + "HOSTNAME" + ], + "userapps": [ + "STATUS", + "CPU", + "RAM", + "UPTIME", + "NUM_DATA_MESSAGES_RXED", + "NUM_DATA_MESSAGES_TXED", + "INCOMING_DATA_BUFFER_PERCENTAGE_REMAINING", + "OUTGOING_DATA_BUFFER_PERCENTAGE_REMAINING" + ] + }, + "interval": 60 + }, + "userappEvents": true, + "warnings": { + "cpu": { + "reportIntervalInSec": 1800, + "threshold": 80 + }, + "database": true, + "flash": { + "reportIntervalInSec": 1800, + "threshold": 80 + }, + "ntp": true, + "radio_api": true, + "radio_control": true, + "ram": { + "reportIntervalInSec": 1800, + "threshold": 80 + }, + "reader_gateway": true, + "temperature": { + "ambient": 75, + "pa": 105 + }, + "userApp": { + "reportIntervalInSec": 1800, + "threshold": 60 + } + } + }, + "retention": [ + { + "maxEventRetentionTimeInMin": 500, + "maxNumEvents": 150000, + "throttle": 100 + }, + { + "maxEventRetentionTimeInMin": 500, + "maxNumEvents": 150000, + "throttle": 100 + } + ] + } + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/brunoApi/rfidReaders/readerSpecific/folder.bru b/brunoApi/rfidReaders/readerSpecific/folder.bru new file mode 100644 index 0000000..c2c163f --- /dev/null +++ b/brunoApi/rfidReaders/readerSpecific/folder.bru @@ -0,0 +1,12 @@ +meta { + name: readerSpecific +} + +auth { + mode: basic +} + +auth:basic { + username: admin + password: Zebra123! +} diff --git a/frontend/.tanstack/tmp/ed188a72-a9536593d3b207d3d1a62b792253ec09 b/frontend/.tanstack/tmp/ed188a72-a9536593d3b207d3d1a62b792253ec09 new file mode 100644 index 0000000..0d251e2 --- /dev/null +++ b/frontend/.tanstack/tmp/ed188a72-a9536593d3b207d3d1a62b792253ec09 @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/admin/settings')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/admin/settings"!
+} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 48b7835..f3c8c9c 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -69,7 +69,7 @@ export default function Header() { - Profile + Account {/* Billing diff --git a/frontend/src/components/Sidebar/AdminBar.tsx b/frontend/src/components/Sidebar/AdminBar.tsx index 0d9f3d5..9959e20 100644 --- a/frontend/src/components/Sidebar/AdminBar.tsx +++ b/frontend/src/components/Sidebar/AdminBar.tsx @@ -11,17 +11,27 @@ import { useSidebar, } from "../ui/sidebar"; -export default function AdminSidebar() { +type AdminSidebarProps = { + session: { + user: { + name?: string | null; + email?: string | null; + role?: string | string[]; + }; + } | null; +}; + +export default function AdminSidebar({ session }: any) { const { setOpen } = useSidebar(); const items = [ - // { - // title: "Users", - // url: "/admin/users", - // icon: User, - // role: ["systemAdmin", "admin"], - // module: "admin", - // active: true, - // }, + { + title: "Settings", + url: "/admin/settings", + icon: Logs, + role: ["systemAdmin"], + module: "admin", + active: true, + }, { title: "Logs", url: "/admin/logs", @@ -53,14 +63,18 @@ export default function AdminSidebar() { {items.map((item) => ( - - - setOpen(false)}> - - {item.title} - - - + <> + {item.role.includes(session.user.role) && ( + + + setOpen(false)}> + + {item.title} + + + + )} + ))} diff --git a/frontend/src/components/Sidebar/sidebar.tsx b/frontend/src/components/Sidebar/sidebar.tsx index 584e5c1..798509b 100644 --- a/frontend/src/components/Sidebar/sidebar.tsx +++ b/frontend/src/components/Sidebar/sidebar.tsx @@ -10,6 +10,7 @@ import AdminSidebar from "./AdminBar"; export function AppSidebar() { const { data: session } = useSession(); + return ( - {session && session.user.role === "admin" && } + {session && + (session.user.role === "admin" || + session.user.role === "systemAdmin") && ( + + )} diff --git a/frontend/src/components/ui/scroll-area.tsx b/frontend/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..f49b0a8 --- /dev/null +++ b/frontend/src/components/ui/scroll-area.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import { ScrollArea as ScrollAreaPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/frontend/src/components/ui/switch.tsx b/frontend/src/components/ui/switch.tsx new file mode 100644 index 0000000..877dbc8 --- /dev/null +++ b/frontend/src/components/ui/switch.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import { Switch as SwitchPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Switch({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + + + ) +} + +export { Switch } diff --git a/frontend/src/components/ui/table.tsx b/frontend/src/components/ui/table.tsx new file mode 100644 index 0000000..1017c00 --- /dev/null +++ b/frontend/src/components/ui/table.tsx @@ -0,0 +1,114 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ) +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ) +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + + ) +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className + )} + {...props} + /> + ) +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ) +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
+ ) +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + + ) +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( +
+ ) +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx new file mode 100644 index 0000000..2cb6924 --- /dev/null +++ b/frontend/src/components/ui/tabs.tsx @@ -0,0 +1,88 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Tabs as TabsPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + orientation = "horizontal", + ...props +}: React.ComponentProps) { + return ( + + ) +} + +const tabsListVariants = cva( + "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none", + { + variants: { + variant: { + default: "bg-muted", + line: "gap-1 bg-transparent", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function TabsList({ + className, + variant = "default", + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } diff --git a/frontend/src/components/ui/toggle.tsx b/frontend/src/components/ui/toggle.tsx new file mode 100644 index 0000000..2284a77 --- /dev/null +++ b/frontend/src/components/ui/toggle.tsx @@ -0,0 +1,44 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Toggle as TogglePrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +const toggleVariants = cva( + "group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground 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 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-transparent", + outline: "border border-input bg-transparent hover:bg-muted", + }, + size: { + default: "h-8 min-w-8 px-2", + sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-1.5 text-[0.8rem]", + lg: "h-9 min-w-9 px-2.5", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Toggle({ + className, + variant = "default", + size = "default", + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ) +} + +export { Toggle, toggleVariants } diff --git a/frontend/src/lib/formSutff/Input.Field.tsx b/frontend/src/lib/formSutff/Input.Field.tsx index 5c39826..66a1c94 100644 --- a/frontend/src/lib/formSutff/Input.Field.tsx +++ b/frontend/src/lib/formSutff/Input.Field.tsx @@ -6,7 +6,7 @@ import { FieldErrors } from "./Errors.Field"; type InputFieldProps = { label: string; inputType: string; - required: boolean; + required?: boolean; }; const autoCompleteMap: Record = { @@ -16,7 +16,11 @@ const autoCompleteMap: Record = { username: "username", }; -export const InputField = ({ label, inputType, required }: InputFieldProps) => { +export const InputField = ({ + label, + inputType, + required = false, +}: InputFieldProps) => { const field = useFieldContext(); return ( diff --git a/frontend/src/lib/formSutff/SubmitButton.tsx b/frontend/src/lib/formSutff/SubmitButton.tsx index 71ac4ae..5859e8b 100644 --- a/frontend/src/lib/formSutff/SubmitButton.tsx +++ b/frontend/src/lib/formSutff/SubmitButton.tsx @@ -1,13 +1,19 @@ import { useStore } from "@tanstack/react-form"; import { Button } from "@/components/ui/button"; -import { useFormContext } from "."; import { Spinner } from "@/components/ui/spinner"; +import { useFormContext } from "."; type SubmitButtonProps = { children: React.ReactNode; + variant?: "default" | "secondary" | "destructive"; + className?: string; }; -export const SubmitButton = ({ children }: SubmitButtonProps) => { +export const SubmitButton = ({ + children, + variant = "default", + className, +}: SubmitButtonProps) => { const form = useFormContext(); const [isSubmitting] = useStore(form.store, (state) => [ @@ -17,10 +23,19 @@ export const SubmitButton = ({ children }: SubmitButtonProps) => { return (
-
); diff --git a/frontend/src/lib/formSutff/Switch.Field.tsx b/frontend/src/lib/formSutff/Switch.Field.tsx new file mode 100644 index 0000000..4733dc3 --- /dev/null +++ b/frontend/src/lib/formSutff/Switch.Field.tsx @@ -0,0 +1,29 @@ +import { Label } from "../../components/ui/label"; +import { Switch } from "../../components/ui/switch"; +import { useFieldContext } from "."; + +type SwitchField = { + trueLabel: string; + falseLabel: string; +}; + +export const SwitchField = ({ + trueLabel = "True", + falseLabel = "False", +}: SwitchField) => { + const field = useFieldContext(); + + const checked = field.state.value ?? false; + + return ( +
+ + +
+ ); +}; diff --git a/frontend/src/lib/formSutff/index.tsx b/frontend/src/lib/formSutff/index.tsx index b784505..a4d8163 100644 --- a/frontend/src/lib/formSutff/index.tsx +++ b/frontend/src/lib/formSutff/index.tsx @@ -3,6 +3,7 @@ import { CheckboxField } from "./CheckBox.Field"; import { InputField } from "./Input.Field"; import { InputPasswordField } from "./InputPassword.Field"; import { SubmitButton } from "./SubmitButton"; +import { SwitchField } from "./Switch.Field"; export const { fieldContext, useFieldContext, formContext, useFormContext } = createFormHookContexts(); @@ -16,6 +17,7 @@ export const { useAppForm } = createFormHook({ //DateField, //TextArea, //Searchable, + SwitchField, }, formComponents: { SubmitButton }, fieldContext, diff --git a/frontend/src/lib/queries/getSettings.ts b/frontend/src/lib/queries/getSettings.ts new file mode 100644 index 0000000..a4f757b --- /dev/null +++ b/frontend/src/lib/queries/getSettings.ts @@ -0,0 +1,22 @@ +import { keepPreviousData, queryOptions } from "@tanstack/react-query"; +import axios from "axios"; + +export function getSettings() { + return queryOptions({ + queryKey: ["getSettings"], + queryFn: () => fetch(), + staleTime: 5000, + refetchOnWindowFocus: true, + placeholderData: keepPreviousData, + }); +} + +const fetch = async () => { + if (window.location.hostname === "localhost") { + await new Promise((res) => setTimeout(res, 5000)); + } + + const { data } = await axios.get("/lst/api/settings"); + + return data.data; +}; diff --git a/frontend/src/lib/tableStuff/EditableCellInput.tsx b/frontend/src/lib/tableStuff/EditableCellInput.tsx new file mode 100644 index 0000000..fddf0e2 --- /dev/null +++ b/frontend/src/lib/tableStuff/EditableCellInput.tsx @@ -0,0 +1,70 @@ +import { useEffect, useRef, useState } from "react"; +import { Input } from "../../components/ui/input"; + +type EditableCell = { + value: string | number | null | undefined; + id: string; + field: string; + className?: string; + onSubmit: (args: { id: string; field: string; value: string }) => void; +}; + +export default function EditableCellInput({ + value, + id, + field, + className = "w-32", + onSubmit, +}: EditableCell) { + const initialValue = String(value ?? ""); + const [localValue, setLocalValue] = useState(initialValue); + const submitting = useRef(false); + + useEffect(() => { + setLocalValue(initialValue); + }, [initialValue]); + + const handleSubmit = (nextValue: string) => { + const trimmedValue = nextValue.trim(); + + if (trimmedValue === initialValue) return; + + onSubmit({ + id, + field, + value: trimmedValue, + }); + }; + + return ( + setLocalValue(e.currentTarget.value)} + onBlur={(e) => { + if (submitting.current) return; + + submitting.current = true; + handleSubmit(e.currentTarget.value); + setTimeout(() => { + submitting.current = false; + }, 100); + }} + onKeyDown={(e) => { + if (e.key !== "Enter") return; + + e.preventDefault(); + + if (submitting.current) return; + + submitting.current = true; + handleSubmit(e.currentTarget.value); + e.currentTarget.blur(); + + setTimeout(() => { + submitting.current = false; + }, 100); + }} + /> + ); +} diff --git a/frontend/src/lib/tableStuff/LstTable.tsx b/frontend/src/lib/tableStuff/LstTable.tsx new file mode 100644 index 0000000..7f3990e --- /dev/null +++ b/frontend/src/lib/tableStuff/LstTable.tsx @@ -0,0 +1,129 @@ +import { + type ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + type SortingState, + useReactTable, +} from "@tanstack/react-table"; +import React, { useState } from "react"; +import { Button } from "../../components/ui/button"; +import { ScrollArea, ScrollBar } from "../../components/ui/scroll-area"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "../../components/ui/table"; +import { cn } from "../utils"; + +type LstTableType = { + className?: string; + tableClassName?: string; + data: any; + columns: any; +}; +export default function LstTable({ + className = "", + tableClassName = "", + data, + columns, +}: LstTableType) { + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + //console.log(data); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + //renderSubComponent: ({ row }: { row: any }) => , + //getRowCanExpand: () => true, + filterFns: {}, + state: { + sorting, + columnFilters, + }, + }); + return ( +
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + + )) + ) : ( + + + No results. + + + )} + +
+ +
+
+ + +
+
+ ); +} diff --git a/frontend/src/lib/tableStuff/SearchableHeader.tsx b/frontend/src/lib/tableStuff/SearchableHeader.tsx new file mode 100644 index 0000000..77224ff --- /dev/null +++ b/frontend/src/lib/tableStuff/SearchableHeader.tsx @@ -0,0 +1,67 @@ +import type { Column } from "@tanstack/react-table"; +import { ArrowDown, ArrowUp, Search } from "lucide-react"; +import React, { useState } from "react"; +import { Button } from "../../components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from "../../components/ui/dropdown-menu"; +import { Input } from "../../components/ui/input"; +import { cn } from "../utils"; + +type SearchableHeaderProps = { + column: Column; + title: string; + searchable?: boolean; +}; + +export default function SearchableHeader({ + column, + title, + searchable = false, +}: SearchableHeaderProps) { + const [open, setOpen] = useState(false); + return ( +
+ + {searchable && ( + + + + + + + column.setFilterValue(e.target.value)} + placeholder={`Search ${title.toLowerCase()}...`} + className="h-8" + /> + + + )} +
+ ); +} diff --git a/frontend/src/lib/tableStuff/SkellyTable.tsx b/frontend/src/lib/tableStuff/SkellyTable.tsx new file mode 100644 index 0000000..54b6efe --- /dev/null +++ b/frontend/src/lib/tableStuff/SkellyTable.tsx @@ -0,0 +1,40 @@ +import { Skeleton } from "../../components/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "../../components/ui/table"; + +type TableSkelly = { + rows?: number; + columns?: number; +}; +export default function SkellyTable({ rows = 5, columns = 4 }: TableSkelly) { + return ( +
+ + + {Array.from({ length: columns }).map((_, i) => ( + + + + ))} + + + {Array.from({ length: rows }).map((_, r) => ( + + {Array.from({ length: columns }).map((_, c) => ( + + + + ))} + + ))} + +
+
+ ); +} diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index a81e988..f8f4a28 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -11,6 +11,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as AboutRouteImport } from './routes/about' import { Route as IndexRouteImport } from './routes/index' +import { Route as AdminSettingsRouteImport } from './routes/admin/settings' import { Route as AdminLogsRouteImport } from './routes/admin/logs' import { Route as authLoginRouteImport } from './routes/(auth)/login' import { Route as authUserSignupRouteImport } from './routes/(auth)/user.signup' @@ -27,6 +28,11 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const AdminSettingsRoute = AdminSettingsRouteImport.update({ + id: '/admin/settings', + path: '/admin/settings', + getParentRoute: () => rootRouteImport, +} as any) const AdminLogsRoute = AdminLogsRouteImport.update({ id: '/admin/logs', path: '/admin/logs', @@ -58,6 +64,7 @@ export interface FileRoutesByFullPath { '/about': typeof AboutRoute '/login': typeof authLoginRoute '/admin/logs': typeof AdminLogsRoute + '/admin/settings': typeof AdminSettingsRoute '/user/profile': typeof authUserProfileRoute '/user/resetpassword': typeof authUserResetpasswordRoute '/user/signup': typeof authUserSignupRoute @@ -67,6 +74,7 @@ export interface FileRoutesByTo { '/about': typeof AboutRoute '/login': typeof authLoginRoute '/admin/logs': typeof AdminLogsRoute + '/admin/settings': typeof AdminSettingsRoute '/user/profile': typeof authUserProfileRoute '/user/resetpassword': typeof authUserResetpasswordRoute '/user/signup': typeof authUserSignupRoute @@ -77,6 +85,7 @@ export interface FileRoutesById { '/about': typeof AboutRoute '/(auth)/login': typeof authLoginRoute '/admin/logs': typeof AdminLogsRoute + '/admin/settings': typeof AdminSettingsRoute '/(auth)/user/profile': typeof authUserProfileRoute '/(auth)/user/resetpassword': typeof authUserResetpasswordRoute '/(auth)/user/signup': typeof authUserSignupRoute @@ -88,6 +97,7 @@ export interface FileRouteTypes { | '/about' | '/login' | '/admin/logs' + | '/admin/settings' | '/user/profile' | '/user/resetpassword' | '/user/signup' @@ -97,6 +107,7 @@ export interface FileRouteTypes { | '/about' | '/login' | '/admin/logs' + | '/admin/settings' | '/user/profile' | '/user/resetpassword' | '/user/signup' @@ -106,6 +117,7 @@ export interface FileRouteTypes { | '/about' | '/(auth)/login' | '/admin/logs' + | '/admin/settings' | '/(auth)/user/profile' | '/(auth)/user/resetpassword' | '/(auth)/user/signup' @@ -116,6 +128,7 @@ export interface RootRouteChildren { AboutRoute: typeof AboutRoute authLoginRoute: typeof authLoginRoute AdminLogsRoute: typeof AdminLogsRoute + AdminSettingsRoute: typeof AdminSettingsRoute authUserProfileRoute: typeof authUserProfileRoute authUserResetpasswordRoute: typeof authUserResetpasswordRoute authUserSignupRoute: typeof authUserSignupRoute @@ -137,6 +150,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/admin/settings': { + id: '/admin/settings' + path: '/admin/settings' + fullPath: '/admin/settings' + preLoaderRoute: typeof AdminSettingsRouteImport + parentRoute: typeof rootRouteImport + } '/admin/logs': { id: '/admin/logs' path: '/admin/logs' @@ -180,6 +200,7 @@ const rootRouteChildren: RootRouteChildren = { AboutRoute: AboutRoute, authLoginRoute: authLoginRoute, AdminLogsRoute: AdminLogsRoute, + AdminSettingsRoute: AdminSettingsRoute, authUserProfileRoute: authUserProfileRoute, authUserResetpasswordRoute: authUserResetpasswordRoute, authUserSignupRoute: authUserSignupRoute, diff --git a/frontend/src/routes/(auth)/-components/ChangePassword.tsx b/frontend/src/routes/(auth)/-components/ChangePassword.tsx index a691340..091296b 100644 --- a/frontend/src/routes/(auth)/-components/ChangePassword.tsx +++ b/frontend/src/routes/(auth)/-components/ChangePassword.tsx @@ -79,7 +79,9 @@ export default function ChangePassword() {
- Update Profile + + Update Password +
diff --git a/frontend/src/routes/(auth)/-components/LoginForm.tsx b/frontend/src/routes/(auth)/-components/LoginForm.tsx index dbf3766..8cef350 100644 --- a/frontend/src/routes/(auth)/-components/LoginForm.tsx +++ b/frontend/src/routes/(auth)/-components/LoginForm.tsx @@ -9,6 +9,7 @@ import { } from "@/components/ui/card"; import { authClient } from "@/lib/auth-client"; import { useAppForm } from "@/lib/formSutff"; +import socket from "../../../lib/socket.io"; export default function LoginForm({ redirectPath }: { redirectPath: string }) { const loginEmail = localStorage.getItem("loginEmail") || ""; @@ -47,8 +48,12 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) { return; } toast.success(`Welcome back ${login.data?.user.name}`); + if (login.data) { + socket.disconnect(); + socket.connect(); + } } catch (error) { - console.log(error); + console.error(error); } }, }); diff --git a/frontend/src/routes/(auth)/login.tsx b/frontend/src/routes/(auth)/login.tsx index 888e9df..a98b572 100644 --- a/frontend/src/routes/(auth)/login.tsx +++ b/frontend/src/routes/(auth)/login.tsx @@ -25,7 +25,7 @@ function RouteComponent() { const redirectPath = search.redirect ?? "/"; return ( -
+
); diff --git a/frontend/src/routes/(auth)/user.profile.tsx b/frontend/src/routes/(auth)/user.profile.tsx index 1a5be2e..d69313c 100644 --- a/frontend/src/routes/(auth)/user.profile.tsx +++ b/frontend/src/routes/(auth)/user.profile.tsx @@ -54,7 +54,7 @@ function RouteComponent() { }, }); return ( -
+
diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 4a0b74a..32cc087 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -11,12 +11,15 @@ const RootLayout = () => (
-
+ +
- - - +
+
+ +
+
diff --git a/frontend/src/routes/admin/-components/FeatureCard.tsx b/frontend/src/routes/admin/-components/FeatureCard.tsx new file mode 100644 index 0000000..17a00dc --- /dev/null +++ b/frontend/src/routes/admin/-components/FeatureCard.tsx @@ -0,0 +1,91 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import axios from "axios"; +import React from "react"; +import { toast } from "sonner"; +import { Card, CardDescription, CardHeader } from "../../../components/ui/card"; +import { useAppForm } from "../../../lib/formSutff"; +import { getSettings } from "../../../lib/queries/getSettings"; + +type Setting = { + id: string; + name: string; + description?: string; + value: string; + active: boolean; + inputType: "text" | "boolean" | "number" | "select"; + options?: string[]; +}; + +export default function FeatureCard({ item }: { item: Setting }) { + const { refetch } = useSuspenseQuery(getSettings()); + const form = useAppForm({ + defaultValues: { + value: item.value ?? "", + active: item.active, + }, + onSubmit: async ({ value }) => { + try { + // adding this in as my base as i need to see timers working + if (window.location.hostname === "localhost") { + await new Promise((res) => setTimeout(res, 1000)); + } + + const { data } = await axios.patch(`/lst/api/settings/${item.name}`, { + value: value.value, + active: value.active ? "true" : "false", + }); + + refetch(); + toast.success( +
+

{data.message}

+

+ This was a feature setting so{" "} + {value.active + ? "processes related to this will start working on there next interval" + : "processes related to this will stop working on there next interval"} +

+
, + ); + } catch (error) { + console.error(error); + } + }, + }); + return ( + + +

{item.name}

+ +

{item.description}

+
+
+ +
{ + e.preventDefault(); + e.stopPropagation(); + void form.handleSubmit(); + }} + > +
+ + {(field) => ( + + )} + +
+ + {(field) => ( + + )} + + + Update + +
+
+
+
+ ); +} diff --git a/frontend/src/routes/admin/-components/FeatureSettings.tsx b/frontend/src/routes/admin/-components/FeatureSettings.tsx new file mode 100644 index 0000000..a97be86 --- /dev/null +++ b/frontend/src/routes/admin/-components/FeatureSettings.tsx @@ -0,0 +1,11 @@ +import FeatureCard from "./FeatureCard"; + +export default function FeatureSettings({ data }: any) { + return ( +
+ {data.map((i: any) => ( + + ))} +
+ ); +} diff --git a/frontend/src/routes/admin/logs.tsx b/frontend/src/routes/admin/logs.tsx index 8f96019..ed172cd 100644 --- a/frontend/src/routes/admin/logs.tsx +++ b/frontend/src/routes/admin/logs.tsx @@ -5,6 +5,7 @@ import { authClient } from "@/lib/auth-client"; export const Route = createFileRoute("/admin/logs")({ beforeLoad: async ({ location }) => { const { data: session } = await authClient.getSession(); + const allowedRole = ["admin", "systemAdmin"]; if (!session?.user) { throw redirect({ @@ -15,7 +16,7 @@ export const Route = createFileRoute("/admin/logs")({ }); } - if (session.user.role !== "admin") { + if (!allowedRole.includes(session.user.role as string)) { throw redirect({ to: "/", }); diff --git a/frontend/src/routes/admin/settings.tsx b/frontend/src/routes/admin/settings.tsx new file mode 100644 index 0000000..92d4eb0 --- /dev/null +++ b/frontend/src/routes/admin/settings.tsx @@ -0,0 +1,209 @@ +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 { Suspense, useMemo } from "react"; +import { toast } from "sonner"; + +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "../../components/ui/card"; + +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "../../components/ui/tabs"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "../../components/ui/tooltip"; +import { authClient } from "../../lib/auth-client"; +import { getSettings } from "../../lib/queries/getSettings"; +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 FeatureSettings from "./-components/FeatureSettings"; + +type Settings = { + settings_id: string; + name: string; + active: boolean; + value: string; + description: string; + moduleName: string; + roles: string[]; +}; + +const updateSettings = async ( + id: string, + data: Record, +) => { + console.log(id, data); + try { + const res = await axios.patch(`/lst/api/settings/${id}`, data, { + withCredentials: true, + }); + toast.success(`Setting just updated`); + return res; + } catch (err) { + toast.error("Error in updating the settings"); + return err; + } +}; + +export const Route = createFileRoute("/admin/settings")({ + beforeLoad: async ({ location }) => { + const { data: session } = await authClient.getSession(); + const allowedRole = ["systemAdmin"]; + + 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, +}); + +function SettingsTableCard() { + const { data, refetch } = useSuspenseQuery(getSettings()); + const columnHelper = createColumnHelper(); + + 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 column = [ + columnHelper.accessor("name", { + header: ({ column }) => ( + + ), + filterFn: "includesString", + cell: (i) => i.getValue(), + }), + columnHelper.accessor("description", { + header: ({ column }) => ( + + ), + cell: (i) => ( + + + {i.getValue().length > 25 ? ( + {i.getValue().slice(0, 25)}... + ) : ( + {i.getValue()} + )} + + {i.getValue()} + + ), + }), + columnHelper.accessor("value", { + header: ({ column }) => ( + + ), + + filterFn: "includesString", + cell: ({ row, getValue }) => ( + { + updateSetting.mutate({ id, field, value }); + }} + /> + ), + }), + ]; + + const { standardSettings, featureSettings, systemSetting } = useMemo(() => { + return { + standardSettings: data.filter( + (setting: any) => setting.settingType === "standard", + ), + featureSettings: data.filter( + (setting: any) => setting.settingType === "feature", + ), + systemSetting: data.filter( + (setting: any) => setting.settingType === "system", + ), + }; + }, [data]); + return ( + <> + + + + + + + + + + + ); +} + +function RouteComponent() { + return ( +
+
+

Settings

+

+ Manage your settings and related data. +

+
+ + + + System Settings + + + + + Features + System + Standard + + }> + + + + + +
+ ); +} diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index c0c81c8..d6fec2e 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -18,10 +18,42 @@ function Index() { if (isPending) return
Loading...
; // if (!session) return + let url: string; + if (window.location.origin.includes("localhost")) { + url = `https://www.youtube.com/watch?v=dQw4w9WgXcQ`; + } else if (window.location.origin.includes("vms006")) { + url = `https://${window.location.hostname.replace("vms006", "prod.alpla.net/")}lst/app/old/ocp`; + } else { + url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; + } return ( -
-

Welcome Home!

+
+

Welcome Lst - V3

+

+

+ This is active in your plant today due to having warehousing activated + and new functions needed to be introduced, you should be still using LST + as you were before +

+

+

+ If you dont know why you are here and looking for One Click Print{" "} + + + Click + + + + + Here + + +

); }