feat(siloadjustments): added email for comments :D

This commit is contained in:
2025-04-04 22:09:47 -05:00
parent 9f26f2334f
commit f1abe7b33d
24 changed files with 8565 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
CREATE TABLE "siloAdjustments" (
"lsiloAdjust_id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"level" integer,
"locationID" integer,
"currentStockLevel" numeric,
"newLevel" numeric,
"comments" text DEFAULT '',
"dateAdjusted" timestamp DEFAULT now(),
"lastDateAdjusted" timestamp DEFAULT now(),
"statusMessage" text DEFAULT '',
"add_user" text DEFAULT 'LST_Serivce'
);

View File

@@ -0,0 +1,5 @@
ALTER TABLE "siloAdjustments" ADD COLUMN "comment" text DEFAULT '';--> statement-breakpoint
ALTER TABLE "siloAdjustments" ADD COLUMN "commentAddedBy" text;--> statement-breakpoint
ALTER TABLE "siloAdjustments" ADD COLUMN "commentDate" text;--> statement-breakpoint
ALTER TABLE "siloAdjustments" DROP COLUMN "comments";--> statement-breakpoint
ALTER TABLE "siloAdjustments" DROP COLUMN "statusMessage";

View File

@@ -0,0 +1 @@
ALTER TABLE "siloAdjustments" ADD COLUMN "commentKey" text;

View File

@@ -0,0 +1 @@
CREATE UNIQUE INDEX "subModule_name" ON "subModules" USING btree ("name");

View File

@@ -0,0 +1 @@
ALTER TABLE "subModules" ADD COLUMN "icon" text;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -302,6 +302,41 @@
"when": 1743778477759,
"tag": "0042_big_power_pack",
"breakpoints": true
},
{
"idx": 43,
"version": "7",
"when": 1743809547351,
"tag": "0043_free_winter_soldier",
"breakpoints": true
},
{
"idx": 44,
"version": "7",
"when": 1743811709366,
"tag": "0044_hot_smasher",
"breakpoints": true
},
{
"idx": 45,
"version": "7",
"when": 1743819367359,
"tag": "0045_heavy_ravenous",
"breakpoints": true
},
{
"idx": 46,
"version": "7",
"when": 1743821039322,
"tag": "0046_keen_firebird",
"breakpoints": true
},
{
"idx": 47,
"version": "7",
"when": 1743822056329,
"tag": "0047_silky_starbolt",
"breakpoints": true
}
]
}

View File

@@ -0,0 +1,39 @@
import {
text,
pgTable,
numeric,
timestamp,
uuid,
integer,
} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { z } from "zod";
export const siloAdjustments = pgTable(
"siloAdjustments",
{
siloAdjust_id: uuid("lsiloAdjust_id").defaultRandom().primaryKey(),
warehouseID: integer("level"),
locationID: integer("locationID"),
currentStockLevel: numeric("currentStockLevel"),
newLevel: numeric("newLevel"),
comment: text("comment").default(""),
dateAdjusted: timestamp("dateAdjusted").defaultNow(),
lastDateAdjusted: timestamp("lastDateAdjusted").defaultNow(),
commentAddedBy: text("commentAddedBy"),
commentDate: text("commentDate"),
commentKey: text("commentKey"),
add_user: text("add_user").default("LST_Serivce"),
},
(table) => [
// uniqueIndex('emailUniqueIndex').on(sql`lower(${table.email})`),
// uniqueIndex("role_name").on(table.name),
]
);
// Schema for inserting a user - can be used to validate API requests
// export const insertRolesSchema = createInsertSchema(roles, {
// name: z.string().min(3, {message: "Role name must be more than 3 letters"}),
// });
// Schema for selecting a Expenses - can be used to validate API responses
export const selectRolesSchema = createSelectSchema(siloAdjustments);

View File

