54 Commits

Author SHA1 Message Date
8dfcbc5720 chore(release): 0.0.1-alpha.2
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m29s
Release and Build Image / release (push) Successful in 17s
2026-04-08 16:13:38 -05:00
103ae77e9f build(release): docker and release corrections
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-04-08 16:12:54 -05:00
beeccc6e8d chore(release): 0.0.1-alpha.1
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
Release and Build Image / release (push) Failing after 15s
2026-04-08 15:58:21 -05:00
0880298cf5 refactor(opendock refactor on how releases are posted): this was a bug maybe just a better refactory
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-04-08 15:57:20 -05:00
34b0abac36 feat(puchase history): purhcase history changed to long running no notification 2026-04-08 15:55:25 -05:00
28c226ddbc build(agent): added westbend into the flow 2026-04-07 22:33:38 -05:00
42861cc69e feat(purchase): historical data capture for alpla purchase 2026-04-07 22:33:11 -05:00
5f3d683a13 refactor(notification): reprint - removed a console log as it shouldnt bc there 2026-04-06 16:41:39 -05:00
a17787e852 feat(notification): reprint added
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m6s
2026-04-06 16:01:06 -05:00
5865ac3b99 feat(notification): base notifcaiton sub and admin compelted
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m59s
can now sub to a notification and user can remove them selfs plus an admin can remove,updates to add
new emails are good as well
2026-04-06 12:59:30 -05:00
637de857f9 feat(user notifications): added the ability for users to sub to notifications and add multi email 2026-04-06 09:29:46 -05:00
3ecf5fb916 refactor(userprofile): changes to have the table be blank and say nothing subscribed
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m32s
later we will leave this off the profile and add it once at least one notification is subscribed
2026-04-05 20:50:27 -05:00
92ba3ef512 docs(readme): updated progress data
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m18s
2026-04-05 20:44:49 -05:00
7d6c2db89c style(notifcaion): style changes to the notificaion card and started the table
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m49s
2026-04-03 17:16:58 -05:00
74262beb65 refactor(notification): select menu looks propper now 2026-04-03 17:16:31 -05:00
f3b8dd94e5 refactor(queries): changed dev version to be 1500ms vs 5000ms 2026-04-03 17:16:02 -05:00
0059b9b850 build(changelog): reset the change log after all crap testing 2026-04-03 17:15:22 -05:00
1ad789b2b9 chore(release): 0.1.0-alpha.12
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m45s
Release and Build Image / release (push) Successful in 10s
2026-04-03 16:54:44 -05:00
079478f932 fix(typo): more dam typos 2026-04-03 16:54:29 -05:00
d6d5b451cd chore(release): 0.1.0-alpha.11
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m45s
Release and Build Image / release (push) Successful in 10s
2026-04-03 16:49:20 -05:00
76747cf917 fix(release): typo that caused errors 2026-04-03 16:49:12 -05:00
6e85991062 refactor(release): changes to only have the changelog in the release 2026-04-03 16:43:17 -05:00
98e408cb85 chore(release): 0.1.0-alpha.10
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m48s
Release and Build Image / release (push) Successful in 1m22s
2026-04-03 15:30:02 -05:00
ed052dff3c refactor(changelog): reverted back to commit-chagnelog, like more than changeset for solo dev 2026-04-03 15:29:49 -05:00
8f59bba614 chore(release): 0.1.0-alpha.9
All checks were successful
Release and Build Image / release (push) Successful in 1m52s
2026-04-03 15:22:26 -05:00
fb2c5609aa chore(release): version packages
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m46s
Release and Build Image / release (push) Successful in 1m20s
2026-04-03 13:06:52 -05:00
17aed6cb89 fix(lala): something here 2026-04-03 13:06:14 -05:00
b02b93b83f chore(release): version packages
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m50s
Release and Build Image / release (push) Successful in 1m26s
2026-04-03 12:51:52 -05:00
9ceba8b5bb fix(i suck): more learning experance 2026-04-03 12:51:11 -05:00
2c0dbf95c7 chore(release): version packages
Some checks failed
Build and Push LST Docker Image / docker (push) Successful in 1m50s
Release and Build Image / release (push) Failing after 1m22s
2026-04-03 12:44:43 -05:00
860207a60b fix(build): typo 2026-04-03 12:44:16 -05:00
5c6460012a chore(release): version packages
Some checks failed
Build and Push LST Docker Image / docker (push) Successful in 1m54s
Release and Build Image / release (push) Failing after 1m43s
2026-04-03 12:37:54 -05:00
be1d4081e0 docs(sop): added more info 2026-04-03 12:37:13 -05:00
83a94cacf3 fix(build): type in how we pushed the header over
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m20s
2026-04-03 12:33:20 -05:00
0ce3790675 chore(release): version packages
Some checks failed
Build and Push LST Docker Image / docker (push) Successful in 1m51s
Release and Build Image / release (push) Failing after 1m23s
2026-04-03 12:23:13 -05:00
5854889eb5 refactor(build): added in more info to the relase section 2026-04-03 12:22:26 -05:00
4caaf74569 chore(release): version packages
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m49s
Release and Build Image / release (push) Successful in 1m22s
2026-04-03 12:09:59 -05:00
fe889ca757 fix(build): issue with how i wrote the release token
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-04-03 12:08:57 -05:00
699c124b0e chore(release): version packages
Some checks failed
Build and Push LST Docker Image / docker (push) Successful in 1m42s
Release and Build Image / release (push) Failing after 6s
2026-04-03 11:56:40 -05:00
7d55c5f431 refactor(build): changes to the way we do release so it builds as well
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m21s
2026-04-03 11:54:41 -05:00
c4fd74fc93 chore(release): version packages
Some checks failed
Build and Push LST Docker Image / docker (push) Successful in 1m44s
Create Gitea Release / release (push) Failing after 17s
2026-04-03 11:42:52 -05:00
3775760734 fix(wrelease): forgot to save
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m18s
2026-04-03 11:41:27 -05:00
643d12ff18 refactor(build): changes to auto release when we cahnge version
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-04-03 11:40:09 -05:00
82eaa23da7 chore(release): version packages
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m57s
2026-04-03 11:18:25 -05:00
b18d1ced6d build(`build): added a personal sop to the setup until we move it
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m54s
2026-04-03 11:17:09 -05:00
69c5cf87fd fix(docker): fixes to allow an external url more easy
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 12s
when running in docker we might be using a different url thats not predefined in the cors so we want
to allow 1 more
2026-04-03 10:49:57 -05:00
1fadf0ad25 testing the docker runner
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m28s
2026-04-03 10:15:18 -05:00
beae6eb648 lots of changes with docker
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m57s
2026-04-03 09:51:52 -05:00
82ab735982 add gitea docker workflow
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-04-03 09:51:02 -05:00
dbd56c1b50 helper command set to correct drive now 2026-03-27 18:31:16 -05:00
037a473ab7 added dayton in 2026-03-27 18:31:02 -05:00
32998d417f table and query work 2026-03-27 18:30:50 -05:00
ddcb7e76a3 fixed imports on several files 2026-03-25 06:56:19 -05:00
191cb2b698 changed limas folder after migration 2026-03-25 06:56:01 -05:00
117 changed files with 15481 additions and 1169 deletions

View File

@@ -1,8 +0,0 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

View File

@@ -1,11 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

View File

@@ -6,4 +6,7 @@ Dockerfile
docker-compose.yml
npm-debug.log
builds
testFiles
testFiles
nssm.exe
postgresql-17.9-2-windows-x64.exe
VSCodeUserSetup-x64-1.112.0.msi

View File

@@ -0,0 +1,31 @@
name: Build and Push LST Docker Image
on:
push:
branches:
- main
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout (local)
run: |
git clone https://git.tuffraid.net/cowch/lst_v3.git .
git checkout ${{ gitea.sha }}
- name: Login to registry
run: echo "${{ secrets.PASSWORD }}" | docker login git.tuffraid.net -u "cowch" --password-stdin
- name: Build image
run: |
docker build \
-t git.tuffraid.net/cowch/lst_v3:latest \
-t git.tuffraid.net/cowch/lst_v3:${{ gitea.sha }} \
.
- name: Push
run: |
docker push git.tuffraid.net/cowch/lst_v3:latest
docker push git.tuffraid.net/cowch/lst_v3:${{ gitea.sha }}

View File

@@ -0,0 +1,229 @@
name: Release and Build Image
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: ubuntu-latest
env:
# Internal/origin Gitea URL. Do NOT use the Cloudflare fronted URL here.
# Examples:
# http://gitea.internal.lan:3000
# https://gitea-origin.yourdomain.local
GITEA_INTERNAL_URL: "https://git.tuffraid.net"
# Internal/origin registry host. Usually same host as above, but without protocol.
# Example:
# gitea.internal:3000
REGISTRY_HOST: "git.tuffraid.net"
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Prepare release metadata
shell: bash
run: |
set -euo pipefail
TAG="${GITHUB_REF_NAME:-${GITHUB_REF##refs/tags/}}"
VERSION="${TAG#v}"
IMAGE_NAME="${REGISTRY_HOST}/${{ gitea.repository }}"
echo "TAG=$TAG" >> "$GITHUB_ENV"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "IMAGE_NAME=$IMAGE_NAME" >> "$GITHUB_ENV"
if [[ "$TAG" == *-* ]]; then
echo "PRERELEASE=true" >> "$GITHUB_ENV"
else
echo "PRERELEASE=false" >> "$GITHUB_ENV"
fi
echo "Resolved TAG=$TAG"
echo "Resolved VERSION=$VERSION"
echo "Resolved IMAGE_NAME=$IMAGE_NAME"
- name: Log in to Gitea container registry
shell: bash
env:
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -euo pipefail
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USERNAME" --password-stdin
- name: Build Docker image
shell: bash
run: |
set -euo pipefail
docker build \
-t "$IMAGE_NAME:$TAG" \
-t "$IMAGE_NAME:latest" \
.
- name: Push version tag
shell: bash
run: |
set -euo pipefail
docker push "$IMAGE_NAME:$TAG"
- name: Push latest tag
if: ${{ !contains(env.TAG, '-') }}
shell: bash
run: |
set -euo pipefail
docker push "$IMAGE_NAME:latest"
- name: Push prerelease channel tag
if: ${{ contains(env.TAG, '-') }}
shell: bash
env:
TAG: ${{ env.TAG }}
run: |
set -euo pipefail
CHANNEL="${TAG#*-}"
CHANNEL="${CHANNEL%%.*}"
echo "Resolved prerelease channel: $CHANNEL"
docker tag "$IMAGE_NAME:$TAG" "$IMAGE_NAME:$CHANNEL"
docker push "$IMAGE_NAME:$CHANNEL"
- name: Extract matching CHANGELOG section
shell: bash
env:
VERSION: ${{ env.VERSION }}
run: |
set -euo pipefail
python3 - <<'PY'
import os
import re
from pathlib import Path
version = os.environ["VERSION"]
changelog_path = Path("CHANGELOG.md")
if not changelog_path.exists():
Path("release_body.md").write_text(f"Release {version}\n", encoding="utf-8")
raise SystemExit(0)
text = changelog_path.read_text(encoding="utf-8")
# Matches headings like:
# ## [0.1.0]
# ## 0.1.0
# ## [0.1.0-alpha.1]
pattern = re.compile(
rf"^##\s+\[?{re.escape(version)}\]?[^\n]*\n(.*?)(?=^##\s+\[?[^\n]+|\Z)",
re.MULTILINE | re.DOTALL,
)
match = pattern.search(text)
if match:
body = match.group(1).strip()
else:
body = f"Release {version}"
if not body:
body = f"Release {version}"
Path("release_body.md").write_text(body + "\n", encoding="utf-8")
print("----- release_body.md -----")
print(body)
print("---------------------------")
PY
- name: Create Gitea release
shell: bash
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_INTERNAL_URL: ${{ env.GITEA_INTERNAL_URL }}
TAG: ${{ env.TAG }}
PRERELEASE: ${{ env.PRERELEASE }}
run: |
set -euo pipefail
python3 - <<'PY'
import json
import os
import urllib.request
import urllib.error
from pathlib import Path
tag = os.environ["TAG"]
prerelease = os.environ["PRERELEASE"].lower() == "true"
server_url = os.environ["GITEA_INTERNAL_URL"].rstrip("/")
repo = os.environ["GITEA_REPOSITORY"]
token = os.environ["RELEASE_TOKEN"]
body = Path("release_body.md").read_text(encoding="utf-8").strip()
# Check if the release already exists for this tag
get_url = f"{server_url}/api/v1/repos/{repo}/releases/tags/{tag}"
get_req = urllib.request.Request(
get_url,
method="GET",
headers={
"Authorization": f"token {token}",
"Accept": "application/json",
"User-Agent": "lst-release-workflow/1.0",
},
)
existing_release = None
try:
with urllib.request.urlopen(get_req) as resp:
existing_release = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
if e.code != 404:
details = e.read().decode("utf-8", errors="replace")
print("Failed checking existing release:")
print(details)
raise
payload = {
"tag_name": tag,
"name": tag,
"body": body,
"draft": False,
"prerelease": prerelease,
}
data = json.dumps(payload).encode("utf-8")
if existing_release:
release_id = existing_release["id"]
url = f"{server_url}/api/v1/repos/{repo}/releases/{release_id}"
method = "PATCH"
print(f"Release already exists for tag {tag}, updating release id {release_id}")
else:
url = f"{server_url}/api/v1/repos/{repo}/releases"
method = "POST"
print(f"No release exists for tag {tag}, creating a new one")
req = urllib.request.Request(
url,
data=data,
method=method,
headers={
"Authorization": f"token {token}",
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "lst-release-workflow/1.0",
},
)
try:
with urllib.request.urlopen(req) as resp:
print(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
details = e.read().decode("utf-8", errors="replace")
print("Release create/update failed:")
print(details)
raise
PY

5
.gitignore vendored
View File

@@ -4,8 +4,13 @@ builds
.includes
.buildNumber
temp
brunoApi
.scriptCreds
node-v24.14.0-x64.msi
postgresql-17.9-2-windows-x64.exe
VSCodeUserSetup-x64-1.112.0.exe
nssm.exe
# Logs
logs
*.log

View File

@@ -11,7 +11,7 @@
{ "type": "ci", "hidden": false, "section": "📈 Project changes" },
{ "type": "build", "hidden": false, "section": "📈 Project Builds" }
],
"commitUrlFormat": "https://git.tuffraid.net/cowch/lst/commits/{{hash}}",
"compareUrlFormat": "https://git.tuffraid.net/cowch/lst/compare/{{previousTag}}...{{currentTag}}",
"commitUrlFormat": "https://git.tuffraid.net/cowch/lst_v3/commits/{{hash}}",
"compareUrlFormat": "https://git.tuffraid.net/cowch/lst_v3/compare/{{previousTag}}...{{currentTag}}",
"header": "# All Changes to LST can be found below.\n"
}

View File

@@ -3,6 +3,8 @@
"workbench.colorTheme": "Default Dark+",
"terminal.integrated.env.windows": {},
"editor.formatOnSave": true,
"typescript.preferences.importModuleSpecifier": "relative",
"javascript.preferences.importModuleSpecifier": "relative",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
@@ -52,8 +54,10 @@
"alpla",
"alplamart",
"alplaprod",
"alplapurchase",
"bookin",
"Datamart",
"dotenvx",
"dyco",
"intiallally",
"manadatory",
@@ -65,6 +69,7 @@
"preseed",
"prodlabels",
"prolink",
"Skelly",
"trycatch"
],
"gitea.token": "8456def90e1c651a761a8711763d6ef225d6b2db",

View File

@@ -1,7 +1,76 @@
# lst_v3
# All Changes to LST can be found below.
## 1.0.1
## [0.0.1-alpha.2](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.1...v0.0.1-alpha.2) (2026-04-08)
### Patch Changes
- cf18e94: core stuff
### 📈 Project Builds
* **release:** docker and release corrections ([103ae77](https://git.tuffraid.net/cowch/lst_v3/commits/103ae77e9f82fc008a8ae143b6feccc3ce802f8c))
## [0.0.1-alpha.1](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.1-alpha.0...v0.0.1-alpha.1) (2026-04-08)
* **notifcaion:** style changes to the notificaion card and started the table ([7d6c2db](https://git.tuffraid.net/cowch/lst_v3/commits/7d6c2db89cae1f137f126f5814dccd373f7ccb76))
### 🌟 Enhancements
* **notification:** base notifcaiton sub and admin compelted ([5865ac3](https://git.tuffraid.net/cowch/lst_v3/commits/5865ac3b99d60005c4245740369b0e0789c8fbbd))
* **notification:** reprint added ([a17787e](https://git.tuffraid.net/cowch/lst_v3/commits/a17787e85217f1fa4a5e5389e29c33ec09c286c5))
* **puchase history:** purhcase history changed to long running no notification ([34b0aba](https://git.tuffraid.net/cowch/lst_v3/commits/34b0abac36f645d0fe5f508881ddbef81ff04b7c))
* **purchase:** historical data capture for alpla purchase ([42861cc](https://git.tuffraid.net/cowch/lst_v3/commits/42861cc69e8d4aba5a9670aaed55417efda2b505))
* **user notifications:** added the ability for users to sub to notifications and add multi email ([637de85](https://git.tuffraid.net/cowch/lst_v3/commits/637de857f99499a41f7175181523f5d809d95d7e))
### 🐛 Bug fixes
* **build:** issue with how i wrote the release token ([fe889ca](https://git.tuffraid.net/cowch/lst_v3/commits/fe889ca75731af08c42ec714b7f2abf17cd1ee40))
* **build:** type in how we pushed the header over ([83a94ca](https://git.tuffraid.net/cowch/lst_v3/commits/83a94cacf3fc87287cdc0c0cc861b339e72e4b83))
* **build:** typo ([860207a](https://git.tuffraid.net/cowch/lst_v3/commits/860207a60b6e04b15736cba631be6c7eab74d020))
* **i suck:** more learning experance ([9ceba8b](https://git.tuffraid.net/cowch/lst_v3/commits/9ceba8b5bba17959f27b16b28f50a83c044863fb))
* **lala:** something here ([17aed6c](https://git.tuffraid.net/cowch/lst_v3/commits/17aed6cb89f8220570f6c66f78dba6bb202c1aaa))
* **release:** typo that caused errors ([76747cf](https://git.tuffraid.net/cowch/lst_v3/commits/76747cf91738bd0d0530afcf7b4f51f0db11ca98))
* **typo:** more dam typos ([079478f](https://git.tuffraid.net/cowch/lst_v3/commits/079478f93217dea31c9a1e8ffed85d2381a6977d))
* **wrelease:** forgot to save ([3775760](https://git.tuffraid.net/cowch/lst_v3/commits/377576073449e95d315defb913dc317759cc3f43))
### 📝 Chore
* **release:** 0.1.0-alpha.10 ([98e408c](https://git.tuffraid.net/cowch/lst_v3/commits/98e408cb8577da18e24821b55474198439434f3e))
* **release:** 0.1.0-alpha.11 ([d6d5b45](https://git.tuffraid.net/cowch/lst_v3/commits/d6d5b451cd9aeba642ef94654ca20f4acd0b827c))
* **release:** 0.1.0-alpha.12 ([1ad789b](https://git.tuffraid.net/cowch/lst_v3/commits/1ad789b2b91a20a2f5a8dc9e6f39af2e19ec9cdc))
* **release:** 0.1.0-alpha.9 ([8f59bba](https://git.tuffraid.net/cowch/lst_v3/commits/8f59bba614a8eaa3105bb56f0db36013d5e68485))
* **release:** version packages ([fb2c560](https://git.tuffraid.net/cowch/lst_v3/commits/fb2c5609aa12ea7823783c364d5bd029c48a64bd))
* **release:** version packages ([b02b93b](https://git.tuffraid.net/cowch/lst_v3/commits/b02b93b83f488fbcee6d24db080ad0d1fe1c5f59))
* **release:** version packages ([2c0dbf9](https://git.tuffraid.net/cowch/lst_v3/commits/2c0dbf95c7b8dfd2c98b476d3f44bc8929668c88))
* **release:** version packages ([5c64600](https://git.tuffraid.net/cowch/lst_v3/commits/5c6460012aa70d336fbc9702240b4f19262a6b41))
* **release:** version packages ([0ce3790](https://git.tuffraid.net/cowch/lst_v3/commits/0ce3790675bc408762eafe76cbd5ab496fd06e73))
* **release:** version packages ([4caaf74](https://git.tuffraid.net/cowch/lst_v3/commits/4caaf745693d4df847aefd3721ac5d0ae792114a))
* **release:** version packages ([699c124](https://git.tuffraid.net/cowch/lst_v3/commits/699c124b0efba8282e436210619504bda8878e90))
* **release:** version packages ([c4fd74f](https://git.tuffraid.net/cowch/lst_v3/commits/c4fd74fc93226cffd9e39602f507a05cd8ea628b))
### 📚 Documentation
* **readme:** updated progress data ([92ba3ef](https://git.tuffraid.net/cowch/lst_v3/commits/92ba3ef5121afd0d82d4f40a5a985e1fdc081011))
* **sop:** added more info ([be1d408](https://git.tuffraid.net/cowch/lst_v3/commits/be1d4081e07b0982b355a270b7850a852a4398f5))
### 🛠️ Code Refactor
* **build:** added in more info to the relase section ([5854889](https://git.tuffraid.net/cowch/lst_v3/commits/5854889eb5398feebda50a5d256ce7aec39ce112))
* **build:** changes to auto release when we cahnge version ([643d12f](https://git.tuffraid.net/cowch/lst_v3/commits/643d12ff182827e724e1569a583bd625a0d1dd0c))
* **build:** changes to the way we do release so it builds as well ([7d55c5f](https://git.tuffraid.net/cowch/lst_v3/commits/7d55c5f43173edb48d8709adcb972b7d8fbc3ebd))
* **changelog:** reverted back to commit-chagnelog, like more than changeset for solo dev ([ed052df](https://git.tuffraid.net/cowch/lst_v3/commits/ed052dff3c81a7064660a7d25685e0505065252c))
* **notification:** reprint - removed a console log as it shouldnt bc there ([5f3d683](https://git.tuffraid.net/cowch/lst_v3/commits/5f3d683a13c831229674166cced699e373131316))
* **notification:** select menu looks propper now ([74262be](https://git.tuffraid.net/cowch/lst_v3/commits/74262beb6596ddc971971cc9214a2688accf3a8e))
* **opendock refactor on how releases are posted:** this was a bug maybe just a better refactory ([0880298](https://git.tuffraid.net/cowch/lst_v3/commits/0880298cf53d83e487c706e73854e0874ae2d9da))
* **queries:** changed dev version to be 1500ms vs 5000ms ([f3b8dd9](https://git.tuffraid.net/cowch/lst_v3/commits/f3b8dd94e5ebae0cc4dd0a2689a19051942e94b8))
* **release:** changes to only have the changelog in the release ([6e85991](https://git.tuffraid.net/cowch/lst_v3/commits/6e8599106298ed13febd069d6fda8b354efb5b7b))
* **userprofile:** changes to have the table be blank and say nothing subscribed ([3ecf5fb](https://git.tuffraid.net/cowch/lst_v3/commits/3ecf5fb916d5dc1b1ffb224e2142d94f7a9cb126))
### 📈 Project Builds
* **agent:** added westbend into the flow ([28c226d](https://git.tuffraid.net/cowch/lst_v3/commits/28c226ddbc37ab85cd6a9a6aec091def3e5623d6))
* **changelog:** reset the change log after all crap testing ([0059b9b](https://git.tuffraid.net/cowch/lst_v3/commits/0059b9b850c9647695a3fecaf5927c2e3ee7b192))

View File

@@ -9,10 +9,13 @@ WORKDIR /app
# Copy package files
COPY . .
# Install production dependencies only
# build backend
RUN npm ci
RUN npm run build:docker
RUN npm run build
# build frontend
RUN npm --prefix frontend ci
RUN npm --prefix frontend run build
###########
# Stage 2 #
@@ -33,6 +36,9 @@ RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
COPY --from=build /app/frontend/dist ./frontend/dist
# TODO add in drizzle migrates
ENV RUNNING_IN_DOCKER=true
EXPOSE 3000

View File

@@ -7,7 +7,7 @@
Quick summary of current rewrite/migration goal.
- **Phase:** Backend rewrite
- **Last updated:** 2024-05-01
- **Last updated:** 2026-04-06
---
@@ -16,9 +16,9 @@ Quick summary of current rewrite/migration goal.
| Feature | Description | Status |
|----------|--------------|--------|
| User Authentication | ~~Login~~, ~~Signup~~, API Key | 🟨 In Progress |
| User Profile | Edit profile, upload avatar | ⏳ Not Started |
| User Profile | ~~Edit profile~~, upload avatar | 🟨 In Progress |
| User Admin | Edit user, create user, remove user, alplaprod user integration | ⏳ Not Started |
| Notifications | Subscribe, Create, Update, Remove, Manual Trigger | ⏳ 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 |
@@ -44,7 +44,7 @@ _Status legend:_
How to run the current version of the app.
```bash
git clone https://github.com/youruser/yourrepo.git
cd yourrepo
git clone https://git.tuffraid.net/cowch/lst_v3.git
cd lst_v3
npm install
npm run dev

View File

@@ -26,7 +26,7 @@ const createApp = async () => {
const __dirname = dirname(__filename);
// well leave this active so we can monitor it to validate
app.use(morgan("tiny"));
app.use(morgan("dev"));
app.set("trust proxy", true);
app.use(lstCors());
app.all(`${baseUrl}/api/auth/*splat`, toNodeHandler(auth));
@@ -34,11 +34,11 @@ const createApp = async () => {
setupRoutes(baseUrl, app);
app.use(
baseUrl + "/app",
`${baseUrl}/app`,
express.static(join(__dirname, "../frontend/dist")),
);
app.get(baseUrl + "/app/*splat", (_, res) => {
app.get(`${baseUrl}/app/*splat`, (_, res) => {
res.sendFile(join(__dirname, "../frontend/dist/index.html"));
});

View File

@@ -0,0 +1,38 @@
import {
integer,
jsonb,
pgTable,
text,
timestamp,
uuid,
} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";
export const alplaPurchaseHistory = pgTable("alpla_purchase_history", {
id: uuid("id").defaultRandom().primaryKey(),
apo: integer("apo"),
revision: integer("revision"),
confirmed: integer("confirmed"),
status: integer("status"),
statusText: text("status_text"),
journalNum: integer("journal_num"),
add_date: timestamp("add_date").defaultNow(),
add_user: text("add_user"),
upd_user: text("upd_user"),
upd_date: timestamp("upd_date").defaultNow(),
remark: text("remark"),
approvedStatus: text("approved_status").default("pending"),
position: jsonb("position").default([]),
createdAt: timestamp("created_at").defaultNow(),
});
export const alplaPurchaseHistorySchema =
createSelectSchema(alplaPurchaseHistory);
export const newAlplaPurchaseHistorySchema =
createInsertSchema(alplaPurchaseHistory);
export type AlplaPurchaseHistory = z.infer<typeof alplaPurchaseHistorySchema>;
export type NewAlplaPurchaseHistory = z.infer<
typeof newAlplaPurchaseHistorySchema
>;

View File

@@ -1,4 +1,5 @@
import {
index,
integer,
jsonb,
pgTable,
@@ -9,14 +10,23 @@ import {
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";
export const opendockApt = pgTable("opendock_apt", {
id: uuid("id").defaultRandom().primaryKey(),
release: integer("release").unique(),
openDockAptId: text("open_dock_apt_id").notNull(),
appointment: jsonb("appointment").default([]),
upd_date: timestamp("upd_date").defaultNow(),
createdAt: timestamp("created_at").defaultNow(),
});
export const opendockApt = pgTable(
"opendock_apt",
{
id: uuid("id").defaultRandom().primaryKey(),
release: integer("release").notNull().unique(),
openDockAptId: text("open_dock_apt_id").notNull(),
appointment: jsonb("appointment").notNull().default([]),
upd_date: timestamp("upd_date").notNull().defaultNow(),
createdAt: timestamp("created_at").notNull().defaultNow(),
},
(table) => ({
releaseIdx: index("opendock_apt_release_idx").on(table.release),
openDockAptIdIdx: index("opendock_apt_opendock_id_idx").on(
table.openDockAptId,
),
}),
);
export const opendockAptSchema = createSelectSchema(opendockApt);
export const newOpendockAptSchema = createInsertSchema(opendockApt);

View File

@@ -0,0 +1,6 @@
import { integer, pgTable, text } from "drizzle-orm/pg-core";
export const opendockApt = pgTable("printer_log", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: text("name").notNull(),
});

View File

@@ -0,0 +1,113 @@
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { prodQuery } from "../prodSql/prodSqlQuery.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";
/**
*
*/
const func = async (data: any, emails: string) => {
// get the actual notification as items will be updated between intervals if no one touches
const { data: l, error: le } = (await tryCatch(
db.select().from(notifications).where(eq(notifications.id, data.id)),
)) as any;
if (le) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `${data.name} encountered an error while trying to get initial info`,
data: [le],
notify: true,
});
}
// search the query db for the query by name
const sqlQuery = sqlQuerySelector(`${data.name}`) as SqlQuery;
// create the ignore audit logs ids
const ignoreIds = l[0].options[0]?.auditId
? `${l[0].options[0]?.auditId}`
: "0";
// run the check
const { data: queryRun, error } = await tryCatch(
prodQuery(
sqlQuery.query
.replace("[intervalCheck]", l[0].interval)
.replace("[ignoreList]", ignoreIds),
`Running notification query: ${l[0].name}`,
),
);
if (error) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `Data for: ${l[0].name} encountered an error while trying to get it`,
data: [error],
notify: true,
});
}
if (queryRun.data.length > 0) {
// update the latest audit id
const { error: dbe } = await tryCatch(
db
.update(notifications)
.set({ options: [{ auditId: `${queryRun.data[0].id}` }] })
.where(eq(notifications.id, data.id)),
);
if (dbe) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `Data for: ${l[0].name} encountered an error while trying to get it`,
data: [dbe],
notify: true,
});
}
// send the email
const sentEmail = await sendEmail({
email: emails,
subject: "Alert! Label Reprinted",
template: "reprintLabels",
context: {
items: queryRun.data,
},
});
if (!sentEmail?.success) {
return returnFunc({
success: false,
level: "error",
module: "email",
subModule: "notification",
message: `${l[0].name} failed to send the email`,
data: [sentEmail],
notify: true,
});
}
} else {
console.log("doing nothing as there is nothing to do.");
}
// TODO send the error to systemAdmin users so they do not always need to be on the notifications.
// these errors are defined per notification.
};
export default func;

