table and query work

This commit is contained in:
2026-03-27 18:30:50 -05:00
parent ddcb7e76a3
commit 32998d417f
41 changed files with 1886 additions and 42 deletions

View File

@@ -67,6 +67,7 @@
"preseed",
"prodlabels",
"prolink",
"Skelly",
"trycatch"
],
"gitea.token": "8456def90e1c651a761a8711763d6ef225d6b2db",

View File

@@ -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": "<?xml version='1.0'?>\n<Motorola xmlns:Falcon='http://www.motorola.com/RFID/Readers/Config/Falcon' xmlns='http://www.motorola.com/RFID/Readers/Config/Falcon'>\n<Config>\n<AppVersion major='3' minor='28' build='1' maintenance='0'/>\n<CommConfig EnabledStacks='IPV4' DisableRAPktProcessing='1' EnableDHCPv6='1' IPv6StaticIPAddr='fe80::1' IPv6SubnetMask='64' IPv6StaticGateway='::' IPv6DNSIP='fe80::20' DHCP='1' IPAddr='10.44.14.39' Mask='255.255.255.0' Gateway='10.44.14.252' DNS='10.44.9.250' DomainSearch='example.com' HttpRunning='2' TelnetActive='2' FtpActive='2' usbMode='0' WatchdogEnabled='1' AvahiEnabled='1' NetBIOSEnabled='0' RDMPAgentEnabled='1' SerialConTimeout='0' SNTP='0.0.0.0' SNTPHostName='pool.ntp.org' sntpHostDisplayMode='0' llrpClientMode='0' llrpSecureMode='0' llrpSecureModeValidatePeer='0' llrpPort='5084' llrpHostIP='192.168.127.2' allowllrpConnOverride='0' shouldReconnect='1'/>\n<Bluetooth discoverable='0' pairable='0' PincodeEnabled='0' passkey='165CB22DA5BE7BBEFB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03' startIP='192.168.0.2' endIP='192.168.0.3'/>\n<WirelessConfig essid='' autoconnect='0'/>\n<RegionConfig RFCountry='United States/Canada' RFRegulatory='US FCC 15' RFScanMode='0' LBTEnable='0' ChannelData='FFFFFFFFFFFFFFFF'/>\n<SnmpConfig snmpVersion='1' heartbeat='1'/>\n<SyslogConfig RemoteIp='0.0.0.0' RemotePort='514' LogMinSeverity='7' ApplyFilter='0' MinimumSeverity='7' ProcessFilter='rmserver.elf,llrpserver.elf,snmpextagent.elf,RDMPAgent'/>\n<UserList>\n<User name='admin' PSWD='$6$weLpDwlv$utr0AwgPIae2O4Gln4cQ2IJJblXye412Xqni0V.ahIFKUOCEDGjzZ4ttthhrw7rmmQYsCXKwA9znyqPkAT.IL/'/>\n<User name='rfidadm' PSWD='15491'/>\n</UserList>\n<IPReader name='FX96007AF832 FX9600 RFID Reader' desc='FX96007AF832 Advanced Reader' flags='0' MonoStatic='0' CheckAntenna='1' gpiDebounceTime='0' gpioMapping='0' idleModeTimeOut='0' diagMode='0' extDiagMode='0' contact='Zebra Technologies Corporation' PowerNegotiation='0' PowerNegotiationProtocol='0' allowGuestLogin='1' configureHostName='0'>\n<ReadPoint name='Read Point 1' flags='0' CableLossPerHundredFt='10' CableLength='10'/>\n<ReadPoint name='Read Point 2' flags='0' CableLossPerHundredFt='10' CableLength='10'/>\n<ReadPoint name='Read Point 3' flags='1' CableLossPerHundredFt='10' CableLength='10'/>\n<ReadPoint name='Read Point 4' flags='1' CableLossPerHundredFt='10' CableLength='10'/>\n</IPReader>\n<SerialPortConf Mode='0' Baudrate='115200' Databits='8' Parity='none' Stopbits='1' Flowcontrol='hardware' TagMetaData='0' InventoryControl='0' IsAutostart='0'/>\n<FXConnectConfig FXConnectMode='0' TagMetaData='0' InventoryControl='None' HeartBeatPeriod='0' IsAutostart='0' PreFilterMode='0' PreFilters='None'/>\n<ProfinetConfig virtualDAP='1'/>\n<NodeJSPortConf Portnumber='8001'/>\n</Config>\n<MOTOROLA_LLRP_CONFIG><LLRP_READER_CONFIG />\n</MOTOROLA_LLRP_CONFIG>\n<IOT_CONNECT_CONFIG><OPERATING_MODE />\n</IOT_CONNECT_CONFIG>\n<RadioProfileData><RadioRegisterData Address='0' Data='00'/>\n</RadioProfileData>\n<CustomProfileData ForceEAPMode='0' FIPS_MODE_ENABLED='0' MaxNumberOfTagsBuffered='512'/>\n</Motorola >\n"
}

