feat(opendock): added in new article link setup for fine tuning how od works
This commit is contained in:
21
backend/db/schema/opendock_docks.ts
Normal file
21
backend/db/schema/opendock_docks.ts
Normal file
@@ -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<typeof opendockDockSetupSchema>;
|
||||||
|
export type NewOpendockArticleSetup = z.infer<
|
||||||
|
typeof newOpendockDockSetupSchema
|
||||||
|
>;
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
type NewOpendockArticleSetup,
|
type NewOpendockArticleSetup,
|
||||||
opendockArticleSetup,
|
opendockArticleSetup,
|
||||||
} from "../db/schema/opendock_articleSetup.js";
|
} from "../db/schema/opendock_articleSetup.js";
|
||||||
|
import { opendockDockSetup } from "../db/schema/opendock_docks.js";
|
||||||
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
|
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
|
||||||
import {
|
import {
|
||||||
type SqlQuery,
|
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) => {
|
r.post("/", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const validated = newArticleLink.parse(req.body) as NewOpendockArticleSetup;
|
const validated = newArticleLink.parse(req.body) as NewOpendockArticleSetup;
|
||||||
@@ -190,7 +196,80 @@ r.get("/customers/:av", async (req, res) => {
|
|||||||
module: "opendock",
|
module: "opendock",
|
||||||
subModule: "articleCheck",
|
subModule: "articleCheck",
|
||||||
message: `All customers linked to av: ${av}`,
|
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,
|
status: 200,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { adminAc, defaultStatements } from "better-auth/plugins/admin/access";
|
|||||||
|
|
||||||
export const statement = {
|
export const statement = {
|
||||||
...defaultStatements,
|
...defaultStatements,
|
||||||
app: ["read", "create", "share", "update", "delete", "readAll"],
|
app: ["read", "create", "update", "delete", "readAll"],
|
||||||
quality: ["read", "create", "share", "update", "delete", "readAll"],
|
quality: ["read", "create", "update", "delete", "readAll"],
|
||||||
logistics: ["read", "create", "share", "update", "delete", "readAll"],
|
logistics: ["read", "create", "update", "delete", "readAll"],
|
||||||
mobile: ["read", "create", "share", "update", "delete", "readAll"],
|
mobile: ["read", "create", "update", "delete", "readAll"],
|
||||||
notifications: ["read", "create", "share", "update", "delete", "readAll"],
|
openDock: ["read", "create", "update", "delete"],
|
||||||
|
notifications: ["read", "create", "update", "delete", "readAll"],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ac = createAccessControl(statement);
|
export const ac = createAccessControl(statement);
|
||||||
@@ -15,24 +16,50 @@ export const ac = createAccessControl(statement);
|
|||||||
export const user = ac.newRole({
|
export const user = ac.newRole({
|
||||||
app: ["read", "create"],
|
app: ["read", "create"],
|
||||||
notifications: ["read", "create"],
|
notifications: ["read", "create"],
|
||||||
|
openDock: ["read"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const manager = ac.newRole({
|
export const manager = ac.newRole({
|
||||||
app: ["read", "create", "update"],
|
app: ["read", "create", "update"],
|
||||||
mobile: ["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({
|
export const admin = ac.newRole({
|
||||||
app: ["read", "create", "update"],
|
app: ["read", "create", "update"],
|
||||||
mobile: ["read", "create", "update"],
|
mobile: ["read", "create", "update"],
|
||||||
user: ["create", "update"],
|
user: ["create", "update", "ban"],
|
||||||
|
openDock: ["read", "create", "update"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const systemAdmin = ac.newRole({
|
export const systemAdmin = ac.newRole({
|
||||||
...adminAc.statements,
|
...adminAc.statements,
|
||||||
app: ["read", "create", "share", "update", "delete", "readAll"],
|
app: ["read", "create", "update", "delete", "readAll"],
|
||||||
quality: ["read", "create", "share", "update", "delete", "readAll"],
|
quality: ["read", "create", "update", "delete", "readAll"],
|
||||||
mobile: ["read", "create", "share", "update", "delete", "readAll"],
|
mobile: ["read", "create", "update", "delete", "readAll"],
|
||||||
logistics: ["read", "create", "share", "update", "delete", "readAll"],
|
logistics: ["read", "create", "update", "delete", "readAll"],
|
||||||
notifications: ["read", "create", "share", "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"]
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
94
frontend/src/components/Sidebar/TransportationBar.tsx
Normal file
94
frontend/src/components/Sidebar/TransportationBar.tsx
Normal file
@@ -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 (
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel>Transportation</SidebarGroupLabel>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{items.map((item) => (
|
||||||
|
<div key={item.title}>
|
||||||
|
{item.isActive && (
|
||||||
|
<Collapsible
|
||||||
|
asChild
|
||||||
|
//defaultOpen={isNotifications}
|
||||||
|
className="group/collapsible"
|
||||||
|
>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<SidebarMenuButton tooltip={item.title}>
|
||||||
|
{item.title}
|
||||||
|
|
||||||
|
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarMenuSub>
|
||||||
|
{item.items?.map((subItem) => (
|
||||||
|
<SidebarMenuSubItem key={subItem.title}>
|
||||||
|
<SidebarMenuSubButton asChild>
|
||||||
|
<Link
|
||||||
|
to={subItem.url}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<subItem.icon />
|
||||||
|
<span>{subItem.title}</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
))}
|
||||||
|
</SidebarMenuSub>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</Collapsible>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -8,13 +8,20 @@ import {
|
|||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { useSession } from "@/lib/auth-client";
|
import { useSession } from "@/lib/auth-client";
|
||||||
import { getSettings } from "../../lib/queries/getSettings";
|
import { getSettings } from "../../lib/queries/getSettings";
|
||||||
|
import { permissionQuery } from "../../lib/queries/permsCheck";
|
||||||
import AdminSidebar from "./AdminBar";
|
import AdminSidebar from "./AdminBar";
|
||||||
import DocBar from "./DocBar";
|
import DocBar from "./DocBar";
|
||||||
import MobileBar from "./MobileBar";
|
import MobileBar from "./MobileBar";
|
||||||
|
import TransportationBar from "./TransportationBar";
|
||||||
|
|
||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
|
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
|
||||||
|
const { data: canRead = false } = useQuery(
|
||||||
|
permissionQuery({
|
||||||
|
openDock: ["read"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar
|
<Sidebar
|
||||||
@@ -32,6 +39,11 @@ export function AppSidebar() {
|
|||||||
<MobileBar />
|
<MobileBar />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!isLoading &&
|
||||||
|
settings.filter((n: any) => n.name === "opendock_sync")[0]
|
||||||
|
?.active &&
|
||||||
|
canRead && <TransportationBar />}
|
||||||
|
|
||||||
{session &&
|
{session &&
|
||||||
(session.user.role === "admin" ||
|
(session.user.role === "admin" ||
|
||||||
session.user.role === "systemAdmin" ||
|
session.user.role === "systemAdmin" ||
|
||||||
|
|||||||
299
frontend/src/components/ui/combobox.tsx
Normal file
299
frontend/src/components/ui/combobox.tsx
Normal file
@@ -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 <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ComboboxPrimitive.Trigger.Props) {
|
||||||
|
return (
|
||||||
|
<ComboboxPrimitive.Trigger
|
||||||
|
data-slot="combobox-trigger"
|
||||||
|
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
|
||||||
|
</ComboboxPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
|
||||||
|
return (
|
||||||
|
<ComboboxPrimitive.Clear
|
||||||
|
data-slot="combobox-clear"
|
||||||
|
render={<InputGroupButton variant="ghost" size="icon-xs" />}
|
||||||
|
className={cn(className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<XIcon className="pointer-events-none" />
|
||||||
|
</ComboboxPrimitive.Clear>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxInput({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
disabled = false,
|
||||||
|
showTrigger = true,
|
||||||
|
showClear = false,
|
||||||
|
...props
|
||||||
|
}: ComboboxPrimitive.Input.Props & {
|
||||||
|
showTrigger?: boolean
|
||||||
|
showClear?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<InputGroup className={cn("w-auto", className)}>
|
||||||
|
<ComboboxPrimitive.Input
|
||||||
|
render={<InputGroupInput disabled={disabled} />}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<InputGroupAddon align="inline-end">
|
||||||
|
{showTrigger && (
|
||||||
|
<InputGroupButton
|
||||||
|
size="icon-xs"
|
||||||
|
variant="ghost"
|
||||||
|
asChild
|
||||||
|
data-slot="input-group-button"
|
||||||
|
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<ComboboxTrigger />
|
||||||
|
</InputGroupButton>
|
||||||
|
)}
|
||||||
|
{showClear && <ComboboxClear disabled={disabled} />}
|
||||||
|
</InputGroupAddon>
|
||||||
|
{children}
|
||||||
|
</InputGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<ComboboxPrimitive.Portal>
|
||||||
|
<ComboboxPrimitive.Positioner
|
||||||
|
side={side}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
align={align}
|
||||||
|
alignOffset={alignOffset}
|
||||||
|
anchor={anchor}
|
||||||
|
className="isolate z-50"
|
||||||
|
>
|
||||||
|
<ComboboxPrimitive.Popup
|
||||||
|
data-slot="combobox-content"
|
||||||
|
data-chips={!!anchor}
|
||||||
|
className={cn("group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</ComboboxPrimitive.Positioner>
|
||||||
|
</ComboboxPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
|
||||||
|
return (
|
||||||
|
<ComboboxPrimitive.List
|
||||||
|
data-slot="combobox-list"
|
||||||
|
className={cn(
|
||||||
|
"no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ComboboxPrimitive.Item.Props) {
|
||||||
|
return (
|
||||||
|
<ComboboxPrimitive.Item
|
||||||
|
data-slot="combobox-item"
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ComboboxPrimitive.ItemIndicator
|
||||||
|
render={
|
||||||
|
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CheckIcon className="pointer-events-none" />
|
||||||
|
</ComboboxPrimitive.ItemIndicator>
|
||||||
|
</ComboboxPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
|
||||||
|
return (
|
||||||
|
<ComboboxPrimitive.Group
|
||||||
|
data-slot="combobox-group"
|
||||||
|
className={cn(className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ComboboxPrimitive.GroupLabel.Props) {
|
||||||
|
return (
|
||||||
|
<ComboboxPrimitive.GroupLabel
|
||||||
|
data-slot="combobox-label"
|
||||||
|
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
|
||||||
|
return (
|
||||||
|
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
|
||||||
|
return (
|
||||||
|
<ComboboxPrimitive.Empty
|
||||||
|
data-slot="combobox-empty"
|
||||||
|
className={cn(
|
||||||
|
"hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ComboboxPrimitive.Separator.Props) {
|
||||||
|
return (
|
||||||
|
<ComboboxPrimitive.Separator
|
||||||
|
data-slot="combobox-separator"
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxChips({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
|
||||||
|
ComboboxPrimitive.Chips.Props) {
|
||||||
|
return (
|
||||||
|
<ComboboxPrimitive.Chips
|
||||||
|
data-slot="combobox-chips"
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-8 flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxChip({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showRemove = true,
|
||||||
|
...props
|
||||||
|
}: ComboboxPrimitive.Chip.Props & {
|
||||||
|
showRemove?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ComboboxPrimitive.Chip
|
||||||
|
data-slot="combobox-chip"
|
||||||
|
className={cn(
|
||||||
|
"flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showRemove && (
|
||||||
|
<ComboboxPrimitive.ChipRemove
|
||||||
|
render={<Button variant="ghost" size="icon-xs" />}
|
||||||
|
className="-ml-1 opacity-50 hover:opacity-100"
|
||||||
|
data-slot="combobox-chip-remove"
|
||||||
|
>
|
||||||
|
<XIcon className="pointer-events-none" />
|
||||||
|
</ComboboxPrimitive.ChipRemove>
|
||||||
|
)}
|
||||||
|
</ComboboxPrimitive.Chip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxChipsInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ComboboxPrimitive.Input.Props) {
|
||||||
|
return (
|
||||||
|
<ComboboxPrimitive.Input
|
||||||
|
data-slot="combobox-chip-input"
|
||||||
|
className={cn("min-w-16 flex-1 outline-none", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function useComboboxAnchor() {
|
||||||
|
return React.useRef<HTMLDivElement | null>(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Combobox,
|
||||||
|
ComboboxInput,
|
||||||
|
ComboboxContent,
|
||||||
|
ComboboxList,
|
||||||
|
ComboboxItem,
|
||||||
|
ComboboxGroup,
|
||||||
|
ComboboxLabel,
|
||||||
|
ComboboxCollection,
|
||||||
|
ComboboxEmpty,
|
||||||
|
ComboboxSeparator,
|
||||||
|
ComboboxChips,
|
||||||
|
ComboboxChip,
|
||||||
|
ComboboxChipsInput,
|
||||||
|
ComboboxTrigger,
|
||||||
|
ComboboxValue,
|
||||||
|
useComboboxAnchor,
|
||||||
|
}
|
||||||
154
frontend/src/components/ui/input-group.tsx
Normal file
154
frontend/src/components/ui/input-group.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
data-slot="input-group"
|
||||||
|
role="group"
|
||||||
|
className={cn(
|
||||||
|
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[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<typeof inputGroupAddonVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
data-slot="input-group-addon"
|
||||||
|
data-align={align}
|
||||||
|
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||||
|
onClick={(e) => {
|
||||||
|
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<React.ComponentProps<typeof Button>, "size"> &
|
||||||
|
VariantProps<typeof inputGroupButtonVariants>) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type={type}
|
||||||
|
data-size={size}
|
||||||
|
variant={variant}
|
||||||
|
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputGroupInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
data-slot="input-group-control"
|
||||||
|
className={cn(
|
||||||
|
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputGroupTextarea({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
data-slot="input-group-control"
|
||||||
|
className={cn(
|
||||||
|
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
InputGroupButton,
|
||||||
|
InputGroupInput,
|
||||||
|
InputGroupText,
|
||||||
|
InputGroupTextarea,
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type * as React from "react";
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
return (
|
return (
|
||||||
@@ -8,14 +8,12 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
|
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||||
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
className
|
||||||
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
|
||||||
className,
|
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Input };
|
export { Input }
|
||||||
|
|||||||
18
frontend/src/components/ui/textarea.tsx
Normal file
18
frontend/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@@ -13,17 +13,19 @@ type SelectableRole = {
|
|||||||
export const selectableRoles: SelectableRole[] = [
|
export const selectableRoles: SelectableRole[] = [
|
||||||
{ label: "User", value: "user" },
|
{ label: "User", value: "user" },
|
||||||
{ label: "Manager", value: "manager" },
|
{ label: "Manager", value: "manager" },
|
||||||
|
{ label: "Transport", value: "transport" },
|
||||||
{ label: "Admin", value: "admin" },
|
{ label: "Admin", value: "admin" },
|
||||||
{ label: "System Admin", value: "systemAdmin" },
|
{ label: "System Admin", value: "systemAdmin" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const statement = {
|
export const statement = {
|
||||||
...defaultStatements,
|
...defaultStatements,
|
||||||
app: ["read", "create", "share", "update", "delete", "readAll"],
|
app: ["read", "create", "update", "delete", "readAll"],
|
||||||
quality: ["read", "create", "share", "update", "delete", "readAll"],
|
quality: ["read", "create", "update", "delete", "readAll"],
|
||||||
logistics: ["read", "create", "share", "update", "delete", "readAll"],
|
logistics: ["read", "create", "update", "delete", "readAll"],
|
||||||
mobile: ["read", "create", "share", "update", "delete", "readAll"],
|
mobile: ["read", "create", "update", "delete", "readAll"],
|
||||||
notifications: ["read", "create", "share", "update", "delete", "readAll"],
|
openDock: ["read", "create", "update", "delete"],
|
||||||
|
notifications: ["read", "create", "update", "delete", "readAll"],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ac = createAccessControl(statement);
|
export const ac = createAccessControl(statement);
|
||||||
@@ -31,41 +33,79 @@ export const ac = createAccessControl(statement);
|
|||||||
export const user = ac.newRole({
|
export const user = ac.newRole({
|
||||||
app: ["read", "create"],
|
app: ["read", "create"],
|
||||||
notifications: ["read", "create"],
|
notifications: ["read", "create"],
|
||||||
|
openDock: ["read"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const manager = ac.newRole({
|
export const manager = ac.newRole({
|
||||||
app: ["read", "create", "update"],
|
app: ["read", "create", "update"],
|
||||||
mobile: ["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({
|
export const admin = ac.newRole({
|
||||||
app: ["read", "create", "update"],
|
app: ["read", "create", "update"],
|
||||||
mobile: ["read", "create", "update"],
|
mobile: ["read", "create", "update"],
|
||||||
user: ["create", "update"],
|
user: ["create", "update", "ban"],
|
||||||
|
openDock: ["read", "create", "update"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const systemAdmin = ac.newRole({
|
export const systemAdmin = ac.newRole({
|
||||||
...adminAc.statements,
|
...adminAc.statements,
|
||||||
app: ["read", "create", "share", "update", "delete", "readAll"],
|
app: ["read", "create", "update", "delete", "readAll"],
|
||||||
quality: ["read", "create", "share", "update", "delete", "readAll"],
|
quality: ["read", "create", "update", "delete", "readAll"],
|
||||||
mobile: ["read", "create", "share", "update", "delete", "readAll"],
|
mobile: ["read", "create", "update", "delete", "readAll"],
|
||||||
logistics: ["read", "create", "share", "update", "delete", "readAll"],
|
logistics: ["read", "create", "update", "delete", "readAll"],
|
||||||
notifications: ["read", "create", "share", "update", "delete", "readAll"],
|
notifications: ["read", "create", "update", "delete", "readAll"],
|
||||||
|
openDock: ["read", "create", "update", "delete"],
|
||||||
});
|
});
|
||||||
|
|
||||||
/* example usage
|
/*
|
||||||
const canCreateProject = await authClient.admin.hasPermission({
|
|
||||||
|
inside a component
|
||||||
|
|
||||||
|
const { data: canImpersonate = false } = useQuery(
|
||||||
|
permissionQuery({
|
||||||
|
user: ["impersonate"],
|
||||||
|
logistics: ["delete"]
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
on the before load use this example
|
||||||
|
|
||||||
|
beforeLoad: async ({ location }) => {
|
||||||
|
const { data: session } = await authClient.getSession();
|
||||||
|
//const allowedRole = ["systemAdmin", "admin", "manager"];
|
||||||
|
|
||||||
|
const canAccess = await authClient.admin.hasPermission({
|
||||||
permissions: {
|
permissions: {
|
||||||
project: ["create"],
|
opendock: ["create"],
|
||||||
},
|
logistics: ["delete"]
|
||||||
});
|
|
||||||
// You can also check multiple resource permissions at the same time
|
|
||||||
const canCreateProjectAndCreateSale = await authClient.admin.hasPermission({
|
|
||||||
permissions: {
|
|
||||||
project: ["create"],
|
|
||||||
sale: ["create"]
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
throw redirect({
|
||||||
|
to: "/",
|
||||||
|
search: {
|
||||||
|
redirect: location.href,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//if (!allowedRole.includes(session.user.role as string)) {
|
||||||
|
|
||||||
|
if (!canAccess) {
|
||||||
|
throw redirect({
|
||||||
|
to: "/",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user: session.user };
|
||||||
|
},
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|||||||
84
frontend/src/lib/formSutff/ComboBox.Field.tsx
Normal file
84
frontend/src/lib/formSutff/ComboBox.Field.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import {
|
||||||
|
Combobox,
|
||||||
|
ComboboxContent,
|
||||||
|
ComboboxEmpty,
|
||||||
|
ComboboxInput,
|
||||||
|
ComboboxItem,
|
||||||
|
ComboboxList,
|
||||||
|
} from "../../components/ui/combobox";
|
||||||
|
import { Label } from "../../components/ui/label";
|
||||||
|
import { useFieldContext } from ".";
|
||||||
|
import { FieldErrors } from "./Errors.Field";
|
||||||
|
|
||||||
|
// type SelectOption = {
|
||||||
|
// value: string;
|
||||||
|
// label: string;
|
||||||
|
// };
|
||||||
|
|
||||||
|
type ComboBoxFieldProps = {
|
||||||
|
data: string[];
|
||||||
|
label: string;
|
||||||
|
placeholder: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ComboBoxField = ({
|
||||||
|
data = [],
|
||||||
|
label,
|
||||||
|
placeholder = "",
|
||||||
|
}: ComboBoxFieldProps) => {
|
||||||
|
const field = useFieldContext<string>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label htmlFor={field.name}>{label}</Label>
|
||||||
|
{/* <Select
|
||||||
|
value={field.state.value}
|
||||||
|
onValueChange={(value) => field.handleChange(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id={field.name}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
className="w-min-2/3 w-max-fit"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder={placeholder} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent position={"popper"}>
|
||||||
|
{options.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select> */}
|
||||||
|
<Combobox
|
||||||
|
items={data}
|
||||||
|
//value={field.state.value ?? ""}
|
||||||
|
// onValueChange={(value) => {
|
||||||
|
// console.log(value);
|
||||||
|
// field.handleChange(value);
|
||||||
|
// }}
|
||||||
|
>
|
||||||
|
<ComboboxInput placeholder={placeholder} />
|
||||||
|
{/* <ComboboxInput
|
||||||
|
//showTrigger={false}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
showClear
|
||||||
|
/> */}
|
||||||
|
<ComboboxContent className="max-h-72 overflow-y-auto">
|
||||||
|
<ComboboxEmpty>No items found.</ComboboxEmpty>
|
||||||
|
<ComboboxList>
|
||||||
|
{(item) => (
|
||||||
|
<ComboboxItem key={item} value={item}>
|
||||||
|
{item}
|
||||||
|
</ComboboxItem>
|
||||||
|
)}
|
||||||
|
</ComboboxList>
|
||||||
|
</ComboboxContent>
|
||||||
|
</Combobox>
|
||||||
|
</div>
|
||||||
|
<FieldErrors meta={field.state.meta} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -18,12 +18,14 @@ type SelectFieldProps = {
|
|||||||
label: string;
|
label: string;
|
||||||
options: SelectOption[];
|
options: SelectOption[];
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectField = ({
|
export const SelectField = ({
|
||||||
label,
|
label,
|
||||||
options,
|
options,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
disabled = false,
|
||||||
}: SelectFieldProps) => {
|
}: SelectFieldProps) => {
|
||||||
const field = useFieldContext<string>();
|
const field = useFieldContext<string>();
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ export const SelectField = ({
|
|||||||
<Select
|
<Select
|
||||||
value={field.state.value}
|
value={field.state.value}
|
||||||
onValueChange={(value) => field.handleChange(value)}
|
onValueChange={(value) => field.handleChange(value)}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id={field.name}
|
id={field.name}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
|
import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
|
||||||
import { CheckboxField } from "./CheckBox.Field";
|
import { CheckboxField } from "./CheckBox.Field";
|
||||||
|
import { ComboBoxField } from "./ComboBox.Field";
|
||||||
import { DynamicInputField } from "./DynamicInput.Field";
|
import { DynamicInputField } from "./DynamicInput.Field";
|
||||||
import { InputField } from "./Input.Field";
|
import { InputField } from "./Input.Field";
|
||||||
import { InputPasswordField } from "./InputPassword.Field";
|
import { InputPasswordField } from "./InputPassword.Field";
|
||||||
@@ -21,6 +22,7 @@ export const { useAppForm } = createFormHook({
|
|||||||
//Searchable,
|
//Searchable,
|
||||||
SwitchField,
|
SwitchField,
|
||||||
DynamicInputField,
|
DynamicInputField,
|
||||||
|
ComboBoxField,
|
||||||
},
|
},
|
||||||
formComponents: { SubmitButton },
|
formComponents: { SubmitButton },
|
||||||
fieldContext,
|
fieldContext,
|
||||||
|
|||||||
25
frontend/src/lib/queries/getActiveArticles.ts
Normal file
25
frontend/src/lib/queries/getActiveArticles.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
||||||
|
import { api } from "../apiHelper";
|
||||||
|
|
||||||
|
export function getActiveArticle() {
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: ["getActiveArticle"],
|
||||||
|
queryFn: () => dataFetch(),
|
||||||
|
staleTime: 5000,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataFetch = async () => {
|
||||||
|
if (window.location.hostname === "localhost") {
|
||||||
|
await new Promise((res) => setTimeout(res, 1500));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await api.get("/datamart/activeArticles");
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.message ?? "Failed to load articles");
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.data ?? [];
|
||||||
|
};
|
||||||
25
frontend/src/lib/queries/getArticleLinks.ts
Normal file
25
frontend/src/lib/queries/getArticleLinks.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
||||||
|
import { api } from "../apiHelper";
|
||||||
|
|
||||||
|
export function getArticleLinks() {
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: ["getArticleLinks"],
|
||||||
|
queryFn: () => dataFetch(),
|
||||||
|
staleTime: 5000,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataFetch = async () => {
|
||||||
|
if (window.location.hostname === "localhost") {
|
||||||
|
await new Promise((res) => setTimeout(res, 1500));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await api.get("/opendock/articleCheck");
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.message ?? "Failed to load article links");
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.data ?? [];
|
||||||
|
};
|
||||||
26
frontend/src/lib/queries/getCustomerByAv.ts
Normal file
26
frontend/src/lib/queries/getCustomerByAv.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import { api } from "../apiHelper";
|
||||||
|
|
||||||
|
export function getCustomerByAv(av: string) {
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: ["getCustomerByAv", av],
|
||||||
|
queryFn: () => dataFetch(av),
|
||||||
|
staleTime: 5000,
|
||||||
|
enabled: !!av,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
//placeholderData: keepPreviousData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataFetch = async (av: string) => {
|
||||||
|
if (window.location.hostname === "localhost") {
|
||||||
|
await new Promise((res) => setTimeout(res, 1500));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await api.get(`/opendock/articleCheck/customers/${av}`);
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.message ?? "Failed to load customers");
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.data ?? [];
|
||||||
|
};
|
||||||
@@ -22,6 +22,7 @@ import { Route as AdminScanUsersRouteImport } from './routes/admin/scanUsers'
|
|||||||
import { Route as AdminNotificationsRouteImport } from './routes/admin/notifications'
|
import { Route as AdminNotificationsRouteImport } from './routes/admin/notifications'
|
||||||
import { Route as AdminLogsRouteImport } from './routes/admin/logs'
|
import { Route as AdminLogsRouteImport } from './routes/admin/logs'
|
||||||
import { Route as authLoginRouteImport } from './routes/(auth)/login'
|
import { Route as authLoginRouteImport } from './routes/(auth)/login'
|
||||||
|
import { Route as TransportationOpendockIndexRouteImport } from './routes/transportation/opendock/index'
|
||||||
import { Route as authUserSignupRouteImport } from './routes/(auth)/user.signup'
|
import { Route as authUserSignupRouteImport } from './routes/(auth)/user.signup'
|
||||||
import { Route as authUserResetpasswordRouteImport } from './routes/(auth)/user.resetpassword'
|
import { Route as authUserResetpasswordRouteImport } from './routes/(auth)/user.resetpassword'
|
||||||
import { Route as authUserProfileRouteImport } from './routes/(auth)/user.profile'
|
import { Route as authUserProfileRouteImport } from './routes/(auth)/user.profile'
|
||||||
@@ -91,6 +92,12 @@ const authLoginRoute = authLoginRouteImport.update({
|
|||||||
path: '/login',
|
path: '/login',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const TransportationOpendockIndexRoute =
|
||||||
|
TransportationOpendockIndexRouteImport.update({
|
||||||
|
id: '/transportation/opendock/',
|
||||||
|
path: '/transportation/opendock/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const authUserSignupRoute = authUserSignupRouteImport.update({
|
const authUserSignupRoute = authUserSignupRouteImport.update({
|
||||||
id: '/(auth)/user/signup',
|
id: '/(auth)/user/signup',
|
||||||
path: '/user/signup',
|
path: '/user/signup',
|
||||||
@@ -124,6 +131,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/user/profile': typeof authUserProfileRoute
|
'/user/profile': typeof authUserProfileRoute
|
||||||
'/user/resetpassword': typeof authUserResetpasswordRoute
|
'/user/resetpassword': typeof authUserResetpasswordRoute
|
||||||
'/user/signup': typeof authUserSignupRoute
|
'/user/signup': typeof authUserSignupRoute
|
||||||
|
'/transportation/opendock/': typeof TransportationOpendockIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
@@ -142,6 +150,7 @@ export interface FileRoutesByTo {
|
|||||||
'/user/profile': typeof authUserProfileRoute
|
'/user/profile': typeof authUserProfileRoute
|
||||||
'/user/resetpassword': typeof authUserResetpasswordRoute
|
'/user/resetpassword': typeof authUserResetpasswordRoute
|
||||||
'/user/signup': typeof authUserSignupRoute
|
'/user/signup': typeof authUserSignupRoute
|
||||||
|
'/transportation/opendock': typeof TransportationOpendockIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
@@ -161,6 +170,7 @@ export interface FileRoutesById {
|
|||||||
'/(auth)/user/profile': typeof authUserProfileRoute
|
'/(auth)/user/profile': typeof authUserProfileRoute
|
||||||
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
|
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
|
||||||
'/(auth)/user/signup': typeof authUserSignupRoute
|
'/(auth)/user/signup': typeof authUserSignupRoute
|
||||||
|
'/transportation/opendock/': typeof TransportationOpendockIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
@@ -181,6 +191,7 @@ export interface FileRouteTypes {
|
|||||||
| '/user/profile'
|
| '/user/profile'
|
||||||
| '/user/resetpassword'
|
| '/user/resetpassword'
|
||||||
| '/user/signup'
|
| '/user/signup'
|
||||||
|
| '/transportation/opendock/'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
@@ -199,6 +210,7 @@ export interface FileRouteTypes {
|
|||||||
| '/user/profile'
|
| '/user/profile'
|
||||||
| '/user/resetpassword'
|
| '/user/resetpassword'
|
||||||
| '/user/signup'
|
| '/user/signup'
|
||||||
|
| '/transportation/opendock'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
@@ -217,6 +229,7 @@ export interface FileRouteTypes {
|
|||||||
| '/(auth)/user/profile'
|
| '/(auth)/user/profile'
|
||||||
| '/(auth)/user/resetpassword'
|
| '/(auth)/user/resetpassword'
|
||||||
| '/(auth)/user/signup'
|
| '/(auth)/user/signup'
|
||||||
|
| '/transportation/opendock/'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
@@ -236,6 +249,7 @@ export interface RootRouteChildren {
|
|||||||
authUserProfileRoute: typeof authUserProfileRoute
|
authUserProfileRoute: typeof authUserProfileRoute
|
||||||
authUserResetpasswordRoute: typeof authUserResetpasswordRoute
|
authUserResetpasswordRoute: typeof authUserResetpasswordRoute
|
||||||
authUserSignupRoute: typeof authUserSignupRoute
|
authUserSignupRoute: typeof authUserSignupRoute
|
||||||
|
TransportationOpendockIndexRoute: typeof TransportationOpendockIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -331,6 +345,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof authLoginRouteImport
|
preLoaderRoute: typeof authLoginRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/transportation/opendock/': {
|
||||||
|
id: '/transportation/opendock/'
|
||||||
|
path: '/transportation/opendock'
|
||||||
|
fullPath: '/transportation/opendock/'
|
||||||
|
preLoaderRoute: typeof TransportationOpendockIndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/(auth)/user/signup': {
|
'/(auth)/user/signup': {
|
||||||
id: '/(auth)/user/signup'
|
id: '/(auth)/user/signup'
|
||||||
path: '/user/signup'
|
path: '/user/signup'
|
||||||
@@ -372,6 +393,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
authUserProfileRoute: authUserProfileRoute,
|
authUserProfileRoute: authUserProfileRoute,
|
||||||
authUserResetpasswordRoute: authUserResetpasswordRoute,
|
authUserResetpasswordRoute: authUserResetpasswordRoute,
|
||||||
authUserSignupRoute: authUserSignupRoute,
|
authUserSignupRoute: authUserSignupRoute,
|
||||||
|
TransportationOpendockIndexRoute: TransportationOpendockIndexRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Button } from "../components/ui/button";
|
|
||||||
import { useSession } from "../lib/auth-client";
|
import { useSession } from "../lib/auth-client";
|
||||||
import { runtimeConfig, trackLstEvent } from "../lib/umami.utils";
|
import { trackLstEvent } from "../lib/umami.utils";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
validateSearch: z.object({
|
validateSearch: z.object({
|
||||||
@@ -14,7 +13,7 @@ export const Route = createFileRoute("/")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Index() {
|
function Index() {
|
||||||
const { data: session, isPending } = useSession();
|
const { isPending } = useSession();
|
||||||
|
|
||||||
if (isPending)
|
if (isPending)
|
||||||
return <div className="flex justify-center mt-10">Loading...</div>;
|
return <div className="flex justify-center mt-10">Loading...</div>;
|
||||||
@@ -38,15 +37,15 @@ function Index() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkConfig = () => {
|
// const checkConfig = () => {
|
||||||
console.log(runtimeConfig);
|
// console.log(runtimeConfig);
|
||||||
trackLstEvent("config_click", {
|
// trackLstEvent("config_click", {
|
||||||
module: "app",
|
// module: "app",
|
||||||
action: "click",
|
// action: "click",
|
||||||
label: "configCheck",
|
// label: "configCheck",
|
||||||
page: window.location.pathname,
|
// page: window.location.pathname,
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center m-10 flex-col">
|
<div className="flex justify-center m-10 flex-col">
|
||||||
@@ -77,9 +76,9 @@ function Index() {
|
|||||||
</a>
|
</a>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
{session && session.user.role === "systemAdmin" && (
|
{/* {session && session.user.role === "systemAdmin" && (
|
||||||
<Button onClick={checkConfig}>Check config</Button>
|
<Button onClick={checkConfig}>Check config</Button>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "../../../../components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "../../../../components/ui/dialog";
|
||||||
|
import { api } from "../../../../lib/apiHelper";
|
||||||
|
import { useAppForm } from "../../../../lib/formSutff";
|
||||||
|
import { getActiveArticle } from "../../../../lib/queries/getActiveArticles";
|
||||||
|
import { getCustomerByAv } from "../../../../lib/queries/getCustomerByAv";
|
||||||
|
|
||||||
|
export default function NewArticleLink({ refetch }: { refetch: any }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [selectedAv, setSelectedAv] = useState<string>("");
|
||||||
|
const { data: articleData } = useSuspenseQuery(getActiveArticle());
|
||||||
|
const {
|
||||||
|
data: customerData,
|
||||||
|
isPending,
|
||||||
|
isLoading,
|
||||||
|
} = useQuery(getCustomerByAv(selectedAv.split(" - ")[0]));
|
||||||
|
|
||||||
|
const form = useAppForm({
|
||||||
|
defaultValues: {
|
||||||
|
av: "",
|
||||||
|
description: "",
|
||||||
|
customer: "",
|
||||||
|
customerDescription: "",
|
||||||
|
loadType: "",
|
||||||
|
dock: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
const corrected = {
|
||||||
|
av: parseInt(value.av.split(" - ")[0], 10),
|
||||||
|
description: value.av.split(" - ")[1],
|
||||||
|
customer: value.customer.split(" - ")[0],
|
||||||
|
customerDescription: value.customer.split(" - ")[1],
|
||||||
|
loadType: value.loadType,
|
||||||
|
dock: value.dock,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.post("/opendock/articleCheck", corrected, {
|
||||||
|
validateStatus: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.data.success) {
|
||||||
|
toast.success(`The link for ${value.av} was just created :D`);
|
||||||
|
refetch();
|
||||||
|
form.reset();
|
||||||
|
setSelectedAv("");
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.data.success) {
|
||||||
|
toast.error(
|
||||||
|
"The article customer combo are not allowed to be created twice please select a different customer.",
|
||||||
|
);
|
||||||
|
|
||||||
|
form.setFieldValue("customer", "");
|
||||||
|
console.log(res.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeModel = (e: boolean) => {
|
||||||
|
setOpen(e);
|
||||||
|
|
||||||
|
if (!e) {
|
||||||
|
form.reset();
|
||||||
|
setSelectedAv("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openForm = () => {
|
||||||
|
setOpen(true);
|
||||||
|
form.reset;
|
||||||
|
setSelectedAv("");
|
||||||
|
};
|
||||||
|
|
||||||
|
let n: any = [];
|
||||||
|
if (articleData) {
|
||||||
|
n = articleData.map((i: any) => ({
|
||||||
|
label: `${i.article} - ${i.Bezeichnung}`,
|
||||||
|
value: `${i.article} - ${i.Bezeichnung}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let c: any = [];
|
||||||
|
if ((selectedAv && !isPending) || !isLoading) {
|
||||||
|
const cusData = customerData ?? [];
|
||||||
|
c = cusData.map((i: any) => ({
|
||||||
|
label: `${i.customer} - ${i.customerDescription}`,
|
||||||
|
value: `${i.customer} - ${i.customerDescription}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: get this from lst as well once we get the actual docks in to link to.
|
||||||
|
// this will be live || drop but also the actaul load types so we can have a little more refined times
|
||||||
|
const loadType = [
|
||||||
|
{
|
||||||
|
label: "Live",
|
||||||
|
value: "live",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Drop",
|
||||||
|
value: "drop",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
//TODO: get the docks from lst to help refine and actually link the dock correctly
|
||||||
|
const dock = [
|
||||||
|
{
|
||||||
|
label: "Cermac",
|
||||||
|
value: "cermac",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Gerber",
|
||||||
|
value: "gerber",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Matrix",
|
||||||
|
value: "matrix",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog onOpenChange={(e) => closeModel(e)} open={open}>
|
||||||
|
<Button onClick={openForm}>Create Article link</Button>
|
||||||
|
|
||||||
|
<DialogContent showCloseButton={false} className="min-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create Article Link.</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create the fine tuned per article setup, selecting an av will pull
|
||||||
|
in only the sales prices for the av, After filling in the form all{" "}
|
||||||
|
<p className="underline">NEW</p> release created will use this as
|
||||||
|
the new default settings.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="w-fit">
|
||||||
|
<div className="w-fill">
|
||||||
|
<form.AppField
|
||||||
|
name="av"
|
||||||
|
listeners={{
|
||||||
|
onChange: ({ value }) => {
|
||||||
|
setSelectedAv(value);
|
||||||
|
|
||||||
|
if (form.getFieldValue("customer")) {
|
||||||
|
form.setFieldValue("customer", "");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(field) => (
|
||||||
|
<field.SelectField
|
||||||
|
label="Select Article"
|
||||||
|
placeholder="Select av to link"
|
||||||
|
options={n}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-fit">
|
||||||
|
<div className="w-fill">
|
||||||
|
<form.AppField name="customer">
|
||||||
|
{(field) => (
|
||||||
|
<field.SelectField
|
||||||
|
label="Select Customer"
|
||||||
|
placeholder={
|
||||||
|
!selectedAv
|
||||||
|
? "Select AV first"
|
||||||
|
: isLoading
|
||||||
|
? "Loading customers..."
|
||||||
|
: c.length === 0
|
||||||
|
? "No customers to select"
|
||||||
|
: "Select customer"
|
||||||
|
}
|
||||||
|
options={c}
|
||||||
|
disabled={!selectedAv || (isLoading && c.length > 0)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className=" flex flex-row w-fit mt-3">
|
||||||
|
<div className="w-fill">
|
||||||
|
<form.AppField name="loadType">
|
||||||
|
{(field) => (
|
||||||
|
<field.SelectField
|
||||||
|
label="Select Load Type"
|
||||||
|
placeholder={"Select LoadType"}
|
||||||
|
options={loadType}
|
||||||
|
disabled={!selectedAv}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
</div>
|
||||||
|
<div className="w-fill">
|
||||||
|
<form.AppField name="dock">
|
||||||
|
{(field) => (
|
||||||
|
<field.SelectField
|
||||||
|
label="Select Dock"
|
||||||
|
placeholder={"Select dock"}
|
||||||
|
options={dock}
|
||||||
|
disabled={!selectedAv}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form.AppField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-2 ">
|
||||||
|
<form.AppForm>
|
||||||
|
<form.SubmitButton>Submit</form.SubmitButton>
|
||||||
|
</form.AppForm>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
122
frontend/src/routes/transportation/opendock/index.tsx
Normal file
122
frontend/src/routes/transportation/opendock/index.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { authClient } from "../../../lib/auth-client";
|
||||||
|
import { getArticleLinks } from "../../../lib/queries/getArticleLinks";
|
||||||
|
import LstTable from "../../../lib/tableStuff/LstTable";
|
||||||
|
import SearchableHeader from "../../../lib/tableStuff/SearchableHeader";
|
||||||
|
import SkellyTable from "../../../lib/tableStuff/SkellyTable";
|
||||||
|
import NewArticleLink from "./-components/NewArticleLink";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/transportation/opendock/")({
|
||||||
|
beforeLoad: async ({ location }) => {
|
||||||
|
const { data: session } = await authClient.getSession();
|
||||||
|
//const allowedRole = ["systemAdmin", "admin", "manager"];
|
||||||
|
|
||||||
|
const canAccess = await authClient.admin.hasPermission({
|
||||||
|
permissions: {
|
||||||
|
openDock: ["create"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
throw redirect({
|
||||||
|
to: "/",
|
||||||
|
search: {
|
||||||
|
redirect: location.href,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//if (!allowedRole.includes(session.user.role as string)) {
|
||||||
|
|
||||||
|
if (!canAccess) {
|
||||||
|
throw redirect({
|
||||||
|
to: "/",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user: session.user };
|
||||||
|
},
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ArticleLinkTable = () => {
|
||||||
|
const { data, refetch } = useSuspenseQuery(getArticleLinks());
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<any>();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor("av", {
|
||||||
|
header: ({ column }) => (
|
||||||
|
<SearchableHeader column={column} title="Article" searchable={true} />
|
||||||
|
),
|
||||||
|
filterFn: "includesString",
|
||||||
|
cell: (i) => i.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("description", {
|
||||||
|
header: ({ column }) => (
|
||||||
|
<SearchableHeader
|
||||||
|
column={column}
|
||||||
|
title="Description"
|
||||||
|
searchable={true}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
filterFn: "includesString",
|
||||||
|
cell: (i) => i.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("customer", {
|
||||||
|
header: ({ column }) => (
|
||||||
|
<SearchableHeader column={column} title="Customer" searchable={true} />
|
||||||
|
),
|
||||||
|
filterFn: "includesString",
|
||||||
|
cell: (i) => (
|
||||||
|
<span>
|
||||||
|
{i.row.original.customer} - {i.row.original.customerDescription}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("loadType", {
|
||||||
|
header: ({ column }) => (
|
||||||
|
<SearchableHeader column={column} title="Load Type" searchable={true} />
|
||||||
|
),
|
||||||
|
filterFn: "includesString",
|
||||||
|
cell: (i) => i.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("dock", {
|
||||||
|
header: ({ column }) => (
|
||||||
|
<SearchableHeader column={column} title="Dock" searchable={true} />
|
||||||
|
),
|
||||||
|
filterFn: "includesString",
|
||||||
|
cell: (i) => i.getValue(),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-end m-2">
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div>
|
||||||
|
<p>Loading...</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<NewArticleLink refetch={refetch} />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<LstTable data={data} columns={columns} pageSize={50} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
function RouteComponent() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<SkellyTable />}>
|
||||||
|
<ArticleLinkTable />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -58,19 +58,25 @@ export default function TabsLayout() {
|
|||||||
// },
|
// },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* <Tabs.Screen
|
<Tabs.Screen
|
||||||
name="ppoo"
|
name="ppoo"
|
||||||
options={{
|
options={{
|
||||||
title: "PPOO",
|
title: "PPOO",
|
||||||
href: isNormalScanner ? null : "/(tabs)/ppoo",
|
href:
|
||||||
|
isNormalScanner || !hasRole(["admin", "manager"])
|
||||||
|
? null
|
||||||
|
: "/(tabs)/ppoo",
|
||||||
tabBarIcon: ({ color, size }) => <Boxes size={size} color={color} />,
|
tabBarIcon: ({ color, size }) => <Boxes size={size} color={color} />,
|
||||||
}}
|
}}
|
||||||
/> */}
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="laneCheck"
|
name="laneCheck"
|
||||||
options={{
|
options={{
|
||||||
title: "Lane Check",
|
title: "Lane Check",
|
||||||
href: isNormalScanner ? null : "/(tabs)/laneCheck",
|
href:
|
||||||
|
isNormalScanner || !hasRole(["admin", "manager"])
|
||||||
|
? null
|
||||||
|
: "/(tabs)/laneCheck",
|
||||||
tabBarIcon: ({ color, size }) => <Rows4 size={size} color={color} />,
|
tabBarIcon: ({ color, size }) => <Rows4 size={size} color={color} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
12
migrations/0056_shallow_chimera.sql
Normal file
12
migrations/0056_shallow_chimera.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE "opendock_dock_setup" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"dock_id" text NOT NULL,
|
||||||
|
"upd_date" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"upd_user" text DEFAULT 'lst-system' NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"add_user" text DEFAULT 'lst-system' NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "opendock_article_setup" DROP CONSTRAINT "opendock_article_setup_av_unique";--> statement-breakpoint
|
||||||
|
ALTER TABLE "opendock_article_setup" ADD CONSTRAINT "uq_opendock_article_setup_av_customer" UNIQUE("av","customer");
|
||||||
2515
migrations/meta/0056_snapshot.json
Normal file
2515
migrations/meta/0056_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -393,6 +393,13 @@
|
|||||||
"when": 1779399354404,
|
"when": 1779399354404,
|
||||||
"tag": "0055_nosy_amphibian",
|
"tag": "0055_nosy_amphibian",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 56,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1779454561527,
|
||||||
|
"tag": "0056_shallow_chimera",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user