feat(siloadjustments): added email for comments :D
This commit is contained in:
12
server/globalUtils/greetingEmail.ts
Normal file
12
server/globalUtils/greetingEmail.ts
Normal 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";
|
||||
}
|
||||
};
|
||||
11
server/globalUtils/singleUseKey.ts
Normal file
11
server/globalUtils/singleUseKey.ts
Normal 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
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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.",
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
26
server/services/sqlServer/querys/silo/siloQuery.ts
Normal file
26
server/services/sqlServer/querys/silo/siloQuery.ts
Normal 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
|
||||
`;
|
||||
Reference in New Issue
Block a user