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:
Ryan Moon
2026-03-30 08:51:54 -05:00
parent 535446696c
commit 9400828f62
84 changed files with 390 additions and 820 deletions

View File

@@ -1,9 +1,9 @@
# Forte — Environment Variables # LunarFront — Environment Variables
# Copy to .env and adjust values for your setup. # Copy to .env and adjust values for your setup.
# Docker Compose overrides host values (postgres, valkey) automatically. # Docker Compose overrides host values (postgres, valkey) automatically.
# Database (PostgreSQL 16) # Database (PostgreSQL 16)
DATABASE_URL=postgresql://forte:forte@localhost:5432/forte DATABASE_URL=postgresql://lunarfront:lunarfront@localhost:5432/lunarfront
# Valkey / Redis # Valkey / Redis
REDIS_URL=redis://localhost:6379 REDIS_URL=redis://localhost:6379
@@ -20,7 +20,7 @@ NODE_ENV=development
# Logging (optional) # Logging (optional)
# LOG_LEVEL=info # LOG_LEVEL=info
# LOG_FILE=./logs/forte.log # LOG_FILE=./logs/lunarfront.log
# File Storage (optional — defaults to local) # File Storage (optional — defaults to local)
# STORAGE_PROVIDER=local # STORAGE_PROVIDER=local

View File

@@ -1,8 +1,8 @@
# Forte — Project Conventions # LunarFront — Project Conventions
## App ## App
- **Name:** Forte - **Name:** LunarFront
- **Purpose:** Music store management platform (POS, inventory, rentals, lessons, repairs, accounting) - **Purpose:** Small business management platform (POS, inventory, rentals, scheduling, repairs, accounting)
- **Company:** Lunarfront Tech LLC - **Company:** Lunarfront Tech LLC
## Tech Stack ## Tech Stack
@@ -18,12 +18,12 @@
- **Linting:** ESLint 9 flat config + Prettier - **Linting:** ESLint 9 flat config + Prettier
## Package Namespace ## Package Namespace
- `@forte/shared` — types, Zod schemas, business logic, utils - `@lunarfront/shared` — types, Zod schemas, business logic, utils
- `@forte/backend` — Fastify API server - `@lunarfront/backend` — Fastify API server
## Database ## Database
- Dev: `forte` on localhost:5432 - Dev: `lunarfront` on localhost:5432
- Test: `forte_test` on localhost:5432 - Test: `lunarfront_test` on localhost:5432
- Multi-tenant: `company_id` (uuid FK) on all domain tables for tenant isolation - 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) - `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`) - 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 - `?sort=name&order=asc` — sorting by field name, asc or desc
- List responses always return `{ data: [...], pagination: { page, limit, total, totalPages } }` - List responses always return `{ data: [...], pagination: { page, limit, total, totalPages } }`
- Search and filtering is ALWAYS server-side, never client-side - 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` - 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. - **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 ## Conventions
- Shared Zod schemas are the single source of truth for validation (used on both frontend and backend) - 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 - API routes are thin — validate with Zod, call a service, return result
- All financial events must be auditable (append-only audit records) - All financial events must be auditable (append-only audit records)
- JSON structured logging with request IDs on every log line - JSON structured logging with request IDs on every log line

View File

@@ -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). Built by [Lunarfront Tech LLC](https://lunarfront.com).

104
bun.lock
View File

@@ -3,7 +3,7 @@
"configVersion": 1, "configVersion": 1,
"workspaces": { "workspaces": {
"": { "": {
"name": "forte", "name": "lunarfront",
"dependencies": { "dependencies": {
"zod": "^4.3.6", "zod": "^4.3.6",
}, },
@@ -16,11 +16,11 @@
}, },
}, },
"packages/admin": { "packages/admin": {
"name": "@forte/admin", "name": "@lunarfront/admin",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@forte/shared": "workspace:*",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@lunarfront/shared": "workspace:*",
"@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
@@ -55,14 +55,14 @@
}, },
}, },
"packages/backend": { "packages/backend": {
"name": "@forte/backend", "name": "@lunarfront/backend",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@fastify/cors": "^10", "@fastify/cors": "^10",
"@fastify/jwt": "^9", "@fastify/jwt": "^9",
"@fastify/multipart": "^9.4.0", "@fastify/multipart": "^9.4.0",
"@fastify/rate-limit": "^10.3.0", "@fastify/rate-limit": "^10.3.0",
"@forte/shared": "workspace:*", "@lunarfront/shared": "workspace:*",
"bcrypt": "^6", "bcrypt": "^6",
"drizzle-orm": "^0.38", "drizzle-orm": "^0.38",
"fastify": "^5", "fastify": "^5",
@@ -80,7 +80,7 @@
}, },
}, },
"packages/shared": { "packages/shared": {
"name": "@forte/shared", "name": "@lunarfront/shared",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"zod": "^4", "zod": "^4",
@@ -243,12 +243,6 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], "@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=="], "@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=="], "@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=="], "@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=="], "@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], "@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=="], "@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=="], "@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-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/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=="], "@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=="], "@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=="], "@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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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/@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=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],

View File

@@ -3,7 +3,7 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile.dev dockerfile: Dockerfile.dev
container_name: forte-api container_name: lunarfront-api
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8000:8000" - "8000:8000"
@@ -14,7 +14,7 @@ services:
- /app/packages/backend/node_modules - /app/packages/backend/node_modules
- /app/packages/shared/node_modules - /app/packages/shared/node_modules
environment: environment:
DATABASE_URL: postgresql://forte:forte@postgres:5432/forte DATABASE_URL: postgresql://lunarfront:lunarfront@postgres:5432/lunarfront
REDIS_URL: redis://valkey:6379 REDIS_URL: redis://valkey:6379
JWT_SECRET: dev-secret-do-not-use-in-production JWT_SECRET: dev-secret-do-not-use-in-production
NODE_ENV: development NODE_ENV: development
@@ -28,30 +28,30 @@ services:
postgres: postgres:
image: postgres:16 image: postgres:16
container_name: forte-postgres container_name: lunarfront-postgres
restart: unless-stopped restart: unless-stopped
environment: environment:
POSTGRES_USER: forte POSTGRES_USER: lunarfront
POSTGRES_PASSWORD: forte POSTGRES_PASSWORD: lunarfront
POSTGRES_DB: forte POSTGRES_DB: lunarfront
ports: ports:
- "5432:5432" - "5432:5432"
volumes: volumes:
- forte-pgdata:/var/lib/postgresql/data - lunarfront-pgdata:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U forte"] test: ["CMD-SHELL", "pg_isready -U lunarfront"]
interval: 5s interval: 5s
timeout: 3s timeout: 3s
retries: 5 retries: 5
valkey: valkey:
image: valkey/valkey:8 image: valkey/valkey:8
container_name: forte-valkey container_name: lunarfront-valkey
restart: unless-stopped restart: unless-stopped
ports: ports:
- "6379:6379" - "6379:6379"
volumes: volumes:
- forte-valkey:/data - lunarfront-valkey:/data
healthcheck: healthcheck:
test: ["CMD", "valkey-cli", "ping"] test: ["CMD", "valkey-cli", "ping"]
interval: 5s interval: 5s
@@ -59,5 +59,5 @@ services:
retries: 5 retries: 5
volumes: volumes:
forte-pgdata: lunarfront-pgdata:
forte-valkey: lunarfront-valkey:

View File

@@ -3,16 +3,16 @@
## Monorepo Structure ## Monorepo Structure
``` ```
forte/ lunarfront/
packages/ packages/
shared/ @forte/shared — Zod schemas, types, business logic, utils shared/ @lunarfront/shared — Zod schemas, types, business logic, utils
backend/ @forte/backend — Fastify API server backend/ @lunarfront/backend — Fastify API server
admin/ @forte/admin — Admin UI (React + Vite) admin/ @lunarfront/admin — Admin UI (React + Vite)
planning/ Domain planning docs (01-26) planning/ Domain planning docs (01-26)
docs/ Technical documentation 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 ## Backend

View File

@@ -6,8 +6,8 @@ PostgreSQL 16. Two databases:
| Database | Port | Usage | | Database | Port | Usage |
|----------|------|-------| |----------|------|-------|
| `forte` | 5432 | Development | | `lunarfront` | 5432 | Development |
| `forte_api_test` | 5432 | API integration tests (auto-created by test runner) | | `lunarfront_api_test` | 5432 | API integration tests (auto-created by test runner) |
## Migrations ## Migrations
@@ -35,7 +35,7 @@ All domain tables include `company_id` (uuid FK to `company`). Every query filte
| Table | Description | | Table | Description |
|-------|-------------| |-------|-------------|
| `company` | Tenant (music store business) | | `company` | Tenant (tenant business) |
| `location` | Physical store location | | `location` | Physical store location |
| `user` | Staff/admin user account | | `user` | Staff/admin user account |

View File

@@ -9,7 +9,7 @@
## Installation ## Installation
```bash ```bash
git clone <repo-url> && cd forte git clone <repo-url> && cd lunarfront
bun install bun install
``` ```
@@ -18,7 +18,7 @@ bun install
Create a `.env` file in the project root: Create a `.env` file in the project root:
```env ```env
DATABASE_URL=postgresql://forte:forte@localhost:5432/forte DATABASE_URL=postgresql://lunarfront:lunarfront@localhost:5432/lunarfront
REDIS_URL=redis://localhost:6379 REDIS_URL=redis://localhost:6379
JWT_SECRET=your-secret-here JWT_SECRET=your-secret-here
NODE_ENV=development NODE_ENV=development
@@ -28,7 +28,7 @@ NODE_ENV=development
| Variable | Default | Description | | 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 | | `REDIS_URL` | `redis://localhost:6379` | Valkey/Redis connection string |
| `JWT_SECRET` | (auto-generated in dev) | Secret for signing JWTs. **Required in production.** | | `JWT_SECRET` | (auto-generated in dev) | Secret for signing JWTs. **Required in production.** |
| `PORT` | `8000` | Backend API port | | `PORT` | `8000` | Backend API port |
@@ -45,7 +45,7 @@ NODE_ENV=development
```bash ```bash
# Create the database # Create the database
createdb forte createdb lunarfront
# Run migrations # Run migrations
cd packages/backend cd packages/backend

View File

@@ -4,7 +4,7 @@
The primary test suite lives at `packages/backend/api-tests/`. It uses a custom runner that: 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 2. Seeds company, lookup tables, RBAC permissions/roles
3. Starts the backend on port 8001 3. Starts the backend on port 8001
4. Registers a test user with admin role 4. Registers a test user with admin role

View File

