From 389211186f00cb8a6fdd5de092a944fa7e5898aa Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Sat, 23 May 2026 11:22:02 -0500 Subject: [PATCH] feat(opendock): added in new article link setup for fine tuning how od works --- backend/db/schema/opendock_docks.ts | 21 + .../opendock/opendock.articleCheck.route.ts | 81 +- backend/utils/auth.permissions.ts | 49 +- .../components/Sidebar/TransportationBar.tsx | 94 + frontend/src/components/Sidebar/sidebar.tsx | 14 +- frontend/src/components/ui/combobox.tsx | 299 ++ frontend/src/components/ui/input-group.tsx | 154 + frontend/src/components/ui/input.tsx | 30 +- frontend/src/components/ui/textarea.tsx | 18 + frontend/src/lib/auth-permissions.ts | 90 +- frontend/src/lib/formSutff/ComboBox.Field.tsx | 84 + frontend/src/lib/formSutff/Select.Field.tsx | 3 + frontend/src/lib/formSutff/index.tsx | 2 + frontend/src/lib/queries/getActiveArticles.ts | 25 + frontend/src/lib/queries/getArticleLinks.ts | 25 + frontend/src/lib/queries/getCustomerByAv.ts | 26 + frontend/src/routeTree.gen.ts | 22 + frontend/src/routes/index.tsx | 27 +- .../opendock/-components/NewArticleLink.tsx | 241 ++ .../routes/transportation/opendock/index.tsx | 122 + lstMobile/src/app/(tabs)/_layout.tsx | 14 +- migrations/0056_shallow_chimera.sql | 12 + migrations/meta/0056_snapshot.json | 2515 +++++++++++++++++ migrations/meta/_journal.json | 7 + 24 files changed, 3903 insertions(+), 72 deletions(-) create mode 100644 backend/db/schema/opendock_docks.ts create mode 100644 frontend/src/components/Sidebar/TransportationBar.tsx create mode 100644 frontend/src/components/ui/combobox.tsx create mode 100644 frontend/src/components/ui/input-group.tsx create mode 100644 frontend/src/components/ui/textarea.tsx create mode 100644 frontend/src/lib/formSutff/ComboBox.Field.tsx create mode 100644 frontend/src/lib/queries/getActiveArticles.ts create mode 100644 frontend/src/lib/queries/getArticleLinks.ts create mode 100644 frontend/src/lib/queries/getCustomerByAv.ts create mode 100644 frontend/src/routes/transportation/opendock/-components/NewArticleLink.tsx create mode 100644 frontend/src/routes/transportation/opendock/index.tsx create mode 100644 migrations/0056_shallow_chimera.sql create mode 100644 migrations/meta/0056_snapshot.json diff --git a/backend/db/schema/opendock_docks.ts b/backend/db/schema/opendock_docks.ts new file mode 100644 index 0000000..c48d87b --- /dev/null +++ b/backend/db/schema/opendock_docks.ts @@ -0,0 +1,21 @@ +import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { createInsertSchema, createSelectSchema } from "drizzle-zod"; +import type { z } from "zod"; + +export const opendockDockSetup = pgTable("opendock_dock_setup", { + id: uuid("id").defaultRandom().primaryKey(), + name: text("name").notNull(), + dockID: text("dock_id").notNull(), + upd_date: timestamp("upd_date").notNull().defaultNow(), + upd_user: text("upd_user").notNull().default("lst-system"), + createdAt: timestamp("created_at").notNull().defaultNow(), + add_user: text("add_user").notNull().default("lst-system"), +}); + +export const opendockDockSetupSchema = createSelectSchema(opendockDockSetup); +export const newOpendockDockSetupSchema = createInsertSchema(opendockDockSetup); + +export type OpendockArticleSetup = z.infer; +export type NewOpendockArticleSetup = z.infer< + typeof newOpendockDockSetupSchema +>; diff --git a/backend/opendock/opendock.articleCheck.route.ts b/backend/opendock/opendock.articleCheck.route.ts index ec1dae3..68289a4 100644 --- a/backend/opendock/opendock.articleCheck.route.ts +++ b/backend/opendock/opendock.articleCheck.route.ts @@ -6,6 +6,7 @@ import { type NewOpendockArticleSetup, opendockArticleSetup, } from "../db/schema/opendock_articleSetup.js"; +import { opendockDockSetup } from "../db/schema/opendock_docks.js"; import { prodQuery } from "../prodSql/prodSqlQuery.controller.js"; import { type SqlQuery, @@ -34,6 +35,11 @@ const newArticleLink = z.object({ ), }); +const newDockLink = z.object({ + name: z.string(), + dockID: z.string(), +}); + r.post("/", async (req, res) => { try { const validated = newArticleLink.parse(req.body) as NewOpendockArticleSetup; @@ -190,7 +196,80 @@ r.get("/customers/:av", async (req, res) => { module: "opendock", subModule: "articleCheck", message: `All customers linked to av: ${av}`, - data: data as any, + data: data?.data ?? ([] as any), + status: 200, + }); +}); + +r.post("/dock", async (req, res) => { + try { + const validated = newDockLink.parse(req.body) as any; + + const newLink = await db + .insert(opendockDockSetup) + .values({ + name: validated.name, + dockID: validated.dockID, + add_user: req.user?.username ?? "lst_user", + }) + .returning(); + + return apiReturn(res, { + success: true, + level: "info", + module: "opendock", + subModule: "articleCheck", + message: `${validated.name} was just added `, + data: newLink as any, + status: 200, + }); + } catch (err) { + if (err instanceof z.ZodError) { + const flattened = z.flattenError(err); + // return res.status(400).json({ + // error: "Validation failed", + // details: flattened, + // }); + + return apiReturn(res, { + success: false, + level: "error", //connect.success ? "info" : "error", + module: "opendock", + subModule: "articleCheck", + message: "Validation failed", + data: [flattened.fieldErrors], + status: 400, //connect.success ? 200 : 400, + }); + } + + return apiReturn(res, { + success: false, + level: "error", //connect.success ? "info" : "error", + module: "opendock", + subModule: "articleCheck", + message: "Internal Server Error adding dock link", + data: [err], + status: 400, //connect.success ? 200 : 400, + }); + } +}); + +r.get("/dock", async (_, res) => { + const { data } = await tryCatch( + db + .select() + .from(opendockDockSetup) + .orderBy(desc(opendockDockSetup.name)) + .limit(1500), + ); + + return apiReturn(res, { + success: true, + level: "info", + module: "opendock", + subModule: "articleCheck", + message: `All dock links`, + data: data ?? [], status: 200, }); }); diff --git a/backend/utils/auth.permissions.ts b/backend/utils/auth.permissions.ts index 34a8775..a3398e9 100644 --- a/backend/utils/auth.permissions.ts +++ b/backend/utils/auth.permissions.ts @@ -3,11 +3,12 @@ import { adminAc, defaultStatements } from "better-auth/plugins/admin/access"; export const statement = { ...defaultStatements, - app: ["read", "create", "share", "update", "delete", "readAll"], - quality: ["read", "create", "share", "update", "delete", "readAll"], - logistics: ["read", "create", "share", "update", "delete", "readAll"], - mobile: ["read", "create", "share", "update", "delete", "readAll"], - notifications: ["read", "create", "share", "update", "delete", "readAll"], + app: ["read", "create", "update", "delete", "readAll"], + quality: ["read", "create", "update", "delete", "readAll"], + logistics: ["read", "create", "update", "delete", "readAll"], + mobile: ["read", "create", "update", "delete", "readAll"], + openDock: ["read", "create", "update", "delete"], + notifications: ["read", "create", "update", "delete", "readAll"], } as const; export const ac = createAccessControl(statement); @@ -15,24 +16,50 @@ export const ac = createAccessControl(statement); export const user = ac.newRole({ app: ["read", "create"], notifications: ["read", "create"], + openDock: ["read"], }); export const manager = ac.newRole({ app: ["read", "create", "update"], mobile: ["read", "create", "update"], + openDock: ["read", "create", "update"], +}); + +export const transport = ac.newRole({ + app: ["read", "create", "update"], + openDock: ["read", "create", "update"], }); export const admin = ac.newRole({ app: ["read", "create", "update"], mobile: ["read", "create", "update"], - user: ["create", "update"], + user: ["create", "update", "ban"], + openDock: ["read", "create", "update"], }); export const systemAdmin = ac.newRole({ ...adminAc.statements, - app: ["read", "create", "share", "update", "delete", "readAll"], - quality: ["read", "create", "share", "update", "delete", "readAll"], - mobile: ["read", "create", "share", "update", "delete", "readAll"], - logistics: ["read", "create", "share", "update", "delete", "readAll"], - notifications: ["read", "create", "share", "update", "delete", "readAll"], + app: ["read", "create", "update", "delete", "readAll"], + quality: ["read", "create", "update", "delete", "readAll"], + mobile: ["read", "create", "update", "delete", "readAll"], + logistics: ["read", "create", "update", "delete", "readAll"], + notifications: ["read", "create", "update", "delete", "readAll"], + openDock: ["read", "create", "update", "delete"], }); + +/* example usage +const canCreateProject = await authClient.admin.hasPermission({ + permissions: { + project: ["create"], + }, +}); +// You can also check multiple resource permissions at the same time +const canCreateProjectAndCreateSale = await authClient.admin.hasPermission({ + permissions: { + project: ["create"], + sale: ["create"] + }, +}); + + +*/ diff --git a/frontend/src/components/Sidebar/TransportationBar.tsx b/frontend/src/components/Sidebar/TransportationBar.tsx new file mode 100644 index 0000000..adee9f5 --- /dev/null +++ b/frontend/src/components/Sidebar/TransportationBar.tsx @@ -0,0 +1,94 @@ +import { useQuery } from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; +import { ChevronRight, Link as link } from "lucide-react"; +import { permissionQuery } from "../../lib/queries/permsCheck"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "../ui/collapsible"; +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + useSidebar, +} from "../ui/sidebar"; + +export default function TransportationBar() { + const { data: canCreate = false } = useQuery( + permissionQuery({ + openDock: ["create"], + }), + ); + + const { setOpen } = useSidebar(); + const items = [ + { + title: "Open Dock", + url: "/transportation", + //icon, + isActive: canCreate, + items: [ + { + title: "ArticleLink", + icon: link, + url: "/transportation/opendock", + }, + ], + }, + ]; + + return ( + + Transportation + + + {items.map((item) => ( +
+ {item.isActive && ( + + + + + {item.title} + + + + + + + {item.items?.map((subItem) => ( + + + setOpen(false)} + > + + {subItem.title} + + + + ))} + + + + + )} +
+ ))} +
+
+
+ ); +} diff --git a/frontend/src/components/Sidebar/sidebar.tsx b/frontend/src/components/Sidebar/sidebar.tsx index 9ff0046..36b81da 100644 --- a/frontend/src/components/Sidebar/sidebar.tsx +++ b/frontend/src/components/Sidebar/sidebar.tsx @@ -1,4 +1,4 @@ -import { useSuspenseQuery } from "@tanstack/react-query"; +import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; import { Sidebar, SidebarContent, @@ -8,13 +8,20 @@ import { } from "@/components/ui/sidebar"; import { useSession } from "@/lib/auth-client"; import { getSettings } from "../../lib/queries/getSettings"; +import { permissionQuery } from "../../lib/queries/permsCheck"; import AdminSidebar from "./AdminBar"; import DocBar from "./DocBar"; import MobileBar from "./MobileBar"; +import TransportationBar from "./TransportationBar"; export function AppSidebar() { const { data: session } = useSession(); const { data: settings, isLoading } = useSuspenseQuery(getSettings()); + const { data: canRead = false } = useQuery( + permissionQuery({ + openDock: ["read"], + }), + ); return ( )} + {!isLoading && + settings.filter((n: any) => n.name === "opendock_sync")[0] + ?.active && + canRead && } + {session && (session.user.role === "admin" || session.user.role === "systemAdmin" || diff --git a/frontend/src/components/ui/combobox.tsx b/frontend/src/components/ui/combobox.tsx new file mode 100644 index 0000000..352afc3 --- /dev/null +++ b/frontend/src/components/ui/combobox.tsx @@ -0,0 +1,299 @@ +"use client" + +import * as React from "react" +import { Combobox as ComboboxPrimitive } from "@base-ui/react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, +} from "@/components/ui/input-group" +import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react" + +const Combobox = ComboboxPrimitive.Root + +function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { + return +} + +function ComboboxTrigger({ + className, + children, + ...props +}: ComboboxPrimitive.Trigger.Props) { + return ( + + {children} + + + ) +} + +function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { + return ( + } + className={cn(className)} + {...props} + > + + + ) +} + +function ComboboxInput({ + className, + children, + disabled = false, + showTrigger = true, + showClear = false, + ...props +}: ComboboxPrimitive.Input.Props & { + showTrigger?: boolean + showClear?: boolean +}) { + return ( + + } + {...props} + /> + + {showTrigger && ( + + + + )} + {showClear && } + + {children} + + ) +} + +function ComboboxContent({ + className, + side = "bottom", + sideOffset = 6, + align = "start", + alignOffset = 0, + anchor, + ...props +}: ComboboxPrimitive.Popup.Props & + Pick< + ComboboxPrimitive.Positioner.Props, + "side" | "align" | "sideOffset" | "alignOffset" | "anchor" + >) { + return ( + + + + + + ) +} + +function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { + return ( + + ) +} + +function ComboboxItem({ + className, + children, + ...props +}: ComboboxPrimitive.Item.Props) { + return ( + + {children} + + } + > + + + + ) +} + +function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { + return ( + + ) +} + +function ComboboxLabel({ + className, + ...props +}: ComboboxPrimitive.GroupLabel.Props) { + return ( + + ) +} + +function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { + return ( + + ) +} + +function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { + return ( + + ) +} + +function ComboboxSeparator({ + className, + ...props +}: ComboboxPrimitive.Separator.Props) { + return ( + + ) +} + +function ComboboxChips({ + className, + ...props +}: React.ComponentPropsWithRef & + ComboboxPrimitive.Chips.Props) { + return ( + + ) +} + +function ComboboxChip({ + className, + children, + showRemove = true, + ...props +}: ComboboxPrimitive.Chip.Props & { + showRemove?: boolean +}) { + return ( + + {children} + {showRemove && ( + } + className="-ml-1 opacity-50 hover:opacity-100" + data-slot="combobox-chip-remove" + > + + + )} + + ) +} + +function ComboboxChipsInput({ + className, + ...props +}: ComboboxPrimitive.Input.Props) { + return ( + + ) +} + +function useComboboxAnchor() { + return React.useRef(null) +} + +export { + Combobox, + ComboboxInput, + ComboboxContent, + ComboboxList, + ComboboxItem, + ComboboxGroup, + ComboboxLabel, + ComboboxCollection, + ComboboxEmpty, + ComboboxSeparator, + ComboboxChips, + ComboboxChip, + ComboboxChipsInput, + ComboboxTrigger, + ComboboxValue, + useComboboxAnchor, +} diff --git a/frontend/src/components/ui/input-group.tsx b/frontend/src/components/ui/input-group.tsx new file mode 100644 index 0000000..b49394c --- /dev/null +++ b/frontend/src/components/ui/input-group.tsx @@ -0,0 +1,154 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import type * as React from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { cn } from "@/lib/utils"; + +/* eslint-disable */ +function InputGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", + className, + )} + {...props} + /> + ); +} + +const inputGroupAddonVariants = cva( + "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", + { + variants: { + align: { + "inline-start": + "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]", + "inline-end": + "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]", + "block-start": + "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2", + "block-end": + "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2", + }, + }, + defaultVariants: { + align: "inline-start", + }, + }, +); + +function InputGroupAddon({ + className, + align = "inline-start", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
{ + if ((e.target as HTMLElement).closest("button")) { + return; + } + e.currentTarget.parentElement?.querySelector("input")?.focus(); + }} + {...props} + /> + ); +} + +const inputGroupButtonVariants = cva( + "flex items-center gap-2 text-sm shadow-none", + { + variants: { + size: { + xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5", + sm: "", + "icon-xs": + "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0", + "icon-sm": "size-8 p-0 has-[>svg]:p-0", + }, + }, + defaultVariants: { + size: "xs", + }, + }, +); + +function InputGroupButton({ + className, + type = "button", + variant = "ghost", + size = "xs", + ...props +}: Omit, "size"> & + VariantProps) { + return ( +