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

@@ -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,14 +63,18 @@ export default function AdminSidebar() {
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<>
{item.role.includes(session.user.role) && (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}>
<item.icon />
<span>{item.title}</span>
</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>
<Outlet />
</SidebarInset>
<main className="w-full p-4">
<div className="mx-auto w-full max-w-7xl">
<Outlet />
</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>
);
}