View File

@@ -1,10 +1,10 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { eq } from "drizzle-orm";
import { createLogger } from "logger/logger.controller.js";
import { minutesToCron } from "utils/croner.minConvert.js";
import { createCronJob, stopCronJob } from "utils/croner.utils.js";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { notificationSub } from "../db/schema/notifications.sub.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { minutesToCron } from "../utils/croner.minConvert.js";
import { createCronJob, stopCronJob } from "../utils/croner.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const log = createLogger({ module: "notifications", subModule: "start" });

View File

@@ -1,6 +1,106 @@
const reprint = (data: any, emails: string) => {
console.log(data);
console.log(emails);
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { prodQuery } from "../prodSql/prodSqlQuery.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";
const reprint = async (data: any, emails: string) => {
// TODO: do the actual logic for the notification.
const { data: l, error: le } = (await tryCatch(
db.select().from(notifications).where(eq(notifications.id, data.id)),
)) as any;
if (le) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `${data.name} encountered an error while trying to get initial info`,
data: [le],
notify: true,
});
}
// search the query db for the query by name
const sqlQuery = sqlQuerySelector(`${data.name}`) as SqlQuery;
// create the ignore audit logs ids
const ignoreIds = l[0].options[0]?.auditId
? `${l[0].options[0]?.auditId}`
: "0";
// run the check
const { data: queryRun, error } = await tryCatch(
prodQuery(
sqlQuery.query
.replace("[intervalCheck]", l[0].interval)
.replace("[ignoreList]", ignoreIds),
`Running notification query: ${l[0].name}`,
),
);
if (error) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `Data for: ${l[0].name} encountered an error while trying to get it`,
data: [error],
notify: true,
});
}
if (queryRun.data.length > 0) {
// update the latest audit id
const { error: dbe } = await tryCatch(
db
.update(notifications)
.set({ options: [{ auditId: `${queryRun.data[0].id}` }] })
.where(eq(notifications.id, data.id)),
);
if (dbe) {
return returnFunc({
success: false,
level: "error",
module: "notification",
subModule: "query",
message: `Data for: ${l[0].name} encountered an error while trying to get it`,
data: [dbe],
notify: true,
});
}
// send the email
const sentEmail = await sendEmail({
email: emails,
subject: "Alert! Label Reprinted",
template: "reprintLabels",
context: {
items: queryRun.data,
},
});
if (!sentEmail?.success) {
return returnFunc({
success: false,
level: "error",
module: "email",
subModule: "notification",
message: `${l[0].name} failed to send the email`,
data: [sentEmail],
notify: true,
});
}
}
};
export default reprint;

