Move business logic out of App Router into module packages, add boundary validation scripts, and keep all routes as thin re-exports without changing URLs or API behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
266 lines
11 KiB
Markdown
266 lines
11 KiB
Markdown
# TorbatYar Frontend — Migration Plan
|
||
|
||
**Date:** 2026-07-26
|
||
**Status:** Planning document — no code changes applied
|
||
**Prerequisite:** Read [frontend-architecture-audit.md](./frontend-architecture-audit.md)
|
||
|
||
This plan prioritizes **stabilization before restructuring**. Each phase is independently shippable.
|
||
|
||
---
|
||
|
||
## Guiding Principles
|
||
|
||
1. **Do not refactor working modules mid-feature** — changes are incremental and gated
|
||
2. **Enforce boundaries with tooling**, not convention alone
|
||
3. **Accounting follows beauty/healthcare route patterns** over time, not immediately
|
||
4. **Shared abstractions emerge from deduplication**, not upfront framework design
|
||
5. **Keep the monolith** until build times or team size force extraction
|
||
|
||
---
|
||
|
||
## Phase 0 — Stabilize (Week 1)
|
||
|
||
**Goal:** Green build, remove dead weight, zero functional changes.
|
||
|
||
| Task | Files | Effort | Risk |
|
||
|------|-------|--------|------|
|
||
| Fix DS `Button` type error OR remove `loading` prop usage | `app/accounting/purchase/returns/page.tsx`, possibly `components/ds/Button.tsx` | S | Low |
|
||
| Remove unused files (5 confirmed dead) | `lib/api-client.ts`, `lib/optional-sso.ts`, `lib/beauty-business-nav.ts`, `components/HealthStatus.tsx`, `components/beauty/pages/mobile.tsx` | S | Low |
|
||
| Remove or use unused npm deps | `@tanstack/react-table`, `recharts` from `package.json` | S | Low |
|
||
| Add `npm run typecheck` script | `package.json` | S | None |
|
||
| Verify `npm run build` passes in CI | CI config | S | None |
|
||
|
||
**Exit criteria:** `npm run build` succeeds; no files with zero imports (except intentional barrels).
|
||
|
||
---
|
||
|
||
## Phase 1 — Boundary Enforcement (Weeks 2–3)
|
||
|
||
**Goal:** Prevent architectural regression as new modules are added.
|
||
|
||
| Task | Description | Effort |
|
||
|------|-------------|--------|
|
||
| Add ESLint `no-restricted-imports` rules | Block `@/components/accounting/*` from beauty/healthcare and vice versa | M |
|
||
| Add ESLint rule: DS must not import domain code | Already clean — add rule to keep it that way | S |
|
||
| Document allowed import matrix | See [module-boundaries.md](./module-boundaries.md) | S |
|
||
| Add path aliases (optional) | `@/ds/*`, `@/platform/*` — reduce `@/` ambiguity | M |
|
||
| Consolidate beauty owner routes | Merge `beauty/(owner)/*` CRUD into `components/beauty/pages/owner.tsx` pattern OR merge `owner/` into `(owner)/` | L |
|
||
|
||
**Exit criteria:** Lint fails on cross-domain imports; beauty owner has single URL tree.
|
||
|
||
---
|
||
|
||
## Phase 2 — Shared Portal Infrastructure (Weeks 4–6)
|
||
|
||
**Goal:** Extract beauty/healthcare duplication into reusable platform layer.
|
||
|
||
### 2.1 Create `components/portals/` (shared)
|
||
|
||
Extract from duplicated files:
|
||
|
||
| Shared abstraction | Source files |
|
||
|--------------------|--------------|
|
||
| `createPortalLayout()` | `beauty/createPortalLayout.tsx`, `healthcare/createPortalLayout.tsx` |
|
||
| `PortalShell` base | `BeautyPortalShell`, `HealthcarePortalShell`, `AccountingShell` (~90% overlap) |
|
||
| `pages/shared.tsx` helpers | `beauty/pages/shared.tsx`, `healthcare/pages/shared.tsx` |
|
||
| `PublicSiteLayout` | `PublicBeautyLayout`, `PublicHealthcareLayout` |
|
||
| Accent-token cards/chips | `BeautyCards`, `HealthcareCards`, `BeautyStatusChip`, `MedicalStatusChip` |
|
||
|
||
**Parameterize by:**
|
||
- Portal ID type (generic)
|
||
- Nav config (`lib/*-portals.ts` stays domain-specific)
|
||
- CSS accent variable (`--beauty-accent` vs `--health-accent`)
|
||
|
||
### 2.2 Normalize module DS folder naming
|
||
|
||
Pick one convention:
|
||
- **Option A (recommended):** `components/{module}/design-system/` for both beauty and healthcare
|
||
- **Option B:** Move module-specific tokens to `components/portals/tokens.ts` with domain presets
|
||
|
||
**Exit criteria:** Single `createPortalLayout` factory; portal shells share one base component; healthcare `ui/` renamed to `design-system/`.
|
||
|
||
---
|
||
|
||
## Phase 3 — API Client Decomposition (Weeks 7–10)
|
||
|
||
**Goal:** Split monolithic API clients into maintainable modules.
|
||
|
||
### Current state
|
||
|
||
| File | Lines | Methods (approx) |
|
||
|------|------:|------------------|
|
||
| `lib/accounting-api.ts` | 1,856 | 200+ |
|
||
| `lib/beauty-business-api.ts` | 1,446 | 150+ |
|
||
| `lib/healthcare-api.ts` | 1,322 | 120+ |
|
||
|
||
### Target structure
|
||
|
||
```
|
||
lib/
|
||
├── api/
|
||
│ ├── core.ts # Shared fetch wrapper, error class, token refresh
|
||
│ ├── accounting/
|
||
│ │ ├── index.ts # Re-export barrel
|
||
│ │ ├── vouchers.ts
|
||
│ │ ├── treasury.ts
|
||
│ │ ├── inventory.ts
|
||
│ │ └── ...
|
||
│ ├── beauty/
|
||
│ │ ├── index.ts
|
||
│ │ ├── appointments.ts
|
||
│ │ ├── catalog.ts
|
||
│ │ └── ...
|
||
│ └── healthcare/
|
||
│ ├── index.ts
|
||
│ └── ...
|
||
├── accounting-api.ts # Deprecated barrel → re-exports from api/accounting/
|
||
```
|
||
|
||
**Migration strategy:**
|
||
1. Extract shared `createApiClient(baseUrl, serviceName)` helper
|
||
2. Move one domain subdomain at a time (e.g. `accounting/vouchers`)
|
||
3. Keep deprecated barrel exports until all imports updated
|
||
4. Run grep after each subdomain to verify zero old imports
|
||
|
||
**Exit criteria:** No API file exceeds 400 lines; shared fetch logic in one place.
|
||
|
||
---
|
||
|
||
## Phase 4 — Accounting Route Extraction (Weeks 11–16)
|
||
|
||
**Goal:** Align accounting with beauty/healthcare thin-route pattern.
|
||
|
||
### Priority extractions (largest route files)
|
||
|
||
| Route file | Lines | Target component |
|
||
|------------|------:|------------------|
|
||
| `app/accounting/chart-of-accounts/page.tsx` | 644 | `components/accounting/ChartOfAccountsPage.tsx` |
|
||
| `app/accounting/reports/page.tsx` | 663 | `components/accounting/ReportsPage.tsx` |
|
||
| `app/accounting/fiscal/page.tsx` | 579 | `components/accounting/FiscalPage.tsx` |
|
||
| `app/accounting/vouchers/[id]/page.tsx` | 550 | `components/accounting/VoucherDetailPage.tsx` |
|
||
| `app/accounting/treasury/page.tsx` | 490 | `components/accounting/TreasuryPage.tsx` |
|
||
| `app/accounting/payroll/page.tsx` | 457 | `components/accounting/PayrollPage.tsx` |
|
||
|
||
### Screen aggregator split
|
||
|
||
| Current file | Lines | Action |
|
||
|--------------|------:|--------|
|
||
| `BusinessDocsPage.tsx` | 1,328 | Split by document type |
|
||
| `WorkflowScreens.tsx` | 1,132 | Split by workflow domain |
|
||
| `OperationalDocumentPage.tsx` | 996 | Keep; already reusable |
|
||
| `DomainScreens.tsx` | 652 | Split by domain section |
|
||
|
||
**Pattern:**
|
||
```tsx
|
||
// app/accounting/reports/page.tsx (after)
|
||
export { ReportsPage as default } from "@/components/accounting/ReportsPage";
|
||
```
|
||
|
||
**Exit criteria:** No accounting `page.tsx` exceeds 50 lines except dynamic route params; no component exceeds 500 lines.
|
||
|
||
---
|
||
|
||
## Phase 5 — Platform Hardening (Weeks 17–20)
|
||
|
||
**Goal:** Production-grade resilience and auth.
|
||
|
||
| Task | Description |
|
||
|------|-------------|
|
||
| Add `middleware.ts` | Edge auth check, tenant header injection, public route allowlist |
|
||
| Add segment `error.tsx` | Per-module error boundaries (`accounting/`, `beauty/`, `healthcare/`) |
|
||
| Add segment `loading.tsx` | Skeleton states using DS `LoadingState` / `Skeleton` |
|
||
| Conditional SiteHeader | Hide platform header on `/beauty/site/*`, `/healthcare/site/*` |
|
||
| BFF for Core API | `/api/core/[...path]` proxy for admin/dashboard |
|
||
| Bundle analysis | `@next/bundle-analyzer` baseline before module #4 |
|
||
|
||
---
|
||
|
||
## Phase 6 — New Module Template (Ongoing)
|
||
|
||
When adding module #4 (e.g. Delivery, CRM, Hospitality):
|
||
|
||
### Required scaffold
|
||
|
||
```
|
||
app/{module}/
|
||
├── layout.tsx # pass-through or module shell
|
||
├── hub/page.tsx # portal picker
|
||
├── {portal}/layout.tsx # createPortalLayout("{portal}")
|
||
├── {portal}/.../page.tsx # thin re-exports
|
||
└── site/layout.tsx # public site (if applicable)
|
||
|
||
components/{module}/
|
||
├── design-system/
|
||
│ ├── tokens.ts
|
||
│ └── index.ts
|
||
├── pages/
|
||
│ ├── shared.tsx
|
||
│ └── {portal}.tsx
|
||
├── {Module}PortalShell.tsx # extends shared PortalShell
|
||
└── createPortalLayout.tsx # OR import from components/portals/
|
||
|
||
lib/
|
||
├── {module}-api.ts # OR lib/api/{module}/
|
||
└── {module}-portals.ts
|
||
|
||
app/api/{module}/[...path]/route.ts
|
||
```
|
||
|
||
### Checklist before merge
|
||
|
||
- [ ] Zero imports from other business modules
|
||
- [ ] All pages use `@/components/ds` (not `components/ui`)
|
||
- [ ] BFF proxy route exists
|
||
- [ ] Nav config in `lib/{module}-portals.ts`
|
||
- [ ] Query keys prefixed `["{module}", tenantId, ...]`
|
||
- [ ] ESLint boundary rules pass
|
||
|
||
---
|
||
|
||
## Phase Summary Timeline
|
||
|
||
```
|
||
Phase 0 ████ Stabilize (Week 1)
|
||
Phase 1 ░░░░████ Boundaries (Weeks 2–3)
|
||
Phase 2 ░░░░░░░░████████ Portal infra (Weeks 4–6)
|
||
Phase 3 ░░░░░░░░░░░░░░████████████ API clients (Weeks 7–10)
|
||
Phase 4 ░░░░░░░░░░░░░░░░░░░░████████████████ Accounting routes (Weeks 11–16)
|
||
Phase 5 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░████████ Hardening (Weeks 17–20)
|
||
Phase 6 ────────────────────────────────────► Module template (ongoing)
|
||
```
|
||
|
||
---
|
||
|
||
## What NOT to do
|
||
|
||
| Anti-pattern | Reason |
|
||
|--------------|--------|
|
||
| Split into npm packages prematurely | Team size doesn't justify overhead yet |
|
||
| Introduce Redux/Zustand | TanStack Query + local state is sufficient |
|
||
| Rewrite accounting in beauty page-bundle style in one PR | Too risky; extract file-by-file |
|
||
| Move public sites to separate Next.js app | Shared auth/tenant infra makes monolith correct for now |
|
||
| Adopt TanStack Table without requirement | Custom DataTable works; add features when needed |
|
||
|
||
---
|
||
|
||
## Success Metrics
|
||
|
||
| Metric | Current | Phase 0 | Phase 4 | Phase 5 |
|
||
|--------|---------|---------|---------|---------|
|
||
| Build passes | ❌ | ✅ | ✅ | ✅ |
|
||
| Max page.tsx lines (accounting) | 663 | 663 | ≤50 | ≤50 |
|
||
| Max component file lines | 1,856 | 1,856 | ≤500 | ≤500 |
|
||
| Cross-domain imports | 0 | 0 | 0 | 0 (enforced) |
|
||
| Dead files | 5 | 0 | 0 | 0 |
|
||
| Unused npm deps | 2 | 0 | 0 | 0 |
|
||
| Portal factory implementations | 2 | 2 | 1 | 1 |
|
||
| Middleware coverage | 0% | 0% | 0% | 100% public/private |
|
||
|
||
---
|
||
|
||
## Related Documents
|
||
|
||
- [frontend-architecture-audit.md](./frontend-architecture-audit.md)
|
||
- [module-boundaries.md](./module-boundaries.md)
|
||
- [refactor-risk-analysis.md](./refactor-risk-analysis.md)
|