This commit is contained in:
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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-name>/
|
||||
├── <module-name>.module.ts
|
||||
├── <module-name>.controller.ts
|
||||
├── <module-name>.service.ts
|
||||
├── dto/
|
||||
│ ├── create-<module-name>.dto.ts
|
||||
│ └── update-<module-name>.dto.ts
|
||||
├── entities/
|
||||
│ └── <module-name>.entity.ts
|
||||
└── <module-name>.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`
|
||||
@@ -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)/<route>/page.tsx
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '<Page Title> | LCBP3-DMS',
|
||||
};
|
||||
|
||||
export default async function <PageName>Page() {
|
||||
return (
|
||||
<div>
|
||||
{/* Page content */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
4. **Create API hook** (if client-side data needed) — add to `hooks/use-<feature>.ts`:
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
|
||||
export function use<Feature>() {
|
||||
return useQuery({
|
||||
queryKey: ['<feature>'],
|
||||
queryFn: () => apiClient.get('<endpoint>'),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
5. **Build UI components** — use Shadcn/UI primitives. Place reusable components in `components/<feature>/`.
|
||||
|
||||
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`
|
||||
@@ -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(<scope>): <description>"
|
||||
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://<QNAP_IP>:3000/health
|
||||
# Expected: { "status": "ok" }
|
||||
```
|
||||
|
||||
5. **Verify frontend**
|
||||
|
||||
```bash
|
||||
curl -I http://<QNAP_IP>: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 <service>=<previous-image-tag>
|
||||
docker compose up -d <service>
|
||||
```
|
||||
|
||||
## 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 |
|
||||
@@ -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/<module>/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: <current date>
|
||||
-- Feature: <feature name>
|
||||
-- Tables affected: <list>
|
||||
--
|
||||
-- ⚠️ 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
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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`).
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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<number> {
|
||||
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<number> {
|
||||
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<number> {
|
||||
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',
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<number> {
|
||||
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<number> {
|
||||
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<Correspondence> = {};
|
||||
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,
|
||||
|
||||
@@ -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' }[];
|
||||
}
|
||||
|
||||
@@ -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<number> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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; // เพิ่ม !
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<string, unknown>) => 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() {
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Contracts</SelectItem>
|
||||
{contracts.map((c: any) => (
|
||||
<SelectItem key={c.uuid || c.id} value={String(c.id || c.uuid)}>
|
||||
<SelectItem key={c.id} value={String(c.id)}>
|
||||
{c.contractName} ({c.contractCode})
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -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<string, unknown>) => 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() {
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Contracts</SelectItem>
|
||||
{contracts.map((c: any) => (
|
||||
<SelectItem key={c.uuid || c.id} value={String(c.id || c.uuid)}>
|
||||
<SelectItem key={c.id} value={String(c.id)}>
|
||||
{c.contractName} ({c.contractCode})
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -16,8 +16,8 @@ export default function TagsPage() {
|
||||
const projectOptions = [
|
||||
{ label: "Global (All Projects)", value: "__none__" },
|
||||
...(projectsData || []).map((p: Record<string, unknown>) => ({
|
||||
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 <span className="text-muted-foreground italic">Global</span>;
|
||||
const p = (projectsData || []).find((proj: Record<string, unknown>) => 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<string, unknown>;
|
||||
const project = item.project as Record<string, unknown> | null;
|
||||
if (!project) return <span className="text-muted-foreground italic">Global</span>;
|
||||
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<string, unknown>) => masterDataService.createTag(formatPayload(data) as unknown as CreateTagDto)}
|
||||
updateFn={(id, data) => masterDataService.updateTag(id, formatPayload(data))}
|
||||
deleteFn={(id) => masterDataService.deleteTag(id)}
|
||||
|
||||
@@ -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() {
|
||||
<CommandList>
|
||||
<CommandEmpty>No document found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{correspondences?.data?.map((doc: { id: number; correspondenceNumber: string }) => (
|
||||
{correspondences?.data?.map((doc: { uuid: string; correspondenceNumber: string }) => (
|
||||
<CommandItem
|
||||
key={doc.id}
|
||||
key={doc.uuid}
|
||||
value={doc.correspondenceNumber}
|
||||
onSelect={() => {
|
||||
form.setValue("correspondenceId", doc.id);
|
||||
form.setValue("correspondenceId", doc.uuid);
|
||||
setDocOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
doc.id === field.value
|
||||
doc.uuid === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
@@ -230,13 +230,13 @@ export default function CreateCirculationPage() {
|
||||
>
|
||||
{selectedAssignees.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{selectedAssignees.map((userId) => {
|
||||
{selectedAssignees.map((userUuid) => {
|
||||
const user = users.find(
|
||||
(u) => u.userId === userId
|
||||
(u) => u.uuid === userUuid
|
||||
);
|
||||
return user ? (
|
||||
<Badge
|
||||
key={userId}
|
||||
key={userUuid}
|
||||
variant="secondary"
|
||||
className="mr-1"
|
||||
>
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
@@ -269,14 +269,14 @@ export default function CreateCirculationPage() {
|
||||
<CommandGroup>
|
||||
{users.map((user) => (
|
||||
<CommandItem
|
||||
key={user.userId ?? user.uuid}
|
||||
key={user.uuid}
|
||||
value={user.username}
|
||||
onSelect={() => user.userId && toggleAssignee(user.userId)}
|
||||
onSelect={() => toggleAssignee(user.uuid)}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
user.userId != null && selectedAssignees.includes(user.userId)
|
||||
selectedAssignees.includes(user.uuid)
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@ import { correspondenceService } from "@/lib/services/correspondence.service";
|
||||
|
||||
// Updated Zod Schema with all required fields
|
||||
const correspondenceSchema = z.object({
|
||||
projectId: z.number().min(1, "Please select a Project"),
|
||||
projectId: z.string().min(1, "Please select a Project"),
|
||||
documentTypeId: z.number().min(1, "Please select a Document Type"),
|
||||
disciplineId: z.number().optional(),
|
||||
subject: z.string().min(5, "Subject must be at least 5 characters"),
|
||||
@@ -34,8 +34,8 @@ const correspondenceSchema = z.object({
|
||||
body: z.string().optional(),
|
||||
remarks: z.string().optional(),
|
||||
dueDate: z.string().optional(), // ISO Date string
|
||||
fromOrganizationId: z.number().min(1, "Please select From Organization"),
|
||||
toOrganizationId: z.number().min(1, "Please select To Organization"),
|
||||
fromOrganizationId: z.string().min(1, "Please select From Organization"),
|
||||
toOrganizationId: z.string().min(1, "Please select To Organization"),
|
||||
importance: z.enum(["NORMAL", "HIGH", "URGENT"]),
|
||||
attachments: z.array(z.instanceof(File)).optional(),
|
||||
});
|
||||
@@ -56,7 +56,7 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
|
||||
// Extract initial values if editing
|
||||
const currentRev = initialData?.revisions?.find((r: any) => r.isCurrent) || initialData?.revisions?.[0];
|
||||
const defaultValues: Partial<FormData> = {
|
||||
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
|
||||
<div className="space-y-2">
|
||||
<Label>Project *</Label>
|
||||
<Select
|
||||
onValueChange={(v) => setValue("projectId", parseInt(v))}
|
||||
value={projectId ? String(projectId) : undefined}
|
||||
onValueChange={(v) => setValue("projectId", v)}
|
||||
value={projectId || undefined}
|
||||
disabled={isLoadingProjects}
|
||||
>
|
||||
<SelectTrigger>
|
||||
@@ -323,8 +325,8 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
|
||||
<div className="space-y-2">
|
||||
<Label>From Organization *</Label>
|
||||
<Select
|
||||
onValueChange={(v) => setValue("fromOrganizationId", parseInt(v))}
|
||||
value={fromOrgId ? String(fromOrgId) : undefined}
|
||||
onValueChange={(v) => setValue("fromOrganizationId", v)}
|
||||
value={fromOrgId || undefined}
|
||||
disabled={isLoadingOrgs}
|
||||
>
|
||||
<SelectTrigger>
|
||||
@@ -332,7 +334,7 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(organizations || []).map((org: Organization) => (
|
||||
<SelectItem key={org.id} value={String(org.id)}>
|
||||
<SelectItem key={org.uuid} value={org.uuid}>
|
||||
{org.organizationName} ({org.organizationCode})
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -346,8 +348,8 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
|
||||
<div className="space-y-2">
|
||||
<Label>To Organization *</Label>
|
||||
<Select
|
||||
onValueChange={(v) => setValue("toOrganizationId", parseInt(v))}
|
||||
value={toOrgId ? String(toOrgId) : undefined}
|
||||
onValueChange={(v) => setValue("toOrganizationId", v)}
|
||||
value={toOrgId || undefined}
|
||||
disabled={isLoadingOrgs}
|
||||
>
|
||||
<SelectTrigger>
|
||||
@@ -355,7 +357,7 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(organizations || []).map((org: Organization) => (
|
||||
<SelectItem key={org.id} value={String(org.id)}>
|
||||
<SelectItem key={org.uuid} value={org.uuid}>
|
||||
{org.organizationName} ({org.organizationCode})
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Drawing } from '@/types/drawing';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowUpDown, MoreHorizontal } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -66,7 +67,9 @@ export const columns: ColumnDef<Drawing>[] = [
|
||||
Copy Drawing No.
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>View Details</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/drawings/${drawing.uuid}`}>View Details</Link>
|
||||
</DropdownMenuItem>
|
||||
{/* Add download/view functionality later */}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -68,8 +68,8 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
|
||||
try {
|
||||
const result = await numberingApi.previewNumber({
|
||||
projectId: projectId,
|
||||
originatorOrganizationId: parseInt(testData.originatorId || "0"),
|
||||
recipientOrganizationId: parseInt(testData.recipientId || "0"),
|
||||
originatorOrganizationId: testData.originatorId || "0",
|
||||
recipientOrganizationId: testData.recipientId || "0",
|
||||
correspondenceTypeId: parseInt(testData.correspondenceTypeId || "0"),
|
||||
disciplineId: parseInt(testData.disciplineId || "0"),
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ export function useDrawings(type: DrawingType, params: DrawingSearchParams) {
|
||||
if (response && response.data) {
|
||||
const mappedData = response.data.map((d: ContractDrawing) => ({
|
||||
...d,
|
||||
uuid: d.uuid,
|
||||
uuid: d.uuid || (d as unknown as { id: string }).id,
|
||||
drawingNumber: d.contractDrawingNo,
|
||||
type: 'CONTRACT',
|
||||
}));
|
||||
@@ -46,7 +46,7 @@ export function useDrawings(type: DrawingType, params: DrawingSearchParams) {
|
||||
if (response && response.data) {
|
||||
const mappedData = response.data.map((d: ShopDrawing) => ({
|
||||
...d,
|
||||
uuid: d.uuid,
|
||||
uuid: d.uuid || (d as unknown as { id: string }).id,
|
||||
type: 'SHOP',
|
||||
title: d.currentRevision?.title || 'Untitled',
|
||||
revision: d.currentRevision?.revisionNumber,
|
||||
@@ -61,7 +61,7 @@ export function useDrawings(type: DrawingType, params: DrawingSearchParams) {
|
||||
if (response && response.data) {
|
||||
const mappedData = response.data.map((d: AsBuiltDrawing) => ({
|
||||
...d,
|
||||
uuid: d.uuid,
|
||||
uuid: d.uuid || (d as unknown as { id: string }).id,
|
||||
type: 'AS_BUILT',
|
||||
title: d.currentRevision?.title || 'Untitled',
|
||||
revision: d.currentRevision?.revisionNumber,
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface NumberingTemplate {
|
||||
*/
|
||||
export interface SaveTemplateDto {
|
||||
id?: number; // If present, update; otherwise create
|
||||
projectId: number;
|
||||
projectId: number | string;
|
||||
correspondenceTypeId: number | null;
|
||||
formatTemplate: string;
|
||||
description?: string;
|
||||
@@ -273,13 +273,13 @@ export const numberingApi = {
|
||||
* Preview what a document number would look like (without generating)
|
||||
*/
|
||||
previewNumber: async (ctx: {
|
||||
projectId: number;
|
||||
originatorOrganizationId: number;
|
||||
projectId: number | string;
|
||||
originatorOrganizationId: number | string;
|
||||
correspondenceTypeId: number;
|
||||
disciplineId?: number;
|
||||
subTypeId?: number;
|
||||
rfaTypeId?: number;
|
||||
recipientOrganizationId?: number;
|
||||
recipientOrganizationId?: number | string;
|
||||
}): Promise<{ previewNumber: string; nextSequence: number }> => {
|
||||
const res = await apiClient.post<{ data: { previewNumber: string; nextSequence: number } }>(
|
||||
'/document-numbering/preview',
|
||||
|
||||
@@ -89,10 +89,10 @@ export interface CirculationListResponse {
|
||||
* DTO for creating a circulation
|
||||
*/
|
||||
export interface CreateCirculationDto {
|
||||
correspondenceId: number;
|
||||
projectId?: number;
|
||||
correspondenceId: number | string;
|
||||
projectId?: number | string;
|
||||
subject: string;
|
||||
assigneeIds: number[];
|
||||
assigneeIds: (number | string)[];
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// File: src/types/dto/circulation/create-circulation.dto.ts
|
||||
|
||||
export interface CreateCirculationDto {
|
||||
/** เอกสารต้นเรื่องที่จะเวียน (Correspondence ID) */
|
||||
correspondenceId: number;
|
||||
/** เอกสารต้นเรื่องที่จะเวียน (Correspondence ID or UUID) */
|
||||
correspondenceId: number | string;
|
||||
|
||||
/** หัวข้อเรื่อง (Subject) */
|
||||
subject: string;
|
||||
subject: string;
|
||||
|
||||
/** รายชื่อ User ID ที่ต้องการส่งให้ (ผู้รับผิดชอบ) */
|
||||
assigneeIds: number[];
|
||||
/** รายชื่อ User ID/UUID ที่ต้องการส่งให้ (ผู้รับผิดชอบ) */
|
||||
assigneeIds: (number | string)[];
|
||||
|
||||
/** หมายเหตุเพิ่มเติม (ถ้ามี) */
|
||||
remarks?: string;
|
||||
}
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// File: src/types/dto/correspondence/create-correspondence.dto.ts
|
||||
|
||||
export interface CreateCorrespondenceDto {
|
||||
/** ID ของโครงการ */
|
||||
projectId: number;
|
||||
/** ID or UUID ของโครงการ */
|
||||
projectId: number | string;
|
||||
|
||||
/** ID ของประเภทเอกสาร (เช่น RFA, LETTER) */
|
||||
typeId: number;
|
||||
@@ -37,8 +37,8 @@ export interface CreateCorrespondenceDto {
|
||||
/** * ✅ Field สำหรับ Impersonation (เลือกองค์กรผู้ส่ง)
|
||||
* ใช้กรณี Admin สร้างเอกสารแทนผู้อื่น
|
||||
*/
|
||||
originatorId?: number;
|
||||
originatorId?: number | string;
|
||||
|
||||
/** รายชื่อผู้รับ */
|
||||
recipients?: { organizationId: number; type: 'TO' | 'CC' }[];
|
||||
recipients?: { organizationId: number | string; type: 'TO' | 'CC' }[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user