View File

@@ -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
}
]
}
}

View File

@@ -8,8 +8,8 @@ type RoomDefinition<T = unknown> = {
};
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<RoomId, RoomDefinition> = {

View File

@@ -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}`,

View File

@@ -1,3 +1,7 @@
vars {
url: http://localhost:3000/lst
readerIp: 10.44.14.215
}
vars:secret [
token
]

View File

@@ -0,0 +1,8 @@
meta {
name: rfidReaders
seq: 8
}
auth {
mode: inherit
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1,12 @@
meta {
name: readerSpecific
}
auth {
mode: basic
}
auth:basic {
username: admin
password: Zebra123!
}

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/admin/settings')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/admin/settings"!</div>
}

View File

@@ -69,7 +69,7 @@ export default function Header() {
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Link to="/user/profile">Profile</Link>
<Link to="/user/profile">Account</Link>
</DropdownMenuItem>
{/* <DropdownMenuItem>Billing</DropdownMenuItem>

View File

@@ -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,6 +63,8 @@ export default function AdminSidebar() {
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<>
{item.role.includes(session.user.role) && (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}>
@@ -61,6 +73,8 @@ export default function AdminSidebar() {
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)}
</>
))}
</SidebarMenu>
</SidebarGroupContent>

View File

@@ -10,6 +10,7 @@ import AdminSidebar from "./AdminBar";
export function AppSidebar() {
const { data: session } = useSession();
return (
<Sidebar
variant="sidebar"
@@ -20,7 +21,11 @@ export function AppSidebar() {
<SidebarMenu>
<SidebarMenuItem>
<SidebarContent>
{session && session.user.role === "admin" && <AdminSidebar />}
{session &&
(session.user.role === "admin" ||
session.user.role === "systemAdmin") && (
<AdminSidebar session={session} />
)}
</SidebarContent>
</SidebarMenuItem>
</SidebarMenu>

View File

@@ -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<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
data-orientation={orientation}
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

View File

@@ -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<typeof SwitchPrimitive.Root> & {
size?: "sm" | "default"
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View File

@@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -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<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-horizontal:flex-col",
className
)}
{...props}
/>
)
}
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<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 text-sm outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }

View File

@@ -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<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Toggle, toggleVariants }

View File

@@ -6,7 +6,7 @@ import { FieldErrors } from "./Errors.Field";
type InputFieldProps = {
label: string;
inputType: string;
required: boolean;
required?: boolean;
};
const autoCompleteMap: Record<string, string> = {
@@ -16,7 +16,11 @@ const autoCompleteMap: Record<string, string> = {
username: "username",
};
export const InputField = ({ label, inputType, required }: InputFieldProps) => {
export const InputField = ({
label,
inputType,
required = false,
}: InputFieldProps) => {
const field = useFieldContext<any>();
return (

View File

@@ -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 (
<div className="">
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? <><Spinner data-icon="inline-start" /> Submitting </> : <>{children}</>
}
<Button
type="submit"
disabled={isSubmitting}
variant={variant}
className={className}
>
{isSubmitting ? (
<>
<Spinner data-icon="inline-start" /> Submitting{" "}
</>
) : (
<>{children}</>
)}
</Button>
</div>
);

View File

@@ -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<boolean>();
const checked = field.state.value ?? false;
return (
<div className="flex items-center space-x-2">
<Switch
id={field.name}
checked={checked}
onCheckedChange={field.handleChange}
onBlur={field.handleBlur}
/>
<Label htmlFor={field.name}>{checked ? trueLabel : falseLabel}</Label>
</div>
);
};

View File

@@ -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,

View File

@@ -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;
};

View File

@@ -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 (
<Input
value={localValue}
className={className}
onChange={(e) => 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);
}}
/>
);
}

View File

@@ -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<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
//console.log(data);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
//renderSubComponent: ({ row }: { row: any }) => <ExpandedRow row={row} />,
//getRowCanExpand: () => true,
filterFns: {},
state: {
sorting,
columnFilters,
},
});
return (
<div className={className}>
<ScrollArea className="w-full rounded-md border whitespace-nowrap">
<Table className={cn("w-full", tableClassName)}>
<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) => (
<React.Fragment key={row.id}>
<TableRow data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
</React.Fragment>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<ScrollBar orientation="horizontal" />
</ScrollArea>
<div className="flex items-center justify-end space-x-2 py-4">
<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>
</div>
);
}

View File

@@ -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<TData> = {
column: Column<TData, unknown>;
title: string;
searchable?: boolean;
};
export default function SearchableHeader<TData>({
column,
title,
searchable = false,
}: SearchableHeaderProps<TData>) {
const [open, setOpen] = useState(false);
return (
<div className="flex items-center gap-1">
<Button
variant="ghost"
className="px-2"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="flex flex-row items-center gap-2">
{title}
{column.getIsSorted() === "asc" ? (
<ArrowUp className="h-4 w-4" />
) : column.getIsSorted() === "desc" ? (
<ArrowDown className="h-4 w-4" />
) : null}
</span>
</Button>
{searchable && (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<Search
className={cn(
"h-4 w-4",
column.getFilterValue() ? "text-primary" : "",
)}
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56 p-2">
<Input
autoFocus
value={(column.getFilterValue() as string) ?? ""}
onChange={(e) => column.setFilterValue(e.target.value)}
placeholder={`Search ${title.toLowerCase()}...`}
className="h-8"
/>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
);
}

View File

@@ -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 (
<div className="rounded-md border">
<Table>
<TableHeader>
{Array.from({ length: columns }).map((_, i) => (
<TableHead key={i}>
<Skeleton className="h-4 w-[80px]" />
</TableHead>
))}
</TableHeader>
<TableBody>
{Array.from({ length: rows }).map((_, r) => (
<TableRow key={r}>
{Array.from({ length: columns }).map((_, c) => (
<TableCell key={c}>
<Skeleton className="h-4 w-full max-w-[120px]" />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}

View File

@@ -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,

View File

@@ -79,7 +79,9 @@ export default function ChangePassword() {
<div className="flex justify-end mt-6">
<form.AppForm>
<form.SubmitButton>Update Profile</form.SubmitButton>
<form.SubmitButton variant="destructive">
Update Password
</form.SubmitButton>
</form.AppForm>
</div>
</form>

View File

@@ -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);
}
},
});

View File

@@ -25,7 +25,7 @@ function RouteComponent() {
const redirectPath = search.redirect ?? "/";
return (
<div className="flex justify-center mt-10">
<div className="flex justify-center mt-2">
<LoginForm redirectPath={redirectPath} />
</div>
);

View File

@@ -54,7 +54,7 @@ function RouteComponent() {
},
});
return (
<div className="flex justify-center mt-2 gap-2">
<div className="flex justify-center flex-col pt-4 gap-2 lg:flex-row">
<div>
<Card className="p-6 w-96">
<CardHeader>

View File

@@ -11,12 +11,15 @@ const RootLayout = () => (
<ThemeProvider>
<SidebarProvider className="flex flex-col" defaultOpen={false}>
<Header />
<div className="flex flex-1">
<div className="relative min-h-[calc(100svh-var(--header-height))]">
<AppSidebar />
<SidebarInset>
<main className="w-full p-4">
<div className="mx-auto w-full max-w-7xl">
<Outlet />
</SidebarInset>
</div>
</main>
</div>
<Toaster expand richColors closeButton />

View File

@@ -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(
<div>
<p>{data.message}</p>
<p>
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"}
</p>
</div>,
);
} catch (error) {
console.error(error);
}
},
});
return (
<Card className="p-2 w-96">
<CardHeader>
<p>{item.name}</p>
<CardDescription>
<p>{item.description}</p>
</CardDescription>
</CardHeader>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
void form.handleSubmit();
}}
>
<div className="flex justify-end mt-2 flex-col gap-4">
<form.AppField name="value">
{(field) => (
<field.InputField label="Setting Value" inputType="string" />
)}
</form.AppField>
<div className="flex flex-row justify-between">
<form.AppField name="active">
{(field) => (
<field.SwitchField trueLabel="Active" falseLabel="Deactivate" />
)}
</form.AppField>
<form.AppForm>
<form.SubmitButton>Update</form.SubmitButton>
</form.AppForm>
</div>
</div>
</form>
</Card>
);
}

View File

@@ -0,0 +1,11 @@
import FeatureCard from "./FeatureCard";
export default function FeatureSettings({ data }: any) {
return (
<div className=" flex flex-wrap gap-2">
{data.map((i: any) => (
<FeatureCard key={i.name} item={i} />
))}
</div>
);
}

View File

@@ -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: "/",
});

View File

@@ -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<string, string | number | boolean | null>,
) => {
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<Settings>();
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 }) => (
<SearchableHeader column={column} title="Name" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("description", {
header: ({ column }) => (
<SearchableHeader column={column} title="Description" />
),
cell: (i) => (
<Tooltip>
<TooltipTrigger>
{i.getValue().length > 25 ? (
<span>{i.getValue().slice(0, 25)}...</span>
) : (
<span>{i.getValue()}</span>
)}
</TooltipTrigger>
<TooltipContent>{i.getValue()}</TooltipContent>
</Tooltip>
),
}),
columnHelper.accessor("value", {
header: ({ column }) => (
<SearchableHeader column={column} title="Value" />
),
filterFn: "includesString",
cell: ({ row, getValue }) => (
<EditableCellInput
value={getValue()}
id={row.original.name}
field="value"
onSubmit={({ id, field, value }) => {
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 (
<>
<TabsContent value="feature">
<FeatureSettings data={featureSettings} />
</TabsContent>
<TabsContent value="system">
<LstTable data={systemSetting} columns={column} />
</TabsContent>
<TabsContent value="standard">
<LstTable data={standardSettings} columns={column} />
</TabsContent>
</>
);
}
function RouteComponent() {
return (
<div className="space-y-6">
<div className="space-y-2">
<h1 className="text-2xl font-semibold">Settings</h1>
<p className="text-sm text-muted-foreground">
Manage your settings and related data.
</p>
</div>
<Card>
<CardHeader>
<CardTitle>System Settings</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="standard" className="w-full">
<TabsList>
<TabsTrigger value="feature">Features</TabsTrigger>
<TabsTrigger value="system">System</TabsTrigger>
<TabsTrigger value="standard">Standard</TabsTrigger>
</TabsList>
<Suspense fallback={<SkellyTable />}>
<SettingsTableCard />
</Suspense>
</Tabs>
</CardContent>
</Card>
</div>
);
}

View File

@@ -18,10 +18,42 @@ function Index() {
if (isPending)
return <div className="flex justify-center mt-10">Loading...</div>;
// if (!session) return <button>Sign In</button>
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 (
<div className="flex justify-center mt-10">
<h3 className="w-2xl text-3xl">Welcome Home!</h3>
<div className="flex justify-center m-10 flex-col">
<h3 className="w-2xl text-3xl">Welcome Lst - V3</h3>
<br></br>
<p>
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
</p>
<br></br>
<p>
If you dont know why you are here and looking for One Click Print{" "}
<a href={`${url}`} target="_blank" rel="noopener">
<b>
<strong>Click</strong>
</b>
</a>
<a
href={`https://www.youtube.com/watch?v=dQw4w9WgXcQ`}
target="_blank"
rel="noopener"
>
<b>
<strong> Here</strong>
</b>
</a>
</p>
</div>
);
}