Rename Forte to LunarFront, generalize for any small business
Rebrand from Forte (music-store-specific) to LunarFront (any small business): - Package namespace @forte/* → @lunarfront/* - Database forte/forte_test → lunarfront/lunarfront_test - Docker containers, volumes, connection strings - UI branding, localStorage keys, test emails - All documentation and planning docs Generalize music-specific terminology: - instrumentDescription → itemDescription - instrumentCount → itemCount - instrumentType → itemCategory (on service templates) - New migration 0027_generalize_terminology for column renames - Seed data updated with generic examples - RBAC descriptions updated
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
# Forte — Environment Variables
|
||||
# LunarFront — Environment Variables
|
||||
# Copy to .env and adjust values for your setup.
|
||||
# Docker Compose overrides host values (postgres, valkey) automatically.
|
||||
|
||||
# Database (PostgreSQL 16)
|
||||
DATABASE_URL=postgresql://forte:forte@localhost:5432/forte
|
||||
DATABASE_URL=postgresql://lunarfront:lunarfront@localhost:5432/lunarfront
|
||||
|
||||
# Valkey / Redis
|
||||
REDIS_URL=redis://localhost:6379
|
||||
@@ -20,7 +20,7 @@ NODE_ENV=development
|
||||
|
||||
# Logging (optional)
|
||||
# LOG_LEVEL=info
|
||||
# LOG_FILE=./logs/forte.log
|
||||
# LOG_FILE=./logs/lunarfront.log
|
||||
|
||||
# File Storage (optional — defaults to local)
|
||||
# STORAGE_PROVIDER=local
|
||||
|
||||
18
CLAUDE.md
18
CLAUDE.md
@@ -1,8 +1,8 @@
|
||||
# Forte — Project Conventions
|
||||
# LunarFront — Project Conventions
|
||||
|
||||
## App
|
||||
- **Name:** Forte
|
||||
- **Purpose:** Music store management platform (POS, inventory, rentals, lessons, repairs, accounting)
|
||||
- **Name:** LunarFront
|
||||
- **Purpose:** Small business management platform (POS, inventory, rentals, scheduling, repairs, accounting)
|
||||
- **Company:** Lunarfront Tech LLC
|
||||
|
||||
## Tech Stack
|
||||
@@ -18,12 +18,12 @@
|
||||
- **Linting:** ESLint 9 flat config + Prettier
|
||||
|
||||
## Package Namespace
|
||||
- `@forte/shared` — types, Zod schemas, business logic, utils
|
||||
- `@forte/backend` — Fastify API server
|
||||
- `@lunarfront/shared` — types, Zod schemas, business logic, utils
|
||||
- `@lunarfront/backend` — Fastify API server
|
||||
|
||||
## Database
|
||||
- Dev: `forte` on localhost:5432
|
||||
- Test: `forte_test` on localhost:5432
|
||||
- Dev: `lunarfront` on localhost:5432
|
||||
- Test: `lunarfront_test` on localhost:5432
|
||||
- Multi-tenant: `company_id` (uuid FK) on all domain tables for tenant isolation
|
||||
- `location_id` (uuid FK) on tables that need per-location scoping (inventory, transactions, drawer)
|
||||
- Migrations via Drizzle Kit (`bunx drizzle-kit generate`, `bunx drizzle-kit migrate`)
|
||||
@@ -46,7 +46,7 @@
|
||||
- `?sort=name&order=asc` — sorting by field name, asc or desc
|
||||
- List responses always return `{ data: [...], pagination: { page, limit, total, totalPages } }`
|
||||
- Search and filtering is ALWAYS server-side, never client-side
|
||||
- Use `PaginationSchema` from `@forte/shared/schemas` to parse query params
|
||||
- Use `PaginationSchema` from `@lunarfront/shared/schemas` to parse query params
|
||||
- Use pagination helpers from `packages/backend/src/utils/pagination.ts`
|
||||
- **Lookup endpoints** (e.g., `/roles/all`, `/statuses/all`) are the exception — these return a flat unpaginated list for populating dropdowns/selects. Use a `/all` suffix to distinguish from the paginated list endpoint for the same resource.
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
## Conventions
|
||||
- Shared Zod schemas are the single source of truth for validation (used on both frontend and backend)
|
||||
- Business logic lives in `@forte/shared`, not in individual app packages
|
||||
- Business logic lives in `@lunarfront/shared`, not in individual app packages
|
||||
- API routes are thin — validate with Zod, call a service, return result
|
||||
- All financial events must be auditable (append-only audit records)
|
||||
- JSON structured logging with request IDs on every log line
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Forte
|
||||
# LunarFront
|
||||
|
||||
Music store management platform — POS, inventory, rentals, lessons, repairs, and accounting.
|
||||
Small business management platform — POS, inventory, rentals, scheduling, repairs, and accounting.
|
||||
|
||||
Built by [Lunarfront Tech LLC](https://lunarfront.com).
|
||||
|
||||
|
||||
104
bun.lock
104
bun.lock
@@ -3,7 +3,7 @@
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "forte",
|
||||
"name": "lunarfront",
|
||||
"dependencies": {
|
||||
"zod": "^4.3.6",
|
||||
},
|
||||
@@ -16,11 +16,11 @@
|
||||
},
|
||||
},
|
||||
"packages/admin": {
|
||||
"name": "@forte/admin",
|
||||
"name": "@lunarfront/admin",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@forte/shared": "workspace:*",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@lunarfront/shared": "workspace:*",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
@@ -55,14 +55,14 @@
|
||||
},
|
||||
},
|
||||
"packages/backend": {
|
||||
"name": "@forte/backend",
|
||||
"name": "@lunarfront/backend",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^10",
|
||||
"@fastify/jwt": "^9",
|
||||
"@fastify/multipart": "^9.4.0",
|
||||
"@fastify/rate-limit": "^10.3.0",
|
||||
"@forte/shared": "workspace:*",
|
||||
"@lunarfront/shared": "workspace:*",
|
||||
"bcrypt": "^6",
|
||||
"drizzle-orm": "^0.38",
|
||||
"fastify": "^5",
|
||||
@@ -80,7 +80,7 @@
|
||||
},
|
||||
},
|
||||
"packages/shared": {
|
||||
"name": "@forte/shared",
|
||||
"name": "@lunarfront/shared",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"zod": "^4",
|
||||
@@ -243,12 +243,6 @@
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
|
||||
|
||||
"@forte/admin": ["@forte/admin@workspace:packages/admin"],
|
||||
|
||||
"@forte/backend": ["@forte/backend@workspace:packages/backend"],
|
||||
|
||||
"@forte/shared": ["@forte/shared@workspace:packages/shared"],
|
||||
|
||||
"@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
@@ -273,6 +267,12 @@
|
||||
|
||||
"@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="],
|
||||
|
||||
"@lunarfront/admin": ["@lunarfront/admin@workspace:packages/admin"],
|
||||
|
||||
"@lunarfront/backend": ["@lunarfront/backend@workspace:packages/backend"],
|
||||
|
||||
"@lunarfront/shared": ["@lunarfront/shared@workspace:packages/shared"],
|
||||
|
||||
"@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
|
||||
|
||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||
@@ -399,55 +399,55 @@
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A=="],
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw=="],
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA=="],
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw=="],
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw=="],
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA=="],
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g=="],
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ=="],
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A=="],
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ=="],
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw=="],
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog=="],
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ=="],
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg=="],
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA=="],
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ=="],
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ=="],
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg=="],
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw=="],
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw=="],
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.0", "", { "os": "none", "cpu": "arm64" }, "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA=="],
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ=="],
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w=="],
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA=="],
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w=="],
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="],
|
||||
|
||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||
|
||||
@@ -487,15 +487,15 @@
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.95.2", "", { "dependencies": { "@tanstack/query-core": "5.95.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA=="],
|
||||
|
||||
"@tanstack/react-router": ["@tanstack/react-router@1.168.7", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.6", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-fW/HvQja4PQeu9lsoyh+pXpZ0UXezbpQkkJvCuH6tHAaW3jvPkjh14lfadrNNiY+pXT7WiMTB3afGhTCC78PDQ=="],
|
||||
"@tanstack/react-router": ["@tanstack/react-router@1.168.8", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.7", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-t0S0QueXubBKmI9eLPcN/A1sLQgTu8/yHerjrvvsGeD12zMdw0uJPKwEKpStQF2OThQtw64cs34uUSYXBUTSNw=="],
|
||||
|
||||
"@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="],
|
||||
|
||||
"@tanstack/router-core": ["@tanstack/router-core@1.168.6", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2" }, "bin": { "intent": "bin/intent.js" } }, "sha512-okCno3pImpLFQMJ/4zqEIGjIV5yhxLGj0JByrzQDQehORN1y1q6lJUezT0KPK5qCQiKUApeWaboLPjgBVx1kaQ=="],
|
||||
"@tanstack/router-core": ["@tanstack/router-core@1.168.7", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2" }, "bin": { "intent": "bin/intent.js" } }, "sha512-z4UEdlzMrFaKBsG4OIxlZEm+wsYBtEp//fnX6kW18jhQpETNcM6u2SXNdX+bcIYp6AaR7ERS3SBENzjC/xxwQQ=="],
|
||||
|
||||
"@tanstack/router-generator": ["@tanstack/router-generator@1.166.21", "", { "dependencies": { "@tanstack/router-core": "1.168.6", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-pJWsP6HaGrkIkfkcg6vzKyCBMbf1vV1BrQH+bFAVzXj3T/afmix3IPV2hiAj4zzjMxuddJD1on0Hn5+WDYA7zQ=="],
|
||||
"@tanstack/router-generator": ["@tanstack/router-generator@1.166.22", "", { "dependencies": { "@tanstack/router-core": "1.168.7", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-wQ7H8/Q2rmSPuaxWnurJ3DATNnqWV2tajxri9TSiW4QHsG7cWPD34+goeIinKG+GajJyEdfVpz6w/gRJXfbAPw=="],
|
||||
|
||||
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.167.8", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.6", "@tanstack/router-generator": "1.166.21", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.168.7", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"], "bin": { "intent": "bin/intent.js" } }, "sha512-/X4ACYsSX4bRmomj5X2TBU75cHuIVI99Fsax6DWnP6hPb4PaSjPUHVBfHhk2NemJzEOZu1L31UQ9QDlbHU4ZTQ=="],
|
||||
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.167.9", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.7", "@tanstack/router-generator": "1.166.22", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.168.8", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"], "bin": { "intent": "bin/intent.js" } }, "sha512-h/VV05FEHd4PVyc5Zy8B3trWLcdLt/Pmp+mfifmBKGRw+MUtvdQKbBHhmy4ouOf67s5zDJMc+n8R3xgU7bDwFA=="],
|
||||
|
||||
"@tanstack/router-utils": ["@tanstack/router-utils@1.161.6", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-nRcYw+w2OEgK6VfjirYvGyPLOK+tZQz1jkYcmH5AjMamQ9PycnlxZF2aEZtPpNoUsaceX2bHptn6Ub5hGXqNvw=="],
|
||||
|
||||
@@ -503,17 +503,17 @@
|
||||
|
||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.7", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ=="],
|
||||
|
||||
"@turbo/darwin-64": ["@turbo/darwin-64@2.8.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-FQ9EX1xMU5nbwjxXxM3yU88AQQ6Sqc6S44exPRroMcx9XZHqqppl5ymJF0Ig/z3nvQNwDmz1Gsnvxubo+nXWjQ=="],
|
||||
"@turbo/darwin-64": ["@turbo/darwin-64@2.8.21", "", { "os": "darwin", "cpu": "x64" }, "sha512-kfGoM0Iw8ZNZpbds+4IzOe0hjvHldqJwUPRAjXJi3KBxg/QOZL95N893SRoMtf2aJ+jJ3dk32yPkp8rvcIjP9g=="],
|
||||
|
||||
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gpyh9ATFGThD6/s9L95YWY54cizg/VRWl2B67h0yofG8BpHf67DFAh9nuJVKG7bY0+SBJDAo5cMur+wOl9YOYw=="],
|
||||
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.8.21", "", { "os": "darwin", "cpu": "arm64" }, "sha512-o9HEflxUEyr987x0cTUzZBhDOyL6u95JmdmlkH2VyxAw7zq2sdtM5e72y9ufv2N5SIoOBw1fVn9UES5VY5H6vQ=="],
|
||||
|
||||
"@turbo/linux-64": ["@turbo/linux-64@2.8.20", "", { "os": "linux", "cpu": "x64" }, "sha512-p2QxWUYyYUgUFG0b0kR+pPi8t7c9uaVlRtjTTI1AbCvVqkpjUfCcReBn6DgG/Hu8xrWdKLuyQFaLYFzQskZbcA=="],
|
||||
"@turbo/linux-64": ["@turbo/linux-64@2.8.21", "", { "os": "linux", "cpu": "x64" }, "sha512-uTxlCcXWy5h1fSSymP8XSJ+AudzEHMDV3IDfKX7+DGB8kgJ+SLoTUAH7z4OFA7I/l2sznz0upPdbNNZs91YMag=="],
|
||||
|
||||
"@turbo/linux-arm64": ["@turbo/linux-arm64@2.8.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gn5yjlZGLRZWarLWqdQzv0wMqyBNIdq1QLi48F1oY5Lo9kiohuf7BPQWtWxeNVS2NgJ1+nb/DzK1JduYC4AWOA=="],
|
||||
"@turbo/linux-arm64": ["@turbo/linux-arm64@2.8.21", "", { "os": "linux", "cpu": "arm64" }, "sha512-cdHIcxNcihHHkCHp0Y4Zb60K4Qz+CK4xw1gb6s/t/9o4SMeMj+hTBCtoW6QpPnl9xPYmxuTou8Zw6+cylTnREg=="],
|
||||
|
||||
"@turbo/windows-64": ["@turbo/windows-64@2.8.20", "", { "os": "win32", "cpu": "x64" }, "sha512-vyaDpYk/8T6Qz5V/X+ihKvKFEZFUoC0oxYpC1sZanK6gaESJlmV3cMRT3Qhcg4D2VxvtC2Jjs9IRkrZGL+exLw=="],
|
||||
"@turbo/windows-64": ["@turbo/windows-64@2.8.21", "", { "os": "win32", "cpu": "x64" }, "sha512-/iBj4OzbqEY8CX+eaeKbBTMZv2CLXNrt0692F7HnK7LcyYwyDecaAiSET6ZzL4opT7sbwkKvzAC/fhqT3Quu1A=="],
|
||||
|
||||
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-voicVULvUV5yaGXo0Iue13BcHGYW3u0VgqSbfQwBaHbpj1zLjYV4KIe+7fYIo6DO8FVUJzxFps3ODCQG/Wy2Qw=="],
|
||||
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.8.21", "", { "os": "win32", "cpu": "arm64" }, "sha512-95tMA/ZbIidJFUUtkmqioQ1gf3n3I1YbRP3ZgVdWTVn2qVbkodcIdGXBKRHHrIbRsLRl99SiHi/L7IxhpZDagQ=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
@@ -597,7 +597,7 @@
|
||||
|
||||
"base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ=="],
|
||||
|
||||
"bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="],
|
||||
|
||||
@@ -615,7 +615,7 @@
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001782", "", {}, "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw=="],
|
||||
|
||||
"canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="],
|
||||
|
||||
@@ -991,7 +991,7 @@
|
||||
|
||||
"rgbcolor": ["rgbcolor@1.0.1", "", {}, "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="],
|
||||
|
||||
"rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.0", "@rollup/rollup-android-arm64": "4.60.0", "@rollup/rollup-darwin-arm64": "4.60.0", "@rollup/rollup-darwin-x64": "4.60.0", "@rollup/rollup-freebsd-arm64": "4.60.0", "@rollup/rollup-freebsd-x64": "4.60.0", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", "@rollup/rollup-linux-arm-musleabihf": "4.60.0", "@rollup/rollup-linux-arm64-gnu": "4.60.0", "@rollup/rollup-linux-arm64-musl": "4.60.0", "@rollup/rollup-linux-loong64-gnu": "4.60.0", "@rollup/rollup-linux-loong64-musl": "4.60.0", "@rollup/rollup-linux-ppc64-gnu": "4.60.0", "@rollup/rollup-linux-ppc64-musl": "4.60.0", "@rollup/rollup-linux-riscv64-gnu": "4.60.0", "@rollup/rollup-linux-riscv64-musl": "4.60.0", "@rollup/rollup-linux-s390x-gnu": "4.60.0", "@rollup/rollup-linux-x64-gnu": "4.60.0", "@rollup/rollup-linux-x64-musl": "4.60.0", "@rollup/rollup-openbsd-x64": "4.60.0", "@rollup/rollup-openharmony-arm64": "4.60.0", "@rollup/rollup-win32-arm64-msvc": "4.60.0", "@rollup/rollup-win32-ia32-msvc": "4.60.0", "@rollup/rollup-win32-x64-gnu": "4.60.0", "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ=="],
|
||||
"rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
@@ -1067,7 +1067,7 @@
|
||||
|
||||
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
|
||||
|
||||
"turbo": ["turbo@2.8.20", "", { "optionalDependencies": { "@turbo/darwin-64": "2.8.20", "@turbo/darwin-arm64": "2.8.20", "@turbo/linux-64": "2.8.20", "@turbo/linux-arm64": "2.8.20", "@turbo/windows-64": "2.8.20", "@turbo/windows-arm64": "2.8.20" }, "bin": { "turbo": "bin/turbo" } }, "sha512-Rb4qk5YT8RUwwdXtkLpkVhNEe/lor6+WV7S5tTlLpxSz6MjV5Qi8jGNn4gS6NAvrYGA/rNrE6YUQM85sCZUDbQ=="],
|
||||
"turbo": ["turbo@2.8.21", "", { "optionalDependencies": { "@turbo/darwin-64": "2.8.21", "@turbo/darwin-arm64": "2.8.21", "@turbo/linux-64": "2.8.21", "@turbo/linux-arm64": "2.8.21", "@turbo/windows-64": "2.8.21", "@turbo/windows-arm64": "2.8.21" }, "bin": { "turbo": "bin/turbo" } }, "sha512-FlJ8OD5Qcp0jTAM7E4a/RhUzRNds2GzKlyxHKA6N247VLy628rrxAGlMpIXSz6VB430+TiQDJ/SMl6PL1lu6wQ=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
@@ -1153,7 +1153,7 @@
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
container_name: forte-api
|
||||
container_name: lunarfront-api
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8000:8000"
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
- /app/packages/backend/node_modules
|
||||
- /app/packages/shared/node_modules
|
||||
environment:
|
||||
DATABASE_URL: postgresql://forte:forte@postgres:5432/forte
|
||||
DATABASE_URL: postgresql://lunarfront:lunarfront@postgres:5432/lunarfront
|
||||
REDIS_URL: redis://valkey:6379
|
||||
JWT_SECRET: dev-secret-do-not-use-in-production
|
||||
NODE_ENV: development
|
||||
@@ -28,30 +28,30 @@ services:
|
||||
|
||||
postgres:
|
||||
image: postgres:16
|
||||
container_name: forte-postgres
|
||||
container_name: lunarfront-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: forte
|
||||
POSTGRES_PASSWORD: forte
|
||||
POSTGRES_DB: forte
|
||||
POSTGRES_USER: lunarfront
|
||||
POSTGRES_PASSWORD: lunarfront
|
||||
POSTGRES_DB: lunarfront
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- forte-pgdata:/var/lib/postgresql/data
|
||||
- lunarfront-pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U forte"]
|
||||
test: ["CMD-SHELL", "pg_isready -U lunarfront"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
valkey:
|
||||
image: valkey/valkey:8
|
||||
container_name: forte-valkey
|
||||
container_name: lunarfront-valkey
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- forte-valkey:/data
|
||||
- lunarfront-valkey:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "valkey-cli", "ping"]
|
||||
interval: 5s
|
||||
@@ -59,5 +59,5 @@ services:
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
forte-pgdata:
|
||||
forte-valkey:
|
||||
lunarfront-pgdata:
|
||||
lunarfront-valkey:
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
## Monorepo Structure
|
||||
|
||||
```
|
||||
forte/
|
||||
lunarfront/
|
||||
packages/
|
||||
shared/ @forte/shared — Zod schemas, types, business logic, utils
|
||||
backend/ @forte/backend — Fastify API server
|
||||
admin/ @forte/admin — Admin UI (React + Vite)
|
||||
shared/ @lunarfront/shared — Zod schemas, types, business logic, utils
|
||||
backend/ @lunarfront/backend — Fastify API server
|
||||
admin/ @lunarfront/admin — Admin UI (React + Vite)
|
||||
planning/ Domain planning docs (01-26)
|
||||
docs/ Technical documentation
|
||||
```
|
||||
|
||||
Managed with Turborepo and Bun workspaces. `@forte/shared` is a dependency of both `backend` and `admin`.
|
||||
Managed with Turborepo and Bun workspaces. `@lunarfront/shared` is a dependency of both `backend` and `admin`.
|
||||
|
||||
## Backend
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ PostgreSQL 16. Two databases:
|
||||
|
||||
| Database | Port | Usage |
|
||||
|----------|------|-------|
|
||||
| `forte` | 5432 | Development |
|
||||
| `forte_api_test` | 5432 | API integration tests (auto-created by test runner) |
|
||||
| `lunarfront` | 5432 | Development |
|
||||
| `lunarfront_api_test` | 5432 | API integration tests (auto-created by test runner) |
|
||||
|
||||
## Migrations
|
||||
|
||||
@@ -35,7 +35,7 @@ All domain tables include `company_id` (uuid FK to `company`). Every query filte
|
||||
|
||||
| Table | Description |
|
||||
|-------|-------------|
|
||||
| `company` | Tenant (music store business) |
|
||||
| `company` | Tenant (tenant business) |
|
||||
| `location` | Physical store location |
|
||||
| `user` | Staff/admin user account |
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
git clone <repo-url> && cd forte
|
||||
git clone <repo-url> && cd lunarfront
|
||||
bun install
|
||||
```
|
||||
|
||||
@@ -18,7 +18,7 @@ bun install
|
||||
Create a `.env` file in the project root:
|
||||
|
||||
```env
|
||||
DATABASE_URL=postgresql://forte:forte@localhost:5432/forte
|
||||
DATABASE_URL=postgresql://lunarfront:lunarfront@localhost:5432/lunarfront
|
||||
REDIS_URL=redis://localhost:6379
|
||||
JWT_SECRET=your-secret-here
|
||||
NODE_ENV=development
|
||||
@@ -28,7 +28,7 @@ NODE_ENV=development
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `DATABASE_URL` | `postgresql://forte:forte@localhost:5432/forte` | PostgreSQL connection string |
|
||||
| `DATABASE_URL` | `postgresql://lunarfront:lunarfront@localhost:5432/lunarfront` | PostgreSQL connection string |
|
||||
| `REDIS_URL` | `redis://localhost:6379` | Valkey/Redis connection string |
|
||||
| `JWT_SECRET` | (auto-generated in dev) | Secret for signing JWTs. **Required in production.** |
|
||||
| `PORT` | `8000` | Backend API port |
|
||||
@@ -45,7 +45,7 @@ NODE_ENV=development
|
||||
|
||||
```bash
|
||||
# Create the database
|
||||
createdb forte
|
||||
createdb lunarfront
|
||||
|
||||
# Run migrations
|
||||
cd packages/backend
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
The primary test suite lives at `packages/backend/api-tests/`. It uses a custom runner that:
|
||||
|
||||
1. Creates/migrates a `forte_api_test` database
|
||||
1. Creates/migrates a `lunarfront_api_test` database
|
||||
2. Seeds company, lookup tables, RBAC permissions/roles
|
||||
3. Starts the backend on port 8001
|
||||
4. Registers a test user with admin role
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "forte",
|
||||
"name": "lunarfront",
|
||||
"private": true,
|
||||
"packageManager": "bun@1.3.11",
|
||||
"workspaces": ["packages/*"],
|
||||
"scripts": {
|
||||
"dev": "turbo run dev",
|
||||
|
||||
@@ -1,473 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as LoginRouteImport } from './routes/login'
|
||||
import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
|
||||
import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index'
|
||||
import { Route as AuthenticatedUsersRouteImport } from './routes/_authenticated/users'
|
||||
import { Route as AuthenticatedProfileRouteImport } from './routes/_authenticated/profile'
|
||||
import { Route as AuthenticatedHelpRouteImport } from './routes/_authenticated/help'
|
||||
import { Route as AuthenticatedRolesIndexRouteImport } from './routes/_authenticated/roles/index'
|
||||
import { Route as AuthenticatedMembersIndexRouteImport } from './routes/_authenticated/members/index'
|
||||
import { Route as AuthenticatedAccountsIndexRouteImport } from './routes/_authenticated/accounts/index'
|
||||
import { Route as AuthenticatedRolesNewRouteImport } from './routes/_authenticated/roles/new'
|
||||
import { Route as AuthenticatedRolesRoleIdRouteImport } from './routes/_authenticated/roles/$roleId'
|
||||
import { Route as AuthenticatedMembersMemberIdRouteImport } from './routes/_authenticated/members/$memberId'
|
||||
import { Route as AuthenticatedAccountsNewRouteImport } from './routes/_authenticated/accounts/new'
|
||||
import { Route as AuthenticatedAccountsAccountIdRouteImport } from './routes/_authenticated/accounts/$accountId'
|
||||
import { Route as AuthenticatedAccountsAccountIdIndexRouteImport } from './routes/_authenticated/accounts/$accountId/index'
|
||||
import { Route as AuthenticatedAccountsAccountIdTaxExemptionsRouteImport } from './routes/_authenticated/accounts/$accountId/tax-exemptions'
|
||||
import { Route as AuthenticatedAccountsAccountIdProcessorLinksRouteImport } from './routes/_authenticated/accounts/$accountId/processor-links'
|
||||
import { Route as AuthenticatedAccountsAccountIdPaymentMethodsRouteImport } from './routes/_authenticated/accounts/$accountId/payment-methods'
|
||||
import { Route as AuthenticatedAccountsAccountIdMembersRouteImport } from './routes/_authenticated/accounts/$accountId/members'
|
||||
|
||||
const LoginRoute = LoginRouteImport.update({
|
||||
id: '/login',
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthenticatedRoute = AuthenticatedRouteImport.update({
|
||||
id: '/_authenticated',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthenticatedIndexRoute = AuthenticatedIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedUsersRoute = AuthenticatedUsersRouteImport.update({
|
||||
id: '/users',
|
||||
path: '/users',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedProfileRoute = AuthenticatedProfileRouteImport.update({
|
||||
id: '/profile',
|
||||
path: '/profile',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedHelpRoute = AuthenticatedHelpRouteImport.update({
|
||||
id: '/help',
|
||||
path: '/help',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedRolesIndexRoute = AuthenticatedRolesIndexRouteImport.update({
|
||||
id: '/roles/',
|
||||
path: '/roles/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedMembersIndexRoute =
|
||||
AuthenticatedMembersIndexRouteImport.update({
|
||||
id: '/members/',
|
||||
path: '/members/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsIndexRoute =
|
||||
AuthenticatedAccountsIndexRouteImport.update({
|
||||
id: '/accounts/',
|
||||
path: '/accounts/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedRolesNewRoute = AuthenticatedRolesNewRouteImport.update({
|
||||
id: '/roles/new',
|
||||
path: '/roles/new',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedRolesRoleIdRoute =
|
||||
AuthenticatedRolesRoleIdRouteImport.update({
|
||||
id: '/roles/$roleId',
|
||||
path: '/roles/$roleId',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedMembersMemberIdRoute =
|
||||
AuthenticatedMembersMemberIdRouteImport.update({
|
||||
id: '/members/$memberId',
|
||||
path: '/members/$memberId',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsNewRoute =
|
||||
AuthenticatedAccountsNewRouteImport.update({
|
||||
id: '/accounts/new',
|
||||
path: '/accounts/new',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdRoute =
|
||||
AuthenticatedAccountsAccountIdRouteImport.update({
|
||||
id: '/accounts/$accountId',
|
||||
path: '/accounts/$accountId',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdIndexRoute =
|
||||
AuthenticatedAccountsAccountIdIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdTaxExemptionsRoute =
|
||||
AuthenticatedAccountsAccountIdTaxExemptionsRouteImport.update({
|
||||
id: '/tax-exemptions',
|
||||
path: '/tax-exemptions',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdProcessorLinksRoute =
|
||||
AuthenticatedAccountsAccountIdProcessorLinksRouteImport.update({
|
||||
id: '/processor-links',
|
||||
path: '/processor-links',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdPaymentMethodsRoute =
|
||||
AuthenticatedAccountsAccountIdPaymentMethodsRouteImport.update({
|
||||
id: '/payment-methods',
|
||||
path: '/payment-methods',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
const AuthenticatedAccountsAccountIdMembersRoute =
|
||||
AuthenticatedAccountsAccountIdMembersRouteImport.update({
|
||||
id: '/members',
|
||||
path: '/members',
|
||||
getParentRoute: () => AuthenticatedAccountsAccountIdRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/login': typeof LoginRoute
|
||||
'/help': typeof AuthenticatedHelpRoute
|
||||
'/profile': typeof AuthenticatedProfileRoute
|
||||
'/users': typeof AuthenticatedUsersRoute
|
||||
'/accounts/$accountId': typeof AuthenticatedAccountsAccountIdRouteWithChildren
|
||||
'/accounts/new': typeof AuthenticatedAccountsNewRoute
|
||||
'/members/$memberId': typeof AuthenticatedMembersMemberIdRoute
|
||||
'/roles/$roleId': typeof AuthenticatedRolesRoleIdRoute
|
||||
'/roles/new': typeof AuthenticatedRolesNewRoute
|
||||
'/accounts/': typeof AuthenticatedAccountsIndexRoute
|
||||
'/members/': typeof AuthenticatedMembersIndexRoute
|
||||
'/roles/': typeof AuthenticatedRolesIndexRoute
|
||||
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
'/accounts/$accountId/tax-exemptions': typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
|
||||
'/accounts/$accountId/': typeof AuthenticatedAccountsAccountIdIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/login': typeof LoginRoute
|
||||
'/help': typeof AuthenticatedHelpRoute
|
||||
'/profile': typeof AuthenticatedProfileRoute
|
||||
'/users': typeof AuthenticatedUsersRoute
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/accounts/new': typeof AuthenticatedAccountsNewRoute
|
||||
'/members/$memberId': typeof AuthenticatedMembersMemberIdRoute
|
||||
'/roles/$roleId': typeof AuthenticatedRolesRoleIdRoute
|
||||
'/roles/new': typeof AuthenticatedRolesNewRoute
|
||||
'/accounts': typeof AuthenticatedAccountsIndexRoute
|
||||
'/members': typeof AuthenticatedMembersIndexRoute
|
||||
'/roles': typeof AuthenticatedRolesIndexRoute
|
||||
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
'/accounts/$accountId/tax-exemptions': typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
|
||||
'/accounts/$accountId': typeof AuthenticatedAccountsAccountIdIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/_authenticated': typeof AuthenticatedRouteWithChildren
|
||||
'/login': typeof LoginRoute
|
||||
'/_authenticated/help': typeof AuthenticatedHelpRoute
|
||||
'/_authenticated/profile': typeof AuthenticatedProfileRoute
|
||||
'/_authenticated/users': typeof AuthenticatedUsersRoute
|
||||
'/_authenticated/': typeof AuthenticatedIndexRoute
|
||||
'/_authenticated/accounts/$accountId': typeof AuthenticatedAccountsAccountIdRouteWithChildren
|
||||
'/_authenticated/accounts/new': typeof AuthenticatedAccountsNewRoute
|
||||
'/_authenticated/members/$memberId': typeof AuthenticatedMembersMemberIdRoute
|
||||
'/_authenticated/roles/$roleId': typeof AuthenticatedRolesRoleIdRoute
|
||||
'/_authenticated/roles/new': typeof AuthenticatedRolesNewRoute
|
||||
'/_authenticated/accounts/': typeof AuthenticatedAccountsIndexRoute
|
||||
'/_authenticated/members/': typeof AuthenticatedMembersIndexRoute
|
||||
'/_authenticated/roles/': typeof AuthenticatedRolesIndexRoute
|
||||
'/_authenticated/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/_authenticated/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/_authenticated/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
'/_authenticated/accounts/$accountId/tax-exemptions': typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
|
||||
'/_authenticated/accounts/$accountId/': typeof AuthenticatedAccountsAccountIdIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/login'
|
||||
| '/help'
|
||||
| '/profile'
|
||||
| '/users'
|
||||
| '/accounts/$accountId'
|
||||
| '/accounts/new'
|
||||
| '/members/$memberId'
|
||||
| '/roles/$roleId'
|
||||
| '/roles/new'
|
||||
| '/accounts/'
|
||||
| '/members/'
|
||||
| '/roles/'
|
||||
| '/accounts/$accountId/members'
|
||||
| '/accounts/$accountId/payment-methods'
|
||||
| '/accounts/$accountId/processor-links'
|
||||
| '/accounts/$accountId/tax-exemptions'
|
||||
| '/accounts/$accountId/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/login'
|
||||
| '/help'
|
||||
| '/profile'
|
||||
| '/users'
|
||||
| '/'
|
||||
| '/accounts/new'
|
||||
| '/members/$memberId'
|
||||
| '/roles/$roleId'
|
||||
| '/roles/new'
|
||||
| '/accounts'
|
||||
| '/members'
|
||||
| '/roles'
|
||||
| '/accounts/$accountId/members'
|
||||
| '/accounts/$accountId/payment-methods'
|
||||
| '/accounts/$accountId/processor-links'
|
||||
| '/accounts/$accountId/tax-exemptions'
|
||||
| '/accounts/$accountId'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/_authenticated'
|
||||
| '/login'
|
||||
| '/_authenticated/help'
|
||||
| '/_authenticated/profile'
|
||||
| '/_authenticated/users'
|
||||
| '/_authenticated/'
|
||||
| '/_authenticated/accounts/$accountId'
|
||||
| '/_authenticated/accounts/new'
|
||||
| '/_authenticated/members/$memberId'
|
||||
| '/_authenticated/roles/$roleId'
|
||||
| '/_authenticated/roles/new'
|
||||
| '/_authenticated/accounts/'
|
||||
| '/_authenticated/members/'
|
||||
| '/_authenticated/roles/'
|
||||
| '/_authenticated/accounts/$accountId/members'
|
||||
| '/_authenticated/accounts/$accountId/payment-methods'
|
||||
| '/_authenticated/accounts/$accountId/processor-links'
|
||||
| '/_authenticated/accounts/$accountId/tax-exemptions'
|
||||
| '/_authenticated/accounts/$accountId/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
|
||||
LoginRoute: typeof LoginRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/login': {
|
||||
id: '/login'
|
||||
path: '/login'
|
||||
fullPath: '/login'
|
||||
preLoaderRoute: typeof LoginRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_authenticated': {
|
||||
id: '/_authenticated'
|
||||
path: ''
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof AuthenticatedRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_authenticated/': {
|
||||
id: '/_authenticated/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof AuthenticatedIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/users': {
|
||||
id: '/_authenticated/users'
|
||||
path: '/users'
|
||||
fullPath: '/users'
|
||||
preLoaderRoute: typeof AuthenticatedUsersRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/profile': {
|
||||
id: '/_authenticated/profile'
|
||||
path: '/profile'
|
||||
fullPath: '/profile'
|
||||
preLoaderRoute: typeof AuthenticatedProfileRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/help': {
|
||||
id: '/_authenticated/help'
|
||||
path: '/help'
|
||||
fullPath: '/help'
|
||||
preLoaderRoute: typeof AuthenticatedHelpRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/roles/': {
|
||||
id: '/_authenticated/roles/'
|
||||
path: '/roles'
|
||||
fullPath: '/roles/'
|
||||
preLoaderRoute: typeof AuthenticatedRolesIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/members/': {
|
||||
id: '/_authenticated/members/'
|
||||
path: '/members'
|
||||
fullPath: '/members/'
|
||||
preLoaderRoute: typeof AuthenticatedMembersIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/accounts/': {
|
||||
id: '/_authenticated/accounts/'
|
||||
path: '/accounts'
|
||||
fullPath: '/accounts/'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/roles/new': {
|
||||
id: '/_authenticated/roles/new'
|
||||
path: '/roles/new'
|
||||
fullPath: '/roles/new'
|
||||
preLoaderRoute: typeof AuthenticatedRolesNewRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/roles/$roleId': {
|
||||
id: '/_authenticated/roles/$roleId'
|
||||
path: '/roles/$roleId'
|
||||
fullPath: '/roles/$roleId'
|
||||
preLoaderRoute: typeof AuthenticatedRolesRoleIdRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/members/$memberId': {
|
||||
id: '/_authenticated/members/$memberId'
|
||||
path: '/members/$memberId'
|
||||
fullPath: '/members/$memberId'
|
||||
preLoaderRoute: typeof AuthenticatedMembersMemberIdRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/accounts/new': {
|
||||
id: '/_authenticated/accounts/new'
|
||||
path: '/accounts/new'
|
||||
fullPath: '/accounts/new'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsNewRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId': {
|
||||
id: '/_authenticated/accounts/$accountId'
|
||||
path: '/accounts/$accountId'
|
||||
fullPath: '/accounts/$accountId'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/': {
|
||||
id: '/_authenticated/accounts/$accountId/'
|
||||
path: '/'
|
||||
fullPath: '/accounts/$accountId/'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/tax-exemptions': {
|
||||
id: '/_authenticated/accounts/$accountId/tax-exemptions'
|
||||
path: '/tax-exemptions'
|
||||
fullPath: '/accounts/$accountId/tax-exemptions'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdTaxExemptionsRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/processor-links': {
|
||||
id: '/_authenticated/accounts/$accountId/processor-links'
|
||||
path: '/processor-links'
|
||||
fullPath: '/accounts/$accountId/processor-links'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdProcessorLinksRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/payment-methods': {
|
||||
id: '/_authenticated/accounts/$accountId/payment-methods'
|
||||
path: '/payment-methods'
|
||||
fullPath: '/accounts/$accountId/payment-methods'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdPaymentMethodsRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
'/_authenticated/accounts/$accountId/members': {
|
||||
id: '/_authenticated/accounts/$accountId/members'
|
||||
path: '/members'
|
||||
fullPath: '/accounts/$accountId/members'
|
||||
preLoaderRoute: typeof AuthenticatedAccountsAccountIdMembersRouteImport
|
||||
parentRoute: typeof AuthenticatedAccountsAccountIdRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthenticatedAccountsAccountIdRouteChildren {
|
||||
AuthenticatedAccountsAccountIdMembersRoute: typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
AuthenticatedAccountsAccountIdPaymentMethodsRoute: typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
AuthenticatedAccountsAccountIdProcessorLinksRoute: typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
AuthenticatedAccountsAccountIdTaxExemptionsRoute: typeof AuthenticatedAccountsAccountIdTaxExemptionsRoute
|
||||
AuthenticatedAccountsAccountIdIndexRoute: typeof AuthenticatedAccountsAccountIdIndexRoute
|
||||
}
|
||||
|
||||
const AuthenticatedAccountsAccountIdRouteChildren: AuthenticatedAccountsAccountIdRouteChildren =
|
||||
{
|
||||
AuthenticatedAccountsAccountIdMembersRoute:
|
||||
AuthenticatedAccountsAccountIdMembersRoute,
|
||||
AuthenticatedAccountsAccountIdPaymentMethodsRoute:
|
||||
AuthenticatedAccountsAccountIdPaymentMethodsRoute,
|
||||
AuthenticatedAccountsAccountIdProcessorLinksRoute:
|
||||
AuthenticatedAccountsAccountIdProcessorLinksRoute,
|
||||
AuthenticatedAccountsAccountIdTaxExemptionsRoute:
|
||||
AuthenticatedAccountsAccountIdTaxExemptionsRoute,
|
||||
AuthenticatedAccountsAccountIdIndexRoute:
|
||||
AuthenticatedAccountsAccountIdIndexRoute,
|
||||
}
|
||||
|
||||
const AuthenticatedAccountsAccountIdRouteWithChildren =
|
||||
AuthenticatedAccountsAccountIdRoute._addFileChildren(
|
||||
AuthenticatedAccountsAccountIdRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticatedRouteChildren {
|
||||
AuthenticatedHelpRoute: typeof AuthenticatedHelpRoute
|
||||
AuthenticatedProfileRoute: typeof AuthenticatedProfileRoute
|
||||
AuthenticatedUsersRoute: typeof AuthenticatedUsersRoute
|
||||
AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute
|
||||
AuthenticatedAccountsAccountIdRoute: typeof AuthenticatedAccountsAccountIdRouteWithChildren
|
||||
AuthenticatedAccountsNewRoute: typeof AuthenticatedAccountsNewRoute
|
||||
AuthenticatedMembersMemberIdRoute: typeof AuthenticatedMembersMemberIdRoute
|
||||
AuthenticatedRolesRoleIdRoute: typeof AuthenticatedRolesRoleIdRoute
|
||||
AuthenticatedRolesNewRoute: typeof AuthenticatedRolesNewRoute
|
||||
AuthenticatedAccountsIndexRoute: typeof AuthenticatedAccountsIndexRoute
|
||||
AuthenticatedMembersIndexRoute: typeof AuthenticatedMembersIndexRoute
|
||||
AuthenticatedRolesIndexRoute: typeof AuthenticatedRolesIndexRoute
|
||||
}
|
||||
|
||||
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
||||
AuthenticatedHelpRoute: AuthenticatedHelpRoute,
|
||||
AuthenticatedProfileRoute: AuthenticatedProfileRoute,
|
||||
AuthenticatedUsersRoute: AuthenticatedUsersRoute,
|
||||
AuthenticatedIndexRoute: AuthenticatedIndexRoute,
|
||||
AuthenticatedAccountsAccountIdRoute:
|
||||
AuthenticatedAccountsAccountIdRouteWithChildren,
|
||||
AuthenticatedAccountsNewRoute: AuthenticatedAccountsNewRoute,
|
||||
AuthenticatedMembersMemberIdRoute: AuthenticatedMembersMemberIdRoute,
|
||||
AuthenticatedRolesRoleIdRoute: AuthenticatedRolesRoleIdRoute,
|
||||
AuthenticatedRolesNewRoute: AuthenticatedRolesNewRoute,
|
||||
AuthenticatedAccountsIndexRoute: AuthenticatedAccountsIndexRoute,
|
||||
AuthenticatedMembersIndexRoute: AuthenticatedMembersIndexRoute,
|
||||
AuthenticatedRolesIndexRoute: AuthenticatedRolesIndexRoute,
|
||||
}
|
||||
|
||||
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
|
||||
AuthenticatedRouteChildren,
|
||||
)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
AuthenticatedRoute: AuthenticatedRouteWithChildren,
|
||||
LoginRoute: LoginRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
@@ -3,13 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Forte Admin</title>
|
||||
<title>LunarFront Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
// Apply mode before React renders to prevent flash
|
||||
(function() {
|
||||
var mode = localStorage.getItem('forte-mode') || 'system';
|
||||
var mode = localStorage.getItem('lunarfront-mode') || 'system';
|
||||
var isDark = mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
if (isDark) document.documentElement.classList.add('dark');
|
||||
})();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@forte/admin",
|
||||
"name": "@lunarfront/admin",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -10,7 +10,7 @@
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@forte/shared": "workspace:*",
|
||||
"@lunarfront/shared": "workspace:*",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { Account } from '@/types/account'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
export const accountKeys = {
|
||||
all: ['accounts'] as const,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { MemberIdentifier } from '@/types/account'
|
||||
import type { PaginatedResponse } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse } from '@lunarfront/shared/schemas'
|
||||
|
||||
export const identifierKeys = {
|
||||
all: (memberId: string) => ['members', memberId, 'identifiers'] as const,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { Member } from '@/types/account'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
interface MemberWithAccount extends Member {
|
||||
accountName: string | null
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { PaymentMethod } from '@/types/account'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
export const paymentMethodKeys = {
|
||||
all: (accountId: string) => ['accounts', accountId, 'payment-methods'] as const,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { ProcessorLink } from '@/types/account'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
export const processorLinkKeys = {
|
||||
all: (accountId: string) => ['accounts', accountId, 'processor-links'] as const,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { Permission, Role } from '@/types/rbac'
|
||||
import type { PaginationInput, PaginatedResponse } from '@forte/shared/schemas'
|
||||
import type { PaginationInput, PaginatedResponse } from '@lunarfront/shared/schemas'
|
||||
|
||||
export const rbacKeys = {
|
||||
permissions: ['permissions'] as const,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { RepairTicket, RepairLineItem, RepairBatch, RepairNote, RepairServiceTemplate } from '@/types/repair'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
// --- Repair Tickets ---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { StorageFolder, StorageFolderPermission, StorageFile } from '@/types/storage'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
// --- Folders ---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { TaxExemption } from '@/types/account'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
export const taxExemptionKeys = {
|
||||
all: (accountId: string) => ['accounts', accountId, 'tax-exemptions'] as const,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { PaginationInput, PaginatedResponse } from '@forte/shared/schemas'
|
||||
import type { PaginationInput, PaginatedResponse } from '@lunarfront/shared/schemas'
|
||||
|
||||
export interface UserRole {
|
||||
id: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { VaultStatus, VaultCategory, VaultCategoryPermission, VaultEntry } from '@/types/vault'
|
||||
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
// --- Keys ---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
import { AccountCreateSchema } from '@forte/shared/schemas'
|
||||
import { AccountCreateSchema } from '@lunarfront/shared/schemas'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { PaymentMethodCreateSchema } from '@forte/shared/schemas'
|
||||
import type { PaymentMethodCreateInput } from '@forte/shared/schemas'
|
||||
import { PaymentMethodCreateSchema } from '@lunarfront/shared/schemas'
|
||||
import type { PaymentMethodCreateInput } from '@lunarfront/shared/schemas'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { ProcessorLinkCreateSchema } from '@forte/shared/schemas'
|
||||
import type { ProcessorLinkCreateInput } from '@forte/shared/schemas'
|
||||
import { ProcessorLinkCreateSchema } from '@lunarfront/shared/schemas'
|
||||
import type { ProcessorLinkCreateInput } from '@lunarfront/shared/schemas'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { TaxExemptionCreateSchema } from '@forte/shared/schemas'
|
||||
import type { TaxExemptionCreateInput } from '@forte/shared/schemas'
|
||||
import { TaxExemptionCreateSchema } from '@lunarfront/shared/schemas'
|
||||
import type { TaxExemptionCreateInput } from '@lunarfront/shared/schemas'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
@@ -24,7 +24,7 @@ interface GeneratePdfOptions {
|
||||
companyName?: string
|
||||
}
|
||||
|
||||
export function generateRepairPdf({ ticket, lineItems, notes, includeNotes, companyName = 'Forte Music' }: GeneratePdfOptions): jsPDF {
|
||||
export function generateRepairPdf({ ticket, lineItems, notes, includeNotes, companyName = 'LunarFront' }: GeneratePdfOptions): jsPDF {
|
||||
const doc = new jsPDF()
|
||||
let y = 20
|
||||
|
||||
@@ -57,11 +57,11 @@ export function generateRepairPdf({ ticket, lineItems, notes, includeNotes, comp
|
||||
doc.setFontSize(10)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text('Customer', 14, y)
|
||||
doc.text('Instrument', 110, y)
|
||||
doc.text('Item', 110, y)
|
||||
y += 5
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.text(ticket.customerName, 14, y)
|
||||
doc.text(ticket.instrumentDescription ?? '-', 110, y)
|
||||
doc.text(ticket.itemDescription ?? '-', 110, y)
|
||||
y += 5
|
||||
if (ticket.customerPhone) { doc.text(ticket.customerPhone, 14, y); y += 5 }
|
||||
if (ticket.serialNumber) { doc.text(`S/N: ${ticket.serialNumber}`, 110, y - 5) }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useNavigate, useSearch } from '@tanstack/react-router'
|
||||
import type { PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
interface PaginationSearch {
|
||||
page?: number
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Route as AuthenticatedUsersRouteImport } from './routes/_authenticated/
|
||||
import { Route as AuthenticatedSettingsRouteImport } from './routes/_authenticated/settings'
|
||||
import { Route as AuthenticatedProfileRouteImport } from './routes/_authenticated/profile'
|
||||
import { Route as AuthenticatedHelpRouteImport } from './routes/_authenticated/help'
|
||||
import { Route as AuthenticatedVaultIndexRouteImport } from './routes/_authenticated/vault/index'
|
||||
import { Route as AuthenticatedRolesIndexRouteImport } from './routes/_authenticated/roles/index'
|
||||
import { Route as AuthenticatedRepairsIndexRouteImport } from './routes/_authenticated/repairs/index'
|
||||
import { Route as AuthenticatedRepairBatchesIndexRouteImport } from './routes/_authenticated/repair-batches/index'
|
||||
@@ -72,6 +73,11 @@ const AuthenticatedHelpRoute = AuthenticatedHelpRouteImport.update({
|
||||
path: '/help',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedVaultIndexRoute = AuthenticatedVaultIndexRouteImport.update({
|
||||
id: '/vault/',
|
||||
path: '/vault/',
|
||||
getParentRoute: () => AuthenticatedRoute,
|
||||
} as any)
|
||||
const AuthenticatedRolesIndexRoute = AuthenticatedRolesIndexRouteImport.update({
|
||||
id: '/roles/',
|
||||
path: '/roles/',
|
||||
@@ -218,6 +224,7 @@ export interface FileRoutesByFullPath {
|
||||
'/repair-batches/': typeof AuthenticatedRepairBatchesIndexRoute
|
||||
'/repairs/': typeof AuthenticatedRepairsIndexRoute
|
||||
'/roles/': typeof AuthenticatedRolesIndexRoute
|
||||
'/vault/': typeof AuthenticatedVaultIndexRoute
|
||||
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
@@ -246,6 +253,7 @@ export interface FileRoutesByTo {
|
||||
'/repair-batches': typeof AuthenticatedRepairBatchesIndexRoute
|
||||
'/repairs': typeof AuthenticatedRepairsIndexRoute
|
||||
'/roles': typeof AuthenticatedRolesIndexRoute
|
||||
'/vault': typeof AuthenticatedVaultIndexRoute
|
||||
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
@@ -277,6 +285,7 @@ export interface FileRoutesById {
|
||||
'/_authenticated/repair-batches/': typeof AuthenticatedRepairBatchesIndexRoute
|
||||
'/_authenticated/repairs/': typeof AuthenticatedRepairsIndexRoute
|
||||
'/_authenticated/roles/': typeof AuthenticatedRolesIndexRoute
|
||||
'/_authenticated/vault/': typeof AuthenticatedVaultIndexRoute
|
||||
'/_authenticated/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
|
||||
'/_authenticated/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
|
||||
'/_authenticated/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
|
||||
@@ -308,6 +317,7 @@ export interface FileRouteTypes {
|
||||
| '/repair-batches/'
|
||||
| '/repairs/'
|
||||
| '/roles/'
|
||||
| '/vault/'
|
||||
| '/accounts/$accountId/members'
|
||||
| '/accounts/$accountId/payment-methods'
|
||||
| '/accounts/$accountId/processor-links'
|
||||
@@ -336,6 +346,7 @@ export interface FileRouteTypes {
|
||||
| '/repair-batches'
|
||||
| '/repairs'
|
||||
| '/roles'
|
||||
| '/vault'
|
||||
| '/accounts/$accountId/members'
|
||||
| '/accounts/$accountId/payment-methods'
|
||||
| '/accounts/$accountId/processor-links'
|
||||
@@ -366,6 +377,7 @@ export interface FileRouteTypes {
|
||||
| '/_authenticated/repair-batches/'
|
||||
| '/_authenticated/repairs/'
|
||||
| '/_authenticated/roles/'
|
||||
| '/_authenticated/vault/'
|
||||
| '/_authenticated/accounts/$accountId/members'
|
||||
| '/_authenticated/accounts/$accountId/payment-methods'
|
||||
| '/_authenticated/accounts/$accountId/processor-links'
|
||||
@@ -429,6 +441,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticatedHelpRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/vault/': {
|
||||
id: '/_authenticated/vault/'
|
||||
path: '/vault'
|
||||
fullPath: '/vault/'
|
||||
preLoaderRoute: typeof AuthenticatedVaultIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRoute
|
||||
}
|
||||
'/_authenticated/roles/': {
|
||||
id: '/_authenticated/roles/'
|
||||
path: '/roles'
|
||||
@@ -628,6 +647,7 @@ interface AuthenticatedRouteChildren {
|
||||
AuthenticatedRepairBatchesIndexRoute: typeof AuthenticatedRepairBatchesIndexRoute
|
||||
AuthenticatedRepairsIndexRoute: typeof AuthenticatedRepairsIndexRoute
|
||||
AuthenticatedRolesIndexRoute: typeof AuthenticatedRolesIndexRoute
|
||||
AuthenticatedVaultIndexRoute: typeof AuthenticatedVaultIndexRoute
|
||||
}
|
||||
|
||||
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
||||
@@ -654,6 +674,7 @@ const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
||||
AuthenticatedRepairBatchesIndexRoute: AuthenticatedRepairBatchesIndexRoute,
|
||||
AuthenticatedRepairsIndexRoute: AuthenticatedRepairsIndexRoute,
|
||||
AuthenticatedRolesIndexRoute: AuthenticatedRolesIndexRoute,
|
||||
AuthenticatedVaultIndexRoute: AuthenticatedVaultIndexRoute,
|
||||
}
|
||||
|
||||
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
|
||||
|
||||
@@ -34,7 +34,7 @@ const STATUS_LABELS: Record<string, string> = {
|
||||
|
||||
const ticketColumns: Column<RepairTicket>[] = [
|
||||
{ key: 'ticket_number', header: 'Ticket #', sortable: true, render: (t) => <span className="font-mono text-sm">{t.ticketNumber}</span> },
|
||||
{ key: 'instrument', header: 'Instrument', render: (t) => <>{t.instrumentDescription ?? '-'}</> },
|
||||
{ key: 'item_description', header: 'Item', render: (t) => <>{t.itemDescription ?? '-'}</> },
|
||||
{ key: 'problem', header: 'Problem', render: (t) => <span className="truncate max-w-[200px] block">{t.problemDescription}</span> },
|
||||
{ key: 'status', header: 'Status', sortable: true, render: (t) => <Badge variant="outline">{STATUS_LABELS[t.status] ?? t.status}</Badge> },
|
||||
{
|
||||
@@ -111,7 +111,7 @@ function RepairBatchDetailPage() {
|
||||
|
||||
doc.setFontSize(18)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text('Forte Music', 14, y)
|
||||
doc.text('LunarFront', 14, y)
|
||||
y += 8
|
||||
doc.setFontSize(12)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
@@ -169,7 +169,7 @@ function RepairBatchDetailPage() {
|
||||
doc.setFillColor(245, 245, 245)
|
||||
doc.rect(14, y - 3, 182, 6, 'F')
|
||||
doc.text('Ticket #', 16, y)
|
||||
doc.text('Instrument', 40, y)
|
||||
doc.text('Item', 40, y)
|
||||
doc.text('Problem', 100, y)
|
||||
doc.text('Status', 155, y)
|
||||
doc.text('Estimate', 190, y, { align: 'right' })
|
||||
@@ -179,7 +179,7 @@ function RepairBatchDetailPage() {
|
||||
for (const ticket of tickets) {
|
||||
if (y > 270) { doc.addPage(); y = 20 }
|
||||
doc.text(ticket.ticketNumber ?? '-', 16, y)
|
||||
doc.text((ticket.instrumentDescription ?? '-').slice(0, 30), 40, y)
|
||||
doc.text((ticket.itemDescription ?? '-').slice(0, 30), 40, y)
|
||||
doc.text(ticket.problemDescription.slice(0, 28), 100, y)
|
||||
doc.text(STATUS_LABELS[ticket.status] ?? ticket.status, 155, y)
|
||||
doc.text(ticket.estimatedCost ? `$${ticket.estimatedCost}` : '-', 190, y, { align: 'right' })
|
||||
|
||||
@@ -49,9 +49,9 @@ const columns: Column<RepairBatch>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'instruments',
|
||||
header: 'Instruments',
|
||||
render: (b) => <>{b.receivedCount}/{b.instrumentCount}</>,
|
||||
key: 'items',
|
||||
header: 'Items',
|
||||
render: (b) => <>{b.receivedCount}/{b.itemCount}</>,
|
||||
},
|
||||
{
|
||||
key: 'due_date',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute, useNavigate, Link } from '@tanstack/react-router'
|
||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { RepairBatchCreateSchema } from '@forte/shared/schemas'
|
||||
import { RepairBatchCreateSchema } from '@lunarfront/shared/schemas'
|
||||
import { repairBatchMutations } from '@/api/repairs'
|
||||
import { accountListOptions } from '@/api/accounts'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -167,7 +167,7 @@ function NewRepairBatchPage() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Notes</Label>
|
||||
<Textarea {...register('notes')} rows={3} placeholder="e.g. Annual instrument checkup, multiple guitars needing setups" />
|
||||
<Textarea {...register('notes')} rows={3} placeholder="e.g. Annual checkup, multiple items needing service" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -118,7 +118,7 @@ function RepairTicketDetailPage() {
|
||||
setEditFields({
|
||||
customerName: ticket!.customerName,
|
||||
customerPhone: ticket!.customerPhone ?? '',
|
||||
instrumentDescription: ticket!.instrumentDescription ?? '',
|
||||
itemDescription: ticket!.itemDescription ?? '',
|
||||
serialNumber: ticket!.serialNumber ?? '',
|
||||
conditionIn: ticket!.conditionIn ?? '',
|
||||
conditionInNotes: ticket!.conditionInNotes ?? '',
|
||||
@@ -134,7 +134,7 @@ function RepairTicketDetailPage() {
|
||||
const data: Record<string, unknown> = {}
|
||||
if (editFields.customerName !== ticket!.customerName) data.customerName = editFields.customerName
|
||||
if (editFields.customerPhone !== (ticket!.customerPhone ?? '')) data.customerPhone = editFields.customerPhone || undefined
|
||||
if (editFields.instrumentDescription !== (ticket!.instrumentDescription ?? '')) data.instrumentDescription = editFields.instrumentDescription || undefined
|
||||
if (editFields.itemDescription !== (ticket!.itemDescription ?? '')) data.itemDescription = editFields.itemDescription || undefined
|
||||
if (editFields.serialNumber !== (ticket!.serialNumber ?? '')) data.serialNumber = editFields.serialNumber || undefined
|
||||
if (editFields.conditionIn !== (ticket!.conditionIn ?? '')) data.conditionIn = editFields.conditionIn || undefined
|
||||
if (editFields.conditionInNotes !== (ticket!.conditionInNotes ?? '')) data.conditionInNotes = editFields.conditionInNotes || undefined
|
||||
@@ -180,7 +180,7 @@ function RepairTicketDetailPage() {
|
||||
</Button>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl font-bold">Ticket #{ticket.ticketNumber}</h1>
|
||||
<p className="text-sm text-muted-foreground">{ticket.customerName} — {ticket.instrumentDescription ?? 'No instrument'}</p>
|
||||
<p className="text-sm text-muted-foreground">{ticket.customerName} — {ticket.itemDescription ?? 'No item description'}</p>
|
||||
</div>
|
||||
<PdfModal ticket={ticket} lineItems={lineItemsData?.data ?? []} ticketId={ticketId} />
|
||||
</div>
|
||||
@@ -278,7 +278,7 @@ function RepairTicketDetailPage() {
|
||||
<div className="space-y-2"><Label>Phone</Label><Input value={editFields.customerPhone} onChange={(e) => setEditFields((p) => ({ ...p, customerPhone: e.target.value }))} /></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2"><Label>Instrument</Label><Input value={editFields.instrumentDescription} onChange={(e) => setEditFields((p) => ({ ...p, instrumentDescription: e.target.value }))} /></div>
|
||||
<div className="space-y-2"><Label>Item Description</Label><Input value={editFields.itemDescription} onChange={(e) => setEditFields((p) => ({ ...p, itemDescription: e.target.value }))} /></div>
|
||||
<div className="space-y-2"><Label>Serial Number</Label><Input value={editFields.serialNumber} onChange={(e) => setEditFields((p) => ({ ...p, serialNumber: e.target.value }))} /></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
@@ -310,7 +310,7 @@ function RepairTicketDetailPage() {
|
||||
<div><span className="text-muted-foreground">Account:</span> {ticket.accountId ?? 'Walk-in'}</div>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div><span className="text-muted-foreground">Instrument:</span> {ticket.instrumentDescription ?? '-'}</div>
|
||||
<div><span className="text-muted-foreground">Item:</span> {ticket.itemDescription ?? '-'}</div>
|
||||
<div><span className="text-muted-foreground">Serial:</span> {ticket.serialNumber ?? '-'}</div>
|
||||
<div><span className="text-muted-foreground">Condition:</span> {ticket.conditionIn ?? '-'}</div>
|
||||
</div>
|
||||
@@ -411,8 +411,8 @@ function AddLineItemDialog({ ticketId, open, onOpenChange }: { ticketId: string;
|
||||
setDescription(''); setQty('1'); setUnitPrice('0'); setCost(''); setItemType('labor'); setTemplateSearch(''); setShowTemplates(false)
|
||||
}
|
||||
|
||||
function selectTemplate(template: { name: string; instrumentType: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) {
|
||||
const desc = [template.name, template.instrumentType, template.size].filter(Boolean).join(' — ')
|
||||
function selectTemplate(template: { name: string; itemCategory: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) {
|
||||
const desc = [template.name, template.itemCategory, template.size].filter(Boolean).join(' — ')
|
||||
setDescription(desc); setItemType(template.itemType); setUnitPrice(template.defaultPrice); setCost(template.defaultCost ?? ''); setShowTemplates(false); setTemplateSearch('')
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ function AddLineItemDialog({ ticketId, open, onOpenChange }: { ticketId: string;
|
||||
<div className="absolute z-50 mt-1 w-full rounded-md border bg-popover shadow-lg max-h-48 overflow-auto">
|
||||
{templates.length === 0 ? <div className="p-3 text-sm text-muted-foreground">No templates found</div> : templates.map((t) => (
|
||||
<button key={t.id} type="button" className="w-full text-left px-3 py-2 text-sm hover:bg-accent flex justify-between" onClick={() => selectTemplate(t)}>
|
||||
<span>{t.name}{t.instrumentType ? ` — ${t.instrumentType}` : ''}{t.size ? ` ${t.size}` : ''}</span>
|
||||
<span>{t.name}{t.itemCategory ? ` — ${t.itemCategory}` : ''}{t.size ? ` ${t.size}` : ''}</span>
|
||||
<span className="text-muted-foreground">${t.defaultPrice}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Plus, Search } from 'lucide-react'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
import type { RepairTicket } from '@/types/repair'
|
||||
import type { PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/repairs/')({
|
||||
validateSearch: (search: Record<string, unknown>) => ({
|
||||
@@ -70,9 +70,9 @@ const columns: Column<RepairTicket>[] = [
|
||||
render: (t) => <span className="font-medium">{t.customerName}</span>,
|
||||
},
|
||||
{
|
||||
key: 'instrument',
|
||||
header: 'Instrument',
|
||||
render: (t) => <>{t.instrumentDescription ?? '-'}</>,
|
||||
key: 'item_description',
|
||||
header: 'Item',
|
||||
render: (t) => <>{t.itemDescription ?? '-'}</>,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute, useNavigate, Link } from '@tanstack/react-router'
|
||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { RepairTicketCreateSchema } from '@forte/shared/schemas'
|
||||
import { RepairTicketCreateSchema } from '@lunarfront/shared/schemas'
|
||||
import { repairTicketMutations, repairLineItemMutations, repairServiceTemplateListOptions } from '@/api/repairs'
|
||||
import { accountListOptions } from '@/api/accounts'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
@@ -88,7 +88,7 @@ function NewRepairPage() {
|
||||
defaultValues: {
|
||||
customerName: linkedContactName ?? '',
|
||||
customerPhone: '',
|
||||
instrumentDescription: '',
|
||||
itemDescription: '',
|
||||
serialNumber: '',
|
||||
problemDescription: '',
|
||||
conditionIn: undefined,
|
||||
@@ -157,8 +157,8 @@ function NewRepairPage() {
|
||||
setValue('customerPhone', '')
|
||||
}
|
||||
|
||||
function addFromTemplate(template: { name: string; instrumentType: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) {
|
||||
const desc = [template.name, template.instrumentType, template.size].filter(Boolean).join(' — ')
|
||||
function addFromTemplate(template: { name: string; itemCategory: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) {
|
||||
const desc = [template.name, template.itemCategory, template.size].filter(Boolean).join(' — ')
|
||||
setLineItems((prev) => [...prev, {
|
||||
itemType: template.itemType,
|
||||
description: desc,
|
||||
@@ -297,14 +297,14 @@ function NewRepairPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Instrument Section */}
|
||||
{/* Item Section */}
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-lg">Instrument</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle className="text-lg">Item</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Instrument Description</Label>
|
||||
<Input {...register('instrumentDescription')} placeholder="e.g. Yamaha Trumpet YTR-2330" />
|
||||
<Label>Item Description</Label>
|
||||
<Input {...register('itemDescription')} placeholder="e.g. Brand, model, description" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Serial Number</Label>
|
||||
@@ -364,7 +364,7 @@ function NewRepairPage() {
|
||||
) : (
|
||||
templates.map((t) => (
|
||||
<button key={t.id} type="button" className="w-full text-left px-3 py-2 text-sm hover:bg-accent flex justify-between" onClick={() => addFromTemplate(t)}>
|
||||
<span>{t.name}{t.instrumentType ? ` — ${t.instrumentType}` : ''}{t.size ? ` ${t.size}` : ''}</span>
|
||||
<span>{t.name}{t.itemCategory ? ` — ${t.itemCategory}` : ''}{t.size ? ` ${t.size}` : ''}</span>
|
||||
<span className="text-muted-foreground">${t.defaultPrice}</span>
|
||||
</button>
|
||||
))
|
||||
@@ -462,7 +462,7 @@ function NewRepairPage() {
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-lg">Intake Photos</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">Optional — document instrument condition at intake</p>
|
||||
<p className="text-sm text-muted-foreground">Optional — document item condition at intake</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{photos.map((photo, i) => (
|
||||
<div key={i} className="relative group">
|
||||
|
||||
@@ -28,7 +28,7 @@ export const Route = createFileRoute('/_authenticated/repairs/templates')({
|
||||
|
||||
const columns: Column<RepairServiceTemplate>[] = [
|
||||
{ key: 'name', header: 'Name', sortable: true, render: (t) => <span className="font-medium">{t.name}</span> },
|
||||
{ key: 'instrument_type', header: 'Instrument', sortable: true, render: (t) => <>{t.instrumentType ?? '-'}</> },
|
||||
{ key: 'item_category', header: 'Item Category', sortable: true, render: (t) => <>{t.itemCategory ?? '-'}</> },
|
||||
{ key: 'size', header: 'Size', render: (t) => <>{t.size ?? '-'}</> },
|
||||
{ key: 'item_type', header: 'Type', render: (t) => <Badge variant="outline">{t.itemType.replace('_', ' ')}</Badge> },
|
||||
{ key: 'default_price', header: 'Price', sortable: true, render: (t) => <>${t.defaultPrice}</> },
|
||||
@@ -110,7 +110,7 @@ function RepairTemplatesPage() {
|
||||
function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
|
||||
const queryClient = useQueryClient()
|
||||
const [name, setName] = useState('')
|
||||
const [instrumentType, setInstrumentType] = useState('')
|
||||
const [itemCategory, setItemCategory] = useState('')
|
||||
const [size, setSize] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [itemType, setItemType] = useState('flat_rate')
|
||||
@@ -131,7 +131,7 @@ function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenCha
|
||||
|
||||
function resetForm() {
|
||||
setName('')
|
||||
setInstrumentType('')
|
||||
setItemCategory('')
|
||||
setSize('')
|
||||
setDescription('')
|
||||
setItemType('flat_rate')
|
||||
@@ -144,7 +144,7 @@ function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenCha
|
||||
e.preventDefault()
|
||||
mutation.mutate({
|
||||
name,
|
||||
instrumentType: instrumentType || undefined,
|
||||
itemCategory: itemCategory || undefined,
|
||||
size: size || undefined,
|
||||
description: description || undefined,
|
||||
itemType,
|
||||
@@ -168,8 +168,8 @@ function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenCha
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Instrument Type</Label>
|
||||
<Input value={instrumentType} onChange={(e) => setInstrumentType(e.target.value)} placeholder="e.g. Violin, Trumpet, Guitar" />
|
||||
<Label>Item Category</Label>
|
||||
<Input value={itemCategory} onChange={(e) => setItemCategory(e.target.value)} placeholder="e.g. Electronics, Appliances, Furniture" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Size</Label>
|
||||
|
||||
@@ -48,8 +48,8 @@ function LoginPage() {
|
||||
style={{ backgroundColor: '#131c2e', borderColor: '#1e2d45' }}
|
||||
>
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold" style={{ color: '#d8dfe9' }}>Forte</h1>
|
||||
<p className="text-sm mt-1" style={{ color: '#6b7a8d' }}>Music Store Management</p>
|
||||
<h1 className="text-3xl font-bold" style={{ color: '#d8dfe9' }}>LunarFront</h1>
|
||||
<p className="text-sm mt-1" style={{ color: '#6b7a8d' }}>Small Business Management</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -50,7 +50,7 @@ function expandPermissions(slugs: string[]): Set<string> {
|
||||
|
||||
function loadSession(): { token: string; user: User; permissions?: string[] } | null {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('forte-auth')
|
||||
const raw = sessionStorage.getItem('lunarfront-auth')
|
||||
if (!raw) return null
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
@@ -59,11 +59,11 @@ function loadSession(): { token: string; user: User; permissions?: string[] } |
|
||||
}
|
||||
|
||||
function saveSession(token: string, user: User, permissions?: string[]) {
|
||||
sessionStorage.setItem('forte-auth', JSON.stringify({ token, user, permissions }))
|
||||
sessionStorage.setItem('lunarfront-auth', JSON.stringify({ token, user, permissions }))
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
sessionStorage.removeItem('forte-auth')
|
||||
sessionStorage.removeItem('lunarfront-auth')
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set, get) => {
|
||||
|
||||
@@ -22,8 +22,8 @@ function apply(mode: Mode, colorTheme: string) {
|
||||
}
|
||||
|
||||
export const useThemeStore = create<ThemeState>((set) => {
|
||||
const initialMode = (typeof window !== 'undefined' ? localStorage.getItem('forte-mode') as Mode : null) ?? 'system'
|
||||
const initialColor = (typeof window !== 'undefined' ? localStorage.getItem('forte-color-theme') : null) ?? 'slate'
|
||||
const initialMode = (typeof window !== 'undefined' ? localStorage.getItem('lunarfront-mode') as Mode : null) ?? 'system'
|
||||
const initialColor = (typeof window !== 'undefined' ? localStorage.getItem('lunarfront-color-theme') : null) ?? 'slate'
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
apply(initialMode, initialColor)
|
||||
@@ -34,14 +34,14 @@ export const useThemeStore = create<ThemeState>((set) => {
|
||||
colorTheme: initialColor,
|
||||
|
||||
setMode: (mode) => {
|
||||
localStorage.setItem('forte-mode', mode)
|
||||
localStorage.setItem('lunarfront-mode', mode)
|
||||
const colorTheme = useThemeStore.getState().colorTheme
|
||||
apply(mode, colorTheme)
|
||||
set({ mode })
|
||||
},
|
||||
|
||||
setColorTheme: (name) => {
|
||||
localStorage.setItem('forte-color-theme', name)
|
||||
localStorage.setItem('lunarfront-color-theme', name)
|
||||
const mode = useThemeStore.getState().mode
|
||||
apply(mode, name)
|
||||
set({ colorTheme: name })
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface RepairTicket {
|
||||
customerName: string
|
||||
customerPhone: string | null
|
||||
inventoryUnitId: string | null
|
||||
instrumentDescription: string | null
|
||||
itemDescription: string | null
|
||||
serialNumber: string | null
|
||||
conditionIn: 'excellent' | 'good' | 'fair' | 'poor' | null
|
||||
conditionInNotes: string | null
|
||||
@@ -55,7 +55,7 @@ export interface RepairBatch {
|
||||
dueDate: string | null
|
||||
completedDate: string | null
|
||||
deliveredDate: string | null
|
||||
instrumentCount: number
|
||||
itemCount: number
|
||||
receivedCount: number
|
||||
estimatedTotal: string | null
|
||||
actualTotal: string | null
|
||||
@@ -79,7 +79,7 @@ export interface RepairNote {
|
||||
export interface RepairServiceTemplate {
|
||||
id: string
|
||||
name: string
|
||||
instrumentType: string | null
|
||||
itemCategory: string | null
|
||||
size: string | null
|
||||
description: string | null
|
||||
itemType: 'labor' | 'part' | 'flat_rate' | 'misc'
|
||||
|
||||
@@ -16,17 +16,17 @@ const pages: WikiPage[] = [
|
||||
title: 'Getting Started',
|
||||
category: 'General',
|
||||
content: `
|
||||
# Getting Started with Forte
|
||||
# Getting Started with LunarFront
|
||||
|
||||
Welcome to Forte — your music store management platform.
|
||||
Welcome to LunarFront — your small business management platform.
|
||||
|
||||
## Signing In
|
||||
|
||||
1. Open Forte in your browser
|
||||
1. Open LunarFront in your browser
|
||||
2. Enter your email and password
|
||||
3. Click **Sign in**
|
||||
|
||||
If you don't have an account, ask your store manager to create one for you.
|
||||
If you don't have an account, ask your admin to create one for you.
|
||||
|
||||
## Navigation
|
||||
|
||||
@@ -34,13 +34,13 @@ Use the sidebar on the left to navigate between sections:
|
||||
|
||||
- **Accounts** — manage customer accounts and their members
|
||||
- **Members** — find and manage individual people across all accounts
|
||||
- **Repairs** — track instrument repair tickets
|
||||
- **Repairs** — track repair tickets
|
||||
- **Repair Batches** — manage bulk school repair jobs
|
||||
- **Help** — you're here!
|
||||
|
||||
## Need Help?
|
||||
|
||||
If you can't find what you're looking for, contact your store manager or system administrator.
|
||||
If you can't find what you're looking for, contact your admin or system administrator.
|
||||
`.trim(),
|
||||
},
|
||||
{
|
||||
@@ -89,7 +89,7 @@ Every account gets a unique 6-digit number automatically. This number appears in
|
||||
content: `
|
||||
# Members
|
||||
|
||||
A **member** is an individual person associated with an account. This could be a parent, a child, a student, a band director — anyone who takes lessons, rents instruments, or needs to be tracked.
|
||||
A **member** is an individual person associated with an account. This could be a parent, a child, a student, a staff member — anyone who takes lessons, uses services, or needs to be tracked.
|
||||
|
||||
## Adding a Member
|
||||
|
||||
@@ -136,7 +136,7 @@ Payment methods are cards on file for an account. These are used for recurring b
|
||||
5. Optionally enter card brand, last four digits, and expiration
|
||||
6. Click **Add Payment Method**
|
||||
|
||||
**Note:** Card details are stored securely with the payment processor — Forte only keeps a reference and display info (last 4 digits, brand).
|
||||
**Note:** Card details are stored securely with the payment processor — LunarFront only keeps a reference and display info (last 4 digits, brand).
|
||||
|
||||
## Default Payment Method
|
||||
|
||||
@@ -154,7 +154,7 @@ If a payment method was migrated from an old system, it may show a "Needs Update
|
||||
content: `
|
||||
# Tax Exemptions
|
||||
|
||||
Schools, churches, and resellers may be exempt from sales tax. Forte tracks tax exemption certificates per account.
|
||||
Schools, churches, and resellers may be exempt from sales tax. LunarFront tracks tax exemption certificates per account.
|
||||
|
||||
## Adding a Tax Exemption
|
||||
|
||||
@@ -191,7 +191,7 @@ All approvals and revocations are logged with who did it and when.
|
||||
content: `
|
||||
# Identity Documents
|
||||
|
||||
You can store identity documents (driver's license, passport, school ID) for any member. This is useful for verifying identity during instrument pickups, rentals, or trade-ins.
|
||||
You can store identity documents (driver's license, passport, school ID) for any member. This is useful for verifying identity during pickups, rentals, or trade-ins.
|
||||
|
||||
## Adding an ID
|
||||
|
||||
@@ -225,7 +225,7 @@ If a member has multiple IDs, mark one as **Primary** — this is the one shown
|
||||
content: `
|
||||
# Users & Roles
|
||||
|
||||
Forte uses a permission-based access control system. **Permissions** are specific actions (like "view accounts" or "edit inventory"). **Roles** are named groups of permissions that you assign to users.
|
||||
LunarFront uses a permission-based access control system. **Permissions** are specific actions (like "view accounts" or "edit inventory"). **Roles** are named groups of permissions that you assign to users.
|
||||
|
||||
## Managing Users
|
||||
|
||||
@@ -306,16 +306,16 @@ Your preferences are saved in your browser and persist across sessions.
|
||||
content: `
|
||||
# Repairs
|
||||
|
||||
The Repairs module tracks instrument repair tickets from intake through completion. It supports walk-in customers, account-linked repairs, and bulk school batch jobs.
|
||||
The Repairs module tracks repair tickets from intake through completion. It supports walk-in customers, account-linked repairs, and bulk school batch jobs.
|
||||
|
||||
## Creating a Repair Ticket
|
||||
|
||||
1. Go to **Repairs** in the sidebar
|
||||
2. Click **New Repair**
|
||||
3. Search for an existing account or enter customer details manually for walk-ins
|
||||
4. Describe the instrument and the problem
|
||||
4. Describe the item and the problem
|
||||
5. Optionally add line items for the estimate (use templates for common services)
|
||||
6. Add intake photos to document the instrument's condition
|
||||
6. Add intake photos to document the item's condition
|
||||
7. Click **Create Ticket**
|
||||
|
||||
## Ticket Status Flow
|
||||
@@ -323,16 +323,16 @@ The Repairs module tracks instrument repair tickets from intake through completi
|
||||
Each ticket moves through these stages:
|
||||
|
||||
- **New** — ticket just created, not yet examined
|
||||
- **In Transit** — instrument being transported to the shop (for school pickups or shipped instruments)
|
||||
- **Intake** — instrument received, condition documented
|
||||
- **Diagnosing** — technician examining the instrument
|
||||
- **In Transit** — item being transported to the shop (for pickups or shipped items)
|
||||
- **Intake** — item received, condition documented
|
||||
- **Diagnosing** — technician examining the item
|
||||
- **Pending Approval** — estimate provided, waiting for customer OK
|
||||
- **Approved** — customer authorized the work
|
||||
- **In Progress** — actively being repaired
|
||||
- **Pending Parts** — waiting on parts order
|
||||
- **Ready** — repair complete, awaiting pickup
|
||||
- **Picked Up** — customer collected the instrument
|
||||
- **Delivered** — instrument returned via delivery (for school batches)
|
||||
- **Picked Up** — customer collected the item
|
||||
- **Delivered** — item returned via delivery (for school batches)
|
||||
|
||||
Click the status buttons on the ticket detail page to advance through the workflow. You can also click steps on the progress bar.
|
||||
|
||||
@@ -340,7 +340,7 @@ Click the status buttons on the ticket detail page to advance through the workfl
|
||||
|
||||
The ticket detail has four tabs:
|
||||
|
||||
- **Details** — customer info, instrument, condition, costs. Click **Edit** to modify.
|
||||
- **Details** — customer info, item, condition, costs. Click **Edit** to modify.
|
||||
- **Line Items** — labor, parts, flat-rate services, and misc charges. Use the template picker for common repairs.
|
||||
- **Notes** — running journal of notes. Choose **Internal** (staff only) or **Customer Visible**. You can attach photos to notes.
|
||||
- **Photos & Docs** — photos organized by repair phase (intake, in progress, completed) plus a documents section for signed approvals, quotes, and receipts.
|
||||
@@ -372,7 +372,7 @@ Templates are pre-defined common repairs (e.g. "Bow Rehair — Violin — 4/4")
|
||||
2. Click **New Template**
|
||||
3. Fill in:
|
||||
- **Name** — e.g. "Bow Rehair", "String Change", "Valve Overhaul"
|
||||
- **Instrument Type** — e.g. "Violin", "Guitar", "Trumpet"
|
||||
- **Item Category** — e.g. "Violin", "Guitar", "Trumpet"
|
||||
- **Size** — e.g. "4/4", "3/4", "Full"
|
||||
- **Type** — Labor, Part, Flat Rate, or Misc
|
||||
- **Default Price** — the customer-facing price
|
||||
@@ -400,19 +400,19 @@ When creating a ticket or adding line items, type in the **Quick Add from Templa
|
||||
content: `
|
||||
# Repair Batches
|
||||
|
||||
Batches group multiple repair tickets under one job — typically for schools bringing in many instruments at once.
|
||||
Batches group multiple repair tickets under one job — typically for schools bringing in many items at once.
|
||||
|
||||
## Creating a Batch
|
||||
|
||||
1. Go to **Repair Batches** in the sidebar
|
||||
2. Click **New Batch**
|
||||
3. Select the school's account
|
||||
4. Enter contact info, instrument count, and any notes
|
||||
4. Enter contact info, item count, and any notes
|
||||
5. Click **Create Batch**
|
||||
|
||||
## Adding Tickets to a Batch
|
||||
|
||||
When creating new repair tickets, select the batch in the form. Each instrument gets its own ticket linked to the batch.
|
||||
When creating new repair tickets, select the batch in the form. Each item gets its own ticket linked to the batch.
|
||||
|
||||
## Batch Approval
|
||||
|
||||
@@ -426,10 +426,10 @@ Only admins can approve or reject batches.
|
||||
|
||||
## Batch Status
|
||||
|
||||
- **Intake** — receiving instruments
|
||||
- **Intake** — receiving items
|
||||
- **In Progress** — work underway
|
||||
- **Completed** — all repairs done
|
||||
- **Delivered** — instruments returned to school
|
||||
- **Delivered** — items returned to customer
|
||||
|
||||
## Filtering
|
||||
|
||||
@@ -468,7 +468,7 @@ Photos appear inline in the note entry. Click to view full size.
|
||||
|
||||
The Photos & Docs tab organizes photos into categories:
|
||||
|
||||
- **Intake Photos** — document instrument condition when received
|
||||
- **Intake Photos** — document item condition when received
|
||||
- **Work in Progress** — during the repair
|
||||
- **Completed** — final result after repair
|
||||
- **Documents** — signed approvals, quotes, receipts (accepts PDFs)
|
||||
|
||||
@@ -7,9 +7,9 @@ import { createClient } from './lib/client.js'
|
||||
// --- Config ---
|
||||
const DB_HOST = process.env.DB_HOST ?? 'localhost'
|
||||
const DB_PORT = Number(process.env.DB_PORT ?? '5432')
|
||||
const DB_USER = process.env.DB_USER ?? 'forte'
|
||||
const DB_PASS = process.env.DB_PASS ?? 'forte'
|
||||
const TEST_DB = 'forte_api_test'
|
||||
const DB_USER = process.env.DB_USER ?? 'lunarfront'
|
||||
const DB_PASS = process.env.DB_PASS ?? 'lunarfront'
|
||||
const TEST_DB = 'lunarfront_api_test'
|
||||
const TEST_PORT = 8001
|
||||
const BASE_URL = `http://localhost:${TEST_PORT}`
|
||||
const COMPANY_ID = 'a0000000-0000-0000-0000-000000000001'
|
||||
@@ -60,7 +60,7 @@ async function setupDatabase() {
|
||||
`)
|
||||
|
||||
// Seed company + location (company table stays as store settings)
|
||||
await testSql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Test Music Co.', 'America/Chicago')`
|
||||
await testSql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Test Store', 'America/Chicago')`
|
||||
await testSql`INSERT INTO location (id, name) VALUES (${LOCATION_ID}, 'Test Location')`
|
||||
|
||||
// Seed lookup tables
|
||||
@@ -96,8 +96,8 @@ async function setupDatabase() {
|
||||
{ slug: 'inventory', name: 'Inventory', description: 'Product catalog, stock tracking, and unit management', enabled: true },
|
||||
{ slug: 'pos', name: 'Point of Sale', description: 'Sales transactions, cash drawer, and receipts', enabled: true },
|
||||
{ slug: 'repairs', name: 'Repairs', description: 'Repair ticket management, batches, and service templates', enabled: true },
|
||||
{ slug: 'rentals', name: 'Rentals', description: 'Instrument rental agreements and billing', enabled: false },
|
||||
{ slug: 'lessons', name: 'Lessons', description: 'Lesson scheduling, instructor management, and billing', enabled: false },
|
||||
{ slug: 'rentals', name: 'Rentals', description: 'Rental agreements and billing', enabled: false },
|
||||
{ slug: 'lessons', name: 'Lessons', description: 'Scheduling, staff management, and billing', enabled: false },
|
||||
{ slug: 'files', name: 'Files', description: 'Shared file storage with folder organization', enabled: true },
|
||||
{ slug: 'vault', name: 'Vault', description: 'Encrypted password and secret manager', enabled: true },
|
||||
{ slug: 'email', name: 'Email', description: 'Email campaigns, templates, and sending', enabled: false },
|
||||
@@ -134,7 +134,7 @@ async function startBackend(): Promise<Subprocess> {
|
||||
HOST: '0.0.0.0',
|
||||
NODE_ENV: 'development',
|
||||
LOG_LEVEL: 'error',
|
||||
STORAGE_LOCAL_PATH: '/tmp/forte-test-files',
|
||||
STORAGE_LOCAL_PATH: '/tmp/lunarfront-test-files',
|
||||
},
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
@@ -170,7 +170,7 @@ async function registerTestUser(): Promise<string> {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
email: 'test@forte.dev',
|
||||
email: 'test@lunarfront.dev',
|
||||
password: testPassword,
|
||||
firstName: 'Test',
|
||||
lastName: 'Runner',
|
||||
@@ -193,7 +193,7 @@ async function registerTestUser(): Promise<string> {
|
||||
const loginRes = await fetch(`${BASE_URL}/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'test@forte.dev', password: testPassword }),
|
||||
body: JSON.stringify({ email: 'test@lunarfront.dev', password: testPassword }),
|
||||
})
|
||||
const loginData = await loginRes.json() as { token?: string }
|
||||
if (loginRes.status !== 200 || !loginData.token) throw new Error(`Auth failed: ${JSON.stringify(loginData)}`)
|
||||
|
||||
@@ -304,7 +304,7 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
|
||||
t.test('cannot disable yourself', { tags: ['users', 'status'] }, async () => {
|
||||
// Get current user ID from the users list
|
||||
const usersRes = await t.api.get('/v1/users')
|
||||
const currentUser = usersRes.data.data.find((u: { email: string }) => u.email === 'test@forte.dev')
|
||||
const currentUser = usersRes.data.data.find((u: { email: string }) => u.email === 'test@lunarfront.dev')
|
||||
t.assert.ok(currentUser)
|
||||
|
||||
const res = await t.api.patch(`/v1/users/${currentUser.id}/status`, { isActive: false })
|
||||
|
||||
@@ -7,8 +7,8 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
|
||||
const res = await t.api.post('/v1/repair-tickets', {
|
||||
customerName: 'Walk-In Customer',
|
||||
customerPhone: '555-0100',
|
||||
instrumentDescription: 'Yamaha Trumpet',
|
||||
problemDescription: 'Stuck valve, needs cleaning',
|
||||
itemDescription: 'Samsung Galaxy S24',
|
||||
problemDescription: 'Cracked screen, touch not working',
|
||||
conditionIn: 'fair',
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
@@ -25,8 +25,8 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
|
||||
const res = await t.api.post('/v1/repair-tickets', {
|
||||
customerName: 'Repair Customer',
|
||||
accountId: acct.data.id,
|
||||
problemDescription: 'Broken bridge on violin',
|
||||
instrumentDescription: 'Student Violin 4/4',
|
||||
problemDescription: 'Screen flickering intermittently',
|
||||
itemDescription: 'Dell XPS 15 Laptop',
|
||||
conditionIn: 'poor',
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
@@ -164,12 +164,12 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
|
||||
t.assert.ok(res.data.data.some((t: { customerName: string }) => t.customerName === 'Searchable Trumpet Guy'))
|
||||
})
|
||||
|
||||
t.test('searches tickets by instrument description', { tags: ['tickets', 'search'] }, async () => {
|
||||
await t.api.post('/v1/repair-tickets', { customerName: 'Instrument Search', problemDescription: 'Test', instrumentDescription: 'Selmer Mark VI Saxophone' })
|
||||
t.test('searches tickets by item description', { tags: ['tickets', 'search'] }, async () => {
|
||||
await t.api.post('/v1/repair-tickets', { customerName: 'Item Search', problemDescription: 'Test', itemDescription: 'Samsung Galaxy S24 Ultra' })
|
||||
|
||||
const res = await t.api.get('/v1/repair-tickets', { q: 'Mark VI' })
|
||||
const res = await t.api.get('/v1/repair-tickets', { q: 'Galaxy S24' })
|
||||
t.assert.status(res, 200)
|
||||
t.assert.ok(res.data.data.some((t: { instrumentDescription: string }) => t.instrumentDescription?.includes('Mark VI')))
|
||||
t.assert.ok(res.data.data.some((t: { itemDescription: string }) => t.itemDescription?.includes('Galaxy S24')))
|
||||
})
|
||||
|
||||
t.test('sorts tickets by customer name descending', { tags: ['tickets', 'sort'] }, async () => {
|
||||
@@ -289,7 +289,7 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
|
||||
const ticket = await t.api.post('/v1/repair-tickets', { customerName: 'Customer Note', problemDescription: 'Test' })
|
||||
|
||||
const res = await t.api.post(`/v1/repair-tickets/${ticket.data.id}/notes`, {
|
||||
content: 'Your instrument is ready for pickup',
|
||||
content: 'Your item is ready for pickup',
|
||||
visibility: 'customer',
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
@@ -341,17 +341,17 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
|
||||
|
||||
const res = await t.api.post('/v1/repair-batches', {
|
||||
accountId: acct.data.id,
|
||||
contactName: 'Band Director',
|
||||
contactName: 'IT Director',
|
||||
contactPhone: '555-0200',
|
||||
instrumentCount: 15,
|
||||
notes: 'Annual instrument checkup',
|
||||
itemCount: 15,
|
||||
notes: 'Annual equipment checkup',
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
t.assert.ok(res.data.batchNumber)
|
||||
t.assert.equal(res.data.batchNumber.length, 6)
|
||||
t.assert.equal(res.data.status, 'intake')
|
||||
t.assert.equal(res.data.approvalStatus, 'pending')
|
||||
t.assert.equal(res.data.instrumentCount, 15)
|
||||
t.assert.equal(res.data.itemCount, 15)
|
||||
})
|
||||
|
||||
t.test('returns 404 for missing batch', { tags: ['batches', 'read'] }, async () => {
|
||||
@@ -377,19 +377,19 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
|
||||
|
||||
t.test('updates a batch', { tags: ['batches', 'update'] }, async () => {
|
||||
const acct = await t.api.post('/v1/accounts', { name: 'Update Batch School', billingMode: 'consolidated' })
|
||||
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, instrumentCount: 5 })
|
||||
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, itemCount: 5 })
|
||||
|
||||
const res = await t.api.patch(`/v1/repair-batches/${batch.data.id}`, { instrumentCount: 10, contactName: 'Updated Director' })
|
||||
const res = await t.api.patch(`/v1/repair-batches/${batch.data.id}`, { itemCount: 10, contactName: 'Updated Director' })
|
||||
t.assert.status(res, 200)
|
||||
t.assert.equal(res.data.contactName, 'Updated Director')
|
||||
})
|
||||
|
||||
t.test('adds tickets to a batch and lists them', { tags: ['batches', 'tickets'] }, async () => {
|
||||
const acct = await t.api.post('/v1/accounts', { name: 'Batch Tickets School', billingMode: 'consolidated' })
|
||||
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, instrumentCount: 2 })
|
||||
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, itemCount: 2 })
|
||||
|
||||
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Flute pads', instrumentDescription: 'Flute' })
|
||||
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Clarinet cork', instrumentDescription: 'Clarinet' })
|
||||
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Screen cracked', itemDescription: 'Chromebook #1' })
|
||||
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Battery dead', itemDescription: 'Chromebook #2' })
|
||||
|
||||
const tickets = await t.api.get(`/v1/repair-batches/${batch.data.id}/tickets`, { limit: 100 })
|
||||
t.assert.status(tickets, 200)
|
||||
@@ -437,36 +437,36 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
|
||||
|
||||
t.test('creates a service template', { tags: ['templates', 'create'] }, async () => {
|
||||
const res = await t.api.post('/v1/repair-service-templates', {
|
||||
name: 'Bow Rehair',
|
||||
instrumentType: 'Violin',
|
||||
size: '4/4',
|
||||
name: 'Screen Repair',
|
||||
itemCategory: 'Electronics',
|
||||
size: 'Phone',
|
||||
itemType: 'flat_rate',
|
||||
defaultPrice: 65,
|
||||
defaultCost: 15,
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
t.assert.equal(res.data.name, 'Bow Rehair')
|
||||
t.assert.equal(res.data.instrumentType, 'Violin')
|
||||
t.assert.equal(res.data.size, '4/4')
|
||||
t.assert.equal(res.data.name, 'Screen Repair')
|
||||
t.assert.equal(res.data.itemCategory, 'Electronics')
|
||||
t.assert.equal(res.data.size, 'Phone')
|
||||
t.assert.equal(res.data.defaultPrice, '65.00')
|
||||
t.assert.equal(res.data.defaultCost, '15.00')
|
||||
})
|
||||
|
||||
t.test('lists service templates with search', { tags: ['templates', 'read'] }, async () => {
|
||||
await t.api.post('/v1/repair-service-templates', { name: 'String Change', instrumentType: 'Guitar', defaultPrice: 25 })
|
||||
await t.api.post('/v1/repair-service-templates', { name: 'Battery Replacement', itemCategory: 'Electronics', defaultPrice: 25 })
|
||||
|
||||
const res = await t.api.get('/v1/repair-service-templates', { q: 'String', limit: 100 })
|
||||
const res = await t.api.get('/v1/repair-service-templates', { q: 'Battery', limit: 100 })
|
||||
t.assert.status(res, 200)
|
||||
t.assert.ok(res.data.data.some((t: { name: string }) => t.name === 'String Change'))
|
||||
t.assert.ok(res.data.data.some((t: { name: string }) => t.name === 'Battery Replacement'))
|
||||
t.assert.ok(res.data.pagination)
|
||||
})
|
||||
|
||||
t.test('updates a service template', { tags: ['templates', 'update'] }, async () => {
|
||||
const created = await t.api.post('/v1/repair-service-templates', { name: 'Pad Replace', defaultPrice: 30 })
|
||||
const res = await t.api.patch(`/v1/repair-service-templates/${created.data.id}`, { defaultPrice: 35, instrumentType: 'Clarinet' })
|
||||
const created = await t.api.post('/v1/repair-service-templates', { name: 'Tune-Up', defaultPrice: 30 })
|
||||
const res = await t.api.patch(`/v1/repair-service-templates/${created.data.id}`, { defaultPrice: 35, itemCategory: 'Bicycles' })
|
||||
t.assert.status(res, 200)
|
||||
t.assert.equal(res.data.defaultPrice, '35.00')
|
||||
t.assert.equal(res.data.instrumentType, 'Clarinet')
|
||||
t.assert.equal(res.data.itemCategory, 'Bicycles')
|
||||
})
|
||||
|
||||
t.test('soft-deletes a service template', { tags: ['templates', 'delete'] }, async () => {
|
||||
|
||||
@@ -96,14 +96,14 @@ suite('Vault', { tags: ['vault'] }, (t) => {
|
||||
|
||||
const res = await t.api.post(`/v1/vault/categories/${catId}/entries`, {
|
||||
name: 'Store WiFi',
|
||||
username: 'ForteMusic',
|
||||
username: 'DemoUser',
|
||||
url: 'http://192.168.1.1',
|
||||
notes: 'Router admin panel',
|
||||
secret: 'supersecretpassword123',
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
t.assert.equal(res.data.name, 'Store WiFi')
|
||||
t.assert.equal(res.data.username, 'ForteMusic')
|
||||
t.assert.equal(res.data.username, 'DemoUser')
|
||||
t.assert.equal(res.data.hasSecret, true)
|
||||
// Secret value should NOT be in the response
|
||||
t.assert.falsy(res.data.encryptedValue)
|
||||
|
||||
@@ -27,7 +27,7 @@ async function dav(
|
||||
|
||||
suite('WebDAV', { tags: ['webdav', 'storage'] }, (t) => {
|
||||
// Use the same test user created by the test runner
|
||||
const email = 'test@forte.dev'
|
||||
const email = 'test@lunarfront.dev'
|
||||
const password = 'testpassword1234'
|
||||
const basicAuth = 'Basic ' + Buffer.from(`${email}:${password}`).toString('base64')
|
||||
const badAuth = 'Basic ' + Buffer.from(`${email}:wrongpassword`).toString('base64')
|
||||
|
||||
@@ -5,6 +5,6 @@ export default defineConfig({
|
||||
out: './src/db/migrations',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL ?? 'postgresql://forte:forte@localhost:5432/forte',
|
||||
url: process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@forte/backend",
|
||||
"name": "@lunarfront/backend",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -13,6 +13,8 @@
|
||||
"db:generate": "bunx drizzle-kit generate",
|
||||
"db:migrate": "bunx drizzle-kit migrate",
|
||||
"db:seed-dev": "bun run src/db/seeds/dev-seed.ts",
|
||||
"db:seed-music": "bun run src/db/seeds/music-store-seed.ts",
|
||||
"db:seed-reset-repairs": "bun run src/db/seeds/reset-repairs.ts",
|
||||
"db:seed": "bun run src/db/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -20,7 +22,7 @@
|
||||
"@fastify/jwt": "^9",
|
||||
"@fastify/multipart": "^9.4.0",
|
||||
"@fastify/rate-limit": "^10.3.0",
|
||||
"@forte/shared": "workspace:*",
|
||||
"@lunarfront/shared": "workspace:*",
|
||||
"bcrypt": "^6",
|
||||
"drizzle-orm": "^0.38",
|
||||
"fastify": "^5",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Generalize music-specific column names to generic terminology
|
||||
|
||||
-- repair_ticket: instrument_description -> item_description
|
||||
ALTER TABLE repair_ticket RENAME COLUMN instrument_description TO item_description;
|
||||
|
||||
-- repair_batch: instrument_count -> item_count
|
||||
ALTER TABLE repair_batch RENAME COLUMN instrument_count TO item_count;
|
||||
|
||||
-- repair_service_template: instrument_type -> item_category
|
||||
ALTER TABLE repair_service_template RENAME COLUMN instrument_type TO item_category;
|
||||
|
||||
-- Update module descriptions to be industry-agnostic
|
||||
UPDATE module_config SET description = 'Rental agreements and billing' WHERE slug = 'rentals';
|
||||
UPDATE module_config SET description = 'Scheduling, staff management, and billing' WHERE slug = 'lessons';
|
||||
@@ -190,6 +190,13 @@
|
||||
"when": 1774860000000,
|
||||
"tag": "0026_modules",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 27,
|
||||
"version": "7",
|
||||
"when": 1774870000000,
|
||||
"tag": "0027_generalize_terminology",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -82,7 +82,7 @@ export const repairBatches = pgTable('repair_batch', {
|
||||
dueDate: timestamp('due_date', { withTimezone: true }),
|
||||
completedDate: timestamp('completed_date', { withTimezone: true }),
|
||||
deliveredDate: timestamp('delivered_date', { withTimezone: true }),
|
||||
instrumentCount: integer('instrument_count').notNull().default(0),
|
||||
itemCount: integer('item_count').notNull().default(0),
|
||||
receivedCount: integer('received_count').notNull().default(0),
|
||||
estimatedTotal: numeric('estimated_total', { precision: 10, scale: 2 }),
|
||||
actualTotal: numeric('actual_total', { precision: 10, scale: 2 }),
|
||||
@@ -101,7 +101,7 @@ export const repairTickets = pgTable('repair_ticket', {
|
||||
customerName: varchar('customer_name', { length: 255 }).notNull(),
|
||||
customerPhone: varchar('customer_phone', { length: 50 }),
|
||||
inventoryUnitId: uuid('inventory_unit_id').references(() => inventoryUnits.id),
|
||||
instrumentDescription: text('instrument_description'),
|
||||
itemDescription: text('item_description'),
|
||||
serialNumber: varchar('serial_number', { length: 255 }),
|
||||
conditionIn: repairConditionInEnum('condition_in'),
|
||||
conditionInNotes: text('condition_in_notes'),
|
||||
@@ -158,7 +158,7 @@ export type RepairNoteInsert = typeof repairNotes.$inferInsert
|
||||
export const repairServiceTemplates = pgTable('repair_service_template', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
instrumentType: varchar('instrument_type', { length: 100 }),
|
||||
itemCategory: varchar('item_category', { length: 100 }),
|
||||
size: varchar('size', { length: 50 }),
|
||||
description: text('description'),
|
||||
itemType: repairLineItemTypeEnum('item_type').notNull().default('flat_rate'),
|
||||
|
||||
@@ -7,7 +7,7 @@ const DEV_LOCATION_ID = '00000000-0000-0000-0000-000000000010'
|
||||
|
||||
async function seed() {
|
||||
const connectionString =
|
||||
process.env.DATABASE_URL ?? 'postgresql://forte:forte@localhost:5432/forte'
|
||||
process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront'
|
||||
const sql = postgres(connectionString)
|
||||
const db = drizzle(sql)
|
||||
|
||||
@@ -17,7 +17,7 @@ async function seed() {
|
||||
.insert(companies)
|
||||
.values({
|
||||
id: DEV_COMPANY_ID,
|
||||
name: 'Dev Music Co.',
|
||||
name: 'Dev Store',
|
||||
timezone: 'America/Chicago',
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
import postgres from 'postgres'
|
||||
|
||||
const DB_URL = process.env.DATABASE_URL ?? 'postgresql://forte:forte@localhost:5432/forte'
|
||||
const DB_URL = process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront'
|
||||
const COMPANY_ID = 'a0000000-0000-0000-0000-000000000001'
|
||||
|
||||
const sql = postgres(DB_URL)
|
||||
@@ -17,7 +17,7 @@ async function seed() {
|
||||
// Create company and location if they don't exist
|
||||
const [company] = await sql`SELECT id FROM company WHERE id = ${COMPANY_ID}`
|
||||
if (!company) {
|
||||
await sql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Forte Music Store', 'America/Chicago')`
|
||||
await sql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Demo Store', 'America/Chicago')`
|
||||
await sql`INSERT INTO location (id, name) VALUES ('a0000000-0000-0000-0000-000000000002', 'Main Store')`
|
||||
console.log(' Created company and location')
|
||||
|
||||
@@ -39,16 +39,16 @@ async function seed() {
|
||||
}
|
||||
|
||||
// --- Admin user (if not exists) ---
|
||||
const [adminUser] = await sql`SELECT id FROM "user" WHERE email = 'admin@forte.dev'`
|
||||
const [adminUser] = await sql`SELECT id FROM "user" WHERE email = 'admin@lunarfront.dev'`
|
||||
if (!adminUser) {
|
||||
const bcrypt = await import('bcrypt')
|
||||
const hashedPw = await (bcrypt.default || bcrypt).hash('admin1234', 10)
|
||||
const [user] = await sql`INSERT INTO "user" (email, password_hash, first_name, last_name, role) VALUES ('admin@forte.dev', ${hashedPw}, 'Admin', 'User', 'admin') RETURNING id`
|
||||
const [user] = await sql`INSERT INTO "user" (email, password_hash, first_name, last_name, role) VALUES ('admin@lunarfront.dev', ${hashedPw}, 'Admin', 'User', 'admin') RETURNING id`
|
||||
const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
|
||||
if (adminRole) {
|
||||
await sql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${user.id}, ${adminRole.id}) ON CONFLICT DO NOTHING`
|
||||
}
|
||||
console.log(' Created admin user: admin@forte.dev / admin1234')
|
||||
console.log(' Created admin user: admin@lunarfront.dev / admin1234')
|
||||
} else {
|
||||
const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
|
||||
if (adminRole) {
|
||||
@@ -61,11 +61,11 @@ async function seed() {
|
||||
const accounts = [
|
||||
{ name: 'Smith Family', email: 'smith@example.com', phone: '555-0101' },
|
||||
{ name: 'Johnson Family', email: 'johnson@example.com', phone: '555-0102' },
|
||||
{ name: 'Lincoln High School', email: 'band@lincoln.edu', phone: '555-0200' },
|
||||
{ name: 'Garcia Music Studio', email: 'garcia@studio.com', phone: '555-0103' },
|
||||
{ name: 'Lincoln High School', email: 'office@lincoln.edu', phone: '555-0200' },
|
||||
{ name: 'Garcia Workshop', email: 'garcia@studio.com', phone: '555-0103' },
|
||||
{ name: 'Mike Thompson', email: 'mike.t@email.com', phone: '555-0104' },
|
||||
{ name: 'Emily Chen', email: 'emily.chen@email.com', phone: '555-0105' },
|
||||
{ name: 'Westside Church', email: 'music@westsidechurch.org', phone: '555-0300' },
|
||||
{ name: 'Westside Church', email: 'admin@westsidechurch.org', phone: '555-0300' },
|
||||
{ name: 'Oak Elementary', email: 'office@oakelementary.edu', phone: '555-0201' },
|
||||
]
|
||||
|
||||
@@ -89,7 +89,7 @@ async function seed() {
|
||||
{ accountName: 'Smith Family', firstName: 'Tommy', lastName: 'Smith', isMinor: true },
|
||||
{ accountName: 'Johnson Family', firstName: 'Lisa', lastName: 'Johnson', email: 'lisa.j@example.com' },
|
||||
{ accountName: 'Johnson Family', firstName: 'Jake', lastName: 'Johnson', isMinor: true },
|
||||
{ accountName: 'Garcia Music Studio', firstName: 'Carlos', lastName: 'Garcia', email: 'carlos@studio.com' },
|
||||
{ accountName: 'Garcia Workshop', firstName: 'Carlos', lastName: 'Garcia', email: 'carlos@studio.com' },
|
||||
{ accountName: 'Mike Thompson', firstName: 'Mike', lastName: 'Thompson', email: 'mike.t@email.com' },
|
||||
{ accountName: 'Emily Chen', firstName: 'Emily', lastName: 'Chen', email: 'emily.chen@email.com' },
|
||||
]
|
||||
@@ -105,39 +105,37 @@ async function seed() {
|
||||
|
||||
// --- Repair Service Templates ---
|
||||
const templates = [
|
||||
{ name: 'Bow Rehair', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '65.00', cost: '15.00' },
|
||||
{ name: 'Bow Rehair', instrumentType: 'Violin', size: '3/4', itemType: 'flat_rate', price: '55.00', cost: '12.00' },
|
||||
{ name: 'Bow Rehair', instrumentType: 'Cello', size: null, itemType: 'flat_rate', price: '80.00', cost: '20.00' },
|
||||
{ name: 'Bow Rehair', instrumentType: 'Bass', size: null, itemType: 'flat_rate', price: '90.00', cost: '25.00' },
|
||||
{ name: 'String Change', instrumentType: 'Guitar', size: 'Acoustic', itemType: 'flat_rate', price: '25.00', cost: '8.00' },
|
||||
{ name: 'String Change', instrumentType: 'Guitar', size: 'Electric', itemType: 'flat_rate', price: '25.00', cost: '7.00' },
|
||||
{ name: 'String Change', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '35.00', cost: '12.00' },
|
||||
{ name: 'Valve Overhaul', instrumentType: 'Trumpet', size: null, itemType: 'labor', price: '85.00', cost: null },
|
||||
{ name: 'Pad Replacement', instrumentType: 'Clarinet', size: null, itemType: 'flat_rate', price: '120.00', cost: '30.00' },
|
||||
{ name: 'Pad Replacement', instrumentType: 'Flute', size: null, itemType: 'flat_rate', price: '110.00', cost: '25.00' },
|
||||
{ name: 'Cork Replacement', instrumentType: 'Clarinet', size: null, itemType: 'flat_rate', price: '45.00', cost: '5.00' },
|
||||
{ name: 'Slide Repair', instrumentType: 'Trombone', size: null, itemType: 'labor', price: '75.00', cost: null },
|
||||
{ name: 'Bridge Setup', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '40.00', cost: '10.00' },
|
||||
{ name: 'Guitar Setup', instrumentType: 'Guitar', size: null, itemType: 'flat_rate', price: '65.00', cost: '5.00' },
|
||||
{ name: 'Dent Removal', instrumentType: 'Brass', size: null, itemType: 'labor', price: '50.00', cost: null },
|
||||
{ name: 'General Cleaning', instrumentType: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
|
||||
{ name: 'Screen Repair', itemCategory: 'Electronics', size: 'Phone', itemType: 'flat_rate', price: '89.00', cost: '25.00' },
|
||||
{ name: 'Screen Repair', itemCategory: 'Electronics', size: 'Tablet', itemType: 'flat_rate', price: '129.00', cost: '45.00' },
|
||||
{ name: 'Battery Replacement', itemCategory: 'Electronics', size: 'Phone', itemType: 'flat_rate', price: '59.00', cost: '15.00' },
|
||||
{ name: 'Battery Replacement', itemCategory: 'Electronics', size: 'Laptop', itemType: 'flat_rate', price: '99.00', cost: '35.00' },
|
||||
{ name: 'Tune-Up', itemCategory: 'Bicycles', size: 'Standard', itemType: 'flat_rate', price: '65.00', cost: '10.00' },
|
||||
{ name: 'Brake Adjustment', itemCategory: 'Bicycles', size: null, itemType: 'flat_rate', price: '35.00', cost: '5.00' },
|
||||
{ name: 'Blade Sharpening', itemCategory: 'Tools', size: null, itemType: 'flat_rate', price: '15.00', cost: '3.00' },
|
||||
{ name: 'Motor Repair', itemCategory: 'Appliances', size: null, itemType: 'labor', price: '85.00', cost: null },
|
||||
{ name: 'Zipper Replacement', itemCategory: 'Clothing', size: null, itemType: 'flat_rate', price: '25.00', cost: '5.00' },
|
||||
{ name: 'Sole Replacement', itemCategory: 'Footwear', size: null, itemType: 'flat_rate', price: '55.00', cost: '15.00' },
|
||||
{ name: 'Watch Battery', itemCategory: 'Watches', size: null, itemType: 'flat_rate', price: '15.00', cost: '3.00' },
|
||||
{ name: 'Furniture Refinishing', itemCategory: 'Furniture', size: null, itemType: 'labor', price: '150.00', cost: null },
|
||||
{ name: 'Diagnostic Check', itemCategory: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
|
||||
{ name: 'General Cleaning', itemCategory: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
|
||||
]
|
||||
|
||||
for (const t of templates) {
|
||||
const existing = await sql`SELECT id FROM repair_service_template WHERE name = ${t.name} AND COALESCE(instrument_type, '') = ${t.instrumentType ?? ''} AND COALESCE(size, '') = ${t.size ?? ''}`
|
||||
const existing = await sql`SELECT id FROM repair_service_template WHERE name = ${t.name} AND COALESCE(item_category, '') = ${t.itemCategory ?? ''} AND COALESCE(size, '') = ${t.size ?? ''}`
|
||||
if (existing.length > 0) continue
|
||||
await sql`INSERT INTO repair_service_template (name, instrument_type, size, item_type, default_price, default_cost, is_active) VALUES (${t.name}, ${t.instrumentType}, ${t.size}, ${t.itemType}, ${t.price}, ${t.cost}, true)`
|
||||
console.log(` Template: ${t.name} ${t.instrumentType ?? ''} ${t.size ?? ''}`)
|
||||
await sql`INSERT INTO repair_service_template (name, item_category, size, item_type, default_price, default_cost, is_active) VALUES (${t.name}, ${t.itemCategory}, ${t.size}, ${t.itemType}, ${t.price}, ${t.cost}, true)`
|
||||
console.log(` Template: ${t.name} ${t.itemCategory ?? ''} ${t.size ?? ''}`)
|
||||
}
|
||||
|
||||
// --- Repair Tickets ---
|
||||
const tickets = [
|
||||
{ customer: 'Mike Thompson', instrument: 'Fender Stratocaster', serial: 'US22-045891', problem: 'Fret buzz on 3rd and 5th fret, needs setup', condition: 'good', status: 'in_progress', estimate: '65.00' },
|
||||
{ customer: 'Emily Chen', instrument: 'Yamaha YTR-2330 Trumpet', serial: 'YTR-78432', problem: 'Stuck 2nd valve, sluggish action on all valves', condition: 'fair', status: 'pending_approval', estimate: '85.00' },
|
||||
{ customer: 'David Smith', instrument: 'Stradivarius Copy Violin', serial: null, problem: 'Bow needs rehair, bridge slightly warped', condition: 'fair', status: 'ready', estimate: '105.00' },
|
||||
{ customer: 'Carlos Garcia', instrument: 'Martin D-28 Acoustic Guitar', serial: 'M2284563', problem: 'Broken tuning peg, needs replacement', condition: 'good', status: 'new', estimate: null },
|
||||
{ customer: 'Lisa Johnson', instrument: 'Yamaha YCL-255 Clarinet', serial: null, problem: 'Several pads worn, keys sticking', condition: 'poor', status: 'diagnosing', estimate: null },
|
||||
{ customer: 'Walk-In Customer', instrument: 'Unknown Flute', serial: null, problem: 'Customer says it squeaks on high notes', condition: 'fair', status: 'intake', estimate: null },
|
||||
{ customer: 'Mike Thompson', item: 'Samsung Galaxy S24', serial: 'IMEI-354789102', problem: 'Cracked screen, touch not responsive in bottom half', condition: 'good', status: 'in_progress', estimate: '89.00' },
|
||||
{ customer: 'Emily Chen', item: 'HP LaserJet Pro M404', serial: 'HP-CNB3K12345', problem: 'Paper jam sensor error, won\'t feed from tray 2', condition: 'fair', status: 'pending_approval', estimate: '85.00' },
|
||||
{ customer: 'David Smith', item: 'Trek Marlin 7 Mountain Bike', serial: null, problem: 'Rear derailleur bent, chain skipping gears', condition: 'fair', status: 'ready', estimate: '65.00' },
|
||||
{ customer: 'Carlos Garcia', item: 'KitchenAid Stand Mixer KSM150', serial: 'W10807813', problem: 'Motor making grinding noise at low speeds', condition: 'good', status: 'new', estimate: null },
|
||||
{ customer: 'Lisa Johnson', item: 'Apple MacBook Pro 14"', serial: null, problem: 'Battery draining rapidly, trackpad click intermittent', condition: 'poor', status: 'diagnosing', estimate: null },
|
||||
{ customer: 'Walk-In Customer', item: 'Leather Work Boots', serial: null, problem: 'Sole separating from upper on both shoes', condition: 'fair', status: 'intake', estimate: null },
|
||||
]
|
||||
|
||||
for (const t of tickets) {
|
||||
@@ -145,8 +143,8 @@ async function seed() {
|
||||
if (existing.length > 0) continue
|
||||
const num = String(Math.floor(100000 + Math.random() * 900000))
|
||||
const acctId = acctIds[t.customer] ?? null
|
||||
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, instrument_description, serial_number, problem_description, condition_in, status, estimated_cost) VALUES (${num}, ${t.customer}, ${acctId}, ${t.instrument}, ${t.serial}, ${t.problem}, ${t.condition}, ${t.status}, ${t.estimate})`
|
||||
console.log(` Ticket: ${t.customer} — ${t.instrument} [${t.status}]`)
|
||||
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, item_description, serial_number, problem_description, condition_in, status, estimated_cost) VALUES (${num}, ${t.customer}, ${acctId}, ${t.item}, ${t.serial}, ${t.problem}, ${t.condition}, ${t.status}, ${t.estimate})`
|
||||
console.log(` Ticket: ${t.customer} — ${t.item} [${t.status}]`)
|
||||
}
|
||||
|
||||
// --- Repair Batch ---
|
||||
@@ -154,22 +152,22 @@ async function seed() {
|
||||
if (batchExists.length === 0) {
|
||||
const batchNum = String(Math.floor(100000 + Math.random() * 900000))
|
||||
const schoolId = acctIds['Lincoln High School']
|
||||
const [batch] = await sql`INSERT INTO repair_batch (batch_number, account_id, contact_name, contact_phone, contact_email, instrument_count, notes, status) VALUES (${batchNum}, ${schoolId}, 'Mr. Williams', '555-0210', 'williams@lincoln.edu', 5, 'Annual band instrument checkup — 5 instruments', 'intake') RETURNING id`
|
||||
const [batch] = await sql`INSERT INTO repair_batch (batch_number, account_id, contact_name, contact_phone, contact_email, item_count, notes, status) VALUES (${batchNum}, ${schoolId}, 'Mr. Williams', '555-0210', 'williams@lincoln.edu', 5, 'Annual equipment checkup — 5 items', 'intake') RETURNING id`
|
||||
|
||||
const batchTickets = [
|
||||
{ instrument: 'Student Flute', problem: 'Pads worn, needs replacement check', condition: 'fair' },
|
||||
{ instrument: 'Student Clarinet #1', problem: 'Keys sticking, cork dried out', condition: 'fair' },
|
||||
{ instrument: 'Student Clarinet #2', problem: 'Barrel crack, needs assessment', condition: 'poor' },
|
||||
{ instrument: 'Student Trumpet', problem: 'Valve oil needed, general checkup', condition: 'good' },
|
||||
{ instrument: 'Student Trombone', problem: 'Slide dent, sluggish movement', condition: 'fair' },
|
||||
{ item: 'Chromebook #101', problem: 'Screen flickering, hinge loose', condition: 'fair' },
|
||||
{ item: 'Chromebook #102', problem: 'Keyboard unresponsive, several keys stuck', condition: 'fair' },
|
||||
{ item: 'Projector — Epson EB-X51', problem: 'Lamp dim, color wheel noise', condition: 'poor' },
|
||||
{ item: 'Label Printer — Dymo 450', problem: 'Feed mechanism jammed', condition: 'good' },
|
||||
{ item: 'PA Speaker — JBL EON715', problem: 'Crackling at high volume', condition: 'fair' },
|
||||
]
|
||||
|
||||
for (const bt of batchTickets) {
|
||||
const num = String(Math.floor(100000 + Math.random() * 900000))
|
||||
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, repair_batch_id, instrument_description, problem_description, condition_in, status) VALUES (${num}, 'Lincoln High School', ${schoolId}, ${batch.id}, ${bt.instrument}, ${bt.problem}, ${bt.condition}, 'new')`
|
||||
console.log(` Batch ticket: ${bt.instrument}`)
|
||||
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, repair_batch_id, item_description, problem_description, condition_in, status) VALUES (${num}, 'Lincoln High School', ${schoolId}, ${batch.id}, ${bt.item}, ${bt.problem}, ${bt.condition}, 'new')`
|
||||
console.log(` Batch ticket: ${bt.item}`)
|
||||
}
|
||||
console.log(` Batch: Lincoln High School — 5 instruments`)
|
||||
console.log(` Batch: Lincoln High School — 5 items`)
|
||||
}
|
||||
|
||||
console.log('\nDev seed complete!')
|
||||
|
||||
@@ -20,14 +20,14 @@ export const SYSTEM_PERMISSIONS = [
|
||||
{ slug: 'pos.admin', domain: 'pos', action: 'admin', description: 'Void transactions, override prices, manage discounts' },
|
||||
|
||||
// Rentals
|
||||
{ slug: 'rentals.view', domain: 'rentals', action: 'view', description: 'View rental contracts, fleet, billing' },
|
||||
{ slug: 'rentals.edit', domain: 'rentals', action: 'edit', description: 'Create rentals, process returns, manage fleet' },
|
||||
{ slug: 'rentals.view', domain: 'rentals', action: 'view', description: 'View rental contracts, inventory, billing' },
|
||||
{ slug: 'rentals.edit', domain: 'rentals', action: 'edit', description: 'Create rentals, process returns, manage rental inventory' },
|
||||
{ slug: 'rentals.admin', domain: 'rentals', action: 'admin', description: 'Override terms, adjust equity, cancel contracts' },
|
||||
|
||||
// Lessons
|
||||
{ slug: 'lessons.view', domain: 'lessons', action: 'view', description: 'View lesson schedules, enrollments, attendance' },
|
||||
// Lessons / Scheduling
|
||||
{ slug: 'lessons.view', domain: 'lessons', action: 'view', description: 'View schedules, enrollments, attendance' },
|
||||
{ slug: 'lessons.edit', domain: 'lessons', action: 'edit', description: 'Manage scheduling, enrollment, attendance' },
|
||||
{ slug: 'lessons.admin', domain: 'lessons', action: 'admin', description: 'Configure lesson settings, manage instructors' },
|
||||
{ slug: 'lessons.admin', domain: 'lessons', action: 'admin', description: 'Configure scheduling settings, manage staff' },
|
||||
|
||||
// Repairs
|
||||
{ slug: 'repairs.view', domain: 'repairs', action: 'view', description: 'View repair tickets, parts inventory' },
|
||||
|
||||
@@ -80,7 +80,7 @@ export function webdavBasicAuth(app: FastifyInstance) {
|
||||
|
||||
const authHeader = request.headers.authorization
|
||||
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
|
||||
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
|
||||
return reply.status(401).send('Authentication required')
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export function webdavBasicAuth(app: FastifyInstance) {
|
||||
const colonIndex = decoded.indexOf(':')
|
||||
if (colonIndex === -1) {
|
||||
recordFailure(ip)
|
||||
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
|
||||
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
|
||||
return reply.status(401).send('Invalid credentials')
|
||||
}
|
||||
|
||||
@@ -103,14 +103,14 @@ export function webdavBasicAuth(app: FastifyInstance) {
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
recordFailure(ip)
|
||||
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
|
||||
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
|
||||
return reply.status(401).send('Invalid credentials')
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(password, user.passwordHash)
|
||||
if (!valid) {
|
||||
recordFailure(ip)
|
||||
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
|
||||
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
|
||||
return reply.status(401).send('Invalid credentials')
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
TaxExemptionUpdateSchema,
|
||||
MemberIdentifierCreateSchema,
|
||||
MemberIdentifierUpdateSchema,
|
||||
} from '@forte/shared/schemas'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import {
|
||||
AccountService,
|
||||
MemberService,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import bcrypt from 'bcrypt'
|
||||
import { RegisterSchema, LoginSchema } from '@forte/shared/schemas'
|
||||
import { RegisterSchema, LoginSchema } from '@lunarfront/shared/schemas'
|
||||
import { users } from '../../db/schema/users.js'
|
||||
|
||||
const SALT_ROUNDS = 10
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
SupplierCreateSchema,
|
||||
SupplierUpdateSchema,
|
||||
PaginationSchema,
|
||||
} from '@forte/shared/schemas'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import { CategoryService, SupplierService } from '../../services/inventory.service.js'
|
||||
|
||||
export const inventoryRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import { LookupCreateSchema, LookupUpdateSchema } from '@forte/shared/schemas'
|
||||
import { LookupCreateSchema, LookupUpdateSchema } from '@lunarfront/shared/schemas'
|
||||
import { UnitStatusService, ItemConditionService } from '../../services/lookup.service.js'
|
||||
import { ConflictError, ValidationError } from '../../lib/errors.js'
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
InventoryUnitCreateSchema,
|
||||
InventoryUnitUpdateSchema,
|
||||
PaginationSchema,
|
||||
} from '@forte/shared/schemas'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import { ProductService, InventoryUnitService } from '../../services/product.service.js'
|
||||
|
||||
export const productRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import { eq, count, sql, type Column } from 'drizzle-orm'
|
||||
import { PaginationSchema } from '@forte/shared/schemas'
|
||||
import { PaginationSchema } from '@lunarfront/shared/schemas'
|
||||
import { RbacService } from '../../services/rbac.service.js'
|
||||
import { ValidationError } from '../../lib/errors.js'
|
||||
import { users } from '../../db/schema/users.js'
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
RepairNoteCreateSchema,
|
||||
RepairServiceTemplateCreateSchema,
|
||||
RepairServiceTemplateUpdateSchema,
|
||||
} from '@forte/shared/schemas'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import { RepairTicketService, RepairLineItemService, RepairBatchService, RepairNoteService, RepairServiceTemplateService } from '../../services/repair.service.js'
|
||||
|
||||
export const repairRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import multipart from '@fastify/multipart'
|
||||
import { PaginationSchema } from '@forte/shared/schemas'
|
||||
import { PaginationSchema } from '@lunarfront/shared/schemas'
|
||||
import { StorageFolderService, StorageFileService, StoragePermissionService } from '../../services/storage.service.js'
|
||||
import { ValidationError } from '../../lib/errors.js'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FastifyPluginAsync } from 'fastify'
|
||||
import { PaginationSchema } from '@forte/shared/schemas'
|
||||
import { PaginationSchema } from '@lunarfront/shared/schemas'
|
||||
import { VaultKeyService, VaultPermissionService, VaultCategoryService, VaultEntryService } from '../../services/vault.service.js'
|
||||
import { ValidationError } from '../../lib/errors.js'
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ import type {
|
||||
TaxExemptionCreateInput,
|
||||
TaxExemptionUpdateInput,
|
||||
PaginationInput,
|
||||
} from '@forte/shared/schemas'
|
||||
import { isMinor, normalizeStateCode } from '@forte/shared/utils'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import { isMinor, normalizeStateCode } from '@lunarfront/shared/utils'
|
||||
import {
|
||||
withPagination,
|
||||
withSort,
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
SupplierCreateInput,
|
||||
SupplierUpdateInput,
|
||||
PaginationInput,
|
||||
} from '@forte/shared/schemas'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import {
|
||||
withPagination,
|
||||
withSort,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
SYSTEM_UNIT_STATUSES,
|
||||
SYSTEM_ITEM_CONDITIONS,
|
||||
} from '../db/schema/lookups.js'
|
||||
import type { LookupCreateInput, LookupUpdateInput } from '@forte/shared/schemas'
|
||||
import type { LookupCreateInput, LookupUpdateInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
function createLookupService(
|
||||
table: typeof inventoryUnitStatuses | typeof itemConditions,
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
InventoryUnitCreateInput,
|
||||
InventoryUnitUpdateInput,
|
||||
PaginationInput,
|
||||
} from '@forte/shared/schemas'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import {
|
||||
withPagination,
|
||||
withSort,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { eq, and, inArray, count, type Column } from 'drizzle-orm'
|
||||
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
||||
import type { PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginationInput } from '@lunarfront/shared/schemas'
|
||||
import { permissions, roles, rolePermissions, userRoles } from '../db/schema/rbac.js'
|
||||
import { SYSTEM_PERMISSIONS, DEFAULT_ROLES } from '../db/seeds/rbac.js'
|
||||
import { ForbiddenError } from '../lib/errors.js'
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
RepairServiceTemplateCreateInput,
|
||||
RepairServiceTemplateUpdateInput,
|
||||
PaginationInput,
|
||||
} from '@forte/shared/schemas'
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import {
|
||||
withPagination,
|
||||
withSort,
|
||||
@@ -59,7 +59,7 @@ export const RepairTicketService = {
|
||||
locationId: input.locationId,
|
||||
repairBatchId: input.repairBatchId,
|
||||
inventoryUnitId: input.inventoryUnitId,
|
||||
instrumentDescription: input.instrumentDescription,
|
||||
itemDescription: input.itemDescription,
|
||||
serialNumber: input.serialNumber,
|
||||
conditionIn: input.conditionIn,
|
||||
conditionInNotes: input.conditionInNotes,
|
||||
@@ -101,7 +101,7 @@ export const RepairTicketService = {
|
||||
repairTickets.ticketNumber,
|
||||
repairTickets.customerName,
|
||||
repairTickets.customerPhone,
|
||||
repairTickets.instrumentDescription,
|
||||
repairTickets.itemDescription,
|
||||
repairTickets.serialNumber,
|
||||
])
|
||||
if (search) conditions.push(search)
|
||||
@@ -151,7 +151,7 @@ export const RepairTicketService = {
|
||||
async listByBatch(db: PostgresJsDatabase<any>, batchId: string, params: PaginationInput) {
|
||||
const baseWhere = eq(repairTickets.repairBatchId, batchId)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.instrumentDescription])
|
||||
? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.itemDescription])
|
||||
: undefined
|
||||
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
||||
|
||||
@@ -292,7 +292,7 @@ export const RepairBatchService = {
|
||||
contactEmail: input.contactEmail,
|
||||
pickupDate: input.pickupDate ? new Date(input.pickupDate) : undefined,
|
||||
dueDate: input.dueDate ? new Date(input.dueDate) : undefined,
|
||||
instrumentCount: input.instrumentCount,
|
||||
itemCount: input.itemCount,
|
||||
notes: input.notes,
|
||||
})
|
||||
.returning()
|
||||
@@ -391,7 +391,7 @@ export const RepairServiceTemplateService = {
|
||||
.insert(repairServiceTemplates)
|
||||
.values({
|
||||
name: input.name,
|
||||
instrumentType: input.instrumentType,
|
||||
itemCategory: input.itemCategory,
|
||||
size: input.size,
|
||||
description: input.description,
|
||||
itemType: input.itemType,
|
||||
@@ -406,13 +406,13 @@ export const RepairServiceTemplateService = {
|
||||
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
|
||||
const baseWhere = eq(repairServiceTemplates.isActive, true)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [repairServiceTemplates.name, repairServiceTemplates.instrumentType, repairServiceTemplates.size, repairServiceTemplates.description])
|
||||
? buildSearchCondition(params.q, [repairServiceTemplates.name, repairServiceTemplates.itemCategory, repairServiceTemplates.size, repairServiceTemplates.description])
|
||||
: undefined
|
||||
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
name: repairServiceTemplates.name,
|
||||
instrument_type: repairServiceTemplates.instrumentType,
|
||||
item_category: repairServiceTemplates.itemCategory,
|
||||
default_price: repairServiceTemplates.defaultPrice,
|
||||
sort_order: repairServiceTemplates.sortOrder,
|
||||
created_at: repairServiceTemplates.createdAt,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { userRoles } from '../db/schema/rbac.js'
|
||||
import type { StorageProvider } from '../storage/index.js'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.js'
|
||||
import type { PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
const MAX_PARENT_DEPTH = 50
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
||||
import { vaultConfig, vaultCategories, vaultCategoryPermissions, vaultEntries } from '../db/schema/vault.js'
|
||||
import { userRoles } from '../db/schema/rbac.js'
|
||||
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.js'
|
||||
import type { PaginationInput } from '@forte/shared/schemas'
|
||||
import type { PaginationInput } from '@lunarfront/shared/schemas'
|
||||
import { randomBytes, createCipheriv, createDecipheriv, pbkdf2Sync } from 'crypto'
|
||||
import bcrypt from 'bcrypt'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { sql, asc, desc, ilike, or, type SQL, type Column } from 'drizzle-orm'
|
||||
import type { PgSelect } from 'drizzle-orm/pg-core'
|
||||
import type { PaginationInput, PaginatedResponse } from '@forte/shared/schemas'
|
||||
import type { PaginationInput, PaginatedResponse } from '@lunarfront/shared/schemas'
|
||||
|
||||
/**
|
||||
* Apply pagination (offset + limit) to a Drizzle query.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@forte/shared",
|
||||
"name": "@lunarfront/shared",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -36,7 +36,7 @@ export const RepairTicketCreateSchema = z.object({
|
||||
locationId: opt(z.string().uuid()),
|
||||
repairBatchId: opt(z.string().uuid()),
|
||||
inventoryUnitId: opt(z.string().uuid()),
|
||||
instrumentDescription: opt(z.string()),
|
||||
itemDescription: opt(z.string()),
|
||||
serialNumber: opt(z.string().max(255)),
|
||||
conditionIn: opt(RepairConditionIn),
|
||||
conditionInNotes: opt(z.string()),
|
||||
@@ -84,7 +84,7 @@ export const RepairBatchCreateSchema = z.object({
|
||||
contactEmail: opt(z.string().email()),
|
||||
pickupDate: opt(z.string()),
|
||||
dueDate: opt(z.string()),
|
||||
instrumentCount: z.coerce.number().int().min(0).default(0),
|
||||
itemCount: z.coerce.number().int().min(0).default(0),
|
||||
notes: opt(z.string()),
|
||||
})
|
||||
export type RepairBatchCreateInput = z.infer<typeof RepairBatchCreateSchema>
|
||||
@@ -112,7 +112,7 @@ export type RepairNoteCreateInput = z.infer<typeof RepairNoteCreateSchema>
|
||||
|
||||
export const RepairServiceTemplateCreateSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
instrumentType: opt(z.string().max(100)),
|
||||
itemCategory: opt(z.string().max(100)),
|
||||
size: opt(z.string().max(50)),
|
||||
description: opt(z.string()),
|
||||
itemType: RepairLineItemType.default('flat_rate'),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @forte/shared types
|
||||
// @lunarfront/shared types
|
||||
// Domain types will be added as each domain is implemented
|
||||
|
||||
export type StoreId = string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Forte — Music Store Management Platform
|
||||
LunarFront — Small Business Management Platform
|
||||
|
||||
Implementation Roadmap
|
||||
|
||||
@@ -8,23 +8,23 @@ Version 1.0 | Draft
|
||||
|
||||
# 1. Purpose
|
||||
|
||||
This document defines the phased implementation order for Forte — a music store management platform built by Lunarfront Tech LLC. Each phase builds on the previous, produces independently testable output, and is scoped for a solo developer working in 2-4 week increments. The goal is a working POS as early as possible, then layer on domain complexity.
|
||||
This document defines the phased implementation order for LunarFront — a music store management platform built by Lunarfront Tech LLC. Each phase builds on the previous, produces independently testable output, and is scoped for a solo developer working in 2-4 week increments. The goal is a working POS as early as possible, then layer on domain complexity.
|
||||
|
||||
Tech stack: TypeScript / Bun / Fastify / Drizzle ORM / Zod / BullMQ / PostgreSQL 16 / Valkey 8 / Turborepo monorepo. See `17_Backend_Technical_Architecture.md` for full stack details.
|
||||
|
||||
## 1.1 Project Conventions
|
||||
|
||||
Name | Value
|
||||
App name | Forte
|
||||
Package namespace | `@forte/shared`, `@forte/backend`, etc.
|
||||
App name | LunarFront
|
||||
Package namespace | `@lunarfront/shared`, `@lunarfront/backend`, etc.
|
||||
Database (dev) | `forte`
|
||||
Database (test) | `forte_test`
|
||||
Database (test) | `lunarfront_test`
|
||||
Logging | JSON structured logging via Pino (Fastify built-in)
|
||||
Linting | ESLint + Prettier at monorepo root
|
||||
Auth (Phase 1-2) | Dev bypass via `X-Dev-User` header — JWT planned, wired in Phase 2
|
||||
Auth (Phase 2+) | Self-issued JWTs + bcrypt, swap to Clerk/Auth0 later
|
||||
Request tracing | Auto-generated request IDs via Fastify (included from Phase 1)
|
||||
Test strategy | Separate `forte_test` database, reset between test runs
|
||||
Test strategy | Separate `lunarfront_test` database, reset between test runs
|
||||
Dev ports | API: 8000, Postgres: 5432, Valkey: 6379 (all exposed to host for dev tooling)
|
||||
Multi-tenant | `company_id` on all tables for tenant scoping, `location_id` where per-location tracking needed (inventory, transactions, drawer, delivery)
|
||||
|
||||
@@ -74,18 +74,18 @@ Area | Files / Artifacts
|
||||
Root config | `turbo.json`, root `package.json` (workspaces), `tsconfig.base.json`, `.env.example`, `CLAUDE.md`
|
||||
Linting | `.eslintrc.cjs`, `.prettierrc`, root lint/format scripts in Turborepo pipeline
|
||||
Docker | `docker-compose.dev.yml` — PostgreSQL 16 (`forte` database) + Valkey 8, ports exposed to host
|
||||
Shared package | `packages/shared/package.json` (`@forte/shared`), `packages/shared/src/types/index.ts`, `packages/shared/src/schemas/index.ts`, `packages/shared/src/utils/currency.ts`, `packages/shared/src/utils/dates.ts`
|
||||
Backend package | `packages/backend/package.json` (`@forte/backend`), `packages/backend/src/main.ts` (Fastify entry with Pino JSON logging + request ID tracing), `packages/backend/src/plugins/database.ts` (Drizzle connection), `packages/backend/src/plugins/redis.ts`, `packages/backend/src/plugins/error-handler.ts`, `packages/backend/src/plugins/cors.ts`, `packages/backend/src/plugins/dev-auth.ts` (dev bypass via `X-Dev-User` header), `packages/backend/src/routes/v1/health.ts`
|
||||
Shared package | `packages/shared/package.json` (`@lunarfront/shared`), `packages/shared/src/types/index.ts`, `packages/shared/src/schemas/index.ts`, `packages/shared/src/utils/currency.ts`, `packages/shared/src/utils/dates.ts`
|
||||
Backend package | `packages/backend/package.json` (`@lunarfront/backend`), `packages/backend/src/main.ts` (Fastify entry with Pino JSON logging + request ID tracing), `packages/backend/src/plugins/database.ts` (Drizzle connection), `packages/backend/src/plugins/redis.ts`, `packages/backend/src/plugins/error-handler.ts`, `packages/backend/src/plugins/cors.ts`, `packages/backend/src/plugins/dev-auth.ts` (dev bypass via `X-Dev-User` header), `packages/backend/src/routes/v1/health.ts`
|
||||
Database | `packages/backend/src/db/index.ts` (client export), `packages/backend/src/db/schema/companies.ts` (company table — the tenant anchor), `packages/backend/src/db/schema/locations.ts` (location table — physical store locations), `drizzle.config.ts`
|
||||
Seed script | `packages/backend/src/db/seed.ts` — creates a test company + location + admin user for local dev
|
||||
Testing | `vitest.config.ts`, health endpoint integration test, `forte_test` database for test isolation
|
||||
Testing | `vitest.config.ts`, health endpoint integration test, `lunarfront_test` database for test isolation
|
||||
|
||||
### Architecture Decisions Settled
|
||||
|
||||
- `company_id` (tenant) + `location_id` (physical store) scoping pattern established on the first domain tables
|
||||
- Inventory and transaction tables use both `company_id` and `location_id`; other tables use `company_id` only
|
||||
- Drizzle migration workflow (generate → migrate)
|
||||
- Shared package import convention (`@forte/shared`)
|
||||
- Shared package import convention (`@lunarfront/shared`)
|
||||
- Standardized error response format (consistent JSON shape for errors)
|
||||
- Request context pattern (`companyId`, `locationId`, and `user` on request)
|
||||
- JSON structured logging via Pino with request ID on every log line
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Forte — Music Store Management Platform
|
||||
LunarFront — Small Business Management Platform
|
||||
|
||||
Domain Design: Personnel Management
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Forte — Music Store Management Platform
|
||||
LunarFront — Small Business Management Platform
|
||||
|
||||
Domain Design: Consignment
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Forte — Music Store Management Platform
|
||||
LunarFront — Small Business Management Platform
|
||||
|
||||
Domain Design: Sales Commission
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Forte — Music Store Management Platform
|
||||
LunarFront — Small Business Management Platform
|
||||
|
||||
Phase 2 Audit: Code Review, Security, and Feature Gap Analysis
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ Version 1.0 | Draft
|
||||
|
||||
# 1. Overview
|
||||
|
||||
Forte includes a built-in password vault for on-premise deployments. Music stores deal with dozens of credentials — supplier portals, distributor accounts, payment processor dashboards, shipping services, insurance logins, ASCAP/BMI licensing portals — and most store owners are not technical enough to adopt a separate password manager.
|
||||
LunarFront includes a built-in password vault for on-premise deployments. Music stores deal with dozens of credentials — supplier portals, distributor accounts, payment processor dashboards, shipping services, insurance logins, ASCAP/BMI licensing portals — and most store owners are not technical enough to adopt a separate password manager.
|
||||
|
||||
The vault is a simple, encrypted credential store baked into Forte. It encrypts all secrets at rest using AES-256-GCM with a key derived from a master passphrase. The key exists only in server memory and is discarded on restart.
|
||||
The vault is a simple, encrypted credential store baked into LunarFront. It encrypts all secrets at rest using AES-256-GCM with a key derived from a master passphrase. The key exists only in server memory and is discarded on restart.
|
||||
|
||||
**On-premise only.** Cloud/hosted deployments would require a more sophisticated key management approach (HSM, envelope encryption) and are out of scope for this design.
|
||||
|
||||
@@ -256,7 +256,7 @@ This is an expensive operation (scales with entry count) but should be rare.
|
||||
After restart:
|
||||
- Vault status: locked
|
||||
- All vault API calls: 423
|
||||
- Everything else in Forte: fully functional
|
||||
- Everything else in LunarFront: fully functional
|
||||
- Admin unlocks when ready — no urgency, no data loss
|
||||
|
||||
## 7.5 Multiple Servers
|
||||
|
||||
Reference in New Issue
Block a user