View File

@@ -1,8 +1,9 @@
import { notifications } from "db/schema/notifications.schema.js";
import { eq } from "drizzle-orm";
import { type Response, Router } from "express";
import { auth } from "utils/auth.utils.js";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { auth } from "../utils/auth.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";

View File

@@ -1,8 +1,8 @@
import { notifications } from "db/schema/notifications.schema.js";
import { eq } from "drizzle-orm";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { notifications } from "../db/schema/notifications.schema.js";
import { requirePermission } from "../middleware/auth.requiredPerms.middleware.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";

View File

@@ -1,36 +1,44 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { and, eq } from "drizzle-orm";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { notificationSub } from "../db/schema/notifications.sub.schema.js";
import { auth } from "../utils/auth.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { modifiedNotification } from "./notification.controller.js";
const newSubscribe = z.object({
emails: z
.email()
.array()
.describe("An array of emails"),
userId: z.string().describe("User id."),
notificationId: z
.string()
.describe("Notification id"),
notificationId: z.string().describe("Notification id"),
});
const r = Router();
r.delete("/", async (req, res: Response) => {
const hasPermissions = await auth.api.userHasPermission({
body: {
//userId: req?.user?.id,
role: req.user?.roles as any,
permissions: {
notifications: ["readAll"], // This must match the structure in your access control
},
},
});
try {
const validated = newSubscribe.parse(req.body);
const { data, error } = await tryCatch(
db
.delete(notificationSub)
.where(
and(
eq(notificationSub.userId, validated.userId),
eq(
notificationSub.userId,
hasPermissions ? validated.userId : (req?.user?.id ?? ""),
), // allows the admin to delete this
//eq(notificationSub.userId, req?.user?.id ?? ""),
eq(notificationSub.notificationId, validated.notificationId),
),
)
@@ -51,6 +59,18 @@ r.delete("/", async (req, res: Response) => {
});
}
if (data.length <= 0) {
return apiReturn(res, {
success: false,
level: "info",
module: "notification",
subModule: "post",
message: `Subscription was not deleted invalid data sent over`,
data: data ?? [],
status: 200,
});
}
return apiReturn(res, {
success: true,
level: "info",

View File

@@ -1,14 +1,16 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { eq } from "drizzle-orm";
import { type Response, Router } from "express";
import { auth } from "utils/auth.utils.js";
import { db } from "../db/db.controller.js";
import { notificationSub } from "../db/schema/notifications.sub.schema.js";
import { auth } from "../utils/auth.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
r.get("/", async (req, res: Response) => {
const { userId } = req.query;
const hasPermissions = await auth.api.userHasPermission({
body: {
//userId: req?.user?.id,
@@ -19,6 +21,10 @@ r.get("/", async (req, res: Response) => {
},
});
if (userId) {
hasPermissions.success = false;
}
const { data, error } = await tryCatch(
db
.select()
@@ -47,7 +53,7 @@ r.get("/", async (req, res: Response) => {
level: "info",
module: "notification",
subModule: "post",
message: `Subscription deleted`,
message: `Subscriptions`,
data: data ?? [],
status: 200,
});

View File

@@ -1,7 +1,7 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { notificationSub } from "../db/schema/notifications.sub.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { modifiedNotification } from "./notification.controller.js";
@@ -25,8 +25,25 @@ r.post("/", async (req, res: Response) => {
try {
const validated = newSubscribe.parse(req.body);
const emails = validated.emails
.map((e) => e.trim().toLowerCase())
.filter(Boolean);
const uniqueEmails = [...new Set(emails)];
const { data, error } = await tryCatch(
db.insert(notificationSub).values(validated).returning(),
db
.insert(notificationSub)
.values({
userId: req?.user?.id ?? "",
notificationId: validated.notificationId,
emails: uniqueEmails,
})
.onConflictDoUpdate({
target: [notificationSub.userId, notificationSub.notificationId],
set: { emails: uniqueEmails },
})
.returning(),
);
await modifiedNotification(validated.notificationId);

View File

@@ -1,8 +1,8 @@
import { notificationSub } from "db/schema/notifications.sub.schema.js";
import { and, eq } from "drizzle-orm";
import { type Response, Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import { notificationSub } from "../db/schema/notifications.sub.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { modifiedNotification } from "./notification.controller.js";

View File

@@ -1,11 +1,11 @@
import { db } from "db/db.controller.js";
import { sql } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import {
type NewNotification,
notifications,
} from "db/schema/notifications.schema.js";
import { sql } from "drizzle-orm";
import { tryCatch } from "utils/trycatch.utils.js";
} from "../db/schema/notifications.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const note: NewNotification[] = [
{
@@ -14,7 +14,27 @@ const note: NewNotification[] = [
"Monitors the labels that are printed and returns a there data, if one falls withing the time frame.",
active: false,
interval: "10",
options: [{ prodID: 1 }],
options: [{ auditId: [0] }],
},
{
name: "qualityBlocking",
description:
"Checks for new blocking orders that have been entered, recommend to get the most recent order in here before activating.",
active: false,
interval: "10",
options: [{ sentBlockingOrders: [{ timeStamp: "0", blockingOrder: 1 }] }],
},
{
name: "alplaPurchaseHistory",
description:
"Will check the alpla purchase data for any changes, if the req has not been sent already then we will send this, for a po or fresh order we will ignore. ",
active: false,
interval: "5",
options: [
{ sentReqs: [{ timeStamp: "0", req: 1, approved: false }] },
{ sentAPOs: [{ timeStamp: "0", apo: 1 }] },
{ sentRCT: [{ timeStamp: "0", rct: 1 }] },
],
},
];

View File

@@ -0,0 +1,36 @@
/**
* the route that listens for the printers post.
*
* and http-post alert should be setup on each printer pointing to at min you will want to make the alert for
* pause printer, you can have all on here as it will also monitor and do things on all messages
*
* http://{serverIP}:2222/lst/api/ocp/printer/listener/{printerName}
*
* the messages will be sent over to the db for logging as well as specific ones will do something
*
* pause will validate if can print
* close head will repause the printer so it wont print a label
* power up will just repause the printer so it wont print a label
*/
import { Router } from "express";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
r.post("/printer/listener/:printer", async (req, res) => {
const { printer: printerName } = req.params;
console.log(req.body);
return apiReturn(res, {
success: true,
level: "info",
module: "ocp",
subModule: "printing",
message: `${printerName} just passed over a message`,
data: req.body ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,19 @@
/**
* this will do a prod sync, update or add alerts to the printer, validate the next pm intervale as well as head replacement.
*
* if a printer is upcoming on a pm or head replacement send to the plant to address.
*
* a trigger on the printer table will have the ability to run this as well
*
* heat beats on all assigned printers
*
* printer status will live here this will be how we manage all the levels of status like 3 paused, 1 printing, 8 error, 10 power up, etc...
*/
export const printerManager = async () => {};
export const printerHeartBeat = async () => {
// heat heats will be defaulted to 60 seconds no reason to allow anything else
};
//export const printerStatus = async (statusNr: number, printerId: number) => {};

22
backend/ocp/ocp.routes.ts Normal file
View File

@@ -0,0 +1,22 @@
import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import listener from "./ocp.printer.listener.js";
export const setupOCPRoutes = (baseUrl: string, app: Express) => {
//setup all the routes
const router = Router();
// is the feature even on?
router.use(featureCheck("ocp"));
// non auth routes up here
router.use(listener);
// auth routes below here
router.use(requireAuth);
//router.use("");
app.use(`${baseUrl}/api/ocp`, router);
};

View File

@@ -17,15 +17,6 @@ import { returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
import { getToken, odToken } from "./opendock.utils.js";
let lastCheck = formatInTimeZone(
new Date().toISOString(),
"America/New_York",
"yyyy-MM-dd HH:mm:ss",
);
//const queue: unknown[] = [];
//const isProcessing: boolean = false;
type Releases = {
ReleaseNumber: number;
DeliveryState: number;
@@ -37,10 +28,38 @@ type Releases = {
LineItemArticleWeight: number;
CustomerReleaseNumber: string;
};
const timeZone = process.env.TIMEZONE as string;
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
const log = createLogger({ module: "opendock", subModule: "releaseMonitor" });
// making the cron more safe when it comes to buffer stuff
let opendockSyncRunning = false;
let lastCheck = formatInTimeZone(
new Date().toISOString(),
timeZone,
"yyyy-MM-dd HH:mm:ss",
);
// const lastCheck = formatInTimeZone(
// new Date().toISOString(),
// `America/New_York`, //TODO: Pull timezone from the .env last as process.env.TIME_ZONE is not working so need to figure itout
// "yyyy-MM-dd HH:mm:ss",
// );
//const queue: unknown[] = [];
//const isProcessing: boolean = false;
// const parseDbDate = (value: string | Date) => {
// if (value instanceof Date) return value;
// // normalize "2026-04-08 13:10:43.280" -> "2026-04-08T13:10:43.280"
// const normalized = value.replace(" ", "T");
// // interpret that wall-clock time as America/New_York
// return fromZonedTime(normalized, timeZone);
// };
const postRelease = async (release: Releases) => {
if (!odToken.odToken) {
log.info({}, "Getting Auth Token");
@@ -152,22 +171,25 @@ const postRelease = async (release: Releases) => {
};
// TODO: pull the current added releases from the db and if one matches then we want to get its id and run the update vs create
const { data: apt, error: aptError } = await tryCatch(
db.select().from(opendockApt),
const { data: existingApt, error: aptError } = await tryCatch(
db
.select()
.from(opendockApt)
.where(eq(opendockApt.release, release.ReleaseNumber))
.limit(1),
);
if (aptError) {
log.error({ error: aptError }, "Error getting apt data");
// TODO: send an error email on this one as it will cause issues
return;
}
const releaseCheck = apt.filter((r) => r.release === release.ReleaseNumber);
const existing = existingApt[0];
//console.log(releaseCheck);
if (releaseCheck.length > 0) {
const id = releaseCheck[0]?.openDockAptId;
if (existing) {
const id = existing.openDockAptId;
try {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
@@ -196,7 +218,11 @@ const postRelease = async (release: Releases) => {
})
.onConflictDoUpdate({
target: opendockApt.release,
set: { appointment: response.data.data, upd_date: sql`NOW()` },
set: {
openDockAptId: response.data.data.id,
appointment: response.data.data,
upd_date: sql`NOW()`,
},
})
.returning();
@@ -250,8 +276,12 @@ const postRelease = async (release: Releases) => {
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.id,
set: { appointment: response.data.data, upd_date: sql`NOW()` },
target: opendockApt.release,
set: {
openDockAptId: response.data.data.id,
appointment: response.data.data,
upd_date: sql`NOW()`,
},
})
.returning();
@@ -270,7 +300,7 @@ const postRelease = async (release: Releases) => {
}
}
await delay(500); // rate limit protection
await delay(750); // rate limit protection
};
export const monitorReleaseChanges = async () => {
@@ -298,184 +328,66 @@ export const monitorReleaseChanges = async () => {
}
if (openDockMonitor[0]?.active) {
createCronJob("opendock_sync", "*/15 * * * * *", async () => {
try {
const result = await prodQuery(
sqlQuery.query.replace("[dateCheck]", `'${lastCheck}'`),
"Get release info",
);
// const BUFFER_MS =
// Math.floor(parseInt(openDockMonitor[0]?.value, 10) || 30) * 1.5 * 1000; // this should be >= to the interval we set in the cron TODO: should pull the buffer from the setting and give it an extra 10% then round to nearest int.
if (result.data.length) {
for (const release of result.data) {
await postRelease(release);
lastCheck = formatInTimeZone(
new Date(release.Upd_Date).toISOString(),
"UTC",
"yyyy-MM-dd HH:mm:ss",
);
await delay(500);
}
createCronJob(
"opendock_sync",
`*/${parseInt(openDockMonitor[0]?.value, 10) || 30} * * * * *`,
async () => {
if (opendockSyncRunning) {
log.warn(
{},
"Skipping opendock_sync because previous run is still active",
);
return;
}
} catch (e) {
console.error(
{ error: e },
"Error occurred while running the monitor job",
);
log.error({ error: e }, "Error occurred while running the monitor job");
}
});
opendockSyncRunning = true;
try {
// set this to the latest time.
const result = await prodQuery(
sqlQuery.query.replace("[dateCheck]", `'${lastCheck}'`),
"Get release info",
);
log.debug(
{ lastCheck },
`${result.data.length} Changes to a release have been made`,
);
if (result.data.length) {
for (const release of result.data) {
await postRelease(release);
// add a 2 seconds to account for a massive influx of orders and when we dont finish in 1 go it wont try to grab the same amount again
const nDate = new Date(release.Upd_Date);
nDate.setSeconds(nDate.getSeconds() + 2);
lastCheck = formatInTimeZone(
nDate.toISOString(),
"UTC",
"yyyy-MM-dd HH:mm:ss",
);
log.debug({ lastCheck }, "Changes to a release have been made");
await delay(500);
}
}
} catch (e) {
console.error(
{ error: e },
"Error occurred while running the monitor job",
);
log.error(
{ error: e },
"Error occurred while running the monitor job",
);
} finally {
opendockSyncRunning = false;
}
},
"monitorReleaseChanges",
);
}
// run the main game loop
// while (openDockSetting) {
// try {
// const result = await prodQuery(
// sqlQuery.query.replace("[dateCheck]", `'${lastCheck}'`),
// "Get release info",
// );
// if (result.data.length) {
// for (const release of result.data) {
// // potentially move this to a buffer table to easy up on memory
// await postRelease(release);
// // Move checkpoint AFTER successful post
// lastCheck = formatInTimeZone(
// new Date(release.Upd_Date).toISOString(),
// "UTC",
// "yyyy-MM-dd HH:mm:ss",
// );
// await delay(500);
// }
// }
// } catch (e) {
// console.error("Monitor error:", e);
// }
// await delay(15 * 1000); // making this 15 seconds as we would really only see issues if we have a mass burst.
// }
};
// export const monitorReleaseChanges = async () => {
// console.log("Starting release monitor", lastCheck);
// setInterval(async () => {
// try {
// const result = await prodQuery(
// releaseQuery.replace("[dateCheck]", `'${lastCheck}'`),
// "get last release change",
// );
// //console.log(releaseQuery.replace("[dateCheck]", `'${lastCheck}'`));
// if (result.data.length > 0) {
// console.log(
// formatInTimeZone(
// result.data[result.data.length - 1].Upd_Date,
// "UTC",
// "yyyy-MM-dd HH:mm:ss",
// ),
// lastCheck,
// );
// lastCheck = formatInTimeZone(
// result.data[result.data.length - 1].Upd_Date,
// "UTC",
// "yyyy-MM-dd HH:mm:ss",
// );
// const releases = result.data;
// for (let i = 0; i < releases.length; i++) {
// const newDockApt = {
// status: "Scheduled",
// userId: "ee956455-e193-47fc-b53b-dff30fabdf4b", // this should be the carrierid
// loadTypeId: "0aa7988e-b17b-4f10-acdd-3d029b44a773", // well get this and make it a default one
// dockId: "00ba4386-ce5a-4dd1-9356-6e6d10a24609", // this the warehouse we want it in to start out
// refNumbers: [releases[i].ReleaseNumber],
// refNumber: releases[i].ReleaseNumber,
// start: releases[i].DeliveryDate,
// end: addHours(releases[i].DeliveryDate, 1),
// notes: "",
// ccEmails: [""],
// muteNotifications: true,
// metadata: {
// externalValidationFailed: false,
// externalValidationErrorMessage: null,
// },
// units: null,
// customFields: [
// {
// name: "strArticle",
// type: "str",
// label: "Article",
// value: `${releases[i].LineItemHumanReadableId} - ${releases[i].ArticleAlias}`,
// description: "What bottle are we sending ",
// placeholder: "",
// dropDownValues: [],
// minLengthOrValue: 1,
// hiddenFromCarrier: false,
// requiredForCarrier: false,
// requiredForWarehouse: false,
// },
// {
// name: "intPallet Count",
// type: "int",
// label: "Pallet Count",
// value: parseInt(releases[i].LoadingUnits, 10),
// description: "How many pallets",
// placeholder: "22",
// dropDownValues: [],
// minLengthOrValue: 1,
// hiddenFromCarrier: false,
// requiredForCarrier: false,
// requiredForWarehouse: false,
// },
// {
// name: "strTotal Weight",
// type: "str",
// label: "Total Weight",
// value: `${(((releases[i].Quantity * releases[i].LineItemArticleWeight) / 1000) * 2.20462).toFixed(2)}`,
// description: "What is the total weight of the load",
// placeholder: "",
// dropDownValues: [],
// minLengthOrValue: 1,
// hiddenFromCarrier: false,
// requiredForCarrier: false,
// requiredForWarehouse: false,
// },
// {
// name: "strCustomer ReleaseNumber",
// type: "str",
// label: "Customer Release Number",
// value: `${releases[i].CustomerReleaseNumber}`,
// description: "What is the customer release number",
// placeholder: "",
// dropDownValues: [],
// minLengthOrValue: 1,
// hiddenFromCarrier: false,
// requiredForCarrier: false,
// requiredForWarehouse: false,
// },
// ],
// };
// //console.log(newDockApt);
// const newDockResult = await axios.post(
// "https://neutron.staging.opendock.com/appointment",
// newDockApt,
// {
// headers: {
// "content-type": "application/json; charset=utf-8",
// },
// },
// );
// console.log(newDockResult.statusText);
// await delay(500);
// }
// }
// } catch (e) {
// console.log(e);
// }
// }, 5 * 1000);
// };

View File

@@ -28,7 +28,7 @@ export const getToken = async () => {
}
odToken = { odToken: data.access_token, tokenDate: new Date() };
log.info({}, "Token added");
log.info({ odToken }, "Token added");
} catch (e) {
log.error({ error: e }, "Error getting/refreshing token");
}

View File

@@ -36,12 +36,12 @@ export const opendockSocketMonitor = async () => {
// console.log(data);
// });
socket.on("create-Appointment", (data) => {
console.log("appt create:", data);
socket.on("create-Appointment", () => {
//console.log("appt create:", data);
});
socket.on("update-Appointment", (data) => {
console.log("appt update:", data);
socket.on("update-Appointment", () => {
//console.log("appt update:", data);
});
socket.on("error", (data) => {

View File

@@ -0,0 +1,63 @@
use AlplaPROD_test1
declare @intervalCheck as int = '[interval]'
/*
Monitors alpla purchase for thing new. this will not update unless the order status is updated.
this means if a user just reopens the order it will update but everything changed in the position will not be updated until the user reorders or cancels the po
*/
select
IdBestellung as apo
,po.revision as revision
,po.Bestaetigt as confirmed
,po.status
,case po.Status
when 1 then 'Created'
when 2 then 'Ordered'
when 22 then 'Reopened'
when 11 then 'Reopened'
when 4 then 'Planned'
when 5 then 'Partly Delivered'
when 6 then 'Delivered'
when 7 then 'Canceled'
when 8 then 'Closed'
else 'Unknown' end as statusText
,po.IdJournal as journalNum -- use this to validate if we used it already.
,po.Add_User as add_user
,po.Add_Date as add_date
,po.Upd_User as upd_user
,po.Upd_Date as upd_Date
,po.Bemerkung as remark
,po.IdJournal as journal -- use this to validate if we used it already.
,isnull((
select
o.IdArtikelVarianten as av
,a.Bezeichnung as alias
,Lieferdatum as deliveryDate
,cast(BestellMenge as decimal(18,2)) as qty
,cast(BestellMengeVPK as decimal(18,0)) as pkg
,cast(PreisProEinheit as decimal(18,0)) as price
,PositionsStatus
,case PositionsStatus
when 1 then 'Created'
when 2 then 'Ordered'
when 22 then 'Reopened'
when 4 then 'Planned'
when 5 then 'Partly Delivered'
when 6 then 'Delivered'
when 7 then 'Canceled'
when 8 then 'Closed'
else 'Unknown' end as statusText
,o.upd_user
,o.upd_date
from T_Bestellpositionen (nolock) as o
left join
T_Artikelvarianten as a on
a.IdArtikelvarianten = o.IdArtikelVarianten
where o.IdBestellung = po.IdBestellung
for json path
), '[]') as position
--,*
from T_Bestellungen (nolock) as po
where po.Upd_Date > dateadd(MINUTE, -@intervalCheck, getdate())

View File

@@ -0,0 +1,28 @@
use [test1_AlplaPROD2.0_Read]
SELECT
--JSON_VALUE(content, '$.EntityId') as labelId
a.id
,ActorName
,FORMAT(PrintDate, 'yyyy-MM-dd HH:mm') as printDate
,FORMAT(CreatedDateTime, 'yyyy-MM-dd HH:mm') createdDateTime
,l.ArticleHumanReadableId as av
,l.ArticleDescription as alias
,PrintedCopies
,p.name as printerName
,RunningNumber
--,*
FROM [support].[AuditLog] (nolock) as a
left join
[labelling].[InternalLabel] (nolock) as l on
l.id = JSON_VALUE(content, '$.EntityId')
left join
[masterData].[printer] (nolock) as p on
p.id = l.PrinterId
where message like '%reprint%'
and CreatedDateTime > DATEADD(minute, -[intervalCheck], SYSDATETIMEOFFSET())
and a.id > [ignoreList]
order by CreatedDateTime desc

View File

@@ -0,0 +1,97 @@
/**
* This will monitor alpla purchase
*/
import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import {
alplaPurchaseHistory,
type NewAlplaPurchaseHistory,
} from "../db/schema/alplapurchase.schema.js";
import { settings } from "../db/schema/settings.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { createCronJob } from "../utils/croner.utils.js";
import { delay } from "../utils/delay.utils.js";
import { returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const log = createLogger({ module: "purchase", subModule: "purchaseMonitor" });
export const monitorAlplaPurchase = async () => {
const purchaseMonitor = await db
.select()
.from(settings)
.where(eq(settings.name, "purchaseMonitor"));
const sqlQuery = sqlQuerySelector(`alplapurchase`) as SqlQuery;
if (!sqlQuery.success) {
return returnFunc({
success: false,
level: "error",
module: "purchase",
subModule: "query",
message: `Error getting alpla purchase info`,
data: [sqlQuery.message],
notify: false,
});
}
if (purchaseMonitor[0]?.active) {
createCronJob("purchaseMonitor", "0 */5 * * * *", async () => {
try {
const result = await prodQuery(
sqlQuery.query.replace(
"[interval]",
`${purchaseMonitor[0]?.value || "5"}`,
),
"Get release info",
);
log.debug(
{},
`There are ${result.data.length} pending to be updated from the last ${purchaseMonitor[0]?.value}`,
);
if (result.data.length) {
const convertedData = result.data.map((i) => ({
...i,
position: JSON.parse(i.position),
})) as NewAlplaPurchaseHistory;
const { data, error } = await tryCatch(
db.insert(alplaPurchaseHistory).values(convertedData).returning(),
);
if (data) {
log.debug(
{ data },
"New data was just added to alpla purchase history",
);
}
if (error) {
log.error(
{ error },
"There was an error adding alpla purchase history",
);
}
await delay(500);
}
} catch (e) {
console.error(
{ error: e },
"Error occurred while running the monitor job",
);
log.error({ error: e }, "Error occurred while running the monitor job");
return;
}
});
}
};

View File

@@ -0,0 +1,188 @@
{
"GPIO-LED": {
"GPODefaults": {
"1": "HIGH",
"2": "HIGH",
"3": "HIGH",
"4": "HIGH"
},
"LEDDefaults": {
"3": "GREEN"
},
"TAG_READ": [
{
"pin": 1,
"state": "HIGH",
"type": "GPO"
}
]
},
"READER-GATEWAY": {
"batching": [
{
"maxPayloadSizePerReport": 256000,
"reportingInterval": 2000
}
],
"endpointConfig": {
"data": {
"event": {
"connections": [
{
"additionalOptions": {
"batching": {
"maxPayloadSizePerReport": 256000,
"reportingInterval": 2000
},
"retention": {
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
},
"description": "",
"name": "LST",
"options": {
"URL": "https://usday1prod.alpla.net/lst/old/api/rfid/taginfo/line3.4",
"security": {
"CACertificateFileLocation": "",
"authenticationOptions": {
"privateKeyFileLocation": "/readerconfig/ssl/server.key",
"publicKeyFileLocation": "/readerconfig/ssl/server.crt"
},
"authenticationType": "NONE",
"verifyHost": false,
"verifyPeer": false
}
},
"type": "httpPost"
}
]
}
}
},
"managementEventConfig": {
"errors": {
"antenna": false,
"cpu": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"database": true,
"flash": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"ntp": true,
"radio": true,
"radio_control": true,
"ram": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"reader_gateway": true,
"userApp": {
"reportIntervalInSec": 1800,
"threshold": 120
}
},
"gpiEvents": true,
"gpoEvents": true,
"heartbeat": {
"fields": {
"radio_control": [
"ANTENNAS",
"RADIO_ACTIVITY",
"RADIO_CONNECTION",
"CPU",
"RAM",
"UPTIME",
"NUM_ERRORS",
"NUM_WARNINGS",
"NUM_TAG_READS",
"NUM_TAG_READS_PER_ANTENNA",
"NUM_DATA_MESSAGES_TXED",
"NUM_RADIO_PACKETS_RXED"
],
"reader_gateway": [
"NUM_DATA_MESSAGES_RXED",
"NUM_MANAGEMENT_EVENTS_TXED",
"NUM_DATA_MESSAGES_TXED",
"NUM_DATA_MESSAGES_RETAINED",
"NUM_DATA_MESSAGES_DROPPED",
"CPU",
"RAM",
"UPTIME",
"NUM_ERRORS",
"NUM_WARNINGS",
"INTERFACE_CONNECTION_STATUS",
"NOLOCKQ_DEPTH"
],
"system": [
"CPU",
"FLASH",
"NTP",
"RAM",
"SYSTEMTIME",
"TEMPERATURE",
"UPTIME",
"GPO",
"GPI",
"POWER_NEGOTIATION",
"POWER_SOURCE",
"MAC_ADDRESS",
"HOSTNAME"
],
"userDefined": null,
"userapps": [
"STATUS",
"CPU",
"RAM",
"UPTIME",
"NUM_DATA_MESSAGES_RXED",
"NUM_DATA_MESSAGES_TXED",
"INCOMING_DATA_BUFFER_PERCENTAGE_REMAINING",
"OUTGOING_DATA_BUFFER_PERCENTAGE_REMAINING"
]
},
"interval": 60
},
"userappEvents": true,
"warnings": {
"cpu": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"database": true,
"flash": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"ntp": true,
"radio_api": true,
"radio_control": true,
"ram": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"reader_gateway": true,
"temperature": {
"ambient": 75,
"pa": 105
},
"userApp": {
"reportIntervalInSec": 1800,
"threshold": 60
}
}
},
"retention": [
{
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
]
},
"xml": "<?xml version='1.0'?>\n<Motorola xmlns:Falcon='http://www.motorola.com/RFID/Readers/Config/Falcon' xmlns='http://www.motorola.com/RFID/Readers/Config/Falcon'>\n<Config>\n<AppVersion major='3' minor='28' build='1' maintenance='0'/>\n<CommConfig EnabledStacks='IPV4' DisableRAPktProcessing='1' EnableDHCPv6='1' IPv6StaticIPAddr='fe80::1' IPv6SubnetMask='64' IPv6StaticGateway='::' IPv6DNSIP='fe80::20' DHCP='1' IPAddr='10.44.14.39' Mask='255.255.255.0' Gateway='10.44.14.252' DNS='10.44.9.250' DomainSearch='example.com' HttpRunning='2' TelnetActive='2' FtpActive='2' usbMode='0' WatchdogEnabled='1' AvahiEnabled='1' NetBIOSEnabled='0' RDMPAgentEnabled='1' SerialConTimeout='0' SNTP='0.0.0.0' SNTPHostName='pool.ntp.org' sntpHostDisplayMode='0' llrpClientMode='0' llrpSecureMode='0' llrpSecureModeValidatePeer='0' llrpPort='5084' llrpHostIP='192.168.127.2' allowllrpConnOverride='0' shouldReconnect='1'/>\n<Bluetooth discoverable='0' pairable='0' PincodeEnabled='0' passkey='165CB22DA5BE7BBEFB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03FB77709DD0A94B03' startIP='192.168.0.2' endIP='192.168.0.3'/>\n<WirelessConfig essid='' autoconnect='0'/>\n<RegionConfig RFCountry='United States/Canada' RFRegulatory='US FCC 15' RFScanMode='0' LBTEnable='0' ChannelData='FFFFFFFFFFFFFFFF'/>\n<SnmpConfig snmpVersion='1' heartbeat='1'/>\n<SyslogConfig RemoteIp='0.0.0.0' RemotePort='514' LogMinSeverity='7' ApplyFilter='0' MinimumSeverity='7' ProcessFilter='rmserver.elf,llrpserver.elf,snmpextagent.elf,RDMPAgent'/>\n<UserList>\n<User name='admin' PSWD='$6$weLpDwlv$utr0AwgPIae2O4Gln4cQ2IJJblXye412Xqni0V.ahIFKUOCEDGjzZ4ttthhrw7rmmQYsCXKwA9znyqPkAT.IL/'/>\n<User name='rfidadm' PSWD='15491'/>\n</UserList>\n<IPReader name='FX96007AF832 FX9600 RFID Reader' desc='FX96007AF832 Advanced Reader' flags='0' MonoStatic='0' CheckAntenna='1' gpiDebounceTime='0' gpioMapping='0' idleModeTimeOut='0' diagMode='0' extDiagMode='0' contact='Zebra Technologies Corporation' PowerNegotiation='0' PowerNegotiationProtocol='0' allowGuestLogin='1' configureHostName='0'>\n<ReadPoint name='Read Point 1' flags='0' CableLossPerHundredFt='10' CableLength='10'/>\n<ReadPoint name='Read Point 2' flags='0' CableLossPerHundredFt='10' CableLength='10'/>\n<ReadPoint name='Read Point 3' flags='1' CableLossPerHundredFt='10' CableLength='10'/>\n<ReadPoint name='Read Point 4' flags='1' CableLossPerHundredFt='10' CableLength='10'/>\n</IPReader>\n<SerialPortConf Mode='0' Baudrate='115200' Databits='8' Parity='none' Stopbits='1' Flowcontrol='hardware' TagMetaData='0' InventoryControl='0' IsAutostart='0'/>\n<FXConnectConfig FXConnectMode='0' TagMetaData='0' InventoryControl='None' HeartBeatPeriod='0' IsAutostart='0' PreFilterMode='0' PreFilters='None'/>\n<ProfinetConfig virtualDAP='1'/>\n<NodeJSPortConf Portnumber='8001'/>\n</Config>\n<MOTOROLA_LLRP_CONFIG><LLRP_READER_CONFIG />\n</MOTOROLA_LLRP_CONFIG>\n<IOT_CONNECT_CONFIG><OPERATING_MODE />\n</IOT_CONNECT_CONFIG>\n<RadioProfileData><RadioRegisterData Address='0' Data='00'/>\n</RadioProfileData>\n<CustomProfileData ForceEAPMode='0' FIPS_MODE_ENABLED='0' MaxNumberOfTagsBuffered='512'/>\n</Motorola >\n"
}

View File

@@ -0,0 +1,206 @@
{
"GPIO-LED": {
"GPODefaults": {
"1": "HIGH",
"2": "HIGH",
"3": "HIGH",
"4": "HIGH"
},
"LEDDefaults": {
"3": "GREEN"
},
"TAG_READ": [
{
"pin": 1,
"state": "HIGH",
"type": "GPO"
}
]
},
"READER-GATEWAY": {
"batching": [
{
"maxPayloadSizePerReport": 256000,
"reportingInterval": 2000
}
],
"endpointConfig": {
"data": {
"event": {
"connections": [
{
"additionalOptions": {
"retention": {
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
},
"description": "",
"name": "lst",
"options": {
"URL": "http://usday1vms006:3100/api/rfid/taginfo/wrapper1",
"security": {
"CACertificateFileLocation": "",
"authenticationOptions": {},
"authenticationType": "NONE",
"verifyHost": false,
"verifyPeer": false
}
},
"type": "httpPost"
},
{
"additionalOptions": {
"retention": {
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
},
"description": "",
"name": "mgt",
"options": {
"URL": "http://usday1vms006:3100/api/rfid/mgtevents/wrapper1",
"security": {
"CACertificateFileLocation": "",
"authenticationOptions": {},
"authenticationType": "NONE",
"verifyHost": false,
"verifyPeer": false
}
},
"type": "httpPost"
}
]
}
}
},
"interfaces": {
"tagDataInterface1": "lst",
"managementEventsInterface": "mgt"
},
"managementEventConfig": {
"errors": {
"antenna": false,
"cpu": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"database": true,
"flash": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"ntp": true,
"radio": true,
"radio_control": true,
"ram": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"reader_gateway": true,
"userApp": {
"reportIntervalInSec": 1800,
"threshold": 120
}
},
"gpiEvents": true,
"gpoEvents": true,
"heartbeat": {
"fields": {
"radio_control": [
"ANTENNAS",
"RADIO_ACTIVITY",
"RADIO_CONNECTION",
"CPU",
"RAM",
"UPTIME",
"NUM_ERRORS",
"NUM_WARNINGS",
"NUM_TAG_READS",
"NUM_TAG_READS_PER_ANTENNA",
"NUM_DATA_MESSAGES_TXED",
"NUM_RADIO_PACKETS_RXED"
],
"reader_gateway": [
"NUM_DATA_MESSAGES_RXED",
"NUM_MANAGEMENT_EVENTS_TXED",
"NUM_DATA_MESSAGES_TXED",
"NUM_DATA_MESSAGES_RETAINED",
"NUM_DATA_MESSAGES_DROPPED",
"CPU",
"RAM",
"UPTIME",
"NUM_ERRORS",
"NUM_WARNINGS",
"INTERFACE_CONNECTION_STATUS",
"NOLOCKQ_DEPTH"
],
"system": [
"CPU",
"FLASH",
"NTP",
"RAM",
"SYSTEMTIME",
"TEMPERATURE",
"UPTIME",
"GPO",
"GPI",
"POWER_NEGOTIATION",
"POWER_SOURCE",
"MAC_ADDRESS",
"HOSTNAME"
],
"userDefined": null,
"userapps": [
"STATUS",
"CPU",
"RAM",
"UPTIME",
"NUM_DATA_MESSAGES_RXED",
"NUM_DATA_MESSAGES_TXED",
"INCOMING_DATA_BUFFER_PERCENTAGE_REMAINING",
"OUTGOING_DATA_BUFFER_PERCENTAGE_REMAINING"
]
},
"interval": 60
},
"userappEvents": true,
"warnings": {
"cpu": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"database": true,
"flash": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"ntp": true,
"radio_api": true,
"radio_control": true,
"ram": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"reader_gateway": true,
"temperature": {
"ambient": 75,
"pa": 105
},
"userApp": {
"reportIntervalInSec": 1800,
"threshold": 60
}
}
},
"retention": [
{
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
]
}
}

View File

@@ -1,9 +1,11 @@
import type { Express } from "express";
import { setupNotificationRoutes } from "notification/notification.routes.js";
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 { setupNotificationRoutes } from "./notification/notification.routes.js";
import { setupOCPRoutes } from "./ocp/ocp.routes.js";
import { setupOpendockRoutes } from "./opendock/opendock.routes.js";
import { setupProdSqlRoutes } from "./prodSql/prodSql.routes.js";
import { setupSystemRoutes } from "./system/system.routes.js";
@@ -19,4 +21,5 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
setupUtilsRoutes(baseUrl, app);
setupOpendockRoutes(baseUrl, app);
setupNotificationRoutes(baseUrl, app);
setupOCPRoutes(baseUrl, app);
};

View File

@@ -1,15 +1,16 @@
import { createServer } from "node:http";
import os from "node:os";
import { startNotifications } from "notification/notification.controller.js";
import { createNotifications } from "notification/notifications.master.js";
import createApp from "./app.js";
import { db } from "./db/db.controller.js";
import { dbCleanup } from "./db/dbCleanup.controller.js";
import { type Setting, settings } from "./db/schema/settings.schema.js";
import { createLogger } from "./logger/logger.controller.js";
import { startNotifications } from "./notification/notification.controller.js";
import { createNotifications } from "./notification/notifications.master.js";
import { monitorReleaseChanges } from "./opendock/openDockRreleaseMonitor.utils.js";
import { opendockSocketMonitor } from "./opendock/opendockSocketMonitor.utils.js";
import { connectProdSql } from "./prodSql/prodSqlConnection.controller.js";
import { monitorAlplaPurchase } from "./purchase/purchase.controller.js";
import { setupSocketIORoutes } from "./socket.io/serverSetup.js";
import { baseSettingValidationCheck } from "./system/settingsBase.controller.js";
import { createCronJob } from "./utils/croner.utils.js";
@@ -36,7 +37,7 @@ const start = async () => {
// also we always want to have long lived processes inside a setting check.
setTimeout(() => {
if (systemSettings.filter((n) => n.name === "opendock_sync")[0]?.active) {
log.info({}, "Opendock is not active");
log.info({}, "Opendock is active");
monitorReleaseChanges(); // this is od monitoring the db for all new releases
opendockSocketMonitor();
createCronJob("opendockAptCleanup", "0 30 5 * * *", () =>
@@ -44,6 +45,10 @@ const start = async () => {
);
}
if (systemSettings.filter((n) => n.name === "purchaseMonitor")[0]?.active) {
monitorAlplaPurchase();
}
// these jobs below are system jobs and should run no matter what.
createCronJob("JobAuditLogCleanUp", "0 0 5 * * *", () =>
dbCleanup("jobs", 30),

View File

@@ -8,8 +8,8 @@ type RoomDefinition<T = unknown> = {
};
export const protectedRooms: any = {
logs: { requiresAuth: true, role: "admin" },
admin: { requiresAuth: true, role: "admin" },
logs: { requiresAuth: true, role: ["admin", "systemAdmin"] },
admin: { requiresAuth: true, role: ["admin", "systemAdmin"] },
};
export const roomDefinition: Record<RoomId, RoomDefinition> = {

View File

@@ -13,9 +13,9 @@ import { createRoomEmitter, preseedRoom } from "./roomService.socket.js";
//const __dirname = dirname(__filename);
const log = createLogger({ module: "socket.io", subModule: "setup" });
import { auth } from "../utils/auth.utils.js";
//import type { Session, User } from "better-auth"; // adjust if needed
import { protectedRooms } from "./roomDefinitions.socket.js";
import { auth } from "../utils/auth.utils.js";
// declare module "socket.io" {
// interface Socket {
@@ -88,7 +88,12 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
});
}
if (config?.role && s.user?.role !== config.role) {
const roles = Array.isArray(config.role) ? config.role : [config.role];
console.log(roles, s.user.role);
//if (config?.role && s.user?.role !== config.role) {
if (config?.role && !roles.includes(s.user?.role)) {
return s.emit("room-error", {
room: rn,
message: `Not authorized to be in room: ${rn}`,

View File

@@ -8,8 +8,8 @@ const newSettings: NewSetting[] = [
// feature settings
{
name: "opendock_sync",
value: "0",
active: true,
value: "15",
active: false,
description: "Dock Scheduling system",
moduleName: "opendock",
settingType: "feature",
@@ -19,7 +19,7 @@ const newSettings: NewSetting[] = [
{
name: "ocp",
value: "1",
active: true,
active: false,
description: "One click print",
moduleName: "ocp",
settingType: "feature",
@@ -29,7 +29,7 @@ const newSettings: NewSetting[] = [
{
name: "ocme",
value: "0",
active: true,
active: false,
description: "Dayton Agv system",
moduleName: "ocme",
settingType: "feature",
@@ -39,7 +39,7 @@ const newSettings: NewSetting[] = [
{
name: "demandManagement",
value: "1",
active: true,
active: false,
description: "Fake EDI System",
moduleName: "demandManagement",
settingType: "feature",
@@ -49,7 +49,7 @@ const newSettings: NewSetting[] = [
{
name: "qualityRequest",
value: "0",
active: true,
active: false,
description: "Quality System",
moduleName: "qualityRequest",
settingType: "feature",
@@ -59,13 +59,23 @@ const newSettings: NewSetting[] = [
{
name: "tms",
value: "0",
active: true,
active: false,
description: "Transport system integration",
moduleName: "tms",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
{
name: "purchaseMonitor",
value: "5",
active: true,
description: "Monitors alpla purchase fo all changes",
moduleName: "purchase",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
// standard settings
{

View File

@@ -10,6 +10,7 @@ import {
killOpendockSocket,
opendockSocketMonitor,
} from "../opendock/opendockSocketMonitor.utils.js";
import { monitorAlplaPurchase } from "../purchase/purchase.controller.js";
import {
createCronJob,
resumeCronJob,
@@ -31,8 +32,24 @@ export const featureControl = async (data: Setting) => {
createCronJob("opendockAptCleanup", "0 30 5 * * *", () =>
dbCleanup("opendockApt", 90),
);
} else {
}
if (data.name === "opendock_sync" && !data.active) {
killOpendockSocket();
stopCronJob("opendockAptCleanup");
}
// purchase stuff
if (data.name === "purchaseMonitor" && data.active) {
monitorAlplaPurchase();
}
if (data.name === "purchaseMonitor" && !data.active) {
stopCronJob("purchaseMonitor");
}
// this means the data time has changed
if (data.name === "purchaseMonitor" && data.value) {
monitorAlplaPurchase();
}
};

View File

@@ -15,6 +15,7 @@ export const allowedOrigins = [
`http://${process.env.PROD_SERVER}:3000`,
`http://${process.env.PROD_SERVER}:3100`, // temp
`http://usmcd1olp082:3000`,
`${process.env.EXTERNAL_URL}`, // internal docker
];
export const lstCors = () => {
return cors({

View File

@@ -18,7 +18,9 @@ export interface JobInfo {
// Store running cronjobs
export const runningCrons: Record<string, Cron> = {};
const activeRuns = new Set<string>();
const log = createLogger({ module: "system", subModule: "croner" });
const cronStats: Record<string, { created: number; replaced: number }> = {};
// how to se the times
// * ┌──────────────── (optional) second (0 - 59)
@@ -38,17 +40,36 @@ const log = createLogger({ module: "system", subModule: "croner" });
* @param name Name of the job we want to run
* @param schedule Cron expression (example: `*\/5 * * * * *`)
* @param task Async function that will run
* @param source we can add where it came from to assist in getting this tracked down, more for debugging
*/
export const createCronJob = async (
name: string,
schedule: string, // cron string with 8 8 IE: */5 * * * * * every 5th second
task: () => Promise<void>, // what function are we passing over
source = "unknown",
) => {
// get the timezone based on the os timezone set
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// initial go so just store it this is more for debugging to see if something crazy keeps happening
if (!cronStats[name]) {
cronStats[name] = { created: 0, replaced: 0 };
}
// Destroy existing job if it exist
if (runningCrons[name]) {
cronStats[name].replaced += 1;
log.warn(
{
job: name,
source,
oldSchedule: runningCrons[name].getPattern?.(),
newSchedule: schedule,
replaceCount: cronStats[name].replaced,
},
`Cron job "${name}" already existed and is being replaced`,
);
runningCrons[name].stop();
}
@@ -61,6 +82,13 @@ export const createCronJob = async (
name: name,
},
async () => {
if (activeRuns.has(name)) {
log.warn({ jobName: name }, "Skipping overlapping cron execution");
return;
}
activeRuns.add(name);
const startedAt = new Date();
const start = Date.now();
@@ -91,14 +119,19 @@ export const createCronJob = async (
.where(eq(jobAuditLog.id, executionId));
} catch (e: any) {
if (executionId) {
await db.update(jobAuditLog).set({
finishedAt: new Date(),
durationMs: Date.now() - start,
status: "error",
errorMessage: e.message,
errorStack: e.stack,
});
await db
.update(jobAuditLog)
.set({
finishedAt: new Date(),
durationMs: Date.now() - start,
status: "error",
errorMessage: e.message,
errorStack: e.stack,
})
.where(eq(jobAuditLog.id, executionId));
}
} finally {
activeRuns.delete(name);
}
},
);

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{!-- <link rel="stylesheet" href="styles/styles.css" /> --}}
{{> styles}}
</head>
<body>
<p>All,</p>
<p>The below labels have been reprinted.</p>
<table >
<thead>
<tr>
<th>AV</th>
<th>Description</th>
<th>Label Number</th>
<th>Date Added</th>
<th>Date Reprinted</th>
<th>Who printed/Updated</th>
<th>What printer it came from</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{av}}</td>
<td>{{alias}}</td>
<td>{{RunningNumber}}</td>
<td>{{printDate}}</td>
<td>{{createdDateTime}}</td>
<td>{{ActorName}}</td>
<td>{{printerName}}</td>
</tr>
{{/each}}
</tbody>
</table>
<div>
<p>Thank you,</p>
<p>LST Team</p>
</div>
</body>
</html>

View File

@@ -10,7 +10,9 @@ interface Data<T = unknown[]> {
| "datamart"
| "utils"
| "opendock"
| "notification";
| "notification"
| "email"
| "purchase";
subModule:
| "db"
| "labeling"
@@ -28,7 +30,8 @@ interface Data<T = unknown[]> {
| "delete"
| "post"
| "notification"
| "delete";
| "delete"
| "printing";
level: "info" | "error" | "debug" | "fatal";
message: string;
room?: string;

View File

@@ -1,3 +1,7 @@
vars {
url: http://localhost:3000/lst
url: http://localhost:3600/lst
readerIp: 10.44.14.215
}
vars:secret [
token
]

View File

@@ -14,7 +14,7 @@ body:json {
{
"userId":"m6AbQXFwOXoX3YKLfwWgq2LIdDqS5jqv",
"notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e",
"emails": ["blake.mattes@alpla.com"]
"emails": ["blake.mattes@alpla.com","cowchmonkey@gmail.com"]
}
}

View File

@@ -10,14 +10,6 @@ get {
auth: inherit
}
body:json {
{
"userId":"0kHd6Kkdub4GW6rK1qa1yjWwqXtvykqT",
"notificationId": "0399eb2a-39df-48b7-9f1c-d233cec94d2e",
"emails": ["blake.mattes@alpla.com"]
}
}
settings {
encodeUrl: true
timeout: 0

View File

@@ -16,8 +16,8 @@ params:path {
body:json {
{
"active" : false,
"options": [{"prodId": 5}]
"active" : true,
"options": []
}
}

View File

@@ -0,0 +1,22 @@
meta {
name: Printer Listenter
type: http
seq: 1
}
post {
url: {{url}}/api/ocp/printer/listener/line_1
body: json
auth: inherit
}
body:json {
{
"message":"xnvjdhhgsdfr"
}
}
settings {
encodeUrl: true
timeout: 0
}

8
brunoApi/ocp/folder.bru Normal file
View File

@@ -0,0 +1,8 @@
meta {
name: ocp
seq: 9
}
auth {
mode: inherit
}

View File

@@ -0,0 +1,8 @@
meta {
name: rfidReaders
seq: 8
}
auth {
mode: inherit
}

View File

@@ -0,0 +1,20 @@
meta {
name: reader
type: http
seq: 2
}
post {
url: https://usday1prod.alpla.net/lst/old/api/rfid/mgtevents/line3.1
body: json
auth: inherit
}
body:json {
{}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,20 @@
meta {
name: Config
type: http
seq: 2
}
get {
url: https://{{readerIp}}/cloud/config
body: none
auth: bearer
}
auth:bearer {
token: {{token}}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,32 @@
meta {
name: Login
type: http
seq: 1
}
get {
url: https://{{readerIp}}/cloud/localRestLogin
body: none
auth: basic
}
auth:basic {
username: admin
password: Zebra123!
}
script:post-response {
const body = res.getBody();
if (body.message) {
bru.setEnvVar("token", body.message);
} else {
bru.setEnvVar("token", "error");
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,237 @@
meta {
name: Update Config
type: http
seq: 3
}
put {
url: https://{{readerIp}}/cloud/config
body: json
auth: bearer
}
headers {
Content-Type: application/json
}
auth:bearer {
token: {{token}}
}
body:json {
{
"GPIO-LED": {
"GPODefaults": {
"1": "HIGH",
"2": "HIGH",
"3": "HIGH",
"4": "HIGH"
},
"LEDDefaults": {
"3": "GREEN"
},
"TAG_READ": [
{
"pin": 1,
"state": "HIGH",
"type": "GPO"
}
]
},
"READER-GATEWAY": {
"batching": [
{
"maxPayloadSizePerReport": 256000,
"reportingInterval": 2000
},
{
"maxPayloadSizePerReport": 256000,
"reportingInterval": 2000
}
],
"endpointConfig": {
"data": {
"event": {
"connections": [
{
"additionalOptions": {
"retention": {
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
},
"description": "",
"name": "LST",
"options": {
"URL": "https://usday1prod.alpla.net/lst/old/api/rfid/taginfo/line3.4",
"security": {
"CACertificateFileLocation": "",
"authenticationOptions": {},
"authenticationType": "NONE",
"verifyHost": false,
"verifyPeer": false
}
},
"type": "httpPost"
},
{
"additionalOptions": {
"retention": {
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
},
"description": "",
"name": "mgt",
"options": {
"URL": "https://usday1prod.alpla.net/lst/old/api/rfid/mgtevents/line3.4",
"security": {
"CACertificateFileLocation": "",
"authenticationOptions": {},
"authenticationType": "NONE",
"verifyHost": false,
"verifyPeer": false
}
},
"type": "httpPost"
}
]
}
}
},
"managementEventConfig": {
"errors": {
"antenna": false,
"cpu": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"database": true,
"flash": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"ntp": true,
"radio": true,
"radio_control": true,
"ram": {
"reportIntervalInSec": 1800,
"threshold": 90
},
"reader_gateway": true,
"userApp": {
"reportIntervalInSec": 1800,
"threshold": 120
}
},
"gpiEvents": true,
"gpoEvents": true,
"heartbeat": {
"fields": {
"radio_control": [
"ANTENNAS",
"RADIO_ACTIVITY",
"RADIO_CONNECTION",
"CPU",
"RAM",
"UPTIME",
"NUM_ERRORS",
"NUM_WARNINGS",
"NUM_TAG_READS",
"NUM_TAG_READS_PER_ANTENNA",
"NUM_DATA_MESSAGES_TXED",
"NUM_RADIO_PACKETS_RXED"
],
"reader_gateway": [
"NUM_DATA_MESSAGES_RXED",
"NUM_MANAGEMENT_EVENTS_TXED",
"NUM_DATA_MESSAGES_TXED",
"NUM_DATA_MESSAGES_RETAINED",
"NUM_DATA_MESSAGES_DROPPED",
"CPU",
"RAM",
"UPTIME",
"NUM_ERRORS",
"NUM_WARNINGS",
"INTERFACE_CONNECTION_STATUS",
"NOLOCKQ_DEPTH"
],
"system": [
"CPU",
"FLASH",
"NTP",
"RAM",
"SYSTEMTIME",
"TEMPERATURE",
"UPTIME",
"GPO",
"GPI",
"POWER_NEGOTIATION",
"POWER_SOURCE",
"MAC_ADDRESS",
"HOSTNAME"
],
"userapps": [
"STATUS",
"CPU",
"RAM",
"UPTIME",
"NUM_DATA_MESSAGES_RXED",
"NUM_DATA_MESSAGES_TXED",
"INCOMING_DATA_BUFFER_PERCENTAGE_REMAINING",
"OUTGOING_DATA_BUFFER_PERCENTAGE_REMAINING"
]
},
"interval": 60
},
"userappEvents": true,
"warnings": {
"cpu": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"database": true,
"flash": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"ntp": true,
"radio_api": true,
"radio_control": true,
"ram": {
"reportIntervalInSec": 1800,
"threshold": 80
},
"reader_gateway": true,
"temperature": {
"ambient": 75,
"pa": 105
},
"userApp": {
"reportIntervalInSec": 1800,
"threshold": 60
}
}
},
"retention": [
{
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
},
{
"maxEventRetentionTimeInMin": 500,
"maxNumEvents": 150000,
"throttle": 100
}
]
}
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -0,0 +1,12 @@
meta {
name: readerSpecific
}
auth {
mode: basic
}
auth:basic {
username: admin
password: Zebra123!
}

View File

@@ -1,16 +1,26 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
lst:
image: git.tuffraid.net/cowch/lst_v3:latest
container_name: lst_app
restart: unless-stopped
# app:
# build:
# context: .
# dockerfile: Dockerfile
# container_name: lst_app
ports:
#- "${VITE_PORT:-4200}:4200"
- "3600:3000"
dns:
- 10.193.9.250
- 10.193.9.251 # your internal DNS server
dns_search:
- alpla.net # or your internal search suffix
environment:
- NODE_ENV=production
- LOG_LEVEL=info
- DATABASE_HOST=host.docker.internal
- EXTERNAL_URL=http://192.168.8.222:3600
- DATABASE_HOST=host.docker.internal # if running on the same docker then do this
- DATABASE_PORT=5433
- DATABASE_USER=${DATABASE_USER}
- DATABASE_PASSWORD=${DATABASE_PASSWORD}
@@ -21,7 +31,6 @@ services:
- PROD_PASSWORD=${PROD_PASSWORD}
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- BETTER_AUTH_URL=${URL}
restart: unless-stopped
# for all host including prod servers, plc's, printers, or other de
# extra_hosts:
# - "${PROD_SERVER}:${PROD_IP}"

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/admin/settings')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/admin/settings"!</div>
}

View File

@@ -1,69 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@@ -69,7 +69,7 @@ export default function Header() {
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Link to="/user/profile">Profile</Link>
<Link to="/user/profile">Account</Link>
</DropdownMenuItem>
{/* <DropdownMenuItem>Billing</DropdownMenuItem>

View File

@@ -1,5 +1,5 @@
import { Link } from "@tanstack/react-router";
import { Logs } from "lucide-react";
import { Bell, Logs, Settings } from "lucide-react";
import {
SidebarGroup,
@@ -11,17 +11,35 @@ import {
useSidebar,
} from "../ui/sidebar";
export default function AdminSidebar() {
// type AdminSidebarProps = {
// session: {
// user: {
// name?: string | null;
// email?: string | null;
// role?: string | string[];
// };
// } | null;
//};
export default function AdminSidebar({ session }: any) {
const { setOpen } = useSidebar();
const items = [
// {
// title: "Users",
// url: "/admin/users",
// icon: User,
// role: ["systemAdmin", "admin"],
// module: "admin",
// active: true,
// },
{
title: "Notifications",
url: "/admin/notifications",
icon: Bell,
role: ["systemAdmin", "admin"],
module: "admin",
active: true,
},
{
title: "Settings",
url: "/admin/settings",
icon: Settings,
role: ["systemAdmin"],
module: "admin",
active: true,
},
{
title: "Logs",
url: "/admin/logs",
@@ -53,14 +71,18 @@ export default function AdminSidebar() {
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<>
{item.role.includes(session.user.role) && (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)}
</>
))}
</SidebarMenu>
</SidebarGroupContent>

View File

@@ -10,6 +10,7 @@ import AdminSidebar from "./AdminBar";
export function AppSidebar() {
const { data: session } = useSession();
return (
<Sidebar
variant="sidebar"
@@ -20,7 +21,11 @@ export function AppSidebar() {
<SidebarMenu>
<SidebarMenuItem>
<SidebarContent>
{session && session.user.role === "admin" && <AdminSidebar />}
{session &&
(session.user.role === "admin" ||
session.user.role === "systemAdmin") && (
<AdminSidebar session={session} />
)}
</SidebarContent>
</SidebarMenuItem>
</SidebarMenu>

View File

@@ -0,0 +1,53 @@
import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
data-orientation={orientation}
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,190 @@
import * as React from "react"
import { Select as SelectPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("scroll-my-1 p-1", className)}
{...props}
/>
)
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
data-align-trigger={position === "item-aligned"}
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
data-position={position}
className={cn(
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
position === "popper" && ""
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronUpIcon
/>
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronDownIcon
/>
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@@ -0,0 +1,31 @@
import * as React from "react"
import { Switch as SwitchPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Switch({
className,
size = "default",
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
size?: "sm" | "default"
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View File

@@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -0,0 +1,88 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Tabs as TabsPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Tabs({
className,
orientation = "horizontal",
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-horizontal:flex-col",
className
)}
{...props}
/>
)
}
const tabsListVariants = cva(
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
{
variants: {
variant: {
default: "bg-muted",
line: "gap-1 bg-transparent",
},
},
defaultVariants: {
variant: "default",
},
}
)
function TabsList({
className,
variant = "default",
...props
}: React.ComponentProps<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 text-sm outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }

View File

@@ -0,0 +1,44 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Toggle as TogglePrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border border-input bg-transparent hover:bg-muted",
},
size: {
default: "h-8 min-w-8 px-2",
sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-1.5 text-[0.8rem]",
lg: "h-9 min-w-9 px-2.5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Toggle({
className,
variant = "default",
size = "default",
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Toggle, toggleVariants }

View File

@@ -0,0 +1,87 @@
import { Trash2 } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "../../components/ui/button";
import { useFieldContext } from ".";
import { FieldErrors } from "./Errors.Field";
type DynamicInputField = {
name?: string;
label: string;
inputType: "text" | "email" | "password" | "number" | "username";
required?: boolean;
description?: string;
addLabel?: string;
placeholder?: string;
disabled?: boolean;
};
const autoCompleteMap: Record<string, string> = {
email: "email",
password: "current-password",
text: "off",
username: "username",
};
export const DynamicInputField = ({
label,
inputType = "text",
required = false,
description,
addLabel,
}: DynamicInputField) => {
const field = useFieldContext<any>();
const values = Array.isArray(field.state.value) ? field.state.value : [];
return (
<div className="grid gap-3 mt-2">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<Label>{label}</Label>
{description ? (
<p className="text-sm text-muted-foreground">{description}</p>
) : null}
</div>
<Button
type="button"
variant="secondary"
onClick={() => {
field.pushValue("");
}}
>
{addLabel}
</Button>
</div>
<div className="grid gap-3">
{values.map((_: string, index: number) => (
<div key={`${field.name}-${index}`} className="grid gap-2">
<div className="flex items-center gap-2">
<Label htmlFor={field.name}>{label}</Label>
<Input
id={field.name}
autoComplete={autoCompleteMap[inputType] ?? "off"}
value={field.state.value?.[index] ?? ""}
onChange={(e) => field.replaceValue(index, e.target.value)}
onBlur={field.handleBlur}
type={inputType}
required={required}
/>
{values.length > 1 ? (
<Button
type="button"
size={"icon"}
variant="destructive"
onClick={() => field.removeValue(index)}
>
<Trash2 className="w-32 h-32" />
</Button>
) : null}
<FieldErrors meta={field.state.meta} />
</div>
</div>
))}
</div>
</div>
);
};

View File

@@ -6,7 +6,7 @@ import { FieldErrors } from "./Errors.Field";
type InputFieldProps = {
label: string;
inputType: string;
required: boolean;
required?: boolean;
};
const autoCompleteMap: Record<string, string> = {
@@ -16,7 +16,11 @@ const autoCompleteMap: Record<string, string> = {
username: "username",
};
export const InputField = ({ label, inputType, required }: InputFieldProps) => {
export const InputField = ({
label,
inputType,
required = false,
}: InputFieldProps) => {
const field = useFieldContext<any>();
return (

View File

@@ -0,0 +1,57 @@
import { Label } from "../../components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
import { useFieldContext } from ".";
import { FieldErrors } from "./Errors.Field";
type SelectOption = {
value: string;
label: string;
};
type SelectFieldProps = {
label: string;
options: SelectOption[];
placeholder?: string;
};
export const SelectField = ({
label,
options,
placeholder,
}: SelectFieldProps) => {
const field = useFieldContext<string>();
return (
<div className="grid gap-3">
<div className="grid gap-3">
<Label htmlFor={field.name}>{label}</Label>
<Select
value={field.state.value}
onValueChange={(value) => field.handleChange(value)}
>
<SelectTrigger
id={field.name}
onBlur={field.handleBlur}
className="w-min-2/3 w-max-fit"
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent position={"popper"}>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<FieldErrors meta={field.state.meta} />
</div>
);
};

View File

@@ -1,13 +1,19 @@
import { useStore } from "@tanstack/react-form";
import { Button } from "@/components/ui/button";
import { useFormContext } from ".";
import { Spinner } from "@/components/ui/spinner";
import { useFormContext } from ".";
type SubmitButtonProps = {
children: React.ReactNode;
variant?: "default" | "secondary" | "destructive";
className?: string;
};
export const SubmitButton = ({ children }: SubmitButtonProps) => {
export const SubmitButton = ({
children,
variant = "default",
className,
}: SubmitButtonProps) => {
const form = useFormContext();
const [isSubmitting] = useStore(form.store, (state) => [
@@ -17,10 +23,19 @@ export const SubmitButton = ({ children }: SubmitButtonProps) => {
return (
<div className="">
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? <><Spinner data-icon="inline-start" /> Submitting </> : <>{children}</>
}
<Button
type="submit"
disabled={isSubmitting}
variant={variant}
className={className}
>
{isSubmitting ? (
<>
<Spinner data-icon="inline-start" /> Submitting{" "}
</>
) : (
<>{children}</>
)}
</Button>
</div>
);

View File

@@ -0,0 +1,29 @@
import { Label } from "../../components/ui/label";
import { Switch } from "../../components/ui/switch";
import { useFieldContext } from ".";
type SwitchField = {
trueLabel: string;
falseLabel: string;
};
export const SwitchField = ({
trueLabel = "True",
falseLabel = "False",
}: SwitchField) => {
const field = useFieldContext<boolean>();
const checked = field.state.value ?? false;
return (
<div className="flex items-center space-x-2">
<Switch
id={field.name}
checked={checked}
onCheckedChange={field.handleChange}
onBlur={field.handleBlur}
/>
<Label htmlFor={field.name}>{checked ? trueLabel : falseLabel}</Label>
</div>
);
};

View File

@@ -1,8 +1,11 @@
import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
import { CheckboxField } from "./CheckBox.Field";
import { DynamicInputField } from "./DynamicInput.Field";
import { InputField } from "./Input.Field";
import { InputPasswordField } from "./InputPassword.Field";
import { SelectField } from "./Select.Field";
import { SubmitButton } from "./SubmitButton";
import { SwitchField } from "./Switch.Field";
export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts();
@@ -11,11 +14,13 @@ export const { useAppForm } = createFormHook({
fieldComponents: {
InputField,
InputPasswordField,
//SelectField,
SelectField,
CheckboxField,
//DateField,
//TextArea,
//Searchable,
SwitchField,
DynamicInputField,
},
formComponents: { SubmitButton },
fieldContext,

View File

@@ -0,0 +1,22 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
export function getSettings() {
return queryOptions({
queryKey: ["getSettings"],
queryFn: () => fetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const fetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await axios.get("/lst/api/settings");
return data.data;
};

View File

@@ -0,0 +1,24 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
export function notificationSubs(userId?: string) {
return queryOptions({
queryKey: ["notificationSubs"],
queryFn: () => fetch(userId),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const fetch = async (userId?: string) => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await axios.get(
`/lst/api/notification/sub${userId ? `?userId=${userId}` : ""}`,
);
return data.data;
};

View File

@@ -0,0 +1,22 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
export function notifications() {
return queryOptions({
queryKey: ["notifications"],
queryFn: () => fetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const fetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await axios.get("/lst/api/notification");
return data.data;
};

View File

@@ -0,0 +1,70 @@
import { useEffect, useRef, useState } from "react";
import { Input } from "../../components/ui/input";
type EditableCell = {
value: string | number | null | undefined;
id: string;
field: string;
className?: string;
onSubmit: (args: { id: string; field: string; value: string }) => void;
};
export default function EditableCellInput({
value,
id,
field,
className = "w-32",
onSubmit,
}: EditableCell) {
const initialValue = String(value ?? "");
const [localValue, setLocalValue] = useState(initialValue);
const submitting = useRef(false);
useEffect(() => {
setLocalValue(initialValue);
}, [initialValue]);
const handleSubmit = (nextValue: string) => {
const trimmedValue = nextValue.trim();
if (trimmedValue === initialValue) return;
onSubmit({
id,
field,
value: trimmedValue,
});
};
return (
<Input
value={localValue}
className={className}
onChange={(e) => setLocalValue(e.currentTarget.value)}
onBlur={(e) => {
if (submitting.current) return;
submitting.current = true;
handleSubmit(e.currentTarget.value);
setTimeout(() => {
submitting.current = false;
}, 100);
}}
onKeyDown={(e) => {
if (e.key !== "Enter") return;
e.preventDefault();
if (submitting.current) return;
submitting.current = true;
handleSubmit(e.currentTarget.value);
e.currentTarget.blur();
setTimeout(() => {
submitting.current = false;
}, 100);
}}
/>
);
}

View File

@@ -0,0 +1,129 @@
import {
type ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
type SortingState,
useReactTable,
} from "@tanstack/react-table";
import React, { useState } from "react";
import { Button } from "../../components/ui/button";
import { ScrollArea, ScrollBar } from "../../components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../components/ui/table";
import { cn } from "../utils";
type LstTableType = {
className?: string;
tableClassName?: string;
data: any;
columns: any;
};
export default function LstTable({
className = "",
tableClassName = "",
data,
columns,
}: LstTableType) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
//console.log(data);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
//renderSubComponent: ({ row }: { row: any }) => <ExpandedRow row={row} />,
//getRowCanExpand: () => true,
filterFns: {},
state: {
sorting,
columnFilters,
},
});
return (
<div className={className}>
<ScrollArea className="w-full rounded-md border whitespace-nowrap">
<Table className={cn("w-full", tableClassName)}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<React.Fragment key={row.id}>
<TableRow data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
</React.Fragment>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<ScrollBar orientation="horizontal" />
</ScrollArea>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,67 @@
import type { Column } from "@tanstack/react-table";
import { ArrowDown, ArrowUp, Search } from "lucide-react";
import { useState } from "react";
import { Button } from "../../components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "../../components/ui/dropdown-menu";
import { Input } from "../../components/ui/input";
import { cn } from "../utils";
type SearchableHeaderProps<TData> = {
column: Column<TData, unknown>;
title: string;
searchable?: boolean;
};
export default function SearchableHeader<TData>({
column,
title,
searchable = false,
}: SearchableHeaderProps<TData>) {
const [open, setOpen] = useState(false);
return (
<div className="flex items-center gap-1">
<Button
variant="ghost"
className="px-2"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="flex flex-row items-center gap-2">
{title}
{column.getIsSorted() === "asc" ? (
<ArrowUp className="h-4 w-4" />
) : column.getIsSorted() === "desc" ? (
<ArrowDown className="h-4 w-4" />
) : null}
</span>
</Button>
{searchable && (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<Search
className={cn(
"h-4 w-4",
column.getFilterValue() ? "text-primary" : "",
)}
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56 p-2">
<Input
autoFocus
value={(column.getFilterValue() as string) ?? ""}
onChange={(e) => column.setFilterValue(e.target.value)}
placeholder={`Search ${title.toLowerCase()}...`}
className="h-8"
/>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { Skeleton } from "../../components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../components/ui/table";
type TableSkelly = {
rows?: number;
columns?: number;
};
export default function SkellyTable({ rows = 5, columns = 4 }: TableSkelly) {
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{Array.from({ length: columns }).map((_, i) => (
<TableHead key={i}>
<Skeleton className="h-4 w-[80px]" />
</TableHead>
))}
</TableHeader>
<TableBody>
{Array.from({ length: rows }).map((_, r) => (
<TableRow key={r}>
{Array.from({ length: columns }).map((_, c) => (
<TableCell key={c}>
<Skeleton className="h-4 w-full max-w-[120px]" />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}

View File

@@ -11,6 +11,8 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index'
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
import { Route as AdminNotificationsRouteImport } from './routes/admin/notifications'
import { Route as AdminLogsRouteImport } from './routes/admin/logs'
import { Route as authLoginRouteImport } from './routes/(auth)/login'
import { Route as authUserSignupRouteImport } from './routes/(auth)/user.signup'
@@ -27,6 +29,16 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AdminSettingsRoute = AdminSettingsRouteImport.update({
id: '/admin/settings',
path: '/admin/settings',
getParentRoute: () => rootRouteImport,
} as any)
const AdminNotificationsRoute = AdminNotificationsRouteImport.update({
id: '/admin/notifications',
path: '/admin/notifications',
getParentRoute: () => rootRouteImport,
} as any)
const AdminLogsRoute = AdminLogsRouteImport.update({
id: '/admin/logs',
path: '/admin/logs',
@@ -58,6 +70,8 @@ export interface FileRoutesByFullPath {
'/about': typeof AboutRoute
'/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute
'/admin/settings': typeof AdminSettingsRoute
'/user/profile': typeof authUserProfileRoute
'/user/resetpassword': typeof authUserResetpasswordRoute
'/user/signup': typeof authUserSignupRoute
@@ -67,6 +81,8 @@ export interface FileRoutesByTo {
'/about': typeof AboutRoute
'/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute
'/admin/settings': typeof AdminSettingsRoute
'/user/profile': typeof authUserProfileRoute
'/user/resetpassword': typeof authUserResetpasswordRoute
'/user/signup': typeof authUserSignupRoute
@@ -77,6 +93,8 @@ export interface FileRoutesById {
'/about': typeof AboutRoute
'/(auth)/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute
'/admin/settings': typeof AdminSettingsRoute
'/(auth)/user/profile': typeof authUserProfileRoute
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
'/(auth)/user/signup': typeof authUserSignupRoute
@@ -88,6 +106,8 @@ export interface FileRouteTypes {
| '/about'
| '/login'
| '/admin/logs'
| '/admin/notifications'
| '/admin/settings'
| '/user/profile'
| '/user/resetpassword'
| '/user/signup'
@@ -97,6 +117,8 @@ export interface FileRouteTypes {
| '/about'
| '/login'
| '/admin/logs'
| '/admin/notifications'
| '/admin/settings'
| '/user/profile'
| '/user/resetpassword'
| '/user/signup'
@@ -106,6 +128,8 @@ export interface FileRouteTypes {
| '/about'
| '/(auth)/login'
| '/admin/logs'
| '/admin/notifications'
| '/admin/settings'
| '/(auth)/user/profile'
| '/(auth)/user/resetpassword'
| '/(auth)/user/signup'
@@ -116,6 +140,8 @@ export interface RootRouteChildren {
AboutRoute: typeof AboutRoute
authLoginRoute: typeof authLoginRoute
AdminLogsRoute: typeof AdminLogsRoute
AdminNotificationsRoute: typeof AdminNotificationsRoute
AdminSettingsRoute: typeof AdminSettingsRoute
authUserProfileRoute: typeof authUserProfileRoute
authUserResetpasswordRoute: typeof authUserResetpasswordRoute
authUserSignupRoute: typeof authUserSignupRoute
@@ -137,6 +163,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/settings': {
id: '/admin/settings'
path: '/admin/settings'
fullPath: '/admin/settings'
preLoaderRoute: typeof AdminSettingsRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/notifications': {
id: '/admin/notifications'
path: '/admin/notifications'
fullPath: '/admin/notifications'
preLoaderRoute: typeof AdminNotificationsRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/logs': {
id: '/admin/logs'
path: '/admin/logs'
@@ -180,6 +220,8 @@ const rootRouteChildren: RootRouteChildren = {
AboutRoute: AboutRoute,
authLoginRoute: authLoginRoute,
AdminLogsRoute: AdminLogsRoute,
AdminNotificationsRoute: AdminNotificationsRoute,
AdminSettingsRoute: AdminSettingsRoute,
authUserProfileRoute: authUserProfileRoute,
authUserResetpasswordRoute: authUserResetpasswordRoute,
authUserSignupRoute: authUserSignupRoute,

View File

@@ -79,7 +79,9 @@ export default function ChangePassword() {
<div className="flex justify-end mt-6">
<form.AppForm>
<form.SubmitButton>Update Profile</form.SubmitButton>
<form.SubmitButton variant="destructive">
Update Password
</form.SubmitButton>
</form.AppForm>
</div>
</form>

View File

@@ -9,6 +9,7 @@ import {
} from "@/components/ui/card";
import { authClient } from "@/lib/auth-client";
import { useAppForm } from "@/lib/formSutff";
import socket from "../../../lib/socket.io";
export default function LoginForm({ redirectPath }: { redirectPath: string }) {
const loginEmail = localStorage.getItem("loginEmail") || "";
@@ -47,8 +48,12 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
return;
}
toast.success(`Welcome back ${login.data?.user.name}`);
if (login.data) {
socket.disconnect();
socket.connect();
}
} catch (error) {
console.log(error);
console.error(error);
}
},
});

View File

@@ -0,0 +1,104 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import axios from "axios";
import { toast } from "sonner";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../components/ui/card";
import { useAppForm } from "../../../lib/formSutff";
import { notificationSubs } from "../../../lib/queries/notificationSubs";
import { notifications } from "../../../lib/queries/notifications";
export default function NotificationsSubCard({ user }: any) {
const { data } = useSuspenseQuery(notifications());
const { refetch } = useSuspenseQuery(notificationSubs(user.id));
const form = useAppForm({
defaultValues: {
notificationId: "",
emails: [user.email],
},
onSubmit: async ({ value }) => {
if (value.notificationId === "") {
toast.error("Please select a notification before trying to subscribe.");
return;
}
const postD = { ...value, userId: user.id };
try {
const res = await axios.post("/lst/api/notification/sub", postD, {
withCredentials: true,
});
if (res.status === 200) {
toast.success("Notification Subbed");
refetch();
form.reset();
}
} catch (error) {
console.error(error);
}
},
});
let n: any = [];
if (data) {
n = data.map((i: any) => ({
label: i.name,
value: i.id,
}));
}
return (
<div>
<Card className="p-3 w-lg">
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>
All currently active notifications you can subscribe to. selecting a
notification will give you a brief description on how it works
</CardDescription>
</CardHeader>
<CardContent>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<div>
<form.AppField name="notificationId">
{(field) => (
<field.SelectField
label="Notifications"
placeholder="Select Notification"
options={n}
/>
)}
</form.AppField>
</div>
<form.AppField name="emails" mode="array">
{(field) => (
<field.DynamicInputField
label="Notification Emails"
description="Add more email addresses for notification delivery."
inputType="email"
addLabel="Add Email"
//initialValue={session?.user.email}
/>
)}
</form.AppField>
<div className="flex justify-end mt-6">
<form.AppForm>
<form.SubmitButton>Subscribe</form.SubmitButton>
</form.AppForm>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,114 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { createColumnHelper } from "@tanstack/react-table";
import axios from "axios";
import { Trash } from "lucide-react";
import { toast } from "sonner";
import type { Notifications } from "../../../../types/notifications";
import { Button } from "../../../components/ui/button";
import { Card, CardContent, CardHeader } from "../../../components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "../../../components/ui/tooltip";
import { notificationSubs } from "../../../lib/queries/notificationSubs";
import { notifications } from "../../../lib/queries/notifications";
import LstTable from "../../../lib/tableStuff/LstTable";
import SearchableHeader from "../../../lib/tableStuff/SearchableHeader";
export default function NotificationsTable({ userId }: any) {
const { data: subs, refetch } = useSuspenseQuery(notificationSubs(userId));
const { data: note } = useSuspenseQuery(notifications());
const columnHelper = createColumnHelper<Notifications>();
// filter out the current
const notificationMap = Object.fromEntries(note.map((n: any) => [n.id, n]));
const data = subs.map((sub: any) => ({
...sub,
name: notificationMap[sub.notificationId].name || null,
description: notificationMap[sub.notificationId].description || null,
emails: sub.emails ? sub.emails.join(",") : null,
}));
const removeNotification = async (ns: any) => {
try {
const res = await axios.delete(`/lst/api/notification/sub`, {
withCredentials: true,
data: {
userId: ns.userId,
notificationId: ns.notificationId,
},
});
if (res.data.success) {
toast.success(`Subscription removed`);
refetch();
} else {
console.info(res);
toast.error(res.data.message);
}
} catch {
toast.error(`There was an error removing subscription.`);
}
};
const column = [
columnHelper.accessor("name", {
header: ({ column }) => (
<SearchableHeader column={column} title="Name" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("description", {
header: ({ column }) => (
<SearchableHeader column={column} title="Description" />
),
cell: (i) => (
<Tooltip>
<TooltipTrigger>
{i.getValue()?.length > 25 ? (
<span>{i.getValue().slice(0, 25)}...</span>
) : (
<span>{i.getValue()}</span>
)}
</TooltipTrigger>
<TooltipContent>{i.getValue()}</TooltipContent>
</Tooltip>
),
}),
columnHelper.accessor("emails", {
header: ({ column }) => (
<SearchableHeader column={column} title="Emails" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("remove", {
header: ({ column }) => (
<SearchableHeader column={column} title="Remove" searchable={false} />
),
filterFn: "includesString",
cell: (i) => {
return (
<Button
size="icon"
variant={"destructive"}
onClick={() => removeNotification(i.row.original)}
>
<Trash />
</Button>
);
},
}),
];
return (
<Card>
<CardHeader className="text-center">Subscriptions</CardHeader>
<CardContent>
<LstTable data={data} columns={column} />
</CardContent>
</Card>
);
}

View File

@@ -25,7 +25,7 @@ function RouteComponent() {
const redirectPath = search.redirect ?? "/";
return (
<div className="flex justify-center mt-10">
<div className="flex justify-center mt-2">
<LoginForm redirectPath={redirectPath} />
</div>
);

View File

@@ -1,4 +1,5 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { Suspense } from "react";
import { toast } from "sonner";
import {
Card,
@@ -9,7 +10,10 @@ import {
} from "@/components/ui/card";
import { authClient, useSession } from "@/lib/auth-client";
import { useAppForm } from "@/lib/formSutff";
import { Spinner } from "../../components/ui/spinner";
import ChangePassword from "./-components/ChangePassword";
import NotificationsSubCard from "./-components/NotificationsSubCard";
import NotificationsTable from "./-components/NotificationsTable";
export const Route = createFileRoute("/(auth)/user/profile")({
beforeLoad: async () => {
@@ -54,44 +58,86 @@ function RouteComponent() {
},
});
return (
<div className="flex justify-center mt-2 gap-2">
<div>
<Card className="p-6 w-96">
<CardHeader>
<CardTitle>Profile</CardTitle>
<CardDescription>
Change your profile and password below
</CardDescription>
</CardHeader>
<div className="flex justify-center flex-col pt-4 gap-2">
<div className="flex justify-center flex-col pt-4 gap-2 lg:flex-row">
<div>
<Card className="p-6 w-96">
<CardHeader>
<CardTitle>Profile</CardTitle>
<CardDescription>
Change your profile and password below
</CardDescription>
</CardHeader>
<CardContent>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.AppField name="name">
{(field) => (
<field.InputField
label="Name"
inputType="string"
required={true}
/>
)}
</form.AppField>
<CardContent>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.AppField name="name">
{(field) => (
<field.InputField
label="Name"
inputType="string"
required={true}
/>
)}
</form.AppField>
<div className="flex justify-end mt-6">
<form.AppForm>
<form.SubmitButton>Update Profile</form.SubmitButton>
</form.AppForm>
</div>
</form>
</CardContent>
</Card>
<div className="flex justify-end mt-6">
<form.AppForm>
<form.SubmitButton>Update Profile</form.SubmitButton>
</form.AppForm>
</div>
</form>
</CardContent>
</Card>
</div>
<div>
<ChangePassword />
</div>
<div>
<Suspense
fallback={
<Card className="p-3 w-lg">
<CardHeader>
<CardTitle>Notifications</CardTitle>
</CardHeader>
<CardContent>
<div className="flex justify-center m-auto">
<div>
<Spinner className="size-32" />
</div>
</div>
</CardContent>
</Card>
}
>
{session && <NotificationsSubCard user={session.user} />}
</Suspense>
</div>
</div>
<div>
<ChangePassword />
<div className="w-fill">
<Suspense
fallback={
<Card className="p-3">
<CardHeader>
<CardTitle className="text-center">Subscriptions</CardTitle>
</CardHeader>
<CardContent>
<div className="flex justify-center m-auto">
<div>
<Spinner className="size-32" />
</div>
</div>
</CardContent>
</Card>
}
>
{session && <NotificationsTable userId={`${session.user.id}`} />}
</Suspense>
</div>
</div>
);

View File

@@ -3,7 +3,7 @@ import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { Toaster } from "sonner";
import Header from "@/components/Header";
import { AppSidebar } from "@/components/Sidebar/sidebar";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { SidebarProvider } from "@/components/ui/sidebar";
import { ThemeProvider } from "@/lib/theme-provider";
const RootLayout = () => (
@@ -11,12 +11,15 @@ const RootLayout = () => (
<ThemeProvider>
<SidebarProvider className="flex flex-col" defaultOpen={false}>
<Header />
<div className="flex flex-1">
<div className="relative min-h-[calc(100svh-var(--header-height))]">
<AppSidebar />
<SidebarInset>
<Outlet />
</SidebarInset>
<main className="w-full p-4">
<div className="mx-auto w-full max-w-7xl">
<Outlet />
</div>
</main>
</div>
<Toaster expand richColors closeButton />

View File

@@ -0,0 +1,92 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import axios from "axios";
import { toast } from "sonner";
import { Card, CardDescription, CardHeader } from "../../../components/ui/card";
import { useAppForm } from "../../../lib/formSutff";
import { getSettings } from "../../../lib/queries/getSettings";
type Setting = {
id: string;
name: string;
description?: string;
value: string;
active: boolean;
inputType: "text" | "boolean" | "number" | "select";
options?: string[];
};
export default function FeatureCard({ item }: { item: Setting }) {
const { refetch } = useSuspenseQuery(getSettings());
const form = useAppForm({
defaultValues: {
value: item.value ?? "",
active: item.active,
},
onSubmit: async ({ value }) => {
try {
// adding this in as my base as i need to see timers working
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1000));
}
const { data } = await axios.patch(`/lst/api/settings/${item.name}`, {
value: value.value,
active: value.active ? "true" : "false",
}, {
withCredentials: true,
});
refetch();
toast.success(
<div>
<p>{data.message}</p>
<p>
This was a feature setting so{" "}
{value.active
? "processes related to this will start working on there next interval"
: "processes related to this will stop working on there next interval"}
</p>
</div>,
);
} catch (error) {
console.error(error);
}
},
});
return (
<Card className="p-2 w-96">
<CardHeader>
<p>{item.name}</p>
<CardDescription>
<p>{item.description}</p>
</CardDescription>
</CardHeader>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
void form.handleSubmit();
}}
>
<div className="flex justify-end mt-2 flex-col gap-4">
<form.AppField name="value">
{(field) => (
<field.InputField label="Setting Value" inputType="string" />
)}
</form.AppField>
<div className="flex flex-row justify-between">
<form.AppField name="active">
{(field) => (
<field.SwitchField trueLabel="Active" falseLabel="Deactivate" />
)}
</form.AppField>
<form.AppForm>
<form.SubmitButton>Update</form.SubmitButton>
</form.AppForm>
</div>
</div>
</form>
</Card>
);
}

View File

@@ -0,0 +1,11 @@
import FeatureCard from "./FeatureCard";
export default function FeatureSettings({ data }: any) {
return (
<div className=" flex flex-wrap gap-2">
{data.map((i: any) => (
<FeatureCard key={i.name} item={i} />
))}
</div>
);
}

View File

@@ -5,6 +5,7 @@ import { authClient } from "@/lib/auth-client";
export const Route = createFileRoute("/admin/logs")({
beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession();
const allowedRole = ["admin", "systemAdmin"];
if (!session?.user) {
throw redirect({
@@ -15,7 +16,7 @@ export const Route = createFileRoute("/admin/logs")({
});
}
if (session.user.role !== "admin") {
if (!allowedRole.includes(session.user.role as string)) {
throw redirect({
to: "/",
});

View File

@@ -0,0 +1,316 @@
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import axios from "axios";
import { Trash } from "lucide-react";
import { Suspense, useState } from "react";
import { toast } from "sonner";
import type { Notifications } from "../../../types/notifications";
import { Button } from "../../components/ui/button";
import { Card, CardContent } from "../../components/ui/card";
import { Label } from "../../components/ui/label";
import { Switch } from "../../components/ui/switch";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "../../components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "../../components/ui/tooltip";
import { authClient } from "../../lib/auth-client";
import { notificationSubs } from "../../lib/queries/notificationSubs";
import { notifications } from "../../lib/queries/notifications";
import EditableCellInput from "../../lib/tableStuff/EditableCellInput";
import LstTable from "../../lib/tableStuff/LstTable";
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
import SkellyTable from "../../lib/tableStuff/SkellyTable";
const updateNotifications = async (
id: string,
data: Record<string, string | number | boolean | null>,
) => {
//console.log(id, data);
try {
const res = await axios.patch(
`/lst/api/notification/${id}`,
{ interval: data.interval },
{
withCredentials: true,
},
);
toast.success(`Notification was just updated`);
return res;
} catch (err) {
toast.error("Error in updating the settings");
return err;
}
};
export const Route = createFileRoute("/admin/notifications")({
beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession();
const allowedRole = ["systemAdmin", "admin"];
if (!session?.user) {
throw redirect({
to: "/",
search: {
redirect: location.href,
},
});
}
if (!allowedRole.includes(session.user.role as string)) {
throw redirect({
to: "/",
});
}
return { user: session.user };
},
component: RouteComponent,
});
const NotificationTable = () => {
const { data, refetch } = useSuspenseQuery(notifications());
const { data: subs, refetch: subRefetch } = useSuspenseQuery(
notificationSubs(),
);
const columnHelper = createColumnHelper<Notifications>();
const notificationMap = Object.fromEntries(data.map((n: any) => [n.id, n]));
const subData = subs.map((sub: any) => ({
...sub,
name: notificationMap[sub.notificationId].name || null,
description: notificationMap[sub.notificationId].description || null,
emails: sub.emails ? sub.emails.join(",") : null,
}));
const updateNotification = useMutation({
mutationFn: ({
id,
field,
value,
}: {
id: string;
field: string;
value: string | number | boolean | null;
}) => updateNotifications(id, { [field]: value }),
onSuccess: () => {
// refetch or update cache
refetch();
},
});
const removeNotification = async (ns: any) => {
try {
const res = await axios.delete(`/lst/api/notification/sub`, {
withCredentials: true,
data: {
userId: ns.userId,
notificationId: ns.notificationId,
},
});
if (res.data.success) {
toast.success(`Subscription removed`);
subRefetch();
} else {
console.info(res);
toast.error(res.data.message);
}
} catch {
toast.error(`There was an error removing subscription.`);
}
};
const column = [
columnHelper.accessor("name", {
header: ({ column }) => (
<SearchableHeader column={column} title="Name" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("description", {
header: ({ column }) => (
<SearchableHeader column={column} title="Description" />
),
cell: (i) => (
<Tooltip>
<TooltipTrigger>
{i.getValue()?.length > 25 ? (
<span>{i.getValue().slice(0, 25)}...</span>
) : (
<span>{i.getValue()}</span>
)}
</TooltipTrigger>
<TooltipContent>{i.getValue()}</TooltipContent>
</Tooltip>
),
}),
columnHelper.accessor("active", {
header: ({ column }) => (
<SearchableHeader column={column} title="Active" searchable={false} />
),
filterFn: "includesString",
cell: (i) => {
// biome-ignore lint: just removing the lint for now to get this going will maybe fix later
const [activeToggle, setActiveToggle] = useState(i.getValue());
const onToggle = async (e: boolean) => {
setActiveToggle(e);
try {
const res = await axios.patch(
`/lst/api/notification/${i.row.original.id}`,
{
active: !activeToggle,
},
{ withCredentials: true },
);
if (res.data.success) {
toast.success(
`${i.row.original.name} was set to ${activeToggle ? "Inactive" : "Active"}`,
);
refetch();
}
} catch (error) {
console.error(error);
}
};
return (
<div className="w-48">
<div className="flex items-center space-x-2">
<Switch
id={i.row.original.id}
checked={activeToggle}
onCheckedChange={(e) => onToggle(e)}
//onBlur={field.handleBlur}
/>
<Label htmlFor={i.row.original.id}>
{activeToggle ? "Active" : "Deactivated"}
</Label>
</div>
</div>
);
},
}),
columnHelper.accessor("interval", {
header: ({ column }) => (
<SearchableHeader column={column} title="Interval" />
),
filterFn: "includesString",
cell: ({ row, getValue }) => {
return (
<EditableCellInput
value={getValue()}
id={row.original.id}
field="interval"
onSubmit={({ id, field, value }) => {
updateNotification.mutate({ id, field, value });
}}
/>
);
},
}),
];
const subsColumn = [
columnHelper.accessor("name", {
header: ({ column }) => (
<SearchableHeader column={column} title="Name" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("description", {
header: ({ column }) => (
<SearchableHeader column={column} title="Description" />
),
cell: (i) => (
<Tooltip>
<TooltipTrigger>
{i.getValue()?.length > 25 ? (
<span>{i.getValue().slice(0, 25)}...</span>
) : (
<span>{i.getValue()}</span>
)}
</TooltipTrigger>
<TooltipContent>{i.getValue()}</TooltipContent>
</Tooltip>
),
}),
columnHelper.accessor("emails", {
header: ({ column }) => (
<SearchableHeader column={column} title="Emails" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("remove", {
header: ({ column }) => (
<SearchableHeader column={column} title="Remove" searchable={false} />
),
filterFn: "includesString",
cell: (i) => {
return (
<Button
size="icon"
variant={"destructive"}
onClick={() => removeNotification(i.row.original)}
>
<Trash />
</Button>
);
},
}),
];
return (
<>
<TabsContent value="notifications">
<LstTable data={data} columns={column} />
</TabsContent>
<TabsContent value="subscriptions">
<LstTable data={subData} columns={subsColumn} />
</TabsContent>
</>
);
};
function RouteComponent() {
return (
<div className="space-y-6">
<div className="space-y-2">
<h1 className="text-2xl font-semibold">Notifications</h1>
<p className="text-sm text-muted-foreground">
Manage all notification settings and user subs.
</p>
</div>
<Card>
<CardContent>
<Tabs defaultValue="notifications" className="w-full">
<TabsList>
<TabsTrigger value="notifications">Notifications</TabsTrigger>
<TabsTrigger value="subscriptions">Subscriptions</TabsTrigger>
</TabsList>
<Suspense fallback={<SkellyTable />}>
<NotificationTable />
</Suspense>
</Tabs>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,209 @@
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import axios from "axios";
import { Suspense, useMemo } from "react";
import { toast } from "sonner";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "../../components/ui/card";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "../../components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "../../components/ui/tooltip";
import { authClient } from "../../lib/auth-client";
import { getSettings } from "../../lib/queries/getSettings";
import EditableCellInput from "../../lib/tableStuff/EditableCellInput";
import LstTable from "../../lib/tableStuff/LstTable";
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
import SkellyTable from "../../lib/tableStuff/SkellyTable";
import FeatureSettings from "./-components/FeatureSettings";
type Settings = {
settings_id: string;
name: string;
active: boolean;
value: string;
description: string;
moduleName: string;
roles: string[];
};
const updateSettings = async (
id: string,
data: Record<string, string | number | boolean | null>,
) => {
//console.log(id, data);
try {
const res = await axios.patch(`/lst/api/settings/${id}`, data, {
withCredentials: true,
});
toast.success(`Setting just updated`);
return res;
} catch (err) {
toast.error("Error in updating the settings");
return err;
}
};
export const Route = createFileRoute("/admin/settings")({
beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession();
const allowedRole = ["systemAdmin"];
if (!session?.user) {
throw redirect({
to: "/",
search: {
redirect: location.href,
},
});
}
if (!allowedRole.includes(session.user.role as string)) {
throw redirect({
to: "/",
});
}
return { user: session.user };
},
component: RouteComponent,
});
function SettingsTableCard() {
const { data, refetch } = useSuspenseQuery(getSettings());
const columnHelper = createColumnHelper<Settings>();
const updateSetting = useMutation({
mutationFn: ({
id,
field,
value,
}: {
id: string;
field: string;
value: string | number | boolean | null;
}) => updateSettings(id, { [field]: value }),
onSuccess: () => {
// refetch or update cache
refetch();
},
});
const column = [
columnHelper.accessor("name", {
header: ({ column }) => (
<SearchableHeader column={column} title="Name" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("description", {
header: ({ column }) => (
<SearchableHeader column={column} title="Description" />
),
cell: (i) => (
<Tooltip>
<TooltipTrigger>
{i.getValue().length > 25 ? (
<span>{i.getValue().slice(0, 25)}...</span>
) : (
<span>{i.getValue()}</span>
)}
</TooltipTrigger>
<TooltipContent>{i.getValue()}</TooltipContent>
</Tooltip>
),
}),
columnHelper.accessor("value", {
header: ({ column }) => (
<SearchableHeader column={column} title="Value" />
),
filterFn: "includesString",
cell: ({ row, getValue }) => (
<EditableCellInput
value={getValue()}
id={row.original.name}
field="value"
onSubmit={({ id, field, value }) => {
updateSetting.mutate({ id, field, value });
}}
/>
),
}),
];
const { standardSettings, featureSettings, systemSetting } = useMemo(() => {
return {
standardSettings: data.filter(
(setting: any) => setting.settingType === "standard",
),
featureSettings: data.filter(
(setting: any) => setting.settingType === "feature",
),
systemSetting: data.filter(
(setting: any) => setting.settingType === "system",
),
};
}, [data]);
return (
<>
<TabsContent value="feature">
<FeatureSettings data={featureSettings} />
</TabsContent>
<TabsContent value="system">
<LstTable data={systemSetting} columns={column} />
</TabsContent>
<TabsContent value="standard">
<LstTable data={standardSettings} columns={column} />
</TabsContent>
</>
);
}
function RouteComponent() {
return (
<div className="space-y-6">
<div className="space-y-2">
<h1 className="text-2xl font-semibold">Settings</h1>
<p className="text-sm text-muted-foreground">
Manage your settings and related data.
</p>
</div>
<Card>
<CardHeader>
<CardTitle>System Settings</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="standard" className="w-full">
<TabsList>
<TabsTrigger value="feature">Features</TabsTrigger>
<TabsTrigger value="system">System</TabsTrigger>
<TabsTrigger value="standard">Standard</TabsTrigger>
</TabsList>
<Suspense fallback={<SkellyTable />}>
<SettingsTableCard />
</Suspense>
</Tabs>
</CardContent>
</Card>
</div>
);
}

View File

@@ -18,10 +18,42 @@ function Index() {
if (isPending)
return <div className="flex justify-center mt-10">Loading...</div>;
// if (!session) return <button>Sign In</button>
let url: string;
if (window.location.origin.includes("localhost")) {
url = `https://www.youtube.com/watch?v=dQw4w9WgXcQ`;
} else if (window.location.origin.includes("vms006")) {
url = `https://${window.location.hostname.replace("vms006", "prod.alpla.net/")}lst/app/old/ocp`;
} else {
url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
}
return (
<div className="flex justify-center mt-10">
<h3 className="w-2xl text-3xl">Welcome Home!</h3>
<div className="flex justify-center m-10 flex-col">
<h3 className="w-2xl text-3xl">Welcome Lst - V3</h3>
<br></br>
<p>
This is active in your plant today due to having warehousing activated
and new functions needed to be introduced, you should be still using LST
as you were before.
</p>
<br></br>
<p>
If you dont know why you are here and looking for One Click Print{" "}
<a href={`${url}`} target="_blank" rel="noopener">
<b>
<strong>Click</strong>
</b>
</a>
<a
href={`https://www.youtube.com/watch?v=dQw4w9WgXcQ`}
target="_blank"
rel="noopener"
>
<b>
<strong> Here</strong>
</b>
</a>
</p>
</div>
);
}

View File

@@ -0,0 +1,10 @@
export type Notifications = {
id: string;
name: string;
emails: string;
description: string;
remove?: unknown;
active?: boolean;
interval: number;
options: unknown[];
};

View File

@@ -0,0 +1,4 @@
CREATE TABLE "printer_log" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "printer_log_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"name" text NOT NULL
);

View File

@@ -0,0 +1,15 @@
CREATE TABLE "alpla_purchase_history" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"apo" integer,
"revision" integer,
"confirmed" integer,
"status" integer,
"status_text" integer,
"add_date" timestamp DEFAULT now(),
"upd_date" timestamp DEFAULT now(),
"add_user" text,
"upd_user" text,
"remark" text,
"position" jsonb DEFAULT '[]'::jsonb,
"created_at" timestamp DEFAULT now()
);

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