64 Commits

Author SHA1 Message Date
3a24d62957 refactor(api docks): added api docks back into the front end and prep for docusorus
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m15s
2026-06-16 18:53:44 -05:00
6a14bab30c refactor(psi): rebuilt the delivery and planning data to 2.0 data 2026-06-16 18:52:58 -05:00
2ebf695526 fix(builds): fixed non used variables
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m32s
2026-06-15 17:16:29 -05:00
24af3ca403 fix(datamart): somereason it stopped working.. and added download types
there was a weird issue with the req.query that cause nothing to pull and make the excel files
lag..... now excel macros are using the csv pull from here and added in the xlsx to download to bc
why not makes it easier for later  and can have bbuttons for every thing in lst too :D
2026-06-15 17:16:00 -05:00
6fbe3a9eed chore(format): formatting changes 2026-06-15 17:14:05 -05:00
7dbc31c046 refactor(dm): mapped remainder of the forecast. will need to run backup test
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 40s
the test server wouldnt do the backup so waiting on this one
2026-06-15 01:03:50 -05:00
ba09a77f29 chore(logistics): renamed the history util to better represent what it is 2026-06-15 01:03:02 -05:00
3f04609f82 refactor(dockdoorscanning): only show a truly active loading order 0 should never show up
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m29s
2026-06-14 03:54:21 -05:00
22a7b612e1 fix(dockdoorscanner): corrections to how things get posted so it makes more sense 2026-06-14 03:53:34 -05:00
39db142db4 feat(dm): standard forecast added in rest of migration to come 2026-06-14 03:52:34 -05:00
d85f08cb19 chore(release): 0.1.0-alpha.3
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m17s
Release and Build Image / release (push) Successful in 9s
2026-06-10 16:31:04 -05:00
15c939ebe8 refactor(server builds): added in proper logging will still need fine tuned though
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m52s
2026-06-10 16:29:23 -05:00
9ff428f5ea refactor(logs): socket io setup to be properly logging 2026-06-10 16:28:52 -05:00
2e460c7f8a refactor(warehouse): fixed ppoo check to match the new changes 2026-06-10 16:27:52 -05:00
7c4c5f980a refactor(opendock): changed the subModule for better logging 2026-06-10 16:27:23 -05:00
86e1237509 refactor(dock door scanning): fixes and final writes for the intial trial went smooth 2026-06-10 16:26:58 -05:00
706ab8b448 refactor(db): added in notifications vs pulling from the db makes it easier on the system 2026-06-10 16:26:21 -05:00
9440b44f3b refactor(socket.io): complete rewrite to manage dynamic rooms and seeding better 2026-06-10 16:25:48 -05:00
a2d9a6c127 refactor(opendock): changed the article to look at the label to match all plants
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m26s
2026-06-09 21:23:49 -05:00
c0a7d4a125 refactor(opendock): added some new goodies to the app to help manage releases
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m24s
2026-06-09 21:09:03 -05:00
8fcb2c66ed refactor(socketio): changes that if we are in dock room to constant reseed the room
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m49s
this will be fixed later when we redo the socket io rooms with dynamic stuff
2026-06-09 12:07:53 -05:00
8fc3129f7d refactor(opend dock): added in default dock so it uses the default setup as a backup
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m40s
This will help the live/drop and dont know what dock
2026-06-09 12:00:50 -05:00
05c553e927 fix(eom): removed un needed imports 2026-06-09 11:59:48 -05:00
6eaae0f537 refactor(dock scanner): more work on dock scanner and semi finished 2026-06-09 11:59:27 -05:00
3ad84dab71 refactor(opendock): added in proper complete and ignore of picksheets 2026-06-09 11:58:25 -05:00
5a7046bacd refactor(opendock): if load type is drop we will not do anything unless its already scanned
this means that if we drag down a bunch of loads we wont instantly change to arrived we will need to
have a single scan before we do this
2026-06-09 11:46:22 -05:00
cfc497c1f2 refactor(opendock): added in check for really using the article link 2026-06-09 11:45:09 -05:00
7bbdd4e555 refactor(mobile): new error found and added in
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 1m24s
2026-06-09 07:01:02 -05:00
e909e8deec feat(eom): migrated eom endpoints from old version validated working 2026-06-09 07:00:27 -05:00
4249e90307 fix(dockscanner): removed console log 2026-06-08 15:29:09 -05:00
2f495739e6 fix(dock door scanning): correction to how the data is posted 2026-06-08 15:26:43 -05:00
7d722c4aac refactor(datamart): if added to query will include plant token now 2026-06-08 15:24:45 -05:00
5f2148f5f0 refactor(servers): all remaining servers added 2026-06-08 15:23:25 -05:00
7671172d97 refactor(dockscanning): more work on the dock door scanning
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m47s
ref #12
2026-06-08 07:49:41 -05:00
6dd1a5b332 fix(scanner): corrected teh endpoint to delete the user if needed 2026-06-04 07:17:18 -05:00
865f280cfa fix(opendock): changed the header of delete to be properly named
ref #23
2026-06-04 07:16:53 -05:00
a717260b8d fix(opendock): correction to article link success on delete
ref #23
2026-06-04 07:02:27 -05:00
4464cea022 feat(opendock): added delete button in the article tab
ref #23
2026-06-04 07:01:10 -05:00
55418e2f09 refactor(new role): added in warehouse role 2026-06-04 06:49:49 -05:00
5281118596 fix(notifications): missed api converstion in the front end for updating 2026-06-03 08:40:15 -05:00
f635415b75 fix(opendock): ref wrong field oops
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m0s
2026-06-02 06:25:34 -05:00
40edbc3eae refactor(times): added a better view for times as we save all db as there respective timezone
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m50s
2026-06-01 16:57:38 -05:00
2558b2e5bb refactor(db): added timezone check in so it comes over correct based on the backend timezone 2026-06-01 16:05:36 -05:00
45a0dee9ca refactor(logs): refactored to show the submodule and stack as well to make it more easy to watch 2026-06-01 15:21:17 -05:00
bb7931d6c8 feat(opendock): added in a new article link system
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 4m11s
2026-06-01 14:26:51 -05:00
4f848bb649 fix(logistics): historical checks for no data errors when feature is activeed 2026-06-01 14:24:12 -05:00
f8335f5217 refactor(backend): dock door scanning socket and perms 2026-06-01 14:23:26 -05:00
2a35381fe4 refactor(mobile): intial addin of dockdoor scanning on mobile 2026-06-01 14:22:40 -05:00
e6b92aeb10 test(test): added in vitest to start doing more testing before deploying 2026-06-01 14:21:19 -05:00
da87e2e1d3 refactor(logger): included error in the stack version so we dont have to remove it all 2026-06-01 14:20:48 -05:00
3ef0f230dd fix(mobile): temp removed the autodownload as its causing issues 2026-05-29 06:16:52 -05:00
78f7b8a179 docs(app): updated last updated to the readme to show current progress
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m58s
2026-05-27 21:19:57 -05:00
1a3d8a7ddc docs(app): updated readme 2026-05-27 21:18:30 -05:00
2a648f6306 fix(app): type in the templates... they all looked the same
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m41s
2026-05-27 21:08:02 -05:00
69a9a81a88 ci(app): added more labels to the templates
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-05-27 21:05:57 -05:00
188fdd0eb7 ci(app): changed the issues templates to be easier to add to the part of the app needed
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m37s
2026-05-27 21:02:18 -05:00
db28635c8c fix(mobile): ui over lapping
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m1s
the ui elements would over lap and cause visual issues with the scanning and seeing the old labels

closes #25
2026-05-27 20:57:49 -05:00
bcdf9566bc refactor(mobile): moved logout to the tab bar 2026-05-27 20:56:35 -05:00
c15ee070e7 refactor(mobile): setup - added button to go home as it caused confustion 2026-05-27 20:56:08 -05:00
347edb7078 fix(mobile users): corrected and endpoint that prevented us from change the pin number 2026-05-27 20:55:25 -05:00
fe0b1573f3 feat(mobile): dock door scanning backend added
ref #12
2026-05-27 20:54:47 -05:00
9c0ef1f5df fix(mobile): scan log incorrect user ref 2026-05-27 20:53:07 -05:00
8b076949a7 feat(warehousing): ppoo monitoring added
this will monitor ppoo every 45 seconds as long as someone is on the page.

closes #13
2026-05-27 20:52:34 -05:00
6d0fb8aee4 feat(mobile): added auto download of latest
this will predownload the latest if its there, this will speed up the update as the user will only
need to scan a single command and it will install restart app
2026-05-27 20:50:07 -05:00
195 changed files with 33359 additions and 1233 deletions

View File

@@ -0,0 +1,66 @@
---
name: Bug Report - Frontend
about: Report something that is broken or not working correctly
title: "[BUG - Frontend] "
ref: "main"
labels:
- bug
- frontend
---
# Summary
Briefly explain the issue.
---
# Steps To Reproduce
1. Go to ...
2. Click ...
3. Scan ...
4. Error occurs ...
---
# Expected Behavior
What should have happened?
---
# Actual Behavior
What actually happened instead?
---
# Severity
- [ ] Low
- [ ] Medium
- [ ] High
- [ ] Critical
---
# Environment
Example:
- Production
- Development
- Zebra Scanner
- Mobile Device
- Windows Server
- Docker
---
# Logs / Screenshots
Paste logs or upload screenshots here.
```txt
Paste logs here

View File

@@ -0,0 +1,66 @@
---
name: Bug Report - Mobile
about: Report something that is broken or not working correctly
title: "[BUG - Mobile] "
ref: "main"
labels:
- bug
- mobile
---
# Summary
Briefly explain the issue.
---
# Steps To Reproduce
1. Go to ...
2. Click ...
3. Scan ...
4. Error occurs ...
---
# Expected Behavior
What should have happened?
---
# Actual Behavior
What actually happened instead?
---
# Severity
- [ ] Low
- [ ] Medium
- [ ] High
- [ ] Critical
---
# Environment
Example:
- Production
- Development
- Zebra Scanner
- Mobile Device
- Windows Server
- Docker
---
# Logs / Screenshots
Paste logs or upload screenshots here.
```txt
Paste logs here

View File

@@ -1,11 +1,12 @@
---
name: Bug Report
name: Bug Report - Server
about: Report something that is broken or not working correctly
title: "[BUG] "
title: "[BUG - Server] "
ref: "main"
labels:
- bug
- server
---

View File

@@ -0,0 +1,48 @@
---
name: Enhancement - Frontend
about: Improve or refine an existing feature
title: "[ENHANCEMENT - Frontend] "
ref: "main"
labels:
- enhancement
- frontend
---
# Existing Feature
What current feature or workflow is being improved?
Example:
- Notifications
- Scanner Login
- Release Monitor
- Printing
- Auth
---
# Proposed Improvement
Describe the improvement.
---
# Expected Benefit
Why would this improvement help?
---
# Impact
- [ ] Small
- [ ] Medium
- [ ] Large
---
# Additional Notes
Anything else worth mentioning.

View File

@@ -1,11 +1,12 @@
---
name: Enhancement
name: Enhancement - Mobile
about: Improve or refine an existing feature
title: "[ENHANCEMENT] "
title: "[ENHANCEMENT - Mobile] "
ref: "main"
labels:
- enhancement
- mobile
---
# Existing Feature

View File

@@ -0,0 +1,48 @@
---
name: Enhancement - Server
about: Improve or refine an existing feature
title: "[ENHANCEMENT - Server] "
ref: "main"
labels:
- enhancement
- server
---
# Existing Feature
What current feature or workflow is being improved?
Example:
- Notifications
- Scanner Login
- Release Monitor
- Printing
- Auth
---
# Proposed Improvement
Describe the improvement.
---
# Expected Benefit
Why would this improvement help?
---
# Impact
- [ ] Small
- [ ] Medium
- [ ] Large
---
# Additional Notes
Anything else worth mentioning.

View File

@@ -0,0 +1,41 @@
---
name: Feature Request - Frontend
about: Suggest a brand new feature or module
title: "[FEATURE - Frontend] "
ref: "main"
labels:
- feature
- frontend
---
# Problem Statement
What problem are you trying to solve?
---
# Proposed Solution
Describe the feature you would like added.
---
# Alternatives Considered
Any other ideas, workarounds, or approaches?
---
# Priority
- [ ] Nice to Have
- [ ] Medium Priority
- [ ] High Priority
- [ ] Critical
---
# Additional Context
Add mockups, screenshots, examples, or notes here.

View File

@@ -1,11 +1,12 @@
---
name: Feature Request
name: Feature Request - Mobile
about: Suggest a brand new feature or module
title: "[FEATURE] "
title: "[FEATURE - Mobile] "
ref: "main"
labels:
- feature
- mobile
---
# Problem Statement

View File

@@ -0,0 +1,41 @@
---
name: Feature Request - Server
about: Suggest a brand new feature or module
title: "[FEATURE - Server] "
ref: "main"
labels:
- feature
- server
---
# Problem Statement
What problem are you trying to solve?
---
# Proposed Solution
Describe the feature you would like added.
---
# Alternatives Considered
Any other ideas, workarounds, or approaches?
---
# Priority
- [ ] Nice to Have
- [ ] Medium Priority
- [ ] High Priority
- [ ] Critical
---
# Additional Context
Add mockups, screenshots, examples, or notes here.

View File

