From 5d89079c2aa5d4b6aa4feae340f926cf5a85550c Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 18 Mar 2026 11:35:51 +0700 Subject: [PATCH] 260318:1135 Fix UUID #3 --- .windsurf/workflows/00-speckit.all.md | 85 +++++++++++ .../workflows/01-speckit.constitution.md | 18 +++ .windsurf/workflows/02-speckit.specify.md | 19 +++ .windsurf/workflows/03-speckit.clarify.md | 18 +++ .windsurf/workflows/04-speckit.plan.md | 18 +++ .windsurf/workflows/05-speckit.tasks.md | 19 +++ .windsurf/workflows/06-speckit.analyze.md | 22 +++ .windsurf/workflows/07-speckit.implement.md | 20 +++ .windsurf/workflows/08-speckit.checker.md | 21 +++ .windsurf/workflows/09-speckit.tester.md | 21 +++ .windsurf/workflows/10-speckit.reviewer.md | 19 +++ .windsurf/workflows/11-speckit.validate.md | 19 +++ .windsurf/workflows/create-backend-module.md | 51 +++++++ .windsurf/workflows/create-frontend-page.md | 64 ++++++++ .windsurf/workflows/deploy.md | 71 +++++++++ .windsurf/workflows/schema-change.md | 108 ++++++++++++++ .windsurf/workflows/speckit.prepare.md | 27 ++++ .windsurf/workflows/util-speckit.checklist.md | 18 +++ .windsurf/workflows/util-speckit.diff.md | 19 +++ .windsurf/workflows/util-speckit.migrate.md | 19 +++ .windsurf/workflows/util-speckit.quizme.md | 20 +++ .windsurf/workflows/util-speckit.status.md | 20 +++ .../workflows/util-speckit.taskstoissues.md | 18 +++ .../circulation/circulation.service.ts | 75 +++++++++- .../circulation/dto/create-circulation.dto.ts | 12 +- .../correspondence/correspondence.service.ts | 138 +++++++++++++----- .../dto/create-correspondence.dto.ts | 12 +- .../services/document-numbering.service.ts | 40 ++++- .../src/modules/master/entities/tag.entity.ts | 8 + backend/src/modules/master/master.service.ts | 5 +- .../modules/rfa/entities/rfa-type.entity.ts | 15 +- .../reference/disciplines/page.tsx | 15 +- .../doc-control/reference/rfa-types/page.tsx | 13 +- .../admin/doc-control/reference/tags/page.tsx | 21 ++- .../app/(dashboard)/circulation/new/page.tsx | 36 ++--- frontend/components/correspondences/form.tsx | 30 ++-- frontend/components/drawings/columns.tsx | 5 +- .../components/numbering/template-tester.tsx | 4 +- frontend/hooks/use-drawing.ts | 6 +- frontend/lib/api/numbering.ts | 8 +- frontend/types/circulation.ts | 6 +- .../dto/circulation/create-circulation.dto.ts | 14 +- .../create-correspondence.dto.ts | 8 +- 43 files changed, 1073 insertions(+), 132 deletions(-) create mode 100644 .windsurf/workflows/00-speckit.all.md create mode 100644 .windsurf/workflows/01-speckit.constitution.md create mode 100644 .windsurf/workflows/02-speckit.specify.md create mode 100644 .windsurf/workflows/03-speckit.clarify.md create mode 100644 .windsurf/workflows/04-speckit.plan.md create mode 100644 .windsurf/workflows/05-speckit.tasks.md create mode 100644 .windsurf/workflows/06-speckit.analyze.md create mode 100644 .windsurf/workflows/07-speckit.implement.md create mode 100644 .windsurf/workflows/08-speckit.checker.md create mode 100644 .windsurf/workflows/09-speckit.tester.md create mode 100644 .windsurf/workflows/10-speckit.reviewer.md create mode 100644 .windsurf/workflows/11-speckit.validate.md create mode 100644 .windsurf/workflows/create-backend-module.md create mode 100644 .windsurf/workflows/create-frontend-page.md create mode 100644 .windsurf/workflows/deploy.md create mode 100644 .windsurf/workflows/schema-change.md create mode 100644 .windsurf/workflows/speckit.prepare.md create mode 100644 .windsurf/workflows/util-speckit.checklist.md create mode 100644 .windsurf/workflows/util-speckit.diff.md create mode 100644 .windsurf/workflows/util-speckit.migrate.md create mode 100644 .windsurf/workflows/util-speckit.quizme.md create mode 100644 .windsurf/workflows/util-speckit.status.md create mode 100644 .windsurf/workflows/util-speckit.taskstoissues.md diff --git a/.windsurf/workflows/00-speckit.all.md b/.windsurf/workflows/00-speckit.all.md new file mode 100644 index 0000000..d9736e7 --- /dev/null +++ b/.windsurf/workflows/00-speckit.all.md @@ -0,0 +1,85 @@ +--- +description: Run the full speckit pipeline from specification to analysis in one command. +--- + +# Workflow: speckit.all + +This meta-workflow orchestrates the **complete development lifecycle**, from specification through implementation and validation. For the preparation-only pipeline (steps 1-5), use `/speckit.prepare` instead. + +## Preparation Phase (Steps 1-5) + +1. **Specify** (`/speckit.specify`): + - Use the `view_file` tool to read: `.agents/skills/speckit.specify/SKILL.md` + - Execute with user's feature description + - Creates: `spec.md` + +2. **Clarify** (`/speckit.clarify`): + - Use the `view_file` tool to read: `.agents/skills/speckit.clarify/SKILL.md` + - Execute to resolve ambiguities + - Updates: `spec.md` + +3. **Plan** (`/speckit.plan`): + - Use the `view_file` tool to read: `.agents/skills/speckit.plan/SKILL.md` + - Execute to create technical design + - Creates: `plan.md` + +4. **Tasks** (`/speckit.tasks`): + - Use the `view_file` tool to read: `.agents/skills/speckit.tasks/SKILL.md` + - Execute to generate task breakdown + - Creates: `tasks.md` + +5. **Analyze** (`/speckit.analyze`): + - Use the `view_file` tool to read: `.agents/skills/speckit.analyze/SKILL.md` + - Execute to validate consistency across spec, plan, and tasks + - Output: Analysis report + - **Gate**: If critical issues found, stop and fix before proceeding + +## Implementation Phase (Steps 6-7) + +6. **Implement** (`/speckit.implement`): + - Use the `view_file` tool to read: `.agents/skills/speckit.implement/SKILL.md` + - Execute all tasks from `tasks.md` with anti-regression protocols + - Output: Working implementation + +7. **Check** (`/speckit.checker`): + - Use the `view_file` tool to read: `.agents/skills/speckit.checker/SKILL.md` + - Run static analysis (linters, type checkers, security scanners) + - Output: Checker report + +## Verification Phase (Steps 8-10) + +8. **Test** (`/speckit.tester`): + - Use the `view_file` tool to read: `.agents/skills/speckit.tester/SKILL.md` + - Run tests with coverage + - Output: Test + coverage report + +9. **Review** (`/speckit.reviewer`): + - Use the `view_file` tool to read: `.agents/skills/speckit.reviewer/SKILL.md` + - Perform code review + - Output: Review report with findings + +10. **Validate** (`/speckit.validate`): + - Use the `view_file` tool to read: `.agents/skills/speckit.validate/SKILL.md` + - Verify implementation matches spec requirements + - Output: Validation report (pass/fail) + +## Usage + +``` +/speckit.all "Build a user authentication system with OAuth2 support" +``` + +## Pipeline Comparison + +| Pipeline | Steps | Use When | +| ------------------ | ------------------------- | -------------------------------------- | +| `/speckit.prepare` | 1-5 (Specify → Analyze) | Planning only — you'll implement later | +| `/speckit.all` | 1-10 (Specify → Validate) | Full lifecycle in one pass | + +## On Error + +If any step fails, stop the pipeline and report: + +- Which step failed +- The error message +- Suggested remediation (e.g., "Run `/speckit.clarify` to resolve ambiguities before continuing") diff --git a/.windsurf/workflows/01-speckit.constitution.md b/.windsurf/workflows/01-speckit.constitution.md new file mode 100644 index 0000000..96544a0 --- /dev/null +++ b/.windsurf/workflows/01-speckit.constitution.md @@ -0,0 +1,18 @@ +--- +description: Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync. +--- + +# Workflow: speckit.constitution + +1. **Context Analysis**: + - The user has provided an input prompt. Treat this as the primary input for the skill. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.constitution/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If `.specify/` directory doesn't exist: Initialize the speckit structure first diff --git a/.windsurf/workflows/02-speckit.specify.md b/.windsurf/workflows/02-speckit.specify.md new file mode 100644 index 0000000..69fd061 --- /dev/null +++ b/.windsurf/workflows/02-speckit.specify.md @@ -0,0 +1,19 @@ +--- +description: Create or update the feature specification from a natural language feature description. +--- + +# Workflow: speckit.specify + +1. **Context Analysis**: + - The user has provided an input prompt. Treat this as the primary input for the skill. + - This is typically the starting point of a new feature. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.specify/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the feature description for the skill's logic. + +4. **On Error**: + - If no feature description provided: Ask the user to describe the feature they want to specify diff --git a/.windsurf/workflows/03-speckit.clarify.md b/.windsurf/workflows/03-speckit.clarify.md new file mode 100644 index 0000000..9217be3 --- /dev/null +++ b/.windsurf/workflows/03-speckit.clarify.md @@ -0,0 +1,18 @@ +--- +description: Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec. +--- + +# Workflow: speckit.clarify + +1. **Context Analysis**: + - The user has provided an input prompt. Treat this as the primary input for the skill. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.clarify/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If `spec.md` is missing: Run `/speckit.specify` first to create the feature specification diff --git a/.windsurf/workflows/04-speckit.plan.md b/.windsurf/workflows/04-speckit.plan.md new file mode 100644 index 0000000..456b83c --- /dev/null +++ b/.windsurf/workflows/04-speckit.plan.md @@ -0,0 +1,18 @@ +--- +description: Execute the implementation planning workflow using the plan template to generate design artifacts. +--- + +# Workflow: speckit.plan + +1. **Context Analysis**: + - The user has provided an input prompt. Treat this as the primary input for the skill. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.plan/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If `spec.md` is missing: Run `/speckit.specify` first to create the feature specification diff --git a/.windsurf/workflows/05-speckit.tasks.md b/.windsurf/workflows/05-speckit.tasks.md new file mode 100644 index 0000000..54967d0 --- /dev/null +++ b/.windsurf/workflows/05-speckit.tasks.md @@ -0,0 +1,19 @@ +--- +description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts. +--- + +# Workflow: speckit.tasks + +1. **Context Analysis**: + - The user has provided an input prompt. Treat this as the primary input for the skill. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.tasks/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If `plan.md` is missing: Run `/speckit.plan` first + - If `spec.md` is missing: Run `/speckit.specify` first diff --git a/.windsurf/workflows/06-speckit.analyze.md b/.windsurf/workflows/06-speckit.analyze.md new file mode 100644 index 0000000..a177a65 --- /dev/null +++ b/.windsurf/workflows/06-speckit.analyze.md @@ -0,0 +1,22 @@ +--- +description: Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation. +--- + +// turbo-all + +# Workflow: speckit.analyze + +1. **Context Analysis**: + - The user has provided an input prompt. Treat this as the primary input for the skill. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.analyze/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If `spec.md` is missing: Run `/speckit.specify` first + - If `plan.md` is missing: Run `/speckit.plan` first + - If `tasks.md` is missing: Run `/speckit.tasks` first diff --git a/.windsurf/workflows/07-speckit.implement.md b/.windsurf/workflows/07-speckit.implement.md new file mode 100644 index 0000000..9a23850 --- /dev/null +++ b/.windsurf/workflows/07-speckit.implement.md @@ -0,0 +1,20 @@ +--- +description: Execute the implementation plan by processing and executing all tasks defined in tasks.md +--- + +# Workflow: speckit.implement + +1. **Context Analysis**: + - The user has provided an input prompt. Treat this as the primary input for the skill. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.implement/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If `tasks.md` is missing: Run `/speckit.tasks` first + - If `plan.md` is missing: Run `/speckit.plan` first + - If `spec.md` is missing: Run `/speckit.specify` first diff --git a/.windsurf/workflows/08-speckit.checker.md b/.windsurf/workflows/08-speckit.checker.md new file mode 100644 index 0000000..821544b --- /dev/null +++ b/.windsurf/workflows/08-speckit.checker.md @@ -0,0 +1,21 @@ +--- +description: Run static analysis tools and aggregate results. +--- + +// turbo-all + +# Workflow: speckit.checker + +1. **Context Analysis**: + - The user may specify paths to check or run on entire project. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.checker/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If no linting tools available: Report which tools to install based on project type + - If tools fail: Show raw error and suggest config fixes diff --git a/.windsurf/workflows/09-speckit.tester.md b/.windsurf/workflows/09-speckit.tester.md new file mode 100644 index 0000000..80f1eab --- /dev/null +++ b/.windsurf/workflows/09-speckit.tester.md @@ -0,0 +1,21 @@ +--- +description: Execute tests, measure coverage, and report results. +--- + +// turbo-all + +# Workflow: speckit.tester + +1. **Context Analysis**: + - The user may specify test paths, options, or just run all tests. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.tester/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If no test framework detected: Report "No test framework found. Install Jest, Vitest, Pytest, or similar." + - If tests fail: Show failure details and suggest fixes diff --git a/.windsurf/workflows/10-speckit.reviewer.md b/.windsurf/workflows/10-speckit.reviewer.md new file mode 100644 index 0000000..e5e18ef --- /dev/null +++ b/.windsurf/workflows/10-speckit.reviewer.md @@ -0,0 +1,19 @@ +--- +description: Perform code review with actionable feedback and suggestions. +--- + +# Workflow: speckit.reviewer + +1. **Context Analysis**: + - The user may specify files to review, "staged" for git staged changes, or "branch" for branch diff. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.reviewer/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If no files to review: Ask user to stage changes or specify file paths + - If not a git repo: Review current directory files instead diff --git a/.windsurf/workflows/11-speckit.validate.md b/.windsurf/workflows/11-speckit.validate.md new file mode 100644 index 0000000..fbc20d1 --- /dev/null +++ b/.windsurf/workflows/11-speckit.validate.md @@ -0,0 +1,19 @@ +--- +description: Validate that implementation matches specification requirements. +--- + +# Workflow: speckit.validate + +1. **Context Analysis**: + - The user has provided an input prompt. Treat this as the primary input for the skill. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.validate/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If `tasks.md` is missing: Run `/speckit.tasks` first + - If implementation not started: Run `/speckit.implement` first diff --git a/.windsurf/workflows/create-backend-module.md b/.windsurf/workflows/create-backend-module.md new file mode 100644 index 0000000..78cf13d --- /dev/null +++ b/.windsurf/workflows/create-backend-module.md @@ -0,0 +1,51 @@ +--- +description: Create a new NestJS backend feature module following project standards +--- + +# Create NestJS Backend Module + +Use this workflow when creating a new feature module in `backend/src/modules/`. +Follows `specs/05-Engineering-Guidelines/05-02-backend-guidelines.md` and ADR-005. + +## Steps + +// turbo + +1. **Verify requirements exist** — confirm the feature is in `specs/01-Requirements/` before starting + +// turbo 2. **Check schema** — read `specs/03-Data-and-Storage/lcbp3-v1.7.0-schema.sql` for relevant tables + +3. **Scaffold module folder** + +``` +backend/src/modules// +├── .module.ts +├── .controller.ts +├── .service.ts +├── dto/ +│ ├── create-.dto.ts +│ └── update-.dto.ts +├── entities/ +│ └── .entity.ts +└── .controller.spec.ts +``` + +4. **Create Entity** — map ONLY columns defined in the schema SQL. Use TypeORM decorators. Add `@VersionColumn()` if the entity needs optimistic locking. + +5. **Create DTOs** — use `class-validator` decorators. Never use `any`. Validate all inputs. + +6. **Create Service** — inject repository via constructor DI. Use transactions for multi-step writes. Add `Idempotency-Key` guard for POST/PUT/PATCH operations. + +7. **Create Controller** — apply `@UseGuards(JwtAuthGuard, CaslAbilityGuard)`. Use proper HTTP status codes. Document with `@ApiTags` and `@ApiOperation`. + +8. **Register in Module** — add to `imports`, `providers`, `controllers`, `exports` as needed. + +9. **Register in AppModule** — import the new module in `app.module.ts`. + +// turbo 10. **Write unit test** — cover service methods with Jest mocks. Run: + +```bash +pnpm test:watch +``` + +// turbo 11. **Citation** — confirm implementation references `specs/01-Requirements/` and `specs/05-Engineering-Guidelines/05-02-backend-guidelines.md` diff --git a/.windsurf/workflows/create-frontend-page.md b/.windsurf/workflows/create-frontend-page.md new file mode 100644 index 0000000..22c4b2e --- /dev/null +++ b/.windsurf/workflows/create-frontend-page.md @@ -0,0 +1,64 @@ +--- +description: Create a new Next.js App Router page following project standards +--- + +# Create Next.js Frontend Page + +Use this workflow when creating a new page in `frontend/app/`. +Follows `specs/05-Engineering-Guidelines/05-03-frontend-guidelines.md`, ADR-011, ADR-012, ADR-013, ADR-014. + +## Steps + +1. **Determine route** — decide the route path, e.g. `app/(dashboard)/documents/page.tsx` + +2. **Classify components** — decide what is Server Component (default) vs Client Component (`'use client'`) + - Server Component: initial data load, static content, SEO + - Client Component: interactivity, forms, TanStack Query hooks, Zustand + +3. **Create page file** — Server Component by default: + +```typescript +// app/(dashboard)//page.tsx +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: ' | LCBP3-DMS', +}; + +export default async function Page() { + return ( +
+ {/* Page content */} +
+ ); +} +``` + +4. **Create API hook** (if client-side data needed) — add to `hooks/use-.ts`: + +```typescript +'use client'; +import { useQuery } from '@tanstack/react-query'; +import { apiClient } from '@/lib/api-client'; + +export function use() { + return useQuery({ + queryKey: [''], + queryFn: () => apiClient.get(''), + }); +} +``` + +5. **Build UI components** — use Shadcn/UI primitives. Place reusable components in `components//`. + +6. **Handle forms** — use React Hook Form + Zod schema validation. Never access form values without validation. + +7. **Handle errors** — add `error.tsx` alongside `page.tsx` for route-level error boundaries. + +8. **Add loading state** — add `loading.tsx` for Suspense fallback if page does async work. + +9. **Add to navigation** — update sidebar/nav config if the page should appear in the menu. + +10. **Access control** — ensure page checks CASL permissions. Redirect unauthorized users via middleware or `notFound()`. + +11. **Citation** — confirm implementation references `specs/01-Requirements/` and `specs/05-Engineering-Guidelines/05-03-frontend-guidelines.md` diff --git a/.windsurf/workflows/deploy.md b/.windsurf/workflows/deploy.md new file mode 100644 index 0000000..4067162 --- /dev/null +++ b/.windsurf/workflows/deploy.md @@ -0,0 +1,71 @@ +--- +description: Deploy the application via Gitea Actions to QNAP Container Station +--- + +# Deploy to Production + +Use this workflow to deploy updated backend and/or frontend to QNAP via Gitea Actions CI/CD. +Follows `specs/04-Infrastructure-OPS/` and ADR-015. + +## Pre-deployment Checklist + +- [ ] All tests pass locally (`pnpm test:watch`) +- [ ] No TypeScript errors (`tsc --noEmit`) +- [ ] No `any` types introduced +- [ ] Schema changes applied to `specs/03-Data-and-Storage/lcbp3-v1.7.0-schema.sql` +- [ ] Environment variables documented (NOT in `.env` files) + +## Steps + +1. **Commit and push to Gitea** + +```bash +git status +git add . +git commit -m "feat(): " +git push origin main +``` + +2. **Monitor Gitea Actions** — open Gitea web UI → Actions tab → verify pipeline starts + +3. **Pipeline stages (automatic)** + - `build-backend` → Docker image build + push to registry + - `build-frontend` → Docker image build + push to registry + - `deploy` → SSH to QNAP → `docker compose pull` + `docker compose up -d` + +4. **Verify backend health** + +```bash +curl http://:3000/health +# Expected: { "status": "ok" } +``` + +5. **Verify frontend** + +```bash +curl -I http://:3001 +# Expected: HTTP 200 +``` + +6. **Check logs in Grafana** — navigate to Grafana → Loki → filter by container name + - Backend: `container_name="lcbp3-backend"` + - Frontend: `container_name="lcbp3-frontend"` + +7. **Verify database** — confirm schema changes are reflected (if any) + +8. **Rollback (if needed)** + +```bash +# SSH into QNAP +docker compose pull = +docker compose up -d +``` + +## Common Issues + +| Symptom | Cause | Fix | +| ----------------- | --------------------- | ----------------------------------- | +| Backend unhealthy | DB connection failed | Check MariaDB container + env vars | +| Frontend blank | Build error | Check Next.js build logs in Grafana | +| 502 Bad Gateway | Container not started | `docker compose ps` to check status | +| Pipeline stuck | Gitea runner offline | Restart runner on QNAP | diff --git a/.windsurf/workflows/schema-change.md b/.windsurf/workflows/schema-change.md new file mode 100644 index 0000000..ef5afb2 --- /dev/null +++ b/.windsurf/workflows/schema-change.md @@ -0,0 +1,108 @@ +--- +description: Manage database schema changes following ADR-009 (no migrations, modify SQL directly) +--- + +# Schema Change Workflow + +Use this workflow when modifying database schema for LCBP3-DMS. +Follows `specs/06-Decision-Records/ADR-009-database-strategy.md` — **NO TypeORM migrations**. + +## Pre-Change Checklist + +- [ ] Change is required by a spec in `specs/01-Requirements/` +- [ ] Existing data impact has been assessed +- [ ] No SQL triggers are being added (business logic in NestJS only) + +## Steps + +1. **Read current schema** — load the full schema file: + +``` +specs/03-Data-and-Storage/lcbp3-v1.7.0-schema.sql +``` + +2. **Read data dictionary** — understand current field definitions: + +``` +specs/03-Data-and-Storage/03-01-data-dictionary.md +``` + +// turbo 3. **Identify impact scope** — determine which tables, columns, indexes, or constraints are affected. List: + +- Tables being modified/created +- Columns being added/renamed/dropped +- Foreign key relationships affected +- Indexes being added/modified +- Seed data impact (if any) + +4. **Modify schema SQL** — edit `specs/03-Data-and-Storage/lcbp3-v1.7.0-schema.sql`: + - Add/modify table definitions + - Maintain consistent formatting (uppercase SQL keywords, lowercase identifiers) + - Add inline comments for new columns explaining purpose + - Ensure `DEFAULT` values and `NOT NULL` constraints are correct + - Add `version` column with `@VersionColumn()` marker comment if optimistic locking is needed + +> [!CAUTION] +> **NEVER use SQL Triggers.** All business logic must live in NestJS services. + +5. **Update data dictionary** — edit `specs/03-Data-and-Storage/03-01-data-dictionary.md`: + - Add new tables/columns with descriptions + - Update data types and constraints + - Document business rules for new fields + - Add enum value definitions if applicable + +6. **Update seed data** (if applicable): + - `specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-basic.sql` — for reference/lookup data + - `specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-permissions.sql` — for new CASL permissions + +7. **Update TypeORM entity** — modify corresponding `backend/src/modules//entities/*.entity.ts`: + - Map ONLY columns defined in schema SQL + - Use correct TypeORM decorators (`@Column`, `@PrimaryGeneratedColumn`, `@ManyToOne`, etc.) + - Add `@VersionColumn()` if optimistic locking is needed + +8. **Update DTOs** — if new columns are exposed via API: + - Add fields to `create-*.dto.ts` and/or `update-*.dto.ts` + - Add `class-validator` decorators for all new fields + - Never use `any` type + +// turbo 9. **Run type check** — verify no TypeScript errors: + +```bash +cd backend && npx tsc --noEmit +``` + +10. **Generate SQL diff** — create a summary of changes for the user to apply manually: + +``` +-- Schema Change Summary +-- Date: +-- Feature: +-- Tables affected: +-- +-- ⚠️ Apply this SQL to the live database manually: + +ALTER TABLE ...; +-- or +CREATE TABLE ...; +``` + +11. **Notify user** — present the SQL diff and remind them: + - Apply the SQL change to the live database manually + - Verify the change doesn't break existing data + - Run `pnpm test` after applying to confirm entity mappings work + +## Common Patterns + +| Change Type | Template | +| ----------- | -------------------------------------------------------------- | +| Add column | `ALTER TABLE \`table\` ADD COLUMN \`col\` TYPE DEFAULT value;` | +| Add table | Full `CREATE TABLE` with constraints and indexes | +| Add index | `CREATE INDEX \`idx_table_col\` ON \`table\` (\`col\`);` | +| Add FK | `ALTER TABLE \`child\` ADD CONSTRAINT ... FOREIGN KEY ...` | +| Add enum | Add to data dictionary + `ENUM('val1','val2')` in column def | + +## On Error + +- If schema SQL has syntax errors → fix and re-validate with `tsc --noEmit` +- If entity mapping doesn't match schema → compare column-by-column against SQL +- If seed data conflicts → check unique constraints and foreign keys diff --git a/.windsurf/workflows/speckit.prepare.md b/.windsurf/workflows/speckit.prepare.md new file mode 100644 index 0000000..d7fb5f7 --- /dev/null +++ b/.windsurf/workflows/speckit.prepare.md @@ -0,0 +1,27 @@ +--- +description: Execute the full preparation pipeline (Specify -> Clarify -> Plan -> Tasks -> Analyze) in sequence. +--- + +# Workflow: speckit.prepare + +This workflow orchestrates the sequential execution of the Speckit preparation phase skills (02-06). + +1. **Step 1: Specify (Skill 02)** + - Goal: Create or update the `spec.md` based on user input. + - Action: Read and execute `.agents/skills/speckit.specify/SKILL.md`. + +2. **Step 2: Clarify (Skill 03)** + - Goal: Refine the `spec.md` by identifying and resolving ambiguities. + - Action: Read and execute `.agents/skills/speckit.clarify/SKILL.md`. + +3. **Step 3: Plan (Skill 04)** + - Goal: Generate `plan.md` from the finalized spec. + - Action: Read and execute `.agents/skills/speckit.plan/SKILL.md`. + +4. **Step 4: Tasks (Skill 05)** + - Goal: Generate actionable `tasks.md` from the plan. + - Action: Read and execute `.agents/skills/speckit.tasks/SKILL.md`. + +5. **Step 5: Analyze (Skill 06)** + - Goal: Validate consistency across all design artifacts (spec, plan, tasks). + - Action: Read and execute `.agents/skills/speckit.analyze/SKILL.md`. diff --git a/.windsurf/workflows/util-speckit.checklist.md b/.windsurf/workflows/util-speckit.checklist.md new file mode 100644 index 0000000..49aa2d9 --- /dev/null +++ b/.windsurf/workflows/util-speckit.checklist.md @@ -0,0 +1,18 @@ +--- +description: Generate a custom checklist for the current feature based on user requirements. +--- + +# Workflow: speckit.checklist + +1. **Context Analysis**: + - The user has provided an input prompt. Treat this as the primary input for the skill. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.checklist/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If `spec.md` is missing: Run `/speckit.specify` first to create the feature specification diff --git a/.windsurf/workflows/util-speckit.diff.md b/.windsurf/workflows/util-speckit.diff.md new file mode 100644 index 0000000..da3dd20 --- /dev/null +++ b/.windsurf/workflows/util-speckit.diff.md @@ -0,0 +1,19 @@ +--- +description: Compare two versions of a spec or plan to highlight changes. +--- + +# Workflow: speckit.diff + +1. **Context Analysis**: + - The user has provided an input prompt (optional file paths or version references). + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.diff/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If no files to compare: Use current feature's `spec.md` vs git HEAD + - If `spec.md` doesn't exist: Run `/speckit.specify` first diff --git a/.windsurf/workflows/util-speckit.migrate.md b/.windsurf/workflows/util-speckit.migrate.md new file mode 100644 index 0000000..cd2e5b4 --- /dev/null +++ b/.windsurf/workflows/util-speckit.migrate.md @@ -0,0 +1,19 @@ +--- +description: Migrate existing projects into the speckit structure by generating spec.md, plan.md, and tasks.md from existing code. +--- + +# Workflow: speckit.migrate + +1. **Context Analysis**: + - The user has provided an input prompt (path to analyze, feature name). + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.migrate/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If path doesn't exist: Ask user to provide valid directory path + - If no code found: Report that no analyzable code was detected diff --git a/.windsurf/workflows/util-speckit.quizme.md b/.windsurf/workflows/util-speckit.quizme.md new file mode 100644 index 0000000..11f70af --- /dev/null +++ b/.windsurf/workflows/util-speckit.quizme.md @@ -0,0 +1,20 @@ +--- +description: Challenge the specification with Socratic questioning to identify logical gaps, unhandled edge cases, and robustness issues. +--- + +// turbo-all + +# Workflow: speckit.quizme + +1. **Context Analysis**: + - The user has provided an input prompt. Treat this as the primary input for the skill. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.quizme/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If required files don't exist, inform the user which prerequisite workflow to run first (e.g., `/speckit.specify` to create `spec.md`). diff --git a/.windsurf/workflows/util-speckit.status.md b/.windsurf/workflows/util-speckit.status.md new file mode 100644 index 0000000..b2f5089 --- /dev/null +++ b/.windsurf/workflows/util-speckit.status.md @@ -0,0 +1,20 @@ +--- +description: Display a dashboard showing feature status, completion percentage, and blockers. +--- + +// turbo-all + +# Workflow: speckit.status + +1. **Context Analysis**: + - The user may optionally specify a feature to focus on. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.status/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If no features exist: Report "No features found. Run `/speckit.specify` to create your first feature." diff --git a/.windsurf/workflows/util-speckit.taskstoissues.md b/.windsurf/workflows/util-speckit.taskstoissues.md new file mode 100644 index 0000000..0cdac6e --- /dev/null +++ b/.windsurf/workflows/util-speckit.taskstoissues.md @@ -0,0 +1,18 @@ +--- +description: Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts. +--- + +# Workflow: speckit.taskstoissues + +1. **Context Analysis**: + - The user has provided an input prompt. Treat this as the primary input for the skill. + +2. **Load Skill**: + - Use the `view_file` tool to read the skill file at: `.agents/skills/speckit.taskstoissues/SKILL.md` + +3. **Execute**: + - Follow the instructions in the `SKILL.md` exactly. + - Apply the user's prompt as the input arguments/context for the skill's logic. + +4. **On Error**: + - If `tasks.md` is missing: Run `/speckit.tasks` first diff --git a/backend/src/modules/circulation/circulation.service.ts b/backend/src/modules/circulation/circulation.service.ts index 6c3f585..6640b83 100644 --- a/backend/src/modules/circulation/circulation.service.ts +++ b/backend/src/modules/circulation/circulation.service.ts @@ -14,6 +14,8 @@ import { CreateCirculationDto } from './dto/create-circulation.dto'; import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto'; import { SearchCirculationDto } from './dto/search-circulation.dto'; import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service'; +import { Project } from '../project/entities/project.entity'; +import { Correspondence } from '../correspondence/entities/correspondence.entity'; @Injectable() export class CirculationService { @@ -26,6 +28,58 @@ export class CirculationService { private dataSource: DataSource ) {} + /** + * ADR-019: Resolve projectId (INT or UUID string) to internal INT ID + */ + private async resolveProjectId(projectId: number | string): Promise { + if (typeof projectId === 'number') return projectId; + const num = Number(projectId); + if (!isNaN(num)) return num; + const project = await this.dataSource.manager.findOne(Project, { + where: { uuid: projectId }, + select: ['id'], + }); + if (!project) + throw new NotFoundException(`Project with UUID ${projectId} not found`); + return project.id; + } + + /** + * ADR-019: Resolve correspondenceId (INT or UUID string) to internal INT ID + */ + private async resolveCorrespondenceId( + corrId: number | string + ): Promise { + if (typeof corrId === 'number') return corrId; + const num = Number(corrId); + if (!isNaN(num)) return num; + const corr = await this.dataSource.manager.findOne(Correspondence, { + where: { uuid: corrId }, + select: ['id'], + }); + if (!corr) + throw new NotFoundException( + `Correspondence with UUID ${corrId} not found` + ); + return corr.id; + } + + /** + * ADR-019: Resolve userId (INT or UUID string) to internal user_id + */ + private async resolveUserId(userId: number | string): Promise { + if (typeof userId === 'number') return userId; + const num = Number(userId); + if (!isNaN(num)) return num; + const user = await this.dataSource.manager.findOne(User, { + where: { uuid: userId }, + select: ['user_id'], + }); + if (!user) + throw new NotFoundException(`User with UUID ${userId} not found`); + return user.user_id; + } + async create(createDto: CreateCirculationDto, user: User) { if (!user.primaryOrganizationId) { throw new BadRequestException('User must belong to an organization'); @@ -36,9 +90,20 @@ export class CirculationService { await queryRunner.startTransaction(); try { + // ADR-019: Resolve UUID references to internal INT IDs + const resolvedProjectId = createDto.projectId + ? await this.resolveProjectId(createDto.projectId) + : 0; + const resolvedCorrId = await this.resolveCorrespondenceId( + createDto.correspondenceId + ); + const resolvedAssigneeIds = await Promise.all( + createDto.assigneeIds.map((id) => this.resolveUserId(id)) + ); + // Generate No. using DocumentNumberingService (Type 900 - Circulation) const result = await this.numberingService.generateNextNumber({ - projectId: createDto.projectId || 0, // Use projectId from DTO or 0 + projectId: resolvedProjectId, originatorOrganizationId: user.primaryOrganizationId, typeId: 900, // Fixed Type ID for Circulation year: new Date().getFullYear(), @@ -50,7 +115,7 @@ export class CirculationService { const circulation = queryRunner.manager.create(Circulation, { organizationId: user.primaryOrganizationId, - correspondenceId: createDto.correspondenceId, + correspondenceId: resolvedCorrId, circulationNo: result.number, subject: createDto.subject, statusCode: 'OPEN', @@ -58,13 +123,13 @@ export class CirculationService { }); const savedCirculation = await queryRunner.manager.save(circulation); - if (createDto.assigneeIds && createDto.assigneeIds.length > 0) { - const routings = createDto.assigneeIds.map((userId, index) => + if (resolvedAssigneeIds.length > 0) { + const routings = resolvedAssigneeIds.map((assigneeId, index) => queryRunner.manager.create(CirculationRouting, { circulationId: savedCirculation.id, stepNumber: index + 1, organizationId: user.primaryOrganizationId, - assignedTo: userId, + assignedTo: assigneeId, status: 'PENDING', }) ); diff --git a/backend/src/modules/circulation/dto/create-circulation.dto.ts b/backend/src/modules/circulation/dto/create-circulation.dto.ts index ecc2c88..4bd31ff 100644 --- a/backend/src/modules/circulation/dto/create-circulation.dto.ts +++ b/backend/src/modules/circulation/dto/create-circulation.dto.ts @@ -1,29 +1,25 @@ import { - IsInt, IsString, IsNotEmpty, IsArray, IsOptional, - ArrayMinSize, // ✅ เพิ่ม + ArrayMinSize, } from 'class-validator'; export class CreateCirculationDto { - @IsInt() @IsNotEmpty() - correspondenceId!: number; // เอกสารต้นเรื่องที่จะเวียน + correspondenceId!: number | string; // เอกสารต้นเรื่องที่จะเวียน (INT or UUID) - @IsInt() @IsOptional() - projectId?: number; // Project ID for Numbering + projectId?: number | string; // Project ID or UUID for Numbering @IsString() @IsNotEmpty() subject!: string; // หัวข้อเรื่อง (Subject) @IsArray() - @IsInt({ each: true }) @ArrayMinSize(1) // ✅ ต้องมีผู้รับอย่างน้อย 1 คน - assigneeIds!: number[]; // รายชื่อ User ID ที่ต้องการส่งให้ (ผู้รับผิดชอบ) + assigneeIds!: (number | string)[]; // รายชื่อ User ID or UUID ที่ต้องการส่งให้ (ADR-019) @IsString() @IsOptional() diff --git a/backend/src/modules/correspondence/correspondence.service.ts b/backend/src/modules/correspondence/correspondence.service.ts index ba1f8d0..d3c088c 100644 --- a/backend/src/modules/correspondence/correspondence.service.ts +++ b/backend/src/modules/correspondence/correspondence.service.ts @@ -35,6 +35,7 @@ import { WorkflowEngineService } from '../workflow-engine/workflow-engine.servic import { UserService } from '../user/user.service'; import { SearchService } from '../search/search.service'; import { FileStorageService } from '../../common/file-storage/file-storage.service'; +import { Project } from '../project/entities/project.entity'; /** * CorrespondenceService - Document management (CRUD) @@ -69,7 +70,52 @@ export class CorrespondenceService { private fileStorageService: FileStorageService ) {} + /** + * ADR-019: Resolve projectId (INT or UUID string) to internal INT ID + */ + private async resolveProjectId(projectId: number | string): Promise { + if (typeof projectId === 'number') return projectId; + const num = Number(projectId); + if (!isNaN(num)) return num; + const project = await this.dataSource.manager.findOne(Project, { + where: { uuid: projectId }, + select: ['id'], + }); + if (!project) + throw new NotFoundException(`Project with UUID ${projectId} not found`); + return project.id; + } + + /** + * ADR-019: Resolve organizationId (INT or UUID string) to internal INT ID + */ + private async resolveOrganizationId(orgId: number | string): Promise { + if (typeof orgId === 'number') return orgId; + const num = Number(orgId); + if (!isNaN(num)) return num; + const org = await this.orgRepo.findOne({ + where: { uuid: orgId }, + select: ['id'], + }); + if (!org) + throw new NotFoundException(`Organization with UUID ${orgId} not found`); + return org.id; + } + async create(createDto: CreateCorrespondenceDto, user: User) { + // ADR-019: Resolve UUID references to internal INT IDs + const resolvedProjectId = await this.resolveProjectId(createDto.projectId); + const resolvedOriginatorId = createDto.originatorId + ? await this.resolveOrganizationId(createDto.originatorId) + : undefined; + const resolvedRecipients = createDto.recipients + ? await Promise.all( + createDto.recipients.map(async (r) => ({ + organizationId: await this.resolveOrganizationId(r.organizationId), + type: r.type, + })) + ) + : undefined; const type = await this.typeRepo.findOne({ where: { id: createDto.typeId }, }); @@ -94,7 +140,7 @@ export class CorrespondenceService { } // Impersonation Logic - if (createDto.originatorId && createDto.originatorId !== userOrgId) { + if (resolvedOriginatorId && resolvedOriginatorId !== userOrgId) { const permissions = await this.userService.getUserPermissions( user.user_id ); @@ -103,7 +149,7 @@ export class CorrespondenceService { 'You do not have permission to create documents on behalf of other organizations.' ); } - userOrgId = createDto.originatorId; + userOrgId = resolvedOriginatorId; } if (!userOrgId) { @@ -134,7 +180,7 @@ export class CorrespondenceService { const orgCode = originatorOrg?.organizationCode ?? 'UNK'; // [v1.5.1] Extract recipient organization from recipients array (Primary TO) - const toRecipient = createDto.recipients?.find((r) => r.type === 'TO'); + const toRecipient = resolvedRecipients?.find((r) => r.type === 'TO'); const recipientOrganizationId = toRecipient?.organizationId; let recipientCode = ''; @@ -146,7 +192,7 @@ export class CorrespondenceService { } const docNumber = await this.numberingService.generateNextNumber({ - projectId: createDto.projectId, + projectId: resolvedProjectId, originatorOrganizationId: userOrgId, typeId: createDto.typeId, disciplineId: createDto.disciplineId, @@ -165,7 +211,7 @@ export class CorrespondenceService { correspondenceNumber: docNumber.number, correspondenceTypeId: createDto.typeId, disciplineId: createDto.disciplineId, - projectId: createDto.projectId, + projectId: resolvedProjectId, originatorId: userOrgId, isInternal: createDto.isInternal || false, createdBy: user.user_id, @@ -195,9 +241,9 @@ export class CorrespondenceService { }); await queryRunner.manager.save(revision); - // Save Recipients - if (createDto.recipients && createDto.recipients.length > 0) { - const recipients = createDto.recipients.map((r) => + // Save Recipients (using resolved INT IDs) + if (resolvedRecipients && resolvedRecipients.length > 0) { + const recipients = resolvedRecipients.map((r) => queryRunner.manager.create(CorrespondenceRecipient, { correspondenceId: savedCorr.id, recipientOrganizationId: r.organizationId, @@ -459,14 +505,30 @@ export class CorrespondenceService { } } + // ADR-019: Resolve UUID references in update DTO + const updResolvedProjectId = updateDto.projectId + ? await this.resolveProjectId(updateDto.projectId) + : undefined; + const updResolvedOriginatorId = updateDto.originatorId + ? await this.resolveOrganizationId(updateDto.originatorId) + : undefined; + const updResolvedRecipients = updateDto.recipients + ? await Promise.all( + updateDto.recipients.map(async (r) => ({ + organizationId: await this.resolveOrganizationId(r.organizationId), + type: r.type, + })) + ) + : undefined; + // 3. Update Correspondence Entity if needed const correspondenceUpdate: DeepPartial = {}; if (updateDto.disciplineId) correspondenceUpdate.disciplineId = updateDto.disciplineId; - if (updateDto.projectId) - correspondenceUpdate.projectId = updateDto.projectId; - if (updateDto.originatorId) - correspondenceUpdate.originatorId = updateDto.originatorId; + if (updResolvedProjectId) + correspondenceUpdate.projectId = updResolvedProjectId; + if (updResolvedOriginatorId) + correspondenceUpdate.originatorId = updResolvedOriginatorId; if (Object.keys(correspondenceUpdate).length > 0) { await this.correspondenceRepo.update(id, correspondenceUpdate); @@ -506,13 +568,13 @@ export class CorrespondenceService { } // 5. Update Recipients if provided - if (updateDto.recipients) { + if (updResolvedRecipients) { const recipientRepo = this.dataSource.getRepository( CorrespondenceRecipient ); await recipientRepo.delete({ correspondenceId: id }); - const newRecipients = updateDto.recipients.map((r) => + const newRecipients = updResolvedRecipients.map((r) => recipientRepo.create({ correspondenceId: id, recipientOrganizationId: r.organizationId, @@ -539,11 +601,11 @@ export class CorrespondenceService { // Check for ACTUAL value changes const isProjectChanged = - updateDto.projectId !== undefined && - updateDto.projectId !== currentCorr.projectId; + updResolvedProjectId !== undefined && + updResolvedProjectId !== currentCorr.projectId; const isOriginatorChanged = - updateDto.originatorId !== undefined && - updateDto.originatorId !== currentCorr.originatorId; + updResolvedOriginatorId !== undefined && + updResolvedOriginatorId !== currentCorr.originatorId; const isDisciplineChanged = updateDto.disciplineId !== undefined && updateDto.disciplineId !== currentCorr.disciplineId; @@ -554,15 +616,9 @@ export class CorrespondenceService { let isRecipientChanged = false; let newRecipientId: number | undefined; - if (updateDto.recipients) { - // Safe check for 'type' or 'recipientType' (mismatch safeguard) - interface RecipientInput { - type?: string; - recipientType?: string; - organizationId?: number; - } - const newToRecipient = updateDto.recipients.find( - (r: RecipientInput) => r.type === 'TO' || r.recipientType === 'TO' + if (updResolvedRecipients) { + const newToRecipient = updResolvedRecipients.find( + (r) => r.type === 'TO' ); newRecipientId = newToRecipient?.organizationId; @@ -594,7 +650,7 @@ export class CorrespondenceService { // [Fix #6] Fetch real ORG Code from originator organization const originatorOrgForUpdate = await this.orgRepo.findOne({ where: { - id: updateDto.originatorId ?? currentCorr.originatorId ?? 0, + id: updResolvedOriginatorId ?? currentCorr.originatorId ?? 0, }, }); const orgCode = originatorOrgForUpdate?.organizationCode ?? 'UNK'; @@ -610,9 +666,9 @@ export class CorrespondenceService { }; const newCtx = { - projectId: updateDto.projectId ?? currentCorr.projectId, + projectId: updResolvedProjectId ?? currentCorr.projectId, originatorOrganizationId: - updateDto.originatorId ?? currentCorr.originatorId ?? 0, + updResolvedOriginatorId ?? currentCorr.originatorId ?? 0, typeId: updateDto.typeId ?? currentCorr.correspondenceTypeId, disciplineId: updateDto.disciplineId ?? currentCorr.disciplineId, recipientOrganizationId: targetRecipientId, @@ -650,6 +706,20 @@ export class CorrespondenceService { } async previewDocumentNumber(createDto: CreateCorrespondenceDto, user: User) { + // ADR-019: Resolve UUID references + const previewProjectId = await this.resolveProjectId(createDto.projectId); + const previewOriginatorId = createDto.originatorId + ? await this.resolveOrganizationId(createDto.originatorId) + : undefined; + const previewRecipients = createDto.recipients + ? await Promise.all( + createDto.recipients.map(async (r) => ({ + organizationId: await this.resolveOrganizationId(r.organizationId), + type: r.type, + })) + ) + : undefined; + const type = await this.typeRepo.findOne({ where: { id: createDto.typeId }, }); @@ -661,13 +731,13 @@ export class CorrespondenceService { if (fullUser) userOrgId = fullUser.primaryOrganizationId; } - if (createDto.originatorId && createDto.originatorId !== userOrgId) { + if (previewOriginatorId && previewOriginatorId !== userOrgId) { // Allow impersonation for preview - userOrgId = createDto.originatorId; + userOrgId = previewOriginatorId; } // Extract recipient from recipients array - const toRecipient = createDto.recipients?.find((r) => r.type === 'TO'); + const toRecipient = previewRecipients?.find((r) => r.type === 'TO'); const recipientOrganizationId = toRecipient?.organizationId; let recipientCode = ''; @@ -679,7 +749,7 @@ export class CorrespondenceService { } return this.numberingService.previewNumber({ - projectId: createDto.projectId, + projectId: previewProjectId, originatorOrganizationId: userOrgId!, typeId: createDto.typeId, disciplineId: createDto.disciplineId, diff --git a/backend/src/modules/correspondence/dto/create-correspondence.dto.ts b/backend/src/modules/correspondence/dto/create-correspondence.dto.ts index 503207f..a57c314 100644 --- a/backend/src/modules/correspondence/dto/create-correspondence.dto.ts +++ b/backend/src/modules/correspondence/dto/create-correspondence.dto.ts @@ -11,10 +11,9 @@ import { import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class CreateCorrespondenceDto { - @ApiProperty({ description: 'Project ID', example: 1 }) - @IsInt() + @ApiProperty({ description: 'Project ID or UUID', example: 1 }) @IsNotEmpty() - projectId!: number; + projectId!: number | string; @ApiProperty({ description: 'Document Type ID', example: 1 }) @IsInt() @@ -110,12 +109,11 @@ export class CreateCorrespondenceDto { // ✅ เพิ่ม Field สำหรับ Impersonation (เลือกองค์กรผู้ส่ง) @ApiPropertyOptional({ - description: 'Originator Organization ID (for impersonation)', + description: 'Originator Organization ID or UUID (for impersonation)', example: 1, }) - @IsInt() @IsOptional() - originatorId?: number; + originatorId?: number | string; @ApiPropertyOptional({ description: 'Recipients', @@ -123,5 +121,5 @@ export class CreateCorrespondenceDto { }) @IsArray() @IsOptional() - recipients?: { organizationId: number; type: 'TO' | 'CC' }[]; + recipients?: { organizationId: number | string; type: 'TO' | 'CC' }[]; } diff --git a/backend/src/modules/document-numbering/services/document-numbering.service.ts b/backend/src/modules/document-numbering/services/document-numbering.service.ts index 851806f..48d0a94 100644 --- a/backend/src/modules/document-numbering/services/document-numbering.service.ts +++ b/backend/src/modules/document-numbering/services/document-numbering.service.ts @@ -1,6 +1,11 @@ -import { Injectable, Logger, BadRequestException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { + Injectable, + Logger, + BadRequestException, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository, InjectEntityManager } from '@nestjs/typeorm'; +import { Repository, EntityManager } from 'typeorm'; import { ConfigService } from '@nestjs/config'; import { DocumentNumberFormat } from '../entities/document-number-format.entity'; @@ -20,6 +25,7 @@ import { CounterKeyDto } from '../dto/counter-key.dto'; import { GenerateNumberContext } from '../interfaces/document-numbering.interface'; import { ReserveNumberDto } from '../dto/reserve-number.dto'; import { ConfirmReservationDto } from '../dto/confirm-reservation.dto'; +import { Project } from '../../project/entities/project.entity'; @Injectable() export class DocumentNumberingService { @@ -39,9 +45,27 @@ export class DocumentNumberingService { private lockService: DocumentNumberingLockService, private configService: ConfigService, private manualOverrideService: ManualOverrideService, - private metricsService: MetricsService + private metricsService: MetricsService, + @InjectEntityManager() + private entityManager: EntityManager ) {} + /** + * ADR-019: Resolve projectId (INT or UUID string) to internal INT ID + */ + private async resolveProjectId(projectId: number | string): Promise { + if (typeof projectId === 'number') return projectId; + const num = Number(projectId); + if (!isNaN(num)) return num; + const project = await this.entityManager.findOne(Project, { + where: { uuid: projectId }, + select: ['id'], + }); + if (!project) + throw new NotFoundException(`Project with UUID ${projectId} not found`); + return project.id; + } + async generateNextNumber( ctx: GenerateNumberContext ): Promise<{ number: string; auditId: number }> { @@ -218,11 +242,15 @@ export class DocumentNumberingService { return this.formatRepo.find(); } - async getTemplatesByProject(projectId: number) { - return this.formatRepo.find({ where: { projectId } }); + async getTemplatesByProject(projectId: number | string) { + const internalId = await this.resolveProjectId(projectId); + return this.formatRepo.find({ where: { projectId: internalId } }); } async saveTemplate(dto: any) { + if (dto.projectId) { + dto.projectId = await this.resolveProjectId(dto.projectId); + } return this.formatRepo.save(dto); } diff --git a/backend/src/modules/master/entities/tag.entity.ts b/backend/src/modules/master/entities/tag.entity.ts index 07a8391..5564dd0 100644 --- a/backend/src/modules/master/entities/tag.entity.ts +++ b/backend/src/modules/master/entities/tag.entity.ts @@ -6,7 +6,10 @@ import { UpdateDateColumn, DeleteDateColumn, Unique, + ManyToOne, + JoinColumn, } from 'typeorm'; +import { Project } from '../../project/entities/project.entity'; @Entity('tags') @Unique('ux_tag_project', ['project_id', 'tag_name']) @@ -26,6 +29,11 @@ export class Tag { @Column({ type: 'text', nullable: true }) description!: string | null; // เพิ่ม ! + // Relations + @ManyToOne(() => Project) + @JoinColumn({ name: 'project_id' }) + project?: Project; + @CreateDateColumn() created_at!: Date; // เพิ่ม ! diff --git a/backend/src/modules/master/master.service.ts b/backend/src/modules/master/master.service.ts index d75876c..89a681d 100644 --- a/backend/src/modules/master/master.service.ts +++ b/backend/src/modules/master/master.service.ts @@ -132,6 +132,7 @@ export class MasterService { } return this.rfaTypeRepo.find({ where, + relations: ['contract'], order: { typeCode: 'ASC' }, }); } @@ -296,7 +297,9 @@ export class MasterService { } async findAllTags(query?: SearchTagDto) { - const qb = this.tagRepo.createQueryBuilder('tag'); + const qb = this.tagRepo + .createQueryBuilder('tag') + .leftJoinAndSelect('tag.project', 'project'); if (query?.project_id) { // In Tags, we use project_id (INT) directly or resolve if UUID passed via query diff --git a/backend/src/modules/rfa/entities/rfa-type.entity.ts b/backend/src/modules/rfa/entities/rfa-type.entity.ts index 433cc0e..193fa18 100644 --- a/backend/src/modules/rfa/entities/rfa-type.entity.ts +++ b/backend/src/modules/rfa/entities/rfa-type.entity.ts @@ -1,4 +1,12 @@ -import { Entity, PrimaryGeneratedColumn, Column, AfterLoad } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + AfterLoad, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Contract } from '../../contract/entities/contract.entity'; @Entity('rfa_types') export class RfaType { @@ -23,6 +31,11 @@ export class RfaType { @Column({ name: 'is_active', default: true }) isActive!: boolean; + // Relations + @ManyToOne(() => Contract) + @JoinColumn({ name: 'contract_id' }) + contract?: Contract; + // Virtual property for backward compatibility typeName!: string; diff --git a/frontend/app/(admin)/admin/doc-control/reference/disciplines/page.tsx b/frontend/app/(admin)/admin/doc-control/reference/disciplines/page.tsx index 270b35b..138f243 100644 --- a/frontend/app/(admin)/admin/doc-control/reference/disciplines/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/reference/disciplines/page.tsx @@ -45,7 +45,7 @@ export default function DisciplinesPage() { const contractOptions = contracts.map((c: any) => ({ label: `${c.contractName} (${c.contractCode})`, - value: String(c.id || c.uuid), + value: String(c.id), })); return ( @@ -55,9 +55,16 @@ export default function DisciplinesPage() { title="Disciplines Management" description="Manage system disciplines (e.g., ARCH, STR, MEC)" queryKey={['disciplines', selectedContractId ?? 'all']} - fetchFn={() => masterDataService.getDisciplines(selectedContractId ? selectedContractId : undefined)} + fetchFn={async () => { + const items = await masterDataService.getDisciplines(selectedContractId ? selectedContractId : undefined); + // ADR-019: Map contractId INT → contract UUID for edit mode select matching + return (items as any[]).map((item: any) => ({ + ...item, + contractId: item.contract?.id || item.contract?.uuid || String(item.contractId), + })); + }} createFn={(data: Record) => masterDataService.createDiscipline(data as any)} - updateFn={(id, data) => Promise.reject('Not implemented yet')} + updateFn={(id, data) => Promise.reject('Not implemented yet')} deleteFn={(id) => masterDataService.deleteDiscipline(id)} columns={columns} filters={ @@ -72,7 +79,7 @@ export default function DisciplinesPage() { All Contracts {contracts.map((c: any) => ( - + {c.contractName} ({c.contractCode}) ))} diff --git a/frontend/app/(admin)/admin/doc-control/reference/rfa-types/page.tsx b/frontend/app/(admin)/admin/doc-control/reference/rfa-types/page.tsx index 5ec70da..29aefbb 100644 --- a/frontend/app/(admin)/admin/doc-control/reference/rfa-types/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/reference/rfa-types/page.tsx @@ -49,7 +49,7 @@ export default function RfaTypesPage() { const contractOptions = contracts.map((c: any) => ({ label: `${c.contractName} (${c.contractCode})`, - value: String(c.id || c.uuid), + value: String(c.id), })); return ( @@ -58,7 +58,14 @@ export default function RfaTypesPage() { entityName="RFA Type" title="RFA Types Management" queryKey={['rfa-types', selectedContractId ?? 'all']} - fetchFn={() => masterDataService.getRfaTypes(selectedContractId ? selectedContractId : undefined)} + fetchFn={async () => { + const items = await masterDataService.getRfaTypes(selectedContractId ? selectedContractId : undefined); + // ADR-019: Map contractId INT → contract UUID for edit mode select matching + return (items as any[]).map((item: any) => ({ + ...item, + contractId: item.contract?.id || item.contract?.uuid || String(item.contractId), + })); + }} createFn={(data: Record) => masterDataService.createRfaType(data as any)} updateFn={(id, data) => masterDataService.updateRfaType(id, data)} deleteFn={(id) => masterDataService.deleteRfaType(id)} @@ -75,7 +82,7 @@ export default function RfaTypesPage() { All Contracts {contracts.map((c: any) => ( - + {c.contractName} ({c.contractCode}) ))} diff --git a/frontend/app/(admin)/admin/doc-control/reference/tags/page.tsx b/frontend/app/(admin)/admin/doc-control/reference/tags/page.tsx index cd3dbba..29b5f24 100644 --- a/frontend/app/(admin)/admin/doc-control/reference/tags/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/reference/tags/page.tsx @@ -16,8 +16,8 @@ export default function TagsPage() { const projectOptions = [ { label: "Global (All Projects)", value: "__none__" }, ...(projectsData || []).map((p: Record) => ({ - label: p.projectName || p.projectCode || p.project_name || p.project_code || `Project ${p.id}`, - value: String(p.id), + label: (p.projectName || p.projectCode || p.project_name || p.project_code || `Project ${p.id}`) as string, + value: String(p.id), // p.id = UUID string via serialization })), ]; @@ -26,10 +26,10 @@ export default function TagsPage() { accessorKey: "project_id", header: "Project", cell: ({ row }) => { - const pId = row.original.project_id; - if (!pId) return Global; - const p = (projectsData || []).find((proj: Record) => proj.id === pId); - return p ? (p.projectName || p.projectCode || p.project_name || p.project_code || `Project ${pId}`) as React.ReactNode : pId as React.ReactNode; + const item = row.original as Record; + const project = item.project as Record | null; + if (!project) return Global; + return (project.projectName || project.projectCode || `Project ${project.id}`) as React.ReactNode; }, }, { @@ -70,7 +70,14 @@ export default function TagsPage() { description="Manage system tags, multi-tenant capable." entityName="Tag" queryKey={["tags"]} - fetchFn={() => masterDataService.getTags()} + fetchFn={async () => { + const items = await masterDataService.getTags(); + // ADR-019: Map project_id INT → project UUID for edit mode select matching + return (items as any[]).map((item: any) => ({ + ...item, + project_id: item.project?.id || item.project?.uuid || (item.project_id ? String(item.project_id) : null), + })); + }} createFn={(data: Record) => masterDataService.createTag(formatPayload(data) as unknown as CreateTagDto)} updateFn={(id, data) => masterDataService.updateTag(id, formatPayload(data))} deleteFn={(id) => masterDataService.deleteTag(id)} diff --git a/frontend/app/(dashboard)/circulation/new/page.tsx b/frontend/app/(dashboard)/circulation/new/page.tsx index 7e512dc..fcdc2ab 100644 --- a/frontend/app/(dashboard)/circulation/new/page.tsx +++ b/frontend/app/(dashboard)/circulation/new/page.tsx @@ -45,9 +45,9 @@ import { cn } from "@/lib/utils"; // Form validation schema const formSchema = z.object({ - correspondenceId: z.number(), + correspondenceId: z.string().min(1, "Please select a document"), subject: z.string().min(1, "Subject is required"), - assigneeIds: z.array(z.number()).min(1, "At least one assignee is required"), + assigneeIds: z.array(z.string()).min(1, "At least one assignee is required"), remarks: z.string().optional(), }); @@ -98,18 +98,18 @@ export default function CreateCirculationPage() { const selectedDocId = form.watch("correspondenceId"); const selectedDoc = correspondences?.data?.find( - (c: { id: number }) => c.id === selectedDocId + (c: { uuid: string }) => c.uuid === selectedDocId ); - const toggleAssignee = (userId: number) => { + const toggleAssignee = (userUuid: string) => { const current = form.getValues("assigneeIds"); - if (current.includes(userId)) { + if (current.includes(userUuid)) { form.setValue( "assigneeIds", - current.filter((id) => id !== userId) + current.filter((id) => id !== userUuid) ); } else { - form.setValue("assigneeIds", [...current, userId]); + form.setValue("assigneeIds", [...current, userUuid]); } }; @@ -168,19 +168,19 @@ export default function CreateCirculationPage() { No document found. - {correspondences?.data?.map((doc: { id: number; correspondenceNumber: string }) => ( + {correspondences?.data?.map((doc: { uuid: string; correspondenceNumber: string }) => ( { - form.setValue("correspondenceId", doc.id); + form.setValue("correspondenceId", doc.uuid); setDocOpen(false); }} > {selectedAssignees.length > 0 ? (
- {selectedAssignees.map((userId) => { + {selectedAssignees.map((userUuid) => { const user = users.find( - (u) => u.userId === userId + (u) => u.uuid === userUuid ); return user ? ( @@ -245,7 +245,7 @@ export default function CreateCirculationPage() { className="ml-1 h-3 w-3 cursor-pointer" onClick={(e) => { e.stopPropagation(); - toggleAssignee(userId); + toggleAssignee(userUuid); }} /> @@ -269,14 +269,14 @@ export default function CreateCirculationPage() { {users.map((user) => ( user.userId && toggleAssignee(user.userId)} + onSelect={() => toggleAssignee(user.uuid)} > r.isCurrent) || initialData?.revisions?.[0]; const defaultValues: Partial = { - projectId: initialData?.projectId || undefined, + projectId: initialData?.projectId ? String(initialData.projectId) : undefined, documentTypeId: initialData?.correspondenceTypeId || undefined, disciplineId: initialData?.disciplineId || undefined, subject: currentRev?.subject || currentRev?.title || "", @@ -64,9 +64,11 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u body: currentRev?.body || "", remarks: currentRev?.remarks || "", dueDate: currentRev?.dueDate ? new Date(currentRev.dueDate).toISOString().split('T')[0] : undefined, - fromOrganizationId: initialData?.originatorId || undefined, + fromOrganizationId: initialData?.originatorId ? String(initialData.originatorId) : undefined, // Map initial recipient (TO) - Simplified for now - toOrganizationId: initialData?.recipients?.find((r: any) => r.recipientType === 'TO')?.recipientOrganizationId || undefined, + toOrganizationId: initialData?.recipients?.find((r: any) => r.recipientType === 'TO')?.recipientOrganizationId + ? String(initialData.recipients.find((r: any) => r.recipientType === 'TO').recipientOrganizationId) + : undefined, importance: currentRev?.details?.importance || "NORMAL", }; @@ -209,8 +211,8 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
setValue("fromOrganizationId", parseInt(v))} - value={fromOrgId ? String(fromOrgId) : undefined} + onValueChange={(v) => setValue("fromOrganizationId", v)} + value={fromOrgId || undefined} disabled={isLoadingOrgs} > @@ -332,7 +334,7 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u {(organizations || []).map((org: Organization) => ( - + {org.organizationName} ({org.organizationCode}) ))} @@ -346,8 +348,8 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u