From a30eebf5d34c77c6c7118faf01776651f8888547 Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Wed, 19 Nov 2025 18:42:58 -0600 Subject: [PATCH] test(materials per day): work on getting this running better --- .../materialsPerDay/materialPerDay.ts | 93 +++++-- .../materialsPerDay/materialPurchases.ts | 45 +++- .../materialsPerDay/materialWithInv.ts | 59 ++++- .../notifications/controller/sendMail.ts | 239 +++++++++--------- .../routes/manualTiggerMaterials.ts | 28 ++ .../notifications/routes/manualTiggerTi.ts | 37 ++- .../utils/views/materialPerDay.hbs | 44 ++++ .../querys/dataMart/materialPerDay.ts | 4 +- .../sqlServer/querys/misc/singleArticle.ts | 10 + 9 files changed, 373 insertions(+), 186 deletions(-) create mode 100644 lstV2/server/services/notifications/routes/manualTiggerMaterials.ts create mode 100644 lstV2/server/services/notifications/utils/views/materialPerDay.hbs create mode 100644 lstV2/server/services/sqlServer/querys/misc/singleArticle.ts diff --git a/lstV2/server/services/notifications/controller/materialsPerDay/materialPerDay.ts b/lstV2/server/services/notifications/controller/materialsPerDay/materialPerDay.ts index feb7a5b..be6e7a7 100644 --- a/lstV2/server/services/notifications/controller/materialsPerDay/materialPerDay.ts +++ b/lstV2/server/services/notifications/controller/materialsPerDay/materialPerDay.ts @@ -9,11 +9,14 @@ import { import { db } from "../../../../../database/dbclient.js"; import { invHistoricalData } from "../../../../../database/schema/historicalINV.js"; import { tryCatch } from "../../../../globalUtils/tryCatch.js"; +import { createLog } from "../../../logger/logger.js"; import { query } from "../../../sqlServer/prodSqlServer.js"; import { materialPerDay, materialPurchasesPerDay, } from "../../../sqlServer/querys/dataMart/materialPerDay.js"; +import { singleArticle } from "../../../sqlServer/querys/misc/singleArticle.js"; +import { sendEmail } from "../sendMail.js"; import { materialPurchases } from "./materialPurchases.js"; import { buildInventoryTimeline } from "./materialWithInv.js"; @@ -99,43 +102,77 @@ export default async function materialPerDayCheck() { // purchases - const { data: p, error: pe } = (await tryCatch( - query( - materialPurchasesPerDay - .replace("[startDate]", startDate) - .replace("[endDate]", endDate), - "material check", - ), - )) as any; - - if (error) { - return { - success: false, - message: "Error getting the material data", - error, - }; - } - - if (!data.success) { - return { - success: false, - message: data.message, - data: [], - }; - } + const pOrders = (await materialPurchases({ startDate, endDate })) as any; + //console.log(pOrders); const openingInventory: Record = {}; const inventoryRows = await getInv(); for (const row of inventoryRows) { openingInventory[row.article] = Number(row.total_QTY) || 0; } + const materialsDemand = buildInventoryTimeline( + sumByMaterialAndWeek(data.data) as any, + openingInventory, + pOrders, + ); + + const { data: av, error: eav } = await tryCatch( + query(singleArticle.replace("[av]", "107"), "single article"), + ); + + const formattedMaterials = materialsDemand + .filter((n) => n.MaterialHumanReadableId === `${av?.data[0].article}`) + .map((i) => ({ + ...i, + OpeningInventory: i.OpeningInventory?.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }), + Purchases: i.Purchases?.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }), + Consumption: i.Consumption?.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }), + ClosingInventory: i.ClosingInventory?.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }), + })); + + // send the email stuff + const emailSetup = { + email: "blake.matthes@alpla.com", + subject: `Material Week.`, + template: "materialPerDay", + context: { + items: formattedMaterials, + article: av?.data[0].combined, + }, + }; + + const { data: sentEmail, error: sendEmailError } = await tryCatch( + sendEmail(emailSetup), + ); + if (sendEmailError) { + createLog( + "error", + "blocking", + "notify", + "Failed to send email, will try again on next interval", + ); + return { + success: false, + message: "Failed to send email, will try again on next interval", + }; + } + return { success: true, message: "material data", - data: buildInventoryTimeline( - sumByMaterialAndWeek(data.data) as any, - openingInventory, - ), + data: formattedMaterials, }; } diff --git a/lstV2/server/services/notifications/controller/materialsPerDay/materialPurchases.ts b/lstV2/server/services/notifications/controller/materialsPerDay/materialPurchases.ts index a635943..e3e12bd 100644 --- a/lstV2/server/services/notifications/controller/materialsPerDay/materialPurchases.ts +++ b/lstV2/server/services/notifications/controller/materialsPerDay/materialPurchases.ts @@ -1,4 +1,7 @@ import { formatISO, parseISO, startOfWeek } from "date-fns"; +import { tryCatch } from "../../../../globalUtils/tryCatch.js"; +import { query } from "../../../sqlServer/prodSqlServer.js"; +import { materialPurchasesPerDay } from "../../../sqlServer/querys/dataMart/materialPerDay.js"; function toDate(val: any) { if (val instanceof Date) return val; @@ -6,20 +9,52 @@ function toDate(val: any) { return new Date(val); } -export const materialPurchases = async (data: any) => { +export const materialPurchases = async ({ + startDate, + endDate, +}: { + startDate: string; + endDate: string; +}) => { /** @type {Record>} */ const grouped: any = {}; - for (const r of data) { + const { data: p, error } = (await tryCatch( + query( + materialPurchasesPerDay + .replace("[startDate]", startDate) + .replace("[endDate]", endDate), + "material check", + ), + )) as any; + + if (error) { + return { + success: false, + message: "Error getting the material data", + error, + }; + } + + if (!p.success) { + return { + success: false, + message: p.message, + data: [], + }; + } + + for (const r of p.data) { + const pOrder = String(r.purhcaseOrder); const mat = String(r.MaterialHumanReadableId); - const d = toDate(r.CalDate); + const d = toDate(r.deliveryDate); const week = formatISO(startOfWeek(d, { weekStartsOn: 1 }), { representation: "date", }); grouped[mat] ??= {}; grouped[mat][week] ??= 0; - grouped[mat][week] += Number(r.DailyMaterialDemand) || 0; + grouped[mat][week] += Number(r.qty) || 0; } const result = []; @@ -29,7 +64,7 @@ export const materialPurchases = async (data: any) => { result.push({ MaterialHumanReadableId: mat, WeekStart: week, - WeeklyDemand: Number(total).toFixed(2), + WeeklyPurchase: Number(total).toFixed(2), }); } } diff --git a/lstV2/server/services/notifications/controller/materialsPerDay/materialWithInv.ts b/lstV2/server/services/notifications/controller/materialsPerDay/materialWithInv.ts index 268b658..ec256d1 100644 --- a/lstV2/server/services/notifications/controller/materialsPerDay/materialWithInv.ts +++ b/lstV2/server/services/notifications/controller/materialsPerDay/materialWithInv.ts @@ -5,25 +5,51 @@ export const buildInventoryTimeline = ( WeeklyDemand: number; }>, opening: Record, + weeklyPurchases?: Array<{ + MaterialHumanReadableId: string; + WeekStart: string; + WeeklyPurchase: number; + }>, ) => { // group weekly demand by material - const grouped: Record< + const groupedDemand: Record< string, Array<{ WeekStart: string; Demand: number }> > = {}; - for (const d of weeklyDemand) { const mat = d.MaterialHumanReadableId; - grouped[mat] ??= []; - grouped[mat].push({ + groupedDemand[mat] ??= []; + groupedDemand[mat].push({ WeekStart: d.WeekStart, Demand: Number(d.WeeklyDemand), }); } - // sort weeks chronologically per material - for (const mat of Object.keys(grouped)) { - grouped[mat].sort( + // group weekly purchases by material + const groupedPurchases: Record< + string, + Array<{ WeekStart: string; Purchase: number }> + > = {}; + if (weeklyPurchases) { + for (const p of weeklyPurchases) { + const mat = p.MaterialHumanReadableId; + groupedPurchases[mat] ??= []; + groupedPurchases[mat].push({ + WeekStart: p.WeekStart, + Purchase: Number(p.WeeklyPurchase), + }); + } + } + + // sort both chronologically + for (const mat of Object.keys(groupedDemand)) { + groupedDemand[mat].sort( + (a, b) => + new Date(a.WeekStart).getTime() - new Date(b.WeekStart).getTime(), + ); + } + for (const mat of Object.keys(groupedPurchases)) { + groupedPurchases[mat].sort( (a, b) => new Date(a.WeekStart).getTime() - new Date(b.WeekStart).getTime(), ); @@ -33,25 +59,30 @@ export const buildInventoryTimeline = ( MaterialHumanReadableId: string; WeekStart: string; OpeningInventory: number; + Purchases: number; Consumption: number; ClosingInventory: number; }> = []; - for (const [mat, weeks] of Object.entries(grouped)) { - // get starting inventory from the ERP result + for (const [mat, weeks] of Object.entries(groupedDemand)) { let inv = opening[mat] ?? 0; + const purchasesForMaterial = groupedPurchases[mat] ?? []; - for (const w of weeks) { - const week = w.WeekStart; - const demand = Number(w.Demand); + for (const week of weeks) { + const demand = Number(week.Demand); + const purchase = + purchasesForMaterial.find((p) => p.WeekStart === week.WeekStart) + ?.Purchase ?? 0; const openingInv = inv; - const closingInv = openingInv - demand; + const adjustedInv = openingInv + purchase; + const closingInv = adjustedInv - demand; result.push({ MaterialHumanReadableId: mat, - WeekStart: week, + WeekStart: week.WeekStart, OpeningInventory: Number(openingInv.toFixed(2)), + Purchases: Number(purchase.toFixed(2)), Consumption: Number(demand.toFixed(2)), ClosingInventory: Number(closingInv.toFixed(2)), }); diff --git a/lstV2/server/services/notifications/controller/sendMail.ts b/lstV2/server/services/notifications/controller/sendMail.ts index 0041e42..3f3b49b 100644 --- a/lstV2/server/services/notifications/controller/sendMail.ts +++ b/lstV2/server/services/notifications/controller/sendMail.ts @@ -1,149 +1,152 @@ -import { tryCatch } from "../../../globalUtils/tryCatch.js"; -import { db } from "../../../../database/dbclient.js"; -import { settings } from "../../../../database/schema/settings.js"; -import nodemailer from "nodemailer"; +import Handlebars from "handlebars"; import type { Transporter } from "nodemailer"; -import type SMTPTransport from "nodemailer/lib/smtp-transport/index.js"; +import nodemailer from "nodemailer"; import type Mail from "nodemailer/lib/mailer/index.js"; import type { Address } from "nodemailer/lib/mailer/index.js"; +import type SMTPTransport from "nodemailer/lib/smtp-transport/index.js"; +import hbs from "nodemailer-express-handlebars"; import path from "path"; import { fileURLToPath } from "url"; -import hbs from "nodemailer-express-handlebars"; import { promisify } from "util"; -import { createLog } from "../../logger/logger.js"; +import { db } from "../../../../database/dbclient.js"; +import { settings } from "../../../../database/schema/settings.js"; +import { tryCatch } from "../../../globalUtils/tryCatch.js"; import { installed } from "../../../index.js"; +import { createLog } from "../../logger/logger.js"; interface HandlebarsMailOptions extends Mail.Options { - template: string; - context: Record; // Use a generic object for context + template: string; + context: Record; // Use a generic object for context } interface EmailData { - email: string; - subject: string; - template: string; - context: []; + email: string; + subject: string; + template: string; + context: []; } export const sendEmail = async (data: any): Promise => { - if (!installed) { - createLog("error", "notify", "notify", "server not installed."); - return; - } - let transporter: Transporter; - let fromEmail: string | Address; - const { data: settingData, error: settingError } = await tryCatch( - db.select().from(settings) - ); + if (!installed) { + createLog("error", "notify", "notify", "server not installed."); + return; + } + let transporter: Transporter; + let fromEmail: string | Address; + const { data: settingData, error: settingError } = await tryCatch( + db.select().from(settings), + ); - if (settingError) { - return { - success: false, - message: "There was an error getting the settings.", - settingError, - }; - } - // get the plantToken - const server = settingData.filter((n) => n.name === "server"); + if (settingError) { + return { + success: false, + message: "There was an error getting the settings.", + settingError, + }; + } + // get the plantToken + const server = settingData.filter((n) => n.name === "server"); - if ( - server[0].value === "localhost" && - process.env.EMAIL_USER && - process.env.EMAIL_PASSWORD - ) { - transporter = nodemailer.createTransport({ - service: "gmail", - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASSWORD, - }, - //debug: true, - }); + if ( + server[0].value === "localhostx" && + process.env.EMAIL_USER && + process.env.EMAIL_PASSWORD + ) { + transporter = nodemailer.createTransport({ + service: "gmail", + host: "smtp.gmail.com", + port: 465, + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASSWORD, + }, + //debug: true, + }); - // update the from email - fromEmail = process.env.EMAIL_USER; - } else { - // convert to the correct plant token. - const plantToken = settingData.filter((s) => s.name === "plantToken"); + // update the from email + fromEmail = process.env.EMAIL_USER; + } else { + // convert to the correct plant token. + const plantToken = settingData.filter((s) => s.name === "plantToken"); - let host = `${plantToken[0].value}-smtp.alpla.net`; + let host = `${plantToken[0].value}-smtp.alpla.net`; - const testServers = ["test1", "test2", "test3"]; + const testServers = ["test1", "test2", "test3"]; - if (testServers.includes(plantToken[0].value)) { - host = "USMCD1-smtp.alpla.net"; - } + if (testServers.includes(plantToken[0].value)) { + host = "USMCD1-smtp.alpla.net"; + } - if (plantToken[0].value === "usiow2") { - host = "USIOW1-smtp.alpla.net"; - } + if (plantToken[0].value === "usiow2") { + host = "USIOW1-smtp.alpla.net"; + } - transporter = nodemailer.createTransport({ - host: host, - port: 25, - rejectUnauthorized: false, - //secure: false, - // auth: { - // user: "alplaprod", - // pass: "obelix", - // }, - debug: true, - } as SMTPTransport.Options); + transporter = nodemailer.createTransport({ + host: host, + port: 25, + rejectUnauthorized: false, + //secure: false, + // auth: { + // user: "alplaprod", + // pass: "obelix", + // }, + debug: true, + } as SMTPTransport.Options); - // update the from email - fromEmail = `noreply@alpla.com`; - } + // update the from email + fromEmail = `noreply@alpla.com`; + } - // creating the handlbar options - const viewPath = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "../utils/views/" - ); + // creating the handlbar options + const viewPath = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../utils/views/", + ); - const handlebarOptions = { - viewEngine: { - extname: ".hbs", - //layoutsDir: path.resolve(viewPath, "layouts"), // Path to layouts directory - defaultLayout: "", // Specify the default layout - partialsDir: viewPath, - }, - viewPath: viewPath, - extName: ".hbs", // File extension for Handlebars templates - }; + const handlebarOptions = { + viewEngine: { + extname: ".hbs", + //layoutsDir: path.resolve(viewPath, "layouts"), // Path to layouts directory + defaultLayout: "", // Specify the default layout + partialsDir: viewPath, + }, + viewPath: viewPath, + extName: ".hbs", // File extension for Handlebars templates + }; - transporter.use("compile", hbs(handlebarOptions)); + transporter.use("compile", hbs(handlebarOptions)); - const mailOptions: HandlebarsMailOptions = { - from: fromEmail, - to: data.email, - subject: data.subject, - //text: "You will have a reset token here and only have 30min to click the link before it expires.", - //html: emailTemplate("BlakesTest", "This is an example with css"), - template: data.template, // Name of the Handlebars template (e.g., 'welcome.hbs') - context: data.context, - }; + const mailOptions: HandlebarsMailOptions = { + from: fromEmail, + to: data.email, + subject: data.subject, + //text: "You will have a reset token here and only have 30min to click the link before it expires.", + //html: emailTemplate("BlakesTest", "This is an example with css"), + template: data.template, // Name of the Handlebars template (e.g., 'welcome.hbs') + context: data.context, + }; - // now verify and send the email - const sendMailPromise = promisify(transporter.sendMail).bind(transporter); + // now verify and send the email + const sendMailPromise = promisify(transporter.sendMail).bind(transporter); - try { - // Send email and await the result - const info = await sendMailPromise(mailOptions); - createLog( - "info", - "notification", - "system", - `Email was sent to: ${data.email}` - ); - return { success: true, message: "Email sent.", data: info }; - } catch (err) { - console.log(err); - createLog( - "error", - "notification", - "system", - `Error sending Email: ${JSON.stringify(err)}` - ); - return { success: false, message: "Error sending email.", error: err }; - } + try { + // Send email and await the result + const info = await sendMailPromise(mailOptions); + createLog( + "info", + "notification", + "system", + `Email was sent to: ${data.email}`, + ); + return { success: true, message: "Email sent.", data: info }; + } catch (err) { + console.log(err); + createLog( + "error", + "notification", + "system", + `Error sending Email: ${JSON.stringify(err)}`, + ); + return { success: false, message: "Error sending email.", error: err }; + } }; diff --git a/lstV2/server/services/notifications/routes/manualTiggerMaterials.ts b/lstV2/server/services/notifications/routes/manualTiggerMaterials.ts new file mode 100644 index 0000000..dbc0810 --- /dev/null +++ b/lstV2/server/services/notifications/routes/manualTiggerMaterials.ts @@ -0,0 +1,28 @@ +// an external way to creating logs +import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; +import { apiHit } from "../../../globalUtils/apiHits.js"; +import { responses } from "../../../globalUtils/routeDefs/responses.js"; +import materialPerDayCheck from "../controller/materialsPerDay/materialPerDay.js"; +import runTiImport from "../controller/notifications/tiIntergration.js"; + +const app = new OpenAPIHono({ strict: false }); + +app.openapi( + createRoute({ + tags: ["notify"], + summary: "Manually trigger TI intergrations.", + method: "get", + path: "/materialPerDayCheck", + //middleware: authMiddleware, + responses: responses(), + }), + async (c) => { + apiHit(c, { endpoint: "/materialPerDayCheck" }); + const tiImport = await materialPerDayCheck(); + return c.json({ + success: tiImport?.success, + message: tiImport?.message, + }); + }, +); +export default app; diff --git a/lstV2/server/services/notifications/routes/manualTiggerTi.ts b/lstV2/server/services/notifications/routes/manualTiggerTi.ts index c0f0eb4..54149cc 100644 --- a/lstV2/server/services/notifications/routes/manualTiggerTi.ts +++ b/lstV2/server/services/notifications/routes/manualTiggerTi.ts @@ -1,28 +1,27 @@ // an external way to creating logs import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; -import { responses } from "../../../globalUtils/routeDefs/responses.js"; - -import runTiImport from "../controller/notifications/tiIntergration.js"; import { apiHit } from "../../../globalUtils/apiHits.js"; +import { responses } from "../../../globalUtils/routeDefs/responses.js"; +import runTiImport from "../controller/notifications/tiIntergration.js"; const app = new OpenAPIHono({ strict: false }); app.openapi( - createRoute({ - tags: ["notify"], - summary: "Manually trigger TI intergrations.", - method: "get", - path: "/tiTrigger", - //middleware: authMiddleware, - responses: responses(), - }), - async (c) => { - apiHit(c, { endpoint: "/tiTrigger" }); - const tiImport = await runTiImport(); - return c.json({ - success: tiImport?.success, - message: tiImport?.message, - }); - } + createRoute({ + tags: ["notify"], + summary: "Manually trigger TI intergrations.", + method: "get", + path: "/tiTrigger", + //middleware: authMiddleware, + responses: responses(), + }), + async (c) => { + apiHit(c, { endpoint: "/tiTrigger" }); + const tiImport = await runTiImport(); + return c.json({ + success: tiImport?.success, + message: tiImport?.message, + }); + }, ); export default app; diff --git a/lstV2/server/services/notifications/utils/views/materialPerDay.hbs b/lstV2/server/services/notifications/utils/views/materialPerDay.hbs new file mode 100644 index 0000000..e2b0bc5 --- /dev/null +++ b/lstV2/server/services/notifications/utils/views/materialPerDay.hbs @@ -0,0 +1,44 @@ + + + + + + {{!-- --}} + {{> styles}} + + +

All,

+

Below is the weekly material demand for Article: {{article}}.

+ + + + + + + + + + + + + + {{#each items}} + + + + + + + + + {{/each}} + +
AVWeekOpening InventoryPurchasesConsumptionClosing Inventory
{{MaterialHumanReadableId}}{{WeekStart}}{{OpeningInventory}}{{Purchases}}{{Consumption}}{{ClosingInventory}}
+ +
+

Thank you,

+

LST Team

+
+ + + \ No newline at end of file diff --git a/lstV2/server/services/sqlServer/querys/dataMart/materialPerDay.ts b/lstV2/server/services/sqlServer/querys/dataMart/materialPerDay.ts index 9b84397..668a6d9 100644 --- a/lstV2/server/services/sqlServer/querys/dataMart/materialPerDay.ts +++ b/lstV2/server/services/sqlServer/querys/dataMart/materialPerDay.ts @@ -3,7 +3,7 @@ * startdate and end date should be passed over */ export const materialPerDay = ` -use [test3_AlplaPROD2.0_Read] +use [test1_AlplaPROD2.0_Read] DECLARE @ShiftStartHour INT = 6 declare @startDate nvarchar(max) = '[startDate]' @@ -121,7 +121,7 @@ declare @endDate nvarchar(max) = '[endDate]' SELECT [IdBestellung] as purhcaseOrder - ,[IdArtikelVarianten] + ,[IdArtikelVarianten] as MaterialHumanReadableId ,convert(date, [BestellDatum], 120) as orderDate ,convert(date, [Lieferdatum], 120) as deliveryDate ,[BestellMenge] as qty diff --git a/lstV2/server/services/sqlServer/querys/misc/singleArticle.ts b/lstV2/server/services/sqlServer/querys/misc/singleArticle.ts new file mode 100644 index 0000000..df428aa --- /dev/null +++ b/lstV2/server/services/sqlServer/querys/misc/singleArticle.ts @@ -0,0 +1,10 @@ +export const singleArticle = ` +use AlplaPROD_test1 + +select +idartikelvarianten as article, +CONCAT(idartikelvarianten , ' - ' , Alias) as combined + +from V_Artikel (nolock) +where idartikelvarianten = '[av]' +`;