@@ -1,5 +1,84 @@
# All Changes to LST can be found below.
## [0.1.0-alpha.3](https://git.tuffraid.net/cowch/lst_v3/compare/v0.1.0-alpha.2...v0.1.0-alpha.3) (2026-06-10)
### 🌟 Enhancements
* **eom:** migrated eom endpoints from old version validated working ([e909e8d](https://git.tuffraid.net/cowch/lst_v3/commits/e909e8deecb54a3e4c39789609b0aa7435b9e08a))
* **mobile:** added auto download of latest ([6d0fb8a](https://git.tuffraid.net/cowch/lst_v3/commits/6d0fb8aee45c8b5c56ccd7d8a010e1dc803408bf))
* **mobile:** dock door scanning backend added ([fe0b157](https://git.tuffraid.net/cowch/lst_v3/commits/fe0b1573f3ba6fd220f181088b994588c52af139)), closes [#12](https://git.tuffraid.net/cowch/lst_v3/issues/12)
* **opendock:** added delete button in the article tab ([4464cea](https://git.tuffraid.net/cowch/lst_v3/commits/4464cea022ba48744b884b83fef0fc3f3421dea5)), closes [#23](https://git.tuffraid.net/cowch/lst_v3/issues/23)
* **opendock:** added in a new article link system ([bb7931d](https://git.tuffraid.net/cowch/lst_v3/commits/bb7931d6c8d0f5ce4065955491e9ee1247b5e92d))
* **warehousing:** ppoo monitoring added ([8b07694](https://git.tuffraid.net/cowch/lst_v3/commits/8b076949a7f8e723bc87619f729082d2c1991b2d)), closes [#13](https://git.tuffraid.net/cowch/lst_v3/issues/13)
### 🐛 Bug fixes
* **app:** type in the templates... they all looked the same ([2a648f6](https://git.tuffraid.net/cowch/lst_v3/commits/2a648f63064a300b0a2888bae3322b857af6a238))
* **dock door scanning:** correction to how the data is posted ([2f49573](https://git.tuffraid.net/cowch/lst_v3/commits/2f495739e653c462eff7f10ff6343d9572f25563))
* **dockscanner:** removed console log ([4249e90](https://git.tuffraid.net/cowch/lst_v3/commits/4249e90307ba1a1992753803e1dc3ab7dd7ac95e))
* **eom:** removed un needed imports ([05c553e](https://git.tuffraid.net/cowch/lst_v3/commits/05c553e9279c6e8384d61073781bf915733b0ab5))
* **logistics:** historical checks for no data errors when feature is activeed ([4f848bb](https://git.tuffraid.net/cowch/lst_v3/commits/4f848bb649f350c9d370daa09c6fc48f7b76e2e2))
* **mobile users:** corrected and endpoint that prevented us from change the pin number ([347edb7](https://git.tuffraid.net/cowch/lst_v3/commits/347edb7078fb4ce959975cd968a6f026bacc98bf))
* **mobile:** scan log incorrect user ref ([9c0ef1f](https://git.tuffraid.net/cowch/lst_v3/commits/9c0ef1f5dfa13e8f7e1f72d47d0d5842b3da3c87))
* **mobile:** temp removed the autodownload as its causing issues ([3ef0f23](https://git.tuffraid.net/cowch/lst_v3/commits/3ef0f230ddbaef4c3d738cc531e7afee25b210dd))
* **mobile:** ui over lapping ([db28635](https://git.tuffraid.net/cowch/lst_v3/commits/db28635c8c260d0f378e109755d32201acdb2328)), closes [#25](https://git.tuffraid.net/cowch/lst_v3/issues/25)
* **notifications:** missed api converstion in the front end for updating ([5281118](https://git.tuffraid.net/cowch/lst_v3/commits/52811185965cb0fe4c9b42e73c447b67e917a5ca))
* **opendock:** changed the header of delete to be properly named ([865f280](https://git.tuffraid.net/cowch/lst_v3/commits/865f280cfaf82a7126ca4b57d658aed50cf19a73)), closes [#23](https://git.tuffraid.net/cowch/lst_v3/issues/23)
* **opendock:** correction to article link success on delete ([a717260](https://git.tuffraid.net/cowch/lst_v3/commits/a717260b8d5cf72534b055721de2b79901506875)), closes [#23](https://git.tuffraid.net/cowch/lst_v3/issues/23)
* **opendock:** ref wrong field oops ([f635415](https://git.tuffraid.net/cowch/lst_v3/commits/f635415b751e11d0e7beb557d18b83915155428a))
* **scanner:** corrected teh endpoint to delete the user if needed ([6dd1a5b](https://git.tuffraid.net/cowch/lst_v3/commits/6dd1a5b332a853f0e2c7226ac1f64e403ef752d7))
### 📚 Documentation
* **app:** updated last updated to the readme to show current progress ([78f7b8a](https://git.tuffraid.net/cowch/lst_v3/commits/78f7b8a179d078fc1b2740fd2bf4af71a3df8292))
* **app:** updated readme ([1a3d8a7](https://git.tuffraid.net/cowch/lst_v3/commits/1a3d8a7ddcb7a2e51da4b77968ffdefb7644a0b1))
### 🛠️ Code Refactor
* **backend:** dock door scanning socket and perms ([f8335f5](https://git.tuffraid.net/cowch/lst_v3/commits/f8335f5217c339a7dc09883fa8ba9c60aa4c35b1))
* **datamart:** if added to query will include plant token now ([7d722c4](https://git.tuffraid.net/cowch/lst_v3/commits/7d722c4aac16bc1d2cfbfbc70539783f34003f77))
* **db:** added in notifications vs pulling from the db makes it easier on the system ([706ab8b](https://git.tuffraid.net/cowch/lst_v3/commits/706ab8b448aafb81da94a76fbf8d3d400cba616b))
* **db:** added timezone check in so it comes over correct based on the backend timezone ([2558b2e](https://git.tuffraid.net/cowch/lst_v3/commits/2558b2e5bb68be7a3f46de09bb509c99423adeb6))
* **dock door scanning:** fixes and final writes for the intial trial went smooth ([86e1237](https://git.tuffraid.net/cowch/lst_v3/commits/86e1237509b81722dee7b42762d0bfced8d26fa3))
* **dock scanner:** more work on dock scanner and semi finished ([6eaae0f](https://git.tuffraid.net/cowch/lst_v3/commits/6eaae0f5378e39c4002dadd8325833698dd960e7))
* **dockscanning:** more work on the dock door scanning ([7671172](https://git.tuffraid.net/cowch/lst_v3/commits/7671172d975355b5d245a482b481726b49153578)), closes [#12](https://git.tuffraid.net/cowch/lst_v3/issues/12)
* **logger:** included error in the stack version so we dont have to remove it all ([da87e2e](https://git.tuffraid.net/cowch/lst_v3/commits/da87e2e1d322a45ca9a7b500d77499fe4c7b999e))
* **logs:** refactored to show the submodule and stack as well to make it more easy to watch ([45a0dee](https://git.tuffraid.net/cowch/lst_v3/commits/45a0dee9caae14df639439b905fa81b340791197))
* **logs:** socket io setup to be properly logging ([9ff428f](https://git.tuffraid.net/cowch/lst_v3/commits/9ff428f5ea1051e62521632947092f74eb93944d))
* **mobile:** intial addin of dockdoor scanning on mobile ([2a35381](https://git.tuffraid.net/cowch/lst_v3/commits/2a35381fe400fc46e6f10c4e72c3ab9ac435e0e5))
* **mobile:** moved logout to the tab bar ([bcdf956](https://git.tuffraid.net/cowch/lst_v3/commits/bcdf9566bcf721a085f46ce3befe783eb7b91949))
* **mobile:** new error found and added in ([7bbdd4e](https://git.tuffraid.net/cowch/lst_v3/commits/7bbdd4e5552889434aff81ed10ea7c64174f7d34))
* **mobile:** setup - added button to go home as it caused confustion ([c15ee07](https://git.tuffraid.net/cowch/lst_v3/commits/c15ee070e7057ad8a5e3d42f51c230d680db9e21))
* **new role:** added in warehouse role ([55418e2](https://git.tuffraid.net/cowch/lst_v3/commits/55418e2f098193b0891129a19532608dce2abd9c))
* **opend dock:** added in default dock so it uses the default setup as a backup ([8fc3129](https://git.tuffraid.net/cowch/lst_v3/commits/8fc3129f7d687d45dc242aca9a6e71f43d0352ab))
* **opendock:** added in check for really using the article link ([cfc497c](https://git.tuffraid.net/cowch/lst_v3/commits/cfc497c1f2254dc12a6e7bb15cfc0dce2a64c1e6))
* **opendock:** added in proper complete and ignore of picksheets ([3ad84da](https://git.tuffraid.net/cowch/lst_v3/commits/3ad84dab71ceea081efc824e97d075c6f33807ad))
* **opendock:** added some new goodies to the app to help manage releases ([c0a7d4a](https://git.tuffraid.net/cowch/lst_v3/commits/c0a7d4a1252e3f14240d14288a3ac4add44a6463))
* **opendock:** changed the article to look at the label to match all plants ([a2d9a6c](https://git.tuffraid.net/cowch/lst_v3/commits/a2d9a6c127cae3b6dc5beb1a996558ab1383fa8c))
* **opendock:** changed the subModule for better logging ([7c4c5f9](https://git.tuffraid.net/cowch/lst_v3/commits/7c4c5f980a15e13e2a43a8583a652622370ec1dd))
* **opendock:** if load type is drop we will not do anything unless its already scanned ([5a7046b](https://git.tuffraid.net/cowch/lst_v3/commits/5a7046bacd9323627a37c89d7a9a329383e8b1be))
* **server builds:** added in proper logging will still need fine tuned though ([15c939e](https://git.tuffraid.net/cowch/lst_v3/commits/15c939ebe8b808b3a9a46a4c1e62fb0488d3649b))
* **servers:** all remaining servers added ([5f2148f](https://git.tuffraid.net/cowch/lst_v3/commits/5f2148f5f0f7688cf71e1dad99d4a5f6c34241dd))
* **socket.io:** complete rewrite to manage dynamic rooms and seeding better ([9440b44](https://git.tuffraid.net/cowch/lst_v3/commits/9440b44f3bb1842d0e6ffc479d340ae9c5b84656))
* **socketio:** changes that if we are in dock room to constant reseed the room ([8fcb2c6](https://git.tuffraid.net/cowch/lst_v3/commits/8fcb2c66ed25c746dc1d63c5290e11fe6a56328e))
* **times:** added a better view for times as we save all db as there respective timezone ([40edbc3](https://git.tuffraid.net/cowch/lst_v3/commits/40edbc3eae9000afea447f81ec127884712b38da))
* **warehouse:** fixed ppoo check to match the new changes ([2e460c7](https://git.tuffraid.net/cowch/lst_v3/commits/2e460c7f8aeb7de1d641c42357001704a4d8d8b2))
### 📝 Testing Code
* **test:** added in vitest to start doing more testing before deploying ([e6b92ae](https://git.tuffraid.net/cowch/lst_v3/commits/e6b92aeb109394691dd2a7d548ddadd02769a159))
### 📈 Project changes
* **app:** added more labels to the templates ([69a9a81](https://git.tuffraid.net/cowch/lst_v3/commits/69a9a81a889aca010ec8835ffab7084f7cd4a9b0))
* **app:** changed the issues templates to be easier to add to the part of the app needed ([188fdd0](https://git.tuffraid.net/cowch/lst_v3/commits/188fdd0eb7c6bedf52ae1dcc9109a0fdbe969a21))
## [0.1.0-alpha.2](https://git.tuffraid.net/cowch/lst_v3/compare/v0.1.0-alpha.1...v0.1.0-alpha.2) (2026-05-23)

View File

@@ -7,7 +7,7 @@
Quick summary of current rewrite/migration goal.
- **Phase:** Backend rewrite
- **Last updated:** 2026-04-06
- **Last updated:** 2026-05-27
---
@@ -17,20 +17,20 @@ Quick summary of current rewrite/migration goal.
|----------|--------------|--------|
| User Authentication | ~~Login~~, ~~Signup~~, API Key | 🟨 In Progress |
| User Profile | ~~Edit profile~~, upload avatar | 🟨 In Progress |
| User Admin | Edit user, create user, remove user, alplaprod user integration | ⏳ Not Started |
| User Admin | ~~Edit user~~, ~~create user~~, remove user, alplaprod user integration | ⏳ Not Started |
| Notifications | ~~Subscribe~~, ~~Create~~, ~~Update~~, ~~~~Remove~~, Manual Trigger | 🟨 In Progress |
| Datamart | ~~Create~~, ~~Update~~, ~~Run~~, Deactivate | 🟨 In Progress |
| Frontend | Analytics and charts | ⏳ Not Started |
| Docs | Instructions and trouble shooting | ⏳ Not Started |
| One Click Print | Get printers, monitor printers, label process, material process, Special processes | ⏳ Not Started |
| One Click Print | ~~Get printers~~, monitor printers, label process, material process, Special processes | 🟨 In Progress |
| Silo Adjustments | Create, History, Comments | ⏳ Not Started |
| Demand Management | Orders, Forecast, Special Mappings, Create trucks, Load Trucks (tablet scanning) | ⏳ Not Started |
| Open Docks | Integrations | ⏳ Not Started |
| Open Docks | Integrations | 🟨 In Progress |
| Transport Insight | Integrations | ⏳ Not Started |
| Quality Request Tool | Add Pallet, Monitor for moved, status changes, alerts | ⏳ Not Started |
| Logistics | Consume material, return and print, label info, relocate | ⏳ Not Started |
| EOM | Endpoints, Report Pull for finance | ⏳ Not Started |
| OCME | Custom integration | ⏳ Not Started |
| EOM | ~~Endpoints~~, Report Pull for finance, SSRS report | 🟨 In Progress |
| ~~OCME~~ | ~~Custom integration~~ | Canceled |
| API Migration | Moving to new REST endpoints | 🔧 In Progress |
| System | Tests,Builds, Updates, Remote Logging, DB Backups, Alerting | ⏳ Not Started |
@@ -47,4 +47,13 @@ How to run the current version of the app.
git clone https://git.tuffraid.net/cowch/lst_v3.git
cd lst_v3
npm install
npm run dev
```
Rename the .env-example to .env
Update all the fields
```bash
npm run dev:db:migrate
npm run dev
```

View File

@@ -46,7 +46,8 @@ const createApp = async () => {
server: ${JSON.stringify(umamiConfig.server ?? "unknown")},
appVersion: ${JSON.stringify(umamiConfig.appVersion ?? "dev")},
umamiHost: ${JSON.stringify(umamiConfig.umamiHost ?? "")},
umamiWebsiteId: ${JSON.stringify(umamiConfig.umamiWebsiteId ?? "")}
umamiWebsiteId: ${JSON.stringify(umamiConfig.umamiWebsiteId ?? "")},
timezone: ${JSON.stringify(process.env.TIMEZONE ?? "America/Chicago")}
};
`);
});

View File

@@ -9,8 +9,6 @@ import os from "node:os";
import { apiReference } from "@scalar/express-api-reference";
// const port = 3000;
import type { OpenAPIV3_1 } from "openapi-types";
import { cronerActiveJobs } from "../scaler/cronerActiveJobs.spec.js";
import { cronerStatusChange } from "../scaler/cronerStatusChange.spec.js";
import { prodLoginSpec } from "../scaler/login.spec.js";
import { openDockApt } from "../scaler/opendockGetRelease.spec.js";
import { prodRestartSpec } from "../scaler/prodSqlRestart.spec.js";
@@ -125,8 +123,6 @@ export const setupApiDocsRoutes = (baseUrl: string, app: Express) => {
...prodLoginSpec,
...prodRegisterSpec,
//...mergedDatamart,
...cronerActiveJobs,
...cronerStatusChange,
...openDockApt,
// Add more specs here as you build features

View File

@@ -37,6 +37,7 @@ const lstDbRun = async (data: Data) => {
if (data.options) {
if (data.name === "psiInventory") {
const ids = data.options.articles.split(",").map((id: any) => id.trim());
const whse = data.options.whseToInclude
? data.options.whseToInclude
.split(",")
@@ -274,10 +275,10 @@ export const runDatamartQuery = async (data: Data) => {
.replace("[startDate]", `${data.options.startDate}`)
.replace("[endDate]", `${data.options.endDate}`)
.replace(
"and p.IdArtikelvarianten in ([articles])",
"and pl.ArticleHumanReadableId IN ([articles]) ",
data.options.articles
? `and p.IdArtikelvarianten in (${data.options.articles})`
: "--and p.IdArtikelvarianten in ([articles])",
? `and pl.ArticleHumanReadableId IN (${data.options.articles})`
: "--and pl.ArticleHumanReadableId IN ([articles])",
);
break;
default:
@@ -326,8 +327,13 @@ export const runDatamartQuery = async (data: Data) => {
level: "info",
module: "datamart",
subModule: "query",
message: `Data for: ${data.name}`,
data: queryRun.data,
message: `Data for: ${data.name} ${data.options.includePlantToken ? "including plant token" : ""}`,
// if includePlantToken was passed we should map this into the data
data: data.options.includePlantToken
? queryRun.data.map((i) => {
return { plantToken: process.env.PROD_PLANT_TOKEN, ...i };
})
: queryRun.data,
notify: false,
});
};

View File

@@ -0,0 +1,68 @@
import express from "express";
import request from "supertest";
import { describe, expect, it, vi } from "vitest";
vi.mock("../db/db.controller.js", () => ({
db: {},
}));
vi.mock("../logger/logger.controller.js", () => ({
createLogger: () => ({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
}),
}));
vi.mock("./datamart.controller.js", () => ({
runDatamartQuery: vi.fn(async ({ name, options }) => ({
success: true,
message: `Ran ${name}`,
data: {
name,
options,
},
})),
}));
import { runDatamartQuery } from "./datamart.controller.js";
import getDatamartRoute from "./getDatamart.route.js";
function createTestApp() {
const app = express();
app.use(express.json());
app.use("/datamart", getDatamartRoute);
return app;
}
describe("GET /datamart/:name", () => {
it("runs a datamart query by name and returns api response", async () => {
const app = createTestApp();
const res = await request(app).get("/datamart/orders").query({
value: "123",
});
expect(res.status).toBe(200);
expect(runDatamartQuery).toHaveBeenCalledWith({
name: "orders",
options: {
value: "123",
},
});
expect(res.body.success).toBe(true);
expect(res.body.module).toBe("datamart");
expect(res.body.subModule).toBe("query");
expect(res.body.data).toEqual({
name: "orders",
options: {
value: "123",
},
});
});
});

View File

@@ -1,4 +1,5 @@
import { Router } from "express";
import * as XLSX from "xlsx";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { runDatamartQuery } from "./datamart.controller.js";
@@ -7,13 +8,73 @@ const r = Router();
type Options = {
name: string;
value: string;
format: string;
};
r.get("/:name", async (req, res) => {
const { name } = req.params;
const options = req.query as Options;
const options = { ...req.query } as Options;
const dataRan = await runDatamartQuery({ name, options });
if (!dataRan.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "datamart",
subModule: "query",
message: dataRan.message,
status: 500,
});
}
// XLSX Export
if (options.format?.toLowerCase() === "xlsx") {
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(dataRan.data);
XLSX.utils.book_append_sheet(wb, ws, name);
const buffer = XLSX.write(wb, {
type: "buffer",
bookType: "xlsx",
});
res.setHeader(
"Content-Type",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
);
res.setHeader("Content-Disposition", `attachment; filename="${name}.xlsx"`);
return res.send(buffer);
}
// CSV Export
if (options.format?.toLowerCase() === "csv") {
const rows = dataRan.data as any;
if (!rows.length) {
return res.status(200).send("");
}
const headers = Object.keys(rows[0]);
const csv = [
headers.join(","),
...rows.map((row: any) =>
headers
.map((h) => `"${String(row[h] ?? "").replace(/"/g, '""')}"`)
.join(","),
),
].join("\r\n");
res.setHeader("Content-Type", "text/csv");
res.setHeader("Content-Disposition", `attachment; filename="${name}.csv"`);
return res.send(csv);
}
return apiReturn(res, {
success: dataRan.success,
level: "info",

View File

@@ -1,5 +1,7 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as dockScans from "./schema/dockdoor.scans.schema.js";
import * as logs from "./schema/logs.schema.js";
import * as opendockAVCheck from "./schema/opendock_articleSetup.js";
import * as scanUserSchema from "./schema/scanUsers.js";
import * as settingsSchema from "./schema/settings.schema.js";
@@ -23,5 +25,7 @@ export const db = drizzle(queryClient, {
...scanUserSchema,
...settingsSchema,
...opendockAVCheck,
...logs,
...dockScans,
},
});

59
backend/db/db.listener.ts Normal file
View File

@@ -0,0 +1,59 @@
import postgres from "postgres";
import { createLogger } from "../logger/logger.controller.js";
import { handleDbNotification } from "./db.router.js";
const log = createLogger({
module: "db",
subModule: "notifications",
});
const CHANNELS = [
"logs_inserted",
// "labels_inserted",
// "dock_scans_inserted",
] as const;
type DbNotificationChannel = (typeof CHANNELS)[number];
type DbNotificationPayload = {
table: string;
action: "INSERT" | "UPDATE" | "DELETE";
id: string;
};
export async function startDbNotificationListener() {
const sql = postgres({
host: `${process.env.DATABASE_HOST}`,
port: Number(process.env.DATABASE_PORT),
database: `${process.env.DATABASE_DB}`,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
});
for (const channel of CHANNELS) {
await sql.listen(channel, async (rawPayload) => {
await processNotification(channel, rawPayload);
});
log.info({ stack: { channel } }, `Listening for ${channel}`);
}
}
async function processNotification(
channel: DbNotificationChannel,
rawPayload: string,
) {
try {
const payload = JSON.parse(rawPayload) as DbNotificationPayload;
await handleDbNotification({
channel,
payload,
});
} catch (e) {
log.error(
{ stack: { channel, rawPayload, e }, notify: true },
"Failed processing DB notification",
);
}
}

41
backend/db/db.router.ts Normal file
View File

@@ -0,0 +1,41 @@
import { handleDockScanInsertedNotification } from "../dockdoorScanning/dockdoor.socket.notifications.js";
import { createLogger } from "../logger/logger.controller.js";
import { handleLogInsertedNotification } from "../logger/logger.socket.notifications.js";
const log = createLogger({
module: "db",
subModule: "notifications-router",
});
type DbNotification = {
channel: string;
payload: {
table: string;
action: "INSERT" | "UPDATE" | "DELETE";
id: string;
};
};
export async function handleDbNotification(notification: DbNotification) {
const { channel, payload } = notification;
switch (channel) {
case "logs_inserted":
await handleLogInsertedNotification(payload.id);
return;
// case "labels_inserted":
// await handleLabelInsertedNotification(payload.id);
// return;
case "dock_scan_inserted":
await handleDockScanInsertedNotification(payload.id);
return;
default:
log.warn(
{ stack: notification },
`Unhandled DB notification channel: ${channel}`,
);
}
}

View File

@@ -0,0 +1,99 @@
import { sql } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { createLogger } from "../logger/logger.controller.js";
const log = createLogger({
module: "db",
subModule: "notifications",
});
/**
* Creates/updates Postgres notification functions + triggers.
*
* Safe to run on every app startup.
* CREATE OR REPLACE updates the function.
* DROP TRIGGER IF EXISTS prevents duplicate triggers.
*/
export async function setupDbNotifications() {
log.info({}, "Setting up DB notifications");
await setupLogsNotifications();
await setupDockScansNotifications();
log.info({}, "DB notifications setup complete");
}
/**
* Logs notification setup.
*
* Flow:
* 1. app inserts into logs table
* 2. trigger runs after insert
* 3. Postgres sends NOTIFY logs_inserted with the new log id
* 4. Node listener receives id and fetches/emits full row
*/
async function setupLogsNotifications() {
await db.execute(sql`
CREATE OR REPLACE FUNCTION notify_logs_inserted()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify(
'logs_inserted',
json_build_object(
'table', TG_TABLE_NAME,
'action', TG_OP,
'id', NEW.id
)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`);
await db.execute(sql`
DROP TRIGGER IF EXISTS logs_inserted_notify_trigger ON logs;
`);
await db.execute(sql`
CREATE TRIGGER logs_inserted_notify_trigger
AFTER INSERT ON logs
FOR EACH ROW
EXECUTE FUNCTION notify_logs_inserted();
`);
log.info({}, "Logs DB notification trigger ready");
}
async function setupDockScansNotifications() {
await db.execute(sql`
CREATE OR REPLACE FUNCTION notify_dock_scan_inserted()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify(
'dock_scan_inserted',
json_build_object(
'table', TG_TABLE_NAME,
'action', TG_OP,
'id', NEW.id
)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`);
await db.execute(sql`
DROP TRIGGER IF EXISTS dock_scan_inserted_notify_trigger ON dock_door_scans;
`);
await db.execute(sql`
CREATE TRIGGER dock_scan_inserted_notify_trigger
AFTER INSERT ON dock_door_scans
FOR EACH ROW
EXECUTE FUNCTION notify_dock_scan_inserted();
`);
log.info({}, "Dock scan DB notification trigger ready");
}

View File

@@ -0,0 +1,21 @@
import { db } from "./db.controller.js";
export const getRecentLogs = ({
module,
submodule,
limit = 200,
}: {
module?: string | undefined;
submodule?: string | undefined;
limit?: number | undefined;
}) => {
return db.query.logs.findMany({
where: (logs, { and, eq }) =>
and(
module ? eq(logs.module, module) : undefined,
submodule ? eq(logs.subModule, submodule) : undefined,
),
orderBy: (logs, { desc }) => [desc(logs.createdAt)],
limit,
});
};

View File

@@ -17,15 +17,15 @@ export const alplaPurchaseHistory = pgTable("alpla_purchase_history", {
status: integer("status"),
statusText: text("status_text"),
journalNum: integer("journal_num"),
add_date: timestamp("add_date").defaultNow(),
add_date: timestamp("add_date", { withTimezone: true }).defaultNow(),
add_user: text("add_user"),
upd_user: text("upd_user"),
upd_date: timestamp("upd_date").defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
remark: text("remark"),
approvedStatus: text("approved_status").default("new"),
position: jsonb("position").default([]),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
});
export const alplaPurchaseHistorySchema =

View File

@@ -3,7 +3,9 @@ import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
export const analytics = pgTable("analytics", {
id: uuid("id").defaultRandom().primaryKey(),
createdAt: timestamp("created_at").defaultNow().notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
method: text("method").notNull(),
routePattern: text("route_pattern").notNull(),

View File

@@ -16,13 +16,13 @@ export const jobAuditLog = pgTable(
id: uuid("id").defaultRandom().primaryKey(),
jobName: text("job_name"),
startedAt: timestamp("start_at"),
finishedAt: timestamp("finished_at"),
finishedAt: timestamp("finished_at", { withTimezone: true }),
durationMs: integer("duration_ms"),
status: text("status"), //success | error
errorMessage: text("error_message"),
errorStack: text("error_stack"),
metadata: jsonb("meta_data"),
createdAt: timestamp("created_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
},
(table) => {
return {

View File

@@ -15,7 +15,7 @@ export const user = pgTable("user", {
emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),

View File

@@ -6,5 +6,5 @@ export const deploymentHistory = pgTable("deployment_history", {
buildNumber: integer("build_number").notNull(),
status: text("status").notNull(), // started, success, failed
message: text("message"),
createdAt: timestamp("created_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
});

View File

@@ -28,11 +28,15 @@ export const analyticsDaily = pgTable(
avgDurationMs: integer("avg_duration_ms").notNull(),
maxDurationMs: integer("max_duration_ms").notNull(),
firstHitAt: timestamp("first_hit_at").notNull(),
lastHitAt: timestamp("last_hit_at").notNull(),
firstHitAt: timestamp("first_hit_at", { withTimezone: true }).notNull(),
lastHitAt: timestamp("last_hit_at", { withTimezone: true }).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(table) => [
unique("analytics_daily_business_route_unique").on(

View File

@@ -18,9 +18,9 @@ export const datamart = pgTable("datamart", {
active: boolean("active").default(true),
options: text("options").default(""),
public: boolean("public_access").default(false),
add_date: timestamp("add_date").defaultNow(),
add_date: timestamp("add_date", { withTimezone: true }).defaultNow(),
add_user: text("add_user").default("lst-system"),
upd_date: timestamp("upd_date").defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
upd_user: text("upd_user").default("lst-system"),
});

View File

@@ -0,0 +1,23 @@
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";
export const dockDoorScans = pgTable("dock_door_scans", {
id: uuid("id").defaultRandom().primaryKey(),
dockId: text("dock_id").notNull(),
loadingOrder: text("loading_order").notNull(),
loadingUnit: text("loading_Unit"), // can be running number or sscc depending on where it came from
loadingUnitStatus: text("loading_unit_status").default("loaded"), // TODO: add enums on the status of each load.
message: text("message"), // the response it gave when scanning
status: text("status").default("active"), // TODO: add in enums for this
add_date: timestamp("add_date", { withTimezone: true }).defaultNow(),
add_user: text("add_user").default("lst-system"),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
upd_user: text("upd_user").default("lst-system"),
});
export const dockDoorScansSchema = createSelectSchema(dockDoorScans);
export const newDockDoorScansSchema = createInsertSchema(dockDoorScans);
export type DockDoorScans = z.infer<typeof dockDoorScansSchema>;
export type NewDockDoorScans = z.infer<typeof newDockDoorScansSchema>;

View File

@@ -0,0 +1,22 @@
import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";
export const dockDoorScanners = pgTable("dock_door_scanners", {
id: uuid("id").defaultRandom().primaryKey(),
ip: text("ip").notNull(),
name: text("name").unique(),
dockId: text("dock_id"),
active: boolean("active").default(true),
currentLoadingOrder: text("current_loading_order").default(""),
add_date: timestamp("add_date", { withTimezone: true }).defaultNow(),
add_user: text("add_user").default("lst-system"),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
upd_user: text("upd_user").default("lst-system"),
});
export const dockDoorScannersSchema = createSelectSchema(dockDoorScanners);
export const newDockDoorScannersSchema = createInsertSchema(dockDoorScanners);
export type DockDoorScanners = z.infer<typeof dockDoorScannersSchema>;
export type NewDockDoorScanners = z.infer<typeof newDockDoorScannersSchema>;

View File

@@ -0,0 +1,22 @@
import { jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";
export const forecastImport = pgTable("forecast_import", {
id: uuid("id").defaultRandom().primaryKey(),
receivingPlantId: text("receiving_plant_id").notNull(),
documentName: text("documentName"),
sender: text("sender"),
customerId: text("customer_id"),
rawData: jsonb("raw_data").default([]),
add_date: timestamp("add_date", { withTimezone: true }).defaultNow(),
add_user: text("add_user").default("lst-system"),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
upd_user: text("upd_user").default("lst-system"),
});
export const forecastImportSchema = createSelectSchema(forecastImport);
export const newForecastImportSchema = createInsertSchema(forecastImport);
export type ForecastImport = z.infer<typeof forecastImportSchema>;
export type NeworecastImport = z.infer<typeof newForecastImportSchema>;

View File

@@ -20,7 +20,7 @@ export const invHistoricalData = pgTable("inv_historical_data", {
whseId: text("whse_id").default(""),
whseName: text("whse_name").default("missing whseName"),
upd_user: text("upd_user").default("lst-system"),
upd_date: timestamp("upd_date").defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
});
export const invHistoricalDataSchema = createSelectSchema(invHistoricalData);

View File

@@ -18,7 +18,7 @@ export const logs = pgTable("logs", {
stack: jsonb("stack").default([]),
checked: boolean("checked").default(false),
hostname: text("hostname"),
createdAt: timestamp("created_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
});
export const logSchema = createSelectSchema(logs);

View File

@@ -16,9 +16,14 @@ export const opendockApt = pgTable(
id: uuid("id").defaultRandom().primaryKey(),
release: integer("release").notNull().unique("opendock_apt_release_unique"),
openDockAptId: text("open_dock_apt_id").notNull(),
status: text("status").default("active"),
appointment: jsonb("appointment").notNull().default([]),
upd_date: timestamp("upd_date").notNull().defaultNow(),
createdAt: timestamp("created_at").notNull().defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true })
.notNull()
.defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(table) => ({
openDockAptIdIdx: index("opendock_apt_opendock_id_idx").on(

View File

@@ -22,9 +22,13 @@ export const opendockArticleSetup = pgTable(
customerDescription: text("customer_description").notNull(),
loadType: loadTypeEnum("load_type").notNull().default("drop"),
dock: text("dock").notNull(),
upd_date: timestamp("upd_date").notNull().defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true })
.notNull()
.defaultNow(),
upd_user: text("upd_user").notNull().default("lst-system"),
createdAt: timestamp("created_at").notNull().defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
add_user: text("add_user").notNull().default("lst-system"),
},
(table) => ({

View File

@@ -6,9 +6,13 @@ export const opendockDockSetup = pgTable("opendock_dock_setup", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
dockID: text("dock_id").notNull(),
upd_date: timestamp("upd_date").notNull().defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true })
.notNull()
.defaultNow(),
upd_user: text("upd_user").notNull().default("lst-system"),
createdAt: timestamp("created_at").notNull().defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
add_user: text("add_user").notNull().default("lst-system"),
});

View File

@@ -7,5 +7,5 @@ export const printerLog = pgTable("printer_log", {
printerSN: text("printer_sn"),
condition: text("condition").notNull(),
message: text("message"),
createdAt: timestamp("created_at").defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
});

View File

@@ -28,8 +28,8 @@ export const printerData = pgTable(
printDelay: integer("printDelay").default(90),
processes: jsonb("processes").default([]),
printDelayOverride: boolean("print_delay_override").default(false), // this will be more for if we have the lot time active but want to over ride this single line for some reason
add_Date: timestamp("add_Date").defaultNow(),
upd_date: timestamp("upd_date").defaultNow(),
add_Date: timestamp("add_Date", { withTimezone: true }).defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
},
(table) => [
//uniqueIndex("emailUniqueIndex").on(sql`lower(${table.email})`),

View File

@@ -30,8 +30,8 @@ export const scanUser = pgTable(
role: mobileRoleEnum("role").notNull().default("user"),
active: boolean("active").default(true),
lastScan: timestamp("last_scan").defaultNow(),
add_Date: timestamp("add_Date").defaultNow(),
upd_date: timestamp("upd_date").defaultNow(),
add_Date: timestamp("add_Date", { withTimezone: true }).defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
},
(table) => ({
userNotificationUnique: unique("scan_user_unique").on(

View File

@@ -13,7 +13,7 @@ export const scanLog = pgTable("scan_log", {
status: text("status"),
scannerVersion: text("scanner_version").default("0"),
lines: jsonb("lines").default([]),
add_Date: timestamp("add_date").defaultNow(),
add_Date: timestamp("add_date", { withTimezone: true }).defaultNow(),
});
export const scanLogSchema = createSelectSchema(scanLog);

View File

@@ -22,7 +22,7 @@ export const serverData = pgTable(
contactPhone: text("contact_phone"),
active: boolean("active").default(true),
serverLoc: text("server_loc"),
lastUpdated: timestamp("last_updated").defaultNow(),
lastUpdated: timestamp("last_updated", { withTimezone: true }).defaultNow(),
buildNumber: integer("build_number"),
isUpgrading: boolean("is_upgrading").default(false),
},

View File

@@ -32,9 +32,9 @@ export const settings = pgTable(
settingType: settingType(),
seedVersion: integer("seed_version").default(1), // this is intended for if we want to update the settings.
add_User: text("add_User").default("LST_System").notNull(),
add_Date: timestamp("add_Date").defaultNow(),
add_Date: timestamp("add_Date", { withTimezone: true }).defaultNow(),
upd_user: text("upd_User").default("LST_System").notNull(),
upd_date: timestamp("upd_date").defaultNow(),
upd_date: timestamp("upd_date", { withTimezone: true }).defaultNow(),
},
(table) => [
// uniqueIndex('emailUniqueIndex').on(sql`lower(${table.email})`),

View File

@@ -12,11 +12,11 @@ import type z from "zod";
export const appStats = pgTable("app_stats", {
id: text("id").primaryKey().default("primary"),
currentBuild: integer("current_build").notNull().default(1),
lastBuildAt: timestamp("last_build_at"),
lastDeployAt: timestamp("last_deploy_at"),
lastBuildAt: timestamp("last_build_at", { withTimezone: true }),
lastDeployAt: timestamp("last_deploy_at", { withTimezone: true }),
building: boolean("building").notNull().default(false),
updating: boolean("updating").notNull().default(false),
lastUpdated: timestamp("last_updated").defaultNow(),
lastUpdated: timestamp("last_updated", { withTimezone: true }).defaultNow(),
meta: jsonb("meta").$type<Record<string, unknown>>().default({}),
});

View File

@@ -0,0 +1,36 @@
import { addDays, subDays } from "date-fns";
import { format } from "date-fns-tz";
import { Router } from "express";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
r.get("/", async (_, res) => {
const orders = await runProdApi({
method: "post",
endpoint: "/public/v1.0/OutboundDeliveries/LoadingOrders/Search",
data: [
{
loadingDateFrom: format(subDays(new Date(Date.now()), 3), "yyyy-MM-dd"),
loadingDateTo: format(addDays(new Date(Date.now()), 3), "yyyy-MM-dd"),
states: [
1, // planned
2, // loading
],
//isCommissioned: true,
},
],
});
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "current Active loading orders",
message: `Current active loading loaders.`,
data: orders?.data ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,181 @@
import { eq, sql } from "drizzle-orm";
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { dockDoorScans } from "../db/schema/dockdoor.scans.schema.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
const endLoading = z.object({
loadingOrder: z.string(),
dockId: z.string(),
});
r.post("/", async (req, res) => {
if (req.body.clear) {
// just clear the loading order and clear out all the pallets to keep it clean.
await tryCatch(
db
.update(dockDoorScans)
.set({
status: "completed",
upd_date: sql`NOW()`,
upd_user: req.user?.username ?? "lst-dock-system",
})
.where(
req.body.loadingOrder
? eq(dockDoorScanners.currentLoadingOrder, req.body.loadingOrder)
: undefined,
)
.returning(),
);
const { data, error } = await tryCatch(
db
.update(dockDoorScanners)
.set({
currentLoadingOrder: "",
upd_date: sql`NOW()`,
upd_user: req.user?.username ?? "lst-dock-system",
})
.where(eq(dockDoorScanners.dockId, req.body.dockId))
.returning(),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "loadingOrder",
message: `Failed to updating the dock.`,
data: (error as any) ?? [],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "loadingOrder",
message: `Loading order: ${req.body.loadingOrder} was just cleared out do to the process being completed in some other means. \nThis includes any scanned pallets as well.`,
data: data ?? [],
status: 200,
});
}
try {
const validated = endLoading.parse(req.body);
const orders = (await runProdApi({
method: "post",
endpoint: `/public/v1.0/OutboundDeliveries/LoadingOrders/${req.body.loadingOrder}/Finish`,
data: [
{
printDeliveryDocuments: true,
},
],
})) as any;
if (orders?.data.errors) {
//console.log(orders.data.errors);
// if (orders.data.errors[0].code === 20) {
// await db
// .update(dockDoorScanners)
// .set({
// currentLoadingOrder: "",
// upd_date: sql`NOW()`,
// upd_user: req.user?.username ?? "lst-dock-system",
// })
// .where(eq(dockDoorScanners.dockId, validated.dockId));
// return apiReturn(res, {
// success: false,
// level: "warn",
// module: "dockdoor",
// subModule: "loadingOrder",
// message: `Loading order: ${validated.loadingOrder} cleared, It was probable completed by a scanner.. or user...`,
// data: (orders.data.errors as any) ?? [],
// status: 200,
// });
// }
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "loadingOrder",
message: `Failed to finish the order: ${orders.data.errors[0].message}`,
data: (orders.data.errors as any) ?? [],
status: 400,
});
}
await tryCatch(
db
.update(dockDoorScans)
.set({
status: "completed",
upd_date: sql`NOW()`,
upd_user: req.user?.username ?? "lst-dock-system",
})
.where(
eq(dockDoorScans.loadingOrder, validated.loadingOrder.toString()),
)
.returning(),
);
const { data, error } = await tryCatch(
db
.update(dockDoorScanners)
.set({
currentLoadingOrder: "",
upd_date: sql`NOW()`,
upd_user: req.user?.username ?? "lst-dock-system",
})
.where(eq(dockDoorScanners.dockId, validated.dockId))
.returning(),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "loadingOrder",
message: `Failed to updating the dock.`,
data: (error as any) ?? [],
status: 400,
});
}
return apiReturn(res, {
success: orders.data.errors ? false : true,
level: orders.data.errors ? "error" : "info",
module: "dockdoor",
subModule: "loadingOrder",
message: orders.data.errors
? `Loading order was cleared but encountered an error: \n${orders.data.errors[0].message} \nPossible reason for this is the loading order was completed via scanner or other means.`
: `Loading order ${validated.loadingOrder} was just closed.`,
data: data ?? [],
status: orders.data.errors ? 400 : 200,
});
} catch (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "loadingOrder",
message: `Failed to Close loading order.`,
data: (error as any) ?? [],
status: 400,
});
}
});
export default r;

View File

@@ -0,0 +1,25 @@
import { Router } from "express";
import { apiReturn } from "../utils/returnHelper.utils.js";
import loadUnit from "./dockdoor.loadUnits.js";
const r = Router();
r.post("/", async (req, res) => {
const unit = await loadUnit({
dockId: req.body.dockId,
runningNo: req.body.runningNo,
});
return apiReturn(res, {
success: unit.success,
level: "info",
module: "dockdoor",
subModule: "loadingUnit",
message: unit.message,
data: unit?.data ?? [],
status: unit.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,201 @@
// sends the units from the dock door scanner here.
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { dockDoorScans } from "../db/schema/dockdoor.scans.schema.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
// validate we are active
type Data = {
dockId?: string;
sscc?: string;
runningNo?: string;
};
const postScan = async (data: any) => {
try {
const newScan = (await db
.insert(dockDoorScans)
.values({
dockId: data.dockId,
loadingOrder: data.loadingOrder,
loadingUnit: data.loadingUnit.sscc ?? data.loadingUnit.runningNo, // can be running number or sscc depending on where it came from
loadingUnitStatus: data.loadingUnitStatus, // TODO: add enums on the status of each load.
message: data.message, // the response it gave when scanning
status: data.status ?? undefined,
})
.returning()) as any;
emitToRoom(`dockDoorLoading:${data.dockId}`, newScan[0]);
} catch (error) {
console.log("Error: ", error);
}
};
const loadUnit = async (data: Data) => {
const log = createLogger({ module: "dockdoor", subModule: "loadunit" });
log.info({ stack: data }, "Data Passed over from the scanner.");
// are we even active at this time?
const dockDoorActive = await db.query.settings.findFirst({
where: (u, { eq }) => eq(u.name, "dockDoorScanning"),
});
const unitToScan = data.sscc
? { sscc: data.sscc !== "noread" ? data.sscc?.slice(2) : data.sscc }
: { runningNo: Number(data.runningNo) };
const dock = await db
.select()
.from(dockDoorScanners)
.where(eq(dockDoorScanners.dockId, data.dockId as string));
if (!dockDoorActive?.active) {
return returnFunc({
success: false,
level: "error",
module: "dockdoor",
subModule: "loadunit",
message: "Dock door scanning feature is not active.",
data: [],
notify: false,
room: "",
});
}
// check if we currently have a loading order attached to the dock door.
if (dock[0]?.currentLoadingOrder === "") {
postScan({
dockId: data.dockId,
loadingOrder: dock[0]?.currentLoadingOrder,
loadingUnit: unitToScan,
loadingUnitStatus: "notLoaded",
message:
"There are know current active loading orders please start one and try again.",
status: "error",
});
return returnFunc({
success: true,
level: "error",
module: "dockdoor",
subModule: "loadingOrders",
message:
"There are know current active loading orders please start one and try again.",
data: [],
notify: false,
});
}
// check if its a valids an sscc
if (data.sscc === "noread") {
postScan({
dockId: data.dockId,
loadingOrder: dock[0]?.currentLoadingOrder,
loadingUnit: unitToScan,
loadingUnitStatus: "noread",
message:
"Failed to load the unit to the truck, there was no pallet read.",
status: "error",
});
return returnFunc({
success: false,
level: "error",
module: "dockdoor",
subModule: "loadUnit",
message:
"Failed to load the unit to the truck, there was no pallet read.",
data: [],
notify: false,
});
}
// TODO: pallet validation, check if we are on hold, then check if we have been in the staging warehouse for more than x time.
// if on hold stop the scan and send a bad read with the reason its on hold and what its on hold for, including coa.
// if precheck is active then check if we have a warehouse, then check if the pallet was in the warehouse for greater than the define min, all fails send a warning and still do the scan
// add the loading units
try {
const prod = (await runProdApi({
method: "post",
endpoint: `/public/v1.0/OutboundDeliveries/LoadingOrders/${dock[0]?.currentLoadingOrder}/LoadUnit`,
data: [unitToScan],
})) as any;
//emitToRoom(`dockDoorLoading:${data.dockId}`, prod?.data ?? []);
if (!prod?.success) {
postScan({
dockId: data.dockId,
loadingOrder: dock[0]?.currentLoadingOrder,
loadingUnit: unitToScan,
loadingUnitStatus: "notLoaded",
message: prod?.data.errors[0].message,
status: "error",
});
return returnFunc({
success: false,
level: "error",
module: "dockdoor",
subModule: "loadUnit",
message: `Unit encountered an error while loading`,
data: prod?.data.errors[0] as any,
notify: false,
//room: `dockDoorLoading:${data.dockId}`,
});
} else {
const emitData = {
message: `The unit ${prod.data.message.messageParams.runningNo} was loaded.`,
data: prod.data,
code: 0,
};
postScan({
dockId: data.dockId,
loadingOrder: dock[0]?.currentLoadingOrder,
loadingUnit: unitToScan,
loadingUnitStatus: "loaded",
message: emitData.message,
});
return returnFunc({
success: true,
level: "info",
module: "dockdoor",
subModule: "loadUnit",
message: `Unit added to loading order`,
data: [
{
message: `The unit ${prod.data.message.messageParams.runningNo} was loaded.`,
data: prod.data,
code: 0,
},
] as any,
notify: false,
//room: `dockDoorLoading:${data.dockId}`,
});
}
} catch (error) {
console.log(error);
return returnFunc({
success: true,
level: "error",
module: "dockdoor",
subModule: "loadUnit",
message: `Failed to load unit`,
data: error as any,
notify: false,
room: `dockDoorLoading:${data.dockId}`,
});
}
};
export default loadUnit;

View File

@@ -0,0 +1,48 @@
import type { Express } from "express";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import activeLoadingOrders from "./dockdoor.activeLoadingOrders.route.js";
import closeLoadingOrder from "./dockdoor.closeLoadingOrder.route.js";
import load from "./dockdoor.loadUnits.route.js";
import startLoad from "./dockdoor.startLoad.route.js";
import prodDocks from "./dockdoors.docks.route.js";
import docks from "./dockdoors.route.js";
export const setupDockDoorRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(
`${baseUrl}/api/dockDoor/scanners`,
featureCheck("dockDoorScanning"),
docks,
);
app.use(
`${baseUrl}/api/dockDoor/finishOrder`,
featureCheck("dockDoorScanning"),
closeLoadingOrder,
);
app.use(
`${baseUrl}/api/dockDoor/activeLoadingOrders`,
featureCheck("dockDoorScanning"),
activeLoadingOrders,
);
app.use(
`${baseUrl}/api/dockDoor/startLoad`,
featureCheck("dockDoorScanning"),
startLoad,
);
app.use(
`${baseUrl}/api/dockDoor/docks`,
featureCheck("dockDoorScanning"),
prodDocks,
);
app.use(
`${baseUrl}/api/dockDoor/loadUnit`,
featureCheck("dockDoorScanning"),
load,
);
// all other system should be under /api/system/*
};

View File

@@ -0,0 +1,17 @@
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { logs } from "../db/schema/logs.schema.js";
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
export async function handleDockScanInsertedNotification(id: string) {
const row = await db.query.dockDoorScans.findFirst({
where: eq(logs.id, id),
});
if (!row) return;
// send only to the current dock door
if (row.dockId) {
emitToRoom(`dockDoorLoading:${row.dockId}`, row);
}
}

View File

@@ -0,0 +1,22 @@
import { db } from "../db/db.controller.js";
export const getRecentDockScans = ({
loadingOrder,
limit = 200,
}: {
loadingOrder: string;
limit?: number | undefined;
}) => {
return db.query.dockDoorScans.findMany({
//where: (scans, { eq }) => eq(scans.status, "active"),
where: (scans, { and, eq }) =>
and(
eq(scans.status, "active"),
loadingOrder
? eq(scans.loadingOrder, loadingOrder)
: eq(scans.loadingOrder, "0"),
),
orderBy: (scans, { desc }) => [desc(scans.upd_date)],
limit,
});
};

View File

@@ -0,0 +1,66 @@
import { eq, sql } from "drizzle-orm";
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
const startLoading = z.object({
loadingOrder: z.string(),
dockId: z.string(),
});
r.post("/", async (req, res) => {
try {
const validated = startLoading.parse(req.body);
const { data, error } = await tryCatch(
db
.update(dockDoorScanners)
.set({
currentLoadingOrder: validated.loadingOrder,
upd_date: sql`NOW()`,
upd_user: req.user?.username,
})
.where(eq(dockDoorScanners.dockId, validated.dockId))
.returning(),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "loadingOrder",
message: `Failed to updating the dock.`,
data: (error as any) ?? [],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "loadingOrder",
message: `Loading order ${validated.loadingOrder} was just added to dockId ${validated.dockId}.`,
data: data ?? [],
status: 200,
});
} catch (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "loadingOrder",
message: `Failed to start loading order.`,
data: (error as any) ?? [],
status: 400,
});
}
});
export default r;

View File

@@ -0,0 +1,54 @@
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (_, res) => {
const activeDocks = sqlQuerySelector(`outbound.docks`) as SqlQuery;
if (!activeDocks.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "docks",
message: `There was an error getting the docks query.`,
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(activeDocks.query, "Current Active Docks"),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "newDock",
message: `There was an error getting the docks.`,
data: (error as any) ?? ([] as any),
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "docks",
message: `Current active docks.`,
data: (data.data as any) ?? ([] as any),
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,76 @@
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { dockDoorScanners } from "../db/schema/dockdoor.schema.js";
import { requireAuth } from "../middleware/auth.middleware.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
const newDockScanner = z.object({
ip: z.string(),
name: z.string(),
dockId: z.string(),
});
r.get("/", async (_, res) => {
try {
const docks = await db
.select()
.from(dockDoorScanners)
.orderBy(dockDoorScanners.name);
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "lane check",
message: `All dock Doors.`,
data: docks ?? [],
status: 200,
});
} catch (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "newDock",
message: `There was an error adding in the new dock.`,
data: error ?? ([] as any),
status: 200,
});
}
});
r.post("/", requireAuth, async (req, res) => {
try {
const validated = newDockScanner.parse(req.body);
const newDock = await db
.insert(dockDoorScanners)
.values(validated)
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "dockdoor",
subModule: "lane check",
message: `${validated.name} was just added.`,
data: newDock ?? [],
status: 200,
});
} catch (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "dockdoor",
subModule: "newDock",
message: `There was an error adding in the new dock.`,
data: error ?? ([] as any),
status: 200,
});
}
});
export default r;

View File

@@ -0,0 +1,107 @@
import { formatInTimeZone } from "date-fns-tz";
import { Router } from "express";
import { gpQuery } from "../gpSql/gpSqlQuery.controller.js";
import {
type SqlGPQuery,
sqlGpQuerySelector,
} from "../gpSql/gpSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { startDate, endDate, glCode, includePlantToken } = req.query;
if (
!startDate ||
startDate === "" ||
!endDate ||
endDate === "" ||
!glCode ||
glCode === ""
) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "GpData",
message:
"The start date, end date, and gl code are required to run this query.",
data: [],
status: 400,
});
}
const sqlQuery = sqlGpQuerySelector(`gp.eom.data`) as SqlGPQuery;
if (!sqlQuery.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "GpData",
message:
"Failed to get GpData sql file please, please contact support if this continues.",
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
gpQuery(
sqlQuery.query
.replace("[startDate]", startDate as string)
.replace("[endDate]", endDate as string)
.replace("[gpCode]", glCode as string),
"Eom GpData data",
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "GpData",
message: `Error getting GpData data info`,
data: error as any,
notify: false,
status: 400,
});
}
return apiReturn(res, {
success: data.success,
level: data.success ? "info" : "error",
module: "eom",
subModule: "GpData",
message: data.message,
data:
includePlantToken === "true" && data.success
? data.data.map((i) => {
return {
plantToken: process.env.PROD_PLANT_TOKEN,
...i,
Date_Received: formatInTimeZone(
i.Date_Received,
"etc/utc",
"M/d/yyyy",
),
};
})
: data.data.map((i) => {
return {
...i,
Date_Received: formatInTimeZone(
i.Date_Received,
"etc/utc",
"M/d/yyyy",
),
};
}),
notify: false,
status: data.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,9 @@
/**
* The flow that will trigger all the history functions to run and store the data each day and clean up as needed
*
* if we are on usmcd1vms036 we will run a get request to all servers in the db so we can store that data as well.
*
* this will be stored in both. the vms036 functions will store in a bigger server so it can be pulled faster and in ssrs
*/
export const eomHistory = async () => {};

View File

@@ -0,0 +1,61 @@
import { format } from "date-fns";
import { eq } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { invHistoricalData } from "../db/schema/historicalInv.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
// the params we are wanting to add in. min required will be the month so we dont pass everything over
const { date } = req.query;
if (!date || date === "") {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "historical inv",
message:
"The day of the month is required to be included in order to pass.",
data: [],
status: 400,
});
}
// get the date passed over.
const { data, error } = await tryCatch(
db
.select()
.from(invHistoricalData)
.where(
eq(invHistoricalData.histDate, format(date as string, "yyyy-MM-dd")),
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "historical inv",
message:
"There was an error getting the historical data from the server.",
data: error as any,
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "eom",
subModule: "historical inv",
message: "Eom Historical Inv Data",
data: data,
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,77 @@
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { includePlantToken } = req.query;
const sqlQuery = sqlQuerySelector(`eom.lastPurchasePrice`) as SqlQuery;
if (!sqlQuery.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "lastPurchasePrice",
message:
"Failed to get last sales price sql file please, please contact support if this continues.",
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(
sqlQuery.query,
"Eom last purchase price data",
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "lastPurchasePrice",
message: `Error getting last purchase Price data info`,
data: error as any,
notify: false,
status: 400,
});
}
return apiReturn(res, {
success: data.success,
level: data.success ? "info" : "error",
module: "eom",
subModule: "lastPurchasePrice",
message: data.message,
data:
includePlantToken === "true" && data.success
? data.data.map((i) => {
return {
plantToken: process.env.PROD_PLANT_TOKEN,
...i,
//validDate: formatInTimeZone(i.validDate, "etc/utc", "M/d/yyyy"),
};
})
: data.data.map((i) => {
return {
...i,
//validDate: formatInTimeZone(i.validDate, "etc/utc", "M/d/yyyy"),
};
}),
notify: false,
status: data.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,89 @@
import { formatInTimeZone } from "date-fns-tz";
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { date, includePlantToken } = req.query;
if (!date || date === "") {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "lastSalesPrice",
message: "A date is required to run this query.",
data: [],
status: 400,
});
}
const sqlQuery = sqlQuerySelector(`eom.lastSalesPrice`) as SqlQuery;
if (!sqlQuery.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "lastSalesPrice",
message:
"Failed to get last sales price sql file please, please contact support if this continues.",
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(
sqlQuery.query.replace("[date]", date as string),
"Eom last sales price data",
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "lastSalesPrice",
message: `Error getting last Sales Price data info`,
data: error as any,
notify: false,
status: 400,
});
}
return apiReturn(res, {
success: data.success,
level: data.success ? "info" : "error",
module: "eom",
subModule: "lastSalesPrice",
message: data.message,
data:
includePlantToken === "true" && data.success
? data.data.map((i) => {
return {
plantToken: process.env.PROD_PLANT_TOKEN,
...i,
validDate: formatInTimeZone(i.validDate, "etc/utc", "M/d/yyyy"),
};
})
: data.data.map((i) => {
return {
...i,
validDate: formatInTimeZone(i.validDate, "etc/utc", "M/d/yyyy"),
};
}),
notify: false,
status: data.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,97 @@
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { startDate, endDate, includePlantToken } = req.query;
if (!startDate || startDate === "" || !endDate || endDate === "") {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "productionConsumption",
message: "The start date and end date are required to run this query.",
data: [],
status: 400,
});
}
const sqlQuery = sqlQuerySelector(`eom.productionConsumption`) as SqlQuery;
if (!sqlQuery.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "productionConsumption",
message:
"Failed to get production consumption sql file please, please contact support if this continues.",
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(
sqlQuery.query
.replace("[startDate]", startDate as string)
.replace("[endDate]", endDate as string),
"Eom production consumption data",
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "productionConsumption",
message: `Error getting production consumption data info`,
data: error as any,
notify: false,
status: 400,
});
}
return apiReturn(res, {
success: data.success,
level: data.success ? "info" : "error",
module: "eom",
subModule: "productionConsumption",
message: data.message,
data:
includePlantToken === "true" && data.success
? data.data.map((i) => {
return {
plantToken: process.env.PROD_PLANT_TOKEN,
...i,
// Prod_Date: formatInTimeZone(
// i.Prod_Date,
// "etc/utc",
// "M/d/yyyy",
// ),
};
})
: data.data.map((i) => {
return {
...i,
// Prod_Date: formatInTimeZone(
// i.Prod_Date,
// "etc/utc",
// "M/d/yyyy",
// ),
};
}),
notify: false,
status: data.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,98 @@
import { formatInTimeZone } from "date-fns-tz";
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { startDate, endDate, includePlantToken } = req.query;
if (!startDate || startDate === "" || !endDate || endDate === "") {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "purchased",
message: "The start date and end date are required to run this query.",
data: [],
status: 400,
});
}
const sqlQuery = sqlQuerySelector(`eom.purchased`) as SqlQuery;
if (!sqlQuery.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "purchased",
message:
"Failed to get purchased sql file please, please contact support if this continues.",
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(
sqlQuery.query
.replace("[startDate]", startDate as string)
.replace("[endDate]", endDate as string),
"Eom purchased data",
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "purchased",
message: `Error getting purchased data info`,
data: error as any,
notify: false,
status: 400,
});
}
return apiReturn(res, {
success: data.success,
level: data.success ? "info" : "error",
module: "eom",
subModule: "purchased",
message: data.message,
data:
includePlantToken === "true" && data.success
? data.data.map((i) => {
return {
plantToken: process.env.PROD_PLANT_TOKEN,
...i,
Received_Date: formatInTimeZone(
i.Received_Date,
"etc/utc",
"M/d/yyyy",
),
};
})
: data.data.map((i) => {
return {
...i,
Received_Date: formatInTimeZone(
i.Received_Date,
"etc/utc",
"M/d/yyyy",
),
};
}),
notify: false,
status: data.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,98 @@
import { formatInTimeZone } from "date-fns-tz";
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { startDate, endDate, includePlantToken } = req.query;
if (!startDate || startDate === "" || !endDate || endDate === "") {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "regrind",
message: "The start date and end date are required to run this query.",
data: [],
status: 400,
});
}
const sqlQuery = sqlQuerySelector(`eom.regrind`) as SqlQuery;
if (!sqlQuery.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "regrind",
message:
"Failed to get regrind sql file please, please contact support if this continues.",
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(
sqlQuery.query
.replace("[startDate]", startDate as string)
.replace("[endDate]", endDate as string),
"Eom regrind data",
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "regrind",
message: `Error getting regrind data info`,
data: error as any,
notify: false,
status: 400,
});
}
return apiReturn(res, {
success: data.success,
level: data.success ? "info" : "error",
module: "eom",
subModule: "regrind",
message: data.message,
data:
includePlantToken === "true" && data.success
? data.data.map((i) => {
return {
plantToken: process.env.PROD_PLANT_TOKEN,
...i,
Buchungsdatum: formatInTimeZone(
i.Buchungsdatum,
"etc/utc",
"M/d/yyyy",
),
};
})
: data.data.map((i) => {
return {
...i,
Buchungsdatum: formatInTimeZone(
i.Buchungsdatum,
"etc/utc",
"M/d/yyyy",
),
};
}),
notify: false,
status: data.success ? 200 : 400,
});
});
export default r;

35
backend/eom/eom.routes.ts Normal file
View File

@@ -0,0 +1,35 @@
import type { Express } from "express";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import gpData from "./eom.gpdata.route.js";
import historyInv from "./eom.historyInv.route.js";
import lastPurchasePrice from "./eom.lastPurchasePrice.route.js";
import lastSalesPrice from "./eom.lastSalesPrice.route.js";
import productionConsumption from "./eom.productionConsumption.route.js";
import purchased from "./eom.purchased.route.js";
import regrind from "./eom.regrind.route.js";
import soldItems from "./eom.soldItems.route.js";
export const setupEomRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/eom/historyInv`, featureCheck("eom"), historyInv);
app.use(`${baseUrl}/api/eom/purchased`, featureCheck("eom"), purchased);
app.use(
`${baseUrl}/api/eom/lastSalesPrice`,
featureCheck("eom"),
lastSalesPrice,
);
app.use(
`${baseUrl}/api/eom/lastPurchasePrice`,
featureCheck("eom"),
lastPurchasePrice,
);
app.use(
`${baseUrl}/api/eom/productionConsumption`,
featureCheck("eom"),
productionConsumption,
);
app.use(`${baseUrl}/api/eom/regrind`, featureCheck("eom"), regrind);
app.use(`${baseUrl}/api/eom/soldItems`, featureCheck("eom"), soldItems);
app.use(`${baseUrl}/api/eom/gpData`, featureCheck("eom"), gpData);
};

View File

@@ -0,0 +1,98 @@
import { formatInTimeZone } from "date-fns-tz";
import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { startDate, endDate, includePlantToken } = req.query;
if (!startDate || startDate === "" || !endDate || endDate === "") {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "soldItems",
message: "The start date and end date are required to run this query.",
data: [],
status: 400,
});
}
const sqlQuery = sqlQuerySelector(`eom.soldItems`) as SqlQuery;
if (!sqlQuery.success) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "soldItems",
message:
"Failed to get soldItems sql file please, please contact support if this continues.",
data: [],
status: 400,
});
}
const { data, error } = await tryCatch(
prodQuery(
sqlQuery.query
.replace("[startDate]", startDate as string)
.replace("[endDate]", endDate as string),
"Eom soldItems data",
),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "eom",
subModule: "soldItems",
message: `Error getting soldItems data info`,
data: error as any,
notify: false,
status: 400,
});
}
return apiReturn(res, {
success: data.success,
level: data.success ? "info" : "error",
module: "eom",
subModule: "soldItems",
message: data.message,
data:
includePlantToken === "true" && data.success
? data.data.map((i) => {
return {
plantToken: process.env.PROD_PLANT_TOKEN,
...i,
DeliveryDate: formatInTimeZone(
i.DeliveryDate,
"etc/utc",
"M/d/yyyy",
),
};
})
: data.data.map((i) => {
return {
...i,
DeliveryDate: formatInTimeZone(
i.DeliveryDate,
"etc/utc",
"M/d/yyyy",
),
};
}),
notify: false,
status: data.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,16 @@
select * from (
select
case when x.POPRCTNM is null then p.POPRCTNM else p.POPRCTNM end as RCT_Num,
PONUMBER PO,
p.VENDORID Supplier,
ITEMNMBR Item,
QTYSHPPD shipped,
UOFM Type,
TRXLOCTN Location,
case when CONVERT(DATE, x.receiptdate) is null then convert(date, p.DATERECD) else CONVERT(DATE, x.receiptdate) end as Date_Received
from ALPLA.dbo.pop10500 (nolock) as p
left join
ALPLA.dbo.POP10300 as x on p.POPRCTNM = x.POPRCTNM
WHERE TRXLOCTN LIKE '[gpCode]%' and p.POPTYPE = 1) a
where Date_Received BETWEEN '[startDate]' AND '[endDate]'

View File

@@ -3,7 +3,6 @@ import { Writable } from "node:stream";
import pino, { type Logger } from "pino";
import { db } from "../db/db.controller.js";
import { logs } from "../db/schema/logs.schema.js";
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { notifySystemIssue } from "./logger.notify.js";
//import build from "pino-abstract-transport";
@@ -37,7 +36,7 @@ const dbStream = new Writable({
subModule: obj?.subModule?.toLowerCase(),
hostname: obj?.hostname?.toLowerCase(),
message: obj.msg,
stack: obj?.stack,
stack: obj?.stack || obj?.error, // this will add in the error or stack depending on how we pass it.
})
.returning(),
);
@@ -50,10 +49,10 @@ const dbStream = new Writable({
notifySystemIssue(obj);
}
if (obj.room) {
emitToRoom(obj.room, res.data ? res.data[0] : obj);
}
emitToRoom("logs", res.data ? res.data[0] : obj);
// if (obj.room) {
// emitToRoom(obj.room, res.data ? res.data[0] : obj);
// }
// emitToRoom("logs", res.data ? res.data[0] : obj);
callback();
} catch (err) {
console.error("DB log insert error:", err);

View File

@@ -0,0 +1,24 @@
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { logs } from "../db/schema/logs.schema.js";
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
export async function handleLogInsertedNotification(id: string) {
const row = await db.query.logs.findFirst({
where: eq(logs.id, id),
});
if (!row) return;
// More targeted rooms.
if (row.module) {
emitToRoom(`logs:${row.module}`, row);
}
if (row.subModule) {
emitToRoom(`logs:${row.subModule}`, row);
}
// Everyone listening to all logs.
emitToRoom("logs", row);
}

View File

@@ -0,0 +1,92 @@
import XLSX from "xlsx";
import { excelDateStuff } from "../utils/excelToDate.utils.js";
import { postData } from "./logistics.dm.postData.js";
export const abbottForecast = async (sheet: any, user: any) => {
/*
This is the forecast but will only be triggered when the actual sheet is passed over from the orders in. this is being done this way due to the truck list being sent over as well.
*/
const customerId = 8;
const posting: any = [];
const customHeaders = [
"date",
"time",
"newton8oz",
"newton10oz",
"E",
"F",
"fDate",
"f8ozqty",
"I",
"J",
"K",
"L",
"M",
"f10ozqty",
];
const forecastData = XLSX.utils.sheet_to_json(sheet, {
range: 5, // Start at row 5 (index 4)
header: customHeaders,
defval: "", // Default value for empty cells
});
for (let i = 1; i < forecastData.length; i++) {
const row: any = forecastData[i];
//console.log(row);
//if (row.fDate == undefined) continue;
if (row.fDate !== "") {
const date = isNaN(row.fDate)
? new Date(row.fDate)
: excelDateStuff(row.fDate);
// for 8oz do
if (row.f8ozqty > 0) {
posting.push({
customerArticleNo: "45300DA",
quantity: row.f8ozqty,
requirementDate: date,
});
}
if (row.f10ozqty > 0) {
posting.push({
customerArticleNo: "43836DA",
quantity: row.f10ozqty,
requirementDate: date,
});
}
}
}
// the predefined data that will never change
const predefinedObject = {
receivingPlantId: process.env.PROD_PLANT_TOKEN ?? "test1",
documentName: `ForecastFromLST-${new Date(Date.now()).toLocaleString(
"en-US",
)}`,
sender: user.username || "lst-system",
customerId: customerId,
positions: [],
};
// add the new forecast to the predefined data
const updatedPredefinedObject = {
...predefinedObject,
positions: [...predefinedObject.positions, ...posting],
};
const forecast: any = await postData(
{
type: "forecast",
endpoint: "/public/v1.0/DemandManagement/DELFOR",
data: updatedPredefinedObject as any,
},
user,
);
return {
success: forecast.success,
message: forecast.message,
data: forecast.data,
};
};

View File

@@ -0,0 +1,86 @@
import XLSX from "xlsx";
import { excelDateStuff } from "../utils/excelToDate.utils.js";
import { postData } from "./logistics.dm.postData.js";
export const energizerForecast = async (data: any, user: any) => {
/**
* Post a standard forecast based on the standard template.
*/
const buffer = Buffer.from(data.buffer);
const workbook = XLSX.read(buffer, { type: "buffer" });
const sheet: any = workbook.Sheets.Sheet1;
// const range = XLSX.utils.decode_range(sheet["!ref"]);
// const headers = [
// "CustomerArticleNumber",
// "Quantity",
// "RequirementDate",
// "CustomerID",
// ];
// formatting the data
const rows = XLSX.utils.sheet_to_json(sheet, { header: 1 }) as any;
const posting: any = [];
const customerId = 44;
for (let i = 1; i < rows.length; i++) {
const row: any = rows[i];
const material = row[0];
if (material === undefined) continue;
for (let j = 1; j < row.length; j++) {
const qty = row[j];
if (qty && qty > 0) {
const requirementDate = rows[0][j]; // first row is dates
const date = Number.isNaN(requirementDate)
? new Date(requirementDate)
: excelDateStuff(requirementDate);
posting.push({
customerArticleNo: material,
quantity: qty,
requirementDate: date,
});
}
}
}
//console.log(posting);
// the predefined data that will never change
const predefinedObject = {
receivingPlantId: process.env.PROD_PLANT_TOKEN ?? "test1",
documentName: `ForecastFromLST-${new Date(Date.now()).toLocaleString(
"en-US",
)}`,
sender: user.username || "lst-system",
customerId: customerId,
positions: [],
};
// add the new forecast to the predefined data
const updatedPredefinedObject = {
...predefinedObject,
positions: [...predefinedObject.positions, ...posting],
};
//post it
const forecastData: any = await postData(
{
type: "forecast",
endpoint: "/public/v1.0/DemandManagement/DELFOR",
data: updatedPredefinedObject as any,
},
user,
);
return {
success: forecastData.success,
message: forecastData.message,
data: forecastData.data,
};
};

View File

@@ -0,0 +1,245 @@
import { addDays } from "date-fns";
import XLSX from "xlsx";
import { runDatamartQuery } from "../datamart/datamart.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
//import { sendEmail } from "../utils/sendEmail.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { postData } from "./logistics.dm.postData.js";
const customerID = 4;
export const lorealForecast = async (data: any, user: any) => {
/**
* Post a standard forecast based on the standard template.
*/
const buffer = Buffer.from(data.buffer);
const workbook = XLSX.read(buffer, { type: "buffer" });
const sheet: any = workbook.Sheets["Alpla HDPE"];
const range = XLSX.utils.decode_range(sheet["!ref"]);
const psheet: any = workbook.Sheets["Alpla PET"];
const prange = XLSX.utils.decode_range(psheet["!ref"]);
const headers = [];
for (let C = range.s.c; C <= range.e.c; ++C) {
const cellAddress = XLSX.utils.encode_cell({ r: 1, c: C }); // row 0 = Excel row 1
const cell = sheet[cellAddress];
headers.push(cell ? cell.v : undefined);
}
const pheaders = [];
for (let C = prange.s.c; C <= prange.e.c; ++C) {
const cellAddress = XLSX.utils.encode_cell({ r: 1, c: C }); // row 0 = Excel row 1
const cell = psheet[cellAddress];
pheaders.push(cell ? cell.v : undefined);
}
const ebmForeCastData: any = XLSX.utils.sheet_to_json(sheet, {
defval: "",
header: headers,
range: 3,
});
const petForeCastData: any = XLSX.utils.sheet_to_json(psheet, {
defval: "",
header: pheaders,
range: 3,
});
const ebmForecastData: any = [];
const missingSku: any = [];
const avSQLQuery = sqlQuerySelector(`datamart.activeArticles`) as SqlQuery;
if (!avSQLQuery.success) {
return returnFunc({
success: false,
level: "error",
module: "logistics",
subModule: "forecast",
message: `Error getting Article info`,
data: [avSQLQuery.message],
notify: true,
});
}
const { data: a, error: ae } = await tryCatch(
runDatamartQuery({ name: "activeArticles", options: {} }),
);
if (ae) {
return {
success: false,
message: "Error getting active av",
data: [],
};
}
const article: any = a?.data;
//console.log(article);
// process the ebm forcast
for (let i = 0; i < ebmForeCastData.length; i++) {
// bottle code
const sku = ebmForeCastData[i]["HDPE Bottle Code"];
// ignore the blanks
if (sku === "") continue;
// ignore zero qty
// if (ebmForeCastData[i][`Day ${i}`]) continue;
for (let f = 0; f <= 90; f++) {
const day = `Day ${f + 1}`;
// if (ebmForeCastData[i][day] === 0) continue;
const forcast = {
customerArticleNo: sku,
requirementDate: addDays(new Date(Date.now()), f), //excelDateStuff(parseInt(date)),
quantity: ebmForeCastData[i][day] ?? 0,
};
if (forcast.quantity === 0) continue;
// checking to make sure there is a real av to add to.
const activeAV = article.filter(
(c: any) =>
c?.CustomerArticleNumber === forcast.customerArticleNo.toString(),
);
if (activeAV.length === 0) {
if (typeof forcast.customerArticleNo === "number") {
missingSku.push(forcast);
}
continue;
}
ebmForecastData.push(forcast);
}
//console.log(ebmForeCastData.length);
}
// pet forecast
for (let i = 0; i < petForeCastData.length; i++) {
// bottle code
const sku = petForeCastData[i]["South PET Bottle Code"];
// ignore the blanks
if (sku === "") continue;
// ignore zero qty
// if (ebmForeCastData[i][`Day ${i}`]) continue;
for (let f = 0; f <= 90; f++) {
const day = `Day ${f + 1}`;
// if (ebmForeCastData[i][day] === 0) continue;
const forcast = {
customerArticleNo: sku,
requirementDate: addDays(new Date(Date.now()), f), //excelDateStuff(parseInt(date)),
quantity: petForeCastData[i][day] ?? 0,
};
if (forcast.quantity === 0 || forcast.quantity === "") continue;
if (forcast.customerArticleNo < 99999) {
//console.log(`Sku a normal av ${forcast.customerArticleNo}`);
continue;
}
// checking to make sure there is a real av to add to.
const activeAV = article.filter(
(c: any) =>
c?.CustomerArticleNumber === forcast.customerArticleNo.toString(),
);
if (activeAV.length === 0) {
if (typeof forcast.customerArticleNo === "number") {
missingSku.push(forcast);
}
continue;
}
ebmForecastData.push(forcast);
}
}
//console.log(comForecast);
// email the for the missing ones
// const missedGrouped = Object.values(
// missingSku.reduce((acc: any, item: any) => {
// const key = item.customerArticleNo;
// if (!acc[key]) {
// // first time we see this customer
// acc[key] = item;
// } else {
// // compare dates and keep the earliest
// if (
// new Date(item.requirementDate) < new Date(acc[key].requirementDate)
// ) {
// acc[key] = item;
// }
// }
// return acc;
// }, {}),
// );
// TODO: change this to a flagged notification so that he users can subscribe or leave it. this removes the hardcody shit.
// const emailSetup = {
// email:
// "Blake.matthes@alpla.com; Stuart.Gladney@alpla.com; Harold.Mccalister@alpla.com; Jenn.Osbourn@alpla.com",
// subject:
// missedGrouped.length > 0
// ? `Alert! There are ${missedGrouped.length}, missing skus.`
// : `Alert! There is a missing SKU.`,
// template: "missingLorealSkus",
// context: {
// items: missedGrouped,
// },
// };
// sendEmail(emailSetup);
// if the customerarticle number is not matching just ignore it
const predefinedObject = {
receivingPlantId: process.env.PROD_PLANT_TOKEN ?? "test1",
documentName: `ForecastFromLST-${new Date(Date.now()).toLocaleString(
"en-US",
)}`,
sender: user.username || "lst-system",
customerId: customerID,
positions: [],
};
const updatedPredefinedObject = {
...predefinedObject,
positions: [...predefinedObject.positions, ...ebmForecastData],
};
// console.log(updatedPredefinedObject);
// posting the data to the new backend so we can store the data.
const posting: any = await postData(
{
type: "forecast",
endpoint: "/public/v1.0/DemandManagement/DELFOR",
data: updatedPredefinedObject as any,
},
user,
);
return {
success: posting.success,
message: posting.message,
data: posting.data === "" ? ebmForecastData : posting.data,
};
};

View File

@@ -0,0 +1,182 @@
import XLSX from "xlsx";
import { runDatamartQuery } from "../datamart/datamart.controller.js";
import { db } from "../db/db.controller.js";
import { settings } from "../db/schema/settings.schema.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { excelDateStuff } from "../utils/excelToDate.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { postData } from "./logistics.dm.postData.js";
export const pNgForecast = async (data: any, user: any) => {
/**
* Post a standard forecast based on the standard template.
*/
const { data: s, error: e } = await tryCatch(db.select().from(settings));
if (e) {
return {
sucess: false,
message: `Error getting settings`,
data: e,
};
}
const pNg = s.filter((n: any) => n.name === "pNgAddress");
const avSQLQuery = sqlQuerySelector(`datamart.activeArticles`) as SqlQuery;
if (!avSQLQuery.success) {
return returnFunc({
success: false,
level: "error",
module: "logistics",
subModule: "forecast",
message: `Error getting Article info`,
data: [avSQLQuery.message],
notify: true,
});
}
const { data: a, error: ae } = await tryCatch(
runDatamartQuery({ name: "activeArticles", options: {} }),
);
if (ae) {
return {
success: false,
message: "Error getting active av",
data: [],
};
}
const article: any = a?.data;
const buffer = Buffer.from(data.buffer);
const workbook = XLSX.read(buffer, { type: "buffer" });
//const sheet: any = workbook.Sheets[sheetName];
const sheet: any = workbook.Sheets["SchedAgreementUIConfigSpreadshe"];
const range = XLSX.utils.decode_range(sheet["!ref"]);
const headers = [];
for (let C = range.s.c; C <= range.e.c; ++C) {
const cellAddress = XLSX.utils.encode_cell({ r: 0, c: C }); // row 0 = Excel row 1
const cell = sheet[cellAddress];
headers.push(cell ? cell.v : undefined);
}
//console.log(headers);
const forecastData: any = XLSX.utils.sheet_to_json(sheet, {
defval: "",
header: headers,
range: 1,
});
const groupedByCustomer: any = forecastData.reduce((acc: any, item: any) => {
const id = item.CustomerID;
if (!acc[id]) {
acc[id] = [];
}
acc[id].push(item);
return acc;
}, {});
const foreCastData: any = [];
for (const [customerID, forecast] of Object.entries(groupedByCustomer)) {
//console.log(`Running for Customer ID: ${customerID}`);
const newForecast: any = forecast;
const predefinedObject = {
receivingPlantId: process.env.PROD_PLANT_TOKEN ?? "test1",
documentName: `ForecastFromLST-${new Date(Date.now()).toLocaleString(
"en-US",
)}`,
sender: user.username || "lst-system",
customerId: pNg[0]?.value,
positions: [],
};
// map everything out for each order
const nForecast = newForecast.map((o: any) => {
// const invoice = i.filter(
// (i: any) => i.deliveryAddress === parseInt(customerID)
// );
// if (!invoice) {
// return;
// }
return {
customerArticleNo: parseInt(o["Customer Item No."] ?? "0", 10),
requirementDate: excelDateStuff(parseInt(o["Request Date"] ?? "0", 10)),
quantity: o["Remaining Qty to be Shipped"],
};
});
// check to make sure the av belongs in this plant.
const onlyNumbers = nForecast.filter((n: any) => n.quantity > 0);
const filteredForecast: any = [];
for (let i = 0; i < nForecast.length; i++) {
//console.log(nForecast[i].customerArticleNo);
const activeAV = article.filter(
(c: any) =>
c?.CustomerArticleNumber ===
nForecast[i]?.customerArticleNo.toString() &&
// validate it works via the default address
c?.IdAdresse === parseInt(pNg[0]?.value ?? "139", 10),
);
if (activeAV.length > 0) {
filteredForecast.push(onlyNumbers[i]);
}
}
if (filteredForecast.length === 0) {
console.log("Nothing to post");
return {
success: true,
message: "No forecast to be posted",
data: foreCastData,
};
}
// do that fun combining thing
const updatedPredefinedObject = {
...predefinedObject,
positions: [...predefinedObject.positions, ...filteredForecast],
};
//console.log(updatedPredefinedObject);
// post the orders to the server
const posting: any = await postData(
{
type: "forecast",
endpoint: "/public/v1.0/DemandManagement/DELFOR",
data: updatedPredefinedObject as any,
},
user,
);
foreCastData.push({
customer: customerID,
//totalOrders: orders?.length(),
success: posting.success,
message: posting.message,
data: posting.data,
});
}
return {
success: foreCastData[0].success,
message: foreCastData[0].message,
data: foreCastData,
};
};

View File

@@ -0,0 +1,104 @@
import * as XLSX from "xlsx";
import { excelDateStuff } from "../utils/excelToDate.utils.js";
import { postData } from "./logistics.dm.postData.js";
export const standardForecast = async (data: any, user: any) => {
/**
* Post a standard forecast based on the standard template.
*/
const plantToken = process.env.PROD_PLANT_TOKEN;
//const arrayBuffer = await data.arrayBuffer();
const buffer = Buffer.from(data.buffer);
const workbook = XLSX.read(buffer, { type: "buffer" });
const sheetName = workbook.SheetNames[0] as string;
const sheet = workbook.Sheets[sheetName] as any;
const headers = [
"CustomerArticleNumber",
"Quantity",
"RequirementDate",
"CustomerID",
];
const forecastData: any = XLSX.utils.sheet_to_json(sheet, {
defval: "",
header: headers,
range: 1,
});
const groupedByCustomer: any = forecastData.reduce((acc: any, item: any) => {
const id = item.CustomerID;
if (!acc[id]) {
acc[id] = [];
}
acc[id].push(item);
return acc;
}, {});
const foreCastData: any = [];
for (const [customerID, forecast] of Object.entries(groupedByCustomer)) {
//console.log(`Running for Customer ID: ${customerID}`);
const newForecast: any = forecast;
const predefinedObject = {
receivingPlantId: plantToken,
documentName: `ForecastFromLST-${new Date(Date.now()).toLocaleString(
"en-US",
)}`,
sender: user.username || "lst-system",
customerId: customerID,
positions: [],
};
// map everything out for each order
const nForecast = newForecast.map((o: any) => {
// const invoice = i.filter(
// (i: any) => i.deliveryAddress === parseInt(customerID)
// );
// if (!invoice) {
// return;
// }
return {
customerArticleNo: o.CustomerArticleNumber,
requirementDate: excelDateStuff(parseInt(o.RequirementDate)),
quantity: o.Quantity,
};
});
// do that fun combining thing
const updatedPredefinedObject = {
...predefinedObject,
positions: [...predefinedObject.positions, ...nForecast],
};
//console.log(updatedPredefinedObject);
// post the orders to the server
const posting: any = await postData(
{
type: "forecast",
endpoint: "/public/v1.0/DemandManagement/DELFOR",
data: updatedPredefinedObject as any,
},
user,
);
foreCastData.push({
customer: customerID,
//totalOrders: orders?.length(),
success: posting.success,
message: posting.message,
data: posting.data,
});
}
return {
success: foreCastData[0].success,
message: foreCastData[0].message,
data: foreCastData,
};
};

View File

@@ -0,0 +1,95 @@
import { Router } from "express";
import multer from "multer";
import { requireAuth } from "../middleware/auth.middleware.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { energizerForecast } from "./logistics.dm.forecast.map.energizer.js";
import { lorealForecast } from "./logistics.dm.forecast.map.loreal.js";
import { pNgForecast } from "./logistics.dm.forecast.map.pNg.js";
import { standardForecast } from "./logistics.dm.forecast.map.standard.js";
type ForecastResult = {
success?: boolean;
message?: string;
data?: unknown;
};
const r = Router();
const upload = multer({
storage: multer.memoryStorage(),
});
r.post("/", requireAuth, upload.single("file"), async (req, res) => {
if (!req.file) {
return apiReturn(res, {
success: false,
level: "error",
module: "dm",
subModule: "forecast",
message: "A file must be added to be able to run the forecast.",
data: [],
status: 400,
});
}
const { fileType } = req.body;
if (typeof fileType !== "string") {
return apiReturn(res, {
success: false,
level: "error",
module: "dm",
subModule: "forecast",
message: "A fileType must be provided.",
data: [],
status: 400,
});
}
//console.log("fileType:", req.body.fileType);
let result: ForecastResult;
switch (fileType) {
case "standard":
result = await standardForecast(req.file, req.user);
break;
case "loreal":
result = await lorealForecast(req.file, req.user);
break;
case "pg":
result = await pNgForecast(req.file, req.user);
break;
case "energizer":
result = await energizerForecast(req.file, req.user);
break;
default:
return apiReturn(res, {
success: false,
level: "error",
module: "dm",
subModule: "forecast",
message: `Invalid fileType: ${fileType}`,
data: [],
status: 400,
});
}
return apiReturn(res, {
success: result.success ?? false,
level: result.success ? "info" : "error",
module: "dm",
subModule: "forecast",
message: result.success
? "The forecast was accepted by Alplaprod 2.0 please check to make sure everything processed properly."
: (result.message as string),
data: result.data ?? ([] as any),
status: result.success ? 200 : 500,
});
});
export default r;

View File

@@ -0,0 +1,65 @@
import { db } from "../db/db.controller.js";
import { forecastImport } from "../db/schema/forecastImports.schema.js";
import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
type PostData = {
receivingPlantId: string;
documentName: string;
sender: string;
customerId: string;
positions: unknown[];
};
type Data = {
type: "orders" | "forecast";
endpoint: string;
data: PostData;
};
export const postData = async (data: Data, user: any) => {
const posting = await runProdApi(
{
method: "post",
endpoint: data.endpoint,
data: [data.data],
},
"Forecast post",
);
if (!posting?.success) {
return returnFunc({
success: false,
level: "error",
module: "dm",
subModule: data.type === "orders" ? "orders" : "forecast",
message:
posting?.message ??
`Error in posting the ${data.type === "orders" ? "orders" : "forecast"} data`,
data: posting?.data ?? [],
notify: false,
});
}
if (posting.success) {
if (data.type === "forecast") {
await db.insert(forecastImport).values({
receivingPlantId: data.data.receivingPlantId ?? "test1",
documentName: data.data.documentName ?? "forecast-data-missing",
sender: data.data.sender ?? "lst-user",
customerId: data.data.customerId ?? "0",
rawData: data ?? [],
add_user: user.username ?? undefined,
upd_user: user.username ?? undefined,
});
}
return returnFunc({
success: true,
level: "info",
module: "dm",
subModule: data.type === "orders" ? "orders" : "forecast",
message: posting?.message ?? "",
data: (data.data as any) ?? [],
notify: false,
});
}
};

View File

@@ -0,0 +1,79 @@
import { format } from "date-fns";
import { Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { excelTemplate, type Template } from "../utils/excelTemplates.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", requireAuth, async (req, res) => {
const { filename } = req.query;
const templateNames = ["orders", "forecast"];
if (typeof filename !== "string" || !templateNames.includes(filename)) {
return apiReturn(res, {
success: false,
level: "error",
module: "dm",
subModule: "template",
message: "You are required to pass over the template name.",
data: [],
status: 500,
});
}
const name = `${filename}-Template-${format(
new Date(Date.now()),
"M-d-yyyy",
)}.xlsx`;
const standardHeaders = [
"CustomerArticleNumber",
"CustomerOrderNumber",
"CustomerLineNumber",
"CustomerRealeaseNumber",
"Quantity",
"DeliveryDate",
"CustomerID",
"Remark",
// "InvoiceID",
];
const forecastHeaders = [
"CustomerArticleNumber",
"Quantity",
"RequirementDate",
"CustomerID",
];
const template = {
name,
headers: filename === "orders" ? standardHeaders : forecastHeaders,
} as Template;
//console.log(template);
const { data, error } = await tryCatch(excelTemplate(template));
if (error || !data) {
return apiReturn(res, {
success: false,
level: "error",
module: "dm",
subModule: "template",
message: "There was an error creating the Excel template.",
data: [],
status: 500,
});
}
res.set({
"Content-Type":
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Content-Disposition": `attachment; filename="${name}"`,
});
return res.status(200).send(data);
});
export default r;

View File

@@ -0,0 +1,21 @@
import type { Express } from "express";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import forecast from "./logistics.dm.forecast.route.js";
import createTemplate from "./logistics.dm.template.route.js";
export const setupLogisticsRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(
`${baseUrl}/api/logistics/dm/template`,
featureCheck("demandManagement"),
createTemplate,
);
app.use(
`${baseUrl}/api/logistics/dm/forecast`,
featureCheck("demandManagement"),
forecast,
);
// all other system should be under /api/system/*
};

View File

@@ -49,7 +49,7 @@ const historicalInvImport = async () => {
});
}
if (data?.length === 0) {
if (data.length === 0) {
const avSQLQuery = sqlQuerySelector(`datamart.activeArticles`) as SqlQuery;
if (!avSQLQuery.success) {
@@ -139,7 +139,7 @@ const historicalInvImport = async () => {
subModule: "inv",
message: `Error adding historical data to lst db`,
data: errorImport as any,
notify: true,
notify: false,
});
}

View File

@@ -13,7 +13,7 @@ router.post("/", async (req, res) => {
await db
.update(scanUser)
.set({ lastScan: sql`NOW()` })
.where(eq(scanUser.name, body.name));
.where(eq(scanUser.name, body.user));
} catch (error) {
console.log(error);
}

View File

@@ -22,7 +22,6 @@ r.patch(
requirePermission({ notifications: ["update"] }),
async (req, res: Response) => {
const { id } = req.params;
try {
const validated = updateNote.parse(req.body);
@@ -37,6 +36,7 @@ r.patch(
await modifiedNotification(id as string);
if (nError) {
return apiReturn(res, {
success: false,
level: "error",
@@ -58,6 +58,7 @@ r.patch(
status: 200,
});
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({

View File

@@ -21,6 +21,7 @@ type Releases = {
ReleaseNumber: number;
DeliveryState: number;
DeliveryDate: Date;
ReleaseState: number;
LineItemHumanReadableId: number;
ArticleAlias: string;
LoadingUnits: string;
@@ -31,6 +32,16 @@ type Releases = {
DeliveryAddressHumanReadableId: string;
AdditionalInformation1: string;
};
// TODO: add these docs into the db
const actaulDocks = [
{ name: "cermac", dockId: "bcb17fae-0b1a-47a7-9fbf-594c5ebccce9" },
{ name: "matrix", dockId: "3e32cbfc-49f4-4138-b491-9d5df9c94754" },
{ name: "gerber", dockId: "9109e789-6c15-4cd9-87cb-de1b18627b6d" },
{ name: "rb", dockId: "6be02526-6183-4789-a73f-e0aa155e6d1e" },
//test server dock
{ name: "second", dockId: "e87c92bd-13b4-4f7e-bf5e-b0182884c47a" },
];
const timeZone = process.env.TIMEZONE as string;
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
const log = createLogger({ module: "opendock", subModule: "releaseMonitor" });
@@ -64,6 +75,7 @@ let lastCheck = formatInTimeZone(
// };
const postRelease = async (release: Releases) => {
log.debug({}, `Release: ${release.ReleaseNumber} is about to be validated`);
if (!odToken.odToken) {
log.info({}, "Getting Auth Token");
await getToken();
@@ -82,21 +94,55 @@ const postRelease = async (release: Releases) => {
where: (u, { eq }) => eq(u.name, "defaultLoadType"),
});
// check if the release has the data in it
// check if the release has the loadtype in it
const releaseLoadtypeCheck = (release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim())
.includes("drop");
// allowed to schedule now, as long as we see od in here somewhere
const releaseOkToSchedule = (release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim())
.includes("od");
// dock was sent over
const releaseDockInfo = actaulDocks.some((dock) =>
(release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim())
.includes(dock.name.toLowerCase()),
);
const opendDockArticleCheck = await db.query.opendockArticleSetup.findFirst({
where: (table, { and, eq }) =>
and(
eq(table.av, release.LineItemArticleWeight),
eq(table.av, release.LineItemHumanReadableId),
eq(table.customer, release.DeliveryAddressHumanReadableId),
),
});
// selected dock
const releaseDocks = (release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim());
const matchedDock = actaulDocks.find((dock) =>
releaseDocks.includes(dock.name.toLowerCase()),
);
const setDock =
// validate we dont have the dock in the release
releaseDockInfo
? matchedDock?.dockId
: // validate we dont have the dock in the aritcle check
(actaulDocks.find((d) => d.name === opendDockArticleCheck?.dock)
?.dockId ?? process.env.DEFAULT_DOCK);
// TODO: add in docks from lst db here to make it more universal for the team
/**
* ReleaseState
@@ -127,7 +173,7 @@ const postRelease = async (release: Releases) => {
userId: process.env.DEFAULT_CARRIER, // this should be the carrierid
loadTypeId: process.env.DEFAULT_LOAD_TYPE, // well get this and make it a default one
// TODO: look in the remarks in the release and if its says
dockId: process.env.DEFAULT_DOCK, // this the warehouse we want it in to start out
dockId: setDock, // this the warehouse we want it in to start out
refNumbers: [release.ReleaseNumber],
//refNumber: release.ReleaseNumber,
start: release.DeliveryDate,
@@ -230,11 +276,65 @@ const postRelease = async (release: Releases) => {
if (existing) {
const id = existing.openDockAptId;
// deal with canceled stuff as we want this gone off od
if (release.ReleaseState === 2 || release.ReleaseState === 4) {
// delete the order in od and change the state to canceled in lst
try {
const response = await axios.delete(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
// {
// hardDelete: true,
// },
);
if (response.status === 400) {
log.error({}, response.data.data.message);
return;
}
// update the release in the db leaving as insert just incase something weird happened
try {
await db
.update(opendockApt)
.set({
status: "canceled",
upd_date: sql`Now()`,
})
.where(eq(opendockApt.release, release.ReleaseNumber))
.returning();
log.info({}, `${release.ReleaseNumber} was canceled`);
} catch (e) {
log.error(
{ stack: e },
`Error canceling the release: ${release.ReleaseNumber}`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
//console.info(newDockApt);
log.error(
{ stack: e.response.data },
`An error has occurred during canceling of the release: ${release.ReleaseNumber}`,
);
return;
}
return;
}
if (
(releaseLoadtypeCheck ||
opendDockArticleCheck?.loadType === "drop" ||
defaultDock?.value === "drop") &&
(release.DeliveryState === 0 || release.DeliveryState === 1)
release.DeliveryState === 2
) {
const setArrival = { ...newDockApt, status: "Arrived" };
@@ -348,7 +448,12 @@ const postRelease = async (release: Releases) => {
return;
}
} else {
// changing to only trigger the change if the state is 2 meaning it has a scan to it and already in progress of being loaded.
} else if (
release.DeliveryState === 0 ||
release.DeliveryState === 1 ||
release.DeliveryState === 2
) {
try {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
@@ -400,10 +505,66 @@ const postRelease = async (release: Releases) => {
`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
);
return;
}
// if we are finished we need to set to completed
} else if (release.DeliveryState === 3 || release.DeliveryState === 4) {
try {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
newDockApt,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
if (response.status === 400) {
log.error({}, response.data.data.message);
return;
}
// update the release in the db leaving as insert just incase something weird happened
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.release,
set: {
openDockAptId: response.data.data.id,
appointment: response.data.data,
status: "completed",
upd_date: sql`NOW()`,
},
})
.returning();
log.info({}, `${release.ReleaseNumber} was updated`);
} catch (e) {
log.error(
{ stack: e },
`Error updating the release: ${release.ReleaseNumber}`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
//console.info(newDockApt);
log.error(
{ stack: e.response.data },
`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
);
return;
}
}
} else {
} else if (opendDockArticleCheck?.loadType === "live") {
try {
const response = await axios.post(
`${process.env.OPENDOCK_URL}/appointment`,
@@ -458,6 +619,136 @@ const postRelease = async (release: Releases) => {
return;
}
} else if (
(releaseLoadtypeCheck ||
opendDockArticleCheck?.loadType === "drop" ||
defaultDock?.value === "drop") &&
releaseOkToSchedule
) {
try {
const response = await axios.post(
`${process.env.OPENDOCK_URL}/appointment`,
newDockApt,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
// we need the id,release#,status from this response, store it in lst, check if we have a release so we can just update it.
// this will be utilized when we are listening for the changes to the apts. that way we can update the state to arrived. we will run our own checks on this guy during the incoming messages.
if (response.status === 400) {
log.error({}, response.data.data.message);
return;
}
// the response to make it simple we want response.data.id, response.data.relNumber, status will be defaulted to Scheduled if we created it here.
// TODO: add this release data to our db. but save it in json format and well parse it out. that way we future proof it and have everything in here vs just a few things
//console.info(response.data.data, "Was Created");
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.release,
set: {
openDockAptId: response.data.data.id,
appointment: response.data.data,
upd_date: sql`NOW()`,
},
})
.returning();
log.info({}, `${release.ReleaseNumber} was created`);
} catch (e) {
log.error({ stack: e }, "Error creating new release");
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
log.error(
{ stack: e?.response?.data },
`Error posting new release to opendock, ${release.ReleaseNumber}`,
);
return;
}
} else {
// try {
// const response = await axios.post(
// `${process.env.OPENDOCK_URL}/appointment`,
// newDockApt,
// {
// headers: {
// "content-type": "application/json; charset=utf-8",
// Authorization: `Bearer ${odToken.odToken}`,
// },
// },
// );
// // we need the id,release#,status from this response, store it in lst, check if we have a release so we can just update it.
// // this will be utilized when we are listening for the changes to the apts. that way we can update the state to arrived. we will run our own checks on this guy during the incoming messages.
// if (response.status === 400) {
// log.error({}, response.data.data.message);
// return;
// }
// // the response to make it simple we want response.data.id, response.data.relNumber, status will be defaulted to Scheduled if we created it here.
// // TODO: add this release data to our db. but save it in json format and well parse it out. that way we future proof it and have everything in here vs just a few things
// //console.info(response.data.data, "Was Created");
// try {
// await db
// .insert(opendockApt)
// .values({
// release: release.ReleaseNumber,
// openDockAptId: response.data.data.id,
// appointment: response.data.data,
// })
// .onConflictDoUpdate({
// target: opendockApt.release,
// set: {
// openDockAptId: response.data.data.id,
// appointment: response.data.data,
// upd_date: sql`NOW()`,
// },
// })
// .returning();
// log.info({}, `${release.ReleaseNumber} was created`);
// } catch (e) {
// log.error({ stack: e }, "Error creating new release");
// }
// // biome-ignore lint/suspicious/noExplicitAny: to many possibilities
// } catch (e: any) {
// log.error(
// { stack: e?.response?.data },
// `Error posting new release to opendock, ${release.ReleaseNumber}`,
// );
// return;
// }
log.info(
{
stack: {
release: release.ReleaseNumber,
releaseLoadtypeCheck,
articleLoadType: opendDockArticleCheck?.loadType,
defaultLoadType: defaultDock?.value,
releaseOkToSchedule,
},
},
`Skipping OpenDock post - release: ${release.ReleaseNumber} is not allowed to schedule`,
);
return;
}
await delay(750); // rate limit protection

View File

@@ -0,0 +1,73 @@
import axios from "axios";
import { eq, sql } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { opendockApt } from "../db/schema/opendock_apt.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { odToken } from "./opendock.utils.js";
const r = Router();
r.patch("/:id", async (req, res) => {
//const limit
const { id } = req.params;
try {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}/undo-latest-status`,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
if (response.status === 400) {
return apiReturn(res, {
success: false,
level: "error",
module: "opendock",
subModule: "apt",
message: response.data.data.message,
data: [],
status: 400,
});
}
// update the release in the db
const { data } = await tryCatch(
db
.update(opendockApt)
.set({
appointment: response.data.data,
upd_date: sql`NOW()`,
})
.where(eq(opendockApt.openDockAptId, id)),
);
return apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "apt",
message: `The release was reverted to the last state.`,
data: data as any,
status: 200,
});
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
return apiReturn(res, {
success: false,
level: "error",
module: "opendock",
subModule: "apt",
message: `An error updating the release in opendock`,
data: e.response.data,
status: 400,
});
}
});
export default r;

View File

@@ -137,7 +137,7 @@ r.delete("/:id", async (req, res) => {
.where(eq(opendockArticleSetup.id, id))
.returning();
return apiReturn(res, {
success: false,
success: true,
level: "info", //connect.success ? "info" : "error",
module: "opendock",
subModule: "articleCheck",

View File

@@ -1,9 +1,9 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import undo from "./openDockUndoLastStatus.js";
import articleCheck from "./opendock.articleCheck.route.js";
import getApt from "./opendockGetRelease.route.js";
import getApt from "./opendockRelease.route.js";
export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
//setup all the routes
@@ -21,4 +21,11 @@ export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
requireAuth,
articleCheck,
);
app.use(
`${baseUrl}/api/opendock/undo-latest-status`,
featureCheck("opendock_sync"),
requireAuth,
undo,
);
};

View File

@@ -12,7 +12,7 @@ export let odToken: ODToken = {
};
export const getToken = async () => {
const log = createLogger({ module: "opendock", subModule: "releaseMonitor" });
const log = createLogger({ module: "opendock", subModule: "auth" });
try {
const { status, data } = await axios.post(
`${process.env.OPENDOCK_URL}/auth/login`,
@@ -29,7 +29,8 @@ export const getToken = async () => {
odToken = { odToken: data.access_token, tokenDate: new Date() };
log.info({ odToken }, "Token added");
return;
} catch (e) {
log.error({ error: e }, "Error getting/refreshing token");
log.error({ stack: e }, "Error getting/refreshing token");
}
};

View File

@@ -1,40 +0,0 @@
import { desc, gte, sql } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { opendockApt } from "../db/schema/opendock_apt.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (_, res) => {
//const limit
const daysCreated = 30;
const { data } = await tryCatch(
db
.select()
.from(opendockApt)
.where(
gte(
opendockApt.createdAt,
sql.raw(`NOW() - INTERVAL '${daysCreated} days'`),
),
)
.orderBy(desc(opendockApt.createdAt))
.limit(500),
);
apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "apt",
message: `The first ${data?.length} Apt(s) that were created in the last ${daysCreated} `,
data: data ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,124 @@
import axios from "axios";
import { and, desc, eq, gte, sql } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { opendockApt } from "../db/schema/opendock_apt.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { odToken } from "./opendock.utils.js";
const r = Router();
r.get("/", async (req, res) => {
//const limit
const daysCreated = req.query.daysCreated ?? 30;
const { data } = await tryCatch(
db
.select()
.from(opendockApt)
.where(
and(
gte(
opendockApt.upd_date,
sql.raw(`NOW() - INTERVAL '${daysCreated} days'`),
),
eq(opendockApt.status, "active"),
),
)
.orderBy(desc(opendockApt.upd_date))
.limit(500),
);
apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "apt",
message: `The first ${data?.length} Apt(s) that were created in the last ${daysCreated} `,
data: data ?? [],
status: 200,
});
});
r.delete("/:id", async (req, res) => {
const { id } = req.params;
const { data: releaseInfo } = (await tryCatch(
db
.select()
.from(opendockApt)
.where(eq(opendockApt.id, id as string)),
)) as any;
if (releaseInfo.length === 0) {
return apiReturn(res, {
success: false,
level: "error",
module: "opendock",
subModule: "apt",
message: "Invalid release id passed over please try again",
data: [],
status: 400,
});
}
try {
const response = await axios.delete(
`${process.env.OPENDOCK_URL}/appointment/${releaseInfo[0].appointment.id}`,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
// {
// hardDelete: true,
// },
);
if (response.status === 400) {
return apiReturn(res, {
success: false,
level: "error",
module: "opendock",
subModule: "apt",
message: response.data.data.message,
data: [],
status: 400,
});
}
// update the release in the db leaving as insert just in-case something weird happened
const { data } = await tryCatch(
db
.delete(opendockApt)
.where(eq(opendockApt.id, id as string))
.returning(),
);
return apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "apt",
message: `The release was deleted, this is un unrecoverable`,
data: data as any,
status: 200,
});
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
return apiReturn(res, {
success: false,
level: "error",
module: "opendock",
subModule: "apt",
message: `An error deleting the release in opendock`,
data: e.response.data,
status: 400,
});
}
});
export default r;

View File

@@ -1,74 +1,73 @@
use [test1_AlplaPROD2.0_Read]
DECLARE @StartDate DATE = '[startDate]' -- 2025-1-1
DECLARE @EndDate DATE = '[endDate]' -- 2025-1-31
SELECT
r.[ArticleHumanReadableId]
,[ReleaseNumber]
,h.CustomerOrderNumber
,x.CustomerLineItemNumber
,[CustomerReleaseNumber]
,[ReleaseState]
,[DeliveryState]
,ea.JournalNummer as BOL_Number
,[ReleaseConfirmationState]
,[PlanningState]
DECLARE @StartDate DATE = '[startDate]'
DECLARE @EndDate DATE = '[endDate]'
;WITH bol_20 AS ( -- 2.0 BOL, one per release (newest doc wins)
SELECT pos.ReleaseId,
dd.JournalNumber,
ROW_NUMBER() OVER (PARTITION BY pos.ReleaseId
ORDER BY dd.ShippingDate DESC) AS rn
FROM [outboundDelivery].[DeliveryDocumentPosition] (nolock) pos
JOIN [outboundDelivery].[DeliveryDocument] (nolock) dd
ON dd.Id = pos.DeliveryDocumentId
-- WHERE dd.DocumentType = <BOL value> -- see note below
)
SELECT
r.[ArticleHumanReadableId]
,[ReleaseNumber]
,h.CustomerOrderNumber
,x.CustomerLineItemNumber
,[CustomerReleaseNumber]
,[ReleaseState]
,[DeliveryState]
,COALESCE(ea.JournalNummer, bol_20.JournalNumber) AS BOL_Number -- 1.0 or 2.0
,[ReleaseConfirmationState]
,[PlanningState]
,format(r.[OrderDate], 'yyyy-MM-dd HH:mm') as OrderDate
--,r.[OrderDate]
,FORMAT(r.[DeliveryDate], 'yyyy-MM-dd HH:mm') as DeliveryDate
--,r.[DeliveryDate]
,FORMAT(r.[LoadingDate], 'yyyy-MM-dd HH:mm') as LoadingDate
--,r.[LoadingDate]
,[Quantity]
,[DeliveredQuantity]
,r.[AdditionalInformation1]
,r.[AdditionalInformation2]
,[TradeUnits]
,[LoadingUnits]
,[Trucks]
,[LoadingToleranceType]
,[SalesPrice]
,[Currency]
,[QuantityUnit]
,[SalesPriceRemark]
,r.[Remark]
,[Irradiated]
,r.[CreatedByEdi]
,[DeliveryAddressHumanReadableId]
,DeliveryAddressDescription
,[CustomerArtNo]
,[TotalPrice]
,r.[ArticleAlias]
,[Quantity]
,[DeliveredQuantity]
,r.[AdditionalInformation1]
,r.[AdditionalInformation2]
,[TradeUnits]
,[LoadingUnits]
,[Trucks]
,[LoadingToleranceType]
,[SalesPrice]
,[Currency]
,[QuantityUnit]
,[SalesPriceRemark]
,r.[Remark]
,[Irradiated]
,r.[CreatedByEdi]
,[DeliveryAddressHumanReadableId]
,DeliveryAddressDescription
,[CustomerArtNo]
,[TotalPrice]
,r.[ArticleAlias]
FROM [order].[Release] (nolock) AS r
LEFT JOIN [order].LineItem AS x ON r.LineItemId = x.id
LEFT JOIN [order].Header AS h ON x.HeaderId = h.id
FROM [order].[Release] (nolock) as r
-- 1.0 BOL (legacy) — unchanged
LEFT JOIN AlplaPROD_test1.dbo.V_LadePlanungenLadeAuftragAbruf (nolock) AS zz
ON zz.AbrufIdAuftragsAbruf = r.ReleaseNumber
LEFT JOIN (
SELECT * FROM (
SELECT ROW_NUMBER() OVER (PARTITION BY IdJournal ORDER BY add_date DESC) AS RowNum, *
FROM [AlplaPROD_test1].[dbo].[T_Lieferungen] (nolock)
) t WHERE RowNum = 1
) AS ea ON zz.IdLieferschein = ea.IdJournal
left join
[order].LineItem as x on
-- 2.0 BOL (new)
LEFT JOIN bol_20 ON bol_20.ReleaseId = r.Id AND bol_20.rn = 1
r.LineItemId = x.id
left join
[order].Header as h on
x.HeaderId = h.id
--bol stuff
left join
AlplaPROD_test1.dbo.V_LadePlanungenLadeAuftragAbruf (nolock) as zz
on zz.AbrufIdAuftragsAbruf = r.ReleaseNumber
left join
(select * from (SELECT
ROW_NUMBER() OVER (PARTITION BY IdJournal ORDER BY add_date DESC) AS RowNum
,*
FROM [AlplaPROD_test1].[dbo].[T_Lieferungen] (nolock)) x
where RowNum = 1) as ea on
zz.IdLieferschein = ea.IdJournal
where
--r.ReleaseNumber = 1452
r.DeliveryDate between @StartDate AND @EndDate
WHERE r.DeliveryDate BETWEEN @StartDate AND @EndDate
and DeliveredQuantity > 0
--and r.ArticleHumanReadableId in ([articles])
--and Journalnummer = 169386

View File

@@ -1,79 +0,0 @@
use AlplaPROD_test1
/**
move this over to the delivery date range query once we have the shift data mapped over correctly.
update the psi stuff on this as well.
**/
DECLARE @StartDate DATE = '[startDate]' -- 2025-1-1
DECLARE @EndDate DATE = '[endDate]' -- 2025-1-31
SELECT
r.[ArticleHumanReadableId]
,[ReleaseNumber]
,h.CustomerOrderNumber
,x.CustomerLineItemNumber
,[CustomerReleaseNumber]
,[ReleaseState]
,[DeliveryState]
,ea.JournalNummer as BOL_Number
,[ReleaseConfirmationState]
,[PlanningState]
--,format(r.[OrderDate], 'yyyy-MM-dd HH:mm') as OrderDate
,r.[OrderDate]
--,FORMAT(r.[DeliveryDate], 'yyyy-MM-dd HH:mm') as DeliveryDate
,r.[DeliveryDate]
--,FORMAT(r.[LoadingDate], 'yyyy-MM-dd HH:mm') as LoadingDate
,r.[LoadingDate]
,[Quantity]
,[DeliveredQuantity]
,r.[AdditionalInformation1]
,r.[AdditionalInformation2]
,[TradeUnits]
,[LoadingUnits]
,[Trucks]
,[LoadingToleranceType]
,[SalesPrice]
,[Currency]
,[QuantityUnit]
,[SalesPriceRemark]
,r.[Remark]
,[Irradiated]
,r.[CreatedByEdi]
,[DeliveryAddressHumanReadableId]
,DeliveryAddressDescription
,[CustomerArtNo]
,[TotalPrice]
,r.[ArticleAlias]
FROM [order].[Release] (nolock) as r
left join
[order].LineItem as x on
r.LineItemId = x.id
left join
[order].Header as h on
x.HeaderId = h.id
--bol stuff
left join
AlplaPROD_test1.dbo.V_LadePlanungenLadeAuftragAbruf (nolock) as zz
on zz.AbrufIdAuftragsAbruf = r.ReleaseNumber
left join
(select * from (SELECT
ROW_NUMBER() OVER (PARTITION BY IdJournal ORDER BY add_date DESC) AS RowNum
,*
FROM [AlplaPROD_test1].[dbo].[T_Lieferungen] (nolock)) x
where RowNum = 1) as ea on
zz.IdLieferschein = ea.IdJournal
where
r.ArticleHumanReadableId in ([articles])
--r.ReleaseNumber = 1452
and r.DeliveryDate between @StartDate AND @EndDate
--and DeliveredQuantity > 0
--and Journalnummer = 169386

View File

@@ -1,32 +1,72 @@
use AlplaPROD_test1
declare @start_date nvarchar(30) = '[startDate]' --'2025-01-01'
declare @end_date nvarchar(30) = '[endDate]' --'2025-08-09'
/*
articles will need to be passed over as well as the date structure we want to see
*/
use [test1_AlplaPROD2.0_Read]
select x.IdArtikelvarianten As Article,
ProduktionAlias as Description,
standort as MachineId,
MaschinenBezeichnung as MachineName,
--MaschZyklus as PlanningCycleTime,
x.IdProdPlanung as LotNumber,
FORMAT(ProdTag, 'MM/dd/yyyy') as ProductionDay,
x.planMenge as TotalPlanned,
ProduktionMenge as QTYPerDay,
round(ProduktionMengeVPK, 2) PalDay,
Status as finished
--MaschStdAuslastung as nee
from dbo.V_ProdLosProduktionJeProdTag_PLANNING (nolock) as x
left join
dbo.V_ProdPlanung (nolock) as p on
x.IdProdPlanung = p.IdProdPlanung
where ProdTag between @start_date and @end_date
and p.IdArtikelvarianten in ([articles])
--and V_ProdLosProduktionJeProdTag_PLANNING.IdKunde = 10
--and IdProdPlanung = 18442
order by ProdTag desc
DECLARE @start_date date = '[startDate]'; --'2025-01-01'
DECLARE @end_date date = '[endDate]'; --'2025-08-09'
DECLARE @tz sysname = 'Eastern Standard Time'; -- usday1; use 'Central Standard Time' for usksc1
DECLARE @shiftSeconds int = 7*3600; -- 07:00 production-day anchor
--TODO: add in the proper time zone based on the env, get correcr shift time as well
;WITH src AS (
SELECT
pl.RunningNumber, pl.ArticleHumanReadableId, pl.ArticleAlias, pl.ArticleDescription,
pl.MachineLocation, pl.MachineDescription, pl.ProductionLotState, pl.ProductionInterrupt,
pl.PlanQuantityPieces, pl.PlanQuantityLoadingUnit,
CAST(pl.ProdStart AT TIME ZONE @tz AS datetime2(0)) AS StartLocal,
CAST(pl.PlanEnd AT TIME ZONE @tz AS datetime2(0)) AS EndLocal
FROM productionScheduling.ProductionLot AS pl
WHERE pl.PlanEnd > pl.ProdStart
and pl.publishState = 1
and pl.ArticleHumanReadableId IN ([articles]) -- <-- article filter (was IdArtikelvarianten)
--AND pl.RunningNumber = 28094 -- <-- lot filter (was IdProdPlanung); comment out for all
),
calc AS (
SELECT *,
DATEADD(SECOND, @shiftSeconds,
CAST(CASE WHEN DATEDIFF(SECOND, CAST(StartLocal AS date), StartLocal) >= @shiftSeconds
THEN CAST(StartLocal AS date)
ELSE DATEADD(DAY,-1, CAST(StartLocal AS date)) END AS datetime2(0))) AS FirstBoundary
FROM src
),
days AS ( -- one row per production day the lot touches
SELECT RunningNumber, ArticleHumanReadableId, ArticleAlias, ArticleDescription, MachineLocation,
MachineDescription, ProductionLotState, PlanQuantityPieces, PlanQuantityLoadingUnit,
StartLocal, EndLocal, FirstBoundary AS DayStart
FROM calc
UNION ALL
SELECT RunningNumber, ArticleHumanReadableId, ArticleAlias, ArticleDescription, MachineLocation,
MachineDescription, ProductionLotState, PlanQuantityPieces, PlanQuantityLoadingUnit,
StartLocal, EndLocal, DATEADD(DAY,1,DayStart)
FROM days
WHERE DATEADD(DAY,1,DayStart) < EndLocal
),
seg AS ( -- working seconds inside each production day
SELECT *,
DATEDIFF(SECOND,
CASE WHEN StartLocal > DayStart THEN StartLocal ELSE DayStart END,
CASE WHEN EndLocal < DATEADD(DAY,1,DayStart) THEN EndLocal ELSE DATEADD(DAY,1,DayStart) END
) AS SegSec
FROM days
),
cum AS ( -- cumulative seconds for telescoping round
SELECT *,
SUM(SegSec) OVER (PARTITION BY RunningNumber ORDER BY DayStart ROWS UNBOUNDED PRECEDING) AS CumEnd,
SUM(SegSec) OVER (PARTITION BY RunningNumber ORDER BY DayStart ROWS UNBOUNDED PRECEDING) - SegSec AS CumStart,
SUM(SegSec) OVER (PARTITION BY RunningNumber) AS DenomSec
FROM seg
)
SELECT
ArticleHumanReadableId AS Article,
ArticleAlias AS Description,
MachineLocation AS MachineId,
MachineDescription AS MachineName,
RunningNumber AS LotNumber,
FORMAT(DayStart, 'MM/dd/yyyy') AS ProductionDay,
PlanQuantityPieces AS TotalPlanned,
ROUND(PlanQuantityPieces * 1.0 * CumEnd / DenomSec, 0)
- ROUND(PlanQuantityPieces * 1.0 * CumStart / DenomSec, 0) AS QTYPerDay,
ROUND(PlanQuantityLoadingUnit * CumEnd / DenomSec, 2)
- ROUND(PlanQuantityLoadingUnit * CumStart / DenomSec, 2) AS PalDay,
ProductionLotState AS finished
FROM cum
WHERE CAST(DayStart AS date) BETWEEN @start_date AND @end_date -- filter AFTER cumulative calc
ORDER BY RunningNumber, DayStart DESC
OPTION (MAXRECURSION 366);

View File

@@ -0,0 +1,15 @@
use AlplaPROD_test1
SELECT plant=(SELECT Wert FROM dbo.T_SystemParameter (nolock) WHERE (Bezeichnung = 'Werkskuerzel')),
plantName=(SELECT Wert FROM dbo.T_SystemParameter AS T_SystemParameter (nolock) WHERE (Bezeichnung = 'Mandant-intern')),*
from
(Select IdBestellung as 'Purchase order number',
IdArtikelVarianten AS AV,
BestellMenge,
BestellMengeVPK,
PreisProEinheit,
convert(varchar,Lieferdatum,23) as deliveryDate,
ROW_NUMBER() over(partition by IdArtikelVarianten order by Lieferdatum desc) rn
from dbo.V_Bestellpositionen_PURCHASE (nolock)
where PositionsStatus = '7' or PositionsStatus = '6' or PositionsStatus = '5' and convert(varchar,Lieferdatum,23) > DATEADD(year, -5, GetDate()) )a
where rn = 1

View File

@@ -0,0 +1,14 @@
use AlplaPROD_test1
select * from
(select IdArtikelvarianten as av,
VKPreis as salesPrice,
MPB, FWMPAlpla,
FWMPB,
ROW_NUMBER() over(partition by IdArtikelVarianten order by gueltigabdatum desc) rn,
convert(date, gueltigabdatum, 120) as validDate
from dbo.T_HistoryVK (nolock)
where convert(date, gueltigabdatum, 120) <= '[date]' and StandardKunde = 1) a
where rn =1
order by av asc,
validDate desc

View File

@@ -0,0 +1,7 @@
use alplaprod_test1
SELECT IdArtikelvarianten AS AV,
Menge AS Quantity,
CONVERT(DATE, BuchDatum) AS Prod_Date
FROM dbo.T_LBW (nolock)
WHERE BuchDatum BETWEEN '[startDate]' AND '[endDate]' ORDER BY BuchDatum DESC

View File

@@ -0,0 +1,40 @@
use AlplaPROD_test1
declare @start_date nvarchar(30) = '[startDate] '
declare @end_date nvarchar(30) = '[endDate] '
select T_Wareneingaenge.IdBestellung AS Purchase_order,
T_Adressen.IdAdressen,
T_Adressen.Bezeichnung,
T_Wareneingaenge.IdArtikelVarianten AS AV,
V_Artikel.Alias,
x.Bemerkung AS Remark,
T_Wareneingaenge.Bemerkung AS Purchase_Remark,
x.Add_User,
CONVERT(DATE, x.Add_Date) AS Received_Date,
x.IdWareneingangPlanung,
T_Wareneingaenge.SollMenge As Ordered_QTY,
x.EntladeMenge As Received_QTY,
case when T_Adressen.Bezeichnung LIKE '%Alpla%' Then 'AlplaPlant' Else 'Supplier' End AS
Supplier,
x.Typ as incoming_goods_type
from dbo.T_WareneingangPlanungen (nolock) as x
join
dbo.T_Wareneingaenge (nolock) on
x.IdWareneingang=
dbo.T_Wareneingaenge.IdWareneingang
join
dbo.V_Artikel (nolock) on
dbo.T_Wareneingaenge.IdArtikelVarianten=
dbo.V_Artikel.IdArtikelvarianten
join
dbo.T_Adressen (nolock) on dbo.T_Wareneingaenge.IdLieferantAdresse =
dbo.T_Adressen.IdAdressen
where x.add_date between @start_date + (select top(1) CONVERT(char(8), StartDate, 108) as startTime from [test1_AlplaPROD2.0_Read].masterData.ShiftDefinition (nolock) where TeamNumber = 1)
AND @end_date + (select top(1) CONVERT(char(8), StartDate, 108) as startTime from [test1_AlplaPROD2.0_Read].masterData.ShiftDefinition (nolock) where TeamNumber = 1)
order by x.add_date desc

View File

@@ -0,0 +1,13 @@
select IdArtikelVarianten,
ArtikelVariantenAlias,
IdRezeptur,
Menge,
IdBuchungsGrund,
Buchungsdatum,
ProduktionsLos,
IdReinheit,
ReinheitBez, HerkunftBez
from alplaprod_test1.[dbo].[V_AbfallLagerBuchungen] (nolock)
where Buchungsdatum between '[startDate] ' + (select top(1) CONVERT(char(8), StartDate, 108) as startTime from [test1_AlplaPROD2.0_Read].masterData.ShiftDefinition (nolock) where TeamNumber = 1)
and '[endDate] ' + (select top(1) CONVERT(char(8), StartDate, 108) as startTime from [test1_AlplaPROD2.0_Read].masterData.ShiftDefinition (nolock) where TeamNumber = 1)
and IdBuchungsGrund in (140, 240) and BuchungsTyp = 1

View File

@@ -0,0 +1,15 @@
select IdArtikelVarianten AS AV,
ArtikelVariantenAlias AS AVDescription,
convert(date,AbrufLadeDatum,23) As DeliveryDate,
idlieferadresse AS DeliveryAddress,
LieferAdressBez,
AuftragsNummer AS PO_Number,
IdAuftragsPosition AS LineITEM,
IdAuftragsAbruf AS ReleaseNumber,
AbrufMengeVPK AS PalletsRequested,
AbrufMenge AS PiecesRequested,
GelieferteMengeVPK AS DeliveredPallets,
GelieferteMenge AS DeliveredQTY,
case when LieferAdressBez Like '%alpla%' Then 'AlplaPlant' ELSE 'Customer' End as CustomerType
from alplaprod_test1.dbo.V_TrackerAuftragsAbrufe (nolock)
where AbrufLadeDatum between '[startDate]' and '[endDate]'

View File

@@ -1,11 +1,80 @@
SELECT count(*) as activated
FROM [test1_AlplaPROD2.0_Read].[support].[FeatureActivation]
where feature in (108,7)
where feature in (7)
/*
as more features get activated and need to have this checked to include the new endpoints add here so we can check this.
108 = waste
7 = warehousing
[DefaultTranslation("Blocking")]
Blocking = 1,
[DefaultTranslation("Users")]
UserManagement = 2,
[DefaultTranslation("Complaint Handling")]
ComplaintHandling = 3,
[DefaultTranslation("Demand Management")]
DemandManagement = 4,
[DefaultTranslation("Issue Material")]
IssueMaterial = 5,
[DefaultTranslation("Production Controlling")]
ProductionControlling = 6,
[DefaultTranslation("Warehousing")]
Warehousing = 7,
[DefaultTranslation("Outbound Deliveries")]
OutboundDeliveries = 8,
[DefaultTranslation("Production Scheduling")]
ProductionScheduling = 9,
[DefaultTranslation("Advanced Scheduling")]
AdvancedScheduling = 10,
[DefaultTranslation("Material Requirements Planning")]
MaterialRequirementsPlanning = 11,
[DefaultTranslation("Production Labelling")]
ProductionLabelling = 12,
[SpecialProcess]
[DefaultTranslation("Accounting")]
Accounting = 100,
[SpecialProcess]
[DefaultTranslation("Irradiation")]
Irradiation = 101,
[SpecialProcess]
[DefaultTranslation("Central Moulds")]
CentralMoulds = 102,
[SpecialProcess]
[DefaultTranslation("Maintenance")]
Maintenance = 103,
[SpecialProcess]
[DefaultTranslation("Disable Manual Bookings")]
DisableManualBookings = 104,
[SpecialProcess]
[DefaultTranslation("Purchasing")]
Purchasing = 105,
[SpecialProcess]
[DefaultTranslation("Tracing")]
Tracing = 106,
[SpecialProcess]
[DefaultTranslation("AlplaERP (D365)")]
AlplaErp = 107,
[SpecialProcess]
[DefaultTranslation("AI chatbot")]
AiChatBot = 108
*/

View File

@@ -0,0 +1,6 @@
USE [test1_AlplaPROD2.0_Read]
SELECT *
FROM [masterData].[Dock] (nolock)
where active = 1
order by Description desc

View File

@@ -4,7 +4,10 @@ import { setupAuthRoutes } from "./auth/auth.routes.js";
// import the routes and route setups
import { setupApiDocsRoutes } from "./configs/scaler.config.js";
import { setupDatamartRoutes } from "./datamart/datamart.routes.js";
import { setupDockDoorRoutes } from "./dockdoorScanning/dockdoor.routes.js";
import { setupEomRoutes } from "./eom/eom.routes.js";
import { setupGPSqlRoutes } from "./gpSql/gpSql.routes.js";
import { setupLogisticsRoutes } from "./logistics/logistics.routes.js";
import { setupMobileRoutes } from "./mobile/mobile.routes.js";
import { setupNotificationRoutes } from "./notification/notification.routes.js";
import { setupOCPRoutes } from "./ocp/ocp.routes.js";
@@ -16,17 +19,20 @@ import { setupUtilsRoutes } from "./utils/utils.routes.js";
export const setupRoutes = (baseUrl: string, app: Express) => {
//routes that are on by default
setupDatamartRoutes(baseUrl, app);
setupMobileRoutes(baseUrl, app);
setupSystemRoutes(baseUrl, app);
setupAdminRoutes(baseUrl, app);
setupApiDocsRoutes(baseUrl, app);
setupProdSqlRoutes(baseUrl, app);
setupGPSqlRoutes(baseUrl, app);
setupDatamartRoutes(baseUrl, app);
setupAuthRoutes(baseUrl, app);
setupUtilsRoutes(baseUrl, app);
setupOpendockRoutes(baseUrl, app);
setupNotificationRoutes(baseUrl, app);
setupOCPRoutes(baseUrl, app);
setupTCPRoutes(baseUrl, app);
setupDockDoorRoutes(baseUrl, app);
setupEomRoutes(baseUrl, app);
setupLogisticsRoutes(baseUrl, app);
};

View File

@@ -1,43 +0,0 @@
import type { OpenAPIV3_1 } from "openapi-types";
export const cronerActiveJobs: OpenAPIV3_1.PathsObject = {
"/api/utils/croner": {
get: {
summary: "Cron jobs",
description: "Returns all jobs on the server.",
tags: ["Utils"],
responses: {
"200": {
description: "Jobs returned",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: {
type: "boolean",
format: "boolean",
example: true,
},
uptime: {
type: "number",
format: "3454.34",
example: 3454.34,
},
memoryUsage: {
type: "string",
format: "Heap: 11.62 MB / RSS: 86.31 MB",
},
sqlServerStats: {
type: "number",
format: "442127",
},
},
},
},
},
},
},
},
},
};

View File

@@ -1,94 +0,0 @@
import type { OpenAPIV3_1 } from "openapi-types";
export const cronerStatusChange: OpenAPIV3_1.PathsObject = {
"/api/utils/croner/{status}": {
patch: {
summary: "Pauses or Resume the Job",
description:
"When sending start or stop with job name it will resume or stop the job",
tags: ["Utils"],
parameters: [
{
name: "status",
in: "path",
required: true,
description: "Status change",
schema: {
type: "string",
},
example: "start",
},
{
name: "limit",
in: "query",
required: false, // 👈 optional
description: "Maximum number of records to return",
schema: {
type: "integer",
minimum: 1,
maximum: 100,
},
example: 10,
},
],
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["name"],
properties: {
name: {
type: "string",
example: "start",
},
},
},
},
},
},
responses: {
"200": {
description: "Successful response",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", example: true },
data: {
type: "object",
example: {
name: "exampleName",
value: "some value",
},
},
},
},
},
},
},
"400": {
description: "Bad request",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", example: false },
message: {
type: "string",
example: "Invalid name parameter",
},
},
},
},
},
},
},
},
},
};

Some files were not shown because too many files have changed in this diff Show More