MMD ae977e5e66 fix(wizard): store material name cleanly instead of brand+category+specs
Adding a new item wrote the material name as 'brand + category + specs'
(e.g. 'پارس لوله لوله PE100 سایز ۱۶'), while specs were also stored
correctly in EAV attributes. The wizard then tried to recover the base
name by stripping brand and specs out of that string, but never stripped
the injected category — so the spec stayed baked into the cached material
name and the مشخصه appeared as part of the material name on later visits.

- api.ts / WizardPage / EditModal: build name from the material itself
  only (brand goes to brand_ids, specs to attributes)
- materialBaseName: strip category too, never strip a part that would
  empty the name, and reject a spec-only result; legacy rows whose name
  cannot be disentangled fall back to the stored name rather than a guess
- WizardPage: match existing materials on category+brand so legacy rows
  are still reused instead of duplicated
2026-09-16 20:24:45 +03:30

Procurement Stack v1.3 — Dockerized (PostgreSQL + FastAPI + React + MinIO)

Self-contained ship package: everything needed to run the stack is in this folder — no external files or sibling folders required.

Service Container Tech Exposed port
postgres procurement-postgres PostgreSQL 18 (one instance: procurement + casdoor DBs) 5433
minio procurement-minio MinIO object storage (S3-compatible) 9000 (API), 9001 (console)
casdoor procurement-casdoor Casdoor (OIDC identity provider) 8080
api procurement-api FastAPI + SQLAlchemy (asyncpg) 8000
frontend procurement-frontend React 19 + Vite, served by nginx (internal) via Traefik
traefik procurement-traefik Traefik v3 — public reverse proxy (edge) 80 (web), 443 (websecure)
Browser ──► Traefik :80 (edge)
                │
                ├── Host(localhost) ──► frontend (nginx SPA :80)
                │                            │
                │                            └── /api/* proxy ──► api :8000 (/api/v1)
                │
                ├── Host(api.localhost) ──► api :8000
                ├── Host(auth.localhost) ──► casdoor :8000 (OIDC login)
                └── Host(s3/storage.localhost) ──► minio (API / console)
                                  │
                                  ├── postgres :5432 (procurement + casdoor DBs)
                                  └── minio :9000 (invoice/proforma files)

📁 Layout

v1.3/
├── docker-compose.yml        ← all paths are relative to this folder
├── docker-compose.prod.yml   ← production overlay (real domains + HTTPS via Let's Encrypt)
├── start.ps1                 ← PowerShell helper (build + start + status)
├── mc.cmd                    ← MinIO Client helper (inspect the bucket)
├── README.md
├── sql/
│   ├── migration.sql         ← DB schema v2.6 (auto-runs on first boot)
│   └── 02-casdoor-db.sql     ← creates the `casdoor` role+DB on the same instance
├── infra/
│   └── traefik-dynamic.yml   ← Traefik shared middlewares (HTTP→HTTPS redirect)
├── docs/                     ← ERDs + migration plan + auth guide
│   ├── erd.txt               (v2.4 — old procurement schema)
│   ├── new erd.txt           (v2.5 — material → supplier → category)
│   ├── NewERD.txt            (v2.6 — integrated procurement + inventory)
│   ├── casdoor-auth.md       (v2.6 — Casdoor login, roles, failure scenarios)
│   └── material-supplier-category-migration-plan.md
├── backend/                  ← build context of the `api` service
│   ├── Dockerfile
│   ├── requirements.txt
│   ├── .dockerignore
│   ├── app/                  (source code — procurement + inventory + auth)
│   └── tools/                (smoke-test + seed scripts, copied into the image)
└── frontend/                 ← build context of the `frontend` service
    ├── Dockerfile
    ├── nginx.conf
    ├── .dockerignore
    ├── .env.example          (copy to .env if you need a custom API base URL)
    ├── index.html
    ├── package.json
    ├── package-lock.json
    ├── tsconfig*.json
    ├── vite.config.ts
    ├── public/
    └── src/
        └── pages/
            ├── Dashboard/    (procurement office)
            ├── Wizard/       (add new offer)
            ├── Suppliers/    (suppliers & topics)
            ├── Projects/     (projects)
            ├── Units/        (unit conversions)
            └── Inventory/    (warehouse management panel — React port)

🔐 Login

The whole system is behind a login page (/login). Login is in-page: the form (username / password) is sent to POST /api/v1/auth/login; the backend exchanges it with Casdoor (password grant), verifies the returned JWT, and starts the session — no browser redirect to Casdoor.

  • مدیر پروژه (PM) → lands in the inventory panel (their project) — can record stock changes, manage the project's warehouses and confirm arrivals («کالاهای در راه»).
  • مدیر تدارکات / ادمین → lands in the procurement office — can manage quotes, suppliers, projects, and in the inventory panel: dashboard, daily control, «کالاهای در راه» (receiving included) and «سازمان‌دهی» (users, PMs, warehouses). The «انبارها» and «تغییر موجودی» sections are PM-only.

Developer Mode (top-left button on the login page) is a 3-step flow for the admin (super user): ① superuser auth gate (admin credentials only) → ② role switcher (ادمین / مدیر تدارکات / مدیر پروژه + project) → ③ session activation by role (admin → پنل مدیریت ادمین at /admin, مدیر تدارکات → procurement office, مدیر پروژه → their project's inventory panel). It is controlled by the VITE_ENABLE_DEV_LOGIN env var (in Docker: the DEV_LOGIN_ENABLED compose build arg, default true — set it to false for production builds).

What Where Credentials
App http://localhost (via Traefik) —
Casdoor console http://localhost:8080 admin / 123
Seeded app users (org inventory) razaei (admin), mousavi (مدیر تدارکات), mohaghegh (مدیر پروژه), tester (مدیر پروژه، پروژه «تست») password 123456

Run backend/tools/seed_casdoor.py once after the first boot to create the Casdoor organization, application and users (idempotent). Full details — addresses, credentials, role model, failure scenarios and the checklist — are in docs/casdoor-auth.md.

The currently committed seed (backups/علی محقق/) instead ships a single admin — ali.mohaghegh («علی محقق») — see Restoring the committed seed data.


🌐 Working with the deployed system (server)

The production deployment of this stack runs on the server and is reachable at these public addresses (HTTPS):

What Address
App (ERP) https://aramesh.bennu-api-bahrestan.ir/
Casdoor admin panel https://casdoor.bennu-api-bahrestan.ir/

Log in to the app with your Casdoor account (org inventory); the role is assigned inside the app (users.role — see «Login» above and docs/casdoor-auth.md).

MinIO console on the server (SSH tunnel)

The MinIO console is deliberately loopback-only on the server (127.0.0.1:9001) — verified unreachable from the internet — so the way in is an SSH tunnel from your machine:

  1. Open PowerShell and start the tunnel (keep the window open while you use the panel):

    ssh -L 9001:127.0.0.1:9001 root@171.22.25.75
    

    You will also land in a normal SSH session — that is fine; the tunnel lives as long as the window stays open.

  2. In your browser, open: http://localhost:9001

  3. Log in with:

    Username Password
    aramesh-minio feb10fadb5441019dfc33bf12a9b0f74e7b6c478

You will see the procurement-files bucket — the invoices/proformas the ERP stores, browsable and downloadable from there.

⚠️ These credentials are full admin access to the file storage. Treat them as a production secret (this README is inside the repo, so anyone with repo access can read them — rotate them on the server if they ever leak).

Close the PowerShell window (or Ctrl+C) when you are done to drop the tunnel.

Notes from the server setup:

  • The panel is published only on the server's loopback, so the internet cannot reach it at all — the tunnel is what carries it to your browser.
  • Host port 9000 stays reserved for the deploy-webhook receiver, which is why the console uses 9001.
  • If you ever need the raw S3 API from your machine, or a clickable URL for the console (e.g. storage.aramesh.bennu-api-bahrestan.ir behind Traefik with a password prompt on top), that can be arranged on request — the tunnel remains the safer default.

Prerequisites (target machine)

  • Docker Desktop (Windows) or Docker Engine + Compose plugin (Linux/macOS)
  • ~3–4 GB free disk for images (postgres:18, python:3.13-slim, node:22-alpine, nginx:alpine, minio/minio, minio/mc)
  • Internet access on first build (image + npm ci downloads)

Start the whole stack

cd <this-folder>
docker compose up -d --build

First run downloads base images and builds the frontend (takes a few minutes). Subsequent runs: docker compose up -d.

Wait until all services are up:

docker compose ps

All should show Up (postgres also (healthy)).


Verify

What Where
Frontend UI http://localhost
API docs (Swagger) http://localhost:8000/docs
API health http://localhost:8000/health → {"status":"ok","service":"procurement-api"}
MinIO console http://localhost:9001 (user/pass minioadmin/minioadmin)
Database localhost:5433, db procurement, user/pass postgres/postgres (Casdoor uses the same instance, db casdoor)

Quick end-to-end check (Traefik → frontend proxy → API → DB):

curl http://localhost/api/v1/quotes?expand=supplier,project,line_items,termsheets,events,decision,receipt

MinIO (object storage for invoice / proforma files)

Item Value
S3 API endpoint localhost:9000 (from the API container: minio:9000)
Web console http://localhost:9001
Root user / password minioadmin / minioadmin (dev — change for production)
Bucket procurement-files (created automatically by the minio-init container)
Object key layout {quote_id}/{kind}/{attachment_id} where kind = invoice | proforma

File bytes live in MinIO; the quote_attachments table holds the metadata.

Ad-hoc bucket inspection from the host (see mc.cmd):

.\mc.cmd ls --recursive

Database credentials / connection

Host:     localhost
Port:     5433
Database: procurement
Username: postgres
Password: postgres
docker exec -it procurement-postgres psql -U postgres -d procurement

Restoring the committed seed data

The databases are not in git — Docker volumes hold the data. The repo ships recovery dumps in backups/علی محقق/:

File Contents
casdoor.dump Casdoor schema + console admin (admin / 123), org inventory, app inventory-app, the JWT signing cert, and the single org user ali.mohaghegh
procurement.dump Procurement schema + the single users row «علی محقق» with role admin; all other tables empty (categories/units re-seed at API startup)

⚠️ This replaces all data in the procurement and casdoor databases. On a running stack:

docker compose stop api casdoor
docker cp "backups/علی محقق/procurement.dump" procurement-postgres:/tmp/
docker cp "backups/علی محقق/casdoor.dump" procurement-postgres:/tmp/
docker exec procurement-postgres pg_restore -U postgres -d procurement --clean --if-exists /tmp/procurement.dump
docker exec procurement-postgres pg_restore -U postgres -d casdoor    --clean --if-exists /tmp/casdoor.dump
docker compose start casdoor api

Login afterwards: ali.mohaghegh / hulkfuck002 (Casdoor rejects spaces in usernames, so «ali mohaghegh» is not a valid login name).

Want demo data instead? Two seeders are still available (they create different users — see the Login table above):

  1. App seeder (realistic demo dataset; keeps the Casdoor org as-is):
    docker exec procurement-api sh -c "cd /srv/backend && python -m tools.seed"
    
  2. Casdoor seeder (creates org inventory + app + users razaei / mousavi / mohaghegh / tester — run once after a fresh volume):
    python backend/tools/seed_casdoor.py   # from the host, or from the api container
    

To create a fresh dump of your own current state: stop api + casdoor, docker exec procurement-postgres pg_dump -U postgres -d procurement -Fc (+ the casdoor DB) into backups/, then start them again.

Common operations

docker compose logs -f api          # see logs
docker compose down                 # stop (keep data)
docker compose up -d --build        # rebuild after code changes
docker compose down -v              # ⚠️ full reset — wipes DB + MinIO volumes

Known behavior

  • Authentication is Casdoor-based (JWT verified via JWKS): /auth, /users, /inventory and /pm are protected (401 without a token, 403 without a role). The procurement CRUD endpoints (quotes, suppliers, materials, …) are still open — dev placeholder, do not expose to the public internet.
  • Roles live in the app's users table (Casdoor is identity-only): pm, procurement_manager, admin. Role changes are admin-only (RBAC); the procurement manager can only assign projects to project managers. The admin has a dedicated panel at /admin (user sync from Casdoor + grant/revoke «مدیر تدارکات»).
  • Partial goods receipt («رسید جزیی») is supported in «کالاهای در راه»: at least one item with a valid quantity is enough to submit; other items stay in transit. The procurement ledger shows «رسیده X از Y» / «در راه Z» badges and a «رسید جزیی» badge on the quote.
  • One PostgreSQL instance serves both the app (procurement) and Casdoor (casdoor) — the separate casdoor-postgres container is gone (data was dumped/restored; see docs/casdoor-auth.md).
  • Traefik v3 is the public edge (ports 80/443); the frontend nginx only serves the SPA and proxies /api internally. Production (real domains, HTTPS via Let's Encrypt, HTTP→HTTPS redirect): docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build (see the prod file header for the required env vars).
  • Schema runs automatically ONLY on the first boot of an empty postgres_data volume (see docker-entrypoint-initdb.d); the API also runs CREATE TABLE IF NOT EXISTS for the units-dashboard tables and quote_attachments at startup.
  • Invoice upload requires an accepted/arrived quote; proforma has no restriction.
  • The supplier detail page (/suppliers/:id) shows the purchase history with invoice/proforma download links from MinIO.
  • If builds fail with disk-full errors: docker system prune -a, then rebuild.
  • If the frontend shows «اتصال به سرور برقرار نشد» → the api container is not reachable; check docker compose ps and docker compose logs api.
S
Description
No description provided
Readme
3.9 MiB
Languages
TypeScript 54.2%
Python 40.6%
PLpgSQL 2.5%
CSS 1.7%
PowerShell 0.6%
Other 0.2%