@@ -0,0 +1,120 @@
import { LstCard } from "@/components/extendedUI/LstCard";
import { Button } from "@/components/ui/button";
import { CardContent, CardFooter, CardHeader } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { useForm } from "@tanstack/react-form";
import { useRouter } from "@tanstack/react-router";
import axios from "axios";
import { useState } from "react";
import { toast } from "sonner";
export default function Comment(data: any) {
const token = localStorage.getItem("auth_token");
const [isSubmitting, setIsSubmitting] = useState(false);
const router = useRouter();
const form = useForm({
defaultValues: {
comment: "",
},
onSubmit: async ({ value }) => {
setIsSubmitting(true);
try {
const res = await axios.post(
`/api/logistics/postcomment/${data.id.split("&")[0]}`,
{
comment: value.comment,
key: data.id.split("&")[1],
},
{ headers: { Authorization: `Bearer ${token}` } }
);
if (res.data.success) {
toast.success(res.data.message);
form.reset();
router.navigate({ to: "/siloAdjustments" });
}
if (!res.data.success) {
toast.error(res.data.message);
form.reset();
}
} catch (error) {
console.log(error);
toast.error(`There was an error posting your comment.`);
}
setIsSubmitting(false);
},
});
return (
<div className="">
<LstCard>
<CardHeader>
Please enter your comment for the silo adjust
</CardHeader>
<CardContent>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<form.Field
name="comment"
validators={{
// We can choose between form-wide and field-specific validators
onChange: ({ value }) =>
value.length > 10
? undefined
: "Comment must be longer than 10 characters.",
}}
children={(field) => {
return (
<div className="m-2 min-w-48 max-w-96 p-2">
<Label
htmlFor="comment"
className="mb-2"
>
Comment
</Label>
<Textarea
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
//type="number"
onChange={(e) =>
field.handleChange(
e.target.value
)
}
/>
{field.state.meta.errors.length ? (
<em>
{field.state.meta.errors.join(
","
)}
</em>
) : null}
</div>
);
}}
/>
</form>
</CardContent>
<CardFooter>
<div className="flex justify-end">
<Button
onClick={form.handleSubmit}
disabled={isSubmitting}
>
Submit
</Button>
</div>
</CardFooter>
</LstCard>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import Comment from "@/components/logistics/siloAdjustments/Comment";
import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute(
"/(logistics)/siloAdjustments/comment/$comment"
)({
beforeLoad: async () => {
const auth = localStorage.getItem("auth_token");
if (!auth) {
throw redirect({
to: "/login",
search: {
// Use the current location to power a redirect after login
// (Do not use `router.state.resolvedLocation` as it can
// potentially lag behind the actual current location)
redirect: location.pathname + location.search,
},
});
}
},
// In a loader
loader: ({ params }) => params.comment,
// Or in a component
component: RouteComponent,
});
function RouteComponent() {
const { comment } = Route.useParams();
return (
<div className="ml-20 mt-20">
<Comment id={comment} />
</div>
);
}

View File

@@ -0,0 +1,32 @@
//import { Input } from "@/components/ui/input";
//import { Label } from "@radix-ui/react-dropdown-menu";
// export const FormInput = (form: any, label: string) => {
// // <form.Field
// // name="username"
// // validators={{
// // // We can choose between form-wide and field-specific validators
// // onChange: ({ value }) =>
// // value.length > 3
// // ? undefined
// // : "Username must be longer than 3 letters",
// // }}
// // children={(field) => {
// // return (
// // <div className="m-2 min-w-48 max-w-96 p-2">
// // <Label htmlFor="username">{label}</Label>
// // <Input
// // name={field.name}
// // value={field.state.value}
// // onBlur={field.handleBlur}
// // //type="number"
// // onChange={(e) => field.handleChange(e.target.value)}
// // />
// // {field.state.meta.errors.length ? (
// // <em>{field.state.meta.errors.join(",")}</em>
// // ) : null}
// // </div>
// // );
// // }}
// // />;
// };

View File

@@ -0,0 +1,12 @@
import { getHours } from "date-fns";
export const greetingStuff = async (date = new Date()) => {
const hour = getHours(date);
if (hour < 12) {
return "Good morning";
} else if (hour < 18) {
return "Good afternoon";
} else {
return "Good evening";
}
};

View File

@@ -0,0 +1,11 @@
import crypto from "crypto";
export const generateOneTimeKey = async (length = 32) => {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let key = "";
const bytes = crypto.randomBytes(length);
for (let i = 0; i < length; i++) {
key += chars[bytes[i] % chars.length];
}
return key.match(/.{1,4}/g)!.join("-"); // group by 4 chars
};

View File

@@ -0,0 +1,130 @@
import { db } from "../../../../../database/dbclient.js";
import { tryCatch } from "../../../../globalUtils/tryCatch.js";
import { query } from "../../../sqlServer/prodSqlServer.js";
import { siloQuery } from "../../../sqlServer/querys/silo/siloQuery.js";
import { postAdjustment } from "./postAdjustment.js";
import { siloAdjustments } from "../../../../../database/schema/siloAdjustments.js";
import { greetingStuff } from "../../../../globalUtils/greetingEmail.js";
import { sendEmail } from "../../../notifications/controller/sendMail.js";
import { settings } from "../../../../../database/schema/settings.js";
import { generateOneTimeKey } from "../../../../globalUtils/singleUseKey.js";
import { eq } from "drizzle-orm";
export const createSiloAdjustment = async (
data: any | null,
user: any | null
) => {
/**
* Creates a silo adjustment based off warehouse, location, and qty.
* qty will come from the hmi, prolink, or silo patrol
*/
const { data: set, error: setError } = await tryCatch(
db.select().from(settings)
);
if (setError) {
return {
success: false,
message: `There was an error getting setting data to post to the server.`,
data: setError,
};
}
// getting stock data first so we have it prior to the adjustment
const { data: stock, error: stockError } = await tryCatch(
query(siloQuery, "Silo data Query")
);
if (stockError) {
return {
success: false,
message: `There was an error getting stock data to post to the server.`,
data: stockError,
};
}
const { data: a, error: errorAdj } = await tryCatch(
postAdjustment(data, user.prod)
);
if (errorAdj) {
return {
success: false,
message: `There was an error doing the silo adjustment.`,
data: errorAdj,
};
}
/**
* Checking to see the difference, and send email if +/- 5% will change later if needed
*/
const stockNummy = stock.filter((s: any) => s.LocationID === data.laneId);
const theDiff =
((data.quantity - stockNummy[0].Stock_Total) /
((data.quantity + stockNummy[0].Stock_Total) / 2)) *
100;
/**
* Post the data to our db.
*/
//console.log(stockNummy);
const { data: postAdj, error: postAdjError } = await tryCatch(
db
.insert(siloAdjustments)
.values({
warehouseID: data.warehouseId,
locationID: data.laneId,
currentStockLevel: stockNummy[0].Stock_Total,
newLevel: data.quantity,
lastDateAdjusted: new Date(stockNummy[0].LastAdjustment),
add_user: user.username,
})
.returning({ id: siloAdjustments.siloAdjust_id })
);
if (postAdjError) {
//console.log(postAdjError);
return {
success: false,
message: `There was an error posting the new adjustment.`,
data: postAdjError,
};
}
if (Math.abs(theDiff) > 5) {
// console.log(`Send for comment due to being: ${theDiff.toFixed(2)}%`);
const server = set.filter((n: any) => n.name === "server");
const port = set.filter((n: any) => n.name === "serverPort");
const key = await generateOneTimeKey();
const updateKey = await db
.update(siloAdjustments)
.set({ commentKey: key })
.where(eq(siloAdjustments.siloAdjust_id, postAdj[0].id));
const emailSetup = {
email: user.email,
subject: `Alert - Siloadjustment was done with a descrepancy of 5% or greater`,
template: "siloAdjustmentComment",
context: {
greeting: await greetingStuff(),
siloName: stockNummy[0].Description,
variance: `${theDiff.toFixed(2)}%`,
currentLevel: stockNummy[0].Stock_Total,
newLevel: data.quantity,
variancePer: 5,
adjustID: `${postAdj[0].id}&${key}`,
server: server[0].value,
port: port[0].value,
},
};
//console.log(emailSetup);
await sendEmail(emailSetup);
}
let adj: any = a;
return { success: adj.success, message: adj.message, data: adj.data };
};

View File

@@ -0,0 +1,81 @@
import axios from "axios";
import { prodEndpointCreation } from "../../../../globalUtils/createUrl.js";
import { tryCatch } from "../../../../globalUtils/tryCatch.js";
export const postAdjustment = async (data: any, prod: any) => {
if (data.warehouseId === undefined) {
return {
sucess: false,
message: `Missing mandatory field: warehouseID`,
data: { error: `Missing mandatory field: warehouseID` },
};
}
if (data.laneId === undefined) {
return {
sucess: false,
message: `Missing mandatory field: locationID`,
data: { error: `Missing mandatory field: locationID` },
};
}
if (data.quantity == "0") {
return {
sucess: false,
message: `You entered 0 for the quantity to post, quantity needs to be at leave 1`,
data: {
error: `You entered 0 for the quantity to post, quantity needs to be at leave 1`,
},
};
}
const siloAdjustment = {
warehouseId: data.warehouseId,
laneId: data.laneId,
quantity: data.quantity,
};
let url = await prodEndpointCreation(
"/public/v1.0/Warehousing/AdjustSiloStockLevel"
);
const { data: silo, error } = await tryCatch(
axios.post(url, siloAdjustment, {
headers: { Authorization: `Basic ${prod}` },
})
);
let e = error as any;
if (error) {
return {
success: false,
message: "Error in posting the silo adjustment.",
data: {
status: e.response?.status,
statusText: e.response?.statusText,
data: e.response?.data,
},
};
}
if (silo.status !== 200) {
return {
success: false,
message: "Error in posting the silo adjustment",
data: {
status: silo.status,
statusText: silo.statusText,
data: silo.data,
},
};
}
return {
success: true,
message: "Adjustment was completed",
data: {
status: silo.status,
statusText: silo.statusText,
data: silo.data,
},
};
};

View File

@@ -0,0 +1,64 @@
import { eq, sql } from "drizzle-orm";
import { db } from "../../../../../database/dbclient.js";
import { siloAdjustments } from "../../../../../database/schema/siloAdjustments.js";
import { tryCatch } from "../../../../globalUtils/tryCatch.js";
export const postSiloComment = async (
id: string,
comment: string,
commentk: string,
user: any
) => {
/**
* We will add the comment to the silo adjustment so we know the why we had this.
*/
// make sure we havea valid key
const { data: key, error: keyErro } = await tryCatch(
db
.select()
.from(siloAdjustments)
.where(eq(siloAdjustments.siloAdjust_id, id))
);
if (keyErro) {
return {
success: false,
message: "There was an error getting the adjustment.",
data: keyErro,
};
}
if (key[0].commentKey != commentk) {
return {
success: false,
message: "The key you provided is invalid.",
data: keyErro,
};
}
const { data, error } = await tryCatch(
db
.update(siloAdjustments)
.set({
comment: comment,
commentAddedBy: user.username,
commentDate: sql`NOW()`,
commentKey: null,
})
.where(eq(siloAdjustments.siloAdjust_id, id))
);
if (error) {
return {
success: false,
message: "There was an error adding the comment.",
data: error,
};
}
return {
success: true,
message: "Comment was successfully added.",
};
};

View File

@@ -0,0 +1,64 @@
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { verify } from "hono/jwt";
import { authMiddleware } from "../../../auth/middleware/authMiddleware.js";
import { responses } from "../../../../globalUtils/routeDefs/responses.js";
import { createSiloAdjustment } from "../../controller/siloAdjustments/createSiloAdjustment.js";
const app = new OpenAPIHono();
const responseSchema = z.object({
success: z.boolean().optional().openapi({ example: true }),
message: z.string().optional().openapi({ example: "user access" }),
});
app.openapi(
createRoute({
tags: ["logistics"],
summary: "Creates silo adjustmennt",
method: "post",
path: "/createsiloadjustment",
middleware: authMiddleware,
description:
"Creates a silo adjustment for the silo if and stores the stock numbers.",
responses: responses(),
}),
async (c) => {
//apiHit(c, { endpoint: "api/sqlProd/close" });
const authHeader = c.req.header("Authorization");
const token = authHeader?.split("Bearer ")[1] || "";
try {
const payload = await verify(token, process.env.JWT_SECRET!);
try {
//return apiReturn(c, true, access?.message, access?.data, 200);
const data = await c.req.json();
const createSiloAdj = await createSiloAdjustment(
data,
payload.user
);
return c.json(
{
success: createSiloAdj.success,
message: createSiloAdj.message,
data: createSiloAdj.data,
},
200
);
} catch (error) {
//console.log(error);
//return apiReturn(c, false, "Error in setting the user access", error, 400);
return c.json(
{
success: false,
message: "Missing data please try again",
error,
},
400
);
}
} catch (error) {
return c.json({ success: false, message: "Unauthorized" }, 401);
}
}
);
export default app;

View File

@@ -0,0 +1,85 @@
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { verify } from "hono/jwt";
import { authMiddleware } from "../../../auth/middleware/authMiddleware.js";
import { responses } from "../../../../globalUtils/routeDefs/responses.js";
import { createSiloAdjustment } from "../../controller/siloAdjustments/createSiloAdjustment.js";
import { postSiloComment } from "../../controller/siloAdjustments/postComment.js";
const app = new OpenAPIHono();
const ParamsSchema = z.object({
adjId: z
.string()
.min(3)
.openapi({
param: {
name: "adjId",
in: "path",
},
example: "3b555052-a960-4301-8d38-a6f1acb98dbe",
}),
});
const Body = z.object({
comment: z
.string()
.openapi({ example: "Reason to why i had a badd adjustment." }),
});
app.openapi(
createRoute({
tags: ["logistics"],
summary: "Post a comment to why you had a discrepancy",
method: "post",
path: "/postcomment/:adjId",
middleware: authMiddleware,
request: {
params: ParamsSchema,
body: { content: { "application/json": { schema: Body } } },
},
// description:
// "Creates a silo adjustment for the silo if and stores the stock numbers.",
responses: responses(),
}),
async (c: any) => {
//apiHit(c, { endpoint: "api/sqlProd/close" });
const authHeader = c.req.header("Authorization");
const token = authHeader?.split("Bearer ")[1] || "";
const { adjId } = c.req.valid("param");
try {
const payload = await verify(token, process.env.JWT_SECRET!);
try {
//return apiReturn(c, true, access?.message, access?.data, 200);
const data = await c.req.json();
const addComment = await postSiloComment(
adjId,
data.comment,
data.key,
payload.user
);
return c.json(
{
success: addComment.success,
message: addComment.message,
data: addComment.data,
},
200
);
} catch (error) {
return c.json(
{
success: false,
message: "Missing data please try again",
error,
},
400
);
}
} catch (error) {
return c.json({ success: false, message: "Unauthorized" }, 401);
}
}
);
export default app;

View File

@@ -0,0 +1,41 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{{!--<title>Order Summary</title> --}}
{{> styles}}
<style>
pre {
background-color: #f8f9fa;
color: #d63384;
padding: 10px;
border-radius: 5px;
white-space: pre-wrap;
font-family: monospace;
}
</style>
{{!-- <link rel="stylesheet" href="styles/styles.css" /> --}}
</head>
<body>
<p>
{{greeting}},<br/><br/>
A silo adjustment was just completed on {{siloName}}, with a variation of {{variance}}.<br/><br/>
The data that was passed over.<br/><br/>
Current stock at the time of the adjustment: {{currentLevel}}.<br/><br/>
What was entered as the new number: {{newLevel}}<br/><br/>
Please add your comment as to why the variance greater than {{variancePer}}<br/><br/>
<a href="http://{{server}}:5173/siloAdjustments/comment/{{adjustID}}"
style="display:inline-block; padding:10px 20px; text-decoration:none; border-radius:5px;">
Add a Comment
</a><br/><br/>
Best regards,<br/><br/>
LST team<br/>
</p>
</body>
</html>

View File

@@ -0,0 +1,26 @@
export const siloQuery = `
SELECT
V_LagerAbteilungen.Bezeichnung AS Description,
V_LagerAbteilungen.IdWarenLager AS WarehouseID,
V_LagerAbteilungen.IdLagerAbteilung AS LocationID,
ROUND(SUM(einlagerungsmengesum), 2) AS Stock_Total,
COALESCE(LastAdjustment, '1900-01-01') AS LastAdjustment
FROM AlplaPROD_test1.dbo.V_LagerAbteilungen (NOLOCK)
JOIN
AlplaPROD_test1.dbo.V_LagerPositionenBarcodes ON
AlplaPROD_test1.dbo.V_LagerAbteilungen.IdLagerAbteilung =
AlplaPROD_test1.dbo.V_LagerPositionenBarcodes.IdLagerAbteilung
LEFT JOIN (
SELECT
IdLagerAbteilung,
MAX(CASE WHEN CONVERT(CHAR(10), Buchungsdatum, 120) IS NULL THEN '1900-01-01' ELSE CONVERT(CHAR(10), Buchungsdatum, 120) END) AS LastAdjustment
FROM AlplaPROD_test1.dbo.V_LagerBuchungen (NOLOCK)
WHERE urheber = 2900
GROUP BY IdLagerAbteilung
) AS LastAdj ON AlplaPROD_test1.dbo.V_LagerAbteilungen.IdLagerAbteilung = LastAdj.IdLagerAbteilung
WHERE materialsilo = 1
AND aktiv = 1
GROUP BY V_LagerAbteilungen.Bezeichnung, V_LagerAbteilungen.IdWarenLager, V_LagerAbteilungen.IdLagerAbteilung, LastAdjustment
ORDER BY V_LagerAbteilungen.Bezeichnung
`;