@@ -1,6 +1,7 @@
{ {
"name": "forte", "name": "lunarfront",
"private": true, "private": true,
"packageManager": "bun@1.3.11",
"workspaces": ["packages/*"], "workspaces": ["packages/*"],
"scripts": { "scripts": {
"dev": "turbo run dev", "dev": "turbo run dev",

View File

@@ -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>()

View File

@@ -3,13 +3,13 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Forte Admin</title> <title>LunarFront Admin</title>
</head> </head>
<body> <body>
<script> <script>
// Apply mode before React renders to prevent flash // Apply mode before React renders to prevent flash
(function() { (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); var isDark = mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) document.documentElement.classList.add('dark'); if (isDark) document.documentElement.classList.add('dark');
})(); })();

View File

@@ -1,5 +1,5 @@
{ {
"name": "@forte/admin", "name": "@lunarfront/admin",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -10,7 +10,7 @@
"lint": "eslint ." "lint": "eslint ."
}, },
"dependencies": { "dependencies": {
"@forte/shared": "workspace:*", "@lunarfront/shared": "workspace:*",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15",

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query' import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client' import { api } from '@/lib/api-client'
import type { Account } from '@/types/account' 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 = { export const accountKeys = {
all: ['accounts'] as const, all: ['accounts'] as const,

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query' import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client' import { api } from '@/lib/api-client'
import type { MemberIdentifier } from '@/types/account' import type { MemberIdentifier } from '@/types/account'
import type { PaginatedResponse } from '@forte/shared/schemas' import type { PaginatedResponse } from '@lunarfront/shared/schemas'
export const identifierKeys = { export const identifierKeys = {
all: (memberId: string) => ['members', memberId, 'identifiers'] as const, all: (memberId: string) => ['members', memberId, 'identifiers'] as const,

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query' import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client' import { api } from '@/lib/api-client'
import type { Member } from '@/types/account' 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 { interface MemberWithAccount extends Member {
accountName: string | null accountName: string | null

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query' import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client' import { api } from '@/lib/api-client'
import type { PaymentMethod } from '@/types/account' 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 = { export const paymentMethodKeys = {
all: (accountId: string) => ['accounts', accountId, 'payment-methods'] as const, all: (accountId: string) => ['accounts', accountId, 'payment-methods'] as const,

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query' import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client' import { api } from '@/lib/api-client'
import type { ProcessorLink } from '@/types/account' 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 = { export const processorLinkKeys = {
all: (accountId: string) => ['accounts', accountId, 'processor-links'] as const, all: (accountId: string) => ['accounts', accountId, 'processor-links'] as const,

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query' import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client' import { api } from '@/lib/api-client'
import type { Permission, Role } from '@/types/rbac' 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 = { export const rbacKeys = {
permissions: ['permissions'] as const, permissions: ['permissions'] as const,

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query' import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client' import { api } from '@/lib/api-client'
import type { RepairTicket, RepairLineItem, RepairBatch, RepairNote, RepairServiceTemplate } from '@/types/repair' 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 --- // --- Repair Tickets ---

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query' import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client' import { api } from '@/lib/api-client'
import type { StorageFolder, StorageFolderPermission, StorageFile } from '@/types/storage' 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 --- // --- Folders ---

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query' import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client' import { api } from '@/lib/api-client'
import type { TaxExemption } from '@/types/account' 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 = { export const taxExemptionKeys = {
all: (accountId: string) => ['accounts', accountId, 'tax-exemptions'] as const, all: (accountId: string) => ['accounts', accountId, 'tax-exemptions'] as const,

View File

@@ -1,6 +1,6 @@
import { queryOptions } from '@tanstack/react-query' import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client' 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 { export interface UserRole {
id: string id: string

View File

@@ -1,7 +1,7 @@
import { queryOptions } from '@tanstack/react-query' import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client' import { api } from '@/lib/api-client'
import type { VaultStatus, VaultCategory, VaultCategoryPermission, VaultEntry } from '@/types/vault' 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 --- // --- Keys ---

View File

@@ -1,7 +1,7 @@
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod' import { z } from 'zod'
import { AccountCreateSchema } from '@forte/shared/schemas' import { AccountCreateSchema } from '@lunarfront/shared/schemas'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'

View File

@@ -1,7 +1,7 @@
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { PaymentMethodCreateSchema } from '@forte/shared/schemas' import { PaymentMethodCreateSchema } from '@lunarfront/shared/schemas'
import type { PaymentMethodCreateInput } from '@forte/shared/schemas' import type { PaymentMethodCreateInput } from '@lunarfront/shared/schemas'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'

View File

@@ -1,7 +1,7 @@
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { ProcessorLinkCreateSchema } from '@forte/shared/schemas' import { ProcessorLinkCreateSchema } from '@lunarfront/shared/schemas'
import type { ProcessorLinkCreateInput } from '@forte/shared/schemas' import type { ProcessorLinkCreateInput } from '@lunarfront/shared/schemas'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'

View File

@@ -1,7 +1,7 @@
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { TaxExemptionCreateSchema } from '@forte/shared/schemas' import { TaxExemptionCreateSchema } from '@lunarfront/shared/schemas'
import type { TaxExemptionCreateInput } from '@forte/shared/schemas' import type { TaxExemptionCreateInput } from '@lunarfront/shared/schemas'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'

View File

@@ -24,7 +24,7 @@ interface GeneratePdfOptions {
companyName?: string 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() const doc = new jsPDF()
let y = 20 let y = 20
@@ -57,11 +57,11 @@ export function generateRepairPdf({ ticket, lineItems, notes, includeNotes, comp
doc.setFontSize(10) doc.setFontSize(10)
doc.setFont('helvetica', 'bold') doc.setFont('helvetica', 'bold')
doc.text('Customer', 14, y) doc.text('Customer', 14, y)
doc.text('Instrument', 110, y) doc.text('Item', 110, y)
y += 5 y += 5
doc.setFont('helvetica', 'normal') doc.setFont('helvetica', 'normal')
doc.text(ticket.customerName, 14, y) doc.text(ticket.customerName, 14, y)
doc.text(ticket.instrumentDescription ?? '-', 110, y) doc.text(ticket.itemDescription ?? '-', 110, y)
y += 5 y += 5
if (ticket.customerPhone) { doc.text(ticket.customerPhone, 14, 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) } if (ticket.serialNumber) { doc.text(`S/N: ${ticket.serialNumber}`, 110, y - 5) }

View File

@@ -1,5 +1,5 @@
import { useNavigate, useSearch } from '@tanstack/react-router' import { useNavigate, useSearch } from '@tanstack/react-router'
import type { PaginationInput } from '@forte/shared/schemas' import type { PaginationInput } from '@lunarfront/shared/schemas'
interface PaginationSearch { interface PaginationSearch {
page?: number page?: number

View File

@@ -16,6 +16,7 @@ import { Route as AuthenticatedUsersRouteImport } from './routes/_authenticated/
import { Route as AuthenticatedSettingsRouteImport } from './routes/_authenticated/settings' import { Route as AuthenticatedSettingsRouteImport } from './routes/_authenticated/settings'
import { Route as AuthenticatedProfileRouteImport } from './routes/_authenticated/profile' import { Route as AuthenticatedProfileRouteImport } from './routes/_authenticated/profile'
import { Route as AuthenticatedHelpRouteImport } from './routes/_authenticated/help' 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 AuthenticatedRolesIndexRouteImport } from './routes/_authenticated/roles/index'
import { Route as AuthenticatedRepairsIndexRouteImport } from './routes/_authenticated/repairs/index' import { Route as AuthenticatedRepairsIndexRouteImport } from './routes/_authenticated/repairs/index'
import { Route as AuthenticatedRepairBatchesIndexRouteImport } from './routes/_authenticated/repair-batches/index' import { Route as AuthenticatedRepairBatchesIndexRouteImport } from './routes/_authenticated/repair-batches/index'
@@ -72,6 +73,11 @@ const AuthenticatedHelpRoute = AuthenticatedHelpRouteImport.update({
path: '/help', path: '/help',
getParentRoute: () => AuthenticatedRoute, getParentRoute: () => AuthenticatedRoute,
} as any) } as any)
const AuthenticatedVaultIndexRoute = AuthenticatedVaultIndexRouteImport.update({
id: '/vault/',
path: '/vault/',
getParentRoute: () => AuthenticatedRoute,
} as any)
const AuthenticatedRolesIndexRoute = AuthenticatedRolesIndexRouteImport.update({ const AuthenticatedRolesIndexRoute = AuthenticatedRolesIndexRouteImport.update({
id: '/roles/', id: '/roles/',
path: '/roles/', path: '/roles/',
@@ -218,6 +224,7 @@ export interface FileRoutesByFullPath {
'/repair-batches/': typeof AuthenticatedRepairBatchesIndexRoute '/repair-batches/': typeof AuthenticatedRepairBatchesIndexRoute
'/repairs/': typeof AuthenticatedRepairsIndexRoute '/repairs/': typeof AuthenticatedRepairsIndexRoute
'/roles/': typeof AuthenticatedRolesIndexRoute '/roles/': typeof AuthenticatedRolesIndexRoute
'/vault/': typeof AuthenticatedVaultIndexRoute
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute '/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute '/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute '/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
@@ -246,6 +253,7 @@ export interface FileRoutesByTo {
'/repair-batches': typeof AuthenticatedRepairBatchesIndexRoute '/repair-batches': typeof AuthenticatedRepairBatchesIndexRoute
'/repairs': typeof AuthenticatedRepairsIndexRoute '/repairs': typeof AuthenticatedRepairsIndexRoute
'/roles': typeof AuthenticatedRolesIndexRoute '/roles': typeof AuthenticatedRolesIndexRoute
'/vault': typeof AuthenticatedVaultIndexRoute
'/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute '/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
'/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute '/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
'/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute '/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
@@ -277,6 +285,7 @@ export interface FileRoutesById {
'/_authenticated/repair-batches/': typeof AuthenticatedRepairBatchesIndexRoute '/_authenticated/repair-batches/': typeof AuthenticatedRepairBatchesIndexRoute
'/_authenticated/repairs/': typeof AuthenticatedRepairsIndexRoute '/_authenticated/repairs/': typeof AuthenticatedRepairsIndexRoute
'/_authenticated/roles/': typeof AuthenticatedRolesIndexRoute '/_authenticated/roles/': typeof AuthenticatedRolesIndexRoute
'/_authenticated/vault/': typeof AuthenticatedVaultIndexRoute
'/_authenticated/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute '/_authenticated/accounts/$accountId/members': typeof AuthenticatedAccountsAccountIdMembersRoute
'/_authenticated/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute '/_authenticated/accounts/$accountId/payment-methods': typeof AuthenticatedAccountsAccountIdPaymentMethodsRoute
'/_authenticated/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute '/_authenticated/accounts/$accountId/processor-links': typeof AuthenticatedAccountsAccountIdProcessorLinksRoute
@@ -308,6 +317,7 @@ export interface FileRouteTypes {
| '/repair-batches/' | '/repair-batches/'
| '/repairs/' | '/repairs/'
| '/roles/' | '/roles/'
| '/vault/'
| '/accounts/$accountId/members' | '/accounts/$accountId/members'
| '/accounts/$accountId/payment-methods' | '/accounts/$accountId/payment-methods'
| '/accounts/$accountId/processor-links' | '/accounts/$accountId/processor-links'
@@ -336,6 +346,7 @@ export interface FileRouteTypes {
| '/repair-batches' | '/repair-batches'
| '/repairs' | '/repairs'
| '/roles' | '/roles'
| '/vault'
| '/accounts/$accountId/members' | '/accounts/$accountId/members'
| '/accounts/$accountId/payment-methods' | '/accounts/$accountId/payment-methods'
| '/accounts/$accountId/processor-links' | '/accounts/$accountId/processor-links'
@@ -366,6 +377,7 @@ export interface FileRouteTypes {
| '/_authenticated/repair-batches/' | '/_authenticated/repair-batches/'
| '/_authenticated/repairs/' | '/_authenticated/repairs/'
| '/_authenticated/roles/' | '/_authenticated/roles/'
| '/_authenticated/vault/'
| '/_authenticated/accounts/$accountId/members' | '/_authenticated/accounts/$accountId/members'
| '/_authenticated/accounts/$accountId/payment-methods' | '/_authenticated/accounts/$accountId/payment-methods'
| '/_authenticated/accounts/$accountId/processor-links' | '/_authenticated/accounts/$accountId/processor-links'
@@ -429,6 +441,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedHelpRouteImport preLoaderRoute: typeof AuthenticatedHelpRouteImport
parentRoute: typeof AuthenticatedRoute parentRoute: typeof AuthenticatedRoute
} }
'/_authenticated/vault/': {
id: '/_authenticated/vault/'
path: '/vault'
fullPath: '/vault/'
preLoaderRoute: typeof AuthenticatedVaultIndexRouteImport
parentRoute: typeof AuthenticatedRoute
}
'/_authenticated/roles/': { '/_authenticated/roles/': {
id: '/_authenticated/roles/' id: '/_authenticated/roles/'
path: '/roles' path: '/roles'
@@ -628,6 +647,7 @@ interface AuthenticatedRouteChildren {
AuthenticatedRepairBatchesIndexRoute: typeof AuthenticatedRepairBatchesIndexRoute AuthenticatedRepairBatchesIndexRoute: typeof AuthenticatedRepairBatchesIndexRoute
AuthenticatedRepairsIndexRoute: typeof AuthenticatedRepairsIndexRoute AuthenticatedRepairsIndexRoute: typeof AuthenticatedRepairsIndexRoute
AuthenticatedRolesIndexRoute: typeof AuthenticatedRolesIndexRoute AuthenticatedRolesIndexRoute: typeof AuthenticatedRolesIndexRoute
AuthenticatedVaultIndexRoute: typeof AuthenticatedVaultIndexRoute
} }
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
@@ -654,6 +674,7 @@ const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
AuthenticatedRepairBatchesIndexRoute: AuthenticatedRepairBatchesIndexRoute, AuthenticatedRepairBatchesIndexRoute: AuthenticatedRepairBatchesIndexRoute,
AuthenticatedRepairsIndexRoute: AuthenticatedRepairsIndexRoute, AuthenticatedRepairsIndexRoute: AuthenticatedRepairsIndexRoute,
AuthenticatedRolesIndexRoute: AuthenticatedRolesIndexRoute, AuthenticatedRolesIndexRoute: AuthenticatedRolesIndexRoute,
AuthenticatedVaultIndexRoute: AuthenticatedVaultIndexRoute,
} }
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren( const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(

View File

@@ -34,7 +34,7 @@ const STATUS_LABELS: Record<string, string> = {
const ticketColumns: Column<RepairTicket>[] = [ const ticketColumns: Column<RepairTicket>[] = [
{ key: 'ticket_number', header: 'Ticket #', sortable: true, render: (t) => <span className="font-mono text-sm">{t.ticketNumber}</span> }, { 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: '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> }, { 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.setFontSize(18)
doc.setFont('helvetica', 'bold') doc.setFont('helvetica', 'bold')
doc.text('Forte Music', 14, y) doc.text('LunarFront', 14, y)
y += 8 y += 8
doc.setFontSize(12) doc.setFontSize(12)
doc.setFont('helvetica', 'normal') doc.setFont('helvetica', 'normal')
@@ -169,7 +169,7 @@ function RepairBatchDetailPage() {
doc.setFillColor(245, 245, 245) doc.setFillColor(245, 245, 245)
doc.rect(14, y - 3, 182, 6, 'F') doc.rect(14, y - 3, 182, 6, 'F')
doc.text('Ticket #', 16, y) doc.text('Ticket #', 16, y)
doc.text('Instrument', 40, y) doc.text('Item', 40, y)
doc.text('Problem', 100, y) doc.text('Problem', 100, y)
doc.text('Status', 155, y) doc.text('Status', 155, y)
doc.text('Estimate', 190, y, { align: 'right' }) doc.text('Estimate', 190, y, { align: 'right' })
@@ -179,7 +179,7 @@ function RepairBatchDetailPage() {
for (const ticket of tickets) { for (const ticket of tickets) {
if (y > 270) { doc.addPage(); y = 20 } if (y > 270) { doc.addPage(); y = 20 }
doc.text(ticket.ticketNumber ?? '-', 16, y) 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(ticket.problemDescription.slice(0, 28), 100, y)
doc.text(STATUS_LABELS[ticket.status] ?? ticket.status, 155, y) doc.text(STATUS_LABELS[ticket.status] ?? ticket.status, 155, y)
doc.text(ticket.estimatedCost ? `$${ticket.estimatedCost}` : '-', 190, y, { align: 'right' }) doc.text(ticket.estimatedCost ? `$${ticket.estimatedCost}` : '-', 190, y, { align: 'right' })

View File

@@ -49,9 +49,9 @@ const columns: Column<RepairBatch>[] = [
}, },
}, },
{ {
key: 'instruments', key: 'items',
header: 'Instruments', header: 'Items',
render: (b) => <>{b.receivedCount}/{b.instrumentCount}</>, render: (b) => <>{b.receivedCount}/{b.itemCount}</>,
}, },
{ {
key: 'due_date', key: 'due_date',

View File

@@ -3,7 +3,7 @@ import { createFileRoute, useNavigate, Link } from '@tanstack/react-router'
import { useQuery, useMutation } from '@tanstack/react-query' import { useQuery, useMutation } from '@tanstack/react-query'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { RepairBatchCreateSchema } from '@forte/shared/schemas' import { RepairBatchCreateSchema } from '@lunarfront/shared/schemas'
import { repairBatchMutations } from '@/api/repairs' import { repairBatchMutations } from '@/api/repairs'
import { accountListOptions } from '@/api/accounts' import { accountListOptions } from '@/api/accounts'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -167,7 +167,7 @@ function NewRepairBatchPage() {
<div className="space-y-2"> <div className="space-y-2">
<Label>Notes</Label> <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> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -118,7 +118,7 @@ function RepairTicketDetailPage() {
setEditFields({ setEditFields({
customerName: ticket!.customerName, customerName: ticket!.customerName,
customerPhone: ticket!.customerPhone ?? '', customerPhone: ticket!.customerPhone ?? '',
instrumentDescription: ticket!.instrumentDescription ?? '', itemDescription: ticket!.itemDescription ?? '',
serialNumber: ticket!.serialNumber ?? '', serialNumber: ticket!.serialNumber ?? '',
conditionIn: ticket!.conditionIn ?? '', conditionIn: ticket!.conditionIn ?? '',
conditionInNotes: ticket!.conditionInNotes ?? '', conditionInNotes: ticket!.conditionInNotes ?? '',
@@ -134,7 +134,7 @@ function RepairTicketDetailPage() {
const data: Record<string, unknown> = {} const data: Record<string, unknown> = {}
if (editFields.customerName !== ticket!.customerName) data.customerName = editFields.customerName if (editFields.customerName !== ticket!.customerName) data.customerName = editFields.customerName
if (editFields.customerPhone !== (ticket!.customerPhone ?? '')) data.customerPhone = editFields.customerPhone || undefined 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.serialNumber !== (ticket!.serialNumber ?? '')) data.serialNumber = editFields.serialNumber || undefined
if (editFields.conditionIn !== (ticket!.conditionIn ?? '')) data.conditionIn = editFields.conditionIn || undefined if (editFields.conditionIn !== (ticket!.conditionIn ?? '')) data.conditionIn = editFields.conditionIn || undefined
if (editFields.conditionInNotes !== (ticket!.conditionInNotes ?? '')) data.conditionInNotes = editFields.conditionInNotes || undefined if (editFields.conditionInNotes !== (ticket!.conditionInNotes ?? '')) data.conditionInNotes = editFields.conditionInNotes || undefined
@@ -180,7 +180,7 @@ function RepairTicketDetailPage() {
</Button> </Button>
<div className="flex-1"> <div className="flex-1">
<h1 className="text-2xl font-bold">Ticket #{ticket.ticketNumber}</h1> <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> </div>
<PdfModal ticket={ticket} lineItems={lineItemsData?.data ?? []} ticketId={ticketId} /> <PdfModal ticket={ticket} lineItems={lineItemsData?.data ?? []} ticketId={ticketId} />
</div> </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 className="space-y-2"><Label>Phone</Label><Input value={editFields.customerPhone} onChange={(e) => setEditFields((p) => ({ ...p, customerPhone: e.target.value }))} /></div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <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 className="space-y-2"><Label>Serial Number</Label><Input value={editFields.serialNumber} onChange={(e) => setEditFields((p) => ({ ...p, serialNumber: e.target.value }))} /></div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <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><span className="text-muted-foreground">Account:</span> {ticket.accountId ?? 'Walk-in'}</div>
</div> </div>
<div className="space-y-2 text-sm"> <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">Serial:</span> {ticket.serialNumber ?? '-'}</div>
<div><span className="text-muted-foreground">Condition:</span> {ticket.conditionIn ?? '-'}</div> <div><span className="text-muted-foreground">Condition:</span> {ticket.conditionIn ?? '-'}</div>
</div> </div>
@@ -411,8 +411,8 @@ function AddLineItemDialog({ ticketId, open, onOpenChange }: { ticketId: string;
setDescription(''); setQty('1'); setUnitPrice('0'); setCost(''); setItemType('labor'); setTemplateSearch(''); setShowTemplates(false) 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 }) { function selectTemplate(template: { name: string; itemCategory: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) {
const desc = [template.name, template.instrumentType, template.size].filter(Boolean).join(' — ') 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('') 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"> <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) => ( {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)}> <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> <span className="text-muted-foreground">${t.defaultPrice}</span>
</button> </button>
))} ))}

View File

@@ -11,7 +11,7 @@ import { Badge } from '@/components/ui/badge'
import { Plus, Search } from 'lucide-react' import { Plus, Search } from 'lucide-react'
import { useAuthStore } from '@/stores/auth.store' import { useAuthStore } from '@/stores/auth.store'
import type { RepairTicket } from '@/types/repair' 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/')({ export const Route = createFileRoute('/_authenticated/repairs/')({
validateSearch: (search: Record<string, unknown>) => ({ validateSearch: (search: Record<string, unknown>) => ({
@@ -70,9 +70,9 @@ const columns: Column<RepairTicket>[] = [
render: (t) => <span className="font-medium">{t.customerName}</span>, render: (t) => <span className="font-medium">{t.customerName}</span>,
}, },
{ {
key: 'instrument', key: 'item_description',
header: 'Instrument', header: 'Item',
render: (t) => <>{t.instrumentDescription ?? '-'}</>, render: (t) => <>{t.itemDescription ?? '-'}</>,
}, },
{ {
key: 'status', key: 'status',

View File

@@ -3,7 +3,7 @@ import { createFileRoute, useNavigate, Link } from '@tanstack/react-router'
import { useQuery, useMutation } from '@tanstack/react-query' import { useQuery, useMutation } from '@tanstack/react-query'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' 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 { repairTicketMutations, repairLineItemMutations, repairServiceTemplateListOptions } from '@/api/repairs'
import { accountListOptions } from '@/api/accounts' import { accountListOptions } from '@/api/accounts'
import { useAuthStore } from '@/stores/auth.store' import { useAuthStore } from '@/stores/auth.store'
@@ -88,7 +88,7 @@ function NewRepairPage() {
defaultValues: { defaultValues: {
customerName: linkedContactName ?? '', customerName: linkedContactName ?? '',
customerPhone: '', customerPhone: '',
instrumentDescription: '', itemDescription: '',
serialNumber: '', serialNumber: '',
problemDescription: '', problemDescription: '',
conditionIn: undefined, conditionIn: undefined,
@@ -157,8 +157,8 @@ function NewRepairPage() {
setValue('customerPhone', '') setValue('customerPhone', '')
} }
function addFromTemplate(template: { name: string; instrumentType: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) { function addFromTemplate(template: { name: string; itemCategory: string | null; size: string | null; itemType: string; defaultPrice: string; defaultCost: string | null }) {
const desc = [template.name, template.instrumentType, template.size].filter(Boolean).join(' — ') const desc = [template.name, template.itemCategory, template.size].filter(Boolean).join(' — ')
setLineItems((prev) => [...prev, { setLineItems((prev) => [...prev, {
itemType: template.itemType, itemType: template.itemType,
description: desc, description: desc,
@@ -297,14 +297,14 @@ function NewRepairPage() {
</CardContent> </CardContent>
</Card> </Card>
{/* Instrument Section */} {/* Item Section */}
<Card> <Card>
<CardHeader><CardTitle className="text-lg">Instrument</CardTitle></CardHeader> <CardHeader><CardTitle className="text-lg">Item</CardTitle></CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>Instrument Description</Label> <Label>Item Description</Label>
<Input {...register('instrumentDescription')} placeholder="e.g. Yamaha Trumpet YTR-2330" /> <Input {...register('itemDescription')} placeholder="e.g. Brand, model, description" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Serial Number</Label> <Label>Serial Number</Label>
@@ -364,7 +364,7 @@ function NewRepairPage() {
) : ( ) : (
templates.map((t) => ( 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)}> <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> <span className="text-muted-foreground">${t.defaultPrice}</span>
</button> </button>
)) ))
@@ -462,7 +462,7 @@ function NewRepairPage() {
<Card> <Card>
<CardHeader><CardTitle className="text-lg">Intake Photos</CardTitle></CardHeader> <CardHeader><CardTitle className="text-lg">Intake Photos</CardTitle></CardHeader>
<CardContent className="space-y-4"> <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"> <div className="flex flex-wrap gap-3">
{photos.map((photo, i) => ( {photos.map((photo, i) => (
<div key={i} className="relative group"> <div key={i} className="relative group">

View File

@@ -28,7 +28,7 @@ export const Route = createFileRoute('/_authenticated/repairs/templates')({
const columns: Column<RepairServiceTemplate>[] = [ const columns: Column<RepairServiceTemplate>[] = [
{ key: 'name', header: 'Name', sortable: true, render: (t) => <span className="font-medium">{t.name}</span> }, { 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: 'size', header: 'Size', render: (t) => <>{t.size ?? '-'}</> },
{ key: 'item_type', header: 'Type', render: (t) => <Badge variant="outline">{t.itemType.replace('_', ' ')}</Badge> }, { 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}</> }, { 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 }) { function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [name, setName] = useState('') const [name, setName] = useState('')
const [instrumentType, setInstrumentType] = useState('') const [itemCategory, setItemCategory] = useState('')
const [size, setSize] = useState('') const [size, setSize] = useState('')
const [description, setDescription] = useState('') const [description, setDescription] = useState('')
const [itemType, setItemType] = useState('flat_rate') const [itemType, setItemType] = useState('flat_rate')
@@ -131,7 +131,7 @@ function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenCha
function resetForm() { function resetForm() {
setName('') setName('')
setInstrumentType('') setItemCategory('')
setSize('') setSize('')
setDescription('') setDescription('')
setItemType('flat_rate') setItemType('flat_rate')
@@ -144,7 +144,7 @@ function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenCha
e.preventDefault() e.preventDefault()
mutation.mutate({ mutation.mutate({
name, name,
instrumentType: instrumentType || undefined, itemCategory: itemCategory || undefined,
size: size || undefined, size: size || undefined,
description: description || undefined, description: description || undefined,
itemType, itemType,
@@ -168,8 +168,8 @@ function CreateTemplateDialog({ open, onOpenChange }: { open: boolean; onOpenCha
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>Instrument Type</Label> <Label>Item Category</Label>
<Input value={instrumentType} onChange={(e) => setInstrumentType(e.target.value)} placeholder="e.g. Violin, Trumpet, Guitar" /> <Input value={itemCategory} onChange={(e) => setItemCategory(e.target.value)} placeholder="e.g. Electronics, Appliances, Furniture" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Size</Label> <Label>Size</Label>

View File

@@ -48,8 +48,8 @@ function LoginPage() {
style={{ backgroundColor: '#131c2e', borderColor: '#1e2d45' }} style={{ backgroundColor: '#131c2e', borderColor: '#1e2d45' }}
> >
<div className="text-center mb-8"> <div className="text-center mb-8">
<h1 className="text-3xl font-bold" style={{ color: '#d8dfe9' }}>Forte</h1> <h1 className="text-3xl font-bold" style={{ color: '#d8dfe9' }}>LunarFront</h1>
<p className="text-sm mt-1" style={{ color: '#6b7a8d' }}>Music Store Management</p> <p className="text-sm mt-1" style={{ color: '#6b7a8d' }}>Small Business Management</p>
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">

View File

@@ -50,7 +50,7 @@ function expandPermissions(slugs: string[]): Set<string> {
function loadSession(): { token: string; user: User; permissions?: string[] } | null { function loadSession(): { token: string; user: User; permissions?: string[] } | null {
try { try {
const raw = sessionStorage.getItem('forte-auth') const raw = sessionStorage.getItem('lunarfront-auth')
if (!raw) return null if (!raw) return null
return JSON.parse(raw) return JSON.parse(raw)
} catch { } catch {
@@ -59,11 +59,11 @@ function loadSession(): { token: string; user: User; permissions?: string[] } |
} }
function saveSession(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() { function clearSession() {
sessionStorage.removeItem('forte-auth') sessionStorage.removeItem('lunarfront-auth')
} }
export const useAuthStore = create<AuthState>((set, get) => { export const useAuthStore = create<AuthState>((set, get) => {

View File

@@ -22,8 +22,8 @@ function apply(mode: Mode, colorTheme: string) {
} }
export const useThemeStore = create<ThemeState>((set) => { export const useThemeStore = create<ThemeState>((set) => {
const initialMode = (typeof window !== 'undefined' ? localStorage.getItem('forte-mode') as Mode : null) ?? 'system' const initialMode = (typeof window !== 'undefined' ? localStorage.getItem('lunarfront-mode') as Mode : null) ?? 'system'
const initialColor = (typeof window !== 'undefined' ? localStorage.getItem('forte-color-theme') : null) ?? 'slate' const initialColor = (typeof window !== 'undefined' ? localStorage.getItem('lunarfront-color-theme') : null) ?? 'slate'
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
apply(initialMode, initialColor) apply(initialMode, initialColor)
@@ -34,14 +34,14 @@ export const useThemeStore = create<ThemeState>((set) => {
colorTheme: initialColor, colorTheme: initialColor,
setMode: (mode) => { setMode: (mode) => {
localStorage.setItem('forte-mode', mode) localStorage.setItem('lunarfront-mode', mode)
const colorTheme = useThemeStore.getState().colorTheme const colorTheme = useThemeStore.getState().colorTheme
apply(mode, colorTheme) apply(mode, colorTheme)
set({ mode }) set({ mode })
}, },
setColorTheme: (name) => { setColorTheme: (name) => {
localStorage.setItem('forte-color-theme', name) localStorage.setItem('lunarfront-color-theme', name)
const mode = useThemeStore.getState().mode const mode = useThemeStore.getState().mode
apply(mode, name) apply(mode, name)
set({ colorTheme: name }) set({ colorTheme: name })

View File

@@ -7,7 +7,7 @@ export interface RepairTicket {
customerName: string customerName: string
customerPhone: string | null customerPhone: string | null
inventoryUnitId: string | null inventoryUnitId: string | null
instrumentDescription: string | null itemDescription: string | null
serialNumber: string | null serialNumber: string | null
conditionIn: 'excellent' | 'good' | 'fair' | 'poor' | null conditionIn: 'excellent' | 'good' | 'fair' | 'poor' | null
conditionInNotes: string | null conditionInNotes: string | null
@@ -55,7 +55,7 @@ export interface RepairBatch {
dueDate: string | null dueDate: string | null
completedDate: string | null completedDate: string | null
deliveredDate: string | null deliveredDate: string | null
instrumentCount: number itemCount: number
receivedCount: number receivedCount: number
estimatedTotal: string | null estimatedTotal: string | null
actualTotal: string | null actualTotal: string | null
@@ -79,7 +79,7 @@ export interface RepairNote {
export interface RepairServiceTemplate { export interface RepairServiceTemplate {
id: string id: string
name: string name: string
instrumentType: string | null itemCategory: string | null
size: string | null size: string | null
description: string | null description: string | null
itemType: 'labor' | 'part' | 'flat_rate' | 'misc' itemType: 'labor' | 'part' | 'flat_rate' | 'misc'

View File

@@ -16,17 +16,17 @@ const pages: WikiPage[] = [
title: 'Getting Started', title: 'Getting Started',
category: 'General', category: 'General',
content: ` 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 ## Signing In
1. Open Forte in your browser 1. Open LunarFront in your browser
2. Enter your email and password 2. Enter your email and password
3. Click **Sign in** 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 ## Navigation
@@ -34,13 +34,13 @@ Use the sidebar on the left to navigate between sections:
- **Accounts** — manage customer accounts and their members - **Accounts** — manage customer accounts and their members
- **Members** — find and manage individual people across all accounts - **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 - **Repair Batches** — manage bulk school repair jobs
- **Help** — you're here! - **Help** — you're here!
## Need Help? ## 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(), `.trim(),
}, },
{ {
@@ -89,7 +89,7 @@ Every account gets a unique 6-digit number automatically. This number appears in
content: ` content: `
# Members # 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 ## 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 5. Optionally enter card brand, last four digits, and expiration
6. Click **Add Payment Method** 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 ## Default Payment Method
@@ -154,7 +154,7 @@ If a payment method was migrated from an old system, it may show a "Needs Update
content: ` content: `
# Tax Exemptions # 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 ## Adding a Tax Exemption
@@ -191,7 +191,7 @@ All approvals and revocations are logged with who did it and when.
content: ` content: `
# Identity Documents # 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 ## Adding an ID
@@ -225,7 +225,7 @@ If a member has multiple IDs, mark one as **Primary** — this is the one shown
content: ` content: `
# Users & Roles # 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 ## Managing Users
@@ -306,16 +306,16 @@ Your preferences are saved in your browser and persist across sessions.
content: ` content: `
# Repairs # 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 ## Creating a Repair Ticket
1. Go to **Repairs** in the sidebar 1. Go to **Repairs** in the sidebar
2. Click **New Repair** 2. Click **New Repair**
3. Search for an existing account or enter customer details manually for walk-ins 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) 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** 7. Click **Create Ticket**
## Ticket Status Flow ## Ticket Status Flow
@@ -323,16 +323,16 @@ The Repairs module tracks instrument repair tickets from intake through completi
Each ticket moves through these stages: Each ticket moves through these stages:
- **New** — ticket just created, not yet examined - **New** — ticket just created, not yet examined
- **In Transit** — instrument being transported to the shop (for school pickups or shipped instruments) - **In Transit** — item being transported to the shop (for pickups or shipped items)
- **Intake** — instrument received, condition documented - **Intake** — item received, condition documented
- **Diagnosing** — technician examining the instrument - **Diagnosing** — technician examining the item
- **Pending Approval** — estimate provided, waiting for customer OK - **Pending Approval** — estimate provided, waiting for customer OK
- **Approved** — customer authorized the work - **Approved** — customer authorized the work
- **In Progress** — actively being repaired - **In Progress** — actively being repaired
- **Pending Parts** — waiting on parts order - **Pending Parts** — waiting on parts order
- **Ready** — repair complete, awaiting pickup - **Ready** — repair complete, awaiting pickup
- **Picked Up** — customer collected the instrument - **Picked Up** — customer collected the item
- **Delivered** — instrument returned via delivery (for school batches) - **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. 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: 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. - **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. - **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. - **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** 2. Click **New Template**
3. Fill in: 3. Fill in:
- **Name** — e.g. "Bow Rehair", "String Change", "Valve Overhaul" - **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" - **Size** — e.g. "4/4", "3/4", "Full"
- **Type** — Labor, Part, Flat Rate, or Misc - **Type** — Labor, Part, Flat Rate, or Misc
- **Default Price** — the customer-facing price - **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: ` content: `
# Repair Batches # 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 ## Creating a Batch
1. Go to **Repair Batches** in the sidebar 1. Go to **Repair Batches** in the sidebar
2. Click **New Batch** 2. Click **New Batch**
3. Select the school's account 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** 5. Click **Create Batch**
## Adding Tickets to a 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 ## Batch Approval
@@ -426,10 +426,10 @@ Only admins can approve or reject batches.
## Batch Status ## Batch Status
- **Intake** — receiving instruments - **Intake** — receiving items
- **In Progress** — work underway - **In Progress** — work underway
- **Completed** — all repairs done - **Completed** — all repairs done
- **Delivered** — instruments returned to school - **Delivered** — items returned to customer
## Filtering ## 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: 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 - **Work in Progress** — during the repair
- **Completed** — final result after repair - **Completed** — final result after repair
- **Documents** — signed approvals, quotes, receipts (accepts PDFs) - **Documents** — signed approvals, quotes, receipts (accepts PDFs)

View File

@@ -7,9 +7,9 @@ import { createClient } from './lib/client.js'
// --- Config --- // --- Config ---
const DB_HOST = process.env.DB_HOST ?? 'localhost' const DB_HOST = process.env.DB_HOST ?? 'localhost'
const DB_PORT = Number(process.env.DB_PORT ?? '5432') const DB_PORT = Number(process.env.DB_PORT ?? '5432')
const DB_USER = process.env.DB_USER ?? 'forte' const DB_USER = process.env.DB_USER ?? 'lunarfront'
const DB_PASS = process.env.DB_PASS ?? 'forte' const DB_PASS = process.env.DB_PASS ?? 'lunarfront'
const TEST_DB = 'forte_api_test' const TEST_DB = 'lunarfront_api_test'
const TEST_PORT = 8001 const TEST_PORT = 8001
const BASE_URL = `http://localhost:${TEST_PORT}` const BASE_URL = `http://localhost:${TEST_PORT}`
const COMPANY_ID = 'a0000000-0000-0000-0000-000000000001' const COMPANY_ID = 'a0000000-0000-0000-0000-000000000001'
@@ -60,7 +60,7 @@ async function setupDatabase() {
`) `)
// Seed company + location (company table stays as store settings) // 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')` await testSql`INSERT INTO location (id, name) VALUES (${LOCATION_ID}, 'Test Location')`
// Seed lookup tables // 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: '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: '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: '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: 'rentals', name: 'Rentals', description: 'Rental agreements and billing', enabled: false },
{ slug: 'lessons', name: 'Lessons', description: 'Lesson scheduling, instructor management, 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: '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: 'vault', name: 'Vault', description: 'Encrypted password and secret manager', enabled: true },
{ slug: 'email', name: 'Email', description: 'Email campaigns, templates, and sending', enabled: false }, { 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', HOST: '0.0.0.0',
NODE_ENV: 'development', NODE_ENV: 'development',
LOG_LEVEL: 'error', LOG_LEVEL: 'error',
STORAGE_LOCAL_PATH: '/tmp/forte-test-files', STORAGE_LOCAL_PATH: '/tmp/lunarfront-test-files',
}, },
stdout: 'pipe', stdout: 'pipe',
stderr: 'pipe', stderr: 'pipe',
@@ -170,7 +170,7 @@ async function registerTestUser(): Promise<string> {
method: 'POST', method: 'POST',
headers, headers,
body: JSON.stringify({ body: JSON.stringify({
email: 'test@forte.dev', email: 'test@lunarfront.dev',
password: testPassword, password: testPassword,
firstName: 'Test', firstName: 'Test',
lastName: 'Runner', lastName: 'Runner',
@@ -193,7 +193,7 @@ async function registerTestUser(): Promise<string> {
const loginRes = await fetch(`${BASE_URL}/v1/auth/login`, { const loginRes = await fetch(`${BASE_URL}/v1/auth/login`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, 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 } const loginData = await loginRes.json() as { token?: string }
if (loginRes.status !== 200 || !loginData.token) throw new Error(`Auth failed: ${JSON.stringify(loginData)}`) if (loginRes.status !== 200 || !loginData.token) throw new Error(`Auth failed: ${JSON.stringify(loginData)}`)

View File

@@ -304,7 +304,7 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
t.test('cannot disable yourself', { tags: ['users', 'status'] }, async () => { t.test('cannot disable yourself', { tags: ['users', 'status'] }, async () => {
// Get current user ID from the users list // Get current user ID from the users list
const usersRes = await t.api.get('/v1/users') 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) t.assert.ok(currentUser)
const res = await t.api.patch(`/v1/users/${currentUser.id}/status`, { isActive: false }) const res = await t.api.patch(`/v1/users/${currentUser.id}/status`, { isActive: false })

View File

@@ -7,8 +7,8 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
const res = await t.api.post('/v1/repair-tickets', { const res = await t.api.post('/v1/repair-tickets', {
customerName: 'Walk-In Customer', customerName: 'Walk-In Customer',
customerPhone: '555-0100', customerPhone: '555-0100',
instrumentDescription: 'Yamaha Trumpet', itemDescription: 'Samsung Galaxy S24',
problemDescription: 'Stuck valve, needs cleaning', problemDescription: 'Cracked screen, touch not working',
conditionIn: 'fair', conditionIn: 'fair',
}) })
t.assert.status(res, 201) t.assert.status(res, 201)
@@ -25,8 +25,8 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
const res = await t.api.post('/v1/repair-tickets', { const res = await t.api.post('/v1/repair-tickets', {
customerName: 'Repair Customer', customerName: 'Repair Customer',
accountId: acct.data.id, accountId: acct.data.id,
problemDescription: 'Broken bridge on violin', problemDescription: 'Screen flickering intermittently',
instrumentDescription: 'Student Violin 4/4', itemDescription: 'Dell XPS 15 Laptop',
conditionIn: 'poor', conditionIn: 'poor',
}) })
t.assert.status(res, 201) 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.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 () => { t.test('searches tickets by item description', { tags: ['tickets', 'search'] }, async () => {
await t.api.post('/v1/repair-tickets', { customerName: 'Instrument Search', problemDescription: 'Test', instrumentDescription: 'Selmer Mark VI Saxophone' }) 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.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 () => { 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 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`, { 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', visibility: 'customer',
}) })
t.assert.status(res, 201) t.assert.status(res, 201)
@@ -341,17 +341,17 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
const res = await t.api.post('/v1/repair-batches', { const res = await t.api.post('/v1/repair-batches', {
accountId: acct.data.id, accountId: acct.data.id,
contactName: 'Band Director', contactName: 'IT Director',
contactPhone: '555-0200', contactPhone: '555-0200',
instrumentCount: 15, itemCount: 15,
notes: 'Annual instrument checkup', notes: 'Annual equipment checkup',
}) })
t.assert.status(res, 201) t.assert.status(res, 201)
t.assert.ok(res.data.batchNumber) t.assert.ok(res.data.batchNumber)
t.assert.equal(res.data.batchNumber.length, 6) t.assert.equal(res.data.batchNumber.length, 6)
t.assert.equal(res.data.status, 'intake') t.assert.equal(res.data.status, 'intake')
t.assert.equal(res.data.approvalStatus, 'pending') 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 () => { 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 () => { t.test('updates a batch', { tags: ['batches', 'update'] }, async () => {
const acct = await t.api.post('/v1/accounts', { name: 'Update Batch School', billingMode: 'consolidated' }) 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.status(res, 200)
t.assert.equal(res.data.contactName, 'Updated Director') t.assert.equal(res.data.contactName, 'Updated Director')
}) })
t.test('adds tickets to a batch and lists them', { tags: ['batches', 'tickets'] }, async () => { 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 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: 'Screen cracked', itemDescription: 'Chromebook #1' })
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: 'Battery dead', itemDescription: 'Chromebook #2' })
const tickets = await t.api.get(`/v1/repair-batches/${batch.data.id}/tickets`, { limit: 100 }) const tickets = await t.api.get(`/v1/repair-batches/${batch.data.id}/tickets`, { limit: 100 })
t.assert.status(tickets, 200) t.assert.status(tickets, 200)
@@ -437,36 +437,36 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
t.test('creates a service template', { tags: ['templates', 'create'] }, async () => { t.test('creates a service template', { tags: ['templates', 'create'] }, async () => {
const res = await t.api.post('/v1/repair-service-templates', { const res = await t.api.post('/v1/repair-service-templates', {
name: 'Bow Rehair', name: 'Screen Repair',
instrumentType: 'Violin', itemCategory: 'Electronics',
size: '4/4', size: 'Phone',
itemType: 'flat_rate', itemType: 'flat_rate',
defaultPrice: 65, defaultPrice: 65,
defaultCost: 15, defaultCost: 15,
}) })
t.assert.status(res, 201) t.assert.status(res, 201)
t.assert.equal(res.data.name, 'Bow Rehair') t.assert.equal(res.data.name, 'Screen Repair')
t.assert.equal(res.data.instrumentType, 'Violin') t.assert.equal(res.data.itemCategory, 'Electronics')
t.assert.equal(res.data.size, '4/4') t.assert.equal(res.data.size, 'Phone')
t.assert.equal(res.data.defaultPrice, '65.00') t.assert.equal(res.data.defaultPrice, '65.00')
t.assert.equal(res.data.defaultCost, '15.00') t.assert.equal(res.data.defaultCost, '15.00')
}) })
t.test('lists service templates with search', { tags: ['templates', 'read'] }, async () => { 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.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.assert.ok(res.data.pagination)
}) })
t.test('updates a service template', { tags: ['templates', 'update'] }, async () => { 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 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, instrumentType: 'Clarinet' }) const res = await t.api.patch(`/v1/repair-service-templates/${created.data.id}`, { defaultPrice: 35, itemCategory: 'Bicycles' })
t.assert.status(res, 200) t.assert.status(res, 200)
t.assert.equal(res.data.defaultPrice, '35.00') 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 () => { t.test('soft-deletes a service template', { tags: ['templates', 'delete'] }, async () => {

View File

@@ -96,14 +96,14 @@ suite('Vault', { tags: ['vault'] }, (t) => {
const res = await t.api.post(`/v1/vault/categories/${catId}/entries`, { const res = await t.api.post(`/v1/vault/categories/${catId}/entries`, {
name: 'Store WiFi', name: 'Store WiFi',
username: 'ForteMusic', username: 'DemoUser',
url: 'http://192.168.1.1', url: 'http://192.168.1.1',
notes: 'Router admin panel', notes: 'Router admin panel',
secret: 'supersecretpassword123', secret: 'supersecretpassword123',
}) })
t.assert.status(res, 201) t.assert.status(res, 201)
t.assert.equal(res.data.name, 'Store WiFi') 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) t.assert.equal(res.data.hasSecret, true)
// Secret value should NOT be in the response // Secret value should NOT be in the response
t.assert.falsy(res.data.encryptedValue) t.assert.falsy(res.data.encryptedValue)

View File

@@ -27,7 +27,7 @@ async function dav(
suite('WebDAV', { tags: ['webdav', 'storage'] }, (t) => { suite('WebDAV', { tags: ['webdav', 'storage'] }, (t) => {
// Use the same test user created by the test runner // Use the same test user created by the test runner
const email = 'test@forte.dev' const email = 'test@lunarfront.dev'
const password = 'testpassword1234' const password = 'testpassword1234'
const basicAuth = 'Basic ' + Buffer.from(`${email}:${password}`).toString('base64') const basicAuth = 'Basic ' + Buffer.from(`${email}:${password}`).toString('base64')
const badAuth = 'Basic ' + Buffer.from(`${email}:wrongpassword`).toString('base64') const badAuth = 'Basic ' + Buffer.from(`${email}:wrongpassword`).toString('base64')

View File

@@ -5,6 +5,6 @@ export default defineConfig({
out: './src/db/migrations', out: './src/db/migrations',
dialect: 'postgresql', dialect: 'postgresql',
dbCredentials: { dbCredentials: {
url: process.env.DATABASE_URL ?? 'postgresql://forte:forte@localhost:5432/forte', url: process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront',
}, },
}) })

View File

@@ -1,5 +1,5 @@
{ {
"name": "@forte/backend", "name": "@lunarfront/backend",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -13,6 +13,8 @@
"db:generate": "bunx drizzle-kit generate", "db:generate": "bunx drizzle-kit generate",
"db:migrate": "bunx drizzle-kit migrate", "db:migrate": "bunx drizzle-kit migrate",
"db:seed-dev": "bun run src/db/seeds/dev-seed.ts", "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" "db:seed": "bun run src/db/seed.ts"
}, },
"dependencies": { "dependencies": {
@@ -20,7 +22,7 @@
"@fastify/jwt": "^9", "@fastify/jwt": "^9",
"@fastify/multipart": "^9.4.0", "@fastify/multipart": "^9.4.0",
"@fastify/rate-limit": "^10.3.0", "@fastify/rate-limit": "^10.3.0",
"@forte/shared": "workspace:*", "@lunarfront/shared": "workspace:*",
"bcrypt": "^6", "bcrypt": "^6",
"drizzle-orm": "^0.38", "drizzle-orm": "^0.38",
"fastify": "^5", "fastify": "^5",

View File

@@ -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';

View File

@@ -190,6 +190,13 @@
"when": 1774860000000, "when": 1774860000000,
"tag": "0026_modules", "tag": "0026_modules",
"breakpoints": true "breakpoints": true
},
{
"idx": 27,
"version": "7",
"when": 1774870000000,
"tag": "0027_generalize_terminology",
"breakpoints": true
} }
] ]
} }

View File

@@ -82,7 +82,7 @@ export const repairBatches = pgTable('repair_batch', {
dueDate: timestamp('due_date', { withTimezone: true }), dueDate: timestamp('due_date', { withTimezone: true }),
completedDate: timestamp('completed_date', { withTimezone: true }), completedDate: timestamp('completed_date', { withTimezone: true }),
deliveredDate: timestamp('delivered_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), receivedCount: integer('received_count').notNull().default(0),
estimatedTotal: numeric('estimated_total', { precision: 10, scale: 2 }), estimatedTotal: numeric('estimated_total', { precision: 10, scale: 2 }),
actualTotal: numeric('actual_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(), customerName: varchar('customer_name', { length: 255 }).notNull(),
customerPhone: varchar('customer_phone', { length: 50 }), customerPhone: varchar('customer_phone', { length: 50 }),
inventoryUnitId: uuid('inventory_unit_id').references(() => inventoryUnits.id), inventoryUnitId: uuid('inventory_unit_id').references(() => inventoryUnits.id),
instrumentDescription: text('instrument_description'), itemDescription: text('item_description'),
serialNumber: varchar('serial_number', { length: 255 }), serialNumber: varchar('serial_number', { length: 255 }),
conditionIn: repairConditionInEnum('condition_in'), conditionIn: repairConditionInEnum('condition_in'),
conditionInNotes: text('condition_in_notes'), conditionInNotes: text('condition_in_notes'),
@@ -158,7 +158,7 @@ export type RepairNoteInsert = typeof repairNotes.$inferInsert
export const repairServiceTemplates = pgTable('repair_service_template', { export const repairServiceTemplates = pgTable('repair_service_template', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
name: varchar('name', { length: 255 }).notNull(), name: varchar('name', { length: 255 }).notNull(),
instrumentType: varchar('instrument_type', { length: 100 }), itemCategory: varchar('item_category', { length: 100 }),
size: varchar('size', { length: 50 }), size: varchar('size', { length: 50 }),
description: text('description'), description: text('description'),
itemType: repairLineItemTypeEnum('item_type').notNull().default('flat_rate'), itemType: repairLineItemTypeEnum('item_type').notNull().default('flat_rate'),

View File

@@ -7,7 +7,7 @@ const DEV_LOCATION_ID = '00000000-0000-0000-0000-000000000010'
async function seed() { async function seed() {
const connectionString = 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 sql = postgres(connectionString)
const db = drizzle(sql) const db = drizzle(sql)
@@ -17,7 +17,7 @@ async function seed() {
.insert(companies) .insert(companies)
.values({ .values({
id: DEV_COMPANY_ID, id: DEV_COMPANY_ID,
name: 'Dev Music Co.', name: 'Dev Store',
timezone: 'America/Chicago', timezone: 'America/Chicago',
}) })
.onConflictDoNothing() .onConflictDoNothing()

View File

@@ -6,7 +6,7 @@
*/ */
import postgres from 'postgres' 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 COMPANY_ID = 'a0000000-0000-0000-0000-000000000001'
const sql = postgres(DB_URL) const sql = postgres(DB_URL)
@@ -17,7 +17,7 @@ async function seed() {
// Create company and location if they don't exist // Create company and location if they don't exist
const [company] = await sql`SELECT id FROM company WHERE id = ${COMPANY_ID}` const [company] = await sql`SELECT id FROM company WHERE id = ${COMPANY_ID}`
if (!company) { 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')` await sql`INSERT INTO location (id, name) VALUES ('a0000000-0000-0000-0000-000000000002', 'Main Store')`
console.log(' Created company and location') console.log(' Created company and location')
@@ -39,16 +39,16 @@ async function seed() {
} }
// --- Admin user (if not exists) --- // --- 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) { if (!adminUser) {
const bcrypt = await import('bcrypt') const bcrypt = await import('bcrypt')
const hashedPw = await (bcrypt.default || bcrypt).hash('admin1234', 10) 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` const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
if (adminRole) { if (adminRole) {
await sql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${user.id}, ${adminRole.id}) ON CONFLICT DO NOTHING` 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 { } else {
const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1` const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
if (adminRole) { if (adminRole) {
@@ -61,11 +61,11 @@ async function seed() {
const accounts = [ const accounts = [
{ name: 'Smith Family', email: 'smith@example.com', phone: '555-0101' }, { name: 'Smith Family', email: 'smith@example.com', phone: '555-0101' },
{ name: 'Johnson Family', email: 'johnson@example.com', phone: '555-0102' }, { name: 'Johnson Family', email: 'johnson@example.com', phone: '555-0102' },
{ name: 'Lincoln High School', email: 'band@lincoln.edu', phone: '555-0200' }, { name: 'Lincoln High School', email: 'office@lincoln.edu', phone: '555-0200' },
{ name: 'Garcia Music Studio', email: 'garcia@studio.com', phone: '555-0103' }, { name: 'Garcia Workshop', email: 'garcia@studio.com', phone: '555-0103' },
{ name: 'Mike Thompson', email: 'mike.t@email.com', phone: '555-0104' }, { name: 'Mike Thompson', email: 'mike.t@email.com', phone: '555-0104' },
{ name: 'Emily Chen', email: 'emily.chen@email.com', phone: '555-0105' }, { 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' }, { 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: 'Smith Family', firstName: 'Tommy', lastName: 'Smith', isMinor: true },
{ accountName: 'Johnson Family', firstName: 'Lisa', lastName: 'Johnson', email: 'lisa.j@example.com' }, { accountName: 'Johnson Family', firstName: 'Lisa', lastName: 'Johnson', email: 'lisa.j@example.com' },
{ accountName: 'Johnson Family', firstName: 'Jake', lastName: 'Johnson', isMinor: true }, { 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: 'Mike Thompson', firstName: 'Mike', lastName: 'Thompson', email: 'mike.t@email.com' },
{ accountName: 'Emily Chen', firstName: 'Emily', lastName: 'Chen', email: 'emily.chen@email.com' }, { accountName: 'Emily Chen', firstName: 'Emily', lastName: 'Chen', email: 'emily.chen@email.com' },
] ]
@@ -105,39 +105,37 @@ async function seed() {
// --- Repair Service Templates --- // --- Repair Service Templates ---
const templates = [ const templates = [
{ name: 'Bow Rehair', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '65.00', cost: '15.00' }, { name: 'Screen Repair', itemCategory: 'Electronics', size: 'Phone', itemType: 'flat_rate', price: '89.00', cost: '25.00' },
{ name: 'Bow Rehair', instrumentType: 'Violin', size: '3/4', itemType: 'flat_rate', price: '55.00', cost: '12.00' }, { name: 'Screen Repair', itemCategory: 'Electronics', size: 'Tablet', itemType: 'flat_rate', price: '129.00', cost: '45.00' },
{ name: 'Bow Rehair', instrumentType: 'Cello', size: null, itemType: 'flat_rate', price: '80.00', cost: '20.00' }, { name: 'Battery Replacement', itemCategory: 'Electronics', size: 'Phone', itemType: 'flat_rate', price: '59.00', cost: '15.00' },
{ name: 'Bow Rehair', instrumentType: 'Bass', size: null, itemType: 'flat_rate', price: '90.00', cost: '25.00' }, { name: 'Battery Replacement', itemCategory: 'Electronics', size: 'Laptop', itemType: 'flat_rate', price: '99.00', cost: '35.00' },
{ name: 'String Change', instrumentType: 'Guitar', size: 'Acoustic', itemType: 'flat_rate', price: '25.00', cost: '8.00' }, { name: 'Tune-Up', itemCategory: 'Bicycles', size: 'Standard', itemType: 'flat_rate', price: '65.00', cost: '10.00' },
{ name: 'String Change', instrumentType: 'Guitar', size: 'Electric', itemType: 'flat_rate', price: '25.00', cost: '7.00' }, { name: 'Brake Adjustment', itemCategory: 'Bicycles', size: null, itemType: 'flat_rate', price: '35.00', cost: '5.00' },
{ name: 'String Change', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '35.00', cost: '12.00' }, { name: 'Blade Sharpening', itemCategory: 'Tools', size: null, itemType: 'flat_rate', price: '15.00', cost: '3.00' },
{ name: 'Valve Overhaul', instrumentType: 'Trumpet', size: null, itemType: 'labor', price: '85.00', cost: null }, { name: 'Motor Repair', itemCategory: 'Appliances', 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: 'Zipper Replacement', itemCategory: 'Clothing', size: null, itemType: 'flat_rate', price: '25.00', cost: '5.00' },
{ name: 'Pad Replacement', instrumentType: 'Flute', size: null, itemType: 'flat_rate', price: '110.00', cost: '25.00' }, { name: 'Sole Replacement', itemCategory: 'Footwear', size: null, itemType: 'flat_rate', price: '55.00', cost: '15.00' },
{ name: 'Cork Replacement', instrumentType: 'Clarinet', size: null, itemType: 'flat_rate', price: '45.00', cost: '5.00' }, { name: 'Watch Battery', itemCategory: 'Watches', size: null, itemType: 'flat_rate', price: '15.00', cost: '3.00' },
{ name: 'Slide Repair', instrumentType: 'Trombone', size: null, itemType: 'labor', price: '75.00', cost: null }, { name: 'Furniture Refinishing', itemCategory: 'Furniture', size: null, itemType: 'labor', price: '150.00', cost: null },
{ name: 'Bridge Setup', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '40.00', cost: '10.00' }, { name: 'Diagnostic Check', itemCategory: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
{ name: 'Guitar Setup', instrumentType: 'Guitar', size: null, itemType: 'flat_rate', price: '65.00', cost: '5.00' }, { name: 'General Cleaning', itemCategory: null, size: null, itemType: 'flat_rate', price: '30.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' },
] ]
for (const t of templates) { 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 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)` 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.instrumentType ?? ''} ${t.size ?? ''}`) console.log(` Template: ${t.name} ${t.itemCategory ?? ''} ${t.size ?? ''}`)
} }
// --- Repair Tickets --- // --- Repair Tickets ---
const 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: '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', 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: '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', instrument: 'Stradivarius Copy Violin', serial: null, problem: 'Bow needs rehair, bridge slightly warped', condition: 'fair', status: 'ready', estimate: '105.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', instrument: 'Martin D-28 Acoustic Guitar', serial: 'M2284563', problem: 'Broken tuning peg, needs replacement', condition: 'good', status: 'new', estimate: null }, { 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', instrument: 'Yamaha YCL-255 Clarinet', serial: null, problem: 'Several pads worn, keys sticking', condition: 'poor', status: 'diagnosing', 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', instrument: 'Unknown Flute', serial: null, problem: 'Customer says it squeaks on high notes', condition: 'fair', status: 'intake', 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) { for (const t of tickets) {
@@ -145,8 +143,8 @@ async function seed() {
if (existing.length > 0) continue if (existing.length > 0) continue
const num = String(Math.floor(100000 + Math.random() * 900000)) const num = String(Math.floor(100000 + Math.random() * 900000))
const acctId = acctIds[t.customer] ?? null 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})` 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.instrument} [${t.status}]`) console.log(` Ticket: ${t.customer}${t.item} [${t.status}]`)
} }
// --- Repair Batch --- // --- Repair Batch ---
@@ -154,22 +152,22 @@ async function seed() {
if (batchExists.length === 0) { if (batchExists.length === 0) {
const batchNum = String(Math.floor(100000 + Math.random() * 900000)) const batchNum = String(Math.floor(100000 + Math.random() * 900000))
const schoolId = acctIds['Lincoln High School'] 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 = [ const batchTickets = [
{ instrument: 'Student Flute', problem: 'Pads worn, needs replacement check', condition: 'fair' }, { item: 'Chromebook #101', problem: 'Screen flickering, hinge loose', condition: 'fair' },
{ instrument: 'Student Clarinet #1', problem: 'Keys sticking, cork dried out', condition: 'fair' }, { item: 'Chromebook #102', problem: 'Keyboard unresponsive, several keys stuck', condition: 'fair' },
{ instrument: 'Student Clarinet #2', problem: 'Barrel crack, needs assessment', condition: 'poor' }, { item: 'Projector — Epson EB-X51', problem: 'Lamp dim, color wheel noise', condition: 'poor' },
{ instrument: 'Student Trumpet', problem: 'Valve oil needed, general checkup', condition: 'good' }, { item: 'Label Printer — Dymo 450', problem: 'Feed mechanism jammed', condition: 'good' },
{ instrument: 'Student Trombone', problem: 'Slide dent, sluggish movement', condition: 'fair' }, { item: 'PA Speaker — JBL EON715', problem: 'Crackling at high volume', condition: 'fair' },
] ]
for (const bt of batchTickets) { for (const bt of batchTickets) {
const num = String(Math.floor(100000 + Math.random() * 900000)) 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')` 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.instrument}`) 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!') console.log('\nDev seed complete!')

View File

@@ -20,14 +20,14 @@ export const SYSTEM_PERMISSIONS = [
{ slug: 'pos.admin', domain: 'pos', action: 'admin', description: 'Void transactions, override prices, manage discounts' }, { slug: 'pos.admin', domain: 'pos', action: 'admin', description: 'Void transactions, override prices, manage discounts' },
// Rentals // Rentals
{ slug: 'rentals.view', domain: 'rentals', action: 'view', description: 'View rental contracts, fleet, billing' }, { 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 fleet' }, { 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' }, { slug: 'rentals.admin', domain: 'rentals', action: 'admin', description: 'Override terms, adjust equity, cancel contracts' },
// Lessons // Lessons / Scheduling
{ slug: 'lessons.view', domain: 'lessons', action: 'view', description: 'View lesson schedules, enrollments, attendance' }, { 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.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 // Repairs
{ slug: 'repairs.view', domain: 'repairs', action: 'view', description: 'View repair tickets, parts inventory' }, { slug: 'repairs.view', domain: 'repairs', action: 'view', description: 'View repair tickets, parts inventory' },

View File

@@ -80,7 +80,7 @@ export function webdavBasicAuth(app: FastifyInstance) {
const authHeader = request.headers.authorization const authHeader = request.headers.authorization
if (!authHeader || !authHeader.startsWith('Basic ')) { 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') return reply.status(401).send('Authentication required')
} }
@@ -88,7 +88,7 @@ export function webdavBasicAuth(app: FastifyInstance) {
const colonIndex = decoded.indexOf(':') const colonIndex = decoded.indexOf(':')
if (colonIndex === -1) { if (colonIndex === -1) {
recordFailure(ip) 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') return reply.status(401).send('Invalid credentials')
} }
@@ -103,14 +103,14 @@ export function webdavBasicAuth(app: FastifyInstance) {
if (!user || !user.isActive) { if (!user || !user.isActive) {
recordFailure(ip) 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') return reply.status(401).send('Invalid credentials')
} }
const valid = await bcrypt.compare(password, user.passwordHash) const valid = await bcrypt.compare(password, user.passwordHash)
if (!valid) { if (!valid) {
recordFailure(ip) 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') return reply.status(401).send('Invalid credentials')
} }

View File

@@ -13,7 +13,7 @@ import {
TaxExemptionUpdateSchema, TaxExemptionUpdateSchema,
MemberIdentifierCreateSchema, MemberIdentifierCreateSchema,
MemberIdentifierUpdateSchema, MemberIdentifierUpdateSchema,
} from '@forte/shared/schemas' } from '@lunarfront/shared/schemas'
import { import {
AccountService, AccountService,
MemberService, MemberService,

View File

@@ -1,7 +1,7 @@
import type { FastifyPluginAsync } from 'fastify' import type { FastifyPluginAsync } from 'fastify'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import bcrypt from 'bcrypt' 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' import { users } from '../../db/schema/users.js'
const SALT_ROUNDS = 10 const SALT_ROUNDS = 10

View File

@@ -5,7 +5,7 @@ import {
SupplierCreateSchema, SupplierCreateSchema,
SupplierUpdateSchema, SupplierUpdateSchema,
PaginationSchema, PaginationSchema,
} from '@forte/shared/schemas' } from '@lunarfront/shared/schemas'
import { CategoryService, SupplierService } from '../../services/inventory.service.js' import { CategoryService, SupplierService } from '../../services/inventory.service.js'
export const inventoryRoutes: FastifyPluginAsync = async (app) => { export const inventoryRoutes: FastifyPluginAsync = async (app) => {

View File

@@ -1,5 +1,5 @@
import type { FastifyPluginAsync } from 'fastify' 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 { UnitStatusService, ItemConditionService } from '../../services/lookup.service.js'
import { ConflictError, ValidationError } from '../../lib/errors.js' import { ConflictError, ValidationError } from '../../lib/errors.js'

View File

@@ -5,7 +5,7 @@ import {
InventoryUnitCreateSchema, InventoryUnitCreateSchema,
InventoryUnitUpdateSchema, InventoryUnitUpdateSchema,
PaginationSchema, PaginationSchema,
} from '@forte/shared/schemas' } from '@lunarfront/shared/schemas'
import { ProductService, InventoryUnitService } from '../../services/product.service.js' import { ProductService, InventoryUnitService } from '../../services/product.service.js'
export const productRoutes: FastifyPluginAsync = async (app) => { export const productRoutes: FastifyPluginAsync = async (app) => {

View File

@@ -1,6 +1,6 @@
import type { FastifyPluginAsync } from 'fastify' import type { FastifyPluginAsync } from 'fastify'
import { eq, count, sql, type Column } from 'drizzle-orm' 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 { RbacService } from '../../services/rbac.service.js'
import { ValidationError } from '../../lib/errors.js' import { ValidationError } from '../../lib/errors.js'
import { users } from '../../db/schema/users.js' import { users } from '../../db/schema/users.js'

View File

@@ -12,7 +12,7 @@ import {
RepairNoteCreateSchema, RepairNoteCreateSchema,
RepairServiceTemplateCreateSchema, RepairServiceTemplateCreateSchema,
RepairServiceTemplateUpdateSchema, RepairServiceTemplateUpdateSchema,
} from '@forte/shared/schemas' } from '@lunarfront/shared/schemas'
import { RepairTicketService, RepairLineItemService, RepairBatchService, RepairNoteService, RepairServiceTemplateService } from '../../services/repair.service.js' import { RepairTicketService, RepairLineItemService, RepairBatchService, RepairNoteService, RepairServiceTemplateService } from '../../services/repair.service.js'
export const repairRoutes: FastifyPluginAsync = async (app) => { export const repairRoutes: FastifyPluginAsync = async (app) => {

View File

@@ -1,6 +1,6 @@
import type { FastifyPluginAsync } from 'fastify' import type { FastifyPluginAsync } from 'fastify'
import multipart from '@fastify/multipart' 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 { StorageFolderService, StorageFileService, StoragePermissionService } from '../../services/storage.service.js'
import { ValidationError } from '../../lib/errors.js' import { ValidationError } from '../../lib/errors.js'

View File

@@ -1,5 +1,5 @@
import type { FastifyPluginAsync } from 'fastify' 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 { VaultKeyService, VaultPermissionService, VaultCategoryService, VaultEntryService } from '../../services/vault.service.js'
import { ValidationError } from '../../lib/errors.js' import { ValidationError } from '../../lib/errors.js'

View File

@@ -20,8 +20,8 @@ import type {
TaxExemptionCreateInput, TaxExemptionCreateInput,
TaxExemptionUpdateInput, TaxExemptionUpdateInput,
PaginationInput, PaginationInput,
} from '@forte/shared/schemas' } from '@lunarfront/shared/schemas'
import { isMinor, normalizeStateCode } from '@forte/shared/utils' import { isMinor, normalizeStateCode } from '@lunarfront/shared/utils'
import { import {
withPagination, withPagination,
withSort, withSort,

View File

@@ -7,7 +7,7 @@ import type {
SupplierCreateInput, SupplierCreateInput,
SupplierUpdateInput, SupplierUpdateInput,
PaginationInput, PaginationInput,
} from '@forte/shared/schemas' } from '@lunarfront/shared/schemas'
import { import {
withPagination, withPagination,
withSort, withSort,

View File

@@ -7,7 +7,7 @@ import {
SYSTEM_UNIT_STATUSES, SYSTEM_UNIT_STATUSES,
SYSTEM_ITEM_CONDITIONS, SYSTEM_ITEM_CONDITIONS,
} from '../db/schema/lookups.js' } from '../db/schema/lookups.js'
import type { LookupCreateInput, LookupUpdateInput } from '@forte/shared/schemas' import type { LookupCreateInput, LookupUpdateInput } from '@lunarfront/shared/schemas'
function createLookupService( function createLookupService(
table: typeof inventoryUnitStatuses | typeof itemConditions, table: typeof inventoryUnitStatuses | typeof itemConditions,

View File

@@ -8,7 +8,7 @@ import type {
InventoryUnitCreateInput, InventoryUnitCreateInput,
InventoryUnitUpdateInput, InventoryUnitUpdateInput,
PaginationInput, PaginationInput,
} from '@forte/shared/schemas' } from '@lunarfront/shared/schemas'
import { import {
withPagination, withPagination,
withSort, withSort,

View File

@@ -1,6 +1,6 @@
import { eq, and, inArray, count, type Column } from 'drizzle-orm' import { eq, and, inArray, count, type Column } from 'drizzle-orm'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js' 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 { permissions, roles, rolePermissions, userRoles } from '../db/schema/rbac.js'
import { SYSTEM_PERMISSIONS, DEFAULT_ROLES } from '../db/seeds/rbac.js' import { SYSTEM_PERMISSIONS, DEFAULT_ROLES } from '../db/seeds/rbac.js'
import { ForbiddenError } from '../lib/errors.js' import { ForbiddenError } from '../lib/errors.js'

View File

@@ -18,7 +18,7 @@ import type {
RepairServiceTemplateCreateInput, RepairServiceTemplateCreateInput,
RepairServiceTemplateUpdateInput, RepairServiceTemplateUpdateInput,
PaginationInput, PaginationInput,
} from '@forte/shared/schemas' } from '@lunarfront/shared/schemas'
import { import {
withPagination, withPagination,
withSort, withSort,
@@ -59,7 +59,7 @@ export const RepairTicketService = {
locationId: input.locationId, locationId: input.locationId,
repairBatchId: input.repairBatchId, repairBatchId: input.repairBatchId,
inventoryUnitId: input.inventoryUnitId, inventoryUnitId: input.inventoryUnitId,
instrumentDescription: input.instrumentDescription, itemDescription: input.itemDescription,
serialNumber: input.serialNumber, serialNumber: input.serialNumber,
conditionIn: input.conditionIn, conditionIn: input.conditionIn,
conditionInNotes: input.conditionInNotes, conditionInNotes: input.conditionInNotes,
@@ -101,7 +101,7 @@ export const RepairTicketService = {
repairTickets.ticketNumber, repairTickets.ticketNumber,
repairTickets.customerName, repairTickets.customerName,
repairTickets.customerPhone, repairTickets.customerPhone,
repairTickets.instrumentDescription, repairTickets.itemDescription,
repairTickets.serialNumber, repairTickets.serialNumber,
]) ])
if (search) conditions.push(search) if (search) conditions.push(search)
@@ -151,7 +151,7 @@ export const RepairTicketService = {
async listByBatch(db: PostgresJsDatabase<any>, batchId: string, params: PaginationInput) { async listByBatch(db: PostgresJsDatabase<any>, batchId: string, params: PaginationInput) {
const baseWhere = eq(repairTickets.repairBatchId, batchId) const baseWhere = eq(repairTickets.repairBatchId, batchId)
const searchCondition = params.q const searchCondition = params.q
? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.instrumentDescription]) ? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.itemDescription])
: undefined : undefined
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
@@ -292,7 +292,7 @@ export const RepairBatchService = {
contactEmail: input.contactEmail, contactEmail: input.contactEmail,
pickupDate: input.pickupDate ? new Date(input.pickupDate) : undefined, pickupDate: input.pickupDate ? new Date(input.pickupDate) : undefined,
dueDate: input.dueDate ? new Date(input.dueDate) : undefined, dueDate: input.dueDate ? new Date(input.dueDate) : undefined,
instrumentCount: input.instrumentCount, itemCount: input.itemCount,
notes: input.notes, notes: input.notes,
}) })
.returning() .returning()
@@ -391,7 +391,7 @@ export const RepairServiceTemplateService = {
.insert(repairServiceTemplates) .insert(repairServiceTemplates)
.values({ .values({
name: input.name, name: input.name,
instrumentType: input.instrumentType, itemCategory: input.itemCategory,
size: input.size, size: input.size,
description: input.description, description: input.description,
itemType: input.itemType, itemType: input.itemType,
@@ -406,13 +406,13 @@ export const RepairServiceTemplateService = {
async list(db: PostgresJsDatabase<any>, params: PaginationInput) { async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
const baseWhere = eq(repairServiceTemplates.isActive, true) const baseWhere = eq(repairServiceTemplates.isActive, true)
const searchCondition = params.q 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 : undefined
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const sortableColumns: Record<string, Column> = { const sortableColumns: Record<string, Column> = {
name: repairServiceTemplates.name, name: repairServiceTemplates.name,
instrument_type: repairServiceTemplates.instrumentType, item_category: repairServiceTemplates.itemCategory,
default_price: repairServiceTemplates.defaultPrice, default_price: repairServiceTemplates.defaultPrice,
sort_order: repairServiceTemplates.sortOrder, sort_order: repairServiceTemplates.sortOrder,
created_at: repairServiceTemplates.createdAt, created_at: repairServiceTemplates.createdAt,

View File

@@ -5,7 +5,7 @@ import { userRoles } from '../db/schema/rbac.js'
import type { StorageProvider } from '../storage/index.js' import type { StorageProvider } from '../storage/index.js'
import { randomUUID } from 'crypto' import { randomUUID } from 'crypto'
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.js' 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 const MAX_PARENT_DEPTH = 50

View File

@@ -3,7 +3,7 @@ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import { vaultConfig, vaultCategories, vaultCategoryPermissions, vaultEntries } from '../db/schema/vault.js' import { vaultConfig, vaultCategories, vaultCategoryPermissions, vaultEntries } from '../db/schema/vault.js'
import { userRoles } from '../db/schema/rbac.js' import { userRoles } from '../db/schema/rbac.js'
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.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 { randomBytes, createCipheriv, createDecipheriv, pbkdf2Sync } from 'crypto'
import bcrypt from 'bcrypt' import bcrypt from 'bcrypt'

View File

@@ -1,6 +1,6 @@
import { sql, asc, desc, ilike, or, type SQL, type Column } from 'drizzle-orm' import { sql, asc, desc, ilike, or, type SQL, type Column } from 'drizzle-orm'
import type { PgSelect } from 'drizzle-orm/pg-core' 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. * Apply pagination (offset + limit) to a Drizzle query.

View File

@@ -1,5 +1,5 @@
{ {
"name": "@forte/shared", "name": "@lunarfront/shared",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@@ -36,7 +36,7 @@ export const RepairTicketCreateSchema = z.object({
locationId: opt(z.string().uuid()), locationId: opt(z.string().uuid()),
repairBatchId: opt(z.string().uuid()), repairBatchId: opt(z.string().uuid()),
inventoryUnitId: opt(z.string().uuid()), inventoryUnitId: opt(z.string().uuid()),
instrumentDescription: opt(z.string()), itemDescription: opt(z.string()),
serialNumber: opt(z.string().max(255)), serialNumber: opt(z.string().max(255)),
conditionIn: opt(RepairConditionIn), conditionIn: opt(RepairConditionIn),
conditionInNotes: opt(z.string()), conditionInNotes: opt(z.string()),
@@ -84,7 +84,7 @@ export const RepairBatchCreateSchema = z.object({
contactEmail: opt(z.string().email()), contactEmail: opt(z.string().email()),
pickupDate: opt(z.string()), pickupDate: opt(z.string()),
dueDate: 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()), notes: opt(z.string()),
}) })
export type RepairBatchCreateInput = z.infer<typeof RepairBatchCreateSchema> export type RepairBatchCreateInput = z.infer<typeof RepairBatchCreateSchema>
@@ -112,7 +112,7 @@ export type RepairNoteCreateInput = z.infer<typeof RepairNoteCreateSchema>
export const RepairServiceTemplateCreateSchema = z.object({ export const RepairServiceTemplateCreateSchema = z.object({
name: z.string().min(1).max(255), 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)), size: opt(z.string().max(50)),
description: opt(z.string()), description: opt(z.string()),
itemType: RepairLineItemType.default('flat_rate'), itemType: RepairLineItemType.default('flat_rate'),

View File

@@ -1,4 +1,4 @@
// @forte/shared types // @lunarfront/shared types
// Domain types will be added as each domain is implemented // Domain types will be added as each domain is implemented
export type StoreId = string export type StoreId = string

View File

@@ -1,4 +1,4 @@
Forte — Music Store Management Platform LunarFront — Small Business Management Platform
Implementation Roadmap Implementation Roadmap
@@ -8,23 +8,23 @@ Version 1.0 | Draft
# 1. Purpose # 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. 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 ## 1.1 Project Conventions
Name | Value Name | Value
App name | Forte App name | LunarFront
Package namespace | `@forte/shared`, `@forte/backend`, etc. Package namespace | `@lunarfront/shared`, `@lunarfront/backend`, etc.
Database (dev) | `forte` Database (dev) | `forte`
Database (test) | `forte_test` Database (test) | `lunarfront_test`
Logging | JSON structured logging via Pino (Fastify built-in) Logging | JSON structured logging via Pino (Fastify built-in)
Linting | ESLint + Prettier at monorepo root 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 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 Auth (Phase 2+) | Self-issued JWTs + bcrypt, swap to Clerk/Auth0 later
Request tracing | Auto-generated request IDs via Fastify (included from Phase 1) 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) 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) 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` 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 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 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` 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` (`@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` 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` 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 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 ### Architecture Decisions Settled
- `company_id` (tenant) + `location_id` (physical store) scoping pattern established on the first domain tables - `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 - Inventory and transaction tables use both `company_id` and `location_id`; other tables use `company_id` only
- Drizzle migration workflow (generate → migrate) - 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) - Standardized error response format (consistent JSON shape for errors)
- Request context pattern (`companyId`, `locationId`, and `user` on request) - Request context pattern (`companyId`, `locationId`, and `user` on request)
- JSON structured logging via Pino with request ID on every log line - JSON structured logging via Pino with request ID on every log line

View File

@@ -1,4 +1,4 @@
Forte — Music Store Management Platform LunarFront — Small Business Management Platform
Domain Design: Personnel Management Domain Design: Personnel Management

View File

@@ -1,4 +1,4 @@
Forte — Music Store Management Platform LunarFront — Small Business Management Platform
Domain Design: Consignment Domain Design: Consignment

View File

@@ -1,4 +1,4 @@
Forte — Music Store Management Platform LunarFront — Small Business Management Platform
Domain Design: Sales Commission Domain Design: Sales Commission

View File

@@ -1,4 +1,4 @@
Forte — Music Store Management Platform LunarFront — Small Business Management Platform
Phase 2 Audit: Code Review, Security, and Feature Gap Analysis Phase 2 Audit: Code Review, Security, and Feature Gap Analysis

View File

@@ -8,9 +8,9 @@ Version 1.0 | Draft
# 1. Overview # 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. **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: After restart:
- Vault status: locked - Vault status: locked
- All vault API calls: 423 - 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 - Admin unlocks when ready — no urgency, no data loss
## 7.5 Multiple Servers ## 7.5 Multiple Servers