Compare commits
303 Commits
Syncfusion
...
Warehouses
| Author | SHA1 | Date | |
|---|---|---|---|
| 89d74428b2 | |||
| 0a7399f015 | |||
| 775c7a2d5c | |||
| 7c61f6c80f | |||
| b17411de19 | |||
| 6fac50225a | |||
| f5ba09f58d | |||
| 4f48158bc7 | |||
| 448e9c554d | |||
| c5b6b10928 | |||
| 9a6f2040cf | |||
| f7926c78ad | |||
| c15b325950 | |||
| 1051e22ab3 | |||
| 6b985d2cf7 | |||
| 8f859e0cc7 | |||
| d007e36081 | |||
| d5c8e3a683 | |||
| 9efa5a132f | |||
| 0200cd4b61 | |||
| ed508765eb | |||
| b0f78e395f | |||
| 260897dd87 | |||
| 065744d15a | |||
| 78806115ba | |||
| cfe7924d25 | |||
| f3060013d3 | |||
| 3d59cb1b68 | |||
| 8b3d04c6aa | |||
| ea6c490edc | |||
| 353737bae0 | |||
| 89de720f93 | |||
| 492c70f384 | |||
| 7d8de5c78f | |||
| e9d6dbaa5c | |||
| c501a84d53 | |||
| 08800a4c10 | |||
| dfc405f4b0 | |||
| 29ef6383b1 | |||
| 86ff97b311 | |||
| cf878470a5 | |||
| a0451c42de | |||
| 76b069a51e | |||
| 606bd31e21 | |||
| e391d06085 | |||
| 216a966f39 | |||
| fa8c0f3eb2 | |||
| c064479a2f | |||
| 531260eb9c | |||
| 81af6f02d9 | |||
| 7c2001ad34 | |||
| 9aaeaa0548 | |||
| 7a8d3cf10c | |||
| 27dbb780ec | |||
| 4863d9120b | |||
| b04546260a | |||
| 6d1f87f9d9 | |||
| b309669102 | |||
| 69aaa4b143 | |||
| 3a8a566350 | |||
| 68e41a6620 | |||
| 18c8916d00 | |||
| df9c187f3d | |||
| d4c5a978e4 | |||
| dc88f4ef94 | |||
| 9a7d2e0ec5 | |||
| 6014547ce7 | |||
| 23167283b0 | |||
| 44efa3fa1b | |||
| b8199a3428 | |||
| 232cb0e00d | |||
| dfb6dbef93 | |||
| f1ce3c57e9 | |||
| eb95d41975 | |||
| 8d626356c7 | |||
| 9fd026ce71 | |||
| 0a81374164 | |||
| c586e2875a | |||
| 638d7faf56 | |||
| 792283f8e2 | |||
| 8eda96003f | |||
| 639a3547b2 | |||
| 7a55775562 | |||
| a4b5bced79 | |||
| c6dadb2d61 | |||
| 9ef085b582 | |||
| 815165b496 | |||
| 9b4b075b0c | |||
| 41c9c69eb1 | |||
| 42f5ef38c8 | |||
| ed93202fd4 | |||
| 7842696a34 | |||
| fe49432d0c | |||
| 865211131e | |||
| ad2268d642 | |||
| f2686188e0 | |||
| 048a2097c3 | |||
| 2fd327e3b8 | |||
| cb71d6abed | |||
| 9cedf4402e | |||
| 94c5c8c436 | |||
| 8cc1981305 | |||
| 8e9a2a0975 | |||
| 51a96f6c6a | |||
| 7d37b7744c | |||
| a26a52b078 | |||
| 2f80dbc13c | |||
| 929e41e312 | |||
| 1e7c6a6694 | |||
| 6297ae92eb | |||
| 14b83bf9d2 | |||
| 8ea4a4139e | |||
| d04e2ec56d | |||
| f9ffdb7126 | |||
| c50560566c | |||
| cf0428dd53 | |||
| 2da07b6da9 | |||
| 55f2c1b862 | |||
| fffbd811ce | |||
| 30b3eb6450 | |||
| 2c97c5054e | |||
| dfc5c52003 | |||
| 6fed42ee64 | |||
| 64390c5430 | |||
| 794e30154b | |||
| 1321c88a31 | |||
| 2b3e36f9b9 | |||
| 38ff2c1673 | |||
| c6468122fd | |||
| b29ff1f1a1 | |||
| 070b538cb2 | |||
| 00ba9700e9 | |||
| b4a9d52fcd | |||
| d832f1b960 | |||
| d33f68f0cd | |||
| db03b35292 | |||
| a674a7300e | |||
| 6f9b94a97c | |||
| c4a99e3c35 | |||
| 1e796e2c13 | |||
| 26d00213d2 | |||
| 03b28eed6e | |||
| 0baee2be8b | |||
| a49efec313 | |||
| 96bfccdd55 | |||
| 1f3a29b293 | |||
| 49f685f109 | |||
| 2c196c68d7 | |||
| f0c4fef3cb | |||
| 2b3bc03049 | |||
| 913a1ce49d | |||
| eb8dc3cd4f | |||
| 4368129802 | |||
| 5d64abace3 | |||
| 1f9ae3998c | |||
| 3c62579a49 | |||
| e4f62fff86 | |||
| 3a26eaa335 | |||
| c0fdf241eb | |||
| 9f0f0d703e | |||
| dae2a08390 | |||
| 7110678c7d | |||
| 0649c85e76 | |||
| 4f40f33666 | |||
| 724fba4177 | |||
| bb44162bd6 | |||
| f1d2ec2083 | |||
| 6fe6625892 | |||
| 61ba728f35 | |||
| df6b0d56b7 | |||
| 22779715b9 | |||
| e96d6fc2f9 | |||
| 55edcd2a2d | |||
| 606de33d41 | |||
| 41bd905bd2 | |||
| bb4be4885d | |||
| e8f0871230 | |||
| 09d131b89b | |||
| 9aa427aef8 | |||
| 397e13d865 | |||
| ac565e7703 | |||
| 2167003ffd | |||
| 7d6c3d9fd6 | |||
| 0407205b5d | |||
| 212a1feafc | |||
| c8b300336e | |||
| 653ab748e7 | |||
| e354881309 | |||
| 107f5b173c | |||
| 4788830d72 | |||
| aa1bd5167e | |||
| 398e0d5796 | |||
| 21d6c76f37 | |||
| 21336e8dc6 | |||
| b87b574b7d | |||
| cfbaaf85ac | |||
| ac8213ff04 | |||
| b290ff47f2 | |||
| bc968a1a0f | |||
| fc72354331 | |||
| 8defd33651 | |||
| dfeecd59d8 | |||
| ae982f59e3 | |||
| be8102c777 | |||
| c9217867df | |||
| e7e5e7fbb0 | |||
| 568d2d3918 | |||
| a301ca1ecc | |||
| 4c3397f5c5 | |||
| f3977428ef | |||
| e04f133b49 | |||
| 6770e41a6e | |||
| 04a43d6f04 | |||
| b336dfe483 | |||
| 4e94cccc88 | |||
| 06491eea1f | |||
| ea018f2da3 | |||
| c17387acde | |||
| 9a4fb30d07 | |||
| 9294b538da | |||
| e844cbfb02 | |||
| d40caafce2 | |||
| 890ab7a67f | |||
| a50e202786 | |||
| 9cc874322a | |||
| 8d8099f9e2 | |||
| 1d8c709910 | |||
| d62212d3ed | |||
| df9ec74142 | |||
| 96a61f306f | |||
| c35060f4d3 | |||
| 1e889f6e9a | |||
| e9a3e91082 | |||
| c4b1561cbc | |||
| 6f477ee174 | |||
| be26bf9b6e | |||
| e57713fabc | |||
| 166eec9181 | |||
| da71596fad | |||
| 309044cb0c | |||
| 9163f330aa | |||
| 8cfded48ae | |||
| f6cc62d6c8 | |||
| 08171522b9 | |||
| e2c5de40b5 | |||
| 01350f0146 | |||
| 50a25ff996 | |||
| 89792d3d28 | |||
| f45baa31a5 | |||
| 85d571ed7f | |||
| bfe325df82 | |||
| d3694ad0c4 | |||
| 0199467f72 | |||
| 47f37d016d | |||
| 23c26b4c65 | |||
| f6deaa3da0 | |||
| 8b67aa3851 | |||
| cd3d939efe | |||
| f0b94bea74 | |||
| de9f6e53aa | |||
| eb756eedec | |||
| 81aae81016 | |||
| 3fd97b10eb | |||
| 9b13c8e099 | |||
| 764ba68f56 | |||
| 8c646d4bc7 | |||
| eb40cc35fe | |||
| a0c7c2f6aa | |||
| f400c35c60 | |||
| d7210417f5 | |||
| 88daf6d392 | |||
| 6139ce97d7 | |||
| 1842fd6146 | |||
| e69b10398f | |||
| 96a6d7772e | |||
| 4392cce6c9 | |||
| 4cfb422ad4 | |||
| 82cb7a3dbe | |||
| d3e6c815ac | |||
| 2bf87c0091 | |||
| 52439e73cb | |||
| 30d2984add | |||
| a8e3a8be66 | |||
| e96bcc95ba | |||
| e55a3926ff | |||
| 9cb7210aa5 | |||
| 24e2836f77 | |||
| 269d9c87a4 | |||
| 53d99bf65f | |||
| ad641560ea | |||
| 1a4ff2ef6d | |||
| 393db2cc7c | |||
| f58d2ab04c | |||
| 71dd78cbd7 | |||
| f4195a540c | |||
| 6ab3960e50 | |||
| bc28c5d63d | |||
| 55f7ed76fd | |||
| da1eae8ca9 | |||
| 1f08ae05f0 | |||
| 9f38998135 | |||
| 04e18374da | |||
| edc4aef0db |
192
.gitea/workflows/build-and-publish.yml
Normal file
192
.gitea/workflows/build-and-publish.yml
Normal file
@@ -0,0 +1,192 @@
|
||||
name: Build, Test & Publish DataModels + Apps
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
create: # nowe tagi
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
workflow_dispatch: # ręczne uruchomienie
|
||||
|
||||
jobs:
|
||||
restore-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # potrzebne do MinVer i git describe
|
||||
repository: FA/FA_WEB # ← nazwa repozytorium (organizacja/repo)
|
||||
ref: ${{ github.ref }} # lub ${{ gitea.ref }} – w Gitea obie działają
|
||||
# Najważniejsze – nadpisz domyślny URL
|
||||
url: http://srv51.mikr.us:20120/git/FA/FA_WEB.git
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x' # zmień na swoją wersję jeśli inna
|
||||
|
||||
- name: Restore all projects
|
||||
run: |
|
||||
echo "=== Restore wszystkich projektów ==="
|
||||
find . -name "*.csproj" -type f -exec dotnet restore "{}" \;
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
echo "=== Uruchamianie testów ==="
|
||||
dotnet test --no-restore --configuration Release --logger "trx"
|
||||
|
||||
pack-datamodels:
|
||||
runs-on: ubuntu-latest
|
||||
needs: restore-and-test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Install MinVer CLI
|
||||
run: dotnet tool install --global minver-cli
|
||||
|
||||
- name: Pack DataModel projects
|
||||
env:
|
||||
MINVER_AUTO_INCREMENT: patch
|
||||
MINVER_MINIMUM_MAJOR_MINOR: 2.0
|
||||
MINVER_DEFAULT_PRE_RELEASE_IDENTIFIERS: alpha.0
|
||||
run: |
|
||||
echo "=== Obliczona wersja przez MinVer ==="
|
||||
MINVER_VERSION=$(minver --verbosity detailed)
|
||||
echo "Wersja: $MINVER_VERSION"
|
||||
|
||||
mkdir -p nupkg
|
||||
|
||||
echo "=== Pełniejsze fetch git (tags) ==="
|
||||
git fetch --prune --tags --force
|
||||
|
||||
echo "=== Dostępne tagi ==="
|
||||
git tag -l
|
||||
|
||||
echo "=== Aktualny opis commitu ==="
|
||||
git describe --tags --always --dirty || echo "Brak tagów"
|
||||
|
||||
echo "=== Pakowanie projektów z PackageId ==="
|
||||
find . -name "*.csproj" -type f | while read -r csproj; do
|
||||
if grep -q '<PackageId>' "$csproj"; then
|
||||
PROJECT_NAME=$(basename "$csproj" .csproj)
|
||||
echo "→ Pakuję $PROJECT_NAME"
|
||||
dotnet pack "$csproj" \
|
||||
--no-restore \
|
||||
--configuration Release \
|
||||
-o "./nupkg" \
|
||||
/p:PackageVersion="$MINVER_VERSION"
|
||||
else
|
||||
echo "→ Pomijam $(basename "$csproj" .csproj) – brak PackageId"
|
||||
fi
|
||||
done
|
||||
|
||||
ls -la nupkg/ || echo "Brak wygenerowanych pakietów"
|
||||
|
||||
- name: Upload NuGet packages as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nuget-packages
|
||||
path: nupkg/*.nupkg
|
||||
retention-days: 3
|
||||
|
||||
publish-to-baget:
|
||||
runs-on: ubuntu-latest
|
||||
needs: pack-datamodels
|
||||
steps:
|
||||
- uses: actions/checkout@v4 # potrzebne tylko jeśli chcesz logować coś z repo
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Download built packages
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: nuget-packages
|
||||
path: nupkg/
|
||||
|
||||
- name: Create custom NuGet.config (allow HTTP)
|
||||
run: |
|
||||
cat > NuGet.Config <<EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="BaGet" value="http://baget:80/v3/index.json" protocolVersion="3" allowInsecureConnections="true" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
EOF
|
||||
|
||||
- name: Publish packages to BaGet
|
||||
env:
|
||||
BAGET_API_KEY: ${{ secrets.BAGET_API_KEY }}
|
||||
run: |
|
||||
echo "=== Publikacja do BaGet ==="
|
||||
find nupkg -name "*.nupkg" -type f | while read -r pkg; do
|
||||
echo "→ Push $(basename "$pkg")"
|
||||
dotnet nuget push "$pkg" \
|
||||
--source "BaGet" \
|
||||
--api-key "$BAGET_API_KEY" \
|
||||
--skip-duplicate
|
||||
done
|
||||
|
||||
build-and-publish-apps:
|
||||
runs-on: ubuntu-latest
|
||||
needs: publish-to-baget
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Restore (with latest DataModels)
|
||||
run: |
|
||||
echo "=== Restore z najnowszymi pakietami z BaGet ==="
|
||||
dotnet restore
|
||||
|
||||
- name: Publish selected applications
|
||||
run: |
|
||||
echo "=== Publish aplikacji z DeployToCentral ==="
|
||||
find . -name "*.csproj" -type f | while read -r csproj; do
|
||||
PROJECT_NAME=$(basename "$csproj" .csproj)
|
||||
PROJECT_DIR=$(dirname "$csproj")
|
||||
|
||||
if [[ "$PROJECT_NAME" == *"Api"* ]] || [[ "$PROJECT_NAME" == *"Blazor"* ]] || \
|
||||
[[ "$PROJECT_DIR" == *"/Api/"* ]] || [[ "$PROJECT_DIR" == *"/Blazor/"* ]]; then
|
||||
|
||||
DEPLOY_TO_CENTRAL=$(dotnet msbuild "$csproj" -getProperty:DeployToCentral -nologo 2>/dev/null || echo "false")
|
||||
|
||||
if [ "$DEPLOY_TO_CENTRAL" != "true" ]; then
|
||||
echo "Pomijam $PROJECT_NAME (DeployToCentral ≠ true)"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "→ Publishing $PROJECT_NAME"
|
||||
dotnet publish "$csproj" \
|
||||
--no-restore \
|
||||
--configuration Release \
|
||||
-o "./publish-$PROJECT_NAME"
|
||||
|
||||
# Tutaj możesz dodać deployment (np. rsync, scp, docker build+push itp.)
|
||||
# Przykład (zakomentowany):
|
||||
# rsync -avz --delete ./publish-$PROJECT_NAME/ user@server:/path/to/app/
|
||||
else
|
||||
echo "Pomijam $PROJECT_NAME (nie Api/Blazor)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Pipeline zakończony"
|
||||
240
.woodpecker.yml
Normal file
240
.woodpecker.yml
Normal file
@@ -0,0 +1,240 @@
|
||||
skip_clone: true
|
||||
when:
|
||||
event: pull_request
|
||||
action:
|
||||
- merge
|
||||
branch: master
|
||||
steps:
|
||||
debug-location:
|
||||
image: alpine
|
||||
commands:
|
||||
- pwd
|
||||
- ls -la
|
||||
- find /woodpecker -maxdepth 4 -type d 2>/dev/null
|
||||
clone-manual:
|
||||
image: woodpeckerci/plugin-git
|
||||
settings:
|
||||
remote: http://gitea:3000/FA/FA_WEB.git
|
||||
branch: master
|
||||
depth: 1
|
||||
restore:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
echo "Aktualna ścieżka: $(pwd)"
|
||||
echo "=== Restore wszystkich projektów ==="
|
||||
find . -name "*.csproj" -type f -exec dotnet restore "{}" \;
|
||||
test:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
echo "=== Uruchamianie testów ==="
|
||||
dotnet test --no-restore --configuration Release --logger "trx"
|
||||
depends_on: [restore]
|
||||
pack-datamodels:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
echo "=== Instalacja MinVer CLI ==="
|
||||
dotnet tool install --tool-path /tmp/minver minver-cli
|
||||
echo "=== Obliczona wersja przez MinVer ==="
|
||||
MINVER_VERSION=$(/tmp/minver/minver \
|
||||
--auto-increment patch \
|
||||
--minimum-major-minor 2.0 \
|
||||
--default-pre-release-identifiers alpha.0 \
|
||||
--verbosity info)
|
||||
echo "Wersja: $MINVER_VERSION"
|
||||
mkdir -p nupkg
|
||||
echo "=== Pełniejsze fetch git (unshallow + tags) ==="
|
||||
git fetch --prune --unshallow || echo "Już full clone – OK"
|
||||
git fetch --tags
|
||||
echo "=== Dostępne tagi ==="
|
||||
git tag -l
|
||||
echo "=== Aktualny commit i opis ==="
|
||||
git describe --tags --always --dirty
|
||||
echo "=== Diagnostyka projektów ==="
|
||||
find . -name "*.csproj" -type f | sort
|
||||
mkdir -p nupkg
|
||||
echo "=== Pakowanie projektów DataModel (wykrywane po obecności <PackageId>) ==="
|
||||
find . -name "*.csproj" -type f | while read csproj; do
|
||||
if grep -q '<PackageId>' "$csproj"; then
|
||||
PROJECT_NAME=$(basename "$csproj" .csproj)
|
||||
echo "→ Pakuję $PROJECT_NAME ($csproj)"
|
||||
dotnet pack "$csproj" \
|
||||
--configuration Release \
|
||||
-o "./nupkg" \
|
||||
/p:PackageVersion=$MINVER_VERSION
|
||||
else
|
||||
PROJECT_NAME=$(basename "$csproj" .csproj)
|
||||
echo "→ Pomijam $PROJECT_NAME – brak <PackageId>"
|
||||
fi
|
||||
done
|
||||
echo "=== Spakowane pakiety ==="
|
||||
ls -la nupkg/ || echo "Brak spakowanych pakietów!"
|
||||
depends_on: [test]
|
||||
publish-datamodels-to-baget:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
environment:
|
||||
BAGETTER_API_KEY:
|
||||
from_secret: baget_api_key
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
echo "=== Test połączenia z BaGetter ==="
|
||||
curl -f http://baget:80/v3/index.json || echo "Nie można połączyć się z BaGetter!"
|
||||
cat <<EOF > NuGet.Config
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="BaGet" value="http://baget:80/v3/index.json" allowInsecureConnections="true" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
EOF
|
||||
echo "=== Publikacja pakietów do BaGetter ==="
|
||||
find ./nupkg -name "*.nupkg" -type f | while read pkg; do
|
||||
echo "→ Push $(basename "$pkg")"
|
||||
dotnet nuget push "$pkg" \
|
||||
--source "BaGet" \
|
||||
--api-key "$BAGETTER_API_KEY" \
|
||||
--skip-duplicate
|
||||
done
|
||||
depends_on: [pack-datamodels]
|
||||
select-projects-for-container:
|
||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
> projects-to-build.txt
|
||||
|
||||
find . -name "*.csproj" -type f | while read csproj; do
|
||||
PROJECT_TYPE=$(dotnet msbuild "$csproj" -getProperty:ProjectType -noLogo 2>/dev/null || echo "Unknown")
|
||||
|
||||
if [[ "$PROJECT_TYPE" == "Linux" || "$PROJECT_TYPE" == "Api" || "$PROJECT_TYPE" == "ServerProcess" || "$PROJECT_TYPE" == "Blazor" ]]; then
|
||||
PROJECT_NAME=$(basename "$csproj" .csproj)
|
||||
PROJECT_DIR=$(dirname "$csproj")
|
||||
echo "→ Dodaję $PROJECT_NAME (typ: $PROJECT_TYPE)"
|
||||
echo "$PROJECT_DIR|$PROJECT_NAME" >> projects-to-build.txt
|
||||
else
|
||||
PROJECT_NAME=$(basename "$csproj" .csproj)
|
||||
echo "→ Pomijam $PROJECT_NAME (typ: $PROJECT_TYPE)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "=== Znaleziono projektow do budowy ==="
|
||||
cat projects-to-build.txt || echo "Brak projektów!"
|
||||
wc -l < projects-to-build.txt
|
||||
depends_on: [publish-datamodels-to-baget]
|
||||
containerize-apps:
|
||||
image: docker:27-cli
|
||||
environment:
|
||||
GITEA_USER:
|
||||
from_secret: gitea_registry_user
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_registry_token
|
||||
commands:
|
||||
- |
|
||||
set -euf
|
||||
cd "${CI_WORKSPACE}"
|
||||
|
||||
echo "$GITEA_TOKEN" | docker login git.modwad.pl -u "$GITEA_USER" --password-stdin
|
||||
|
||||
if [ ! -s projects-to-build.txt ]; then
|
||||
echo "Brak projektow"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
rm -f NuGet.Config
|
||||
|
||||
cat > NuGet.Config <<EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="BaGet" value="http://127.0.0.1:8080/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
EOF
|
||||
|
||||
cat projects-to-build.txt | while IFS='|' read -r PDIR PNAME; do
|
||||
PDIR=$(echo "$PDIR" | xargs)
|
||||
PNAME=$(echo "$PNAME" | xargs)
|
||||
|
||||
test -z "$PNAME" && continue
|
||||
test ! -d "$PDIR" && continue
|
||||
|
||||
echo -n "git.modwad.pl/fa/" > /tmp/tag.txt
|
||||
echo "$PNAME" | tr 'A-Z' 'a-z' | tr -d '\n' >> /tmp/tag.txt
|
||||
|
||||
echo "$PNAME" > /tmp/pname.txt
|
||||
echo "$PDIR" | sed 's|^\./||' > /tmp/pdir.txt
|
||||
|
||||
cat > "Dockerfile.temp" <<'ENDOFDOCKERFILE'
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN dotnet restore "%%PROJECTDIR%%/%%PROJECTNAME%%.csproj"
|
||||
RUN dotnet publish "%%PROJECTDIR%%/%%PROJECTNAME%%.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
ENTRYPOINT ["dotnet", "%%PROJECTNAME%%.dll"]
|
||||
ENDOFDOCKERFILE
|
||||
|
||||
REAL_PNAME=$(cat /tmp/pname.txt)
|
||||
REAL_PDIR=$(cat /tmp/pdir.txt)
|
||||
sed "s|%%PROJECTNAME%%|$REAL_PNAME|g; s|%%PROJECTDIR%%|$REAL_PDIR|g" "Dockerfile.temp" > "Dockerfile.final"
|
||||
|
||||
echo "=== Budowanie obrazu ==="
|
||||
DOCKER_BUILDKIT=0 docker build \
|
||||
--network=host \
|
||||
-t "$(cat /tmp/tag.txt):${CI_COMMIT_SHA}" \
|
||||
-t "$(cat /tmp/tag.txt):latest" \
|
||||
-f "Dockerfile.final" \
|
||||
. || { echo "BUILD FAILED"; exit 1; }
|
||||
|
||||
echo "=== Obraz zbudowany, sprawdzam ==="
|
||||
docker images | grep "$(cat /tmp/tag.txt | sed 's|git.modwad.pl/||')" || echo "Obraz nie znaleziony w docker images"
|
||||
|
||||
echo "=== Pushing SHA tag ==="
|
||||
docker push "$(cat /tmp/tag.txt):${CI_COMMIT_SHA}" || { echo "PUSH SHA FAILED"; exit 1; }
|
||||
|
||||
echo "=== Pushing latest tag ==="
|
||||
docker push "$(cat /tmp/tag.txt):latest" || { echo "PUSH LATEST FAILED"; exit 1; }
|
||||
|
||||
echo "=== SUCCESS: $(cat /tmp/tag.txt) ==="
|
||||
|
||||
rm -f "Dockerfile.temp" "Dockerfile.final"
|
||||
done
|
||||
|
||||
echo "=== WSZYSTKIE PROJEKTY ZAKONCZONE ==="
|
||||
depends_on: [select-projects-for-container]
|
||||
cleanup-docker:
|
||||
image: docker:27-cli
|
||||
commands:
|
||||
- |
|
||||
echo "=== Czyszczenie starych obrazów ==="
|
||||
docker image prune -af --filter "until=24h"
|
||||
echo "=== Czyszczenie nieużywanych kontenerów ==="
|
||||
docker container prune -f
|
||||
echo "=== Czyszczenie build cache ==="
|
||||
docker builder prune -af --filter "until=24h"
|
||||
echo "=== Status po czyszczeniu ==="
|
||||
docker system df
|
||||
depends_on: [containerize-apps]
|
||||
when:
|
||||
status: [success, failure]
|
||||
17
FaKrosnoApi/Controllers/CustomerController.cs
Normal file
17
FaKrosnoApi/Controllers/CustomerController.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SytelineSaAppEfDataModel.Dtos;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace FaKrosnoApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class CustomerController(ICustomerService service) : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<CustomerDto>>> GetAllCustomers()
|
||||
{
|
||||
IList<CustomerDto> customers = await service.GetAllCustomers();
|
||||
return Ok(customers);
|
||||
}
|
||||
}
|
||||
@@ -12,15 +12,96 @@ namespace FaKrosnoApi.Controllers
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<CustomerOrderDto>>> GetAll()
|
||||
{
|
||||
IEnumerable<CustomerOrderDto?> ediCustomerOrders = await service.GetAll();
|
||||
return Ok(ediCustomerOrders);
|
||||
IEnumerable<CustomerOrderDto?> customerOrders = await service.GetAll();
|
||||
return Ok(customerOrders);
|
||||
}
|
||||
|
||||
[HttpGet("by-order-number")]
|
||||
public async Task<ActionResult<CustomerOrderDto?>> GetByCustomerOrderNumber([FromQuery] Guid customerOrderNumber)
|
||||
public async Task<ActionResult<CustomerOrderDto?>> GetByCustomerOrderNumber(
|
||||
[FromQuery] Guid customerOrderNumber)
|
||||
{
|
||||
CustomerOrderDto? scheduleOrder = await service.GetByOrderNumber(customerOrderNumber);
|
||||
return scheduleOrder != null ? Ok(scheduleOrder) : NotFound();
|
||||
CustomerOrderDto? customerOrder = await service.GetByOrderNumber(customerOrderNumber);
|
||||
return customerOrder != null ? Ok(customerOrder) : NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("by-co-number")]
|
||||
public async Task<ActionResult<CustomerOrderDto?>> GetByCoNumber([FromQuery] string customerOrderNumber)
|
||||
{
|
||||
CustomerOrderDto? customerOrder = await service.GetByCoNumber(customerOrderNumber);
|
||||
return customerOrder != null ? Ok(customerOrder) : NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("lines-by-co-number")]
|
||||
public async Task<ActionResult<IEnumerable<CustomerOrderLineDto>?>> GetLinesByCoNumber(
|
||||
[FromQuery] string customerOrderNumber)
|
||||
{
|
||||
var customerOrderLines = await service.GetLinesByCoNumber(customerOrderNumber);
|
||||
return customerOrderLines != null ? Ok(customerOrderLines) : NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("items-by-co-number")]
|
||||
public async Task<ActionResult<IEnumerable<CustomerOrderLineItemDto>?>> GetItemsByCoNumber(
|
||||
[FromQuery] string customerOrderNumber)
|
||||
{
|
||||
var customerOrderLineItems = await service.GetItemsByCoNumber(customerOrderNumber);
|
||||
return customerOrderLineItems != null ? Ok(customerOrderLineItems) : NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("by-customer-and-po")]
|
||||
public async Task<ActionResult<CustomerOrderDto?>> GetByCustomerAndPo([FromQuery] string customerNumber,
|
||||
[FromQuery] int customerSequence, [FromQuery] string poNumber)
|
||||
{
|
||||
CustomerOrderDto? customerOrder =
|
||||
await service.GetByCustomerAndPo(customerNumber, customerSequence, poNumber);
|
||||
return customerOrder != null ? Ok(customerOrder) : NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("by-po")]
|
||||
public async Task<ActionResult<CustomerOrderDto?>> GetByPo([FromQuery] string poNumber)
|
||||
{
|
||||
CustomerOrderDto? customerOrder = await service.GetByPo(poNumber);
|
||||
return customerOrder != null ? Ok(customerOrder) : NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("list-by-customer-and-po")]
|
||||
public async Task<ActionResult<IList<CustomerOrderDto?>>> GetListByCustomerAndPo(
|
||||
[FromQuery] string customerNumber, [FromQuery] int customerSequence, [FromQuery] string poNumber)
|
||||
{
|
||||
IList<CustomerOrderDto> customerOrders =
|
||||
await service.GetListByCustomerAndPo(customerNumber, customerSequence, poNumber);
|
||||
|
||||
foreach (CustomerOrderDto customerOrder in customerOrders)
|
||||
{
|
||||
customerOrder.CustomerOrderLines = await service.GetLinesByCoNumber(customerOrder.CoNum) ?? [];
|
||||
|
||||
foreach (CustomerOrderLineDto customerOrderLine in customerOrder.CustomerOrderLines)
|
||||
{
|
||||
customerOrderLine.CustomerOrderLineItems =
|
||||
await service.GetItemsByCoNumber(customerOrder.CoNum) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(customerOrders);
|
||||
}
|
||||
|
||||
[HttpGet("list-by-customer")]
|
||||
public async Task<ActionResult<IList<CustomerOrderDto?>>> GetListByCustomer([FromQuery] string customerNumber,
|
||||
[FromQuery] int customerSequence)
|
||||
{
|
||||
IList<CustomerOrderDto> customerOrders = await service.GetListByCustomer(customerNumber, customerSequence);
|
||||
|
||||
foreach (CustomerOrderDto customerOrder in customerOrders)
|
||||
{
|
||||
customerOrder.CustomerOrderLines = await service.GetLinesByCoNumber(customerOrder.CoNum) ?? [];
|
||||
|
||||
foreach (CustomerOrderLineDto customerOrderLine in customerOrder.CustomerOrderLines)
|
||||
{
|
||||
customerOrderLine.CustomerOrderLineItems =
|
||||
await service.GetItemsByCoNumber(customerOrder.CoNum) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(customerOrders);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
FaKrosnoApi/Controllers/CustomerTpController.cs
Normal file
17
FaKrosnoApi/Controllers/CustomerTpController.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SytelineSaAppEfDataModel.Dtos;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace FaKrosnoApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class CustomerTpController(ICustomerTpService service) : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<CustomerTpDto>>> GetAllCustomers()
|
||||
{
|
||||
IList<CustomerTpDto> customers = await service.GetAllCustomersTp();
|
||||
return Ok(customers);
|
||||
}
|
||||
}
|
||||
35
FaKrosnoApi/Controllers/EdiCustomerOrderImportController.cs
Normal file
35
FaKrosnoApi/Controllers/EdiCustomerOrderImportController.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SytelineSaAppEfDataModel.Dtos;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace FaKrosnoApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class EdiCustomerOrderImportController(IEdiCustomerOrderImportService service) : Controller
|
||||
{
|
||||
[HttpGet("last-update-date")]
|
||||
public async Task<ActionResult<DateTime>> GetLastUpdateDate()
|
||||
{
|
||||
DateTime lastUpdateDate = await service.GetLastUpdateDate();
|
||||
return Ok(lastUpdateDate);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> Add([FromBody] EdiCustomerOrderImportDto ediCustomerOrderImport)
|
||||
{
|
||||
var result = await service.AddEdiCustomerOrderImport(ediCustomerOrderImport);
|
||||
return result
|
||||
? Ok("Utworzono rekord w tabeli EdiCustomerOrderImport.")
|
||||
: BadRequest($"Nie mogę utworzyć rekordu w tabeli EdiCustomerOrderImport.");
|
||||
}
|
||||
|
||||
[HttpPost("add-bulk")]
|
||||
public async Task<ActionResult> Add([FromBody] IList<EdiCustomerOrderImportDto> ediCustomerOrderImports)
|
||||
{
|
||||
var result = await service.AddEdiCustomerOrderImports(ediCustomerOrderImports);
|
||||
return result
|
||||
? Ok("Utworzono rekordy w tabeli EdiCustomerOrderImport.")
|
||||
: BadRequest($"Nie mogę utworzyć rekordów w tabeli EdiCustomerOrderImport.");
|
||||
}
|
||||
}
|
||||
@@ -30,11 +30,33 @@ namespace FaKrosnoApi.Controllers
|
||||
return scheduleOrder != null ? Ok(scheduleOrder) : NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("last-order-number")]
|
||||
public async Task<ActionResult<int>> GetLastOrderNumber()
|
||||
{
|
||||
int lastOrderNumber = await service.GetLastOrderNumber();
|
||||
return Ok(lastOrderNumber);
|
||||
}
|
||||
|
||||
[HttpPost("send-to-syteline")]
|
||||
public async Task<ActionResult<int>> SendOrderToSyteline([FromQuery] Guid customerOrderNumber)
|
||||
{
|
||||
int result = await service.SendOrderToSyteline(customerOrderNumber);
|
||||
return result > 0 ? Ok() : BadRequest();
|
||||
}
|
||||
|
||||
[HttpPost("save-orders")]
|
||||
public async Task<ActionResult> SaveOrdersWithDetails([FromBody] List<EdiCustomerOrderDto> ediCustomerOrders)
|
||||
{
|
||||
if (!ediCustomerOrders.Any())
|
||||
{
|
||||
return BadRequest("The list of orders cannot be null or empty.");
|
||||
}
|
||||
|
||||
var result = await service.SaveOrdersWithDetails(ediCustomerOrders);
|
||||
|
||||
return result.Item1
|
||||
? Ok(result)
|
||||
: BadRequest($"Failed to save orders. Error: {result.Item2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
FaKrosnoApi/Controllers/EdiLogController.cs
Normal file
19
FaKrosnoApi/Controllers/EdiLogController.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SytelineSaAppEfDataModel.Dtos;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace FaKrosnoApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class EdiLogController(IEdiLogService service) : Controller
|
||||
{
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> Add([FromBody] EdiLogDto ediLog)
|
||||
{
|
||||
var result = await service.AddEdiLog(ediLog);
|
||||
return result
|
||||
? Ok("Utworzono rekord w tabeli EdiLog.")
|
||||
: BadRequest($"Nie mogę utworzyć rekordu w tabeli EdiLog.");
|
||||
}
|
||||
}
|
||||
224
FaKrosnoApi/Controllers/ExcelGeneratorController.cs
Normal file
224
FaKrosnoApi/Controllers/ExcelGeneratorController.cs
Normal file
@@ -0,0 +1,224 @@
|
||||
using System.Net;
|
||||
using System.Net.Mail;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Syncfusion.Drawing;
|
||||
using Syncfusion.XlsIO;
|
||||
using SytelineSaAppEfDataModel.Dtos;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace FaKrosnoApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ExcelGeneratorController(IWzHeaderService wzHeaderService, IWzClientService wzClientService, IMaterialTransactionService materialTransactionService, IConfiguration configuration) : Controller
|
||||
{
|
||||
[HttpGet("generate-meyle")]
|
||||
public async Task GeneratePackListForMeyle(Guid packListId)
|
||||
{
|
||||
WzHeaderDto wzHeader = await wzHeaderService.GetById(packListId);
|
||||
MaterialTransactionDto? materialTransaction = await materialTransactionService.GetByWzNumber(wzHeader.WzRowsMeyle.First().WzNumber);
|
||||
|
||||
using ExcelEngine excelEngine = new ExcelEngine();
|
||||
IApplication application = excelEngine.Excel;
|
||||
application.DefaultVersion = ExcelVersion.Xlsx;
|
||||
|
||||
IWorkbook workbook = application.Workbooks.Create(1);
|
||||
IWorksheet worksheet = workbook.Worksheets[0];
|
||||
|
||||
IStyle boldFontStyle = workbook.Styles.Add("BoldFontStyle");
|
||||
boldFontStyle.Font.Bold = true;
|
||||
|
||||
var mainHeaders = new List<string>
|
||||
{ "Numer zamówienia Meyle", "Meyle Numer", "Ilość w dostawie", "Numer Palety", "Nr Wz", "Nr Partii" };
|
||||
|
||||
worksheet["B1"].Value = "Packing List";
|
||||
worksheet["B1"].CellStyle = boldFontStyle;
|
||||
|
||||
worksheet["B3"].Value = "Supplier Name";
|
||||
worksheet["B3"].CellStyle = boldFontStyle;
|
||||
|
||||
worksheet["D3"].Value = "FA KROSNO";
|
||||
worksheet["D3"].CellStyle = boldFontStyle;
|
||||
|
||||
worksheet["B4"].Value = "Packing List nr";
|
||||
worksheet["B4"].CellStyle = boldFontStyle;
|
||||
|
||||
worksheet["B6"].Value = "Related delivery note";
|
||||
worksheet["B6"].CellStyle = boldFontStyle;
|
||||
|
||||
worksheet["D6"].Value = string.Join(", ", wzHeader.WzRowsMeyle.Select(x => x.WzNumber).Distinct());
|
||||
worksheet["D6"].CellStyle = boldFontStyle;
|
||||
|
||||
worksheet["B9"].Value = "Forwarder";
|
||||
worksheet["B9"].CellStyle = boldFontStyle;
|
||||
|
||||
worksheet["B10"].Value = "Date";
|
||||
worksheet["B10"].CellStyle = boldFontStyle;
|
||||
|
||||
worksheet["D10"].DateTime = materialTransaction?.CreateDate ?? DateTime.Now;
|
||||
|
||||
int currentRow = 12;
|
||||
|
||||
for (int i = 0; i < mainHeaders.Count; i++)
|
||||
{
|
||||
string columnLetter = GetColumnLetter(i);
|
||||
worksheet.Range[$"{columnLetter}{currentRow}"].Text = mainHeaders[i];
|
||||
worksheet.Range[$"{columnLetter}{currentRow}"].CellStyle = boldFontStyle;
|
||||
}
|
||||
|
||||
currentRow++;
|
||||
|
||||
foreach (var wzRow in wzHeader.WzRowsMeyle)
|
||||
{
|
||||
worksheet.Range[$"A{currentRow}"].Text = wzRow.OrderNumber;
|
||||
worksheet.Range[$"B{currentRow}"].Text = wzRow.ItemNumber;
|
||||
worksheet.Range[$"C{currentRow}"].Number = (double)wzRow.Quantity!;
|
||||
worksheet.Range[$"D{currentRow}"].Number = (double)wzRow.PalletNumber!;
|
||||
worksheet.Range[$"E{currentRow}"].Text = wzRow.WzNumber;
|
||||
worksheet.Range[$"F{currentRow}"].Text = wzRow.PartNumber;
|
||||
|
||||
currentRow++;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
worksheet.AutofitColumn(i + 1);
|
||||
}
|
||||
|
||||
using MemoryStream stream = new MemoryStream();
|
||||
workbook.SaveAs(stream);
|
||||
stream.Position = 0;
|
||||
|
||||
SendEmail(stream, wzHeader, "Meyle");
|
||||
}
|
||||
|
||||
[HttpGet("generate-marelli")]
|
||||
public async Task GeneratePackListForMarelli(Guid packListId)
|
||||
{
|
||||
WzHeaderDto wzHeader = await wzHeaderService.GetByIdMarelli(packListId);
|
||||
WzClientDto? wzClient = await wzClientService.GetById(wzHeader.FK_Client ?? Guid.NewGuid());
|
||||
MaterialTransactionDto? materialTransaction = await materialTransactionService.GetByWzNumber(wzHeader.WzRowsMarelli.First().WzNumber);
|
||||
|
||||
using ExcelEngine excelEngine = new ExcelEngine();
|
||||
IApplication application = excelEngine.Excel;
|
||||
application.DefaultVersion = ExcelVersion.Xlsx;
|
||||
|
||||
IWorkbook workbook = application.Workbooks.Create(1);
|
||||
IWorksheet worksheet = workbook.Worksheets[0];
|
||||
|
||||
IStyle boldFontStyle = workbook.Styles.Add("BoldFontStyle");
|
||||
boldFontStyle.Font.Bold = true;
|
||||
|
||||
var mainHeaders = new List<string>
|
||||
{ "", "ColliNr", "Magneti Marelli Sales nr", "Engineer number", "Quantity", "Order nr", "Supplier Comments" };
|
||||
|
||||
worksheet.Range["A1:B1"].Merge();
|
||||
worksheet.Range["A1"].Text = "PACKING LIST";
|
||||
worksheet.Range["A1"].CellStyle = boldFontStyle;
|
||||
|
||||
worksheet["A3"].Value = "Supplier Name";
|
||||
worksheet["A3"].CellStyle = boldFontStyle;
|
||||
|
||||
worksheet["B3"].Value = "FA KROSNO";
|
||||
worksheet["B3"].CellStyle = boldFontStyle;
|
||||
|
||||
worksheet["A4"].Value = "Packing List nr";
|
||||
worksheet["A4"].CellStyle = boldFontStyle;
|
||||
|
||||
worksheet["B4"].Value = string.Join(", ", wzHeader.WzRowsMarelli.Select(x => x.WzNumber).Distinct());
|
||||
worksheet["B4"].CellStyle = boldFontStyle;
|
||||
|
||||
worksheet["A6"].Value = "Date";
|
||||
worksheet["A6"].CellStyle = boldFontStyle;
|
||||
|
||||
worksheet["B6"].DateTime = materialTransaction?.CreateDate ?? DateTime.Now;
|
||||
|
||||
if (!string.IsNullOrEmpty(wzClient?.LogoBase64))
|
||||
{
|
||||
byte[] logoBytes = Convert.FromBase64String(wzClient.LogoBase64);
|
||||
|
||||
worksheet.Range["D1:F6"].Merge();
|
||||
|
||||
using MemoryStream imageStream = new MemoryStream(logoBytes);
|
||||
worksheet.Pictures.AddPicture(1, 4, imageStream);
|
||||
}
|
||||
|
||||
int currentRow = 12;
|
||||
|
||||
for (int i = 0; i < mainHeaders.Count; i++)
|
||||
{
|
||||
string columnLetter = GetColumnLetter(i);
|
||||
worksheet.Range[$"{columnLetter}{currentRow}"].Text = mainHeaders[i];
|
||||
worksheet.Range[$"{columnLetter}{currentRow}"].CellStyle = boldFontStyle;
|
||||
}
|
||||
|
||||
currentRow++;
|
||||
|
||||
foreach (var wzRow in wzHeader.WzRowsMarelli)
|
||||
{
|
||||
worksheet.Range[$"A{currentRow}"].Text = "MIX";
|
||||
worksheet.Range[$"B{currentRow}"].Number = (double)wzRow.PalletNumber!;
|
||||
worksheet.Range[$"C{currentRow}"].Text = wzRow.ItemNumber;
|
||||
worksheet.Range[$"D{currentRow}"].Text = wzRow.EngineerNumber;
|
||||
worksheet.Range[$"E{currentRow}"].Number = (double)wzRow.Quantity!;
|
||||
worksheet.Range[$"F{currentRow}"].Text = wzRow.OrderNumber;
|
||||
worksheet.Range[$"G{currentRow}"].Text = wzRow.WzNumber;
|
||||
|
||||
currentRow++;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
worksheet.AutofitColumn(i + 1);
|
||||
}
|
||||
|
||||
using MemoryStream stream = new MemoryStream();
|
||||
workbook.SaveAs(stream);
|
||||
stream.Position = 0;
|
||||
|
||||
SendEmail(stream, wzHeader, "Marelli Magneti");
|
||||
}
|
||||
|
||||
private static string GetColumnLetter(int columnIndex)
|
||||
{
|
||||
string columnName = string.Empty;
|
||||
while (columnIndex >= 0)
|
||||
{
|
||||
columnName = (char)('A' + (columnIndex % 26)) + columnName;
|
||||
columnIndex = (columnIndex / 26) - 1;
|
||||
}
|
||||
|
||||
return columnName;
|
||||
}
|
||||
|
||||
private void SendEmail(MemoryStream stream, WzHeaderDto wzHeader, string client)
|
||||
{
|
||||
IConfigurationSection smtpSettings = configuration.GetSection("EmailSettings");
|
||||
string smtpHost = smtpSettings["SmtpServer"] ?? string.Empty;
|
||||
int smtpPort = int.Parse(smtpSettings["Port"] ?? "0");
|
||||
string smtpUsername = smtpSettings["SenderEmail"] ?? string.Empty;
|
||||
string smtpPassword = smtpSettings["SenderPassword"] ?? string.Empty;
|
||||
string fromEmail = smtpSettings["SenderEmail"] ?? string.Empty;
|
||||
List<string> toEmail = wzHeader.EmailAddresses?.Split(',')?.ToList() ??
|
||||
(smtpSettings["RecipientEmail"] ?? string.Empty).Split(',').ToList();
|
||||
string subject = $"{client}: Packing List";
|
||||
string body =
|
||||
$"W załączeniu znajduje się Packing List dla klienta ${client} wygenerowany {DateTime.Now:dd.MM.yyyy HH:mm:ss}";
|
||||
|
||||
using var mailMessage = new MailMessage();
|
||||
mailMessage.From = new MailAddress(fromEmail);
|
||||
mailMessage.Subject = subject;
|
||||
mailMessage.Body = body;
|
||||
mailMessage.IsBodyHtml = false;
|
||||
|
||||
mailMessage.Attachments.Add(new Attachment(stream, $"PackingList_{client}_{DateTime.Now:yyyyMMddHHmmss}.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
|
||||
|
||||
toEmail.ForEach(x => mailMessage.To.Add(x));
|
||||
|
||||
using var smtpClient = new SmtpClient(smtpHost, smtpPort);
|
||||
smtpClient.EnableSsl = true;
|
||||
smtpClient.Credentials = new NetworkCredential(smtpUsername, smtpPassword);
|
||||
smtpClient.Send(mailMessage);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ using System.Diagnostics;
|
||||
using FaKrosnoApi.Models;
|
||||
using Hangfire;
|
||||
using Hangfire.Storage;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OrdersManagementDataModel.Dtos;
|
||||
using OrdersManagementDataModel.Services;
|
||||
@@ -16,7 +15,6 @@ public class HangfireJobsController(
|
||||
IRecurringJobManager recurringJobManager,
|
||||
ITaskSchedulerService service) : Controller
|
||||
{
|
||||
[HttpGet("GetJobsToRun")]
|
||||
public async Task<ActionResult<IEnumerable<JobModel>>> GetJobsToRun()
|
||||
{
|
||||
IList<JobModel> jobsToRun = new List<JobModel>();
|
||||
@@ -102,6 +100,22 @@ public class HangfireJobsController(
|
||||
return Ok("Zadanie zostało usunięte.");
|
||||
}
|
||||
|
||||
[HttpPost("update")]
|
||||
public async Task<IActionResult> UpdateTask([FromBody] TaskSchedulerDto taskSchedulerDto)
|
||||
{
|
||||
int result = await service.UpdateTaskScheduler(taskSchedulerDto);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
return BadRequest("Nie udało się uaktualnic zadania.");
|
||||
}
|
||||
|
||||
recurringJobManager.AddOrUpdate(taskSchedulerDto.Name, () => RunConsoleApplication(taskSchedulerDto.Path),
|
||||
taskSchedulerDto.CronOptions, new RecurringJobOptions { TimeZone = TimeZoneInfo.Local });
|
||||
|
||||
return Ok("Zadanie zostało zaktualizowane.");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<TaskSchedulerDto>>> GetTasks()
|
||||
{
|
||||
|
||||
17
FaKrosnoApi/Controllers/ItemController.cs
Normal file
17
FaKrosnoApi/Controllers/ItemController.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SytelineSaAppEfDataModel.Dtos;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace FaKrosnoApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ItemController(IItemService service) : Controller
|
||||
{
|
||||
[HttpGet("by-number")]
|
||||
public async Task<ActionResult<ItemDto>> Get([FromQuery] string itemNumber)
|
||||
{
|
||||
ItemDto item = await service.GetItem(itemNumber);
|
||||
return Ok(item);
|
||||
}
|
||||
}
|
||||
16
FaKrosnoApi/Controllers/ItemCustController.cs
Normal file
16
FaKrosnoApi/Controllers/ItemCustController.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SytelineSaAppEfDataModel.Dtos;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace FaKrosnoApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ItemCustController(IItemCustService service) : Controller
|
||||
{
|
||||
public async Task<ActionResult<ItemCustDto>> GetItem([FromQuery] string itemNumber, [FromQuery] string customerNumber)
|
||||
{
|
||||
ItemCustDto item = await service.GetItem(itemNumber, customerNumber);
|
||||
return item != null ? Ok(item) : NotFound();
|
||||
}
|
||||
}
|
||||
23
FaKrosnoApi/Controllers/ItemCustPriceAllController.cs
Normal file
23
FaKrosnoApi/Controllers/ItemCustPriceAllController.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SytelineSaAppEfDataModel.Dtos;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace FaKrosnoApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ItemCustPriceAllController(IItemCustPriceAllService service, IMapper mapper) : Controller
|
||||
{
|
||||
[HttpGet("by-parameters")]
|
||||
public async Task<ActionResult<ItemCustPriceAllDto>> GetItemCustPriceAll([FromQuery] string itemNumber, [FromQuery] string customerNumber)
|
||||
{
|
||||
var result = await service.GetItemCustPriceAllAsync(itemNumber, customerNumber);
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(mapper.Map<ItemCustPriceAllDto>(result));
|
||||
}
|
||||
}
|
||||
42
FaKrosnoApi/Controllers/MaterialTransactionsController.cs
Normal file
42
FaKrosnoApi/Controllers/MaterialTransactionsController.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SytelineSaAppEfDataModel.Dtos;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace FaKrosnoApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class MaterialTransactionsController(IMaterialTransactionService service) : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IEnumerable<MaterialTransactionDto>> GetAll()
|
||||
{
|
||||
return await service.GetAll();
|
||||
}
|
||||
|
||||
[HttpGet("by-wz-number")]
|
||||
public Task<MaterialTransactionDto?> GetByWzNumber([FromQuery] string wzNumber)
|
||||
{
|
||||
return service.GetByWzNumber(wzNumber);
|
||||
}
|
||||
|
||||
[HttpGet("list-by-wz-numbers")]
|
||||
public Task<IEnumerable<MaterialTransactionDto>> GetListByWzNumber([FromBody] ISet<string> wzNumbers)
|
||||
{
|
||||
return service.GetOrderNumbersByWz(wzNumbers);
|
||||
}
|
||||
|
||||
[HttpGet("by-order-number")]
|
||||
public Task<IEnumerable<MaterialTransactionDto>> GetByOrderNumber([FromQuery] string orderNumber)
|
||||
{
|
||||
return service.GetByOrderNumber(orderNumber);
|
||||
}
|
||||
|
||||
// public Task<IEnumerable<MaterialTransactionDto>> GetOrderNumbersByWz(ISet<string> wzNumbers);
|
||||
// public Task<IEnumerable<MaterialTransactionDto>> GetByCustomerNumber(string customerNumber, int customerSequence);
|
||||
// public Task<MaterialTransactionDto?> GetByPartNumber(string partNumber);
|
||||
// public Task<IEnumerable<MaterialTransactionDto>> GetWithPartNumber();
|
||||
|
||||
|
||||
}
|
||||
25
FaKrosnoApi/Controllers/VatCodeAssociationController.cs
Normal file
25
FaKrosnoApi/Controllers/VatCodeAssociationController.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SytelineSaAppEfDataModel.Dtos;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace FaKrosnoApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class VatCodeAssociationController(IVatCodeAssociationService service, IMapper mapper) : Controller
|
||||
{
|
||||
[HttpGet("by-parameters")]
|
||||
public async Task<ActionResult<VatCodeAssociationDto>> GetVatCodesAssociation(string customerDoInvoice,
|
||||
string endUserType, string productCode)
|
||||
{
|
||||
var result = await service.GetVatCodesAssociation(customerDoInvoice, endUserType, productCode);
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(mapper.Map<VatCodeAssociationDto>(result));
|
||||
}
|
||||
|
||||
}
|
||||
24
FaKrosnoApi/Controllers/WzClientController.cs
Normal file
24
FaKrosnoApi/Controllers/WzClientController.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SytelineSaAppEfDataModel.Dtos;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace FaKrosnoApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class WzClientController(IWzClientService service) : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<WzClientDto>>> GetAll()
|
||||
{
|
||||
IEnumerable<WzClientDto> wzClients = await service.GetAll();
|
||||
return Ok(wzClients);
|
||||
}
|
||||
|
||||
[HttpGet("by-id")]
|
||||
public async Task<ActionResult<WzClientDto>> GetById(Guid id)
|
||||
{
|
||||
WzClientDto? wzClient = await service.GetById(id);
|
||||
return Ok(wzClient);
|
||||
}
|
||||
}
|
||||
67
FaKrosnoApi/Controllers/WzHeaderController.cs
Normal file
67
FaKrosnoApi/Controllers/WzHeaderController.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SytelineSaAppEfDataModel.Dtos;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace FaKrosnoApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class WzHeaderController(IWzHeaderService service, IMaterialTransactionService materialTransactionService) : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<WzHeaderDto>>> GetAll()
|
||||
{
|
||||
IEnumerable<WzHeaderDto> wzHeaders = await service.GetAll();
|
||||
return Ok(wzHeaders);
|
||||
}
|
||||
|
||||
[HttpGet("by-customer-number")]
|
||||
public async Task<ActionResult<IEnumerable<MaterialTransactionDto>>> GetByCustomerNumber(
|
||||
[FromQuery] string customerNumber, [FromQuery] int customerSequence)
|
||||
{
|
||||
IEnumerable<MaterialTransactionDto> materialTransactions =
|
||||
await materialTransactionService.GetByCustomerNumber(customerNumber, customerSequence);
|
||||
return Ok(materialTransactions);
|
||||
}
|
||||
|
||||
[HttpGet("all-wz-headers")]
|
||||
public async Task<ActionResult<IEnumerable<MaterialTransactionDto>>> GetHeadersByCustomerNumber(
|
||||
[FromQuery] string customerNumber, [FromQuery] int customerSequence)
|
||||
{
|
||||
IEnumerable<WzHeaderDto> wzHeaders =
|
||||
await service.GetByCustomerNumber(customerNumber, customerSequence);
|
||||
|
||||
return Ok(wzHeaders.OrderByDescending(x => x.CreatedDate));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> CreateHeader([FromBody] WzHeaderDto wzHeader)
|
||||
{
|
||||
if (wzHeader == null)
|
||||
{
|
||||
return BadRequest("WzHeader cannot be null.");
|
||||
}
|
||||
|
||||
await service.CreateHeader(wzHeader);
|
||||
return CreatedAtAction(nameof(CreateHeader), wzHeader);
|
||||
}
|
||||
|
||||
[HttpGet("by-id")]
|
||||
public async Task<ActionResult<WzHeaderDto>> GetById([FromQuery] Guid id)
|
||||
{
|
||||
WzHeaderDto? wzHeader = await service.GetById(id);
|
||||
return wzHeader != null ? Ok(wzHeader) : NotFound();
|
||||
}
|
||||
|
||||
[HttpPost("add-emails")]
|
||||
public async Task<IActionResult> AddEmailsToWzHeader([FromQuery] Guid id, [FromBody] string emailAddresses)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(emailAddresses))
|
||||
{
|
||||
return BadRequest("Email addresses cannot be empty.");
|
||||
}
|
||||
|
||||
await service.AddEmailsToWzHeader(id, emailAddresses);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
62
FaKrosnoApi/Controllers/WzRowMarelliController.cs
Normal file
62
FaKrosnoApi/Controllers/WzRowMarelliController.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SytelineSaAppEfDataModel.Dtos;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace FaKrosnoApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class WzRowMarelliController(IWzRowMareliService service, IMaterialTransactionService materialTransactionService) : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<WzRowMarelliDto>>> GetAll()
|
||||
{
|
||||
IEnumerable<WzRowMarelliDto> wzRows = await service.GetAll();
|
||||
return Ok(wzRows);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> CreateRows([FromBody] IEnumerable<WzRowMarelliDto> rows)
|
||||
{
|
||||
if (rows == null || !rows.Any())
|
||||
{
|
||||
return BadRequest("No rows provided.");
|
||||
}
|
||||
|
||||
await service.CreateRows(rows);
|
||||
return CreatedAtAction(nameof(GetAll), new { count = rows.Count() }, rows);
|
||||
}
|
||||
|
||||
[HttpGet("by-wz-header-id")]
|
||||
public async Task<ActionResult<IEnumerable<WzRowMarelliDto>>> GetByWzHeaderId(Guid wzHeaderId)
|
||||
{
|
||||
IEnumerable<WzRowMarelliDto> wzRows = await service.GetByWzHeaderId(wzHeaderId);
|
||||
return Ok(wzRows);
|
||||
}
|
||||
|
||||
[HttpGet("by-part-number")]
|
||||
public async Task<ActionResult<MaterialTransactionDto>> GetByPartNumber([FromQuery] string partNumber)
|
||||
{
|
||||
MaterialTransactionDto materialTransaction = await materialTransactionService.GetByPartNumber(partNumber);
|
||||
return Ok(materialTransaction);
|
||||
}
|
||||
|
||||
[HttpGet("transactions-with-part-number")]
|
||||
public async Task<ActionResult<MaterialTransactionDto>> GetTransactionsWithPartNumber()
|
||||
{
|
||||
IEnumerable<MaterialTransactionDto> materialTransactions = await materialTransactionService.GetWithPartNumber();
|
||||
return Ok(materialTransactions);
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
public async Task<ActionResult> UpdateRows([FromBody] IEnumerable<WzRowMarelliDto> rows)
|
||||
{
|
||||
if (rows == null || !rows.Any())
|
||||
{
|
||||
return BadRequest("No rows provided.");
|
||||
}
|
||||
|
||||
await service.UpdateRows(rows);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
62
FaKrosnoApi/Controllers/WzRowMeyleController.cs
Normal file
62
FaKrosnoApi/Controllers/WzRowMeyleController.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SytelineSaAppEfDataModel.Dtos;
|
||||
using SytelineSaAppEfDataModel.Services;
|
||||
|
||||
namespace FaKrosnoApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class WzRowMeyleController(IWzRowMeyleService service, IMaterialTransactionService materialTransactionService) : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<WzRowMeyleDto>>> GetAll()
|
||||
{
|
||||
IEnumerable<WzRowMeyleDto> wzRows = await service.GetAll();
|
||||
return Ok(wzRows);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> CreateRows([FromBody] IEnumerable<WzRowMeyleDto> rows)
|
||||
{
|
||||
if (rows == null || !rows.Any())
|
||||
{
|
||||
return BadRequest("No rows provided.");
|
||||
}
|
||||
|
||||
await service.CreateRows(rows);
|
||||
return CreatedAtAction(nameof(GetAll), new { count = rows.Count() }, rows);
|
||||
}
|
||||
|
||||
[HttpGet("by-wz-header-id")]
|
||||
public async Task<ActionResult<IEnumerable<WzRowMeyleDto>>> GetByWzHeaderId(Guid wzHeaderId)
|
||||
{
|
||||
IEnumerable<WzRowMeyleDto> wzRows = await service.GetByWzHeaderId(wzHeaderId);
|
||||
return Ok(wzRows);
|
||||
}
|
||||
|
||||
[HttpGet("by-part-number")]
|
||||
public async Task<ActionResult<MaterialTransactionDto>> GetByPartNumber([FromQuery] string partNumber)
|
||||
{
|
||||
MaterialTransactionDto materialTransaction = await materialTransactionService.GetByPartNumber(partNumber);
|
||||
return Ok(materialTransaction);
|
||||
}
|
||||
|
||||
[HttpGet("transactions-with-part-number")]
|
||||
public async Task<ActionResult<MaterialTransactionDto>> GetTransactionsWithPartNumber()
|
||||
{
|
||||
IEnumerable<MaterialTransactionDto> materialTransactions = await materialTransactionService.GetWithPartNumber();
|
||||
return Ok(materialTransactions);
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
public async Task<ActionResult> UpdateRows([FromBody] IEnumerable<WzRowMeyleDto> rows)
|
||||
{
|
||||
if (rows == null || !rows.Any())
|
||||
{
|
||||
return BadRequest("No rows provided.");
|
||||
}
|
||||
|
||||
await service.UpdateRows(rows);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ProjectType>Api</ProjectType>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Core" Version="1.6.0" />
|
||||
<PackageReference Include="FaKrosnoEfDataModel" Version="2.0.1-alpha.0.65" />
|
||||
<PackageReference Include="Hangfire" Version="1.8.17" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" />
|
||||
@@ -18,13 +20,10 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
|
||||
<PackageReference Include="NSwag.AspNetCore" Version="14.2.0" />
|
||||
<PackageReference Include="OrdersManagementDataModel" Version="2.0.0-alpha.0" />
|
||||
<PackageReference Include="Syncfusion.XlsIO.Net.Core" Version="29.2.4" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FaKrosnoEfDataModel\FaKrosnoEfDataModel.csproj" />
|
||||
<ProjectReference Include="..\OrdersManagementDataModel\OrdersManagementDataModel.csproj" />
|
||||
<ProjectReference Include="..\SytelineSaAppEfDataModel\SytelineSaAppEfDataModel.csproj" />
|
||||
<PackageReference Include="SytelineSaAppEfDataModel" Version="2.0.1-alpha.0.65" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
using FaKrosnoApi.Models;
|
||||
using FaKrosnoApi.Services;
|
||||
using FaKrosnoEfDataModel;
|
||||
using FaKrosnoEfDataModel.Services;
|
||||
using Hangfire;
|
||||
using Hangfire.SqlServer;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using OrdersManagementDataModel;
|
||||
using OrdersManagementDataModel.Services;
|
||||
using SytelineSaAppEfDataModel;
|
||||
@@ -100,9 +97,24 @@ builder.Services.AddScoped<IRoleService, RoleService>();
|
||||
builder.Services.AddScoped<IFunctionService, FunctionService>();
|
||||
builder.Services.AddScoped<IUserRoleService, UserRoleService>();
|
||||
builder.Services.AddScoped<IProductService, ProductService>();
|
||||
builder.Services.AddScoped<IMaterialTransactionService, MaterialTransactionService>();
|
||||
builder.Services.AddScoped<IWzClientService, WzClientService>();
|
||||
builder.Services.AddScoped<IWzHeaderService, WzHeaderService>();
|
||||
builder.Services.AddScoped<IWzRowMeyleService, WzRowMeyleService>();
|
||||
builder.Services.AddScoped<IItemCustService, ItemCustService>();
|
||||
builder.Services.AddScoped<IEdiCustomerOrderImportService, EdiCustomerOrderImportService>();
|
||||
builder.Services.AddScoped<ICustomerService, CustomerService>();
|
||||
builder.Services.AddScoped<ICustomerTpService, CustomerTpService>();
|
||||
builder.Services.AddScoped<IItemService, ItemService>();
|
||||
builder.Services.AddScoped<IVatCodeAssociationService, VatCodeAssociationService>();
|
||||
builder.Services.AddScoped<IItemCustPriceAllService, ItemCustPriceAllService>();
|
||||
builder.Services.AddScoped<IEdiLogService, EdiLogService>();
|
||||
builder.Services.AddScoped<IWzRowMareliService, WzRowMareliService>();
|
||||
|
||||
builder.Services.AddHostedService<TimedHostedService>();
|
||||
|
||||
Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense("NRAiBiAaIQQuGjN/V09+XU9HdVRDX3xKf0x/TGpQb19xflBPallYVBYiSV9jS3tTckVgWHldc3ZUR2lfVE90Vg==");
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseOpenApi();
|
||||
|
||||
@@ -6,4 +6,7 @@ public class ProductDto : DtoBase
|
||||
public int RecipientID { get; set; }
|
||||
public string RecipientIdx { get; set; }
|
||||
public string FaIdx { get; set; }
|
||||
public string RecipientName { get; set; }
|
||||
|
||||
public RecipientDto Recipient { get; set; }
|
||||
}
|
||||
@@ -6,4 +6,6 @@ public class Product : EntityBase
|
||||
public int RecipientID { get; set; }
|
||||
public string RecipientIdx { get; set; }
|
||||
public string FaIdx { get; set; }
|
||||
|
||||
public Recipient Recipient { get; set; }
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>FaKrosnoEfDataModel</PackageId>
|
||||
<Authors>Piotr Kus</Authors>
|
||||
<Description>FaKrosno Entity Framework Data Model</Description>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ProjectType>DataModel</ProjectType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<MinVerAutoIncrement>patch</MinVerAutoIncrement>
|
||||
<MinVerMinimumMajorMinor>1.0</MinVerMinimumMajorMinor>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -20,6 +27,10 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
||||
<PackageReference Include="MinVer" Version="6.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -5,12 +5,9 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace FaKrosnoEfDataModel.Services;
|
||||
|
||||
public class ProductService : ServiceBase<ProductDto>, IProductService
|
||||
public class ProductService(FaKrosnoDbContext context, IMapper mapper)
|
||||
: ServiceBase<ProductDto>(context, mapper), IProductService
|
||||
{
|
||||
public ProductService(FaKrosnoDbContext context, IMapper mapper) : base(context, mapper)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ProductDto?>> GetEntities()
|
||||
{
|
||||
IList<ProductDto> products = (await GetAll()).ToList();
|
||||
@@ -20,9 +17,32 @@ public class ProductService : ServiceBase<ProductDto>, IProductService
|
||||
|
||||
public async Task<IEnumerable<ProductDto?>> GetEntitiesToFix(string indexName)
|
||||
{
|
||||
IList<RecipientDto> recipients =
|
||||
(await Context.Recipients.ToListAsync()).Select(x => Mapper.Map<RecipientDto>(x)).ToList();
|
||||
IList<PurchaserDto> purchasers = (await Context.Purchasers.ToListAsync()).Select(x => Mapper.Map<PurchaserDto>(x)).ToList();
|
||||
IList<ProductDto> products = (await GetAll()).ToList();
|
||||
|
||||
return products.Where(x => x?.FaIdx == indexName);
|
||||
IEnumerable<ProductDto> productDtos = products.Where(x => x.FaIdx == indexName).ToList();
|
||||
|
||||
foreach (ProductDto productDto in productDtos)
|
||||
{
|
||||
RecipientDto? recipient = recipients.FirstOrDefault(x => x.ID == productDto.RecipientID);
|
||||
|
||||
if (recipient != null)
|
||||
{
|
||||
productDto.Recipient = recipient;
|
||||
productDto.RecipientName = recipient.RecipientDesc;
|
||||
}
|
||||
|
||||
PurchaserDto? purchaser = purchasers.FirstOrDefault(x => x.ID == productDto.Recipient.PurchaserID);
|
||||
|
||||
if (purchaser != null)
|
||||
{
|
||||
productDto.Recipient.Purchaser = purchaser;
|
||||
}
|
||||
}
|
||||
|
||||
return productDtos;
|
||||
}
|
||||
|
||||
public async Task UpdateEntity(ProductDto entity)
|
||||
|
||||
841
FaKrosnoManagement/generate_docs.py
Normal file
841
FaKrosnoManagement/generate_docs.py
Normal file
@@ -0,0 +1,841 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Skrypt generujący dokumentację PDF projektu FaKrosno Management.
|
||||
Uruchomienie: python3 generate_docs.py
|
||||
"""
|
||||
|
||||
from fpdf import FPDF
|
||||
import os
|
||||
|
||||
FONT_PATH = "/Library/Fonts/Arial Unicode.ttf"
|
||||
OUTPUT_FILE = os.path.join(os.path.dirname(__file__), "dokumentacja_fakrosno.pdf")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Pomocnicze struktury danych – opis każdego pliku z komentarzami
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
DOCS = [
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
("PUNKT WEJŚCIA I ROUTING APLIKACJI", None, None),
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
("main.tsx", "Główny punkt wejścia aplikacji (odpowiednik index.html + bootstrap w Blazorze)", [
|
||||
("1", "import { StrictMode } from 'react'",
|
||||
"Importuje tryb ścisły React'a – włącza dodatkowe ostrzeżenia podczas programowania."),
|
||||
("2", "import { createRoot } from 'react-dom/client'",
|
||||
"Importuje funkcję tworzącą korzeń aplikacji React w drzewie DOM (React 18+)."),
|
||||
("3", "import { BrowserRouter } from 'react-router-dom'",
|
||||
"Importuje router URL-owy: śledzi adres przeglądarki i aktualizuje widok bez przeładowania strony."),
|
||||
("4", "import { QueryClient, QueryClientProvider } from '@tanstack/react-query'",
|
||||
"QueryClient – zarządza pamięcią podręczną zapytań HTTP. QueryClientProvider – udostępnia go całej aplikacji."),
|
||||
("5", "import { ReactQueryDevtools } from '@tanstack/react-query-devtools'",
|
||||
"Narzędzie deweloperskie do podglądu stanu zapytań (widoczne tylko w trybie development)."),
|
||||
("6", "import App from './App'",
|
||||
"Główny komponent aplikacji zawierający definicję tras (routingu)."),
|
||||
("7", "import { initSyncfusion } from '@/lib/syncfusion'",
|
||||
"Importuje funkcję inicjalizującą bibliotekę tabel/siatek Syncfusion z polską lokalizacją."),
|
||||
("8", "import { Toaster } from '@/components/ui/Toaster'",
|
||||
"Komponent wyświetlający powiadomienia (np. 'Zapisano', 'Błąd') w rogu ekranu."),
|
||||
("9", "import { ErrorBoundary } from '@/components/ErrorBoundary'",
|
||||
"Komponent-otoczka chwytający nieobsłużone błędy React i pokazujący przyjazny komunikat zamiast białego ekranu."),
|
||||
("10", "import './index.css'",
|
||||
"Globalne style CSS (Tailwind) – definiuje bazowe reguły wyglądu całej strony."),
|
||||
("12", "initSyncfusion()",
|
||||
"Wywołuje inicjalizację Syncfusion – rejestruje klucz licencyjny i ustawia język polski."),
|
||||
("14-22", "const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 30_000, retry: 1, refetchOnWindowFocus: false } } })",
|
||||
"Tworzy klienta zarządzającego pamięcią podręczną danych. Ustawienia: dane są 'świeże' przez 30 sekund, przy błędzie ponawia raz, NIE odświeża po powrocie do zakładki przeglądarki."),
|
||||
("24-25", "const rootElement = document.getElementById('root')",
|
||||
"Szuka w HTML elementu <div id='root'> – to kontener, w którym wyrenderuje się cała aplikacja React. Jeśli nie istnieje, rzuca błędem."),
|
||||
("27-39", "createRoot(rootElement).render(...)",
|
||||
"Renderuje całą aplikację: StrictMode (ostrzeżenia dev) → ErrorBoundary (siatka bezpieczeństwa) → QueryClientProvider (cache HTTP) → BrowserRouter (routing URL) → App (strony) + Toaster (powiadomienia) + ReactQueryDevtools (narzędzie dev)."),
|
||||
]),
|
||||
|
||||
("App.tsx", "Definicja wszystkich tras (stron) aplikacji – odpowiednik routingu w Blazorze", [
|
||||
("1-6", "Importy bazowe",
|
||||
"lazy – wczytuje strony dopiero gdy są potrzebne (oszczędza czas ładowania). Suspense – pokazuje 'kółko ładowania' gdy strona się wczytuje. Routes/Route – definiują mapę adresów URL → strony."),
|
||||
("9-45", "const LoginPage = lazy(...), const RegisterPage = lazy(...) ...",
|
||||
"Definiuje 15 stron aplikacji jako 'leniwie ładowane'. Oznacza to, że kod strony (np. SchedulerPage.tsx) jest pobierany z serwera dopiero gdy użytkownik po raz pierwszy wejdzie na daną stronę – nie przy starcie aplikacji."),
|
||||
("47-101", "export default function App()",
|
||||
"Główna funkcja aplikacji. Renderuje NavigatorBridge (obsługa przekierowań z kodu JavaScript) oraz drzewo tras:"),
|
||||
("56-58", "Route path='/login', '/register', '/unauthorized'",
|
||||
"Trasy publiczne – dostępne bez logowania. Każda ładuje odpowiednią stronę."),
|
||||
("61-95", "Route element={<ProtectedRoute />}",
|
||||
"Trasy chronione – ProtectedRoute sprawdza czy użytkownik jest zalogowany. Jeśli nie – przekierowuje na /login. W środku AppShell (szablon z bocznym menu i nagłówkiem)."),
|
||||
("63-76", "Route index, Route path='ScheduleOrder/:id', 'Products', 'Warehouse/...'",
|
||||
"Strony dostępne dla zalogowanych pracowników: harmonogramy, produkty, magazyn i listy pakowe."),
|
||||
("79-93", "Route element={<AdminRoute />}",
|
||||
"Trasy tylko dla administratorów: zamówienia klienta, EDI, tłumaczenia, zarządzanie użytkownikami, harmonogram zadań."),
|
||||
("97", "Route path='*'",
|
||||
"Trasa 'złap wszystko' – jeśli adres URL nie pasuje do żadnej trasy, wyświetla stronę 404."),
|
||||
]),
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
("WARSTWA API – KOMUNIKACJA Z SERWEREM", None, None),
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
("src/api/client.ts", "Bazowy klient HTTP – fundament całej komunikacji z serwerem .NET", [
|
||||
("1-4", "Importy",
|
||||
"axios – biblioteka do zapytań HTTP (jak fetch, ale z dodatkowymi funkcjami). Importuje stałe, sklep autoryzacji i system powiadomień."),
|
||||
("7", "export type FieldErrors = Record<string, string[]>",
|
||||
"Typ opisujący błędy walidacji pól formularza zwracane przez serwer (słownik: nazwa_pola → lista_błędów)."),
|
||||
("9-19", "export class ApiError extends Error",
|
||||
"Niestandardowa klasa błędu API. Przechowuje: message (treść błędu), status (kod HTTP np. 404), fieldErrors (błędy pól formularza z serwera). Używana w całej aplikacji do obsługi błędów."),
|
||||
("25-32", "let navigator, registerNavigator, navigate",
|
||||
"Mechanizm przekierowań: gdy Axios złapie błąd 401/403, musi przekierować użytkownika na /login lub /unauthorized. Nie może bezpośrednio używać React Router (jest poza komponentem), więc przechowuje referencję do funkcji navigate, którą rejestruje komponent NavigatorBridge."),
|
||||
("34-37", "export const apiClient = axios.create(...)",
|
||||
"Tworzy instancję Axios z domyślnym adresem serwera API (np. http://localhost:5001) i nagłówkiem Accept: application/json."),
|
||||
("40-46", "apiClient.interceptors.request.use(...)",
|
||||
"Interceptor żądań (uruchamiany przed KAŻDYM zapytaniem HTTP): pobiera token JWT ze sklepu i dołącza go do nagłówka Authorization: Bearer TOKEN. Dzięki temu każde zapytanie jest automatycznie autoryzowane."),
|
||||
("52-60", "export function reqConfig(...)",
|
||||
"Buduje obiekt konfiguracyjny dla Axios: opcjonalnie dodaje signal (do anulowania zapytania) i params (parametry URL np. ?id=123). Zwraca tylko zdefiniowane właściwości."),
|
||||
("63-66", "export async function apiGet<T>(...)",
|
||||
"Uproszczona funkcja GET: wysyła zapytanie i zwraca od razu dane (data) zamiast całego obiektu odpowiedzi Axios."),
|
||||
("75-79", "function extractMessage(...)",
|
||||
"Wyciąga czytelny komunikat błędu z odpowiedzi serwera: obsługuje zarówno JSON (ProblemDetails z .NET) jak i zwykły tekst."),
|
||||
("82-120", "apiClient.interceptors.response.use(...)",
|
||||
"Interceptor odpowiedzi – centralna obsługa błędów HTTP:\n• 401 Unauthorized: wylogowuje użytkownika i przekierowuje na /login ('Sesja wygasła')\n• 403 Forbidden: przekierowuje na /unauthorized ('Brak uprawnień')\n• 422/400: błędy walidacji formularza\n• 5xx: błąd serwera – pokazuje toast 'Błąd serwera'\n• status 0: brak połączenia z siecią"),
|
||||
]),
|
||||
|
||||
("src/api/users.ts", "Funkcje API dla użytkowników i autoryzacji", [
|
||||
("4-10", "export async function login(...)",
|
||||
"Wysyła login i hasło do serwera POST /api/Users/login. Zwraca token JWT i datę wygaśnięcia."),
|
||||
("12-18", "export interface RegisterPayload",
|
||||
"Interfejs opisujący dane potrzebne do rejestracji: login, email, imię, nazwisko, hasło."),
|
||||
("21-23", "export async function register(...)",
|
||||
"Wysyła dane rejestracji do POST /api/Users/register. Hasło jest wysyłane jako tekst (szyfrowane po stronie serwera przez BCrypt)."),
|
||||
("26-28", "export async function changePassword(...)",
|
||||
"Zmienia hasło zalogowanego użytkownika. POST /api/Users/change-password. Serwer identyfikuje użytkownika z tokenu JWT."),
|
||||
("30-32", "export async function getUsers(...)",
|
||||
"Pobiera listę wszystkich użytkowników. GET /api/Users."),
|
||||
("34-36", "export async function getUserByUsername(...)",
|
||||
"Pobiera konkretnego użytkownika po nazwie loginu. GET /api/Users/by-username/?username=..."),
|
||||
("38-40", "export async function addUser(...)",
|
||||
"Dodaje nowego użytkownika. POST /api/Users."),
|
||||
("42-52", "export interface TemporaryPasswordResponse, AdminCreateUserPayload",
|
||||
"Typy dla operacji administratora: odpowiedź z tymczasowym hasłem oraz dane nowego użytkownika."),
|
||||
("55-63", "export async function adminCreateUser(...)",
|
||||
"Administrator tworzy nowego użytkownika. Serwer generuje tymczasowe hasło i zwraca je jednorazowo. POST /api/Users/admin-create."),
|
||||
("66-72", "export async function adminResetPassword(...)",
|
||||
"Administrator resetuje hasło użytkownika. Serwer generuje nowe tymczasowe hasło i zwraca je jednorazowo. POST /api/Users/admin-reset-password."),
|
||||
("74-76", "export async function updateUser(...)",
|
||||
"Aktualizuje dane użytkownika. PUT /api/Users."),
|
||||
("78-80", "export async function deleteUser(...)",
|
||||
"Usuwa użytkownika po jego identyfikatorze (rowPointer). DELETE /api/Users/?id=..."),
|
||||
]),
|
||||
|
||||
("src/api/customerOrders.ts", "Funkcje API dla zamówień klientów (Syteline)", [
|
||||
("4-6", "getCustomerOrders(signal?)",
|
||||
"Pobiera listę wszystkich zamówień klientów z Syteline. GET /api/CustomerOrders. Signal to token anulowania – gdy użytkownik opuści stronę, zapytanie jest przerywane."),
|
||||
("8-16", "getCustomerOrderByNumber(customerOrderNumber, signal?)",
|
||||
"Pobiera szczegóły jednego zamówienia po jego numerze (używane na stronie szczegółów zamówienia). GET /api/CustomerOrders/by-order-number/?customerOrderNumber=..."),
|
||||
("19-27", "getCustomerOrderByCoNumber(customerOrderNumber, signal?)",
|
||||
"Pobiera zamówienie po numerze CO z Syteline (używane przy tworzeniu listy pakowej). GET /api/CustomerOrders/by-co-number/?customerOrderNumber=..."),
|
||||
]),
|
||||
|
||||
("src/api/ediCustomerOrders.ts", "Funkcje API dla zamówień EDI", [
|
||||
("5-7", "getEdiCustomerOrders(signal?)",
|
||||
"Pobiera wszystkie zamówienia EDI. GET /api/EdiCustomerOrders."),
|
||||
("9-17", "getEdiCustomerOrderByNumber(customerOrderNumber, signal?)",
|
||||
"Pobiera jedno zamówienie EDI po numerze. GET /api/EdiCustomerOrders/by-order-number/?customerOrderNumber=..."),
|
||||
("25-44", "sendEdiOrderToSyteline(rowPointer, displayNumber)",
|
||||
"Wysyła zamówienie EDI do systemu Syteline. POST /api/EdiCustomerOrders/send-to-syteline. Jeśli operacja się powiodła – zwraca status=1 z numerem zamówienia. Jeśli błąd – próbuje pobrać logi błędów z serwera i zwraca status=0 z wiadomością błędu."),
|
||||
]),
|
||||
|
||||
("src/api/ediTranslations.ts", "Funkcje API dla powiązań zamówień EDI z Syteline", [
|
||||
("4-11", "getEdiTranslations(signal?)",
|
||||
"Pobiera listę wszystkich powiązań (mapowań) zamówień EDI z zamówieniami Syteline. GET /api/EdiCustomerOrdersTranslations."),
|
||||
("13-15", "deleteEdiTranslation(id)",
|
||||
"Usuwa konkretne powiązanie po ID. DELETE /api/EdiCustomerOrdersTranslations/?id=..."),
|
||||
]),
|
||||
|
||||
("src/api/errorLog.ts", "Funkcje API dla logów błędów", [
|
||||
("9-17", "getErrorLogs(customerOrderNumber, signal?)",
|
||||
"Pobiera logi błędów dla danego zamówienia. UWAGA: zgodnie z komentarzem w kodzie, ta funkcja uderza w ten sam endpoint co zamówienia klientów – to znany błąd z oryginalnej aplikacji Blazor, zachowany dla zachowania zgodności."),
|
||||
]),
|
||||
|
||||
("src/api/functions.ts", "Funkcje API dla uprawnień/funkcji systemowych", [
|
||||
("4-6", "getFunctions(signal?)", "Pobiera listę wszystkich funkcji systemowych. GET /api/Functions."),
|
||||
("8-10", "addFunction(fn)", "Dodaje nową funkcję. POST /api/Functions."),
|
||||
("12-14", "updateFunction(fn)", "Aktualizuje istniejącą funkcję. PUT /api/Functions."),
|
||||
("16-18", "deleteFunction(id)", "Usuwa funkcję po ID. DELETE /api/Functions/?id=..."),
|
||||
]),
|
||||
|
||||
("src/api/products.ts", "Funkcje API dla produktów", [
|
||||
("4-9", "getProductsByIndex(indexName, signal?)",
|
||||
"Wyszukuje produkty po indeksie/nazwie. GET /api/Product/by-index?indexName=... Używane na stronie Produkty."),
|
||||
("11-13", "updateProduct(product)",
|
||||
"Aktualizuje dane produktu (np. kod FA). PUT /api/Product."),
|
||||
]),
|
||||
|
||||
("src/api/roles.ts", "Funkcje API dla ról użytkowników", [
|
||||
("4-6", "getRoles(signal?)", "Pobiera listę wszystkich ról. GET /api/Roles."),
|
||||
("8-10", "addRole(role)", "Dodaje nową rolę. POST /api/Roles."),
|
||||
("12-14", "updateRole(role)", "Aktualizuje rolę. PUT /api/Roles."),
|
||||
("16-18", "deleteRole(id)", "Usuwa rolę. DELETE /api/Roles/?id=..."),
|
||||
]),
|
||||
|
||||
("src/api/scheduleOrders.ts", "Funkcje API dla harmonogramów zamówień (DELFOR)", [
|
||||
("4-6", "getScheduleOrders(signal?)",
|
||||
"Pobiera listę wszystkich harmonogramów DELFOR (zamówień cyklicznych). GET /api/ScheduleOrders."),
|
||||
("8-13", "getScheduleOrder(scheduleOrderId, signal?)",
|
||||
"Pobiera jeden harmonogram ze szczegółami (pozycje i daty). GET /api/ScheduleOrders/{id}."),
|
||||
]),
|
||||
|
||||
("src/api/scheduler.ts", "Funkcje API dla harmonogramu zadań automatycznych (Hangfire)", [
|
||||
("4-6", "getTaskSchedulers(signal?)", "Pobiera listę zaplanowanych zadań. GET /api/HangfireJobs/."),
|
||||
("8-10", "addTaskScheduler(task)", "Dodaje nowe zadanie cykliczne. POST /api/HangfireJobs/add."),
|
||||
("12-14", "updateTaskScheduler(task)", "Aktualizuje zadanie. POST /api/HangfireJobs/update."),
|
||||
("16-18", "deleteTaskScheduler(task)", "Usuwa zadanie. POST /api/HangfireJobs/delete."),
|
||||
]),
|
||||
|
||||
("src/api/warehouse.ts", "Funkcje API dla modułu magazynowego (WZ i listy pakowe)", [
|
||||
("13-15", "getWzHeaderById(id, signal?)", "Pobiera nagłówek dokumentu WZ po ID (GUID). GET /api/WzHeader/by-id?id=..."),
|
||||
("17-19", "getAllClients(signal?)", "Pobiera listę klientów magazynowych. GET /api/WzClient."),
|
||||
("21-30", "getAllClientWzs(customerNumber, customerSequence, signal?)",
|
||||
"Pobiera transakcje materiałowe (dokumenty WZ) dla konkretnego klienta. GET /api/WzHeader/by-customer-number."),
|
||||
("32-41", "getAllClientWzHeaders(customerNumber, customerSequence, signal?)",
|
||||
"Pobiera nagłówki list pakowych dla klienta. GET /api/WzHeader/all-wz-headers."),
|
||||
("43-45", "createWzHeader(header)", "Tworzy nowy nagłówek dokumentu WZ. POST /api/WzHeader."),
|
||||
("47-49", "addEmailsToWzHeader(id, emailAddresses)",
|
||||
"Zapisuje adresy e-mail przy nagłówku WZ (do wysyłki). POST /api/WzHeader/add-emails?id=..."),
|
||||
("51-59", "getCustomerOrder(customerOrderNumber, signal?)",
|
||||
"Pobiera zamówienie klienta po numerze CO (używane przy tworzeniu wierszy WZ). GET /api/CustomerOrders/by-co-number."),
|
||||
("61-67", "getItem(itemNumber, customerNumber, signal?)",
|
||||
"Pobiera powiązanie pozycja–klient (numer klienta, EAN13, kod FA). GET /api/ItemCust."),
|
||||
("69-87", "getWzRowsMeyleByHeaderId / getWzRowsMarelliByHeaderId",
|
||||
"Pobierają wiersze listy pakowej dla Meyle lub Marelli po ID nagłówka."),
|
||||
("89-95", "createWzRowsMeyle / createWzRowsMarelli",
|
||||
"Tworzą nowe wiersze listy pakowej (Meyle: POST /api/WzRowMeyle, Marelli: POST /api/WzRowMarelli)."),
|
||||
("97-103", "updateWzRowsMeyle / updateWzRowsMarelli",
|
||||
"Aktualizują istniejące wiersze listy pakowej."),
|
||||
("106-124", "getTransactionModels(signal?)",
|
||||
"Pobiera transakcje materiałowe z numerem karty kontrolnej. Grupuje wyniki w słownik: klucz = numer_karty_kontrolnej → lista transakcji. Używane w skanerowaniu kart kontrolnych przy tworzeniu listy pakowej."),
|
||||
("126-134", "generateXlsForMeyle / generateXlsForMarelli",
|
||||
"Wywołuje generowanie pliku Excel dla listy pakowej Meyle lub Marelli na serwerze. GET /api/ExcelGenerator/generate-meyle lub generate-marelli."),
|
||||
]),
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
("MAGAZYN STANU (STORE) – GLOBALNE DANE APLIKACJI", None, None),
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
("src/store/authStore.ts", "Magazyn autoryzacji – zarządza sesją zalogowanego użytkownika", [
|
||||
("1-3", "Importy",
|
||||
"zustand – biblioteka do zarządzania globalnym stanem (lżejsza alternatywa dla Redux). persist – middleware zapisujące stan w localStorage (dane przetrwają odświeżenie strony). jwtDecode – dekoduje token JWT bez weryfikacji podpisu."),
|
||||
("6-12", "interface JwtClaims",
|
||||
"Opisuje pola dostępne w tokenie JWT: sub (ID użytkownika), unique_name (nazwa), exp (czas wygaśnięcia w Unix timestamp). Pole [claim: string]: unknown pozwala na dodatkowe niestandardowe pola."),
|
||||
("14-19", "export interface AuthUser",
|
||||
"Dane zalogowanego użytkownika dostępne w aplikacji: name (wyświetlana nazwa) i claims (surowe dane z tokenu JWT)."),
|
||||
("21-28", "interface AuthState",
|
||||
"Struktura stanu autoryzacji: token (JWT), user (dane użytkownika), isAuthenticated (bool), setToken (funkcja zapisu), logout (funkcja wylogowania)."),
|
||||
("30-43", "function decode(token)",
|
||||
"Prywatna funkcja dekodująca token JWT: sprawdza czy token nie wygasł (exp * 1000 < teraz?). Wyciąga nazwę użytkownika z kilku możliwych pól JWT. Zwraca null jeśli token nieważny lub wygasły."),
|
||||
("51-82", "export const useAuthStore = create(...)(...)",
|
||||
"Tworzy globalny magazyn stanu autoryzacji z persist (zapisywanie w localStorage pod kluczem 'authToken'). Zawiera: setToken – zapisuje token, dekoduje go, jeśli nieważny czyści stan; logout – czyści token i dane użytkownika; onRehydrateStorage – przy starcie aplikacji weryfikuje zapisany token (czy nie wygasł)."),
|
||||
("85-87", "export function getAuthToken()",
|
||||
"Odczytuje token JWT poza komponentem React (używane przez interceptor Axios). Wzorzec: useAuthStore.getState().token."),
|
||||
]),
|
||||
|
||||
("src/store/toastStore.ts", "Magazyn powiadomień (toastów) – system komunikatów dla użytkownika", [
|
||||
("3", "export type ToastVariant",
|
||||
"Typ wariantu powiadomienia: success (zielony), error (czerwony), info (niebieski), warning (żółty)."),
|
||||
("5-10", "export interface Toast",
|
||||
"Struktura powiadomienia: id (unikalny identyfikator), variant (typ), title (tytuł), description (opcjonalny opis)."),
|
||||
("18-30", "export const useToastStore = create(...)",
|
||||
"Magazyn powiadomień: toasts – lista aktywnych powiadomień; push – dodaje nowe powiadomienie, generuje UUID, po 5 sekundach automatycznie je usuwa; dismiss – usuwa powiadomienie po kliknięciu X."),
|
||||
("32-42", "export const toast = { success, error, info, warning }",
|
||||
"Imperatywne API do wyświetlania powiadomień poza komponentami React (np. w interceptorze Axios). Przykład: toast.error('Błąd serwera', 'Spróbuj ponownie później')."),
|
||||
]),
|
||||
|
||||
("src/store/uiStore.ts", "Magazyn interfejsu użytkownika – stan wizualny aplikacji", [
|
||||
("5-14", "interface UiState",
|
||||
"Stan UI: sidebarOpen (czy boczne menu jest otwarte na mobile), setSidebarOpen/toggleSidebar (kontrola menu), selectedWarehouseClientId (ostatnio wybrany klient w module magazynowym – zapamiętywany w localStorage)."),
|
||||
("16-30", "export const useUiStore = create(...)(...)",
|
||||
"Magazyn z persist: selectedWarehouseClientId jest zapisywany w localStorage (użytkownik nie musi wybierać klienta przy każdej wizycie). Stan menu mobilnego NIE jest zapisywany (zawsze zamknięte po odświeżeniu)."),
|
||||
]),
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
("BIBLIOTEKA POMOCNICZA (LIB)", None, None),
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
("src/lib/cn.ts", "Łączenie klas CSS", [
|
||||
("2-4", "export function cn(...values)",
|
||||
"Funkcja łącząca klasy CSS Tailwind. Przyjmuje dowolną liczbę argumentów (string lub false/null/undefined), filtruje wartości fałszywe i łączy pozostałe spacjami. Przykład: cn('px-4', isActive && 'bg-blue-600', undefined) → 'px-4 bg-blue-600'."),
|
||||
]),
|
||||
|
||||
("src/lib/combinations.ts", "Algorytm znajdowania kombinacji sum (backtracking)", [
|
||||
("6-33", "export function findCombinations<T>(records, targetSum, getQuantity)",
|
||||
"Implementuje algorytm backtrackingu do znajdowania podzbiorów rekordów, których sumy ilości są równe zadanej wartości docelowej. Używany w skanerowaniu palet przy tworzeniu list pakowych. Port z oryginalnej funkcji Blazor FindCombinations. Parametry: records – lista elementów, targetSum – szukana suma, getQuantity – funkcja pobierająca ilość z elementu. Zwraca tablicę tablic – każda to zestaw elementów sumujący się do targetSum."),
|
||||
]),
|
||||
|
||||
("src/lib/constants.ts", "Stałe konfiguracyjne aplikacji", [
|
||||
("2-3", "API_BASE_URL",
|
||||
"Adres bazowy API .NET. Pobiera wartość ze zmiennej środowiskowej VITE_API_BASE_URL (z pliku .env) lub używa domyślnego http://localhost:5001."),
|
||||
("6", "AUTH_TOKEN_STORAGE_KEY = 'authToken'",
|
||||
"Klucz localStorage do przechowywania tokenu JWT. Zachowany z oryginalnej aplikacji Blazor (Blazored.LocalStorage)."),
|
||||
("9", "UI_STORAGE_KEY = 'fakrosno-ui'",
|
||||
"Klucz localStorage do przechowywania ustawień UI (wybrany klient magazynu)."),
|
||||
("15-16", "SYNCFUSION_LICENSE_KEY",
|
||||
"Klucz licencyjny do biblioteki Syncfusion (tabele, siatki). Skopiowany z oryginalnej aplikacji Blazor."),
|
||||
]),
|
||||
|
||||
("src/lib/format.ts", "Funkcje formatowania dat i liczb", [
|
||||
("8-12", "parseApiDate(value)",
|
||||
"Parsuje datę z formatu ISO 8601 (jak zwraca API .NET) do obiektu Date JavaScript. Zwraca null dla pustych wartości lub nieprawidłowych dat. Zawsze używaj tej funkcji zamiast new Date(str) dla dat z API."),
|
||||
("15-18", "formatDate(value, pattern?)",
|
||||
"Formatuje datę do wyświetlenia. Domyślny format: dd.MM.yyyy (np. 15.06.2024). Zwraca 'N/A' dla null/undefined."),
|
||||
("21-24", "formatDateTime(value)",
|
||||
"Formatuje datę z godziną: yyyy-MM-dd HH:mm:ss (np. 2024-06-15 14:30:00). Zwraca 'N/A' dla null."),
|
||||
("27-29", "toApiDate(value)", "Konwertuje obiekt Date do formatu ISO 8601 do wysłania do API."),
|
||||
("32-34", "toApiDateOnly(value)", "Konwertuje datę do formatu yyyy-MM-dd (tylko data, bez czasu)."),
|
||||
("40-43", "formatDecimal(value, fractionDigits?)",
|
||||
"Formatuje liczbę dziesiętną z zadaną precyzją (domyślnie 2 miejsca po przecinku). Używa biblioteki decimal.js, która eliminuje błędy zaokrąglania liczb zmiennoprzecinkowych. Zwraca 'N/A' dla null."),
|
||||
]),
|
||||
|
||||
("src/lib/queryKeys.ts", "Klucze pamięci podręcznej zapytań HTTP (TanStack Query)", [
|
||||
("2-21", "export const queryKeys = { ... }",
|
||||
"Scentralizowany słownik kluczy cache dla wszystkich typów zapytań. TanStack Query identyfikuje zapytania po kluczu – jeśli dwa miejsca w aplikacji używają tego samego klucza, dzielą jeden cache. Przykład: queryKeys.scheduleOrders → ['scheduleOrders'], queryKeys.scheduleOrder(5) → ['scheduleOrders', 5]. Pozwala łatwo unieważnić cache po operacjach zapisu (invalidateQueries)."),
|
||||
]),
|
||||
|
||||
("src/lib/status.ts", "Tłumaczenie kodów statusów zamówień", [
|
||||
("5-20", "export function translateStatus(status)",
|
||||
"Tłumaczy skrótowe kody statusów Syteline na czytelne opisy po polsku: O→Zamówione, S→Zatrzymane, P→Planowane, C→Zakończone, F→Wypełnione. Funkcja pomocnicza po stronie klienta (serwer zazwyczaj zwraca już przetłumaczony status w polu translatedStatus)."),
|
||||
]),
|
||||
|
||||
("src/lib/syncfusion.ts", "Inicjalizacja biblioteki tabel Syncfusion", [
|
||||
("4", "let registered = false",
|
||||
"Flaga zapobiegająca wielokrotnemu wywoływaniu inicjalizacji (singleton)."),
|
||||
("7-37", "export function initSyncfusion()",
|
||||
"Jednorazowo inicjalizuje Syncfusion: registerLicense – rejestruje klucz licencyjny; setCulture('pl') – ustawia język polski; L10n.load({ pl: {...} }) – ładuje polskie tłumaczenia interfejsu siatek: 'Brak rekordów do wyświetlenia', 'Zapisz', 'Anuluj', etykiety stron itp."),
|
||||
]),
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
("ROUTER – NAWIGACJA I OCHRONA TRAS", None, None),
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
("src/router/navConfig.ts", "Konfiguracja elementów menu bocznego", [
|
||||
("13-21", "export interface NavItem",
|
||||
"Opisuje element menu: label (widoczna etykieta), to (docelowy URL), icon (ikona z biblioteki Lucide), admin (czy tylko dla administratora), end (czy dopasowanie URL musi być dokładne)."),
|
||||
("23-32", "export const navItems: NavItem[]",
|
||||
"Lista wszystkich pozycji menu: Harmonogramy (strona główna /), Produkty, Magazyn – dla wszystkich użytkowników. Zamówienia klienta, Zamówienia EDI, Tłumaczenia, Użytkownicy, Harmonogram zadań – tylko dla administratorów (admin: true)."),
|
||||
]),
|
||||
|
||||
("src/router/NavigatorBridge.tsx", "Mostek łączący React Router z klientem API", [
|
||||
("6-12", "export function NavigatorBridge()",
|
||||
"Komponent renderujący pusty element (null – nic nie widać na ekranie). Jego jedynym zadaniem jest zarejestrowanie funkcji navigate z React Router w kliencie Axios. Dzięki temu, gdy Axios złapie błąd 401, może przekierować użytkownika na /login bez pełnego przeładowania strony. Montowany raz w App.tsx."),
|
||||
]),
|
||||
|
||||
("src/router/ProtectedRoute.tsx", "Ochrona tras przed nieautoryzowanym dostępem", [
|
||||
("5-15", "function getRoles()",
|
||||
"Wyciąga role użytkownika z zdekodowanego tokenu JWT. Obsługuje zarówno pojedynczą rolę (string) jak i tablicę ról (string[]). Szuka w standardowym polu Microsoft JWT lub w polu role/roles."),
|
||||
("17-24", "function isAdmin()",
|
||||
"Sprawdza czy użytkownik ma rolę 'admin' lub 'administrator'. Jeśli JWT nie zawiera żadnych ról (stary format API), zakłada że użytkownik jest adminem (dla wstecznej zgodności z Blazorem)."),
|
||||
("27-34", "export function ProtectedRoute()",
|
||||
"Komponent chroniący trasy: sprawdza isAuthenticated ze sklepu. Jeśli nie zalogowany – przekierowuje na /login, zapamiętując obecny adres URL (by po zalogowaniu wrócić). Jeśli zalogowany – renderuje Outlet (zawartość trasy)."),
|
||||
("37-47", "export function AdminRoute()",
|
||||
"Komponent chroniący trasy administracyjne: wymaga zarówno zalogowania jak i roli admina. Jeśli brak zalogowania – /login. Jeśli brak uprawnień – /unauthorized."),
|
||||
]),
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
("KOMPONENTY INTERFEJSU UŻYTKOWNIKA", None, None),
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
("src/components/ErrorBoundary.tsx", "Komponent chwytający błędy React (Error Boundary)", [
|
||||
("13-44", "export class ErrorBoundary extends Component<Props, State>",
|
||||
"Klasowy komponent React (jedyna forma obsługi błędów w React). getDerivedStateFromError – przechwytuje błąd i zapisuje go w stanie. componentDidCatch – loguje błąd do konsoli. render – jeśli wystąpił błąd: pokazuje 'Coś poszło nie tak' z przyciskami 'Odśwież stronę' i 'Spróbuj ponownie'. Jeśli nie ma błędu – renderuje children (normalna zawartość). Zamontowany na szczycie drzewa komponentów w main.tsx oraz wewnątrz AppShell dla każdej strony."),
|
||||
]),
|
||||
|
||||
("src/components/layout/AppShell.tsx", "Główny szablon aplikacji po zalogowaniu", [
|
||||
("8-24", "export function AppShell()",
|
||||
"Buduje układ strony: flex h-screen (zajmuje pełen ekran). Sidebar (boczne menu) + div z Header (nagłówek) i main (treść strony). Wewnątrz main: ErrorBoundary + Suspense (z animacją ładowania PageSkeleton) + Outlet (tu renderuje się aktualna strona)."),
|
||||
]),
|
||||
|
||||
("src/components/layout/Header.tsx", "Górny pasek nawigacyjny", [
|
||||
("7-45", "export function Header()",
|
||||
"Pasek nagłówka: po lewej – przycisk hamburger (tylko na mobile, toggle bocznego menu) + tytuł 'System zarządzania zamówieniami'. Po prawej – ikona użytkownika z jego nazwą + przycisk 'Wyloguj' (wywołuje logout() i przekierowuje na /login)."),
|
||||
]),
|
||||
|
||||
("src/components/layout/PageHeader.tsx", "Nagłówek strony z tytułem i akcjami", [
|
||||
("3-21", "export function PageHeader({ title, description, actions })",
|
||||
"Prosty komponent nagłówka strony: tytuł (h1), opcjonalny opis (p), opcjonalne przyciski akcji (np. 'Powrót', 'Nowy'). Responsywny: na mobile wierszowo, na desktop kolumnowo z przyciskami po prawej."),
|
||||
]),
|
||||
|
||||
("src/components/layout/Sidebar.tsx", "Boczny panel nawigacyjny", [
|
||||
("6-82", "export function Sidebar()",
|
||||
"Sidebar wyświetla listę pozycji menu z navConfig.ts. Dzieli je na dwie sekcje: 'Główne' i 'Administracja'. Każda pozycja to NavLink – automatycznie dodaje klasę bg-blue-600 gdy URL pasuje (aktywna strona). Na desktop: stały panel 256px. Na mobile: wysuwana szuflada (fixed, full-height) z ciemnym tłem – kliknięcie tła zamyka menu."),
|
||||
]),
|
||||
|
||||
("src/components/ui/Badge.tsx", "Komponent etykietki/plakietki", [
|
||||
("14-25", "export function Badge({ tone, children })",
|
||||
"Wyświetla małą zaokrągloną plakietkę z tekstem. Pięć kolorów (tone): gray, blue, green, red, amber. Przykład użycia: <Badge tone='blue'>5</Badge> – niebieska plakietka z liczbą 5 (używana przy liczbie pozycji harmonogramu)."),
|
||||
]),
|
||||
|
||||
("src/components/ui/Button.tsx", "Komponent przycisku", [
|
||||
("8-12", "export interface ButtonProps",
|
||||
"Rozszerzenie standardowych właściwości przycisku HTML o: variant (styl), size (rozmiar), loading (stan ładowania – pokazuje kółko obrotu)."),
|
||||
("14-20", "variantClasses, sizeClasses",
|
||||
"Słowniki klas CSS dla każdego wariantu i rozmiaru. Warianty: primary (niebieski), secondary (biały z ramką), danger (czerwony), success (zielony), ghost (przezroczysty)."),
|
||||
("28-50", "export const Button = forwardRef(...)",
|
||||
"Komponent przycisku z forwardRef (można przekazać ref do DOM). Gdy loading=true: wyłącza kliknięcie i pokazuje obracającą się ikonę Loader2. Łączy klasy CSS na podstawie wariantu i rozmiaru."),
|
||||
]),
|
||||
|
||||
("src/components/ui/Card.tsx", "Komponenty karty/panelu", [
|
||||
("4-30", "Card, CardHeader, CardTitle, CardContent, CardFooter",
|
||||
"Zestaw komponentów tworzących kartę: Card – kontener z zaokrąglonymi rogami i cieniem. CardHeader – nagłówek z dolną linią. CardTitle – niebieski tytuł h2. CardContent – obszar treści z paddingiem. CardFooter – stopka z górną linią (np. '© 2024 FA Krosno')."),
|
||||
]),
|
||||
|
||||
("src/components/ui/EmptyState.tsx", "Komponent pustego stanu", [
|
||||
("4-25", "export function EmptyState({ icon, title, message, action })",
|
||||
"Wyświetlany gdy lista danych jest pusta lub wystąpił błąd ładowania. Pokazuje ikonę (domyślnie skrzynkę Inbox), tytuł i opcjonalny opis. Przykład: 'Brak harmonogramów', 'Nie ma jeszcze żadnych zamówień'."),
|
||||
]),
|
||||
|
||||
("src/components/ui/Input.tsx", "Komponent pola tekstowego formularza", [
|
||||
("9-43", "export const Input = forwardRef(...)",
|
||||
"Pole tekstowe z automatycznym ID (useId), etykietą i obsługą błędu walidacji. Gdy error jest podany: czerwona ramka + komunikat błędu + aria-describedby (dostępność). Używany w formularzach logowania, rejestracji itp."),
|
||||
]),
|
||||
|
||||
("src/components/ui/Modal.tsx", "Komponent okna modalnego (dialogu)", [
|
||||
("17-93", "export function Modal({ open, onClose, title, children, footer })",
|
||||
"Okno modalne z pełną obsługą dostępności: pułapka focusu (Tab/Shift+Tab porusza tylko wewnątrz modala), Escape zamyka, kliknięcie tła zamyka. Po zamknięciu focus wraca do poprzedniego elementu. Renderowany przez createPortal (poza normalnym drzewem DOM). Zawiera: nagłówek z tytułem i X, treść (children), stopkę z przyciskami."),
|
||||
]),
|
||||
|
||||
("src/components/ui/Skeleton.tsx", "Komponenty szkieletowego ładowania (loading state)", [
|
||||
("3-10", "Skeleton", "Animowany szary prostokąt (pulsowanie) – placeholder dla ładujących się treści."),
|
||||
("13-23", "GridSkeleton({ rows })", "Skeleton tabeli: nagłówek + N wierszy. Wyświetlany podczas ładowania danych do siatek."),
|
||||
("26-34", "PageSkeleton()", "Skeleton całej strony: nagłówek + duży blok treści. Używany jako fallback Suspense w AppShell."),
|
||||
("38-49", "FullPageSpinner()", "Kółko obrotu na pełnym ekranie – używany gdy lazy-loaded strony się wczytują (przed AppShell)."),
|
||||
]),
|
||||
|
||||
("src/components/ui/Toaster.tsx", "Komponent wyświetlający powiadomienia", [
|
||||
("5-10", "const config",
|
||||
"Słownik konfiguracji dla każdego wariantu powiadomienia: ikona i kolory tła/tekstu."),
|
||||
("12-51", "export function Toaster()",
|
||||
"Kontener powiadomień: stały, w prawym dolnym rogu (fixed bottom-4 right-4). Nasłuchuje na zmiany w toastStore. Każde powiadomienie: ikona + tytuł + opis + przycisk X. Znika automatycznie po 5 sekundach lub po kliknięciu X."),
|
||||
]),
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
("TYPY DANYCH API", None, None),
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
("src/types/api/common.ts", "Wspólne typy opakowań odpowiedzi API", [
|
||||
("7-12", "interface PagedResult<T>",
|
||||
"Wynik stronicowania: items (lista elementów), totalCount (łączna liczba), pageNumber, pageSize."),
|
||||
("14-19", "interface ApiResponse<T>",
|
||||
"Ogólna odpowiedź API: data (dane lub null), success (bool), message (komunikat), errors (błędy walidacji)."),
|
||||
("21-24", "interface ValidationError", "Błąd walidacji: field (pole formularza) i message (treść błędu)."),
|
||||
("26", "type SortOrder = 'asc' | 'desc'", "Typ kierunku sortowania."),
|
||||
("29", "type OrderStatusCode",
|
||||
"Typ kodów statusów zamówień Syteline: O, S, P, C, F lub dowolny inny string."),
|
||||
]),
|
||||
|
||||
("src/types/api/faKrosno.ts", "Typy DTO z modelu danych FaKrosno (EF)", [
|
||||
("8-12", "PurchaserDto", "Nabywca: id, purchaserCode (kod nabywcy), purchaserDesc (opis)."),
|
||||
("15-21", "RecipientDto", "Odbiorca: id, purchaserID (powiązany nabywca), recipientCode, recipientDesc. Zawiera zagnieżdżony obiekt PurchaserDto."),
|
||||
("24-31", "ProductDto", "Produkt: id, recipientID, recipientIdx (indeks odbiorcy), faIdx (indeks FA), recipientName. Zawiera RecipientDto."),
|
||||
("34-42", "ScheduleOrderMiscDto", "Dodatkowe dane harmonogramu: type, value, label, display (czy wyświetlać). Pole text = '{Label}: {Value}' generowane przez serwer."),
|
||||
("44-64", "ScheduleOrderDetailMiscDto, ScheduleOrderDetailDetailMiscDto",
|
||||
"Analogiczne dodatkowe dane dla pozycji i szczegółów pozycji harmonogramu."),
|
||||
("67-81", "ScheduleOrderDetailDetailDto", "Szczegół pozycji harmonogramu DELFOR: ilość (qty), zakres dat (dateFrom–dateTo), typ harmonogramu (sccType), status, data wysyłki."),
|
||||
("87-99", "ScheduleOrderDetailDto", "Pozycja harmonogramu: kody produktów (SC/SH), numer zamówienia, odbiorca/nabywca. Zawiera tablicę ScheduleOrderDetailDetailDto."),
|
||||
("105-120", "ScheduleOrderDto", "Pełny harmonogram DELFOR: numer PO, identyfikator zamówienia, odbiorca, data aktualizacji, typ dokumentu. Zawiera pozycje i dane miscellaneous."),
|
||||
]),
|
||||
|
||||
("src/types/api/syteline.ts", "Typy DTO z modelu danych Syteline (ERP)", [
|
||||
("9-39", "CustomerOrderLineItemDto",
|
||||
"Pozycja-zwolnienie zamówienia klienta z Syteline: numery CO/linia/zwolnienie, pozycja, ilość, cena, koszt, kody EDI, daty (due/release/promise)."),
|
||||
("42-64", "CustomerOrderLineDto",
|
||||
"Linia zamówienia: pozycja, łączna ilość (blanketQty), ceny, daty, kody EDI dla adresu/typu pudełka/przeznaczenia. Zawiera tablicę CustomerOrderLineItemDto."),
|
||||
("67-94", "CustomerOrderDto",
|
||||
"Pełne zamówienie klienta: numer CO, klient, kontakt, data, status, magazyn, kody EDI (gate/recipient/seller/sender/buyer). Zawiera linie i tłumaczenia EDI."),
|
||||
("97-124", "EdiCustomerOrderLineItemDto",
|
||||
"Pozycja-zwolnienie zamówienia EDI: podobna do CustomerOrderLineItemDto ale z polami EDI-specific (routingCode, deliveryCallNumber, unloadingPoint, palletCode, palletNumber)."),
|
||||
("127-146", "EdiCustomerOrderLineDto", "Linia zamówienia EDI: pozycja, ilość, cena, typ pudełka, adres, przeznaczenie."),
|
||||
("149-177", "EdiCustomerOrderDto",
|
||||
"Pełne zamówienie EDI: numer, klient, daty, status, kody EDI, slOrderNumber (numer w Syteline po wysłaniu), sentToSl ('TAK'/'NIE')."),
|
||||
("183-197", "EdiCustomerOrderTranslateDto",
|
||||
"Powiązanie (tłumaczenie) zamówienia EDI z zamówieniem Syteline i harmonogramem DELFOR: coEdiOrder, coRowPointer, coCoNum (nr Syteline), ediCoCoNum (nr EDI)."),
|
||||
("200-212", "ErrorLogDto", "Log błędu z Syteline: numer transakcji, numer błędu, komunikat, daty."),
|
||||
("215-227", "WzRowMeyleDto", "Wiersz listy pakowej Meyle: ID, nagłówek, numer zamówienia, pozycja, ilość, numer palety, nr WZ, numer części."),
|
||||
("230-242", "WzRowMarelliDto", "Wiersz listy pakowej Marelli: ID, nagłówek, typ, pozycja, numer inżyniera, ilość, numer zamówienia."),
|
||||
("245-253", "WzHeaderDto", "Nagłówek dokumentu WZ: ID, klient, data, adresy email, numery WZ. Zawiera wiersze Meyle i Marelli."),
|
||||
("256-264", "WzClientDto", "Klient magazynowy: ID, numer klienta, sekwencja, nazwa, skrócona nazwa, logo Base64."),
|
||||
("267-280", "MaterialTransactionDto", "Transakcja materiałowa z Syteline: grupa, numer transakcji, pozycja, data, ilość, magazyn, numer zamówienia, numer karty kontrolnej."),
|
||||
("283-292", "ItemCustDto", "Powiązanie pozycja–klient: numer pozycji FA, numer klienta, numer pozycji klienta (custItem), kod EAN13."),
|
||||
]),
|
||||
|
||||
("src/types/api/ordersManagement.ts", "Typy DTO z modelu zarządzania zamówieniami", [
|
||||
("8-12", "RoleDto", "Rola użytkownika: id, name (nazwa), rowPointer (GUID identyfikator)."),
|
||||
("15-20", "FunctionDto", "Funkcja systemowa (uprawnienie): id, roleId (do jakiej roli należy), name, rowPointer."),
|
||||
("26-30", "UserRoleDto", "Powiązanie użytkownik–rola: userId, roleId, rowPointer."),
|
||||
("37-55", "UserDto",
|
||||
"Pełny obiekt użytkownika systemu: login, passwordHash (UWAGA BEZPIECZEŃSTWA: serwer nie powinien zwracać hasha hasła), isTemporaryPassword, isActive, daty aktywności, email, imię, nazwisko, data utworzenia, liczba nieudanych logowań, blokada konta, lista ról."),
|
||||
("61-67", "TaskSchedulerDetailDto", "Wpis historii uruchomień zadania: data uruchomienia (jobRunDate), log (treść logu)."),
|
||||
("70-82", "TaskSchedulerDto", "Zaplanowane zadanie automatyczne: nazwa, ścieżka, wyrażenie cron (cronOptions), daty aktywności, ostatnie i następne uruchomienie, historia uruchomień."),
|
||||
]),
|
||||
|
||||
("src/types/api/local.ts", "Lokalne typy modeli odpowiedzi API", [
|
||||
("7-10", "LoginResponseDto", "Odpowiedź na żądanie logowania: token (JWT lub null) i expires (data wygaśnięcia)."),
|
||||
("13-18", "ResponseModel", "Ogólny model odpowiedzi: status (1=sukces, 0=błąd), identifier (numer zamówienia), message (wiadomość błędu), externalIdentifier (zewnętrzny numer)."),
|
||||
("24-28", "TransactionModel", "Model wartościowy dla skanowania kart kontrolnych: numer części, numer pozycji, ilość."),
|
||||
]),
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
("FUNKCJONALNOŚCI (FEATURES) – STRONY APLIKACJI", None, None),
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
("src/features/auth/schemas/login.schema.ts", "Schemat walidacji formularza logowania", [
|
||||
("3-6", "loginSchema = z.object(...)",
|
||||
"Schemat Zod walidujący dane logowania: login (wymagany, min 1 znak), password (wymagany, min 1 znak). Biblioteka Zod automatycznie generuje błędy walidacji po polsku."),
|
||||
("8", "type LoginFormValues = z.infer<typeof loginSchema>",
|
||||
"TypeScript automatycznie wyprowadza typ z schematu Zod – nie trzeba pisać interfejsu osobno."),
|
||||
]),
|
||||
|
||||
("src/features/auth/schemas/register.schema.ts", "Schemat walidacji formularza rejestracji", [
|
||||
("3-15", "registerSchema = z.object(...).refine(...)",
|
||||
"Schemat rejestracji: login (1-50 znaków), email (prawidłowy adres, 1-100 znaków), firstName/lastName (1-50 znaków), password (6-100 znaków), confirmPassword. Warunek dodatkowy (refine): password musi być identyczne z confirmPassword – jeśli nie, błąd 'Hasła nie są zgodne' pojawia się przy polu confirmPassword."),
|
||||
]),
|
||||
|
||||
("src/features/auth/schemas/changePassword.schema.ts", "Schemat walidacji zmiany hasła", [
|
||||
("3-11", "changePasswordSchema = z.object(...).refine(...)",
|
||||
"Schemat zmiany tymczasowego hasła: newPassword (6-100 znaków) i confirmPassword z weryfikacją zgodności."),
|
||||
]),
|
||||
|
||||
("src/features/auth/hooks/useLogin.ts", "Hook logiki logowania", [
|
||||
("25-73", "export function useLogin(onAuthenticated)",
|
||||
"Niestandardowy hook React zarządzający procesem logowania. Używa useMutation z TanStack Query. Kroki: 1) Wywołuje loginRequest (POST /api/Users/login). 2) Jeśli sukces – zapisuje token JWT w authStore (setToken). 3) Pobiera dane użytkownika (getUserByUsername) by sprawdzić czy ma tymczasowe hasło. 4) Jeśli tymczasowe – ustawia requiresPasswordChange=true (pojawia się formularz zmiany hasła). 5) Jeśli normalne – wywołuje onAuthenticated() (przekierowanie na właściwą stronę). Mapuje błędy API na czytelne komunikaty po polsku."),
|
||||
]),
|
||||
|
||||
("src/features/auth/hooks/useRegister.ts", "Hook logiki rejestracji", [
|
||||
("11-26", "export function useRegister(onSuccess)",
|
||||
"Hook obsługujący rejestrację konta. Wysyła dane do POST /api/Users/register. Po sukcesie: pokazuje toast 'Konto utworzone, możesz się zalogować' i wywołuje onSuccess() (przekierowanie na /login)."),
|
||||
]),
|
||||
|
||||
("src/features/auth/hooks/useChangeTemporaryPassword.ts", "Hook zmiany tymczasowego hasła", [
|
||||
("19-45", "export function useChangeTemporaryPassword(onDone)",
|
||||
"Hook obsługujący pierwszą zmianę hasła po zalogowaniu z hasłem tymczasowym. Wysyła nowe hasło do POST /api/Users/change-password (serwer identyfikuje użytkownika z tokenu JWT). Po sukcesie: pokazuje toast 'Hasło zmienione', wylogowuje użytkownika (logout()), wywołuje onDone() (powrót do /login)."),
|
||||
]),
|
||||
|
||||
("src/features/auth/components/LoginPage.tsx", "Strona logowania", [
|
||||
("22-101", "export default function LoginPage()",
|
||||
"Strona logowania. Używa react-hook-form z zodResolver (walidacja przez loginSchema). Sprawdza czy użytkownik jest już zalogowany (wtedy przekierowuje). Jeśli requiresPasswordChange – pokazuje ChangePasswordView zamiast formularza logowania. Formularz: pola Login i Hasło, przycisk 'Zaloguj się' z animacją ładowania, komunikat błędu (np. 'Nieprawidłowy login lub hasło'), link do rejestracji."),
|
||||
("105-167", "function ChangePasswordView()",
|
||||
"Ekran wyświetlany przy pierwszym logowaniu gdy hasło jest tymczasowe. Formularz: pola 'Nowe hasło' i 'Potwierdź hasło', przycisk 'Zmień hasło'. Po pomyślnej zmianie – wylogowanie i powrót do /login."),
|
||||
]),
|
||||
|
||||
("src/features/auth/components/RegisterPage.tsx", "Strona rejestracji", [
|
||||
("12-94", "export default function RegisterPage()",
|
||||
"Strona rejestracji nowego konta. Formularz z polami: Login, Email, Imię, Nazwisko, Hasło, Powtórz hasło. Walidacja przez registerSchema (Zod). Po pomyślnej rejestracji: przekierowanie na /login. Wyświetla błędy z serwera (np. 'Login jest zajęty')."),
|
||||
]),
|
||||
|
||||
("src/features/auth/components/UnauthorizedPage.tsx", "Strona braku dostępu (403)", [
|
||||
("4-17", "export default function UnauthorizedPage()",
|
||||
"Prosta strona wyświetlana gdy użytkownik nie ma uprawnień do zasobu (403). Ikona tarczy z wykrzyknikiem, tekst 'Brak dostępu' i link powrotu do strony głównej."),
|
||||
]),
|
||||
|
||||
("src/features/auth/components/NotFoundPage.tsx", "Strona 404", [
|
||||
("4-15", "export default function NotFoundPage()",
|
||||
"Strona 404 wyświetlana gdy adres URL nie istnieje. Ikona kompasu, tekst '404 – Nie znaleziono' i link powrotu do strony głównej."),
|
||||
]),
|
||||
|
||||
("src/features/schedule-orders/ScheduleOrdersPage.tsx", "Strona listy harmonogramów DELFOR", [
|
||||
("12", "const FILTER_STORAGE_KEY = 'scheduleOrdersGridFilter'",
|
||||
"Klucz localStorage do zapisywania filtrów tabeli – użytkownik nie traci ustawionych filtrów po odświeżeniu."),
|
||||
("13-20", "const query = useQuery(...)",
|
||||
"Pobiera listę harmonogramów z API. select – sortuje od najnowszych (malejąco wg lastUpdateDate)."),
|
||||
("22-48", "return (...)",
|
||||
"Warunkowe renderowanie: ładowanie → GridSkeleton (animacja), błąd → EmptyState 'Błąd ładowania', brak danych → EmptyState 'Brak harmonogramów', dane → Card z ScheduleOrdersTreeGrid."),
|
||||
]),
|
||||
|
||||
("src/features/schedule-orders/ScheduleOrdersTreeGrid.tsx", "Trzypoziomowa tabela harmonogramów DELFOR", [
|
||||
("73-115", "function DetailDetailGrid(detail)",
|
||||
"Poziom 3 – tabela szczegółów-szczegółów harmonogramu. Kolumny: daty Od/Do, ilość, typ qty, opis, status. Wiersze z typem qty w liście HIGHLIGHT_QTY_TYPES (54, 83, 84) są podświetlone na czerwono. Dwuklik nawiguje do szczegółów harmonogramu."),
|
||||
("122-179", "function ScheduleOrderDetailsGrid(order)",
|
||||
"Poziom 2 – tabela pozycji harmonogramu. Leniwie ładuje pełne dane harmonogramu (useQuery) dopiero gdy użytkownik rozwinie wiersz. Kopiuje dane nagłówkowe (numer PO, odbiorca) na każdą pozycję (zachowanie z Blazora). Zagnieżdża DetailDetailGrid."),
|
||||
("197-337", "export function ScheduleOrdersTreeGrid({ data, pageSize, enableFilterPersistence, filterStorageKey })",
|
||||
"Główna trzypoziomowa tabela: Poziom 1 (główna lista) z funkcjami: stronicowanie, sortowanie, filtrowanie Excel, eksport do XLS, dwuklik → strona szczegółów. Opcjonalne: przyciski 'Zapisz filtry' / 'Usuń filtry' zapisujące stan filtrów w localStorage. getMasterWidget() – dostęp do instancji widgetu EJ2 przez DOM (bo React ref zwraca tylko props, nie stan runtime). readLiveFilterColumns() – serializuje aktywne filtry (z wykluczeniem cyklicznych referencji). handleDataBound() – przywraca zapisane filtry po załadowaniu danych."),
|
||||
]),
|
||||
|
||||
("src/features/schedule-orders/ScheduleOrderDetailPage.tsx", "Strona szczegółów harmonogramu", [
|
||||
("27-60", "function DetailDetailGrid(props)",
|
||||
"Tabela szczegółów pozycji: ilość, daty Od/Do, typ SCC, typ ilości, status, data wysyłki."),
|
||||
("62-139", "export default function ScheduleOrderDetailPage()",
|
||||
"Strona szczegółów harmonogramu DELFOR: odczytuje ID z parametru URL, pobiera dane (getScheduleOrder). Wyświetla: karta informacyjna (kod odbiorcy, odbiorca, nabywca, data aktualizacji) + tabela pozycji z możliwością rozwinięcia do szczegółów-szczegółów."),
|
||||
]),
|
||||
|
||||
("src/features/products/ProductsPage.tsx", "Strona zarządzania produktami", [
|
||||
("30-35", "editSettings, toolbar, pageSettings",
|
||||
"Konfiguracja tabeli Syncfusion: tryb edycji wiersz-po-wierszu (Normal), pasek narzędzi z przyciskami Edytuj/Aktualizuj/Anuluj/Szukaj, 10 wierszy na stronę."),
|
||||
("39", "const DEFAULT_INDEX = 'Uzupelnij'",
|
||||
"Domyślny indeks wyszukiwania: 'Uzupelnij' – pokazuje produkty czekające na przypisanie kodu FA (zachowanie z Blazora)."),
|
||||
("41-58", "const query = useQuery(...), const mutation = useMutation(...)",
|
||||
"Pobieranie produktów po indeksie (searchTerm). mutation – zapisuje zaktualizowany produkt na serwerze i odświeża cache."),
|
||||
("60-128", "return (...)",
|
||||
"Formularz wyszukiwania: pole tekstowe + przycisk 'Szukaj'. Tabela z edycją inline: ID (tylko do odczytu), Odbiorca (tylko do odczytu), Indeks odbiorcy (tylko do odczytu), Kod FA (edytowalny). Po kliknięciu 'Aktualizuj' w tabeli – mutation.mutate() wysyła zmianę do API."),
|
||||
]),
|
||||
|
||||
("src/features/warehouse/WarehousePage.tsx", "Strona modułu magazynowego", [
|
||||
("51-57", "function clientFlavor(client)",
|
||||
"Funkcja rozpoznająca typ klienta ('meyle' lub 'marelli') na podstawie nazwy. Używana do wyboru odpowiedniego formatu listy pakowej i ścieżki URL."),
|
||||
("70-131", "Zapytania i dane",
|
||||
"clientsQuery – lista klientów magazynowych (filtrowana do MAGNETI MARELLI i MEYLE). wzDocsQuery – transakcje materiałowe (dokumenty WZ) dla wybranego klienta. headersQuery – istniejące listy pakowe klienta. wzDocuments – zdeduplikowana lista WZ (jedna pozycja na grupę, zachowanie z Blazora GroupBy)."),
|
||||
("132-221", "const create = useMutation(...)",
|
||||
"Główna logika tworzenia listy pakowej: 1) Sprawdza czy zaznaczono wiersze w tabeli. 2) Sprawdza czy nie istnieje już lista dla tych numerów WZ. 3) Tworzy nagłówek WzHeader z losowym UUID. 4) Dla każdej zaznaczonej transakcji: pobiera zamówienie klienta i dane pozycji. 5) Tworzy wiersze Meyle lub Marelli (zależnie od klienta). 6) Przekierowuje na stronę listy pakowej."),
|
||||
("228-361", "return (...)",
|
||||
"Interfejs strony: dropdown wyboru klienta → tabela WZ z checkboxami (wielokrotny wybór) → przycisk 'Utwórz Packing List' → tabela istniejących list pakowych z przyciskiem 'Otwórz'. Okna modalne dla błędów: 'Zaznacz przynajmniej jeden rekord' i 'Dla zaznaczonego rekordu istnieje już Packing List'."),
|
||||
]),
|
||||
|
||||
("src/features/warehouse/packing-lists/PackListShell.tsx", "Wspólny szablon strony listy pakowej", [
|
||||
("23-102", "export function PackListShell({ title, header, toggleTo, toggleLabel, onGenerateXls, children })",
|
||||
"Szablon używany przez wszystkie cztery strony list pakowych (Meyle pełna/uproszczona, Marelli pełna/uproszczona). Zawiera: nagłówek strony z numerami WZ, link przełączający widok pełny/uproszczony, przycisk 'Generuj XLS' (wywołuje onGenerateXls), pole adresów e-mail z zapisywaniem (addEmailsToWzHeader), slot children (tu renderuje się właściwa tabela). Inicjalizuje pole email z danych nagłówka (render-phase reset pattern)."),
|
||||
]),
|
||||
|
||||
("src/features/warehouse/packing-lists/MeylePackListPage.tsx", "Lista pakowa Meyle – widok pełny (edytowalny)", [
|
||||
("40-121", "export default function MeylePackListPage()",
|
||||
"Edytowalna lista pakowa dla klienta Meyle. Pobiera nagłówek WZ (headerQuery) i wiersze (rowsQuery). Inicjalizuje lokalny stan rows z danych serwera. save – zapisuje wszystkie wiersze: PUT jeśli już istniały (aktualizacja), POST jeśli nowe (tworzenie). Tabela z pełną edycją: nr palety, nr zamówienia, nr pozycji, indeks FA, nr części SL, ilość, nr WZ."),
|
||||
]),
|
||||
|
||||
("src/features/warehouse/packing-lists/MeylePackListSimplePage.tsx", "Lista pakowa Meyle – widok uproszczony (tylko odczyt)", [
|
||||
("21-63", "export default function MeylePackListSimplePage()",
|
||||
"Uproszczony podgląd listy pakowej Meyle (tylko do odczytu). Pokazuje tylko: nr palety, nr pozycji, indeks FA, ilość. Używa tego samego PackListShell (z przyciskiem 'Generuj XLS' i adresami email)."),
|
||||
]),
|
||||
|
||||
("src/features/warehouse/packing-lists/MarelliPackListPage.tsx", "Lista pakowa Marelli – widok pełny (edytowalny)", [
|
||||
("40-116", "export default function MarelliPackListPage()",
|
||||
"Edytowalna lista pakowa dla klienta Marelli. Analogiczna do MeylePackListPage, ale z kolumnami: typ, nr inżyniera zamiast nr części SL. Zapisuje przez PUT (update) lub POST (create)."),
|
||||
]),
|
||||
|
||||
("src/features/warehouse/packing-lists/MarelliPackListSimplePage.tsx", "Lista pakowa Marelli – widok uproszczony", [
|
||||
("21-63", "export default function MarelliPackListSimplePage()",
|
||||
"Uproszczony podgląd listy pakowej Marelli (tylko odczyt): nr palety, nr pozycji, indeks FA, ilość."),
|
||||
]),
|
||||
|
||||
("src/features/customer-orders/CustomerOrdersPage.tsx", "Strona listy zamówień klientów (Syteline)", [
|
||||
("33-40", "function Field({ label, value })",
|
||||
"Mały pomocniczy komponent: wyświetla etykietę z podkreśleniem i wartość pogrubioną. Odwzorowanie Blazor <u>label:</u> <b>value</b>."),
|
||||
("43-80", "export function CustomerOrderSummary(order)",
|
||||
"Rozwijany szczegół zamówienia (detail template tabeli): dwa kolumny informacji o zamówieniu: numer CO, numer PO klienta, klient, odbiorca, kontakt, data, warunki, wartość, status, magazyn, kody EDI."),
|
||||
("82-139", "export default function CustomerOrdersPage()",
|
||||
"Lista zamówień klientów posortowana od najnowszych. Tabela: numer zamówienia, nr klienta, odbiorca, data, status. Dwuklik → strona szczegółów zamówienia. Rozwinięcie wiersza → CustomerOrderSummary."),
|
||||
]),
|
||||
|
||||
("src/features/customer-orders/CustomerOrderDetailPage.tsx", "Strona szczegółów zamówienia klienta", [
|
||||
("49-78", "function CustomerOrderLineDetail(line)",
|
||||
"Szczegóły linii zamówienia: numer CO, linia, pozycja, opis, ilość, cena, daty, kody EDI pudełka."),
|
||||
("81-114", "function CustomerOrderLineItemDetail(item)",
|
||||
"Szczegóły zwolnienia (line item): numer CO, linia, zwolnienie, pozycja, ilość, cena, daty, magazyn, kody EDI."),
|
||||
("116-295", "export default function CustomerOrderDetailPage()",
|
||||
"Strona szczegółów: podsumowanie zamówienia (CustomerOrderSummary) + przycisk 'Pokaż powiązane DELFOR' (lazy-load harmonogramów powiązanych przez EDI translates) + tabela linii zamówienia (z rozwinięciem do szczegółów) + tabela harmonogramów (po kliknięciu linii, z rozwinięciem do szczegółów harmonogramów). Powiązane harmonogramy filtrowane po ID z ediCustomerOrderTranslates."),
|
||||
]),
|
||||
|
||||
("src/features/edi-customer-orders/EdiCustomerOrdersPage.tsx", "Strona listy zamówień EDI", [
|
||||
("52-87", "export function EdiOrderSummary(order)",
|
||||
"Rozwijany panel szczegółów zamówienia EDI: dwie kolumny z numerami, datami, statusem, kodami EDI (gate, recipient, sender, seller, buyer)."),
|
||||
("89-243", "export default function EdiCustomerOrdersPage()",
|
||||
"Lista zamówień EDI z filtrowaniem (domyślnie tylko niezaksięgowane, poster=0). Checkbox 'Pokaż wszystkie' przełącza widok. Wielokrotny wybór wierszy (checkboxy). Przycisk 'Księguj zaznaczone' – uruchamia handleSend. handleSend: sekwencyjnie wysyła każde zaznaczone zamówienie do Syteline, zbiera wyniki, wyświetla okno modalne z sukcesami (zielone) i błędami (czerwone)."),
|
||||
]),
|
||||
|
||||
("src/features/edi-customer-orders/EdiCustomerOrderDetailPage.tsx", "Strona szczegółów zamówienia EDI", [
|
||||
("45-71", "function EdiLineDetail(line)",
|
||||
"Szczegóły linii zamówienia EDI: numer, pozycja, opis, ilość, cena, BoxType, adres, przeznaczenie."),
|
||||
("73-107", "function EdiLineItemDetail(item)",
|
||||
"Szczegóły zwolnienia EDI: wszystkie pola pozycji + kody EDI (routing, delivery, unloading, destination, pallet)."),
|
||||
("109-237", "export default function EdiCustomerOrderDetailPage()",
|
||||
"Strona szczegółów zamówienia EDI: podsumowanie (EdiOrderSummary) + tabela linii (z rozwinięciem) + tabela harmonogramów po wybraniu linii."),
|
||||
]),
|
||||
|
||||
("src/features/translations/TranslationsPage.tsx", "Strona zarządzania powiązaniami zamówień EDI z DELFORami", [
|
||||
("40-135", "export default function TranslationsPage()",
|
||||
"Tabela powiązań zamówień EDI z Syteline i DELFORami: DELFOR Id, numer EDI, numer SL, nr PO, liczba zamówień, czy znaleziono, data. Każdy wiersz ma przycisk 'Usuń'. Kliknięcie 'Usuń' → modal potwierdzenia → deleteEdiTranslation → odświeżenie listy."),
|
||||
]),
|
||||
|
||||
("src/features/users/UsersManagerPage.tsx", "Strona zarządzania użytkownikami (zakładki)", [
|
||||
("57-93", "export default function UsersManagerPage()",
|
||||
"Strona z trzema zakładkami (tab navigation): Użytkownicy, Role, Funkcje. Przełącza między komponentami UsersGrid, RolesGrid, FunctionsGrid."),
|
||||
("96-210", "function UsersGrid()",
|
||||
"Tabela użytkowników z pełną edycją CRUD: kolumny: ID, Login, Email, Imię, Nazwisko, Aktywny, Data utworzenia + przycisk 'Zresetuj hasło'. Nowy wiersz bez ID → adminCreateUser (generuje tymczasowe hasło). Istniejący wiersz → updateUser. Usunięcie → deleteUser. 'Zresetuj hasło' → adminResetPassword. Po create/reset → modal z tymczasowym hasłem."),
|
||||
("212-268", "function RolesGrid()",
|
||||
"Tabela ról: ID, Nazwa roli. Pełna edycja: add/edit/delete z rowPointer (UUID)."),
|
||||
("270-330", "function FunctionsGrid()",
|
||||
"Tabela funkcji systemowych: ID, ID roli, Nazwa funkcji. Pełna edycja z przypisaniem do roli (roleId)."),
|
||||
]),
|
||||
|
||||
("src/features/scheduler/SchedulerPage.tsx", "Strona harmonogramu zadań automatycznych (Hangfire)", [
|
||||
("38-59", "function RunLogGrid(props)",
|
||||
"Tabela historii uruchomień zadania: data uruchomienia i treść logu. Renderowana jako detail template."),
|
||||
("66-149", "export default function SchedulerPage()",
|
||||
"Tabela zaplanowanych zadań automatycznych (Hangfire). Tryb edycji Dialog (okno modalne). Kolumny: ID, Nazwa, Ścieżka, Cron (wyrażenie harmonogramu), Ostatnie uruchomienie, Następne uruchomienie. Rozwinięcie wiersza → historia uruchomień. Add/Edit/Delete wysyłają do API HangfireJobs."),
|
||||
]),
|
||||
]
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Klasa generatora PDF
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class DocPDF(FPDF):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.add_font("Arial", "", FONT_PATH)
|
||||
self.add_font("Arial", "B", FONT_PATH)
|
||||
self.add_font("Arial", "I", FONT_PATH)
|
||||
self.set_auto_page_break(auto=True, margin=15)
|
||||
|
||||
def header(self):
|
||||
if self.page_no() == 1:
|
||||
return
|
||||
self.set_font("Arial", "I", 8)
|
||||
self.set_text_color(150, 150, 150)
|
||||
self.cell(0, 8, "Dokumentacja projektu FaKrosno Management", align="L")
|
||||
self.cell(0, 8, f"Strona {self.page_no()}", align="R", new_x="LMARGIN", new_y="NEXT")
|
||||
self.set_text_color(0, 0, 0)
|
||||
self.ln(2)
|
||||
|
||||
def footer(self):
|
||||
self.set_y(-13)
|
||||
self.set_font("Arial", "I", 8)
|
||||
self.set_text_color(150, 150, 150)
|
||||
self.cell(0, 8, f"FA Krosno Management | Wygenerowano automatycznie | s. {self.page_no()}", align="C")
|
||||
self.set_text_color(0, 0, 0)
|
||||
|
||||
|
||||
def add_cover(pdf: DocPDF):
|
||||
pdf.add_page()
|
||||
pdf.set_fill_color(30, 64, 175)
|
||||
pdf.rect(0, 0, 210, 297, "F")
|
||||
pdf.set_text_color(255, 255, 255)
|
||||
pdf.set_font("Arial", "B", 28)
|
||||
pdf.set_y(80)
|
||||
pdf.multi_cell(0, 12, "Dokumentacja\nTechniczna", align="C")
|
||||
pdf.ln(6)
|
||||
pdf.set_font("Arial", "B", 18)
|
||||
pdf.multi_cell(0, 10, "FA Krosno Management", align="C")
|
||||
pdf.ln(12)
|
||||
pdf.set_font("Arial", "", 13)
|
||||
pdf.multi_cell(0, 8, "Szczegółowy opis kodu źródłowego\naplikacji webowej React + TypeScript", align="C")
|
||||
pdf.ln(20)
|
||||
pdf.set_font("Arial", "", 11)
|
||||
pdf.multi_cell(0, 8, "Projekt: FaKrosnoManagement\nStos: React 19 · TypeScript · Vite · Tailwind CSS\nBackend: .NET Web API\nData generacji: 2026-06-01", align="C")
|
||||
pdf.set_text_color(0, 0, 0)
|
||||
|
||||
|
||||
def add_toc(pdf: DocPDF):
|
||||
pdf.add_page()
|
||||
pdf.set_font("Arial", "B", 18)
|
||||
pdf.cell(0, 12, "Spis treści", new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.set_font("Arial", "", 11)
|
||||
pdf.ln(4)
|
||||
|
||||
sections = []
|
||||
for item in DOCS:
|
||||
if item[1] is None:
|
||||
sections.append(("section", item[0]))
|
||||
else:
|
||||
sections.append(("file", item[0]))
|
||||
|
||||
for kind, name in sections:
|
||||
if kind == "section":
|
||||
pdf.ln(4)
|
||||
pdf.set_font("Arial", "B", 11)
|
||||
pdf.set_text_color(30, 64, 175)
|
||||
pdf.cell(0, 7, f" {name}", new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.set_text_color(0, 0, 0)
|
||||
else:
|
||||
pdf.set_font("Arial", "", 10)
|
||||
pdf.set_text_color(60, 60, 60)
|
||||
pdf.cell(8, 6, "", new_x="RIGHT", new_y="TOP")
|
||||
pdf.cell(0, 6, name, new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.set_text_color(0, 0, 0)
|
||||
|
||||
|
||||
def add_section_header(pdf: DocPDF, title: str):
|
||||
pdf.add_page()
|
||||
pdf.set_fill_color(30, 64, 175)
|
||||
pdf.set_text_color(255, 255, 255)
|
||||
pdf.set_font("Arial", "B", 16)
|
||||
pdf.rect(0, pdf.get_y() - 2, 210, 20, "F")
|
||||
pdf.cell(0, 16, f" {title}", new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.set_text_color(0, 0, 0)
|
||||
pdf.ln(6)
|
||||
|
||||
|
||||
def add_file_section(pdf: DocPDF, filename: str, description: str, lines: list):
|
||||
# File header
|
||||
pdf.set_fill_color(241, 245, 249)
|
||||
pdf.set_draw_color(203, 213, 225)
|
||||
y = pdf.get_y()
|
||||
pdf.set_font("Arial", "B", 12)
|
||||
pdf.set_text_color(30, 64, 175)
|
||||
pdf.set_fill_color(224, 231, 255)
|
||||
pdf.cell(0, 10, f" {filename}", fill=True, new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.set_text_color(0, 0, 0)
|
||||
pdf.set_font("Arial", "I", 10)
|
||||
pdf.set_text_color(80, 80, 80)
|
||||
pdf.multi_cell(0, 6, f" {description}", new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.set_text_color(0, 0, 0)
|
||||
pdf.ln(3)
|
||||
|
||||
# Lines table
|
||||
for (lineno, code, explanation) in lines:
|
||||
if pdf.get_y() > 265:
|
||||
pdf.add_page()
|
||||
|
||||
# Line number / code range
|
||||
pdf.set_fill_color(248, 250, 252)
|
||||
pdf.set_font("Arial", "B", 9)
|
||||
pdf.set_text_color(99, 102, 241)
|
||||
pdf.cell(30, 6, f"Linia {lineno}", fill=True, new_x="RIGHT", new_y="TOP")
|
||||
|
||||
# Code snippet
|
||||
pdf.set_font("Arial", "I", 8.5)
|
||||
pdf.set_text_color(55, 65, 81)
|
||||
code_short = (code[:65] + "...") if len(code) > 68 else code
|
||||
pdf.cell(0, 6, code_short, new_x="LMARGIN", new_y="NEXT")
|
||||
|
||||
# Explanation
|
||||
pdf.set_font("Arial", "", 10)
|
||||
pdf.set_text_color(30, 30, 30)
|
||||
pdf.set_x(10)
|
||||
|
||||
# Handle multi-line explanations
|
||||
for part in explanation.split("\n"):
|
||||
if pdf.get_y() > 268:
|
||||
pdf.add_page()
|
||||
pdf.set_x(12)
|
||||
pdf.multi_cell(185, 5.5, part, new_x="LMARGIN", new_y="NEXT")
|
||||
|
||||
pdf.set_draw_color(226, 232, 240)
|
||||
pdf.line(10, pdf.get_y(), 200, pdf.get_y())
|
||||
pdf.ln(2)
|
||||
|
||||
pdf.ln(6)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Main
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
print("Generowanie dokumentacji PDF...")
|
||||
pdf = DocPDF()
|
||||
|
||||
add_cover(pdf)
|
||||
add_toc(pdf)
|
||||
|
||||
for item in DOCS:
|
||||
if item[1] is None:
|
||||
# Section header
|
||||
add_section_header(pdf, item[0])
|
||||
else:
|
||||
filename, description, lines = item
|
||||
if pdf.get_y() > 230:
|
||||
pdf.add_page()
|
||||
add_file_section(pdf, filename, description, lines)
|
||||
|
||||
pdf.output(OUTPUT_FILE)
|
||||
print(f"✅ Gotowe! Plik: {OUTPUT_FILE}")
|
||||
print(f" Stron: {pdf.page}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
17
FaKrosnoManagerAngular/.editorconfig
Normal file
17
FaKrosnoManagerAngular/.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
ij_typescript_use_double_quotes = false
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
44
FaKrosnoManagerAngular/.gitignore
vendored
Normal file
44
FaKrosnoManagerAngular/.gitignore
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/mcp.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
__screenshots__/
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
1
FaKrosnoManagerAngular/.npmrc
Normal file
1
FaKrosnoManagerAngular/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
legacy-peer-deps=true
|
||||
12
FaKrosnoManagerAngular/.prettierrc
Normal file
12
FaKrosnoManagerAngular/.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.html",
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
4
FaKrosnoManagerAngular/.vscode/extensions.json
vendored
Normal file
4
FaKrosnoManagerAngular/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||
"recommendations": ["angular.ng-template"]
|
||||
}
|
||||
20
FaKrosnoManagerAngular/.vscode/launch.json
vendored
Normal file
20
FaKrosnoManagerAngular/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ng serve",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: start",
|
||||
"url": "http://localhost:4200/"
|
||||
},
|
||||
{
|
||||
"name": "ng test",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: test",
|
||||
"url": "http://localhost:9876/debug.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
FaKrosnoManagerAngular/.vscode/tasks.json
vendored
Normal file
42
FaKrosnoManagerAngular/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "Changes detected"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation (complete|failed)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "Changes detected"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation (complete|failed)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
59
FaKrosnoManagerAngular/README.md
Normal file
59
FaKrosnoManagerAngular/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# FaKrosnoManagerAngular
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 22.0.5.
|
||||
|
||||
## Development server
|
||||
|
||||
To start a local development server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build the project run:
|
||||
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
|
||||
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
82
FaKrosnoManagerAngular/angular.json
Normal file
82
FaKrosnoManagerAngular/angular.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"cli": {
|
||||
"packageManager": "npm",
|
||||
"analytics": false
|
||||
},
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"FaKrosnoManagerAngular": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"browser": "src/main.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles/styles.scss"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "FaKrosnoManagerAngular:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "FaKrosnoManagerAngular:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"port": 5173
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:unit-test"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9015
FaKrosnoManagerAngular/package-lock.json
generated
Normal file
9015
FaKrosnoManagerAngular/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
FaKrosnoManagerAngular/package.json
Normal file
39
FaKrosnoManagerAngular/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "fa-krosno-manager-angular",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"packageManager": "npm@11.13.0",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^22.0.5",
|
||||
"@angular/common": "^22.0.0",
|
||||
"@angular/compiler": "^22.0.0",
|
||||
"@angular/core": "^22.0.0",
|
||||
"@angular/forms": "^22.0.0",
|
||||
"@angular/platform-browser": "^22.0.0",
|
||||
"@angular/router": "^22.0.0",
|
||||
"@primeuix/themes": "^2.0.3",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"chart.js": "^4.5.1",
|
||||
"primeicons": "^7.0.0",
|
||||
"primeng": "^22.0.0-rc.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^22.0.5",
|
||||
"@angular/cli": "^22.0.5",
|
||||
"@angular/compiler-cli": "^22.0.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"jsdom": "^28.0.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "~6.0.2",
|
||||
"vitest": "^4.0.8"
|
||||
}
|
||||
}
|
||||
BIN
FaKrosnoManagerAngular/public/favicon.ico
Normal file
BIN
FaKrosnoManagerAngular/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
43
FaKrosnoManagerAngular/public/logo.svg
Normal file
43
FaKrosnoManagerAngular/public/logo.svg
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Warstwa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="112.8 177 280.4 441.6" style="enable-background:new 112.8 177 280.4 441.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#706F6F;}
|
||||
.st1{fill:#B12009;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M133.7,590.8l20.1-19.8h-13.2l-18.1,18.5V571h-9.7v46.9h9.7v-16.1l4.5-4.4l14.5,20.5h12.2L133.7,590.8
|
||||
L133.7,590.8z M192.9,603.7c-1.1-2.9-2.5-5.1-4.3-6.7c0.2-0.1,0.4-0.1,0.5-0.3c1.4-0.8,2.6-1.8,3.6-2.9c1-1.2,1.7-2.5,2.2-4
|
||||
s0.8-3.1,0.8-4.8c0-1.9-0.4-3.9-1.1-5.8c-0.8-1.9-1.9-3.6-3.5-4.9c-0.9-0.8-1.9-1.4-2.9-1.9c-1-0.4-2-0.8-3.1-1
|
||||
c-1.1-0.2-2.1-0.3-3.3-0.4c-1.1,0-2.2-0.1-3.4-0.1h-21v46.9h9.7v-18.4h8.6c0.9,0,1.7,0.1,2.5,0.4c0.8,0.2,1.5,0.6,2.2,1.1
|
||||
c0.6,0.5,1.2,1.1,1.7,1.7c0.5,0.7,0.9,1.4,1.2,2.3l4.6,12.8h10.2C198.1,617.9,192.9,603.7,192.9,603.7L192.9,603.7z M178,590.5
|
||||
h-10.8v-10.9h11.6c6.4,0,7.3,2.7,7.3,5.3c0,2.1-0.5,3.5-1.5,4.3C183.7,589.8,181.9,590.5,178,590.5L178,590.5z M247.1,584.7
|
||||
c-1.1-3-2.7-5.6-4.6-7.7c-2-2.1-4.4-3.8-7.2-5c-2.7-1.2-5.8-1.8-9-1.8s-6.3,0.6-9,1.8c-2.7,1.1-5.2,2.8-7.2,4.9
|
||||
c-2,2.1-3.6,4.7-4.7,7.7c-1.1,2.9-1.6,6.3-1.6,9.8s0.5,6.8,1.6,9.8s2.7,5.6,4.7,7.7c2,2.1,4.4,3.8,7.2,5c2.8,1.2,5.8,1.7,9.1,1.7
|
||||
s6.3-0.6,9.1-1.8c2.7-1.2,5.1-2.9,7.1-5.1c2-2.1,3.5-4.7,4.6-7.7c1.1-2.9,1.6-6.2,1.6-9.7C248.8,590.9,248.2,587.6,247.1,584.7
|
||||
L247.1,584.7z M239.1,594.4c0,2.5-0.3,4.8-1,6.7c-0.6,1.9-1.5,3.5-2.7,4.8c-1.1,1.3-2.5,2.3-4,2.9c-1.6,0.7-3.3,1-5.2,1
|
||||
s-3.6-0.3-5.2-1c-1.5-0.7-2.8-1.6-3.9-2.9c-1.2-1.3-2-2.9-2.7-4.8c-0.7-1.9-1-4.2-1-6.7s0.3-4.8,1-6.7c0.7-1.9,1.5-3.5,2.7-4.8
|
||||
c1.1-1.3,2.4-2.2,4-2.9c1.6-0.7,3.3-1,5.2-1c1.8,0,3.5,0.3,5.1,1c1.5,0.6,2.9,1.6,3.9,2.9c1.1,1.3,2,2.9,2.7,4.8
|
||||
C238.7,589.6,239.1,591.9,239.1,594.4L239.1,594.4z M292.4,599.2c-0.5-1.4-1.2-2.6-2.1-3.6c-0.9-1-1.9-1.9-3.1-2.6
|
||||
c-1.1-0.7-2.3-1.3-3.5-1.7c-1.2-0.4-2.6-0.9-4.1-1.3s-3.3-0.9-5.3-1.3c-1.9-0.4-3.4-0.8-4.6-1.2c-1.1-0.3-1.9-0.7-2.5-1.1
|
||||
c-0.5-0.3-0.8-0.6-1-1c-0.2-0.4-0.3-0.9-0.3-1.5c0-1.4,0.6-2.4,1.7-3.2c1.4-1,3.5-1.5,6.2-1.5c7.1,0,8.4,3.3,8.7,6.1l0.2,1.8h9.3
|
||||
l-0.1-2c-0.1-2.3-0.6-4.4-1.5-6.3c-1-1.9-2.3-3.4-3.9-4.7c-1.6-1.3-3.6-2.2-5.8-2.9c-2.2-0.6-4.6-1-7.2-1c-2.5,0-4.8,0.3-6.8,1
|
||||
c-2.1,0.7-4,1.6-5.5,2.8c-1.6,1.2-2.8,2.7-3.6,4.4c-0.9,1.7-1.3,3.6-1.3,5.6c0,1.9,0.3,3.5,1,5c0.6,1.5,1.6,2.8,3,4
|
||||
c1.2,1,2.9,2,4.8,2.8c1.8,0.8,4,1.4,6.5,1.9s4.5,1.1,6.1,1.5c1.5,0.4,2.8,0.9,3.7,1.5c0.7,0.4,1.3,0.9,1.6,1.4s0.4,1.2,0.4,2
|
||||
c0,0.8-0.2,1.5-0.5,2.1c-0.4,0.6-0.9,1.2-1.6,1.7s-1.6,0.9-2.7,1.2c-1.1,0.3-2.4,0.4-3.8,0.4c-1.5,0-2.9-0.2-4.1-0.5
|
||||
c-1.2-0.3-2.3-0.8-3.2-1.4s-1.6-1.4-2.2-2.3c-0.6-0.9-0.9-2.1-1-3.4l-0.1-1.8h-7.3h-2v2c0.1,2.4,0.5,4.6,1.4,6.6
|
||||
c0.9,2,2.2,3.8,4,5.3c1.7,1.5,3.8,2.6,6.3,3.4c2.4,0.8,5.3,1.1,8.5,1.1c2.5,0,4.9-0.3,7-1c2.2-0.7,4.1-1.7,5.7-3
|
||||
c1.6-1.3,2.9-2.9,3.8-4.7c1-1.9,1.4-3.9,1.4-6.1C293.2,602.1,293,600.6,292.4,599.2L292.4,599.2z M330.3,571v29.5L310.7,571h-8.8
|
||||
v46.9h9.4v-29.5l19.6,29.5h8.8V571H330.3L330.3,571z M391.6,584.7c-1.1-3-2.7-5.6-4.7-7.7s-4.4-3.8-7.2-5c-2.8-1.2-5.8-1.8-9-1.8
|
||||
s-6.3,0.6-9,1.8c-2.7,1.1-5.2,2.8-7.2,4.9c-2,2.1-3.6,4.7-4.7,7.7c-1.1,2.9-1.6,6.3-1.6,9.8s0.6,6.8,1.6,9.8c1.1,3,2.7,5.6,4.7,7.7
|
||||
c2,2.1,4.4,3.8,7.2,5c2.7,1.2,5.8,1.7,9.1,1.7c3.3,0,6.3-0.6,9-1.8c2.8-1.2,5.2-2.9,7.2-5.1c2-2.1,3.5-4.7,4.6-7.7
|
||||
c1.1-2.9,1.6-6.2,1.6-9.7C393.2,590.9,392.6,587.6,391.6,584.7L391.6,584.7z M383.5,594.4c0,2.5-0.3,4.8-1,6.7
|
||||
c-0.6,1.9-1.5,3.5-2.7,4.8c-1.1,1.3-2.5,2.3-4,2.9c-1.6,0.7-3.3,1-5.2,1c-1.9,0-3.6-0.3-5.1-1s-2.9-1.6-4-2.9
|
||||
c-1.1-1.3-2-2.9-2.7-4.8c-0.6-1.9-1-4.2-1-6.7s0.3-4.8,1-6.7c0.7-1.9,1.6-3.5,2.7-4.8c1.1-1.3,2.5-2.2,4-2.9c1.6-0.7,3.3-1,5.2-1
|
||||
c1.8,0,3.5,0.3,5.1,1c1.5,0.6,2.8,1.6,4,2.9c1.1,1.3,2,2.9,2.7,4.8C383.2,589.6,383.5,591.9,383.5,594.4L383.5,594.4z"/>
|
||||
<path class="st1" d="M112.8,177v373h274.3V177H112.8L112.8,177z M365.1,528.1H134.7V198.9h230.4V528.1L365.1,528.1z M307.5,303.2
|
||||
l-62-0.1v78.4h-25.9V281.1h87.9v-53.3H163.9l-0.1,223.8l55.8,55.9V403.6h25.9v103.8l29.6-29.5v-74.2h31.1l12.4,30.9l30-30
|
||||
L307.5,303.2L307.5,303.2z M275.1,381.5v-55.7l22.2,55.7H275.1L275.1,381.5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,104 @@
|
||||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { ConfirmationService } from 'primeng/api';
|
||||
import { TaskSchedulerDto } from '../../core/models';
|
||||
import { HangfireService } from '../../core/services/hangfire.service';
|
||||
import { generateGuid } from '../../shared/utils/business-logic';
|
||||
import { PageShellComponent } from '../../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../../shared/components/content-section/content-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-scheduler',
|
||||
standalone: true,
|
||||
imports: [TableModule, ButtonDirective, InputTextModule, DialogModule, FormsModule, DatePipe, PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
title="Zarządzanie Zadaniami"
|
||||
subtitle="Konfiguracja zadań harmonogramu Hangfire"
|
||||
icon="pi pi-clock"
|
||||
>
|
||||
<app-content-section title="Zadania harmonogramu" icon="pi pi-calendar" [flush]="true">
|
||||
<div headerActions>
|
||||
<button pButton type="button" (click)="startAdd()"><i class="pi pi-plus"></i> Dodaj zadanie</button>
|
||||
</div>
|
||||
<p-table [value]="tasks()" [paginator]="true" [rows]="10" dataKey="rowPointer" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header>
|
||||
<tr><th>Nazwa</th><th>Ścieżka</th><th>CRON</th><th>Ostatnie Uruchomienie</th><th>Następne Uruchomienie</th><th></th></tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-task>
|
||||
<tr>
|
||||
<td>{{ task.name }}</td><td>{{ task.path }}</td><td>{{ task.cronOptions }}</td>
|
||||
<td>{{ task.lastExecution | date: 'dd.MM.yyyy HH:mm' }}</td><td>{{ task.nextExecution | date: 'dd.MM.yyyy HH:mm' }}</td>
|
||||
<td>
|
||||
<button pButton type="button" [text]="true" [iconOnly]="true" (click)="startEdit(task)"><i class="pi pi-pencil"></i></button>
|
||||
<button pButton type="button" severity="danger" [text]="true" [iconOnly]="true" (click)="confirmDelete(task)"><i class="pi pi-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
</app-page-shell>
|
||||
|
||||
<p-dialog header="Zadanie" [(visible)]="showDialog" [modal]="true" [style]="{ width: '520px' }">
|
||||
<div class="field-group"><label class="field-label">Nazwa</label><input pInputText class="w-full" [(ngModel)]="editTask.name" /></div>
|
||||
<div class="field-group"><label class="field-label">Ścieżka</label><input pInputText class="w-full" [(ngModel)]="editTask.path" /></div>
|
||||
<div class="field-group"><label class="field-label">CRON</label><input pInputText class="w-full" [(ngModel)]="editTask.cronOptions" /></div>
|
||||
<ng-template #footer>
|
||||
<button pButton type="button" (click)="save()">Zapisz</button>
|
||||
<button pButton type="button" severity="secondary" (click)="showDialog = false">Anuluj</button>
|
||||
</ng-template>
|
||||
</p-dialog>
|
||||
`,
|
||||
})
|
||||
export class SchedulerComponent implements OnInit {
|
||||
private readonly hangfireService = inject(HangfireService);
|
||||
private readonly confirmation = inject(ConfirmationService);
|
||||
|
||||
readonly tasks = signal<TaskSchedulerDto[]>([]);
|
||||
showDialog = false;
|
||||
isNew = false;
|
||||
editTask: TaskSchedulerDto = this.emptyTask();
|
||||
|
||||
async ngOnInit(): Promise<void> { await this.load(); }
|
||||
|
||||
startAdd(): void {
|
||||
this.isNew = true;
|
||||
this.editTask = this.emptyTask();
|
||||
this.editTask.rowPointer = generateGuid();
|
||||
this.showDialog = true;
|
||||
}
|
||||
|
||||
startEdit(task: TaskSchedulerDto): void {
|
||||
this.isNew = false;
|
||||
this.editTask = { ...task, taskSchedulerDetails: [...(task.taskSchedulerDetails ?? [])] };
|
||||
this.showDialog = true;
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
if (this.isNew) await this.hangfireService.add(this.editTask);
|
||||
else await this.hangfireService.update(this.editTask);
|
||||
this.showDialog = false;
|
||||
await this.load();
|
||||
}
|
||||
|
||||
confirmDelete(task: TaskSchedulerDto): void {
|
||||
this.confirmation.confirm({
|
||||
message: `Usunąć zadanie ${task.name}?`,
|
||||
accept: async () => { await this.hangfireService.delete(task); await this.load(); },
|
||||
});
|
||||
}
|
||||
|
||||
private emptyTask(): TaskSchedulerDto {
|
||||
return {
|
||||
id: 0, rowPointer: generateGuid(), name: '', path: '', cronOptions: '',
|
||||
createDate: new Date().toISOString(), activeFrom: new Date().toISOString(), taskSchedulerDetails: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async load(): Promise<void> { this.tasks.set(await this.hangfireService.getAll()); }
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { CheckboxModule } from 'primeng/checkbox';
|
||||
import { ConfirmationService } from 'primeng/api';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { FunctionDto, RoleDto, UserDto } from '../../core/models';
|
||||
import { UserService } from '../../core/services/user.service';
|
||||
import { RoleService } from '../../core/services/role.service';
|
||||
import { FunctionService } from '../../core/services/function.service';
|
||||
import { generateGuid, generateTempPassword } from '../../shared/utils/business-logic';
|
||||
import { PageShellComponent } from '../../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../../shared/components/content-section/content-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-users-manager',
|
||||
standalone: true,
|
||||
imports: [TableModule, ButtonDirective, InputTextModule, DialogModule, CheckboxModule, FormsModule, DatePipe, PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
title="Zarządzanie Użytkownikami i Rolami"
|
||||
subtitle="Administracja kont użytkowników, ról i uprawnień systemowych"
|
||||
icon="pi pi-users"
|
||||
>
|
||||
<app-content-section title="Użytkownicy" icon="pi pi-user" [flush]="true">
|
||||
<div headerActions>
|
||||
<button pButton type="button" (click)="startAddUser()"><i class="pi pi-plus"></i> Dodaj użytkownika</button>
|
||||
</div>
|
||||
<p-table [value]="users()" [paginator]="true" [rows]="10" dataKey="rowPointer" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header>
|
||||
<tr><th>Login</th><th>Email</th><th>Imię</th><th>Nazwisko</th><th>Aktywny</th><th>Utworzono</th><th></th></tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-user>
|
||||
<tr>
|
||||
<td>{{ user.login }}</td><td>{{ user.email }}</td><td>{{ user.firstName }}</td><td>{{ user.lastName }}</td>
|
||||
<td>{{ user.isActive ? 'Tak' : 'Nie' }}</td><td>{{ user.createdDate | date: 'dd.MM.yyyy' }}</td>
|
||||
<td>
|
||||
<button pButton type="button" [text]="true" [iconOnly]="true" (click)="startEditUser(user)"><i class="pi pi-pencil"></i></button>
|
||||
<button pButton type="button" [text]="true" (click)="resetPassword(user)"><i class="pi pi-key"></i> Zresetuj haslo</button>
|
||||
<button pButton type="button" severity="danger" [text]="true" [iconOnly]="true" (click)="confirmDeleteUser(user)"><i class="pi pi-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
|
||||
<app-content-section title="Role" icon="pi pi-shield" [flush]="true">
|
||||
<div headerActions>
|
||||
<button pButton type="button" (click)="startAddRole()"><i class="pi pi-plus"></i> Dodaj rolę</button>
|
||||
</div>
|
||||
<p-table [value]="roles()" [paginator]="true" [rows]="10" dataKey="id" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header><tr><th>ID</th><th>Nazwa</th><th></th></tr></ng-template>
|
||||
<ng-template #body let-role>
|
||||
<tr>
|
||||
<td>{{ role.id }}</td><td>{{ role.name }}</td>
|
||||
<td>
|
||||
<button pButton type="button" [text]="true" [iconOnly]="true" (click)="startEditRole(role)"><i class="pi pi-pencil"></i></button>
|
||||
<button pButton type="button" severity="danger" [text]="true" [iconOnly]="true" (click)="confirmDeleteRole(role)"><i class="pi pi-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
|
||||
<app-content-section title="Funkcje" icon="pi pi-cog" [flush]="true">
|
||||
<div headerActions>
|
||||
<button pButton type="button" (click)="startAddFunction()"><i class="pi pi-plus"></i> Dodaj funkcję</button>
|
||||
</div>
|
||||
<p-table [value]="functions()" [paginator]="true" [rows]="10" dataKey="id" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header><tr><th>ID</th><th>ID Roli</th><th>Nazwa Funkcji</th><th></th></tr></ng-template>
|
||||
<ng-template #body let-fn>
|
||||
<tr>
|
||||
<td>{{ fn.id }}</td><td>{{ fn.roleId }}</td><td>{{ fn.name }}</td>
|
||||
<td>
|
||||
<button pButton type="button" [text]="true" [iconOnly]="true" (click)="startEditFunction(fn)"><i class="pi pi-pencil"></i></button>
|
||||
<button pButton type="button" severity="danger" [text]="true" [iconOnly]="true" (click)="confirmDeleteFunction(fn)"><i class="pi pi-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
</app-page-shell>
|
||||
|
||||
<p-dialog header="Użytkownik" [(visible)]="showUserDialog" [modal]="true" [style]="{ width: '480px' }">
|
||||
<div class="field-group"><label class="field-label">Login</label><input pInputText class="w-full" [(ngModel)]="editUser.login" /></div>
|
||||
<div class="field-group"><label class="field-label">Email</label><input pInputText class="w-full" [(ngModel)]="editUser.email" /></div>
|
||||
<div class="field-group"><label class="field-label">Imię</label><input pInputText class="w-full" [(ngModel)]="editUser.firstName" /></div>
|
||||
<div class="field-group"><label class="field-label">Nazwisko</label><input pInputText class="w-full" [(ngModel)]="editUser.lastName" /></div>
|
||||
<div class="field-group"><p-checkbox [(ngModel)]="editUser.isActive" binary label="Aktywny" /></div>
|
||||
<ng-template #footer>
|
||||
<button pButton type="button" (click)="saveUser()">Zapisz</button>
|
||||
<button pButton type="button" severity="secondary" (click)="showUserDialog = false">Anuluj</button>
|
||||
</ng-template>
|
||||
</p-dialog>
|
||||
|
||||
<p-dialog header="Rola" [(visible)]="showRoleDialog" [modal]="true" [style]="{ width: '400px' }">
|
||||
<div class="field-group"><label class="field-label">Nazwa</label><input pInputText class="w-full" [(ngModel)]="editRole.name" /></div>
|
||||
<ng-template #footer><button pButton type="button" (click)="saveRole()">Zapisz</button><button pButton type="button" severity="secondary" (click)="showRoleDialog = false">Anuluj</button></ng-template>
|
||||
</p-dialog>
|
||||
|
||||
<p-dialog header="Funkcja" [(visible)]="showFunctionDialog" [modal]="true" [style]="{ width: '400px' }">
|
||||
<div class="field-group"><label class="field-label">ID Roli</label><input pInputText type="number" class="w-full" [(ngModel)]="editFunction.roleId" /></div>
|
||||
<div class="field-group"><label class="field-label">Nazwa Funkcji</label><input pInputText class="w-full" [(ngModel)]="editFunction.name" /></div>
|
||||
<ng-template #footer><button pButton type="button" (click)="saveFunction()">Zapisz</button><button pButton type="button" severity="secondary" (click)="showFunctionDialog = false">Anuluj</button></ng-template>
|
||||
</p-dialog>
|
||||
|
||||
<p-dialog header="Dodano użytkownika!" [(visible)]="showTempPasswordDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
<p>Użytkownik <strong>{{ tempLogin }}</strong> został dodany pomyślnie!</p>
|
||||
<p>Hasło tymczasowe: <strong>{{ tempPassword }}</strong></p>
|
||||
<ng-template #footer><button pButton type="button" (click)="showTempPasswordDialog = false">OK</button></ng-template>
|
||||
</p-dialog>
|
||||
`,
|
||||
})
|
||||
export class UsersManagerComponent implements OnInit {
|
||||
private readonly userService = inject(UserService);
|
||||
private readonly roleService = inject(RoleService);
|
||||
private readonly functionService = inject(FunctionService);
|
||||
private readonly confirmation = inject(ConfirmationService);
|
||||
|
||||
readonly users = signal<UserDto[]>([]);
|
||||
readonly roles = signal<RoleDto[]>([]);
|
||||
readonly functions = signal<FunctionDto[]>([]);
|
||||
|
||||
showUserDialog = false;
|
||||
showRoleDialog = false;
|
||||
showFunctionDialog = false;
|
||||
showTempPasswordDialog = false;
|
||||
tempLogin = '';
|
||||
tempPassword = '';
|
||||
|
||||
editUser: UserDto = this.emptyUser();
|
||||
editRole: RoleDto = { id: 0, name: '', rowPointer: '' };
|
||||
editFunction: FunctionDto = { id: 0, roleId: 0, name: '', rowPointer: '' };
|
||||
isNewUser = false;
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await Promise.all([this.loadUsers(), this.loadRoles(), this.loadFunctions()]);
|
||||
}
|
||||
|
||||
startAddUser(): void { this.isNewUser = true; this.editUser = this.emptyUser(); this.showUserDialog = true; }
|
||||
startEditUser(user: UserDto): void { this.isNewUser = false; this.editUser = { ...user }; this.showUserDialog = true; }
|
||||
startAddRole(): void { this.editRole = { id: 0, name: '', rowPointer: generateGuid() }; this.showRoleDialog = true; }
|
||||
startEditRole(role: RoleDto): void { this.editRole = { ...role }; this.showRoleDialog = true; }
|
||||
startAddFunction(): void { this.editFunction = { id: 0, roleId: 0, name: '', rowPointer: generateGuid() }; this.showFunctionDialog = true; }
|
||||
startEditFunction(fn: FunctionDto): void { this.editFunction = { ...fn }; this.showFunctionDialog = true; }
|
||||
|
||||
async saveUser(): Promise<void> {
|
||||
if (this.isNewUser) {
|
||||
const temp = generateTempPassword();
|
||||
this.editUser.passwordHash = bcrypt.hashSync(temp, 10);
|
||||
this.editUser.isTemporaryPassword = true;
|
||||
this.editUser.rowPointer = generateGuid();
|
||||
this.editUser.createdDate = new Date().toISOString();
|
||||
this.editUser.activeFrom = new Date().toISOString();
|
||||
this.editUser.isActive = true;
|
||||
await this.userService.create(this.editUser);
|
||||
this.tempLogin = this.editUser.login;
|
||||
this.tempPassword = temp;
|
||||
this.showTempPasswordDialog = true;
|
||||
} else {
|
||||
await this.userService.update(this.editUser);
|
||||
}
|
||||
this.showUserDialog = false;
|
||||
await this.loadUsers();
|
||||
}
|
||||
|
||||
async resetPassword(user: UserDto): Promise<void> {
|
||||
const temp = generateTempPassword();
|
||||
user.passwordHash = bcrypt.hashSync(temp, 10);
|
||||
user.isTemporaryPassword = true;
|
||||
await this.userService.update(user);
|
||||
this.tempLogin = user.login;
|
||||
this.tempPassword = temp;
|
||||
this.showTempPasswordDialog = true;
|
||||
await this.loadUsers();
|
||||
}
|
||||
|
||||
async saveRole(): Promise<void> {
|
||||
if (this.editRole.id === 0) await this.roleService.create(this.editRole);
|
||||
else await this.roleService.update(this.editRole);
|
||||
this.showRoleDialog = false;
|
||||
await this.loadRoles();
|
||||
}
|
||||
|
||||
async saveFunction(): Promise<void> {
|
||||
if (this.editFunction.id === 0) await this.functionService.create(this.editFunction);
|
||||
else await this.functionService.update(this.editFunction);
|
||||
this.showFunctionDialog = false;
|
||||
await this.loadFunctions();
|
||||
}
|
||||
|
||||
confirmDeleteUser(user: UserDto): void {
|
||||
this.confirmation.confirm({
|
||||
message: `Usunąć użytkownika ${user.login}?`,
|
||||
accept: async () => { await this.userService.delete(user.rowPointer); await this.loadUsers(); },
|
||||
});
|
||||
}
|
||||
|
||||
confirmDeleteRole(role: RoleDto): void {
|
||||
this.confirmation.confirm({
|
||||
message: `Usunąć rolę ${role.name}?`,
|
||||
accept: async () => { await this.roleService.delete(role.rowPointer); await this.loadRoles(); },
|
||||
});
|
||||
}
|
||||
|
||||
confirmDeleteFunction(fn: FunctionDto): void {
|
||||
this.confirmation.confirm({
|
||||
message: `Usunąć funkcję ${fn.name}?`,
|
||||
accept: async () => { await this.functionService.delete(fn.rowPointer); await this.loadFunctions(); },
|
||||
});
|
||||
}
|
||||
|
||||
private emptyUser(): UserDto {
|
||||
return {
|
||||
id: 0, login: '', passwordHash: '', isTemporaryPassword: false, isActive: true,
|
||||
email: '', firstName: '', lastName: '', createdDate: new Date().toISOString(),
|
||||
failedLoginAttempts: 0, isLocked: false, rowPointer: generateGuid(), userRoles: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async loadUsers(): Promise<void> { this.users.set(await this.userService.getAll()); }
|
||||
private async loadRoles(): Promise<void> { this.roles.set(await this.roleService.getAll()); }
|
||||
private async loadFunctions(): Promise<void> { this.functions.set(await this.functionService.getAll()); }
|
||||
}
|
||||
59
FaKrosnoManagerAngular/src/app/app.config.ts
Normal file
59
FaKrosnoManagerAngular/src/app/app.config.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
import { providePrimeNG } from 'primeng/config';
|
||||
import { MessageService, ConfirmationService } from 'primeng/api';
|
||||
import { definePreset } from '@primeuix/themes';
|
||||
import Aura from '@primeuix/themes/aura';
|
||||
import { routes } from './app.routes';
|
||||
import { authInterceptor } from './core/interceptors/auth.interceptor';
|
||||
import { errorInterceptor } from './core/interceptors/error.interceptor';
|
||||
import { environment } from '../environments/environment';
|
||||
|
||||
const FaPreset = definePreset(Aura, {
|
||||
semantic: {
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#1a56db',
|
||||
600: '#1648b8',
|
||||
700: '#123a94',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
950: '#172554',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
none: '0',
|
||||
xs: '2px',
|
||||
sm: '4px',
|
||||
md: '6px',
|
||||
lg: '8px',
|
||||
xl: '10px',
|
||||
},
|
||||
});
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideZonelessChangeDetection(),
|
||||
provideRouter(routes),
|
||||
provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])),
|
||||
provideAnimationsAsync(),
|
||||
providePrimeNG({
|
||||
license: environment.primeNgLicense || undefined,
|
||||
theme: {
|
||||
preset: FaPreset,
|
||||
options: {
|
||||
darkModeSelector: '[data-theme="dark"]',
|
||||
},
|
||||
},
|
||||
}),
|
||||
MessageService,
|
||||
ConfirmationService,
|
||||
],
|
||||
};
|
||||
32
FaKrosnoManagerAngular/src/app/app.routes.ts
Normal file
32
FaKrosnoManagerAngular/src/app/app.routes.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () =>
|
||||
import('./layout/main-layout/main-layout.component').then((m) => m.MainLayoutComponent),
|
||||
children: [
|
||||
{ path: '', loadComponent: () => import('./schedule-orders/schedule-orders-list.component').then((m) => m.ScheduleOrdersListComponent) },
|
||||
{ path: 'Products', loadComponent: () => import('./products/products-list.component').then((m) => m.ProductsListComponent) },
|
||||
{ path: 'Warehouse', loadComponent: () => import('./warehouse/warehouse.component').then((m) => m.WarehouseComponent) },
|
||||
{ path: 'ScheduleOrder/:scheduleOrderId', loadComponent: () => import('./schedule-order/schedule-order-detail.component').then((m) => m.ScheduleOrderDetailComponent) },
|
||||
{ path: 'Warehouse/Meyle/PackList/:WzHeader', loadComponent: () => import('./packing-lists/meyle/meyle-pack-list.component').then((m) => m.MeylePackListComponent) },
|
||||
{ path: 'Warehouse/Meyle/PackList/:WzHeader/Simple', loadComponent: () => import('./packing-lists/meyle/meyle-pack-list-simple.component').then((m) => m.MeylePackListSimpleComponent) },
|
||||
{ path: 'Warehouse/Marelli/PackList/:WzHeader', loadComponent: () => import('./packing-lists/marelli/marelli-pack-list.component').then((m) => m.MarelliPackListComponent) },
|
||||
{ path: 'Warehouse/Marelli/PackList/:WzHeader/Simple', loadComponent: () => import('./packing-lists/marelli/marelli-pack-list-simple.component').then((m) => m.MarelliPackListSimpleComponent) },
|
||||
{ path: 'Admin/PK/CustomerOrders', loadComponent: () => import('./customer-orders/customer-orders-list.component').then((m) => m.CustomerOrdersListComponent) },
|
||||
{ path: 'Admin/PK/CustomerOrder/:customerOrderId', loadComponent: () => import('./customer-order/customer-order-detail.component').then((m) => m.CustomerOrderDetailComponent) },
|
||||
{ path: 'Admin/PK/EdiCustomerOrders', loadComponent: () => import('./edi-customer-orders/edi-customer-orders-list.component').then((m) => m.EdiCustomerOrdersListComponent) },
|
||||
{ path: 'Admin/PK/EdiCustomerOrder/:customerOrderId', loadComponent: () => import('./edi-customer-order/edi-customer-order-detail.component').then((m) => m.EdiCustomerOrderDetailComponent) },
|
||||
{ path: 'admin/pk', loadComponent: () => import('./customer-orders-translations/customer-orders-translations.component').then((m) => m.CustomerOrdersTranslationsComponent) },
|
||||
{ path: 'Admin/PK/UsersManager', loadComponent: () => import('./admin/users-manager/users-manager.component').then((m) => m.UsersManagerComponent) },
|
||||
{ path: 'Admin/PK/Scheduler', loadComponent: () => import('./admin/scheduler/scheduler.component').then((m) => m.SchedulerComponent) },
|
||||
],
|
||||
},
|
||||
{ path: 'login', loadComponent: () => import('./auth/login/login.component').then((m) => m.LoginComponent) },
|
||||
{ path: 'register', loadComponent: () => import('./auth/register/register.component').then((m) => m.RegisterComponent) },
|
||||
{ path: 'Main', loadComponent: () => import('./main/main.component').then((m) => m.MainComponent) },
|
||||
{ path: 'Unauthorized', loadComponent: () => import('./unauthorized/unauthorized.component').then((m) => m.UnauthorizedComponent) },
|
||||
{ path: 'Error', loadComponent: () => import('./error/error.component').then((m) => m.ErrorComponent) },
|
||||
{ path: '**', redirectTo: '' },
|
||||
];
|
||||
0
FaKrosnoManagerAngular/src/app/app.scss
Normal file
0
FaKrosnoManagerAngular/src/app/app.scss
Normal file
23
FaKrosnoManagerAngular/src/app/app.spec.ts
Normal file
23
FaKrosnoManagerAngular/src/app/app.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { App } from './app';
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [App],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render title', async () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, FaKrosnoManagerAngular');
|
||||
});
|
||||
});
|
||||
10
FaKrosnoManagerAngular/src/app/app.ts
Normal file
10
FaKrosnoManagerAngular/src/app/app.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
template: `<router-outlet />`,
|
||||
styles: `:host { display: block; height: 100dvh; }`,
|
||||
})
|
||||
export class App {}
|
||||
134
FaKrosnoManagerAngular/src/app/auth/login/login.component.ts
Normal file
134
FaKrosnoManagerAngular/src/app/auth/login/login.component.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { InputPasswordModule } from 'primeng/inputpassword';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { AuthService } from '../../core/services/auth.service';
|
||||
import { UserService } from '../../core/services/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
standalone: true,
|
||||
imports: [FormsModule, InputTextModule, InputPasswordModule, ButtonDirective, RouterLink],
|
||||
template: `
|
||||
<div class="auth-layout">
|
||||
<aside class="auth-layout__brand">
|
||||
<div class="auth-layout__brand-logo">
|
||||
<img src="logo.svg" width="40" height="40" alt="" />
|
||||
<h1 class="auth-layout__brand-title">FA Krosno Manager</h1>
|
||||
</div>
|
||||
<p class="auth-layout__brand-tagline">
|
||||
System zarządzania zamówieniami DELFOR, magazynem i procesami EDI dla operacji produkcyjnych.
|
||||
</p>
|
||||
<div class="auth-layout__features">
|
||||
<div class="auth-layout__feature"><i class="pi pi-building"></i> Zamówienia DELFOR i harmonogramy</div>
|
||||
<div class="auth-layout__feature"><i class="pi pi-warehouse"></i> Magazyn i packing listy</div>
|
||||
<div class="auth-layout__feature"><i class="pi pi-database"></i> Integracja EDI / Syteline</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="auth-layout__form">
|
||||
<div class="auth-card">
|
||||
<h2 class="auth-card__title">Logowanie</h2>
|
||||
<p class="auth-card__subtitle">Zaloguj się do systemu FA Krosno Manager</p>
|
||||
|
||||
@if (tempPassword()) {
|
||||
<div class="alert-info mb-3">
|
||||
Twoje tymczasowe hasło to: <strong>{{ tempPassword() }}</strong>. Użyj go do pierwszego logowania.
|
||||
</div>
|
||||
}
|
||||
|
||||
<form (ngSubmit)="handleLogin()">
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="login">Login</label>
|
||||
<input pInputText id="login" class="w-full" [(ngModel)]="login" name="login" required placeholder="Wprowadź login" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="password">Hasło</label>
|
||||
<input pInputPassword pInputText id="password" name="password" [(ngModel)]="password" placeholder="Wprowadź hasło" [fluid]="true" class="w-full" />
|
||||
</div>
|
||||
<button pButton type="submit" [fluid]="true"><i class="pi pi-sign-in"></i> Zaloguj</button>
|
||||
@if (errorMessage()) {
|
||||
<div class="alert-danger mt-3">{{ errorMessage() }}</div>
|
||||
}
|
||||
</form>
|
||||
|
||||
@if (showChangePassword()) {
|
||||
<hr class="divider" />
|
||||
<h3 class="auth-card__title" style="font-size: var(--font-size-lg)">Zmień hasło</h3>
|
||||
<p class="auth-card__subtitle">Ustaw nowe hasło przed pierwszym użyciem systemu</p>
|
||||
<form (ngSubmit)="handleChangePassword()">
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="newPassword">Nowe hasło</label>
|
||||
<input pInputPassword pInputText id="newPassword" name="newPassword" [(ngModel)]="newPassword" placeholder="Wprowadź nowe hasło" [fluid]="true" class="w-full" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="confirmPassword">Potwierdź hasło</label>
|
||||
<input pInputPassword pInputText id="confirmPassword" name="confirmPassword" [(ngModel)]="confirmPassword" placeholder="Potwierdź nowe hasło" [fluid]="true" class="w-full" />
|
||||
</div>
|
||||
<button pButton type="submit" severity="success" [fluid]="true">Zmień hasło</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
<p class="text-secondary" style="margin-top: var(--spacing-4); font-size: var(--font-size-sm); text-align: center">
|
||||
Nie masz konta? <a routerLink="/register">Zarejestruj się</a>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
private readonly auth = inject(AuthService);
|
||||
private readonly userService = inject(UserService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
|
||||
login = '';
|
||||
password = '';
|
||||
newPassword = '';
|
||||
confirmPassword = '';
|
||||
|
||||
readonly tempPassword = signal('');
|
||||
readonly errorMessage = signal('');
|
||||
readonly showChangePassword = signal(false);
|
||||
|
||||
ngOnInit(): void {
|
||||
const param = this.route.snapshot.queryParamMap.get('tempPassword');
|
||||
if (param) this.tempPassword.set(decodeURIComponent(param));
|
||||
}
|
||||
|
||||
async handleLogin(): Promise<void> {
|
||||
this.errorMessage.set('');
|
||||
const user = await this.auth.login(this.login, this.password);
|
||||
if (!user) {
|
||||
this.errorMessage.set('Nieprawidłowy login lub hasło');
|
||||
return;
|
||||
}
|
||||
if (user.isTemporaryPassword) {
|
||||
this.showChangePassword.set(true);
|
||||
return;
|
||||
}
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
|
||||
async handleChangePassword(): Promise<void> {
|
||||
if (this.newPassword !== this.confirmPassword) {
|
||||
this.errorMessage.set('Hasła nie są zgodne');
|
||||
return;
|
||||
}
|
||||
const user = await this.auth.getUserByUsername(this.login);
|
||||
if (!user) return;
|
||||
user.passwordHash = bcrypt.hashSync(this.newPassword, 10);
|
||||
user.isTemporaryPassword = false;
|
||||
await this.userService.update(user);
|
||||
this.showChangePassword.set(false);
|
||||
this.login = '';
|
||||
this.password = '';
|
||||
this.newPassword = '';
|
||||
this.confirmPassword = '';
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { UserService } from '../../core/services/user.service';
|
||||
import { UserDto } from '../../core/models';
|
||||
import { generateGuid, generateTempPassword } from '../../shared/utils/business-logic';
|
||||
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
standalone: true,
|
||||
imports: [FormsModule, InputTextModule, ButtonDirective, RouterLink],
|
||||
template: `
|
||||
<div class="auth-layout">
|
||||
<aside class="auth-layout__brand">
|
||||
<div class="auth-layout__brand-logo">
|
||||
<img src="logo.svg" width="40" height="40" alt="" />
|
||||
<h1 class="auth-layout__brand-title">FA Krosno Manager</h1>
|
||||
</div>
|
||||
<p class="auth-layout__brand-tagline">
|
||||
System zarządzania zamówieniami DELFOR, magazynem i procesami EDI dla operacji produkcyjnych.
|
||||
</p>
|
||||
<div class="auth-layout__features">
|
||||
<div class="auth-layout__feature"><i class="pi pi-building"></i> Zamówienia DELFOR i harmonogramy</div>
|
||||
<div class="auth-layout__feature"><i class="pi pi-warehouse"></i> Magazyn i packing listy</div>
|
||||
<div class="auth-layout__feature"><i class="pi pi-database"></i> Integracja EDI / Syteline</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="auth-layout__form">
|
||||
<div class="auth-card">
|
||||
<h2 class="auth-card__title">Rejestracja</h2>
|
||||
<p class="auth-card__subtitle">Utwórz nowe konto w systemie FA Krosno Manager</p>
|
||||
|
||||
<form (ngSubmit)="handleRegister()">
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="login">Login</label>
|
||||
<input pInputText id="login" class="w-full" [(ngModel)]="model.login" name="login" required placeholder="Wprowadź login" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="email">Email</label>
|
||||
<input pInputText id="email" class="w-full" [(ngModel)]="model.email" name="email" required placeholder="Wprowadź email" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="firstName">Imię</label>
|
||||
<input pInputText id="firstName" class="w-full" [(ngModel)]="model.firstName" name="firstName" required placeholder="Wprowadź imię" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="lastName">Nazwisko</label>
|
||||
<input pInputText id="lastName" class="w-full" [(ngModel)]="model.lastName" name="lastName" required placeholder="Wprowadź nazwisko" />
|
||||
</div>
|
||||
<button pButton type="submit" [fluid]="true"><i class="pi pi-user-plus"></i> Zarejestruj</button>
|
||||
</form>
|
||||
|
||||
<p class="text-secondary" style="margin-top: var(--spacing-4); font-size: var(--font-size-sm); text-align: center">
|
||||
Masz już konto? <a routerLink="/login">Zaloguj się</a>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class RegisterComponent {
|
||||
private readonly userService = inject(UserService);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
model = { login: '', email: '', firstName: '', lastName: '' };
|
||||
|
||||
async handleRegister(): Promise<void> {
|
||||
const temporaryPassword = generateTempPassword();
|
||||
const user: UserDto = {
|
||||
id: 0,
|
||||
login: this.model.login,
|
||||
passwordHash: bcrypt.hashSync(temporaryPassword, 10),
|
||||
isTemporaryPassword: true,
|
||||
isActive: true,
|
||||
activeFrom: new Date().toISOString(),
|
||||
email: this.model.email,
|
||||
firstName: this.model.firstName,
|
||||
lastName: this.model.lastName,
|
||||
createdDate: new Date().toISOString(),
|
||||
failedLoginAttempts: 0,
|
||||
isLocked: false,
|
||||
rowPointer: generateGuid(),
|
||||
userRoles: [],
|
||||
};
|
||||
|
||||
await this.userService.create(user);
|
||||
this.router.navigate(['/login'], { queryParams: { tempPassword: temporaryPassword } });
|
||||
}
|
||||
}
|
||||
12
FaKrosnoManagerAngular/src/app/core/guards/auth.guard.ts
Normal file
12
FaKrosnoManagerAngular/src/app/core/guards/auth.guard.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { inject } from '@angular/core';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
export const authGuard: CanActivateFn = () => {
|
||||
const auth = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
if (auth.isAuthenticated()) {
|
||||
return true;
|
||||
}
|
||||
return router.createUrlTree(['/login']);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const auth = inject(AuthService);
|
||||
const token = auth.getToken();
|
||||
if (token) {
|
||||
req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
|
||||
}
|
||||
return next(req);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { catchError, finalize, throwError } from 'rxjs';
|
||||
import { MessageService } from 'primeng/api';
|
||||
import { Router } from '@angular/router';
|
||||
import { LoadingService } from '../services/loading.service';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const messageService = inject(MessageService);
|
||||
const router = inject(Router);
|
||||
const auth = inject(AuthService);
|
||||
const loading = inject(LoadingService);
|
||||
|
||||
loading.show();
|
||||
return next(req).pipe(
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
if (error.status === 401) {
|
||||
auth.logout();
|
||||
router.navigate(['/login']);
|
||||
messageService.add({ severity: 'error', summary: 'Sesja wygasła', detail: 'Zaloguj się ponownie.' });
|
||||
} else {
|
||||
const detail = error.error?.message ?? error.message ?? 'Wystąpił błąd podczas komunikacji z serwerem.';
|
||||
messageService.add({ severity: 'error', summary: 'Błąd', detail });
|
||||
}
|
||||
return throwError(() => error);
|
||||
}),
|
||||
finalize(() => loading.hide()),
|
||||
);
|
||||
};
|
||||
414
FaKrosnoManagerAngular/src/app/core/models/index.ts
Normal file
414
FaKrosnoManagerAngular/src/app/core/models/index.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
expires: string;
|
||||
}
|
||||
|
||||
export interface ResponseModel {
|
||||
status: number;
|
||||
identifier: string;
|
||||
message: string;
|
||||
externalIdentifier?: string;
|
||||
}
|
||||
|
||||
export interface TransactionModel {
|
||||
partNumber?: string;
|
||||
itemNumber?: string;
|
||||
quantity?: number;
|
||||
}
|
||||
|
||||
export interface UserDto {
|
||||
id: number;
|
||||
login: string;
|
||||
passwordHash: string;
|
||||
isTemporaryPassword: boolean;
|
||||
isActive: boolean;
|
||||
activeFrom?: string;
|
||||
activeTo?: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
createdDate: string;
|
||||
lastLoginDate?: string;
|
||||
failedLoginAttempts: number;
|
||||
isLocked: boolean;
|
||||
lockoutEndDate?: string;
|
||||
rowPointer: string;
|
||||
userRoles: UserRoleDto[];
|
||||
}
|
||||
|
||||
export interface UserRoleDto {
|
||||
userId: number;
|
||||
roleId: number;
|
||||
rowPointer: string;
|
||||
}
|
||||
|
||||
export interface RoleDto {
|
||||
id: number;
|
||||
name: string;
|
||||
rowPointer: string;
|
||||
}
|
||||
|
||||
export interface FunctionDto {
|
||||
id: number;
|
||||
roleId: number;
|
||||
name: string;
|
||||
rowPointer: string;
|
||||
}
|
||||
|
||||
export interface TaskSchedulerDto {
|
||||
id: number;
|
||||
rowPointer: string;
|
||||
name: string;
|
||||
path: string;
|
||||
cronOptions: string;
|
||||
createDate: string;
|
||||
activeFrom: string;
|
||||
activeUntil?: string;
|
||||
lastExecution?: string;
|
||||
nextExecution?: string;
|
||||
taskSchedulerDetails: TaskSchedulerDetailDto[];
|
||||
}
|
||||
|
||||
export interface TaskSchedulerDetailDto {
|
||||
id: number;
|
||||
taskSchedulerId: number;
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ProductDto {
|
||||
id: number;
|
||||
recipientID: number;
|
||||
recipientIdx: string;
|
||||
faIdx: string;
|
||||
recipientName: string;
|
||||
}
|
||||
|
||||
export interface ScheduleOrderDto {
|
||||
id: number;
|
||||
poNum: string;
|
||||
orderID: number;
|
||||
recipientID: number;
|
||||
lastUpdateDate: string;
|
||||
docNumber: number;
|
||||
docType?: string;
|
||||
recipientCode: string;
|
||||
purchaserCode?: string;
|
||||
recipientName: string;
|
||||
purchaserName?: string;
|
||||
scheduleOrderDetails: ScheduleOrderDetailDto[];
|
||||
scheduleOrderMiscs: ScheduleOrderMiscDto[];
|
||||
}
|
||||
|
||||
export interface ScheduleOrderDetailDto {
|
||||
id: number;
|
||||
scheduleOrderID: number;
|
||||
sc_productCode: string;
|
||||
sh_productCode: string;
|
||||
price?: number;
|
||||
orderNumber?: string;
|
||||
recipientName?: string;
|
||||
recipientCode?: string;
|
||||
purchaserName?: string;
|
||||
scheduleOrderDetailDetails: ScheduleOrderDetailDetailDto[];
|
||||
}
|
||||
|
||||
export interface ScheduleOrderDetailDetailDto {
|
||||
id: number;
|
||||
scheduleOrderDetailID: number;
|
||||
qty: number;
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
sccType: string;
|
||||
sccDesc?: string;
|
||||
qtyType?: string;
|
||||
qtyDesc?: string;
|
||||
status: string;
|
||||
shipDate?: string;
|
||||
lastUpdateDate: string;
|
||||
}
|
||||
|
||||
export interface ScheduleOrderMiscDto {
|
||||
id: number;
|
||||
scheduleOrderID: number;
|
||||
type: string;
|
||||
value: string;
|
||||
label?: string;
|
||||
display: boolean;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface CustomerOrderDto {
|
||||
rowPointer: string;
|
||||
coNum: string;
|
||||
custPo: string;
|
||||
custNum: string;
|
||||
custSeq: number;
|
||||
createDate: string;
|
||||
contact?: string;
|
||||
phone?: string;
|
||||
orderDate?: string;
|
||||
termsCode?: string;
|
||||
price?: number;
|
||||
translatedStatus?: string;
|
||||
whse?: string;
|
||||
frtTaxCode1?: string;
|
||||
endUserType?: string;
|
||||
exchRate?: number;
|
||||
uf_FKR_EDI_IdentificationCode?: string;
|
||||
uf_FKR_EDI_RSSBussFolder?: string;
|
||||
uf_DocType?: string;
|
||||
uf_FKR_EDI_Gate?: string;
|
||||
uf_FKR_EDI_RecipientCode?: string;
|
||||
uf_FKR_EDI_SellerCode?: string;
|
||||
uf_FKR_EDI_SenderCode?: string;
|
||||
uf_FKR_EDI_BuyerCode?: string;
|
||||
ediCustomerOrderTranslates?: EdiCustomerOrderTranslateDto[];
|
||||
customerOrderLines: CustomerOrderLineDto[];
|
||||
}
|
||||
|
||||
export interface CustomerOrderLineDto {
|
||||
coLine: number;
|
||||
coNum?: string;
|
||||
item: string;
|
||||
custItem: string;
|
||||
description: string;
|
||||
blanketQty?: number;
|
||||
um: string;
|
||||
contPrice?: number;
|
||||
translatedStatus?: string;
|
||||
effDate?: string;
|
||||
uf_FKR_EDI_BLN_BoxType?: string;
|
||||
uf_FKR_EDI_BLN_Address?: string;
|
||||
uf_FKR_EDI_BLN_FinalDestination?: string;
|
||||
uf_FKR_EDI_BLN_QtyPerBox?: number;
|
||||
customerOrderLineItems: CustomerOrderLineItemDto[];
|
||||
}
|
||||
|
||||
export interface CustomerOrderLineItemDto {
|
||||
coLine: number;
|
||||
coRelease: number;
|
||||
coNum?: string;
|
||||
item: string;
|
||||
custItem: string;
|
||||
qtyOrdered?: number;
|
||||
dueDate?: string;
|
||||
translatedStatus?: string;
|
||||
price?: number;
|
||||
releaseDate?: string;
|
||||
whse?: string;
|
||||
uf_FKR_EDI_ITEM_DocumentType?: string;
|
||||
taxCode1?: string;
|
||||
um?: string;
|
||||
coCustNum?: string;
|
||||
description?: string;
|
||||
uf_FKR_EDI_ITEM_RoutingCode?: string;
|
||||
uf_FKR_EDI_ITEM_DeliveryCallNum?: string;
|
||||
uf_LOC_11_UnloadingPoint?: string;
|
||||
uf_LOC_159_DestinationPoint?: string;
|
||||
uf_FKR_EDI_ITEM_PalletCode?: string;
|
||||
}
|
||||
|
||||
export interface EdiCustomerOrderDto {
|
||||
rowPointer: string;
|
||||
customerOrderNumber: string;
|
||||
customerPoNumber: string;
|
||||
customerNumber: string;
|
||||
customerSequence?: number;
|
||||
customerName?: string;
|
||||
createDate: string;
|
||||
recivedDate?: string;
|
||||
posted?: number;
|
||||
postedDate?: string;
|
||||
orderDate?: string;
|
||||
price?: number;
|
||||
weight?: number;
|
||||
warehouse?: string;
|
||||
gate?: string;
|
||||
recipientCode?: string;
|
||||
senderCode?: string;
|
||||
sellerCode?: string;
|
||||
buyerCode?: string;
|
||||
docType?: string;
|
||||
slOrderNumber?: string;
|
||||
sentToSl?: string;
|
||||
translatedStatus?: string;
|
||||
ediCustomerOrderLines: EdiCustomerOrderLineDto[];
|
||||
ediCustomerOrderTranslates: EdiCustomerOrderTranslateDto[];
|
||||
}
|
||||
|
||||
export interface EdiCustomerOrderLineDto {
|
||||
customerOrderLine: number;
|
||||
customerOrderNumber?: string;
|
||||
item: string;
|
||||
customerItemNumber: string;
|
||||
description: string;
|
||||
blanketQty?: number;
|
||||
uom: string;
|
||||
contPrice?: number;
|
||||
translatedStatus?: string;
|
||||
effectiveDate?: string;
|
||||
boxType?: string;
|
||||
address?: string;
|
||||
finalDestination?: string;
|
||||
qtyPerBox?: number;
|
||||
ediCustomerOrderLineItems: EdiCustomerOrderLineItemDto[];
|
||||
}
|
||||
|
||||
export interface EdiCustomerOrderLineItemDto {
|
||||
customerOrderLine: number;
|
||||
customerOrderRelease: number;
|
||||
customerOrderNumber?: string;
|
||||
item: string;
|
||||
customerItem: string;
|
||||
qtyOrdered?: number;
|
||||
dueDate?: string;
|
||||
translatedStatus?: string;
|
||||
price?: number;
|
||||
releaseDate?: string;
|
||||
warehouse?: string;
|
||||
documentType?: string;
|
||||
taxCodeOne?: string;
|
||||
uom?: string;
|
||||
customerOrderCustomerNumber?: string;
|
||||
description?: string;
|
||||
routingCode?: string;
|
||||
deliveryCallNumber?: string;
|
||||
unloadingPoint?: string;
|
||||
destinationPoint?: string;
|
||||
palletCode?: string;
|
||||
palletNumber?: string;
|
||||
}
|
||||
|
||||
export interface EdiCustomerOrderTranslateDto {
|
||||
id: number;
|
||||
coEdiOrder: number;
|
||||
coRowPointer: string;
|
||||
coCoNum?: string;
|
||||
coType?: string;
|
||||
coTakenBy?: string;
|
||||
ediCoCoNum?: string;
|
||||
ordersCount: number;
|
||||
orderFound: boolean;
|
||||
createdDate: string;
|
||||
foundNumbers?: string;
|
||||
scheduleOrderId: number;
|
||||
}
|
||||
|
||||
export interface ErrorLogDto {
|
||||
trxNum: string;
|
||||
seq: number;
|
||||
lineNum: number;
|
||||
releaseNum: number;
|
||||
errDate?: string;
|
||||
errNum?: number;
|
||||
errMsg: string;
|
||||
trxCode: string;
|
||||
noteExistsFlag: boolean;
|
||||
recordDate: string;
|
||||
rowPointer: string;
|
||||
createdBy: string;
|
||||
updatedBy: string;
|
||||
createDate: string;
|
||||
inWorkflow: boolean;
|
||||
}
|
||||
|
||||
export interface WzHeaderDto {
|
||||
id: string;
|
||||
fk_Client?: string;
|
||||
createdDate: string;
|
||||
emailAddresses?: string;
|
||||
wzNumbers?: string;
|
||||
wzRowsMeyle?: WzRowMeyleDto[];
|
||||
wzRowsMarelli?: WzRowMarelliDto[];
|
||||
}
|
||||
|
||||
export interface WzRowMeyleDto {
|
||||
id: string;
|
||||
fk_Header?: string;
|
||||
orderNumber: string;
|
||||
itemNumber: string;
|
||||
quantity?: number;
|
||||
palletNumber?: number;
|
||||
wzNumber: string;
|
||||
partNumber?: string;
|
||||
transactionNumber?: number;
|
||||
faIndex?: string;
|
||||
partNumberSl?: string;
|
||||
}
|
||||
|
||||
export interface WzRowMarelliDto {
|
||||
id: string;
|
||||
fkHeader?: string;
|
||||
type: string;
|
||||
palletNumber?: number;
|
||||
itemNumber: string;
|
||||
engineerNumber: string;
|
||||
quantity?: number;
|
||||
orderNumber: string;
|
||||
wzNumber: string;
|
||||
faIndex: string;
|
||||
transactionNumber?: number;
|
||||
}
|
||||
|
||||
export interface WzClientDto {
|
||||
id: string;
|
||||
customerNumber: string;
|
||||
customerSequence?: number;
|
||||
createdDate: string;
|
||||
name: string;
|
||||
shortName: string;
|
||||
logoBase64?: string;
|
||||
}
|
||||
|
||||
export interface MaterialTransactionDto {
|
||||
mtGroup?: string;
|
||||
mtGroupNum?: string;
|
||||
transNum?: number;
|
||||
item?: string;
|
||||
transDate?: string;
|
||||
qty?: number;
|
||||
cost?: number;
|
||||
whse?: string;
|
||||
loc?: string;
|
||||
refNum?: string;
|
||||
refLineSuf?: number;
|
||||
refRelease?: number;
|
||||
reasonCode?: string;
|
||||
transType?: string;
|
||||
refType?: string;
|
||||
mtReasonType?: string;
|
||||
prefixId?: number;
|
||||
sequenceId?: number;
|
||||
whseSequenceId?: number;
|
||||
whseSplit?: boolean;
|
||||
variableId?: string;
|
||||
formName?: string;
|
||||
inWorkflow?: boolean;
|
||||
noteExistsFlag?: boolean;
|
||||
recordDate?: string;
|
||||
rowPointer?: string;
|
||||
createdBy?: string;
|
||||
updatedBy?: string;
|
||||
createDate?: string;
|
||||
custNum?: string;
|
||||
vendNum?: string;
|
||||
recipNum?: number;
|
||||
uf_FKR_internal_num_matltran_zn?: string;
|
||||
session_Id?: string;
|
||||
uf_MobileAppUser?: string;
|
||||
nr_KARTY_KONTROLNEJ?: string;
|
||||
}
|
||||
|
||||
export interface ItemCustDto {
|
||||
item: string;
|
||||
custNum: string;
|
||||
custItemSeq: number;
|
||||
custItem: string;
|
||||
um: string;
|
||||
uf_FKR_CustItem2?: string;
|
||||
}
|
||||
|
||||
export interface GridFilterState {
|
||||
columns: Record<string, unknown>;
|
||||
}
|
||||
58
FaKrosnoManagerAngular/src/app/core/services/auth.service.ts
Normal file
58
FaKrosnoManagerAngular/src/app/core/services/auth.service.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Injectable, signal, computed, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { LoginResponse, UserDto } from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly tokenKey = 'authToken';
|
||||
|
||||
private readonly tokenSignal = signal<string | null>(this.readToken());
|
||||
readonly token = this.tokenSignal.asReadonly();
|
||||
readonly isAuthenticated = computed(() => !!this.tokenSignal());
|
||||
readonly userName = signal<string>('');
|
||||
|
||||
private readToken(): string | null {
|
||||
return sessionStorage.getItem(this.tokenKey);
|
||||
}
|
||||
|
||||
async login(login: string, password: string): Promise<UserDto | null> {
|
||||
try {
|
||||
const result = await firstValueFrom(
|
||||
this.http.post<LoginResponse>(`${environment.apiBaseUrl}/api/Users/login`, { login, password }),
|
||||
);
|
||||
if (!result?.token) return null;
|
||||
sessionStorage.setItem(this.tokenKey, result.token);
|
||||
this.tokenSignal.set(result.token);
|
||||
const user = await this.getUserByUsername(login);
|
||||
if (user) {
|
||||
this.userName.set(user.login);
|
||||
}
|
||||
return user;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getUserByUsername(username: string): Promise<UserDto | null> {
|
||||
try {
|
||||
return await firstValueFrom(
|
||||
this.http.get<UserDto>(`${environment.apiBaseUrl}/api/Users/by-username`, { params: { username } }),
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
sessionStorage.removeItem(this.tokenKey);
|
||||
this.tokenSignal.set(null);
|
||||
this.userName.set('');
|
||||
}
|
||||
|
||||
getToken(): string | null {
|
||||
return this.tokenSignal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { CustomerOrderDto } from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CustomerOrderService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly base = `${environment.apiBaseUrl}/api/CustomerOrders`;
|
||||
|
||||
getAll(): Promise<CustomerOrderDto[]> {
|
||||
return firstValueFrom(this.http.get<CustomerOrderDto[]>(this.base));
|
||||
}
|
||||
|
||||
getByOrderNumber(customerOrderNumber: string): Promise<CustomerOrderDto> {
|
||||
return firstValueFrom(
|
||||
this.http.get<CustomerOrderDto>(`${this.base}/by-order-number`, { params: { customerOrderNumber } }),
|
||||
);
|
||||
}
|
||||
|
||||
getByCoNumber(customerOrderNumber: string): Promise<CustomerOrderDto> {
|
||||
return firstValueFrom(
|
||||
this.http.get<CustomerOrderDto>(`${this.base}/by-co-number`, { params: { customerOrderNumber } }),
|
||||
);
|
||||
}
|
||||
|
||||
getByRowPointer(rowPointer: string): Promise<CustomerOrderDto> {
|
||||
return firstValueFrom(
|
||||
this.http.get<CustomerOrderDto>(`${this.base}/by-order-number`, { params: { customerOrderNumber: rowPointer } }),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { EdiCustomerOrderTranslateDto } from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class EdiCustomerOrderTranslateService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly base = `${environment.apiBaseUrl}/api/EdiCustomerOrdersTranslations`;
|
||||
|
||||
getAll(): Promise<EdiCustomerOrderTranslateDto[]> {
|
||||
return firstValueFrom(this.http.get<EdiCustomerOrderTranslateDto[]>(this.base));
|
||||
}
|
||||
|
||||
delete(id: number): Promise<void> {
|
||||
return firstValueFrom(this.http.delete<void>(this.base, { params: { id: id.toString() } }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { EdiCustomerOrderDto, ErrorLogDto, ResponseModel } from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class EdiCustomerOrderService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly base = `${environment.apiBaseUrl}/api/EdiCustomerOrders`;
|
||||
|
||||
getAll(): Promise<EdiCustomerOrderDto[]> {
|
||||
return firstValueFrom(this.http.get<EdiCustomerOrderDto[]>(this.base));
|
||||
}
|
||||
|
||||
getByOrderNumber(customerOrderNumber: string): Promise<EdiCustomerOrderDto> {
|
||||
return firstValueFrom(
|
||||
this.http.get<EdiCustomerOrderDto>(`${this.base}/by-order-number`, { params: { customerOrderNumber } }),
|
||||
);
|
||||
}
|
||||
|
||||
getByRowPointer(rowPointer: string): Promise<EdiCustomerOrderDto> {
|
||||
return firstValueFrom(
|
||||
this.http.get<EdiCustomerOrderDto>(`${this.base}/by-order-number`, { params: { customerOrderNumber: rowPointer } }),
|
||||
);
|
||||
}
|
||||
|
||||
async sendToSyteline(rowPointer: string, identifier: string, externalIdentifier?: string): Promise<ResponseModel> {
|
||||
try {
|
||||
await firstValueFrom(
|
||||
this.http.post(`${this.base}/send-to-syteline`, null, { params: { customerOrderNumber: rowPointer } }),
|
||||
);
|
||||
return { status: 1, identifier, message: '', externalIdentifier: externalIdentifier ?? '' };
|
||||
} catch {
|
||||
const errors = await firstValueFrom(
|
||||
this.http.get<ErrorLogDto[]>(`${environment.apiBaseUrl}/api/ErrorLog/by-order-number`, {
|
||||
params: { customerOrderNumber: rowPointer },
|
||||
}),
|
||||
).catch(() => [] as ErrorLogDto[]);
|
||||
const message = errors.map((e) => e.errMsg).join('\n');
|
||||
return { status: 0, identifier, message, externalIdentifier: '' };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { FunctionDto } from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FunctionService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly base = `${environment.apiBaseUrl}/api/Functions`;
|
||||
|
||||
getAll(): Promise<FunctionDto[]> {
|
||||
return firstValueFrom(this.http.get<FunctionDto[]>(this.base));
|
||||
}
|
||||
|
||||
create(fn: FunctionDto): Promise<FunctionDto> {
|
||||
return firstValueFrom(this.http.post<FunctionDto>(this.base, fn));
|
||||
}
|
||||
|
||||
update(fn: FunctionDto): Promise<FunctionDto> {
|
||||
return firstValueFrom(this.http.put<FunctionDto>(this.base, fn));
|
||||
}
|
||||
|
||||
delete(id: string): Promise<void> {
|
||||
return firstValueFrom(this.http.delete<void>(this.base, { params: { id } }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { TaskSchedulerDto } from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HangfireService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly base = `${environment.apiBaseUrl}/api/HangfireJobs`;
|
||||
|
||||
getAll(): Promise<TaskSchedulerDto[]> {
|
||||
return firstValueFrom(this.http.get<TaskSchedulerDto[]>(`${this.base}/`));
|
||||
}
|
||||
|
||||
getById(id: string): Promise<TaskSchedulerDto> {
|
||||
return firstValueFrom(this.http.get<TaskSchedulerDto>(`${this.base}/${id}`));
|
||||
}
|
||||
|
||||
add(job: TaskSchedulerDto): Promise<void> {
|
||||
return firstValueFrom(this.http.post<void>(`${this.base}/add`, job));
|
||||
}
|
||||
|
||||
update(job: TaskSchedulerDto): Promise<void> {
|
||||
return firstValueFrom(this.http.post<void>(`${this.base}/update`, job));
|
||||
}
|
||||
|
||||
delete(job: TaskSchedulerDto): Promise<void> {
|
||||
return firstValueFrom(this.http.post<void>(`${this.base}/delete`, job));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Injectable, signal, computed } from '@angular/core';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class LoadingService {
|
||||
private readonly pendingCount = signal(0);
|
||||
readonly isLoading = computed(() => this.pendingCount() > 0);
|
||||
|
||||
show(): void {
|
||||
this.pendingCount.update((c) => c + 1);
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.pendingCount.update((c) => Math.max(0, c - 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { ProductDto } from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ProductService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly base = `${environment.apiBaseUrl}/api/Product`;
|
||||
|
||||
getByIndex(indexName: string): Promise<ProductDto[]> {
|
||||
return firstValueFrom(this.http.get<ProductDto[]>(`${this.base}/by-index`, { params: { indexName } }));
|
||||
}
|
||||
|
||||
update(product: ProductDto): Promise<void> {
|
||||
return firstValueFrom(this.http.put<void>(this.base, product));
|
||||
}
|
||||
}
|
||||
27
FaKrosnoManagerAngular/src/app/core/services/role.service.ts
Normal file
27
FaKrosnoManagerAngular/src/app/core/services/role.service.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { RoleDto } from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class RoleService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly base = `${environment.apiBaseUrl}/api/Roles`;
|
||||
|
||||
getAll(): Promise<RoleDto[]> {
|
||||
return firstValueFrom(this.http.get<RoleDto[]>(this.base));
|
||||
}
|
||||
|
||||
create(role: RoleDto): Promise<RoleDto> {
|
||||
return firstValueFrom(this.http.post<RoleDto>(this.base, role));
|
||||
}
|
||||
|
||||
update(role: RoleDto): Promise<RoleDto> {
|
||||
return firstValueFrom(this.http.put<RoleDto>(this.base, role));
|
||||
}
|
||||
|
||||
delete(id: string): Promise<void> {
|
||||
return firstValueFrom(this.http.delete<void>(this.base, { params: { id } }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { ScheduleOrderDto } from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ScheduleOrderService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly base = `${environment.apiBaseUrl}/api/ScheduleOrders`;
|
||||
|
||||
getAll(): Promise<ScheduleOrderDto[]> {
|
||||
return firstValueFrom(this.http.get<ScheduleOrderDto[]>(this.base));
|
||||
}
|
||||
|
||||
getById(id: number): Promise<ScheduleOrderDto> {
|
||||
return firstValueFrom(this.http.get<ScheduleOrderDto>(`${this.base}/${id}`));
|
||||
}
|
||||
}
|
||||
31
FaKrosnoManagerAngular/src/app/core/services/user.service.ts
Normal file
31
FaKrosnoManagerAngular/src/app/core/services/user.service.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { UserDto } from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UserService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly base = `${environment.apiBaseUrl}/api/Users`;
|
||||
|
||||
getAll(): Promise<UserDto[]> {
|
||||
return firstValueFrom(this.http.get<UserDto[]>(this.base));
|
||||
}
|
||||
|
||||
getById(id: string): Promise<UserDto> {
|
||||
return firstValueFrom(this.http.get<UserDto>(`${this.base}/by-id`, { params: { id } }));
|
||||
}
|
||||
|
||||
create(user: UserDto): Promise<UserDto> {
|
||||
return firstValueFrom(this.http.post<UserDto>(this.base, user));
|
||||
}
|
||||
|
||||
update(user: UserDto): Promise<UserDto> {
|
||||
return firstValueFrom(this.http.put<UserDto>(this.base, user));
|
||||
}
|
||||
|
||||
delete(id: string): Promise<void> {
|
||||
return firstValueFrom(this.http.delete<void>(this.base, { params: { id } }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import {
|
||||
CustomerOrderDto,
|
||||
ItemCustDto,
|
||||
MaterialTransactionDto,
|
||||
TransactionModel,
|
||||
WzClientDto,
|
||||
WzHeaderDto,
|
||||
WzRowMarelliDto,
|
||||
WzRowMeyleDto,
|
||||
} from '../models';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class WarehouseService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly base = environment.apiBaseUrl;
|
||||
|
||||
getWzHeaderById(id: string): Promise<WzHeaderDto> {
|
||||
return firstValueFrom(this.http.get<WzHeaderDto>(`${this.base}/api/WzHeader/by-id`, { params: { id } }));
|
||||
}
|
||||
|
||||
getAllClients(): Promise<WzClientDto[]> {
|
||||
return firstValueFrom(this.http.get<WzClientDto[]>(`${this.base}/api/WzClient`));
|
||||
}
|
||||
|
||||
getAllClientWzs(customerNumber: string, customerSequence: number): Promise<MaterialTransactionDto[]> {
|
||||
return firstValueFrom(
|
||||
this.http.get<MaterialTransactionDto[]>(`${this.base}/api/WzHeader/by-customer-number`, {
|
||||
params: { customerNumber, customerSequence: customerSequence.toString() },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getAllClientWzHeaders(customerNumber: string, customerSequence: number): Promise<WzHeaderDto[]> {
|
||||
return firstValueFrom(
|
||||
this.http.get<WzHeaderDto[]>(`${this.base}/api/WzHeader/all-wz-headers`, {
|
||||
params: { customerNumber, customerSequence: customerSequence.toString() },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
createWzHeader(header: WzHeaderDto): Promise<void> {
|
||||
return firstValueFrom(this.http.post<void>(`${this.base}/api/WzHeader`, header));
|
||||
}
|
||||
|
||||
createWzRowsMeyle(rows: WzRowMeyleDto[]): Promise<void> {
|
||||
return firstValueFrom(this.http.post<void>(`${this.base}/api/WzRowMeyle`, rows));
|
||||
}
|
||||
|
||||
createWzRowsMarelli(rows: WzRowMarelliDto[]): Promise<void> {
|
||||
return firstValueFrom(this.http.post<void>(`${this.base}/api/WzRowMarelli`, rows));
|
||||
}
|
||||
|
||||
getCustomerOrder(customerOrderNumber: string): Promise<CustomerOrderDto> {
|
||||
return firstValueFrom(
|
||||
this.http.get<CustomerOrderDto>(`${this.base}/api/CustomerOrders/by-co-number`, {
|
||||
params: { customerOrderNumber },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getItem(itemNumber: string, customerNumber: string): Promise<ItemCustDto> {
|
||||
return firstValueFrom(
|
||||
this.http.get<ItemCustDto>(`${this.base}/api/ItemCust`, { params: { itemNumber, customerNumber } }),
|
||||
);
|
||||
}
|
||||
|
||||
getWzRowsMeyleByWzHeaderId(wzHeaderId: string): Promise<WzRowMeyleDto[]> {
|
||||
return firstValueFrom(
|
||||
this.http.get<WzRowMeyleDto[]>(`${this.base}/api/WzRowMeyle/by-wz-header-id`, {
|
||||
params: { wzHeaderId },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getWzRowsMarelliByWzHeaderId(wzHeaderId: string): Promise<WzRowMarelliDto[]> {
|
||||
return firstValueFrom(
|
||||
this.http.get<WzRowMarelliDto[]>(`${this.base}/api/WzRowMarelli/by-wz-header-id`, {
|
||||
params: { wzHeaderId },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getTransactionsModels(): Promise<Record<string, TransactionModel[]>> {
|
||||
const materialTransactions = await firstValueFrom(
|
||||
this.http.get<MaterialTransactionDto[]>(`${this.base}/api/WzRowMeyle/transactions-with-part-number`),
|
||||
);
|
||||
const result: Record<string, TransactionModel[]> = {};
|
||||
for (const mt of materialTransactions) {
|
||||
const key = mt.nr_KARTY_KONTROLNEJ ?? '';
|
||||
if (!result[key]) result[key] = [];
|
||||
result[key].push({
|
||||
itemNumber: mt.item,
|
||||
partNumber: mt.nr_KARTY_KONTROLNEJ,
|
||||
quantity: mt.qty,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
updateWzRowsMeyle(rows: WzRowMeyleDto[]): Promise<void> {
|
||||
return firstValueFrom(this.http.put<void>(`${this.base}/api/WzRowMeyle`, rows));
|
||||
}
|
||||
|
||||
updateWzRowsMarelli(rows: WzRowMarelliDto[]): Promise<void> {
|
||||
return firstValueFrom(this.http.put<void>(`${this.base}/api/WzRowMarelli`, rows));
|
||||
}
|
||||
|
||||
generateXlsForMeyle(packListId: string): Promise<void> {
|
||||
return firstValueFrom(
|
||||
this.http.get<void>(`${this.base}/api/ExcelGenerator/generate-meyle`, { params: { packListId } }),
|
||||
);
|
||||
}
|
||||
|
||||
generateXlsForMarelli(packListId: string): Promise<void> {
|
||||
return firstValueFrom(
|
||||
this.http.get<void>(`${this.base}/api/ExcelGenerator/generate-marelli`, { params: { packListId } }),
|
||||
);
|
||||
}
|
||||
|
||||
addEmailsToWzHeader(wzHeaderId: string, emailAddresses: string): Promise<void> {
|
||||
return firstValueFrom(
|
||||
this.http.post<void>(`${this.base}/api/WzHeader/add-emails`, emailAddresses, {
|
||||
params: { id: wzHeaderId },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { RowToggler, SelectableRow, TableModule, TableRowSelectEvent } from 'primeng/table';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { CustomerOrderDto, CustomerOrderLineDto, CustomerOrderLineItemDto, ScheduleOrderDto } from '../core/models';
|
||||
import { CustomerOrderService } from '../core/services/customer-order.service';
|
||||
import { ScheduleOrderService } from '../core/services/schedule-order.service';
|
||||
import { ScheduleOrdersGridComponent } from '../shared/components/schedule-orders-grid/schedule-orders-grid.component';
|
||||
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-customer-order-detail',
|
||||
standalone: true,
|
||||
imports: [TableModule, RowToggler, SelectableRow, ButtonDirective, DatePipe, ScheduleOrdersGridComponent, PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
[title]="'Zamówienie klienta nr ' + (order()?.coNum ?? 'Brak numeru')"
|
||||
subtitle="Szczegóły zamówienia klienta w systemie Syteline"
|
||||
icon="pi pi-shopping-cart"
|
||||
>
|
||||
<app-content-section title="Informacje o zamówieniu" icon="pi pi-info-circle">
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<u>Numer Zamówienia:</u> <b>{{ order()?.coNum }}</b><br />
|
||||
<u>Numer Zamówienia Klienta:</u> <b>{{ order()?.custPo }}</b><br />
|
||||
<u>Klient:</u> <b>{{ order()?.custNum }}</b><br />
|
||||
<u>Numer Odbiorcy:</u> <b>{{ order()?.custSeq }}</b><br />
|
||||
<u>Kontakt:</u> <b>{{ order()?.contact }}</b><br />
|
||||
<u>Telefon:</u> <b>{{ order()?.phone }}</b><br />
|
||||
<u>Data Zamówienia:</u> <b>{{ order()?.orderDate | date: 'yyyy-MM-dd HH:mm:ss' }}</b><br />
|
||||
<u>Warunki:</u> <b>{{ order()?.termsCode }}</b><br />
|
||||
<u>Wartość Brutto:</u> <b>{{ order()?.price?.toFixed(2) ?? 'N/A' }}</b><br />
|
||||
<u>Status:</u> <b>{{ order()?.translatedStatus }}</b>
|
||||
</div>
|
||||
<div>
|
||||
<u>Magazyn:</u> <b>{{ order()?.whse }}</b><br />
|
||||
<u>VAT:</u> <b>{{ order()?.frtTaxCode1 }}</b><br />
|
||||
<u>Typ Odbiorcy:</u> <b>{{ order()?.endUserType }}</b><br />
|
||||
<u>Kurs Wymiany:</u> <b>{{ order()?.exchRate?.toFixed(4) ?? 'N/A' }}</b><br />
|
||||
<u>Gate:</u> <b>{{ order()?.uf_FKR_EDI_Gate }}</b><br />
|
||||
<u>RecipientCode:</u> <b>{{ order()?.uf_FKR_EDI_RecipientCode }}</b><br />
|
||||
<u>SellerCode:</u> <b>{{ order()?.uf_FKR_EDI_SellerCode }}</b><br />
|
||||
<u>SenderCode:</u> <b>{{ order()?.uf_FKR_EDI_SenderCode }}</b><br />
|
||||
<u>BuyerCode:</u> <b>{{ order()?.uf_FKR_EDI_BuyerCode }}</b><br />
|
||||
<u>Typ Dokumentu:</u> <b>{{ order()?.uf_DocType }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</app-content-section>
|
||||
|
||||
<app-content-section title="Powiązane zamówienia DELFOR" icon="pi pi-building">
|
||||
<button pButton type="button" (click)="toggleDelfors()">{{ delforButtonText() }}</button>
|
||||
@if (showDelfors()) {
|
||||
<div class="mt-3">
|
||||
<app-schedule-orders-grid [gridData]="scheduleOrders()" [pageSize]="5" />
|
||||
</div>
|
||||
}
|
||||
</app-content-section>
|
||||
|
||||
<app-content-section title="Indeksy" icon="pi pi-list" [flush]="true">
|
||||
<p-table [value]="lines()" dataKey="coLine" [paginator]="true" [rows]="10" (onRowSelect)="onLineSelect($event)" selectionMode="single" [(selection)]="selectedLine" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header>
|
||||
<tr><th style="width:3rem"></th><th>Linia</th><th>Pozycja</th><th>Pozycja Klienta</th><th>Opis</th><th>Ilość</th><th>J/M</th><th>Cena</th><th>Status</th></tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-line let-expanded="expanded">
|
||||
<tr [pSelectableRow]="line">
|
||||
<td><button pButton type="button" [text]="true" [rounded]="true" [iconOnly]="true" [pRowToggler]="line"><i [class]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i></button></td>
|
||||
<td>{{ line.coLine }}</td><td>{{ line.item }}</td><td>{{ line.custItem }}</td><td>{{ line.description }}</td>
|
||||
<td>{{ line.blanketQty?.toFixed(2) }}</td><td>{{ line.um }}</td><td>{{ line.contPrice }}</td><td>{{ line.translatedStatus }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #expandedrow let-line>
|
||||
<tr><td colspan="9">
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<h6>Szczegóły</h6>
|
||||
<u>Numer zamówienia:</u> <b>{{ line.coNum }}</b><br />
|
||||
<u>Linia:</u> <b>{{ line.coLine }}</b><br />
|
||||
<u>Pozycja:</u> <b>{{ line.item }}</b><br />
|
||||
<u>Pozycja Klienta:</u> <b>{{ line.custItem }}</b><br />
|
||||
<u>Opis:</u> <b>{{ line.description }}</b><br />
|
||||
<u>Łączna Ilość:</u> <b>{{ line.blanketQty?.toFixed(2) }}</b><br />
|
||||
<u>Status:</u> <b>{{ line.translatedStatus }}</b>
|
||||
</div>
|
||||
<div>
|
||||
<u>Cena:</u> <b>{{ line.contPrice?.toFixed(2) ?? 'N/A' }}</b><br />
|
||||
<u>Ważne Od:</u> <b>{{ line.effDate | date: 'dd.MM.yyyy' }}</b><br />
|
||||
<u>J/M:</u> <b>{{ line.um }}</b><br />
|
||||
<u>BoxType:</u> <b>{{ line.uf_FKR_EDI_BLN_BoxType }}</b><br />
|
||||
<u>Address:</u> <b>{{ line.uf_FKR_EDI_BLN_Address }}</b><br />
|
||||
<u>FinalDestination:</u> <b>{{ line.uf_FKR_EDI_BLN_FinalDestination }}</b><br />
|
||||
<u>QtyPerBox:</u> <b>{{ line.uf_FKR_EDI_BLN_QtyPerBox }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</td></tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
|
||||
@if (lineItems().length) {
|
||||
<app-content-section title="Harmonogramy" icon="pi pi-calendar" [flush]="true">
|
||||
<p-table [value]="lineItems()" [paginator]="true" [rows]="10" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header>
|
||||
<tr><th>Linia</th><th>Zwolnienie</th><th>Pozycja</th><th>Pozycja Klienta</th><th>Łączna Ilość</th><th>Data Wykonania</th><th>Status</th></tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-item>
|
||||
<tr>
|
||||
<td>{{ item.coLine }}</td><td>{{ item.coRelease }}</td><td>{{ item.item }}</td><td>{{ item.custItem }}</td>
|
||||
<td>{{ item.qtyOrdered?.toFixed(2) }}</td><td>{{ item.dueDate | date: 'dd.MM.yyyy' }}</td><td>{{ item.translatedStatus }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
}
|
||||
</app-page-shell>
|
||||
`,
|
||||
})
|
||||
export class CustomerOrderDetailComponent implements OnInit {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly customerOrderService = inject(CustomerOrderService);
|
||||
private readonly scheduleOrderService = inject(ScheduleOrderService);
|
||||
|
||||
readonly order = signal<CustomerOrderDto | null>(null);
|
||||
readonly lines = signal<CustomerOrderLineDto[]>([]);
|
||||
readonly lineItems = signal<CustomerOrderLineItemDto[]>([]);
|
||||
readonly scheduleOrders = signal<ScheduleOrderDto[]>([]);
|
||||
readonly showDelfors = signal(false);
|
||||
readonly delforButtonText = signal('Pokaż powiązane zamówienia DELFOR');
|
||||
selectedLine: CustomerOrderLineDto | null = null;
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
const id = this.route.snapshot.paramMap.get('customerOrderId') ?? '';
|
||||
const data = await this.customerOrderService.getByRowPointer(id);
|
||||
this.order.set(data);
|
||||
this.lines.set(data.customerOrderLines ?? []);
|
||||
}
|
||||
|
||||
onLineSelect(event: TableRowSelectEvent<CustomerOrderLineDto>): void {
|
||||
const data = event.data;
|
||||
if (data && !Array.isArray(data)) {
|
||||
this.lineItems.set(data.customerOrderLineItems ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleDelfors(): Promise<void> {
|
||||
if (this.showDelfors()) {
|
||||
this.showDelfors.set(false);
|
||||
this.delforButtonText.set('Pokaż powiązane zamówienia DELFOR');
|
||||
this.scheduleOrders.set([]);
|
||||
return;
|
||||
}
|
||||
const order = this.order();
|
||||
const ids = [...new Set((order?.ediCustomerOrderTranslates ?? []).map((x) => x.scheduleOrderId))];
|
||||
if (ids.length) {
|
||||
const all = await this.scheduleOrderService.getAll();
|
||||
this.scheduleOrders.set(all.filter((x) => ids.includes(x.id)).sort((a, b) => new Date(b.lastUpdateDate).getTime() - new Date(a.lastUpdateDate).getTime()));
|
||||
}
|
||||
this.showDelfors.set(true);
|
||||
this.delforButtonText.set('Ukryj');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { ConfirmationService } from 'primeng/api';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { EdiCustomerOrderTranslateDto } from '../core/models';
|
||||
import { EdiCustomerOrderTranslateService } from '../core/services/edi-customer-order-translate.service';
|
||||
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-customer-orders-translations',
|
||||
standalone: true,
|
||||
imports: [TableModule, ButtonDirective, DatePipe, PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
title="Zarządzanie powiązaniami zamówień z DELFORami"
|
||||
subtitle="Powiązania między zamówieniami EDI, SL i harmonogramami DELFOR"
|
||||
icon="pi pi-link"
|
||||
>
|
||||
<app-content-section title="Powiązania" icon="pi pi-sitemap" [flush]="true">
|
||||
<p-table [value]="translations()" [paginator]="true" [rows]="10" dataKey="id" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th>DELFOR Id</th>
|
||||
<th>Numer zamówienia EDI</th>
|
||||
<th>Numer zamówienia SL</th>
|
||||
<th>Data utworzenia</th>
|
||||
<th style="width: 6rem"></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-row>
|
||||
<tr>
|
||||
<td>{{ row.scheduleOrderId }}</td>
|
||||
<td>{{ row.ediCoCoNum }}</td>
|
||||
<td>{{ row.coCoNum }}</td>
|
||||
<td>{{ row.createdDate | date: 'dd.MM.yyyy HH:mm' }}</td>
|
||||
<td><button pButton type="button" severity="danger" [text]="true" [iconOnly]="true" (click)="confirmDelete(row)"><i class="pi pi-trash"></i></button></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
</app-page-shell>
|
||||
`,
|
||||
})
|
||||
export class CustomerOrdersTranslationsComponent implements OnInit {
|
||||
private readonly translateService = inject(EdiCustomerOrderTranslateService);
|
||||
private readonly confirmation = inject(ConfirmationService);
|
||||
|
||||
readonly translations = signal<EdiCustomerOrderTranslateDto[]>([]);
|
||||
|
||||
async ngOnInit(): Promise<void> { await this.load(); }
|
||||
|
||||
confirmDelete(row: EdiCustomerOrderTranslateDto): void {
|
||||
this.confirmation.confirm({
|
||||
message: 'Czy na pewno usunąć to powiązanie?',
|
||||
header: 'Potwierdzenie',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptLabel: 'Tak',
|
||||
rejectLabel: 'Anuluj',
|
||||
accept: () => this.delete(row),
|
||||
});
|
||||
}
|
||||
|
||||
private async delete(row: EdiCustomerOrderTranslateDto): Promise<void> {
|
||||
await this.translateService.delete(row.id);
|
||||
await this.load();
|
||||
}
|
||||
|
||||
private async load(): Promise<void> {
|
||||
const data = await this.translateService.getAll();
|
||||
this.translations.set([...data].sort((a, b) => new Date(b.createdDate).getTime() - new Date(a.createdDate).getTime()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { RowToggler, TableModule } from 'primeng/table';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { CustomerOrderDto } from '../core/models';
|
||||
import { CustomerOrderService } from '../core/services/customer-order.service';
|
||||
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-customer-orders-list',
|
||||
standalone: true,
|
||||
imports: [TableModule, RowToggler, ButtonDirective, DatePipe, PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
title="Zamówienia Klienta"
|
||||
subtitle="Przegląd zamówień klientów w systemie Syteline"
|
||||
icon="pi pi-shopping-cart"
|
||||
>
|
||||
<app-content-section title="Lista zamówień" icon="pi pi-list" [flush]="true">
|
||||
<p-table [value]="orders()" [paginator]="true" [rows]="10" dataKey="rowPointer" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th style="width: 3rem"></th>
|
||||
<th pSortableColumn="coNum">Numer Zamówienia</th>
|
||||
<th pSortableColumn="custPo">Zamówienie Klienta</th>
|
||||
<th pSortableColumn="custNum">Numer Klienta</th>
|
||||
<th pSortableColumn="custSeq">Odbiorca</th>
|
||||
<th pSortableColumn="createDate">Data zamówienia</th>
|
||||
<th pSortableColumn="translatedStatus">Status</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-order let-expanded="expanded">
|
||||
<tr (dblclick)="openDetail(order.rowPointer)">
|
||||
<td><button pButton type="button" [text]="true" [rounded]="true" [iconOnly]="true" [pRowToggler]="order"><i [class]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i></button></td>
|
||||
<td>{{ order.coNum }}</td>
|
||||
<td>{{ order.custPo }}</td>
|
||||
<td>{{ order.custNum }}</td>
|
||||
<td>{{ order.custSeq }}</td>
|
||||
<td>{{ order.createDate | date: 'dd.MM.yyyy HH:mm:ss' }}</td>
|
||||
<td>{{ order.translatedStatus }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #expandedrow let-order>
|
||||
<tr><td colspan="7">
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<u>Numer Zamówienia:</u> <b>{{ order.coNum }}</b><br />
|
||||
<u>Numer Zamówienia Klienta:</u> <b>{{ order.custPo }}</b><br />
|
||||
<u>Klient:</u> <b>{{ order.custNum }}</b><br />
|
||||
<u>Numer Odbiorcy:</u> <b>{{ order.custSeq }}</b><br />
|
||||
<u>Kontakt:</u> <b>{{ order.contact }}</b><br />
|
||||
<u>Telefon:</u> <b>{{ order.phone }}</b><br />
|
||||
<u>Data Zamówienia:</u> <b>{{ order.orderDate | date: 'yyyy-MM-dd HH:mm:ss' }}</b><br />
|
||||
<u>Warunki:</u> <b>{{ order.termsCode }}</b><br />
|
||||
<u>Wartość Brutto:</u> <b>{{ order.price?.toFixed(2) ?? 'N/A' }}</b><br />
|
||||
<u>Status:</u> <b>{{ order.translatedStatus }}</b>
|
||||
</div>
|
||||
<div>
|
||||
<u>Magazyn:</u> <b>{{ order.whse }}</b><br />
|
||||
<u>VAT:</u> <b>{{ order.frtTaxCode1 }}</b><br />
|
||||
<u>Typ Odbiorcy:</u> <b>{{ order.endUserType }}</b><br />
|
||||
<u>Kurs Wymiany:</u> <b>{{ order.exchRate?.toFixed(4) ?? 'N/A' }}</b><br />
|
||||
<u>Gate:</u> <b>{{ order.uf_FKR_EDI_Gate }}</b><br />
|
||||
<u>RecipientCode:</u> <b>{{ order.uf_FKR_EDI_RecipientCode }}</b><br />
|
||||
<u>SellerCode:</u> <b>{{ order.uf_FKR_EDI_SellerCode }}</b><br />
|
||||
<u>SenderCode:</u> <b>{{ order.uf_FKR_EDI_SenderCode }}</b><br />
|
||||
<u>BuyerCode:</u> <b>{{ order.uf_FKR_EDI_BuyerCode }}</b><br />
|
||||
<u>Typ Dokumentu:</u> <b>{{ order.uf_DocType }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</td></tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
</app-page-shell>
|
||||
`,
|
||||
})
|
||||
export class CustomerOrdersListComponent implements OnInit {
|
||||
private readonly customerOrderService = inject(CustomerOrderService);
|
||||
private readonly router = inject(Router);
|
||||
readonly orders = signal<CustomerOrderDto[]>([]);
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
const data = await this.customerOrderService.getAll();
|
||||
this.orders.set([...data].sort((a, b) => new Date(b.createDate).getTime() - new Date(a.createDate).getTime()));
|
||||
}
|
||||
|
||||
openDetail(id: string): void {
|
||||
this.router.navigate(['/Admin/PK/CustomerOrder', id]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { RowToggler, SelectableRow, TableModule, TableRowSelectEvent } from 'primeng/table';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { EdiCustomerOrderDto, EdiCustomerOrderLineDto, EdiCustomerOrderLineItemDto } from '../core/models';
|
||||
import { EdiCustomerOrderService } from '../core/services/edi-customer-order.service';
|
||||
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edi-customer-order-detail',
|
||||
standalone: true,
|
||||
imports: [TableModule, RowToggler, SelectableRow, ButtonDirective, DatePipe, PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
[title]="'Zamówienie klienta EDI nr ' + (order()?.customerOrderNumber ?? 'Brak numeru')"
|
||||
subtitle="Szczegóły zamówienia otrzymanego przez EDI"
|
||||
icon="pi pi-database"
|
||||
>
|
||||
<app-content-section title="Informacje o zamówieniu" icon="pi pi-info-circle">
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<u>Numer zamówienia EDI:</u> <b>{{ order()?.customerOrderNumber }}</b><br />
|
||||
<u>Numer zamówienia Klienta:</u> <b>{{ order()?.customerPoNumber }}</b><br />
|
||||
<u>Numer klienta:</u> <b>{{ order()?.customerNumber }}</b><br />
|
||||
<u>Klient:</u> <b>{{ order()?.customerName }}</b><br />
|
||||
<u>Numer odbiorcy:</u> <b>{{ order()?.customerSequence ?? 'N/A' }}</b><br />
|
||||
<u>Data otrzymania:</u> <b>{{ order()?.recivedDate | date: 'dd.MM.yyyy' }}</b><br />
|
||||
<u>Wysłano do Syteline?:</u> <b>{{ (order()?.posted ?? 0) === 0 ? 'NIE' : 'TAK' }}</b><br />
|
||||
<u>Data wysyłki do Syteline:</u> <b>{{ order()?.postedDate | date: 'dd.MM.yyyy' }}</b><br />
|
||||
<u>Data zamówienia:</u> <b>{{ order()?.orderDate | date: 'dd.MM.yyyy' }}</b>
|
||||
</div>
|
||||
<div>
|
||||
<u>Cena:</u> <b>{{ order()?.price?.toFixed(2) ?? 'N/A' }}</b><br />
|
||||
<u>Waga:</u> <b>{{ order()?.weight?.toFixed(2) ?? 'N/A' }}</b><br />
|
||||
<u>Magazyn:</u> <b>{{ order()?.warehouse }}</b><br />
|
||||
<u>Gate:</u> <b>{{ order()?.gate }}</b><br />
|
||||
<u>Kod odbiorcy:</u> <b>{{ order()?.recipientCode }}</b><br />
|
||||
<u>Kod wysyłającego:</u> <b>{{ order()?.senderCode }}</b><br />
|
||||
<u>Kod sprzedawcy:</u> <b>{{ order()?.sellerCode }}</b><br />
|
||||
<u>Kod kupującego:</u> <b>{{ order()?.buyerCode }}</b><br />
|
||||
<u>Typ dokumentu:</u> <b>{{ order()?.docType }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</app-content-section>
|
||||
|
||||
<app-content-section title="Indeksy" icon="pi pi-list" [flush]="true">
|
||||
<p-table [value]="lines()" dataKey="customerOrderLine" [paginator]="true" [rows]="10" selectionMode="single" [(selection)]="selectedLine" (onRowSelect)="onLineSelect($event)" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header>
|
||||
<tr><th style="width:3rem"></th><th>Linia</th><th>Pozycja</th><th>Pozycja Klienta</th><th>Opis</th><th>Ilość</th><th>J/M</th><th>Cena</th><th>Status</th></tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-line let-expanded="expanded">
|
||||
<tr [pSelectableRow]="line">
|
||||
<td><button pButton type="button" [text]="true" [rounded]="true" [iconOnly]="true" [pRowToggler]="line"><i [class]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i></button></td>
|
||||
<td>{{ line.customerOrderLine }}</td><td>{{ line.item }}</td><td>{{ line.customerItemNumber }}</td><td>{{ line.description }}</td>
|
||||
<td>{{ line.blanketQty?.toFixed(2) }}</td><td>{{ line.uom }}</td><td>{{ line.contPrice }}</td><td>{{ line.translatedStatus }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #expandedrow let-line>
|
||||
<tr><td colspan="9">
|
||||
<div class="form-grid">
|
||||
<div>
|
||||
<h6>Szczegóły</h6>
|
||||
<u>Numer zamówienia EDI:</u> <b>{{ line.customerOrderNumber }}</b><br />
|
||||
<u>Linia:</u> <b>{{ line.customerOrderLine }}</b><br />
|
||||
<u>Pozycja:</u> <b>{{ line.item }}</b><br />
|
||||
<u>Pozycja Klienta:</u> <b>{{ line.customerItemNumber }}</b><br />
|
||||
<u>Opis:</u> <b>{{ line.description }}</b><br />
|
||||
<u>Łączna Ilość:</u> <b>{{ line.blanketQty?.toFixed(2) ?? 'N/A' }}</b><br />
|
||||
<u>Status:</u> <b>{{ line.translatedStatus }}</b>
|
||||
</div>
|
||||
<div>
|
||||
<u>Cena:</u> <b>{{ line.contPrice?.toFixed(2) ?? 'N/A' }}</b><br />
|
||||
<u>Ważne Od:</u> <b>{{ line.effectiveDate | date: 'dd.MM.yyyy' }}</b><br />
|
||||
<u>J/M:</u> <b>{{ line.uom }}</b><br />
|
||||
<u>BoxType:</u> <b>{{ line.boxType }}</b><br />
|
||||
<u>Address:</u> <b>{{ line.address }}</b><br />
|
||||
<u>FinalDestination:</u> <b>{{ line.finalDestination }}</b><br />
|
||||
<u>QtyPerBox:</u> <b>{{ line.qtyPerBox }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</td></tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
|
||||
@if (lineItems().length) {
|
||||
<app-content-section title="Harmonogramy" icon="pi pi-calendar" [flush]="true">
|
||||
<p-table [value]="lineItems()" [paginator]="true" [rows]="10" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header>
|
||||
<tr><th>Linia</th><th>Zwolnienie</th><th>Pozycja</th><th>Pozycja Klienta</th><th>Łączna Ilość</th><th>Data Wykonania</th><th>Status</th></tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-item>
|
||||
<tr>
|
||||
<td>{{ item.customerOrderLine }}</td><td>{{ item.customerOrderRelease }}</td><td>{{ item.item }}</td><td>{{ item.customerItem }}</td>
|
||||
<td>{{ item.qtyOrdered?.toFixed(2) }}</td><td>{{ item.dueDate | date: 'dd.MM.yyyy' }}</td><td>{{ item.translatedStatus }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
}
|
||||
</app-page-shell>
|
||||
`,
|
||||
})
|
||||
export class EdiCustomerOrderDetailComponent implements OnInit {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly ediService = inject(EdiCustomerOrderService);
|
||||
|
||||
readonly order = signal<EdiCustomerOrderDto | null>(null);
|
||||
readonly lines = signal<EdiCustomerOrderLineDto[]>([]);
|
||||
readonly lineItems = signal<EdiCustomerOrderLineItemDto[]>([]);
|
||||
selectedLine: EdiCustomerOrderLineDto | null = null;
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
const id = this.route.snapshot.paramMap.get('customerOrderId') ?? '';
|
||||
const data = await this.ediService.getByRowPointer(id);
|
||||
this.order.set(data);
|
||||
this.lines.set(data.ediCustomerOrderLines ?? []);
|
||||
}
|
||||
|
||||
onLineSelect(event: TableRowSelectEvent<EdiCustomerOrderLineDto>): void {
|
||||
const data = event.data;
|
||||
if (data && !Array.isArray(data)) {
|
||||
this.lineItems.set(data.ediCustomerOrderLineItems ?? []);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TableModule, TableCheckbox, TableHeaderCheckbox } from 'primeng/table';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { ToggleSwitchModule } from 'primeng/toggleswitch';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { EdiCustomerOrderDto, ResponseModel } from '../core/models';
|
||||
import { EdiCustomerOrderService } from '../core/services/edi-customer-order.service';
|
||||
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edi-customer-orders-list',
|
||||
standalone: true,
|
||||
imports: [TableModule, TableCheckbox, TableHeaderCheckbox, ButtonDirective, ToggleSwitchModule, DialogModule, FormsModule, DatePipe, PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
title="Zamówienia Klienta EDI"
|
||||
subtitle="Zamówienia otrzymane przez EDI i księgowanie do Syteline"
|
||||
icon="pi pi-database"
|
||||
>
|
||||
<app-content-section title="Lista zamówień EDI" icon="pi pi-list" [flush]="true">
|
||||
<div class="data-toolbar mb-3">
|
||||
<div class="data-toolbar__left">
|
||||
<label class="font-medium">Pokaż wszystkie</label>
|
||||
<p-toggleswitch [(ngModel)]="showAll" (ngModelChange)="loadData()" />
|
||||
<span class="text-secondary">{{ showAll ? 'Pokaż wszystkie' : 'Pokaż tylko Wysłane do SL' }}</span>
|
||||
</div>
|
||||
<div class="data-toolbar__right">
|
||||
@if (showPostButton()) {
|
||||
<button pButton type="button" (click)="sendToSyteline()">{{ postButtonText() }}</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p-table [value]="orders()" [(selection)]="selectedOrders" dataKey="rowPointer" [paginator]="true" [rows]="10" selectionMode="multiple" (selectionChange)="onSelectionChange()" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header>
|
||||
<tr><th style="width:3rem"><p-table-header-checkbox /></th><th>Numer Zamówienia</th><th>Zamówienie Klienta</th><th>Numer Klienta</th><th>Odbiorca</th><th>Data Otrzymania</th><th>Zamówienie SL</th><th>Wysłane do SL</th></tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-order>
|
||||
<tr (dblclick)="openDetail(order.rowPointer)">
|
||||
<td><p-table-checkbox [value]="order" /></td>
|
||||
<td>{{ order.customerOrderNumber }}</td><td>{{ order.customerPoNumber }}</td><td>{{ order.customerNumber }}</td>
|
||||
<td>{{ order.customerSequence }}</td><td>{{ order.createDate | date: 'dd.MM.yyyy' }}</td>
|
||||
<td>{{ order.slOrderNumber }}</td><td>{{ order.sentToSl }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
</app-page-shell>
|
||||
|
||||
<p-dialog [header]="postButtonText()" [(visible)]="showResultDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
@for (r of responses(); track r.identifier) {
|
||||
@if (r.status === 1) {
|
||||
<p>Zamówienie EDI {{ r.identifier }} zostało poprawnie zaksięgowane w Zamówieniach klienta pod numerem '{{ r.externalIdentifier }}'</p>
|
||||
} @else {
|
||||
<p>Błąd: Zamówienie EDI {{ r.identifier }} nie zostało poprawnie zaksięgowane w Zamówieniach klienta.<br />Lista błędów:<br />{{ r.message }}</p>
|
||||
}
|
||||
}
|
||||
<ng-template #footer><button pButton type="button" (click)="showResultDialog = false">OK</button></ng-template>
|
||||
</p-dialog>
|
||||
`,
|
||||
})
|
||||
export class EdiCustomerOrdersListComponent implements OnInit {
|
||||
private readonly ediService = inject(EdiCustomerOrderService);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
readonly orders = signal<EdiCustomerOrderDto[]>([]);
|
||||
readonly responses = signal<ResponseModel[]>([]);
|
||||
readonly showPostButton = signal(false);
|
||||
readonly postButtonText = signal('Księguj bieżący');
|
||||
|
||||
showAll = false;
|
||||
selectedOrders: EdiCustomerOrderDto[] = [];
|
||||
showResultDialog = false;
|
||||
|
||||
async ngOnInit(): Promise<void> { await this.loadData(); }
|
||||
|
||||
async loadData(): Promise<void> {
|
||||
let data = await this.ediService.getAll();
|
||||
if (!this.showAll) data = data.filter((x) => x.posted === 0);
|
||||
this.orders.set([...data].sort((a, b) => new Date(b.createDate).getTime() - new Date(a.createDate).getTime()));
|
||||
}
|
||||
|
||||
onSelectionChange(): void {
|
||||
this.showPostButton.set(this.selectedOrders.some((x) => x.posted === 0));
|
||||
this.postButtonText.set(this.selectedOrders.length > 1 ? 'Księguj zaznaczone' : 'Księguj bieżący');
|
||||
}
|
||||
|
||||
async sendToSyteline(): Promise<void> {
|
||||
if (!this.selectedOrders.length) return;
|
||||
const results: ResponseModel[] = [];
|
||||
for (const order of this.selectedOrders) {
|
||||
const response = await this.ediService.sendToSyteline(order.rowPointer, order.customerOrderNumber);
|
||||
if (response.status === 1) {
|
||||
response.externalIdentifier = order.ediCustomerOrderTranslates?.[0]?.coCoNum ?? '';
|
||||
}
|
||||
results.push(response);
|
||||
}
|
||||
this.responses.set(results);
|
||||
this.showResultDialog = true;
|
||||
this.showPostButton.set(false);
|
||||
await this.loadData();
|
||||
}
|
||||
|
||||
openDetail(id: string): void { this.router.navigate(['/Admin/PK/EdiCustomerOrder', id]); }
|
||||
}
|
||||
33
FaKrosnoManagerAngular/src/app/error/error.component.ts
Normal file
33
FaKrosnoManagerAngular/src/app/error/error.component.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-error',
|
||||
standalone: true,
|
||||
imports: [PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell title="Error" subtitle="An error occurred while processing your request" icon="pi pi-exclamation-triangle">
|
||||
<app-content-section>
|
||||
<h1 class="error-title">Error.</h1>
|
||||
<h2 class="error-subtitle">An error occurred while processing your request.</h2>
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the
|
||||
<strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
</app-content-section>
|
||||
</app-page-shell>
|
||||
`,
|
||||
styles: `
|
||||
.error-title { color: var(--color-error); margin: 0 0 var(--spacing-2); }
|
||||
.error-subtitle { color: var(--color-error); font-size: var(--font-size-lg); margin: 0 0 var(--spacing-4); }
|
||||
`,
|
||||
})
|
||||
export class ErrorComponent {}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { ProgressSpinner } from 'primeng/progressspinner';
|
||||
import { ToastModule } from 'primeng/toast';
|
||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||
import { SidebarComponent } from '../sidebar/sidebar.component';
|
||||
import { TopbarComponent } from '../topbar/topbar.component';
|
||||
import { LoadingService } from '../../core/services/loading.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main-layout',
|
||||
standalone: true,
|
||||
imports: [RouterOutlet, SidebarComponent, TopbarComponent, ProgressSpinner, ToastModule, ConfirmDialogModule],
|
||||
template: `
|
||||
<p-toast position="top-right" />
|
||||
<p-confirm-dialog />
|
||||
@if (loading.isLoading()) {
|
||||
<div class="loading-overlay" role="status" aria-live="polite" aria-label="Ładowanie">
|
||||
<div class="loading-overlay__card">
|
||||
<p-progress-spinner ariaLabel="Ładowanie" strokeWidth="3" styleClass="loading-spinner" />
|
||||
<span>Ładowanie danych…</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="app-shell">
|
||||
<app-sidebar />
|
||||
<div class="app-shell__main">
|
||||
<app-topbar />
|
||||
<main class="app-shell__content">
|
||||
<router-outlet />
|
||||
</main>
|
||||
<footer class="app-shell__footer">
|
||||
FA Krosno Manager © {{ year }}
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: `
|
||||
.app-shell {
|
||||
display: flex;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.app-shell__main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-shell__content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: var(--content-padding);
|
||||
}
|
||||
|
||||
.app-shell__footer {
|
||||
flex-shrink: 0;
|
||||
padding: var(--spacing-2) var(--content-padding);
|
||||
text-align: center;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-muted);
|
||||
border-top: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.loading-overlay__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
padding: var(--spacing-5) var(--spacing-6);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
:host ::ng-deep .loading-spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class MainLayoutComponent {
|
||||
readonly loading = inject(LoadingService);
|
||||
readonly year = new Date().getFullYear();
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { Router, RouterLink, RouterLinkActive, NavigationEnd } from '@angular/router';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { filter, map, startWith } from 'rxjs';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
|
||||
interface NavItem {
|
||||
label: string;
|
||||
icon: string;
|
||||
route: string;
|
||||
exact?: boolean;
|
||||
}
|
||||
|
||||
interface NavGroup {
|
||||
label?: string;
|
||||
items: NavItem[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-sidebar',
|
||||
standalone: true,
|
||||
imports: [RouterLink, RouterLinkActive, TooltipModule],
|
||||
template: `
|
||||
<aside class="sidebar" [class.sidebar--collapsed]="collapsed()">
|
||||
<div class="sidebar__brand">
|
||||
@if (!collapsed()) {
|
||||
<img src="logo.svg" width="28" height="28" alt="" class="sidebar__logo" />
|
||||
<span class="sidebar__brand-text">FA Manager</span>
|
||||
} @else {
|
||||
<img src="logo.svg" width="24" height="24" alt="" class="sidebar__logo" />
|
||||
}
|
||||
</div>
|
||||
|
||||
<nav class="sidebar__nav">
|
||||
@for (group of navGroups(); track group.label ?? 'main') {
|
||||
@if (group.label && !collapsed()) {
|
||||
<div class="sidebar__group-label">{{ group.label }}</div>
|
||||
}
|
||||
@for (item of group.items; track item.route) {
|
||||
<a
|
||||
class="sidebar__link"
|
||||
[routerLink]="item.route"
|
||||
routerLinkActive="sidebar__link--active"
|
||||
[routerLinkActiveOptions]="{ exact: item.exact ?? false }"
|
||||
[pTooltip]="collapsed() ? item.label : undefined"
|
||||
tooltipPosition="right"
|
||||
>
|
||||
<i [class]="item.icon" class="sidebar__link-icon"></i>
|
||||
@if (!collapsed()) {
|
||||
<span class="sidebar__link-label">{{ item.label }}</span>
|
||||
}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
</nav>
|
||||
|
||||
<div class="sidebar__footer">
|
||||
<button type="button" class="sidebar__collapse-btn" (click)="toggleCollapsed()" [attr.aria-label]="collapsed() ? 'Rozwiń menu' : 'Zwiń menu'">
|
||||
<i [class]="collapsed() ? 'pi pi-angle-double-right' : 'pi pi-angle-double-left'"></i>
|
||||
@if (!collapsed()) {
|
||||
<span>Zwiń menu</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
`,
|
||||
styles: `
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
min-width: var(--sidebar-width);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-sidebar-bg);
|
||||
border-right: 1px solid var(--color-sidebar-border);
|
||||
transition: width 0.2s ease, min-width 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar--collapsed {
|
||||
width: var(--sidebar-collapsed-width);
|
||||
min-width: var(--sidebar-collapsed-width);
|
||||
}
|
||||
|
||||
.sidebar__brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
height: var(--topbar-height);
|
||||
padding: 0 var(--spacing-4);
|
||||
border-bottom: 1px solid var(--color-sidebar-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .sidebar__brand {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar__logo {
|
||||
filter: brightness(0) invert(1);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.sidebar__brand-text {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 700;
|
||||
color: var(--color-sidebar-text-active);
|
||||
letter-spacing: -0.01em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar__nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: var(--spacing-3) var(--spacing-2);
|
||||
}
|
||||
|
||||
.sidebar__group-label {
|
||||
padding: var(--spacing-3) var(--spacing-3) var(--spacing-1);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--color-sidebar-text-muted);
|
||||
}
|
||||
|
||||
.sidebar__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
padding: 9px var(--spacing-3);
|
||||
margin-bottom: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-sidebar-text);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .sidebar__link {
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.sidebar__link:hover {
|
||||
background: var(--color-sidebar-surface);
|
||||
color: var(--color-sidebar-text-active);
|
||||
}
|
||||
|
||||
.sidebar__link--active {
|
||||
background: var(--color-sidebar-accent-soft) !important;
|
||||
color: var(--color-sidebar-text-active) !important;
|
||||
}
|
||||
|
||||
.sidebar__link--active .sidebar__link-icon {
|
||||
color: var(--color-sidebar-accent);
|
||||
}
|
||||
|
||||
.sidebar__link-icon {
|
||||
font-size: 15px;
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--color-sidebar-text-muted);
|
||||
}
|
||||
|
||||
.sidebar__link-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sidebar__footer {
|
||||
padding: var(--spacing-2);
|
||||
border-top: 1px solid var(--color-sidebar-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar__collapse-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
width: 100%;
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--color-sidebar-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .sidebar__collapse-btn {
|
||||
justify-content: center;
|
||||
padding: var(--spacing-2);
|
||||
}
|
||||
|
||||
.sidebar__collapse-btn:hover {
|
||||
background: var(--color-sidebar-surface);
|
||||
color: var(--color-sidebar-text);
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class SidebarComponent {
|
||||
private readonly router = inject(Router);
|
||||
|
||||
readonly collapsed = signal(false);
|
||||
|
||||
private readonly url = toSignal(
|
||||
this.router.events.pipe(
|
||||
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
||||
map((e) => e.urlAfterRedirects),
|
||||
startWith(this.router.url),
|
||||
),
|
||||
{ initialValue: this.router.url },
|
||||
);
|
||||
|
||||
readonly isAdminRoute = computed(() => this.url().split('?')[0].toLowerCase().startsWith('/admin'));
|
||||
|
||||
readonly navGroups = computed<NavGroup[]>(() => {
|
||||
const main: NavGroup = {
|
||||
items: [
|
||||
{ label: 'Zamówienia DELFOR', icon: 'pi pi-building', route: '/', exact: true },
|
||||
{ label: 'Zarządzanie Indeksami', icon: 'pi pi-box', route: '/Products' },
|
||||
{ label: 'Magazyn', icon: 'pi pi-warehouse', route: '/Warehouse' },
|
||||
],
|
||||
};
|
||||
|
||||
const groups = [main];
|
||||
|
||||
if (this.isAdminRoute()) {
|
||||
groups.push({
|
||||
label: 'Administracja',
|
||||
items: [
|
||||
{ label: 'Użytkownicy', icon: 'pi pi-users', route: '/Admin/PK/UsersManager' },
|
||||
{ label: 'Scheduler', icon: 'pi pi-calendar', route: '/Admin/PK/Scheduler' },
|
||||
{ label: 'Zamówienia klienta EDI', icon: 'pi pi-list-check', route: '/Admin/PK/EdiCustomerOrders' },
|
||||
{ label: 'Zamówienia klienta', icon: 'pi pi-database', route: '/Admin/PK/CustomerOrders' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
});
|
||||
|
||||
toggleCollapsed(): void {
|
||||
this.collapsed.update((v) => !v);
|
||||
}
|
||||
}
|
||||
203
FaKrosnoManagerAngular/src/app/layout/topbar/topbar.component.ts
Normal file
203
FaKrosnoManagerAngular/src/app/layout/topbar/topbar.component.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { Router, NavigationEnd } from '@angular/router';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { filter, map, startWith } from 'rxjs';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { BreadcrumbModule } from 'primeng/breadcrumb';
|
||||
import { MenuModule } from 'primeng/menu';
|
||||
import { MenuItem } from 'primeng/api';
|
||||
import { AuthService } from '../../core/services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-topbar',
|
||||
standalone: true,
|
||||
imports: [ButtonDirective, BreadcrumbModule, MenuModule],
|
||||
template: `
|
||||
<header class="topbar">
|
||||
<div class="topbar__breadcrumbs">
|
||||
<p-breadcrumb [model]="breadcrumbItems()" [home]="homeItem" />
|
||||
</div>
|
||||
|
||||
<div class="topbar__actions">
|
||||
<button pButton type="button" severity="secondary" [text]="true" [rounded]="true" [iconOnly]="true" aria-label="Przełącz tryb ciemny" (click)="toggleTheme()"><i [class]="isDark() ? 'pi pi-sun' : 'pi pi-moon'"></i></button>
|
||||
|
||||
<button type="button" class="topbar__user" (click)="userMenu.toggle($event)">
|
||||
<span class="topbar__avatar">{{ userInitials() }}</span>
|
||||
<span class="topbar__username">{{ userLabel() }}</span>
|
||||
<i class="pi pi-chevron-down topbar__chevron"></i>
|
||||
</button>
|
||||
<p-menu #userMenu [model]="userMenuItems" [popup]="true" />
|
||||
</div>
|
||||
</header>
|
||||
`,
|
||||
styles: `
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: var(--topbar-height);
|
||||
padding: 0 var(--content-padding);
|
||||
background: var(--color-surface);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
flex-shrink: 0;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.topbar__breadcrumbs {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.topbar__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.topbar__user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: 4px 8px 4px 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.topbar__user:hover {
|
||||
background: var(--color-surface-hover);
|
||||
border-color: var(--color-border-strong);
|
||||
}
|
||||
|
||||
.topbar__avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-accent);
|
||||
color: var(--color-text-inverse);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.topbar__username {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.topbar__chevron {
|
||||
font-size: 10px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
:host ::ng-deep .p-breadcrumb {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:host ::ng-deep .p-breadcrumb-list {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
:host ::ng-deep .p-breadcrumb-item-label {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.topbar__username {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class TopbarComponent {
|
||||
private readonly router = inject(Router);
|
||||
private readonly auth = inject(AuthService);
|
||||
|
||||
readonly isDark = signal(document.documentElement.getAttribute('data-theme') === 'dark');
|
||||
|
||||
readonly homeItem: MenuItem = { icon: 'pi pi-home', routerLink: '/' };
|
||||
|
||||
private readonly url = toSignal(
|
||||
this.router.events.pipe(
|
||||
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
||||
map((e) => e.urlAfterRedirects),
|
||||
startWith(this.router.url),
|
||||
),
|
||||
{ initialValue: this.router.url },
|
||||
);
|
||||
|
||||
readonly breadcrumbItems = computed<MenuItem[]>(() => {
|
||||
const segments = this.url()
|
||||
.split('?')[0]
|
||||
.split('/')
|
||||
.filter(Boolean);
|
||||
const labels: Record<string, string> = {
|
||||
Products: 'Zarządzanie Indeksami',
|
||||
Warehouse: 'Magazyn',
|
||||
ScheduleOrder: 'Zamówienie DELFOR',
|
||||
Admin: 'Administracja',
|
||||
PK: 'PK',
|
||||
UsersManager: 'Użytkownicy',
|
||||
Scheduler: 'Scheduler',
|
||||
EdiCustomerOrders: 'Zamówienia klienta EDI',
|
||||
EdiCustomerOrder: 'Zamówienie EDI',
|
||||
CustomerOrders: 'Zamówienia klienta',
|
||||
CustomerOrder: 'Zamówienie klienta',
|
||||
Meyle: 'Meyle',
|
||||
Marelli: 'Marelli',
|
||||
PackList: 'Packing List',
|
||||
Simple: 'Widok prosty',
|
||||
};
|
||||
|
||||
let path = '';
|
||||
return segments.map((seg) => {
|
||||
path += `/${seg}`;
|
||||
return { label: labels[seg] ?? seg, routerLink: path };
|
||||
});
|
||||
});
|
||||
|
||||
readonly userLabel = computed(() => this.auth.userName() || 'Użytkownik');
|
||||
|
||||
readonly userInitials = computed(() => {
|
||||
const name = this.userLabel();
|
||||
return name.slice(0, 2).toUpperCase();
|
||||
});
|
||||
|
||||
readonly userMenuItems: MenuItem[] = [
|
||||
{
|
||||
label: 'Wyloguj',
|
||||
icon: 'pi pi-sign-out',
|
||||
command: () => this.logout(),
|
||||
},
|
||||
];
|
||||
|
||||
toggleTheme(): void {
|
||||
const next = this.isDark() ? 'light' : 'dark';
|
||||
if (next === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}
|
||||
this.isDark.set(next === 'dark');
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.auth.logout();
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
||||
18
FaKrosnoManagerAngular/src/app/main/main.component.ts
Normal file
18
FaKrosnoManagerAngular/src/app/main/main.component.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthService } from '../core/services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
export class MainComponent implements OnInit {
|
||||
private readonly auth = inject(AuthService);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.auth.logout();
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Component, ElementRef, inject, OnInit, signal, viewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { TransactionModel, WzRowMarelliDto } from '../../core/models';
|
||||
import { WarehouseService } from '../../core/services/warehouse.service';
|
||||
import { findMarelliCombinations } from '../../shared/utils/business-logic';
|
||||
import { PageShellComponent } from '../../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../../shared/components/content-section/content-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-marelli-pack-list-simple',
|
||||
standalone: true,
|
||||
imports: [ButtonDirective, InputTextModule, DialogModule, FormsModule, PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
title="Packing List"
|
||||
subtitle="Packing list Marelli — widok uproszczony ze skanowaniem"
|
||||
icon="pi pi-box"
|
||||
>
|
||||
<div actions>
|
||||
<button pButton type="button" (click)="changeView()"><i class="pi pi-eye"></i> Zmień widok</button>
|
||||
<button pButton type="button" (click)="exportXls()"><i class="pi pi-file-excel"></i> Generuj XLS i Wyślij</button>
|
||||
</div>
|
||||
|
||||
<app-content-section title="Dane wysyłki" icon="pi pi-envelope">
|
||||
<div class="form-grid form-grid--2">
|
||||
<div class="field-group">
|
||||
<label class="field-label">Adresy Email do Wysyłki raportu:</label>
|
||||
<input pInputText class="w-full" placeholder="Wprowadź adresy..." [(ngModel)]="emailAddresses" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label">Numer WZ:</label>
|
||||
<input pInputText class="w-full" [(ngModel)]="wzNumber" />
|
||||
</div>
|
||||
</div>
|
||||
</app-content-section>
|
||||
|
||||
<app-content-section title="Skanowanie" icon="pi pi-barcode">
|
||||
<div class="form-grid form-grid--2">
|
||||
<div class="field-group">
|
||||
<label class="field-label">Wprowadź numer palety:</label>
|
||||
<input pInputText type="number" class="w-full" [(ngModel)]="palletNumber" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label">Zeskanowana wartość:</label>
|
||||
<input #scanner pInputText class="w-full" [(ngModel)]="scannedValue" (change)="scanValue()" />
|
||||
</div>
|
||||
</div>
|
||||
</app-content-section>
|
||||
|
||||
<app-content-section title="Ostatnio zeskanowana pozycja" icon="pi pi-info-circle">
|
||||
<div class="form-grid form-grid--3">
|
||||
<div class="field-group"><label class="field-label">Numer Indeksu FA:</label><input pInputText readonly class="w-full" [(ngModel)]="itemNumber" /></div>
|
||||
<div class="field-group"><label class="field-label">Ilość w Dostawie:</label><input pInputText readonly class="w-full" [(ngModel)]="qty" /></div>
|
||||
<div class="field-group"><label class="field-label">Numer Palety:</label><input pInputText readonly class="w-full" [(ngModel)]="palletNumberOutput" /></div>
|
||||
</div>
|
||||
</app-content-section>
|
||||
</app-page-shell>
|
||||
|
||||
<p-dialog header="Informacja" [(visible)]="showInfoDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
@if (exportValid()) { <p>Packing List został wygenerowany i wysłany!</p> }
|
||||
@else if (!emailAddresses?.trim()) { <p>Błąd: Proszę wprowadzić przynajmniej jeden <b>ADRES EMAIL</b> do wysyłki raportu!</p> }
|
||||
@else { <p>Błąd: Nie Wszystkie linie mają wypełniony <b>NUMER PALETY</b>.<br />Packing List nie zostanie wygenerowany!</p> }
|
||||
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
|
||||
</p-dialog>
|
||||
<p-dialog header="Błąd" [(visible)]="showPalletDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
<p>Błąd skanowania! <b>Wybierz NUMER PALETY większy niż 0</b> (Aktualnie '{{ palletNumber }}'):</p>
|
||||
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
|
||||
</p-dialog>
|
||||
`,
|
||||
})
|
||||
export class MarelliPackListSimpleComponent implements OnInit {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly warehouseService = inject(WarehouseService);
|
||||
readonly scanner = viewChild<ElementRef<HTMLInputElement>>('scanner');
|
||||
|
||||
wzHeaderId = '';
|
||||
rows: WzRowMarelliDto[] = [];
|
||||
transactionModels: Record<string, TransactionModel[]> = {};
|
||||
changedRecords: WzRowMarelliDto[] = [];
|
||||
selectedRow: WzRowMarelliDto | null = null;
|
||||
selectedRows: WzRowMarelliDto[] = [];
|
||||
|
||||
emailAddresses = '';
|
||||
wzNumber = '';
|
||||
palletNumber = '0';
|
||||
scannedValue = '';
|
||||
itemNumber = '';
|
||||
qty = '0';
|
||||
palletNumberOutput = '0';
|
||||
|
||||
showInfoDialog = false;
|
||||
showPalletDialog = false;
|
||||
readonly exportValid = signal(false);
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.wzHeaderId = this.route.snapshot.paramMap.get('WzHeader') ?? '';
|
||||
const header = await this.warehouseService.getWzHeaderById(this.wzHeaderId);
|
||||
this.rows = await this.warehouseService.getWzRowsMarelliByWzHeaderId(this.wzHeaderId);
|
||||
this.transactionModels = await this.warehouseService.getTransactionsModels();
|
||||
this.emailAddresses = header.emailAddresses ?? '';
|
||||
this.wzNumber = header.wzNumbers ?? '';
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
changeView(): void { this.router.navigate(['/Warehouse/Marelli/PackList', this.wzHeaderId]); }
|
||||
hideModal(): void { this.showInfoDialog = false; this.showPalletDialog = false; this.scannedValue = ''; setTimeout(() => this.scanner()?.nativeElement.focus()); }
|
||||
|
||||
async saveChanges(): Promise<void> {
|
||||
if (this.emailAddresses?.trim()) await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
|
||||
if (this.changedRecords.length) {
|
||||
await this.warehouseService.updateWzRowsMarelli(this.changedRecords);
|
||||
this.rows = await this.warehouseService.getWzRowsMarelliByWzHeaderId(this.wzHeaderId);
|
||||
}
|
||||
}
|
||||
|
||||
async exportXls(): Promise<void> {
|
||||
const valid = !this.rows.some((x) => x.palletNumber == null) && !!this.emailAddresses?.trim();
|
||||
this.exportValid.set(valid);
|
||||
if (valid) {
|
||||
await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
|
||||
await this.warehouseService.generateXlsForMarelli(this.wzHeaderId);
|
||||
}
|
||||
this.showInfoDialog = true;
|
||||
}
|
||||
|
||||
async scanValue(): Promise<void> {
|
||||
const value = this.scannedValue.trim();
|
||||
if (!value || parseInt(this.palletNumber, 10) <= 0) { this.showPalletDialog = true; return; }
|
||||
const material = this.transactionModels[value]?.[0];
|
||||
if (material) await this.fillPalletNumber(material);
|
||||
this.scannedValue = '';
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
private async fillPalletNumber(material: TransactionModel): Promise<void> {
|
||||
const palletNum = parseInt(this.palletNumber, 10);
|
||||
let rowIndex = this.rows.findIndex((x) => x.faIndex === material.itemNumber && x.quantity === material.quantity);
|
||||
if (rowIndex === -1) {
|
||||
this.selectedRows = this.rows.filter((x) => x.faIndex === material.itemNumber);
|
||||
const validCombinations = findMarelliCombinations(this.selectedRows, material.quantity ?? 0);
|
||||
for (const combination of validCombinations) {
|
||||
for (const record of combination) {
|
||||
record.palletNumber = palletNum;
|
||||
this.changedRecords.push(record);
|
||||
}
|
||||
}
|
||||
this.selectedRows = [...this.changedRecords];
|
||||
this.selectedRow = this.selectedRows[0] ?? null;
|
||||
} else {
|
||||
this.selectedRow = this.rows[rowIndex];
|
||||
this.selectedRow.palletNumber = palletNum;
|
||||
if (!this.changedRecords.some((x) => x.transactionNumber === this.selectedRow!.transactionNumber)) this.changedRecords.push(this.selectedRow);
|
||||
}
|
||||
this.itemNumber = this.selectedRow?.faIndex ?? '';
|
||||
this.qty = String(this.selectedRow?.quantity ?? 0);
|
||||
this.palletNumberOutput = String(this.selectedRow?.palletNumber ?? 0);
|
||||
await this.saveChanges();
|
||||
this.changedRecords = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { Component, ElementRef, inject, OnInit, signal, viewChild } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { SelectableRow, TableModule, TableRowSelectEvent } from 'primeng/table';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { TransactionModel, WzRowMarelliDto } from '../../core/models';
|
||||
import { WarehouseService } from '../../core/services/warehouse.service';
|
||||
import { findMarelliCombinations } from '../../shared/utils/business-logic';
|
||||
import { PageShellComponent } from '../../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../../shared/components/content-section/content-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-marelli-pack-list',
|
||||
standalone: true,
|
||||
imports: [TableModule, SelectableRow, ButtonDirective, InputTextModule, DialogModule, FormsModule, PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
title="Packing List"
|
||||
subtitle="Packing list Marelli — edycja pozycji i skanowanie"
|
||||
icon="pi pi-box"
|
||||
>
|
||||
<app-content-section title="Dane wysyłki" icon="pi pi-envelope">
|
||||
<div class="form-grid form-grid--2">
|
||||
<div class="field-group">
|
||||
<label class="field-label">Adresy Email do Wysyłki raportu:</label>
|
||||
<input pInputText class="w-full" placeholder="Wprowadź adresy..." [(ngModel)]="emailAddresses" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label">Numer WZ:</label>
|
||||
<input pInputText class="w-full" [(ngModel)]="wzNumber" />
|
||||
</div>
|
||||
</div>
|
||||
</app-content-section>
|
||||
|
||||
<app-content-section title="Skanowanie" icon="pi pi-barcode">
|
||||
<div class="form-grid form-grid--2">
|
||||
<div class="field-group">
|
||||
<label class="field-label">Wprowadź numer palety:</label>
|
||||
<input pInputText type="number" class="w-full" [(ngModel)]="palletNumber" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label">Zeskanowana wartość:</label>
|
||||
<input #scanner pInputText class="w-full" [(ngModel)]="scannedValue" (change)="scanValue()" />
|
||||
</div>
|
||||
</div>
|
||||
</app-content-section>
|
||||
|
||||
<app-content-section title="Pozycje packing list" icon="pi pi-list" [flush]="true">
|
||||
<div headerActions>
|
||||
<button pButton type="button" (click)="saveChanges()"><i class="pi pi-save"></i> Zapisz zmiany</button>
|
||||
<button pButton type="button" (click)="exportXls()"><i class="pi pi-file-excel"></i> Generuj XLS i Wyślij</button>
|
||||
</div>
|
||||
<p-table [value]="rows()" [(selection)]="selectedRowsList" dataKey="id" [paginator]="true" [rows]="10" selectionMode="single" (onRowSelect)="onRowSelect($event)" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th>Nr Palety</th><th>Numer Indeksu Marelli</th><th>Numer Inżynieryjny</th>
|
||||
<th>Ilość w Dostawie</th><th>Nr Zamówienia</th><th>Nr WZ</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-row>
|
||||
<tr [pSelectableRow]="row">
|
||||
<td><input pInputText type="number" [(ngModel)]="row.palletNumber" (ngModelChange)="markChanged(row)" /></td>
|
||||
<td>{{ row.itemNumber }}</td>
|
||||
<td>{{ row.engineerNumber }}</td>
|
||||
<td>{{ row.quantity }}</td>
|
||||
<td><input pInputText [(ngModel)]="row.orderNumber" (ngModelChange)="markChanged(row)" /></td>
|
||||
<td><input pInputText [(ngModel)]="row.wzNumber" (ngModelChange)="markChanged(row)" /></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
</app-page-shell>
|
||||
|
||||
<p-dialog header="Informacja" [(visible)]="showInfoDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
@if (exportValid()) { <p>Packing List został wygenerowany i wysłany!</p> }
|
||||
@else if (!emailAddresses?.trim()) { <p>Błąd: Proszę wprowadzić przynajmniej jeden <b>ADRES EMAIL</b> do wysyłki raportu!</p> }
|
||||
@else { <p>Błąd: Nie Wszystkie linie mają wypełniony <b>NUMER PALETY</b>.<br />Packing List nie zostanie wygenerowany!</p> }
|
||||
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
|
||||
</p-dialog>
|
||||
<p-dialog header="Błąd" [(visible)]="showPalletDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
<p>Błąd skanowania! <b>Wybierz NUMER PALETY większy niż 0</b> (Aktualnie '{{ palletNumber }}'):</p>
|
||||
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
|
||||
</p-dialog>
|
||||
<p-dialog header="Błąd" [(visible)]="showIndexQtyDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
<p>Nie znaleziono indeksu z ilością sztuk ('<b>{{ indexWithQty }}</b>') odpowiadającemu skanowanym wartościom!</p>
|
||||
<p>Znaleziono indeks, który ma ilość sztuk: '<b>{{ indexQty }}</b>'!</p>
|
||||
<p><b>Uzupełnij numer palety ręcznie i kliknij 'Zapisz'!</b></p>
|
||||
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
|
||||
</p-dialog>
|
||||
<p-dialog header="Błąd" [(visible)]="showNotFoundDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
<p>Na liście nie znaleziono skanowanego numeru partii '<b>{{ notFoundItem }}</b>'!</p>
|
||||
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
|
||||
</p-dialog>
|
||||
`,
|
||||
})
|
||||
export class MarelliPackListComponent implements OnInit {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly warehouseService = inject(WarehouseService);
|
||||
readonly scanner = viewChild<ElementRef<HTMLInputElement>>('scanner');
|
||||
|
||||
wzHeaderId = '';
|
||||
emailAddresses = '';
|
||||
wzNumber = '';
|
||||
palletNumber = '0';
|
||||
scannedValue = '';
|
||||
indexWithQty = '';
|
||||
indexQty = '';
|
||||
notFoundItem = '';
|
||||
|
||||
readonly rows = signal<WzRowMarelliDto[]>([]);
|
||||
readonly selectedRow = signal<WzRowMarelliDto | null>(null);
|
||||
selectedRowsList: WzRowMarelliDto[] = [];
|
||||
changedRecords: WzRowMarelliDto[] = [];
|
||||
transactionModels: Record<string, TransactionModel[]> = {};
|
||||
|
||||
showInfoDialog = false;
|
||||
showPalletDialog = false;
|
||||
showIndexQtyDialog = false;
|
||||
showNotFoundDialog = false;
|
||||
readonly exportValid = signal(false);
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.wzHeaderId = this.route.snapshot.paramMap.get('WzHeader') ?? '';
|
||||
const header = await this.warehouseService.getWzHeaderById(this.wzHeaderId);
|
||||
this.rows.set(await this.warehouseService.getWzRowsMarelliByWzHeaderId(this.wzHeaderId));
|
||||
this.transactionModels = await this.warehouseService.getTransactionsModels();
|
||||
this.emailAddresses = header.emailAddresses ?? '';
|
||||
this.wzNumber = header.wzNumbers ?? '';
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
onRowSelect(event: TableRowSelectEvent<WzRowMarelliDto>): void {
|
||||
const data = event.data;
|
||||
if (data && !Array.isArray(data)) this.selectedRow.set(data);
|
||||
}
|
||||
markChanged(row: WzRowMarelliDto): void { if (!this.changedRecords.includes(row)) this.changedRecords.push(row); }
|
||||
|
||||
hideModal(): void {
|
||||
this.showInfoDialog = false;
|
||||
this.showPalletDialog = false;
|
||||
this.showIndexQtyDialog = false;
|
||||
this.showNotFoundDialog = false;
|
||||
this.scannedValue = '';
|
||||
this.indexQty = '';
|
||||
this.indexWithQty = '';
|
||||
this.notFoundItem = '';
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
async saveChanges(): Promise<void> {
|
||||
if (this.emailAddresses?.trim()) await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
|
||||
if (this.changedRecords.length) {
|
||||
await this.updateRows(this.changedRecords);
|
||||
this.changedRecords = [];
|
||||
}
|
||||
this.selectedRowsList = [];
|
||||
}
|
||||
|
||||
async exportXls(): Promise<void> {
|
||||
const valid = !this.rows().some((x) => x.palletNumber == null) && !!this.emailAddresses?.trim();
|
||||
this.exportValid.set(valid);
|
||||
if (valid) {
|
||||
await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
|
||||
await this.warehouseService.generateXlsForMarelli(this.wzHeaderId);
|
||||
}
|
||||
this.showInfoDialog = true;
|
||||
}
|
||||
|
||||
async scanValue(): Promise<void> {
|
||||
const value = this.scannedValue.trim();
|
||||
if (!value || parseInt(this.palletNumber, 10) <= 0) { this.showPalletDialog = true; return; }
|
||||
const material = this.transactionModels[value]?.[0];
|
||||
if (material) {
|
||||
this.selectedRowsList = [];
|
||||
await this.fillPalletNumber(material, value);
|
||||
} else {
|
||||
this.notFoundItem = value;
|
||||
this.showNotFoundDialog = true;
|
||||
}
|
||||
this.scannedValue = '';
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
private async fillPalletNumber(material: TransactionModel, scanned: string): Promise<void> {
|
||||
const palletNum = parseInt(this.palletNumber, 10);
|
||||
const currentRows = this.rows();
|
||||
let rowIndex = currentRows.findIndex((x) => x.faIndex === material.itemNumber && x.quantity === material.quantity);
|
||||
|
||||
if (rowIndex === -1) {
|
||||
const candidates = currentRows.filter((x) => x.faIndex === material.itemNumber);
|
||||
if (!candidates.length) return;
|
||||
rowIndex = currentRows.findIndex((x) => x.faIndex === candidates[0].faIndex && x.quantity === candidates[0].quantity);
|
||||
const validCombinations = findMarelliCombinations(candidates, material.quantity ?? 0);
|
||||
if (!validCombinations.length && candidates.length > 0) {
|
||||
this.indexWithQty = `${material.itemNumber}, Qty = ${material.quantity}`;
|
||||
this.indexQty = String(candidates[0].quantity ?? 0);
|
||||
this.showIndexQtyDialog = true;
|
||||
return;
|
||||
}
|
||||
for (const combination of validCombinations) {
|
||||
for (const record of combination) {
|
||||
record.palletNumber = palletNum;
|
||||
this.changedRecords.push(record);
|
||||
}
|
||||
}
|
||||
this.selectedRowsList = [...this.changedRecords];
|
||||
this.selectedRow.set(this.selectedRowsList[0] ?? null);
|
||||
} else {
|
||||
const row = currentRows[rowIndex];
|
||||
this.selectedRow.set(row);
|
||||
row.palletNumber = palletNum;
|
||||
if (!this.changedRecords.some((x) => x.transactionNumber === row.transactionNumber)) this.changedRecords.push(row);
|
||||
}
|
||||
await this.saveChanges();
|
||||
}
|
||||
|
||||
private async updateRows(changed: WzRowMarelliDto[]): Promise<void> {
|
||||
await this.warehouseService.updateWzRowsMarelli(changed);
|
||||
this.rows.set(await this.warehouseService.getWzRowsMarelliByWzHeaderId(this.wzHeaderId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import { Component, ElementRef, inject, OnInit, signal, viewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { TransactionModel, WzRowMeyleDto } from '../../core/models';
|
||||
import { WarehouseService } from '../../core/services/warehouse.service';
|
||||
import { findCombinations, isValidMeyleScannedValue } from '../../shared/utils/business-logic';
|
||||
import { PageShellComponent } from '../../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../../shared/components/content-section/content-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-meyle-pack-list-simple',
|
||||
standalone: true,
|
||||
imports: [ButtonDirective, InputTextModule, DialogModule, FormsModule, PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
title="Packing List"
|
||||
subtitle="Packing list Meyle — widok uproszczony ze skanowaniem"
|
||||
icon="pi pi-box"
|
||||
>
|
||||
<div actions>
|
||||
<button pButton type="button" (click)="changeView()"><i class="pi pi-eye"></i> Zmień widok</button>
|
||||
<button pButton type="button" (click)="exportXls()"><i class="pi pi-file-excel"></i> Generuj XLS i Wyślij</button>
|
||||
</div>
|
||||
|
||||
<app-content-section title="Dane wysyłki" icon="pi pi-envelope">
|
||||
<div class="form-grid form-grid--2">
|
||||
<div class="field-group">
|
||||
<label class="field-label">Adresy Email do Wysyłki raportu:</label>
|
||||
<input pInputText class="w-full" placeholder="Wprowadź adresy..." [(ngModel)]="emailAddresses" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label">Numer WZ:</label>
|
||||
<input pInputText class="w-full" [(ngModel)]="wzNumber" />
|
||||
</div>
|
||||
</div>
|
||||
</app-content-section>
|
||||
|
||||
<app-content-section title="Skanowanie" icon="pi pi-barcode">
|
||||
<div class="form-grid form-grid--2">
|
||||
<div class="field-group">
|
||||
<label class="field-label">Wprowadź numer palety:</label>
|
||||
<input pInputText type="number" class="w-full" [(ngModel)]="palletNumber" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label">Zeskanowana wartość:</label>
|
||||
<input #scanner pInputText class="w-full" [(ngModel)]="scannedValue" (change)="scanValue()" />
|
||||
</div>
|
||||
</div>
|
||||
</app-content-section>
|
||||
|
||||
<app-content-section title="Ostatnio zeskanowana pozycja" icon="pi pi-info-circle">
|
||||
<div class="form-grid form-grid--3">
|
||||
<div class="field-group"><label class="field-label">Numer Indeksu FA:</label><input pInputText readonly class="w-full" [(ngModel)]="itemNumber" /></div>
|
||||
<div class="field-group"><label class="field-label">Ilość w Dostawie:</label><input pInputText readonly class="w-full" [(ngModel)]="qty" /></div>
|
||||
<div class="field-group"><label class="field-label">Numer Palety:</label><input pInputText readonly class="w-full" [(ngModel)]="palletNumberOutput" /></div>
|
||||
<div class="field-group"><label class="field-label">Nr Partii SL:</label><input pInputText readonly class="w-full" [(ngModel)]="partNumberSl" /></div>
|
||||
<div class="field-group"><label class="field-label">Numer Partii Meyle:</label><input pInputText readonly class="w-full" [(ngModel)]="partNumberMeyle" /></div>
|
||||
</div>
|
||||
</app-content-section>
|
||||
</app-page-shell>
|
||||
|
||||
<p-dialog header="Informacja" [(visible)]="showInfoDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
@if (exportValid()) { <p>Packing List został wygenerowany i wysłany!</p> }
|
||||
@else if (!emailAddresses?.trim()) { <p>Błąd: Proszę wprowadzić przynajmniej jeden <b>ADRES EMAIL</b> do wysyłki raportu!</p> }
|
||||
@else { <p>Błąd: Nie Wszystkie linie mają wypełniony <b>NUMER PALETY</b>.<br />Packing List nie zostanie wygenerowany!</p> }
|
||||
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
|
||||
</p-dialog>
|
||||
<p-dialog header="Błąd" [(visible)]="showValidationDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
<p>Błąd skanowania! Wystąpił jeden z wyjątków (Zeskanowana wartość '<b>{{ lastScannedValue }}</b>'):</p>
|
||||
<ul>
|
||||
<li>Zeskanowano niepoprawny Numer Partii SL (nieistniejący w tabeli)</li>
|
||||
<li>Zeskanowano niepoprawny numer Partii Meyle (niezaczynający się od <b>{{ yearPrefix }}X</b>)</li>
|
||||
<li>Numer Palety nie jest większy niż 0 (aktualnie wybrany numer palety: '<b>{{ palletNumber }}</b>')</li>
|
||||
</ul>
|
||||
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
|
||||
</p-dialog>
|
||||
<p-dialog header="Błąd" [(visible)]="showPalletDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
<p>Błąd skanowania! <b>Wybierz NUMER PALETY większy niż 0</b> (Aktualnie '{{ palletNumber }}'):</p>
|
||||
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
|
||||
</p-dialog>
|
||||
`,
|
||||
})
|
||||
export class MeylePackListSimpleComponent implements OnInit {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly warehouseService = inject(WarehouseService);
|
||||
readonly scanner = viewChild<ElementRef<HTMLInputElement>>('scanner');
|
||||
|
||||
wzHeaderId = '';
|
||||
rows: WzRowMeyleDto[] = [];
|
||||
transactionModels: Record<string, TransactionModel[]> = {};
|
||||
changedRecords: WzRowMeyleDto[] = [];
|
||||
selectedRow: WzRowMeyleDto | null = null;
|
||||
selectedRows: WzRowMeyleDto[] = [];
|
||||
|
||||
emailAddresses = '';
|
||||
wzNumber = '';
|
||||
palletNumber = '0';
|
||||
scannedValue = '';
|
||||
lastScannedValue = '';
|
||||
itemNumber = '';
|
||||
qty = '0';
|
||||
palletNumberOutput = '0';
|
||||
partNumberSl = '';
|
||||
partNumberMeyle = '';
|
||||
|
||||
showInfoDialog = false;
|
||||
showValidationDialog = false;
|
||||
showPalletDialog = false;
|
||||
readonly exportValid = signal(false);
|
||||
|
||||
get yearPrefix(): number { return new Date().getFullYear() - 2000; }
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.wzHeaderId = this.route.snapshot.paramMap.get('WzHeader') ?? '';
|
||||
const header = await this.warehouseService.getWzHeaderById(this.wzHeaderId);
|
||||
this.rows = await this.warehouseService.getWzRowsMeyleByWzHeaderId(this.wzHeaderId);
|
||||
this.transactionModels = await this.warehouseService.getTransactionsModels();
|
||||
this.emailAddresses = header.emailAddresses ?? '';
|
||||
this.wzNumber = header.wzNumbers ?? '';
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
changeView(): void { this.router.navigate(['/Warehouse/Meyle/PackList', this.wzHeaderId]); }
|
||||
|
||||
hideModal(): void {
|
||||
this.showInfoDialog = false;
|
||||
this.showValidationDialog = false;
|
||||
this.showPalletDialog = false;
|
||||
this.lastScannedValue = this.scannedValue;
|
||||
this.scannedValue = '';
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
async saveChanges(): Promise<void> {
|
||||
if (this.emailAddresses?.trim()) await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
|
||||
if (this.changedRecords.length) {
|
||||
await this.warehouseService.updateWzRowsMeyle(this.changedRecords);
|
||||
this.rows = await this.warehouseService.getWzRowsMeyleByWzHeaderId(this.wzHeaderId);
|
||||
}
|
||||
}
|
||||
|
||||
async exportXls(): Promise<void> {
|
||||
const valid = !this.rows.some((x) => x.palletNumber == null) && !!this.emailAddresses?.trim();
|
||||
this.exportValid.set(valid);
|
||||
if (valid) {
|
||||
await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
|
||||
await this.warehouseService.generateXlsForMeyle(this.wzHeaderId);
|
||||
}
|
||||
this.showInfoDialog = true;
|
||||
}
|
||||
|
||||
async scanValue(): Promise<void> {
|
||||
const value = this.scannedValue.trim();
|
||||
if (!value || parseInt(this.palletNumber, 10) <= 0) { this.showPalletDialog = true; return; }
|
||||
const material = this.transactionModels[value]?.[0];
|
||||
if (!material && isValidMeyleScannedValue(value)) {
|
||||
await this.fillMeylePartNumber(value);
|
||||
this.scannedValue = '';
|
||||
return;
|
||||
}
|
||||
if (material) await this.fillFaPartNumberAndPalletNumber(material, value);
|
||||
else { this.showValidationDialog = true; this.changedRecords = []; return; }
|
||||
this.lastScannedValue = value;
|
||||
this.scannedValue = '';
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
private async fillMeylePartNumber(scanned: string): Promise<void> {
|
||||
if (this.selectedRow && !this.selectedRows.length) {
|
||||
this.selectedRow.partNumber = scanned;
|
||||
this.changedRecords.push(this.selectedRow);
|
||||
this.partNumberMeyle = scanned;
|
||||
}
|
||||
for (const row of this.selectedRows) { row.partNumber = scanned; this.changedRecords.push(row); }
|
||||
await this.saveChanges();
|
||||
this.changedRecords = [];
|
||||
this.selectedRows = [];
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
private async fillFaPartNumberAndPalletNumber(material: TransactionModel, scanned: string): Promise<void> {
|
||||
const palletNum = parseInt(this.palletNumber, 10);
|
||||
let rowIndex = this.rows.findIndex((x) => x.faIndex === material.itemNumber && x.quantity === material.quantity);
|
||||
if (rowIndex === -1) {
|
||||
this.selectedRows = this.rows.filter((x) => x.faIndex === material.itemNumber);
|
||||
if (!this.selectedRows.length) { this.showValidationDialog = true; return; }
|
||||
const validCombinations = findCombinations(this.selectedRows, material.quantity ?? 0);
|
||||
for (const combination of validCombinations) {
|
||||
for (const record of combination) {
|
||||
record.partNumberSl = scanned;
|
||||
record.palletNumber = palletNum;
|
||||
this.changedRecords.push(record);
|
||||
}
|
||||
}
|
||||
this.selectedRows = [...this.changedRecords];
|
||||
this.selectedRow = this.selectedRows[0] ?? null;
|
||||
} else {
|
||||
this.selectedRow = this.rows[rowIndex];
|
||||
this.selectedRow.partNumberSl = scanned;
|
||||
this.selectedRow.palletNumber = palletNum;
|
||||
if (!this.changedRecords.some((x) => x.transactionNumber === this.selectedRow!.transactionNumber)) {
|
||||
this.changedRecords.push(this.selectedRow);
|
||||
}
|
||||
}
|
||||
this.partNumberSl = this.selectedRow?.partNumberSl ?? '';
|
||||
this.palletNumberOutput = String(this.selectedRow?.palletNumber ?? 0);
|
||||
this.itemNumber = this.selectedRow?.faIndex ?? '';
|
||||
this.qty = String(this.selectedRow?.quantity ?? 0);
|
||||
await this.saveChanges();
|
||||
this.changedRecords = [];
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
import { Component, ElementRef, inject, OnInit, signal, viewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { SelectableRow, TableModule, TableRowSelectEvent } from 'primeng/table';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { TransactionModel, WzHeaderDto, WzRowMeyleDto } from '../../core/models';
|
||||
import { WarehouseService } from '../../core/services/warehouse.service';
|
||||
import {
|
||||
findCombinations,
|
||||
isValidMeyleScannedValue,
|
||||
} from '../../shared/utils/business-logic';
|
||||
import { PageShellComponent } from '../../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../../shared/components/content-section/content-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-meyle-pack-list',
|
||||
standalone: true,
|
||||
imports: [TableModule, SelectableRow, ButtonDirective, InputTextModule, DialogModule, FormsModule, PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
title="Packing List"
|
||||
subtitle="Packing list Meyle — widok szczegółowy z edycją pozycji"
|
||||
icon="pi pi-box"
|
||||
>
|
||||
<div actions>
|
||||
<button pButton type="button" (click)="changeView()"><i class="pi pi-eye"></i> Zmień widok</button>
|
||||
</div>
|
||||
|
||||
<app-content-section title="Dane wysyłki" icon="pi pi-envelope">
|
||||
<div class="form-grid form-grid--2">
|
||||
<div class="field-group">
|
||||
<label class="field-label">Adresy Email do Wysyłki raportu:</label>
|
||||
<input pInputText class="w-full" placeholder="Wprowadź adresy..." [(ngModel)]="emailAddresses" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label">Numer WZ:</label>
|
||||
<input pInputText class="w-full" [(ngModel)]="wzNumber" />
|
||||
</div>
|
||||
</div>
|
||||
</app-content-section>
|
||||
|
||||
<app-content-section title="Skanowanie" icon="pi pi-barcode">
|
||||
<div class="form-grid form-grid--2">
|
||||
<div class="field-group">
|
||||
<label class="field-label">Wprowadź numer palety:</label>
|
||||
<input pInputText type="number" class="w-full" [(ngModel)]="palletNumber" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label">Zeskanowana wartość:</label>
|
||||
<input #scanner pInputText class="w-full" [(ngModel)]="scannedValue" (change)="scanValue()" />
|
||||
</div>
|
||||
</div>
|
||||
</app-content-section>
|
||||
|
||||
<app-content-section title="Pozycje packing list" icon="pi pi-list" [flush]="true">
|
||||
<div headerActions>
|
||||
<button pButton type="button" (click)="saveChanges()"><i class="pi pi-save"></i> Zapisz zmiany</button>
|
||||
<button pButton type="button" [disabled]="!selectedRow()" (click)="showSplitDialog.set(true)"><i class="pi pi-arrows-h"></i> Podziel Linię</button>
|
||||
<button pButton type="button" (click)="exportXls()"><i class="pi pi-file-excel"></i> Generuj XLS i Wyślij</button>
|
||||
</div>
|
||||
<p-table
|
||||
[value]="rows()"
|
||||
[(selection)]="selectedRowsList"
|
||||
dataKey="id"
|
||||
[paginator]="true"
|
||||
[rows]="10"
|
||||
selectionMode="single"
|
||||
(onRowSelect)="onRowSelect($event)"
|
||||
styleClass="p-datatable-sm"
|
||||
[showGridlines]="true"
|
||||
[stripedRows]="true"
|
||||
>
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th>Numer Zamówienia Meyle</th>
|
||||
<th>Numer Indeksu FA</th>
|
||||
<th>Numer Indeksu Meyle</th>
|
||||
<th>Ilość w Dostawie</th>
|
||||
<th>Nr Palety</th>
|
||||
<th>Nr Partii SL</th>
|
||||
<th>Nr Partii Meyle</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-row>
|
||||
<tr [pSelectableRow]="row">
|
||||
<td>{{ row.orderNumber }}</td>
|
||||
<td>{{ row.faIndex }}</td>
|
||||
<td>{{ row.itemNumber }}</td>
|
||||
<td>{{ row.quantity }}</td>
|
||||
<td><input pInputText type="number" [(ngModel)]="row.palletNumber" (ngModelChange)="markChanged(row)" /></td>
|
||||
<td><input pInputText [(ngModel)]="row.partNumberSl" (ngModelChange)="markChanged(row)" /></td>
|
||||
<td><input pInputText [(ngModel)]="row.partNumber" (ngModelChange)="markChanged(row)" /></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
</app-page-shell>
|
||||
|
||||
<p-dialog header="Informacja" [(visible)]="showInfoDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
@if (exportValid()) {
|
||||
<p>Packing List został wygenerowany i wysłany!</p>
|
||||
} @else if (!emailAddresses?.trim()) {
|
||||
<p>Błąd: Proszę wprowadzić przynajmniej jeden <b>ADRES EMAIL</b> do wysyłki raportu!</p>
|
||||
} @else {
|
||||
<p>Błąd: Nie Wszystkie linie mają wypełniony <b>NUMER PALETY</b>.<br />Packing List nie zostanie wygenerowany!</p>
|
||||
}
|
||||
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
|
||||
</p-dialog>
|
||||
|
||||
<p-dialog header="Błąd" [(visible)]="showValidationDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
<p>Błąd skanowania! Wystąpił jeden z wyjątków (Zeskanowana wartość '<b>{{ lastScannedValue }}</b>'):</p>
|
||||
<ul>
|
||||
<li>Zeskanowano niepoprawny Numer Partii SL (nieistniejący w tabeli)</li>
|
||||
<li>Zeskanowano niepoprawny numer Partii Meyle (niezaczynający się od <b>{{ yearPrefix }}X</b>)</li>
|
||||
<li>Numer Palety nie jest większy niż 0 (aktualnie wybrany numer palety: '<b>{{ palletNumber }}</b>')</li>
|
||||
</ul>
|
||||
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
|
||||
</p-dialog>
|
||||
|
||||
<p-dialog header="Błąd" [(visible)]="showPalletDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
<p>Błąd skanowania! <b>Wybierz NUMER PALETY większy niż 0</b> (Aktualnie '{{ palletNumber }}'):</p>
|
||||
<ng-template #footer><button pButton type="button" (click)="hideModal()">OK</button></ng-template>
|
||||
</p-dialog>
|
||||
|
||||
<p-dialog header="Podziel Linię" [(visible)]="showSplitDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
<label class="field-label">Podziel linię <b>{{ selectedRow()?.faIndex }}</b> podając ilość sztuk dla nowej linii:</label>
|
||||
<input pInputText type="number" class="w-full" [(ngModel)]="newQuantity" />
|
||||
<ng-template #footer>
|
||||
<button pButton type="button" (click)="splitLine()">Zapisz</button>
|
||||
<button pButton type="button" severity="secondary" (click)="showSplitDialog.set(false)">Anuluj</button>
|
||||
</ng-template>
|
||||
</p-dialog>
|
||||
`,
|
||||
})
|
||||
export class MeylePackListComponent implements OnInit {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly warehouseService = inject(WarehouseService);
|
||||
|
||||
readonly scanner = viewChild<ElementRef<HTMLInputElement>>('scanner');
|
||||
|
||||
wzHeaderId = '';
|
||||
emailAddresses = '';
|
||||
wzNumber = '';
|
||||
palletNumber = '0';
|
||||
scannedValue = '';
|
||||
lastScannedValue = '';
|
||||
newQuantity = '0';
|
||||
|
||||
readonly rows = signal<WzRowMeyleDto[]>([]);
|
||||
readonly selectedRow = signal<WzRowMeyleDto | null>(null);
|
||||
selectedRowsList: WzRowMeyleDto[] = [];
|
||||
changedRecords: WzRowMeyleDto[] = [];
|
||||
transactionModels: Record<string, TransactionModel[]> = {};
|
||||
|
||||
showInfoDialog = false;
|
||||
showValidationDialog = false;
|
||||
showPalletDialog = false;
|
||||
readonly showSplitDialog = signal(false);
|
||||
readonly exportValid = signal(false);
|
||||
|
||||
get yearPrefix(): number {
|
||||
return new Date().getFullYear() - 2000;
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.wzHeaderId = this.route.snapshot.paramMap.get('WzHeader') ?? '';
|
||||
const header = await this.warehouseService.getWzHeaderById(this.wzHeaderId);
|
||||
this.rows.set(await this.warehouseService.getWzRowsMeyleByWzHeaderId(this.wzHeaderId));
|
||||
this.transactionModels = await this.warehouseService.getTransactionsModels();
|
||||
this.emailAddresses = header.emailAddresses ?? '';
|
||||
this.wzNumber = header.wzNumbers ?? '';
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
changeView(): void {
|
||||
this.router.navigate(['/Warehouse/Meyle/PackList', this.wzHeaderId, 'Simple']);
|
||||
}
|
||||
|
||||
onRowSelect(event: TableRowSelectEvent<WzRowMeyleDto>): void {
|
||||
const data = event.data;
|
||||
if (data && !Array.isArray(data)) this.selectedRow.set(data);
|
||||
}
|
||||
|
||||
markChanged(row: WzRowMeyleDto): void {
|
||||
if (!this.changedRecords.includes(row)) this.changedRecords.push(row);
|
||||
}
|
||||
|
||||
hideModal(): void {
|
||||
this.showInfoDialog = false;
|
||||
this.showValidationDialog = false;
|
||||
this.showPalletDialog = false;
|
||||
this.showSplitDialog.set(false);
|
||||
this.lastScannedValue = this.scannedValue;
|
||||
this.scannedValue = '';
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
async saveChanges(): Promise<void> {
|
||||
if (this.emailAddresses?.trim()) {
|
||||
await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
|
||||
}
|
||||
if (this.changedRecords.length) {
|
||||
await this.updateRows(this.changedRecords);
|
||||
this.changedRecords = [];
|
||||
}
|
||||
}
|
||||
|
||||
async exportXls(): Promise<void> {
|
||||
const missingPallet = this.rows().some((x) => x.palletNumber == null);
|
||||
const valid = !missingPallet && !!this.emailAddresses?.trim();
|
||||
this.exportValid.set(valid);
|
||||
if (valid) {
|
||||
await this.warehouseService.addEmailsToWzHeader(this.wzHeaderId, this.emailAddresses);
|
||||
await this.warehouseService.generateXlsForMeyle(this.wzHeaderId);
|
||||
}
|
||||
this.showInfoDialog = true;
|
||||
}
|
||||
|
||||
async scanValue(): Promise<void> {
|
||||
const value = this.scannedValue.trim();
|
||||
if (!value) return;
|
||||
if (parseInt(this.palletNumber, 10) <= 0) {
|
||||
this.showPalletDialog = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.transactionModels[value]?.[0];
|
||||
const materialTransaction = this.transactionModels[value]?.[0];
|
||||
|
||||
if (!materialTransaction && isValidMeyleScannedValue(value)) {
|
||||
await this.fillMeylePartNumber(value);
|
||||
this.lastScannedValue = value;
|
||||
this.scannedValue = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (materialTransaction) {
|
||||
this.selectedRowsList = [];
|
||||
await this.fillFaPartNumberAndPalletNumber(materialTransaction, value);
|
||||
} else {
|
||||
this.showValidationDialog = true;
|
||||
this.changedRecords = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastScannedValue = value;
|
||||
this.scannedValue = '';
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
private async fillMeylePartNumber(scanned: string): Promise<void> {
|
||||
const selected = this.selectedRow();
|
||||
if (selected && !this.selectedRowsList.length) {
|
||||
selected.partNumber = scanned;
|
||||
this.changedRecords.push(selected);
|
||||
}
|
||||
for (const row of this.selectedRowsList) {
|
||||
row.partNumber = scanned;
|
||||
this.changedRecords.push(row);
|
||||
}
|
||||
await this.saveChanges();
|
||||
this.changedRecords = [];
|
||||
this.selectedRowsList = [];
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
private async fillFaPartNumberAndPalletNumber(material: TransactionModel, scanned: string): Promise<void> {
|
||||
const palletNum = parseInt(this.palletNumber, 10);
|
||||
const currentRows = this.rows();
|
||||
let rowIndex = currentRows.findIndex((x) => x.faIndex === material.itemNumber && x.quantity === material.quantity);
|
||||
|
||||
if (rowIndex === -1) {
|
||||
const candidates = currentRows.filter((x) => x.faIndex === material.itemNumber);
|
||||
if (!candidates.length) {
|
||||
this.showValidationDialog = true;
|
||||
return;
|
||||
}
|
||||
rowIndex = currentRows.findIndex((x) => x.faIndex === candidates[0].faIndex && x.quantity === candidates[0].quantity);
|
||||
const validCombinations = findCombinations(candidates, material.quantity ?? 0);
|
||||
for (const combination of validCombinations) {
|
||||
for (const record of combination) {
|
||||
record.partNumberSl = scanned;
|
||||
record.palletNumber = palletNum;
|
||||
this.changedRecords.push(record);
|
||||
}
|
||||
}
|
||||
this.selectedRowsList = [...this.changedRecords];
|
||||
this.selectedRow.set(this.selectedRowsList[0] ?? null);
|
||||
} else {
|
||||
const row = currentRows[rowIndex];
|
||||
this.selectedRow.set(row);
|
||||
row.partNumberSl = scanned;
|
||||
row.palletNumber = palletNum;
|
||||
if (!this.changedRecords.some((x) => x.transactionNumber === row.transactionNumber)) {
|
||||
this.changedRecords.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
await this.saveChanges();
|
||||
this.changedRecords = [];
|
||||
setTimeout(() => this.scanner()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
async splitLine(): Promise<void> {
|
||||
const qty = parseInt(this.newQuantity, 10);
|
||||
const selected = this.selectedRow();
|
||||
if (qty <= 0 || !selected) return;
|
||||
|
||||
const splitRow: WzRowMeyleDto = {
|
||||
id: crypto.randomUUID(),
|
||||
fk_Header: selected.fk_Header,
|
||||
quantity: qty,
|
||||
faIndex: selected.faIndex,
|
||||
itemNumber: selected.itemNumber,
|
||||
orderNumber: selected.orderNumber,
|
||||
palletNumber: selected.palletNumber,
|
||||
wzNumber: selected.wzNumber,
|
||||
transactionNumber: (selected.transactionNumber ?? 0) + 10000,
|
||||
partNumberSl: selected.partNumberSl,
|
||||
partNumber: selected.partNumber,
|
||||
};
|
||||
|
||||
selected.quantity = (selected.quantity ?? 0) - qty;
|
||||
await this.warehouseService.createWzRowsMeyle([splitRow]);
|
||||
await this.updateRows([selected]);
|
||||
this.showSplitDialog.set(false);
|
||||
}
|
||||
|
||||
private async updateRows(changed: WzRowMeyleDto[]): Promise<void> {
|
||||
await this.warehouseService.updateWzRowsMeyle(changed);
|
||||
this.rows.set(await this.warehouseService.getWzRowsMeyleByWzHeaderId(this.wzHeaderId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
CancelEditableRow,
|
||||
CellEditor,
|
||||
EditableRow,
|
||||
InitEditableRow,
|
||||
SaveEditableRow,
|
||||
TableModule,
|
||||
} from 'primeng/table';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { ProductDto } from '../core/models';
|
||||
import { ProductService } from '../core/services/product.service';
|
||||
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-products-list',
|
||||
standalone: true,
|
||||
imports: [TableModule, CellEditor, EditableRow, InitEditableRow, SaveEditableRow, CancelEditableRow, InputTextModule, ButtonDirective, FormsModule, PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
title="Zarządzanie Indeksami"
|
||||
subtitle="Mapowanie indeksów odbiorców na kody FA"
|
||||
icon="pi pi-box"
|
||||
>
|
||||
<app-content-section title="Indeksy produktów" icon="pi pi-list" [flush]="true">
|
||||
<p-table [value]="products()" [paginator]="true" [rows]="10" editMode="row" dataKey="id" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Odbiorca</th>
|
||||
<th>Indeks odbiorcy</th>
|
||||
<th>Kod FA</th>
|
||||
<th style="width: 8rem"></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-product let-editing="editing" let-ri="rowIndex">
|
||||
<tr [pEditableRow]="product">
|
||||
<td>{{ product.id }}</td>
|
||||
<td>{{ product.recipientName }}</td>
|
||||
<td>{{ product.recipientIdx }}</td>
|
||||
<td>
|
||||
<p-cell-editor>
|
||||
<ng-template #input>
|
||||
<input pInputText [(ngModel)]="product.faIdx" />
|
||||
</ng-template>
|
||||
<ng-template #output>{{ product.faIdx }}</ng-template>
|
||||
</p-cell-editor>
|
||||
</td>
|
||||
<td>
|
||||
@if (!editing) {
|
||||
<button pButton type="button" [text]="true" [iconOnly]="true" pInitEditableRow><i class="pi pi-pencil"></i></button>
|
||||
} @else {
|
||||
<button pButton type="button" [text]="true" [iconOnly]="true" (click)="saveProduct(product)" pSaveEditableRow><i class="pi pi-check"></i></button>
|
||||
<button pButton type="button" severity="secondary" [text]="true" [iconOnly]="true" pCancelEditableRow><i class="pi pi-times"></i></button>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
</app-page-shell>
|
||||
`,
|
||||
})
|
||||
export class ProductsListComponent implements OnInit {
|
||||
private readonly productService = inject(ProductService);
|
||||
readonly products = signal<ProductDto[]>([]);
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this.loadProducts();
|
||||
}
|
||||
|
||||
async saveProduct(product: ProductDto): Promise<void> {
|
||||
await this.productService.update(product);
|
||||
await this.loadProducts();
|
||||
}
|
||||
|
||||
private async loadProducts(): Promise<void> {
|
||||
this.products.set(await this.productService.getByIndex('Uzupelnij'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { RowToggler, TableModule } from 'primeng/table';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { ScheduleOrderDto, ScheduleOrderDetailDto } from '../core/models';
|
||||
import { ScheduleOrderService } from '../core/services/schedule-order.service';
|
||||
import { isQtyTypeHighlighted } from '../shared/utils/business-logic';
|
||||
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-schedule-order-detail',
|
||||
standalone: true,
|
||||
imports: [TableModule, RowToggler, ButtonDirective, DatePipe, PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
[title]="'Zamówienie DELFOR nr ' + (order()?.poNum ?? 'Brak numeru') + ' (ID: ' + (order()?.id ?? '') + ')'"
|
||||
subtitle="Szczegóły harmonogramu dostaw DELFOR"
|
||||
icon="pi pi-building"
|
||||
>
|
||||
@if (order()?.scheduleOrderMiscs?.length) {
|
||||
<app-content-section title="Informacje dodatkowe" icon="pi pi-info-circle">
|
||||
<ul class="misc-list">
|
||||
@for (misc of order()!.scheduleOrderMiscs; track misc.id) {
|
||||
@if (misc.display !== false) {
|
||||
<li><b>{{ misc.label }}:</b> {{ misc.value }}</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</app-content-section>
|
||||
}
|
||||
|
||||
<app-content-section title="Indeksy" icon="pi pi-list" [flush]="true">
|
||||
<p-table [value]="details()" dataKey="id" [paginator]="true" [rows]="10" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th style="width: 3rem"></th>
|
||||
<th>Numer Zamówienia</th>
|
||||
<th>Klient</th>
|
||||
<th>Odbiorca</th>
|
||||
<th>Kod odbiorcy</th>
|
||||
<th>Pozycja</th>
|
||||
<th>Pozycja Klienta</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-detail let-expanded="expanded">
|
||||
<tr>
|
||||
<td>
|
||||
<button pButton type="button" [text]="true" [rounded]="true" [iconOnly]="true" [pRowToggler]="detail"><i [class]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i></button>
|
||||
</td>
|
||||
<td>{{ detail.orderNumber }}</td>
|
||||
<td>{{ detail.purchaserName }}</td>
|
||||
<td>{{ detail.recipientName }}</td>
|
||||
<td>{{ detail.recipientCode }}</td>
|
||||
<td>{{ detail.sh_productCode }}</td>
|
||||
<td>{{ detail.sc_productCode }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #expandedrow let-detail>
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<app-content-section title="Harmonogramy" icon="pi pi-calendar" [flush]="true">
|
||||
<p-table [value]="detail.scheduleOrderDetailDetails ?? []" [paginator]="true" [rows]="10" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th>Data Od</th>
|
||||
<th>Data Do</th>
|
||||
<th>Ilość Sztuk</th>
|
||||
<th>Typ Qty</th>
|
||||
<th>Opis Typu</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-row>
|
||||
<tr [class.qty-type-highlight]="isHighlighted(row.qtyType)">
|
||||
<td>{{ row.dateFrom | date: 'dd.MM.yyyy' }}</td>
|
||||
<td>{{ row.dateTo | date: 'dd.MM.yyyy' }}</td>
|
||||
<td>{{ row.qty }}</td>
|
||||
<td>{{ row.qtyType }}</td>
|
||||
<td>{{ row.qtyDesc }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
</app-page-shell>
|
||||
`,
|
||||
styles: `
|
||||
.misc-list { list-style: none; padding: 0; margin: 0; }
|
||||
.misc-list li { margin-bottom: var(--spacing-1); }
|
||||
`,
|
||||
})
|
||||
export class ScheduleOrderDetailComponent implements OnInit {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly scheduleOrderService = inject(ScheduleOrderService);
|
||||
|
||||
readonly order = signal<ScheduleOrderDto | null>(null);
|
||||
readonly details = signal<ScheduleOrderDetailDto[]>([]);
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
const id = Number(this.route.snapshot.paramMap.get('scheduleOrderId'));
|
||||
const data = await this.scheduleOrderService.getById(id);
|
||||
this.order.set(data);
|
||||
const details = [...(data.scheduleOrderDetails ?? [])];
|
||||
for (const d of details) {
|
||||
d.orderNumber = data.poNum;
|
||||
d.recipientCode = data.recipientCode;
|
||||
d.recipientName = data.recipientName;
|
||||
d.purchaserName = data.purchaserCode;
|
||||
}
|
||||
this.details.set(details);
|
||||
}
|
||||
|
||||
isHighlighted(qtyType?: string): boolean {
|
||||
return qtyType === '83' || qtyType === '84' || isQtyTypeHighlighted(qtyType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { ScheduleOrderDto } from '../core/models';
|
||||
import { ScheduleOrderService } from '../core/services/schedule-order.service';
|
||||
import { ScheduleOrdersGridComponent } from '../shared/components/schedule-orders-grid/schedule-orders-grid.component';
|
||||
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-schedule-orders-list',
|
||||
standalone: true,
|
||||
imports: [ScheduleOrdersGridComponent, PageShellComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
title="Zamówienia DELFOR"
|
||||
subtitle="Przegląd harmonogramów dostaw i zamówień klientów"
|
||||
icon="pi pi-building"
|
||||
>
|
||||
<app-schedule-orders-grid [gridData]="orders()" [pageSize]="20" />
|
||||
</app-page-shell>
|
||||
`,
|
||||
})
|
||||
export class ScheduleOrdersListComponent implements OnInit {
|
||||
private readonly scheduleOrderService = inject(ScheduleOrderService);
|
||||
readonly orders = signal<ScheduleOrderDto[]>([]);
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
const data = await this.scheduleOrderService.getAll();
|
||||
this.orders.set([...data].sort((a, b) => new Date(b.lastUpdateDate).getTime() - new Date(a.lastUpdateDate).getTime()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Select } from 'primeng/select';
|
||||
|
||||
@Component({
|
||||
selector: 'app-column-values-filter',
|
||||
standalone: true,
|
||||
imports: [Select, FormsModule],
|
||||
template: `
|
||||
<p-select
|
||||
[ngModel]="value()"
|
||||
[options]="options()"
|
||||
[optionLabel]="optionLabel()"
|
||||
[optionValue]="optionValue()"
|
||||
[multiple]="true"
|
||||
placeholder="Wybierz wartości"
|
||||
emptyMessage="Brak wartości"
|
||||
emptyFilterMessage="Brak wyników"
|
||||
[showClear]="true"
|
||||
[filter]="true"
|
||||
appendTo="body"
|
||||
styleClass="column-values-filter w-full"
|
||||
(onChange)="onChange($event.value)"
|
||||
/>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
min-width: 14rem;
|
||||
max-width: 20rem;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class ColumnValuesFilterComponent {
|
||||
value = input<unknown>();
|
||||
options = input<unknown[]>([]);
|
||||
optionLabel = input<string | undefined>(undefined);
|
||||
optionValue = input<string | undefined>(undefined);
|
||||
valueChange = output<unknown>();
|
||||
|
||||
onChange(value: unknown): void {
|
||||
this.valueChange.emit(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-content-section',
|
||||
standalone: true,
|
||||
template: `
|
||||
<section class="content-section" [class.content-section--flush]="flush()">
|
||||
@if (title()) {
|
||||
<header class="content-section__header">
|
||||
<div class="content-section__heading">
|
||||
@if (icon()) {
|
||||
<i [class]="icon()" class="content-section__icon" aria-hidden="true"></i>
|
||||
}
|
||||
<h2 class="content-section__title">{{ title() }}</h2>
|
||||
</div>
|
||||
<div class="content-section__actions">
|
||||
<ng-content select="[headerActions]" />
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
<div class="content-section__body">
|
||||
<ng-content />
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class ContentSectionComponent {
|
||||
readonly title = input<string>('');
|
||||
readonly icon = input<string>('');
|
||||
readonly flush = input<boolean>(false);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-shell',
|
||||
standalone: true,
|
||||
template: `
|
||||
<div class="page-shell">
|
||||
<header class="page-shell__header">
|
||||
<div class="page-shell__heading">
|
||||
@if (icon()) {
|
||||
<div class="page-shell__icon" aria-hidden="true">
|
||||
<i [class]="icon()"></i>
|
||||
</div>
|
||||
}
|
||||
<div class="page-shell__titles">
|
||||
<h1 class="page-shell__title">{{ title() }}</h1>
|
||||
@if (subtitle()) {
|
||||
<p class="page-shell__subtitle">{{ subtitle() }}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-shell__actions">
|
||||
<ng-content select="[actions]" />
|
||||
</div>
|
||||
</header>
|
||||
<div class="page-shell__content">
|
||||
<ng-content />
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class PageShellComponent {
|
||||
readonly title = input.required<string>();
|
||||
readonly subtitle = input<string>('');
|
||||
readonly icon = input<string>('');
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
OnInit,
|
||||
untracked,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
ColumnFilter,
|
||||
RowToggler,
|
||||
SelectableRow,
|
||||
SortableColumn,
|
||||
SortIcon,
|
||||
Table,
|
||||
TableModule,
|
||||
} from 'primeng/table';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { IconFieldModule } from 'primeng/iconfield';
|
||||
import { InputIconModule } from 'primeng/inputicon';
|
||||
import { MessageService } from 'primeng/api';
|
||||
import { ScheduleOrderDto, ScheduleOrderDetailDetailDto } from '../../../core/models';
|
||||
import { ScheduleOrderService } from '../../../core/services/schedule-order.service';
|
||||
import { ContentSectionComponent } from '../content-section/content-section.component';
|
||||
import { ColumnValuesFilterComponent } from '../column-values-filter/column-values-filter.component';
|
||||
import { isQtyTypeHighlighted } from '../../utils/business-logic';
|
||||
import {
|
||||
applySavedFiltersToTable,
|
||||
clearTableFilters,
|
||||
deleteGridFilters,
|
||||
loadSavedGridFilters,
|
||||
saveGridFilters,
|
||||
serializeTableFilters,
|
||||
} from '../../utils/grid-filter-storage';
|
||||
import {
|
||||
distinctDateValues,
|
||||
distinctNumericValues,
|
||||
distinctStringValues,
|
||||
LabeledFilterValue,
|
||||
} from '../../utils/grid-filter-values';
|
||||
|
||||
const GRID_FILTER_KEY = 'gridFilter';
|
||||
|
||||
@Component({
|
||||
selector: 'app-schedule-orders-grid',
|
||||
standalone: true,
|
||||
imports: [
|
||||
TableModule,
|
||||
Table,
|
||||
RowToggler,
|
||||
SelectableRow,
|
||||
SortableColumn,
|
||||
SortIcon,
|
||||
ColumnFilter,
|
||||
ButtonDirective,
|
||||
InputTextModule,
|
||||
IconFieldModule,
|
||||
InputIconModule,
|
||||
FormsModule,
|
||||
ContentSectionComponent,
|
||||
ColumnValuesFilterComponent,
|
||||
DatePipe,
|
||||
],
|
||||
template: `
|
||||
<app-content-section title="Lista zamówień" icon="pi pi-table" [flush]="true">
|
||||
<div headerActions>
|
||||
<button pButton type="button" size="small" (click)="saveFilters()"><i class="pi pi-save"></i> Zapisz filtry</button>
|
||||
<button pButton type="button" severity="secondary" size="small" (click)="deleteFilters()"><i class="pi pi-trash"></i> Usuń zapisane filtry</button>
|
||||
</div>
|
||||
|
||||
<p-table
|
||||
#mainTable
|
||||
[value]="gridData()"
|
||||
[paginator]="true"
|
||||
[rows]="pageSize()"
|
||||
[rowsPerPageOptions]="[10, 20, 50]"
|
||||
[globalFilterFields]="globalFilterFields"
|
||||
[filterDelay]="300"
|
||||
dataKey="id"
|
||||
[expandedRowKeys]="expandedKeys"
|
||||
(onRowExpand)="onRowExpand($event)"
|
||||
styleClass="p-datatable-sm erp-table"
|
||||
[showGridlines]="true"
|
||||
[stripedRows]="true"
|
||||
>
|
||||
<ng-template #caption>
|
||||
<div class="data-toolbar">
|
||||
<div class="data-toolbar__left">
|
||||
<span class="text-secondary" style="font-size: var(--font-size-sm)">
|
||||
{{ filteredCount() }} / {{ gridData().length }} rekordów · dwuklik otwiera szczegóły
|
||||
</span>
|
||||
</div>
|
||||
<div class="data-toolbar__right">
|
||||
<p-iconfield iconPosition="left" class="data-toolbar__search">
|
||||
<p-inputicon styleClass="pi pi-search" />
|
||||
<input
|
||||
pInputText
|
||||
type="text"
|
||||
[(ngModel)]="globalFilterValue"
|
||||
placeholder="Szukaj w tabeli…"
|
||||
(ngModelChange)="onGlobalFilterChange($event)"
|
||||
/>
|
||||
</p-iconfield>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th style="width: 3rem"></th>
|
||||
<th pSortableColumn="poNum">
|
||||
<div class="column-header">
|
||||
<span>Zamówienie Klienta</span>
|
||||
<p-sort-icon field="poNum" />
|
||||
<p-column-filter field="poNum" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctMainColumn('poNum')" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="purchaserCode">
|
||||
<div class="column-header">
|
||||
<span>Klient</span>
|
||||
<p-sort-icon field="purchaserCode" />
|
||||
<p-column-filter field="purchaserCode" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctMainColumn('purchaserCode')" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="recipientName">
|
||||
<div class="column-header">
|
||||
<span>Odbiorca</span>
|
||||
<p-sort-icon field="recipientName" />
|
||||
<p-column-filter field="recipientName" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctMainColumn('recipientName')" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="recipientCode">
|
||||
<div class="column-header">
|
||||
<span>Kod odbiorcy</span>
|
||||
<p-sort-icon field="recipientCode" />
|
||||
<p-column-filter field="recipientCode" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctMainColumn('recipientCode')" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="lastUpdateDate">
|
||||
<div class="column-header">
|
||||
<span>Data</span>
|
||||
<p-sort-icon field="lastUpdateDate" />
|
||||
<p-column-filter field="lastUpdateDate" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctMainDates()" optionLabel="label" optionValue="value" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div>
|
||||
</th>
|
||||
<th pSortableColumn="docType">
|
||||
<div class="column-header">
|
||||
<span>Typ Dokumentu</span>
|
||||
<p-sort-icon field="docType" />
|
||||
<p-column-filter field="docType" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctMainColumn('docType')" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-order let-expanded="expanded">
|
||||
<tr (dblclick)="navigateToOrder(order.id)" [pSelectableRow]="order" class="erp-table__row">
|
||||
<td>
|
||||
<button pButton type="button" size="small" [text]="true" [rounded]="true" [iconOnly]="true" [pRowToggler]="order"><i [class]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i></button>
|
||||
</td>
|
||||
<td><span class="font-medium">{{ order.poNum }}</span></td>
|
||||
<td>{{ order.purchaserCode }}</td>
|
||||
<td>{{ order.recipientName }}</td>
|
||||
<td>{{ order.recipientCode }}</td>
|
||||
<td>{{ order.lastUpdateDate | date: 'dd.MM.yyyy' }}</td>
|
||||
<td>{{ order.docType }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #expandedrow let-order>
|
||||
<tr>
|
||||
<td colspan="7" class="erp-table__nested">
|
||||
<p-table
|
||||
[value]="order.scheduleOrderDetails ?? []"
|
||||
dataKey="id"
|
||||
[paginator]="true"
|
||||
[rows]="10"
|
||||
[filterDelay]="300"
|
||||
styleClass="p-datatable-sm"
|
||||
[showGridlines]="true"
|
||||
>
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th style="width: 3rem"></th>
|
||||
<th>
|
||||
<div class="column-header"><span>Numer Zamówienia</span>
|
||||
<p-column-filter field="orderNumber" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctFromRows(order.scheduleOrderDetails, 'orderNumber')" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="column-header"><span>Klient</span>
|
||||
<p-column-filter field="purchaserName" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctFromRows(order.scheduleOrderDetails, 'purchaserName')" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="column-header"><span>Odbiorca</span>
|
||||
<p-column-filter field="recipientName" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctFromRows(order.scheduleOrderDetails, 'recipientName')" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="column-header"><span>Kod odbiorcy</span>
|
||||
<p-column-filter field="recipientCode" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctFromRows(order.scheduleOrderDetails, 'recipientCode')" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="column-header"><span>Pozycja Klienta</span>
|
||||
<p-column-filter field="sc_productCode" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctFromRows(order.scheduleOrderDetails, 'sc_productCode')" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="column-header"><span>Pozycja</span>
|
||||
<p-column-filter field="sh_productCode" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctFromRows(order.scheduleOrderDetails, 'sh_productCode')" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-detail let-detailExpanded="expanded">
|
||||
<tr (dblclick)="navigateToOrder(order.id)">
|
||||
<td>
|
||||
<button pButton type="button" size="small" [text]="true" [rounded]="true" [iconOnly]="true" [pRowToggler]="detail"><i [class]="detailExpanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i></button>
|
||||
</td>
|
||||
<td>{{ detail.orderNumber }}</td>
|
||||
<td>{{ detail.purchaserName }}</td>
|
||||
<td>{{ detail.recipientName }}</td>
|
||||
<td>{{ detail.recipientCode }}</td>
|
||||
<td>{{ detail.sc_productCode }}</td>
|
||||
<td>{{ detail.sh_productCode }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #expandedrow let-detail>
|
||||
<tr>
|
||||
<td colspan="7" class="erp-table__nested">
|
||||
<p-table
|
||||
[value]="detail.scheduleOrderDetailDetails ?? []"
|
||||
[paginator]="true"
|
||||
[rows]="10"
|
||||
[filterDelay]="300"
|
||||
styleClass="p-datatable-sm"
|
||||
[showGridlines]="true"
|
||||
>
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th><div class="column-header"><span>Data Od</span>
|
||||
<p-column-filter field="dateFrom" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctDatesFromRows(detail.scheduleOrderDetailDetails, 'dateFrom')" optionLabel="label" optionValue="value" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div></th>
|
||||
<th><div class="column-header"><span>Data Do</span>
|
||||
<p-column-filter field="dateTo" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctDatesFromRows(detail.scheduleOrderDetailDetails, 'dateTo')" optionLabel="label" optionValue="value" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div></th>
|
||||
<th><div class="column-header"><span>Ilość Sztuk</span>
|
||||
<p-column-filter field="qty" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctNumbersFromRows(detail.scheduleOrderDetailDetails, 'qty')" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div></th>
|
||||
<th><div class="column-header"><span>Typ Qty</span>
|
||||
<p-column-filter field="qtyType" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctFromRows(detail.scheduleOrderDetailDetails, 'qtyType')" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div></th>
|
||||
<th><div class="column-header"><span>Opis Typu</span>
|
||||
<p-column-filter field="qtyDesc" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showApplyButton]="false" [showAddButton]="false">
|
||||
<ng-template #filter let-value let-filter="filterCallback">
|
||||
<app-column-values-filter [value]="value" [options]="distinctFromRows(detail.scheduleOrderDetailDetails, 'qtyDesc')" (valueChange)="filter($event)" />
|
||||
</ng-template>
|
||||
</p-column-filter>
|
||||
</div></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-qtyRow>
|
||||
<tr [class.qty-type-highlight]="isHighlighted(qtyRow.qtyType)" (dblclick)="navigateToOrder(order.id)">
|
||||
<td>{{ qtyRow.dateFrom | date: 'dd.MM.yyyy' }}</td>
|
||||
<td>{{ qtyRow.dateTo | date: 'dd.MM.yyyy' }}</td>
|
||||
<td>{{ qtyRow.qty }}</td>
|
||||
<td>{{ qtyRow.qtyType }}</td>
|
||||
<td>{{ qtyRow.qtyDesc }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
`,
|
||||
styles: `
|
||||
.column-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:host ::ng-deep .erp-table__nested {
|
||||
padding: var(--spacing-3) !important;
|
||||
background: var(--color-bg-subtle) !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .p-datatable-column-filter-button {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class ScheduleOrdersGridComponent implements OnInit, AfterViewInit {
|
||||
private readonly scheduleOrderService = inject(ScheduleOrderService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly messageService = inject(MessageService);
|
||||
|
||||
readonly gridData = input.required<ScheduleOrderDto[]>();
|
||||
readonly pageSize = input.required<number>();
|
||||
|
||||
@ViewChild('mainTable') mainTable?: Table;
|
||||
|
||||
readonly globalFilterFields = ['poNum', 'purchaserCode', 'recipientName', 'recipientCode', 'docType'];
|
||||
|
||||
expandedKeys: Record<string, boolean> = {};
|
||||
globalFilterValue = '';
|
||||
private viewReady = false;
|
||||
private lastDataLength = -1;
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const data = this.gridData();
|
||||
const length = data.length;
|
||||
|
||||
untracked(() => {
|
||||
if (!this.viewReady || length === this.lastDataLength) return;
|
||||
this.lastDataLength = length;
|
||||
queueMicrotask(() => this.restoreFilters());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const saved = loadSavedGridFilters(GRID_FILTER_KEY);
|
||||
if (saved?.global) {
|
||||
this.globalFilterValue = saved.global;
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.viewReady = true;
|
||||
this.lastDataLength = this.gridData().length;
|
||||
queueMicrotask(() => this.restoreFilters());
|
||||
}
|
||||
|
||||
filteredCount(): number {
|
||||
if (!this.mainTable?.filteredValue) {
|
||||
return this.gridData().length;
|
||||
}
|
||||
return this.mainTable.filteredValue.length;
|
||||
}
|
||||
|
||||
isHighlighted(qtyType?: string): boolean {
|
||||
return isQtyTypeHighlighted(qtyType);
|
||||
}
|
||||
|
||||
distinctMainColumn(field: keyof ScheduleOrderDto): string[] {
|
||||
return distinctStringValues(this.gridData(), field);
|
||||
}
|
||||
|
||||
distinctMainDates(): LabeledFilterValue[] {
|
||||
return distinctDateValues(this.gridData(), 'lastUpdateDate');
|
||||
}
|
||||
|
||||
distinctFromRows<T>(rows: T[] | null | undefined, field: keyof T): string[] {
|
||||
return distinctStringValues(rows, field);
|
||||
}
|
||||
|
||||
distinctDatesFromRows(
|
||||
rows: ScheduleOrderDetailDetailDto[] | null | undefined,
|
||||
field: keyof ScheduleOrderDetailDetailDto,
|
||||
): LabeledFilterValue[] {
|
||||
return distinctDateValues(rows, field);
|
||||
}
|
||||
|
||||
distinctNumbersFromRows(
|
||||
rows: ScheduleOrderDetailDetailDto[] | null | undefined,
|
||||
field: keyof ScheduleOrderDetailDetailDto,
|
||||
): number[] {
|
||||
return distinctNumericValues(rows, field);
|
||||
}
|
||||
|
||||
navigateToOrder(id: number): void {
|
||||
this.router.navigate(['/ScheduleOrder', id]);
|
||||
}
|
||||
|
||||
onGlobalFilterChange(value: string): void {
|
||||
this.globalFilterValue = value;
|
||||
this.mainTable?.filterGlobal(value, 'contains');
|
||||
}
|
||||
|
||||
async onRowExpand(event: { data: ScheduleOrderDto }): Promise<void> {
|
||||
const order = event.data;
|
||||
if (order.scheduleOrderDetails?.length) return;
|
||||
|
||||
const full = await this.scheduleOrderService.getById(order.id);
|
||||
order.scheduleOrderDetails = full.scheduleOrderDetails ?? [];
|
||||
for (const detail of order.scheduleOrderDetails) {
|
||||
detail.orderNumber = full.poNum;
|
||||
detail.recipientCode = full.recipientCode;
|
||||
detail.recipientName = full.recipientName;
|
||||
detail.purchaserName = full.purchaserCode;
|
||||
}
|
||||
}
|
||||
|
||||
saveFilters(): void {
|
||||
if (!this.mainTable) return;
|
||||
|
||||
const state = serializeTableFilters(this.mainTable);
|
||||
if (!state) {
|
||||
this.messageService.add({
|
||||
severity: 'warn',
|
||||
summary: 'Brak filtrów',
|
||||
detail: 'Ustaw filtry kolumn lub wyszukiwanie globalne przed zapisaniem.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
saveGridFilters(GRID_FILTER_KEY, state);
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: 'Filtry zapisane',
|
||||
detail: 'Filtry zostaną przywrócone przy następnym wejściu na listę.',
|
||||
});
|
||||
}
|
||||
|
||||
deleteFilters(): void {
|
||||
deleteGridFilters(GRID_FILTER_KEY);
|
||||
this.globalFilterValue = '';
|
||||
|
||||
if (this.mainTable) {
|
||||
clearTableFilters(this.mainTable);
|
||||
}
|
||||
|
||||
this.messageService.add({
|
||||
severity: 'info',
|
||||
summary: 'Filtry usunięte',
|
||||
detail: 'Zapisane filtry zostały usunięte z pamięci przeglądarki.',
|
||||
});
|
||||
}
|
||||
|
||||
private restoreFilters(): void {
|
||||
if (!this.mainTable) return;
|
||||
|
||||
const saved = loadSavedGridFilters(GRID_FILTER_KEY);
|
||||
if (!saved) return;
|
||||
|
||||
this.globalFilterValue = applySavedFiltersToTable(this.mainTable, saved, this.globalFilterValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { WzRowMarelliDto, WzRowMeyleDto } from '../../core/models';
|
||||
|
||||
export function isValidMeyleScannedValue(scannedValue: string): boolean {
|
||||
const year = new Date().getFullYear() - 2000;
|
||||
return scannedValue.startsWith(`${year}X`);
|
||||
}
|
||||
|
||||
export function findCombinationsByQuantity<T extends { quantity?: number | null }>(
|
||||
records: T[],
|
||||
targetSum: number,
|
||||
): T[][] {
|
||||
const result: T[][] = [];
|
||||
const currentCombination: T[] = [];
|
||||
|
||||
function backtrack(start: number, currentSum: number): void {
|
||||
if (currentSum === targetSum) {
|
||||
result.push([...currentCombination]);
|
||||
return;
|
||||
}
|
||||
for (let i = start; i < records.length; i++) {
|
||||
const qty = records[i].quantity ?? 0;
|
||||
if (currentSum + qty <= targetSum) {
|
||||
currentCombination.push(records[i]);
|
||||
backtrack(i + 1, currentSum + qty);
|
||||
currentCombination.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
backtrack(0, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function findCombinations(records: WzRowMeyleDto[], targetSum: number): WzRowMeyleDto[][] {
|
||||
return findCombinationsByQuantity(records, targetSum);
|
||||
}
|
||||
|
||||
export function findMarelliCombinations(records: WzRowMarelliDto[], targetSum: number): WzRowMarelliDto[][] {
|
||||
return findCombinationsByQuantity(records, targetSum);
|
||||
}
|
||||
|
||||
export function isQtyTypeHighlighted(qtyType?: string): boolean {
|
||||
return qtyType === '54' || qtyType === '83' || qtyType === '84';
|
||||
}
|
||||
|
||||
export function generateTempPassword(): string {
|
||||
return crypto.randomUUID().substring(0, 8);
|
||||
}
|
||||
|
||||
export function generateGuid(): string {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { FilterMetadata } from 'primeng/api';
|
||||
import { Table } from 'primeng/table';
|
||||
|
||||
export interface SavedGridFilterState {
|
||||
global?: string;
|
||||
columns?: Record<string, FilterMetadata | FilterMetadata[]>;
|
||||
}
|
||||
|
||||
export function serializeTableFilters(table: Table): SavedGridFilterState | null {
|
||||
const columns: Record<string, FilterMetadata | FilterMetadata[]> = {};
|
||||
|
||||
for (const [key, meta] of Object.entries(table.filters ?? {})) {
|
||||
if (key === 'global') continue;
|
||||
if (hasFilterValue(meta)) {
|
||||
columns[key] = JSON.parse(JSON.stringify(meta));
|
||||
}
|
||||
}
|
||||
|
||||
const globalMeta = table.filters?.['global'] as FilterMetadata | undefined;
|
||||
const global = typeof globalMeta?.value === 'string' ? globalMeta.value : '';
|
||||
|
||||
if (!global && Object.keys(columns).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { global, columns };
|
||||
}
|
||||
|
||||
type RestorableFilterMetadata = FilterMetadata & { applyFilter?: boolean };
|
||||
|
||||
export function applySavedFiltersToTable(table: Table, saved: SavedGridFilterState, globalFilterValue: string): string {
|
||||
table.clearFilterValues();
|
||||
|
||||
if (saved.columns && Object.keys(saved.columns).length > 0) {
|
||||
table.restoringFilter = true;
|
||||
const restored: Record<string, RestorableFilterMetadata | RestorableFilterMetadata[]> = {};
|
||||
|
||||
for (const [key, meta] of Object.entries(saved.columns)) {
|
||||
if (Array.isArray(meta)) {
|
||||
restored[key] = meta.map((m) => ({ ...m, applyFilter: hasFilterValue(m) }));
|
||||
} else if (meta) {
|
||||
restored[key] = { ...meta, applyFilter: hasFilterValue(meta) };
|
||||
}
|
||||
}
|
||||
|
||||
table.filters = restored as Record<string, FilterMetadata | FilterMetadata[]>;
|
||||
}
|
||||
|
||||
const global = saved.global ?? globalFilterValue ?? '';
|
||||
if (global) {
|
||||
table.filterGlobal(global, 'contains');
|
||||
return global;
|
||||
}
|
||||
|
||||
table._filter();
|
||||
return '';
|
||||
}
|
||||
|
||||
export function clearTableFilters(table: Table): void {
|
||||
table.clearFilterValues();
|
||||
table.filteredValue = null;
|
||||
table._filter();
|
||||
table.tableService.onValueChange(table.value);
|
||||
}
|
||||
|
||||
function hasFilterValue(meta: FilterMetadata | FilterMetadata[] | undefined): boolean {
|
||||
if (!meta) return false;
|
||||
if (Array.isArray(meta)) {
|
||||
return meta.some((m) => hasSingleFilterValue(m));
|
||||
}
|
||||
return hasSingleFilterValue(meta);
|
||||
}
|
||||
|
||||
function hasSingleFilterValue(meta: FilterMetadata): boolean {
|
||||
if (meta.value == null) return false;
|
||||
if (Array.isArray(meta.value)) return meta.value.length > 0;
|
||||
return meta.value !== '';
|
||||
}
|
||||
|
||||
export function loadSavedGridFilters(storageKey: string): SavedGridFilterState | null {
|
||||
const raw = localStorage.getItem(storageKey);
|
||||
if (!raw) return null;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as SavedGridFilterState | { global?: string };
|
||||
|
||||
// Legacy format from earlier Angular migration (global only)
|
||||
if (parsed && !('columns' in parsed) && parsed.global) {
|
||||
return { global: parsed.global, columns: {} };
|
||||
}
|
||||
|
||||
return parsed as SavedGridFilterState;
|
||||
} catch {
|
||||
localStorage.removeItem(storageKey);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveGridFilters(storageKey: string, state: SavedGridFilterState): void {
|
||||
localStorage.setItem(storageKey, JSON.stringify(state));
|
||||
}
|
||||
|
||||
export function deleteGridFilters(storageKey: string): void {
|
||||
localStorage.removeItem(storageKey);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
export interface LabeledFilterValue {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function formatGridDate(iso: string): string {
|
||||
if (!iso) return '';
|
||||
const date = new Date(iso);
|
||||
if (Number.isNaN(date.getTime())) return iso;
|
||||
return date.toLocaleDateString('pl-PL', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
}
|
||||
|
||||
export function distinctStringValues<T>(rows: T[] | null | undefined, field: keyof T): string[] {
|
||||
if (!rows?.length) return [];
|
||||
|
||||
const values = new Set<string>();
|
||||
for (const row of rows) {
|
||||
const raw = row[field];
|
||||
if (raw == null || raw === '') continue;
|
||||
values.add(String(raw));
|
||||
}
|
||||
|
||||
return [...values].sort((a, b) => a.localeCompare(b, 'pl'));
|
||||
}
|
||||
|
||||
export function distinctNumericValues<T>(rows: T[] | null | undefined, field: keyof T): number[] {
|
||||
if (!rows?.length) return [];
|
||||
|
||||
const values = new Set<number>();
|
||||
for (const row of rows) {
|
||||
const raw = row[field];
|
||||
if (typeof raw === 'number' && !Number.isNaN(raw)) {
|
||||
values.add(raw);
|
||||
}
|
||||
}
|
||||
|
||||
return [...values].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
export function distinctDateValues<T>(
|
||||
rows: T[] | null | undefined,
|
||||
field: keyof T,
|
||||
format: (iso: string) => string = formatGridDate,
|
||||
): LabeledFilterValue[] {
|
||||
if (!rows?.length) return [];
|
||||
|
||||
const byValue = new Map<string, LabeledFilterValue>();
|
||||
for (const row of rows) {
|
||||
const raw = row[field];
|
||||
if (raw == null || raw === '') continue;
|
||||
const value = String(raw);
|
||||
if (!byValue.has(value)) {
|
||||
byValue.set(value, { value, label: format(value) });
|
||||
}
|
||||
}
|
||||
|
||||
return [...byValue.values()].sort((a, b) => a.label.localeCompare(b.label, 'pl'));
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
|
||||
@Component({
|
||||
selector: 'app-unauthorized',
|
||||
standalone: true,
|
||||
imports: [ButtonDirective],
|
||||
template: `
|
||||
<div class="auth-layout">
|
||||
<aside class="auth-layout__brand">
|
||||
<div class="auth-layout__brand-logo">
|
||||
<img src="logo.svg" width="40" height="40" alt="" />
|
||||
<h1 class="auth-layout__brand-title">FA Krosno Manager</h1>
|
||||
</div>
|
||||
<p class="auth-layout__brand-tagline">
|
||||
System zarządzania zamówieniami DELFOR, magazynem i procesami EDI dla operacji produkcyjnych.
|
||||
</p>
|
||||
<div class="auth-layout__features">
|
||||
<div class="auth-layout__feature"><i class="pi pi-building"></i> Zamówienia DELFOR i harmonogramy</div>
|
||||
<div class="auth-layout__feature"><i class="pi pi-warehouse"></i> Magazyn i packing listy</div>
|
||||
<div class="auth-layout__feature"><i class="pi pi-database"></i> Integracja EDI / Syteline</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="auth-layout__form">
|
||||
<div class="auth-card">
|
||||
<h2 class="auth-card__title">Brak autoryzacji</h2>
|
||||
<p class="auth-card__subtitle">Ups! Wygląda na to, że nie masz dostępu do tej strony.</p>
|
||||
<p class="text-secondary mb-3">
|
||||
Aby kontynuować, zaloguj się do swojego konta.
|
||||
</p>
|
||||
<button pButton type="button" [fluid]="true" (click)="goLogin()"><i class="pi pi-sign-in"></i> Przejdź do logowania</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class UnauthorizedComponent {
|
||||
private readonly router = inject(Router);
|
||||
goLogin(): void { this.router.navigate(['/login']); }
|
||||
}
|
||||
250
FaKrosnoManagerAngular/src/app/warehouse/warehouse.component.ts
Normal file
250
FaKrosnoManagerAngular/src/app/warehouse/warehouse.component.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Table, TableCheckbox, TableHeaderCheckbox, TableModule } from 'primeng/table';
|
||||
import { SelectModule } from 'primeng/select';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { MaterialTransactionDto, WzClientDto, WzHeaderDto, WzRowMarelliDto, WzRowMeyleDto } from '../core/models';
|
||||
import { WarehouseService } from '../core/services/warehouse.service';
|
||||
import { generateGuid } from '../shared/utils/business-logic';
|
||||
import { PageShellComponent } from '../shared/components/page-shell/page-shell.component';
|
||||
import { ContentSectionComponent } from '../shared/components/content-section/content-section.component';
|
||||
|
||||
const SELECTED_CLIENT_KEY = 'SelectedClientId';
|
||||
|
||||
@Component({
|
||||
selector: 'app-warehouse',
|
||||
standalone: true,
|
||||
imports: [TableModule, Table, TableCheckbox, TableHeaderCheckbox, SelectModule, ButtonDirective, DialogModule, FormsModule, DatePipe, PageShellComponent, ContentSectionComponent],
|
||||
template: `
|
||||
<app-page-shell
|
||||
title="Dokumenty WZ na Magazynie"
|
||||
subtitle="Zarządzanie dokumentami WZ i packing listami dla klientów Meyle i Marelli"
|
||||
icon="pi pi-warehouse"
|
||||
>
|
||||
<app-content-section title="Klient" icon="pi pi-users">
|
||||
<div class="field-group">
|
||||
<label class="field-label">Wybierz klienta</label>
|
||||
<p-select
|
||||
[options]="clients()"
|
||||
optionLabel="name"
|
||||
optionValue="id"
|
||||
placeholder="Wybierz Klienta"
|
||||
[(ngModel)]="selectedClientId"
|
||||
(onChange)="onClientChange()"
|
||||
styleClass="w-full"
|
||||
/>
|
||||
</div>
|
||||
</app-content-section>
|
||||
|
||||
@if (isVisible()) {
|
||||
<app-content-section title="Dokumenty WZ" icon="pi pi-file" [flush]="true">
|
||||
<div headerActions>
|
||||
<button pButton type="button" (click)="createPackingList()"><i class="pi pi-save"></i> Utwórz Packing List</button>
|
||||
</div>
|
||||
<p-table
|
||||
#wzGrid
|
||||
[value]="wzDataSource()"
|
||||
[(selection)]="selectedWzRows"
|
||||
dataKey="mtGroupNum"
|
||||
[paginator]="true"
|
||||
[rows]="5"
|
||||
selectionMode="multiple"
|
||||
styleClass="p-datatable-sm"
|
||||
[showGridlines]="true"
|
||||
[stripedRows]="true"
|
||||
>
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th style="width: 3rem"><p-table-header-checkbox /></th>
|
||||
<th pSortableColumn="mtGroupNum">Numer WZ</th>
|
||||
<th pSortableColumn="createDate">Data utworzenia</th>
|
||||
<th pSortableColumn="refNum">Numer zamówienia</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #body let-row>
|
||||
<tr>
|
||||
<td><p-table-checkbox [value]="row" /></td>
|
||||
<td>{{ row.mtGroupNum }}</td>
|
||||
<td>{{ row.createDate | date: 'dd.MM.yyyy' }}</td>
|
||||
<td>{{ row.refNum }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
|
||||
<app-content-section title="Packling Listy" icon="pi pi-list" [flush]="true">
|
||||
<p-table [value]="wzHeaders()" [paginator]="true" [rows]="5" selectionMode="single" [(selection)]="selectedHeader" dataKey="id" styleClass="p-datatable-sm" [showGridlines]="true" [stripedRows]="true">
|
||||
<ng-template #body let-header>
|
||||
<tr (dblclick)="openPackList(header)">
|
||||
<td>{{ header.id }}</td>
|
||||
<td>{{ header.wzNumbers }}</td>
|
||||
<td>{{ header.createdDate | date: 'dd.MM.yyyy' }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #header>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Numery WZ</th>
|
||||
<th>Data utworzenia</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</app-content-section>
|
||||
}
|
||||
</app-page-shell>
|
||||
|
||||
<p-dialog header="Informacja" [(visible)]="showNoSelectionDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
<p>Błąd: Zaznacz przynajmniej jeden rekord, żeby wygenerowac Pack List!</p>
|
||||
<ng-template #footer>
|
||||
<button pButton type="button" (click)="showNoSelectionDialog = false">OK</button>
|
||||
</ng-template>
|
||||
</p-dialog>
|
||||
|
||||
<p-dialog header="Błąd" [(visible)]="showDuplicateDialog" [modal]="true" [style]="{ width: '500px' }">
|
||||
<p>Błąd: Dla zaznaczonego rekordu istnieje już PackingList!</p>
|
||||
<ng-template #footer>
|
||||
<button pButton type="button" (click)="showDuplicateDialog = false">OK</button>
|
||||
</ng-template>
|
||||
</p-dialog>
|
||||
`,
|
||||
styles: `:host ::ng-deep .w-full { width: 100%; max-width: 400px; }`,
|
||||
})
|
||||
export class WarehouseComponent implements OnInit {
|
||||
private readonly warehouseService = inject(WarehouseService);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
readonly clients = signal<WzClientDto[]>([]);
|
||||
readonly wzDataSource = signal<MaterialTransactionDto[]>([]);
|
||||
readonly wzHeaders = signal<WzHeaderDto[]>([]);
|
||||
readonly materialTransactions = signal<MaterialTransactionDto[]>([]);
|
||||
readonly isVisible = signal(false);
|
||||
|
||||
selectedClientId: string | null = null;
|
||||
selectedClient: WzClientDto | null = null;
|
||||
selectedWzRows: MaterialTransactionDto[] = [];
|
||||
selectedHeader: WzHeaderDto | null = null;
|
||||
|
||||
showNoSelectionDialog = false;
|
||||
showDuplicateDialog = false;
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
const all = await this.warehouseService.getAllClients();
|
||||
const filtered = all.filter((c) => ['MAGNETI MARELLI', 'MEYLE'].some((n) => n.toLowerCase() === c.name.toLowerCase()));
|
||||
this.clients.set(filtered);
|
||||
|
||||
const saved = localStorage.getItem(SELECTED_CLIENT_KEY);
|
||||
if (saved && filtered.some((c) => c.id === saved)) {
|
||||
this.selectedClientId = saved;
|
||||
await this.onClientChange();
|
||||
}
|
||||
}
|
||||
|
||||
async onClientChange(): Promise<void> {
|
||||
if (!this.selectedClientId) {
|
||||
this.isVisible.set(false);
|
||||
this.selectedClient = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedClient = this.clients().find((c) => c.id === this.selectedClientId) ?? null;
|
||||
if (!this.selectedClient) return;
|
||||
|
||||
this.isVisible.set(true);
|
||||
localStorage.setItem(SELECTED_CLIENT_KEY, this.selectedClientId);
|
||||
|
||||
const txs = await this.warehouseService.getAllClientWzs(
|
||||
this.selectedClient.customerNumber,
|
||||
this.selectedClient.customerSequence ?? 0,
|
||||
);
|
||||
this.materialTransactions.set(txs);
|
||||
const grouped = Object.values(
|
||||
txs.reduce<Record<string, MaterialTransactionDto>>((acc, t) => {
|
||||
const key = t.mtGroupNum ?? '';
|
||||
if (!acc[key]) acc[key] = t;
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
this.wzDataSource.set(grouped);
|
||||
this.wzHeaders.set(
|
||||
await this.warehouseService.getAllClientWzHeaders(
|
||||
this.selectedClient.customerNumber,
|
||||
this.selectedClient.customerSequence ?? 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
openPackList(header: WzHeaderDto): void {
|
||||
if (!this.selectedClient) return;
|
||||
this.router.navigate([`/Warehouse/${this.selectedClient.shortName}/PackList/${header.id}`]);
|
||||
}
|
||||
|
||||
async createPackingList(): Promise<void> {
|
||||
if (!this.selectedWzRows.length) {
|
||||
this.showNoSelectionDialog = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const wzNumbers = [...new Set(this.selectedWzRows.map((x) => x.mtGroupNum))].join(', ');
|
||||
if (this.wzHeaders().some((x) => x.wzNumbers === wzNumbers)) {
|
||||
this.showDuplicateDialog = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const wzHeader: WzHeaderDto = {
|
||||
id: generateGuid(),
|
||||
fk_Client: this.selectedClient?.id,
|
||||
createdDate: new Date().toISOString(),
|
||||
wzNumbers,
|
||||
};
|
||||
|
||||
await this.warehouseService.createWzHeader(wzHeader);
|
||||
|
||||
const shortName = this.selectedClient?.shortName.toUpperCase();
|
||||
const selectedNums = new Set(this.selectedWzRows.map((x) => x.mtGroupNum));
|
||||
const related = this.materialTransactions().filter((x) => selectedNums.has(x.mtGroupNum));
|
||||
|
||||
if (shortName === 'MEYLE') {
|
||||
const rows: WzRowMeyleDto[] = [];
|
||||
for (const mt of related) {
|
||||
const customerOrder = await this.warehouseService.getCustomerOrder(mt.refNum ?? '');
|
||||
const item = await this.warehouseService.getItem(mt.item ?? '', customerOrder.custNum);
|
||||
rows.push({
|
||||
id: generateGuid(),
|
||||
quantity: Math.abs(mt.qty ?? 0),
|
||||
itemNumber: item.custItem,
|
||||
orderNumber: customerOrder.custPo,
|
||||
wzNumber: mt.mtGroupNum ?? '',
|
||||
fk_Header: wzHeader.id,
|
||||
transactionNumber: mt.transNum ?? 0,
|
||||
partNumberSl: mt.nr_KARTY_KONTROLNEJ,
|
||||
faIndex: item.item,
|
||||
});
|
||||
}
|
||||
await this.warehouseService.createWzRowsMeyle(rows);
|
||||
this.router.navigate(['/Warehouse/Meyle/PackList', wzHeader.id]);
|
||||
} else if (shortName === 'MARELLI') {
|
||||
const rows: WzRowMarelliDto[] = [];
|
||||
for (const mt of related) {
|
||||
const customerOrder = await this.warehouseService.getCustomerOrder(mt.refNum ?? '');
|
||||
const item = await this.warehouseService.getItem(mt.item ?? '', customerOrder.custNum);
|
||||
rows.push({
|
||||
id: generateGuid(),
|
||||
quantity: Math.abs(mt.qty ?? 0),
|
||||
itemNumber: item.custItem,
|
||||
orderNumber: customerOrder.custPo,
|
||||
wzNumber: mt.mtGroupNum ?? '',
|
||||
fkHeader: wzHeader.id,
|
||||
transactionNumber: mt.transNum ?? 0,
|
||||
type: 'MIX',
|
||||
faIndex: item.item,
|
||||
engineerNumber: item.uf_FKR_CustItem2 ?? '',
|
||||
});
|
||||
}
|
||||
await this.warehouseService.createWzRowsMarelli(rows);
|
||||
this.router.navigate(['/Warehouse/Marelli/PackList', wzHeader.id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
apiBaseUrl: 'http://localhost:5001',
|
||||
/** Free community key from https://primeui.dev/licenses/community */
|
||||
primeNgLicense: '',
|
||||
};
|
||||
6
FaKrosnoManagerAngular/src/environments/environment.ts
Normal file
6
FaKrosnoManagerAngular/src/environments/environment.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBaseUrl: 'http://localhost:5001',
|
||||
/** Free community key from https://primeui.dev/licenses/community */
|
||||
primeNgLicense: 'eyJpZCI6IjZmN2Q2YTA3LWE3NWEtNDdmOC1hYzIwLWI0ZTJlN2JmM2NiYyIsInByb2R1Y3QiOiJwcmltZXVpIiwidGllciI6ImNvbW11bml0eSIsInR5cGUiOiJkZXYiLCJpYXQiOjE3ODQ0MDM4MTUsImV4cCI6MTgxNTkzOTgxNX0.wFfRpKvBUOoX9xV2XahDj_gPlaYRKWtIVS04uFXdDUgDxI9c5UvsagWuhzCc0nFISCdZDVTol2s7Y9WTyoxdDA',
|
||||
};
|
||||
16
FaKrosnoManagerAngular/src/index.html
Normal file
16
FaKrosnoManagerAngular/src/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>FA Krosno Manager</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
6
FaKrosnoManagerAngular/src/main.ts
Normal file
6
FaKrosnoManagerAngular/src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
|
||||
bootstrapApplication(App, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
131
FaKrosnoManagerAngular/src/styles/_design-tokens.scss
Normal file
131
FaKrosnoManagerAngular/src/styles/_design-tokens.scss
Normal file
@@ -0,0 +1,131 @@
|
||||
:root {
|
||||
/* Surfaces */
|
||||
--color-bg: #eef1f6;
|
||||
--color-bg-subtle: #e4e8ef;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-raised: #ffffff;
|
||||
--color-surface-hover: #f3f5f9;
|
||||
--color-surface-active: #e8edf5;
|
||||
--color-border: #d8dee9;
|
||||
--color-border-strong: #c2cad8;
|
||||
|
||||
/* Sidebar */
|
||||
--color-sidebar-bg: #1e293b;
|
||||
--color-sidebar-surface: #243044;
|
||||
--color-sidebar-border: rgba(255, 255, 255, 0.08);
|
||||
--color-sidebar-text: #cbd5e1;
|
||||
--color-sidebar-text-muted: #94a3b8;
|
||||
--color-sidebar-text-active: #ffffff;
|
||||
--color-sidebar-accent: #3b82f6;
|
||||
--color-sidebar-accent-soft: rgba(59, 130, 246, 0.16);
|
||||
|
||||
/* Text */
|
||||
--color-text-primary: #0f172a;
|
||||
--color-text-secondary: #64748b;
|
||||
--color-text-muted: #94a3b8;
|
||||
--color-text-disabled: #cbd5e1;
|
||||
--color-text-inverse: #ffffff;
|
||||
|
||||
/* Brand accent */
|
||||
--color-accent: #1a56db;
|
||||
--color-accent-hover: #1648b8;
|
||||
--color-accent-active: #123a94;
|
||||
--color-accent-soft: rgba(26, 86, 219, 0.08);
|
||||
--color-accent-border: rgba(26, 86, 219, 0.24);
|
||||
|
||||
/* Status */
|
||||
--color-success: #047857;
|
||||
--color-success-soft: rgba(4, 120, 87, 0.1);
|
||||
--color-warning: #b45309;
|
||||
--color-warning-soft: rgba(180, 83, 9, 0.1);
|
||||
--color-error: #b91c1c;
|
||||
--color-error-soft: rgba(185, 28, 28, 0.1);
|
||||
--color-info: #0369a1;
|
||||
--color-info-soft: rgba(3, 105, 161, 0.1);
|
||||
|
||||
/* Spacing */
|
||||
--spacing-1: 4px;
|
||||
--spacing-2: 8px;
|
||||
--spacing-3: 12px;
|
||||
--spacing-4: 16px;
|
||||
--spacing-5: 20px;
|
||||
--spacing-6: 24px;
|
||||
--spacing-7: 32px;
|
||||
|
||||
/* Layout */
|
||||
--sidebar-width: 252px;
|
||||
--sidebar-collapsed-width: 64px;
|
||||
--topbar-height: 52px;
|
||||
--page-max-width: 1440px;
|
||||
--content-padding: 20px;
|
||||
|
||||
/* Typography */
|
||||
--font-family: 'Inter', 'IBM Plex Sans', 'Segoe UI', sans-serif;
|
||||
--font-size-xs: 11px;
|
||||
--font-size-sm: 12.5px;
|
||||
--font-size-base: 13.5px;
|
||||
--font-size-md: 14px;
|
||||
--font-size-lg: 16px;
|
||||
--font-size-xl: 18px;
|
||||
--font-size-2xl: 22px;
|
||||
--line-height-tight: 1.25;
|
||||
--line-height-normal: 1.5;
|
||||
|
||||
/* Elevation */
|
||||
--shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
--shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
--shadow-md: 0 4px 12px rgba(15, 23, 42, 0.08);
|
||||
--shadow-lg: 0 12px 32px rgba(15, 23, 42, 0.12);
|
||||
|
||||
/* Radius — ERP: subtle, not bubbly */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
|
||||
/* Tables */
|
||||
--table-header-bg: #f8fafc;
|
||||
--table-row-hover: #f1f5f9;
|
||||
--table-row-stripe: #fafbfc;
|
||||
--table-border: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--color-bg: #0b1220;
|
||||
--color-bg-subtle: #111827;
|
||||
--color-surface: #151f32;
|
||||
--color-surface-raised: #1a2438;
|
||||
--color-surface-hover: #1e293b;
|
||||
--color-surface-active: #243044;
|
||||
--color-border: #2d3a52;
|
||||
--color-border-strong: #3d4f6f;
|
||||
|
||||
--color-sidebar-bg: #0f172a;
|
||||
--color-sidebar-surface: #151f32;
|
||||
--color-sidebar-border: rgba(255, 255, 255, 0.06);
|
||||
--color-sidebar-text: #94a3b8;
|
||||
--color-sidebar-text-muted: #64748b;
|
||||
--color-sidebar-text-active: #f8fafc;
|
||||
--color-sidebar-accent: #60a5fa;
|
||||
--color-sidebar-accent-soft: rgba(96, 165, 250, 0.14);
|
||||
|
||||
--color-text-primary: #f1f5f9;
|
||||
--color-text-secondary: #94a3b8;
|
||||
--color-text-muted: #64748b;
|
||||
--color-text-disabled: #475569;
|
||||
|
||||
--color-accent: #3b82f6;
|
||||
--color-accent-hover: #2563eb;
|
||||
--color-accent-active: #1d4ed8;
|
||||
--color-accent-soft: rgba(59, 130, 246, 0.12);
|
||||
--color-accent-border: rgba(59, 130, 246, 0.3);
|
||||
|
||||
--table-header-bg: #1a2438;
|
||||
--table-row-hover: #1e293b;
|
||||
--table-row-stripe: #151f32;
|
||||
--table-border: #2d3a52;
|
||||
|
||||
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.24);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.32);
|
||||
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
572
FaKrosnoManagerAngular/src/styles/styles.scss
Normal file
572
FaKrosnoManagerAngular/src/styles/styles.scss
Normal file
@@ -0,0 +1,572 @@
|
||||
@use 'design-tokens';
|
||||
|
||||
@import 'primeicons/primeicons.css';
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-normal);
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* ─── Page shell ─── */
|
||||
.page-shell {
|
||||
max-width: var(--page-max-width);
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.page-shell__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-4) var(--spacing-5);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
|
||||
.page-shell__heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-3);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.page-shell__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.page-shell__title {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
line-height: var(--line-height-tight);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.page-shell__subtitle {
|
||||
margin: var(--spacing-1) 0 0;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.page-shell__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.page-shell__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
/* ─── Content sections ─── */
|
||||
.content-section {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-xs);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-section--flush .content-section__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content-section__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-3);
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
background: var(--table-header-bg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.content-section__heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content-section__icon {
|
||||
color: var(--color-accent);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content-section__title {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.content-section__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.content-section__body {
|
||||
padding: var(--spacing-4);
|
||||
}
|
||||
|
||||
/* ─── Data table toolbar ─── */
|
||||
.data-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-3);
|
||||
flex-wrap: wrap;
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
background: var(--table-header-bg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.data-toolbar__left,
|
||||
.data-toolbar__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.data-toolbar__search {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
/* ─── Form layout ─── */
|
||||
.field-label {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-1);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.field-group {
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.form-grid--2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.form-grid--3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.form-grid--2,
|
||||
.form-grid--3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Alerts ─── */
|
||||
.alert-info {
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
background: var(--color-info-soft);
|
||||
border: 1px solid rgba(3, 105, 161, 0.25);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
background: var(--color-error-soft);
|
||||
border: 1px solid rgba(185, 28, 28, 0.25);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-error);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
background: var(--color-success-soft);
|
||||
border: 1px solid rgba(4, 120, 87, 0.25);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-success);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* ─── Detail metadata grid ─── */
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--spacing-3) var(--spacing-6);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.detail-grid__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.detail-grid__label {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-grid__value {
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Row highlight (business rule) ─── */
|
||||
.qty-type-highlight {
|
||||
background-color: var(--color-error-soft) !important;
|
||||
}
|
||||
|
||||
.qty-type-highlight > td {
|
||||
color: var(--color-error) !important;
|
||||
}
|
||||
|
||||
/* ─── Auth pages ─── */
|
||||
.auth-layout {
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 480px;
|
||||
}
|
||||
|
||||
.auth-layout__brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-7);
|
||||
background: linear-gradient(145deg, #1e293b 0%, #0f172a 55%, #1a3a6b 100%);
|
||||
color: var(--color-text-inverse);
|
||||
}
|
||||
|
||||
.auth-layout__brand-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
margin-bottom: var(--spacing-6);
|
||||
}
|
||||
|
||||
.auth-layout__brand-logo img {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.auth-layout__brand-title {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.auth-layout__brand-tagline {
|
||||
margin: 0 0 var(--spacing-6);
|
||||
font-size: var(--font-size-lg);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
max-width: 420px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.auth-layout__features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.auth-layout__feature {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
font-size: var(--font-size-sm);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.auth-layout__feature i {
|
||||
color: var(--color-sidebar-accent);
|
||||
}
|
||||
|
||||
.auth-layout__form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-6);
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: var(--spacing-6);
|
||||
}
|
||||
|
||||
.auth-card__title {
|
||||
margin: 0 0 var(--spacing-1);
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.auth-card__subtitle {
|
||||
margin: 0 0 var(--spacing-5);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.auth-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.auth-layout__brand {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── PrimeNG global overrides ─── */
|
||||
.p-datatable {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.p-datatable .p-datatable-thead > tr > th {
|
||||
padding: 10px 12px;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--table-header-bg) !important;
|
||||
border-color: var(--table-border) !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.p-datatable .p-datatable-tbody > tr > td {
|
||||
padding: 8px 12px;
|
||||
border-color: var(--table-border) !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.p-datatable .p-datatable-tbody > tr {
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.p-datatable .p-datatable-tbody > tr:nth-child(even) {
|
||||
background: var(--table-row-stripe);
|
||||
}
|
||||
|
||||
.p-datatable .p-datatable-tbody > tr:hover {
|
||||
background: var(--table-row-hover) !important;
|
||||
}
|
||||
|
||||
.p-datatable .p-datatable-tbody > tr.erp-table__row,
|
||||
.p-datatable .p-datatable-tbody > tr:has(+ tr .erp-table__nested),
|
||||
.p-datatable .p-datatable-tbody > tr:not(.p-datatable-emptymessage-row) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.p-datatable .p-datatable-tbody > tr:has(td[colspan]) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.p-datatable .p-datatable-tbody > tr:not(.p-row-expanded):not(.p-datatable-emptymessage-row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.p-datatable.p-datatable-sm .p-datatable-thead > tr > th,
|
||||
.p-datatable.p-datatable-sm .p-datatable-tbody > tr > td {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.p-datatable .p-datatable-header {
|
||||
background: var(--table-header-bg);
|
||||
border-color: var(--table-border);
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
}
|
||||
|
||||
.p-datatable .p-paginator {
|
||||
background: var(--table-header-bg);
|
||||
border-color: var(--table-border);
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.p-panel {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-xs);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.p-panel .p-panel-header {
|
||||
background: var(--table-header-bg);
|
||||
border-color: var(--color-border);
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.p-panel .p-panel-content {
|
||||
padding: var(--spacing-4);
|
||||
}
|
||||
|
||||
.p-dialog .p-dialog-header {
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.p-dialog .p-dialog-content {
|
||||
padding: var(--spacing-4);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.p-inputtext,
|
||||
.p-select,
|
||||
.p-password input {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.p-button {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.column-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.column-header span {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.p-breadcrumb {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.p-button.w-full,
|
||||
.p-button-fluid {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: var(--spacing-5) 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.grid-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
14
FaKrosnoManagerAngular/tsconfig.app.json
Normal file
14
FaKrosnoManagerAngular/tsconfig.app.json
Normal file
@@ -0,0 +1,14 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
31
FaKrosnoManagerAngular/tsconfig.json
Normal file
31
FaKrosnoManagerAngular/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
FaKrosnoManagerAngular/tsconfig.spec.json
Normal file
15
FaKrosnoManagerAngular/tsconfig.spec.json
Normal file
@@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"vitest/globals"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
@@ -34,14 +34,15 @@
|
||||
<MenuItems>
|
||||
<MenuItem Text="Zamówienia DELFOR" Url="/" IconCss="fa-solid fa-landmark"></MenuItem>
|
||||
<MenuItem Text="Zarządzanie Indeksami" Url="/Products" IconCss="fa-solid fa-basket-shopping"></MenuItem>
|
||||
@* <MenuItem Text="Zamówienia klienta EDI" Url="/EdiCustomerOrders" IconCss="fa-solid fa-list-check"></MenuItem> *@
|
||||
@* <MenuItem Text="Zamówienia klienta" Url="/CustomerOrders" IconCss="fa-solid fa-database"></MenuItem> *@
|
||||
@if (UserName == "pkus")
|
||||
<MenuItem Text="Magazyn" Url="/Warehouse" IconCss="fa-solid fa-warehouse"></MenuItem>
|
||||
@if (IsAdminRoute())
|
||||
{
|
||||
<MenuItem Text="Admin" IconCss="fa-solid fa-screwdriver-wrench">
|
||||
<MenuItem Text="Administracja" Url="/Admin/PK" IconCss="fa-solid fa-screwdriver-wrench">
|
||||
<MenuItems>
|
||||
<MenuItem Text = "Użytkownicy" Url = "/Admin/UsersManager" IconCss="fa-solid fa-user-tie"></MenuItem>
|
||||
<MenuItem Text= "Scheduler" Url = "/Admin/Scheduler" IconCss="fa-solid fa-calendar-week"></MenuItem>
|
||||
<MenuItem Text="Użytkownicy" Url = "/Admin/PK/UsersManager" IconCss="fa-solid fa-user-tie"></MenuItem>
|
||||
<MenuItem Text="Scheduler" Url = "/Admin/PK/Scheduler" IconCss="fa-solid fa-calendar-week"></MenuItem>
|
||||
<MenuItem Text="Zamówienia klienta EDI" Url="/Admin/PK/EdiCustomerOrders" IconCss="fa-solid fa-list-check"></MenuItem>
|
||||
<MenuItem Text="Zamówienia klienta" Url="/Admin/PK/CustomerOrders" IconCss="fa-solid fa-database"></MenuItem>
|
||||
</MenuItems>
|
||||
</MenuItem>
|
||||
}
|
||||
@@ -63,6 +64,12 @@
|
||||
private bool IsAuthenticated { get; set; }
|
||||
private string UserName { get; set; } = string.Empty;
|
||||
|
||||
private bool IsAdminRoute()
|
||||
{
|
||||
var path = new Uri(NavigationManager.Uri).AbsolutePath;
|
||||
return path.StartsWith("/admin", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// ClaimsPrincipal currentUser = AuthenticationStateProvider.GetCurrentUser();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user