Compare commits
52 Commits
ddcb7e76a3
...
v0.0.1-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| 8dfcbc5720 | |||
| 103ae77e9f | |||
| beeccc6e8d | |||
| 0880298cf5 | |||
| 34b0abac36 | |||
| 28c226ddbc | |||
| 42861cc69e | |||
| 5f3d683a13 | |||
| a17787e852 | |||
| 5865ac3b99 | |||
| 637de857f9 | |||
| 3ecf5fb916 | |||
| 92ba3ef512 | |||
| 7d6c2db89c | |||
| 74262beb65 | |||
| f3b8dd94e5 | |||
| 0059b9b850 | |||
| 1ad789b2b9 | |||
| 079478f932 | |||
| d6d5b451cd | |||
| 76747cf917 | |||
| 6e85991062 | |||
| 98e408cb85 | |||
| ed052dff3c | |||
| 8f59bba614 | |||
| fb2c5609aa | |||
| 17aed6cb89 | |||
| b02b93b83f | |||
| 9ceba8b5bb | |||
| 2c0dbf95c7 | |||
| 860207a60b | |||
| 5c6460012a | |||
| be1d4081e0 | |||
| 83a94cacf3 | |||
| 0ce3790675 | |||
| 5854889eb5 | |||
| 4caaf74569 | |||
| fe889ca757 | |||
| 699c124b0e | |||
| 7d55c5f431 | |||
| c4fd74fc93 | |||
| 3775760734 | |||
| 643d12ff18 | |||
| 82eaa23da7 | |||
| b18d1ced6d | |||
| 69c5cf87fd | |||
| 1fadf0ad25 | |||
| beae6eb648 | |||
| 82ab735982 | |||
| dbd56c1b50 | |||
| 037a473ab7 | |||
| 32998d417f |
@@ -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)
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
31
.gitea/workflows/docker-build.yml
Normal file
31
.gitea/workflows/docker-build.yml
Normal 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 }}
|
||||
229
.gitea/workflows/release.yml
Normal file
229
.gitea/workflows/release.yml
Normal 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
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,10 +4,12 @@ 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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -54,8 +54,10 @@
|
||||
"alpla",
|
||||
"alplamart",
|
||||
"alplaprod",
|
||||
"alplapurchase",
|
||||
"bookin",
|
||||
"Datamart",
|
||||
"dotenvx",
|
||||
"dyco",
|
||||
"intiallally",
|
||||
"manadatory",
|
||||
@@ -67,6 +69,7 @@
|
||||
"preseed",
|
||||
"prodlabels",
|
||||
"prolink",
|
||||
"Skelly",
|
||||
"trycatch"
|
||||
],
|
||||
"gitea.token": "8456def90e1c651a761a8711763d6ef225d6b2db",
|
||||
|
||||
77
CHANGELOG.md
77
CHANGELOG.md
@@ -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))
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -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
|
||||
|
||||
10
README.md
10
README.md
@@ -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
|
||||
@@ -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"));
|
||||
});
|
||||
|
||||
|
||||
38
backend/db/schema/alplapurchase.schema.ts
Normal file
38
backend/db/schema/alplapurchase.schema.ts
Normal 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
|
||||
>;
|
||||
@@ -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);
|
||||
|
||||
6
backend/db/schema/printerLogs.schema.ts
Normal file
6
backend/db/schema/printerLogs.schema.ts
Normal 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(),
|
||||
});
|
||||
113
backend/notification/notification.alplapurchase.ts
Normal file
113
backend/notification/notification.alplapurchase.ts
Normal 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;
|
||||
@@ -1,10 +1,106 @@
|
||||
const reprint = (data: any, emails: string) => {
|
||||
// TODO: do the actual logic for the notification.
|
||||
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";
|
||||
|
||||
// TODO send the error to systemAdmin users so they do not always need to be on the notifications.
|
||||
// these errors are defined per notification.
|
||||
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;
|
||||
|
||||
@@ -3,12 +3,12 @@ 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"),
|
||||
});
|
||||
@@ -16,14 +16,29 @@ const newSubscribe = z.object({
|
||||
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),
|
||||
),
|
||||
)
|
||||
@@ -44,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",
|
||||
|
||||
@@ -9,6 +9,8 @@ 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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }] },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
36
backend/ocp/ocp.printer.listener.ts
Normal file
36
backend/ocp/ocp.printer.listener.ts
Normal 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;
|
||||
19
backend/ocp/ocp.printer.manage.ts
Normal file
19
backend/ocp/ocp.printer.manage.ts
Normal 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
22
backend/ocp/ocp.routes.ts
Normal 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);
|
||||
};
|
||||
@@ -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);
|
||||
// };
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
63
backend/prodSql/queries/alplapurchase.sql
Normal file
63
backend/prodSql/queries/alplapurchase.sql
Normal 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())
|
||||
28
backend/prodSql/queries/reprintLabels.sql
Normal file
28
backend/prodSql/queries/reprintLabels.sql
Normal 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
|
||||
97
backend/purchase/purchase.controller.ts
Normal file
97
backend/purchase/purchase.controller.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
188
backend/rfid/daytonConfig copy.json
Normal file
188
backend/rfid/daytonConfig copy.json
Normal 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"
|
||||
}
|
||||
206
backend/rfid/daytonConfig.json
Normal file
206
backend/rfid/daytonConfig.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { setupAuthRoutes } from "./auth/auth.routes.js";
|
||||
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";
|
||||
@@ -20,4 +21,5 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
|
||||
setupUtilsRoutes(baseUrl, app);
|
||||
setupOpendockRoutes(baseUrl, app);
|
||||
setupNotificationRoutes(baseUrl, app);
|
||||
setupOCPRoutes(baseUrl, app);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ 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),
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
47
backend/utils/mailViews/reprintLabels.hbs
Normal file
47
backend/utils/mailViews/reprintLabels.hbs
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
vars {
|
||||
url: http://localhost:3000/lst
|
||||
url: http://localhost:3600/lst
|
||||
readerIp: 10.44.14.215
|
||||
}
|
||||
vars:secret [
|
||||
token
|
||||
]
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
22
brunoApi/ocp/Printer Listenter.bru
Normal file
22
brunoApi/ocp/Printer Listenter.bru
Normal 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
8
brunoApi/ocp/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: ocp
|
||||
seq: 9
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
8
brunoApi/rfidReaders/folder.bru
Normal file
8
brunoApi/rfidReaders/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: rfidReaders
|
||||
seq: 8
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
20
brunoApi/rfidReaders/reader.bru
Normal file
20
brunoApi/rfidReaders/reader.bru
Normal 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
|
||||
}
|
||||
20
brunoApi/rfidReaders/readerSpecific/Config.bru
Normal file
20
brunoApi/rfidReaders/readerSpecific/Config.bru
Normal 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
|
||||
}
|
||||
32
brunoApi/rfidReaders/readerSpecific/Login.bru
Normal file
32
brunoApi/rfidReaders/readerSpecific/Login.bru
Normal 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
|
||||
}
|
||||
237
brunoApi/rfidReaders/readerSpecific/Update Config.bru
Normal file
237
brunoApi/rfidReaders/readerSpecific/Update Config.bru
Normal 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
|
||||
}
|
||||
12
brunoApi/rfidReaders/readerSpecific/folder.bru
Normal file
12
brunoApi/rfidReaders/readerSpecific/folder.bru
Normal file
@@ -0,0 +1,12 @@
|
||||
meta {
|
||||
name: readerSpecific
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: admin
|
||||
password: Zebra123!
|
||||
}
|
||||
@@ -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}"
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
53
frontend/src/components/ui/scroll-area.tsx
Normal file
53
frontend/src/components/ui/scroll-area.tsx
Normal 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 }
|
||||
190
frontend/src/components/ui/select.tsx
Normal file
190
frontend/src/components/ui/select.tsx
Normal 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,
|
||||
}
|
||||
31
frontend/src/components/ui/switch.tsx
Normal file
31
frontend/src/components/ui/switch.tsx
Normal 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 }
|
||||
114
frontend/src/components/ui/table.tsx
Normal file
114
frontend/src/components/ui/table.tsx
Normal 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,
|
||||
}
|
||||
88
frontend/src/components/ui/tabs.tsx
Normal file
88
frontend/src/components/ui/tabs.tsx
Normal 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 }
|
||||
44
frontend/src/components/ui/toggle.tsx
Normal file
44
frontend/src/components/ui/toggle.tsx
Normal 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 }
|
||||
87
frontend/src/lib/formSutff/DynamicInput.Field.tsx
Normal file
87
frontend/src/lib/formSutff/DynamicInput.Field.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
|
||||
57
frontend/src/lib/formSutff/Select.Field.tsx
Normal file
57
frontend/src/lib/formSutff/Select.Field.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
29
frontend/src/lib/formSutff/Switch.Field.tsx
Normal file
29
frontend/src/lib/formSutff/Switch.Field.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
22
frontend/src/lib/queries/getSettings.ts
Normal file
22
frontend/src/lib/queries/getSettings.ts
Normal 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;
|
||||
};
|
||||
24
frontend/src/lib/queries/notificationSubs.ts
Normal file
24
frontend/src/lib/queries/notificationSubs.ts
Normal 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;
|
||||
};
|
||||
22
frontend/src/lib/queries/notifications.ts
Normal file
22
frontend/src/lib/queries/notifications.ts
Normal 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;
|
||||
};
|
||||
70
frontend/src/lib/tableStuff/EditableCellInput.tsx
Normal file
70
frontend/src/lib/tableStuff/EditableCellInput.tsx
Normal 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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
129
frontend/src/lib/tableStuff/LstTable.tsx
Normal file
129
frontend/src/lib/tableStuff/LstTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
67
frontend/src/lib/tableStuff/SearchableHeader.tsx
Normal file
67
frontend/src/lib/tableStuff/SearchableHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
40
frontend/src/lib/tableStuff/SkellyTable.tsx
Normal file
40
frontend/src/lib/tableStuff/SkellyTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
104
frontend/src/routes/(auth)/-components/NotificationsSubCard.tsx
Normal file
104
frontend/src/routes/(auth)/-components/NotificationsSubCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
114
frontend/src/routes/(auth)/-components/NotificationsTable.tsx
Normal file
114
frontend/src/routes/(auth)/-components/NotificationsTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 />
|
||||
|
||||
92
frontend/src/routes/admin/-components/FeatureCard.tsx
Normal file
92
frontend/src/routes/admin/-components/FeatureCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
frontend/src/routes/admin/-components/FeatureSettings.tsx
Normal file
11
frontend/src/routes/admin/-components/FeatureSettings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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: "/",
|
||||
});
|
||||
|
||||
316
frontend/src/routes/admin/notifications.tsx
Normal file
316
frontend/src/routes/admin/notifications.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
209
frontend/src/routes/admin/settings.tsx
Normal file
209
frontend/src/routes/admin/settings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
10
frontend/types/notifications.ts
Normal file
10
frontend/types/notifications.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type Notifications = {
|
||||
id: string;
|
||||
name: string;
|
||||
emails: string;
|
||||
description: string;
|
||||
remove?: unknown;
|
||||
active?: boolean;
|
||||
interval: number;
|
||||
options: unknown[];
|
||||
};
|
||||
4
migrations/0019_large_thunderbird.sql
Normal file
4
migrations/0019_large_thunderbird.sql
Normal 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
|
||||
);
|
||||
15
migrations/0020_stale_ma_gnuci.sql
Normal file
15
migrations/0020_stale_ma_gnuci.sql
Normal 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()
|
||||
);
|
||||
2
migrations/0021_slimy_master_mold.sql
Normal file
2
migrations/0021_slimy_master_mold.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "alpla_purchase_history" ADD COLUMN "journal_num" integer;--> statement-breakpoint
|
||||
ALTER TABLE "alpla_purchase_history" ADD COLUMN "approved_status" text;
|
||||
1
migrations/0022_large_sumo.sql
Normal file
1
migrations/0022_large_sumo.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "alpla_purchase_history" ALTER COLUMN "approved_status" SET DEFAULT 'pending';
|
||||
1
migrations/0023_normal_hellion.sql
Normal file
1
migrations/0023_normal_hellion.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "alpla_purchase_history" ALTER COLUMN "status_text" SET DATA TYPE text;
|
||||
6
migrations/0024_absent_barracuda.sql
Normal file
6
migrations/0024_absent_barracuda.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "opendock_apt" ALTER COLUMN "release" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "opendock_apt" ALTER COLUMN "appointment" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "opendock_apt" ALTER COLUMN "upd_date" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "opendock_apt" ALTER COLUMN "created_at" SET NOT NULL;--> statement-breakpoint
|
||||
CREATE INDEX "opendock_apt_release_idx" ON "opendock_apt" USING btree ("release");--> statement-breakpoint
|
||||
CREATE INDEX "opendock_apt_opendock_id_idx" ON "opendock_apt" USING btree ("open_dock_apt_id");
|
||||
1327
migrations/meta/0019_snapshot.json
Normal file
1327
migrations/meta/0019_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1423
migrations/meta/0020_snapshot.json
Normal file
1423
migrations/meta/0020_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user