docs: update CLAUDE.md with CMS, API, and architecture details
Add headless CMS API documentation, port 8001 convention, dual role architecture, CMS content models, Next.js integration, and document library sections. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
85
CLAUDE.md
85
CLAUDE.md
@@ -4,13 +4,15 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
Taiwan NPO (Non-Profit Organization) management platform built with Laravel 10 (PHP 8.1+). Features member lifecycle management, multi-tier financial approval workflows, issue tracking, and document management. UI is in Traditional Chinese. Currency is TWD (NT$).
|
Taiwan NPO (Non-Profit Organization) management platform built with Laravel 10 (PHP 8.1+). Features member lifecycle management, multi-tier financial approval workflows, issue tracking, CMS with headless API, and document management. UI is in Traditional Chinese. Currency is TWD (NT$).
|
||||||
|
|
||||||
|
A companion Next.js frontend (`usher-site`) consumes the headless CMS API for the public website.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Development
|
# Development
|
||||||
php artisan serve && npm run dev # Start both servers
|
php artisan serve --port=8001 && npm run dev # Start both servers (port 8000 often occupied)
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
php artisan test # Run all tests
|
php artisan test # Run all tests
|
||||||
@@ -38,17 +40,56 @@ php artisan db:seed --class=FinancialWorkflowTestDataSeeder # Finance test data
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
### Dual Role: Admin App + Headless CMS API
|
||||||
|
|
||||||
|
This app serves two purposes:
|
||||||
|
1. **Admin app** — Blade-based UI at `/admin/*` for internal management (members, finance, issues, CMS)
|
||||||
|
2. **Headless CMS API** — JSON API at `/api/v1/*` consumed by the Next.js public site
|
||||||
|
|
||||||
### Route Structure
|
### Route Structure
|
||||||
|
|
||||||
Routes split across `routes/web.php` and `routes/auth.php`. Admin routes use group pattern:
|
Routes split across `routes/web.php`, `routes/auth.php`, and `routes/api.php`.
|
||||||
|
|
||||||
|
**Admin routes** use group pattern:
|
||||||
```php
|
```php
|
||||||
Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(...)
|
Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(...)
|
||||||
```
|
```
|
||||||
- **Public**: `/register/member`, `/documents`
|
- **Public**: `/register/member` (controlled by `REGISTRATION_ENABLED` env), `/documents`
|
||||||
- **Member** (auth only): dashboard, payment submission
|
- **Member** (auth only): dashboard, payment submission
|
||||||
- **Admin** (auth + admin): `/admin/*` with `admin.{module}.{action}` named routes
|
- **Admin** (auth + admin): `/admin/*` with `admin.{module}.{action}` named routes
|
||||||
|
|
||||||
The `admin` middleware (`EnsureUserIsAdmin`) allows access if user has `admin` role OR any permissions at all.
|
The `admin` middleware (`EnsureUserIsAdmin`) allows access if user has `admin` role OR any permissions at all. A separate `CheckPaidMembership` middleware gates member-only features.
|
||||||
|
|
||||||
|
**API routes** (`/api/v1/*`):
|
||||||
|
- `GET /articles` — paginated, filter by type/category/tag/search
|
||||||
|
- `GET /articles/{slug}` — detail with related articles
|
||||||
|
- `GET /categories` — all categories with article counts
|
||||||
|
- `GET /pages/{slug}` — page with hierarchical children
|
||||||
|
- `GET /homepage` — aggregated homepage data (featured, latest by type, about, categories)
|
||||||
|
- `GET /public-documents` — document library with search/filter
|
||||||
|
- `GET /public-documents/{uuid}` — document detail by UUID
|
||||||
|
|
||||||
|
**API response wrapping**: Single resources are wrapped in `{ "data": {...} }`. The homepage endpoint is NOT wrapped. Collections use Laravel pagination.
|
||||||
|
|
||||||
|
### CMS Content Models
|
||||||
|
|
||||||
|
**Article** — 4 content types (`news`, `notice`, `column`, `activity`), 4 access levels (`public`, `members`, `board`, `admin`), 3 statuses (`draft`, `published`, `archived`). Supports scheduled publishing (`published_at`), expiration (`expires_at`), pinning (`display_order`), EasyMDE markdown editor, and file attachments.
|
||||||
|
|
||||||
|
**Page** — Hierarchical parent/child pages with custom fields (JSON), optional template field. Used for static content like "About Us".
|
||||||
|
|
||||||
|
**Document** — Versioned document library with auto-generated `public_uuid` for shareable URLs. Semantic versioning (1.0, 1.1...), SHA256 file integrity, access levels, auto-archive on expiry. QR code generation at `/documents/{uuid}/qrcode`.
|
||||||
|
|
||||||
|
Common query scopes across content models: `active()`, `published()`, `forAccessLevel(?User)`, `pinned()`, `byContentType($type)`.
|
||||||
|
|
||||||
|
### Next.js Static Site Integration
|
||||||
|
|
||||||
|
The public site is fully static after `next build`. Laravel triggers cache revalidation on content changes:
|
||||||
|
|
||||||
|
- `SiteRevalidationService::revalidateArticle($slug)` — called in admin CMS controllers after publish/update
|
||||||
|
- `SiteRevalidationService::revalidateDocument($slug)` — called via `DocumentObserver` on CRUD
|
||||||
|
- Config in `config/services.php` → `nextjs.revalidate_url`, `nextjs.revalidate_token`
|
||||||
|
- `SiteAssetSyncService` copies uploads to the Next.js `public/` directory for static serving
|
||||||
|
- CORS allows `usher.org.tw`, `localhost:3000`, `*.vercel.app` (see `config/cors.php`)
|
||||||
|
|
||||||
### Multi-Tier Approval Workflows
|
### Multi-Tier Approval Workflows
|
||||||
|
|
||||||
@@ -81,27 +122,33 @@ Complex business logic in `app/Services/`:
|
|||||||
- `SettingsService`: System-wide settings with caching
|
- `SettingsService`: System-wide settings with caching
|
||||||
- `FinanceDocumentApprovalService`: Multi-tier approval orchestration
|
- `FinanceDocumentApprovalService`: Multi-tier approval orchestration
|
||||||
- `PaymentVerificationService`: Payment workflow coordination
|
- `PaymentVerificationService`: Payment workflow coordination
|
||||||
|
- `SiteRevalidationService`: Next.js cache invalidation webhook
|
||||||
|
- `SiteAssetSyncService`: Copy uploads to Next.js `public/` for static serving
|
||||||
|
|
||||||
Global helper: `settings('key')` returns cached setting values (defined in `app/helpers.php`, autoloaded).
|
Global helper: `settings('key')` returns cached setting values (defined in `app/helpers.php`, autoloaded).
|
||||||
|
|
||||||
### RBAC Structure
|
### RBAC Structure
|
||||||
|
|
||||||
Uses Spatie Laravel Permission. Core financial roles:
|
Uses Spatie Laravel Permission. Core roles:
|
||||||
- `finance_requester`, `finance_cashier`, `finance_accountant`, `finance_chair`, `finance_board_member`
|
- **Financial**: `finance_requester`, `finance_cashier`, `finance_accountant`, `finance_chair`, `finance_board_member`
|
||||||
|
- **CMS**: `secretary_general` (full CMS), `membership_manager` (article management)
|
||||||
|
- `admin` role has full access
|
||||||
|
|
||||||
|
CMS permissions: `view/create/edit/delete/publish_articles`, `manage_all_articles`, plus equivalents for pages and announcements.
|
||||||
|
|
||||||
Permission checks: `$user->can('permission-name')` or `@can('permission-name')` in Blade.
|
Permission checks: `$user->can('permission-name')` or `@can('permission-name')` in Blade.
|
||||||
|
|
||||||
### Data Security Patterns
|
### Data Security Patterns
|
||||||
|
|
||||||
- **National ID**: AES-256 encrypted (`national_id_encrypted`), SHA256 hashed for search (`national_id_hash`), virtual accessor `national_id` decrypts on read
|
- **National ID**: AES-256 encrypted (`national_id_encrypted`), SHA256 hashed for search (`national_id_hash`), virtual accessor `national_id` decrypts on read
|
||||||
- **File uploads**: Private disk storage, served via authenticated controller
|
- **File uploads**: Private disk storage, served via authenticated controller. `DownloadFile` helper handles RFC 5987 UTF-8 filenames (Chinese filename support)
|
||||||
- **Audit logging**: `AuditLogger::log($action, $auditable, $metadata)` — static class, call in controllers
|
- **Audit logging**: `AuditLogger::log($action, $auditable, $metadata)` — static class, call in controllers (not automatic via observers)
|
||||||
|
|
||||||
### Member Lifecycle
|
### Member Lifecycle
|
||||||
|
|
||||||
States: `pending` → `active` → `expired` / `suspended`
|
States: `pending` → `active` → `expired` / `suspended`
|
||||||
|
|
||||||
Identity types: `patient`, `parent`, `social`, `other` (病友/家長分類). Members can have `guardian_member_id` for parent-child relationships.
|
Identity types: `patient`, `parent`, `social`, `other` (病友/家長分類). Members can have `guardian_member_id` for parent-child relationships. Members have optional `line_id` for LINE messaging.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$member->hasPaidMembership() // Active with future expiry
|
$member->hasPaidMembership() // Active with future expiry
|
||||||
@@ -109,6 +156,12 @@ $member->canSubmitPayment() // Pending with no pending payment
|
|||||||
$member->getNextFeeType() // entrance_fee or annual_fee
|
$member->getNextFeeType() // entrance_fee or annual_fee
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Public registration controlled by `REGISTRATION_ENABLED` env var — when disabled, existing members can still login but `/register/member` routes are not registered.
|
||||||
|
|
||||||
|
### Issue Tracking
|
||||||
|
|
||||||
|
Built-in issue tracker with auto-generated numbers (`ISS-{year}-{seq}`). Statuses: `new` → `assigned` → `in_progress` → `review` → `closed`. Types: `work_item`, `project_task`, `maintenance`, `member_request`. Supports parent/child relationships, time logging (estimated vs actual hours), and member association.
|
||||||
|
|
||||||
### Conventions
|
### Conventions
|
||||||
|
|
||||||
- **Status values**: Defined as class constants (e.g., `FinanceDocument::STATUS_PENDING`), not enums or magic strings
|
- **Status values**: Defined as class constants (e.g., `FinanceDocument::STATUS_PENDING`), not enums or magic strings
|
||||||
@@ -116,13 +169,15 @@ $member->getNextFeeType() // entrance_fee or annual_fee
|
|||||||
- **DB transactions**: Used in controllers for multi-step operations
|
- **DB transactions**: Used in controllers for multi-step operations
|
||||||
- **UI text**: Hardcoded Traditional Chinese strings (no `__()` translation layer)
|
- **UI text**: Hardcoded Traditional Chinese strings (no `__()` translation layer)
|
||||||
- **Dark mode**: Supported throughout — use `dark:` Tailwind prefix for styles
|
- **Dark mode**: Supported throughout — use `dark:` Tailwind prefix for styles
|
||||||
|
- **Blade stacks**: `@stack('styles')` in `<head>` and `@stack('scripts')` before `</body>` for page-specific assets (EasyMDE, charts, etc.)
|
||||||
|
|
||||||
### Custom Artisan Commands
|
### Custom Artisan Commands
|
||||||
|
|
||||||
- `assign:role` — Manual role assignment
|
- `assign:role` — Manual role assignment
|
||||||
- `import:members` / `import:accounting-data` / `import:documents` — Bulk imports
|
- `import:members` / `import:accounting-data` / `import:documents` / `import:article-documents` — Bulk imports
|
||||||
- `send:membership-expiry-reminders` — Automated email notifications
|
- `send:membership-expiry-reminders` — Automated email notifications
|
||||||
- `archive:expired-documents` — Document lifecycle cleanup
|
- `documents:archive-expired` — Auto-archive documents past expiry (respects `auto_archive_on_expiry` flag)
|
||||||
|
- `nextjs:push-assets` — Manual trigger for git push of synced assets to Next.js repo
|
||||||
|
|
||||||
### Testing Patterns
|
### Testing Patterns
|
||||||
|
|
||||||
@@ -145,11 +200,17 @@ Test accounts (password: `password`):
|
|||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
- `routes/web.php` — All web routes (admin routes under `/admin` prefix)
|
- `routes/web.php` — All web routes (admin routes under `/admin` prefix)
|
||||||
|
- `routes/api.php` — Headless CMS API v1 routes
|
||||||
- `config/accounting.php` — Account codes, amount tier thresholds, currency settings
|
- `config/accounting.php` — Account codes, amount tier thresholds, currency settings
|
||||||
|
- `config/services.php` → `nextjs` — Next.js integration config (revalidation, asset sync)
|
||||||
|
- `config/cors.php` — CORS allowed origins for API
|
||||||
- `app/Models/FinanceDocument.php` — Core financial workflow logic with 27+ status constants
|
- `app/Models/FinanceDocument.php` — Core financial workflow logic with 27+ status constants
|
||||||
- `app/Models/Member.php` — Member lifecycle with encrypted field handling
|
- `app/Models/Member.php` — Member lifecycle with encrypted field handling
|
||||||
|
- `app/Models/Article.php` — CMS article with content types, access levels, scopes
|
||||||
|
- `app/Models/Document.php` — Versioned document library with UUID access
|
||||||
- `app/Traits/HasApprovalWorkflow.php` — Shared multi-tier approval behavior
|
- `app/Traits/HasApprovalWorkflow.php` — Shared multi-tier approval behavior
|
||||||
- `app/Traits/HasAccountingEntries.php` — Double-entry bookkeeping behavior
|
- `app/Traits/HasAccountingEntries.php` — Double-entry bookkeeping behavior
|
||||||
|
- `app/Services/SiteRevalidationService.php` — Next.js revalidation webhook
|
||||||
- `app/helpers.php` — Global `settings()` helper (autoloaded via composer)
|
- `app/helpers.php` — Global `settings()` helper (autoloaded via composer)
|
||||||
- `database/seeders/` — RoleSeeder, ChartOfAccountSeeder, TestDataSeeder
|
- `database/seeders/` — RoleSeeder, ChartOfAccountSeeder, TestDataSeeder
|
||||||
- `docs/SYSTEM_SPECIFICATION.md` — Complete system specification
|
- `docs/SYSTEM_SPECIFICATION.md` — Complete system specification
|
||||||
|
|||||||
Reference in New Issue
Block a user