diff --git a/app/src/pkg/prodSql/querys/scheduler/scheduler.ts b/app/src/pkg/prodSql/querys/scheduler/scheduler.ts new file mode 100644 index 0000000..59dd269 --- /dev/null +++ b/app/src/pkg/prodSql/querys/scheduler/scheduler.ts @@ -0,0 +1,109 @@ +export const scheduler = ` +use AlplaPROD_test1 +/* +This query will combine both the incoming goods and the deliveries in 1 as it will be the query that populates into lst db then later updated +*/ +DECLARE @checkDateString NVARCHAR(MAX) = '[dateCheck]' --'2025-10-14 08:00'; -- will be passed over to validate new updates. +DECLARE @checkDate DATETIME2 = CONVERT(datetime2, @checkDateString); + +-- orders +select * from (select +type = 'orders' +,l.ArticleHumanReadableId as av +,l.ArticleDescription as description +,[ReleaseNumber] as orderNumber +,h.CustomerOrderNumber as header +,l.CustomerLineItemNumber as lineItemNumber +,CustomerReleaseNumber as customerReleaseNumber +,FORMAT(r.[Deliverydate], 'yyyy-MM-dd HH:mm') as deliveryDate +,FORMAT([LoadingDate], 'yyyy-MM-dd HH:mm') as loadingDate +,[Quantity] as orderQTY +,[LoadingUnits] as orderLu +,case when t.GelieferteMenge is null then 0 else t.GelieferteMenge end as deliveredQTY +,case when t.GelieferteMengeVPK is null then 0 else t.GelieferteMengeVPK end as deliveredLu +,r.Remark as remark +,h.CreatedByEdi as createdAsEDI -- if 1 then we run the new function to change this in true edi as well as lstdb. +,r.ReleaseState as currentState -- anything other than 0 should lock this and not allow for changes +--,lstDate = getdate() --'this is a place holder for date see edi comment' +--,dock = 'dock3' --another place holder for what dock we will go to' +--,orderType = 'maunal or prod' -- this is for if we manually add in the order from lst. this way the dates will be ignored if this is manual. +,h.CustomerHumanReadableId as addressId +,h.CustomerDescription as customer +,orderType = 20 +,r.[Add_User] +,r.[Add_Date] +,r.[Upd_User] +,r.[Upd_Date] + FROM [test1_AlplaPROD2.0_Reporting].[reporting_order].[Release] as r + +left join +[test1_AlplaPROD2.0_Reporting].[reporting_order].LineItem as l on +l.id = r.LineItemId + +left join +[test1_AlplaPROD2.0_Read].[order].Header as h on +h.Id = l.HeaderId + +left join +dbo.V_TrackerAuftragsAbrufe (nolock) as t on +t.IdAuftragsAbruf = r.ReleaseNumber + +--where r.[Upd_Date] >= getdate() -@age +where CAST(r.[Upd_Date] AS datetime2) >= @checkDate + + +union all +-- incoming goods +select +type = 'incoming' +,inc.IdArtikelVarianten as av +,av.Bezeichnung as description +,IdBestellung as orderNumber +,case when inc.Bemerkung <> '' then case when CHARINDEX(',',inc.Bemerkung) = 0 then inc.Bemerkung else left(inc.Bemerkung, CHARINDEX(',',inc.Bemerkung) -1) end else 'Missing PO' end as header +,case when inc.Bemerkung <> '' then case when CHARINDEX(',',inc.Bemerkung) = 0 then inc.Bemerkung else left(inc.Bemerkung, CHARINDEX(',',inc.Bemerkung) -1) end else 'Missing PO' end as lineItemNumber +,case when inc.Bemerkung <> '' then case when CHARINDEX(',',inc.Bemerkung) = 0 then inc.Bemerkung else left(inc.Bemerkung, CHARINDEX(',',inc.Bemerkung) -1) end else 'Missing PO' end as customerReleaseNumber +,FORMAT(inc.Datum, 'yyyy-MM-dd HH:mm') as deliveryDate +,FORMAT(inc.Datum, 'yyyy-MM-dd HH:mm') as loadingDate +,sollMenge as orderQty +,sollmengevpk as orderLu +,case when l.EntladeMenge is null then 0 else l.EntladeMenge end as deliveredQTY +,case when l.EntladeMengeVPK is null then 0 else l.EntladeMengeVPK end as deliveredLu +,inc.Bemerkung as remark +,createdAsEDI = 0 +,inc.Status as currentState -- anything other than 0 should lock this and not allow for changes +--,lstDate = getdate() --'this is a place holder for date see edi comment' +--,dock = 'dock3' --another place holder for what dock we will go to' +--,orderType = 'maunal or prod' -- this is for if we manually add in the order from lst. this way the dates will be ignored if this is manual. +,inc.IdAdresse as addressId +,a.Bezeichnung +,inc.Typ as orderType -- this is just the id +,inc.Add_User +,inc.Add_Date +,inc.Upd_User +,inc.Upd_Date +from T_Wareneingaenge (nolock) as inc + +--article stuff +left join +T_Artikelvarianten (nolock) as av on +av.IdArtikelvarianten = inc.IdArtikelVarianten + +left join +T_WareneingangAuftraege (nolock) as w on +w.Beleg = inc.Beleg + +left join +T_WareneingangPlanungen (nolock) as l on +l.IdWareneingangAuftrag = w.IdWareneingangAuftrag + +left join +T_adressen as a on +a.idadressen = inc.IdAdresse + +--where inc.Upd_Date >= getdate() -@age +where inc.Upd_Date >= @checkDate +--and inc.Typ not in (40) +)a + +order by upd_date desc +`; diff --git a/app/src/pkg/utils/corsController.ts b/app/src/pkg/utils/corsController.ts new file mode 100644 index 0000000..45703bf --- /dev/null +++ b/app/src/pkg/utils/corsController.ts @@ -0,0 +1,15 @@ +import { validateEnv } from "./envValidator.js"; + +const env = validateEnv(process.env); +// export const allowedOrigins = [ +// /^https?:\/\/localhost:(5173|5500|4200|3000|4000)$/, // all the allowed backend ports +// /^http?:\/\/localhost:(5173|5500|4200|3000|4000)$/, +// /^https?:\/\/.*\.alpla\.net$/, +// env.BETTER_AUTH_URL, // prod +// ]; + +export const allowedOrigins: (string | RegExp)[] = [ + /^https?:\/\/localhost:(5173|5500|4200|3000|4000)$/, // all local dev ports + /^https?:\/\/.*\.alpla\.net$/, // any subdomain of alpla.net + env.BETTER_AUTH_URL, // production URL +]; diff --git a/app/src/ws/channels/labelLogs.ts b/app/src/ws/channels/labelLogs.ts new file mode 100644 index 0000000..e69de29 diff --git a/app/src/ws/channels/logs.ts b/app/src/ws/channels/logs.ts new file mode 100644 index 0000000..b8c4733 --- /dev/null +++ b/app/src/ws/channels/logs.ts @@ -0,0 +1,43 @@ +import { Namespace, Socket } from "socket.io"; +import { requireAuth } from "../../pkg/middleware/authMiddleware.js"; + +export const setupAllLogs = (io: Namespace) => { + // Wrap middleware for Socket.IO + io.use(async (socket: Socket, next) => { + try { + // Mock Express req/res object + const req = socket.request as any; + + // Create a mini next function that throws an error if unauthorized + await new Promise((resolve, reject) => { + const fakeRes: any = { + status: () => ({ + json: (obj: any) => reject(new Error(obj.error)), + }), + }; + const nextFn = (err?: any) => (err ? reject(err) : resolve()); + + // Call your middleware + requireAuth("", ["systemAdmin", "admin"])(req, fakeRes, nextFn); + }); + + // Auth passed + next(); + } catch (err: any) { + next(new Error(err.message || "Unauthorized")); + } + }); + + io.on("connection", (socket: Socket) => { + console.log( + "✅ Authenticated client connected to channel2:", + socket.id + ); + + socket.on("ping", () => socket.emit("pong")); + + socket.on("disconnect", () => { + console.log("🔴 Client disconnected from channel2:", socket.id); + }); + }); +}; diff --git a/app/src/ws/channels/scheduler.ts b/app/src/ws/channels/scheduler.ts new file mode 100644 index 0000000..6732302 --- /dev/null +++ b/app/src/ws/channels/scheduler.ts @@ -0,0 +1,59 @@ +import { formatDate } from "date-fns"; +import type { Server, Socket } from "socket.io"; +import { db } from "../../pkg/db/db.js"; +import { orderScheduler } from "../../pkg/db/schema/orderScheduler.js"; +import { tryCatch } from "../../pkg/utils/tryCatch.js"; + +let ioInstance: Server | null = null; + +export const setupScheduler = (io: Server, socket: Socket) => { + ioInstance = io; + + socket.on("joinScheduler", async () => { + socket.join("scheduler"); + //console.log(`Socket ${socket.id} joined room "scheduler"`); + + const { data, error } = await tryCatch(db.select().from(orderScheduler)); + + if (error) { + setTimeout(() => { + io.to("scheduler").emit("scheduler:update", { + type: "init", + error: error, + }); + }, 1000); + return; + } + + // initial push + setTimeout(() => { + io.to("scheduler").emit("scheduler:update", { type: "init", data: data }); + }, 1000); + }); + + // socket.on("ping", () => { + // io.to("scheduler").emit("pong", { from: socket.id }); + // }); + + // setInterval(() => { + // io.to("scheduler").emit("heartbeat", { + // ts: formatDate(Date.now(), "M/d/yyyy HH:mm"), + // }); + // }, 60_000); + + // add in listen for changes to the db with pg listen? +}; + +// --- Change Broadcast Function --- +export const schedulerChange = async (data: any) => { + if (!ioInstance) { + console.error("⚠️ schedulerChange called before setupScheduler initialized"); + return; + } + + ioInstance.to("scheduler").emit("scheduler:update", { + ts: formatDate(Date.now(), "M/d/yyyy HH:mm"), + data, + }); + console.log("📢 Scheduler update broadcasted:", data); +}; diff --git a/app/src/ws/server.ts b/app/src/ws/server.ts new file mode 100644 index 0000000..7ee4318 --- /dev/null +++ b/app/src/ws/server.ts @@ -0,0 +1,66 @@ +import { Server } from "socket.io"; +import type { Server as HttpServer } from "http"; +import { allowedOrigins } from "../pkg/utils/corsController.js"; +import { setupScheduler } from "./channels/scheduler.js"; + +export const setupIoServer = async (server: HttpServer, basePath: string) => { + const io = new Server(server, { + path: `${basePath}/api/ws`, + cors: { + // origin: ["http://localhost:5500"], + origin: (origin, callback) => { + // Allow non-browser clients (Postman, direct websocket, etc.) + if (!origin) return callback(null, true); + + try { + // Check if origin matches any allowed string or regex + const allowed = allowedOrigins.some((o) => { + if (typeof o === "string") return o === origin; + if (o instanceof RegExp) return o.test(origin); + return false; + }); + + if (allowed) { + return callback(null, true); + } + } catch (err) { + console.error("Invalid Origin header:", origin); + } + + // Deny all others + return callback(new Error("Not allowed by CORS: " + origin)); + }, + // methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + credentials: true, + }, + pingInterval: 25_000, // send ping every 25s (default 25s) + pingTimeout: 60_000, // wait 60s before disconnecting (default 60s) + }); + + io.on("connection", (socket) => { + console.log("🟢 User connected:", socket.id); + + // Join a room + socket.on("joinRoom", (room: string) => { + socket.join(room); + console.log(`Socket ${socket.id} joined room ${room}`); + }); + + // Leave a room + socket.on("leaveRoom", (room: string) => { + socket.leave(room); + console.log(`Socket ${socket.id} left room ${room}`); + }); + + // Example: broadcast a message to a room + socket.on("message", ({ room, payload }) => { + io.to(room).emit("message", payload); + }); + + socket.on("disconnect", (reason) => { + console.log("🔴 User disconnected:", socket.id, reason); + }); + + setupScheduler(io, socket); + }); +};