feat(dm): migrated all the dm topics
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 4m26s

This commit is contained in:
2026-06-26 11:05:17 -05:00
parent 012a7e83b2
commit 47b149d1ea
48 changed files with 14156 additions and 44 deletions

View File

@@ -0,0 +1,53 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { Truck } from "lucide-react";
import { getSettings } from "../../lib/queries/getSettings";
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "../ui/sidebar";
export default function LogisticsSidebar({ session }: any) {
const { setOpen } = useSidebar();
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
const items = [
{
title: "Demand Management",
url: "/logistics/dm",
icon: Truck,
role: ["systemAdmin", "admin", "warehouse", "transport"],
module: "logistics",
active:
!isLoading &&
settings.filter((n: any) => n.name === "demandManagement")[0].active,
},
];
return (
<SidebarGroup>
<SidebarGroupLabel>Logistics</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<div key={item.title}>
{item.role.includes(session.user.role) && item.active && (
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)}
</div>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}

View File

@@ -15,6 +15,7 @@ import { getSettings } from "../../lib/queries/getSettings";
import { permissionQuery } from "../../lib/queries/permsCheck";
import AdminSidebar from "./AdminBar";
import DocBar from "./DocBar";
import LogisticsSidebar from "./LogisticsBar";
import MobileBar from "./MobileBar";
import TransportationBar from "./TransportationBar";
import WarehouseBar from "./Warhouse";
@@ -27,6 +28,12 @@ export function AppSidebar() {
openDock: ["read"],
}),
);
const { data: canReadWarehouse = false } = useQuery(
permissionQuery({
warehouse: ["update"],
}),
);
const { setOpen } = useSidebar();
// const { data: canReadWarehouse = false } = useQuery(
@@ -47,9 +54,11 @@ export function AppSidebar() {
<SidebarContent>
<DocBar />
{!isLoading &&
canReadWarehouse &&
settings.filter((n: any) => n.name === "mobile")[0].active && (
<MobileBar />
)}
{!isLoading && session && <LogisticsSidebar session={session} />}
{!isLoading &&
settings.filter((n: any) => n.name === "opendock_sync")[0]

View File

@@ -15,6 +15,7 @@ import { Route as ApidocsRouteImport } from './routes/apidocs'
import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index'
import { Route as DocsIndexRouteImport } from './routes/docs/index'
import { Route as LogisticsDmRouteImport } from './routes/logistics/dm'
import { Route as DocsDatamartRouteImport } from './routes/docs/datamart'
import { Route as DocsSplatRouteImport } from './routes/docs/$'
import { Route as AdminUsersRouteImport } from './routes/admin/users'
@@ -63,6 +64,11 @@ const DocsIndexRoute = DocsIndexRouteImport.update({
path: '/docs/',
getParentRoute: () => rootRouteImport,
} as any)
const LogisticsDmRoute = LogisticsDmRouteImport.update({
id: '/logistics/dm',
path: '/logistics/dm',
getParentRoute: () => rootRouteImport,
} as any)
const DocsDatamartRoute = DocsDatamartRouteImport.update({
id: '/docs/datamart',
path: '/docs/datamart',
@@ -169,6 +175,7 @@ export interface FileRoutesByFullPath {
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute
'/docs/datamart': typeof DocsDatamartRoute
'/logistics/dm': typeof LogisticsDmRoute
'/docs/': typeof DocsIndexRoute
'/user/profile': typeof authUserProfileRoute
'/user/resetpassword': typeof authUserResetpasswordRoute
@@ -194,6 +201,7 @@ export interface FileRoutesByTo {
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute
'/docs/datamart': typeof DocsDatamartRoute
'/logistics/dm': typeof LogisticsDmRoute
'/docs': typeof DocsIndexRoute
'/user/profile': typeof authUserProfileRoute
'/user/resetpassword': typeof authUserResetpasswordRoute
@@ -220,6 +228,7 @@ export interface FileRoutesById {
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute
'/docs/datamart': typeof DocsDatamartRoute
'/logistics/dm': typeof LogisticsDmRoute
'/docs/': typeof DocsIndexRoute
'/(auth)/user/profile': typeof authUserProfileRoute
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
@@ -247,6 +256,7 @@ export interface FileRouteTypes {
| '/admin/users'
| '/docs/$'
| '/docs/datamart'
| '/logistics/dm'
| '/docs/'
| '/user/profile'
| '/user/resetpassword'
@@ -272,6 +282,7 @@ export interface FileRouteTypes {
| '/admin/users'
| '/docs/$'
| '/docs/datamart'
| '/logistics/dm'
| '/docs'
| '/user/profile'
| '/user/resetpassword'
@@ -297,6 +308,7 @@ export interface FileRouteTypes {
| '/admin/users'
| '/docs/$'
| '/docs/datamart'
| '/logistics/dm'
| '/docs/'
| '/(auth)/user/profile'
| '/(auth)/user/resetpassword'
@@ -323,6 +335,7 @@ export interface RootRouteChildren {
AdminUsersRoute: typeof AdminUsersRoute
DocsSplatRoute: typeof DocsSplatRoute
DocsDatamartRoute: typeof DocsDatamartRoute
LogisticsDmRoute: typeof LogisticsDmRoute
DocsIndexRoute: typeof DocsIndexRoute
authUserProfileRoute: typeof authUserProfileRoute
authUserResetpasswordRoute: typeof authUserResetpasswordRoute
@@ -378,6 +391,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DocsIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/logistics/dm': {
id: '/logistics/dm'
path: '/logistics/dm'
fullPath: '/logistics/dm'
preLoaderRoute: typeof LogisticsDmRouteImport
parentRoute: typeof rootRouteImport
}
'/docs/datamart': {
id: '/docs/datamart'
path: '/docs/datamart'
@@ -515,6 +535,7 @@ const rootRouteChildren: RootRouteChildren = {
AdminUsersRoute: AdminUsersRoute,
DocsSplatRoute: DocsSplatRoute,
DocsDatamartRoute: DocsDatamartRoute,
LogisticsDmRoute: LogisticsDmRoute,
DocsIndexRoute: DocsIndexRoute,
authUserProfileRoute: authUserProfileRoute,
authUserResetpasswordRoute: authUserResetpasswordRoute,

View File

@@ -248,13 +248,13 @@ function RouteComponent() {
};
//console.log(logs);
return (
<div className="flex flex-col gap-1 max-w-7xl">
<div className="flex flex-col gap-1 w-7xl">
<div className="flex gap-1 justify-end">
<Button onClick={triggerBuild}>Trigger Build</Button>
<Button onClick={() => clearRoom()}>Clear Logs</Button>
</div>
<div className="flex gap-1 w-full">
<div className="w-full">
<div className="w-3/4">
<Suspense fallback={<SkellyTable />}>
<ServerTable />
</Suspense>

View File

@@ -178,10 +178,10 @@ function SettingsTableCard() {
function RouteComponent() {
return (
<div className="space-y-6">
<div className="space-y-6 w-7xl flex flex-col justify-center">
<div className="space-y-2">
<h1 className="text-2xl font-semibold">Settings</h1>
<p className="text-sm text-muted-foreground">
<h1 className="text-2xl font-semibold text-center">Settings</h1>
<p className="text-sm text-muted-foreground text-center">
Manage your settings and related data.
</p>
</div>

View File

@@ -0,0 +1,146 @@
import { useRef, useState } from "react";
import { toast } from "sonner";
import { Button } from "../../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardTitle,
} from "../../../components/ui/card";
import { Separator } from "../../../components/ui/separator";
import { api } from "../../../lib/apiHelper";
import { useSession } from "../../../lib/auth-client";
export default function ForecastUpload({
server,
responseData,
}: {
server: string;
responseData: any;
}) {
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [posting, setPosting] = useState(false);
const [selectedFileType, setSelectedFileType] = useState<string>("");
const { data: session } = useSession();
const importOrders = async (e: React.ChangeEvent<HTMLInputElement>) => {
console.log("clicked import");
responseData([]);
const file = e.target.files?.[0];
if (!file) {
toast.error("Missing or no file was selected please try again");
setPosting(false);
return;
}
// create the form data with the correct fileType
const formData = new FormData();
formData.append("file", file);
formData.append("fileType", selectedFileType); // extra field
formData.append("username", `${session?.user.username}`);
toast.success("Import started.");
try {
const response = await api.post("/logistics/dm/forecast", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
validateStatus: (status) => status < 500,
});
//console.log("Upload successful:", response.data);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
setPosting(false);
if (response.status === 200) {
toast.success(response?.data?.message);
responseData(response.data.data);
} else {
toast.error(response?.data?.message);
}
} catch (error) {
console.error(error);
toast.error("Upload failed");
}
setPosting(false);
};
const handleButtonClick = (type: string) => {
setPosting(true);
const handleFocus = () => {
setTimeout(() => {
if (!fileInputRef.current?.files?.length) {
setPosting(false);
}
}, 0);
};
window.addEventListener("focus", handleFocus, { once: true });
setSelectedFileType(type);
fileInputRef.current?.click();
};
const ForecastButton = ({ name, type }: { name: string; type: string }) => {
return (
<div>
<Button onClick={() => handleButtonClick(type)} disabled={posting}>
{name}
</Button>
</div>
);
};
// For plants that basically use the same import set like this so we dont have weird looking cards
const pngForecast = ["usiow1", "usiow2", "usksc1"];
return (
<div>
<div>
<Card>
<CardTitle>
<p className="text-center">Forecast</p>
</CardTitle>
<CardDescription className="w-64 p-2 ">
<p className="text-xs">
When clicking on one of the below options you will need to upload
the respective file to be processed to 2.0
</p>
<Separator />
</CardDescription>
<CardContent>
<div className="grid grid-cols-2 gap-2">
<ForecastButton name={"Standard"} type={"standard"} />
{server === "usday1" ||
(server.includes("test") && (
<ForecastButton
name={"Energizer Forecast"}
type={"energizer"}
/>
))}
{pngForecast.includes(server) ||
(server.includes("test") && (
<ForecastButton name={"PnG"} type={"pg"} />
))}
{server === "usflo1" ||
(server.includes("test") && (
<ForecastButton name={"VMI Import"} type={"loreal"} />
))}
</div>
</CardContent>
</Card>
</div>
<input
type="file"
accept=".xlsx, .xls, .xlsm"
ref={fileInputRef}
style={{ display: "none" }}
onChange={importOrders}
/>
</div>
);
}

View File

@@ -0,0 +1,139 @@
import { useRef, useState } from "react";
import { toast } from "sonner";
import { Button } from "../../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardTitle,
} from "../../../components/ui/card";
import { Separator } from "../../../components/ui/separator";
import { api } from "../../../lib/apiHelper";
import { useSession } from "../../../lib/auth-client";
export default function OrdersUpload({
server,
responseData,
}: {
server: string;
responseData: any;
}) {
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [posting, setPosting] = useState(false);
const [selectedFileType, setSelectedFileType] = useState<string>("");
const { data: session } = useSession();
const importOrders = async (e: React.ChangeEvent<HTMLInputElement>) => {
responseData([]);
const file = e.target.files?.[0];
if (!file) {
toast.error("Missing or no file was selected please try again");
setPosting(false);
return;
}
// create the form data with the correct fileType
const formData = new FormData();
formData.append("file", file);
formData.append("fileType", selectedFileType); // extra field
formData.append("username", `${session?.user.username}`);
toast.success("Import started.");
try {
const response = await api.post("/logistics/dm/orders", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
validateStatus: (status) => status < 500,
});
//console.log("Upload successful:", response.data);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
setPosting(false);
if (response.status === 200) {
toast.success(response?.data?.message);
//console.log(response.data);
responseData(response.data.data);
} else {
toast.error(response?.data?.message);
}
} catch (error) {
console.error(error);
toast.error("Upload failed");
}
setPosting(false);
};
const handleButtonClick = (type: string) => {
setPosting(true);
const handleFocus = () => {
setTimeout(() => {
if (!fileInputRef.current?.files?.length) {
setPosting(false);
}
}, 0);
};
window.addEventListener("focus", handleFocus, { once: true });
setSelectedFileType(type);
fileInputRef.current?.click();
};
const OrderButton = ({ name, type }: { name: string; type: string }) => {
return (
<div>
<Button onClick={() => handleButtonClick(type)} disabled={posting}>
{name}
</Button>
</div>
);
};
return (
<div>
<div>
<Card>
<CardTitle>
<p className="text-center">Orders</p>
</CardTitle>
<CardDescription className="w-64 p-2 ">
<p className="text-xs">
When clicking on one of the below options you will need to upload
the respective file to be processed to 2.0
</p>
<Separator />
</CardDescription>
<CardContent>
<div className="grid grid-cols-2 gap-2">
<OrderButton name={"Standard"} type={"standard"} />
{server === "usweb1" ||
(server.includes("test") && (
<OrderButton name={"SCJ Orders"} type={"scj"} />
))}
{server === "usday1" ||
(server.includes("test") && (
<>
<OrderButton name={"Abbott"} type={"abbott"} />
<OrderButton
name={"Energizer Truck List"}
type={"energizer"}
/>
</>
))}
</div>
</CardContent>
</Card>
</div>
<input
type="file"
accept=".xlsx, .xls, .xlsm"
ref={fileInputRef}
style={{ display: "none" }}
onChange={importOrders}
/>
</div>
);
}

View File

@@ -0,0 +1,72 @@
import { format } from "date-fns";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "../../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardTitle,
} from "../../../components/ui/card";
import { Separator } from "../../../components/ui/separator";
import { api } from "../../../lib/apiHelper";
export default function Templates() {
const [template, setTemplate] = useState(false);
const getTemplate = async (type: "orders" | "forecast") => {
setTemplate(true);
try {
const res = await api.get(`/logistics/dm/template?filename=${type}`, {
responseType: "blob",
});
const blob = new Blob([res.data], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = `Bulk${type.charAt(0).toUpperCase() + type.slice(1)}_Template-${format(new Date(Date.now()), "M-d-yyyy")}.xlsx`; // You can make this dynamic
document.body.appendChild(link);
link.click();
// Clean up
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
toast.success(`Template created for ${type}`);
setTemplate(false);
} catch {
setTemplate(false);
toast.error("There was an error getting the template");
}
};
return (
<div>
<Card>
<CardTitle>
<p className="text-center">Templates</p>
</CardTitle>
<CardDescription className="w-64 p-2 ">
<p>
Clicking one of the template's below will generate an excel file you
can fill out and re-upload, using the standard orders or standard
forecast button
</p>
<Separator />
</CardDescription>
<CardContent>
<div className="flex flex-rol justify-center gap-2">
<Button onClick={() => getTemplate("orders")} disabled={template}>
Orders
</Button>
<Button onClick={() => getTemplate("forecast")} disabled={template}>
Forecast
</Button>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,127 @@
import { createFileRoute } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import { useState } from "react";
import { Separator } from "../../components/ui/separator";
import LstTable from "../../lib/tableStuff/LstTable";
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
import { runtimeConfig } from "../../lib/umami.utils";
import ForecastUpload from "./-components/dm.ForecastUpload";
import OrdersUpload from "./-components/dm.OrdersUpload";
import Templates from "./-components/dm.Templates";
export const Route = createFileRoute("/logistics/dm")({
component: RouteComponent,
});
function RouteComponent() {
const server = runtimeConfig.server;
const columnHelper = createColumnHelper<any>();
const [uploadInfo, setUploadInfo] = useState<any>([]);
console.log(uploadInfo?.data?.orders);
const columns = [
columnHelper.accessor("customerId", {
header: ({ column }) => (
<SearchableHeader
column={column}
title="Customer ID"
searchable={true}
/>
),
filterFn: "includesString",
cell: (i) => <>{i.getValue()}</>,
}),
columnHelper.accessor("customerOrderNo", {
header: ({ column }) => (
<SearchableHeader
column={column}
title="Customer Order Number"
searchable={true}
/>
),
filterFn: "includesString",
cell: (i) => <>{i.getValue()}</>,
}),
columnHelper.accessor((row) => row.positions?.[0]?.customerLineItemNo, {
id: "customerLineItemNo",
header: ({ column }) => (
<SearchableHeader
column={column}
title="Customer Line Item No"
searchable={true}
/>
),
filterFn: "includesString",
cell: (i) => <>{i.getValue()}</>,
}),
columnHelper.accessor((row) => row.positions?.[0]?.deliveryDate, {
id: "deliveryDate",
header: ({ column }) => (
<SearchableHeader
column={column}
title="Delivery date"
searchable={true}
/>
),
filterFn: "includesString",
cell: (i) => <>{i.getValue()}</>,
}),
columnHelper.accessor((row) => row.positions?.[0]?.quantity, {
id: "quantity",
header: ({ column }) => (
<SearchableHeader column={column} title="Quantity" searchable={true} />
),
filterFn: "includesString",
cell: (i) => <>{i.getValue()}</>,
}),
];
return (
<div>
<div className="w-7x1 mt-2 flex flex-row gap-2 justify-center">
<div>
<Templates />
</div>
<div>
<ForecastUpload server={server} responseData={setUploadInfo} />
</div>
<div>
<OrdersUpload server={server} responseData={setUploadInfo} />
</div>
</div>
<div className="mt-2">
<p>
As a reminder, All forecast and orders, uploaded will always show
processed as good or error, you will still need to go into AlplaPROD
2.0 and validate the file processed all orders with no errors.
</p>
<p>
This is due to AlplaPROD basically saying it has accepted your file
only.
</p>
</div>
<Separator />
<p className="text-sm">
The info in the table below is what was processed and sent over to
AlplaPROD, if releases were in any other state than planned they would
be skipped and not show up below.
</p>
<p className="text-sm">
We only pass back the data you sent in no releases you will need to
validate this on your own.
</p>
<Separator className="m-2" />
<p className="text-center">
The below table will show orders only forecast will be added in later
</p>
<div className="w-7xl flex justify-center mt-3">
<LstTable
data={uploadInfo?.data?.orders ?? []}
columns={columns}
pageSize={50}
className="max-h-96 "
/>
</div>
</div>
);
}